optimized selection via selectionbox, implemented proper defaultEdge/-NodeOptions
This commit is contained in:
@@ -27,8 +27,8 @@
|
|||||||
targetY,
|
targetY,
|
||||||
sourcePosition,
|
sourcePosition,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
animated,
|
animated = false,
|
||||||
selected,
|
selected = false,
|
||||||
label,
|
label,
|
||||||
labelStyle,
|
labelStyle,
|
||||||
data = {},
|
data = {},
|
||||||
@@ -40,12 +40,12 @@
|
|||||||
markerStart,
|
markerStart,
|
||||||
markerEnd,
|
markerEnd,
|
||||||
selectable: edgeSelectable,
|
selectable: edgeSelectable,
|
||||||
deletable,
|
deletable = true,
|
||||||
hidden,
|
hidden,
|
||||||
zIndex,
|
zIndex,
|
||||||
class: className,
|
class: className,
|
||||||
ariaLabel
|
ariaLabel
|
||||||
} = $derived(edge);
|
} = $derived(store.defaultEdgeOptions ? { ...store.defaultEdgeOptions, ...edge } : edge);
|
||||||
|
|
||||||
const { id } = edge;
|
const { id } = edge;
|
||||||
setContext('svelteflow__edge_id', id);
|
setContext('svelteflow__edge_id', id);
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
const edge = store.edgeLookup.get(id);
|
const edge = store.edgeLookup.get(id);
|
||||||
|
|
||||||
if (edge) {
|
if (edge) {
|
||||||
store.handleEdgeSelection(id);
|
if (selectable) store.handleEdgeSelection(id);
|
||||||
onedgeclick?.({ event, edge });
|
onedgeclick?.({ event, edge });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,6 +79,8 @@
|
|||||||
callback({ event, edge });
|
callback({ event, edge });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$inspect(selected);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
@@ -132,7 +134,7 @@
|
|||||||
{style}
|
{style}
|
||||||
{interactionWidth}
|
{interactionWidth}
|
||||||
{selectable}
|
{selectable}
|
||||||
deletable={deletable ?? true}
|
{deletable}
|
||||||
{type}
|
{type}
|
||||||
sourceHandleId={sourceHandle}
|
sourceHandleId={sourceHandle}
|
||||||
targetHandleId={targetHandle}
|
targetHandleId={targetHandle}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
let store = useStore();
|
let store = useStore();
|
||||||
// let { edges } = store;
|
|
||||||
|
|
||||||
function onPointerDown(event: MouseEvent | TouchEvent) {
|
function onPointerDown(event: MouseEvent | TouchEvent) {
|
||||||
const isMouseTriggered = isMouseEvent(event);
|
const isMouseTriggered = isMouseEvent(event);
|
||||||
@@ -91,7 +90,6 @@
|
|||||||
$effect.pre(() => {
|
$effect.pre(() => {
|
||||||
// connectionLookup is not reactive, so we use edges to get notified about updates
|
// connectionLookup is not reactive, so we use edges to get notified about updates
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
// $edges;
|
|
||||||
store.edges;
|
store.edges;
|
||||||
if (onconnect || ondisconnect) {
|
if (onconnect || ondisconnect) {
|
||||||
let connections = store.connectionLookup.get(`${nodeId}-${type}-${handleId}`);
|
let connections = store.connectionLookup.get(`${nodeId}-${type}-${handleId}`);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
z: zIndex = 0,
|
z: zIndex = 0,
|
||||||
positionAbsolute: { x: positionX, y: positionY }
|
positionAbsolute: { x: positionX, y: positionY }
|
||||||
}
|
}
|
||||||
} = $derived(node);
|
} = $derived(store.defaultNodeOptions ? { ...store.defaultNodeOptions, ...node } : node);
|
||||||
|
|
||||||
let { id } = node;
|
let { id } = node;
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
use:drag={{
|
use:drag={{
|
||||||
nodeId: id,
|
nodeId: id,
|
||||||
isSelectable: selectable,
|
isSelectable: selectable,
|
||||||
disabled: false,
|
disabled: !draggable,
|
||||||
handleSelector: dragHandle,
|
handleSelector: dragHandle,
|
||||||
noDragClass: 'nodrag',
|
noDragClass: 'nodrag',
|
||||||
nodeClickDistance,
|
nodeClickDistance,
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { useStore } from '$lib/store';
|
|
||||||
import { Selection } from '$lib/components/Selection';
|
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Selection
|
|
||||||
isVisible={!!(store.selectionRect && store.selectionRectMode === 'user')}
|
|
||||||
width={store.selectionRect?.width}
|
|
||||||
height={store.selectionRect?.height}
|
|
||||||
x={store.selectionRect?.x}
|
|
||||||
y={store.selectionRect?.y}
|
|
||||||
/>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as UserSelection } from './UserSelection.svelte';
|
|
||||||
@@ -11,9 +11,9 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleSelected<Item extends Node | Edge>(ids: string[]) {
|
export function toggleSelected<Item extends Node | Edge>(ids: Set<string>) {
|
||||||
return (item: Item) => {
|
return (item: Item) => {
|
||||||
const isSelected = ids.includes(item.id);
|
const isSelected = ids.has(item.id);
|
||||||
|
|
||||||
if (item.selected !== isSelected) {
|
if (item.selected !== isSelected) {
|
||||||
return { ...item, selected: isSelected };
|
return { ...item, selected: isSelected };
|
||||||
@@ -22,17 +22,27 @@
|
|||||||
return item;
|
return item;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: maybe replace with set.intersection?
|
||||||
|
function setEq(a: Set<string>, b: Set<string>) {
|
||||||
|
if (a.size !== b.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of a) {
|
||||||
|
if (!b.has(item)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { SelectionMode, getEventPosition, getNodesInside } from '@xyflow/system';
|
||||||
SelectionMode,
|
|
||||||
getEventPosition,
|
|
||||||
getNodesInside,
|
|
||||||
getConnectedEdges
|
|
||||||
} from '@xyflow/system';
|
|
||||||
|
|
||||||
import type { Node, Edge, InternalNode } from '$lib/types';
|
import type { Node, Edge } from '$lib/types';
|
||||||
import type { PaneProps } from './types';
|
import type { PaneProps } from './types';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -47,7 +57,10 @@
|
|||||||
// svelte-ignore non_reactive_update
|
// svelte-ignore non_reactive_update
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let containerBounds: DOMRect | null = null;
|
let containerBounds: DOMRect | null = null;
|
||||||
let selectedNodes: InternalNode[] = [];
|
// let selectedNodes: InternalNode[] = [];
|
||||||
|
|
||||||
|
let selectedNodeIds: Set<string> = new Set();
|
||||||
|
let selectedEdgeIds: Set<string> = new Set();
|
||||||
|
|
||||||
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
|
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
|
||||||
let isSelecting = $derived(
|
let isSelecting = $derived(
|
||||||
@@ -109,7 +122,7 @@
|
|||||||
if (!isSelecting || !containerBounds || !store.selectionRect) {
|
if (!isSelecting || !containerBounds || !store.selectionRect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const start = performance.now();
|
||||||
selectionInProgress = true;
|
selectionInProgress = true;
|
||||||
|
|
||||||
const mousePos = getEventPosition(event, containerBounds);
|
const mousePos = getEventPosition(event, containerBounds);
|
||||||
@@ -122,36 +135,48 @@
|
|||||||
width: Math.abs(mousePos.x - startX),
|
width: Math.abs(mousePos.x - startX),
|
||||||
height: Math.abs(mousePos.y - startY)
|
height: Math.abs(mousePos.y - startY)
|
||||||
};
|
};
|
||||||
const prevSelectedNodeIds = selectedNodes.map((n) => n.id);
|
|
||||||
const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, store.edges).map((e) => e.id);
|
|
||||||
|
|
||||||
selectedNodes = getNodesInside(
|
const prevSelectedNodeIds = selectedNodeIds;
|
||||||
store.nodeLookup,
|
const prevSelectedEdgeIds = selectedEdgeIds;
|
||||||
nextUserSelectRect,
|
|
||||||
[store.viewport.x, store.viewport.y, store.viewport.zoom],
|
selectedNodeIds = new Set(
|
||||||
store.selectionMode === SelectionMode.Partial,
|
getNodesInside(
|
||||||
true
|
store.nodeLookup,
|
||||||
|
nextUserSelectRect,
|
||||||
|
[store.viewport.x, store.viewport.y, store.viewport.zoom],
|
||||||
|
store.selectionMode === SelectionMode.Partial,
|
||||||
|
true,
|
||||||
|
store.defaultNodeOptions.selectable
|
||||||
|
).map((n) => n.id)
|
||||||
);
|
);
|
||||||
const selectedEdgeIds = getConnectedEdges(selectedNodes, store.edges).map((e) => e.id);
|
|
||||||
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
// TODO: replace with extended connectionLookup
|
||||||
|
let edgesSelectable = store.defaultEdgeOptions.selectable ?? true;
|
||||||
|
selectedEdgeIds = new Set();
|
||||||
|
store.edges.forEach((edge) => {
|
||||||
|
if (
|
||||||
|
selectedNodeIds.has(edge.source) &&
|
||||||
|
selectedNodeIds.has(edge.target) &&
|
||||||
|
(edge.selectable ?? edgesSelectable)
|
||||||
|
) {
|
||||||
|
selectedEdgeIds.add(edge.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// this prevents unnecessary updates while updating the selection rectangle
|
// this prevents unnecessary updates while updating the selection rectangle
|
||||||
if (
|
if (setEq(prevSelectedNodeIds, selectedNodeIds)) {
|
||||||
prevSelectedNodeIds.length !== selectedNodeIds.length ||
|
|
||||||
selectedNodeIds.some((id) => !prevSelectedNodeIds.includes(id))
|
|
||||||
) {
|
|
||||||
store.nodes = store.nodes.map(toggleSelected(selectedNodeIds));
|
store.nodes = store.nodes.map(toggleSelected(selectedNodeIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (setEq(prevSelectedEdgeIds, selectedEdgeIds)) {
|
||||||
prevSelectedEdgeIds.length !== selectedEdgeIds.length ||
|
|
||||||
selectedEdgeIds.some((id) => !prevSelectedEdgeIds.includes(id))
|
|
||||||
) {
|
|
||||||
store.edges = store.edges.map(toggleSelected(selectedEdgeIds));
|
store.edges = store.edges.map(toggleSelected(selectedEdgeIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
store.selectionRectMode = 'user';
|
store.selectionRectMode = 'user';
|
||||||
store.selectionRect = nextUserSelectRect;
|
store.selectionRect = nextUserSelectRect;
|
||||||
|
|
||||||
|
const end = performance.now();
|
||||||
|
// console.log('onPointerMove', end - start);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPointerUp(event: PointerEvent) {
|
function onPointerUp(event: PointerEvent) {
|
||||||
@@ -168,7 +193,7 @@
|
|||||||
}
|
}
|
||||||
store.selectionRect = null;
|
store.selectionRect = null;
|
||||||
|
|
||||||
if (selectedNodes.length > 0) {
|
if (selectedNodeIds.size > 0) {
|
||||||
store.selectionRectMode = 'nodes';
|
store.selectionRectMode = 'nodes';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
import { Viewport as ViewportComponent } from '$lib/container/Viewport';
|
import { Viewport as ViewportComponent } from '$lib/container/Viewport';
|
||||||
import { NodeRenderer } from '$lib/container/NodeRenderer';
|
import { NodeRenderer } from '$lib/container/NodeRenderer';
|
||||||
import { EdgeRenderer } from '$lib/container/EdgeRenderer';
|
import { EdgeRenderer } from '$lib/container/EdgeRenderer';
|
||||||
import { UserSelection } from '$lib/components/UserSelection';
|
|
||||||
import { NodeSelection } from '$lib/components/NodeSelection';
|
import { NodeSelection } from '$lib/components/NodeSelection';
|
||||||
|
import { Selection } from '$lib/components/Selection';
|
||||||
import { KeyHandler } from '$lib/components/KeyHandler';
|
import { KeyHandler } from '$lib/components/KeyHandler';
|
||||||
import { ConnectionLine } from '$lib/components/ConnectionLine';
|
import { ConnectionLine } from '$lib/components/ConnectionLine';
|
||||||
import { Attribution } from '$lib/components/Attribution';
|
import { Attribution } from '$lib/components/Attribution';
|
||||||
@@ -194,7 +194,13 @@
|
|||||||
{onnodedragstop}
|
{onnodedragstop}
|
||||||
/>
|
/>
|
||||||
</ViewportComponent>
|
</ViewportComponent>
|
||||||
<UserSelection />
|
<Selection
|
||||||
|
isVisible={!!(store.selectionRect && store.selectionRectMode === 'user')}
|
||||||
|
width={store.selectionRect?.width}
|
||||||
|
height={store.selectionRect?.height}
|
||||||
|
x={store.selectionRect?.x}
|
||||||
|
y={store.selectionRect?.y}
|
||||||
|
/>
|
||||||
</Pane>
|
</Pane>
|
||||||
</Zoom>
|
</Zoom>
|
||||||
<Attribution {proOptions} position={attributionPosition} />
|
<Attribution {proOptions} position={attributionPosition} />
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export type NodeTypes = Record<
|
|||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// TODO: we should be more selective about this but otherwise good to go
|
||||||
export type DefaultNodeOptions = Partial<Omit<Node, 'id'>>;
|
export type DefaultNodeOptions = Partial<Omit<Node, 'id'>>;
|
||||||
|
|
||||||
export type BuiltInNode =
|
export type BuiltInNode =
|
||||||
|
|||||||
@@ -36,5 +36,5 @@ export type HandleProps = {
|
|||||||
/** Id of the handle
|
/** Id of the handle
|
||||||
* @remarks optional if there is only one handle of this type
|
* @remarks optional if there is only one handle of this type
|
||||||
*/
|
*/
|
||||||
id?: string;
|
id?: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -202,7 +202,8 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
|
|||||||
[tx, ty, tScale]: Transform = [0, 0, 1],
|
[tx, ty, tScale]: Transform = [0, 0, 1],
|
||||||
partially = false,
|
partially = false,
|
||||||
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
|
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
|
||||||
excludeNonSelectableNodes = false
|
excludeNonSelectableNodes = false,
|
||||||
|
defaultNodeSelectable: boolean = true
|
||||||
): InternalNodeBase<NodeType>[] => {
|
): InternalNodeBase<NodeType>[] => {
|
||||||
const paneRect = {
|
const paneRect = {
|
||||||
...pointToRendererPoint(rect, [tx, ty, tScale]),
|
...pointToRendererPoint(rect, [tx, ty, tScale]),
|
||||||
@@ -213,7 +214,7 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
|
|||||||
const visibleNodes: InternalNodeBase<NodeType>[] = [];
|
const visibleNodes: InternalNodeBase<NodeType>[] = [];
|
||||||
|
|
||||||
for (const node of nodes.values()) {
|
for (const node of nodes.values()) {
|
||||||
const { measured, selectable = true, hidden = false } = node;
|
const { measured, selectable = defaultNodeSelectable, hidden = false } = node;
|
||||||
|
|
||||||
if ((excludeNonSelectableNodes && !selectable) || hidden) {
|
if ((excludeNonSelectableNodes && !selectable) || hidden) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user