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