diff --git a/README.md b/README.md index 30fd192..7429153 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ -### NOTE: Please use the develop branch +# 29 July 2014 +**nestedSortable is currently maintained at [ilikenwf/nestedSortable](https://github.com/ilikenwf/nestedSortable). Please [go there](https://github.com/ilikenwf/nestedSortable).** -Until further notice, the develop branch is to be considered the most stable and active one. I will only accept pull requests made on the develp branch. +**~~DEVELOPER NEEDED~~** +**~~I'm sorry to say that I am not able to keep the pace developing this project anymore. I know how much nestedSortable is important for web applications, and I still can't understand why it's not part of jQuery-UI. I also think the base of the plugin is very strong, and deserves much more attention and involvement. If anybody is willing to take this project, please say so [here](https://github.com/mjsarfatti/nestedSortable/issues/95).~~** +**~~Thank you.~~** # nestedSortable jQuery plugin -*nestedSortable* is a jQuery plugin that extends jQuery Sortable UI functionalities to nested lists. +*nestedSortable* is a jQuery plugin that extends jQuery Sortable UI functionalities to nested lists. +*Note:* **Version 2.0** *is published in branch '2.0alpha' and is ready for testing! At the moment it has only been tested in Firefox and Chrome, if you work with IE feel free to give it a shot and let me know if something goes wrong.* ## Features @@ -12,8 +16,8 @@ Until further notice, the develop branch is to be considered the most stable and - Items can be sorted in their own list, moved across the tree, or nested under other items. - Sublists are created and deleted on the fly - All jQuery Sortable options, events and methods are available -- It is possible to define elements that will not accept a new nested item/list -- It is possible to define a maximum depth for nested items +- It is possible to define elements that will not accept a new nested item/list and a maximum depth for nested items +- The root level can be protected ## Usage @@ -31,21 +35,43 @@ Until further notice, the develop branch is to be considered the most stable and ``` +``` + $(document).ready(function(){ + + $('.sortable').nestedSortable({ + handle: 'div', + items: 'li', + toleranceElement: '> div' + }); + + }); +``` + Please note: every `
  • ` must have either one or two direct children, the first one being a container element (such as `
    ` in the above example), and the (optional) second one being the nested list. The container element has to be set as the 'toleranceElement' in the options, and this, or one of its children, as the 'handle'. +Also, the default list type is `
      `. + ## Custom Options
      tabSize
      How far right or left (in pixels) the item has to travel in order to be nested or to be sent outside its current list. Default: 20
      -
      disableNesting
      +
      disableNesting
      The class name of the items that will not accept nested lists. Default: ui-nestedSortable-no-nesting
      -
      errorClass
      +
      errorClass
      The class given to the placeholder in case of error. Default: ui-nestedSortable-error
      -
      listType
      +
      listType
      The list type used (ordered or unordered). Default: ol
      -
      maxLevels
      +
      maxLevels
      The maximum depth of nested items the list can accept. If set to '0' the levels are unlimited. Default: 0
      +
      protectRoot
      +
      Wether to protect the root level (i.e. root items can be sorted but not nested, sub-items cannot become root items). Default: false
      +
      rootID
      +
      The id given to the root element (set this to whatever suits your data structure). Default: null
      +
      rtl
      +
      Set this to true if you have a right-to-left page. Default: false
      +
      isAllowed (function)
      +
      You can specify a custom function to verify if a drop location is allowed. Default: function(item, parent) { return true; }
      ## Custom Methods @@ -98,7 +124,7 @@ Tested with: IE 6/7/8, Firefox 3.6/4, Chrome, Safari 3 ## License -This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License. +This work is licensed under the MIT License. -This work is *pizzaware*. If it saved your life, or you just feel good at heart, please consider offering me a pizza. This can be done in two ways: (1) use the Paypal button at the bottom of the project's [home page](http://mjsarfatti.com/sandbox/nestedSortable); (2) send me cash via traditional mail to my home address in Italy. Is the second method legal? It is in Italy if you use Posta assicurata. You should check with your local laws if you live elsewhere. - \ No newline at end of file +This work is *pizzaware*. If it saved your life, or you just feel good at heart, please consider offering me a pizza. This can be done in two ways: (1) follow [this link](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=RSJEW3N9PRMYY&lc=IT&item_name=Manuele%20Sarfatti¤cy_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) to donate through paypal; (2) send me cash via traditional mail to my home address in Italy. Is the second method legal? It is in Italy if you use Posta assicurata. You should check with your local laws if you live elsewhere. + diff --git a/jquery.ui.nestedSortable.js b/jquery.mjs.nestedSortable.js similarity index 56% rename from jquery.ui.nestedSortable.js rename to jquery.mjs.nestedSortable.js index aad83af..e6c0d62 100644 --- a/jquery.ui.nestedSortable.js +++ b/jquery.mjs.nestedSortable.js @@ -1,30 +1,39 @@ /* * jQuery UI Nested Sortable - * v 1.3.4 / 28 apr 2011 - * http://mjsarfatti.com/sandbox/nestedSortable + * v 1.3.5 / 21 jun 2012 + * http://mjsarfatti.com/code/nestedSortable * - * Depends: + * Depends on: * jquery.ui.sortable.js 1.8+ * - * License CC BY-SA 3.0 - * Copyright 2010-2011, Manuele J Sarfatti + * Copyright (c) 2010-2012 Manuele J Sarfatti + * Licensed under the MIT License + * http://www.opensource.org/licenses/mit-license.php */ (function($) { - $.widget("ui.nestedSortable", $.extend({}, $.ui.sortable.prototype, { + $.widget("mjs.nestedSortable", $.extend({}, $.ui.sortable.prototype, { options: { tabSize: 20, - disableNesting: 'ui-nestedSortable-no-nesting', - errorClass: 'ui-nestedSortable-error', + disableNesting: 'mjs-nestedSortable-no-nesting', + errorClass: 'mjs-nestedSortable-error', + doNotClear: false, listType: 'ol', maxLevels: 0, - revertOnError: 1 + protectRoot: false, + rootID: null, + rtl: false, + isAllowed: function(item, parent) { return true; } }, _create: function() { this.element.data('sortable', this.element.data('nestedSortable')); + + if (!this.element.is(this.options.listType)) + throw new Error('nestedSortable: Please check the listType option is set to your actual list type'); + return $.ui.sortable.prototype._create.apply(this, arguments); }, @@ -45,9 +54,11 @@ this.lastPositionAbs = this.positionAbs; } + var o = this.options; + //Do scrolling if(this.options.scroll) { - var o = this.options, scrolled = false; + var scrolled = false; if(this.scrollParent[0] != document && this.scrollParent[0].tagName != 'HTML') { if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) @@ -81,6 +92,9 @@ //Regenerate the absolute position used for position checks this.positionAbs = this._convertPositionTo("absolute"); + // Find the top offset before rearrangement, + var previousTopOffset = this.placeholder.offset().top; + //Set the helper position if(!this.options.axis || this.options.axis != "y") this.helper[0].style.left = this.position.left+'px'; if(!this.options.axis || this.options.axis != "x") this.helper[0].style.top = this.position.top+'px'; @@ -118,16 +132,17 @@ } } - var parentItem = (this.placeholder[0].parentNode.parentNode - && $(this.placeholder[0].parentNode.parentNode).closest('.ui-sortable').length) - ? $(this.placeholder[0].parentNode.parentNode) - : null, + var parentItem = (this.placeholder[0].parentNode.parentNode && + $(this.placeholder[0].parentNode.parentNode).closest('.ui-sortable').length) + ? $(this.placeholder[0].parentNode.parentNode) + : null, level = this._getLevel(this.placeholder), - childLevels = this._getChildLevels(this.helper), - previousItem = this.placeholder[0].previousSibling ? $(this.placeholder[0].previousSibling) : null; + childLevels = this._getChildLevels(this.helper); + // To find the previous sibling in the list, keep backtracking until we hit a valid list item. + var previousItem = this.placeholder[0].previousSibling ? $(this.placeholder[0].previousSibling) : null; if (previousItem != null) { - while (previousItem[0].nodeName.toLowerCase() != 'li' || previousItem[0] == this.currentItem[0]) { + while (previousItem[0].nodeName.toLowerCase() != 'li' || previousItem[0] == this.currentItem[0] || previousItem[0] == this.helper[0]) { if (previousItem[0].previousSibling) { previousItem = $(previousItem[0].previousSibling); } else { @@ -137,27 +152,51 @@ } } - newList = document.createElement(o.listType); + // To find the next sibling in the list, keep stepping forward until we hit a valid list item. + var nextItem = this.placeholder[0].nextSibling ? $(this.placeholder[0].nextSibling) : null; + if (nextItem != null) { + while (nextItem[0].nodeName.toLowerCase() != 'li' || nextItem[0] == this.currentItem[0] || nextItem[0] == this.helper[0]) { + if (nextItem[0].nextSibling) { + nextItem = $(nextItem[0].nextSibling); + } else { + nextItem = null; + break; + } + } + } + + var newList = document.createElement(o.listType); this.beyondMaxLevels = 0; - - // If the item is moved to the left, send it to its parent level - if (parentItem != null && this.positionAbs.left < parentItem.offset().left) { + + // If the item is moved to the left, send it to its parent's level unless there are siblings below it. + if (parentItem != null && nextItem == null && + (o.rtl && (this.positionAbs.left + this.helper.outerWidth() > parentItem.offset().left + parentItem.outerWidth()) || + !o.rtl && (this.positionAbs.left < parentItem.offset().left))) { parentItem.after(this.placeholder[0]); this._clearEmpty(parentItem[0]); this._trigger("change", event, this._uiHash()); } - // If the item is below another one and is moved to the right, make it a children of it - else if (previousItem != null && this.positionAbs.left > previousItem.offset().left + o.tabSize) { - this._isAllowed(previousItem, level+childLevels+1); + // If the item is below a sibling and is moved to the right, make it a child of that sibling. + else if (previousItem != null && + (o.rtl && (this.positionAbs.left + this.helper.outerWidth() < previousItem.offset().left + previousItem.outerWidth() - o.tabSize) || + !o.rtl && (this.positionAbs.left > previousItem.offset().left + o.tabSize))) { + this._isAllowed(previousItem, level, level+childLevels+1); if (!previousItem.children(o.listType).length) { previousItem[0].appendChild(newList); } - previousItem.children(o.listType)[0].appendChild(this.placeholder[0]); + // If this item is being moved from the top, add it to the top of the list. + if (previousTopOffset && (previousTopOffset <= previousItem.offset().top)) { + previousItem.children(o.listType).prepend(this.placeholder); + } + // Otherwise, add it to the bottom of the list. + else { + previousItem.children(o.listType)[0].appendChild(this.placeholder[0]); + } this._trigger("change", event, this._uiHash()); } else { - this._isAllowed(parentItem, level+childLevels); + this._isAllowed(parentItem, level, level+childLevels); } //Post events to containers @@ -181,24 +220,14 @@ this.placeholder.removeClass(this.options.errorClass); - if (this.options.revertOnError) { - if (this.domPosition.prev) { - $(this.domPosition.prev).after(this.placeholder); - } else { - $(this.domPosition.parent).prepend(this.placeholder); - } - this._trigger("revert", event, this._uiHash()); + if (this.domPosition.prev) { + $(this.domPosition.prev).after(this.placeholder); } else { - var parent = this.placeholder.parent().closest(this.options.items); - - for (var i = this.beyondMaxLevels - 1; i > 0; i--) { - parent = parent.parent().closest(this.options.items); - } - - parent.after(this.placeholder); - this._trigger("change", event, this._uiHash()); + $(this.domPosition.parent).prepend(this.placeholder); } + this._trigger("revert", event, this._uiHash()); + } // Clean last empty ul/ol @@ -211,23 +240,24 @@ }, - serialize: function(o) { + serialize: function(options) { - var items = this._getItemsAsjQuery(o && o.connected), - str = []; o = o || {}; + var o = $.extend({}, this.options, options), + items = this._getItemsAsjQuery(o && o.connected), + str = []; $(items).each(function() { var res = ($(o.item || this).attr(o.attribute || 'id') || '') .match(o.expression || (/(.+)[-=_](.+)/)), pid = ($(o.item || this).parent(o.listType) - .parent('li') + .parent(o.items) .attr(o.attribute || 'id') || '') .match(o.expression || (/(.+)[-=_](.+)/)); if (res) { str.push(((o.key || res[1]) + '[' + (o.key && o.expression ? res[1] : res[2]) + ']') + '=' - + (pid ? (o.key && o.expression ? pid[1] : pid[2]) : 'root')); + + (pid ? (o.key && o.expression ? pid[1] : pid[2]) : o.rootID)); } }); @@ -239,51 +269,51 @@ }, - toHierarchy: function(o) { + toHierarchy: function(options) { - o = o || {}; - var sDepth = o.startDepthCount || 0, + var o = $.extend({}, this.options, options), + sDepth = o.startDepthCount || 0, ret = []; - $(this.element).children('li').each(function () { - var level = _recursiveItems($(this)); + $(this.element).children(o.items).each(function () { + var level = _recursiveItems(this); ret.push(level); }); return ret; - function _recursiveItems(li) { - var id = ($(li).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/)); + function _recursiveItems(item) { + var id = ($(item).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/)); if (id) { - var item = {"id" : id[2]}; - if ($(li).children(o.listType).children('li').length > 0) { - item.children = []; - $(li).children(o.listType).children('li').each(function() { - var level = _recursiveItems($(this)); - item.children.push(level); + var currentItem = {"id" : id[2]}; + if ($(item).children(o.listType).children(o.items).length > 0) { + currentItem.children = []; + $(item).children(o.listType).children(o.items).each(function() { + var level = _recursiveItems(this); + currentItem.children.push(level); }); } - return item; + return currentItem; } } }, - toArray: function(o) { + toArray: function(options) { - o = o || {}; - var sDepth = o.startDepthCount || 0, + var o = $.extend({}, this.options, options), + sDepth = o.startDepthCount || 0, ret = [], left = 2; ret.push({ - "item_id": 'root', + "item_id": o.rootID, "parent_id": 'none', "depth": sDepth, "left": '1', - "right": ($('li', this.element).length + 1) * 2 + "right": ($(o.items, this.element).length + 1) * 2 }); - $(this.element).children('li').each(function () { + $(this.element).children(o.items).each(function () { left = _recursiveArray(this, sDepth + 1, left); }); @@ -297,9 +327,9 @@ id, pid; - if ($(item).children(o.listType).children('li').length > 0) { + if ($(item).children(o.listType).children(o.items).length > 0) { depth ++; - $(item).children(o.listType).children('li').each(function () { + $(item).children(o.listType).children(o.items).each(function () { right = _recursiveArray($(this), depth, right); }); depth --; @@ -308,12 +338,12 @@ id = ($(item).attr(o.attribute || 'id')).match(o.expression || (/(.+)[-=_](.+)/)); if (depth === sDepth + 1) { - pid = 'root'; + pid = o.rootID; } else { var parentItem = ($(item).parent(o.listType) - .parent('li') - .attr(o.attribute || 'id')) - .match(o.expression || (/(.+)[-=_](.+)/)); + .parent(o.items) + .attr(o.attribute || 'id')) + .match(o.expression || (/(.+)[-=_](.+)/)); pid = parentItem[2]; } @@ -330,7 +360,7 @@ _clearEmpty: function(item) { var emptyList = $(item).children(this.options.listType); - if (emptyList.length && !emptyList.children().length) { + if (emptyList.length && !emptyList.children().length && !this.options.doNotClear) { emptyList.remove(); } @@ -342,7 +372,8 @@ if (this.options.listType) { var list = item.closest(this.options.listType); - while (!list.is('.ui-sortable')) { + while (list && list.length > 0 && + !list.is('.ui-sortable')) { level++; list = list.parent().closest(this.options.listType); } @@ -364,28 +395,35 @@ return depth ? result + 1 : result; }, - _isAllowed: function(parentItem, levels) { - var o = this.options; - // Are we trying to nest under a no-nest or are we nesting too deep? - if (parentItem == null || !(parentItem.hasClass(o.disableNesting))) { - if (o.maxLevels < levels && o.maxLevels != 0) { + _isAllowed: function(parentItem, level, levels) { + var o = this.options, + isRoot = $(this.domPosition.parent).hasClass('ui-sortable') ? true : false, + maxLevels = this.placeholder.closest('.ui-sortable').nestedSortable('option', 'maxLevels'); // this takes into account the maxLevels set to the recipient list + + // Is the root protected? + // Are we trying to nest under a no-nest? + // Are we nesting too deep? + if (!o.isAllowed(this.currentItem, parentItem) || + parentItem && parentItem.hasClass(o.disableNesting) || + o.protectRoot && (parentItem == null && !isRoot || isRoot && level > 1)) { this.placeholder.addClass(o.errorClass); - this.beyondMaxLevels = levels - o.maxLevels; + if (maxLevels < levels && maxLevels != 0) { + this.beyondMaxLevels = levels - maxLevels; + } else { + this.beyondMaxLevels = 1; + } + } else { + if (maxLevels < levels && maxLevels != 0) { + this.placeholder.addClass(o.errorClass); + this.beyondMaxLevels = levels - maxLevels; } else { this.placeholder.removeClass(o.errorClass); this.beyondMaxLevels = 0; } - } else { - this.placeholder.addClass(o.errorClass); - if (o.maxLevels < levels && o.maxLevels != 0) { - this.beyondMaxLevels = levels - o.maxLevels; - } else { - this.beyondMaxLevels = 1; - } } } })); - $.ui.nestedSortable.prototype.options = $.extend({}, $.ui.sortable.prototype.options, $.ui.nestedSortable.prototype.options); -})(jQuery); \ No newline at end of file + $.mjs.nestedSortable.prototype.options = $.extend({}, $.ui.sortable.prototype.options, $.mjs.nestedSortable.prototype.options); +})(jQuery);