Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
/**!
 * Sortable
 * @author  RubaXa   <trash@rubaxa.org>
 * @license MIT
 */

(function sortableModule(factory) {
  "use strict";

  if (typeof define === "function" && define.amd) {
    define(factory);
  }
  else if (typeof module != "undefined" && typeof module.exports != "undefined") {
    module.exports = factory();
  }
  else if (typeof Package !== "undefined") {
    //noinspection JSUnresolvedVariable
    Sortable = factory();  // export for Meteor.js
  }
  else {
    /* jshint sub:true */
    window["Sortable"] = factory();
  }
})(function sortableFactory() {
  "use strict";

  if (typeof window == "undefined" || !window.document) {
    return function sortableError() {
      throw new Error("Sortable.js requires a window with a document");
    };
  }

  var dragEl,
    parentEl,
    ghostEl,
    cloneEl,
    rootEl,
    nextEl,

    scrollEl,
    scrollParentEl,
    scrollCustomFn,

    lastEl,
    lastCSS,
    lastParentCSS,

    oldIndex,
    newIndex,

    activeGroup,
    putSortable,

    autoScroll = {},

    tapEvt,
    touchEvt,

    moved,

    /** @const */
    RSPACE = /\s+/g,

    expando = 'Sortable' + (new Date).getTime(),

    win = window,
    document = win.document,
    parseInt = win.parseInt,

    $ = win.jQuery || win.Zepto,
    Polymer = win.Polymer,

    supportDraggable = !!('draggable' in document.createElement('div')),
    supportCssPointerEvents = (function (el) {
      // false when IE11
      if (!!navigator.userAgent.match(/Trident.*rv[ :]?11\./)) {
        return false;
      }
      el = document.createElement('x');
      el.style.cssText = 'pointer-events:auto';
      return el.style.pointerEvents === 'auto';
    })(),

    _silent = false,

    abs = Math.abs,
    min = Math.min,
    slice = [].slice,

    touchDragOverListeners = [],

    _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) {
      // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
      if (rootEl && options.scroll) {
        var el,
          rect,
          sens = options.scrollSensitivity,
          speed = options.scrollSpeed,

          x = evt.clientX,
          y = evt.clientY,

          winWidth = window.innerWidth,
          winHeight = window.innerHeight,

          vx,
          vy,

          scrollOffsetX,
          scrollOffsetY
        ;

        // Delect scrollEl
        if (scrollParentEl !== rootEl) {
          scrollEl = options.scroll;
          scrollParentEl = rootEl;
          scrollCustomFn = options.scrollFn;

          if (scrollEl === true) {
            scrollEl = rootEl;

            do {
              if ((scrollEl.offsetWidth < scrollEl.scrollWidth) ||
                (scrollEl.offsetHeight < scrollEl.scrollHeight)
              ) {
                break;
              }
              /* jshint boss:true */
            } while (scrollEl = scrollEl.parentNode);
          }
        }

        if (scrollEl) {
          el = scrollEl;
          rect = scrollEl.getBoundingClientRect();
          vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens);
          vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens);
        }


        if (!(vx || vy)) {
          vx = (winWidth - x <= sens) - (x <= sens);
          vy = (winHeight - y <= sens) - (y <= sens);

          /* jshint expr:true */
          (vx || vy) && (el = win);
        }


        if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) {
          autoScroll.el = el;
          autoScroll.vx = vx;
          autoScroll.vy = vy;

          clearInterval(autoScroll.pid);

          if (el) {
            autoScroll.pid = setInterval(function () {
              scrollOffsetY = vy ? vy * speed : 0;
              scrollOffsetX = vx ? vx * speed : 0;

              if ('function' === typeof(scrollCustomFn)) {
                return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt);
              }

              if (el === win) {
                win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY);
              } else {
                el.scrollTop += scrollOffsetY;
                el.scrollLeft += scrollOffsetX;
              }
            }, 24);
          }
        }
      }
    }, 30),

    _prepareGroup = function (options) {
      function toFn(value, pull) {
        if (value === void 0 || value === true) {
          value = group.name;
        }

        if (typeof value === 'function') {
          return value;
        } else {
          return function (to, from) {
            var fromGroup = from.options.group.name;

            return pull
              ? value
              : value && (value.join
                ? value.indexOf(fromGroup) > -1
                : (fromGroup == value)
              );
          };
        }
      }

      var group = {};
      var originalGroup = options.group;

      if (!originalGroup || typeof originalGroup != 'object') {
        originalGroup = {name: originalGroup};
      }

      group.name = originalGroup.name;
      group.checkPull = toFn(originalGroup.pull, true);
      group.checkPut = toFn(originalGroup.put);

      options.group = group;
    }
  ;



  /**
   * @class  Sortable
   * @param  {HTMLElement}  el
   * @param  {Object}       [options]
   */
  function Sortable(el, options) {
    if (!(el && el.nodeType && el.nodeType === 1)) {
      throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el);
    }

    this.el = el; // root element
    this.options = options = _extend({}, options);


    // Export instance
    el[expando] = this;


    // Default options
    var defaults = {
      group: Math.random(),
      sort: true,
      disabled: false,
      store: null,
      handle: null,
      scroll: true,
      scrollSensitivity: 30,
      scrollSpeed: 10,
      draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',
      ghostClass: 'sortable-ghost',
      chosenClass: 'sortable-chosen',
      dragClass: 'sortable-drag',
      ignore: 'a, img',
      filter: null,
      animation: 0,
      setData: function (dataTransfer, dragEl) {
        dataTransfer.setData('Text', dragEl.textContent);
      },
      dropBubble: false,
      dragoverBubble: false,
      dataIdAttr: 'data-id',
      delay: 0,
      forceFallback: false,
      fallbackClass: 'sortable-fallback',
      fallbackOnBody: false,
      fallbackTolerance: 0,
      fallbackOffset: {x: 0, y: 0}
    };


    // Set default options
    for (var name in defaults) {
      !(name in options) && (options[name] = defaults[name]);
    }

    _prepareGroup(options);

    // Bind all private methods
    for (var fn in this) {
      if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
        this[fn] = this[fn].bind(this);
      }
    }

    // Setup drag mode
    this.nativeDraggable = options.forceFallback ? false : supportDraggable;

    // Bind events
    _on(el, 'mousedown', this._onTapStart);
    _on(el, 'touchstart', this._onTapStart);

    if (this.nativeDraggable) {
      _on(el, 'dragover', this);
      _on(el, 'dragenter', this);
    }

    touchDragOverListeners.push(this._onDragOver);

    // Restore sorting
    options.store && this.sort(options.store.get(this));
  }


  Sortable.prototype = /** @lends Sortable.prototype */ {
    constructor: Sortable,

    _onTapStart: function (/** Event|TouchEvent */evt) {
      var _this = this,
        el = this.el,
        options = this.options,
        type = evt.type,
        touch = evt.touches && evt.touches[0],
        target = (touch || evt).target,
        originalTarget = evt.target.shadowRoot && evt.path[0] || target,
        filter = options.filter,
        startIndex;

      // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group.
      if (dragEl) {
        return;
      }

      if (type === 'mousedown' && evt.button !== 0 || options.disabled) {
        return; // only left button or enabled
      }

      if (options.handle && !_closest(originalTarget, options.handle, el)) {
        return;
      }

      target = _closest(target, options.draggable, el);

      if (!target) {
        return;
      }

      // Get the index of the dragged element within its parent
      startIndex = _index(target, options.draggable);

      // Check filter
      if (typeof filter === 'function') {
        if (filter.call(this, evt, target, this)) {
          _dispatchEvent(_this, originalTarget, 'filter', target, el, startIndex);
          evt.preventDefault();
          return; // cancel dnd
        }
      }
      else if (filter) {
        filter = filter.split(',').some(function (criteria) {
          criteria = _closest(originalTarget, criteria.trim(), el);

          if (criteria) {
            _dispatchEvent(_this, criteria, 'filter', target, el, startIndex);
            return true;
          }
        });

        if (filter) {
          evt.preventDefault();
          return; // cancel dnd
        }
      }

      // Prepare `dragstart`
      this._prepareDragStart(evt, touch, target, startIndex);
    },

    _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) {
      var _this = this,
        el = _this.el,
        options = _this.options,
        ownerDocument = el.ownerDocument,
        dragStartFn;

      if (target && !dragEl && (target.parentNode === el)) {
        tapEvt = evt;

        rootEl = el;
        dragEl = target;
        parentEl = dragEl.parentNode;
        nextEl = dragEl.nextSibling;
        activeGroup = options.group;
        oldIndex = startIndex;

        this._lastX = (touch || evt).clientX;
        this._lastY = (touch || evt).clientY;

        dragEl.style['will-change'] = 'transform';

        dragStartFn = function () {
          // Delayed drag has been triggered
          // we can re-enable the events: touchmove/mousemove
          _this._disableDelayedDrag();

          // Make the element draggable
          dragEl.draggable = _this.nativeDraggable;

          // Chosen item
          _toggleClass(dragEl, options.chosenClass, true);

          // Bind the events: dragstart/dragend
          _this._triggerDragStart(touch);

          // Drag start event
          _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, oldIndex);
        };

        // Disable "draggable"
        options.ignore.split(',').forEach(function (criteria) {
          _find(dragEl, criteria.trim(), _disableDraggable);
        });

        _on(ownerDocument, 'mouseup', _this._onDrop);
        _on(ownerDocument, 'touchend', _this._onDrop);
        _on(ownerDocument, 'touchcancel', _this._onDrop);

        if (options.delay) {
          // If the user moves the pointer or let go the click or touch
          // before the delay has been reached:
          // disable the delayed drag
          _on(ownerDocument, 'mouseup', _this._disableDelayedDrag);
          _on(ownerDocument, 'touchend', _this._disableDelayedDrag);
          _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag);
          _on(ownerDocument, 'mousemove', _this._disableDelayedDrag);
          _on(ownerDocument, 'touchmove', _this._disableDelayedDrag);

          _this._dragStartTimer = setTimeout(dragStartFn, options.delay);
        } else {
          dragStartFn();
        }
      }
    },

    _disableDelayedDrag: function () {
      var ownerDocument = this.el.ownerDocument;

      clearTimeout(this._dragStartTimer);
      _off(ownerDocument, 'mouseup', this._disableDelayedDrag);
      _off(ownerDocument, 'touchend', this._disableDelayedDrag);
      _off(ownerDocument, 'touchcancel', this._disableDelayedDrag);
      _off(ownerDocument, 'mousemove', this._disableDelayedDrag);
      _off(ownerDocument, 'touchmove', this._disableDelayedDrag);
    },

    _triggerDragStart: function (/** Touch */touch) {
      if (touch) {
        // Touch device support
        tapEvt = {
          target: dragEl,
          clientX: touch.clientX,
          clientY: touch.clientY
        };

        this._onDragStart(tapEvt, 'touch');
      }
      else if (!this.nativeDraggable) {
        this._onDragStart(tapEvt, true);
      }
      else {
        _on(dragEl, 'dragend', this);
        _on(rootEl, 'dragstart', this._onDragStart);
      }

      try {
        if (document.selection) {
          // Timeout neccessary for IE9
          setTimeout(function () {
            document.selection.empty();
          });
        } else {
          window.getSelection().removeAllRanges();
        }
      } catch (err) {
      }
    },

    _dragStarted: function () {
      if (rootEl && dragEl) {
        var options = this.options;

        // Apply effect
        _toggleClass(dragEl, options.ghostClass, true);
        _toggleClass(dragEl, options.dragClass, false);

        Sortable.active = this;

        // Drag start event
        _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, oldIndex);
      }
    },

    _emulateDragOver: function () {
      if (touchEvt) {
        if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) {
          return;
        }

        this._lastX = touchEvt.clientX;
        this._lastY = touchEvt.clientY;

        if (!supportCssPointerEvents) {
          _css(ghostEl, 'display', 'none');
        }

        var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
          parent = target,
          i = touchDragOverListeners.length;

        if (parent) {
          do {
            if (parent[expando]) {
              while (i--) {
                touchDragOverListeners[i]({
                  clientX: touchEvt.clientX,
                  clientY: touchEvt.clientY,
                  target: target,
                  rootEl: parent
                });
              }

              break;
            }

            target = parent; // store last element
          }
          /* jshint boss:true */
          while (parent = parent.parentNode);
        }

        if (!supportCssPointerEvents) {
          _css(ghostEl, 'display', '');
        }
      }
    },


    _onTouchMove: function (/**TouchEvent*/evt) {
      if (tapEvt) {
        var options = this.options,
          fallbackTolerance = options.fallbackTolerance,
          fallbackOffset = options.fallbackOffset,
          touch = evt.touches ? evt.touches[0] : evt,
          dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x,
          dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y,
          translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';

        // only set the status to dragging, when we are actually dragging
        if (!Sortable.active) {
          if (fallbackTolerance &&
            min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance
          ) {
            return;
          }

          this._dragStarted();
        }

        // as well as creating the ghost element on the document body
        this._appendGhost();

        moved = true;
        touchEvt = touch;

        _css(ghostEl, 'webkitTransform', translate3d);
        _css(ghostEl, 'mozTransform', translate3d);
        _css(ghostEl, 'msTransform', translate3d);
        _css(ghostEl, 'transform', translate3d);

        evt.preventDefault();
      }
    },

    _appendGhost: function () {
      if (!ghostEl) {
        var rect = dragEl.getBoundingClientRect(),
          css = _css(dragEl),
          options = this.options,
          ghostRect;

        ghostEl = dragEl.cloneNode(true);

        _toggleClass(ghostEl, options.ghostClass, false);
        _toggleClass(ghostEl, options.fallbackClass, true);
        _toggleClass(ghostEl, options.dragClass, true);

        _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
        _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
        _css(ghostEl, 'width', rect.width);
        _css(ghostEl, 'height', rect.height);
        _css(ghostEl, 'opacity', '0.8');
        _css(ghostEl, 'position', 'fixed');
        _css(ghostEl, 'zIndex', '100000');
        _css(ghostEl, 'pointerEvents', 'none');

        options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl);

        // Fixing dimensions.
        ghostRect = ghostEl.getBoundingClientRect();
        _css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
        _css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
      }
    },

    _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) {
      var dataTransfer = evt.dataTransfer,
        options = this.options;

      this._offUpEvents();

      if (activeGroup.checkPull(this, this, dragEl, evt) == 'clone') {
        cloneEl = _clone(dragEl);
        _css(cloneEl, 'display', 'none');
        rootEl.insertBefore(cloneEl, dragEl);
        _dispatchEvent(this, rootEl, 'clone', dragEl);
      }

      _toggleClass(dragEl, options.dragClass, true);

      if (useFallback) {
        if (useFallback === 'touch') {
          // Bind touch events
          _on(document, 'touchmove', this._onTouchMove);
          _on(document, 'touchend', this._onDrop);
          _on(document, 'touchcancel', this._onDrop);
        } else {
          // Old brwoser
          _on(document, 'mousemove', this._onTouchMove);
          _on(document, 'mouseup', this._onDrop);
        }

        this._loopId = setInterval(this._emulateDragOver, 50);
      }
      else {
        if (dataTransfer) {
          dataTransfer.effectAllowed = 'move';
          options.setData && options.setData.call(this, dataTransfer, dragEl);
        }

        _on(document, 'drop', this);
        setTimeout(this._dragStarted, 0);
      }
    },

    _onDragOver: function (/**Event*/evt) {
      var el = this.el,
        target,
        dragRect,
        targetRect,
        revert,
        options = this.options,
        group = options.group,
        activeSortable = Sortable.active,
        isOwner = (activeGroup === group),
        canSort = options.sort;

      if (evt.preventDefault !== void 0) {
        evt.preventDefault();
        !options.dragoverBubble && evt.stopPropagation();
      }

      moved = true;

      if (activeGroup && !options.disabled &&
        (isOwner
          ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
          : (
            putSortable === this ||
            activeGroup.checkPull(this, activeSortable, dragEl, evt) && group.checkPut(this, activeSortable, dragEl, evt)
          )
        ) &&
        (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback
      ) {
        // Smart auto-scrolling
        _autoScroll(evt, options, this.el);

        if (_silent) {
          return;
        }

        target = _closest(evt.target, options.draggable, el);
        dragRect = dragEl.getBoundingClientRect();
        putSortable = this;

        if (revert) {
          _cloneHide(true);
          parentEl = rootEl; // actualization

          if (cloneEl || nextEl) {
            rootEl.insertBefore(dragEl, cloneEl || nextEl);
          }
          else if (!canSort) {
            rootEl.appendChild(dragEl);
          }

          return;
        }


        if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
          (el === evt.target) && (target = _ghostIsLast(el, evt))
        ) {
          if (target) {
            if (target.animated) {
              return;
            }

            targetRect = target.getBoundingClientRect();
          }

          _cloneHide(isOwner);

          if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) {
            if (!dragEl.contains(el)) {
              el.appendChild(dragEl);
              parentEl = el; // actualization
            }

            this._animate(dragRect, dragEl);
            target && this._animate(targetRect, target);
          }
        }
        else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) {
          if (lastEl !== target) {
            lastEl = target;
            lastCSS = _css(target);
            lastParentCSS = _css(target.parentNode);
          }

          targetRect = target.getBoundingClientRect();

          var width = targetRect.right - targetRect.left,
            height = targetRect.bottom - targetRect.top,
            floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display)
              || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0),
            isWide = (target.offsetWidth > dragEl.offsetWidth),
            isLong = (target.offsetHeight > dragEl.offsetHeight),
            halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
            nextSibling = target.nextElementSibling,
            moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt),
            after
          ;

          if (moveVector !== false) {
            _silent = true;
            setTimeout(_unsilent, 30);

            _cloneHide(isOwner);

            if (moveVector === 1 || moveVector === -1) {
              after = (moveVector === 1);
            }
            else if (floating) {
              var elTop = dragEl.offsetTop,
                tgTop = target.offsetTop;

              if (elTop === tgTop) {
                after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
              }
              else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) {
                after = (evt.clientY - targetRect.top) / height > 0.5;
              } else {
                after = tgTop > elTop;
              }
            } else {
              after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
            }

            if (!dragEl.contains(el)) {
              if (after && !nextSibling) {
                el.appendChild(dragEl);
              } else {
                target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
              }
            }

            parentEl = dragEl.parentNode; // actualization

            this._animate(dragRect, dragEl);
            this._animate(targetRect, target);
          }
        }
      }
    },

    _animate: function (prevRect, target) {
      var ms = this.options.animation;

      if (ms) {
        var currentRect = target.getBoundingClientRect();

        _css(target, 'transition', 'none');
        _css(target, 'transform', 'translate3d('
          + (prevRect.left - currentRect.left) + 'px,'
          + (prevRect.top - currentRect.top) + 'px,0)'
        );

        target.offsetWidth; // repaint

        _css(target, 'transition', 'all ' + ms + 'ms');
        _css(target, 'transform', 'translate3d(0,0,0)');

        clearTimeout(target.animated);
        target.animated = setTimeout(function () {
          _css(target, 'transition', '');
          _css(target, 'transform', '');
          target.animated = false;
        }, ms);
      }
    },

    _offUpEvents: function () {
      var ownerDocument = this.el.ownerDocument;

      _off(document, 'touchmove', this._onTouchMove);
      _off(ownerDocument, 'mouseup', this._onDrop);
      _off(ownerDocument, 'touchend', this._onDrop);
      _off(ownerDocument, 'touchcancel', this._onDrop);
    },

    _onDrop: function (/**Event*/evt) {
      var el = this.el,
        options = this.options;

      clearInterval(this._loopId);
      clearInterval(autoScroll.pid);
      clearTimeout(this._dragStartTimer);

      // Unbind events
      _off(document, 'mousemove', this._onTouchMove);

      if (this.nativeDraggable) {
        _off(document, 'drop', this);
        _off(el, 'dragstart', this._onDragStart);
      }

      this._offUpEvents();

      if (evt) {
        if (moved) {
          evt.preventDefault();
          !options.dropBubble && evt.stopPropagation();
        }

        ghostEl && ghostEl.parentNode.removeChild(ghostEl);

        if (dragEl) {
          if (this.nativeDraggable) {
            _off(dragEl, 'dragend', this);
          }

          _disableDraggable(dragEl);
          dragEl.style['will-change'] = '';

          // Remove class's
          _toggleClass(dragEl, this.options.ghostClass, false);
          _toggleClass(dragEl, this.options.chosenClass, false);

          if (rootEl !== parentEl) {
            newIndex = _index(dragEl, options.draggable);

            if (newIndex >= 0) {

              // Add event
              _dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex);

              // Remove event
              _dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex);

              // drag from one list and drop into another
              _dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
              _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
            }
          }
          else {
            // Remove clone
            cloneEl && cloneEl.parentNode.removeChild(cloneEl);

            if (dragEl.nextSibling !== nextEl) {
              // Get the index of the dragged element within its parent
              newIndex = _index(dragEl, options.draggable);

              if (newIndex >= 0) {
                // drag & drop within the same list
                _dispatchEvent(this, rootEl, 'update', dragEl, rootEl, oldIndex, newIndex);
                _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
              }
            }
          }

          if (Sortable.active) {
            /* jshint eqnull:true */
            if (newIndex == null || newIndex === -1) {
              newIndex = oldIndex;
            }

            _dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex);

            // Save sorting
            this.save();
          }
        }

      }

      this._nulling();
    },

    _nulling: function() {
      rootEl =
      dragEl =
      parentEl =
      ghostEl =
      nextEl =
      cloneEl =

      scrollEl =
      scrollParentEl =

      tapEvt =
      touchEvt =

      moved =
      newIndex =

      lastEl =
      lastCSS =

      putSortable =
      activeGroup =
      Sortable.active = null;
    },

    handleEvent: function (/**Event*/evt) {
      var type = evt.type;

      if (type === 'dragover' || type === 'dragenter') {
        if (dragEl) {
          this._onDragOver(evt);
          _globalDragOver(evt);
        }
      }
      else if (type === 'drop' || type === 'dragend') {
        this._onDrop(evt);
      }
    },


    /**
     * Serializes the item into an array of string.
     * @returns {String[]}
     */
    toArray: function () {
      var order = [],
        el,
        children = this.el.children,
        i = 0,
        n = children.length,
        options = this.options;

      for (; i < n; i++) {
        el = children[i];
        if (_closest(el, options.draggable, this.el)) {
          order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
        }
      }

      return order;
    },


    /**
     * Sorts the elements according to the array.
     * @param  {String[]}  order  order of the items
     */
    sort: function (order) {
      var items = {}, rootEl = this.el;

      this.toArray().forEach(function (id, i) {
        var el = rootEl.children[i];

        if (_closest(el, this.options.draggable, rootEl)) {
          items[id] = el;
        }
      }, this);

      order.forEach(function (id) {
        if (items[id]) {
          rootEl.removeChild(items[id]);
          rootEl.appendChild(items[id]);
        }
      });
    },


    /**
     * Save the current sorting
     */
    save: function () {
      var store = this.options.store;
      store && store.set(this);
    },


    /**
     * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
     * @param   {HTMLElement}  el
     * @param   {String}       [selector]  default: `options.draggable`
     * @returns {HTMLElement|null}
     */
    closest: function (el, selector) {
      return _closest(el, selector || this.options.draggable, this.el);
    },


    /**
     * Set/get option
     * @param   {string} name
     * @param   {*}      [value]
     * @returns {*}
     */
    option: function (name, value) {
      var options = this.options;

      if (value === void 0) {
        return options[name];
      } else {
        options[name] = value;

        if (name === 'group') {
          _prepareGroup(options);
        }
      }
    },


    /**
     * Destroy
     */
    destroy: function () {
      var el = this.el;

      el[expando] = null;

      _off(el, 'mousedown', this._onTapStart);
      _off(el, 'touchstart', this._onTapStart);

      if (this.nativeDraggable) {
        _off(el, 'dragover', this);
        _off(el, 'dragenter', this);
      }

      // Remove draggable attributes
      Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
        el.removeAttribute('draggable');
      });

      touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);

      this._onDrop();

      this.el = el = null;
    }
  };


  function _cloneHide(state) {
    if (cloneEl && (cloneEl.state !== state)) {
      _css(cloneEl, 'display', state ? 'none' : '');
      !state && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl);
      cloneEl.state = state;
    }
  }


  function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) {
    if (el) {
      ctx = ctx || document;

      do {
        if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) {
          return el;
        }
        /* jshint boss:true */
      } while (el = _getParentOrHost(el));
    }

    return null;
  }


  function _getParentOrHost(el) {
    var parent = el.host;

    return (parent && parent.nodeType) ? parent : el.parentNode;
  }


  function _globalDragOver(/**Event*/evt) {
    if (evt.dataTransfer) {
      evt.dataTransfer.dropEffect = 'move';
    }
    evt.preventDefault();
  }


  function _on(el, event, fn) {
    el.addEventListener(event, fn, false);
  }


  function _off(el, event, fn) {
    el.removeEventListener(event, fn, false);
  }


  function _toggleClass(el, name, state) {
    if (el) {
      if (el.classList) {
        el.classList[state ? 'add' : 'remove'](name);
      }
      else {
        var className = (' ' + el.className + ' ').replace(RSPACE, ' ').replace(' ' + name + ' ', ' ');
        el.className = (className + (state ? ' ' + name : '')).replace(RSPACE, ' ');
      }
    }
  }


  function _css(el, prop, val) {
    var style = el && el.style;

    if (style) {
      if (val === void 0) {
        if (document.defaultView && document.defaultView.getComputedStyle) {
          val = document.defaultView.getComputedStyle(el, '');
        }
        else if (el.currentStyle) {
          val = el.currentStyle;
        }

        return prop === void 0 ? val : val[prop];
      }
      else {
        if (!(prop in style)) {
          prop = '-webkit-' + prop;
        }

        style[prop] = val + (typeof val === 'string' ? '' : 'px');
      }
    }
  }


  function _find(ctx, tagName, iterator) {
    if (ctx) {
      var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;

      if (iterator) {
        for (; i < n; i++) {
          iterator(list[i], i);
        }
      }

      return list;
    }

    return [];
  }



  function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) {
    sortable = (sortable || rootEl[expando]);

    var evt = document.createEvent('Event'),
      options = sortable.options,
      onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);

    evt.initEvent(name, true, true);

    evt.to = rootEl;
    evt.from = fromEl || rootEl;
    evt.item = targetEl || rootEl;
    evt.clone = cloneEl;

    evt.oldIndex = startIndex;
    evt.newIndex = newIndex;

    rootEl.dispatchEvent(evt);

    if (options[onName]) {
      options[onName].call(sortable, evt);
    }
  }


  function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt) {
    var evt,
      sortable = fromEl[expando],
      onMoveFn = sortable.options.onMove,
      retVal;

    evt = document.createEvent('Event');
    evt.initEvent('move', true, true);

    evt.to = toEl;
    evt.from = fromEl;
    evt.dragged = dragEl;
    evt.draggedRect = dragRect;
    evt.related = targetEl || toEl;
    evt.relatedRect = targetRect || toEl.getBoundingClientRect();

    fromEl.dispatchEvent(evt);

    if (onMoveFn) {
      retVal = onMoveFn.call(sortable, evt, originalEvt);
    }

    return retVal;
  }


  function _disableDraggable(el) {
    el.draggable = false;
  }


  function _unsilent() {
    _silent = false;
  }


  /** @returns {HTMLElement|false} */
  function _ghostIsLast(el, evt) {
    var lastEl = el.lastElementChild,
      rect = lastEl.getBoundingClientRect();

    // 5 — min delta
    // abs — нельзя добавлять, а то глюки при наведении сверху
    return (
      (evt.clientY - (rect.top + rect.height) > 5) ||
      (evt.clientX - (rect.right + rect.width) > 5)
    ) && lastEl;
  }


  /**
   * Generate id
   * @param   {HTMLElement} el
   * @returns {String}
   * @private
   */
  function _generateId(el) {
    var str = el.tagName + el.className + el.src + el.href + el.textContent,
      i = str.length,
      sum = 0;

    while (i--) {
      sum += str.charCodeAt(i);
    }

    return sum.toString(36);
  }

  /**
   * Returns the index of an element within its parent for a selected set of
   * elements
   * @param  {HTMLElement} el
   * @param  {selector} selector
   * @return {number}
   */
  function _index(el, selector) {
    var index = 0;

    if (!el || !el.parentNode) {
      return -1;
    }

    while (el && (el = el.previousElementSibling)) {
      if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) {
        index++;
      }
    }

    return index;
  }

  function _matches(/**HTMLElement*/el, /**String*/selector) {
    if (el) {
      selector = selector.split('.');

      var tag = selector.shift().toUpperCase(),
        re = new RegExp('\\s(' + selector.join('|') + ')(?=\\s)', 'g');

      return (
        (tag === '' || el.nodeName.toUpperCase() == tag) &&
        (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length)
      );
    }

    return false;
  }

  function _throttle(callback, ms) {
    var args, _this;

    return function () {
      if (args === void 0) {
        args = arguments;
        _this = this;

        setTimeout(function () {
          if (args.length === 1) {
            callback.call(_this, args[0]);
          } else {
            callback.apply(_this, args);
          }

          args = void 0;
        }, ms);
      }
    };
  }

  function _extend(dst, src) {
    if (dst && src) {
      for (var key in src) {
        if (src.hasOwnProperty(key)) {
          dst[key] = src[key];
        }
      }
    }

    return dst;
  }

  function _clone(el) {
    return $
      ? $(el).clone(true)[0]
      : (Polymer && Polymer.dom
        ? Polymer.dom(el).cloneNode(true)
        : el.cloneNode(true)
      );
  }


  // Export utils
  Sortable.utils = {
    on: _on,
    off: _off,
    css: _css,
    find: _find,
    is: function (el, selector) {
      return !!_closest(el, selector, el);
    },
    extend: _extend,
    throttle: _throttle,
    closest: _closest,
    toggleClass: _toggleClass,
    clone: _clone,
    index: _index
  };


  /**
   * Create sortable instance
   * @param {HTMLElement}  el
   * @param {Object}      [options]
   */
  Sortable.create = function (el, options) {
    return new Sortable(el, options);
  };


  // Export
  Sortable.version = '1.4.2';
  return Sortable;
});