1742 lines
50 KiB
JavaScript
1742 lines
50 KiB
JavaScript
/**
|
|
* 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;
|