JAVASCRIPT   63

jquery nestable js

Guest on 29th July 2022 01:39:14 PM

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

Raw Paste


Login or Register to edit or fork this paste. It's free.