/**
 * Copyright (c) 2015-present, Haltu Oy
 * Released under the MIT license
 * https://github.com/haltu/muuri/blob/master/LICENSE.md
 */

import {
  ACTION_MOVE,
  ACTION_SWAP,
  EVENT_MOVE,
  EVENT_SEND,
  EVENT_BEFORE_SEND,
  EVENT_RECEIVE,
  EVENT_BEFORE_RECEIVE,
  EVENT_DRAG_INIT,
  EVENT_DRAG_START,
  EVENT_DRAG_MOVE,
  EVENT_DRAG_SCROLL,
  EVENT_DRAG_END,
  GRID_INSTANCES,
} from '../constants';

import Dragger from '../Dragger/Dragger';
import AutoScroller from '../AutoScroller/AutoScroller';

import {
  addDragStartTick,
  cancelDragStartTick,
  addDragMoveTick,
  cancelDragMoveTick,
  addDragScrollTick,
  cancelDragScrollTick,
  addDragSortTick,
  cancelDragSortTick,
} from '../ticker';

import addClass from '../utils/addClass';
import arrayInsert from '../utils/arrayInsert';
import arrayMove from '../utils/arrayMove';
import arraySwap from '../utils/arraySwap';
import getContainingBlock from '../utils/getContainingBlock';
import getIntersectionScore from '../utils/getIntersectionScore';
import getOffsetDiff from '../utils/getOffsetDiff';
import getScrollableAncestors from '../utils/getScrollableAncestors';
import getStyle from '../utils/getStyle';
import getTranslate from '../utils/getTranslate';
import hasPassiveEvents from '../utils/hasPassiveEvents';
import isFunction from '../utils/isFunction';
import normalizeArrayIndex from '../utils/normalizeArrayIndex';
import removeClass from '../utils/removeClass';

var IS_IOS =
  /^(iPad|iPhone|iPod)/.test(window.navigator.platform) ||
  (/^Mac/.test(window.navigator.platform) && window.navigator.maxTouchPoints > 1);
var START_PREDICATE_INACTIVE = 0;
var START_PREDICATE_PENDING = 1;
var START_PREDICATE_RESOLVED = 2;
var SCROLL_LISTENER_OPTIONS = hasPassiveEvents() ? { passive: true } : false;

/**
 * Bind touch interaction to an item.
 *
 * @class
 * @param {Item} item
 */
function ItemDrag(item) {
  var element = item._element;
  var grid = item.getGrid();
  var settings = grid._settings;

  this._item = item;
  this._gridId = grid._id;
  this._isDestroyed = false;
  this._isMigrating = false;

  // Start predicate data.
  this._startPredicate = isFunction(settings.dragStartPredicate)
    ? settings.dragStartPredicate
    : ItemDrag.defaultStartPredicate;
  this._startPredicateState = START_PREDICATE_INACTIVE;
  this._startPredicateResult = undefined;

  // Data for drag sort predicate heuristics.
  this._isSortNeeded = false;
  this._sortTimer = undefined;
  this._blockedSortIndex = null;
  this._sortX1 = 0;
  this._sortX2 = 0;
  this._sortY1 = 0;
  this._sortY2 = 0;

  // Setup item's initial drag data.
  this._reset();

  // Bind the methods that needs binding.
  this._preStartCheck = this._preStartCheck.bind(this);
  this._preEndCheck = this._preEndCheck.bind(this);
  this._onScroll = this._onScroll.bind(this);
  this._prepareStart = this._prepareStart.bind(this);
  this._applyStart = this._applyStart.bind(this);
  this._prepareMove = this._prepareMove.bind(this);
  this._applyMove = this._applyMove.bind(this);
  this._prepareScroll = this._prepareScroll.bind(this);
  this._applyScroll = this._applyScroll.bind(this);
  this._handleSort = this._handleSort.bind(this);
  this._handleSortDelayed = this._handleSortDelayed.bind(this);

  // Get drag handle element.
  this._handle = (settings.dragHandle && element.querySelector(settings.dragHandle)) || element;

  // Init dragger.
  this._dragger = new Dragger(this._handle, settings.dragCssProps);
  this._dragger.on('start', this._preStartCheck);
  this._dragger.on('move', this._preStartCheck);
  this._dragger.on('cancel', this._preEndCheck);
  this._dragger.on('end', this._preEndCheck);
}

/**
 * Public properties
 * *****************
 */

/**
 * @public
 * @static
 * @type {AutoScroller}
 */
ItemDrag.autoScroller = new AutoScroller();

/**
 * Public static methods
 * *********************
 */

/**
 * Default drag start predicate handler that handles anchor elements
 * gracefully. The return value of this function defines if the drag is
 * started, rejected or pending. When true is returned the dragging is started
 * and when false is returned the dragging is rejected. If nothing is returned
 * the predicate will be called again on the next drag movement.
 *
 * @public
 * @static
 * @param {Item} item
 * @param {Object} event
 * @param {Object} [options]
 *   - An optional options object which can be used to pass the predicate
 *     it's options manually. By default the predicate retrieves the options
 *     from the grid's settings.
 * @returns {(Boolean|undefined)}
 */
ItemDrag.defaultStartPredicate = function (item, event, options) {
  var drag = item._drag;

  // Make sure left button is pressed on mouse.
  if (event.isFirst && event.srcEvent.button) {
    return false;
  }

  // If the start event is trusted, non-cancelable and it's default action has
  // not been prevented it is in most cases a sign that the gesture would be
  // cancelled anyways right after it has started (e.g. starting drag while
  // the page is scrolling).
  if (
    !IS_IOS &&
    event.isFirst &&
    event.srcEvent.isTrusted === true &&
    event.srcEvent.defaultPrevented === false &&
    event.srcEvent.cancelable === false
  ) {
    return false;
  }

  // Final event logic. At this stage return value does not matter anymore,
  // the predicate is either resolved or it's not and there's nothing to do
  // about it. Here we just reset data and if the item element is a link
  // we follow it (if there has only been slight movement).
  if (event.isFinal) {
    drag._finishStartPredicate(event);
    return;
  }

  // Setup predicate data from options if not already set.
  var predicate = drag._startPredicateData;
  if (!predicate) {
    var config = options || drag._getGrid()._settings.dragStartPredicate || {};
    drag._startPredicateData = predicate = {
      distance: Math.max(config.distance, 0) || 0,
      delay: Math.max(config.delay, 0) || 0,
    };
  }

  // If delay is defined let's keep track of the latest event and initiate
  // delay if it has not been done yet.
  if (predicate.delay) {
    predicate.event = event;
    if (!predicate.delayTimer) {
      predicate.delayTimer = window.setTimeout(function () {
        predicate.delay = 0;
        if (drag._resolveStartPredicate(predicate.event)) {
          drag._forceResolveStartPredicate(predicate.event);
          drag._resetStartPredicate();
        }
      }, predicate.delay);
    }
  }

  return drag._resolveStartPredicate(event);
};

/**
 * Default drag sort predicate.
 *
 * @public
 * @static
 * @param {Item} item
 * @param {Object} [options]
 * @param {Number} [options.threshold=50]
 * @param {String} [options.action='move']
 * @returns {?Object}
 *   - Returns `null` if no valid index was found. Otherwise returns drag sort
 *     command.
 */
ItemDrag.defaultSortPredicate = (function () {
  var itemRect = {};
  var targetRect = {};
  var returnData = {};
  var gridsArray = [];
  var minThreshold = 1;
  var maxThreshold = 100;
  var pointerRows = [];
  var pointerTargets = [];
  var pointerOrdered = [];
  var pointerRowToleranceRatio = 0.6;
  var pointerEdgePadding = 6;

  function resolvePointer(event) {
    if (!event) return null;
    var x =
      typeof event.clientX === 'number'
        ? event.clientX
        : typeof event.pageX === 'number'
        ? event.pageX
        : typeof event.x === 'number'
        ? event.x
        : event.srcEvent && typeof event.srcEvent.clientX === 'number'
        ? event.srcEvent.clientX
        : event.srcEvent && typeof event.srcEvent.pageX === 'number'
        ? event.srcEvent.pageX
        : undefined;
    var y =
      typeof event.clientY === 'number'
        ? event.clientY
        : typeof event.pageY === 'number'
        ? event.pageY
        : typeof event.y === 'number'
        ? event.y
        : event.srcEvent && typeof event.srcEvent.clientY === 'number'
        ? event.srcEvent.clientY
        : event.srcEvent && typeof event.srcEvent.pageY === 'number'
        ? event.srcEvent.pageY
        : undefined;
    if (x === undefined || y === undefined) return null;
    return { x: x, y: y };
  }

  function getVisibleGridRect(grid) {
    var left = Math.max(0, grid._left);
    var top = Math.max(0, grid._top);
    var right = Math.min(window.innerWidth, grid._right);
    var bottom = Math.min(window.innerHeight, grid._bottom);
    var container = grid._element.parentNode;
    var containerRect;

    while (
      container &&
      container !== document &&
      container !== document.documentElement &&
      container !== document.body
    ) {
      if (container.getRootNode && container instanceof DocumentFragment) {
        container = container.getRootNode().host;
        continue;
      }

      if (getStyle(container, 'overflow') !== 'visible') {
        containerRect = container.getBoundingClientRect();
        left = Math.max(left, containerRect.left);
        top = Math.max(top, containerRect.top);
        right = Math.min(right, containerRect.right);
        bottom = Math.min(bottom, containerRect.bottom);
      }

      if (getStyle(container, 'position') === 'fixed') {
        break;
      }

      container = container.parentNode;
    }

    if (left >= right || top >= bottom) return null;
    return { left: left, top: top, right: right, bottom: bottom };
  }

  function buildPointerRows(targets) {
    if (!targets.length) return [];
    var sorted = targets.slice().sort(function (a, b) {
      var topDiff = a.top - b.top;
      if (Math.abs(topDiff) > 1) return topDiff;
      return a.left - b.left;
    });
    var minSize = Infinity;
    for (var i = 0; i < sorted.length; i++) {
      minSize = Math.min(minSize, sorted[i].width, sorted[i].height);
    }
    var tolerance = minSize === Infinity ? 0 : minSize * pointerRowToleranceRatio;
    var rows = [];
    var current = {
      items: [sorted[0]],
      top: sorted[0].top,
      bottom: sorted[0].bottom,
      mid: (sorted[0].top + sorted[0].bottom) / 2,
    };

    for (var j = 1; j < sorted.length; j++) {
      var entry = sorted[j];
      if (Math.abs(entry.top - current.top) <= tolerance) {
        current.items.push(entry);
        current.top = Math.min(current.top, entry.top);
        current.bottom = Math.max(current.bottom, entry.bottom);
        current.mid = (current.top + current.bottom) / 2;
      } else {
        current.items.sort(function (a, b) {
          return a.left - b.left;
        });
        rows.push(current);
        current = {
          items: [entry],
          top: entry.top,
          bottom: entry.bottom,
          mid: (entry.top + entry.bottom) / 2,
        };
      }
    }

    current.items.sort(function (a, b) {
      return a.left - b.left;
    });
    rows.push(current);
    return rows;
  }

  function getPointerInsertIndex(pointer, rows) {
    if (!rows.length) return 0;
    var total = 0;
    for (var i = 0; i < rows.length; i++) {
      total += rows[i].items.length;
    }
    var first = rows[0];
    var last = rows[rows.length - 1];

    if (pointer.y < first.top - pointerEdgePadding) return 0;
    if (pointer.y > last.bottom + pointerEdgePadding) return total;

    var rowIndex = rows.length - 1;
    for (var r = 0; r < rows.length; r++) {
      if (pointer.y < rows[r].mid) {
        rowIndex = r;
        break;
      }
    }

    var row = rows[rowIndex];
    var colIndex = row.items.length;
    for (var c = 0; c < row.items.length; c++) {
      var rect = row.items[c];
      var midX = rect.left + rect.width / 2;
      if (pointer.x < midX) {
        colIndex = c;
        break;
      }
    }

    var baseIndex = 0;
    for (var k = 0; k < rowIndex; k++) {
      baseIndex += rows[k].items.length;
    }
    return baseIndex + colIndex;
  }

  function getTargetGrid(item, rootGrid, threshold) {
    var target = null;
    var dragSort = rootGrid._settings.dragSort;
    var bestScore = -1;
    var gridScore;
    var grids;
    var grid;
    var container;
    var containerRect;
    var left;
    var top;
    var right;
    var bottom;
    var i;

    // Get potential target grids.
    if (dragSort === true) {
      gridsArray[0] = rootGrid;
      grids = gridsArray;
    } else if (isFunction(dragSort)) {
      grids = dragSort.call(rootGrid, item);
    }

    // Return immediately if there are no grids.
    if (!grids || !Array.isArray(grids) || !grids.length) {
      return target;
    }

    // Loop through the grids and get the best match.
    for (i = 0; i < grids.length; i++) {
      grid = grids[i];

      // Filter out all destroyed grids.
      if (grid._isDestroyed) continue;

      // Compute the grid's client rect an clamp the initial boundaries to
      // viewport dimensions.
      grid._updateBoundingRect();
      left = Math.max(0, grid._left);
      top = Math.max(0, grid._top);
      right = Math.min(window.innerWidth, grid._right);
      bottom = Math.min(window.innerHeight, grid._bottom);

      // The grid might be inside one or more elements that clip it's visibility
      // (e.g overflow scroll/hidden) so we want to find out the visible portion
      // of the grid in the viewport and use that in our calculations.
      container = grid._element.parentNode;
      while (
        container &&
        container !== document &&
        container !== document.documentElement &&
        container !== document.body
      ) {
        if (container.getRootNode && container instanceof DocumentFragment) {
          container = container.getRootNode().host;
          continue;
        }

        if (getStyle(container, 'overflow') !== 'visible') {
          containerRect = container.getBoundingClientRect();
          left = Math.max(left, containerRect.left);
          top = Math.max(top, containerRect.top);
          right = Math.min(right, containerRect.right);
          bottom = Math.min(bottom, containerRect.bottom);
        }

        if (getStyle(container, 'position') === 'fixed') {
          break;
        }

        container = container.parentNode;
      }

      // No need to go further if target rect does not have visible area.
      if (left >= right || top >= bottom) continue;

      // Check how much dragged element overlaps the container element.
      targetRect.left = left;
      targetRect.top = top;
      targetRect.width = right - left;
      targetRect.height = bottom - top;
      gridScore = getIntersectionScore(itemRect, targetRect);

      // Check if this grid is the best match so far.
      if (gridScore > threshold && gridScore > bestScore) {
        bestScore = gridScore;
        target = grid;
      }
    }

    // Always reset grids array.
    gridsArray.length = 0;

    return target;
  }

  return function (item, options, event) {
    var drag = item._drag;
    var rootGrid = drag._getGrid();

    // Get drag sort predicate settings.
    var sortThreshold = options && typeof options.threshold === 'number' ? options.threshold : 50;
    var sortAction = options && options.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE;
    var migrateAction =
      options && options.migrateAction === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE;

    // Sort threshold must be a positive number capped to a max value of 100. If
    // that's not the case this function will not work correctly. So let's clamp
    // the threshold just in case.
    sortThreshold = Math.min(Math.max(sortThreshold, minThreshold), maxThreshold);

    // Populate item rect data.
    itemRect.width = item._width;
    itemRect.height = item._height;
    itemRect.left = drag._clientX;
    itemRect.top = drag._clientY;

    // Calculate the target grid.
    var grid = getTargetGrid(item, rootGrid, sortThreshold);

    // Return early if we found no grid container element that overlaps the
    // dragged item enough.
    if (!grid) return null;

    var pointer = resolvePointer(event);

    var isMigration = item.getGrid() !== grid;
    var gridOffsetLeft = 0;
    var gridOffsetTop = 0;
    var matchScore = 0;
    var matchIndex = -1;
    var hasValidTargets = false;
    var target;
    var score;
    var i;

    // If item is moved within it's originating grid adjust item's left and
    // top props. Otherwise if item is moved to/within another grid get the
    // container element's offset (from the element's content edge).
    if (grid === rootGrid) {
      itemRect.left = drag._gridX + item._marginLeft;
      itemRect.top = drag._gridY + item._marginTop;
    } else {
      grid._updateBorders(1, 0, 1, 0);
      gridOffsetLeft = grid._left + grid._borderLeft;
      gridOffsetTop = grid._top + grid._borderTop;
    }

    if (pointer) {
      grid._updateBoundingRect();
      var visibleRect = getVisibleGridRect(grid);
      if (visibleRect) {
        if (
          pointer.x >= visibleRect.left &&
          pointer.x <= visibleRect.right &&
          pointer.y >= visibleRect.top &&
          pointer.y <= visibleRect.bottom
        ) {
          grid._updateBorders(1, 0, 1, 0);
          var clientOffsetLeft = grid._left + grid._borderLeft;
          var clientOffsetTop = grid._top + grid._borderTop;

          pointerTargets.length = 0;
          for (i = 0; i < grid._items.length; i++) {
            target = grid._items[i];
            if (!target._isActive || target === item) {
              continue;
            }
            hasValidTargets = true;
            var left = target._left + target._marginLeft + clientOffsetLeft;
            var top = target._top + target._marginTop + clientOffsetTop;
            var width = target._width;
            var height = target._height;
            pointerTargets.push({
              index: i,
              left: left,
              top: top,
              right: left + width,
              bottom: top + height,
              width: width,
              height: height,
            });
          }

          if (!hasValidTargets) {
            returnData.grid = grid;
            returnData.index = 0;
            returnData.action = isMigration ? migrateAction : sortAction;
            return returnData;
          }

          pointerRows = buildPointerRows(pointerTargets);
          pointerOrdered = [];
          for (i = 0; i < pointerRows.length; i++) {
            pointerOrdered = pointerOrdered.concat(pointerRows[i].items);
          }
          var insertIndex = getPointerInsertIndex(pointer, pointerRows);
          var targetIndex = insertIndex;
          if (targetIndex < 0) targetIndex = 0;
          if (targetIndex > grid._items.length - 1) {
            targetIndex = grid._items.length - 1;
          }
          if (targetIndex === grid._items.indexOf(item)) {
            return null;
          }
          returnData.grid = grid;
          returnData.index = targetIndex;
          returnData.action = isMigration ? migrateAction : sortAction;
          return returnData;
        }
      }
    }

    // Loop through the target grid items and try to find the best match.
    for (i = 0; i < grid._items.length; i++) {
      target = grid._items[i];

      // If the target item is not active or the target item is the dragged
      // item let's skip to the next item.
      if (!target._isActive || target === item) {
        continue;
      }

      // Mark the grid as having valid target items.
      hasValidTargets = true;

      // Calculate the target's overlap score with the dragged item.
      targetRect.width = target._width;
      targetRect.height = target._height;
      targetRect.left = target._left + target._marginLeft + gridOffsetLeft;
      targetRect.top = target._top + target._marginTop + gridOffsetTop;
      score = getIntersectionScore(itemRect, targetRect);

      // Update best match index and score if the target's overlap score with
      // the dragged item is higher than the current best match score.
      if (score > matchScore) {
        matchIndex = i;
        matchScore = score;
      }
    }

    // If there is no valid match and the dragged item is being moved into
    // another grid we need to do some guess work here. If there simply are no
    // valid targets (which means that the dragged item will be the only active
    // item in the new grid) we can just add it as the first item. If we have
    // valid items in the new grid and the dragged item is overlapping one or
    // more of the items in the new grid let's make an exception with the
    // threshold and just pick the item which the dragged item is overlapping
    // most. However, if the dragged item is not overlapping any of the valid
    // items in the new grid let's position it as the last item in the grid.
    if (isMigration && matchScore < sortThreshold) {
      matchIndex = hasValidTargets ? matchIndex : 0;
      matchScore = sortThreshold;
    }

    // Check if the best match overlaps enough to justify a placement switch.
    if (matchScore >= sortThreshold) {
      returnData.grid = grid;
      returnData.index = matchIndex;
      returnData.action = isMigration ? migrateAction : sortAction;
      return returnData;
    }

    return null;
  };
})();

/**
 * Public prototype methods
 * ************************
 */

/**
 * Abort dragging and reset drag data.
 *
 * @public
 */
ItemDrag.prototype.stop = function () {
  if (!this._isActive) return;

  // If the item is being dropped into another grid, finish it up and return
  // immediately.
  if (this._isMigrating) {
    this._finishMigration();
    return;
  }

  var item = this._item;
  var itemId = item._id;

  // Stop auto-scroll.
  ItemDrag.autoScroller.removeItem(item);

  // Cancel queued ticks.
  cancelDragStartTick(itemId);
  cancelDragMoveTick(itemId);
  cancelDragScrollTick(itemId);

  // Cancel sort procedure.
  this._cancelSort();

  if (this._isStarted) {
    // Remove scroll listeners.
    this._unbindScrollListeners();

    var element = item._element;
    var grid = this._getGrid();
    var draggingClass = grid._settings.itemDraggingClass;

    // Append item element to the container if it's not it's child. Also make
    // sure the translate values are adjusted to account for the DOM shift.
    if (element.parentNode !== grid._element) {
      grid._element.appendChild(element);
      item._setTranslate(this._gridX, this._gridY);

      // We need to do forced reflow to make sure the dragging class is removed
      // gracefully.
      // eslint-disable-next-line
      if (draggingClass) element.clientWidth;
    }

    // Remove dragging class.
    removeClass(element, draggingClass);
  }

  // Reset drag data.
  this._reset();
};

/**
 * Manually trigger drag sort. This is only needed for special edge cases where
 * e.g. you have disabled sort and want to trigger a sort right after enabling
 * it (and don't want to wait for the next move/scroll event).
 *
 * @private
 * @param {Boolean} [force=false]
 */
ItemDrag.prototype.sort = function (force) {
  var item = this._item;
  if (this._isActive && item._isActive && this._dragMoveEvent) {
    if (force === true) {
      this._handleSort(true);
    } else {
      addDragSortTick(item._id, this._handleSort);
    }
  }
};

/**
 * Destroy instance.
 *
 * @public
 */
ItemDrag.prototype.destroy = function () {
  if (this._isDestroyed) return;
  this.stop();
  this._dragger.destroy();
  ItemDrag.autoScroller.removeItem(this._item);
  this._isDestroyed = true;
};

/**
 * Private prototype methods
 * *************************
 */

/**
 * Get Grid instance.
 *
 * @private
 * @returns {?Grid}
 */
ItemDrag.prototype._getGrid = function () {
  return GRID_INSTANCES[this._gridId] || null;
};

/**
 * Setup/reset drag data.
 *
 * @private
 */
ItemDrag.prototype._reset = function () {
  this._isActive = false;
  this._isStarted = false;

  // The dragged item's container element.
  this._container = null;

  // The dragged item's containing block.
  this._containingBlock = null;

  // Drag/scroll event data.
  this._dragStartEvent = null;
  this._dragMoveEvent = null;
  this._dragPrevMoveEvent = null;
  this._scrollEvent = null;

  // All the elements which need to be listened for scroll events during
  // dragging.
  this._scrollers = [];

  // The current translateX/translateY position.
  this._left = 0;
  this._top = 0;

  // Dragged element's current position within the grid.
  this._gridX = 0;
  this._gridY = 0;

  // Dragged element's current offset from window's northwest corner. Does
  // not account for element's margins.
  this._clientX = 0;
  this._clientY = 0;

  // Keep track of the clientX/Y diff for scrolling.
  this._scrollDiffX = 0;
  this._scrollDiffY = 0;

  // Keep track of the clientX/Y diff for moving.
  this._moveDiffX = 0;
  this._moveDiffY = 0;

  // Offset difference between the dragged element's temporary drag
  // container and it's original container.
  this._containerDiffX = 0;
  this._containerDiffY = 0;
};

/**
 * Bind drag scroll handlers to all scrollable ancestor elements of the
 * dragged element and the drag container element.
 *
 * @private
 */
ItemDrag.prototype._bindScrollListeners = function () {
  var gridContainer = this._getGrid()._element;
  var dragContainer = this._container;
  var scrollers = this._scrollers;
  var gridScrollers;
  var i;

  // Get dragged element's scrolling parents.
  scrollers.length = 0;
  getScrollableAncestors(this._item._element.parentNode, scrollers);

  // If drag container is defined and it's not the same element as grid
  // container then we need to add the grid container and it's scroll parents
  // to the elements which are going to be listener for scroll events.
  if (dragContainer !== gridContainer) {
    gridScrollers = [];
    getScrollableAncestors(gridContainer, gridScrollers);
    for (i = 0; i < gridScrollers.length; i++) {
      if (scrollers.indexOf(gridScrollers[i]) < 0) {
        scrollers.push(gridScrollers[i]);
      }
    }
  }

  // Bind scroll listeners.
  for (i = 0; i < scrollers.length; i++) {
    scrollers[i].addEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS);
  }
};

/**
 * Unbind currently bound drag scroll handlers from all scrollable ancestor
 * elements of the dragged element and the drag container element.
 *
 * @private
 */
ItemDrag.prototype._unbindScrollListeners = function () {
  var scrollers = this._scrollers;
  var i;

  for (i = 0; i < scrollers.length; i++) {
    scrollers[i].removeEventListener('scroll', this._onScroll, SCROLL_LISTENER_OPTIONS);
  }

  scrollers.length = 0;
};

/**
 * Unbind currently bound drag scroll handlers from all scrollable ancestor
 * elements of the dragged element and the drag container element.
 *
 * @private
 * @param {Object} event
 * @returns {Boolean}
 */
ItemDrag.prototype._resolveStartPredicate = function (event) {
  var predicate = this._startPredicateData;
  if (event.distance < predicate.distance || predicate.delay) return;
  this._resetStartPredicate();
  return true;
};

/**
 * Forcefully resolve drag start predicate.
 *
 * @private
 * @param {Object} event
 */
ItemDrag.prototype._forceResolveStartPredicate = function (event) {
  if (!this._isDestroyed && this._startPredicateState === START_PREDICATE_PENDING) {
    this._startPredicateState = START_PREDICATE_RESOLVED;
    this._onStart(event);
  }
};

/**
 * Finalize start predicate.
 *
 * @private
 * @param {Object} event
 */
ItemDrag.prototype._finishStartPredicate = function (event) {
  var element = this._item._element;

  // Check if this is a click (very subjective heuristics).
  var isClick = Math.abs(event.deltaX) < 2 && Math.abs(event.deltaY) < 2 && event.deltaTime < 200;

  // Reset predicate.
  this._resetStartPredicate();

  // If the gesture can be interpreted as click let's try to open the element's
  // href url (if it is an anchor element).
  if (isClick) openAnchorHref(element);
};

/**
 * Reset drag sort heuristics.
 *
 * @private
 * @param {Number} x
 * @param {Number} y
 */
ItemDrag.prototype._resetHeuristics = function (x, y) {
  this._blockedSortIndex = null;
  this._sortX1 = this._sortX2 = x;
  this._sortY1 = this._sortY2 = y;
};

/**
 * Run heuristics and return true if overlap check can be performed, and false
 * if it can not.
 *
 * @private
 * @param {Number} x
 * @param {Number} y
 * @returns {Boolean}
 */
ItemDrag.prototype._checkHeuristics = function (x, y) {
  var settings = this._getGrid()._settings.dragSortHeuristics;
  var minDist = settings.minDragDistance;

  // Skip heuristics if not needed.
  if (minDist <= 0) {
    this._blockedSortIndex = null;
    return true;
  }

  var diffX = x - this._sortX2;
  var diffY = y - this._sortY2;

  // If we can't do proper bounce back check make sure that the blocked index
  // is not set.
  var canCheckBounceBack = minDist > 3 && settings.minBounceBackAngle > 0;
  if (!canCheckBounceBack) {
    this._blockedSortIndex = null;
  }

  if (Math.abs(diffX) > minDist || Math.abs(diffY) > minDist) {
    // Reset blocked index if angle changed enough. This check requires a
    // minimum value of 3 for minDragDistance to function properly.
    if (canCheckBounceBack) {
      var angle = Math.atan2(diffX, diffY);
      var prevAngle = Math.atan2(this._sortX2 - this._sortX1, this._sortY2 - this._sortY1);
      var deltaAngle = Math.atan2(Math.sin(angle - prevAngle), Math.cos(angle - prevAngle));
      if (Math.abs(deltaAngle) > settings.minBounceBackAngle) {
        this._blockedSortIndex = null;
      }
    }

    // Update points.
    this._sortX1 = this._sortX2;
    this._sortY1 = this._sortY2;
    this._sortX2 = x;
    this._sortY2 = y;

    return true;
  }

  return false;
};

/**
 * Reset for default drag start predicate function.
 *
 * @private
 */
ItemDrag.prototype._resetStartPredicate = function () {
  var predicate = this._startPredicateData;
  if (predicate) {
    if (predicate.delayTimer) {
      predicate.delayTimer = window.clearTimeout(predicate.delayTimer);
    }
    this._startPredicateData = null;
  }
};

/**
 * Handle the sorting procedure. Manage drag sort heuristics/interval and
 * check overlap when necessary.
 *
 * @private
 */
ItemDrag.prototype._handleSort = function (force) {
  if (!this._isActive) return;

  var settings = this._getGrid()._settings;
  var sortLock = settings.dragSortLock;

  if (typeof sortLock === 'function' && sortLock(this._item, this._dragMoveEvent) === true) {
    this._sortX1 = this._sortX2 = this._gridX;
    this._sortY1 = this._sortY2 = this._gridY;
    this._isSortNeeded = true;
    if (this._sortTimer !== undefined) {
      this._sortTimer = window.clearTimeout(this._sortTimer);
    }
    return;
  }

  // No sorting when drag sort is disabled. Also, account for the scenario where
  // dragSort is temporarily disabled during drag procedure so we need to reset
  // sort timer heuristics state too.
  if (
    !settings.dragSort ||
    (!settings.dragAutoScroll.sortDuringScroll && ItemDrag.autoScroller.isItemScrolling(this._item))
  ) {
    this._sortX1 = this._sortX2 = this._gridX;
    this._sortY1 = this._sortY2 = this._gridY;
    // We set this to true intentionally so that overlap check would be
    // triggered as soon as possible after sort becomes enabled again.
    this._isSortNeeded = true;
    if (this._sortTimer !== undefined) {
      this._sortTimer = window.clearTimeout(this._sortTimer);
    }
    return;
  }

  // If sorting is enabled we always need to run the heuristics check to keep
  // the tracked coordinates updated. We also allow an exception when the sort
  // timer is finished because the heuristics are intended to prevent overlap
  // checks based on the dragged element's immediate movement and a delayed
  // overlap check is valid if it comes through, because it was valid when it
  // was invoked.
  var shouldSort = force === true || this._checkHeuristics(this._gridX, this._gridY);
  if (!this._isSortNeeded && !shouldSort) return;

  var sortInterval = settings.dragSortHeuristics.sortInterval;
  if (sortInterval <= 0 || this._isSortNeeded) {
    this._isSortNeeded = false;
    if (this._sortTimer !== undefined) {
      this._sortTimer = window.clearTimeout(this._sortTimer);
    }
    this._checkOverlap();
  } else if (this._sortTimer === undefined) {
    this._sortTimer = window.setTimeout(this._handleSortDelayed, sortInterval);
  }
};

/**
 * Delayed sort handler.
 *
 * @private
 */
ItemDrag.prototype._handleSortDelayed = function () {
  this._isSortNeeded = true;
  this._sortTimer = undefined;
  addDragSortTick(this._item._id, this._handleSort);
};

/**
 * Cancel and reset sort procedure.
 *
 * @private
 */
ItemDrag.prototype._cancelSort = function () {
  this._isSortNeeded = false;
  if (this._sortTimer !== undefined) {
    this._sortTimer = window.clearTimeout(this._sortTimer);
  }
  cancelDragSortTick(this._item._id);
};

/**
 * Handle the ending of the drag procedure for sorting.
 *
 * @private
 */
ItemDrag.prototype._finishSort = function () {
  var isSortEnabled = this._getGrid()._settings.dragSort;
  var needsFinalCheck = isSortEnabled && (this._isSortNeeded || this._sortTimer !== undefined);
  this._cancelSort();
  if (needsFinalCheck) this._checkOverlap();
};

/**
 * Check (during drag) if an item is overlapping other items and based on
 * the configuration layout the items.
 *
 * @private
 */
ItemDrag.prototype._checkOverlap = function () {
  if (!this._isActive) return;

  var item = this._item;
  var settings = this._getGrid()._settings;
  var result;
  var currentGrid;
  var currentIndex;
  var targetGrid;
  var targetIndex;
  var targetItem;
  var sortAction;
  var isMigration;

  // Get overlap check result.
  if (isFunction(settings.dragSortPredicate)) {
    result = settings.dragSortPredicate(item, this._dragMoveEvent);
  } else {
    result = ItemDrag.defaultSortPredicate(item, settings.dragSortPredicate, this._dragMoveEvent);
  }

  // Let's make sure the result object has a valid index before going further.
  if (!result || typeof result.index !== 'number') return;

  sortAction = result.action === ACTION_SWAP ? ACTION_SWAP : ACTION_MOVE;
  currentGrid = item.getGrid();
  targetGrid = result.grid || currentGrid;
  isMigration = currentGrid !== targetGrid;
  currentIndex = currentGrid._items.indexOf(item);
  targetIndex = normalizeArrayIndex(
    targetGrid._items,
    result.index,
    isMigration && sortAction === ACTION_MOVE ? 1 : 0
  );

  // Prevent position bounce.
  if (!isMigration && targetIndex === this._blockedSortIndex) {
    return;
  }

  // If the item was moved within it's current grid.
  if (!isMigration) {
    // Make sure the target index is not the current index.
    if (currentIndex !== targetIndex) {
      this._blockedSortIndex = currentIndex;

      // Do the sort.
      (sortAction === ACTION_SWAP ? arraySwap : arrayMove)(
        currentGrid._items,
        currentIndex,
        targetIndex
      );

      // Emit move event.
      if (currentGrid._hasListeners(EVENT_MOVE)) {
        currentGrid._emit(EVENT_MOVE, {
          item: item,
          fromIndex: currentIndex,
          toIndex: targetIndex,
          action: sortAction,
        });
      }

      // Layout the grid.
      currentGrid.layout();
    }
  }

  // If the item was moved to another grid.
  else {
    this._blockedSortIndex = null;

    // Let's fetch the target item when it's still in it's original index.
    targetItem = targetGrid._items[targetIndex];

    // Emit beforeSend event.
    if (currentGrid._hasListeners(EVENT_BEFORE_SEND)) {
      currentGrid._emit(EVENT_BEFORE_SEND, {
        item: item,
        fromGrid: currentGrid,
        fromIndex: currentIndex,
        toGrid: targetGrid,
        toIndex: targetIndex,
      });
    }

    // Emit beforeReceive event.
    if (targetGrid._hasListeners(EVENT_BEFORE_RECEIVE)) {
      targetGrid._emit(EVENT_BEFORE_RECEIVE, {
        item: item,
        fromGrid: currentGrid,
        fromIndex: currentIndex,
        toGrid: targetGrid,
        toIndex: targetIndex,
      });
    }

    // Update item's grid id reference.
    item._gridId = targetGrid._id;

    // Update drag instance's migrating indicator.
    this._isMigrating = item._gridId !== this._gridId;

    // Move item instance from current grid to target grid.
    currentGrid._items.splice(currentIndex, 1);
    arrayInsert(targetGrid._items, item, targetIndex);

    // Reset sort data.
    item._sortData = null;

    // Emit send event.
    if (currentGrid._hasListeners(EVENT_SEND)) {
      currentGrid._emit(EVENT_SEND, {
        item: item,
        fromGrid: currentGrid,
        fromIndex: currentIndex,
        toGrid: targetGrid,
        toIndex: targetIndex,
      });
    }

    // Emit receive event.
    if (targetGrid._hasListeners(EVENT_RECEIVE)) {
      targetGrid._emit(EVENT_RECEIVE, {
        item: item,
        fromGrid: currentGrid,
        fromIndex: currentIndex,
        toGrid: targetGrid,
        toIndex: targetIndex,
      });
    }

    // If the sort action is "swap" let's respect it and send the target item
    // (if it exists) from the target grid to the originating grid. This process
    // is done on purpose after the dragged item placed within the target grid
    // so that we can keep this implementation as simple as possible utilizing
    // the existing API.
    if (sortAction === ACTION_SWAP && targetItem && targetItem.isActive()) {
      // Sanity check to make sure that the target item is still part of the
      // target grid. It could have been manipulated in the event handlers.
      if (targetGrid._items.indexOf(targetItem) > -1) {
        targetGrid.send(targetItem, currentGrid, currentIndex, {
          appendTo: this._container || document.body,
          layoutSender: false,
          layoutReceiver: false,
        });
      }
    }

    // Layout both grids.
    currentGrid.layout();
    targetGrid.layout();
  }
};

/**
 * If item is dragged into another grid, finish the migration process
 * gracefully.
 *
 * @private
 */
ItemDrag.prototype._finishMigration = function () {
  var item = this._item;
  var release = item._dragRelease;
  var element = item._element;
  var isActive = item._isActive;
  var targetGrid = item.getGrid();
  var targetGridElement = targetGrid._element;
  var targetSettings = targetGrid._settings;
  var targetContainer = targetSettings.dragContainer || targetGridElement;
  var currentSettings = this._getGrid()._settings;
  var currentContainer = element.parentNode;
  var currentVisClass = isActive
    ? currentSettings.itemVisibleClass
    : currentSettings.itemHiddenClass;
  var nextVisClass = isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass;
  var translate;
  var offsetDiff;

  // Destroy current drag. Note that we need to set the migrating flag to
  // false first, because otherwise we create an infinite loop between this
  // and the drag.stop() method.
  this._isMigrating = false;
  this.destroy();

  // Update item class.
  if (currentSettings.itemClass !== targetSettings.itemClass) {
    removeClass(element, currentSettings.itemClass);
    addClass(element, targetSettings.itemClass);
  }

  // Update visibility class.
  if (currentVisClass !== nextVisClass) {
    removeClass(element, currentVisClass);
    addClass(element, nextVisClass);
  }

  // Move the item inside the target container if it's different than the
  // current container.
  if (targetContainer !== currentContainer) {
    targetContainer.appendChild(element);
    offsetDiff = getOffsetDiff(currentContainer, targetContainer, true);
    translate = getTranslate(element);
    translate.x -= offsetDiff.left;
    translate.y -= offsetDiff.top;
  }

  // Update item's cached dimensions.
  item._refreshDimensions();

  // Calculate the offset difference between target's drag container (if any)
  // and actual grid container element. We save it later for the release
  // process.
  offsetDiff = getOffsetDiff(targetContainer, targetGridElement, true);
  release._containerDiffX = offsetDiff.left;
  release._containerDiffY = offsetDiff.top;

  // Recreate item's drag handler.
  item._drag = targetSettings.dragEnabled ? new ItemDrag(item) : null;

  // Adjust the position of the item element if it was moved from a container
  // to another.
  if (targetContainer !== currentContainer) {
    item._setTranslate(translate.x, translate.y);
  }

  // Update child element's styles to reflect the current visibility state.
  item._visibility.setStyles(isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles);

  // Start the release.
  release.start();
};

/**
 * Drag pre-start handler.
 *
 * @private
 * @param {Object} event
 */
ItemDrag.prototype._preStartCheck = function (event) {
  // Let's activate drag start predicate state.
  if (this._startPredicateState === START_PREDICATE_INACTIVE) {
    this._startPredicateState = START_PREDICATE_PENDING;
  }

  // If predicate is pending try to resolve it.
  if (this._startPredicateState === START_PREDICATE_PENDING) {
    this._startPredicateResult = this._startPredicate(this._item, event);
    if (this._startPredicateResult === true) {
      this._startPredicateState = START_PREDICATE_RESOLVED;
      this._onStart(event);
    } else if (this._startPredicateResult === false) {
      this._resetStartPredicate(event);
      this._dragger._reset();
      this._startPredicateState = START_PREDICATE_INACTIVE;
    }
  }

  // Otherwise if predicate is resolved and drag is active, move the item.
  else if (this._startPredicateState === START_PREDICATE_RESOLVED && this._isActive) {
    this._onMove(event);
  }
};

/**
 * Drag pre-end handler.
 *
 * @private
 * @param {Object} event
 */
ItemDrag.prototype._preEndCheck = function (event) {
  var isResolved = this._startPredicateState === START_PREDICATE_RESOLVED;

  // Do final predicate check to allow user to unbind stuff for the current
  // drag procedure within the predicate callback. The return value of this
  // check will have no effect to the state of the predicate.
  this._startPredicate(this._item, event);

  this._startPredicateState = START_PREDICATE_INACTIVE;

  if (!isResolved || !this._isActive) return;

  if (this._isStarted) {
    this._onEnd(event);
  } else {
    this.stop();
  }
};

/**
 * Drag start handler.
 *
 * @private
 * @param {Object} event
 */
ItemDrag.prototype._onStart = function (event) {
  var item = this._item;
  if (!item._isActive) return;

  this._isActive = true;
  this._dragStartEvent = event;
  ItemDrag.autoScroller.addItem(item);

  addDragStartTick(item._id, this._prepareStart, this._applyStart);
};

/**
 * Prepare item to be dragged.
 *
 * @private
 *  ItemDrag.prototype
 */
ItemDrag.prototype._prepareStart = function () {
  if (!this._isActive) return;

  var item = this._item;
  if (!item._isActive) return;

  var element = item._element;
  var grid = this._getGrid();
  var settings = grid._settings;
  var gridContainer = grid._element;
  var dragContainer = settings.dragContainer || gridContainer;
  var containingBlock = getContainingBlock(dragContainer);
  var translate = getTranslate(element);
  var elementRect = element.getBoundingClientRect();
  var hasDragContainer = dragContainer !== gridContainer;

  this._container = dragContainer;
  this._containingBlock = containingBlock;
  this._clientX = elementRect.left;
  this._clientY = elementRect.top;
  this._left = this._gridX = translate.x;
  this._top = this._gridY = translate.y;
  this._scrollDiffX = this._scrollDiffY = 0;
  this._moveDiffX = this._moveDiffY = 0;

  this._resetHeuristics(this._gridX, this._gridY);

  // If a specific drag container is set and it is different from the
  // grid's container element we store the offset between containers.
  if (hasDragContainer) {
    var offsetDiff = getOffsetDiff(containingBlock, gridContainer);
    this._containerDiffX = offsetDiff.left;
    this._containerDiffY = offsetDiff.top;
  }
};

/**
 * Start drag for the item.
 *
 * @private
 */
ItemDrag.prototype._applyStart = function () {
  if (!this._isActive) return;

  var item = this._item;
  if (!item._isActive) return;

  var grid = this._getGrid();
  var element = item._element;
  var release = item._dragRelease;
  var migrate = item._migrate;
  var hasDragContainer = this._container !== grid._element;

  if (item.isPositioning()) {
    item._layout.stop(true, this._left, this._top);
  }

  if (migrate._isActive) {
    this._left -= migrate._containerDiffX;
    this._top -= migrate._containerDiffY;
    this._gridX -= migrate._containerDiffX;
    this._gridY -= migrate._containerDiffY;
    migrate.stop(true, this._left, this._top);
  }

  if (item.isReleasing()) {
    release._reset();
  }

  if (grid._settings.dragPlaceholder.enabled) {
    item._dragPlaceholder.create();
  }

  this._isStarted = true;

  grid._emit(EVENT_DRAG_INIT, item, this._dragStartEvent);

  if (hasDragContainer) {
    // If the dragged element is a child of the drag container all we need to
    // do is setup the relative drag position data.
    if (element.parentNode === this._container) {
      this._gridX -= this._containerDiffX;
      this._gridY -= this._containerDiffY;
    }
    // Otherwise we need to append the element inside the correct container,
    // setup the actual drag position data and adjust the element's translate
    // values to account for the DOM position shift.
    else {
      this._left += this._containerDiffX;
      this._top += this._containerDiffY;
      this._container.appendChild(element);
      item._setTranslate(this._left, this._top);
    }
  }

  addClass(element, grid._settings.itemDraggingClass);
  this._bindScrollListeners();
  grid._emit(EVENT_DRAG_START, item, this._dragStartEvent);
};

/**
 * Drag move handler.
 *
 * @private
 * @param {Object} event
 */
ItemDrag.prototype._onMove = function (event) {
  var item = this._item;

  if (!item._isActive) {
    this.stop();
    return;
  }

  this._dragMoveEvent = event;
  addDragMoveTick(item._id, this._prepareMove, this._applyMove);
  addDragSortTick(item._id, this._handleSort);
};

/**
 * Prepare dragged item for moving.
 *
 * @private
 */
ItemDrag.prototype._prepareMove = function () {
  if (!this._isActive) return;

  var item = this._item;
  if (!item._isActive) return;

  var settings = this._getGrid()._settings;
  var axis = settings.dragAxis;
  var nextEvent = this._dragMoveEvent;
  var prevEvent = this._dragPrevMoveEvent || this._dragStartEvent || nextEvent;

  // Update horizontal position data.
  if (axis !== 'y') {
    var moveDiffX = nextEvent.clientX - prevEvent.clientX;
    this._left = this._left - this._moveDiffX + moveDiffX;
    this._gridX = this._gridX - this._moveDiffX + moveDiffX;
    this._clientX = this._clientX - this._moveDiffX + moveDiffX;
    this._moveDiffX = moveDiffX;
  }

  // Update vertical position data.
  if (axis !== 'x') {
    var moveDiffY = nextEvent.clientY - prevEvent.clientY;
    this._top = this._top - this._moveDiffY + moveDiffY;
    this._gridY = this._gridY - this._moveDiffY + moveDiffY;
    this._clientY = this._clientY - this._moveDiffY + moveDiffY;
    this._moveDiffY = moveDiffY;
  }

  this._dragPrevMoveEvent = nextEvent;
};

/**
 * Apply movement to dragged item.
 *
 * @private
 */
ItemDrag.prototype._applyMove = function () {
  if (!this._isActive) return;

  var item = this._item;
  if (!item._isActive) return;

  this._moveDiffX = this._moveDiffY = 0;
  item._setTranslate(this._left, this._top);
  this._getGrid()._emit(EVENT_DRAG_MOVE, item, this._dragMoveEvent);
  ItemDrag.autoScroller.updateItem(item);
};

/**
 * Drag scroll handler.
 *
 * @private
 * @param {Object} event
 */
ItemDrag.prototype._onScroll = function (event) {
  var item = this._item;

  if (!item._isActive) {
    this.stop();
    return;
  }

  this._scrollEvent = event;
  addDragScrollTick(item._id, this._prepareScroll, this._applyScroll);
  addDragSortTick(item._id, this._handleSort);
};

/**
 * Prepare dragged item for scrolling.
 *
 * @private
 */
ItemDrag.prototype._prepareScroll = function () {
  if (!this._isActive) return;

  // If item is not active do nothing.
  var item = this._item;
  if (!item._isActive) return;

  var element = item._element;
  var grid = this._getGrid();
  var gridContainer = grid._element;
  var rect = element.getBoundingClientRect();

  // Update container diff.
  if (this._container !== gridContainer) {
    var offsetDiff = getOffsetDiff(this._containingBlock, gridContainer);
    this._containerDiffX = offsetDiff.left;
    this._containerDiffY = offsetDiff.top;
  }

  // Update horizontal position data.
  var scrollDiffX = this._clientX - this._moveDiffX - rect.left;
  this._left = this._left - this._scrollDiffX + scrollDiffX;
  this._scrollDiffX = scrollDiffX;

  // Update vertical position data.
  var scrollDiffY = this._clientY - this._moveDiffY - rect.top;
  this._top = this._top - this._scrollDiffY + scrollDiffY;
  this._scrollDiffY = scrollDiffY;

  // Update grid position.
  this._gridX = this._left - this._containerDiffX;
  this._gridY = this._top - this._containerDiffY;
};

/**
 * Apply scroll to dragged item.
 *
 * @private
 */
ItemDrag.prototype._applyScroll = function () {
  if (!this._isActive) return;

  var item = this._item;
  if (!item._isActive) return;

  this._scrollDiffX = this._scrollDiffY = 0;
  item._setTranslate(this._left, this._top);
  this._getGrid()._emit(EVENT_DRAG_SCROLL, item, this._scrollEvent);
};

/**
 * Drag end handler.
 *
 * @private
 * @param {Object} event
 */
ItemDrag.prototype._onEnd = function (event) {
  var item = this._item;
  var element = item._element;
  var grid = this._getGrid();
  var settings = grid._settings;
  var release = item._dragRelease;

  // If item is not active, reset drag.
  if (!item._isActive) {
    this.stop();
    return;
  }

  // Cancel queued ticks.
  cancelDragStartTick(item._id);
  cancelDragMoveTick(item._id);
  cancelDragScrollTick(item._id);

  // Finish sort procedure (does final overlap check if needed).
  this._finishSort();

  // Remove scroll listeners.
  this._unbindScrollListeners();

  // Setup release data.
  release._containerDiffX = this._containerDiffX;
  release._containerDiffY = this._containerDiffY;

  // Reset drag data.
  this._reset();

  // Remove drag class name from element.
  removeClass(element, settings.itemDraggingClass);

  // Stop auto-scroll.
  ItemDrag.autoScroller.removeItem(item);

  // Emit dragEnd event.
  grid._emit(EVENT_DRAG_END, item, event);

  // Finish up the migration process or start the release process.
  this._isMigrating ? this._finishMigration() : release.start();
};

/**
 * Private helpers
 * ***************
 */

/**
 * Check if an element is an anchor element and open the href url if possible.
 *
 * @param {HTMLElement} element
 */
function openAnchorHref(element) {
  // Make sure the element is anchor element.
  if (element.tagName.toLowerCase() !== 'a') return;

  // Get href and make sure it exists.
  var href = element.getAttribute('href');
  if (!href) return;

  // Finally let's navigate to the link href.
  var target = element.getAttribute('target');
  if (target && target !== '_self') {
    window.open(href, target);
  } else {
    window.location.href = href;
  }
}

export default ItemDrag;
