UNPKG

@react-querybuilder/dnd

Version:

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

551 lines (550 loc) 19.3 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/quadrantDetection.ts /** * Determines whether the cursor is in the upper quadrant (top 25%), * lower quadrant (bottom 25%), or middle zone (center 50%) of an element. * * Returns `null` when the cursor is in the middle zone, indicating * no positional change should occur. */ const getQuadrant = (element, clientY) => { const rect = element.getBoundingClientRect(); const quarterHeight = rect.height / 4; if (clientY < rect.top + quarterHeight) return "upper"; if (clientY > rect.bottom - quarterHeight) return "lower"; return null; }; //#endregion //#region src/adapters/pragmatic-dnd.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("_")}`; /** * Creates a {@link DndAdapter} backed by `@atlaskit/pragmatic-drag-and-drop`. * * @example * ```tsx * import { QueryBuilderDnD } from '@react-querybuilder/dnd'; * import { createPragmaticDndAdapter } from '@react-querybuilder/dnd/pragmatic-dnd'; * import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; * import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; * * const adapter = createPragmaticDndAdapter({ draggable, dropTargetForElements, monitorForElements, combine }); * * <QueryBuilderDnD dnd={adapter}> * <QueryBuilder /> * </QueryBuilderDnD> * ``` * * @group DnD */ const createPragmaticDndAdapter = (pdndExports) => { const { draggable, dropTargetForElements, monitorForElements, combine } = pdndExports; 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 already 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); }, []); useEffect(() => { const cleanup = monitorForElements({ onDragStart({ source }) { const data = source.data; if (data.__rqbPath && data.__rqbSchema) { const item = getDragItem(data.__rqbPath, data.__rqbSchema); activeDragItemRef.current = item; setActiveDragItem(item); if (updateWhileDragging) { const schema = data.__rqbSchema; dragSchemaRef.current = schema; const originalQuery = schema.getQuery(); const initialState = { shadowQuery: originalQuery, originalQuery, draggedPath: data.__rqbPath, previewPath: data.__rqbPath, dropEffect: "move", groupItems: false, qbId: schema.qbId }; dragPreviewStateRef.current = initialState; setDragPreviewState(initialState); } } }, onDrag({ location }) { const dropTargets = location.current.dropTargets; if (dropTargets.length > 0) { const target = dropTargets[0]; const targetType = target.data.__rqbType; const targetPath = target.data.__rqbPath; if (targetType && targetPath) startHoverTimers(`${targetType}-${targetPath.join("_")}`); } else clearHoverTimers(); // v8 ignore next if (!updateWhileDragging || !dragPreviewStateRef.current) return; if (dropTargets.length === 0) return; const target = dropTargets[0]; const targetType = target.data.__rqbType; const targetPath = target.data.__rqbPath; if (!targetType || !targetPath) return; const quadrant = targetType === "ruleGroup" ? "upper" : getQuadrant(target.element, location.current.input.clientY); if (!quadrant) return; const dragItem = activeDragItemRef.current; // v8 ignore next if (!dragItem) return; const validate = target.data.__rqbValidate; if (validate && !validate(dragItem)) return; updatePreviewPosition(targetPath, targetType, quadrant); }, onDrop({ source, location }) { const dragItem = activeDragItemRef.current; const sourceData = source.data; const dropTargets = location.current.dropTargets; const copyOverride = timerCopyModeRef.current; const groupOverride = timerGroupModeRef.current; clearHoverTimers(); if (updateWhileDragging && dragPreviewStateRef.current) if (dropTargets.length > 0) commitDrag(); else cancelDrag(); else if (dragItem && dropTargets.length > 0) { const targetData = dropTargets[0].data; const validate = targetData.__rqbValidate; if (validate?.(dragItem)) { const getDropResultFn = targetData.__rqbGetDropResult; const dropResult = getDropResultFn?.(); handleDrop({ item: dragItem, dropResult, schema: sourceData.__rqbSchema, actions: sourceData.__rqbActions, copyModeModifierKey: sourceData.__rqbCopyModeModifierKey, groupModeModifierKey: sourceData.__rqbGroupModeModifierKey, copyModeOverride: copyOverride, groupModeOverride: groupOverride, onRuleDrop: sourceData.__rqbOnRuleDrop }); } } activeDragItemRef.current = null; setActiveDragItem(null); } }); return () => { cleanup(); clearHoverTimers(); }; }, [ updateWhileDragging, commitDrag, cancelDrag, updatePreviewPosition, startHoverTimers, 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(DragStateContext.Provider, { value: dragStateValue }, /* @__PURE__ */ React.createElement(DragPreviewContext.Provider, { value: dragPreviewContextValue }, children)); }; const useRuleDnD = (params) => { const { activeDragItem, timerCopyMode, timerGroupMode } = useContext(DragStateContext); const containerNodeRef = useRef(null); const handleNodeRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [isOver, setIsOver] = useState(false); const dragId = getDragId("rule", params.path, params.schema.qbId); const dropId = getDropId("rule", params.path, params.schema.qbId); const paramsRef = useRef(params); paramsRef.current = params; useEffect(() => { const container = containerNodeRef.current; const handle = handleNodeRef.current; if (!container || !handle) return void 0; return combine(draggable({ element: container, dragHandle: handle, canDrag: () => !paramsRef.current.disabled, getInitialData: () => ({ __rqbPath: paramsRef.current.path, __rqbSchema: paramsRef.current.schema, __rqbActions: paramsRef.current.actions, __rqbCopyModeModifierKey: paramsRef.current.copyModeModifierKey, __rqbGroupModeModifierKey: paramsRef.current.groupModeModifierKey, __rqbOnRuleDrop: paramsRef.current.onRuleDrop }), onDragStart: () => setIsDragging(true), onDrop: () => setIsDragging(false) }), dropTargetForElements({ element: container, getData: () => ({ __rqbType: "rule", __rqbPath: paramsRef.current.path, __rqbValidate: (dragging) => { const cp = paramsRef.current; return canDropOnRule({ dragging, path: cp.path, schema: cp.schema, canDrop: cp.canDrop, groupModeModifierKey: cp.groupModeModifierKey, disabled: cp.disabled, rule: cp.rule }); }, __rqbGetDropResult: () => { const cp = paramsRef.current; return buildDropResult({ type: "rule", path: cp.path, schema: cp.schema, copyModeModifierKey: cp.copyModeModifierKey, groupModeModifierKey: cp.groupModeModifierKey }); } }), onDragEnter: () => setIsOver(true), onDragLeave: () => setIsOver(false), onDrop: () => setIsOver(false) })); }, [ params.path, params.schema.qbId, params.disabled ]); const canDropHere = isOver && !!activeDragItem && canDropOnRule({ dragging: activeDragItem, path: params.path, schema: params.schema, canDrop: params.canDrop, groupModeModifierKey: params.groupModeModifierKey, disabled: params.disabled, rule: params.rule }); const validatedIsOver = isOver && canDropHere; const dropNotAllowed = isOver && !canDropHere; return { isDragging, dragMonitorId: dragId, isOver: validatedIsOver, dropMonitorId: dropId, dndRef: useCallback((node) => { containerNodeRef.current = node; }, []), dragRef: useCallback((node) => { handleNodeRef.current = node; }, []), dropEffect: timerCopyMode || isHotkeyPressed(params.copyModeModifierKey) ? "copy" : "move", groupItems: timerGroupMode || isHotkeyPressed(params.groupModeModifierKey), dropNotAllowed }; }; const useRuleGroupDnD = (params) => { const { activeDragItem, timerCopyMode, timerGroupMode } = useContext(DragStateContext); const previewNodeRef = useRef(null); const handleNodeRef = useRef(null); const dropNodeRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [isOver, setIsOver] = useState(false); const isDragDisabled = params.disabled || params.path.length === 0; const dragId = getDragId("ruleGroup", params.path, params.schema.qbId); const dropId = getDropId("ruleGroup", params.path, params.schema.qbId); const paramsRef = useRef(params); paramsRef.current = params; useEffect(() => { const previewEl = previewNodeRef.current; const handleEl = handleNodeRef.current; if (!previewEl || !handleEl || isDragDisabled) return void 0; return draggable({ element: previewEl, dragHandle: handleEl, canDrag: () => !paramsRef.current.disabled && paramsRef.current.path.length > 0, getInitialData: () => ({ __rqbPath: paramsRef.current.path, __rqbSchema: paramsRef.current.schema, __rqbActions: paramsRef.current.actions, __rqbCopyModeModifierKey: paramsRef.current.copyModeModifierKey, __rqbGroupModeModifierKey: paramsRef.current.groupModeModifierKey, __rqbOnRuleDrop: paramsRef.current.onRuleDrop }), onDragStart: () => setIsDragging(true), onDrop: () => setIsDragging(false) }); }, [ isDragDisabled, params.path, params.schema.qbId ]); useEffect(() => { const dropEl = dropNodeRef.current; if (!dropEl) return void 0; return dropTargetForElements({ element: dropEl, getData: () => ({ __rqbType: "ruleGroup", __rqbPath: paramsRef.current.path, __rqbValidate: (dragging) => { const cp = paramsRef.current; return canDropOnRuleGroup({ dragging, path: cp.path, schema: cp.schema, canDrop: cp.canDrop, disabled: cp.disabled, ruleGroup: cp.ruleGroup }); }, __rqbGetDropResult: () => { const cp = paramsRef.current; return buildDropResult({ type: "ruleGroup", path: cp.path, schema: cp.schema, copyModeModifierKey: cp.copyModeModifierKey, groupModeModifierKey: cp.groupModeModifierKey }); } }), onDragEnter: () => setIsOver(true), onDragLeave: () => setIsOver(false), onDrop: () => setIsOver(false) }); }, [ params.path, params.schema.qbId, params.disabled ]); const canDropHere = isOver && !!activeDragItem && canDropOnRuleGroup({ dragging: activeDragItem, path: params.path, schema: params.schema, canDrop: params.canDrop, disabled: params.disabled, ruleGroup: params.ruleGroup }); const validatedIsOver = isOver && canDropHere; const dropNotAllowed = isOver && !canDropHere; const previewRef = useCallback((node) => { previewNodeRef.current = node; }, []); const dropRef = useCallback((node) => { dropNodeRef.current = node; }, []); return { isDragging, dragMonitorId: dragId, isOver: validatedIsOver, dropMonitorId: dropId, previewRef, dragRef: useCallback((node) => { handleNodeRef.current = node; }, []), dropRef, dropEffect: timerCopyMode || isHotkeyPressed(params.copyModeModifierKey) ? "copy" : "move", groupItems: timerGroupMode || isHotkeyPressed(params.groupModeModifierKey), dropNotAllowed }; }; const useInlineCombinatorDnD = (params) => { const { activeDragItem, timerCopyMode } = useContext(DragStateContext); const { dragPreviewState } = useContext(DragPreviewContext); const dropNodeRef = useRef(null); const [isOver, setIsOver] = useState(false); const dropId = getDropId("inlineCombinator", params.path, params.schema.qbId); const isUpdateWhileDragging = dragPreviewState !== null; const hoveringItem = (params.rules ?? [])[params.path.at(-1) - 1]; const paramsRef = useRef(params); paramsRef.current = params; useEffect(() => { const dropEl = dropNodeRef.current; if (!dropEl || isUpdateWhileDragging) return void 0; return dropTargetForElements({ element: dropEl, getData: () => ({ __rqbType: "inlineCombinator", __rqbValidate: (dragging) => { const cp = paramsRef.current; const hItem = (cp.rules ?? [])[cp.path.at(-1) - 1]; return canDropOnInlineCombinator({ dragging, path: cp.path, schema: cp.schema, canDrop: cp.canDrop, groupModeModifierKey: cp.groupModeModifierKey, hoveringItem: hItem }); }, __rqbGetDropResult: () => { const cp = paramsRef.current; return buildDropResult({ type: "inlineCombinator", path: cp.path, schema: cp.schema, copyModeModifierKey: cp.copyModeModifierKey, groupModeModifierKey: cp.groupModeModifierKey }); } }), onDragEnter: () => setIsOver(true), onDragLeave: () => setIsOver(false), onDrop: () => setIsOver(false) }); }, [ params.path, params.schema.qbId, isUpdateWhileDragging ]); const canDropHere = !isUpdateWhileDragging && isOver && !!activeDragItem && canDropOnInlineCombinator({ dragging: activeDragItem, path: params.path, schema: params.schema, canDrop: params.canDrop, groupModeModifierKey: params.groupModeModifierKey, hoveringItem }); const validatedIsOver = isOver && canDropHere; const dropNotAllowed = !isUpdateWhileDragging && isOver && !canDropHere; return { dropRef: useCallback((node) => { dropNodeRef.current = node; }, []), dropMonitorId: dropId, isOver: validatedIsOver, dropEffect: timerCopyMode || isHotkeyPressed(params.copyModeModifierKey) ? "copy" : "move", dropNotAllowed }; }; return { DndProvider, useRuleDnD, useRuleGroupDnD, useInlineCombinatorDnD }; }; //#endregion export { getQuadrant as n, createPragmaticDndAdapter as t }; //# sourceMappingURL=pragmatic-dnd-7bGZbKfc.mjs.map