FanaHOVA/blazer

View on GitHub
app/assets/javascripts/blazer/Sortable.js

Summary

Maintainability
F
4 days
Test Coverage
/**!
 * Sortable
 * @author  RubaXa   <trash@rubaxa.org>
 * @license MIT
 */


(function (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") {
    Sortable = factory();  // export for Meteor.js
  }
  else {
    /* jshint sub:true */
    window["Sortable"] = factory();
  }
})(function () {
  "use strict";

  var dragEl,
    ghostEl,
    cloneEl,
    rootEl,
    nextEl,

    scrollEl,
    scrollParentEl,

    lastEl,
    lastCSS,

    oldIndex,
    newIndex,

    activeGroup,
    autoScroll = {},

    tapEvt,
    touchEvt,

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

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

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

    supportDraggable = !!('draggable' in document.createElement('div')),

    _silent = false,

    abs = Math.abs,
    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
        ;

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

          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 () {
              if (el === win) {
                win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed);
              } else {
                vy && (el.scrollTop += vy * speed);
                vx && (el.scrollLeft += vx * speed);
              }
            }, 24);
          }
        }
      }
    }, 30)
  ;



  /**
   * @class  Sortable
   * @param  {HTMLElement}  el
   * @param  {Object}       [options]
   */
  function Sortable(el, options) {
    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',
      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
    };


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


    var group = options.group;

    if (!group || typeof group != 'object') {
      group = options.group = { name: group };
    }


    ['pull', 'put'].forEach(function (key) {
      if (!(key in group)) {
        group[key] = true;
      }
    });


    options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' ';


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


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

    _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 = target,
        filter = options.filter;


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

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

      if (!target) {
        return;
      }

      // get the index of the dragged element within its parent
      oldIndex = _index(target);

      // Check filter
      if (typeof filter === 'function') {
        if (filter.call(this, evt, target, this)) {
          _dispatchEvent(_this, originalTarget, 'filter', target, el, oldIndex);
          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, oldIndex);
            return true;
          }
        });

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


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


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

    _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target) {
      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;
        nextEl = dragEl.nextSibling;
        activeGroup = options.group;

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

          // Make the element draggable
          dragEl.draggable = true;

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

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

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

        if (options.delay) {
          // If the user moves the pointer before the delay has been reached:
          // disable the delayed drag
          _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, '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 (!supportDraggable) {
        this._onDragStart(tapEvt, true);
      }
      else {
        _on(dragEl, 'dragend', this);
        _on(rootEl, 'dragstart', this._onDragStart);
      }

      try {
        if (document.selection) {
          document.selection.empty();
        } else {
          window.getSelection().removeAllRanges();
        }
      } catch (err) {
      }
    },

    _dragStarted: function () {
      if (rootEl && dragEl) {
        // Apply effect
        _toggleClass(dragEl, this.options.ghostClass, true);

        Sortable.active = this;

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

    _emulateDragOver: function () {
      if (touchEvt) {
        _css(ghostEl, 'display', 'none');

        var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
          parent = target,
          groupName = ' ' + this.options.group.name + '',
          i = touchDragOverListeners.length;

        if (parent) {
          do {
            if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) {
              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);
        }

        _css(ghostEl, 'display', '');
      }
    },


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

        touchEvt = touch;

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

        evt.preventDefault();
      }
    },


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

      this._offUpEvents();

      if (activeGroup.pull == 'clone') {
        cloneEl = dragEl.cloneNode(true);
        _css(cloneEl, 'display', 'none');
        rootEl.insertBefore(cloneEl, dragEl);
      }

      if (useFallback) {
        var rect = dragEl.getBoundingClientRect(),
          css = _css(dragEl),
          ghostRect;

        ghostEl = dragEl.cloneNode(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');

        rootEl.appendChild(ghostEl);

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

        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, 150);
      }
      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,
        revert,
        options = this.options,
        group = options.group,
        groupPut = group.put,
        isOwner = (activeGroup === group),
        canSort = options.sort;

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

      if (activeGroup && !options.disabled &&
        (isOwner
          ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
          : activeGroup.pull && groupPut && (
            (activeGroup.name === group.name) || // by Name
            (groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array
          )
        ) &&
        (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();


        if (revert) {
          _cloneHide(true);

          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 = _ghostInBottom(el, evt))
        ) {
          if (target) {
            if (target.animated) {
              return;
            }
            targetRect = target.getBoundingClientRect();
          }

          _cloneHide(isOwner);

          if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect) !== false) {
            el.appendChild(dragEl);
            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);
          }


          var targetRect = target.getBoundingClientRect(),
            width = targetRect.right - targetRect.left,
            height = targetRect.bottom - targetRect.top,
            floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display),
            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),
            after
          ;

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

            _cloneHide(isOwner);

            if (moveVector === 1 || moveVector === -1) {
              after = (moveVector === 1);
            }
            else if (floating) {
              after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
            } else {
              after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
            }

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

            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, 'drop', this);
      _off(document, 'mousemove', this._onTouchMove);
      _off(el, 'dragstart', this._onDragStart);

      this._offUpEvents();

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

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

        if (dragEl) {
          _off(dragEl, 'dragend', this);

          _disableDraggable(dragEl);
          _toggleClass(dragEl, this.options.ghostClass, false);

          if (rootEl !== dragEl.parentNode) {
            newIndex = _index(dragEl);

            // drag from one list and drop into another
            _dispatchEvent(null, dragEl.parentNode, 'sort', dragEl, rootEl, oldIndex, newIndex);
            _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);

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

            // Remove event
            _dispatchEvent(this, rootEl, 'remove', 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);

              // 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) {
            // Drag end event
            _dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex);

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

        // Nulling
        rootEl =
        dragEl =
        ghostEl =
        nextEl =
        cloneEl =

        scrollEl =
        scrollParentEl =

        tapEvt =
        touchEvt =

        lastEl =
        lastCSS =

        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;
      }
    },


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

      el[expando] = null;

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

      _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 _bind(ctx, fn) {
    var args = slice.call(arguments, 2);
    return  fn.bind ? fn.bind.apply(fn, [ctx].concat(args)) : function () {
      return fn.apply(ctx, args.concat(slice.call(arguments)));
    };
  }


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

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

      do {
        if (
          (tag === '>*' && el.parentNode === ctx) || (
            (tag === '' || el.nodeName.toUpperCase() == tag) &&
            (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length)
          )
        ) {
          return el;
        }
      }
      while (el !== ctx && (el = el.parentNode));
    }

    return null;
  }


  function _globalDragOver(/**Event*/evt) {
    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) {
    var evt = document.createEvent('Event'),
      options = (sortable || rootEl[expando]).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) {
    var evt,
      sortable = fromEl[expando],
      onMoveFn = sortable.options.onMove,
      retVal;

    if (onMoveFn) {
      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();

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

    return retVal;
  }


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


  function _unsilent() {
    _silent = false;
  }


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

    return (evt.clientY - (rect.top + rect.height) > 5) && lastEl; // min delta
  }


  /**
   * 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
   * @param el
   * @returns {number}
   * @private
   */
  function _index(/**HTMLElement*/el) {
    var index = 0;
    while (el && (el = el.previousElementSibling)) {
      if (el.nodeName.toUpperCase() !== 'TEMPLATE') {
        index++;
      }
    }
    return index;
  }

  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;
  }


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


  Sortable.version = '1.2.1';


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

  // Export
  return Sortable;
});