import { __rest, __assign } from 'tslib'; import arrayMove from 'array-move'; import React from 'react'; /** * This function check if a given point is inside of the items rect. * If it's not inside any rect, it will return the index of the closest rect */ var findItemIndexAtPosition = function findItemIndexAtPosition(_a, itemsRect, _b) { var x = _a.x, y = _a.y; var _c = (_b === void 0 ? {} : _b).fallbackToClosest, fallbackToClosest = _c === void 0 ? false : _c; var smallestDistance = 10000; var smallestDistanceIndex = -1; for (var index = 0; index < itemsRect.length; index += 1) { var rect = itemsRect[index]; // if it's inside the rect, we return the current index directly if (x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom) { return index; } if (fallbackToClosest) { // otherwise we compute the distance and update the smallest distance index if needed var itemCenterX = (rect.left + rect.right) / 2; var itemCenterY = (rect.top + rect.bottom) / 2; var distance = Math.sqrt(Math.pow(x - itemCenterX, 2) + Math.pow(y - itemCenterY, 2)); // ** 2 operator is not supported on IE11 if (distance < smallestDistance) { smallestDistance = distance; smallestDistanceIndex = index; } } } return smallestDistanceIndex; }; var getMousePoint = function getMousePoint(e) { return { x: Number(e.clientX), y: Number(e.clientY) }; }; var getTouchPoint = function getTouchPoint(touch) { return { x: Number(touch.clientX), y: Number(touch.clientY) }; }; var getPointInContainer = function getPointInContainer(point, containerTopLeft) { return { x: point.x - containerTopLeft.x, y: point.y - containerTopLeft.y }; }; var preventDefault = function preventDefault(event) { event.preventDefault(); }; var disableContextMenu = function disableContextMenu() { window.addEventListener('contextmenu', preventDefault, { capture: true, passive: false }); }; var enableContextMenu = function enableContextMenu() { window.removeEventListener('contextmenu', preventDefault); }; var useDrag = function useDrag(_a) { var onStart = _a.onStart, onMove = _a.onMove, onEnd = _a.onEnd, _b = _a.allowDrag, allowDrag = _b === void 0 ? true : _b, containerRef = _a.containerRef, knobs = _a.knobs; // contains the top-left coordinates of the container in the window. Set on drag start and used in drag move var containerPositionRef = React.useRef({ x: 0, y: 0 }); // on touch devices, we only start the drag gesture after pressing the item 200ms. // this ref contains the timer id to be able to cancel it var handleTouchStartTimerRef = React.useRef(undefined); // on non-touch device, we don't call onStart on mouse down but on the first mouse move // we do this to let the user clicks on clickable element inside the container // this means that the drag gesture actually starts on the fist move var isFirstMoveRef = React.useRef(false); // see https://twitter.com/ValentinHervieu/status/1324407814970920968 // we do this so that the parent doesn't have to use `useCallback()` for these callbacks var callbacksRef = React.useRef({ onStart: onStart, onMove: onMove, onEnd: onEnd }); // instead of relying on hacks to know if the device is a touch device or not, // we track this using an onTouchStart listener on the document. (see https://codeburst.io/the-only-way-to-detect-touch-with-javascript-7791a3346685) var _c = React.useState(false), isTouchDevice = _c[0], setTouchDevice = _c[1]; React.useEffect(function () { callbacksRef.current = { onStart: onStart, onMove: onMove, onEnd: onEnd }; }, [onStart, onMove, onEnd]); var cancelTouchStart = function cancelTouchStart() { if (handleTouchStartTimerRef.current) { window.clearTimeout(handleTouchStartTimerRef.current); } }; var saveContainerPosition = React.useCallback(function () { if (containerRef.current) { var bounds = containerRef.current.getBoundingClientRect(); containerPositionRef.current = { x: bounds.left, y: bounds.top }; } }, [containerRef]); var onDrag = React.useCallback(function (pointInWindow) { var point = getPointInContainer(pointInWindow, containerPositionRef.current); if (callbacksRef.current.onMove) { callbacksRef.current.onMove({ pointInWindow: pointInWindow, point: point }); } }, []); var onMouseMove = React.useCallback(function (e) { // if this is the first move, we trigger the onStart logic if (isFirstMoveRef.current) { isFirstMoveRef.current = false; var pointInWindow = getMousePoint(e); var point = getPointInContainer(pointInWindow, containerPositionRef.current); if (callbacksRef.current.onStart) { callbacksRef.current.onStart({ point: point, pointInWindow: pointInWindow }); } } // otherwise, we do the normal move logic else { onDrag(getMousePoint(e)); } }, [onDrag]); var onTouchMove = React.useCallback(function (e) { if (e.cancelable) { // Prevent the whole page from scrolling e.preventDefault(); onDrag(getTouchPoint(e.touches[0])); } else { // if the event is not cancelable, it means the browser is currently scrolling // which cannot be interrupted. Thus we cancel the drag gesture. document.removeEventListener('touchmove', onTouchMove); if (callbacksRef.current.onEnd) { callbacksRef.current.onEnd(); } } }, [onDrag]); var onMouseUp = React.useCallback(function () { isFirstMoveRef.current = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); if (callbacksRef.current.onEnd) { callbacksRef.current.onEnd(); } }, [onMouseMove]); var onTouchEnd = React.useCallback(function () { document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); enableContextMenu(); if (callbacksRef.current.onEnd) { callbacksRef.current.onEnd(); } }, [onTouchMove]); var onMouseDown = React.useCallback(function (e) { if (e.button !== 0) { // we don't want to handle clicks other than left ones return; } if ((knobs === null || knobs === void 0 ? void 0 : knobs.length) && !knobs.find(function (knob) { return knob.contains(e.target); })) { return; } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); saveContainerPosition(); // mark the next move as being the first one isFirstMoveRef.current = true; }, [onMouseMove, onMouseUp, saveContainerPosition, knobs]); var handleTouchStart = React.useCallback(function (point, pointInWindow) { document.addEventListener('touchmove', onTouchMove, { capture: false, passive: false }); document.addEventListener('touchend', onTouchEnd); disableContextMenu(); if (callbacksRef.current.onStart) { callbacksRef.current.onStart({ point: point, pointInWindow: pointInWindow }); } }, [onTouchEnd, onTouchMove]); var onTouchStart = React.useCallback(function (e) { if ((knobs === null || knobs === void 0 ? void 0 : knobs.length) && !knobs.find(function (knob) { return knob.contains(e.target); })) { return; } saveContainerPosition(); var pointInWindow = getTouchPoint(e.touches[0]); var point = getPointInContainer(pointInWindow, containerPositionRef.current); // we wait 120ms to start the gesture to be sure that the user // is not trying to scroll the page handleTouchStartTimerRef.current = window.setTimeout(function () { return handleTouchStart(point, pointInWindow); }, 120); }, [handleTouchStart, saveContainerPosition, knobs]); var detectTouchDevice = React.useCallback(function () { setTouchDevice(true); document.removeEventListener('touchstart', detectTouchDevice); }, []); // if the user is scrolling on mobile, we cancel the drag gesture var touchScrollListener = React.useCallback(function () { cancelTouchStart(); }, []); React.useLayoutEffect(function () { if (isTouchDevice) { var container_1 = containerRef.current; if (allowDrag) { container_1 === null || container_1 === void 0 ? void 0 : container_1.addEventListener('touchstart', onTouchStart, { capture: true, passive: false }); // we are adding this touchmove listener to cancel drag if user is scrolling // however, it's also important to have a touchmove listener always set // with non-capture and non-passive option to prevent an issue on Safari // with e.preventDefault (https://github.com/atlassian/react-beautiful-dnd/issues/1374) document.addEventListener('touchmove', touchScrollListener, { capture: false, passive: false }); document.addEventListener('touchend', touchScrollListener, { capture: false, passive: false }); } return function () { container_1 === null || container_1 === void 0 ? void 0 : container_1.removeEventListener('touchstart', onTouchStart, { capture: true }); document.removeEventListener('touchmove', touchScrollListener, { capture: false }); document.removeEventListener('touchend', touchScrollListener, { capture: false }); document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); enableContextMenu(); cancelTouchStart(); }; } // if non-touch device document.addEventListener('touchstart', detectTouchDevice); return function () { document.removeEventListener('touchstart', detectTouchDevice); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; }, [isTouchDevice, allowDrag, detectTouchDevice, onMouseMove, onTouchMove, touchScrollListener, onTouchEnd, onMouseUp, containerRef, onTouchStart]); // on touch devices, we cannot attach the onTouchStart directly via React: // Touch handlers must be added with {passive: false} to be cancelable. // https://developers.google.com/web/updates/2017/01/scrolling-intervention return isTouchDevice ? {} : { onMouseDown: onMouseDown }; }; var DEFAULT_CONTAINER_TAG = 'div'; var SortableListContext = /*#__PURE__*/React.createContext(undefined); var SortableList = function SortableList(_a) { var children = _a.children, _b = _a.allowDrag, allowDrag = _b === void 0 ? true : _b, onSortEnd = _a.onSortEnd, draggedItemClassName = _a.draggedItemClassName, as = _a.as, lockAxis = _a.lockAxis, customHolderRef = _a.customHolderRef, rest = __rest(_a, ["children", "allowDrag", "onSortEnd", "draggedItemClassName", "as", "lockAxis", "customHolderRef"]); // this array contains the elements than can be sorted (wrapped inside SortableItem) var itemsRef = React.useRef([]); // this array contains the coordinates of each sortable element (only computed on dragStart and used in dragMove for perf reason) var itemsRect = React.useRef([]); // Hold all registered knobs var knobs = React.useRef([]); // contains the container element var containerRef = React.useRef(null); // contains the target element (copy of the source element) var targetRef = React.useRef(null); // contains the index in the itemsRef array of the element being dragged var sourceIndexRef = React.useRef(undefined); // contains the index in the itemsRef of the element to be exchanged with the source item var lastTargetIndexRef = React.useRef(undefined); // contains the offset point where the initial drag occurred to be used when dragging the item var offsetPointRef = React.useRef({ x: 0, y: 0 }); React.useEffect(function () { var holder = (customHolderRef === null || customHolderRef === void 0 ? void 0 : customHolderRef.current) || document.body; return function () { // cleanup the target element from the DOM when SortableList in unmounted if (targetRef.current) { holder.removeChild(targetRef.current); } }; }, [customHolderRef]); var updateTargetPosition = function updateTargetPosition(position) { if (targetRef.current && sourceIndexRef.current !== undefined) { var offset = offsetPointRef.current; var sourceRect = itemsRect.current[sourceIndexRef.current]; var newX = lockAxis === 'y' ? sourceRect.left : position.x - offset.x; var newY = lockAxis === 'x' ? sourceRect.top : position.y - offset.y; // we use `translate3d` to force using the GPU if available targetRef.current.style.transform = "translate3d(" + newX + "px, " + newY + "px, 0px)"; } }; var copyItem = React.useCallback(function (sourceIndex) { if (!containerRef.current) { return; } var source = itemsRef.current[sourceIndex]; var sourceRect = itemsRect.current[sourceIndex]; var copy = source.cloneNode(true); // added the "dragged" class name if (draggedItemClassName) { draggedItemClassName.split(' ').forEach(function (c) { return copy.classList.add(c); }); } // we ensure the copy has the same size than the source element copy.style.width = sourceRect.width + "px"; copy.style.height = sourceRect.height + "px"; // we place the target starting position to the top left of the window // it will then be moved relatively using `transform: translate3d()` copy.style.position = 'fixed'; copy.style.margin = '0'; copy.style.top = '0'; copy.style.left = '0'; var sourceCanvases = source.querySelectorAll('canvas'); copy.querySelectorAll('canvas').forEach(function (canvas, index) { var _a; (_a = canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.drawImage(sourceCanvases[index], 0, 0); }); var holder = (customHolderRef === null || customHolderRef === void 0 ? void 0 : customHolderRef.current) || document.body; holder.appendChild(copy); targetRef.current = copy; }, [customHolderRef, draggedItemClassName]); var listeners = useDrag({ allowDrag: allowDrag, containerRef: containerRef, knobs: knobs.current, onStart: function onStart(_a) { var pointInWindow = _a.pointInWindow; if (!containerRef.current) { return; } itemsRect.current = itemsRef.current.map(function (item) { return item.getBoundingClientRect(); }); var sourceIndex = findItemIndexAtPosition(pointInWindow, itemsRect.current); // if we are not starting the drag gesture on a SortableItem, we exit early if (sourceIndex === -1) { return; } // saving the index of the item being dragged sourceIndexRef.current = sourceIndex; // the item being dragged is copied to the document body and will be used as the target copyItem(sourceIndex); // hide source during the drag gesture var source = itemsRef.current[sourceIndex]; source.style.opacity = '0'; source.style.visibility = 'hidden'; // get the offset between the source item's window position relative to the point in window var sourceRect = source.getBoundingClientRect(); offsetPointRef.current = { x: pointInWindow.x - sourceRect.left, y: pointInWindow.y - sourceRect.top }; updateTargetPosition(pointInWindow); // Adds a nice little physical feedback if (window.navigator.vibrate) { window.navigator.vibrate(100); } }, onMove: function onMove(_a) { var pointInWindow = _a.pointInWindow; updateTargetPosition(pointInWindow); var sourceIndex = sourceIndexRef.current; // if there is no source, we exit early (happened when drag gesture was started outside a SortableItem) if (sourceIndex === undefined || sourceIndexRef.current === undefined) { return; } var sourceRect = itemsRect.current[sourceIndexRef.current]; var targetPoint = { x: lockAxis === 'y' ? sourceRect.left : pointInWindow.x, y: lockAxis === 'x' ? sourceRect.top : pointInWindow.y }; var targetIndex = findItemIndexAtPosition(targetPoint, itemsRect.current, { fallbackToClosest: true }); // if not target detected, we don't need to update other items' position if (targetIndex === -1) { return; } // we keep track of the last target index (to be passed to the onSortEnd callback) lastTargetIndexRef.current = targetIndex; var isMovingRight = sourceIndex < targetIndex; // in this loop, we go over each sortable item and see if we need to update their position for (var index = 0; index < itemsRef.current.length; index += 1) { var currentItem = itemsRef.current[index]; var currentItemRect = itemsRect.current[index]; // if current index is between sourceIndex and targetIndex, we need to translate them if (isMovingRight && index >= sourceIndex && index <= targetIndex || !isMovingRight && index >= targetIndex && index <= sourceIndex) { // we need to move the item to the previous or next item position var nextItemRects = itemsRect.current[isMovingRight ? index - 1 : index + 1]; if (nextItemRects) { var translateX = nextItemRects.left - currentItemRect.left; var translateY = nextItemRects.top - currentItemRect.top; // we use `translate3d` to force using the GPU if available currentItem.style.transform = "translate3d(" + translateX + "px, " + translateY + "px, 0px)"; } } // otherwise, the item should be at its original position else { currentItem.style.transform = 'translate3d(0,0,0)'; } // we want the translation to be animated currentItem.style.transitionDuration = '300ms'; } }, onEnd: function onEnd() { // we reset all items translations (the parent is expected to sort the items in the onSortEnd callback) for (var index = 0; index < itemsRef.current.length; index += 1) { var currentItem = itemsRef.current[index]; currentItem.style.transform = ''; currentItem.style.transitionDuration = ''; } var sourceIndex = sourceIndexRef.current; if (sourceIndex !== undefined) { // show the source item again var source = itemsRef.current[sourceIndex]; if (source) { source.style.opacity = '1'; source.style.visibility = ''; } var targetIndex = lastTargetIndexRef.current; if (targetIndex !== undefined) { if (sourceIndex !== targetIndex) { // sort our internal items array itemsRef.current = arrayMove(itemsRef.current, sourceIndex, targetIndex); // let the parent know onSortEnd(sourceIndex, targetIndex); } } } sourceIndexRef.current = undefined; lastTargetIndexRef.current = undefined; // cleanup the target element from the DOM if (targetRef.current) { var holder = (customHolderRef === null || customHolderRef === void 0 ? void 0 : customHolderRef.current) || document.body; holder.removeChild(targetRef.current); targetRef.current = null; } } }); var registerItem = React.useCallback(function (item) { itemsRef.current.push(item); }, []); var removeItem = React.useCallback(function (item) { var index = itemsRef.current.indexOf(item); if (index !== -1) { itemsRef.current.splice(index, 1); } }, []); var registerKnob = React.useCallback(function (item) { knobs.current.push(item); }, []); var removeKnob = React.useCallback(function (item) { var index = knobs.current.indexOf(item); if (index !== -1) { knobs.current.splice(index, 1); } }, []); // we need to memoize the context to avoid re-rendering every children of the context provider // when not needed var context = React.useMemo(function () { return { registerItem: registerItem, removeItem: removeItem, registerKnob: registerKnob, removeKnob: removeKnob }; }, [registerItem, removeItem, registerKnob, removeKnob]); return /*#__PURE__*/React.createElement(as || DEFAULT_CONTAINER_TAG, __assign(__assign(__assign({}, allowDrag ? listeners : {}), rest), { ref: containerRef }), /*#__PURE__*/React.createElement(SortableListContext.Provider, { value: context }, children)); }; /** * SortableItem only adds a ref to its children so that we can register it to the main Sortable */ var SortableItem = function SortableItem(_a) { var children = _a.children; var context = React.useContext(SortableListContext); if (!context) { throw new Error('SortableItem must be a child of SortableList'); } var registerItem = context.registerItem, removeItem = context.removeItem; var elementRef = React.useRef(null); React.useEffect(function () { var currentItem = elementRef.current; if (currentItem) { registerItem(currentItem); } return function () { if (currentItem) { removeItem(currentItem); } }; // if the children changes, we want to re-register the DOM node }, [registerItem, removeItem, children]); return /*#__PURE__*/React.cloneElement(children, { ref: elementRef }); }; var SortableKnob = function SortableKnob(_a) { var children = _a.children; var context = React.useContext(SortableListContext); if (!context) { throw new Error('SortableKnob must be a child of SortableList'); } var registerKnob = context.registerKnob, removeKnob = context.removeKnob; var elementRef = React.useRef(null); React.useEffect(function () { var currentItem = elementRef.current; if (currentItem) { registerKnob(currentItem); } return function () { if (currentItem) { removeKnob(currentItem); } }; // if the children changes, we want to re-register the DOM node }, [registerKnob, removeKnob, children]); return /*#__PURE__*/React.cloneElement(children, { ref: elementRef }); }; export default SortableList; export { SortableItem, SortableKnob }; //# sourceMappingURL=index.module.js.map