jquery.nestable.js 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087
  1. /*!
  2. * Nestable jQuery Plugin - Copyright (c) 2014 Ramon Smit - https://github.com/RamonSmit/Nestable
  3. */
  4. (function($, window, document, undefined) {
  5. var hasTouch = 'ontouchstart' in document;
  6. /**
  7. * Detect CSS pointer-events property
  8. * events are normally disabled on the dragging element to avoid conflicts
  9. * https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js
  10. */
  11. var hasPointerEvents = (function() {
  12. var el = document.createElement('div'),
  13. docEl = document.documentElement;
  14. if (!('pointerEvents' in el.style)) {
  15. return false;
  16. }
  17. el.style.pointerEvents = 'auto';
  18. el.style.pointerEvents = 'x';
  19. docEl.appendChild(el);
  20. var supports = window.getComputedStyle && window.getComputedStyle(el, '').pointerEvents === 'auto';
  21. docEl.removeChild(el);
  22. return !!supports;
  23. })();
  24. var defaults = {
  25. contentCallback: function(item) {return item.content || '' ? item.content : item.id;},
  26. listNodeName: 'ol',
  27. itemNodeName: 'li',
  28. handleNodeName: 'div',
  29. contentNodeName: 'span',
  30. rootClass: 'dd',
  31. listClass: 'dd-list',
  32. itemClass: 'dd-item',
  33. dragClass: 'dd-dragel',
  34. handleClass: 'dd-handle',
  35. contentClass: 'dd-content',
  36. collapsedClass: 'dd-collapsed',
  37. placeClass: 'dd-placeholder',
  38. noDragClass: 'dd-nodrag',
  39. noChildrenClass: 'dd-nochildren',
  40. emptyClass: 'dd-empty',
  41. expandBtnHTML: '<button class="dd-expand" data-action="expand" type="button">Expand</button>',
  42. collapseBtnHTML: '<button class="dd-collapse" data-action="collapse" type="button">Collapse</button>',
  43. group: 0,
  44. maxDepth: 5,
  45. threshold: 20,
  46. fixedDepth: false, //fixed item's depth
  47. fixed: false,
  48. includeContent: false,
  49. scroll: false,
  50. scrollSensitivity: 1,
  51. scrollSpeed: 5,
  52. scrollTriggers: {
  53. top: 40,
  54. left: 40,
  55. right: -40,
  56. bottom: -40
  57. },
  58. effect: {
  59. animation: 'none',
  60. time: 'slow'
  61. },
  62. callback: function(l, e, p) {},
  63. onDragStart: function(l, e, p) {},
  64. beforeDragStop: function(l, e, p) {},
  65. listRenderer: function(children, options) {
  66. var html = '<' + options.listNodeName + ' class="' + options.listClass + '">';
  67. html += children;
  68. html += '</' + options.listNodeName + '>';
  69. return html;
  70. },
  71. itemRenderer: function(item_attrs, content, children, options, item) {
  72. var item_attrs_string = $.map(item_attrs, function(value, key) {
  73. return ' ' + key + '="' + value + '"';
  74. }).join(' ');
  75. var html = '<' + options.itemNodeName + item_attrs_string + '>';
  76. html += '<' + options.handleNodeName + ' class="' + options.handleClass + '">';
  77. html += '<' + options.contentNodeName + ' class="' + options.contentClass + '">';
  78. html += content;
  79. html += '</' + options.contentNodeName + '>';
  80. html += '</' + options.handleNodeName + '>';
  81. html += children;
  82. html += '</' + options.itemNodeName + '>';
  83. return html;
  84. }
  85. };
  86. function Plugin(element, options) {
  87. this.w = $(document);
  88. this.el = $(element);
  89. options = options || defaults;
  90. if (options.rootClass !== undefined && options.rootClass !== 'dd') {
  91. options.listClass = options.listClass ? options.listClass : options.rootClass + '-list';
  92. options.itemClass = options.itemClass ? options.itemClass : options.rootClass + '-item';
  93. options.dragClass = options.dragClass ? options.dragClass : options.rootClass + '-dragel';
  94. options.handleClass = options.handleClass ? options.handleClass : options.rootClass + '-handle';
  95. options.collapsedClass = options.collapsedClass ? options.collapsedClass : options.rootClass + '-collapsed';
  96. options.placeClass = options.placeClass ? options.placeClass : options.rootClass + '-placeholder';
  97. options.noDragClass = options.noDragClass ? options.noDragClass : options.rootClass + '-nodrag';
  98. options.noChildrenClass = options.noChildrenClass ? options.noChildrenClass : options.rootClass + '-nochildren';
  99. options.emptyClass = options.emptyClass ? options.emptyClass : options.rootClass + '-empty';
  100. }
  101. this.options = $.extend({}, defaults, options);
  102. // build HTML from serialized JSON if passed
  103. if (this.options.json !== undefined) {
  104. this._build();
  105. }
  106. this.init();
  107. }
  108. Plugin.prototype = {
  109. init: function() {
  110. var list = this;
  111. list.reset();
  112. list.el.data('nestable-group', this.options.group);
  113. list.placeEl = $('<div class="' + list.options.placeClass + '"/>');
  114. var items = this.el.find(list.options.itemNodeName);
  115. $.each(items, function(k, el) {
  116. var item = $(el),
  117. parent = item.parent();
  118. list.setParent(item);
  119. if (parent.hasClass(list.options.collapsedClass)) {
  120. list.collapseItem(parent.parent());
  121. }
  122. });
  123. // Append the .dd-empty div if the list don't have any items on init
  124. if (!items.length) {
  125. this.appendEmptyElement(this.el);
  126. }
  127. list.el.on('click', 'button', function(e) {
  128. if (list.dragEl) {
  129. return;
  130. }
  131. var target = $(e.currentTarget),
  132. action = target.data('action'),
  133. item = target.parents(list.options.itemNodeName).eq(0);
  134. if (action === 'collapse') {
  135. list.collapseItem(item);
  136. }
  137. if (action === 'expand') {
  138. list.expandItem(item);
  139. }
  140. });
  141. var onStartEvent = function(e) {
  142. var handle = $(e.target);
  143. if (!handle.hasClass(list.options.handleClass)) {
  144. if (handle.closest('.' + list.options.noDragClass).length) {
  145. return;
  146. }
  147. handle = handle.closest('.' + list.options.handleClass);
  148. }
  149. if (!handle.length || list.dragEl) {
  150. return;
  151. }
  152. list.isTouch = /^touch/.test(e.type);
  153. if (list.isTouch && e.touches.length !== 1) {
  154. return;
  155. }
  156. e.preventDefault();
  157. list.dragStart(e.touches ? e.touches[0] : e);
  158. };
  159. var onMoveEvent = function(e) {
  160. if (list.dragEl) {
  161. e.preventDefault();
  162. list.dragMove(e.touches ? e.touches[0] : e);
  163. }
  164. };
  165. var onEndEvent = function(e) {
  166. if (list.dragEl) {
  167. e.preventDefault();
  168. list.dragStop(e.touches ? e.changedTouches[0] : e);
  169. }
  170. };
  171. if (hasTouch) {
  172. list.el[0].addEventListener('touchstart', onStartEvent, false);
  173. window.addEventListener('touchmove', onMoveEvent, false);
  174. window.addEventListener('touchend', onEndEvent, false);
  175. window.addEventListener('touchcancel', onEndEvent, false);
  176. }
  177. list.el.on('mousedown', onStartEvent);
  178. list.w.on('mousemove', onMoveEvent);
  179. list.w.on('mouseup', onEndEvent);
  180. var destroyNestable = function()
  181. {
  182. if (hasTouch) {
  183. list.el[0].removeEventListener('touchstart', onStartEvent, false);
  184. window.removeEventListener('touchmove', onMoveEvent, false);
  185. window.removeEventListener('touchend', onEndEvent, false);
  186. window.removeEventListener('touchcancel', onEndEvent, false);
  187. }
  188. list.el.off('mousedown', onStartEvent);
  189. list.w.off('mousemove', onMoveEvent);
  190. list.w.off('mouseup', onEndEvent);
  191. list.el.off('click');
  192. list.el.unbind('destroy-nestable');
  193. list.el.data("nestable", null);
  194. };
  195. list.el.bind('destroy-nestable', destroyNestable);
  196. },
  197. destroy: function ()
  198. {
  199. this.el.trigger('destroy-nestable');
  200. },
  201. add: function (item)
  202. {
  203. var listClassSelector = '.' + this.options.listClass;
  204. var tree = $(this.el).children(listClassSelector);
  205. if (item.parent_id !== undefined) {
  206. tree = tree.find('[data-id="' + item.parent_id + '"]');
  207. delete item.parent_id;
  208. if (tree.children(listClassSelector).length === 0) {
  209. tree = tree.append(this.options.listRenderer('', this.options));
  210. }
  211. tree = tree.find(listClassSelector + ':first');
  212. this.setParent(tree.parent());
  213. }
  214. tree.append(this._buildItem(item, this.options));
  215. },
  216. replace: function (item)
  217. {
  218. var html = this._buildItem(item, this.options);
  219. this._getItemById(item.id)
  220. .replaceWith(html);
  221. },
  222. //removes item and additional elements from list
  223. removeItem: function (item){
  224. var opts = this.options,
  225. el = this.el;
  226. // remove item
  227. item = item || this;
  228. item.remove();
  229. // remove empty children lists
  230. var emptyListsSelector = '.' + opts.listClass
  231. + ' .' + opts.listClass + ':not(:has(*))';
  232. $(el).find(emptyListsSelector).remove();
  233. // remove buttons if parents do not have children
  234. var buttonsSelector = '[data-action="expand"], [data-action="collapse"]';
  235. $(el).find(buttonsSelector).each(function() {
  236. var siblings = $(this).siblings('.' + opts.listClass);
  237. if (siblings.length === 0) {
  238. $(this).remove();
  239. }
  240. });
  241. },
  242. //removes item by itemId and run callback at the end
  243. remove: function (itemId, callback)
  244. {
  245. var opts = this.options;
  246. var list = this;
  247. var item = this._getItemById(itemId);
  248. //animation style
  249. var animation = opts.effect.animation || 'fade';
  250. //animation time
  251. var time = opts.effect.time || 'slow';
  252. //add fadeOut effect when removing
  253. if (animation === 'fade'){
  254. item.fadeOut(time, function(){
  255. list.removeItem(item);
  256. });
  257. }
  258. else {
  259. this.removeItem(item);
  260. }
  261. if (callback) callback();
  262. },
  263. //removes all items from the list and run callback at the end
  264. removeAll: function(callback){
  265. var list = this,
  266. opts = this.options,
  267. node = list.el.find(opts.listNodeName).first(),
  268. items = node.children(opts.itemNodeName);
  269. //animation style
  270. var animation = opts.effect.animation || 'fade';
  271. //animation time
  272. var time = opts.effect.time || 'slow';
  273. function remove(){
  274. //Removes each item and its children.
  275. items.each(function() {
  276. list.removeItem($(this));
  277. });
  278. //Now we can again show our node element
  279. node.show();
  280. if (callback) callback();
  281. }
  282. //add fadeOut effect when removing
  283. if (animation === 'fade'){
  284. node.fadeOut(time, remove);
  285. }
  286. else {
  287. remove();
  288. }
  289. },
  290. _getItemById: function(itemId) {
  291. return $(this.el).children('.' + this.options.listClass)
  292. .find('[data-id="' + itemId + '"]');
  293. },
  294. _build: function() {
  295. var json = this.options.json;
  296. if (typeof json === 'string') {
  297. json = JSON.parse(json);
  298. }
  299. $(this.el).html(this._buildList(json, this.options));
  300. },
  301. _buildList: function(items, options) {
  302. if (!items) {
  303. return '';
  304. }
  305. var children = '';
  306. var that = this;
  307. $.each(items, function(index, sub) {
  308. children += that._buildItem(sub, options);
  309. });
  310. return options.listRenderer(children, options);
  311. },
  312. _buildItem: function(item, options) {
  313. function escapeHtml(text) {
  314. var map = {
  315. '&': '&amp;',
  316. '<': '&lt;',
  317. '>': '&gt;',
  318. '"': '&quot;',
  319. "'": '&#039;'
  320. };
  321. return text + "".replace(/[&<>"']/g, function(m) { return map[m]; });
  322. }
  323. function filterClasses(classes) {
  324. var new_classes = {};
  325. for (var k in classes) {
  326. // Remove duplicates
  327. new_classes[classes[k]] = classes[k];
  328. }
  329. return new_classes;
  330. }
  331. function createClassesString(item, options) {
  332. var classes = item.classes || {};
  333. if (typeof classes === 'string') {
  334. classes = [classes];
  335. }
  336. var item_classes = filterClasses(classes);
  337. item_classes[options.itemClass] = options.itemClass;
  338. // create class string
  339. return $.map(item_classes, function(val) {
  340. return val;
  341. }).join(' ');
  342. }
  343. function createDataAttrs(attr) {
  344. attr = $.extend({}, attr);
  345. delete attr.children;
  346. delete attr.classes;
  347. delete attr.content;
  348. var data_attrs = {};
  349. $.each(attr, function(key, value) {
  350. if (typeof value === 'object') {
  351. value = JSON.stringify(value);
  352. }
  353. data_attrs["data-" + key] = escapeHtml(value);
  354. });
  355. return data_attrs;
  356. }
  357. var item_attrs = createDataAttrs(item);
  358. item_attrs["class"] = createClassesString(item, options);
  359. var content = options.contentCallback(item);
  360. var children = this._buildList(item.children, options);
  361. var html = $(options.itemRenderer(item_attrs, content, children, options, item));
  362. this.setParent(html);
  363. return html[0].outerHTML;
  364. },
  365. serialize: function() {
  366. var data, list = this, step = function(level) {
  367. var array = [],
  368. items = level.children(list.options.itemNodeName);
  369. items.each(function() {
  370. var li = $(this),
  371. item = $.extend({}, li.data()),
  372. sub = li.children(list.options.listNodeName);
  373. if (list.options.includeContent) {
  374. var content = li.find('.' + list.options.contentClass).html();
  375. if (content) {
  376. item.content = content;
  377. }
  378. }
  379. if (sub.length) {
  380. item.children = step(sub);
  381. }
  382. array.push(item);
  383. });
  384. return array;
  385. };
  386. data = step(list.el.find(list.options.listNodeName).first());
  387. return data;
  388. },
  389. asNestedSet: function() {
  390. var list = this, o = list.options, depth = -1, ret = [], lft = 1;
  391. var items = list.el.find(o.listNodeName).first().children(o.itemNodeName);
  392. items.each(function () {
  393. lft = traverse(this, depth + 1, lft);
  394. });
  395. ret = ret.sort(function(a,b){ return (a.lft - b.lft); });
  396. return ret;
  397. function traverse(item, depth, lft) {
  398. var rgt = lft + 1, id, pid;
  399. if ($(item).children(o.listNodeName).children(o.itemNodeName).length > 0 ) {
  400. depth++;
  401. $(item).children(o.listNodeName).children(o.itemNodeName).each(function () {
  402. rgt = traverse($(this), depth, rgt);
  403. });
  404. depth--;
  405. }
  406. id = $(item).attr('data-id');
  407. if (isInt(id)) {
  408. id = parseInt(id);
  409. }
  410. pid = $(item).parent(o.listNodeName).parent(o.itemNodeName).attr('data-id') || '';
  411. if (isInt(pid)) {
  412. id = parseInt(pid);
  413. }
  414. if (id) {
  415. ret.push({"id": id, "parent_id": pid, "depth": depth, "lft": lft, "rgt": rgt});
  416. }
  417. lft = rgt + 1;
  418. return lft;
  419. }
  420. function isInt(value) {
  421. return $.isNumeric(value) && Math.floor(value) == value;
  422. }
  423. },
  424. returnOptions: function() {
  425. return this.options;
  426. },
  427. serialise: function() {
  428. return this.serialize();
  429. },
  430. toHierarchy: function(options) {
  431. var o = $.extend({}, this.options, options),
  432. ret = [];
  433. $(this.element).children(o.items).each(function() {
  434. var level = _recursiveItems(this);
  435. ret.push(level);
  436. });
  437. return ret;
  438. function _recursiveItems(item) {
  439. var id = ($(item).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/));
  440. if (id) {
  441. var currentItem = {
  442. "id": id[2]
  443. };
  444. if ($(item).children(o.listType).children(o.items).length > 0) {
  445. currentItem.children = [];
  446. $(item).children(o.listType).children(o.items).each(function() {
  447. var level = _recursiveItems(this);
  448. currentItem.children.push(level);
  449. });
  450. }
  451. return currentItem;
  452. }
  453. }
  454. },
  455. toArray: function() {
  456. var o = $.extend({}, this.options, this),
  457. sDepth = o.startDepthCount || 0,
  458. ret = [],
  459. left = 2,
  460. list = this,
  461. element = list.el.find(list.options.listNodeName).first();
  462. var items = element.children(list.options.itemNodeName);
  463. items.each(function() {
  464. left = _recursiveArray($(this), sDepth + 1, left);
  465. });
  466. ret = ret.sort(function(a, b) {
  467. return (a.left - b.left);
  468. });
  469. return ret;
  470. function _recursiveArray(item, depth, left) {
  471. var right = left + 1,
  472. id,
  473. pid;
  474. if (item.children(o.options.listNodeName).children(o.options.itemNodeName).length > 0) {
  475. depth++;
  476. item.children(o.options.listNodeName).children(o.options.itemNodeName).each(function() {
  477. right = _recursiveArray($(this), depth, right);
  478. });
  479. depth--;
  480. }
  481. id = item.data().id;
  482. if (depth === sDepth + 1) {
  483. pid = o.rootID;
  484. } else {
  485. var parentItem = (item.parent(o.options.listNodeName)
  486. .parent(o.options.itemNodeName)
  487. .data());
  488. pid = parentItem.id;
  489. }
  490. if (id) {
  491. ret.push({
  492. "id": id,
  493. "parent_id": pid,
  494. "depth": depth,
  495. "left": left,
  496. "right": right
  497. });
  498. }
  499. left = right + 1;
  500. return left;
  501. }
  502. },
  503. reset: function() {
  504. this.mouse = {
  505. offsetX: 0,
  506. offsetY: 0,
  507. startX: 0,
  508. startY: 0,
  509. lastX: 0,
  510. lastY: 0,
  511. nowX: 0,
  512. nowY: 0,
  513. distX: 0,
  514. distY: 0,
  515. dirAx: 0,
  516. dirX: 0,
  517. dirY: 0,
  518. lastDirX: 0,
  519. lastDirY: 0,
  520. distAxX: 0,
  521. distAxY: 0
  522. };
  523. this.isTouch = false;
  524. this.moving = false;
  525. this.dragEl = null;
  526. this.dragRootEl = null;
  527. this.dragDepth = 0;
  528. this.hasNewRoot = false;
  529. this.pointEl = null;
  530. },
  531. expandItem: function(li) {
  532. li.removeClass(this.options.collapsedClass);
  533. },
  534. collapseItem: function(li) {
  535. var lists = li.children(this.options.listNodeName);
  536. if (lists.length) {
  537. li.addClass(this.options.collapsedClass);
  538. }
  539. },
  540. expandAll: function() {
  541. var list = this;
  542. list.el.find(list.options.itemNodeName).each(function() {
  543. list.expandItem($(this));
  544. });
  545. },
  546. collapseAll: function() {
  547. var list = this;
  548. list.el.find(list.options.itemNodeName).each(function() {
  549. list.collapseItem($(this));
  550. });
  551. },
  552. setParent: function(li) {
  553. //Check if li is an element of itemNodeName type and has children
  554. if (li.is(this.options.itemNodeName) && li.children(this.options.listNodeName).length) {
  555. // make sure NOT showing two or more sets data-action buttons
  556. li.children('[data-action]').remove();
  557. li.prepend($(this.options.expandBtnHTML));
  558. li.prepend($(this.options.collapseBtnHTML));
  559. }
  560. },
  561. unsetParent: function(li) {
  562. li.removeClass(this.options.collapsedClass);
  563. li.children('[data-action]').remove();
  564. li.children(this.options.listNodeName).remove();
  565. },
  566. dragStart: function(e) {
  567. var mouse = this.mouse,
  568. target = $(e.target),
  569. dragItem = target.closest(this.options.itemNodeName),
  570. position = {
  571. top : e.pageY,
  572. left : e.pageX
  573. };
  574. var continueExecution = this.options.onDragStart.call(this, this.el, dragItem, position);
  575. if (typeof continueExecution !== 'undefined' && continueExecution === false) {
  576. return;
  577. }
  578. this.placeEl.css('height', dragItem.height());
  579. mouse.offsetX = e.pageX - dragItem.offset().left;
  580. mouse.offsetY = e.pageY - dragItem.offset().top;
  581. mouse.startX = mouse.lastX = e.pageX;
  582. mouse.startY = mouse.lastY = e.pageY;
  583. this.dragRootEl = this.el;
  584. this.dragEl = $(document.createElement(this.options.listNodeName)).addClass(this.options.listClass + ' ' + this.options.dragClass);
  585. this.dragEl.css('width', dragItem.outerWidth());
  586. this.setIndexOfItem(dragItem);
  587. // fix for zepto.js
  588. //dragItem.after(this.placeEl).detach().appendTo(this.dragEl);
  589. dragItem.after(this.placeEl);
  590. dragItem[0].parentNode.removeChild(dragItem[0]);
  591. dragItem.appendTo(this.dragEl);
  592. $(document.body).append(this.dragEl);
  593. this.dragEl.css({
  594. 'left': e.pageX - mouse.offsetX,
  595. 'top': e.pageY - mouse.offsetY
  596. });
  597. // total depth of dragging item
  598. var i, depth,
  599. items = this.dragEl.find(this.options.itemNodeName);
  600. for (i = 0; i < items.length; i++) {
  601. depth = $(items[i]).parents(this.options.listNodeName).length;
  602. if (depth > this.dragDepth) {
  603. this.dragDepth = depth;
  604. }
  605. }
  606. },
  607. //Create sublevel.
  608. // element : element which become parent
  609. // item : something to place into new sublevel
  610. createSubLevel: function(element, item) {
  611. var list = $('<' + this.options.listNodeName + '/>').addClass(this.options.listClass);
  612. if (item) list.append(item);
  613. element.append(list);
  614. this.setParent(element);
  615. return list;
  616. },
  617. setIndexOfItem: function(item, index) {
  618. index = index || [];
  619. index.unshift(item.index());
  620. if ($(item[0].parentNode)[0] !== this.dragRootEl[0]) {
  621. this.setIndexOfItem($(item[0].parentNode), index);
  622. }
  623. else {
  624. this.dragEl.data('indexOfItem', index);
  625. }
  626. },
  627. restoreItemAtIndex: function(dragElement, indexArray) {
  628. var currentEl = this.el,
  629. lastIndex = indexArray.length - 1;
  630. //Put drag element at current element position.
  631. function placeElement(currentEl, dragElement) {
  632. if (indexArray[lastIndex] === 0) {
  633. $(currentEl).prepend(dragElement.clone(true)); //using true saves added to element events.
  634. }
  635. else {
  636. $(currentEl.children[indexArray[lastIndex] - 1]).after(dragElement.clone(true)); //using true saves added to element events.
  637. }
  638. }
  639. //Diggin through indexArray to get home for dragElement.
  640. for (var i = 0; i < indexArray.length; i++) {
  641. if (lastIndex === parseInt(i)) {
  642. placeElement(currentEl, dragElement);
  643. return;
  644. }
  645. //element can have no indexes, so we have to use conditional here to avoid errors.
  646. //if element doesn't exist we defenetly need to add new list.
  647. var element = (currentEl[0]) ? currentEl[0] : currentEl;
  648. var nextEl = element.children[indexArray[i]];
  649. currentEl = (!nextEl) ? this.createSubLevel($(element)) : nextEl;
  650. }
  651. },
  652. dragStop: function(e) {
  653. // fix for zepto.js
  654. //this.placeEl.replaceWith(this.dragEl.children(this.options.itemNodeName + ':first').detach());
  655. var position = {
  656. top : e.pageY,
  657. left : e.pageX
  658. };
  659. //Get indexArray of item at drag start.
  660. var srcIndex = this.dragEl.data('indexOfItem');
  661. var el = this.dragEl.children(this.options.itemNodeName).first();
  662. el[0].parentNode.removeChild(el[0]);
  663. this.dragEl.remove(); //Remove dragEl, cause it can affect on indexing in html collection.
  664. //Before drag stop callback
  665. var continueExecution = this.options.beforeDragStop.call(this, this.el, el, this.placeEl.parent());
  666. if (typeof continueExecution !== 'undefined' && continueExecution === false) {
  667. var parent = this.placeEl.parent();
  668. this.placeEl.remove();
  669. if (!parent.children().length) {
  670. this.unsetParent(parent.parent());
  671. }
  672. this.restoreItemAtIndex(el, srcIndex);
  673. this.reset();
  674. return;
  675. }
  676. this.placeEl.replaceWith(el);
  677. if (this.hasNewRoot) {
  678. if (this.options.fixed === true) {
  679. this.restoreItemAtIndex(el, srcIndex);
  680. }
  681. else {
  682. this.el.trigger('lostItem');
  683. }
  684. this.dragRootEl.trigger('gainedItem');
  685. }
  686. else {
  687. this.dragRootEl.trigger('change');
  688. }
  689. this.options.callback.call(this, this.dragRootEl, el, position);
  690. this.reset();
  691. },
  692. dragMove: function(e) {
  693. var list, parent, prev, next, depth,
  694. opt = this.options,
  695. mouse = this.mouse;
  696. this.dragEl.css({
  697. 'left': e.pageX - mouse.offsetX,
  698. 'top': e.pageY - mouse.offsetY
  699. });
  700. // mouse position last events
  701. mouse.lastX = mouse.nowX;
  702. mouse.lastY = mouse.nowY;
  703. // mouse position this events
  704. mouse.nowX = e.pageX;
  705. mouse.nowY = e.pageY;
  706. // distance mouse moved between events
  707. mouse.distX = mouse.nowX - mouse.lastX;
  708. mouse.distY = mouse.nowY - mouse.lastY;
  709. // direction mouse was moving
  710. mouse.lastDirX = mouse.dirX;
  711. mouse.lastDirY = mouse.dirY;
  712. // direction mouse is now moving (on both axis)
  713. mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1;
  714. mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1;
  715. // axis mouse is now moving on
  716. var newAx = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0;
  717. // do nothing on first move
  718. if (!mouse.moving) {
  719. mouse.dirAx = newAx;
  720. mouse.moving = true;
  721. return;
  722. }
  723. // do scrolling if enable
  724. if (opt.scroll) {
  725. if (typeof window.jQuery.fn.scrollParent !== 'undefined') {
  726. var scrolled = false;
  727. var scrollParent = this.el.scrollParent()[0];
  728. if (scrollParent !== document && scrollParent.tagName !== 'HTML') {
  729. if ((opt.scrollTriggers.bottom + scrollParent.offsetHeight) - e.pageY < opt.scrollSensitivity)
  730. scrollParent.scrollTop = scrolled = scrollParent.scrollTop + opt.scrollSpeed;
  731. else if (e.pageY - opt.scrollTriggers.top < opt.scrollSensitivity)
  732. scrollParent.scrollTop = scrolled = scrollParent.scrollTop - opt.scrollSpeed;
  733. if ((opt.scrollTriggers.right + scrollParent.offsetWidth) - e.pageX < opt.scrollSensitivity)
  734. scrollParent.scrollLeft = scrolled = scrollParent.scrollLeft + opt.scrollSpeed;
  735. else if (e.pageX - opt.scrollTriggers.left < opt.scrollSensitivity)
  736. scrollParent.scrollLeft = scrolled = scrollParent.scrollLeft - opt.scrollSpeed;
  737. } else {
  738. if (e.pageY - $(document).scrollTop() < opt.scrollSensitivity)
  739. scrolled = $(document).scrollTop($(document).scrollTop() - opt.scrollSpeed);
  740. else if ($(window).height() - (e.pageY - $(document).scrollTop()) < opt.scrollSensitivity)
  741. scrolled = $(document).scrollTop($(document).scrollTop() + opt.scrollSpeed);
  742. if (e.pageX - $(document).scrollLeft() < opt.scrollSensitivity)
  743. scrolled = $(document).scrollLeft($(document).scrollLeft() - opt.scrollSpeed);
  744. else if ($(window).width() - (e.pageX - $(document).scrollLeft()) < opt.scrollSensitivity)
  745. scrolled = $(document).scrollLeft($(document).scrollLeft() + opt.scrollSpeed);
  746. }
  747. } else {
  748. console.warn('To use scrolling you need to have scrollParent() function, check documentation for more information');
  749. }
  750. }
  751. if (this.scrollTimer) {
  752. clearTimeout(this.scrollTimer);
  753. }
  754. if (opt.scroll && scrolled) {
  755. this.scrollTimer = setTimeout(function() {
  756. $(window).trigger(e);
  757. }, 10);
  758. }
  759. // calc distance moved on this axis (and direction)
  760. if (mouse.dirAx !== newAx) {
  761. mouse.distAxX = 0;
  762. mouse.distAxY = 0;
  763. }
  764. else {
  765. mouse.distAxX += Math.abs(mouse.distX);
  766. if (mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) {
  767. mouse.distAxX = 0;
  768. }
  769. mouse.distAxY += Math.abs(mouse.distY);
  770. if (mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) {
  771. mouse.distAxY = 0;
  772. }
  773. }
  774. mouse.dirAx = newAx;
  775. /**
  776. * move horizontal
  777. */
  778. if (mouse.dirAx && mouse.distAxX >= opt.threshold) {
  779. // reset move distance on x-axis for new phase
  780. mouse.distAxX = 0;
  781. prev = this.placeEl.prev(opt.itemNodeName);
  782. // increase horizontal level if previous sibling exists, is not collapsed, and can have children
  783. if (mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass) && !prev.hasClass(opt.noChildrenClass)) {
  784. // cannot increase level when item above is collapsed
  785. list = prev.find(opt.listNodeName).last();
  786. // check if depth limit has reached
  787. depth = this.placeEl.parents(opt.listNodeName).length;
  788. if (depth + this.dragDepth <= opt.maxDepth) {
  789. // create new sub-level if one doesn't exist
  790. if (!list.length) {
  791. this.createSubLevel(prev, this.placeEl);
  792. }
  793. else {
  794. // else append to next level up
  795. list = prev.children(opt.listNodeName).last();
  796. list.append(this.placeEl);
  797. }
  798. }
  799. }
  800. // decrease horizontal level
  801. if (mouse.distX < 0) {
  802. // we can't decrease a level if an item preceeds the current one
  803. next = this.placeEl.next(opt.itemNodeName);
  804. if (!next.length) {
  805. parent = this.placeEl.parent();
  806. this.placeEl.closest(opt.itemNodeName).after(this.placeEl);
  807. if (!parent.children().length) {
  808. this.unsetParent(parent.parent());
  809. }
  810. }
  811. }
  812. }
  813. var isEmpty = false;
  814. // find list item under cursor
  815. if (!hasPointerEvents) {
  816. this.dragEl[0].style.visibility = 'hidden';
  817. }
  818. this.pointEl = $(document.elementFromPoint(e.pageX - document.body.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop)));
  819. if (!hasPointerEvents) {
  820. this.dragEl[0].style.visibility = 'visible';
  821. }
  822. if (this.pointEl.hasClass(opt.handleClass)) {
  823. this.pointEl = this.pointEl.closest(opt.itemNodeName);
  824. }
  825. if (this.pointEl.hasClass(opt.emptyClass)) {
  826. isEmpty = true;
  827. }
  828. else if (!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) {
  829. return;
  830. }
  831. // find parent list of item under cursor
  832. var pointElRoot = this.pointEl.closest('.' + opt.rootClass),
  833. isNewRoot = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id');
  834. /**
  835. * move vertical
  836. */
  837. if (!mouse.dirAx || isNewRoot || isEmpty) {
  838. // check if groups match if dragging over new root
  839. if (isNewRoot && opt.group !== pointElRoot.data('nestable-group')) {
  840. return;
  841. }
  842. // fixed item's depth, use for some list has specific type, eg:'Volume, Section, Chapter ...'
  843. if (this.options.fixedDepth && this.dragDepth + 1 !== this.pointEl.parents(opt.listNodeName).length) {
  844. return;
  845. }
  846. // check depth limit
  847. depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length;
  848. if (depth > opt.maxDepth) {
  849. return;
  850. }
  851. var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2);
  852. parent = this.placeEl.parent();
  853. // if empty create new list to replace empty placeholder
  854. if (isEmpty) {
  855. list = $(document.createElement(opt.listNodeName)).addClass(opt.listClass);
  856. list.append(this.placeEl);
  857. this.pointEl.replaceWith(list);
  858. }
  859. else if (before) {
  860. this.pointEl.before(this.placeEl);
  861. }
  862. else {
  863. this.pointEl.after(this.placeEl);
  864. }
  865. if (!parent.children().length) {
  866. this.unsetParent(parent.parent());
  867. }
  868. if (!this.dragRootEl.find(opt.itemNodeName).length) {
  869. this.appendEmptyElement(this.dragRootEl);
  870. }
  871. // parent root list has changed
  872. this.dragRootEl = pointElRoot;
  873. if (isNewRoot) {
  874. this.hasNewRoot = this.el[0] !== this.dragRootEl[0];
  875. }
  876. }
  877. },
  878. // Append the .dd-empty div to the list so it can be populated and styled
  879. appendEmptyElement: function(element) {
  880. element.append('<div class="' + this.options.emptyClass + '"/>');
  881. }
  882. };
  883. $.fn.nestable = function(params) {
  884. var lists = this,
  885. retval = this,
  886. args = arguments;
  887. if (!('Nestable' in window)) {
  888. window.Nestable = {};
  889. Nestable.counter = 0;
  890. }
  891. lists.each(function() {
  892. var plugin = $(this).data("nestable");
  893. if (!plugin) {
  894. Nestable.counter++;
  895. $(this).data("nestable", new Plugin(this, params));
  896. $(this).data("nestable-id", Nestable.counter);
  897. }
  898. else {
  899. if (typeof params === 'string' && typeof plugin[params] === 'function') {
  900. if (args.length > 1){
  901. var pluginArgs = [];
  902. for (var i = 1; i < args.length; i++) {
  903. pluginArgs.push(args[i]);
  904. }
  905. retval = plugin[params].apply(plugin, pluginArgs);
  906. }
  907. else {
  908. retval = plugin[params]();
  909. }
  910. }
  911. }
  912. });
  913. return retval || lists;
  914. };
  915. })(window.jQuery || window.Zepto, window, document);