diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..fe87b66 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,11 @@ +{ + "globals": { + "window": false, + "document": false + }, + "rules": { + "no-underscore-dangle": false, + "no-loop-func": false, + "no-use-before-define": false + } +} \ No newline at end of file diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..8729368 --- /dev/null +++ b/.jscsrc @@ -0,0 +1,3 @@ +{ + "preset": "jquery" +} \ No newline at end of file diff --git a/README.md b/README.md index 57fab08..3a00e15 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,14 @@ -**ANNOUNCEMENT** - -**I'm sorry to say that I am not able to keep up the pace in 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. *Note: Version 2.0 is published in branch '2.0alpha' and is available 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.* +## Meteor Installation + meteor add ilikenwf:nested-sortable ## What's new in version 2.0 -The biggest change is that your nested list can now behave as a tree with expand/collapse funcionality. Simply set `isTree` to **true** in the options and you are good to go! Check the [demo](http://mjsarfatti.com/sandbox/nestedSortable) out to see what can be done with nestedSortable and a little CSS. (Note that all **nestedSortable** does is to assign/remove classes on the fly) +The biggest change is that your nested list can now behave as a tree with expand/collapse funcionality. Simply set `isTree` to **true** in the options and you are good to go! Check the [demo](http://ilikenwf.github.io/example.html) out to see what can be done with nestedSortable and a little CSS. (Note that all **nestedSortable** does is to assign/remove classes on the fly) Also: - **isAllowed** function finally works as expected, see the docs below - Fixed: a small bug in the **protectRoot** function @@ -28,6 +24,7 @@ Also: - All jQuery Sortable options, events and methods are available - 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 +- The parentship of items can be locked, just as if it was a family tree. ## Usage @@ -61,11 +58,13 @@ Please note: every `
  • ` must have either one or two direct children, the first Also, the default list type is `
      `. -*This is the bare minimum to have a working nestedSortable. Check the [demo](http://mjsarfatti.com/sandbox/nestedSortable) out to see what can be accomplished with a little more.* +*This is the bare minimum to have a working nestedSortable. Check the [demo](http://ilikenwf.github.io/example.html) out to see what can be accomplished with a little more.* ## Custom Options
      +
      disableParentChange (2.0)
      +
      Set this to true to lock the parentship of items. They can only be re-ordered within theire current parent container.
      doNotClear (2.0)
      Set this to true if you don't want empty lists to be removed. Default: false
      expandOnHover (2.0)
      @@ -109,6 +108,8 @@ Also, the default list type is `
        `.
        Given to collapsed branches when dragging an item over them. Default: mjs-nestedSortable-hovering
        leafClass (2.0)
        Given to items that do not have children. Default: mjs-nestedSortable-leaf
        +
        disabledClass (2.0)
        +
        Given to items that should be skipped when sorting over them. For example, non-visible items that are still part of the list. Default: mjs-nestedSortable-disabled
      ## Custom Methods @@ -143,7 +144,21 @@ Also, the default list type is `
        `. '2' ... 'id' => itemId - Similarly to toArray, it accepts attribute and expression options. + Similarly to toArray, it accepts attribute and expression options. + Optionally adding `data-` attributes will cause them to show up in the hierarchy. See demo for example. + + + +## Events +
        +
        change
        +
        Fires when the item is dragged to a new location. This triggers for each location it is dragged into not just the ending location. +
        sort
        +
        Fires when the item is dragged.
        +
        revert
        +
        Fires once the object has moved if the new location is invalid.
        +
        relocate
        +
        Only fires once when the item is done bing moved at its final location.
        ## Known Bugs @@ -163,8 +178,3 @@ Tested with: Firefox, Chrome This work is licensed under the MIT License. Which means you can do pretty much whatever you want with it. - -Nonetheless if this plugin saved you money, saved you time or saved your life please take a moment to think about the work I've been doing for you and consider sharing a bit of your joy with me. Your donation, however small, will be greatly appreciated. -Thank you. - -[Donate with PayPal](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) diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..467d2a5 --- /dev/null +++ b/bower.json @@ -0,0 +1,6 @@ +{ + "name": "nestedSortable", + "version": "2.0", + "main": "", + "license": "MIT" +} diff --git a/example.html b/example.html new file mode 100644 index 0000000..a358947 --- /dev/null +++ b/example.html @@ -0,0 +1,451 @@ + + + + + nestedSortable jQuery Plugin + + + + + + + + + + + + +
        +

        nestedSortable jQuery Plugin

        + +

        2.0

        +
        + +
        +

        This is the demo page for the nestedSortable jQuery plugin.

        + +

        Follow the development, read the docs and download the + latest version directly from the GitHub + page.

        +
        + +
        +
          + +
            +
          + + +
        + +

        Try the custom methods:

        + +


        +

        +
        +		
        + +

        +
        +		
        + +

        +
        +		
        + +

        Note: This demo has the maxLevels option set to '4'.

        +
        + +
        +

        License

        + +

        This work is licensed under the MIT License.
        + Which means you can do pretty much whatever you want with it.

        + +

        © 2010‐2014 Manuele J Sarfatti

        +
        + + diff --git a/jquery.mjs.nestedSortable.js b/jquery.mjs.nestedSortable.js index 72ed56b..3f89e94 100644 --- a/jquery.mjs.nestedSortable.js +++ b/jquery.mjs.nestedSortable.js @@ -1,17 +1,32 @@ /* * jQuery UI Nested Sortable - * v 2.0 / 29 oct 2012 - * http://mjsarfatti.com/sandbox/nestedSortable + * v 2.0.0 / 2016-03-30 "Not April Fools" + * https://github.com/ilikenwf/nestedSortable * * Depends on: * jquery.ui.sortable.js 1.10+ * - * Copyright (c) 2010-2013 Manuele J Sarfatti + * Copyright (c) 2010-2016 Manuele J Sarfatti and contributors * Licensed under the MIT License * http://www.opensource.org/licenses/mit-license.php */ +(function( factory ) { + "use strict"; -(function($) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ + "jquery", + "jquery-ui/sortable" + ], factory ); + } else { + + // Browser globals + factory( window.jQuery ); + } +}(function($) { + "use strict"; function isOverAxis( x, reference, size ) { return ( x > reference ) && ( x < ( reference + size ) ); @@ -20,11 +35,12 @@ $.widget("mjs.nestedSortable", $.extend({}, $.ui.sortable.prototype, { options: { + disableParentChange: false, doNotClear: false, expandOnHover: 700, - isAllowed: function(placeholder, placeholderParent, originalItem) { return true; }, + isAllowed: function() { return true; }, isTree: false, - listType: 'ol', + listType: "ol", maxLevels: 0, protectRoot: false, rootID: null, @@ -32,43 +48,61 @@ startCollapsed: false, tabSize: 20, - branchClass: 'mjs-nestedSortable-branch', - collapsedClass: 'mjs-nestedSortable-collapsed', - disableNestingClass: 'mjs-nestedSortable-no-nesting', - errorClass: 'mjs-nestedSortable-error', - expandedClass: 'mjs-nestedSortable-expanded', - hoveringClass: 'mjs-nestedSortable-hovering', - leafClass: 'mjs-nestedSortable-leaf' + branchClass: "mjs-nestedSortable-branch", + collapsedClass: "mjs-nestedSortable-collapsed", + disableNestingClass: "mjs-nestedSortable-no-nesting", + errorClass: "mjs-nestedSortable-error", + expandedClass: "mjs-nestedSortable-expanded", + hoveringClass: "mjs-nestedSortable-hovering", + leafClass: "mjs-nestedSortable-leaf", + disabledClass: "mjs-nestedSortable-disabled" }, _create: function() { - this.element.data('ui-sortable', this.element.data('mjs-nestedSortable')); + var self = this, + err; + + this.element.data("ui-sortable", this.element.data("mjs-nestedSortable")); // mjs - prevent browser from freezing if the HTML is not correct - if (!this.element.is(this.options.listType)) - throw new Error('nestedSortable: Please check that the listType option is set to your actual list type'); + if (!this.element.is(this.options.listType)) { + err = "nestedSortable: " + + "Please check that the listType option is set to your actual list type"; - // mjs - force 'intersect' tolerance method if we have a tree with expanding/collapsing functionality + throw new Error(err); + } + + // if we have a tree with expanding/collapsing functionality, + // force 'intersect' tolerance method if (this.options.isTree && this.options.expandOnHover) { - this.options.tolerance = 'intersect'; + this.options.tolerance = "intersect"; } $.ui.sortable.prototype._create.apply(this, arguments); - // mjs - prepare the tree by applying the right classes (the CSS is responsible for actual hide/show functionality) + // prepare the tree by applying the right classes + // (the CSS is responsible for actual hide/show functionality) if (this.options.isTree) { - var self = this; $(this.items).each(function() { - var $li = this.item; + var $li = this.item, + hasCollapsedClass = $li.hasClass(self.options.collapsedClass), + hasExpandedClass = $li.hasClass(self.options.expandedClass); + if ($li.children(self.options.listType).length) { $li.addClass(self.options.branchClass); // expand/collapse class only if they have children - if (self.options.startCollapsed) $li.addClass(self.options.collapsedClass); - else $li.addClass(self.options.expandedClass); + + if ( !hasCollapsedClass && !hasExpandedClass ) { + if (self.options.startCollapsed) { + $li.addClass(self.options.collapsedClass); + } else { + $li.addClass(self.options.expandedClass); + } + } } else { $li.addClass(self.options.leafClass); } - }) + }); } }, @@ -80,9 +114,26 @@ }, _mouseDrag: function(event) { - var i, item, itemElement, intersection, + var i, + item, + itemElement, + intersection, + self = this, o = this.options, - scrolled = false; + scrolled = false, + $document = $(document), + previousTopOffset, + parentItem, + level, + childLevels, + itemAfter, + itemBefore, + newList, + method, + a, + previousItem, + nextItem, + helperIsNotSibling; //Compute the helpers position this.position = this._generatePosition(event); @@ -93,53 +144,106 @@ } //Do scrolling - if(this.options.scroll) { - if(this.scrollParent[0] != document && this.scrollParent[0].tagName != 'HTML') { - - if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { - this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; - } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) { - this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; + if (this.options.scroll) { + if (this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { + + if ( + ( + this.overflowOffset.top + + this.scrollParent[0].offsetHeight + ) - + event.pageY < + o.scrollSensitivity + ) { + scrolled = this.scrollParent.scrollTop() + o.scrollSpeed; + this.scrollParent.scrollTop(scrolled); + } else if ( + event.pageY - + this.overflowOffset.top < + o.scrollSensitivity + ) { + scrolled = this.scrollParent.scrollTop() - o.scrollSpeed; + this.scrollParent.scrollTop(scrolled); } - if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { - this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; - } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) { - this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; + if ( + ( + this.overflowOffset.left + + this.scrollParent[0].offsetWidth + ) - + event.pageX < + o.scrollSensitivity + ) { + scrolled = this.scrollParent.scrollLeft() + o.scrollSpeed; + this.scrollParent.scrollLeft(scrolled); + } else if ( + event.pageX - + this.overflowOffset.left < + o.scrollSensitivity + ) { + scrolled = this.scrollParent.scrollLeft() - o.scrollSpeed; + this.scrollParent.scrollLeft(scrolled); } } else { - if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); - } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); + if ( + event.pageY - + $document.scrollTop() < + o.scrollSensitivity + ) { + scrolled = $document.scrollTop() - o.scrollSpeed; + $document.scrollTop(scrolled); + } else if ( + $(window).height() - + ( + event.pageY - + $document.scrollTop() + ) < + o.scrollSensitivity + ) { + scrolled = $document.scrollTop() + o.scrollSpeed; + $document.scrollTop(scrolled); } - if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); - } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); + if ( + event.pageX - + $document.scrollLeft() < + o.scrollSensitivity + ) { + scrolled = $document.scrollLeft() - o.scrollSpeed; + $document.scrollLeft(scrolled); + } else if ( + $(window).width() - + ( + event.pageX - + $document.scrollLeft() + ) < + o.scrollSensitivity + ) { + scrolled = $document.scrollLeft() + o.scrollSpeed; + $document.scrollLeft(scrolled); } } - if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) + if (scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { $.ui.ddmanager.prepareOffsets(this, event); + } } //Regenerate the absolute position used for position checks this.positionAbs = this._convertPositionTo("absolute"); // mjs - find the top offset before rearrangement, - var previousTopOffset = this.placeholder.offset().top; + 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 !== "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"; + if (!this.options.axis || this.options.axis !== "x") { + this.helper[0].style.top = (this.position.top) + "px"; } // mjs - check and reset hovering state at each cycle @@ -147,14 +251,16 @@ this.mouseentered = this.mouseentered ? this.mouseentered : false; // mjs - let's start caching some variables - 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); + (function() { + var _parentItem = this.placeholder.parent().parent(); + if (_parentItem && _parentItem.closest(".ui-sortable").length) { + parentItem = _parentItem; + } + }.call(this)); - var newList = document.createElement(o.listType); + level = this._getLevel(this.placeholder); + childLevels = this._getChildLevels(this.helper); + newList = document.createElement(o.listType); //Rearrange for (i = this.items.length - 1; i >= 0; i--) { @@ -178,60 +284,107 @@ continue; } + // No action if intersected item is disabled + // and the element above or below in the direction we're going is also disabled + if (itemElement.className.indexOf(o.disabledClass) !== -1) { + // Note: intersection hardcoded direction values from + // jquery.ui.sortable.js:_intersectsWithPointer + if (intersection === 2) { + // Going down + itemAfter = this.items[i + 1]; + if (itemAfter && itemAfter.item.hasClass(o.disabledClass)) { + continue; + } + + } else if (intersection === 1) { + // Going up + itemBefore = this.items[i - 1]; + if (itemBefore && itemBefore.item.hasClass(o.disabledClass)) { + continue; + } + } + } + + method = intersection === 1 ? "next" : "prev"; + // cannot intersect with itself // no useless actions that have been done before // no action if the item moved is the parent of the item checked if (itemElement !== this.currentItem[0] && - this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement && + this.placeholder[method]()[0] !== itemElement && !$.contains(this.placeholder[0], itemElement) && - (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true) + ( + this.options.type === "semi-dynamic" ? + !$.contains(this.element[0], itemElement) : + true + ) ) { - // mjs - we are intersecting an element: trigger the mouseenter event and store this state + // mjs - we are intersecting an element: + // trigger the mouseenter event and store this state if (!this.mouseentered) { $(itemElement).mouseenter(); this.mouseentered = true; } - // mjs - if the element has children and they are hidden, show them after a delay (CSS responsible) + // mjs - if the element has children and they are hidden, + // show them after a delay (CSS responsible) if (o.isTree && $(itemElement).hasClass(o.collapsedClass) && o.expandOnHover) { if (!this.hovering) { $(itemElement).addClass(o.hoveringClass); - var self = this; this.hovering = window.setTimeout(function() { - $(itemElement).removeClass(o.collapsedClass).addClass(o.expandedClass); + $(itemElement) + .removeClass(o.collapsedClass) + .addClass(o.expandedClass); + self.refreshPositions(); self._trigger("expand", event, self._uiHash()); }, o.expandOnHover); } } - this.direction = intersection == 1 ? "down" : "up"; + this.direction = intersection === 1 ? "down" : "up"; // mjs - rearrange the elements and reset timeouts and hovering state - if (this.options.tolerance == "pointer" || this._intersectsWithSides(item)) { + if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { $(itemElement).mouseleave(); this.mouseentered = false; $(itemElement).removeClass(o.hoveringClass); - this.hovering && window.clearTimeout(this.hovering); + if (this.hovering) { + window.clearTimeout(this.hovering); + } this.hovering = null; - // mjs - do not switch container if it's a root item and 'protectRoot' is true + // mjs - do not switch container if + // it's a root item and 'protectRoot' is true // or if it's not a root item but we are trying to make it root - if (o.protectRoot - && ! (this.currentItem[0].parentNode == this.element[0] // it's a root item - && itemElement.parentNode != this.element[0]) // it's intersecting a non-root item + if (o.protectRoot && + !( + this.currentItem[0].parentNode === this.element[0] && + // it's a root item + itemElement.parentNode !== this.element[0] + // it's intersecting a non-root item + ) ) { - if (this.currentItem[0].parentNode != this.element[0] - && itemElement.parentNode == this.element[0] + if (this.currentItem[0].parentNode !== this.element[0] && + itemElement.parentNode === this.element[0] ) { - if ( ! $(itemElement).children(o.listType).length) { + if ( !$(itemElement).children(o.listType).length) { itemElement.appendChild(newList); - o.isTree && $(itemElement).removeClass(o.leafClass).addClass(o.branchClass + ' ' + o.expandedClass); + if (o.isTree) { + $(itemElement) + .removeClass(o.leafClass) + .addClass(o.branchClass + " " + o.expandedClass); + } + } + + if (this.direction === "down") { + a = $(itemElement).prev().children(o.listType); + } else { + a = $(itemElement).children(o.listType); } - var a = this.direction === "down" ? $(itemElement).prev().children(o.listType) : $(itemElement).children(o.listType); if (a[0] !== undefined) { this._rearrange(event, null, a); } @@ -239,7 +392,7 @@ } else { this._rearrange(event, item); } - } else if ( ! o.protectRoot) { + } else if (!o.protectRoot) { this._rearrange(event, item); } } else { @@ -254,10 +407,24 @@ } } - // mjs - 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; + // mjs - to find the previous sibling in the list, + // keep backtracking until we hit a valid list item. + (function() { + var _previousItem = this.placeholder.prev(); + if (_previousItem.length) { + previousItem = _previousItem; + } else { + previousItem = null; + } + }.call(this)); + if (previousItem != null) { - while (previousItem[0].nodeName.toLowerCase() != 'li' || previousItem[0] == this.currentItem[0] || previousItem[0] == this.helper[0]) { + while ( + previousItem[0].nodeName.toLowerCase() !== "li" || + previousItem[0].className.indexOf(o.disabledClass) !== -1 || + previousItem[0] === this.currentItem[0] || + previousItem[0] === this.helper[0] + ) { if (previousItem[0].previousSibling) { previousItem = $(previousItem[0].previousSibling); } else { @@ -267,10 +434,24 @@ } } - // mjs - 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; + // mjs - to find the next sibling in the list, + // keep stepping forward until we hit a valid list item. + (function() { + var _nextItem = this.placeholder.next(); + if (_nextItem.length) { + nextItem = _nextItem; + } else { + nextItem = null; + } + }.call(this)); + if (nextItem != null) { - while (nextItem[0].nodeName.toLowerCase() != 'li' || nextItem[0] == this.currentItem[0] || nextItem[0] == this.helper[0]) { + while ( + nextItem[0].nodeName.toLowerCase() !== "li" || + nextItem[0].className.indexOf(o.disabledClass) !== -1 || + nextItem[0] === this.currentItem[0] || + nextItem[0] === this.helper[0] + ) { if (nextItem[0].nextSibling) { nextItem = $(nextItem[0].nextSibling); } else { @@ -282,75 +463,101 @@ this.beyondMaxLevels = 0; - // mjs - if the item is moved to the left, send it one level up but only if it's at the bottom of the list - if (parentItem != null - && nextItem == null - && ! (o.protectRoot && parentItem[0].parentNode == this.element[0]) - && - (o.rtl && (this.positionAbs.left + this.helper.outerWidth() > parentItem.offset().left + parentItem.outerWidth()) - || ! o.rtl && (this.positionAbs.left < parentItem.offset().left)) + // mjs - if the item is moved to the left, send it one level up + // but only if it's at the bottom of the list + if (parentItem != null && + nextItem == null && + !(o.protectRoot && parentItem[0].parentNode == this.element[0]) && + ( + 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]); - if (o.isTree && parentItem.children(o.listItem).children('li:visible:not(.ui-sortable-helper)').length < 1) { - parentItem.removeClass(this.options.branchClass + ' ' + this.options.expandedClass) - .addClass(this.options.leafClass); + helperIsNotSibling = !parentItem + .children(o.listItem) + .children("li:visible:not(.ui-sortable-helper)") + .length; + if (o.isTree && helperIsNotSibling) { + parentItem + .removeClass(this.options.branchClass + " " + this.options.expandedClass) + .addClass(this.options.leafClass); } - this._clearEmpty(parentItem[0]); + if(typeof parentItem !== 'undefined') + this._clearEmpty(parentItem[0]); this._trigger("change", event, this._uiHash()); - } - // mjs - if the item is below a sibling and is moved to the right, make it a child of that sibling - else if (previousItem != null - && ! previousItem.hasClass(o.disableNestingClass) - && - (previousItem.children(o.listType).length && previousItem.children(o.listType).is(':visible') - || ! previousItem.children(o.listType).length) - && ! (o.protectRoot && this.currentItem[0].parentNode == this.element[0]) - && - (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)) + // mjs - if the item is below a sibling and is moved to the right, + // make it a child of that sibling + } else if (previousItem != null && + !previousItem.hasClass(o.disableNestingClass) && + ( + previousItem.children(o.listType).length && + previousItem.children(o.listType).is(":visible") || + !previousItem.children(o.listType).length + ) && + !(o.protectRoot && this.currentItem[0].parentNode === this.element[0]) && + ( + 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); + this._isAllowed(previousItem, level, level + childLevels + 1); if (!previousItem.children(o.listType).length) { previousItem[0].appendChild(newList); - o.isTree && previousItem.removeClass(o.leafClass).addClass(o.branchClass + ' ' + o.expandedClass); + if (o.isTree) { + previousItem + .removeClass(o.leafClass) + .addClass(o.branchClass + " " + o.expandedClass); + } } - // mjs - 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); - } - // mjs - otherwise, add it to the bottom of the list. - else { + // mjs - 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); + } else { + // mjs - otherwise, add it to the bottom of the list. previousItem.children(o.listType)[0].appendChild(this.placeholder[0]); } - + if(typeof parentItem !== 'undefined') + this._clearEmpty(parentItem[0]); this._trigger("change", event, this._uiHash()); - } - else { - this._isAllowed(parentItem, level, level+childLevels); + } else { + this._isAllowed(parentItem, level, level + childLevels); } //Post events to containers this._contactContainers(event); //Interconnect with droppables - if($.ui.ddmanager) { + if ($.ui.ddmanager) { $.ui.ddmanager.drag(this, event); } //Call callbacks - this._trigger('sort', event, this._uiHash()); + this._trigger("sort", event, this._uiHash()); this.lastPositionAbs = this.positionAbs; return false; }, - _mouseStop: function(event, noPropagation) { - + _mouseStop: function(event) { // mjs - if the item is in a position not allowed, send it back if (this.beyondMaxLevels) { @@ -366,39 +573,63 @@ } - // mjs - clear the hovering timeout, just to be sure - $('.'+this.options.hoveringClass).mouseleave().removeClass(this.options.hoveringClass); + $("." + this.options.hoveringClass) + .mouseleave() + .removeClass(this.options.hoveringClass); + this.mouseentered = false; - this.hovering && window.clearTimeout(this.hovering); + if (this.hovering) { + window.clearTimeout(this.hovering); + } this.hovering = null; - $.ui.sortable.prototype._mouseStop.apply(this, arguments); - + this._relocate_event = event; + this._pid_current = $(this.domPosition.parent).parent().attr("id"); + this._sort_current = this.domPosition.prev ? $(this.domPosition.prev).next().index() : 0; + $.ui.sortable.prototype._mouseStop.apply(this, arguments); //asybnchronous execution, @see _clear for the relocate event. }, - // mjs - this function is slightly modified to make it easier to hover over a collapsed element and have it expand + // mjs - this function is slightly modified + // to make it easier to hover over a collapsed element and have it expand _intersectsWithSides: function(item) { - var half = this.options.isTree ? .8 : .5; - - var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height*half), item.height), - isOverTopHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top - (item.height*half), item.height), - isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width), + var half = this.options.isTree ? .8 : .5, + isOverBottomHalf = isOverAxis( + this.positionAbs.top + this.offset.click.top, + item.top + (item.height * half), + item.height + ), + isOverTopHalf = isOverAxis( + this.positionAbs.top + this.offset.click.top, + item.top - (item.height * half), + item.height + ), + isOverRightHalf = isOverAxis( + this.positionAbs.left + this.offset.click.left, + item.left + (item.width / 2), + item.width + ), verticalDirection = this._getDragVerticalDirection(), horizontalDirection = this._getDragHorizontalDirection(); if (this.floating && horizontalDirection) { - return ((horizontalDirection == "right" && isOverRightHalf) || (horizontalDirection == "left" && !isOverRightHalf)); + return ( + (horizontalDirection === "right" && isOverRightHalf) || + (horizontalDirection === "left" && !isOverRightHalf) + ); } else { - return verticalDirection && ((verticalDirection == "down" && isOverBottomHalf) || (verticalDirection == "up" && isOverTopHalf)); + return verticalDirection && ( + (verticalDirection === "down" && isOverBottomHalf) || + (verticalDirection === "up" && isOverTopHalf) + ); } }, - _contactContainers: function(event) { + _contactContainers: function() { - if (this.options.protectRoot && this.currentItem[0].parentNode == this.element[0] ) { + if (this.options.protectRoot && this.currentItem[0].parentNode === this.element[0] ) { return; } @@ -406,13 +637,21 @@ }, - _clear: function(event, noPropagation) { + _clear: function() { + var i, + item; $.ui.sortable.prototype._clear.apply(this, arguments); + //relocate event + if (!(this._pid_current === this._uiHash().item.parent().parent().attr("id") && + this._sort_current === this._uiHash().item.index())) { + this._trigger("relocate", this._relocate_event, this._uiHash()); + } + // mjs - clean last empty ul/ol - for (var i = this.items.length - 1; i >= 0; i--) { - var item = this.items[i].item[0]; + for (i = this.items.length - 1; i >= 0; i--) { + item = this.items[i].item[0]; this._clearEmpty(item); } @@ -422,38 +661,42 @@ var o = $.extend({}, this.options, options), items = this._getItemsAsjQuery(o && o.connected), - str = []; + str = []; $(items).each(function() { - var res = ($(o.item || this).attr(o.attribute || 'id') || '') + var res = ($(o.item || this).attr(o.attribute || "id") || "") .match(o.expression || (/(.+)[-=_](.+)/)), - pid = ($(o.item || this).parent(o.listType) + pid = ($(o.item || this).parent(o.listType) .parent(o.items) - .attr(o.attribute || 'id') || '') + .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]) : o.rootID)); + str.push( + ( + (o.key || res[1]) + + "[" + + (o.key && o.expression ? res[1] : res[2]) + "]" + ) + + "=" + + (pid ? (o.key && o.expression ? pid[1] : pid[2]) : o.rootID)); } }); - if(!str.length && o.key) { - str.push(o.key + '='); + if (!str.length && o.key) { + str.push(o.key + "="); } - return str.join('&'); + return str.join("&"); }, toHierarchy: function(options) { var o = $.extend({}, this.options, options), - sDepth = o.startDepthCount || 0, - ret = []; + ret = []; - $(this.element).children(o.items).each(function () { + $(this.element).children(o.items).each(function() { var level = _recursiveItems(this); ret.push(level); }); @@ -461,9 +704,21 @@ return ret; function _recursiveItems(item) { - var id = ($(item).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/)); + var id = ($(item).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[-=_](.+)/)), + currentItem; + + var data = $(item).data(); + if (data.nestedSortableItem) { + delete data.nestedSortableItem; // Remove the nestedSortableItem object from the data + } + if (id) { - var currentItem = {"id" : id[2]}; + currentItem = { + "id": id[2] + }; + + currentItem = $.extend({}, currentItem, data); // Combine the two objects + if ($(item).children(o.listType).children(o.items).length > 0) { currentItem.children = []; $(item).children(o.listType).children(o.items).each(function() { @@ -480,8 +735,8 @@ var o = $.extend({}, this.options, options), sDepth = o.startDepthCount || 0, - ret = [], - left = 1; + ret = [], + left = 1; if (!o.excludeRoot) { ret.push({ @@ -491,77 +746,101 @@ "left": left, "right": ($(o.items, this.element).length + 1) * 2 }); - left++ + left++; } - $(this.element).children(o.items).each(function () { - left = _recursiveArray(this, sDepth + 1, left); + $(this.element).children(o.items).each(function() { + left = _recursiveArray(this, sDepth, left); }); - ret = ret.sort(function(a,b){ return (a.left - b.left); }); + ret = ret.sort(function(a, b) { return (a.left - b.left); }); return ret; - function _recursiveArray(item, depth, left) { + function _recursiveArray(item, depth, _left) { - var right = left + 1, - id, - pid; + var right = _left + 1, + id, + pid, + parentItem; if ($(item).children(o.listType).children(o.items).length > 0) { - depth ++; - $(item).children(o.listType).children(o.items).each(function () { + depth++; + $(item).children(o.listType).children(o.items).each(function() { right = _recursiveArray($(this), depth, right); }); - depth --; + depth--; } - id = ($(item).attr(o.attribute || 'id')).match(o.expression || (/(.+)[-=_](.+)/)); + id = ($(item).attr(o.attribute || "id")).match(o.expression || (/(.+)[-=_](.+)/)); - if (depth === sDepth + 1) { + if (depth === sDepth) { pid = o.rootID; } else { - var parentItem = ($(item).parent(o.listType) - .parent(o.items) - .attr(o.attribute || 'id')) - .match(o.expression || (/(.+)[-=_](.+)/)); + parentItem = ($(item).parent(o.listType) + .parent(o.items) + .attr(o.attribute || "id")) + .match(o.expression || (/(.+)[-=_](.+)/)); pid = parentItem[2]; } if (id) { - ret.push({"item_id": id[2], "parent_id": pid, "depth": depth, "left": left, "right": right}); + var name = $(item).data("name"); + ret.push({ + "id": id[2], + "parent_id": pid, + "depth": depth, + "left": _left, + "right": right, + "name":name + }); } - left = right + 1; - return left; + _left = right + 1; + return _left; } }, - _clearEmpty: function(item) { - var o = this.options; + _clearEmpty: function (item) { + function replaceClass(elem, search, replace, swap) { + if (swap) { + search = [replace, replace = search][0]; + } - var emptyList = $(item).children(o.listType); + $(elem).removeClass(search).addClass(replace); + } + + var o = this.options, + childrenList = $(item).children(o.listType), + hasChildren = childrenList.is(':not(:empty)'); - if (emptyList.length && !emptyList.children().length && !o.doNotClear) { - o.isTree && $(item).removeClass(o.branchClass + ' ' + o.expandedClass).addClass(o.leafClass); - emptyList.remove(); - } else if (o.isTree && emptyList.length && emptyList.children().length && emptyList.is(':visible')) { - $(item).removeClass(o.leafClass).addClass(o.branchClass + ' ' + o.expandedClass); - } else if (o.isTree && emptyList.length && emptyList.children().length && !emptyList.is(':visible')) { - $(item).removeClass(o.leafClass).addClass(o.branchClass + ' ' + o.collapsedClass); + var doNotClear = + o.doNotClear || + hasChildren || + o.protectRoot && $(item)[0] === this.element[0]; + + if (o.isTree) { + replaceClass(item, o.branchClass, o.leafClass, doNotClear); + + if (doNotClear && hasChildren) { + replaceClass(item, o.collapsedClass, o.expandedClass); + } } + if (!doNotClear) { + childrenList.remove(); + } }, _getLevel: function(item) { - var level = 1; + var level = 1, + list; if (this.options.listType) { - var list = item.closest(this.options.listType); - while (list && list.length > 0 && - !list.is('.ui-sortable')) { + list = item.closest(this.options.listType); + while (list && list.length > 0 && !list.is(".ui-sortable")) { level++; list = list.parent().closest(this.options.listType); } @@ -572,12 +851,12 @@ _getChildLevels: function(parent, depth) { var self = this, - o = this.options, - result = 0; + o = this.options, + result = 0; depth = depth || 0; - $(parent).children(o.listType).children(o.items).each(function (index, child) { - result = Math.max(self._getChildLevels(child, depth + 1), result); + $(parent).children(o.listType).children(o.items).each(function(index, child) { + result = Math.max(self._getChildLevels(child, depth + 1), result); }); return depth ? result + 1 : result; @@ -585,19 +864,33 @@ _isAllowed: function(parentItem, level, levels) { var o = this.options, - maxLevels = this.placeholder.closest('.ui-sortable').nestedSortable('option', 'maxLevels'); // this takes into account the maxLevels set to the recipient list - + // this takes into account the maxLevels set to the recipient list + maxLevels = this + .placeholder + .closest(".ui-sortable") + .nestedSortable("option", "maxLevels"), + + // Check if the parent has changed to prevent it, when o.disableParentChange is true + oldParent = this.currentItem.parent().parent(), + disabledByParentchange = o.disableParentChange && ( + //From somewhere to somewhere else, except the root + typeof parentItem !== 'undefined' && !oldParent.is(parentItem) || + typeof parentItem === 'undefined' && oldParent.is("li") //From somewhere to the root + ); // mjs - is the root protected? // mjs - are we nesting too deep? - if ( ! o.isAllowed(this.placeholder, parentItem, this.currentItem)) { - this.placeholder.addClass(o.errorClass); - if (maxLevels < levels && maxLevels != 0) { - this.beyondMaxLevels = levels - maxLevels; - } else { - this.beyondMaxLevels = 1; - } + if ( + disabledByParentchange || + !o.isAllowed(this.placeholder, parentItem, this.currentItem) + ) { + this.placeholder.addClass(o.errorClass); + if (maxLevels < levels && maxLevels !== 0) { + this.beyondMaxLevels = levels - maxLevels; + } else { + this.beyondMaxLevels = 1; + } } else { - if (maxLevels < levels && maxLevels != 0) { + if (maxLevels < levels && maxLevels !== 0) { this.placeholder.addClass(o.errorClass); this.beyondMaxLevels = levels - maxLevels; } else { @@ -609,5 +902,9 @@ })); - $.mjs.nestedSortable.prototype.options = $.extend({}, $.ui.sortable.prototype.options, $.mjs.nestedSortable.prototype.options); -})(jQuery); + $.mjs.nestedSortable.prototype.options = $.extend( + {}, + $.ui.sortable.prototype.options, + $.mjs.nestedSortable.prototype.options + ); +})); diff --git a/meteor/nested-sortable-tests.js b/meteor/nested-sortable-tests.js new file mode 100644 index 0000000..e69de29 diff --git a/package.js b/package.js new file mode 100644 index 0000000..d554b10 --- /dev/null +++ b/package.js @@ -0,0 +1,28 @@ +Package.describe({ + name: 'ilikenwf:nested-sortable', + version: '0.0.1', + // Brief, one-line summary of the package. + summary: 'A jQuery plugin that extends Sortable UI functionalities to nested lists.', + // URL to the Git repository containing the source code for this package. + git: 'https://github.com/ilikenwf/nestedSortable', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md' +}); + +Package.onUse(function(api) { + api.versionsFrom('1.1.0.2'); + + api.use('jquery', 'client'); + api.use('mizzao:jquery-ui', 'client'); + + api.imply('jquery', 'client'); + + api.addFiles('jquery.mjs.nestedSortable.js', 'client'); +}); + +Package.onTest(function(api) { + api.use('tinytest'); + api.use('ilikenwf:nested-sortable'); + api.addFiles('meteor/nested-sortable-tests.js'); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..d739765 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "nestedSortable", + "version": "1.3.4", + "author": "Manuele J Sarfatti ", + "description": "NestedSortable is a jQuery plugin that extends jQuery Sortable UI functionalities to nested lists.", + "repository": { + "type": "git", + "url": "https://github.com/ilikenwf/nestedSortable" + }, + "main": "jquery.mjs.nestedSortable.js" +} \ No newline at end of file