5 * Description: AutoFill for DataTables
6 * Author: Allan Jardine (www.sprymedia.co.uk)
7 * Created: Mon 6 Sep 2010 16:54:41 BST
8 * Modified: $Date$ by $Author$
10 * License: GPL v2 or BSD 3 point
12 * Contact: www.sprymedia.co.uk/contact
14 * Copyright 2010-2011 Allan Jardine, all rights reserved.
16 * This source file is free software, under either the GPL v2 license or a
17 * BSD style license, available at:
18 * http://datatables.net/license_gpl2
19 * http://datatables.net/license_bsd
23 /* Global scope for AutoFill */
29 * AutoFill provides Excel like auto fill features for a DataTable
32 * @param {object} DataTables settings object
33 * @param {object} Configuration object for AutoFill
35 AutoFill = function( oDT, oConfig )
37 /* Santiy check that we are a new instance */
38 if ( !this.CLASS || this.CLASS != "AutoFill" )
40 alert( "Warning: AutoFill must be initialised with the keyword 'new'" );
44 if ( !$.fn.dataTableExt.fnVersionCheck('1.7.0') )
46 alert( "Warning: AutoFill requires DataTables 1.7 or greater - www.datatables.net/download");
51 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
52 * Public class variables
53 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
56 * @namespace Settings object which contains customisable information for AutoFill instance
60 * @namespace Cached information about the little dragging icon (the filler)
68 * @namespace Cached information about the border display
75 * @namespace Store for live information for the current drag
86 * @namespace Data cache for information that we need for scrolling the screen when we near
97 * @namespace Data cache for the position of the DataTables scrolling element (when scrolling
107 * @namespace Information stored for each column. An array of objects
114 * @namespace Common and useful DOM elements for the class instance
121 "borderBottom": null,
123 "currentTarget": null
128 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
129 * Public class methods
130 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
133 * Retreieve the settings object from an instance
135 * @returns {object} AutoFill settings object
137 this.fnSettings = function () {
142 /* Constructor logic */
143 this._fnInit( oDT, oConfig );
149 AutoFill.prototype = {
150 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
151 * Private methods (they are of course public in JS, but recommended as private)
152 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
157 * @param {object} oDT DataTables settings object
158 * @param {object} oConfig Configuration object for AutoFill
161 "_fnInit": function ( oDT, oConfig )
170 this.s.dt = oDT.fnSettings();
172 this.dom.table = this.s.dt.nTable;
174 /* Add and configure the columns */
175 for ( i=0, iLen=this.s.dt.aoColumns.length ; i<iLen ; i++ )
177 this._fnAddColumn( i );
180 if ( typeof oConfig != 'undefined' && typeof oConfig.aoColumnDefs != 'undefined' )
182 this._fnColumnDefs( oConfig.aoColumnDefs );
185 if ( typeof oConfig != 'undefined' && typeof oConfig.aoColumns != 'undefined' )
187 this._fnColumnsAll( oConfig.aoColumns );
195 /* Auto Fill click and drag icon */
196 var filler = document.createElement('div');
197 filler.className = "AutoFill_filler";
198 document.body.appendChild( filler );
199 this.dom.filler = filler;
201 filler.style.display = "block";
202 this.s.filler.height = $(filler).height();
203 this.s.filler.width = $(filler).width();
204 filler.style.display = "none";
206 /* Border display - one div for each side. We can't just use a single one with a border, as
207 * we want the events to effectively pass through the transparent bit of the box
210 var appender = document.body;
211 if ( that.s.dt.oScroll.sY !== "" )
213 that.s.dt.nTable.parentNode.style.position = "relative";
214 appender = that.s.dt.nTable.parentNode;
217 border = document.createElement('div');
218 border.className = "AutoFill_border";
219 appender.appendChild( border );
220 this.dom.borderTop = border;
222 border = document.createElement('div');
223 border.className = "AutoFill_border";
224 appender.appendChild( border );
225 this.dom.borderRight = border;
227 border = document.createElement('div');
228 border.className = "AutoFill_border";
229 appender.appendChild( border );
230 this.dom.borderBottom = border;
232 border = document.createElement('div');
233 border.className = "AutoFill_border";
234 appender.appendChild( border );
235 this.dom.borderLeft = border;
241 $(filler).mousedown( function (e) {
242 this.onselectstart = function() { return false; };
243 that._fnFillerDragStart.call( that, e );
247 $('tbody>tr>td', this.dom.table).live( 'mouseover mouseout', function (e) {
248 that._fnFillerDisplay.call( that, e );
253 "_fnColumnDefs": function ( aoColumnDefs )
256 i, j, k, iLen, jLen, kLen,
259 /* Loop over the column defs array - loop in reverse so first instace has priority */
260 for ( i=aoColumnDefs.length-1 ; i>=0 ; i-- )
262 /* Each column def can target multiple columns, as it is an array */
263 aTargets = aoColumnDefs[i].aTargets;
264 for ( j=0, jLen=aTargets.length ; j<jLen ; j++ )
266 if ( typeof aTargets[j] == 'number' && aTargets[j] >= 0 )
268 /* 0+ integer, left to right column counting. */
269 this._fnColumnOptions( aTargets[j], aoColumnDefs[i] );
271 else if ( typeof aTargets[j] == 'number' && aTargets[j] < 0 )
273 /* Negative integer, right to left column counting */
274 this._fnColumnOptions( this.s.dt.aoColumns.length+aTargets[j], aoColumnDefs[i] );
276 else if ( typeof aTargets[j] == 'string' )
278 /* Class name matching on TH element */
279 for ( k=0, kLen=this.s.dt.aoColumns.length ; k<kLen ; k++ )
281 if ( aTargets[j] == "_all" ||
282 this.s.dt.aoColumns[k].nTh.className.indexOf( aTargets[j] ) != -1 )
284 this._fnColumnOptions( k, aoColumnDefs[i] );
293 "_fnColumnsAll": function ( aoColumns )
295 for ( var i=0, iLen=this.s.dt.aoColumns.length ; i<iLen ; i++ )
297 this._fnColumnOptions( i, aoColumns[i] );
302 "_fnAddColumn": function ( i )
304 this.s.columns[i] = {
306 "read": this._fnReadCell,
307 "write": this._fnWriteCell,
308 "step": this._fnStep,
313 "_fnColumnOptions": function ( i, opts )
315 if ( typeof opts.bEnable != 'undefined' )
317 this.s.columns[i].enable = opts.bEnable;
320 if ( typeof opts.fnRead != 'undefined' )
322 this.s.columns[i].read = opts.fnRead;
325 if ( typeof opts.fnWrite != 'undefined' )
327 this.s.columns[i].write = opts.fnWrite;
330 if ( typeof opts.fnStep != 'undefined' )
332 this.s.columns[i].step = opts.fnStep;
335 if ( typeof opts.fnCallback != 'undefined' )
337 this.s.columns[i].complete = opts.fnCallback;
343 * Find out the coordinates of a given TD cell in a table
344 * @method _fnTargetCoords
346 * @returns {Object} x and y properties, for the position of the cell in the tables DOM
348 "_fnTargetCoords": function ( nTd )
350 var nTr = $(nTd).parents('tr')[0];
353 "x": $('td', nTr).index(nTd),
354 "y": $('tr', nTr.parentNode).index(nTr)
360 * Display the border around one or more cells (from start to end)
361 * @method _fnUpdateBorder
362 * @param {Node} nStart Starting cell
363 * @param {Node} nEnd Ending cell
366 "_fnUpdateBorder": function ( nStart, nEnd )
369 border = this.s.border.width,
370 offsetStart = $(nStart).offset(),
371 offsetEnd = $(nEnd).offset(),
372 x1 = offsetStart.left - border,
373 x2 = offsetEnd.left + $(nEnd).outerWidth(),
374 y1 = offsetStart.top - border,
375 y2 = offsetEnd.top + $(nEnd).outerHeight(),
376 width = offsetEnd.left + $(nEnd).outerWidth() - offsetStart.left + (2*border),
377 height = offsetEnd.top + $(nEnd).outerHeight() - offsetStart.top + (2*border),
380 if ( this.s.dt.oScroll.sY !== "" )
382 /* The border elements are inside the DT scroller - so position relative to that */
384 offsetScroll = $(this.s.dt.nTable.parentNode).offset(),
385 scrollTop = $(this.s.dt.nTable.parentNode).scrollTop(),
386 scrollLeft = $(this.s.dt.nTable.parentNode).scrollLeft();
388 x1 -= offsetScroll.left - scrollLeft;
389 x2 -= offsetScroll.left - scrollLeft;
390 y1 -= offsetScroll.top - scrollTop;
391 y2 -= offsetScroll.top - scrollTop;
395 oStyle = this.dom.borderTop.style;
396 oStyle.top = y1+"px";
397 oStyle.left = x1+"px";
398 oStyle.height = this.s.border.width+"px";
399 oStyle.width = width+"px";
402 oStyle = this.dom.borderBottom.style;
403 oStyle.top = y2+"px";
404 oStyle.left = x1+"px";
405 oStyle.height = this.s.border.width+"px";
406 oStyle.width = width+"px";
409 oStyle = this.dom.borderLeft.style;
410 oStyle.top = y1+"px";
411 oStyle.left = x1+"px";
412 oStyle.height = height+"px";
413 oStyle.width = this.s.border.width+"px";
416 oStyle = this.dom.borderRight.style;
417 oStyle.top = y1+"px";
418 oStyle.left = x2+"px";
419 oStyle.height = height+"px";
420 oStyle.width = this.s.border.width+"px";
425 * Mouse down event handler for starting a drag
426 * @method _fnFillerDragStart
427 * @param {Object} e Event object
430 "_fnFillerDragStart": function (e)
433 var startingTd = this.dom.currentTarget;
435 this.s.drag.dragging = true;
437 that.dom.borderTop.style.display = "block";
438 that.dom.borderRight.style.display = "block";
439 that.dom.borderBottom.style.display = "block";
440 that.dom.borderLeft.style.display = "block";
442 var coords = this._fnTargetCoords( startingTd );
443 this.s.drag.startX = coords.x;
444 this.s.drag.startY = coords.y;
446 this.s.drag.startTd = startingTd;
447 this.s.drag.endTd = startingTd;
449 this._fnUpdateBorder( startingTd, startingTd );
451 $(document).bind('mousemove.AutoFill', function (e) {
452 that._fnFillerDragMove.call( that, e );
455 $(document).bind('mouseup.AutoFill', function (e) {
456 that._fnFillerFinish.call( that, e );
459 /* Scrolling information cache */
460 this.s.screen.y = e.pageY;
461 this.s.screen.height = $(window).height();
462 this.s.screen.scrollTop = $(document).scrollTop();
464 if ( this.s.dt.oScroll.sY !== "" )
466 this.s.scroller.top = $(this.s.dt.nTable.parentNode).offset().top;
467 this.s.scroller.bottom = this.s.scroller.top + $(this.s.dt.nTable.parentNode).height();
470 /* Scrolling handler - we set an interval (which is cancelled on mouse up) which will fire
471 * regularly and see if we need to do any scrolling
473 this.s.screen.interval = setInterval( function () {
474 var iScrollTop = $(document).scrollTop();
475 var iScrollDelta = iScrollTop - that.s.screen.scrollTop;
476 that.s.screen.y += iScrollDelta;
478 if ( that.s.screen.height - that.s.screen.y + iScrollTop < 50 )
480 $('html, body').animate( {
481 "scrollTop": iScrollTop + 50
484 else if ( that.s.screen.y - iScrollTop < 50 )
486 $('html, body').animate( {
487 "scrollTop": iScrollTop - 50
491 if ( that.s.dt.oScroll.sY !== "" )
493 if ( that.s.screen.y > that.s.scroller.bottom - 50 )
495 $(that.s.dt.nTable.parentNode).animate( {
496 "scrollTop": $(that.s.dt.nTable.parentNode).scrollTop() + 50
499 else if ( that.s.screen.y < that.s.scroller.top + 50 )
501 $(that.s.dt.nTable.parentNode).animate( {
502 "scrollTop": $(that.s.dt.nTable.parentNode).scrollTop() - 50
511 * Mouse move event handler for during a move. See if we want to update the display based on the
512 * new cursor position
513 * @method _fnFillerDragMove
514 * @param {Object} e Event object
517 "_fnFillerDragMove": function (e)
519 if ( e.target && e.target.nodeName.toUpperCase() == "TD" &&
520 e.target != this.s.drag.endTd )
522 var coords = this._fnTargetCoords( e.target );
524 if ( coords.x != this.s.drag.startX )
526 e.target = $('tbody>tr:eq('+coords.y+')>td:eq('+this.s.drag.startX+')', this.dom.table)[0];
527 coords = this._fnTargetCoords( e.target );
530 if ( coords.x == this.s.drag.startX )
532 var drag = this.s.drag;
533 drag.endTd = e.target;
535 if ( coords.y >= this.s.drag.startY )
537 this._fnUpdateBorder( drag.startTd, drag.endTd );
541 this._fnUpdateBorder( drag.endTd, drag.startTd );
543 this._fnFillerPosition( e.target );
547 /* Update the screen information so we can perform scrolling */
548 this.s.screen.y = e.pageY;
549 this.s.screen.scrollTop = $(document).scrollTop();
551 if ( this.s.dt.oScroll.sY !== "" )
553 this.s.scroller.scrollTop = $(this.s.dt.nTable.parentNode).scrollTop();
554 this.s.scroller.top = $(this.s.dt.nTable.parentNode).offset().top;
555 this.s.scroller.bottom = this.s.scroller.top + $(this.s.dt.nTable.parentNode).height();
561 * Mouse release handler - end the drag and take action to update the cells with the needed values
562 * @method _fnFillerFinish
563 * @param {Object} e Event object
566 "_fnFillerFinish": function (e)
570 $(document).unbind('mousemove.AutoFill');
571 $(document).unbind('mouseup.AutoFill');
573 this.dom.borderTop.style.display = "none";
574 this.dom.borderRight.style.display = "none";
575 this.dom.borderBottom.style.display = "none";
576 this.dom.borderLeft.style.display = "none";
578 this.s.drag.dragging = false;
580 clearInterval( this.s.screen.interval );
582 var coordsStart = this._fnTargetCoords( this.s.drag.startTd );
583 var coordsEnd = this._fnTargetCoords( this.s.drag.endTd );
587 if ( coordsStart.y <= coordsEnd.y )
590 for ( i=coordsStart.y ; i<=coordsEnd.y ; i++ )
592 aTds.push( $('tbody>tr:eq('+i+')>td:eq('+coordsStart.x+')', this.dom.table)[0] );
598 for ( i=coordsStart.y ; i>=coordsEnd.y ; i-- )
600 aTds.push( $('tbody>tr:eq('+i+')>td:eq('+coordsStart.x+')', this.dom.table)[0] );
605 var iColumn = coordsStart.x;
608 var sStart = this.s.columns[iColumn].read.call( this, this.s.drag.startTd );
609 var oPrepped = this._fnPrep( sStart );
611 for ( i=0, iLen=aTds.length ; i<iLen ; i++ )
618 var original = this.s.columns[iColumn].read.call( this, aTds[i] );
619 var step = this.s.columns[iColumn].step.call( this, aTds[i], oPrepped, i, bIncrement,
620 'SPRYMEDIA_AUTOFILL_STEPPER' );
621 this.s.columns[iColumn].write.call( this, aTds[i], step, bLast );
630 if ( this.s.columns[iColumn].complete !== null )
632 this.s.columns[iColumn].complete.call( this, aoEdited );
638 * Chunk a string such that it can be filled in by the stepper function
640 * @param {String} sStr String to prep
641 * @returns {Object} with parameters, iStart, sStr and sPostFix
643 "_fnPrep": function ( sStr )
645 var aMatch = sStr.match(/[\d\.]+/g);
646 if ( !aMatch || aMatch.length === 0 )
655 var sLast = aMatch[ aMatch.length-1 ];
656 var num = parseInt(sLast, 10);
657 var regex = new RegExp( '^(.*)'+sLast+'(.*?)$' );
658 var decimal = sLast.match(/\./) ? "."+sLast.split('.')[1] : "";
662 "sStr": sStr.replace(regex, "$1SPRYMEDIA_AUTOFILL_STEPPER$2"),
669 * Render a string for it's position in the table after the drag (incrememt numbers)
671 * @param {Node} nTd Cell being written to
672 * @param {Object} oPrepped Prepared object for the stepper (from _fnPrep)
673 * @param {Int} iDiff Step difference
674 * @param {Boolean} bIncrement Increment (true) or decriment (false)
675 * @param {String} sToken Token to replace
676 * @returns {String} Rendered information
678 "_fnStep": function ( nTd, oPrepped, iDiff, bIncrement, sToken )
680 var iReplace = bIncrement ? (oPrepped.iStart+iDiff) : (oPrepped.iStart-iDiff);
681 if ( isNaN(iReplace) )
685 return oPrepped.sStr.replace( sToken, iReplace+oPrepped.sPostFix );
690 * Read informaiton from a cell, possibly using live DOM elements if suitable
691 * @method _fnReadCell
692 * @param {Node} nTd Cell to read
693 * @returns {String} Read value
695 "_fnReadCell": function ( nTd )
697 var jq = $('input', nTd);
703 jq = $('select', nTd);
709 return nTd.innerHTML;
714 * Write informaiton to a cell, possibly using live DOM elements if suitable
715 * @method _fnWriteCell
716 * @param {Node} nTd Cell to write
717 * @param {String} sVal Value to write
718 * @param {Boolean} bLast Flag to show if this is that last update
721 "_fnWriteCell": function ( nTd, sVal, bLast )
723 var jq = $('input', nTd);
730 jq = $('select', nTd);
737 var pos = this.s.dt.oInstance.fnGetPosition( nTd );
738 this.s.dt.oInstance.fnUpdate( sVal, pos[0], pos[2], bLast );
743 * Display the drag handle on mouse over cell
744 * @method _fnFillerDisplay
745 * @param {Object} e Event object
748 "_fnFillerDisplay": function (e)
750 /* Don't display automatically when dragging */
751 if ( this.s.drag.dragging)
756 /* Check that we are allowed to AutoFill this column or not */
757 var nTd = (e.target.nodeName.toLowerCase() == 'td') ? e.target : $(e.target).parents('td')[0];
758 var iX = this._fnTargetCoords(nTd).x;
759 if ( !this.s.columns[iX].enable )
764 var filler = this.dom.filler;
765 if (e.type == 'mouseover')
767 this.dom.currentTarget = nTd;
768 this._fnFillerPosition( nTd );
770 filler.style.display = "block";
772 else if ( !e.relatedTarget || !e.relatedTarget.className.match(/AutoFill/) )
774 filler.style.display = "none";
780 * Position the filler icon over a cell
781 * @method _fnFillerPosition
782 * @param {Node} nTd Cell to position filler icon over
785 "_fnFillerPosition": function ( nTd )
787 var offset = $(nTd).offset();
788 var filler = this.dom.filler;
789 filler.style.top = (offset.top - (this.s.filler.height / 2)-1 + $(nTd).outerHeight())+"px";
790 filler.style.left = (offset.left - (this.s.filler.width / 2)-1 + $(nTd).outerWidth())+"px";
797 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
799 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
807 AutoFill.prototype.CLASS = "AutoFill";
816 AutoFill.VERSION = "1.1.2";
817 AutoFill.prototype.VERSION = AutoFill.VERSION;