193 lines
5.3 KiB
TypeScript
193 lines
5.3 KiB
TypeScript
import { markRaw } from 'vue'
|
|
import { ErrorCode, VueFlowError, clampPosition, isNumber, isParentSelected } from '.'
|
|
import type {
|
|
Actions,
|
|
CoordinateExtent,
|
|
CoordinateExtentRange,
|
|
Dimensions,
|
|
GraphNode,
|
|
NodeDragItem,
|
|
State,
|
|
XYPosition,
|
|
} from '~/types'
|
|
|
|
export function hasSelector(target: Element, selector: string, node: Element): boolean {
|
|
let current = target
|
|
|
|
do {
|
|
if (current && current.matches(selector)) {
|
|
return true
|
|
} else if (current === node) {
|
|
return false
|
|
}
|
|
|
|
current = current.parentElement as Element
|
|
} while (current)
|
|
|
|
return false
|
|
}
|
|
|
|
export function getDragItems(
|
|
nodes: GraphNode[],
|
|
nodesDraggable: boolean,
|
|
mousePos: XYPosition,
|
|
findNode: Actions['findNode'],
|
|
nodeId?: string,
|
|
): NodeDragItem[] {
|
|
return nodes
|
|
.filter(
|
|
(n) =>
|
|
(n.selected || n.id === nodeId) &&
|
|
(!n.parentNode || !isParentSelected(n, findNode)) &&
|
|
(n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')),
|
|
)
|
|
.map((n) =>
|
|
markRaw({
|
|
id: n.id,
|
|
position: n.position || { x: 0, y: 0 },
|
|
distance: {
|
|
x: mousePos.x - n.computedPosition?.x || 0,
|
|
y: mousePos.y - n.computedPosition?.y || 0,
|
|
},
|
|
from: n.computedPosition,
|
|
extent: n.extent,
|
|
parentNode: n.parentNode,
|
|
dimensions: n.dimensions,
|
|
}),
|
|
)
|
|
}
|
|
|
|
export function getEventHandlerParams({
|
|
id,
|
|
dragItems,
|
|
findNode,
|
|
}: {
|
|
id?: string
|
|
dragItems: NodeDragItem[]
|
|
findNode: Actions['findNode']
|
|
}): [GraphNode, GraphNode[]] {
|
|
const extendedDragItems = dragItems.reduce((acc, dragItem) => {
|
|
const node = findNode(dragItem.id)
|
|
|
|
if (node) {
|
|
acc.push(node)
|
|
}
|
|
|
|
return acc
|
|
}, [] as GraphNode[])
|
|
|
|
return [id ? extendedDragItems.find((n) => n.id === id)! : extendedDragItems[0], extendedDragItems]
|
|
}
|
|
|
|
function getExtentPadding(padding: CoordinateExtentRange['padding']): [number, number, number, number] {
|
|
if (Array.isArray(padding)) {
|
|
switch (padding.length) {
|
|
case 1:
|
|
return [padding[0], padding[0], padding[0], padding[0]]
|
|
case 2:
|
|
return [padding[0], padding[1], padding[0], padding[1]]
|
|
case 3:
|
|
return [padding[0], padding[1], padding[2], padding[1]]
|
|
case 4:
|
|
return padding
|
|
default:
|
|
return [0, 0, 0, 0]
|
|
}
|
|
}
|
|
|
|
return [padding, padding, padding, padding]
|
|
}
|
|
|
|
function getParentExtent(
|
|
currentExtent: CoordinateExtentRange | 'parent',
|
|
node: GraphNode | NodeDragItem,
|
|
parent: GraphNode,
|
|
): CoordinateExtent | false {
|
|
const [top, right, bottom, left] = typeof currentExtent !== 'string' ? getExtentPadding(currentExtent.padding) : [0, 0, 0, 0]
|
|
|
|
if (
|
|
parent &&
|
|
isNumber(parent.computedPosition.x) &&
|
|
isNumber(parent.computedPosition.y) &&
|
|
isNumber(parent.dimensions.width) &&
|
|
isNumber(parent.dimensions.height)
|
|
) {
|
|
return [
|
|
[parent.computedPosition.x + left, parent.computedPosition.y + top],
|
|
[
|
|
parent.computedPosition.x + parent.dimensions.width - right,
|
|
parent.computedPosition.y + parent.dimensions.height - bottom,
|
|
],
|
|
]
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
export function getExtent<T extends NodeDragItem | GraphNode>(
|
|
item: T,
|
|
onError: State['hooks']['error']['trigger'],
|
|
extent?: State['nodeExtent'],
|
|
parent?: GraphNode,
|
|
) {
|
|
let currentExtent = item.extent || extent
|
|
|
|
if (currentExtent === 'parent' || (!Array.isArray(currentExtent) && currentExtent?.range === 'parent')) {
|
|
if (item.parentNode && parent && item.dimensions.width && item.dimensions.height) {
|
|
const parentExtent = getParentExtent(currentExtent, item, parent)
|
|
|
|
if (parentExtent) {
|
|
currentExtent = parentExtent
|
|
}
|
|
} else {
|
|
onError(new VueFlowError(ErrorCode.NODE_EXTENT_INVALID, item.id))
|
|
|
|
currentExtent = extent
|
|
}
|
|
} else if (Array.isArray(currentExtent)) {
|
|
const parentX = parent?.computedPosition.x || 0
|
|
const parentY = parent?.computedPosition.y || 0
|
|
|
|
currentExtent = [
|
|
[currentExtent[0][0] + parentX, currentExtent[0][1] + parentY],
|
|
[currentExtent[1][0] + parentX, currentExtent[1][1] + parentY],
|
|
]
|
|
} else if (currentExtent?.range && Array.isArray(currentExtent.range)) {
|
|
const [top, right, bottom, left] = getExtentPadding(currentExtent.padding)
|
|
|
|
const parentX = parent?.computedPosition.x || 0
|
|
const parentY = parent?.computedPosition.y || 0
|
|
|
|
currentExtent = [
|
|
[currentExtent.range[0][0] + parentX + left, currentExtent.range[0][1] + parentY + top],
|
|
[currentExtent.range[1][0] + parentX - right, currentExtent.range[1][1] + parentY - bottom],
|
|
]
|
|
}
|
|
|
|
return currentExtent as CoordinateExtent
|
|
}
|
|
|
|
function clampNodeExtent({ width, height }: Dimensions, extent: CoordinateExtent): CoordinateExtent {
|
|
return [extent[0], [extent[1][0] - (width || 0), extent[1][1] - (height || 0)]]
|
|
}
|
|
|
|
export function calcNextPosition(
|
|
node: GraphNode | NodeDragItem,
|
|
nextPosition: XYPosition,
|
|
onError: State['hooks']['error']['trigger'],
|
|
nodeExtent?: State['nodeExtent'],
|
|
parentNode?: GraphNode,
|
|
) {
|
|
const extent = clampNodeExtent(node.dimensions, getExtent(node, onError, nodeExtent, parentNode))
|
|
|
|
const clampedPos = clampPosition(nextPosition, extent)
|
|
|
|
return {
|
|
position: {
|
|
x: clampedPos.x - (parentNode?.computedPosition.x || 0),
|
|
y: clampedPos.y - (parentNode?.computedPosition.y || 0),
|
|
},
|
|
computedPosition: clampedPos,
|
|
}
|
|
}
|