| 1 |
/*! |
| 2 |
* dragtable |
| 3 |
* |
| 4 |
* @Version 1.1.0 |
| 5 |
* |
| 6 |
* Copyright (c) 2010, Andres Koetter akottr@gmail.com |
| 7 |
* Dual licensed under the MIT (MIT-LICENSE.txt) |
| 8 |
* and GPL (GPL-LICENSE.txt) licenses. |
| 9 |
* |
| 10 |
* Inspired by the the dragtable from Dan Vanderkam (danvk.org/dragtable/) |
| 11 |
* Thanks to the jquery and jqueryui comitters |
| 12 |
* |
| 13 |
* Any comment, bug report, feature-request is welcome |
| 14 |
* Feel free to contact me. |
| 15 |
*/ |
| 16 |
|
| 17 |
/* TOKNOW: |
| 18 |
* For IE7 you need this css rule: |
| 19 |
* table { |
| 20 |
* border-collapse: collapse; |
| 21 |
* } |
| 22 |
* Or take a clean reset.css (see http://meyerweb.com/eric/tools/css/reset/) |
| 23 |
*/ |
| 24 |
|
| 25 |
/* TODO: investigate |
| 26 |
* Does not work properly with css rule: |
| 27 |
* html { |
| 28 |
* overflow: -moz-scrollbars-vertical; |
| 29 |
* } |
| 30 |
* Workaround: |
| 31 |
* Fixing Firefox issues by scrolling down the page |
| 32 |
* http://stackoverflow.com/questions/2451528/jquery-ui-sortable-scroll-helper-element-offset-firefox-issue |
| 33 |
* |
| 34 |
* var start = $.noop; |
| 35 |
* var beforeStop = $.noop; |
| 36 |
* if($.browser.mozilla) { |
| 37 |
* var start = function (event, ui) { |
| 38 |
* if( ui.helper !== undefined ) |
| 39 |
* ui.helper.css('position','absolute').css('margin-top', $(window).scrollTop() ); |
| 40 |
* } |
| 41 |
* var beforeStop = function (event, ui) { |
| 42 |
* if( ui.offset !== undefined ) |
| 43 |
* ui.helper.css('margin-top', 0); |
| 44 |
* } |
| 45 |
* } |
| 46 |
* |
| 47 |
* and pass this as start and stop function to the sortable initialisation |
| 48 |
* start: start, |
| 49 |
* beforeStop: beforeStop |
| 50 |
*/ |
| 51 |
|
| 52 |
/* TODO: fix it |
| 53 |
* jqueryui sortable Ticket #4482 |
| 54 |
* Hotfixed it, but not very nice (deprecated api) |
| 55 |
* if(!p.height() || (jQuery.browser.msie && jQuery.browser.version.match('^7|^6'))) { p.height(self.currentItem.innerHeight() - parseInt(self.currentItem.css('paddingTop')||0, 10) - parseInt(self.currentItem.css('paddingBottom')||0, 10)); }; |
| 56 |
* if(!p.width() || (jQuery.browser.msie && jQuery.browser.version.match('^7|^6'))) { p.width(self.currentItem.innerWidth() - parseInt(self.currentItem.css('paddingLeft')||0, 10) - parseInt(self.currentItem.css('paddingRight')||0, 10)); }; |
| 57 |
*/ |
| 58 |
|
| 59 |
/* TODO: support colgroups |
| 60 |
*/ |
| 61 |
|
| 62 |
(function($) { |
| 63 |
$.fn.dragtable = function(options) { |
| 64 |
var defaults = { |
| 65 |
revert:true, // smooth revert |
| 66 |
dragHandle:'.table-handle', // handle for moving cols, if not exists the whole 'th' is the handle |
| 67 |
maxMovingRows:40, // 1 -> only header. 40 row should be enough, the rest is usually not in the viewport |
| 68 |
onlyHeaderThreshold:100, // TODO: not implemented yet, switch automatically between entire col moving / only header moving |
| 69 |
dragaccept:null, // draggable cols -> default all |
| 70 |
persistState:null, // url or function -> plug in your custom persistState function right here. function call is persistState(originalTable) |
| 71 |
restoreState:null, // JSON-Object or function: some kind of experimental aka Quick-Hack TODO: do it better |
| 72 |
beforeStart:$.noop, |
| 73 |
beforeMoving:$.noop, |
| 74 |
beforeReorganize:$.noop, |
| 75 |
beforeStop:$.noop |
| 76 |
}; |
| 77 |
var opts = $.extend(defaults, options); |
| 78 |
|
| 79 |
// here comes the logic. Why var-name _D? My laziness is the culprit! |
| 80 |
var _D = { |
| 81 |
// this is the underlying -original- table |
| 82 |
originalTable:{ |
| 83 |
el:$(), |
| 84 |
selectedHandle:$(), |
| 85 |
sortOrder:{}, |
| 86 |
startIndex:0, |
| 87 |
endIndex:0 |
| 88 |
}, |
| 89 |
// this the sortable table on the layer above the original table |
| 90 |
sortableTable:{ |
| 91 |
el:$(), |
| 92 |
selectedHandle:$(), |
| 93 |
movingRow:$() |
| 94 |
}, |
| 95 |
swapNodes: function(a, b) { |
| 96 |
var aparent= a.parentNode; |
| 97 |
var asibling= a.nextSibling===b? a : a.nextSibling; |
| 98 |
b.parentNode.insertBefore(a, b); |
| 99 |
aparent.insertBefore(b, asibling); |
| 100 |
}, |
| 101 |
// send ids=index as req-param to server |
| 102 |
persistState: function() { |
| 103 |
_D.originalTable.el.find('th').each(function(i) { |
| 104 |
if(this.id != '') {_D.originalTable.sortOrder[this.id]=i;} |
| 105 |
}); |
| 106 |
$.ajax({url: opts.persistState, |
| 107 |
data: _D.originalTable.sortOrder}); |
| 108 |
}, |
| 109 |
/* |
| 110 |
* persistObj looks like |
| 111 |
* {'id1','2','id3':'3','id2':'1'} |
| 112 |
* table looks like |
| 113 |
* | id2 | id1 | id3 | |
| 114 |
*/ |
| 115 |
restoreState: function(persistObj) {; |
| 116 |
for(n in persistObj) { |
| 117 |
_D.originalTable.startIndex = $('#'+n).closest('th').prevAll().size() + 1; |
| 118 |
_D.originalTable.endIndex = parseInt(persistObj[n] + 1); |
| 119 |
_D.bubbleCols(); |
| 120 |
} |
| 121 |
}, |
| 122 |
// bubble the moved col left or right |
| 123 |
bubbleCols: function() { |
| 124 |
var from = _D.originalTable.startIndex; |
| 125 |
var to = _D.originalTable.endIndex; |
| 126 |
if(from < to) { |
| 127 |
for(var i = from; i < to; i++) { |
| 128 |
var row1 = _D.originalTable.el.find('tr > td:nth-child('+i+')') |
| 129 |
.add(_D.originalTable.el.find('tr > th:nth-child('+i+')')); |
| 130 |
var row2 = _D.originalTable.el.find('tr > td:nth-child('+(i+1)+')') |
| 131 |
.add(_D.originalTable.el.find('tr > th:nth-child('+(i+1)+')')); |
| 132 |
for(var j = 0; j < row1.length; j++) { |
| 133 |
_D.swapNodes(row1[j],row2[j]); |
| 134 |
} |
| 135 |
} |
| 136 |
} |
| 137 |
else { |
| 138 |
for(var i = from; i > to; i--) { |
| 139 |
var row1 = _D.originalTable.el.find('tr > td:nth-child('+i+')') |
| 140 |
.add(_D.originalTable.el.find('tr > th:nth-child('+i+')')); |
| 141 |
var row2 = _D.originalTable.el.find('tr > td:nth-child('+(i-1)+')') |
| 142 |
.add(_D.originalTable.el.find('tr > th:nth-child('+(i-1)+')')); |
| 143 |
for(var j = 0; j < row1.length; j++) { |
| 144 |
_D.swapNodes(row1[j],row2[j]); |
| 145 |
} |
| 146 |
} |
| 147 |
} |
| 148 |
}, |
| 149 |
rearrangeTableBackroundProcessing: function() { |
| 150 |
return function() { |
| 151 |
_D.bubbleCols(); |
| 152 |
opts.beforeStop(_D.originalTable); |
| 153 |
_D.sortableTable.el.remove(); |
| 154 |
// persist state if necessary |
| 155 |
if(opts.persistState !== null) { |
| 156 |
$.isFunction(opts.persistState) ? opts.persistState(_D.originalTable) : _D.persistState(); |
| 157 |
} |
| 158 |
}; |
| 159 |
}, |
| 160 |
rearrangeTable: function() { |
| 161 |
// remove handler-class -> handler is now finished |
| 162 |
_D.originalTable.selectedHandle.removeClass('dragtable-handle-selected'); |
| 163 |
// add disabled class -> reorgorganisation starts soon |
| 164 |
_D.sortableTable.el.sortable("disable"); |
| 165 |
_D.sortableTable.el.addClass('dragtable-disabled'); |
| 166 |
opts.beforeReorganize(_D.originalTable,_D.sortableTable); |
| 167 |
// do reorganisation asynchronous |
| 168 |
// for chrome a little bit more than 1 ms because we want to force a rerender |
| 169 |
_D.originalTable.endIndex = _D.sortableTable.movingRow.prevAll().size() + 1; |
| 170 |
setTimeout(_D.rearrangeTableBackroundProcessing(),50); |
| 171 |
}, |
| 172 |
/* |
| 173 |
* Disrupts the table. The original table stays the same. |
| 174 |
* But on a layer above the original table we are constructing a list (ul > li) |
| 175 |
* each li with a separate table representig a single col of the original table. |
| 176 |
*/ |
| 177 |
generateSortable:function(e) { |
| 178 |
// table attributes |
| 179 |
var attrs = _D.originalTable.el[0].attributes; |
| 180 |
var attrsString = ''; |
| 181 |
for(var i=0; i < attrs.length;i++) { |
| 182 |
if(attrs[i].nodeValue) { |
| 183 |
attrsString += attrs[i].nodeName + '="' + attrs[i].nodeValue+'" '; |
| 184 |
} |
| 185 |
} |
| 186 |
|
| 187 |
// row attributes |
| 188 |
var rowAttrsArr = []; |
| 189 |
//compute height, special handling for ie needed :-( |
| 190 |
var heightArr = []; |
| 191 |
_D.originalTable.el.find('tr').slice(0,opts.maxMovingRows).each(function(i,v) { |
| 192 |
// row attributes |
| 193 |
var attrs = this.attributes; |
| 194 |
var attrsString = ""; |
| 195 |
for(var j=0; j < attrs.length;j++) { |
| 196 |
if(attrs[j].nodeValue) { |
| 197 |
attrsString += " " + attrs[j].nodeName + '="' + attrs[j].nodeValue+'"'; |
| 198 |
} |
| 199 |
} |
| 200 |
rowAttrsArr.push(attrsString); |
| 201 |
/* the not so easy way */ |
| 202 |
if(jQuery.browser.msie && jQuery.browser.version.match('^7|^6')) { |
| 203 |
var maxCellHeight = null; |
| 204 |
$(this).children().each(function() { |
| 205 |
var tmp = $(this).height(); |
| 206 |
if(maxCellHeight == null || tmp > maxCellHeight) {maxCellHeight = tmp;} |
| 207 |
}); |
| 208 |
heightArr.push(maxCellHeight); |
| 209 |
} |
| 210 |
/* the easy way, but does not work very good in IE < 8 */ |
| 211 |
else { |
| 212 |
heightArr.push($(this).height()); |
| 213 |
} |
| 214 |
}); |
| 215 |
|
| 216 |
// compute width, no special handling for ie needed :-) |
| 217 |
var widthArr = []; |
| 218 |
// compute total width, needed for not wrapping around after the screen ends (floating) |
| 219 |
var totalWidth=0; |
| 220 |
_D.originalTable.el.find('tr > th').each(function(i,v) { |
| 221 |
// one extra px on right and left side |
| 222 |
totalWidth+=$(this).outerWidth()+2; |
| 223 |
widthArr.push($(this).width()); |
| 224 |
}); |
| 225 |
|
| 226 |
var sortableHtml = '<ul class="dragtable-sortable" style="position:absolute; width:'+totalWidth+'px;">'; |
| 227 |
// assemble the needed html |
| 228 |
_D.originalTable.el.find('tr > th').each(function(i,v) { |
| 229 |
sortableHtml += '<li>'; |
| 230 |
sortableHtml += '<table ' + attrsString + '>'; |
| 231 |
var row = _D.originalTable.el.find('tr > th:nth-child('+(i+1)+')'); |
| 232 |
if(opts.maxMovingRows > 1) { |
| 233 |
row = row.add(_D.originalTable.el.find('tr > td:nth-child('+(i+1)+')').slice(0,opts.maxMovingRows-1)); |
| 234 |
} |
| 235 |
row.each(function(j) { |
| 236 |
/* the not so easy way (part 2)*/ |
| 237 |
if(jQuery.browser.msie && jQuery.browser.version.match('^7|^6')) { |
| 238 |
sortableHtml += '<tr '+ rowAttrsArr[j] + '>'; |
| 239 |
// TODO: May cause duplicate style-Attribute |
| 240 |
sortableHtml += $(this).clone().wrap('<div></div>').parent().html().replace('<TD','<TD style="height:'+heightArr[j]+'px;"'); |
| 241 |
} |
| 242 |
/* the easy way, but does not work very good in IE < 8 (part 2) */ |
| 243 |
else { |
| 244 |
// TODO: May cause duplicate style-Attribute |
| 245 |
sortableHtml += '<tr ' + rowAttrsArr[j] + '" style="height:'+heightArr[j]+'px;">'; |
| 246 |
sortableHtml += $(this).clone().wrap('<div></div>').parent().html(); |
| 247 |
} |
| 248 |
sortableHtml += '</tr>'; |
| 249 |
}); |
| 250 |
sortableHtml += '</table>'; |
| 251 |
sortableHtml += '</li>'; |
| 252 |
}); |
| 253 |
sortableHtml += '</ul>'; |
| 254 |
_D.sortableTable.el = _D.originalTable.el.before(sortableHtml).prev(); |
| 255 |
// set width if necessary |
| 256 |
_D.sortableTable.el.find('th').each(function(i,v) { |
| 257 |
var _this = $(this); |
| 258 |
if(widthArr[i] > _this.width()) { |
| 259 |
_this.css({'width':widthArr[i]}); |
| 260 |
} |
| 261 |
}); |
| 262 |
|
| 263 |
// assign _D.sortableTable.selectedHandle |
| 264 |
_D.sortableTable.selectedHandle = _D.sortableTable.el.find('th') |
| 265 |
.find('.dragtable-handle-selected'); |
| 266 |
|
| 267 |
var items = !opts.dragaccept ? 'li' : 'li:has(' + opts.dragaccept + ')'; |
| 268 |
_D.sortableTable.el.sortable({stop:_D.rearrangeTable, |
| 269 |
items:items, |
| 270 |
revert:opts.revert, |
| 271 |
distance: 0 |
| 272 |
}) |
| 273 |
.disableSelection(); |
| 274 |
|
| 275 |
// assign start index |
| 276 |
_D.originalTable.startIndex = $(e.target).closest('th').prevAll().size() + 1; |
| 277 |
|
| 278 |
opts.beforeMoving(_D.originalTable, _D.sortableTable); |
| 279 |
// Start moving by delegating the original event to the new sortable table |
| 280 |
_D.sortableTable.movingRow = _D.sortableTable.el.find('li:nth-child('+_D.originalTable.startIndex+')'); |
| 281 |
// TODO: learn more about events. Is this the right way? |
| 282 |
// create down event and delegate it to sortable |
| 283 |
var mousedownEvt = $.Event('mousedown'); |
| 284 |
mousedownEvt.pageX=e.pageX; |
| 285 |
mousedownEvt.pageY=e.pageY; |
| 286 |
mousedownEvt.which=1; |
| 287 |
_D.sortableTable.movingRow.trigger(mousedownEvt); |
| 288 |
|
| 289 |
if($.support.noCloneEvent) { |
| 290 |
// create move event and delegate it to sortable |
| 291 |
var mousemoveEvt = $.Event('mousemove'); |
| 292 |
mousemoveEvt.pageX=e.pageX+5; |
| 293 |
mousemoveEvt.pageY=e.pageY+5; |
| 294 |
_D.sortableTable.movingRow.trigger(mousemoveEvt); |
| 295 |
} |
| 296 |
}, |
| 297 |
/* Start asynchronously to be able to give the user a feedback on mousedown (rerender the dom) |
| 298 |
* Currently disabled. Is it a bug in jQuery? |
| 299 |
* TODO: Bugreport jQuery -> data.events not undefined-save line 4519, when using delayed events |
| 300 |
* It issues only warnings in IE, but that offends me anyway. |
| 301 |
*/ |
| 302 |
delayedStart:function(evt) { |
| 303 |
return function() { |
| 304 |
_D.generateSortable(evt); |
| 305 |
}; |
| 306 |
} |
| 307 |
}; |
| 308 |
|
| 309 |
return this.each(function(){ |
| 310 |
_D.originalTable.el = $(this); |
| 311 |
// bind draggable to 'th' by default |
| 312 |
var bindTo = _D.originalTable.el.find('th'); |
| 313 |
// filter only the cols that are accepted |
| 314 |
if(opts.dragaccept) { bindTo = bindTo.filter(opts.dragaccept); } |
| 315 |
// bind draggable to handle if exists |
| 316 |
if(bindTo.find(opts.dragHandle).size() > 0) { bindTo = bindTo.find(opts.dragHandle);} |
| 317 |
// restore state if necessary |
| 318 |
if(opts.restoreState !== null) { |
| 319 |
$.isFunction(opts.restoreState) ? opts.restoreState(_D.originalTable) : _D.restoreState(opts.restoreState); |
| 320 |
} |
| 321 |
bindTo.bind('mousedown',function(evt) { |
| 322 |
_D.originalTable.selectedHandle = $(this); |
| 323 |
_D.originalTable.selectedHandle.addClass('dragtable-handle-selected'); |
| 324 |
opts.beforeStart(_D.originalTable); |
| 325 |
// take a breath and rerender before creating sortable table |
| 326 |
// setTimeout(_D.delayedStart(evt),10); |
| 327 |
// for immediate start (no delay) |
| 328 |
_D.generateSortable(evt); |
| 329 |
}); |
| 330 |
}); |
| 331 |
}; |
| 332 |
})(jQuery); |