design/js/jquery-sortable.js
changeset 121 21ac67ebf9e7
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/design/js/jquery-sortable.js	Tue Jul 18 18:15:51 2017 +0200
@@ -0,0 +1,693 @@
+/* ===================================================
+ *  jquery-sortable.js v0.9.13
+ *  http://johnny.github.com/jquery-sortable/
+ * ===================================================
+ *  Copyright (c) 2012 Jonas von Andrian
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *  * The name of the author may not be used to endorse or promote products
+ *    derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ *  DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ *  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ *  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ * ========================================================== */
+
+
+!function ( $, window, pluginName, undefined){
+  var containerDefaults = {
+    // If true, items can be dragged from this container
+    drag: true,
+    // If true, items can be droped onto this container
+    drop: true,
+    // Exclude items from being draggable, if the
+    // selector matches the item
+    exclude: "",
+    // If true, search for nested containers within an item.If you nest containers,
+    // either the original selector with which you call the plugin must only match the top containers,
+    // or you need to specify a group (see the bootstrap nav example)
+    nested: true,
+    // If true, the items are assumed to be arranged vertically
+    vertical: true
+  }, // end container defaults
+  groupDefaults = {
+    // This is executed after the placeholder has been moved.
+    // $closestItemOrContainer contains the closest item, the placeholder
+    // has been put at or the closest empty Container, the placeholder has
+    // been appended to.
+    afterMove: function ($placeholder, container, $closestItemOrContainer) {
+    },
+    // The exact css path between the container and its items, e.g. "> tbody"
+    containerPath: "",
+    // The css selector of the containers
+    containerSelector: "ol, ul",
+    // Distance the mouse has to travel to start dragging
+    distance: 0,
+    // Time in milliseconds after mousedown until dragging should start.
+    // This option can be used to prevent unwanted drags when clicking on an element.
+    delay: 0,
+    // The css selector of the drag handle
+    handle: "",
+    // The exact css path between the item and its subcontainers.
+    // It should only match the immediate items of a container.
+    // No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div"
+    itemPath: "",
+    // The css selector of the items
+    itemSelector: "li",
+    // The class given to "body" while an item is being dragged
+    bodyClass: "dragging",
+    // The class giving to an item while being dragged
+    draggedClass: "dragged",
+    // Check if the dragged item may be inside the container.
+    // Use with care, since the search for a valid container entails a depth first search
+    // and may be quite expensive.
+    isValidTarget: function ($item, container) {
+      return true
+    },
+    // Executed before onDrop if placeholder is detached.
+    // This happens if pullPlaceholder is set to false and the drop occurs outside a container.
+    onCancel: function ($item, container, _super, event) {
+    },
+    // Executed at the beginning of a mouse move event.
+    // The Placeholder has not been moved yet.
+    onDrag: function ($item, position, _super, event) {
+      $item.css(position)
+    },
+    // Called after the drag has been started,
+    // that is the mouse button is being held down and
+    // the mouse is moving.
+    // The container is the closest initialized container.
+    // Therefore it might not be the container, that actually contains the item.
+    onDragStart: function ($item, container, _super, event) {
+      $item.css({
+        height: $item.outerHeight(),
+        width: $item.outerWidth()
+      })
+      $item.addClass(container.group.options.draggedClass)
+      $("body").addClass(container.group.options.bodyClass)
+    },
+    // Called when the mouse button is being released
+    onDrop: function ($item, container, _super, event) {
+      $item.removeClass(container.group.options.draggedClass).removeAttr("style")
+      $("body").removeClass(container.group.options.bodyClass)
+    },
+    // Called on mousedown. If falsy value is returned, the dragging will not start.
+    // Ignore if element clicked is input, select or textarea
+    onMousedown: function ($item, _super, event) {
+      if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) {
+        event.preventDefault()
+        return true
+      }
+    },
+    // The class of the placeholder (must match placeholder option markup)
+    placeholderClass: "placeholder",
+    // Template for the placeholder. Can be any valid jQuery input
+    // e.g. a string, a DOM element.
+    // The placeholder must have the class "placeholder"
+    placeholder: '<li class="placeholder"></li>',
+    // If true, the position of the placeholder is calculated on every mousemove.
+    // If false, it is only calculated when the mouse is above a container.
+    pullPlaceholder: true,
+    // Specifies serialization of the container group.
+    // The pair $parent/$children is either container/items or item/subcontainers.
+    serialize: function ($parent, $children, parentIsContainer) {
+      var result = $.extend({}, $parent.data())
+
+      if(parentIsContainer)
+        return [$children]
+      else if ($children[0]){
+        result.children = $children
+      }
+
+      delete result.subContainers
+      delete result.sortable
+
+      return result
+    },
+    // Set tolerance while dragging. Positive values decrease sensitivity,
+    // negative values increase it.
+    tolerance: 0
+  }, // end group defaults
+  containerGroups = {},
+  groupCounter = 0,
+  emptyBox = {
+    left: 0,
+    top: 0,
+    bottom: 0,
+    right:0
+  },
+  eventNames = {
+    start: "touchstart.sortable mousedown.sortable",
+    drop: "touchend.sortable touchcancel.sortable mouseup.sortable",
+    drag: "touchmove.sortable mousemove.sortable",
+    scroll: "scroll.sortable"
+  },
+  subContainerKey = "subContainers"
+
+  /*
+   * a is Array [left, right, top, bottom]
+   * b is array [left, top]
+   */
+  function d(a,b) {
+    var x = Math.max(0, a[0] - b[0], b[0] - a[1]),
+    y = Math.max(0, a[2] - b[1], b[1] - a[3])
+    return x+y;
+  }
+
+  function setDimensions(array, dimensions, tolerance, useOffset) {
+    var i = array.length,
+    offsetMethod = useOffset ? "offset" : "position"
+    tolerance = tolerance || 0
+
+    while(i--){
+      var el = array[i].el ? array[i].el : $(array[i]),
+      // use fitting method
+      pos = el[offsetMethod]()
+      pos.left += parseInt(el.css('margin-left'), 10)
+      pos.top += parseInt(el.css('margin-top'),10)
+      dimensions[i] = [
+        pos.left - tolerance,
+        pos.left + el.outerWidth() + tolerance,
+        pos.top - tolerance,
+        pos.top + el.outerHeight() + tolerance
+      ]
+    }
+  }
+
+  function getRelativePosition(pointer, element) {
+    var offset = element.offset()
+    return {
+      left: pointer.left - offset.left,
+      top: pointer.top - offset.top
+    }
+  }
+
+  function sortByDistanceDesc(dimensions, pointer, lastPointer) {
+    pointer = [pointer.left, pointer.top]
+    lastPointer = lastPointer && [lastPointer.left, lastPointer.top]
+
+    var dim,
+    i = dimensions.length,
+    distances = []
+
+    while(i--){
+      dim = dimensions[i]
+      distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)]
+    }
+    distances = distances.sort(function  (a,b) {
+      return b[1] - a[1] || b[2] - a[2] || b[0] - a[0]
+    })
+
+    // last entry is the closest
+    return distances
+  }
+
+  function ContainerGroup(options) {
+    this.options = $.extend({}, groupDefaults, options)
+    this.containers = []
+
+    if(!this.options.rootGroup){
+      this.scrollProxy = $.proxy(this.scroll, this)
+      this.dragProxy = $.proxy(this.drag, this)
+      this.dropProxy = $.proxy(this.drop, this)
+      this.placeholder = $(this.options.placeholder)
+
+      if(!options.isValidTarget)
+        this.options.isValidTarget = undefined
+    }
+  }
+
+  ContainerGroup.get = function  (options) {
+    if(!containerGroups[options.group]) {
+      if(options.group === undefined)
+        options.group = groupCounter ++
+
+      containerGroups[options.group] = new ContainerGroup(options)
+    }
+
+    return containerGroups[options.group]
+  }
+
+  ContainerGroup.prototype = {
+    dragInit: function  (e, itemContainer) {
+      this.$document = $(itemContainer.el[0].ownerDocument)
+
+      // get item to drag
+      var closestItem = $(e.target).closest(this.options.itemSelector);
+      // using the length of this item, prevents the plugin from being started if there is no handle being clicked on.
+      // this may also be helpful in instantiating multidrag.
+      if (closestItem.length) {
+        this.item = closestItem;
+        this.itemContainer = itemContainer;
+        if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) {
+            return;
+        }
+        this.setPointer(e);
+        this.toggleListeners('on');
+        this.setupDelayTimer();
+        this.dragInitDone = true;
+      }
+    },
+    drag: function  (e) {
+      if(!this.dragging){
+        if(!this.distanceMet(e) || !this.delayMet)
+          return
+
+        this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e)
+        this.item.before(this.placeholder)
+        this.dragging = true
+      }
+
+      this.setPointer(e)
+      // place item under the cursor
+      this.options.onDrag(this.item,
+                          getRelativePosition(this.pointer, this.item.offsetParent()),
+                          groupDefaults.onDrag,
+                          e)
+
+      var p = this.getPointer(e),
+      box = this.sameResultBox,
+      t = this.options.tolerance
+
+      if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left)
+        if(!this.searchValidTarget()){
+          this.placeholder.detach()
+          this.lastAppendedItem = undefined
+        }
+    },
+    drop: function  (e) {
+      this.toggleListeners('off')
+
+      this.dragInitDone = false
+
+      if(this.dragging){
+        // processing Drop, check if placeholder is detached
+        if(this.placeholder.closest("html")[0]){
+          this.placeholder.before(this.item).detach()
+        } else {
+          this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e)
+        }
+        this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e)
+
+        // cleanup
+        this.clearDimensions()
+        this.clearOffsetParent()
+        this.lastAppendedItem = this.sameResultBox = undefined
+        this.dragging = false
+      }
+    },
+    searchValidTarget: function  (pointer, lastPointer) {
+      if(!pointer){
+        pointer = this.relativePointer || this.pointer
+        lastPointer = this.lastRelativePointer || this.lastPointer
+      }
+
+      var distances = sortByDistanceDesc(this.getContainerDimensions(),
+                                         pointer,
+                                         lastPointer),
+      i = distances.length
+
+      while(i--){
+        var index = distances[i][0],
+        distance = distances[i][1]
+
+        if(!distance || this.options.pullPlaceholder){
+          var container = this.containers[index]
+          if(!container.disabled){
+            if(!this.$getOffsetParent()){
+              var offsetParent = container.getItemOffsetParent()
+              pointer = getRelativePosition(pointer, offsetParent)
+              lastPointer = getRelativePosition(lastPointer, offsetParent)
+            }
+            if(container.searchValidTarget(pointer, lastPointer))
+              return true
+          }
+        }
+      }
+      if(this.sameResultBox)
+        this.sameResultBox = undefined
+    },
+    movePlaceholder: function  (container, item, method, sameResultBox) {
+      var lastAppendedItem = this.lastAppendedItem
+      if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0])
+        return;
+
+      item[method](this.placeholder)
+      this.lastAppendedItem = item
+      this.sameResultBox = sameResultBox
+      this.options.afterMove(this.placeholder, container, item)
+    },
+    getContainerDimensions: function  () {
+      if(!this.containerDimensions)
+        setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent())
+      return this.containerDimensions
+    },
+    getContainer: function  (element) {
+      return element.closest(this.options.containerSelector).data(pluginName)
+    },
+    $getOffsetParent: function  () {
+      if(this.offsetParent === undefined){
+        var i = this.containers.length - 1,
+        offsetParent = this.containers[i].getItemOffsetParent()
+
+        if(!this.options.rootGroup){
+          while(i--){
+            if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){
+              // If every container has the same offset parent,
+              // use position() which is relative to this parent,
+              // otherwise use offset()
+              // compare #setDimensions
+              offsetParent = false
+              break;
+            }
+          }
+        }
+
+        this.offsetParent = offsetParent
+      }
+      return this.offsetParent
+    },
+    setPointer: function (e) {
+      var pointer = this.getPointer(e)
+
+      if(this.$getOffsetParent()){
+        var relativePointer = getRelativePosition(pointer, this.$getOffsetParent())
+        this.lastRelativePointer = this.relativePointer
+        this.relativePointer = relativePointer
+      }
+
+      this.lastPointer = this.pointer
+      this.pointer = pointer
+    },
+    distanceMet: function (e) {
+      var currentPointer = this.getPointer(e)
+      return (Math.max(
+        Math.abs(this.pointer.left - currentPointer.left),
+        Math.abs(this.pointer.top - currentPointer.top)
+      ) >= this.options.distance)
+    },
+    getPointer: function(e) {
+      var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0]
+      return {
+        left: e.pageX || o.pageX,
+        top: e.pageY || o.pageY
+      }
+    },
+    setupDelayTimer: function () {
+      var that = this
+      this.delayMet = !this.options.delay
+
+      // init delay timer if needed
+      if (!this.delayMet) {
+        clearTimeout(this._mouseDelayTimer);
+        this._mouseDelayTimer = setTimeout(function() {
+          that.delayMet = true
+        }, this.options.delay)
+      }
+    },
+    scroll: function  (e) {
+      this.clearDimensions()
+      this.clearOffsetParent() // TODO is this needed?
+    },
+    toggleListeners: function (method) {
+      var that = this,
+      events = ['drag','drop','scroll']
+
+      $.each(events,function  (i,event) {
+        that.$document[method](eventNames[event], that[event + 'Proxy'])
+      })
+    },
+    clearOffsetParent: function () {
+      this.offsetParent = undefined
+    },
+    // Recursively clear container and item dimensions
+    clearDimensions: function  () {
+      this.traverse(function(object){
+        object._clearDimensions()
+      })
+    },
+    traverse: function(callback) {
+      callback(this)
+      var i = this.containers.length
+      while(i--){
+        this.containers[i].traverse(callback)
+      }
+    },
+    _clearDimensions: function(){
+      this.containerDimensions = undefined
+    },
+    _destroy: function () {
+      containerGroups[this.options.group] = undefined
+    }
+  }
+
+  function Container(element, options) {
+    this.el = element
+    this.options = $.extend( {}, containerDefaults, options)
+
+    this.group = ContainerGroup.get(this.options)
+    this.rootGroup = this.options.rootGroup || this.group
+    this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector
+
+    var itemPath = this.rootGroup.options.itemPath
+    this.target = itemPath ? this.el.find(itemPath) : this.el
+
+    this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this))
+
+    if(this.options.drop)
+      this.group.containers.push(this)
+  }
+
+  Container.prototype = {
+    dragInit: function  (e) {
+      var rootGroup = this.rootGroup
+
+      if( !this.disabled &&
+          !rootGroup.dragInitDone &&
+          this.options.drag &&
+          this.isValidDrag(e)) {
+        rootGroup.dragInit(e, this)
+      }
+    },
+    isValidDrag: function(e) {
+      return e.which == 1 ||
+        e.type == "touchstart" && e.originalEvent.touches.length == 1
+    },
+    searchValidTarget: function  (pointer, lastPointer) {
+      var distances = sortByDistanceDesc(this.getItemDimensions(),
+                                         pointer,
+                                         lastPointer),
+      i = distances.length,
+      rootGroup = this.rootGroup,
+      validTarget = !rootGroup.options.isValidTarget ||
+        rootGroup.options.isValidTarget(rootGroup.item, this)
+
+      if(!i && validTarget){
+        rootGroup.movePlaceholder(this, this.target, "append")
+        return true
+      } else
+        while(i--){
+          var index = distances[i][0],
+          distance = distances[i][1]
+          if(!distance && this.hasChildGroup(index)){
+            var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer)
+            if(found)
+              return true
+          }
+          else if(validTarget){
+            this.movePlaceholder(index, pointer)
+            return true
+          }
+        }
+    },
+    movePlaceholder: function  (index, pointer) {
+      var item = $(this.items[index]),
+      dim = this.itemDimensions[index],
+      method = "after",
+      width = item.outerWidth(),
+      height = item.outerHeight(),
+      offset = item.offset(),
+      sameResultBox = {
+        left: offset.left,
+        right: offset.left + width,
+        top: offset.top,
+        bottom: offset.top + height
+      }
+      if(this.options.vertical){
+        var yCenter = (dim[2] + dim[3]) / 2,
+        inUpperHalf = pointer.top <= yCenter
+        if(inUpperHalf){
+          method = "before"
+          sameResultBox.bottom -= height / 2
+        } else
+          sameResultBox.top += height / 2
+      } else {
+        var xCenter = (dim[0] + dim[1]) / 2,
+        inLeftHalf = pointer.left <= xCenter
+        if(inLeftHalf){
+          method = "before"
+          sameResultBox.right -= width / 2
+        } else
+          sameResultBox.left += width / 2
+      }
+      if(this.hasChildGroup(index))
+        sameResultBox = emptyBox
+      this.rootGroup.movePlaceholder(this, item, method, sameResultBox)
+    },
+    getItemDimensions: function  () {
+      if(!this.itemDimensions){
+        this.items = this.$getChildren(this.el, "item").filter(
+          ":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")"
+        ).get()
+        setDimensions(this.items, this.itemDimensions = [], this.options.tolerance)
+      }
+      return this.itemDimensions
+    },
+    getItemOffsetParent: function  () {
+      var offsetParent,
+      el = this.el
+      // Since el might be empty we have to check el itself and
+      // can not do something like el.children().first().offsetParent()
+      if(el.css("position") === "relative" || el.css("position") === "absolute"  || el.css("position") === "fixed")
+        offsetParent = el
+      else
+        offsetParent = el.offsetParent()
+      return offsetParent
+    },
+    hasChildGroup: function (index) {
+      return this.options.nested && this.getContainerGroup(index)
+    },
+    getContainerGroup: function  (index) {
+      var childGroup = $.data(this.items[index], subContainerKey)
+      if( childGroup === undefined){
+        var childContainers = this.$getChildren(this.items[index], "container")
+        childGroup = false
+
+        if(childContainers[0]){
+          var options = $.extend({}, this.options, {
+            rootGroup: this.rootGroup,
+            group: groupCounter ++
+          })
+          childGroup = childContainers[pluginName](options).data(pluginName).group
+        }
+        $.data(this.items[index], subContainerKey, childGroup)
+      }
+      return childGroup
+    },
+    $getChildren: function (parent, type) {
+      var options = this.rootGroup.options,
+      path = options[type + "Path"],
+      selector = options[type + "Selector"]
+
+      parent = $(parent)
+      if(path)
+        parent = parent.find(path)
+
+      return parent.children(selector)
+    },
+    _serialize: function (parent, isContainer) {
+      var that = this,
+      childType = isContainer ? "item" : "container",
+
+      children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () {
+        return that._serialize($(this), !isContainer)
+      }).get()
+
+      return this.rootGroup.options.serialize(parent, children, isContainer)
+    },
+    traverse: function(callback) {
+      $.each(this.items || [], function(item){
+        var group = $.data(this, subContainerKey)
+        if(group)
+          group.traverse(callback)
+      });
+
+      callback(this)
+    },
+    _clearDimensions: function  () {
+      this.itemDimensions = undefined
+    },
+    _destroy: function() {
+      var that = this;
+
+      this.target.off(eventNames.start, this.handle);
+      this.el.removeData(pluginName)
+
+      if(this.options.drop)
+        this.group.containers = $.grep(this.group.containers, function(val){
+          return val != that
+        })
+
+      $.each(this.items || [], function(){
+        $.removeData(this, subContainerKey)
+      })
+    }
+  }
+
+  var API = {
+    enable: function() {
+      this.traverse(function(object){
+        object.disabled = false
+      })
+    },
+    disable: function (){
+      this.traverse(function(object){
+        object.disabled = true
+      })
+    },
+    serialize: function () {
+      return this._serialize(this.el, true)
+    },
+    refresh: function() {
+      this.traverse(function(object){
+        object._clearDimensions()
+      })
+    },
+    destroy: function () {
+      this.traverse(function(object){
+        object._destroy();
+      })
+    }
+  }
+
+  $.extend(Container.prototype, API)
+
+  /**
+   * jQuery API
+   *
+   * Parameters are
+   *   either options on init
+   *   or a method name followed by arguments to pass to the method
+   */
+  $.fn[pluginName] = function(methodOrOptions) {
+    var args = Array.prototype.slice.call(arguments, 1)
+
+    return this.map(function(){
+      var $t = $(this),
+      object = $t.data(pluginName)
+
+      if(object && API[methodOrOptions])
+        return API[methodOrOptions].apply(object, args) || this
+      else if(!object && (methodOrOptions === undefined ||
+                          typeof methodOrOptions === "object"))
+        $t.data(pluginName, new Container($t, methodOrOptions))
+
+      return this
+    });
+  };
+
+}(jQuery, window, 'sortable');