jquery.nestable.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. /*!
  2. * Nestable jQuery Plugin - Copyright (c) 2012 David Bushell - http://dbushell.com/
  3. * Dual-licensed under the BSD or MIT licenses
  4. */
  5. ;(function($, window, document, undefined)
  6. {
  7. var hasTouch = 'ontouchstart' in window;
  8. /**
  9. * Detect CSS pointer-events property
  10. * events are normally disabled on the dragging element to avoid conflicts
  11. * https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js
  12. */
  13. var hasPointerEvents = (function()
  14. {
  15. var el = document.createElement('div'),
  16. docEl = document.documentElement;
  17. if (!('pointerEvents' in el.style)) {
  18. return false;
  19. }
  20. el.style.pointerEvents = 'auto';
  21. el.style.pointerEvents = 'x';
  22. docEl.appendChild(el);
  23. var supports = window.getComputedStyle && window.getComputedStyle(el, '').pointerEvents === 'auto';
  24. docEl.removeChild(el);
  25. return !!supports;
  26. })();
  27. var eStart = hasTouch ? 'touchstart' : 'mousedown',
  28. eMove = hasTouch ? 'touchmove' : 'mousemove',
  29. eEnd = hasTouch ? 'touchend' : 'mouseup';
  30. eCancel = hasTouch ? 'touchcancel' : 'mouseup';
  31. var defaults = {
  32. listNodeName : 'ol',
  33. itemNodeName : 'li',
  34. rootClass : 'dd',
  35. listClass : 'dd-list',
  36. itemClass : 'dd-item',
  37. dragClass : 'dd-dragel',
  38. handleClass : 'dd-handle',
  39. collapsedClass : 'dd-collapsed',
  40. placeClass : 'dd-placeholder',
  41. noDragClass : 'dd-nodrag',
  42. emptyClass : 'dd-empty',
  43. expandBtnHTML : '<button data-action="expand" type="button">Expand</button>',
  44. collapseBtnHTML : '<button data-action="collapse" type="button">Collapse</button>',
  45. group : 0,
  46. maxDepth : 5,
  47. threshold : 20,
  48. //method for call when an item has been successfully dropped
  49. //method has 1 argument in which sends an object containing all
  50. //necessary details
  51. dropCallback : null
  52. };
  53. function Plugin(element, options)
  54. {
  55. this.w = $(window);
  56. this.el = $(element);
  57. this.options = $.extend({}, defaults, options);
  58. this.init();
  59. }
  60. Plugin.prototype = {
  61. init: function()
  62. {
  63. var list = this;
  64. list.reset();
  65. list.el.data('nestable-group', this.options.group);
  66. list.placeEl = $('<div class="' + list.options.placeClass + '"/>');
  67. $.each(this.el.find(list.options.itemNodeName), function(k, el) {
  68. list.setParent($(el));
  69. });
  70. list.el.on('click', 'button', function(e) {
  71. if (list.dragEl || (!hasTouch && e.button !== 0)) {
  72. return;
  73. }
  74. var target = $(e.currentTarget),
  75. action = target.data('action'),
  76. item = target.parent(list.options.itemNodeName);
  77. if (action === 'collapse') {
  78. list.collapseItem(item);
  79. }
  80. if (action === 'expand') {
  81. list.expandItem(item);
  82. }
  83. });
  84. var onStartEvent = function(e)
  85. {
  86. var handle = $(e.target);
  87. if (!handle.hasClass(list.options.handleClass)) {
  88. if (handle.closest('.' + list.options.noDragClass).length) {
  89. return;
  90. }
  91. handle = handle.closest('.' + list.options.handleClass);
  92. }
  93. if (!handle.length || list.dragEl || (!hasTouch && e.button !== 0) || (hasTouch && e.touches.length !== 1)) {
  94. return;
  95. }
  96. e.preventDefault();
  97. list.dragStart(hasTouch ? e.touches[0] : e);
  98. };
  99. var onMoveEvent = function(e)
  100. {
  101. if (list.dragEl) {
  102. e.preventDefault();
  103. list.dragMove(hasTouch ? e.touches[0] : e);
  104. }
  105. };
  106. var onEndEvent = function(e)
  107. {
  108. if (list.dragEl) {
  109. e.preventDefault();
  110. list.dragStop(hasTouch ? e.touches[0] : e);
  111. }
  112. };
  113. if (hasTouch) {
  114. list.el[0].addEventListener(eStart, onStartEvent, false);
  115. window.addEventListener(eMove, onMoveEvent, false);
  116. window.addEventListener(eEnd, onEndEvent, false);
  117. window.addEventListener(eCancel, onEndEvent, false);
  118. } else {
  119. list.el.on(eStart, onStartEvent);
  120. list.w.on(eMove, onMoveEvent);
  121. list.w.on(eEnd, onEndEvent);
  122. }
  123. },
  124. serialize: function()
  125. {
  126. var data,
  127. depth = 0,
  128. list = this;
  129. step = function(level, depth)
  130. {
  131. var array = [ ],
  132. items = level.children(list.options.itemNodeName);
  133. items.each(function()
  134. {
  135. var li = $(this),
  136. item = $.extend({}, li.data()),
  137. sub = li.children(list.options.listNodeName);
  138. if (sub.length) {
  139. item.children = step(sub, depth + 1);
  140. }
  141. array.push(item);
  142. });
  143. return array;
  144. };
  145. data = step(list.el.find(list.options.listNodeName).first(), depth);
  146. return data;
  147. },
  148. serialise: function()
  149. {
  150. return this.serialize();
  151. },
  152. reset: function()
  153. {
  154. this.mouse = {
  155. offsetX : 0,
  156. offsetY : 0,
  157. startX : 0,
  158. startY : 0,
  159. lastX : 0,
  160. lastY : 0,
  161. nowX : 0,
  162. nowY : 0,
  163. distX : 0,
  164. distY : 0,
  165. dirAx : 0,
  166. dirX : 0,
  167. dirY : 0,
  168. lastDirX : 0,
  169. lastDirY : 0,
  170. distAxX : 0,
  171. distAxY : 0
  172. };
  173. this.moving = false;
  174. this.dragEl = null;
  175. this.dragRootEl = null;
  176. this.dragDepth = 0;
  177. this.hasNewRoot = false;
  178. this.pointEl = null;
  179. this.sourceRoot = null;
  180. },
  181. expandItem: function(li)
  182. {
  183. li.removeClass(this.options.collapsedClass);
  184. li.children('[data-action="expand"]').hide();
  185. li.children('[data-action="collapse"]').show();
  186. li.children(this.options.listNodeName).show();
  187. },
  188. collapseItem: function(li)
  189. {
  190. var lists = li.children(this.options.listNodeName);
  191. if (lists.length) {
  192. li.addClass(this.options.collapsedClass);
  193. li.children('[data-action="collapse"]').hide();
  194. li.children('[data-action="expand"]').show();
  195. li.children(this.options.listNodeName).hide();
  196. }
  197. },
  198. expandAll: function()
  199. {
  200. var list = this;
  201. list.el.find(list.options.itemNodeName).each(function() {
  202. list.expandItem($(this));
  203. });
  204. },
  205. collapseAll: function()
  206. {
  207. var list = this;
  208. list.el.find(list.options.itemNodeName).each(function() {
  209. list.collapseItem($(this));
  210. });
  211. },
  212. setParent: function(li)
  213. {
  214. if (li.children(this.options.listNodeName).length) {
  215. li.prepend($(this.options.expandBtnHTML));
  216. li.prepend($(this.options.collapseBtnHTML));
  217. }
  218. li.children('[data-action="expand"]').hide();
  219. },
  220. unsetParent: function(li)
  221. {
  222. li.removeClass(this.options.collapsedClass);
  223. li.children('[data-action]').remove();
  224. li.children(this.options.listNodeName).remove();
  225. },
  226. dragStart: function(e)
  227. {
  228. var mouse = this.mouse,
  229. target = $(e.target),
  230. dragItem = target.closest(this.options.itemNodeName);
  231. this.sourceRoot = target.closest('.' + this.options.rootClass);
  232. this.placeEl.css('height', dragItem.height());
  233. mouse.offsetX = e.offsetX !== undefined ? e.offsetX : e.pageX - target.offset().left;
  234. mouse.offsetY = e.offsetY !== undefined ? e.offsetY : e.pageY - target.offset().top;
  235. mouse.startX = mouse.lastX = e.pageX;
  236. mouse.startY = mouse.lastY = e.pageY;
  237. this.dragRootEl = this.el;
  238. this.dragEl = $(document.createElement(this.options.listNodeName)).addClass(this.options.listClass + ' ' + this.options.dragClass);
  239. this.dragEl.css('width', dragItem.width());
  240. // fix for zepto.js
  241. //dragItem.after(this.placeEl).detach().appendTo(this.dragEl);
  242. dragItem.after(this.placeEl);
  243. dragItem[0].parentNode.removeChild(dragItem[0]);
  244. dragItem.appendTo(this.dragEl);
  245. $(document.body).append(this.dragEl);
  246. this.dragEl.css({
  247. 'left' : e.pageX - mouse.offsetX,
  248. 'top' : e.pageY - mouse.offsetY
  249. });
  250. // total depth of dragging item
  251. var i, depth,
  252. items = this.dragEl.find(this.options.itemNodeName);
  253. for (i = 0; i < items.length; i++) {
  254. depth = $(items[i]).parents(this.options.listNodeName).length;
  255. if (depth > this.dragDepth) {
  256. this.dragDepth = depth;
  257. }
  258. }
  259. },
  260. dragStop: function(e)
  261. {
  262. // fix for zepto.js
  263. //this.placeEl.replaceWith(this.dragEl.children(this.options.itemNodeName + ':first').detach());
  264. var el = this.dragEl.children(this.options.itemNodeName).first();
  265. el[0].parentNode.removeChild(el[0]);
  266. this.placeEl.replaceWith(el);
  267. this.dragEl.remove();
  268. this.el.trigger('change');
  269. //Let's find out new parent id
  270. var parentItem = el.parent().parent();
  271. var parentId = null;
  272. if(parentItem !== null && !parentItem.is('.' + this.options.rootClass))
  273. parentId = parentItem.data('id');
  274. if($.isFunction(this.options.dropCallback)) {
  275. var details = {
  276. sourceId : el.data('id'),
  277. destId : parentId,
  278. sourceEl : el,
  279. destParent : parentItem,
  280. destRoot : el.closest('.' + this.options.rootClass),
  281. sourceRoot : this.sourceRoot
  282. };
  283. this.options.dropCallback.call(this, details);
  284. }
  285. if (this.hasNewRoot) {
  286. this.dragRootEl.trigger('change');
  287. }
  288. this.reset();
  289. },
  290. dragMove: function(e)
  291. {
  292. var list, parent, prev, next, depth,
  293. opt = this.options,
  294. mouse = this.mouse;
  295. this.dragEl.css({
  296. 'left' : e.pageX - mouse.offsetX,
  297. 'top' : e.pageY - mouse.offsetY
  298. });
  299. // mouse position last events
  300. mouse.lastX = mouse.nowX;
  301. mouse.lastY = mouse.nowY;
  302. // mouse position this events
  303. mouse.nowX = e.pageX;
  304. mouse.nowY = e.pageY;
  305. // distance mouse moved between events
  306. mouse.distX = mouse.nowX - mouse.lastX;
  307. mouse.distY = mouse.nowY - mouse.lastY;
  308. // direction mouse was moving
  309. mouse.lastDirX = mouse.dirX;
  310. mouse.lastDirY = mouse.dirY;
  311. // direction mouse is now moving (on both axis)
  312. mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1;
  313. mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1;
  314. // axis mouse is now moving on
  315. var newAx = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0;
  316. // do nothing on first move
  317. if (!mouse.moving) {
  318. mouse.dirAx = newAx;
  319. mouse.moving = true;
  320. return;
  321. }
  322. // calc distance moved on this axis (and direction)
  323. if (mouse.dirAx !== newAx) {
  324. mouse.distAxX = 0;
  325. mouse.distAxY = 0;
  326. } else {
  327. mouse.distAxX += Math.abs(mouse.distX);
  328. if (mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) {
  329. mouse.distAxX = 0;
  330. }
  331. mouse.distAxY += Math.abs(mouse.distY);
  332. if (mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) {
  333. mouse.distAxY = 0;
  334. }
  335. }
  336. mouse.dirAx = newAx;
  337. /**
  338. * move horizontal
  339. */
  340. if (mouse.dirAx && mouse.distAxX >= opt.threshold) {
  341. // reset move distance on x-axis for new phase
  342. mouse.distAxX = 0;
  343. prev = this.placeEl.prev(opt.itemNodeName);
  344. // increase horizontal level if previous sibling exists and is not collapsed
  345. if (mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass)) {
  346. // cannot increase level when item above is collapsed
  347. list = prev.find(opt.listNodeName).last();
  348. // check if depth limit has reached
  349. depth = this.placeEl.parents(opt.listNodeName).length;
  350. if (depth + this.dragDepth <= opt.maxDepth) {
  351. // create new sub-level if one doesn't exist
  352. if (!list.length) {
  353. list = $('<' + opt.listNodeName + '/>').addClass(opt.listClass);
  354. list.append(this.placeEl);
  355. prev.append(list);
  356. this.setParent(prev);
  357. } else {
  358. // else append to next level up
  359. list = prev.children(opt.listNodeName).last();
  360. list.append(this.placeEl);
  361. }
  362. }
  363. }
  364. // decrease horizontal level
  365. if (mouse.distX < 0) {
  366. // we can't decrease a level if an item preceeds the current one
  367. next = this.placeEl.next(opt.itemNodeName);
  368. if (!next.length) {
  369. parent = this.placeEl.parent();
  370. this.placeEl.closest(opt.itemNodeName).after(this.placeEl);
  371. if (!parent.children().length) {
  372. this.unsetParent(parent.parent());
  373. }
  374. }
  375. }
  376. }
  377. var isEmpty = false;
  378. // find list item under cursor
  379. if (!hasPointerEvents) {
  380. this.dragEl[0].style.visibility = 'hidden';
  381. }
  382. this.pointEl = $(document.elementFromPoint(e.pageX - document.body.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop)));
  383. if (!hasPointerEvents) {
  384. this.dragEl[0].style.visibility = 'visible';
  385. }
  386. if (this.pointEl.hasClass(opt.handleClass)) {
  387. this.pointEl = this.pointEl.parent(opt.itemNodeName);
  388. }
  389. if (this.pointEl.hasClass(opt.emptyClass)) {
  390. isEmpty = true;
  391. }
  392. else if (!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) {
  393. return;
  394. }
  395. // find parent list of item under cursor
  396. var pointElRoot = this.pointEl.closest('.' + opt.rootClass),
  397. isNewRoot = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id');
  398. /**
  399. * move vertical
  400. */
  401. if (!mouse.dirAx || isNewRoot || isEmpty) {
  402. // check if groups match if dragging over new root
  403. if (isNewRoot && opt.group !== pointElRoot.data('nestable-group')) {
  404. return;
  405. }
  406. // check depth limit
  407. depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length;
  408. if (depth > opt.maxDepth) {
  409. return;
  410. }
  411. var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2);
  412. parent = this.placeEl.parent();
  413. // if empty create new list to replace empty placeholder
  414. if (isEmpty) {
  415. list = $(document.createElement(opt.listNodeName)).addClass(opt.listClass);
  416. list.append(this.placeEl);
  417. this.pointEl.replaceWith(list);
  418. }
  419. else if (before) {
  420. this.pointEl.before(this.placeEl);
  421. }
  422. else {
  423. this.pointEl.after(this.placeEl);
  424. }
  425. if (!parent.children().length) {
  426. this.unsetParent(parent.parent());
  427. }
  428. if (!this.dragRootEl.find(opt.itemNodeName).length) {
  429. this.dragRootEl.append('<div class="' + opt.emptyClass + '"/>');
  430. }
  431. // parent root list has changed
  432. if (isNewRoot) {
  433. this.dragRootEl = pointElRoot;
  434. this.hasNewRoot = this.el[0] !== this.dragRootEl[0];
  435. }
  436. }
  437. }
  438. };
  439. $.fn.nestable = function(params)
  440. {
  441. var lists = this,
  442. retval = this;
  443. lists.each(function()
  444. {
  445. var plugin = $(this).data("nestable");
  446. if (!plugin) {
  447. $(this).data("nestable", new Plugin(this, params));
  448. $(this).data("nestable-id", new Date().getTime());
  449. } else {
  450. if (typeof params === 'string' && typeof plugin[params] === 'function') {
  451. retval = plugin[params]();
  452. }
  453. }
  454. });
  455. return retval || lists;
  456. };
  457. })(window.jQuery || window.Zepto, window, document);