UNPKG

@react-querybuilder/dnd

Version:

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

500 lines (499 loc) 18.5 kB
import { n as computeShadowQuery, r as DragPreviewContext, t as computeDestinationFromQuadrant } from "./shadowQuery-XxKzMrJJ.mjs"; import { t as createDndKitAdapter } from "./dnd-kit-D5Ge4yu_.mjs"; import { n as getQuadrant, t as createPragmaticDndAdapter } from "./pragmatic-dnd-7bGZbKfc.mjs"; import { n as isTouchDevice, t as createReactDnDAdapter } from "./react-dnd-llotf5dK.mjs"; import * as React from "react"; import { createContext, useContext, useEffect, useMemo, useState } from "react"; import { QueryBuilderContext, TestID, defaultControlElements, messages, preferAnyProp, preferProp, standardClassnames, useMergedContext } from "react-querybuilder"; //#region src/adapter.ts /** * Type guard to check if a value is a {@link DndAdapter}. */ const isDndAdapter = (value) => typeof value === "object" && value !== null && "DndProvider" in value && "useRuleDnD" in value && "useRuleGroupDnD" in value && "useInlineCombinatorDnD" in value; //#endregion //#region src/flipAnimation.ts /** * FLIP (First, Last, Invert, Play) animation utility. * * Captures the positions of elements before a DOM update, then after the * update animates them from their old positions to their new ones using * CSS transforms, producing a smooth layout transition. * * @example * ```ts * const flip = createFlipAnimator('.rule, .ruleGroup'); * flip.captureFirst(containerEl); * // ... React re-render happens ... * useLayoutEffect(() => { flip.playLast(containerEl); }, [shadowQuery]); * ``` */ const FLIP_DURATION_MS = 150; const FLIP_EASING = "ease"; const getElementKey = (el) => el.getAttribute("data-rule-id") ?? el.getAttribute("data-testid") ?? null; /** * Creates a FLIP animator that tracks elements matching the given CSS selector * within a container. Elements are identified by their `data-rule-id` or `data-testid` * attribute. */ const createFlipAnimator = (selector) => { let firstPositions = /* @__PURE__ */ new Map(); const captureFirst = (container) => { firstPositions = /* @__PURE__ */ new Map(); const elements = container.querySelectorAll(selector); for (const el of elements) { const key = getElementKey(el); if (key) firstPositions.set(key, el.getBoundingClientRect()); } }; const playLast = (container) => { const elements = container.querySelectorAll(selector); for (const el of elements) { const key = getElementKey(el); if (!key) continue; const firstRect = firstPositions.get(key); if (!firstRect) continue; const lastRect = el.getBoundingClientRect(); const deltaX = firstRect.left - lastRect.left; const deltaY = firstRect.top - lastRect.top; if (deltaX === 0 && deltaY === 0) continue; const htmlEl = el; htmlEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`; htmlEl.style.transition = "none"; requestAnimationFrame(() => { htmlEl.style.transition = `transform ${FLIP_DURATION_MS}ms ${FLIP_EASING}`; htmlEl.style.transform = ""; const handleTransitionEnd = () => { htmlEl.style.transition = ""; htmlEl.removeEventListener("transitionend", handleTransitionEnd); }; htmlEl.addEventListener("transitionend", handleTransitionEnd); }); } firstPositions = /* @__PURE__ */ new Map(); }; return { captureFirst, playLast }; }; //#endregion //#region src/QueryBuilderDndContext.ts const { rule, ruleGroup, combinatorSelector } = defaultControlElements; /** * @group Components */ const QueryBuilderDndContext = createContext({ baseControls: { rule, ruleGroup, combinatorSelector } }); //#endregion //#region src/InlineCombinatorDnD.tsx /** * The drag-and-drop-enabled inline combinator component. * * @group Components */ const InlineCombinatorDnD = ({ component: CombinatorSelectorComponent, ...props }) => { const { adapter, canDrop, copyModeModifierKey, copyModeAfterHoverMs, groupModeModifierKey, groupModeAfterHoverMs } = useContext(QueryBuilderDndContext); const { dragPreviewState } = useContext(DragPreviewContext); const isUpdateWhileDragging = dragPreviewState !== null; const { dropRef, dropMonitorId, isOver } = adapter.useInlineCombinatorDnD({ path: props.path, schema: props.schema, rules: props.rules, canDrop, copyModeModifierKey: copyModeModifierKey ?? "alt", copyModeAfterHoverMs, groupModeModifierKey: groupModeModifierKey ?? "ctrl", groupModeAfterHoverMs }); // v8 ignore next -- same pattern as RuleDnD/RuleGroupDnD; IC only used in specific mode const effectiveIsOver = isUpdateWhileDragging ? false : isOver; const wrapperClassName = [ props.schema.suppressStandardClassnames || standardClassnames.betweenRules, effectiveIsOver && !props.schema.classNames.dndOver || false, effectiveIsOver && !props.schema.suppressStandardClassnames && standardClassnames.dndOver || false ].filter((c) => typeof c === "string").join(" "); return /* @__PURE__ */ React.createElement("div", { key: "dnd", ref: dropRef, className: wrapperClassName, "data-dropmonitorid": dropMonitorId, "data-testid": TestID.inlineCombinator }, /* @__PURE__ */ React.createElement(CombinatorSelectorComponent, { ...props, testID: TestID.combinators })); }; //#endregion //#region src/RuleDnD.tsx /** * Rule component for drag-and-drop. Renders the provided rule component * ({@link react-querybuilder!Rule Rule} by default), but forwards the * drag-and-drop context. * * @group Components */ const RuleDnD = (props) => { const rqbDndContext = useContext(QueryBuilderDndContext); const { dragPreviewState } = useContext(DragPreviewContext); const { adapter, canDrop, copyModeModifierKey, copyModeAfterHoverMs, groupModeModifierKey, groupModeAfterHoverMs, hideDefaultDragPreview, onRuleDrop } = rqbDndContext; const disabled = !!props.parentDisabled || !!props.disabled; const dndRefs = adapter.useRuleDnD({ path: props.path, disabled, schema: props.schema, actions: props.actions, rule: props.rule, canDrop, copyModeModifierKey: copyModeModifierKey ?? "alt", copyModeAfterHoverMs, groupModeModifierKey: groupModeModifierKey ?? "ctrl", groupModeAfterHoverMs, hideDefaultDragPreview, onRuleDrop }); const overriddenDndRefs = dragPreviewState ? { ...dndRefs, isDragging: false, isOver: false, dropNotAllowed: false } : dndRefs; const { rule: BaseRuleComponent } = rqbDndContext.baseControls; return /* @__PURE__ */ React.createElement(QueryBuilderDndContext.Provider, { value: rqbDndContext }, /* @__PURE__ */ React.createElement(BaseRuleComponent, { ...props, ...overriddenDndRefs })); }; //#endregion //#region src/RuleGroupDnD.tsx /** * Rule group component for drag-and-drop. Renders the provided rule group component * ({@link react-querybuilder!RuleGroup RuleGroup} by default), but forwards the drag-and-drop * context so that child rules and groups will render within the appropriate drag-and-drop wrappers. * * @group Components */ const RuleGroupDnD = (props) => { const rqbDndContext = useContext(QueryBuilderDndContext); const { dragPreviewState } = useContext(DragPreviewContext); const { adapter, canDrop, baseControls: { ruleGroup: BaseRuleGroupComponent }, copyModeModifierKey, copyModeAfterHoverMs, groupModeModifierKey, groupModeAfterHoverMs, hideDefaultDragPreview, onRuleDrop } = rqbDndContext; const effectiveProps = useMemo(() => { if (props.path.length === 0 && dragPreviewState) { const sq = dragPreviewState.shadowQuery; return { ...props, ruleGroup: sq, rules: sq.rules }; } return props; }, [props, dragPreviewState]); const dndRefs = adapter.useRuleGroupDnD({ disabled: !!effectiveProps.parentDisabled || !!effectiveProps.disabled, path: effectiveProps.path, schema: effectiveProps.schema, actions: effectiveProps.actions, ruleGroup: effectiveProps.ruleGroup, canDrop, copyModeModifierKey: copyModeModifierKey ?? "alt", copyModeAfterHoverMs, groupModeModifierKey: groupModeModifierKey ?? "ctrl", groupModeAfterHoverMs, hideDefaultDragPreview, onRuleDrop }); const overriddenDndRefs = dragPreviewState ? { ...dndRefs, isDragging: false, isOver: false, dropNotAllowed: false } : dndRefs; return /* @__PURE__ */ React.createElement(BaseRuleGroupComponent, { ...effectiveProps, ...overriddenDndRefs }); }; //#endregion //#region src/QueryBuilderDnD.tsx /** * Context provider to enable drag-and-drop. If the application already implements * `react-dnd`, use {@link QueryBuilderDndWithoutProvider} instead. * * @group Components */ const QueryBuilderDnD = (props) => { const { controlClassnames, controlElements, debugMode, enableDragAndDrop: enableDragAndDropProp, enableMountQueryChange, translations } = props; const rqbContext = useMergedContext({ controlClassnames, controlElements, debugMode, enableDragAndDrop: enableDragAndDropProp ?? true, enableMountQueryChange, translations: translations ?? {} }); const { enableDragAndDrop } = rqbContext; const adapter = useResolvedAdapter(props.dnd); const key = enableDragAndDrop && adapter ? "dnd" : "no-dnd"; const contextWithoutDnD = useMemo(() => ({ ...rqbContext, enableDragAndDrop: false, debugMode }), [rqbContext, debugMode]); const contextWithDnD = useMemo(() => ({ ...rqbContext, enableDragAndDrop, debugMode }), [ rqbContext, debugMode, enableDragAndDrop ]); if (!enableDragAndDrop || !adapter) return /* @__PURE__ */ React.createElement(QueryBuilderContext.Provider, { key, value: contextWithoutDnD }, props.children); const { DndProvider } = adapter; return /* @__PURE__ */ React.createElement(DndProvider, { key, debugMode, updateWhileDragging: props.updateWhileDragging, copyModeAfterHoverMs: props.copyModeAfterHoverMs, groupModeAfterHoverMs: props.groupModeAfterHoverMs }, /* @__PURE__ */ React.createElement(QueryBuilderContext.Provider, { key, value: contextWithDnD }, /* @__PURE__ */ React.createElement(QueryBuilderDndWithoutProvider, { dnd: adapter, canDrop: props.canDrop, copyModeModifierKey: props.copyModeModifierKey, copyModeAfterHoverMs: props.copyModeAfterHoverMs, groupModeModifierKey: props.groupModeModifierKey, groupModeAfterHoverMs: props.groupModeAfterHoverMs, hideDefaultDragPreview: props.hideDefaultDragPreview, updateWhileDragging: props.updateWhileDragging, onDragMove: props.onDragMove, onRuleDrop: props.onRuleDrop }, props.children))); }; /** * Context provider to enable drag-and-drop. Only use this provider if the application * already implements `react-dnd`, otherwise use {@link QueryBuilderDnD}. * * @group Components */ const QueryBuilderDndWithoutProvider = (props) => { const rqbContext = useContext(QueryBuilderContext); const rqbDndContext = useContext(QueryBuilderDndContext); const adapter = useResolvedAdapter(props.dnd) ?? rqbDndContext.adapter; const copyModeModifierKey = preferAnyProp(void 0, props.copyModeModifierKey, rqbDndContext.copyModeModifierKey); const groupModeModifierKey = preferAnyProp(void 0, props.groupModeModifierKey, rqbDndContext.groupModeModifierKey); const copyModeAfterHoverMs = preferAnyProp(void 0, props.copyModeAfterHoverMs, rqbDndContext.copyModeAfterHoverMs); const groupModeAfterHoverMs = preferAnyProp(void 0, props.groupModeAfterHoverMs, rqbDndContext.groupModeAfterHoverMs); const enableDragAndDrop = preferProp(true, props.enableDragAndDrop, rqbContext.enableDragAndDrop); const debugMode = preferProp(false, props.debugMode, rqbContext.debugMode); const hideDefaultDragPreview = preferProp(false, props.hideDefaultDragPreview, rqbDndContext.hideDefaultDragPreview); const canDrop = preferAnyProp(void 0, props.canDrop, rqbDndContext.canDrop); const updateWhileDragging = preferProp(false, props.updateWhileDragging, rqbDndContext.updateWhileDragging); const onDragMove = preferAnyProp(void 0, props.onDragMove, rqbDndContext.onDragMove); const onRuleDrop = preferAnyProp(void 0, props.onRuleDrop, rqbDndContext.onRuleDrop); const key = enableDragAndDrop && adapter ? "dnd" : "no-dnd"; const baseControls = useMemo(() => ({ rule: props.controlElements?.rule ?? rqbContext.controlElements?.rule ?? rqbDndContext.baseControls.rule, ruleGroup: props.controlElements?.ruleGroup ?? rqbContext.controlElements?.ruleGroup ?? rqbDndContext.baseControls.ruleGroup, combinatorSelector: props.controlElements?.combinatorSelector ?? rqbContext.controlElements?.combinatorSelector ?? rqbDndContext.baseControls.combinatorSelector }), [ props.controlElements?.combinatorSelector, props.controlElements?.rule, props.controlElements?.ruleGroup, rqbContext.controlElements?.combinatorSelector, rqbContext.controlElements?.rule, rqbContext.controlElements?.ruleGroup, rqbDndContext.baseControls.combinatorSelector, rqbDndContext.baseControls.rule, rqbDndContext.baseControls.ruleGroup ]); const newContext = useMemo(() => ({ ...rqbContext, enableDragAndDrop, debugMode, controlElements: { ...rqbContext.controlElements, ruleGroup: RuleGroupDnD, rule: RuleDnD, inlineCombinator: InlineCombinatorDnD } }), [ debugMode, enableDragAndDrop, rqbContext ]); const dndContextValue = useMemo(() => ({ baseControls, canDrop, copyModeModifierKey, copyModeAfterHoverMs, groupModeModifierKey, groupModeAfterHoverMs, hideDefaultDragPreview, updateWhileDragging, onDragMove, onRuleDrop, adapter }), [ baseControls, canDrop, copyModeModifierKey, copyModeAfterHoverMs, groupModeModifierKey, groupModeAfterHoverMs, hideDefaultDragPreview, updateWhileDragging, onDragMove, onRuleDrop, adapter ]); const contextWithoutDnD = useMemo(() => ({ ...rqbContext, enableDragAndDrop: false, debugMode }), [rqbContext, debugMode]); if (!enableDragAndDrop || !adapter) return /* @__PURE__ */ React.createElement(QueryBuilderContext.Provider, { key, value: contextWithoutDnD }, props.children); return /* @__PURE__ */ React.createElement(QueryBuilderContext.Provider, { key, value: newContext }, /* @__PURE__ */ React.createElement(QueryBuilderDndContext.Provider, { value: dndContextValue }, props.children)); }; let didWarnEnabledDndWithoutReactDnD = false; /** * Resolves a `dnd` prop (which may be a {@link DndAdapter}, a legacy {@link DndProp}, * or `undefined`) into a {@link DndAdapter} or `null`. * * Hooks are always called in the same order regardless of the `dndParam` value * to satisfy the Rules of Hooks. */ const useResolvedAdapter = (dndParam) => { const directAdapter = dndParam && isDndAdapter(dndParam) ? dndParam : null; const legacyAdapter = useMemo(() => dndParam && !isDndAdapter(dndParam) ? createReactDnDAdapter(dndParam) : null, [dndParam]); const asyncAdapter = useAsyncReactDnDAdapter(directAdapter !== null || legacyAdapter !== null); return directAdapter ?? legacyAdapter ?? asyncAdapter; }; const useAsyncReactDnDAdapter = (skip) => { const [adapter, setAdapter] = useState(null); useEffect(() => { if (skip) return void 0; let didCancel = false; const loadDnD = async () => { const [reactDnD, reactDndHTML5Be, reactDndTouchBe] = await Promise.all([ "", "-html5-backend", "-touch-backend" ].map((pn) => import( /* @vite-ignore */ `react-dnd${pn}` ).catch( /* v8 ignore next -- @preserve */ () => null ))); // v8 ignore else if (!didCancel) { // v8 ignore else -- react-dnd is always importable in the test environment if (reactDnD) { let dndExports; // v8 ignore next if (reactDndHTML5Be && (!reactDndTouchBe || reactDndTouchBe && !isTouchDevice())) dndExports = { ...reactDnD, ...reactDndHTML5Be, ...reactDndTouchBe, ReactDndBackend: reactDndHTML5Be.HTML5Backend }; else if (reactDndTouchBe) dndExports = { ...reactDnD, ...reactDndTouchBe, ...reactDndHTML5Be, ReactDndBackend: reactDndTouchBe.TouchBackend }; else return; setAdapter(() => createReactDnDAdapter(dndExports)); } else if (process.env.NODE_ENV !== "production" && !didWarnEnabledDndWithoutReactDnD) { console.error(messages.errorEnabledDndWithoutReactDnD); didWarnEnabledDndWithoutReactDnD = true; } } }; if (!adapter) loadDnD(); return () => { didCancel = true; }; }, [adapter, skip]); return skip ? null : adapter; }; /** * @group Hooks * @deprecated Use `createReactDnDAdapter` instead. This hook is kept for backward compatibility. */ const useReactDnD = (dndParam) => { const [dnd, setDnd] = useState(dndParam ?? null); useEffect(() => { let didCancel = false; const getDnD = async () => { const [reactDnD, reactDndHTML5Be, reactDndTouchBe] = await Promise.all([ "", "-html5-backend", "-touch-backend" ].map((pn) => import( /* @vite-ignore */ `react-dnd${pn}` ).catch(() => null))); // v8 ignore else if (!didCancel) { if (reactDnD) { // v8 ignore next if (reactDndHTML5Be && (!reactDndTouchBe || reactDndTouchBe && !isTouchDevice())) setDnd(() => ({ ...reactDnD, ...reactDndHTML5Be, ...reactDndTouchBe, ReactDndBackend: reactDndHTML5Be.HTML5Backend })); else if (reactDndTouchBe) setDnd(() => ({ ...reactDnD, ...reactDndTouchBe, ...reactDndHTML5Be, ReactDndBackend: reactDndTouchBe.TouchBackend })); } else if (process.env.NODE_ENV !== "production" && !didWarnEnabledDndWithoutReactDnD) { console.error(messages.errorEnabledDndWithoutReactDnD); didWarnEnabledDndWithoutReactDnD = true; } } }; if (!dnd) getDnD(); return () => { didCancel = true; }; }, [dnd]); // v8 ignore next if (dnd && !dnd.ReactDndBackend) dnd.ReactDndBackend = isTouchDevice() ? dnd.TouchBackend ?? dnd.HTML5Backend : dnd.HTML5Backend ?? dnd.TouchBackend; return dnd; }; //#endregion //#region src/useShadowQuery.ts /** * Hook for consuming the shadow query during an active drag with * `updateWhileDragging` enabled. * * @returns The shadow query if a drag is in progress, otherwise `undefined`. * * @group Hooks */ const useShadowQuery = () => { const { dragPreviewState } = useContext(DragPreviewContext); return dragPreviewState?.shadowQuery; }; //#endregion export { DragPreviewContext, InlineCombinatorDnD, QueryBuilderDnD, QueryBuilderDndWithoutProvider, RuleDnD, RuleGroupDnD, computeDestinationFromQuadrant, computeShadowQuery, createDndKitAdapter, createFlipAnimator, createPragmaticDndAdapter, createReactDnDAdapter, getQuadrant, isDndAdapter, useReactDnD, useShadowQuery }; //# sourceMappingURL=react-querybuilder_dnd.mjs.map