/*! * dragtable - jquery ui widget to re-order table columns * version 3.0 * * Copyright (c) 2010, Jesse Baird * 12/2/2010 * https://github.com/jebaird/dragtable * * Licensed under the MIT (LICENSE.txt) * * * * Forked from https://github.com/akottr/dragtable - Andres Koetter akottr@gmail.com * * * * * quick down and and dirty on how this works * ########################################### * so when a column is selected we grab all of the cells in that row and clone append them to a semi copy of the parent table and the * "real" cells get a place holder class witch is removed when the dragstop event is triggered * * * make it easy to have a button swap columns * * * Events - in order of trigger * dragtablestart - when the user mouses down on handle or th, use in favor of display helper * dragtablebeforeChange - called when a col will be moved * dragtablechange - called after the col has been moved * dragtablestop - the user mouses up and stops dragging and the drag display is removed from the dom * * * * * IE notes * ie8 in quirks mode will only drag once after that the events are lost * */ (function($) { $.widget("jb.dragtable", { eventWidgetPrefix: 'dragtable', options: { //used to the col headers, data contained in here is used to set / get the name of the col dataHeader:'data-header', //class name that handles have handle:'dragtable-drag-handle', //draggable items in cols, .dragtable-drag-handle has to match the handle options items: 'th:not( :has( .dragtable-drag-handle ) ), .dragtable-drag-handle', //if a col header as this class, cols cant be dragged past it boundary: 'dragtable-drag-boundary', //classnames that get applied to the real td, th placeholder: 'dragtable-col-placeholder', /* the drag display will be appended to this element, if this is set to document.body and has been zeroed off the display will seem to jump Its either : parent or a dom node */ appendTarget: ':parent', //if true,this will scroll the appendTarget offsetParent when the dragDisplay is dragged past its boundaries scroll: false }, // when a col is dragged use this to find the semantic elements, for speed tableElemIndex:{ head: '0', body: '1', foot: '2' }, tbodyRegex: /(tbody|TBODY)/, theadRegex: /(thead|THEAD)/, tfootRegex: /(tfoot|TFOOT)/, _create: function() { //console.log(this); //used start/end of drag this.startIndex = null; this.endIndex = null; //the references to the table cells that are getting dragged this.currentColumnCollection = []; //the references the position of the first element in the currentColunmCollection position this.currentColumnCollectionOffset = {}; //the div wrapping the drag display table this.dragDisplay = $([]) var self = this, o = self.options, el = self.element; o.appendTarget = ( o.appendTarget === ':parent' ) ? this.element.parent() : $( o.appendTarget ); //grab the ths and the handles and bind them el.delegate(o.items, 'mousedown.' + self.widgetEventPrefix, function(e){ var $handle = $(this), elementOffsetTop = self.element.position().top; //make sure we are working with a th instead of a handle if( $handle.hasClass( o.handle ) ){ $handle = $handle.closest('th'); //change the target to the th, so the handler can pick up the offsetleft e.currentTarget = $handle.closest('th')[0] } self.getCol( $handle.index() ) .attr( 'tabindex', -1 ) .focus() .disableSelection() .css({ top: elementOffsetTop, //need to account for the scroll left of the append target, other wise the display will be off by that many pix left: ( self.currentColumnCollectionOffset.left + o.appendTarget[0].scrollLeft ) }) .appendTo( o.appendTarget ) self._mousemoveHandler( e ); //############ }); }, /* * e.currentTarget is used for figuring out offsetLeft * getCol must be called before this is * */ _mousemoveHandler: function( e ){ //call this first, catch any drag display issues this._start( e ) var self = this, o = self.options, prevMouseX = e.pageX, dragDisplayWidth = self.dragDisplay.outerWidth(), halfDragDisplayWidth = dragDisplayWidth / 2, appendTargetOP = o.appendTarget.offsetParent()[0], scroll = o.scroll, //get the col count, used to contain col swap colCount = self.element[ 0 ] .getElementsByTagName( 'thead' )[ 0 ] .getElementsByTagName( 'tr' )[ 0 ] .getElementsByTagName( 'th' ) .length - 1; $( document ).bind('mousemove.' + self.widgetEventPrefix, function( e ){ var columnPos = self._setCurrentColumnCollectionOffset(), mouseXDiff = e.pageX - prevMouseX, appendTarget = o.appendTarget[0], left = ( parseInt( self.dragDisplay[0].style.left ) + mouseXDiff ); self.dragDisplay.css( 'left', left ) /* * when moving left and e.pageX and prevMouseX are the same it will trigger right when moving left * * it should only swap cols when the col dragging is half over the prev/next col */ if( e.pageX < prevMouseX ){ //move left var threshold = columnPos.left - halfDragDisplayWidth; //scroll left if( left < ( appendTarget.clientWidth - dragDisplayWidth ) && scroll == true ) { var scrollLeft = appendTarget.scrollLeft + mouseXDiff /* * firefox does scroll the body with target being body but chome does */ if( appendTarget.tagName == 'BODY' ) { window.scroll( window.scrollX + scrollLeft, window.scrollY ); } else { appendTarget.scrollLeft = scrollLeft; } } if( left < threshold ){ self._swapCol(self.startIndex-1); } }else{ //move right var threshold = columnPos.left + halfDragDisplayWidth ; //scroll right if( left > (appendTarget.clientWidth - dragDisplayWidth ) && scroll == true ) { //console.log( o.appendTarget[0].clientWidth + (e.pageX - prevMouseX)) var scrollLeft = appendTarget.scrollLeft + mouseXDiff /* * firefox does scroll the body with target being body but chome does */ if( appendTarget.tagName == 'BODY' ) { window.scroll( window.scrollX + scrollLeft, window.scrollY ); } else { appendTarget.scrollLeft = scrollLeft; } } //move to the right only if x is greater than threshold and the current col isn' the last one if( left > threshold && colCount != self.startIndex ){ self._swapCol( self.startIndex + 1 ); } } //update mouse position prevMouseX = e.pageX; }) .one( 'mouseup.' + self.widgetEventPrefix ,function(e ){ self._stop( e ); }); }, _start: function( e ){ $( document ) //move disableselection and cursor to default handlers of the start event .disableSelection() .css( 'cursor', 'move'); // guess the width of the column that is getting dragged and apply it to the drag display. fixes issues with // cols / tables having fixed widths this.dragDisplay.width( this.currentColumnCollection[0].clientWidth ) return this._eventHelper('start',e); }, _stop: function( e ){ // issue #25 reorder the stop event order always remove the stop event $( document ) .unbind( 'mousemove.' + this.widgetEventPrefix ) .enableSelection() .css( 'cursor', ''); // clean up this .dropCol() .dragDisplay.remove(); // let the world know we have stopped this._eventHelper('stop',e,{}); }, _setOption: function(option, value) { $.Widget.prototype._setOption.apply( this, arguments ); }, /* * get the selected index cell out of table row * needs to work as fast as possible. and performance gains in this method are worth the time * because its used to build the drag display and get the cells on col swap * http://jsperf.com/binary-regex-vs-string-equality/4 */ _getCells: function( elem, index ){ //console.time('getcells'); var td, parentNodeName, ei = this.tableElemIndex, //TODO: clean up this format tds = { //store where the cells came from 'semantic':{ '0': [],//head throws error if ei.head or ei['head'] '1': [],//body '2': []//footer }, //keep a ref in a flat array for easy access 'array':[] }, //cache regex, reduces looking up the chain theadRegex = this.theadRegex, tbodyRegex = this.tbodyRegex, tfootRegex = this.tfootRegex, tdsSemanticHead = tds.semantic[ei.head], tdsSemanticBody = tds.semantic[ei.body], tdsSemanticFoot = tds.semantic[ei.foot]; //console.log(index); //check does this col exsist if(index <= -1 || typeof elem.rows[0].cells[index] == undefined){ return tds; } for(var i = 0, length = elem.rows.length; i < length; i++){ td = elem.rows[i].cells[index]; //if the row has no cells dont error out; if( td == undefined ){ continue; } parentNodeName = td.parentNode.parentNode.nodeName; tds.array.push(td); //faster to leave out ^ and $ in the regular expression if( tbodyRegex.test( parentNodeName ) ){ tdsSemanticBody.push( td ); }else if( theadRegex.test( parentNodeName ) ){ tdsSemanticHead.push( td ); }else if( this.tfootRegex.test( parentNodeName ) ){ tdsSemanticFoot.push( td ); } } return tds; }, /* * returns all element attrs in a string key="value" key2="value" */ _getElementAttributes: function(element){ var attrsString = [], attrs = element.attributes, i = 0, length = attrs.length; for( ; i < length; i++) { attrsString.push( attrs[i].nodeName + '="' + attrs[i].value+'"' ); } return attrsString.join(' '); }, /* * faster than swap nodes * only works if a b parent are the same, works great for columns */ _swapCells: function(a, b) { a.parentNode.insertBefore(b, a); }, /* * used to trigger optional events */ _eventHelper: function(eventName ,eventObj, additionalData){ return this._trigger( eventName, eventObj, $.extend({ column: this.currentColumnCollection, order: this.order(), startIndex: this.startIndex, endIndex: this.endIndex, dragDisplay: this.dragDisplay, columnOffset: this.currentColumnCollectionOffset },additionalData) ); }, /* * build copy of table and attach the selected col to it, also removes the select col out of the table * @returns copy of table with the selected col * * populates self.dragDisplay * TODO: name this something better, like select col or get dragDisplay * */ getCol: function(index){ //console.log('index of col '+index); //drag display is just simple html var target, cells, clone, tr, i, length, $table = this.element, self = this, eIndex = self.tableElemIndex, placholderClassnames = ' ' + this.options.placeholder;; //BUG: IE thinks that this table is disabled, dont know how that happend self.dragDisplay = $('
') .addClass('dragtable-drag-col'); //start and end are the same to start out with self.startIndex = self.endIndex = index; cells = self._getCells($table[0], index); self.currentColumnCollection = cells.array; //################################ //TODO: convert to for in // its faster than each $.each(cells.semantic,function(k,collection){ //dont bother processing if there is nothing here if(collection.length == 0){ return; } if ( k == '0' ){ target = document.createElement('thead'); self.dragDisplay[0].appendChild(target); }else if ( k == 1 ) { target = document.createElement('tbody'); self.dragDisplay[0].appendChild(target); }else { target = document.createElement('tfoot'); self.dragDisplay[0].appendChild(target); } for(i = 0,length = collection.length; i < length; i++){ clone = collection[i].cloneNode(true); collection[i].className+=placholderClassnames; tr = document.createElement('tr'); tr.appendChild(clone); target.appendChild(tr); } }); this._setCurrentColumnCollectionOffset(); self.dragDisplay = $('
').append(self.dragDisplay) return self.dragDisplay; }, _setCurrentColumnCollectionOffset: function(){ return this.currentColumnCollectionOffset = $( this.currentColumnCollection[0] ).position(); }, /* * move column left or right */ _swapCol: function( to ){ //cant swap if same position if(to == this.startIndex){ return false; } var from = this.startIndex; this.endIndex = to; //this col cant be moved past me var th = this.element.find('th').eq( to ); //check on th if( th.hasClass( this.options.boundary ) == true ){ return false; } //check handle element if( th.find( '.' + this.options.handle ).hasClass( this.options.boundary ) == true ){ return false; } if( this._eventHelper('beforeChange',{}) === false ){ return false; }; if(from < to) { //console.log('move right'); for(var i = from; i < to; i++) { var row2 = this._getCells(this.element[0],i+1); // console.log(row2) for(var j = 0, length = row2.array.length; j < length; j++){ this._swapCells(this.currentColumnCollection[j],row2.array[j]); } } } else { //console.log('move left'); for(var i = from; i > to; i--) { var row2 = this._getCells(this.element[0],i-1); for(var j = 0, length = row2.array.length; j < length; j++){ this._swapCells(row2.array[j],this.currentColumnCollection[j]); } } } this._eventHelper('change',{}); this.startIndex = this.endIndex; }, /* * called when drag start is finished */ dropCol: function(){ //TODO: cache this when the option is set var regex = new RegExp("(?:^|\\s)" + this.options.placeholder + "(?!\\S)",'g'); //remove placeholder class //dont use jquery.fn.removeClass for performance reasons for(var i = 0, length = this.currentColumnCollection.length; i < length; i++){ var td = this.currentColumnCollection[i]; td.className = td.className.replace(regex,'') } return this; }, /* * get / set the current order of the cols */ order: function(order){ var self = this, elem = self.element, options = self.options, headers = elem.find('thead tr:first').children('th'); if(order == undefined){ //get var ret = []; headers.each(function(){ var header = this.getAttribute(options.dataHeader); if(header == null){ //the attr is missing so grab the text and use that header = $(this).text(); } ret.push(header); }); return ret; }else{ //set //headers and order have to match up if(order.length != headers.length){ return self; } for(var i = 0, length = order.length; i < length; i++){ var start = headers.filter('['+ options.dataHeader +'='+ order[i] +']').index(); if(start != -1){ self.startIndex = start; self.currentColumnCollection = self._getCells(self.element[0], start).array; self._swapCol(i); } } return self; } }, destroy: function() { var self = this, o = self.options; this.element.undelegate( o.items, 'mousedown.' + self.widgetEventPrefix ); $( document ).unbind('.' + self.widgetEventPrefix ) } }); })(jQuery);