UNPKG

muuri

Version:

Responsive, sortable, filterable and draggable grid layouts.

1,418 lines (1,208 loc) 41.1 kB
/** * Copyright (c) 2015-present, Haltu Oy * Released under the MIT license * https://github.com/haltu/muuri/blob/master/LICENSE.md */ import Hammer from 'hammerjs'; import { eventMove, eventSend, eventBeforeSend, eventReceive, eventBeforeReceive, eventDragInit, eventDragStart, eventDragMove, eventDragScroll, eventDragEnd, gridInstances, namespace } from '../shared.js'; import { addMoveTick, cancelMoveTick, addScrollTick, cancelScrollTick } from '../ticker.js'; import addClass from '../utils/addClass.js'; import arrayMove from '../utils/arrayMove.js'; import arraySwap from '../utils/arraySwap.js'; import debounce from '../utils/debounce.js'; import elementMatches from '../utils/elementMatches.js'; import getContainingBlock from '../utils/getContainingBlock.js'; import getOffsetDiff from '../utils/getOffsetDiff.js'; import getStyle from '../utils/getStyle.js'; import getTranslate from '../utils/getTranslate.js'; import getTranslateString from '../utils/getTranslateString.js'; import arrayInsert from '../utils/arrayInsert.js'; import isPlainObject from '../utils/isPlainObject.js'; import isTransformed from '../utils/isTransformed.js'; import normalizeArrayIndex from '../utils/normalizeArrayIndex.js'; import removeClass from '../utils/removeClass.js'; import setStyles from '../utils/setStyles'; import { isTransformSupported, transformProp } from '../utils/supportedTransform.js'; // To provide consistently correct dragging experience we need to know if // transformed elements leak fixed elements or not. var hasTransformLeak = checkTransformLeak(); // Drag start predicate states. var startPredicateInactive = 0; var startPredicatePending = 1; var startPredicateResolved = 2; var startPredicateRejected = 3; /** * Bind Hammer touch interaction to an item. * * @class * @param {Item} item */ function ItemDrag(item) { if (!Hammer) { throw new Error('[' + namespace + '] required dependency Hammer is not defined.'); } // If we don't have a valid transform leak test result yet, let's run the // test on first ItemDrag init. The test needs body element to be ready and // here we can be sure that it is ready. if (hasTransformLeak === null) { hasTransformLeak = checkTransformLeak(); } var drag = this; var element = item._element; var grid = item.getGrid(); var settings = grid._settings; var hammer; // Start predicate private data. var startPredicate = typeof settings.dragStartPredicate === 'function' ? settings.dragStartPredicate : ItemDrag.defaultStartPredicate; var startPredicateState = startPredicateInactive; var startPredicateResult; // Protected data. this._item = item; this._gridId = grid._id; this._hammer = hammer = new Hammer.Manager(element); this._isDestroyed = false; this._isMigrating = false; // Setup item's initial drag data. this._reset(); // Bind some methods that needs binding. this._onScroll = this._onScroll.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._checkOverlap = this._checkOverlap.bind(this); // Create a private drag start resolver that can be used to resolve the drag // start predicate asynchronously. this._forceResolveStartPredicate = function(event) { if (!this._isDestroyed && startPredicateState === startPredicatePending) { startPredicateState = startPredicateResolved; this._onStart(event); } }; // Create debounce overlap checker function. this._checkOverlapDebounce = debounce(this._checkOverlap, settings.dragSortInterval); // Add drag recognizer to hammer. hammer.add( new Hammer.Pan({ event: 'drag', pointers: 1, threshold: 0, direction: Hammer.DIRECTION_ALL }) ); // Add drag init recognizer to hammer. hammer.add( new Hammer.Press({ event: 'draginit', pointers: 1, threshold: 1000, time: 0 }) ); // Configure the hammer instance. if (isPlainObject(settings.dragHammerSettings)) { hammer.set(settings.dragHammerSettings); } // Bind drag events. hammer .on('draginit dragstart dragmove', function(e) { // Let's activate drag start predicate state. if (startPredicateState === startPredicateInactive) { startPredicateState = startPredicatePending; } // If predicate is pending try to resolve it. if (startPredicateState === startPredicatePending) { startPredicateResult = startPredicate(drag._item, e); if (startPredicateResult === true) { startPredicateState = startPredicateResolved; drag._onStart(e); } else if (startPredicateResult === false) { startPredicateState = startPredicateRejected; } } // Otherwise if predicate is resolved and drag is active, move the item. else if (startPredicateState === startPredicateResolved && drag._isActive) { drag._onMove(e); } }) .on('dragend dragcancel draginitup', function(e) { // Check if the start predicate was resolved during drag. var isResolved = startPredicateState === startPredicateResolved; // 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. startPredicate(drag._item, e); // Reset start predicate state. startPredicateState = startPredicateInactive; // If predicate is resolved and dragging is active, call the end handler. if (isResolved && drag._isActive) drag._onEnd(e); }); // Prevent native link/image dragging for the item and it's ancestors. element.addEventListener('dragstart', preventDefault, false); } /** * 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 * @memberof ItemDrag * @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} */ ItemDrag.defaultStartPredicate = function(item, event, options) { var drag = item._drag; var predicate = drag._startPredicateData || drag._setupStartPredicate(options); // 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; } // Find and store the handle element so we can check later on if the // cursor is within the handle. If we have a handle selector let's find // the corresponding element. Otherwise let's use the item element as the // handle. if (!predicate.handleElement) { predicate.handleElement = drag._getStartPredicateHandle(event); if (!predicate.handleElement) return false; } // 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 * @memberof ItemDrag * @param {Item} item * @param {Object} [options] * @param {Number} [options.threshold=50] * @param {String} [options.action='move'] * @returns {(Boolean|DragSortCommand)} * - Returns false if no valid index was found. Otherwise returns drag sort * command. */ ItemDrag.defaultSortPredicate = (function() { var itemRect = {}; var targetRect = {}; var returnData = {}; var rootGridArray = []; function getTargetGrid(item, rootGrid, threshold) { var target = null; var dragSort = rootGrid._settings.dragSort; var bestScore = -1; var gridScore; var grids; var grid; var i; // Get potential target grids. if (dragSort === true) { rootGridArray[0] = rootGrid; grids = rootGridArray; } else { grids = dragSort.call(rootGrid, item); } // Return immediately if there are no grids. if (!Array.isArray(grids)) 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; // We need to update the grid's offsets and dimensions since they might // have changed (e.g during scrolling). grid._updateBoundingRect(); // Check how much dragged element overlaps the container element. targetRect.width = grid._width; targetRect.height = grid._height; targetRect.left = grid._left; targetRect.top = grid._top; gridScore = getRectOverlapScore(itemRect, targetRect); // Check if this grid is the best match so far. if (gridScore > threshold && gridScore > bestScore) { bestScore = gridScore; target = grid; } } // Always reset root grid array. rootGridArray.length = 0; return target; } return function(item, options) { 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 === 'swap' ? 'swap' : 'move'; // Populate item rect data. itemRect.width = item._width; itemRect.height = item._height; itemRect.left = drag._elementClientX; itemRect.top = drag._elementClientY; // 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 false; var gridOffsetLeft = 0; var gridOffsetTop = 0; var matchScore = -1; var matchIndex; var hasValidTargets; 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; } // 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 = getRectOverlapScore(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 item is being moved into another // grid. if (matchScore < sortThreshold && item.getGrid() !== grid) { matchIndex = hasValidTargets ? -1 : 0; matchScore = Infinity; } // Check if the best match overlaps enough to justify a placement switch. if (matchScore >= sortThreshold) { returnData.grid = grid; returnData.index = matchIndex; returnData.action = sortAction; return returnData; } return false; }; })(); /** * Public prototype methods * ************************ */ /** * Abort dragging and reset drag data. * * @public * @memberof ItemDrag.prototype * @returns {ItemDrag} */ ItemDrag.prototype.stop = function() { var item = this._item; var element = item._element; var grid = this._getGrid(); if (!this._isActive) return this; // If the item is being dropped into another grid, finish it up and return // immediately. if (this._isMigrating) { this._finishMigration(); return this; } // Cancel queued move and scroll ticks. cancelMoveTick(item._id); cancelScrollTick(item._id); // Remove scroll listeners. this._unbindScrollListeners(); // Cancel overlap check. this._checkOverlapDebounce('cancel'); // 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); element.style[transformProp] = getTranslateString(this._gridX, this._gridY); } // Remove dragging class. removeClass(element, grid._settings.itemDraggingClass); // Reset drag data. this._reset(); return this; }; /** * Destroy instance. * * @public * @memberof ItemDrag.prototype * @returns {ItemDrag} */ ItemDrag.prototype.destroy = function() { if (this._isDestroyed) return this; this.stop(); this._hammer.destroy(); this._item._element.removeEventListener('dragstart', preventDefault, false); this._isDestroyed = true; return this; }; /** * Private prototype methods * ************************* */ /** * Get Grid instance. * * @private * @memberof ItemDrag.prototype * @returns {?Grid} */ ItemDrag.prototype._getGrid = function() { return gridInstances[this._gridId] || null; }; /** * Setup/reset drag data. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._reset = function() { // Is item being dragged? this._isActive = false; // The dragged item's container element. this._container = null; // The dragged item's containing block. this._containingBlock = null; // Hammer event data. this._lastEvent = null; this._lastScrollEvent = 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._elementClientX = 0; this._elementClientY = 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 * @memberof ItemDrag.prototype */ ItemDrag.prototype._bindScrollListeners = function() { var gridContainer = this._getGrid()._element; var dragContainer = this._container; var scrollers = this._scrollers; var containerScrollers; var i; // Get dragged element's scrolling parents. scrollers.length = 0; getScrollParents(this._item._element, 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) { containerScrollers = []; getScrollParents(gridContainer, containerScrollers); containerScrollers.push(gridContainer); for (i = 0; i < containerScrollers.length; i++) { if (scrollers.indexOf(containerScrollers[i]) < 0) { scrollers.push(containerScrollers[i]); } } } // Bind scroll listeners. for (i = 0; i < scrollers.length; i++) { scrollers[i].addEventListener('scroll', this._onScroll); } }; /** * Unbind currently bound drag scroll handlers from all scrollable ancestor * elements of the dragged element and the drag container element. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._unbindScrollListeners = function() { var scrollers = this._scrollers; var i; for (i = 0; i < scrollers.length; i++) { scrollers[i].removeEventListener('scroll', this._onScroll); } scrollers.length = 0; }; /** * Setup default start predicate. * * @private * @memberof ItemDrag.prototype * @param {Object} [options] * @returns {Object} */ ItemDrag.prototype._setupStartPredicate = function(options) { var config = options || this._getGrid()._settings.dragStartPredicate || 0; return (this._startPredicateData = { distance: Math.abs(config.distance) || 0, delay: Math.max(config.delay, 0) || 0, handle: typeof config.handle === 'string' ? config.handle : false }); }; /** * Setup default start predicate handle. * * @private * @memberof ItemDrag.prototype * @param {Object} event * @returns {?HTMLElement} */ ItemDrag.prototype._getStartPredicateHandle = function(event) { var predicate = this._startPredicateData; var element = this._item._element; var handleElement = element; // No handle, no hassle -> let's use the item element as the handle. if (!predicate.handle) return handleElement; // If there is a specific predicate handle defined, let's try to get it. handleElement = (event.changedPointers[0] || 0).target; while (handleElement && !elementMatches(handleElement, predicate.handle)) { handleElement = handleElement !== element ? handleElement.parentElement : null; } return handleElement || null; }; /** * Unbind currently bound drag scroll handlers from all scrollable ancestor * elements of the dragged element and the drag container element. * * @private * @memberof ItemDrag.prototype * @param {Object} event * @returns {Boolean} */ ItemDrag.prototype._resolveStartPredicate = function(event) { var predicate = this._startPredicateData; var pointer = event.changedPointers[0]; var pageX = (pointer && pointer.pageX) || 0; var pageY = (pointer && pointer.pageY) || 0; var handleRect; var handleLeft; var handleTop; var handleWidth; var handleHeight; // If the moved distance is smaller than the threshold distance or there is // some delay left, ignore this predicate cycle. if (event.distance < predicate.distance || predicate.delay) return; // Get handle rect data. handleRect = predicate.handleElement.getBoundingClientRect(); handleLeft = handleRect.left + (window.pageXOffset || 0); handleTop = handleRect.top + (window.pageYOffset || 0); handleWidth = handleRect.width; handleHeight = handleRect.height; // Reset predicate data. this._resetStartPredicate(); // If the cursor is still within the handle let's start the drag. return ( handleWidth && handleHeight && pageX >= handleLeft && pageX < handleLeft + handleWidth && pageY >= handleTop && pageY < handleTop + handleHeight ); }; /** * Finalize start predicate. * * @private * @memberof ItemDrag.prototype * @param {Object} event */ ItemDrag.prototype._finishStartPredicate = function(event) { var element = this._item._element; // 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(event)) openAnchorHref(element); }; /** * Reset for default drag start predicate function. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._resetStartPredicate = function() { var predicate = this._startPredicateData; if (predicate) { if (predicate.delayTimer) { predicate.delayTimer = window.clearTimeout(predicate.delayTimer); } this._startPredicateData = null; } }; /** * Check (during drag) if an item is overlapping other items and based on * the configuration layout the items. * * @private * @memberof ItemDrag.prototype */ 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 sortAction; var isMigration; // Get overlap check result. if (typeof settings.dragSortPredicate === 'function') { result = settings.dragSortPredicate(item, this._lastEvent); } else { result = ItemDrag.defaultSortPredicate(item, settings.dragSortPredicate); } // Let's make sure the result object has a valid index before going further. if (!result || typeof result.index !== 'number') return; currentGrid = item.getGrid(); targetGrid = result.grid || currentGrid; isMigration = currentGrid !== targetGrid; currentIndex = currentGrid._items.indexOf(item); targetIndex = normalizeArrayIndex(targetGrid._items, result.index, isMigration); sortAction = result.action === 'swap' ? 'swap' : 'move'; // 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) { // Do the sort. (sortAction === 'swap' ? arraySwap : arrayMove)( currentGrid._items, currentIndex, targetIndex ); // Emit move event. if (currentGrid._hasListeners(eventMove)) { currentGrid._emit(eventMove, { item: item, fromIndex: currentIndex, toIndex: targetIndex, action: sortAction }); } // Layout the grid. currentGrid.layout(); } } // If the item was moved to another grid. else { // Emit beforeSend event. if (currentGrid._hasListeners(eventBeforeSend)) { currentGrid._emit(eventBeforeSend, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, toIndex: targetIndex }); } // Emit beforeReceive event. if (targetGrid._hasListeners(eventBeforeReceive)) { targetGrid._emit(eventBeforeReceive, { 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); // Set sort data as null, which is an indicator for the item comparison // function that the sort data of this specific item should be fetched // lazily. item._sortData = null; // Emit send event. if (currentGrid._hasListeners(eventSend)) { currentGrid._emit(eventSend, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, toIndex: targetIndex }); } // Emit receive event. if (targetGrid._hasListeners(eventReceive)) { targetGrid._emit(eventReceive, { item: item, fromGrid: currentGrid, fromIndex: currentIndex, toGrid: targetGrid, toIndex: targetIndex }); } // Layout both grids. currentGrid.layout(); targetGrid.layout(); } }; /** * If item is dragged into another grid, finish the migration process * gracefully. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._finishMigration = function() { var item = this._item; var release = item._release; 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 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(); // Remove current classnames. removeClass(element, currentSettings.itemClass); removeClass(element, currentSettings.itemVisibleClass); removeClass(element, currentSettings.itemHiddenClass); // Add new classnames. addClass(element, targetSettings.itemClass); addClass(element, isActive ? targetSettings.itemVisibleClass : targetSettings.itemHiddenClass); // 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 and sort data. item._refreshDimensions(); item._refreshSortData(); // 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) { element.style[transformProp] = getTranslateString(translate.x, translate.y); } // Update child element's styles to reflect the current visibility state. item._child.removeAttribute('style'); setStyles(item._child, isActive ? targetSettings.visibleStyles : targetSettings.hiddenStyles); // Start the release. release.start(); }; /** * Drag start handler. * * @private * @memberof ItemDrag.prototype * @param {Object} event */ ItemDrag.prototype._onStart = function(event) { var item = this._item; // If item is not active, don't start the drag. if (!item._isActive) return; var element = item._element; var grid = this._getGrid(); var settings = grid._settings; var release = item._release; var migrate = item._migrate; var gridContainer = grid._element; var dragContainer = settings.dragContainer || gridContainer; var containingBlock = getContainingBlock(dragContainer, true); var translate = getTranslate(element); var currentLeft = translate.x; var currentTop = translate.y; var elementRect = element.getBoundingClientRect(); var hasDragContainer = dragContainer !== gridContainer; var offsetDiff; // If grid container is not the drag container, we need to calculate the // offset difference between grid container and drag container's containing // element. if (hasDragContainer) { offsetDiff = getOffsetDiff(containingBlock, gridContainer); } // Stop current positioning animation. if (item.isPositioning()) { item._layout.stop(true, { transform: getTranslateString(currentLeft, currentTop) }); } // Stop current migration animation. if (migrate._isActive) { currentLeft -= migrate._containerDiffX; currentTop -= migrate._containerDiffY; migrate.stop(true, { transform: getTranslateString(currentLeft, currentTop) }); } // If item is being released reset release data. if (item.isReleasing()) release._reset(); // Setup drag data. this._isActive = true; this._lastEvent = event; this._container = dragContainer; this._containingBlock = containingBlock; this._elementClientX = elementRect.left; this._elementClientY = elementRect.top; this._left = this._gridX = currentLeft; this._top = this._gridY = currentTop; // Emit dragInit event. grid._emit(eventDragInit, item, event); // If a specific drag container is set and it is different from the // grid's container element we need to cast some extra spells. if (hasDragContainer) { // Store the container offset diffs to drag data. this._containerDiffX = offsetDiff.left; this._containerDiffY = offsetDiff.top; // 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 === dragContainer) { this._gridX = currentLeft - this._containerDiffX; this._gridY = currentTop - 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 = currentLeft + this._containerDiffX; this._top = currentTop + this._containerDiffY; dragContainer.appendChild(element); element.style[transformProp] = getTranslateString(this._left, this._top); } } // Set drag class and bind scrollers. addClass(element, settings.itemDraggingClass); this._bindScrollListeners(); // Emit dragStart event. grid._emit(eventDragStart, item, event); }; /** * Drag move handler. * * @private * @memberof ItemDrag.prototype * @param {Object} event */ ItemDrag.prototype._onMove = function(event) { var item = this._item; // If item is not active, reset drag. if (!item._isActive) { this.stop(); return; } var settings = this._getGrid()._settings; var axis = settings.dragAxis; var xDiff = event.deltaX - this._lastEvent.deltaX; var yDiff = event.deltaY - this._lastEvent.deltaY; // Update last event. this._lastEvent = event; // Update horizontal position data. if (axis !== 'y') { this._left += xDiff; this._gridX += xDiff; this._elementClientX += xDiff; } // Update vertical position data. if (axis !== 'x') { this._top += yDiff; this._gridY += yDiff; this._elementClientY += yDiff; } // Do move prepare/apply handling in the next tick. addMoveTick(item._id, this._prepareMove, this._applyMove); }; /** * Prepare dragged item for moving. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._prepareMove = function() { // Do nothing if item is not active. if (!this._item._isActive) return; // If drag sort is enabled -> check overlap. if (this._getGrid()._settings.dragSort) this._checkOverlapDebounce(); }; /** * Apply movement to dragged item. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._applyMove = function() { var item = this._item; // Do nothing if item is not active. if (!item._isActive) return; // Update element's translateX/Y values. item._element.style[transformProp] = getTranslateString(this._left, this._top); // Emit dragMove event. this._getGrid()._emit(eventDragMove, item, this._lastEvent); }; /** * Drag scroll handler. * * @private * @memberof ItemDrag.prototype * @param {Object} event */ ItemDrag.prototype._onScroll = function(event) { var item = this._item; // If item is not active, reset drag. if (!item._isActive) { this.stop(); return; } // Update last scroll event. this._lastScrollEvent = event; // Do scroll prepare/apply handling in the next tick. addScrollTick(item._id, this._prepareScroll, this._applyScroll); }; /** * Prepare dragged item for scrolling. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._prepareScroll = function() { var item = this._item; // If item is not active do nothing. if (!item._isActive) return; var element = item._element; var grid = this._getGrid(); var settings = grid._settings; var axis = settings.dragAxis; var gridContainer = grid._element; var offsetDiff; // Calculate element's rect and x/y diff. var rect = element.getBoundingClientRect(); var xDiff = this._elementClientX - rect.left; var yDiff = this._elementClientY - rect.top; // Update container diff. if (this._container !== gridContainer) { offsetDiff = getOffsetDiff(this._containingBlock, gridContainer); this._containerDiffX = offsetDiff.left; this._containerDiffY = offsetDiff.top; } // Update horizontal position data. if (axis !== 'y') { this._left += xDiff; this._gridX = this._left - this._containerDiffX; } // Update vertical position data. if (axis !== 'x') { this._top += yDiff; this._gridY = this._top - this._containerDiffY; } // Overlap handling. if (settings.dragSort) this._checkOverlapDebounce(); }; /** * Apply scroll to dragged item. * * @private * @memberof ItemDrag.prototype */ ItemDrag.prototype._applyScroll = function() { var item = this._item; // If item is not active do nothing. if (!item._isActive) return; // Update element's translateX/Y values. item._element.style[transformProp] = getTranslateString(this._left, this._top); // Emit dragScroll event. this._getGrid()._emit(eventDragScroll, item, this._lastScrollEvent); }; /** * Drag end handler. * * @private * @memberof ItemDrag.prototype * @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._release; // If item is not active, reset drag. if (!item._isActive) { this.stop(); return; } // Cancel queued move and scroll ticks. cancelMoveTick(item._id); cancelScrollTick(item._id); // Finish currently queued overlap check. settings.dragSort && this._checkOverlapDebounce('finish'); // 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); // Emit dragEnd event. grid._emit(eventDragEnd, item, event); // Finish up the migration process or start the release process. this._isMigrating ? this._finishMigration() : release.start(); }; /** * Private helpers * *************** */ /** * Prevent default. * * @param {Object} e */ function preventDefault(e) { if (e.preventDefault) e.preventDefault(); } /** * Calculate how many percent the intersection area of two rectangles is from * the maximum potential intersection area between the rectangles. * * @param {Rectangle} a * @param {Rectangle} b * @returns {Number} * - A number between 0-100. */ function getRectOverlapScore(a, b) { // Return 0 immediately if the rectangles do not overlap. if ( a.left + a.width <= b.left || b.left + b.width <= a.left || a.top + a.height <= b.top || b.top + b.height <= a.top ) { return 0; } // Calculate intersection area's width, height, max height and max width. var width = Math.min(a.left + a.width, b.left + b.width) - Math.max(a.left, b.left); var height = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top); var maxWidth = Math.min(a.width, b.width); var maxHeight = Math.min(a.height, b.height); return ((width * height) / (maxWidth * maxHeight)) * 100; } /** * Get element's scroll parents. * * @param {HTMLElement} element * @param {Array} [data] * @returns {HTMLElement[]} */ function getScrollParents(element, data) { var ret = data || []; var parent = element.parentNode; // // If transformed elements leak fixed elements. // if (hasTransformLeak) { // If the element is fixed it can not have any scroll parents. if (getStyle(element, 'position') === 'fixed') return ret; // Find scroll parents. while (parent && parent !== document && parent !== document.documentElement) { if (isScrollable(parent)) ret.push(parent); parent = getStyle(parent, 'position') === 'fixed' ? null : parent.parentNode; } // If parent is not fixed element, add window object as the last scroll // parent. parent !== null && ret.push(window); return ret; } // // If fixed elements behave as defined in the W3C specification. // // Find scroll parents. while (parent && parent !== document) { // If the currently looped element is fixed ignore all parents that are // not transformed. if (getStyle(element, 'position') === 'fixed' && !isTransformed(parent)) { parent = parent.parentNode; continue; } // Add the parent element to return items if it is scrollable. if (isScrollable(parent)) ret.push(parent); // Update element and parent references. element = parent; parent = parent.parentNode; } // If the last item is the root element, replace it with window. The root // element scroll is propagated to the window. if (ret[ret.length - 1] === document.documentElement) { ret[ret.length - 1] = window; } // Otherwise add window as the last scroll parent. else { ret.push(window); } return ret; } /** * Check if an element is scrollable. * * @param {HTMLElement} element * @returns {Boolean} */ function isScrollable(element) { var overflow = getStyle(element, 'overflow'); if (overflow === 'auto' || overflow === 'scroll') return true; overflow = getStyle(element, 'overflow-x'); if (overflow === 'auto' || overflow === 'scroll') return true; overflow = getStyle(element, 'overflow-y'); if (overflow === 'auto' || overflow === 'scroll') return true; return false; } /** * Check if drag gesture can be interpreted as a click, based on final drag * event data. * * @param {Object} element * @returns {Boolean} */ function isClick(event) { return Math.abs(event.deltaX) < 2 && Math.abs(event.deltaY) < 2 && event.deltaTime < 200; } /** * 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; } } /** * Detects if transformed elements leak fixed elements. According W3C * transform rendering spec a transformed element should contain even fixed * elements. Meaning that fixed elements are positioned relative to the * closest transformed ancestor element instead of window. However, not every * browser follows the spec (IE and older Firefox). So we need to test it. * https://www.w3.org/TR/css3-2d-transforms/#transform-rendering * * Borrowed from Mezr (v0.6.1): * https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L607 */ function checkTransformLeak() { // No transforms -> definitely leaks. if (!isTransformSupported) return true; // No body available -> can't check it. if (!document.body) return null; // Do the test. var elems = [0, 1].map(function(elem, isInner) { elem = document.createElement('div'); elem.style.position = isInner ? 'fixed' : 'absolute'; elem.style.display = 'block'; elem.style.visibility = 'hidden'; elem.style.left = isInner ? '0px' : '1px'; elem.style[transformProp] = 'none'; return elem; }); var outer = document.body.appendChild(elems[0]); var inner = outer.appendChild(elems[1]); var left = inner.getBoundingClientRect().left; outer.style[transformProp] = 'scale(1)'; var ret = left === inner.getBoundingClientRect().left; document.body.removeChild(outer); return ret; } export default ItemDrag;