nestable.js 17 KB

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