UNPKG

@human-tools/use-sortable

Version:

Simple implementation that allows you to reorder items easily using useSortable hook.

238 lines (216 loc) 9.36 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var react = require('react'); function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } /** * Shift array items moving srcIndex before/after targetIndex. * TODO: Support multiple adjacent indecies being shifted. * TODO: Move this to its own package. * @param array The array you want to manipulate * @param srcIndex Index of the item that is moving. * @param targetIndex Index where the item should be placed. * @param insertBeforeTarget Wether the srcItem should be placed before or after the target index. * @returns The new array */ const shift = (array, srcIndex, targetIndex, insertBeforeTarget = false) => { if (srcIndex < 0 || srcIndex > array.length || targetIndex < 0 || targetIndex > array.length || srcIndex === targetIndex) return array; const minIndex = Math.min(srcIndex, targetIndex); const maxIndex = Math.max(srcIndex, targetIndex); const newArr = [...array.slice(0, minIndex), ...(srcIndex < targetIndex ? array.slice(minIndex + 1, maxIndex) : []), insertBeforeTarget ? array[srcIndex] : array[targetIndex], insertBeforeTarget ? array[targetIndex] : array[srcIndex], ...(srcIndex > targetIndex ? array.slice(minIndex + 1, maxIndex) : []), ...array.slice(maxIndex + 1)]; return newArr; }; /** * TODO: Allow user to cancel a reorder while dragging an element * by hitting ESC. */ const useSortable = (items, options) => { const settings = react.useMemo(() => _objectSpread2({ multiple: false, animate: true, animationDelayFunction: () => 0, animationDurationFunction: () => 0.3, animationTimingFunction: () => 'cubic-bezier(0, 1.28, 1, 1)', draggingClassNames: ['dragging'], dragoverClassNames: ['dragover'] }, options), [options]); const [draggedItemIndex, setDraggedItemIndex] = react.useState(); const [dropTargetIndex, setDropTargetIndex] = react.useState(); const [shouldInsertBefore, setShouldInsertBefore] = react.useState(false); const [draggableNodes, setDraggableNodes] = react.useState([]); const [orderedItems, setOrderedItems] = react.useState(items); // FIXME: originalRects need to update when the layout updates or resize. // Need a ResizingObserver on the container ref probably. const [originalRects, setOriginalRects] = react.useState([]); const [isUpdatingRects, setIsUpdatingRects] = react.useState(false); const setItems = react.useCallback(items => { setDraggableNodes([]); setOriginalRects([]); setDropTargetIndex(undefined); setDraggedItemIndex(undefined); setOrderedItems(items); }, []); const addDraggableNodeRef = react.useCallback(node => { if (!node) return; setDraggableNodes(nodes => [...nodes, node]); setOriginalRects(rects => [...rects, node.getBoundingClientRect()]); }, [orderedItems]); const findItemIndexFromNode = react.useCallback(node => draggableNodes.findIndex(draggableNode => draggableNode === node), [draggableNodes]); const onDragStart = react.useCallback(e => { const el = e.target; if (!el) return; el.classList.add(...settings.draggingClassNames); const draggedIndex = findItemIndexFromNode(el); setDraggedItemIndex(draggedIndex); setDropTargetIndex(undefined); setShouldInsertBefore(false); }, [findItemIndexFromNode, setDraggedItemIndex, settings.draggingClassNames]); const updateTransforms = react.useCallback((srcIdx, targetIdx) => { setIsUpdatingRects(true); const newItemRects = shift(originalRects, srcIdx, targetIdx, shouldInsertBefore); const newDraggableNodes = shift(draggableNodes, srcIdx, targetIdx, shouldInsertBefore); newDraggableNodes.forEach((node, index) => { const oldRect = originalRects[index]; const newRect = newItemRects[index]; const sign = -1; const translateX = sign * (newRect.x - oldRect.x); const translateY = sign * (newRect.y - oldRect.y); node.style.transition = `transform ${settings.animationDurationFunction(index)}s ${settings.animationDelayFunction(index)}s ${settings.animationTimingFunction(index)}`; node.style.transform = `translate(${translateX}px, ${translateY}px)`; }); setIsUpdatingRects(false); }, [shouldInsertBefore, originalRects, draggableNodes, setIsUpdatingRects, settings.animationDelayFunction, settings.animationDurationFunction, settings.animationTimingFunction]); const onDragOver = react.useCallback(e => { e.preventDefault(); const el = e.target; if (!el || draggedItemIndex === undefined) return; const srcIdx = draggedItemIndex; const targetIdx = findItemIndexFromNode(el); const draggedNode = draggableNodes[draggedItemIndex]; if (targetIdx === -1) { return; } if (draggedNode === el || srcIdx === targetIdx) { return; } if (!isUpdatingRects) { settings.animate && updateTransforms(srcIdx, targetIdx); setDropTargetIndex(targetIdx); setShouldInsertBefore(srcIdx > targetIdx); } return false; }, [draggedItemIndex, draggableNodes, findItemIndexFromNode, updateTransforms, isUpdatingRects, setDropTargetIndex, setShouldInsertBefore, settings.animate]); const onDragEnter = react.useCallback(e => { e.preventDefault(); const el = e.target; if (!el) return; el.classList.add(...settings.dragoverClassNames); return false; }, [settings.dragoverClassNames]); const onDragLeave = react.useCallback(e => { e.preventDefault(); const el = e.target; if (!el) return; el.classList.remove(...settings.dragoverClassNames); return false; }, [settings.dragoverClassNames]); const onDrop = react.useCallback(e => { e.preventDefault(); const el = e.target; if (!el) return; el.classList.remove(...settings.dragoverClassNames); return false; }, [settings.dragoverClassNames]); const onDragEnd = react.useCallback(e => { e.preventDefault(); const el = e.target; if (!el) return; el.classList.remove(...settings.draggingClassNames); if (draggedItemIndex === undefined || dropTargetIndex === undefined) return; settings.animate && draggableNodes.forEach(node => { node.style.transition = ''; node.style.transform = ''; }); setItems(shift(orderedItems, draggedItemIndex, dropTargetIndex, shouldInsertBefore)); setDraggableNodes(shift(draggableNodes, draggedItemIndex, dropTargetIndex, shouldInsertBefore)); return false; }, [draggedItemIndex, dropTargetIndex, setItems, orderedItems, setDraggableNodes, draggableNodes, shouldInsertBefore, settings.draggingClassNames]); const containerRef = react.useRef(); const setContainerRef = react.useCallback(containerNode => { if (containerNode) containerRef.current = containerNode; }, []); react.useEffect(() => { const containerNode = containerRef.current; if (containerNode) { containerNode.addEventListener('dragstart', onDragStart); containerNode.addEventListener('dragover', onDragOver); containerNode.addEventListener('dragenter', onDragEnter); containerNode.addEventListener('dragleave', onDragLeave); containerNode.addEventListener('dragend', onDragEnd); containerNode.addEventListener('drop', onDrop); draggableNodes.forEach(node => { node.setAttribute('draggable', 'true'); }); } return () => { if (containerNode) { containerNode.removeEventListener('dragstart', onDragStart); containerNode.removeEventListener('dragover', onDragOver); containerNode.removeEventListener('dragenter', onDragEnter); containerNode.removeEventListener('dragleave', onDragLeave); containerNode.removeEventListener('dragend', onDragEnd); containerNode.removeEventListener('drop', onDrop); draggableNodes.forEach(node => { node.removeAttribute('draggable'); }); } }; }, [onDragStart, onDragOver, onDragEnd, onDragEnter, onDragLeave, onDrop, draggableNodes]); return { orderedItems, setItems, setContainerRef, addDraggableNodeRef }; }; exports.shift = shift; exports.useSortable = useSortable; //# sourceMappingURL=index.js.map