/*! AutoFill 1.2.1 * ©2008-2014 SpryMedia Ltd - datatables.net/license */ /** * @summary AutoFill * @description Add Excel like click and drag auto-fill options to DataTables * @version 1.2.1 * @file dataTables.autoFill.js * @author SpryMedia Ltd (www.sprymedia.co.uk) * @contact www.sprymedia.co.uk/contact * @copyright Copyright 2010-2014 SpryMedia Ltd. * * This source file is free software, available under the following license: * MIT license - http://datatables.net/license/mit * * This source file is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. * * For details please refer to: http://www.datatables.net */ (function( window, document, undefined ) { var factory = function( $, DataTable ) { "use strict"; /** * AutoFill provides Excel like auto-fill features for a DataTable * * @class AutoFill * @constructor * @param {object} oTD DataTables settings object * @param {object} oConfig Configuration object for AutoFill */ var AutoFill = function( oDT, oConfig ) { /* Sanity check that we are a new instance */ if ( ! (this instanceof AutoFill) ) { throw( "Warning: AutoFill must be initialised with the keyword 'new'" ); } if ( ! $.fn.dataTableExt.fnVersionCheck('1.7.0') ) { throw( "Warning: AutoFill requires DataTables 1.7 or greater"); } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Public class variables * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ this.c = {}; /** * @namespace Settings object which contains customisable information for AutoFill instance */ this.s = { /** * @namespace Cached information about the little dragging icon (the filler) */ "filler": { "height": 0, "width": 0 }, /** * @namespace Cached information about the border display */ "border": { "width": 2 }, /** * @namespace Store for live information for the current drag */ "drag": { "startX": -1, "startY": -1, "startTd": null, "endTd": null, "dragging": false }, /** * @namespace Data cache for information that we need for scrolling the screen when we near * the edges */ "screen": { "interval": null, "y": 0, "height": 0, "scrollTop": 0 }, /** * @namespace Data cache for the position of the DataTables scrolling element (when scrolling * is enabled) */ "scroller": { "top": 0, "bottom": 0 }, /** * @namespace Information stored for each column. An array of objects */ "columns": [] }; /** * @namespace Common and useful DOM elements for the class instance */ this.dom = { "table": null, "filler": null, "borderTop": null, "borderRight": null, "borderBottom": null, "borderLeft": null, "currentTarget": null }; /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Public class methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /** * Retreieve the settings object from an instance * @method fnSettings * @returns {object} AutoFill settings object */ this.fnSettings = function () { return this.s; }; /* Constructor logic */ this._fnInit( oDT, oConfig ); return this; }; AutoFill.prototype = { /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Private methods (they are of course public in JS, but recommended as private) * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /** * Initialisation * @method _fnInit * @param {object} dt DataTables settings object * @param {object} config Configuration object for AutoFill * @returns void */ "_fnInit": function ( dt, config ) { var that = this, i, iLen; // Use DataTables API to get the settings allowing selectors, instances // etc to be used, or for backwards compatibility get from the old // fnSettings method this.s.dt = DataTable.Api ? new DataTable.Api( dt ).settings()[0] : dt.fnSettings(); this.s.init = config || {}; this.dom.table = this.s.dt.nTable; $.extend( true, this.c, AutoFill.defaults, config ); /* Add and configure the columns */ this._initColumns(); /* Auto Fill click and drag icon */ var filler = $('
', { 'class': 'AutoFill_filler' } ) .appendTo( 'body' ); this.dom.filler = filler[0]; // Get the height / width of the click element this.s.filler.height = filler.height(); this.s.filler.width = filler.width(); filler[0].style.display = "none"; /* Border display - one div for each side. We can't just use a single * one with a border, as we want the events to effectively pass through * the transparent bit of the box */ var border; var appender = document.body; if ( that.s.dt.oScroll.sY !== "" ) { that.s.dt.nTable.parentNode.style.position = "relative"; appender = that.s.dt.nTable.parentNode; } border = $('
', { "class": "AutoFill_border" } ); this.dom.borderTop = border.clone().appendTo( appender )[0]; this.dom.borderRight = border.clone().appendTo( appender )[0]; this.dom.borderBottom = border.clone().appendTo( appender )[0]; this.dom.borderLeft = border.clone().appendTo( appender )[0]; /* Events */ filler.on( 'mousedown.DTAF', function (e) { this.onselectstart = function() { return false; }; that._fnFillerDragStart.call( that, e ); return false; } ); $('tbody', this.dom.table).on( 'mouseover.DTAF mouseout.DTAF', '>tr>td, >tr>th', function (e) { that._fnFillerDisplay.call( that, e ); } ); $(this.dom.table).on( 'destroy.dt.DTAF', function () { filler.off( 'mousedown.DTAF' ).remove(); $('tbody', this.dom.table).off( 'mouseover.DTAF mouseout.DTAF' ); } ); }, _initColumns: function ( ) { var that = this; var i, ien; var dt = this.s.dt; var config = this.s.init; for ( i=0, ien=dt.aoColumns.length ; i offsetEnd.left) { x1 = offsetEnd.left - border; x2 = offsetStart.left + $(nStart).outerWidth(); width = offsetStart.left + $(nStart).outerWidth() - offsetEnd.left + (2*border); } if ( this.s.dt.oScroll.sY !== "" ) { /* The border elements are inside the DT scroller - so position relative to that */ var offsetScroll = $(this.s.dt.nTable.parentNode).offset(), scrollTop = $(this.s.dt.nTable.parentNode).scrollTop(), scrollLeft = $(this.s.dt.nTable.parentNode).scrollLeft(); x1 -= offsetScroll.left - scrollLeft; x2 -= offsetScroll.left - scrollLeft; y1 -= offsetScroll.top - scrollTop; y2 -= offsetScroll.top - scrollTop; } /* Top */ oStyle = this.dom.borderTop.style; oStyle.top = y1+"px"; oStyle.left = x1+"px"; oStyle.height = this.s.border.width+"px"; oStyle.width = width+"px"; /* Bottom */ oStyle = this.dom.borderBottom.style; oStyle.top = y2+"px"; oStyle.left = x1+"px"; oStyle.height = this.s.border.width+"px"; oStyle.width = width+"px"; /* Left */ oStyle = this.dom.borderLeft.style; oStyle.top = y1+"px"; oStyle.left = x1+"px"; oStyle.height = height+"px"; oStyle.width = this.s.border.width+"px"; /* Right */ oStyle = this.dom.borderRight.style; oStyle.top = y1+"px"; oStyle.left = x2+"px"; oStyle.height = height+"px"; oStyle.width = this.s.border.width+"px"; }, /** * Mouse down event handler for starting a drag * @method _fnFillerDragStart * @param {Object} e Event object * @returns void */ "_fnFillerDragStart": function (e) { var that = this; var startingTd = this.dom.currentTarget; this.s.drag.dragging = true; that.dom.borderTop.style.display = "block"; that.dom.borderRight.style.display = "block"; that.dom.borderBottom.style.display = "block"; that.dom.borderLeft.style.display = "block"; var coords = this._fnTargetCoords( startingTd ); this.s.drag.startX = coords.x; this.s.drag.startY = coords.y; this.s.drag.startTd = startingTd; this.s.drag.endTd = startingTd; this._fnUpdateBorder( startingTd, startingTd ); $(document).bind('mousemove.AutoFill', function (e) { that._fnFillerDragMove.call( that, e ); } ); $(document).bind('mouseup.AutoFill', function (e) { that._fnFillerFinish.call( that, e ); } ); /* Scrolling information cache */ this.s.screen.y = e.pageY; this.s.screen.height = $(window).height(); this.s.screen.scrollTop = $(document).scrollTop(); if ( this.s.dt.oScroll.sY !== "" ) { this.s.scroller.top = $(this.s.dt.nTable.parentNode).offset().top; this.s.scroller.bottom = this.s.scroller.top + $(this.s.dt.nTable.parentNode).height(); } /* Scrolling handler - we set an interval (which is cancelled on mouse up) which will fire * regularly and see if we need to do any scrolling */ this.s.screen.interval = setInterval( function () { var iScrollTop = $(document).scrollTop(); var iScrollDelta = iScrollTop - that.s.screen.scrollTop; that.s.screen.y += iScrollDelta; if ( that.s.screen.height - that.s.screen.y + iScrollTop < 50 ) { $('html, body').animate( { "scrollTop": iScrollTop + 50 }, 240, 'linear' ); } else if ( that.s.screen.y - iScrollTop < 50 ) { $('html, body').animate( { "scrollTop": iScrollTop - 50 }, 240, 'linear' ); } if ( that.s.dt.oScroll.sY !== "" ) { if ( that.s.screen.y > that.s.scroller.bottom - 50 ) { $(that.s.dt.nTable.parentNode).animate( { "scrollTop": $(that.s.dt.nTable.parentNode).scrollTop() + 50 }, 240, 'linear' ); } else if ( that.s.screen.y < that.s.scroller.top + 50 ) { $(that.s.dt.nTable.parentNode).animate( { "scrollTop": $(that.s.dt.nTable.parentNode).scrollTop() - 50 }, 240, 'linear' ); } } }, 250 ); }, /** * Mouse move event handler for during a move. See if we want to update the display based on the * new cursor position * @method _fnFillerDragMove * @param {Object} e Event object * @returns void */ "_fnFillerDragMove": function (e) { if ( e.target && e.target.nodeName.toUpperCase() == "TD" && e.target != this.s.drag.endTd ) { var coords = this._fnTargetCoords( e.target ); if ( this.c.mode == "y" && coords.x != this.s.drag.startX ) { e.target = $('tbody>tr:eq('+coords.y+')>td:eq('+this.s.drag.startX+')', this.dom.table)[0]; } if ( this.c.mode == "x" && coords.y != this.s.drag.startY ) { e.target = $('tbody>tr:eq('+this.s.drag.startY+')>td:eq('+coords.x+')', this.dom.table)[0]; } if ( this.c.mode == "either") { if(coords.x != this.s.drag.startX ) { e.target = $('tbody>tr:eq('+this.s.drag.startY+')>td:eq('+coords.x+')', this.dom.table)[0]; } else if ( coords.y != this.s.drag.startY ) { e.target = $('tbody>tr:eq('+coords.y+')>td:eq('+this.s.drag.startX+')', this.dom.table)[0]; } } // update coords if ( this.c.mode !== "both" ) { coords = this._fnTargetCoords( e.target ); } var drag = this.s.drag; drag.endTd = e.target; if ( coords.y >= this.s.drag.startY ) { this._fnUpdateBorder( drag.startTd, drag.endTd ); } else { this._fnUpdateBorder( drag.endTd, drag.startTd ); } this._fnFillerPosition( e.target ); } /* Update the screen information so we can perform scrolling */ this.s.screen.y = e.pageY; this.s.screen.scrollTop = $(document).scrollTop(); if ( this.s.dt.oScroll.sY !== "" ) { this.s.scroller.scrollTop = $(this.s.dt.nTable.parentNode).scrollTop(); this.s.scroller.top = $(this.s.dt.nTable.parentNode).offset().top; this.s.scroller.bottom = this.s.scroller.top + $(this.s.dt.nTable.parentNode).height(); } }, /** * Mouse release handler - end the drag and take action to update the cells with the needed values * @method _fnFillerFinish * @param {Object} e Event object * @returns void */ "_fnFillerFinish": function (e) { var that = this, i, iLen, j; $(document).unbind('mousemove.AutoFill mouseup.AutoFill'); this.dom.borderTop.style.display = "none"; this.dom.borderRight.style.display = "none"; this.dom.borderBottom.style.display = "none"; this.dom.borderLeft.style.display = "none"; this.s.drag.dragging = false; clearInterval( this.s.screen.interval ); var cells = []; var table = this.dom.table; var coordsStart = this._fnTargetCoords( this.s.drag.startTd ); var coordsEnd = this._fnTargetCoords( this.s.drag.endTd ); var columnIndex = function ( visIdx ) { return that.s.dt.oApi._fnVisibleToColumnIndex( that.s.dt, visIdx ); }; // xxx - urgh - there must be a way of reducing this... if ( coordsStart.y <= coordsEnd.y ) { for ( i=coordsStart.y ; i<=coordsEnd.y ; i++ ) { if ( coordsStart.x <= coordsEnd.x ) { for ( j=coordsStart.x ; j<=coordsEnd.x ; j++ ) { cells.push( { node: $('tbody>tr:eq('+i+')>td:eq('+j+')', table)[0], x: j - coordsStart.x, y: i - coordsStart.y, colIdx: columnIndex( j ) } ); } } else { for ( j=coordsStart.x ; j>=coordsEnd.x ; j-- ) { cells.push( { node: $('tbody>tr:eq('+i+')>td:eq('+j+')', table)[0], x: j - coordsStart.x, y: i - coordsStart.y, colIdx: columnIndex( j ) } ); } } } } else { for ( i=coordsStart.y ; i>=coordsEnd.y ; i-- ) { if ( coordsStart.x <= coordsEnd.x ) { for ( j=coordsStart.x ; j<=coordsEnd.x ; j++ ) { cells.push( { node: $('tbody>tr:eq('+i+')>td:eq('+j+')', table)[0], x: j - coordsStart.x, y: i - coordsStart.y, colIdx: columnIndex( j ) } ); } } else { for ( j=coordsStart.x ; j>=coordsEnd.x ; j-- ) { cells.push( { node: $('tbody>tr:eq('+i+')>td:eq('+j+')', table)[0], x: coordsStart.x - j, y: coordsStart.y - i, colIdx: columnIndex( j ) } ); } } } } // An auto-fill requires 2 or more cells if ( cells.length <= 1 ) { return; } var edited = []; var previous; for ( i=0, iLen=cells.length ; i