Merge pull request #2136 from wbkd/refactor/d3-drag-handle-selector

Refactor/d3 drag handle selector
This commit is contained in:
Moritz Klack
2022-05-10 15:43:01 +02:00
committed by GitHub
12 changed files with 74 additions and 59 deletions
+9 -5
View File
@@ -26,9 +26,13 @@
Cypress.Commands.add('drag', (selector, { x, y }) => { Cypress.Commands.add('drag', (selector, { x, y }) => {
return cy return cy
.get(selector) .window()
.trigger('mousedown', { which: 1 }) .then((window) =>
.trigger('mousemove', { clientX: x, clientY: y }) cy
.wait(50) .get(selector)
.trigger('mouseup', { force: true }); .trigger('mousedown', { which: 1, view: window })
.trigger('mousemove', { clientX: x, clientY: y, force: true })
.wait(50)
.trigger('mouseup', { view: window, force: tru })
);
}); });
@@ -5,18 +5,17 @@ import { ReactFlowState, Node } from '../../types';
function useMemoizedMouseHandler( function useMemoizedMouseHandler(
id: string, id: string,
dragging: boolean,
getState: GetState<ReactFlowState>, getState: GetState<ReactFlowState>,
handler?: (event: MouseEvent, node: Node) => void handler?: (event: MouseEvent, node: Node) => void
) { ) {
const memoizedHandler = useCallback( const memoizedHandler = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
if (typeof handler !== 'undefined' && !dragging) { if (typeof handler !== 'undefined') {
const node = getState().nodeInternals.get(id)!; const node = getState().nodeInternals.get(id)!;
handler(event, { ...node }); handler(event, { ...node });
} }
}, },
[handler, dragging, id] [handler, id]
); );
return memoizedHandler; return memoizedHandler;
+26 -19
View File
@@ -1,4 +1,14 @@
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react'; import React, {
useEffect,
useRef,
memo,
ComponentType,
CSSProperties,
useMemo,
MouseEvent,
useCallback,
useState,
} from 'react';
import cc from 'classcat'; import cc from 'classcat';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
@@ -41,7 +51,6 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
sourcePosition, sourcePosition,
targetPosition, targetPosition,
hidden, hidden,
dragging,
resizeObserver, resizeObserver,
dragHandle, dragHandle,
zIndex, zIndex,
@@ -49,6 +58,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
noPanClassName, noPanClassName,
noDragClassName, noDragClassName,
}: WrapNodeProps) => { }: WrapNodeProps) => {
const draggingRef = useRef<boolean>(false);
const [dragging, setDragging] = useState<boolean>(false);
const store = useStoreApi(); const store = useStoreApi();
const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore( const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore(
selector, selector,
@@ -69,11 +80,11 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
[zIndex, xPos, yPos, hasPointerEvents, style] [zIndex, xPos, yPos, hasPointerEvents, style]
); );
const onMouseEnterHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseEnter); const onMouseEnterHandler = useMemoizedMouseHandler(id, store.getState, onMouseEnter);
const onMouseMoveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseMove); const onMouseMoveHandler = useMemoizedMouseHandler(id, store.getState, onMouseMove);
const onMouseLeaveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseLeave); const onMouseLeaveHandler = useMemoizedMouseHandler(id, store.getState, onMouseLeave);
const onContextMenuHandler = useMemoizedMouseHandler(id, false, store.getState, onContextMenu); const onContextMenuHandler = useMemoizedMouseHandler(id, store.getState, onContextMenu);
const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, false, store.getState, onNodeDoubleClick); const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, store.getState, onNodeDoubleClick);
const onSelectNodeHandler = useCallback( const onSelectNodeHandler = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
@@ -123,13 +134,14 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
const onDrag = useCallback( const onDrag = useCallback(
(event: UseDragEvent, dragPos: UseDragData) => { (event: UseDragEvent, dragPos: UseDragData) => {
updateNodePosition({ id, dragging: true, diff: { x: dragPos.dx, y: dragPos.dy } }); updateNodePosition({ id, diff: { x: dragPos.dx, y: dragPos.dy } });
setDragging(true);
draggingRef.current = true;
if (onNodeDrag) { if (onNodeDrag) {
const node = store.getState().nodeInternals.get(id)!; const node = store.getState().nodeInternals.get(id)!;
onNodeDrag(event.sourceEvent as MouseEvent, { onNodeDrag(event.sourceEvent as MouseEvent, {
...node, ...node,
dragging: true,
position: { position: {
x: node.position.x + dragPos.dx, x: node.position.x + dragPos.dx,
y: node.position.y + dragPos.dy, y: node.position.y + dragPos.dy,
@@ -146,15 +158,13 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
const onDragStop = useCallback( const onDragStop = useCallback(
(event: UseDragEvent) => { (event: UseDragEvent) => {
// onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here
let node; let node;
if (onClick || onNodeDragStop) { if (onClick || onNodeDragStop) {
node = store.getState().nodeInternals.get(id)!; node = store.getState().nodeInternals.get(id)!;
} }
if (!dragging) { if (!draggingRef.current) {
if (isSelectable && !selectNodesOnDrag && !selected) { if (isSelectable && !selectNodesOnDrag && !selected) {
addSelectedNodes([id]); addSelectedNodes([id]);
} }
@@ -165,17 +175,14 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
return; return;
} }
draggingRef.current = false;
updateNodePosition({ setDragging(false);
id,
dragging: false,
});
if (onNodeDragStop && node) { if (onNodeDragStop && node) {
onNodeDragStop(event.sourceEvent as MouseEvent, { ...node, dragging: false }); onNodeDragStop(event.sourceEvent as MouseEvent, { ...node });
} }
}, },
[id, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected] [id, onNodeDragStop, onClick]
); );
useEffect(() => { useEffect(() => {
-5
View File
@@ -72,7 +72,6 @@ function NodesSelection({
x: data.dx, x: data.dx,
y: data.dy, y: data.dy,
}, },
dragging: true,
}); });
onSelectionDrag?.(event.sourceEvent, selectedNodes); onSelectionDrag?.(event.sourceEvent, selectedNodes);
@@ -82,10 +81,6 @@ function NodesSelection({
const onStop = useCallback( const onStop = useCallback(
(event: UseDragEvent) => { (event: UseDragEvent) => {
updateNodePosition({
dragging: false,
});
onSelectionDragStop?.(event.sourceEvent, selectedNodes); onSelectionDragStop?.(event.sourceEvent, selectedNodes);
}, },
[selectedNodes, onSelectionDragStop] [selectedNodes, onSelectionDragStop]
-1
View File
@@ -93,7 +93,6 @@ const NodeRenderer = (props: NodeRendererProps) => {
hidden={node.hidden} hidden={node.hidden}
xPos={node.positionAbsolute?.x ?? 0} xPos={node.positionAbsolute?.x ?? 0}
yPos={node.positionAbsolute?.y ?? 0} yPos={node.positionAbsolute?.y ?? 0}
dragging={!!node.dragging}
selectNodesOnDrag={props.selectNodesOnDrag} selectNodesOnDrag={props.selectNodesOnDrag}
onClick={props.onNodeClick} onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter} onMouseEnter={props.onNodeMouseEnter}
+31 -5
View File
@@ -16,7 +16,6 @@ type UseDragParams = {
nodeRef: RefObject<Element>; nodeRef: RefObject<Element>;
disabled?: boolean; disabled?: boolean;
noDragClassName?: string; noDragClassName?: string;
// @TODO: implement handleSelector functionality
handleSelector?: string; handleSelector?: string;
nodeId?: string; nodeId?: string;
}; };
@@ -42,7 +41,27 @@ function getParentNodePosition(nodeInternals: NodeInternals, nodeId?: string): X
}; };
} }
function useDrag({ onStart, onDrag, onStop, nodeRef, disabled = false, noDragClassName, nodeId }: UseDragParams) { function selectorExistsTargetToNode(target: Element, selector: string, nodeRef: RefObject<Element>): boolean {
let current = target;
do {
if (current?.matches(selector)) return true;
if (current === nodeRef.current) return false;
current = current.parentElement as Element;
} while (current);
return false;
}
function useDrag({
onStart,
onDrag,
onStop,
nodeRef,
disabled = false,
noDragClassName,
handleSelector,
nodeId,
}: UseDragParams) {
const store = useStoreApi(); const store = useStoreApi();
const startPos = useRef<XYPosition>({ x: 0, y: 0 }); const startPos = useRef<XYPosition>({ x: 0, y: 0 });
const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null }); const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null });
@@ -93,8 +112,15 @@ function useDrag({ onStart, onDrag, onStop, nodeRef, disabled = false, noDragCla
}); });
} }
}) })
.on('end', onStop) .on('end', (event) => {
.filter((event: any) => !event.ctrlKey && !event.button && !event.target.className.includes(noDragClassName)); onStop(event);
})
.filter((event: any) => {
const filter = !event.ctrlKey && !event.button && !event.target.className.includes(noDragClassName);
return handleSelector
? selectorExistsTargetToNode(event.target, handleSelector, nodeRef) && filter
: filter;
});
selection.call(dragHandler); selection.call(dragHandler);
@@ -103,7 +129,7 @@ function useDrag({ onStart, onDrag, onStop, nodeRef, disabled = false, noDragCla
}; };
} }
} }
}, [disabled, noDragClassName, nodeId]); }, [onStart, onDrag, onStop, nodeRef, disabled, noDragClassName, handleSelector, nodeId]);
return null; return null;
} }
+3 -7
View File
@@ -96,7 +96,7 @@ const createStore = () =>
onNodesChange?.(changes); onNodesChange?.(changes);
} }
}, },
updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => { updateNodePosition: ({ id, diff }: NodeDiffUpdate) => {
const { onNodesChange, nodeExtent, nodeInternals, hasDefaultNodes, snapGrid, snapToGrid } = get(); const { onNodesChange, nodeExtent, nodeInternals, hasDefaultNodes, snapGrid, snapToGrid } = get();
if (hasDefaultNodes || onNodesChange) { if (hasDefaultNodes || onNodesChange) {
@@ -105,14 +105,10 @@ const createStore = () =>
nodeInternals.forEach((node) => { nodeInternals.forEach((node) => {
if (node.selected) { if (node.selected) {
if (!node.parentNode || !isParentSelected(node, nodeInternals)) { if (!node.parentNode || !isParentSelected(node, nodeInternals)) {
changes.push( changes.push(createPositionChange({ node, diff, nodeExtent, nodeInternals, snapToGrid, snapGrid }));
createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals, snapToGrid, snapGrid })
);
} }
} else if (node.id === id) { } else if (node.id === id) {
changes.push( changes.push(createPositionChange({ node, diff, nodeExtent, nodeInternals, snapToGrid, snapGrid }));
createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals, snapToGrid, snapGrid })
);
} }
}); });
+1 -4
View File
@@ -44,7 +44,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
const parentNodes: ParentNodes = {}; const parentNodes: ParentNodes = {};
nodes.forEach((node) => { nodes.forEach((node) => {
const z = isNumeric(node.zIndex) ? node.zIndex : node.dragging || node.selected ? 1000 : 0; const z = isNumeric(node.zIndex) ? node.zIndex : node.selected ? 1000 : 0;
const currInternals = nodeInternals.get(node.id); const currInternals = nodeInternals.get(node.id);
const internals: Node = { const internals: Node = {
@@ -115,7 +115,6 @@ type CreatePostionChangeParams = {
nodeExtent: CoordinateExtent; nodeExtent: CoordinateExtent;
nodeInternals: NodeInternals; nodeInternals: NodeInternals;
diff?: XYPosition; diff?: XYPosition;
dragging?: boolean;
snapToGrid?: boolean; snapToGrid?: boolean;
snapGrid?: SnapGrid; snapGrid?: SnapGrid;
}; };
@@ -123,7 +122,6 @@ type CreatePostionChangeParams = {
export function createPositionChange({ export function createPositionChange({
node, node,
diff, diff,
dragging,
nodeExtent, nodeExtent,
nodeInternals, nodeInternals,
snapToGrid, snapToGrid,
@@ -132,7 +130,6 @@ export function createPositionChange({
const change: NodePositionChange = { const change: NodePositionChange = {
id: node.id, id: node.id,
type: 'position', type: 'position',
dragging: !!dragging,
}; };
if (diff) { if (diff) {
-1
View File
@@ -13,7 +13,6 @@ export type NodePositionChange = {
id: string; id: string;
type: 'position'; type: 'position';
position?: XYPosition; position?: XYPosition;
dragging?: boolean;
}; };
export type NodeSelectionChange = { export type NodeSelectionChange = {
-3
View File
@@ -15,7 +15,6 @@ export interface Node<T = any> {
sourcePosition?: Position; sourcePosition?: Position;
hidden?: boolean; hidden?: boolean;
selected?: boolean; selected?: boolean;
dragging?: boolean;
draggable?: boolean; draggable?: boolean;
selectable?: boolean; selectable?: boolean;
connectable?: boolean; connectable?: boolean;
@@ -79,7 +78,6 @@ export interface WrapNodeProps<T = any> {
sourcePosition: Position; sourcePosition: Position;
targetPosition: Position; targetPosition: Position;
hidden?: boolean; hidden?: boolean;
dragging: boolean;
resizeObserver: ResizeObserver | null; resizeObserver: ResizeObserver | null;
dragHandle?: string; dragHandle?: string;
zIndex: number; zIndex: number;
@@ -96,7 +94,6 @@ export type NodeHandleBounds = {
export type NodeDiffUpdate = { export type NodeDiffUpdate = {
id?: string; id?: string;
diff?: XYPosition; diff?: XYPosition;
dragging?: boolean;
}; };
export type NodeDimensionUpdate = { export type NodeDimensionUpdate = {
-4
View File
@@ -69,10 +69,6 @@ function applyChanges(changes: any[], elements: any[]): any[] {
updateItem.position = currentChange.position; updateItem.position = currentChange.position;
} }
if (typeof currentChange.dragging !== 'undefined') {
updateItem.dragging = currentChange.dragging;
}
if (updateItem.expandParent) { if (updateItem.expandParent) {
handleParentExpand(res, updateItem); handleParentExpand(res, updateItem);
} }
+2 -2
View File
@@ -176,7 +176,7 @@ export const getNodesInside = (
const visibleNodes: Node[] = []; const visibleNodes: Node[] = [];
nodeInternals.forEach((node) => { nodeInternals.forEach((node) => {
const { positionAbsolute, width, height, dragging, selectable = true } = node; const { positionAbsolute, width, height, selectable = true } = node;
if (excludeNonSelectableNodes && !selectable) { if (excludeNonSelectableNodes && !selectable) {
return false; return false;
@@ -187,7 +187,7 @@ export const getNodesInside = (
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y)); const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap); const overlappingArea = Math.ceil(xOverlap * yOverlap);
const notInitialized = const notInitialized =
typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null || dragging; typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null;
const partiallyVisible = partially && overlappingArea > 0; const partiallyVisible = partially && overlappingArea > 0;
const area = (width || 0) * (height || 0); const area = (width || 0) * (height || 0);