jquery.nestable.js 18 KB

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