Merge pull request #5551 from xyflow/chore/selection
refactor(selection): make it possible to start a selection above a node
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
'@xyflow/svelte': minor
|
||||||
|
'@xyflow/system': patch
|
||||||
|
'@xyflow/react': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Allow to start a selection above a node
|
||||||
@@ -30,7 +30,7 @@ function ColorSelectorNode({ data, isConnectable }: NodeProps<ColorSelectorNode>
|
|||||||
<div>
|
<div>
|
||||||
Custom Color Picker Node: <strong>{data.color}</strong>
|
Custom Color Picker Node: <strong>{data.color}</strong>
|
||||||
</div>
|
</div>
|
||||||
<input className="nodrag" type="color" onChange={data.onChange} defaultValue={data.color} />
|
<input className="nodrag nokey" type="color" onChange={data.onChange} defaultValue={data.color} />
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
Custom Color Picker Node: <strong>{bg.color}</strong>
|
Custom Color Picker Node: <strong>{bg.color}</strong>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class="nodrag"
|
class="nodrag nokey"
|
||||||
type="color"
|
type="color"
|
||||||
oninput={(evt) => (bg.color = evt.currentTarget.value)}
|
oninput={(evt) => (bg.color = evt.currentTarget.value)}
|
||||||
value={bg.color}
|
value={bg.color}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
/**
|
|
||||||
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useRef,
|
useRef,
|
||||||
type MouseEvent as ReactMouseEvent,
|
type MouseEvent as ReactMouseEvent,
|
||||||
@@ -119,21 +115,43 @@ export function Pane({
|
|||||||
|
|
||||||
const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
|
const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
|
||||||
|
|
||||||
const onPointerDown = (event: ReactPointerEvent): void => {
|
const onClickCapture = (event: ReactMouseEvent) => {
|
||||||
|
const isSelectionOnDragActive =
|
||||||
|
(selectionOnDrag && container.current === event.target) || !selectionOnDrag || selectionKeyPressed;
|
||||||
|
|
||||||
|
if (!isSelectionOnDragActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
// We are using capture here in order to prevent other pointer events
|
||||||
|
// to be able to create a selection above a node or an edge
|
||||||
|
const onPointerDownCapture = (event: ReactPointerEvent): void => {
|
||||||
const { resetSelectedElements, domNode } = store.getState();
|
const { resetSelectedElements, domNode } = store.getState();
|
||||||
containerBounds.current = domNode?.getBoundingClientRect();
|
containerBounds.current = domNode?.getBoundingClientRect();
|
||||||
|
|
||||||
|
const isNoKeyEvent = event.target !== container.current && !!(event.target as HTMLElement).closest('.nokey');
|
||||||
|
const isSelectionActive =
|
||||||
|
(selectionOnDrag && container.current === event.target) || !selectionOnDrag || selectionKeyPressed;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!elementsSelectable ||
|
!elementsSelectable ||
|
||||||
!isSelecting ||
|
!isSelecting ||
|
||||||
event.button !== 0 ||
|
event.button !== 0 ||
|
||||||
event.target !== container.current ||
|
!containerBounds.current ||
|
||||||
!containerBounds.current
|
isNoKeyEvent ||
|
||||||
|
!isSelectionActive ||
|
||||||
|
!event.isPrimary
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(event.target as Partial<Element> | null)?.setPointerCapture?.(event.pointerId);
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
(event.target as Partial<Element>)?.setPointerCapture?.(event.pointerId);
|
||||||
|
|
||||||
selectionStarted.current = true;
|
selectionStarted.current = true;
|
||||||
selectionInProgress.current = false;
|
selectionInProgress.current = false;
|
||||||
@@ -233,8 +251,8 @@ export function Pane({
|
|||||||
}
|
}
|
||||||
|
|
||||||
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
|
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
|
||||||
const { userSelectionRect } = store.getState();
|
|
||||||
|
|
||||||
|
const { userSelectionRect } = store.getState();
|
||||||
/*
|
/*
|
||||||
* We only want to trigger click functions when in selection mode if
|
* We only want to trigger click functions when in selection mode if
|
||||||
* the user did not move the mouse.
|
* the user did not move the mouse.
|
||||||
@@ -270,9 +288,10 @@ export function Pane({
|
|||||||
onContextMenu={wrapHandler(onContextMenu, container)}
|
onContextMenu={wrapHandler(onContextMenu, container)}
|
||||||
onWheel={wrapHandler(onWheel, container)}
|
onWheel={wrapHandler(onWheel, container)}
|
||||||
onPointerEnter={hasActiveSelection ? undefined : onPaneMouseEnter}
|
onPointerEnter={hasActiveSelection ? undefined : onPaneMouseEnter}
|
||||||
onPointerDown={hasActiveSelection ? onPointerDown : onPaneMouseMove}
|
|
||||||
onPointerMove={hasActiveSelection ? onPointerMove : onPaneMouseMove}
|
onPointerMove={hasActiveSelection ? onPointerMove : onPaneMouseMove}
|
||||||
onPointerUp={hasActiveSelection ? onPointerUp : undefined}
|
onPointerUp={hasActiveSelection ? onPointerUp : undefined}
|
||||||
|
onPointerDownCapture={hasActiveSelection ? onPointerDownCapture : undefined}
|
||||||
|
onClickCapture={hasActiveSelection ? onClickCapture : undefined}
|
||||||
onPointerLeave={onPaneMouseLeave}
|
onPointerLeave={onPaneMouseLeave}
|
||||||
ref={container}
|
ref={container}
|
||||||
style={containerStyle}
|
style={containerStyle}
|
||||||
|
|||||||
@@ -91,20 +91,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We start the selection process when the user clicks down on the pane
|
// We start the selection process when the user clicks down on the pane
|
||||||
function onPointerDown(event: PointerEvent) {
|
function onPointerDownCapture(event: PointerEvent) {
|
||||||
containerBounds = container?.getBoundingClientRect();
|
containerBounds = container?.getBoundingClientRect();
|
||||||
|
|
||||||
|
const isNoKeyEvent =
|
||||||
|
event.target !== container && !!(event.target as HTMLElement).closest('.nokey');
|
||||||
|
|
||||||
|
const isSelectionActive =
|
||||||
|
(selectionOnDrag && container === event.target) ||
|
||||||
|
!selectionOnDrag ||
|
||||||
|
store.selectionKeyPressed;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!store.elementsSelectable ||
|
!store.elementsSelectable ||
|
||||||
!isSelecting ||
|
!isSelecting ||
|
||||||
event.button !== 0 ||
|
event.button !== 0 ||
|
||||||
event.target !== container ||
|
!containerBounds ||
|
||||||
!containerBounds
|
isNoKeyEvent ||
|
||||||
|
!isSelectionActive ||
|
||||||
|
!event.isPrimary
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(event.target as Partial<Element> | null)?.setPointerCapture?.(event.pointerId);
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
(event.target as Partial<Element>)?.setPointerCapture?.(event.pointerId);
|
||||||
|
|
||||||
const { x, y } = getEventPosition(event, containerBounds);
|
const { x, y } = getEventPosition(event, containerBounds);
|
||||||
|
|
||||||
@@ -186,7 +199,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(event.target as Partial<Element> | null)?.releasePointerCapture?.(event.pointerId);
|
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
|
||||||
|
|
||||||
// We only want to trigger click functions when in selection mode if
|
// We only want to trigger click functions when in selection mode if
|
||||||
// the user did not move the mouse.
|
// the user did not move the mouse.
|
||||||
@@ -216,6 +229,19 @@
|
|||||||
|
|
||||||
onpanecontextmenu?.({ event });
|
onpanecontextmenu?.({ event });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onClickCapture = (event: MouseEvent) => {
|
||||||
|
const isSelectionActive =
|
||||||
|
(selectionOnDrag && container === event.target) ||
|
||||||
|
!selectionOnDrag ||
|
||||||
|
store.selectionKeyPressed;
|
||||||
|
|
||||||
|
if (!isSelectionActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
@@ -227,10 +253,11 @@
|
|||||||
class:dragging={store.dragging}
|
class:dragging={store.dragging}
|
||||||
class:selection={isSelecting}
|
class:selection={isSelecting}
|
||||||
onclick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
onclick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||||
onpointerdown={hasActiveSelection ? onPointerDown : undefined}
|
onpointerdowncapture={hasActiveSelection ? onPointerDownCapture : undefined}
|
||||||
onpointermove={hasActiveSelection ? onPointerMove : undefined}
|
onpointermove={hasActiveSelection ? onPointerMove : undefined}
|
||||||
onpointerup={hasActiveSelection ? onPointerUp : undefined}
|
onpointerup={hasActiveSelection ? onPointerUp : undefined}
|
||||||
oncontextmenu={wrapHandler(onContextMenu, container)}
|
oncontextmenu={wrapHandler(onContextMenu, container)}
|
||||||
|
onclickcapture={hasActiveSelection ? onClickCapture : undefined}
|
||||||
>
|
>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export function isInputDOMNode(event: KeyboardEvent): boolean {
|
|||||||
if (target?.nodeType !== 1 /* Node.ELEMENT_NODE */) return false;
|
if (target?.nodeType !== 1 /* Node.ELEMENT_NODE */) return false;
|
||||||
|
|
||||||
const isInput = inputTags.includes(target.nodeName) || target.hasAttribute('contenteditable');
|
const isInput = inputTags.includes(target.nodeName) || target.hasAttribute('contenteditable');
|
||||||
|
|
||||||
// when an input field is focused we don't want to trigger deletion or movement of nodes
|
// when an input field is focused we don't want to trigger deletion or movement of nodes
|
||||||
return isInput || !!target.closest('.nokey');
|
return isInput || !!target.closest('.nokey');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user