UNPKG

@react-querybuilder/dnd

Version:

Drag-and-drop-enabled version of react-querybuilder (DnD-library-agnostic)

526 lines (525 loc) 18.6 kB
import { a as getDragItem, i as canDropOnRuleGroup, n as canDropOnInlineCombinator, o as handleDrop, r as canDropOnRule, s as isHotkeyPressed, t as buildDropResult } from "./dndLogic-Cg0Rq-DI.mjs"; import { n as computeShadowQuery, r as DragPreviewContext } from "./shadowQuery-XxKzMrJJ.mjs"; import * as React from "react"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; //#region src/adapters/dnd-kit.tsx const DragStateContext = createContext({ activeDragItem: null, timerCopyMode: false, timerGroupMode: false }); const getDragId = (type, path, qbId) => `drag-${type}-${qbId}-${path.join("_")}`; const getDropId = (type, path, qbId) => `drop-${type}-${qbId}-${path.join("_")}`; /** * Attaches dnd-kit's React synthetic event listeners (e.g. `onPointerDown`) * as native DOM event listeners on the given node. This bridges the gap * between the ref-based adapter interface and dnd-kit's listener-based API. */ const useNativeListeners = (nodeRef, listeners) => { useEffect(() => { const node = nodeRef.current; if (!node || !listeners) return void 0; const nativeHandlers = []; for (const [reactEventName, handler] of Object.entries(listeners)) { const nativeEventName = reactEventName.slice(2).toLowerCase(); const nativeHandler = (e) => { e.nativeEvent = e; return handler(e); }; node.addEventListener(nativeEventName, nativeHandler); nativeHandlers.push([nativeEventName, nativeHandler]); } return () => { for (const [name, handler] of nativeHandlers) node.removeEventListener(name, handler); }; }, [nodeRef, listeners]); }; /** * Creates a {@link DndAdapter} backed by `@dnd-kit/core`. * * The adapter uses `setActivatorNodeRef` for drag handles, so sensor listeners * are automatically attached to the correct element without imperative DOM * manipulation. * * @example * ```tsx * import { QueryBuilderDnD } from '@react-querybuilder/dnd'; * import { createDndKitAdapter } from '@react-querybuilder/dnd/dnd-kit'; * import * as DndKit from '@dnd-kit/core'; * * const adapter = createDndKitAdapter(DndKit); * * <QueryBuilderDnD dnd={adapter}> * <QueryBuilder /> * </QueryBuilderDnD> * ``` * * @group DnD */ const createDndKitAdapter = (dndKitExports) => { const { DndContext, useDraggable, useDroppable, PointerSensor, KeyboardSensor, useSensor, useSensors } = dndKitExports; const DndProvider = ({ children, updateWhileDragging, copyModeAfterHoverMs, groupModeAfterHoverMs }) => { const [activeDragItem, setActiveDragItem] = useState(null); const activeDragItemRef = useRef(null); const dragSchemaRef = useRef(null); const [timerCopyMode, setTimerCopyMode] = useState(false); const [timerGroupMode, setTimerGroupMode] = useState(false); const timerCopyModeRef = useRef(false); const timerGroupModeRef = useRef(false); const copyTimerIdRef = useRef(null); const groupTimerIdRef = useRef(null); const lastHoverTargetIdRef = useRef(null); const clearHoverTimers = useCallback(() => { if (copyTimerIdRef.current !== null) { clearTimeout(copyTimerIdRef.current); copyTimerIdRef.current = null; } if (groupTimerIdRef.current !== null) { clearTimeout(groupTimerIdRef.current); groupTimerIdRef.current = null; } timerCopyModeRef.current = false; timerGroupModeRef.current = false; setTimerCopyMode(false); setTimerGroupMode(false); lastHoverTargetIdRef.current = null; }, []); const startHoverTimers = useCallback((targetId) => { if (lastHoverTargetIdRef.current === targetId) return; clearHoverTimers(); lastHoverTargetIdRef.current = targetId; if (copyModeAfterHoverMs && copyModeAfterHoverMs > 0) copyTimerIdRef.current = setTimeout(() => { timerCopyModeRef.current = true; setTimerCopyMode(true); copyTimerIdRef.current = null; }, copyModeAfterHoverMs); if (groupModeAfterHoverMs && groupModeAfterHoverMs > 0) groupTimerIdRef.current = setTimeout(() => { timerGroupModeRef.current = true; setTimerGroupMode(true); groupTimerIdRef.current = null; }, groupModeAfterHoverMs); }, [ clearHoverTimers, copyModeAfterHoverMs, groupModeAfterHoverMs ]); const [dragPreviewState, setDragPreviewState] = useState(null); const dragPreviewStateRef = useRef(null); const onDragMoveRef = useRef(void 0); const lastTargetRef = useRef(null); const updatePreviewPosition = useCallback((targetPath, targetType, quadrant) => { const currentPreview = dragPreviewStateRef.current; // v8 ignore next if (!currentPreview || !updateWhileDragging) return; const last = lastTargetRef.current; if (last && last.quadrant === quadrant && last.targetType === targetType && last.targetPath.length === targetPath.length && last.targetPath.every((v, i) => v === targetPath[i])) return; lastTargetRef.current = { targetPath, targetType, quadrant }; // v8 ignore next -- hotkey branch tested in hotkey-specific tests const dropEffect = timerCopyModeRef.current || isHotkeyPressed(currentPreview.dropEffect === "copy" ? "alt" : "") ? "copy" : "move"; const groupItems = timerGroupModeRef.current || isHotkeyPressed("ctrl"); const result = computeShadowQuery({ originalQuery: currentPreview.originalQuery, draggedItem: activeDragItemRef.current, draggedPath: currentPreview.draggedPath, targetPath, targetType, quadrant, dropEffect, groupItems }); if (result) { const newState = { ...currentPreview, shadowQuery: result.shadowQuery, previewPath: result.previewPath, dropEffect, groupItems }; dragPreviewStateRef.current = newState; setDragPreviewState(newState); onDragMoveRef.current?.({ draggedItem: activeDragItemRef.current, shadowQuery: result.shadowQuery, originalQuery: currentPreview.originalQuery, previewPath: result.previewPath }); } }, [updateWhileDragging]); const commitDrag = useCallback(() => { const preview = dragPreviewStateRef.current; // v8 ignore next if (!preview) return; const schema = dragSchemaRef.current; if (schema && preview.shadowQuery !== preview.originalQuery) schema.dispatchQuery(preview.shadowQuery); dragSchemaRef.current = null; dragPreviewStateRef.current = null; lastTargetRef.current = null; setDragPreviewState(null); }, []); const cancelDrag = useCallback(() => { dragSchemaRef.current = null; dragPreviewStateRef.current = null; lastTargetRef.current = null; setDragPreviewState(null); }, []); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor)); const handleDragStart = useCallback((event) => { const data = event.active?.data?.current; if (data?.path && data?.schema) { const item = getDragItem(data.path, data.schema); activeDragItemRef.current = item; setActiveDragItem(item); if (updateWhileDragging) { dragSchemaRef.current = data.schema; const originalQuery = data.schema.getQuery(); const initialState = { shadowQuery: originalQuery, originalQuery, draggedPath: data.path, previewPath: data.path, dropEffect: "move", groupItems: false, qbId: data.schema.qbId }; dragPreviewStateRef.current = initialState; setDragPreviewState(initialState); } } }, [updateWhileDragging]); const handleDragOver = useCallback((event) => { const { over } = event; if (over) { const targetData = over.data?.current; const targetType = targetData?.type; const targetPath = targetData?.path; if (targetType && targetPath) startHoverTimers(`${targetType}-${targetPath.join("_")}`); } else clearHoverTimers(); // v8 ignore next if (!updateWhileDragging || !dragPreviewStateRef.current) return; if (!over) return; const { activatorEvent, delta } = event; const targetData = over.data?.current; const targetType = targetData?.type; const targetPath = targetData?.path; if (!targetType || !targetPath) return; // v8 ignore next const clientY = (activatorEvent?.clientY ?? 0) + (delta?.y ?? 0); let quadrant; if (targetType === "ruleGroup") quadrant = "upper"; else { const rect = over.rect; if (rect) { // v8 ignore next -- rect always has height from dnd-kit const height = rect.height ?? rect.bottom - rect.top; const quarterHeight = height / 4; // v8 ignore next -- rect always has top from dnd-kit const top = rect.top ?? rect.offsetTop ?? 0; const bottom = top + height; if (clientY < top + quarterHeight) quadrant = "upper"; else if (clientY > bottom - quarterHeight) quadrant = "lower"; else quadrant = null; } else quadrant = null; } if (!quadrant) return; const dragItem = activeDragItemRef.current; // v8 ignore next if (!dragItem) return; const validate = targetData?.validate; if (validate && !validate(dragItem)) return; updatePreviewPosition(targetPath, targetType, quadrant); }, [ updateWhileDragging, updatePreviewPosition, startHoverTimers, clearHoverTimers ]); const handleDragEnd = useCallback((event) => { const dragItem = activeDragItemRef.current; const { over, active } = event; const copyOverride = timerCopyModeRef.current; const groupOverride = timerGroupModeRef.current; clearHoverTimers(); if (updateWhileDragging && dragPreviewStateRef.current) if (over) commitDrag(); else cancelDrag(); else if (over && dragItem) { const sourceData = active?.data?.current; const targetData = over?.data?.current; if (sourceData && targetData?.validate?.(dragItem)) handleDrop({ item: dragItem, dropResult: targetData.getDropResult(), schema: sourceData.schema, actions: sourceData.actions, copyModeModifierKey: sourceData.copyModeModifierKey, groupModeModifierKey: sourceData.groupModeModifierKey, copyModeOverride: copyOverride, groupModeOverride: groupOverride, onRuleDrop: sourceData.onRuleDrop }); } activeDragItemRef.current = null; setActiveDragItem(null); }, [ updateWhileDragging, commitDrag, cancelDrag, clearHoverTimers ]); const handleDragCancel = useCallback(() => { clearHoverTimers(); if (updateWhileDragging && dragPreviewStateRef.current) cancelDrag(); activeDragItemRef.current = null; setActiveDragItem(null); }, [ updateWhileDragging, cancelDrag, clearHoverTimers ]); const dragStateValue = useMemo(() => ({ activeDragItem, timerCopyMode, timerGroupMode }), [ activeDragItem, timerCopyMode, timerGroupMode ]); const dragPreviewContextValue = useMemo(() => ({ dragPreviewState, updatePreviewPosition, commitDrag, cancelDrag }), [ dragPreviewState, updatePreviewPosition, commitDrag, cancelDrag ]); return /* @__PURE__ */ React.createElement(DndContext, { sensors, onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDragOver: handleDragOver, onDragCancel: handleDragCancel }, /* @__PURE__ */ React.createElement(DragStateContext.Provider, { value: dragStateValue }, /* @__PURE__ */ React.createElement(DragPreviewContext.Provider, { value: dragPreviewContextValue }, children))); }; const useRuleDnD = (params) => { const { activeDragItem, timerCopyMode, timerGroupMode } = useContext(DragStateContext); const activatorNodeRef = useRef(null); const containerNodeRef = useRef(null); const dragId = getDragId("rule", params.path, params.schema.qbId); const dropId = getDropId("rule", params.path, params.schema.qbId); const { setNodeRef: setDragNodeRef, setActivatorNodeRef, isDragging, listeners, attributes } = useDraggable({ id: dragId, disabled: params.disabled, data: { path: params.path, schema: params.schema, actions: params.actions, copyModeModifierKey: params.copyModeModifierKey, groupModeModifierKey: params.groupModeModifierKey, onRuleDrop: params.onRuleDrop } }); const { setNodeRef: setDropNodeRef, isOver: rawIsOver } = useDroppable({ id: dropId, data: { type: "rule", path: params.path, schema: params.schema, validate: (dragging) => canDropOnRule({ dragging, path: params.path, schema: params.schema, canDrop: params.canDrop, groupModeModifierKey: params.groupModeModifierKey, disabled: params.disabled, rule: params.rule }), getDropResult: () => buildDropResult({ type: "rule", path: params.path, schema: params.schema, copyModeModifierKey: params.copyModeModifierKey, groupModeModifierKey: params.groupModeModifierKey }) } }); const canDropHere = rawIsOver && !!activeDragItem && canDropOnRule({ dragging: activeDragItem, path: params.path, schema: params.schema, canDrop: params.canDrop, groupModeModifierKey: params.groupModeModifierKey, disabled: params.disabled, rule: params.rule }); const isOver = rawIsOver && canDropHere; const dropNotAllowed = rawIsOver && !canDropHere; const dndRef = useCallback((node) => { containerNodeRef.current = node; setDragNodeRef(node); setDropNodeRef(node); }, [setDragNodeRef, setDropNodeRef]); const dragRef = useCallback((node) => { activatorNodeRef.current = node; setActivatorNodeRef(node); }, [setActivatorNodeRef]); useEffect(() => { const node = activatorNodeRef.current; if (!node || !attributes) return; for (const [key, value] of Object.entries(attributes)) if (value != null) node.setAttribute(key === "tabIndex" ? "tabindex" : key, String(value)); }, [attributes]); useNativeListeners(activatorNodeRef, listeners); return { isDragging, dragMonitorId: dragId, isOver, dropMonitorId: dropId, dndRef, dragRef, dropEffect: timerCopyMode || isHotkeyPressed(params.copyModeModifierKey) ? "copy" : "move", groupItems: timerGroupMode || isHotkeyPressed(params.groupModeModifierKey), dropNotAllowed }; }; const useRuleGroupDnD = (params) => { const { activeDragItem, timerCopyMode, timerGroupMode } = useContext(DragStateContext); const activatorNodeRef = useRef(null); const dragId = getDragId("ruleGroup", params.path, params.schema.qbId); const dropId = getDropId("ruleGroup", params.path, params.schema.qbId); const isDragDisabled = params.disabled || params.path.length === 0; const { setNodeRef: setDragNodeRef, setActivatorNodeRef, isDragging, listeners, attributes } = useDraggable({ id: dragId, disabled: isDragDisabled, data: { path: params.path, schema: params.schema, actions: params.actions, copyModeModifierKey: params.copyModeModifierKey, groupModeModifierKey: params.groupModeModifierKey, onRuleDrop: params.onRuleDrop } }); const { setNodeRef: setDropNodeRef, isOver: rawIsOver } = useDroppable({ id: dropId, data: { type: "ruleGroup", path: params.path, schema: params.schema, validate: (dragging) => canDropOnRuleGroup({ dragging, path: params.path, schema: params.schema, canDrop: params.canDrop, disabled: params.disabled, ruleGroup: params.ruleGroup }), getDropResult: () => buildDropResult({ type: "ruleGroup", path: params.path, schema: params.schema, copyModeModifierKey: params.copyModeModifierKey, groupModeModifierKey: params.groupModeModifierKey }) } }); const canDropHere = rawIsOver && !!activeDragItem && canDropOnRuleGroup({ dragging: activeDragItem, path: params.path, schema: params.schema, canDrop: params.canDrop, disabled: params.disabled, ruleGroup: params.ruleGroup }); const isOver = rawIsOver && canDropHere; const dropNotAllowed = rawIsOver && !canDropHere; const previewRef = useCallback((node) => { setDragNodeRef(node); }, [setDragNodeRef]); const dropRef = useCallback((node) => { setDropNodeRef(node); }, [setDropNodeRef]); const dragRef = useCallback((node) => { activatorNodeRef.current = node; setActivatorNodeRef(node); }, [setActivatorNodeRef]); useEffect(() => { const node = activatorNodeRef.current; if (!node || !attributes || isDragDisabled) return; for (const [key, value] of Object.entries(attributes)) if (value != null) node.setAttribute(key === "tabIndex" ? "tabindex" : key, String(value)); }, [attributes, isDragDisabled]); useNativeListeners(activatorNodeRef, listeners); return { isDragging, dragMonitorId: dragId, isOver, dropMonitorId: dropId, previewRef, dragRef, dropRef, dropEffect: timerCopyMode || isHotkeyPressed(params.copyModeModifierKey) ? "copy" : "move", groupItems: timerGroupMode || isHotkeyPressed(params.groupModeModifierKey), dropNotAllowed }; }; const useInlineCombinatorDnD = (params) => { const { activeDragItem, timerCopyMode } = useContext(DragStateContext); const dropId = getDropId("inlineCombinator", params.path, params.schema.qbId); const hoveringItem = (params.rules ?? [])[params.path.at(-1) - 1]; const { setNodeRef: setDropNodeRef, isOver: rawIsOver } = useDroppable({ id: dropId, data: { type: "inlineCombinator", path: params.path, schema: params.schema, validate: (dragging) => canDropOnInlineCombinator({ dragging, path: params.path, schema: params.schema, canDrop: params.canDrop, groupModeModifierKey: params.groupModeModifierKey, hoveringItem }), getDropResult: () => buildDropResult({ type: "inlineCombinator", path: params.path, schema: params.schema, copyModeModifierKey: params.copyModeModifierKey, groupModeModifierKey: params.groupModeModifierKey }) } }); const canDropHere = rawIsOver && !!activeDragItem && canDropOnInlineCombinator({ dragging: activeDragItem, path: params.path, schema: params.schema, canDrop: params.canDrop, groupModeModifierKey: params.groupModeModifierKey, hoveringItem }); const isOver = rawIsOver && canDropHere; const dropNotAllowed = rawIsOver && !canDropHere; return { dropRef: useCallback((node) => { setDropNodeRef(node); }, [setDropNodeRef]), dropMonitorId: dropId, isOver, dropEffect: timerCopyMode || isHotkeyPressed(params.copyModeModifierKey) ? "copy" : "move", dropNotAllowed }; }; return { DndProvider, useRuleDnD, useRuleGroupDnD, useInlineCombinatorDnD }; }; //#endregion export { createDndKitAdapter as t }; //# sourceMappingURL=dnd-kit-D5Ge4yu_.mjs.map