chore(core): cleanup pane (#1507)

* chore(core): cleanup pane

* chore(core): cleanup actions
This commit is contained in:
Braks
2024-07-01 09:44:55 +02:00
parent e57f22c185
commit 266f8744ad
6 changed files with 144 additions and 164 deletions
+74 -52
View File
@@ -2,9 +2,10 @@
import { ref, toRef, watch } from 'vue' import { ref, toRef, watch } from 'vue'
import UserSelection from '../../components/UserSelection/UserSelection.vue' import UserSelection from '../../components/UserSelection/UserSelection.vue'
import NodesSelection from '../../components/NodesSelection/NodesSelection.vue' import NodesSelection from '../../components/NodesSelection/NodesSelection.vue'
import type { NodeChange } from '../../types'
import { SelectionMode } from '../../types' import { SelectionMode } from '../../types'
import { useKeyPress, useVueFlow } from '../../composables' import { useKeyPress, useVueFlow } from '../../composables'
import { getConnectedEdges, getNodesInside } from '../../utils' import { getEventPosition, getNodesInside, getSelectionChanges } from '../../utils'
import { getMousePosition } from './utils' import { getMousePosition } from './utils'
const { isSelecting, selectionKeyPressed } = defineProps<{ isSelecting: boolean; selectionKeyPressed: boolean }>() const { isSelecting, selectionKeyPressed } = defineProps<{ isSelecting: boolean; selectionKeyPressed: boolean }>()
@@ -12,7 +13,6 @@ const { isSelecting, selectionKeyPressed } = defineProps<{ isSelecting: boolean;
const { const {
vueFlowRef, vueFlowRef,
getNodes, getNodes,
getEdges,
viewport, viewport,
emits, emits,
userSelectionActive, userSelectionActive,
@@ -21,7 +21,6 @@ const {
userSelectionRect, userSelectionRect,
elementsSelectable, elementsSelectable,
nodesSelectionActive, nodesSelectionActive,
addSelectedElements,
getSelectedEdges, getSelectedEdges,
getSelectedNodes, getSelectedNodes,
removeNodes, removeNodes,
@@ -30,6 +29,8 @@ const {
deleteKeyCode, deleteKeyCode,
multiSelectionKeyCode, multiSelectionKeyCode,
multiSelectionActive, multiSelectionActive,
edgeLookup,
nodeLookup,
} = useVueFlow() } = useVueFlow()
const container = ref<HTMLDivElement | null>(null) const container = ref<HTMLDivElement | null>(null)
@@ -40,6 +41,8 @@ const prevSelectedEdgesCount = ref(0)
const containerBounds = ref<DOMRect>() const containerBounds = ref<DOMRect>()
const edgeIdLookup = ref<Map<string, Set<string>>>(new Map())
const hasActiveSelection = toRef(() => elementsSelectable.value && (isSelecting || userSelectionActive.value)) const hasActiveSelection = toRef(() => elementsSelectable.value && (isSelecting || userSelectionActive.value))
// Used to prevent click events when the user lets go of the selectionKey during a selection // Used to prevent click events when the user lets go of the selectionKey during a selection
@@ -65,6 +68,16 @@ watch(multiSelectKeyPressed, (isKeyPressed) => {
multiSelectionActive.value = isKeyPressed multiSelectionActive.value = isKeyPressed
}) })
function wrapHandler(handler: Function, containerRef: HTMLDivElement | null) {
return (event: MouseEvent) => {
if (event.target !== containerRef) {
return
}
handler?.(event)
}
}
function resetUserSelection() { function resetUserSelection() {
userSelectionActive.value = false userSelectionActive.value = false
userSelectionRect.value = null userSelectionRect.value = null
@@ -74,7 +87,7 @@ function resetUserSelection() {
} }
function onClick(event: MouseEvent) { function onClick(event: MouseEvent) {
if (selectionInProgress.value || event.target !== container.value || hasActiveSelection.value) { if (selectionInProgress.value) {
selectionInProgress.value = false selectionInProgress.value = false
return return
} }
@@ -87,10 +100,6 @@ function onClick(event: MouseEvent) {
} }
function onContextMenu(event: MouseEvent) { function onContextMenu(event: MouseEvent) {
if (event.target !== container.value) {
return
}
if (Array.isArray(panOnDrag.value) && panOnDrag.value?.includes(2)) { if (Array.isArray(panOnDrag.value) && panOnDrag.value?.includes(2)) {
event.preventDefault() event.preventDefault()
return return
@@ -100,27 +109,32 @@ function onContextMenu(event: MouseEvent) {
} }
function onWheel(event: WheelEvent) { function onWheel(event: WheelEvent) {
if (event.target !== container.value) {
return
}
emits.paneScroll(event) emits.paneScroll(event)
} }
function onPointerDown(event: PointerEvent) { function onPointerDown(event: PointerEvent) {
if (!hasActiveSelection.value) {
return emits.paneMouseMove(event)
}
containerBounds.value = vueFlowRef.value?.getBoundingClientRect() containerBounds.value = vueFlowRef.value?.getBoundingClientRect()
container.value?.setPointerCapture(event.pointerId) container.value?.setPointerCapture(event.pointerId)
if (!elementsSelectable || !isSelecting || event.button !== 0 || event.target !== container.value || !containerBounds.value) { if (
!elementsSelectable.value ||
!isSelecting ||
event.button !== 0 ||
event.target !== container.value ||
!containerBounds.value
) {
return return
} }
const { x, y } = getMousePosition(event, containerBounds.value) const { x, y } = getMousePosition(event, containerBounds.value)
edgeIdLookup.value = new Map()
for (const [id, edge] of edgeLookup.value) {
edgeIdLookup.value.set(edge.source, edgeIdLookup.value.get(edge.source)?.add(id) || new Set([id]))
edgeIdLookup.value.set(edge.target, edgeIdLookup.value.get(edge.target)?.add(id) || new Set([id]))
}
removeSelectedElements() removeSelectedElements()
userSelectionRect.value = { userSelectionRect.value = {
@@ -138,43 +152,62 @@ function onPointerDown(event: PointerEvent) {
} }
function onPointerMove(event: PointerEvent) { function onPointerMove(event: PointerEvent) {
if (!hasActiveSelection.value) {
return emits.paneMouseMove(event)
}
if (!containerBounds.value || !userSelectionRect.value) { if (!containerBounds.value || !userSelectionRect.value) {
return return
} }
selectionInProgress.value = true selectionInProgress.value = true
const mousePos = getMousePosition(event, containerBounds.value) const { x: mouseX, y: mouseY } = getEventPosition(event, containerBounds.value)
const { startX = 0, startY = 0 } = userSelectionRect.value const { startX = 0, startY = 0 } = userSelectionRect.value
const nextUserSelectRect = { const nextUserSelectRect = {
...userSelectionRect.value, startX,
x: mousePos.x < startX ? mousePos.x : startX, startY,
y: mousePos.y < startY ? mousePos.y : startY, x: mouseX < startX ? mouseX : startX,
width: Math.abs(mousePos.x - startX), y: mouseY < startY ? mouseY : startY,
height: Math.abs(mousePos.y - startY), width: Math.abs(mouseX - startX),
height: Math.abs(mouseY - startY),
} }
const selectedNodes = getNodesInside( const selectedNodes = getNodesInside(
getNodes.value, getNodes.value,
userSelectionRect.value, nextUserSelectRect,
viewport.value, viewport.value,
selectionMode.value === SelectionMode.Partial, selectionMode.value === SelectionMode.Partial,
true, true,
) )
const selectedEdges = getConnectedEdges(selectedNodes, getEdges.value) const selectedEdgeIds = new Set<string>()
const selectedNodeIds = new Set<string>()
prevSelectedNodesCount.value = selectedNodes.length for (const selectedNode of selectedNodes) {
prevSelectedEdgesCount.value = selectedEdges.length selectedNodeIds.add(selectedNode.id)
const edgeIds = edgeIdLookup.value.get(selectedNode.id)
if (edgeIds) {
for (const edgeId of edgeIds) {
selectedEdgeIds.add(edgeId)
}
}
}
if (prevSelectedNodesCount.value !== selectedNodeIds.size) {
prevSelectedNodesCount.value = selectedNodeIds.size
const changes = getSelectionChanges(nodeLookup.value, selectedNodeIds, true) as NodeChange[]
emits.nodesChange(changes)
}
if (prevSelectedEdgesCount.value !== selectedEdgeIds.size) {
prevSelectedEdgesCount.value = selectedEdgeIds.size
const changes = getSelectionChanges(edgeLookup.value, selectedEdgeIds)
emits.edgesChange(changes)
}
userSelectionRect.value = nextUserSelectRect userSelectionRect.value = nextUserSelectRect
userSelectionActive.value = true
addSelectedElements([...selectedNodes, ...selectedEdges]) nodesSelectionActive.value = false
} }
function onPointerUp(event: PointerEvent) { function onPointerUp(event: PointerEvent) {
@@ -184,10 +217,6 @@ function onPointerUp(event: PointerEvent) {
container.value?.releasePointerCapture(event.pointerId) container.value?.releasePointerCapture(event.pointerId)
if (!hasActiveSelection.value) {
return
}
// 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.
if (!userSelectionActive.value && userSelectionRect.value && event.target === container.value) { if (!userSelectionActive.value && userSelectionRect.value && event.target === container.value) {
@@ -206,14 +235,6 @@ function onPointerUp(event: PointerEvent) {
selectionInProgress.value = false selectionInProgress.value = false
} }
} }
function onPointerEnter(event: PointerEvent) {
if (hasActiveSelection.value) {
return
}
emits.paneMouseEnter(event)
}
</script> </script>
<script lang="ts"> <script lang="ts">
@@ -228,13 +249,14 @@ export default {
ref="container" ref="container"
class="vue-flow__pane vue-flow__container" class="vue-flow__pane vue-flow__container"
:class="{ selection: isSelecting }" :class="{ selection: isSelecting }"
@click="onClick" @click="(event) => (hasActiveSelection ? undefined : wrapHandler(onClick, container)(event))"
@contextmenu="onContextMenu" @contextmenu="wrapHandler(onContextMenu, container)($event)"
@wheel.passive="onWheel" @wheel.passive="wrapHandler(onWheel, container)($event)"
@pointerenter="onPointerEnter" @pointerenter="(event) => (hasActiveSelection ? undefined : emits.paneMouseEnter(event))"
@pointerdown="onPointerDown" @pointerdown="(event) => (hasActiveSelection ? onPointerDown(event) : emits.paneMouseMove(event))"
@pointermove="onPointerMove" @pointermove="(event) => (hasActiveSelection ? onPointerMove(event) : emits.paneMouseMove(event))"
@pointerup="onPointerUp" @pointerup="(event) => (hasActiveSelection ? onPointerUp(event) : undefined)"
@pointerleave="emits.paneMouseLeave($event)"
> >
<slot /> <slot />
<UserSelection v-if="userSelectionActive && userSelectionRect" :user-selection-rect="userSelectionRect" /> <UserSelection v-if="userSelectionActive && userSelectionRect" :user-selection-rect="userSelectionRect" />
+34 -87
View File
@@ -6,7 +6,6 @@ import type {
CoordinateExtent, CoordinateExtent,
Edge, Edge,
EdgeAddChange, EdgeAddChange,
EdgeChange,
EdgeLookup, EdgeLookup,
EdgeRemoveChange, EdgeRemoveChange,
EdgeSelectionChange, EdgeSelectionChange,
@@ -16,7 +15,6 @@ import type {
GraphNode, GraphNode,
Node, Node,
NodeAddChange, NodeAddChange,
NodeChange,
NodeDimensionChange, NodeDimensionChange,
NodeLookup, NodeLookup,
NodePositionChange, NodePositionChange,
@@ -178,76 +176,20 @@ export function useActions(state: State, nodeLookup: ComputedRef<NodeLookup>, ed
} }
} }
const nodeSelectionHandler = (nodes: GraphNode[], selected: boolean) => {
const nodeIds: string[] = []
for (const node of nodes) {
nodeIds.push(node.id)
}
let changedNodes: NodeChange[] = []
let changedEdges: EdgeChange[] = []
if (state.multiSelectionActive) {
for (const nodeId of nodeIds) {
changedNodes.push(createSelectionChange(nodeId, selected))
}
} else {
const selectionChanges = getSelectionChanges([...state.nodes, ...state.edges], nodeIds)
changedNodes = selectionChanges.changedNodes
changedEdges = selectionChanges.changedEdges
}
if (changedNodes.length) {
state.hooks.nodesChange.trigger(changedNodes)
}
if (changedEdges.length) {
state.hooks.edgesChange.trigger(changedEdges)
}
}
const edgeSelectionHandler = (edges: GraphEdge[], selected: boolean) => {
const edgeIds: string[] = []
for (const edge of edges) {
edgeIds.push(edge.id)
}
let changedNodes: NodeChange[] = []
let changedEdges: EdgeChange[] = []
if (state.multiSelectionActive) {
for (const edgeId of edgeIds) {
changedEdges.push(createSelectionChange(edgeId, selected))
}
} else {
const selectionChanges = getSelectionChanges([...state.nodes, ...state.edges], edgeIds)
changedNodes = selectionChanges.changedNodes
changedEdges = selectionChanges.changedEdges
}
if (changedNodes.length) {
state.hooks.nodesChange.trigger(changedNodes)
}
if (changedEdges.length) {
state.hooks.edgesChange.trigger(changedEdges)
}
}
const elementSelectionHandler = (elements: Elements, selected: boolean) => { const elementSelectionHandler = (elements: Elements, selected: boolean) => {
const nodeIds: string[] = [] const nodeIds = new Set<string>()
const edgeIds: string[] = [] const edgeIds = new Set<string>()
for (const element of elements) { for (const element of elements) {
if (isNode(element)) { if (isNode(element)) {
nodeIds.push(element.id) nodeIds.add(element.id)
} else if (isEdge(element)) { } else if (isEdge(element)) {
edgeIds.push(element.id) edgeIds.add(element.id)
} }
} }
const { changedNodes, changedEdges } = getSelectionChanges([...state.nodes, ...state.edges], [...nodeIds, ...edgeIds]) const changedNodes = getSelectionChanges(nodeLookup.value, nodeIds, true)
const changedEdges = getSelectionChanges(edgeLookup.value, edgeIds)
if (state.multiSelectionActive) { if (state.multiSelectionActive) {
for (const nodeId of nodeIds) { for (const nodeId of nodeIds) {
@@ -269,11 +211,25 @@ export function useActions(state: State, nodeLookup: ComputedRef<NodeLookup>, ed
} }
const addSelectedNodes: Actions['addSelectedNodes'] = (nodes) => { const addSelectedNodes: Actions['addSelectedNodes'] = (nodes) => {
nodeSelectionHandler(nodes, true) if (state.multiSelectionActive) {
const nodeChanges = nodes.map((node) => createSelectionChange(node.id, true))
state.hooks.nodesChange.trigger(nodeChanges)
return
}
state.hooks.nodesChange.trigger(getSelectionChanges(nodeLookup.value, new Set(nodes.map((n) => n.id)), true))
state.hooks.edgesChange.trigger(getSelectionChanges(edgeLookup.value))
} }
const addSelectedEdges: Actions['addSelectedEdges'] = (edges) => { const addSelectedEdges: Actions['addSelectedEdges'] = (edges) => {
edgeSelectionHandler(edges, true) if (state.multiSelectionActive) {
const changedEdges = edges.map((edge) => createSelectionChange(edge.id, true))
state.hooks.edgesChange.trigger(changedEdges as EdgeSelectionChange[])
return
}
state.hooks.edgesChange.trigger(getSelectionChanges(edgeLookup.value, new Set(edges.map((e) => e.id))))
state.hooks.nodesChange.trigger(getSelectionChanges(nodeLookup.value, new Set(), true))
} }
const addSelectedElements: Actions['addSelectedElements'] = (elements) => { const addSelectedElements: Actions['addSelectedElements'] = (elements) => {
@@ -281,34 +237,25 @@ export function useActions(state: State, nodeLookup: ComputedRef<NodeLookup>, ed
} }
const removeSelectedNodes: Actions['removeSelectedNodes'] = (nodes) => { const removeSelectedNodes: Actions['removeSelectedNodes'] = (nodes) => {
if (!nodes.length) { const nodesToUnselect = nodes || state.nodes
return nodeSelectionHandler(nodes, false)
}
const changedNodes: NodeSelectionChange[] = [] const nodeChanges = nodesToUnselect.map((n) => {
n.selected = false
return createSelectionChange(n.id, false)
})
for (const node of nodes) { state.hooks.nodesChange.trigger(nodeChanges)
changedNodes.push(createSelectionChange(node.id, false))
}
if (changedNodes.length) {
state.hooks.nodesChange.trigger(changedNodes)
}
} }
const removeSelectedEdges: Actions['removeSelectedEdges'] = (edges) => { const removeSelectedEdges: Actions['removeSelectedEdges'] = (edges) => {
if (!edges.length) { const edgesToUnselect = edges || state.edges
return edgeSelectionHandler(edges, false)
}
const changedEdges: EdgeSelectionChange[] = [] const edgeChanges = edgesToUnselect.map((e) => {
for (const edge of edges) { e.selected = false
changedEdges.push(createSelectionChange(edge.id, false)) return createSelectionChange(e.id, false)
} })
if (changedEdges.length) { state.hooks.edgesChange.trigger(edgeChanges)
state.hooks.edgesChange.trigger(changedEdges)
}
} }
const removeSelectedElements: Actions['removeSelectedElements'] = (elements) => { const removeSelectedElements: Actions['removeSelectedElements'] = (elements) => {
+3 -3
View File
@@ -78,9 +78,9 @@ export interface FlowEvents {
paneScroll: WheelEvent | undefined paneScroll: WheelEvent | undefined
paneClick: MouseEvent paneClick: MouseEvent
paneContextMenu: MouseEvent paneContextMenu: MouseEvent
paneMouseEnter: MouseEvent paneMouseEnter: PointerEvent
paneMouseMove: MouseEvent paneMouseMove: PointerEvent
paneMouseLeave: MouseEvent paneMouseLeave: PointerEvent
edgeContextMenu: EdgeMouseEvent edgeContextMenu: EdgeMouseEvent
edgeMouseEnter: EdgeMouseEvent edgeMouseEnter: EdgeMouseEvent
edgeMouseMove: EdgeMouseEvent edgeMouseMove: EdgeMouseEvent
+10 -2
View File
@@ -258,7 +258,10 @@ export interface Actions extends Omit<ViewportHelper, 'viewportInitialized'> {
applyEdgeChanges: (changes: EdgeChange[]) => GraphEdge[] applyEdgeChanges: (changes: EdgeChange[]) => GraphEdge[]
/** applies default node change handler */ /** applies default node change handler */
applyNodeChanges: (changes: NodeChange[]) => GraphNode[] applyNodeChanges: (changes: NodeChange[]) => GraphNode[]
/** manually select elements and add to state */ /**
* @deprecated will be removed in the next major, use `addSelectedNodes`/`addSelectedEdges` instead
* manually select elements and add to state
*/
addSelectedElements: (elements: FlowElements) => void addSelectedElements: (elements: FlowElements) => void
/** manually select edges and add to state */ /** manually select edges and add to state */
addSelectedEdges: (edges: GraphEdge[]) => void addSelectedEdges: (edges: GraphEdge[]) => void
@@ -268,7 +271,10 @@ export interface Actions extends Omit<ViewportHelper, 'viewportInitialized'> {
removeSelectedEdges: (edges: GraphEdge[]) => void removeSelectedEdges: (edges: GraphEdge[]) => void
/** manually unselect nodes and remove from state */ /** manually unselect nodes and remove from state */
removeSelectedNodes: (nodes: GraphNode[]) => void removeSelectedNodes: (nodes: GraphNode[]) => void
/** unselect selected elements (if none are passed, all elements are unselected) */ /**
* @deprecated will be replaced in the next major
* unselect selected elements (if none are passed, all elements are unselected)
*/
removeSelectedElements: (elements?: Elements) => void removeSelectedElements: (elements?: Elements) => void
/** apply min zoom value to d3 */ /** apply min zoom value to d3 */
setMinZoom: (zoom: number) => void setMinZoom: (zoom: number) => void
@@ -371,6 +377,8 @@ export type ComputedGetters = {
export type VueFlowStore = { export type VueFlowStore = {
readonly id: string readonly id: string
readonly emits: FlowHooksEmit readonly emits: FlowHooksEmit
readonly nodeLookup: ComputedRef<NodeLookup>
readonly edgeLookup: ComputedRef<EdgeLookup>
/** current vue flow version you're using */ /** current vue flow version you're using */
readonly vueFlowVersion: string readonly vueFlowVersion: string
} & FlowHooksOn & } & FlowHooksOn &
+21 -20
View File
@@ -6,7 +6,6 @@ import type {
EdgeSelectionChange, EdgeSelectionChange,
ElementChange, ElementChange,
FlowElement, FlowElement,
FlowElements,
GraphEdge, GraphEdge,
GraphNode, GraphNode,
NodeAddChange, NodeAddChange,
@@ -16,7 +15,7 @@ import type {
StyleFunc, StyleFunc,
Styles, Styles,
} from '../types' } from '../types'
import { isDef, isGraphNode } from '.' import { isGraphNode } from '.'
function handleParentExpand(updateItem: GraphNode, parent: GraphNode) { function handleParentExpand(updateItem: GraphNode, parent: GraphNode) {
if (parent) { if (parent) {
@@ -255,25 +254,27 @@ export function createEdgeRemoveChange(
} }
} }
export function getSelectionChanges(elements: FlowElements, selectedIds: string[]) { export function getSelectionChanges(
return elements.reduce( items: Map<string, any>,
(res, item) => { selectedIds: Set<string> = new Set(),
let willBeSelected = selectedIds.includes(item.id) mutateItem = false,
): NodeSelectionChange[] | EdgeSelectionChange[] {
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = []
if (isDef(item.selectable) && !item.selectable) { for (const [id, item] of items) {
willBeSelected = false const willBeSelected = selectedIds.has(id)
// we don't want to set all items to selected=false on the first selection
if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) {
if (mutateItem) {
// this hack is needed for nodes. When the user dragged a node, it's selected.
// When another node gets dragged, we need to deselect the previous one,
// in order to have only one selected node at a time - the onNodesChange callback comes too late here :/
item.selected = willBeSelected
} }
changes.push(createSelectionChange(item.id, willBeSelected))
}
}
const key = isGraphNode(item) ? 'changedNodes' : 'changedEdges' return changes
if (!item.selected && willBeSelected) {
res[key].push(createSelectionChange(item.id, true))
} else if (item.selected && !willBeSelected) {
res[key].push(createSelectionChange(item.id, false))
}
return res
},
{ changedNodes: [], changedEdges: [] } as { changedNodes: NodeSelectionChange[]; changedEdges: EdgeSelectionChange[] },
)
} }
+2
View File
@@ -87,6 +87,8 @@ export class Storage {
...getters, ...getters,
...actions, ...actions,
...toRefs(reactiveState), ...toRefs(reactiveState),
nodeLookup,
edgeLookup,
emits, emits,
id, id,
vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : 'UNKNOWN', vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : 'UNKNOWN',