123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- /*!
- * Nestable jQuery Plugin - Copyright (c) 2012 David Bushell - http://dbushell.com/
- * Dual-licensed under the BSD or MIT licenses
- */
- ;(function($, window, document, undefined)
- {
- var hasTouch = 'ontouchstart' in document;
- /**
- * Detect CSS pointer-events property
- * events are normally disabled on the dragging element to avoid conflicts
- * https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js
- */
- var hasPointerEvents = (function()
- {
- var el = document.createElement('div'),
- docEl = document.documentElement;
- if (!('pointerEvents' in el.style)) {
- return false;
- }
- el.style.pointerEvents = 'auto';
- el.style.pointerEvents = 'x';
- docEl.appendChild(el);
- var supports = window.getComputedStyle && window.getComputedStyle(el, '').pointerEvents === 'auto';
- docEl.removeChild(el);
- return !!supports;
- })();
- var defaults = {
- listNodeName : 'ol',
- itemNodeName : 'li',
- rootClass : 'dd',
- listClass : 'dd-list',
- itemClass : 'dd-item',
- dragClass : 'dd-dragel',
- handleClass : 'dd-handle',
- collapsedClass : 'dd-collapsed',
- placeClass : 'dd-placeholder',
- noDragClass : 'dd-nodrag',
- emptyClass : 'dd-empty',
- expandBtnHTML : '<button data-action="expand" type="button">Expand</button>',
- collapseBtnHTML : '<button data-action="collapse" type="button">Collapse</button>',
- group : 0,
- maxDepth : 5,
- threshold : 20
- };
- function Plugin(element, options)
- {
- this.w = $(document);
- this.el = $(element);
- this.options = $.extend({}, defaults, options);
- this.init();
- }
- Plugin.prototype = {
- init: function()
- {
- var list = this;
- list.reset();
- list.el.data('nestable-group', this.options.group);
- list.placeEl = $('<div class="' + list.options.placeClass + '"/>');
- $.each(this.el.find(list.options.itemNodeName), function(k, el) {
- list.setParent($(el));
- });
- list.el.on('click', 'button', function(e) {
- if (list.dragEl) {
- return;
- }
- var target = $(e.currentTarget),
- action = target.data('action'),
- item = target.parent(list.options.itemNodeName);
- if (action === 'collapse') {
- list.collapseItem(item);
- }
- if (action === 'expand') {
- list.expandItem(item);
- }
- });
- var onStartEvent = function(e)
- {
- var handle = $(e.target);
- if (!handle.hasClass(list.options.handleClass)) {
- if (handle.closest('.' + list.options.noDragClass).length) {
- return;
- }
- handle = handle.closest('.' + list.options.handleClass);
- }
- if (!handle.length || list.dragEl) {
- return;
- }
- list.isTouch = /^touch/.test(e.type);
- if (list.isTouch && e.touches.length !== 1) {
- return;
- }
- e.preventDefault();
- list.dragStart(e.touches ? e.touches[0] : e);
- };
- var onMoveEvent = function(e)
- {
- if (list.dragEl) {
- e.preventDefault();
- list.dragMove(e.touches ? e.touches[0] : e);
- }
- };
- var onEndEvent = function(e)
- {
- if (list.dragEl) {
- e.preventDefault();
- list.dragStop(e.touches ? e.touches[0] : e);
- }
- };
- if (hasTouch) {
- list.el[0].addEventListener('touchstart', onStartEvent, false);
- window.addEventListener('touchmove', onMoveEvent, false);
- window.addEventListener('touchend', onEndEvent, false);
- window.addEventListener('touchcancel', onEndEvent, false);
- }
- list.el.on('mousedown', onStartEvent);
- list.w.on('mousemove', onMoveEvent);
- list.w.on('mouseup', onEndEvent);
- },
- serialize: function()
- {
- var data,
- depth = 0,
- list = this;
- step = function(level, depth)
- {
- var array = [ ],
- items = level.children(list.options.itemNodeName);
- items.each(function()
- {
- var li = $(this),
- item = $.extend({}, li.data()),
- sub = li.children(list.options.listNodeName);
- if (sub.length) {
- item.children = step(sub, depth + 1);
- }
- array.push(item);
- });
- return array;
- };
- data = step(list.el.find(list.options.listNodeName).first(), depth);
- return data;
- },
- serialise: function()
- {
- return this.serialize();
- },
- reset: function()
- {
- this.mouse = {
- offsetX : 0,
- offsetY : 0,
- startX : 0,
- startY : 0,
- lastX : 0,
- lastY : 0,
- nowX : 0,
- nowY : 0,
- distX : 0,
- distY : 0,
- dirAx : 0,
- dirX : 0,
- dirY : 0,
- lastDirX : 0,
- lastDirY : 0,
- distAxX : 0,
- distAxY : 0
- };
- this.isTouch = false;
- this.moving = false;
- this.dragEl = null;
- this.dragRootEl = null;
- this.dragDepth = 0;
- this.hasNewRoot = false;
- this.pointEl = null;
- },
- expandItem: function(li)
- {
- li.removeClass(this.options.collapsedClass);
- li.children('[data-action="expand"]').hide();
- li.children('[data-action="collapse"]').show();
- li.children(this.options.listNodeName).show();
- },
- collapseItem: function(li)
- {
- var lists = li.children(this.options.listNodeName);
- if (lists.length) {
- li.addClass(this.options.collapsedClass);
- li.children('[data-action="collapse"]').hide();
- li.children('[data-action="expand"]').show();
- li.children(this.options.listNodeName).hide();
- }
- },
- expandAll: function()
- {
- var list = this;
- list.el.find(list.options.itemNodeName).each(function() {
- list.expandItem($(this));
- });
- },
- collapseAll: function()
- {
- var list = this;
- list.el.find(list.options.itemNodeName).each(function() {
- list.collapseItem($(this));
- });
- },
- setParent: function(li)
- {
- if (li.children(this.options.listNodeName).length) {
- li.prepend($(this.options.expandBtnHTML));
- li.prepend($(this.options.collapseBtnHTML));
- }
- li.children('[data-action="expand"]').hide();
- },
- unsetParent: function(li)
- {
- li.removeClass(this.options.collapsedClass);
- li.children('[data-action]').remove();
- li.children(this.options.listNodeName).remove();
- },
- dragStart: function(e)
- {
- var mouse = this.mouse,
- target = $(e.target),
- dragItem = target.closest(this.options.itemNodeName);
- this.placeEl.css('height', dragItem.height());
- mouse.offsetX = e.offsetX !== undefined ? e.offsetX : e.pageX - target.offset().left;
- mouse.offsetY = e.offsetY !== undefined ? e.offsetY : e.pageY - target.offset().top;
- mouse.startX = mouse.lastX = e.pageX;
- mouse.startY = mouse.lastY = e.pageY;
- this.dragRootEl = this.el;
- this.dragEl = $(document.createElement(this.options.listNodeName)).addClass(this.options.listClass + ' ' + this.options.dragClass);
- this.dragEl.css('width', dragItem.width());
- dragItem.after(this.placeEl);
- dragItem[0].parentNode.removeChild(dragItem[0]);
- dragItem.appendTo(this.dragEl);
- $(document.body).append(this.dragEl);
- this.dragEl.css({
- 'left' : e.pageX - mouse.offsetX,
- 'top' : e.pageY - mouse.offsetY
- });
- // total depth of dragging item
- var i, depth,
- items = this.dragEl.find(this.options.itemNodeName);
- for (i = 0; i < items.length; i++) {
- depth = $(items[i]).parents(this.options.listNodeName).length;
- if (depth > this.dragDepth) {
- this.dragDepth = depth;
- }
- }
- },
- dragStop: function(e)
- {
- var el = this.dragEl.children(this.options.itemNodeName).first();
- el[0].parentNode.removeChild(el[0]);
- this.placeEl.replaceWith(el);
- this.dragEl.remove();
- this.el.trigger('change');
- if (this.hasNewRoot) {
- this.dragRootEl.trigger('change');
- }
- this.reset();
- },
- dragMove: function(e)
- {
- var list, parent, prev, next, depth,
- opt = this.options,
- mouse = this.mouse;
- this.dragEl.css({
- 'left' : e.pageX - mouse.offsetX,
- 'top' : e.pageY - mouse.offsetY
- });
- // mouse position last events
- mouse.lastX = mouse.nowX;
- mouse.lastY = mouse.nowY;
- // mouse position this events
- mouse.nowX = e.pageX;
- mouse.nowY = e.pageY;
- // distance mouse moved between events
- mouse.distX = mouse.nowX - mouse.lastX;
- mouse.distY = mouse.nowY - mouse.lastY;
- // direction mouse was moving
- mouse.lastDirX = mouse.dirX;
- mouse.lastDirY = mouse.dirY;
- // direction mouse is now moving (on both axis)
- mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1;
- mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1;
- // axis mouse is now moving on
- var newAx = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0;
- // do nothing on first move
- if (!mouse.moving) {
- mouse.dirAx = newAx;
- mouse.moving = true;
- return;
- }
- // calc distance moved on this axis (and direction)
- if (mouse.dirAx !== newAx) {
- mouse.distAxX = 0;
- mouse.distAxY = 0;
- } else {
- mouse.distAxX += Math.abs(mouse.distX);
- if (mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) {
- mouse.distAxX = 0;
- }
- mouse.distAxY += Math.abs(mouse.distY);
- if (mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) {
- mouse.distAxY = 0;
- }
- }
- mouse.dirAx = newAx;
- /**
- * move horizontal
- */
- if (mouse.dirAx && mouse.distAxX >= opt.threshold) {
- // reset move distance on x-axis for new phase
- mouse.distAxX = 0;
- prev = this.placeEl.prev(opt.itemNodeName);
- // increase horizontal level if previous sibling exists and is not collapsed
- if (mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass)) {
- // cannot increase level when item above is collapsed
- list = prev.find(opt.listNodeName).last();
- // check if depth limit has reached
- depth = this.placeEl.parents(opt.listNodeName).length;
- if (depth + this.dragDepth <= opt.maxDepth) {
- // create new sub-level if one doesn't exist
- if (!list.length) {
- list = $('<' + opt.listNodeName + '/>').addClass(opt.listClass);
- list.append(this.placeEl);
- prev.append(list);
- this.setParent(prev);
- } else {
- // else append to next level up
- list = prev.children(opt.listNodeName).last();
- list.append(this.placeEl);
- }
- }
- }
- // decrease horizontal level
- if (mouse.distX < 0) {
- // we can't decrease a level if an item preceeds the current one
- next = this.placeEl.next(opt.itemNodeName);
- if (!next.length) {
- parent = this.placeEl.parent();
- this.placeEl.closest(opt.itemNodeName).after(this.placeEl);
- if (!parent.children().length) {
- this.unsetParent(parent.parent());
- }
- }
- }
- }
- var isEmpty = false;
- // find list item under cursor
- if (!hasPointerEvents) {
- this.dragEl[0].style.visibility = 'hidden';
- }
- this.pointEl = $(document.elementFromPoint(e.pageX - document.body.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop)));
- if (!hasPointerEvents) {
- this.dragEl[0].style.visibility = 'visible';
- }
- if (this.pointEl.hasClass(opt.handleClass)) {
- this.pointEl = this.pointEl.parent(opt.itemNodeName);
- }
- if (this.pointEl.hasClass(opt.emptyClass)) {
- isEmpty = true;
- }
- else if (!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) {
- return;
- }
- // find parent list of item under cursor
- var pointElRoot = this.pointEl.closest('.' + opt.rootClass),
- isNewRoot = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id');
- /**
- * move vertical
- */
- if (!mouse.dirAx || isNewRoot || isEmpty) {
- // check if groups match if dragging over new root
- if (isNewRoot && opt.group !== pointElRoot.data('nestable-group')) {
- return;
- }
- // check depth limit
- depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length;
- if (depth > opt.maxDepth) {
- return;
- }
- var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2);
- parent = this.placeEl.parent();
- // if empty create new list to replace empty placeholder
- if (isEmpty) {
- list = $(document.createElement(opt.listNodeName)).addClass(opt.listClass);
- list.append(this.placeEl);
- this.pointEl.replaceWith(list);
- }
- else if (before) {
- this.pointEl.before(this.placeEl);
- }
- else {
- this.pointEl.after(this.placeEl);
- }
- if (!parent.children().length) {
- this.unsetParent(parent.parent());
- }
- if (!this.dragRootEl.find(opt.itemNodeName).length) {
- this.dragRootEl.append('<div class="' + opt.emptyClass + '"/>');
- }
- // parent root list has changed
- if (isNewRoot) {
- this.dragRootEl = pointElRoot;
- this.hasNewRoot = this.el[0] !== this.dragRootEl[0];
- }
- }
- }
- };
- $.fn.nestable = function(params)
- {
- var lists = this,
- retval = this;
- lists.each(function()
- {
- var plugin = $(this).data("nestable");
- if (!plugin) {
- $(this).data("nestable", new Plugin(this, params));
- $(this).data("nestable-id", new Date().getTime());
- } else {
- if (typeof params === 'string' && typeof plugin[params] === 'function') {
- retval = plugin[params]();
- }
- }
- });
- return retval || lists;
- };
- })(window.jQuery || window.Zepto, window, document);
|