feat(svelte): selection box
This commit is contained in:
@@ -31,5 +31,7 @@
|
||||
.react-flow__nodes {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
import { useStore } from '$lib/store';
|
||||
import { SelectionMode, type Node, type Edge } from '@reactflow/system';
|
||||
import { getConnectedEdges, getEventPosition, getNodesInside } from '@reactflow/utils';
|
||||
|
||||
const { nodesStore, edgesStore, transformStore, nodeOriginStore, draggingStore, selectionRectStore, selectionRectModeStore, selectionKeyPressedStore, resetSelectedElements } = useStore();
|
||||
|
||||
const wrapHandler = (
|
||||
handler: (evt: MouseEvent) => void,
|
||||
container: HTMLDivElement
|
||||
): (evt: MouseEvent) => void => {
|
||||
return (event: MouseEvent) => {
|
||||
if (event.target !== container) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler?.(event);
|
||||
};
|
||||
};
|
||||
|
||||
// @todo take from props
|
||||
const elementsSelectable = true;
|
||||
const selectionMode = SelectionMode.Partial
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let containerBounds: DOMRect | null = null
|
||||
let selectedNodes: Node[] = [];
|
||||
|
||||
$: isSelecting = $selectionKeyPressedStore;
|
||||
$: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectModeStore === 'user');
|
||||
|
||||
|
||||
function onClick (event: MouseEvent) {
|
||||
// onPaneClick?.(event);
|
||||
|
||||
resetSelectedElements();
|
||||
selectionRectModeStore.set(null)
|
||||
}
|
||||
|
||||
function onMouseDown(event: MouseEvent) {
|
||||
containerBounds = container.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
!elementsSelectable ||
|
||||
!isSelecting ||
|
||||
event.button !== 0 ||
|
||||
event.target !== container ||
|
||||
!containerBounds
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y } = getEventPosition(event, containerBounds);
|
||||
|
||||
resetSelectedElements();
|
||||
|
||||
selectionRectStore.set({
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: x,
|
||||
startY: y,
|
||||
x,
|
||||
y
|
||||
});
|
||||
|
||||
|
||||
// onSelectionStart?.(event);
|
||||
};
|
||||
|
||||
function toggleSelected<Item extends Node | Edge>(ids: string[]) {
|
||||
return (item: Item) => {
|
||||
const isSelected = ids.includes(item.id);
|
||||
|
||||
if (item.selected !== isSelected) {
|
||||
return {
|
||||
...item,
|
||||
selected: isSelected,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
if (!isSelecting || !containerBounds || !$selectionRectStore) {
|
||||
return;
|
||||
}
|
||||
const mousePos = getEventPosition(event, containerBounds);
|
||||
const startX = $selectionRectStore.startX ?? 0;
|
||||
const startY = $selectionRectStore.startY ?? 0;
|
||||
const nextUserSelectRect = {
|
||||
...$selectionRectStore,
|
||||
x: mousePos.x < startX ? mousePos.x : startX,
|
||||
y: mousePos.y < startY ? mousePos.y : startY,
|
||||
width: Math.abs(mousePos.x - startX),
|
||||
height: Math.abs(mousePos.y - startY),
|
||||
};
|
||||
|
||||
selectedNodes = getNodesInside(
|
||||
new Map($nodesStore.map(node => [node.id, node])),
|
||||
nextUserSelectRect,
|
||||
$transformStore,
|
||||
selectionMode === SelectionMode.Partial,
|
||||
true,
|
||||
$nodeOriginStore
|
||||
);
|
||||
const selectedEdgeIds = getConnectedEdges(selectedNodes, $edgesStore).map((e) => e.id);
|
||||
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
||||
|
||||
nodesStore.update(nodes => nodes.map(toggleSelected(selectedNodeIds)));
|
||||
edgesStore.update(edges => edges.map(toggleSelected(selectedEdgeIds)));
|
||||
|
||||
selectionRectModeStore.set('user');
|
||||
selectionRectStore.set(nextUserSelectRect);
|
||||
}
|
||||
|
||||
function onMouseUp(event: MouseEvent) {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only want to trigger click functions when in selection mode if
|
||||
// the user did not move the mouse.
|
||||
// if (!userSelectionActive && userSelectionRect && event.target === container.current) {
|
||||
// onClick?.(event);
|
||||
// }
|
||||
selectionRectStore.set(null);
|
||||
|
||||
if (selectedNodes.length > 0) {
|
||||
selectionRectModeStore.set('nodes');
|
||||
}
|
||||
|
||||
|
||||
// onSelectionEnd?.(event);
|
||||
}
|
||||
|
||||
const onMouseLeave = (event: MouseEvent) => {
|
||||
if ($selectionRectModeStore === 'user') {
|
||||
selectionRectModeStore.set(selectedNodes.length > 0 ? 'nodes' : null);
|
||||
// onSelectionEnd?.(event);
|
||||
}
|
||||
|
||||
selectionRectStore.set(null);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={container}
|
||||
class="react-flow__pane"
|
||||
class:dragging={$draggingStore}
|
||||
class:selection={isSelecting}
|
||||
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||
on:mousedown={hasActiveSelection ? onMouseDown : undefined}
|
||||
on:mousemove={hasActiveSelection? onMouseMove : undefined}
|
||||
on:mouseup={hasActiveSelection ? onMouseUp : undefined}
|
||||
on:mouseleave={hasActiveSelection ? onMouseLeave : undefined}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.react-flow__pane {
|
||||
cursor: grab;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.selection {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
@@ -4,18 +4,23 @@
|
||||
import type { Node, Edge } from '@reactflow/system';
|
||||
|
||||
import { key, createStore } from '$lib/store';
|
||||
import Zoom from '$lib/container/Zoom/index.svelte';
|
||||
import Pane from '$lib/container/Pane/index.svelte';
|
||||
import Viewport from '$lib/container/Viewport.svelte';
|
||||
import NodeRenderer from '$lib/container/NodeRenderer.svelte';
|
||||
import EdgeRenderer from '$lib/container/EdgeRenderer/index.svelte';
|
||||
import UserSelection from '$lib/components/UserSelection/index.svelte';
|
||||
import NodeSelection from '$lib/components/NodeSelection/index.svelte';
|
||||
import KeyHandler from '$lib/components/KeyHandler/index.svelte';
|
||||
|
||||
export let nodes: Node[];
|
||||
export let edges: Edge[];
|
||||
export let fitView: boolean;
|
||||
|
||||
export let className: string | null = null;
|
||||
export let id: string = '1';
|
||||
export let nodes: Node[] = [];
|
||||
export let edges: Edge[] = [];
|
||||
export let fitView: boolean = false;
|
||||
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
|
||||
let props = { ...$$restProps };
|
||||
let domNode: Element;
|
||||
|
||||
const store = createStore({
|
||||
@@ -35,19 +40,24 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<div
|
||||
{...props}
|
||||
{...$$restProps}
|
||||
class={cc(['react-flow', className])}
|
||||
data-testid="rf__wrapper"
|
||||
id={id}
|
||||
bind:this={domNode}
|
||||
>
|
||||
<Viewport>
|
||||
<NodeRenderer />
|
||||
<EdgeRenderer />
|
||||
</Viewport>
|
||||
|
||||
<KeyHandler />
|
||||
<Zoom>
|
||||
<Pane>
|
||||
<Viewport>
|
||||
<NodeSelection />
|
||||
<NodeRenderer />
|
||||
<EdgeRenderer />
|
||||
</Viewport>
|
||||
<UserSelection />
|
||||
</Pane>
|
||||
</Zoom>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -59,5 +69,4 @@
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,35 +1,25 @@
|
||||
<script lang="ts">
|
||||
import zoom from '$lib/hooks/zoom'
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
const { transformStore, d3Store } = useStore();
|
||||
const { transformStore} = useStore();
|
||||
</script>
|
||||
|
||||
<div class="react-flow__pane" use:zoom={{ transformStore, d3Store }}>
|
||||
<div
|
||||
class="react-flow__viewport"
|
||||
style="transform: translate({$transformStore[0]}px, {$transformStore[1]}px) scale({$transformStore[2]})"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div
|
||||
class="react-flow__viewport"
|
||||
style="transform: translate({$transformStore[0]}px, {$transformStore[1]}px) scale({$transformStore[2]})"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.react-flow__pane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.react-flow__viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transform-origin: 0 0;
|
||||
top:0;
|
||||
left:0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { useStore } from '$lib/store';
|
||||
import zoom from '$lib/actions/zoom';
|
||||
|
||||
const { transformStore, d3Store, selectionKeyPressedStore, selectionRectModeStore } = useStore();
|
||||
|
||||
$: selecting = $selectionKeyPressedStore || $selectionRectModeStore === 'user';
|
||||
</script>
|
||||
|
||||
<div class="react-flow__zoom" use:zoom={{ transformStore, d3Store, selecting }}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.react-flow__zoom {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user