jquery.dragtable.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. /*!
  2. * dragtable
  3. *
  4. * @Version 2.0.15
  5. *
  6. * Copyright (c) 2010-2013, Andres 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. /* TOKNOW:
  17. * For IE7 you need this css rule:
  18. * table {
  19. * border-collapse: collapse;
  20. * }
  21. * Or take a clean reset.css (see http://meyerweb.com/eric/tools/css/reset/)
  22. */
  23. /* TODO: investigate
  24. * Does not work properly with css rule:
  25. * html {
  26. * overflow: -moz-scrollbars-vertical;
  27. * }
  28. * Workaround:
  29. * Fixing Firefox issues by scrolling down the page
  30. * http://stackoverflow.com/questions/2451528/jquery-ui-sortable-scroll-helper-element-offset-firefox-issue
  31. *
  32. * var start = $.noop;
  33. * var beforeStop = $.noop;
  34. * if($.browser.mozilla) {
  35. * var start = function (event, ui) {
  36. * if( ui.helper !== undefined )
  37. * ui.helper.css('position','absolute').css('margin-top', $(window).scrollTop() );
  38. * }
  39. * var beforeStop = function (event, ui) {
  40. * if( ui.offset !== undefined )
  41. * ui.helper.css('margin-top', 0);
  42. * }
  43. * }
  44. *
  45. * and pass this as start and stop function to the sortable initialisation
  46. * start: start,
  47. * beforeStop: beforeStop
  48. */
  49. /*
  50. * Special thx to all pull requests comitters
  51. */
  52. (function($) {
  53. $.widget("akottr.dragtable", {
  54. options: {
  55. revert: false, // smooth revert
  56. dragHandle: '.table-handle', // handle for moving cols, if not exists the whole 'th' is the handle
  57. maxMovingRows: 40, // 1 -> only header. 40 row should be enough, the rest is usually not in the viewport
  58. excludeFooter: false, // excludes the footer row(s) while moving other columns. Make sense if there is a footer with a colspan. */
  59. onlyHeaderThreshold: 100, // TODO: not implemented yet, switch automatically between entire col moving / only header moving
  60. dragaccept: null, // draggable cols -> default all
  61. persistState: null, // url or function -> plug in your custom persistState function right here. function call is persistState(originalTable)
  62. restoreState: null, // JSON-Object or function: some kind of experimental aka Quick-Hack TODO: do it better
  63. exact: true, // removes pixels, so that the overlay table width fits exactly the original table width
  64. clickDelay: 10, // ms to wait before rendering sortable list and delegating click event
  65. containment: null, // @see http://api.jqueryui.com/sortable/#option-containment, use it if you want to move in 2 dimesnions (together with axis: null)
  66. cursor: 'move', // @see http://api.jqueryui.com/sortable/#option-cursor
  67. cursorAt: false, // @see http://api.jqueryui.com/sortable/#option-cursorAt
  68. distance: 0, // @see http://api.jqueryui.com/sortable/#option-distance, for immediate feedback use "0"
  69. tolerance: 'pointer', // @see http://api.jqueryui.com/sortable/#option-tolerance
  70. axis: 'x', // @see http://api.jqueryui.com/sortable/#option-axis, Only vertical moving is allowed. Use 'x' or null. Use this in conjunction with the 'containment' setting
  71. beforeStart: $.noop, // returning FALSE will stop the execution chain.
  72. beforeMoving: $.noop,
  73. beforeReorganize: $.noop,
  74. beforeStop: $.noop
  75. },
  76. originalTable: {
  77. el: null,
  78. selectedHandle: null,
  79. sortOrder: null,
  80. startIndex: 0,
  81. endIndex: 0
  82. },
  83. sortableTable: {
  84. el: $(),
  85. selectedHandle: $(),
  86. movingRow: $()
  87. },
  88. persistState: function() {
  89. var _this = this;
  90. this.originalTable.el.find('th').each(function(i) {
  91. if (this.id !== '') {
  92. _this.originalTable.sortOrder[this.id] = i;
  93. }
  94. });
  95. $.ajax({
  96. url: this.options.persistState,
  97. data: this.originalTable.sortOrder
  98. });
  99. },
  100. /*
  101. * persistObj looks like
  102. * {'id1':'2','id3':'3','id2':'1'}
  103. * table looks like
  104. * | id2 | id1 | id3 |
  105. */
  106. _restoreState: function(persistObj) {
  107. for (var n in persistObj) {
  108. this.originalTable.startIndex = $('#' + n).closest('th').prevAll().length + 1;
  109. this.originalTable.endIndex = parseInt(persistObj[n], 10) + 1;
  110. this._bubbleCols();
  111. }
  112. },
  113. // bubble the moved col left or right
  114. _bubbleCols: function() {
  115. var i, j, col1, col2;
  116. var from = this.originalTable.startIndex;
  117. var to = this.originalTable.endIndex;
  118. /* Find children thead and tbody.
  119. * Only to process the immediate tr-children. Bugfix for inner tables
  120. */
  121. var thtb = this.originalTable.el.children();
  122. if (this.options.excludeFooter) {
  123. thtb = thtb.not('tfoot');
  124. }
  125. if (from < to) {
  126. for (i = from; i < to; i++) {
  127. col1 = thtb.find('> tr > td:nth-child(' + i + ')')
  128. .add(thtb.find('> tr > th:nth-child(' + i + ')'));
  129. col2 = thtb.find('> tr > td:nth-child(' + (i + 1) + ')')
  130. .add(thtb.find('> tr > th:nth-child(' + (i + 1) + ')'));
  131. for (j = 0; j < col1.length; j++) {
  132. swapNodes(col1[j], col2[j]);
  133. }
  134. }
  135. } else {
  136. for (i = from; i > to; i--) {
  137. col1 = thtb.find('> tr > td:nth-child(' + i + ')')
  138. .add(thtb.find('> tr > th:nth-child(' + i + ')'));
  139. col2 = thtb.find('> tr > td:nth-child(' + (i - 1) + ')')
  140. .add(thtb.find('> tr > th:nth-child(' + (i - 1) + ')'));
  141. for (j = 0; j < col1.length; j++) {
  142. swapNodes(col1[j], col2[j]);
  143. }
  144. }
  145. }
  146. },
  147. _rearrangeTableBackroundProcessing: function() {
  148. var _this = this;
  149. return function() {
  150. _this._bubbleCols();
  151. _this.options.beforeStop(_this.originalTable);
  152. _this.sortableTable.el.remove();
  153. restoreTextSelection();
  154. // persist state if necessary
  155. if (_this.options.persistState !== null) {
  156. $.isFunction(_this.options.persistState) ? _this.options.persistState(_this.originalTable) : _this.persistState();
  157. }
  158. };
  159. },
  160. _rearrangeTable: function() {
  161. var _this = this;
  162. return function() {
  163. // remove handler-class -> handler is now finished
  164. _this.originalTable.selectedHandle.removeClass('dragtable-handle-selected');
  165. // add disabled class -> reorgorganisation starts soon
  166. _this.sortableTable.el.sortable("disable");
  167. _this.sortableTable.el.addClass('dragtable-disabled');
  168. _this.options.beforeReorganize(_this.originalTable, _this.sortableTable);
  169. // do reorganisation asynchronous
  170. // for chrome a little bit more than 1 ms because we want to force a rerender
  171. _this.originalTable.endIndex = _this.sortableTable.movingRow.prevAll().length + 1;
  172. setTimeout(_this._rearrangeTableBackroundProcessing(), 50);
  173. };
  174. },
  175. /*
  176. * Disrupts the table. The original table stays the same.
  177. * But on a layer above the original table we are constructing a list (ul > li)
  178. * each li with a separate table representig a single col of the original table.
  179. */
  180. _generateSortable: function(e) {
  181. !e.cancelBubble && (e.cancelBubble = true);
  182. var _this = this;
  183. // table attributes
  184. var attrs = this.originalTable.el[0].attributes;
  185. var attrsString = '';
  186. for (var i = 0; i < attrs.length; i++) {
  187. if (attrs[i].nodeValue && attrs[i].nodeName != 'id' && attrs[i].nodeName != 'width') {
  188. attrsString += attrs[i].nodeName + '="' + attrs[i].nodeValue + '" ';
  189. }
  190. }
  191. // row attributes
  192. var rowAttrsArr = [];
  193. //compute height, special handling for ie needed :-(
  194. var heightArr = [];
  195. this.originalTable.el.find('tr').slice(0, this.options.maxMovingRows).each(function(i, v) {
  196. // row attributes
  197. var attrs = this.attributes;
  198. var attrsString = "";
  199. for (var j = 0; j < attrs.length; j++) {
  200. if (attrs[j].nodeValue && attrs[j].nodeName != 'id') {
  201. attrsString += " " + attrs[j].nodeName + '="' + attrs[j].nodeValue + '"';
  202. }
  203. }
  204. rowAttrsArr.push(attrsString);
  205. heightArr.push($(this).height());
  206. });
  207. // compute width, no special handling for ie needed :-)
  208. var widthArr = [];
  209. // compute total width, needed for not wrapping around after the screen ends (floating)
  210. var totalWidth = 0;
  211. /* Find children thead and tbody.
  212. * Only to process the immediate tr-children. Bugfix for inner tables
  213. */
  214. var thtb = _this.originalTable.el.children();
  215. if (this.options.excludeFooter) {
  216. thtb = thtb.not('tfoot');
  217. }
  218. thtb.find('> tr > th').each(function(i, v) {
  219. var w = $(this).is(':visible') ? $(this).outerWidth() : 0;
  220. widthArr.push(w);
  221. totalWidth += w;
  222. });
  223. if(_this.options.exact) {
  224. var difference = totalWidth - _this.originalTable.el.outerWidth();
  225. widthArr[0] -= difference;
  226. }
  227. // one extra px on right and left side
  228. totalWidth += 2
  229. var sortableHtml = '<ul class="dragtable-sortable" style="position:absolute; width:' + totalWidth + 'px;">';
  230. // assemble the needed html
  231. thtb.find('> tr > th').each(function(i, v) {
  232. var width_li = $(this).is(':visible') ? $(this).outerWidth() : 0;
  233. sortableHtml += '<li style="width:' + width_li + 'px;">';
  234. sortableHtml += '<table ' + attrsString + '>';
  235. var row = thtb.find('> tr > th:nth-child(' + (i + 1) + ')');
  236. if (_this.options.maxMovingRows > 1) {
  237. row = row.add(thtb.find('> tr > td:nth-child(' + (i + 1) + ')').slice(0, _this.options.maxMovingRows - 1));
  238. }
  239. row.each(function(j) {
  240. // TODO: May cause duplicate style-Attribute
  241. var row_content = $(this).clone().wrap('<div></div>').parent().html();
  242. if (row_content.toLowerCase().indexOf('<th') === 0) sortableHtml += "<thead>";
  243. sortableHtml += '<tr ' + rowAttrsArr[j] + '" style="height:' + heightArr[j] + 'px;">';
  244. sortableHtml += row_content;
  245. if (row_content.toLowerCase().indexOf('<th') === 0) sortableHtml += "</thead>";
  246. sortableHtml += '</tr>';
  247. });
  248. sortableHtml += '</table>';
  249. sortableHtml += '</li>';
  250. });
  251. sortableHtml += '</ul>';
  252. this.sortableTable.el = this.originalTable.el.before(sortableHtml).prev();
  253. // set width if necessary
  254. this.sortableTable.el.find('> li > table').each(function(i, v) {
  255. $(this).css('width', widthArr[i] + 'px');
  256. });
  257. // assign this.sortableTable.selectedHandle
  258. this.sortableTable.selectedHandle = this.sortableTable.el.find('th .dragtable-handle-selected');
  259. var items = !this.options.dragaccept ? 'li' : 'li:has(' + this.options.dragaccept + ')';
  260. this.sortableTable.el.sortable({
  261. items: items,
  262. stop: this._rearrangeTable(),
  263. // pass thru options for sortable widget
  264. revert: this.options.revert,
  265. tolerance: this.options.tolerance,
  266. containment: this.options.containment,
  267. cursor: this.options.cursor,
  268. cursorAt: this.options.cursorAt,
  269. distance: this.options.distance,
  270. axis: this.options.axis
  271. });
  272. // assign start index
  273. this.originalTable.startIndex = $(e.target).closest('th').prevAll().length + 1;
  274. this.options.beforeMoving(this.originalTable, this.sortableTable);
  275. // Start moving by delegating the original event to the new sortable table
  276. this.sortableTable.movingRow = this.sortableTable.el.find('> li:nth-child(' + this.originalTable.startIndex + ')');
  277. // prevent the user from drag selecting "highlighting" surrounding page elements
  278. disableTextSelection();
  279. // clone the initial event and trigger the sort with it
  280. this.sortableTable.movingRow.trigger($.extend($.Event(e.type), {
  281. which: 1,
  282. clientX: e.clientX,
  283. clientY: e.clientY,
  284. pageX: e.pageX,
  285. pageY: e.pageY,
  286. screenX: e.screenX,
  287. screenY: e.screenY
  288. }));
  289. // Some inner divs to deliver the posibillity to style the placeholder more sophisticated
  290. var placeholder = this.sortableTable.el.find('.ui-sortable-placeholder');
  291. if(!placeholder.height() <= 0) {
  292. placeholder.css('height', this.sortableTable.el.find('.ui-sortable-helper').height());
  293. }
  294. placeholder.html('<div class="outer" style="height:100%;"><div class="inner" style="height:100%;"></div></div>');
  295. },
  296. bindTo: {},
  297. _create: function() {
  298. this.originalTable = {
  299. el: this.element,
  300. selectedHandle: $(),
  301. sortOrder: {},
  302. startIndex: 0,
  303. endIndex: 0
  304. };
  305. // bind draggable to 'th' by default
  306. this.bindTo = this.originalTable.el.find('th');
  307. // filter only the cols that are accepted
  308. if (this.options.dragaccept) {
  309. this.bindTo = this.bindTo.filter(this.options.dragaccept);
  310. }
  311. // bind draggable to handle if exists
  312. if (this.bindTo.find(this.options.dragHandle).length > 0) {
  313. this.bindTo = this.bindTo.find(this.options.dragHandle);
  314. }
  315. // restore state if necessary
  316. if (this.options.restoreState !== null) {
  317. $.isFunction(this.options.restoreState) ? this.options.restoreState(this.originalTable) : this._restoreState(this.options.restoreState);
  318. }
  319. var _this = this;
  320. this.bindTo.mousedown(function(evt) {
  321. // listen only to left mouse click
  322. if(evt.which!==1) return;
  323. if (_this.options.beforeStart(_this.originalTable) === false) {
  324. return;
  325. }
  326. clearTimeout(this.downTimer);
  327. this.downTimer = setTimeout(function() {
  328. _this.originalTable.selectedHandle = $(this);
  329. _this.originalTable.selectedHandle.addClass('dragtable-handle-selected');
  330. _this._generateSortable(evt);
  331. }, _this.options.clickDelay);
  332. }).mouseup(function(evt) {
  333. clearTimeout(this.downTimer);
  334. });
  335. },
  336. redraw: function(){
  337. this.destroy();
  338. this._create();
  339. },
  340. destroy: function() {
  341. this.bindTo.unbind('mousedown');
  342. $.Widget.prototype.destroy.apply(this, arguments); // default destroy
  343. // now do other stuff particular to this widget
  344. }
  345. });
  346. /** closure-scoped "private" functions **/
  347. var body_onselectstart_save = $(document.body).attr('onselectstart'),
  348. body_unselectable_save = $(document.body).attr('unselectable');
  349. // css properties to disable user-select on the body tag by appending a <style> tag to the <head>
  350. // remove any current document selections
  351. function disableTextSelection() {
  352. // jQuery doesn't support the element.text attribute in MSIE 8
  353. // http://stackoverflow.com/questions/2692770/style-style-textcss-appendtohead-does-not-work-in-ie
  354. var $style = $('<style id="__dragtable_disable_text_selection__" type="text/css">body { -ms-user-select:none;-moz-user-select:-moz-none;-khtml-user-select:none;-webkit-user-select:none;user-select:none; }</style>');
  355. $(document.head).append($style);
  356. $(document.body).attr('onselectstart', 'return false;').attr('unselectable', 'on');
  357. if (window.getSelection) {
  358. window.getSelection().removeAllRanges();
  359. } else {
  360. document.selection.empty(); // MSIE http://msdn.microsoft.com/en-us/library/ms535869%28v=VS.85%29.aspx
  361. }
  362. }
  363. // remove the <style> tag, and restore the original <body> onselectstart attribute
  364. function restoreTextSelection() {
  365. $('#__dragtable_disable_text_selection__').remove();
  366. if (body_onselectstart_save) {
  367. $(document.body).attr('onselectstart', body_onselectstart_save);
  368. } else {
  369. $(document.body).removeAttr('onselectstart');
  370. }
  371. if (body_unselectable_save) {
  372. $(document.body).attr('unselectable', body_unselectable_save);
  373. } else {
  374. $(document.body).removeAttr('unselectable');
  375. }
  376. }
  377. function swapNodes(a, b) {
  378. var aparent = a.parentNode;
  379. var asibling = a.nextSibling === b ? a : a.nextSibling;
  380. b.parentNode.insertBefore(a, b);
  381. aparent.insertBefore(b, asibling);
  382. }
  383. })(jQuery);