1 /*
  2  * File:        ColVis.js
  3  * Version:     1.0.3
  4  * CVS:         $Id$
  5  * Description: Controls for column visiblity in DataTables
  6  * Author:      Allan Jardine (www.sprymedia.co.uk)
  7  * Created:     Wed Sep 15 18:23:29 BST 2010
  8  * Modified:    $Date$ by $Author$
  9  * Language:    Javascript
 10  * License:     LGPL
 11  * Project:     Just a little bit of fun :-)
 12  * Contact:     www.sprymedia.co.uk/contact
 13  * 
 14  * Copyright 2010 Allan Jardine, all rights reserved.
 15  *
 16  */
 17 
 18 (function($) {
 19 
 20 /** 
 21  * ColVis provides column visiblity control for DataTables
 22  * @class ColVis
 23  * @constructor
 24  * @param {object} DataTables settings object
 25  */
 26 ColVis = function( oDTSettings )
 27 {
 28 	/* Santiy check that we are a new instance */
 29 	if ( !this.CLASS || this.CLASS != "ColVis" )
 30 	{
 31 		alert( "Warning: ColVis must be initialised with the keyword 'new'" );
 32 	}
 33 	
 34 	
 35 	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 36 	 * Public class variables
 37 	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
 38 	
 39 	/**
 40 	 * @namespace Settings object which contains customisable information for ColVis instance
 41 	 */
 42 	this.s = {
 43 		/**
 44 		 * DataTables settings object
 45 		 *  @property dt
 46 		 *  @type     Object
 47 		 *  @default  null
 48 		 */
 49 		dt: null,
 50 		
 51 		/**
 52 		 * Mode of activation. Can be 'click' or 'mouseover'
 53 		 *  @property activate
 54 		 *  @type     String
 55 		 *  @default  click
 56 		 */
 57 		activate: "click",
 58 		
 59 		/**
 60 		 * Text used for the button
 61 		 *  @property buttonText
 62 		 *  @type     String
 63 		 *  @default  Show / hide columns
 64 		 */
 65 		buttonText: "Show / hide columns",
 66 		
 67 		/**
 68 		 * Flag to say if the collection is hidden
 69 		 *  @property hidden
 70 		 *  @type     boolean
 71 		 *  @default  true
 72 		 */
 73 		hidden: true,
 74 		
 75 		/**
 76 		 * List of columns (integers) which should be excluded from the list
 77 		 *  @property aiExclude
 78 		 *  @type     Array
 79 		 *  @default  []
 80 		 */
 81 		aiExclude: []
 82 	};
 83 	
 84 	
 85 	/**
 86 	 * @namespace Common and useful DOM elements for the class instance
 87 	 */
 88 	this.dom = {
 89 		/**
 90 		 * Wrapper for the button - given back to DataTables as the node to insert
 91 		 *  @property wrapper
 92 		 *  @type     Node
 93 		 *  @default  null
 94 		 */
 95 		wrapper: null,
 96 		
 97 		/**
 98 		 * Activation button
 99 		 *  @property button
100 		 *  @type     Node
101 		 *  @default  null
102 		 */
103 		button: null,
104 		
105 		/**
106 		 * Collection list node
107 		 *  @property collection
108 		 *  @type     Node
109 		 *  @default  null
110 		 */
111 		collection: null,
112 		
113 		/**
114 		 * Background node used for shading the display and event capturing
115 		 *  @property background
116 		 *  @type     Node
117 		 *  @default  null
118 		 */
119 		background: null,
120 		
121 		/**
122 		 * Element to position over the activation button to catch mouse events when using mouseover
123 		 *  @property catcher
124 		 *  @type     Node
125 		 *  @default  null
126 		 */
127 		catcher: null,
128 		
129 		/**
130 		 * List of button elements
131 		 *  @property buttons
132 		 *  @type     Array
133 		 *  @default  []
134 		 */
135 		buttons: []
136 	};
137 	
138 	
139 	
140 	
141 	
142 	/* Constructor logic */
143 	this.s.dt = oDTSettings;
144 	this._fnConstruct();
145 	return this;
146 };
147 
148 
149 
150 ColVis.prototype = {
151 	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
152 	 * Public methods
153 	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
154 	
155 	/**
156 	 * Rebuild the list of buttons for this instance (i.e. if there is a column header update)
157 	 *  @method  fnRebuild
158 	 *  @returns void
159 	 */
160 	fnRebuild: function ()
161 	{
162 		/* Remove the old buttons */
163 		for ( var i=this.dom.buttons.length-1 ; i>=0 ; i-- )
164 		{
165 			this.dom.collection.removeChild( this.dom.buttons[i] )
166 		}
167 		this.dom.buttons.splice( 0, this.dom.buttons.length );
168 		
169 		/* Re-add them (this is not the optimal way of doing this, it is fast and effective) */
170 		this._fnAddButtons();
171 		
172 		/* Update the checkboxes */
173 		this._fnDrawCallback();
174 	},
175 	
176 	
177 	
178 	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
179 	 * Private methods (they are of course public in JS, but recommended as private)
180 	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
181 	
182 	/**
183 	 * Constructor logic
184 	 *  @method  _fnConstruct
185 	 *  @returns void
186 	 *  @private 
187 	 */
188 	_fnConstruct: function ()
189 	{
190 		this._fnApplyCustomisation();
191 		
192 		var that = this;
193 		this.dom.wrapper = document.createElement('div');
194 		this.dom.wrapper.className = "ColVis TableTools";
195 		
196 		this.dom.button = this._fnDomBaseButton( this.s.buttonText );
197 		this.dom.wrapper.appendChild( this.dom.button );
198 		
199 		this.dom.catcher = this._fnDomCatcher();
200 		this.dom.collection = this._fnDomCollection();
201 		this.dom.background = this._fnDomBackground();
202 		
203 		this._fnAddButtons();
204 		
205 		this.s.dt.aoDrawCallback.push( {
206 			fn: function () {
207 				that._fnDrawCallback.call( that );
208 			},
209 			sName: "ColVis"
210 		} );
211 	},
212 	
213 	
214 	/**
215 	 * Apply any customisation to the settings from the DataTables initialisation
216 	 *  @method  _fnApplyCustomisation
217 	 *  @returns void
218 	 *  @private 
219 	 */
220 	_fnApplyCustomisation: function ()
221 	{
222 		if ( typeof this.s.dt.oInit.oColVis != 'undefined' )
223 		{
224 			var oConfig = this.s.dt.oInit.oColVis;
225 			
226 			if ( typeof oConfig.activate != 'undefined' )
227 			{
228 				this.s.activate = oConfig.activate;
229 			}
230 			
231 			if ( typeof oConfig.buttonText != 'undefined' )
232 			{
233 				this.s.buttonText = oConfig.buttonText;
234 			}
235 			
236 			if ( typeof oConfig.aiExclude != 'undefined' )
237 			{
238 				this.s.aiExclude = oConfig.aiExclude;
239 			}
240 		}
241 	},
242 	
243 	
244 	/**
245 	 * On each table draw, check the visiblity checkboxes as needed. This allows any process to
246 	 * update the table's column visiblity and ColVis will still be accurate.
247 	 *  @method  _fnDrawCallback
248 	 *  @returns void
249 	 *  @private 
250 	 */
251 	_fnDrawCallback: function ()
252 	{
253 		var aoColumns = this.s.dt.aoColumns;
254 		
255 		for ( var i=0, iLen=aoColumns.length ; i<iLen ; i++ )
256 		{
257 			if ( this.dom.buttons[i] !== null )
258 			{
259 				if ( aoColumns[i].bVisible )
260 				{
261 					$('input', this.dom.buttons[i]).attr('checked','checked');
262 				}
263 				else
264 				{
265 					$('input', this.dom.buttons[i]).removeAttr('checked');
266 				}
267 			}
268 		}
269 	},
270 	
271 	
272 	/**
273 	 * Loop through the columns in the table and as a new button for each one.
274 	 *  @method  _fnAddButtons
275 	 *  @returns void
276 	 *  @private 
277 	 */
278 	_fnAddButtons: function ()
279 	{
280 		var
281 			nButton,
282 			sExclude = ","+this.s.aiExclude.join(',')+",";
283 		
284 		for ( var i=0, iLen=this.s.dt.aoColumns.length ; i<iLen ; i++ )
285 		{
286 			if ( sExclude.indexOf( ","+i+"," ) == -1 )
287 			{
288 				nButton = this._fnDomColumnButton( i );
289 				this.dom.buttons.push( nButton );
290 				this.dom.collection.appendChild( nButton );
291 			}
292 			else
293 			{
294 				this.dom.buttons.push( null );
295 			}
296 		}
297 	},
298 	
299 	
300 	/**
301 	 * Create the DOM for a show / hide button
302 	 *  @method  _fnDomColumnButton
303 	 *  @param {int} i Column in question
304 	 *  @returns {Node} Created button
305 	 *  @private 
306 	 */
307 	_fnDomColumnButton: function ( i )
308 	{
309 		var
310 			that = this,
311 			oColumn = this.s.dt.aoColumns[i],
312 		  nButton = document.createElement('button'),
313 		  nSpan = document.createElement('span');
314 		
315 		nButton.className = !this.s.dt.bJUI ? "ColVis_Button TableTools_Button" :
316 			"ColVis_Button TableTools_Button ui-button ui-state-default";
317 		nButton.appendChild( nSpan );
318 		$(nSpan).html(
319 			'<span class="ColVis_radio"><input type="checkbox"></span>'+
320 			'<span class="ColVis_title">'+oColumn.sTitle+'</span>' );
321 		
322 		$(nButton).click( function (e) {
323 			var showHide = $('input',this).attr('checked')===true ? false : true;
324 			if ( e.target.nodeName.toLowerCase() == "input" )
325 			{
326 				showHide = $('input',this).attr('checked');
327 			}
328 			
329 			/* Need to consider the case where the initialiser created more than one table - change the
330 			 * API index that DataTables is using
331 			 */
332 			var oldIndex = $.fn.dataTableExt.iApiIndex;
333 			$.fn.dataTableExt.iApiIndex = that._fnDataTablesApiIndex.call(that);
334 			that.s.dt.oInstance.fnSetColumnVis( i, showHide );
335 			$.fn.dataTableExt.iApiIndex = oldIndex; /* Restore */
336 		} );
337 		
338 		return nButton;
339 	},
340 	
341 	
342 	/**
343 	 * Get the position in the DataTables instance array of the table for this instance of ColVis
344 	 *  @method  _fnDataTablesApiIndex
345 	 *  @returns {int} Index
346 	 *  @private 
347 	 */
348 	_fnDataTablesApiIndex: function ()
349 	{
350 		for ( var i=0, iLen=this.s.dt.oInstance.length ; i<iLen ; i++ )
351 		{
352 			if ( this.s.dt.oInstance[i] == this.s.dt.nTable )
353 			{
354 				return i;
355 			}
356 		}
357 		return 0;
358 	},
359 	
360 	
361 	/**
362 	 * Create the DOM needed for the button and apply some base properties. All buttons start here
363 	 *  @method  _fnDomBaseButton
364 	 *  @param   {String} text Button text
365 	 *  @returns {Node} DIV element for the button
366 	 *  @private 
367 	 */
368 	_fnDomBaseButton: function ( text )
369 	{
370 		var
371 			that = this,
372 		  nButton = document.createElement('button'),
373 		  nSpan = document.createElement('span'),
374 			sEvent = this.s.activate=="mouseover" ? "mouseover" : "click";
375 		
376 		nButton.className = !this.s.dt.bJUI ? "ColVis_Button TableTools_Button" :
377 			"ColVis_Button TableTools_Button ui-button ui-state-default";
378 		nButton.appendChild( nSpan );
379 		nSpan.innerHTML = text;
380 		
381 		$(nButton).bind( sEvent, function (e) {
382 			that._fnCollectionShow();
383 			e.preventDefault();
384 		} );
385 		
386 		return nButton;
387 	},
388 	
389 	
390 	/**
391 	 * Create the element used to contain list the columns (it is shown and hidden as needed)
392 	 *  @method  _fnDomCollection
393 	 *  @returns {Node} div container for the collection
394 	 *  @private 
395 	 */
396 	_fnDomCollection: function ()
397 	{
398 		var that = this;
399 		var nHidden = document.createElement('div');
400 		nHidden.style.display = "none";
401 		nHidden.className = !this.s.dt.bJUI ? "ColVis_collection TableTools_collection" :
402 			"ColVis_collection TableTools_collection ui-buttonset ui-buttonset-multi";
403 		nHidden.style.position = "absolute";
404 		$(nHidden).css('opacity', 0);
405 		
406 		return nHidden;
407 	},
408 	
409 	
410 	/**
411 	 * An element to be placed on top of the activate button to catch events
412 	 *  @method  _fnDomCatcher
413 	 *  @returns {Node} div container for the collection
414 	 *  @private 
415 	 */
416 	_fnDomCatcher: function ()
417 	{
418 		var 
419 			that = this,
420 			nCatcher = document.createElement('div');
421 		nCatcher.className = "ColVis_catcher TableTools_catcher";
422 		
423 		$(nCatcher).click( function () {
424 			that._fnCollectionHide.call( that, null, null );
425 		} );
426 		
427 		return nCatcher;
428 	},
429 	
430 	
431 	/**
432 	 * Create the element used to shade the background, and capture hide events (it is shown and 
433 	 * hidden as needed)
434 	 *  @method  _fnDomBackground
435 	 *  @returns {Node} div container for the background
436 	 *  @private 
437 	 */
438 	_fnDomBackground: function ()
439 	{
440 		var that = this;
441 		
442 		var nBackground = document.createElement('div');
443 		nBackground.style.position = "absolute";
444 		nBackground.style.left = "0px";
445 		nBackground.style.top = "0px";
446 		nBackground.className = "TableTools_collectionBackground";
447 		$(nBackground).css('opacity', 0);
448 		
449 		$(nBackground).click( function () {
450 			that._fnCollectionHide.call( that, null, null );
451 		} );
452 		
453 		/* When considering a mouse over action for the activation, we also consider a mouse out
454 		 * which is the same as a mouse over the background - without all the messing around of
455 		 * bubbling events. Use the catcher element to avoid messing around with bubbling
456 		 */
457 		if ( this.s.activate == "mouseover" )
458 		{
459 			$(nBackground).mouseover( function () {
460 				that.s.overcollection = false;
461 				that._fnCollectionHide.call( that, null, null );
462 			} );
463 		}
464 		
465 		return nBackground;
466 	},
467 	
468 	
469 	/**
470 	 * Show the show / hide list and the background
471 	 *  @method  _fnCollectionShow
472 	 *  @returns void
473 	 *  @private 
474 	 */
475 	_fnCollectionShow: function ()
476 	{
477 		var that = this;
478 		var oPos = $(this.dom.button).offset();
479 		var nHidden = this.dom.collection;
480 		var nBackground = this.dom.background;
481 		var iDivX = parseInt(oPos.left, 10);
482 		var iDivY = parseInt(oPos.top + $(this.dom.button).outerHeight(), 10);
483 		
484 		nHidden.style.left = iDivX+"px";
485 		nHidden.style.top = iDivY+"px";
486 		nHidden.style.display = "block";
487 		$(nHidden).css('opacity',0);
488 		
489 		var iWinHeight = $(window).height(), iDocHeight = $(document).height(),
490 		 	iWinWidth = $(window).width(), iDocWidth = $(document).width();
491 		
492 		nBackground.style.height = ((iWinHeight>iDocHeight)? iWinHeight : iDocHeight) +"px";
493 		nBackground.style.width = ((iWinWidth<iDocWidth)? iWinWidth : iDocWidth) +"px";
494 		
495 		var oStyle = this.dom.catcher.style;
496 		oStyle.height = $(this.dom.button).outerHeight()+"px";
497 		oStyle.width = $(this.dom.button).outerWidth()+"px";
498 		oStyle.top = oPos.top+"px";
499 		oStyle.left = iDivX+"px";
500 		
501 		document.body.appendChild( nBackground );
502 		document.body.appendChild( nHidden );
503 		document.body.appendChild( this.dom.catcher );
504 		
505 		/* Visual corrections to try and keep the collection visible */
506 		var iDivWidth = $(nHidden).outerWidth();
507 		var iDivHeight = $(nHidden).outerHeight();
508 		
509 		if ( iDivX + iDivWidth > iDocWidth )
510 		{
511 			nHidden.style.left = (iDocWidth-iDivWidth)+"px";
512 		}
513 		
514 		if ( iDivY + iDivHeight > iDocHeight )
515 		{
516 			nHidden.style.top = (iDivY-iDivHeight-$(this.dom.button).outerHeight())+"px";
517 		}
518 		
519 		
520 		/* This results in a very small delay for the end user but it allows the animation to be
521 		 * much smoother. If you don't want the animation, then the setTimeout can be removed
522 		 */
523 		setTimeout( function () {
524 			$(nHidden).animate({opacity: 1}, 500);
525 			$(nBackground).animate({opacity: 0.1}, 500, 'linear', function () {
526 				/* In IE6 if you set the checked attribute of a hidden checkbox, then this is not visually
527 				 * reflected. As such, we need to do it here, once it is visible. Unbelievable.
528 				 */
529 				if ( jQuery.browser.msie && jQuery.browser.version == "6.0" )
530 				{
531 					that._fnDrawCallback();
532 				}
533 			});
534 		}, 10 );
535 		
536 		this.s.hidden = false;
537 	},
538 	
539 	
540 	/**
541 	 * Hide the show / hide list and the background
542 	 *  @method  _fnCollectionHide
543 	 *  @returns void
544 	 *  @private 
545 	 */
546 	_fnCollectionHide: function (  )
547 	{
548 		var that = this;
549 		
550 		if ( !this.s.hidden && this.dom.collection !== null )
551 		{
552 			this.s.hidden = true;
553 			
554 			$(this.dom.collection).animate({opacity: 0}, 500, function (e) {
555 				this.style.display = "none";
556 			} );
557 			
558 			$(this.dom.background).animate({opacity: 0}, 500, function (e) {
559 				document.body.removeChild( that.dom.background );
560 				document.body.removeChild( that.dom.catcher );
561 			} );
562 		}
563 	}
564 };
565 
566 
567 
568 
569 
570 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
571  * Static object methods
572  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
573 
574 /**
575  * Rebuild the collection for a given table, or all tables if no parameter given
576  *  @method  ColVis.fnRebuild
577  *  @static
578  *  @param   object oTable DataTable instance to consider - optional
579  *  @returns void
580  */
581 ColVis.fnRebuild = function ( oTable )
582 {
583 	var nTable = null;
584 	if ( typeof oTable != 'undefined' )
585 	{
586 		nTable = oTable.fnSettings().nTable;
587 	}
588 	
589 	for ( var i=0, iLen=ColVis.aInstances.length ; i<iLen ; i++ )
590 	{
591 		if ( typeof oTable == 'undefined' || nTable == ColVis.aInstances[i].s.dt.nTable )
592 		{
593 			ColVis.aInstances[i].fnRebuild();
594 		}
595 	}
596 };
597 
598 
599 
600 
601 
602 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
603  * Static object propterties
604  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
605 
606 /**
607  * Collection of all ColVis instances
608  *  @property ColVis.aInstances
609  *  @static
610  *  @type     Array
611  *  @default  []
612  */
613 ColVis.aInstances = [];
614 
615 
616 
617 
618 
619 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
620  * Constants
621  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
622 
623 /**
624  * Name of this class
625  *  @constant CLASS
626  *  @type     String
627  *  @default  ColVis
628  */
629 ColVis.prototype.CLASS = "ColVis";
630 
631 
632 /**
633  * ColVis version
634  *  @constant  VERSION
635  *  @type      String
636  *  @default   1.0.3
637  */
638 ColVis.VERSION = "1.0.3";
639 ColVis.prototype.VERSION = ColVis.VERSION;
640 
641 
642 
643 
644 
645 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
646  * Initialisation
647  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
648 
649 /*
650  * Register a new feature with DataTables
651  */
652 if ( typeof $.fn.dataTable == "function" &&
653      typeof $.fn.dataTableExt.fnVersionCheck == "function" &&
654      $.fn.dataTableExt.fnVersionCheck('1.7.0') )
655 {
656 	$.fn.dataTableExt.aoFeatures.push( {
657 		fnInit: function( oDTSettings ) {
658 			var oColvis = new ColVis( oDTSettings );
659 			ColVis.aInstances.push( oColvis );
660 			return oColvis.dom.wrapper;
661 		},
662 		cFeature: "C",
663 		sFeature: "ColVis"
664 	} );
665 }
666 else
667 {
668 	alert( "Warning: ColVis requires DataTables 1.7 or greater - www.datatables.net/download");
669 }
670 
671 })(jQuery);
672