@human-tools/use-sortable
Version:
Simple implementation that allows you to reorder items easily using useSortable hook.
238 lines (216 loc) • 9.36 kB
JavaScript
;
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