UNPKG

fluid-dnd

Version:

An agnostic drag and drop library to sort all kind of lists. With current support for vue, react and svelte

335 lines (334 loc) 15 kB
import { assignDraggingEvent, convetEventToDragMouseTouchEvent, moveTranslate, setCustomFixedSize, setEventWithInterval, setTranistion } from './utils/SetStyles'; import { usePositioning } from './positioning/usePositioning'; import { DRAG_EVENT, draggableTargetTimingFunction, START_DRAG_EVENT, START_DROP_EVENT } from './utils'; import ConfigHandler from './config/configHandler'; import { IsHTMLElement, isTouchEvent } from './utils/typesCheckers'; import { addTempChild, removeTempChildrens } from './tempChildren'; import { DroppableConfigurator } from './config/droppableConfigurator'; import { addClass, containClass, getClassesList, getClassesSelector, toggleClass } from './utils/dom/classList'; import { DRAGGABLE_CLASS, DRAGGING_CLASS, DROPPABLE_CLASS, HANDLER_CLASS } from './utils/classes'; import useDragAndDropEvents from './events/dragAndDrop/dragAndDrop'; import { getRect, isSameNode } from './utils/GetStyles'; const ON_MOUSEDOWN = 'onmousedown'; var DraggingState; (function (DraggingState) { DraggingState[DraggingState["NOT_DRAGGING"] = 0] = "NOT_DRAGGING"; DraggingState[DraggingState["START_DRAGGING"] = 1] = "START_DRAGGING"; DraggingState[DraggingState["DRAGING"] = 2] = "DRAGING"; DraggingState[DraggingState["END_DRAGGING"] = 3] = "END_DRAGGING"; })(DraggingState || (DraggingState = {})); export default function useDraggable(draggableElement, index, config, parent, handlerPublisher) { const { handlerSelector, isDraggable, droppableGroup, animationDuration, draggingClass, droppableClass, onDragStart, delayBeforeTouchMoveEvent, coordinateTransform } = config; const droppableGroupClass = getClassesList(droppableGroup) .map((classGroup) => `droppable-group-${classGroup}`) .join(' '); let draggingState = DraggingState.NOT_DRAGGING; let windowScroll = { scrollX: 0, scrollY: 0 }; let pagePosition = { pageX: 0, pageY: 0 }; let delayTimeout; let initialTouch; const [setTransform, updateTransformState] = usePositioning(draggableElement, coordinateTransform); const endDraggingState = () => { draggingState = DraggingState.NOT_DRAGGING; }; const [emitDraggingEvent, emitDroppingEvent, toggleDraggingClass] = useDragAndDropEvents(config, index, parent, droppableGroupClass, handlerPublisher, endDraggingState); const setDraggable = () => { addClass(draggableElement, DRAGGABLE_CLASS); }; const addHandlerClass = (handlerElement) => { addClass(handlerElement, HANDLER_CLASS); handlerPublisher.addSubscriber(handlerElement); }; const setHandlerStyles = () => { if (isDraggable(draggableElement)) { const handlerElement = draggableElement.querySelector(handlerSelector); if (handlerElement) { addHandlerClass(handlerElement); } else { addHandlerClass(draggableElement); } } }; const setCssStyles = () => { setHandlerStyles(); setDraggable(); }; const getHandler = (element) => { const handler = element?.querySelector(`.${HANDLER_CLASS}`); const handlerParent = handler?.parentElement; if (handler && handlerParent && containClass(handlerParent, DROPPABLE_CLASS) && !isSameNode(parent, handlerParent)) { return null; } return handler; }; const setSlotRefElementParams = (element) => { const handlerElement = (getHandler(element) ?? element); if (handlerElement && isDraggable(element)) { assignDraggingEvent(handlerElement, ON_MOUSEDOWN, onmousedown('mousemove', 'mouseup')); assignDraggingEvent(handlerElement, 'ontouchstart', onmousedown('touchmove', 'touchend'), (event) => { const touch = event.touches[0]; initialTouch = { x: touch.clientX, y: touch.clientY }; }); disableMousedownEventFromImages(handlerElement); } if (!isSameNode(element, handlerElement)) { assignDraggingEvent(element, ON_MOUSEDOWN, mousedownOnDraggablefunction); } addClass(parent, DROPPABLE_CLASS); }; const disableMousedownEventFromImages = (handlerElement) => { const images = handlerElement.querySelectorAll('img'); Array.from(images).forEach((image) => { image.onmousedown = () => false; }); }; const setTransformDragEvent = () => { if (pagePosition.pageX == 0 && pagePosition.pageY == 0) { return; } if (!droppableConfigurator.current) { return; } const { droppable, config } = droppableConfigurator.current; setTransform(draggableElement, droppable, pagePosition, config.direction); emitDraggingEvent(draggableElement, DRAG_EVENT, droppable, config); }; const removeTranslates = (droppable) => { const drgagables = droppable.querySelectorAll(`.${DRAGGABLE_CLASS}`); for (const draggable of drgagables) { moveTranslate(draggable); } }; const changeDroppable = (newdDroppableConfig, oldDroppableConfig) => { if (oldDroppableConfig && draggingState == DraggingState.DRAGING && !isSameNode(newdDroppableConfig?.droppable, oldDroppableConfig.droppable)) { emitDraggingEvent(draggableElement, DRAG_EVENT, oldDroppableConfig.droppable, oldDroppableConfig.config); removeTranslates(oldDroppableConfig.droppable); } }; const droppableConfigurator = new DroppableConfigurator(draggableElement, droppableGroupClass, parent, setTransformDragEvent, changeDroppable, config.mapFrom); const toggleDroppableClass = (isOutside) => { if (!droppableConfigurator.current) { return; } const droppables = droppableGroupClass ? Array.from(document.querySelectorAll(getClassesSelector(droppableGroupClass))) : [parent]; for (const droppable of droppables) { droppable.classList.toggle(droppableClass, !isOutside && isSameNode(droppable, droppableConfigurator.current.droppable)); } }; const onMove = (event, isTouchEvent = false) => { droppableConfigurator.updateConfig(event); const isOutside = droppableConfigurator.isOutside(event); toggleDroppableClass(isOutside); if (draggingState === DraggingState.START_DRAGGING && !isTouchEvent) { startDragging(event); } else if (draggingState === DraggingState.DRAGING) { updateTempChildren(isOutside); setTransformEvent(event); } }; const updateTempChildren = (isOutside = true) => { if (!droppableConfigurator.current) { return; } const { droppable } = droppableConfigurator.current; removeTempChildrens(droppable, parent, droppableGroupClass, animationDuration, isOutside); if (isOutside) { return; } addTempChild(draggableElement, parent, draggingState == DraggingState.START_DRAGGING, droppableConfigurator.current); }; const cursorWasNotMoved = (event) => { if (isTouchEvent(event) && initialTouch && draggingState == DraggingState.START_DRAGGING) { const touch = event.touches[0]; const movedX = Math.abs(touch.clientX - initialTouch.x); const movedY = Math.abs(touch.clientY - initialTouch.y); if (Math.abs(movedX) > 5 && Math.abs(movedY) > 5) { clearTimeout(delayTimeout); return false; } } return true; }; const handleMove = (event) => { clearTimeout(delayTimeout); const eventToDragMouse = convetEventToDragMouseTouchEvent(event); if (isTouchEvent(event) && event.cancelable && draggingState == DraggingState.DRAGING) { event.preventDefault(); } if ((isTouchEvent(event) && !event.cancelable) || !cursorWasNotMoved(event)) { disableDragging('touchmove', event); return; } onMove(eventToDragMouse, isTouchEvent(event)); }; const addTouchDeviceDelay = (event, callback) => { if (event == 'touchmove') { delayTimeout = setTimeout(() => { callback(); }, delayBeforeTouchMoveEvent); } else { callback(); } }; const cursorIsOnChildDraggable = (event, element) => { const { clientX, clientY } = event; const elementBelow = document.elementFromPoint(clientX, clientY); const draggableAncestor = elementBelow?.closest(`.${DRAGGABLE_CLASS}`); return draggableAncestor && isSameNode(element, draggableAncestor); }; const getDragStartEventData = (element) => { const value = config.onGetValue(index); return { index, element, value }; }; const startTouchMoveEvent = (event) => { droppableConfigurator.updateConfig(event); toggleDroppableClass(droppableConfigurator.isOutside(event)); startDragging(event); }; const onmousedown = (moveEvent, onLeaveEvent) => { return (event) => { if (!cursorIsOnChildDraggable(event, draggableElement)) { return; } ConfigHandler.updateScrolls(parent, droppableGroupClass); const { scrollX, scrollY } = window; windowScroll = { scrollX, scrollY }; if (draggingState === DraggingState.NOT_DRAGGING) { draggingState = DraggingState.START_DRAGGING; const data = getDragStartEventData(draggableElement); data && onDragStart(data); addTouchDeviceDelay(moveEvent, () => { if (moveEvent == 'touchmove') { startTouchMoveEvent(event); } }); document.addEventListener(moveEvent, handleMove, { passive: false }); makeScrollEventOnDroppable(parent); document.addEventListener(onLeaveEvent, onLeave(moveEvent), { once: true }); } }; }; const mousedownOnDraggablefunction = (event) => { return droppableConfigurator.updateConfig(event); }; const onLeave = (moveEvent) => { return (event) => { disableDragging(moveEvent, event); }; }; const disableDragging = (moveEvent, event) => { toggleDroppableClass(true); const convertedEvent = convetEventToDragMouseTouchEvent(event); onDropDraggingEvent(droppableConfigurator.isOutside(convertedEvent)); clearTimeout(delayTimeout); document.removeEventListener(moveEvent, handleMove); droppableConfigurator.updateConfig(convertedEvent); const currentConfig = droppableConfigurator.getCurrentConfig(convertedEvent); if (currentConfig) { const { droppable } = currentConfig; removeOnScrollEvents(droppable); } parent.onscroll = null; endDraggingState(); }; const removeOnScrollEvents = (droppable) => { droppable.onscroll = null; if (!droppableGroupClass) { return; } const droppables = Array.from(document.querySelectorAll(getClassesSelector(droppableGroupClass))); for (const droppable of droppables) { if (IsHTMLElement(droppable)) { droppable.onscroll = null; } } }; const startDragging = (event) => { droppableConfigurator.current && addTempChild(draggableElement, parent, draggingState == DraggingState.START_DRAGGING, droppableConfigurator.current); updateDraggingStateBeforeDragging(); droppableConfigurator.current && emitDraggingEvent(draggableElement, START_DRAG_EVENT, droppableConfigurator.current.droppable, droppableConfigurator.current.config); setDraggingStyles(draggableElement); updateTransformState(event, draggableElement); }; const updateDraggingStateBeforeDragging = () => { draggingState = DraggingState.DRAGING; }; const setTransformEvent = (event) => { const { pageX, pageY } = event; pagePosition = { pageX, pageY }; setTransformDragEvent(); }; const makeScrollEventOnDroppable = (droppable) => { return setEventWithInterval(droppable, 'onscroll', onScrollEvent); }; const onScrollEvent = () => { return setTransformDragEvent(); }; const onDropDraggingEvent = (isOutsideAllDroppables) => { if (draggingState !== DraggingState.DRAGING && draggingState !== DraggingState.START_DRAGGING) { endDraggingState(); return; } draggingState = DraggingState.END_DRAGGING; removeDraggingStyles(draggableElement); if (draggableElement.classList.contains(DRAGGING_CLASS)) { emitDroppingEvent(draggableElement, START_DROP_EVENT, isOutsideAllDroppables ? droppableConfigurator.initial : droppableConfigurator.current, windowScroll, index); } }; const removeDraggingStyles = (element) => { setTranistion(element, animationDuration, draggableTargetTimingFunction); moveTranslate(element); }; const setDraggingStyles = (element) => { const { height, width } = getRect(element); setCustomFixedSize(element, { fixedHeight: `${height}px`, fixedWidth: `${width}px` }); toggleDraggingClass(element, true); toggleClass(element, draggingClass, true); element.style.transition = ''; }; const removeAtFromElement = (targetIndex) => { import('./events/remove').then((module) => { const [removeAt] = module.default(config, parent, handlerPublisher, endDraggingState); droppableConfigurator.initial && removeAt(index, targetIndex, draggableElement, droppableConfigurator.initial); }); }; const insertAtFromElement = (targetIndex, value) => { if (targetIndex === index || (targetIndex === config.onGetLegth() && index === targetIndex - 1)) { import('./events/insert').then((module) => { const [emitInsertEventToSiblings] = module.default(config, parent, handlerPublisher, endDraggingState); droppableConfigurator.initial && emitInsertEventToSiblings(targetIndex, draggableElement, parent, value, droppableConfigurator.initial); }); } }; setCssStyles(); setSlotRefElementParams(draggableElement); return [removeAtFromElement, insertAtFromElement]; }