diff --git a/README.md b/README.md
index b86e091..10404d5 100644
--- a/README.md
+++ b/README.md
@@ -1,107 +1,47 @@
-Nestable
+Nestable with 5 little callbacks
========
-## PLEASE NOTE
-
-**I cannot provide any support or guidance beyond this README. If this code helps you that's great but I have no plans to develop Nestable beyond this demo (it's not a final product and has limited functionality). I cannot reply to any requests for help.**
-
+## This is a modified version of Nestable
+
+Original can be found here: https://github.com/dbushell/Nestable
+
+## Example
+```
+$('#example-list-element').nestable({
+ afterInit: function ( event ) {
+ console.log( event );
+ }
+})
+.on('beforeDragStart', function(handle) {
+ console.log('dragStart', handle);
+})
+.on('dragStart', function(event, item, source) {
+ console.log('dragStart', event, item, source);
+})
+.on('dragMove', function(event, item, source, destination) {
+ console.log('dragMove', event, item, source);
+})
+.on('dragEnd', function(event, item, source, destination) {
+ console.log('dragEnd', event, item, source, destination);
+})
+.on('beforeDragEnd', function(event, item, source, destination, position, feedback) {
+ // If you need to persist list items order if changes, you need to comment the next line
+ if (source[0] === destination[0]) { feedback.abort = true; return; }
+
+ feedback.abort = !window.confirm('Continue?');
+})
+.on('dragEnd', function(event, item, source, destination, position) {
+ // Make an ajax request to persist move on database
+ // here you can pass item-id, source-id, destination-id and position index to the server
+ // ....
+
+ console.log('dragEnd', event, item, source, destination, position);
+});
+```
* * *
-### Drag & drop hierarchical list with mouse and touch compatibility (jQuery / Zepto plugin)
-
-[**Try Nestable Demo**](http://dbushell.github.com/Nestable/)
-
-Nestable is an experimental example and not under active development. If it suits your requirements feel free to expand upon it!
-
-## Usage
-
-Write your nested HTML lists like so:
-
-
-
-
- Item 1
-
-
- Item 2
-
-
- Item 3
-
-
- Item 4
-
-
- Item 5
-
-
-
-
-
-
-Then activate with jQuery like so:
-
- $('.dd').nestable({ /* config options */ });
-
-### Events
-
-The `change` event is fired when items are reordered.
-
- $('.dd').on('change', function() {
- /* on change event */
- });
-
-### Methods
-
-You can get a serialised object with all `data-*` attributes for each item.
-
- $('.dd').nestable('serialize');
-
-The serialised JSON for the example above would be:
-
- [{"id":1},{"id":2},{"id":3,"children":[{"id":4},{"id":5}]}]
-
-### Configuration
-
-You can change the follow options:
-
-* `maxDepth` number of levels an item can be nested (default `5`)
-* `group` group ID to allow dragging between lists (default `0`)
-
-These advanced config options are also available:
-
-* `listNodeName` The HTML element to create for lists (default `'ol'`)
-* `itemNodeName` The HTML element to create for list items (default `'li'`)
-* `rootClass` The class of the root element `.nestable()` was used on (default `'dd'`)
-* `listClass` The class of all list elements (default `'dd-list'`)
-* `itemClass` The class of all list item elements (default `'dd-item'`)
-* `dragClass` The class applied to the list element that is being dragged (default `'dd-dragel'`)
-* `handleClass` The class of the content element inside each list item (default `'dd-handle'`)
-* `collapsedClass` The class applied to lists that have been collapsed (default `'dd-collapsed'`)
-* `placeClass` The class of the placeholder element (default `'dd-placeholder'`)
-* `emptyClass` The class used for empty list placeholder elements (default `'dd-empty'`)
-* `expandBtnHTML` The HTML text used to generate a list item expand button (default `'Expand> '`)
-* `collapseBtnHTML` The HTML text used to generate a list item collapse button (default `'Collapse '`)
-
-**Inspect the [Nestable Demo](http://dbushell.github.com/Nestable/) for guidance.**
-
-## Change Log
-
-### 15th October 2012
-
-* Merge for Zepto.js support
-* Merge fix for remove/detach items
-
-### 27th June 2012
-
-* Added `maxDepth` option (default to 5)
-* Added empty placeholder
-* Updated CSS class structure with options for `listClass` and `itemClass`.
-* Fixed to allow drag and drop between multiple Nestable instances (off by default).
-* Added `group` option to enabled the above.
-
-* * *
+Original Author: David Bushell [http://dbushell.com](http://dbushell.com/) [@dbushell](http://twitter.com/dbushell/)
-Author: David Bushell [http://dbushell.com](http://dbushell.com/) [@dbushell](http://twitter.com/dbushell/)
+Big thanks to @bigfoot90 !
Copyright © 2012 David Bushell | BSD & MIT license
diff --git a/jquery.nestable.js b/jquery.nestable.js
index 7323fb4..b1d4fb4 100644
--- a/jquery.nestable.js
+++ b/jquery.nestable.js
@@ -1,484 +1,541 @@
/*!
* Nestable jQuery Plugin - Copyright (c) 2012 David Bushell - http://dbushell.com/
* Dual-licensed under the BSD or MIT licenses
+ *
+ * Modified :
+ * Added a few callbacks
+ * afterInit
+ * beforeDragStart
+ * dragStart
+ * beforeDragEnd
+ * dragEnd
+ * dragMove
*/
;(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 : 'Expand ',
- collapseBtnHTML : 'Collapse ',
- 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 = $('
');
-
- $.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('
');
- }
- // 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;
- };
+ 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 : 'Expand ',
+ collapseBtnHTML : 'Collapse ',
+ group : 0,
+ maxDepth : 5,
+ threshold : 20,
+ /* callback */
+ afterInit: null,
+ };
+
+ 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 = $('
');
+
+ $.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);
+
+ /* callback for beforeDragStart */
+ list.el.trigger('beforeDragStart', [handle]);
+
+ 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);
+
+ /* callback for dragStart */
+ var item = list.dragEl.find('.'+list.options.itemClass);
+ list.dragRootEl.trigger('dragStart', [
+ item, // List item
+ list.el // Source list
+ ]);
+ };
+
+ var onMoveEvent = function(e)
+ {
+ if (list.dragEl) {
+ e.preventDefault();
+ list.dragMove(e.touches ? e.touches[0] : e);
+ /* callback for dragMove */
+ var item = list.dragEl.find('.'+list.options.itemClass);
+ list.dragRootEl.trigger('dragMove', [
+ item, // List item
+ list.el, // Source list
+ list.dragRootEl // Destination
+ ]);
+ }
+ };
+
+ var onEndEvent = function(e)
+ {
+ if (!list.dragEl) return;
+ e.preventDefault();
+
+ var feedback = {abort: false};
+
+ var item = list.dragEl.find('.'+list.options.itemClass);
+ var sourceList = list.el;
+ var destinationList = list.dragRootEl;
+ var position = list.placeEl.index();
+
+ destinationList.trigger('beforeDragEnd', [
+ item, // List item
+ sourceList, // Source list
+ destinationList, // Destination list
+ position, // Position
+ feedback
+ ]);
+
+ if (feedback.abort) return;
+
+ list.dragStop(e.touches ? e.touches[0] : e);
+
+ destinationList.trigger('dragEnd', [
+ item, // List item
+ sourceList, // Source list
+ destinationList, // Destination list
+ position // Position
+ ]);
+ };
+
+ 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);
+
+ /* callback for afterInit */
+ if (typeof list.options.afterInit == 'function') {
+ list.options.afterInit.call(window, this);
+ }
+ },
+
+ 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('
');
+ }
+ // 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);