You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

518 lines
19 KiB

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