@react-querybuilder/dnd
Version:
Drag-and-drop-enabled version of react-querybuilder (DnD-library-agnostic)
500 lines (499 loc) • 18.5 kB
JavaScript
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