|
<script context="module">export function wrapHandler(handler, container) { |
|
return (event) => { |
|
if (event.target !== container) { |
|
return; |
|
} |
|
handler?.(event); |
|
}; |
|
} |
|
export function toggleSelected(ids) { |
|
return (item) => { |
|
const isSelected = ids.includes(item.id); |
|
if (item.selected !== isSelected) { |
|
item.selected = isSelected; |
|
} |
|
return item; |
|
}; |
|
} |
|
</script> |
|
|
|
<script>import { createEventDispatcher } from 'svelte'; |
|
import { SelectionMode, getEventPosition, getNodesInside, getConnectedEdges } from '@xyflow/system'; |
|
import { useStore } from '../../store'; |
|
export let panOnDrag = undefined; |
|
export let selectionOnDrag = undefined; |
|
const dispatch = createEventDispatcher(); |
|
const { nodes, nodeLookup, edges, viewport, dragging, elementsSelectable, selectionRect, selectionRectMode, selectionKeyPressed, selectionMode, panActivationKeyPressed, unselectNodesAndEdges } = useStore(); |
|
let container; |
|
let containerBounds = null; |
|
let selectedNodes = []; |
|
$: _panOnDrag = $panActivationKeyPressed || panOnDrag; |
|
$: isSelecting = |
|
$selectionKeyPressed || $selectionRect || (selectionOnDrag && _panOnDrag !== true); |
|
$: hasActiveSelection = $elementsSelectable && (isSelecting || $selectionRectMode === 'user'); |
|
|
|
let selectionInProgress = false; |
|
function onClick(event) { |
|
|
|
if (selectionInProgress) { |
|
selectionInProgress = false; |
|
return; |
|
} |
|
dispatch('paneclick', { event }); |
|
unselectNodesAndEdges(); |
|
selectionRectMode.set(null); |
|
} |
|
function onPointerDown(event) { |
|
containerBounds = container.getBoundingClientRect(); |
|
if (!elementsSelectable || |
|
!isSelecting || |
|
event.button !== 0 || |
|
event.target !== container || |
|
!containerBounds) { |
|
return; |
|
} |
|
event.target?.setPointerCapture?.(event.pointerId); |
|
const { x, y } = getEventPosition(event, containerBounds); |
|
unselectNodesAndEdges(); |
|
selectionRect.set({ |
|
width: 0, |
|
height: 0, |
|
startX: x, |
|
startY: y, |
|
x, |
|
y |
|
}); |
|
|
|
} |
|
function onPointerMove(event) { |
|
if (!isSelecting || !containerBounds || !$selectionRect) { |
|
return; |
|
} |
|
selectionInProgress = true; |
|
const mousePos = getEventPosition(event, containerBounds); |
|
const startX = $selectionRect.startX ?? 0; |
|
const startY = $selectionRect.startY ?? 0; |
|
const nextUserSelectRect = { |
|
...$selectionRect, |
|
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) |
|
}; |
|
const prevSelectedNodeIds = selectedNodes.map((n) => n.id); |
|
const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id); |
|
selectedNodes = getNodesInside($nodeLookup, nextUserSelectRect, [$viewport.x, $viewport.y, $viewport.zoom], $selectionMode === SelectionMode.Partial, true); |
|
const selectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id); |
|
const selectedNodeIds = selectedNodes.map((n) => n.id); |
|
|
|
if (prevSelectedNodeIds.length !== selectedNodeIds.length || |
|
selectedNodeIds.some((id) => !prevSelectedNodeIds.includes(id))) { |
|
nodes.update((nodes) => nodes.map(toggleSelected(selectedNodeIds))); |
|
} |
|
if (prevSelectedEdgeIds.length !== selectedEdgeIds.length || |
|
selectedEdgeIds.some((id) => !prevSelectedEdgeIds.includes(id))) { |
|
edges.update((edges) => edges.map(toggleSelected(selectedEdgeIds))); |
|
} |
|
selectionRectMode.set('user'); |
|
selectionRect.set(nextUserSelectRect); |
|
} |
|
function onPointerUp(event) { |
|
if (event.button !== 0) { |
|
return; |
|
} |
|
event.target?.releasePointerCapture?.(event.pointerId); |
|
|
|
|
|
if (!isSelecting && $selectionRectMode === 'user' && event.target === container) { |
|
onClick?.(event); |
|
} |
|
selectionRect.set(null); |
|
if (selectedNodes.length > 0) { |
|
$selectionRectMode = 'nodes'; |
|
} |
|
|
|
|
|
if ($selectionKeyPressed) { |
|
selectionInProgress = false; |
|
} |
|
|
|
} |
|
const onContextMenu = (event) => { |
|
if (Array.isArray(_panOnDrag) && _panOnDrag?.includes(2)) { |
|
event.preventDefault(); |
|
return; |
|
} |
|
dispatch('panecontextmenu', { event }); |
|
}; |
|
</script> |
|
|
|
|
|
|
|
<div |
|
bind:this={container} |
|
class="svelte-flow__pane" |
|
class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))} |
|
class:dragging={$dragging} |
|
class:selection={isSelecting} |
|
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)} |
|
on:pointerdown={hasActiveSelection ? onPointerDown : undefined} |
|
on:pointermove={hasActiveSelection ? onPointerMove : undefined} |
|
on:pointerup={hasActiveSelection ? onPointerUp : undefined} |
|
on:contextmenu={wrapHandler(onContextMenu, container)} |
|
> |
|
<slot /> |
|
</div> |
|
|
|
<style> |
|
.svelte-flow__pane { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
</style> |
|
|