UNPKG

@react-querybuilder/dnd

Version:

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

531 lines (530 loc) 19.2 kB
const require_dndLogic = require("./dndLogic-DRubwv4d.js"); const require_shadowQuery = require("./shadowQuery-DXKizE6P.js"); let react = require("react"); react = require_dndLogic.__toESM(react); //#region src/adapters/dnd-kit.tsx const DragStateContext = (0, react.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) => { (0, react.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] = (0, react.useState)(null); const activeDragItemRef = (0, react.useRef)(null); const dragSchemaRef = (0, react.useRef)(null); const [timerCopyMode, setTimerCopyMode] = (0, react.useState)(false); const [timerGroupMode, setTimerGroupMode] = (0, react.useState)(false); const timerCopyModeRef = (0, react.useRef)(false); const timerGroupModeRef = (0, react.useRef)(false); const copyTimerIdRef = (0, react.useRef)(null); const groupTimerIdRef = (0, react.useRef)(null); const lastHoverTargetIdRef = (0, react.useRef)(null); const clearHoverTimers = (0, react.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 = (0, react.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] = (0, react.useState)(null); const dragPreviewStateRef = (0, react.useRef)(null); const onDragMoveRef = (0, react.useRef)(void 0); const lastTargetRef = (0, react.useRef)(null); const updatePreviewPosition = (0, react.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 || require_dndLogic.isHotkeyPressed(currentPreview.dropEffect === "copy" ? "alt" : "") ? "copy" : "move"; const groupItems = timerGroupModeRef.current || require_dndLogic.isHotkeyPressed("ctrl"); const result = require_shadowQuery.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 = (0, react.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 = (0, react.useCallback)(() => { dragSchemaRef.current = null; dragPreviewStateRef.current = null; lastTargetRef.current = null; setDragPreviewState(null); }, []); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor)); const handleDragStart = (0, react.useCallback)((event) => { const data = event.active?.data?.current; if (data?.path && data?.schema) { const item = require_dndLogic.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 = (0, react.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 = (0, react.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)) require_dndLogic.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 = (0, react.useCallback)(() => { clearHoverTimers(); if (updateWhileDragging && dragPreviewStateRef.current) cancelDrag(); activeDragItemRef.current = null; setActiveDragItem(null); }, [ updateWhileDragging, cancelDrag, clearHoverTimers ]); const dragStateValue = (0, react.useMemo)(() => ({ activeDragItem, timerCopyMode, timerGroupMode }), [ activeDragItem, timerCopyMode, timerGroupMode ]); const dragPreviewContextValue = (0, react.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(require_shadowQuery.DragPreviewContext.Provider, { value: dragPreviewContextValue }, children))); }; const useRuleDnD = (params) => { const { activeDragItem, timerCopyMode, timerGroupMode } = (0, react.useContext)(DragStateContext); const activatorNodeRef = (0, react.useRef)(null); const containerNodeRef = (0, react.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) => require_dndLogic.canDropOnRule({ dragging, path: params.path, schema: params.schema, canDrop: params.canDrop, groupModeModifierKey: params.groupModeModifierKey, disabled: params.disabled, rule: params.rule }), getDropResult: () => require_dndLogic.buildDropResult({ type: "rule", path: params.path, schema: params.schema, copyModeModifierKey: params.copyModeModifierKey, groupModeModifierKey: params.groupModeModifierKey }) } }); const canDropHere = rawIsOver && !!activeDragItem && require_dndLogic.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 = (0, react.useCallback)((node) => { containerNodeRef.current = node; setDragNodeRef(node); setDropNodeRef(node); }, [setDragNodeRef, setDropNodeRef]); const dragRef = (0, react.useCallback)((node) => { activatorNodeRef.current = node; setActivatorNodeRef(node); }, [setActivatorNodeRef]); (0, react.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 || require_dndLogic.isHotkeyPressed(params.copyModeModifierKey) ? "copy" : "move", groupItems: timerGroupMode || require_dndLogic.isHotkeyPressed(params.groupModeModifierKey), dropNotAllowed }; }; const useRuleGroupDnD = (params) => { const { activeDragItem, timerCopyMode, timerGroupMode } = (0, react.useContext)(DragStateContext); const activatorNodeRef = (0, react.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) => require_dndLogic.canDropOnRuleGroup({ dragging, path: params.path, schema: params.schema, canDrop: params.canDrop, disabled: params.disabled, ruleGroup: params.ruleGroup }), getDropResult: () => require_dndLogic.buildDropResult({ type: "ruleGroup", path: params.path, schema: params.schema, copyModeModifierKey: params.copyModeModifierKey, groupModeModifierKey: params.groupModeModifierKey }) } }); const canDropHere = rawIsOver && !!activeDragItem && require_dndLogic.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 = (0, react.useCallback)((node) => { setDragNodeRef(node); }, [setDragNodeRef]); const dropRef = (0, react.useCallback)((node) => { setDropNodeRef(node); }, [setDropNodeRef]); const dragRef = (0, react.useCallback)((node) => { activatorNodeRef.current = node; setActivatorNodeRef(node); }, [setActivatorNodeRef]); (0, react.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 || require_dndLogic.isHotkeyPressed(params.copyModeModifierKey) ? "copy" : "move", groupItems: timerGroupMode || require_dndLogic.isHotkeyPressed(params.groupModeModifierKey), dropNotAllowed }; }; const useInlineCombinatorDnD = (params) => { const { activeDragItem, timerCopyMode } = (0, react.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) => require_dndLogic.canDropOnInlineCombinator({ dragging, path: params.path, schema: params.schema, canDrop: params.canDrop, groupModeModifierKey: params.groupModeModifierKey, hoveringItem }), getDropResult: () => require_dndLogic.buildDropResult({ type: "inlineCombinator", path: params.path, schema: params.schema, copyModeModifierKey: params.copyModeModifierKey, groupModeModifierKey: params.groupModeModifierKey }) } }); const canDropHere = rawIsOver && !!activeDragItem && require_dndLogic.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: (0, react.useCallback)((node) => { setDropNodeRef(node); }, [setDropNodeRef]), dropMonitorId: dropId, isOver, dropEffect: timerCopyMode || require_dndLogic.isHotkeyPressed(params.copyModeModifierKey) ? "copy" : "move", dropNotAllowed }; }; return { DndProvider, useRuleDnD, useRuleGroupDnD, useInlineCombinatorDnD }; }; //#endregion Object.defineProperty(exports, "createDndKitAdapter", { enumerable: true, get: function() { return createDndKitAdapter; } }); //# sourceMappingURL=dnd-kit-BW69ArxK.js.map