jquery.tablednd.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. /**
  2. * TableDnD plug-in for JQuery, allows you to drag and drop table rows
  3. * You can set up various options to control how the system will work
  4. * Copyright (c) Denis Howlett <denish@isocra.com>
  5. * License: MIT.
  6. * See https://github.com/isocra/TableDnD
  7. */
  8. /*jshint -W054 */
  9. /*jshint laxbreak: true */
  10. /*jshint expr: true */
  11. !function ($, window, document, undefined) {
  12. // Determine if this is a touch device
  13. var hasTouch = 'ontouchstart' in document.documentElement,
  14. startEvent = 'touchstart mousedown',
  15. moveEvent = 'touchmove mousemove',
  16. endEvent = 'touchend mouseup';
  17. $(document).ready(function () {
  18. function parseStyle(css) {
  19. var objMap = {},
  20. parts = css.match(/([^;:]+)/g) || [];
  21. while (parts.length)
  22. objMap[parts.shift()] = parts.shift().trim();
  23. return objMap;
  24. }
  25. $('table').each(function () {
  26. if ($(this).data('table') === 'dnd') {
  27. $(this).tableDnD({
  28. onDragStyle: $(this).data('ondragstyle') && parseStyle($(this).data('ondragstyle')) || null,
  29. onDropStyle: $(this).data('ondropstyle') && parseStyle($(this).data('ondropstyle')) || null,
  30. onDragClass: $(this).data('ondragclass') === undefined && "tDnD_whileDrag" || $(this).data('ondragclass'),
  31. onDrop: $(this).data('ondrop') && new Function('table', 'row', $(this).data('ondrop')), // 'return eval("'+$(this).data('ondrop')+'");') || null,
  32. onDragStart: $(this).data('ondragstart') && new Function('table', 'row' ,$(this).data('ondragstart')), // 'return eval("'+$(this).data('ondragstart')+'");') || null,
  33. onDragStop: $(this).data('ondragstop') && new Function('table', 'row' ,$(this).data('ondragstop')),
  34. scrollAmount: $(this).data('scrollamount') || 5,
  35. sensitivity: $(this).data('sensitivity') || 10,
  36. hierarchyLevel: $(this).data('hierarchylevel') || 0,
  37. indentArtifact: $(this).data('indentartifact') || '<div class="indent">&nbsp;</div>',
  38. autoWidthAdjust: $(this).data('autowidthadjust') || true,
  39. autoCleanRelations: $(this).data('autocleanrelations') || true,
  40. jsonPretifySeparator: $(this).data('jsonpretifyseparator') || '\t',
  41. serializeRegexp: $(this).data('serializeregexp') && new RegExp($(this).data('serializeregexp')) || /[^\-]*$/,
  42. serializeParamName: $(this).data('serializeparamname') || false,
  43. dragHandle: $(this).data('draghandle') || null
  44. });
  45. }
  46. });
  47. });
  48. jQuery.tableDnD = {
  49. /** Keep hold of the current table being dragged */
  50. currentTable: null,
  51. /** Keep hold of the current drag object if any */
  52. dragObject: null,
  53. /** The current mouse offset */
  54. mouseOffset: null,
  55. /** Remember the old value of X and Y so that we don't do too much processing */
  56. oldX: 0,
  57. oldY: 0,
  58. /** Actually build the structure */
  59. build: function(options) {
  60. // Set up the defaults if any
  61. this.each(function() {
  62. // This is bound to each matching table, set up the defaults and override with user options
  63. this.tableDnDConfig = $.extend({
  64. onDragStyle: null,
  65. onDropStyle: null,
  66. // Add in the default class for whileDragging
  67. onDragClass: "tDnD_whileDrag",
  68. onDrop: null,
  69. onDragStart: null,
  70. onDragStop: null,
  71. scrollAmount: 5,
  72. /** Sensitivity setting will throttle the trigger rate for movement detection */
  73. sensitivity: 10,
  74. /** Hierarchy level to support parent child. 0 switches this functionality off */
  75. hierarchyLevel: 0,
  76. /** The html artifact to prepend the first cell with as indentation */
  77. indentArtifact: '<div class="indent">&nbsp;</div>',
  78. /** Automatically adjust width of first cell */
  79. autoWidthAdjust: true,
  80. /** Automatic clean-up to ensure relationship integrity */
  81. autoCleanRelations: true,
  82. /** Specify a number (4) as number of spaces or any indent string for JSON.stringify */
  83. jsonPretifySeparator: '\t',
  84. /** The regular expression to use to trim row IDs */
  85. serializeRegexp: /[^\-]*$/,
  86. /** If you want to specify another parameter name instead of the table ID */
  87. serializeParamName: false,
  88. /** If you give the name of a class here, then only Cells with this class will be draggable */
  89. dragHandle: null
  90. }, options || {});
  91. // Now make the rows draggable
  92. $.tableDnD.makeDraggable(this);
  93. // Prepare hierarchy support
  94. this.tableDnDConfig.hierarchyLevel
  95. && $.tableDnD.makeIndented(this);
  96. });
  97. // Don't break the chain
  98. return this;
  99. },
  100. makeIndented: function (table) {
  101. var config = table.tableDnDConfig,
  102. rows = table.rows,
  103. firstCell = $(rows).first().find('td:first')[0],
  104. indentLevel = 0,
  105. cellWidth = 0,
  106. longestCell,
  107. tableStyle;
  108. if ($(table).hasClass('indtd'))
  109. return null;
  110. tableStyle = $(table).addClass('indtd').attr('style');
  111. $(table).css({whiteSpace: "nowrap"});
  112. for (var w = 0; w < rows.length; w++) {
  113. if (cellWidth < $(rows[w]).find('td:first').text().length) {
  114. cellWidth = $(rows[w]).find('td:first').text().length;
  115. longestCell = w;
  116. }
  117. }
  118. $(firstCell).css({width: 'auto'});
  119. for (w = 0; w < config.hierarchyLevel; w++)
  120. $(rows[longestCell]).find('td:first').prepend(config.indentArtifact);
  121. firstCell && $(firstCell).css({width: firstCell.offsetWidth});
  122. tableStyle && $(table).css(tableStyle);
  123. for (w = 0; w < config.hierarchyLevel; w++)
  124. $(rows[longestCell]).find('td:first').children(':first').remove();
  125. config.hierarchyLevel
  126. && $(rows).each(function () {
  127. indentLevel = $(this).data('level') || 0;
  128. indentLevel <= config.hierarchyLevel
  129. && $(this).data('level', indentLevel)
  130. || $(this).data('level', 0);
  131. for (var i = 0; i < $(this).data('level'); i++)
  132. $(this).find('td:first').prepend(config.indentArtifact);
  133. });
  134. return this;
  135. },
  136. /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
  137. makeDraggable: function(table) {
  138. var config = table.tableDnDConfig;
  139. config.dragHandle
  140. // We only need to add the event to the specified cells
  141. && $(config.dragHandle, table).each(function() {
  142. // The cell is bound to "this"
  143. $(this).bind(startEvent, function(e) {
  144. $.tableDnD.initialiseDrag($(this).parents('tr')[0], table, this, e, config);
  145. return false;
  146. });
  147. })
  148. // For backwards compatibility, we add the event to the whole row
  149. // get all the rows as a wrapped set
  150. || $(table.rows).each(function() {
  151. // Iterate through each row, the row is bound to "this"
  152. if (! $(this).hasClass("nodrag")) {
  153. $(this).bind(startEvent, function(e) {
  154. if (e.target.tagName === "TD") {
  155. $.tableDnD.initialiseDrag(this, table, this, e, config);
  156. return false;
  157. }
  158. }).css("cursor", "move"); // Store the tableDnD object
  159. } else {
  160. $(this).css("cursor", ""); // Remove the cursor if we don't have the nodrag class
  161. }
  162. });
  163. },
  164. currentOrder: function() {
  165. var rows = this.currentTable.rows;
  166. return $.map(rows, function (val) {
  167. return ($(val).data('level') + val.id).replace(/\s/g, '');
  168. }).join('');
  169. },
  170. initialiseDrag: function(dragObject, table, target, e, config) {
  171. this.dragObject = dragObject;
  172. this.currentTable = table;
  173. this.mouseOffset = this.getMouseOffset(target, e);
  174. this.originalOrder = this.currentOrder();
  175. // Now we need to capture the mouse up and mouse move event
  176. // We can use bind so that we don't interfere with other event handlers
  177. $(document)
  178. .bind(moveEvent, this.mousemove)
  179. .bind(endEvent, this.mouseup);
  180. // Call the onDragStart method if there is one
  181. config.onDragStart
  182. && config.onDragStart(table, target);
  183. },
  184. updateTables: function() {
  185. this.each(function() {
  186. // this is now bound to each matching table
  187. if (this.tableDnDConfig)
  188. $.tableDnD.makeDraggable(this);
  189. });
  190. },
  191. /** Get the mouse coordinates from the event (allowing for browser differences) */
  192. mouseCoords: function(e) {
  193. if (e.originalEvent.changedTouches)
  194. return {
  195. x: e.originalEvent.changedTouches[0].clientX,
  196. y: e.originalEvent.changedTouches[0].clientY
  197. };
  198. if(e.pageX || e.pageY)
  199. return {
  200. x: e.pageX,
  201. y: e.pageY
  202. };
  203. return {
  204. x: e.clientX + document.body.scrollLeft - document.body.clientLeft,
  205. y: e.clientY + document.body.scrollTop - document.body.clientTop
  206. };
  207. },
  208. /** Given a target element and a mouse eent, get the mouse offset from that element.
  209. To do this we need the element's position and the mouse position */
  210. getMouseOffset: function(target, e) {
  211. var mousePos,
  212. docPos;
  213. e = e || window.event;
  214. docPos = this.getPosition(target);
  215. mousePos = this.mouseCoords(e);
  216. return {
  217. x: mousePos.x - docPos.x,
  218. y: mousePos.y - docPos.y
  219. };
  220. },
  221. /** Get the position of an element by going up the DOM tree and adding up all the offsets */
  222. getPosition: function(element) {
  223. var left = 0,
  224. top = 0;
  225. // Safari fix -- thanks to Luis Chato for this!
  226. // Safari 2 doesn't correctly grab the offsetTop of a table row
  227. // this is detailed here:
  228. // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
  229. // the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
  230. // note that firefox will return a text node as a first child, so designing a more thorough
  231. // solution may need to take that into account, for now this seems to work in firefox, safari, ie
  232. if (element.offsetHeight === 0)
  233. element = element.firstChild; // a table cell
  234. while (element.offsetParent) {
  235. left += element.offsetLeft;
  236. top += element.offsetTop;
  237. element = element.offsetParent;
  238. }
  239. left += element.offsetLeft;
  240. top += element.offsetTop;
  241. return {
  242. x: left,
  243. y: top
  244. };
  245. },
  246. autoScroll: function (mousePos) {
  247. var config = this.currentTable.tableDnDConfig,
  248. yOffset = window.pageYOffset,
  249. windowHeight = window.innerHeight
  250. ? window.innerHeight
  251. : document.documentElement.clientHeight
  252. ? document.documentElement.clientHeight
  253. : document.body.clientHeight;
  254. // Windows version
  255. // yOffset=document.body.scrollTop;
  256. if (document.all)
  257. if (typeof document.compatMode !== 'undefined'
  258. && document.compatMode !== 'BackCompat')
  259. yOffset = document.documentElement.scrollTop;
  260. else if (typeof document.body !== 'undefined')
  261. yOffset = document.body.scrollTop;
  262. mousePos.y - yOffset < config.scrollAmount
  263. && window.scrollBy(0, - config.scrollAmount)
  264. || windowHeight - (mousePos.y - yOffset) < config.scrollAmount
  265. && window.scrollBy(0, config.scrollAmount);
  266. },
  267. moveVerticle: function (moving, currentRow) {
  268. if (0 !== moving.vertical
  269. // If we're over a row then move the dragged row to there so that the user sees the
  270. // effect dynamically
  271. && currentRow
  272. && this.dragObject !== currentRow
  273. && this.dragObject.parentNode === currentRow.parentNode)
  274. 0 > moving.vertical
  275. && this.dragObject.parentNode.insertBefore(this.dragObject, currentRow.nextSibling)
  276. || 0 < moving.vertical
  277. && this.dragObject.parentNode.insertBefore(this.dragObject, currentRow);
  278. },
  279. moveHorizontal: function (moving, currentRow) {
  280. var config = this.currentTable.tableDnDConfig,
  281. currentLevel;
  282. if (!config.hierarchyLevel
  283. || 0 === moving.horizontal
  284. // We only care if moving left or right on the current row
  285. || !currentRow
  286. || this.dragObject !== currentRow)
  287. return null;
  288. currentLevel = $(currentRow).data('level');
  289. 0 < moving.horizontal
  290. && currentLevel > 0
  291. && $(currentRow).find('td:first').children(':first').remove()
  292. && $(currentRow).data('level', --currentLevel);
  293. 0 > moving.horizontal
  294. && currentLevel < config.hierarchyLevel
  295. && $(currentRow).prev().data('level') >= currentLevel
  296. && $(currentRow).children(':first').prepend(config.indentArtifact)
  297. && $(currentRow).data('level', ++currentLevel);
  298. },
  299. mousemove: function(e) {
  300. var dragObj = $($.tableDnD.dragObject),
  301. config = $.tableDnD.currentTable.tableDnDConfig,
  302. currentRow,
  303. mousePos,
  304. moving,
  305. x,
  306. y;
  307. e && e.preventDefault();
  308. if (!$.tableDnD.dragObject)
  309. return false;
  310. // prevent touch device screen scrolling
  311. e.type === 'touchmove'
  312. && event.preventDefault(); // TODO verify this is event and not really e
  313. // update the style to show we're dragging
  314. config.onDragClass
  315. && dragObj.addClass(config.onDragClass)
  316. || dragObj.css(config.onDragStyle);
  317. mousePos = $.tableDnD.mouseCoords(e);
  318. x = mousePos.x - $.tableDnD.mouseOffset.x;
  319. y = mousePos.y - $.tableDnD.mouseOffset.y;
  320. // auto scroll the window
  321. $.tableDnD.autoScroll(mousePos);
  322. currentRow = $.tableDnD.findDropTargetRow(dragObj, y);
  323. moving = $.tableDnD.findDragDirection(x, y);
  324. $.tableDnD.moveVerticle(moving, currentRow);
  325. $.tableDnD.moveHorizontal(moving, currentRow);
  326. return false;
  327. },
  328. findDragDirection: function (x,y) {
  329. var sensitivity = this.currentTable.tableDnDConfig.sensitivity,
  330. oldX = this.oldX,
  331. oldY = this.oldY,
  332. xMin = oldX - sensitivity,
  333. xMax = oldX + sensitivity,
  334. yMin = oldY - sensitivity,
  335. yMax = oldY + sensitivity,
  336. moving = {
  337. horizontal: x >= xMin && x <= xMax ? 0 : x > oldX ? -1 : 1,
  338. vertical : y >= yMin && y <= yMax ? 0 : y > oldY ? -1 : 1
  339. };
  340. // update the old value
  341. if (moving.horizontal !== 0)
  342. this.oldX = x;
  343. if (moving.vertical !== 0)
  344. this.oldY = y;
  345. return moving;
  346. },
  347. /** We're only worried about the y position really, because we can only move rows up and down */
  348. findDropTargetRow: function(draggedRow, y) {
  349. var rowHeight = 0,
  350. rows = this.currentTable.rows,
  351. config = this.currentTable.tableDnDConfig,
  352. rowY = 0,
  353. row = null;
  354. for (var i = 0; i < rows.length; i++) {
  355. row = rows[i];
  356. rowY = this.getPosition(row).y;
  357. rowHeight = parseInt(row.offsetHeight) / 2;
  358. if (row.offsetHeight === 0) {
  359. rowY = this.getPosition(row.firstChild).y;
  360. rowHeight = parseInt(row.firstChild.offsetHeight) / 2;
  361. }
  362. // Because we always have to insert before, we need to offset the height a bit
  363. if (y > (rowY - rowHeight) && y < (rowY + rowHeight))
  364. // that's the row we're over
  365. // If it's the same as the current row, ignore it
  366. if (draggedRow.is(row)
  367. || (config.onAllowDrop
  368. && !config.onAllowDrop(draggedRow, row))
  369. // If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
  370. || $(row).hasClass("nodrop"))
  371. return null;
  372. else
  373. return row;
  374. }
  375. return null;
  376. },
  377. processMouseup: function() {
  378. if (!this.currentTable || !this.dragObject)
  379. return null;
  380. var config = this.currentTable.tableDnDConfig,
  381. droppedRow = this.dragObject,
  382. parentLevel = 0,
  383. myLevel = 0;
  384. // Unbind the event handlers
  385. $(document)
  386. .unbind(moveEvent, this.mousemove)
  387. .unbind(endEvent, this.mouseup);
  388. config.hierarchyLevel
  389. && config.autoCleanRelations
  390. && $(this.currentTable.rows).first().find('td:first').children().each(function () {
  391. myLevel = $(this).parents('tr:first').data('level');
  392. myLevel
  393. && $(this).parents('tr:first').data('level', --myLevel)
  394. && $(this).remove();
  395. })
  396. && config.hierarchyLevel > 1
  397. && $(this.currentTable.rows).each(function () {
  398. myLevel = $(this).data('level');
  399. if (myLevel > 1) {
  400. parentLevel = $(this).prev().data('level');
  401. while (myLevel > parentLevel + 1) {
  402. $(this).find('td:first').children(':first').remove();
  403. $(this).data('level', --myLevel);
  404. }
  405. }
  406. });
  407. // If we have a dragObject, then we need to release it,
  408. // The row will already have been moved to the right place so we just reset stuff
  409. config.onDragClass
  410. && $(droppedRow).removeClass(config.onDragClass)
  411. || $(droppedRow).css(config.onDropStyle);
  412. this.dragObject = null;
  413. // Call the onDrop method if there is one
  414. config.onDrop
  415. && this.originalOrder !== this.currentOrder()
  416. && $(droppedRow).hide().fadeIn('fast')
  417. && config.onDrop(this.currentTable, droppedRow);
  418. // Call the onDragStop method if there is one
  419. config.onDragStop
  420. && config.onDragStop(this.currentTable, droppedRow);
  421. this.currentTable = null; // let go of the table too
  422. },
  423. mouseup: function(e) {
  424. e && e.preventDefault();
  425. $.tableDnD.processMouseup();
  426. return false;
  427. },
  428. jsonize: function(pretify) {
  429. var table = this.currentTable;
  430. if (pretify)
  431. return JSON.stringify(
  432. this.tableData(table),
  433. null,
  434. table.tableDnDConfig.jsonPretifySeparator
  435. );
  436. return JSON.stringify(this.tableData(table));
  437. },
  438. serialize: function() {
  439. return $.param(this.tableData(this.currentTable));
  440. },
  441. serializeTable: function(table) {
  442. var result = "";
  443. var paramName = table.tableDnDConfig.serializeParamName || table.id;
  444. var rows = table.rows;
  445. for (var i=0; i<rows.length; i++) {
  446. if (result.length > 0) result += "&";
  447. var rowId = rows[i].id;
  448. if (rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
  449. rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
  450. result += paramName + '[]=' + rowId;
  451. }
  452. }
  453. return result;
  454. },
  455. serializeTables: function() {
  456. var result = [];
  457. $('table').each(function() {
  458. this.id && result.push($.param($.tableDnD.tableData(this)));
  459. });
  460. return result.join('&');
  461. },
  462. tableData: function (table) {
  463. var config = table.tableDnDConfig,
  464. previousIDs = [],
  465. currentLevel = 0,
  466. indentLevel = 0,
  467. rowID = null,
  468. data = {},
  469. getSerializeRegexp,
  470. paramName,
  471. currentID,
  472. rows;
  473. if (!table)
  474. table = this.currentTable;
  475. if (!table || !table.rows || !table.rows.length)
  476. return {error: { code: 500, message: "Not a valid table."}};
  477. if (!table.id && !config.serializeParamName)
  478. return {error: { code: 500, message: "No serializable unique id provided."}};
  479. rows = config.autoCleanRelations
  480. && table.rows
  481. || $.makeArray(table.rows);
  482. paramName = config.serializeParamName || table.id;
  483. currentID = paramName;
  484. getSerializeRegexp = function (rowId) {
  485. if (rowId && config && config.serializeRegexp)
  486. return rowId.match(config.serializeRegexp)[0];
  487. return rowId;
  488. };
  489. data[currentID] = [];
  490. !config.autoCleanRelations
  491. && $(rows[0]).data('level')
  492. && rows.unshift({id: 'undefined'});
  493. for (var i=0; i < rows.length; i++) {
  494. if (config.hierarchyLevel) {
  495. indentLevel = $(rows[i]).data('level') || 0;
  496. if (indentLevel === 0) {
  497. currentID = paramName;
  498. previousIDs = [];
  499. }
  500. else if (indentLevel > currentLevel) {
  501. previousIDs.push([currentID, currentLevel]);
  502. currentID = getSerializeRegexp(rows[i-1].id);
  503. }
  504. else if (indentLevel < currentLevel) {
  505. for (var h = 0; h < previousIDs.length; h++) {
  506. if (previousIDs[h][1] === indentLevel)
  507. currentID = previousIDs[h][0];
  508. if (previousIDs[h][1] >= currentLevel)
  509. previousIDs[h][1] = 0;
  510. }
  511. }
  512. currentLevel = indentLevel;
  513. if (!$.isArray(data[currentID]))
  514. data[currentID] = [];
  515. rowID = getSerializeRegexp(rows[i].id);
  516. rowID && data[currentID].push(rowID);
  517. }
  518. else {
  519. rowID = getSerializeRegexp(rows[i].id);
  520. rowID && data[currentID].push(rowID);
  521. }
  522. }
  523. return data;
  524. }
  525. };
  526. jQuery.fn.extend(
  527. {
  528. tableDnD : $.tableDnD.build,
  529. tableDnDUpdate : $.tableDnD.updateTables,
  530. tableDnDSerialize : $.proxy($.tableDnD.serialize, $.tableDnD),
  531. tableDnDSerializeAll : $.tableDnD.serializeTables,
  532. tableDnDData : $.proxy($.tableDnD.tableData, $.tableDnD)
  533. }
  534. );
  535. }(jQuery, window, window.document);