design/js/jquery-sortable.js
changeset 121 21ac67ebf9e7
equal deleted inserted replaced
120:892980a3af09 121:21ac67ebf9e7
       
     1 /* ===================================================
       
     2  *  jquery-sortable.js v0.9.13
       
     3  *  http://johnny.github.com/jquery-sortable/
       
     4  * ===================================================
       
     5  *  Copyright (c) 2012 Jonas von Andrian
       
     6  *  All rights reserved.
       
     7  *
       
     8  *  Redistribution and use in source and binary forms, with or without
       
     9  *  modification, are permitted provided that the following conditions are met:
       
    10  *  * Redistributions of source code must retain the above copyright
       
    11  *    notice, this list of conditions and the following disclaimer.
       
    12  *  * Redistributions in binary form must reproduce the above copyright
       
    13  *    notice, this list of conditions and the following disclaimer in the
       
    14  *    documentation and/or other materials provided with the distribution.
       
    15  *  * The name of the author may not be used to endorse or promote products
       
    16  *    derived from this software without specific prior written permission.
       
    17  *
       
    18  *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
       
    19  *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
    20  *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
       
    21  *  DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
       
    22  *  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
       
    23  *  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
       
    24  *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
       
    25  *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
       
    26  *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
       
    27  *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    28  * ========================================================== */
       
    29 
       
    30 
       
    31 !function ( $, window, pluginName, undefined){
       
    32   var containerDefaults = {
       
    33     // If true, items can be dragged from this container
       
    34     drag: true,
       
    35     // If true, items can be droped onto this container
       
    36     drop: true,
       
    37     // Exclude items from being draggable, if the
       
    38     // selector matches the item
       
    39     exclude: "",
       
    40     // If true, search for nested containers within an item.If you nest containers,
       
    41     // either the original selector with which you call the plugin must only match the top containers,
       
    42     // or you need to specify a group (see the bootstrap nav example)
       
    43     nested: true,
       
    44     // If true, the items are assumed to be arranged vertically
       
    45     vertical: true
       
    46   }, // end container defaults
       
    47   groupDefaults = {
       
    48     // This is executed after the placeholder has been moved.
       
    49     // $closestItemOrContainer contains the closest item, the placeholder
       
    50     // has been put at or the closest empty Container, the placeholder has
       
    51     // been appended to.
       
    52     afterMove: function ($placeholder, container, $closestItemOrContainer) {
       
    53     },
       
    54     // The exact css path between the container and its items, e.g. "> tbody"
       
    55     containerPath: "",
       
    56     // The css selector of the containers
       
    57     containerSelector: "ol, ul",
       
    58     // Distance the mouse has to travel to start dragging
       
    59     distance: 0,
       
    60     // Time in milliseconds after mousedown until dragging should start.
       
    61     // This option can be used to prevent unwanted drags when clicking on an element.
       
    62     delay: 0,
       
    63     // The css selector of the drag handle
       
    64     handle: "",
       
    65     // The exact css path between the item and its subcontainers.
       
    66     // It should only match the immediate items of a container.
       
    67     // No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div"
       
    68     itemPath: "",
       
    69     // The css selector of the items
       
    70     itemSelector: "li",
       
    71     // The class given to "body" while an item is being dragged
       
    72     bodyClass: "dragging",
       
    73     // The class giving to an item while being dragged
       
    74     draggedClass: "dragged",
       
    75     // Check if the dragged item may be inside the container.
       
    76     // Use with care, since the search for a valid container entails a depth first search
       
    77     // and may be quite expensive.
       
    78     isValidTarget: function ($item, container) {
       
    79       return true
       
    80     },
       
    81     // Executed before onDrop if placeholder is detached.
       
    82     // This happens if pullPlaceholder is set to false and the drop occurs outside a container.
       
    83     onCancel: function ($item, container, _super, event) {
       
    84     },
       
    85     // Executed at the beginning of a mouse move event.
       
    86     // The Placeholder has not been moved yet.
       
    87     onDrag: function ($item, position, _super, event) {
       
    88       $item.css(position)
       
    89     },
       
    90     // Called after the drag has been started,
       
    91     // that is the mouse button is being held down and
       
    92     // the mouse is moving.
       
    93     // The container is the closest initialized container.
       
    94     // Therefore it might not be the container, that actually contains the item.
       
    95     onDragStart: function ($item, container, _super, event) {
       
    96       $item.css({
       
    97         height: $item.outerHeight(),
       
    98         width: $item.outerWidth()
       
    99       })
       
   100       $item.addClass(container.group.options.draggedClass)
       
   101       $("body").addClass(container.group.options.bodyClass)
       
   102     },
       
   103     // Called when the mouse button is being released
       
   104     onDrop: function ($item, container, _super, event) {
       
   105       $item.removeClass(container.group.options.draggedClass).removeAttr("style")
       
   106       $("body").removeClass(container.group.options.bodyClass)
       
   107     },
       
   108     // Called on mousedown. If falsy value is returned, the dragging will not start.
       
   109     // Ignore if element clicked is input, select or textarea
       
   110     onMousedown: function ($item, _super, event) {
       
   111       if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) {
       
   112         event.preventDefault()
       
   113         return true
       
   114       }
       
   115     },
       
   116     // The class of the placeholder (must match placeholder option markup)
       
   117     placeholderClass: "placeholder",
       
   118     // Template for the placeholder. Can be any valid jQuery input
       
   119     // e.g. a string, a DOM element.
       
   120     // The placeholder must have the class "placeholder"
       
   121     placeholder: '<li class="placeholder"></li>',
       
   122     // If true, the position of the placeholder is calculated on every mousemove.
       
   123     // If false, it is only calculated when the mouse is above a container.
       
   124     pullPlaceholder: true,
       
   125     // Specifies serialization of the container group.
       
   126     // The pair $parent/$children is either container/items or item/subcontainers.
       
   127     serialize: function ($parent, $children, parentIsContainer) {
       
   128       var result = $.extend({}, $parent.data())
       
   129 
       
   130       if(parentIsContainer)
       
   131         return [$children]
       
   132       else if ($children[0]){
       
   133         result.children = $children
       
   134       }
       
   135 
       
   136       delete result.subContainers
       
   137       delete result.sortable
       
   138 
       
   139       return result
       
   140     },
       
   141     // Set tolerance while dragging. Positive values decrease sensitivity,
       
   142     // negative values increase it.
       
   143     tolerance: 0
       
   144   }, // end group defaults
       
   145   containerGroups = {},
       
   146   groupCounter = 0,
       
   147   emptyBox = {
       
   148     left: 0,
       
   149     top: 0,
       
   150     bottom: 0,
       
   151     right:0
       
   152   },
       
   153   eventNames = {
       
   154     start: "touchstart.sortable mousedown.sortable",
       
   155     drop: "touchend.sortable touchcancel.sortable mouseup.sortable",
       
   156     drag: "touchmove.sortable mousemove.sortable",
       
   157     scroll: "scroll.sortable"
       
   158   },
       
   159   subContainerKey = "subContainers"
       
   160 
       
   161   /*
       
   162    * a is Array [left, right, top, bottom]
       
   163    * b is array [left, top]
       
   164    */
       
   165   function d(a,b) {
       
   166     var x = Math.max(0, a[0] - b[0], b[0] - a[1]),
       
   167     y = Math.max(0, a[2] - b[1], b[1] - a[3])
       
   168     return x+y;
       
   169   }
       
   170 
       
   171   function setDimensions(array, dimensions, tolerance, useOffset) {
       
   172     var i = array.length,
       
   173     offsetMethod = useOffset ? "offset" : "position"
       
   174     tolerance = tolerance || 0
       
   175 
       
   176     while(i--){
       
   177       var el = array[i].el ? array[i].el : $(array[i]),
       
   178       // use fitting method
       
   179       pos = el[offsetMethod]()
       
   180       pos.left += parseInt(el.css('margin-left'), 10)
       
   181       pos.top += parseInt(el.css('margin-top'),10)
       
   182       dimensions[i] = [
       
   183         pos.left - tolerance,
       
   184         pos.left + el.outerWidth() + tolerance,
       
   185         pos.top - tolerance,
       
   186         pos.top + el.outerHeight() + tolerance
       
   187       ]
       
   188     }
       
   189   }
       
   190 
       
   191   function getRelativePosition(pointer, element) {
       
   192     var offset = element.offset()
       
   193     return {
       
   194       left: pointer.left - offset.left,
       
   195       top: pointer.top - offset.top
       
   196     }
       
   197   }
       
   198 
       
   199   function sortByDistanceDesc(dimensions, pointer, lastPointer) {
       
   200     pointer = [pointer.left, pointer.top]
       
   201     lastPointer = lastPointer && [lastPointer.left, lastPointer.top]
       
   202 
       
   203     var dim,
       
   204     i = dimensions.length,
       
   205     distances = []
       
   206 
       
   207     while(i--){
       
   208       dim = dimensions[i]
       
   209       distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)]
       
   210     }
       
   211     distances = distances.sort(function  (a,b) {
       
   212       return b[1] - a[1] || b[2] - a[2] || b[0] - a[0]
       
   213     })
       
   214 
       
   215     // last entry is the closest
       
   216     return distances
       
   217   }
       
   218 
       
   219   function ContainerGroup(options) {
       
   220     this.options = $.extend({}, groupDefaults, options)
       
   221     this.containers = []
       
   222 
       
   223     if(!this.options.rootGroup){
       
   224       this.scrollProxy = $.proxy(this.scroll, this)
       
   225       this.dragProxy = $.proxy(this.drag, this)
       
   226       this.dropProxy = $.proxy(this.drop, this)
       
   227       this.placeholder = $(this.options.placeholder)
       
   228 
       
   229       if(!options.isValidTarget)
       
   230         this.options.isValidTarget = undefined
       
   231     }
       
   232   }
       
   233 
       
   234   ContainerGroup.get = function  (options) {
       
   235     if(!containerGroups[options.group]) {
       
   236       if(options.group === undefined)
       
   237         options.group = groupCounter ++
       
   238 
       
   239       containerGroups[options.group] = new ContainerGroup(options)
       
   240     }
       
   241 
       
   242     return containerGroups[options.group]
       
   243   }
       
   244 
       
   245   ContainerGroup.prototype = {
       
   246     dragInit: function  (e, itemContainer) {
       
   247       this.$document = $(itemContainer.el[0].ownerDocument)
       
   248 
       
   249       // get item to drag
       
   250       var closestItem = $(e.target).closest(this.options.itemSelector);
       
   251       // using the length of this item, prevents the plugin from being started if there is no handle being clicked on.
       
   252       // this may also be helpful in instantiating multidrag.
       
   253       if (closestItem.length) {
       
   254         this.item = closestItem;
       
   255         this.itemContainer = itemContainer;
       
   256         if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) {
       
   257             return;
       
   258         }
       
   259         this.setPointer(e);
       
   260         this.toggleListeners('on');
       
   261         this.setupDelayTimer();
       
   262         this.dragInitDone = true;
       
   263       }
       
   264     },
       
   265     drag: function  (e) {
       
   266       if(!this.dragging){
       
   267         if(!this.distanceMet(e) || !this.delayMet)
       
   268           return
       
   269 
       
   270         this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e)
       
   271         this.item.before(this.placeholder)
       
   272         this.dragging = true
       
   273       }
       
   274 
       
   275       this.setPointer(e)
       
   276       // place item under the cursor
       
   277       this.options.onDrag(this.item,
       
   278                           getRelativePosition(this.pointer, this.item.offsetParent()),
       
   279                           groupDefaults.onDrag,
       
   280                           e)
       
   281 
       
   282       var p = this.getPointer(e),
       
   283       box = this.sameResultBox,
       
   284       t = this.options.tolerance
       
   285 
       
   286       if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left)
       
   287         if(!this.searchValidTarget()){
       
   288           this.placeholder.detach()
       
   289           this.lastAppendedItem = undefined
       
   290         }
       
   291     },
       
   292     drop: function  (e) {
       
   293       this.toggleListeners('off')
       
   294 
       
   295       this.dragInitDone = false
       
   296 
       
   297       if(this.dragging){
       
   298         // processing Drop, check if placeholder is detached
       
   299         if(this.placeholder.closest("html")[0]){
       
   300           this.placeholder.before(this.item).detach()
       
   301         } else {
       
   302           this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e)
       
   303         }
       
   304         this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e)
       
   305 
       
   306         // cleanup
       
   307         this.clearDimensions()
       
   308         this.clearOffsetParent()
       
   309         this.lastAppendedItem = this.sameResultBox = undefined
       
   310         this.dragging = false
       
   311       }
       
   312     },
       
   313     searchValidTarget: function  (pointer, lastPointer) {
       
   314       if(!pointer){
       
   315         pointer = this.relativePointer || this.pointer
       
   316         lastPointer = this.lastRelativePointer || this.lastPointer
       
   317       }
       
   318 
       
   319       var distances = sortByDistanceDesc(this.getContainerDimensions(),
       
   320                                          pointer,
       
   321                                          lastPointer),
       
   322       i = distances.length
       
   323 
       
   324       while(i--){
       
   325         var index = distances[i][0],
       
   326         distance = distances[i][1]
       
   327 
       
   328         if(!distance || this.options.pullPlaceholder){
       
   329           var container = this.containers[index]
       
   330           if(!container.disabled){
       
   331             if(!this.$getOffsetParent()){
       
   332               var offsetParent = container.getItemOffsetParent()
       
   333               pointer = getRelativePosition(pointer, offsetParent)
       
   334               lastPointer = getRelativePosition(lastPointer, offsetParent)
       
   335             }
       
   336             if(container.searchValidTarget(pointer, lastPointer))
       
   337               return true
       
   338           }
       
   339         }
       
   340       }
       
   341       if(this.sameResultBox)
       
   342         this.sameResultBox = undefined
       
   343     },
       
   344     movePlaceholder: function  (container, item, method, sameResultBox) {
       
   345       var lastAppendedItem = this.lastAppendedItem
       
   346       if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0])
       
   347         return;
       
   348 
       
   349       item[method](this.placeholder)
       
   350       this.lastAppendedItem = item
       
   351       this.sameResultBox = sameResultBox
       
   352       this.options.afterMove(this.placeholder, container, item)
       
   353     },
       
   354     getContainerDimensions: function  () {
       
   355       if(!this.containerDimensions)
       
   356         setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent())
       
   357       return this.containerDimensions
       
   358     },
       
   359     getContainer: function  (element) {
       
   360       return element.closest(this.options.containerSelector).data(pluginName)
       
   361     },
       
   362     $getOffsetParent: function  () {
       
   363       if(this.offsetParent === undefined){
       
   364         var i = this.containers.length - 1,
       
   365         offsetParent = this.containers[i].getItemOffsetParent()
       
   366 
       
   367         if(!this.options.rootGroup){
       
   368           while(i--){
       
   369             if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){
       
   370               // If every container has the same offset parent,
       
   371               // use position() which is relative to this parent,
       
   372               // otherwise use offset()
       
   373               // compare #setDimensions
       
   374               offsetParent = false
       
   375               break;
       
   376             }
       
   377           }
       
   378         }
       
   379 
       
   380         this.offsetParent = offsetParent
       
   381       }
       
   382       return this.offsetParent
       
   383     },
       
   384     setPointer: function (e) {
       
   385       var pointer = this.getPointer(e)
       
   386 
       
   387       if(this.$getOffsetParent()){
       
   388         var relativePointer = getRelativePosition(pointer, this.$getOffsetParent())
       
   389         this.lastRelativePointer = this.relativePointer
       
   390         this.relativePointer = relativePointer
       
   391       }
       
   392 
       
   393       this.lastPointer = this.pointer
       
   394       this.pointer = pointer
       
   395     },
       
   396     distanceMet: function (e) {
       
   397       var currentPointer = this.getPointer(e)
       
   398       return (Math.max(
       
   399         Math.abs(this.pointer.left - currentPointer.left),
       
   400         Math.abs(this.pointer.top - currentPointer.top)
       
   401       ) >= this.options.distance)
       
   402     },
       
   403     getPointer: function(e) {
       
   404       var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0]
       
   405       return {
       
   406         left: e.pageX || o.pageX,
       
   407         top: e.pageY || o.pageY
       
   408       }
       
   409     },
       
   410     setupDelayTimer: function () {
       
   411       var that = this
       
   412       this.delayMet = !this.options.delay
       
   413 
       
   414       // init delay timer if needed
       
   415       if (!this.delayMet) {
       
   416         clearTimeout(this._mouseDelayTimer);
       
   417         this._mouseDelayTimer = setTimeout(function() {
       
   418           that.delayMet = true
       
   419         }, this.options.delay)
       
   420       }
       
   421     },
       
   422     scroll: function  (e) {
       
   423       this.clearDimensions()
       
   424       this.clearOffsetParent() // TODO is this needed?
       
   425     },
       
   426     toggleListeners: function (method) {
       
   427       var that = this,
       
   428       events = ['drag','drop','scroll']
       
   429 
       
   430       $.each(events,function  (i,event) {
       
   431         that.$document[method](eventNames[event], that[event + 'Proxy'])
       
   432       })
       
   433     },
       
   434     clearOffsetParent: function () {
       
   435       this.offsetParent = undefined
       
   436     },
       
   437     // Recursively clear container and item dimensions
       
   438     clearDimensions: function  () {
       
   439       this.traverse(function(object){
       
   440         object._clearDimensions()
       
   441       })
       
   442     },
       
   443     traverse: function(callback) {
       
   444       callback(this)
       
   445       var i = this.containers.length
       
   446       while(i--){
       
   447         this.containers[i].traverse(callback)
       
   448       }
       
   449     },
       
   450     _clearDimensions: function(){
       
   451       this.containerDimensions = undefined
       
   452     },
       
   453     _destroy: function () {
       
   454       containerGroups[this.options.group] = undefined
       
   455     }
       
   456   }
       
   457 
       
   458   function Container(element, options) {
       
   459     this.el = element
       
   460     this.options = $.extend( {}, containerDefaults, options)
       
   461 
       
   462     this.group = ContainerGroup.get(this.options)
       
   463     this.rootGroup = this.options.rootGroup || this.group
       
   464     this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector
       
   465 
       
   466     var itemPath = this.rootGroup.options.itemPath
       
   467     this.target = itemPath ? this.el.find(itemPath) : this.el
       
   468 
       
   469     this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this))
       
   470 
       
   471     if(this.options.drop)
       
   472       this.group.containers.push(this)
       
   473   }
       
   474 
       
   475   Container.prototype = {
       
   476     dragInit: function  (e) {
       
   477       var rootGroup = this.rootGroup
       
   478 
       
   479       if( !this.disabled &&
       
   480           !rootGroup.dragInitDone &&
       
   481           this.options.drag &&
       
   482           this.isValidDrag(e)) {
       
   483         rootGroup.dragInit(e, this)
       
   484       }
       
   485     },
       
   486     isValidDrag: function(e) {
       
   487       return e.which == 1 ||
       
   488         e.type == "touchstart" && e.originalEvent.touches.length == 1
       
   489     },
       
   490     searchValidTarget: function  (pointer, lastPointer) {
       
   491       var distances = sortByDistanceDesc(this.getItemDimensions(),
       
   492                                          pointer,
       
   493                                          lastPointer),
       
   494       i = distances.length,
       
   495       rootGroup = this.rootGroup,
       
   496       validTarget = !rootGroup.options.isValidTarget ||
       
   497         rootGroup.options.isValidTarget(rootGroup.item, this)
       
   498 
       
   499       if(!i && validTarget){
       
   500         rootGroup.movePlaceholder(this, this.target, "append")
       
   501         return true
       
   502       } else
       
   503         while(i--){
       
   504           var index = distances[i][0],
       
   505           distance = distances[i][1]
       
   506           if(!distance && this.hasChildGroup(index)){
       
   507             var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer)
       
   508             if(found)
       
   509               return true
       
   510           }
       
   511           else if(validTarget){
       
   512             this.movePlaceholder(index, pointer)
       
   513             return true
       
   514           }
       
   515         }
       
   516     },
       
   517     movePlaceholder: function  (index, pointer) {
       
   518       var item = $(this.items[index]),
       
   519       dim = this.itemDimensions[index],
       
   520       method = "after",
       
   521       width = item.outerWidth(),
       
   522       height = item.outerHeight(),
       
   523       offset = item.offset(),
       
   524       sameResultBox = {
       
   525         left: offset.left,
       
   526         right: offset.left + width,
       
   527         top: offset.top,
       
   528         bottom: offset.top + height
       
   529       }
       
   530       if(this.options.vertical){
       
   531         var yCenter = (dim[2] + dim[3]) / 2,
       
   532         inUpperHalf = pointer.top <= yCenter
       
   533         if(inUpperHalf){
       
   534           method = "before"
       
   535           sameResultBox.bottom -= height / 2
       
   536         } else
       
   537           sameResultBox.top += height / 2
       
   538       } else {
       
   539         var xCenter = (dim[0] + dim[1]) / 2,
       
   540         inLeftHalf = pointer.left <= xCenter
       
   541         if(inLeftHalf){
       
   542           method = "before"
       
   543           sameResultBox.right -= width / 2
       
   544         } else
       
   545           sameResultBox.left += width / 2
       
   546       }
       
   547       if(this.hasChildGroup(index))
       
   548         sameResultBox = emptyBox
       
   549       this.rootGroup.movePlaceholder(this, item, method, sameResultBox)
       
   550     },
       
   551     getItemDimensions: function  () {
       
   552       if(!this.itemDimensions){
       
   553         this.items = this.$getChildren(this.el, "item").filter(
       
   554           ":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")"
       
   555         ).get()
       
   556         setDimensions(this.items, this.itemDimensions = [], this.options.tolerance)
       
   557       }
       
   558       return this.itemDimensions
       
   559     },
       
   560     getItemOffsetParent: function  () {
       
   561       var offsetParent,
       
   562       el = this.el
       
   563       // Since el might be empty we have to check el itself and
       
   564       // can not do something like el.children().first().offsetParent()
       
   565       if(el.css("position") === "relative" || el.css("position") === "absolute"  || el.css("position") === "fixed")
       
   566         offsetParent = el
       
   567       else
       
   568         offsetParent = el.offsetParent()
       
   569       return offsetParent
       
   570     },
       
   571     hasChildGroup: function (index) {
       
   572       return this.options.nested && this.getContainerGroup(index)
       
   573     },
       
   574     getContainerGroup: function  (index) {
       
   575       var childGroup = $.data(this.items[index], subContainerKey)
       
   576       if( childGroup === undefined){
       
   577         var childContainers = this.$getChildren(this.items[index], "container")
       
   578         childGroup = false
       
   579 
       
   580         if(childContainers[0]){
       
   581           var options = $.extend({}, this.options, {
       
   582             rootGroup: this.rootGroup,
       
   583             group: groupCounter ++
       
   584           })
       
   585           childGroup = childContainers[pluginName](options).data(pluginName).group
       
   586         }
       
   587         $.data(this.items[index], subContainerKey, childGroup)
       
   588       }
       
   589       return childGroup
       
   590     },
       
   591     $getChildren: function (parent, type) {
       
   592       var options = this.rootGroup.options,
       
   593       path = options[type + "Path"],
       
   594       selector = options[type + "Selector"]
       
   595 
       
   596       parent = $(parent)
       
   597       if(path)
       
   598         parent = parent.find(path)
       
   599 
       
   600       return parent.children(selector)
       
   601     },
       
   602     _serialize: function (parent, isContainer) {
       
   603       var that = this,
       
   604       childType = isContainer ? "item" : "container",
       
   605 
       
   606       children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () {
       
   607         return that._serialize($(this), !isContainer)
       
   608       }).get()
       
   609 
       
   610       return this.rootGroup.options.serialize(parent, children, isContainer)
       
   611     },
       
   612     traverse: function(callback) {
       
   613       $.each(this.items || [], function(item){
       
   614         var group = $.data(this, subContainerKey)
       
   615         if(group)
       
   616           group.traverse(callback)
       
   617       });
       
   618 
       
   619       callback(this)
       
   620     },
       
   621     _clearDimensions: function  () {
       
   622       this.itemDimensions = undefined
       
   623     },
       
   624     _destroy: function() {
       
   625       var that = this;
       
   626 
       
   627       this.target.off(eventNames.start, this.handle);
       
   628       this.el.removeData(pluginName)
       
   629 
       
   630       if(this.options.drop)
       
   631         this.group.containers = $.grep(this.group.containers, function(val){
       
   632           return val != that
       
   633         })
       
   634 
       
   635       $.each(this.items || [], function(){
       
   636         $.removeData(this, subContainerKey)
       
   637       })
       
   638     }
       
   639   }
       
   640 
       
   641   var API = {
       
   642     enable: function() {
       
   643       this.traverse(function(object){
       
   644         object.disabled = false
       
   645       })
       
   646     },
       
   647     disable: function (){
       
   648       this.traverse(function(object){
       
   649         object.disabled = true
       
   650       })
       
   651     },
       
   652     serialize: function () {
       
   653       return this._serialize(this.el, true)
       
   654     },
       
   655     refresh: function() {
       
   656       this.traverse(function(object){
       
   657         object._clearDimensions()
       
   658       })
       
   659     },
       
   660     destroy: function () {
       
   661       this.traverse(function(object){
       
   662         object._destroy();
       
   663       })
       
   664     }
       
   665   }
       
   666 
       
   667   $.extend(Container.prototype, API)
       
   668 
       
   669   /**
       
   670    * jQuery API
       
   671    *
       
   672    * Parameters are
       
   673    *   either options on init
       
   674    *   or a method name followed by arguments to pass to the method
       
   675    */
       
   676   $.fn[pluginName] = function(methodOrOptions) {
       
   677     var args = Array.prototype.slice.call(arguments, 1)
       
   678 
       
   679     return this.map(function(){
       
   680       var $t = $(this),
       
   681       object = $t.data(pluginName)
       
   682 
       
   683       if(object && API[methodOrOptions])
       
   684         return API[methodOrOptions].apply(object, args) || this
       
   685       else if(!object && (methodOrOptions === undefined ||
       
   686                           typeof methodOrOptions === "object"))
       
   687         $t.data(pluginName, new Container($t, methodOrOptions))
       
   688 
       
   689       return this
       
   690     });
       
   691   };
       
   692 
       
   693 }(jQuery, window, 'sortable');