refactor(react/svelte): cleanup types

This commit is contained in:
moklick
2023-02-28 18:16:51 +01:00
parent dda21e1fd2
commit 63bb2045f4
96 changed files with 899 additions and 847 deletions
@@ -1,7 +1,7 @@
import { get, type Writable } from 'svelte/store';
import { drag as d3Drag, type D3DragEvent, type SubjectPosition } from 'd3-drag';
import { select } from 'd3-selection';
import type { XYPosition, CoordinateExtent, Transform, Node as RFNode } from '@reactflow/system';
import type { XYPosition, CoordinateExtent, Transform } from '@reactflow/system';
import { getDragItems, hasSelector, calcNextPosition } from './utils';
import type { Node } from '$lib/types';
@@ -62,7 +62,7 @@ export default function drag(
dragItems = dragItems.map((n) => {
const nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
const updatedPos = calcNextPosition(n, nextPosition, get(nodes) as RFNode[]);
const updatedPos = calcNextPosition(n, nextPosition, get(nodes));
// we want to make sure that we only fire a change event when there is a changes
hasChange =
@@ -89,7 +89,7 @@ export default function drag(
const pointerPos = getPointerPosition(event);
console.log(pointerPos);
lastPos = pointerPos;
dragItems = getDragItems(get(nodes) as RFNode[], pointerPos, nodeId);
dragItems = getDragItems(get(nodes), pointerPos, nodeId);
})
.on('drag', (event: UseDragEvent) => {
const pointerPos = getPointerPosition(event);
+3 -37
View File
@@ -1,13 +1,7 @@
import type {
CoordinateExtent,
Node,
NodeDragItem,
NodeInternals,
NodeOrigin,
XYPosition
} from '@reactflow/system';
import type { CoordinateExtent, NodeDragItem, NodeOrigin, XYPosition } from '@reactflow/system';
import { clampPosition, isNumeric } from '@reactflow/utils';
import { clampPosition, isNumeric } from '../../../utils';
import type { Node } from '$lib/types';
export function isParentSelected(node: Node, nodes: Node[]): boolean {
if (!node.parentNode) {
@@ -122,31 +116,3 @@ export function calcNextPosition(
positionAbsolute
};
}
// returns two params:
// 1. the dragged node (or the first of the list, if we are dragging a node selection)
// 2. array of selected nodes (for multi selections)
export function getEventHandlerParams({
nodeId,
dragItems,
nodeInternals
}: {
nodeId?: string;
dragItems: NodeDragItem[];
nodeInternals: NodeInternals;
}): [Node, Node[]] {
const extentedDragItems: Node[] = dragItems.map((n) => {
const node = nodeInternals.get(n.id)!;
return {
...node,
position: n.position,
positionAbsolute: n.positionAbsolute
};
});
return [
nodeId ? extentedDragItems.find((n) => n.id === nodeId)! : extentedDragItems[0],
extentedDragItems
];
}
@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Writable } from 'svelte/store';
import { select } from 'd3-selection';
import { zoom as d3Zoom, zoomIdentity } from 'd3-zoom';
import type { D3ZoomEvent } from 'd3-zoom';
import { zoom as d3Zoom, zoomIdentity, type D3ZoomEvent } from 'd3-zoom';
import type { D3SelectionInstance, D3ZoomInstance, Transform } from '@reactflow/system';
const isWrappedWithClass = (event: any, className: string | undefined) =>
@@ -1,3 +1,4 @@
import { get, type Writable } from 'svelte/store';
import {
getHostForElement,
calcAutoPan,
@@ -10,7 +11,6 @@ import type {
HandleType,
Connection,
ConnectionMode,
Node,
XYPosition,
Transform
} from '@reactflow/system';
@@ -25,8 +25,7 @@ import {
type ConnectionHandle,
type ValidConnectionFunc
} from './utils';
import { get, type Writable } from 'svelte/store';
import type { ConnectionData } from '$lib/types';
import type { ConnectionData, Node } from '$lib/types';
export function handlePointerDown({
event,
@@ -9,9 +9,9 @@
type $$Props = HandleProps;
export let id: $$Props['id'] = undefined;
export let type: $$Props['type'] = 'source';
export let position: $$Props['position'] = Position.Top;
export let id: $$Props['id'] = undefined;
export let isConnectable: $$Props['isConnectable'] = true;
export let isValidConnection: $$Props['isValidConnection'] = (_: Connection) => true;
let className: string | null = null;
@@ -1,7 +1,9 @@
import { internalsSymbol, ConnectionMode, type ConnectionStatus } from '@reactflow/system';
import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '@reactflow/system';
import type { Connection, HandleType, XYPosition, NodeHandleBounds } from '@reactflow/system';
import { getEventPosition } from '@reactflow/utils';
import type { Node } from '$lib/types';
export type ConnectionHandle = {
id: string | null;
type: HandleType;
@@ -1,13 +1,13 @@
<script lang="ts">
import type { BaseEdgeProps } from '$lib/types';
import EdgeLabelRenderer from '$lib/components/EdgeLabelRenderer/index.svelte';
import type { BaseEdgeProps } from '$lib/types';
type $$Props = BaseEdgeProps;
export let path: $$Props['path'];
export let label: $$Props['label'];
export let labelX: $$Props['labelX'];
export let labelY: $$Props['labelY'];
export let path: $$Props['path'] = '';
export let label: $$Props['label'] = undefined;
export let labelX: $$Props['labelX'] = undefined;
export let labelY: $$Props['labelY'] = undefined;
export let interactionWidth: $$Props['interactionWidth'] = 20;
</script>
@@ -4,9 +4,9 @@
import { useStore } from '$lib/store';
import BezierEdge from '$lib/components/edges/StraightEdge.svelte';
import type { EdgeProps, WrapEdgeProps } from '$lib/types';
import type { EdgeProps, EdgeLayouted } from '$lib/types';
type $$Props = WrapEdgeProps;
type $$Props = EdgeLayouted;
export let id: $$Props['id'];
export let type: $$Props['type'] = 'default';
@@ -16,6 +16,8 @@
export let sourceY: $$Props['sourceY'] = 0;
export let targetX: $$Props['targetX'] = 0;
export let targetY: $$Props['targetY'] = 0;
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
export let targetHandleId: $$Props['targetHandleId'] = undefined;
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
export let targetPosition: $$Props['targetPosition'] = Position.Top;
export let animated: $$Props['animated'] = false;
@@ -30,8 +30,11 @@
<script lang="ts">
import { useStore } from '$lib/store';
import { SelectionMode, type Node, type Edge } from '@reactflow/system';
import { getConnectedEdges, getEventPosition, getNodesInside } from '@reactflow/utils';
import { SelectionMode } from '@reactflow/system';
import { getEventPosition, getNodesInside } from '@reactflow/utils';
import { getConnectedEdges} from '$lib/utils';
import type { Node, Edge } from '$lib/types';
const {
nodes,
@@ -107,8 +110,8 @@
height: Math.abs(mousePos.y - startY)
};
selectedNodes = getNodesInside(
new Map($nodes.map((node) => [node.id, node])),
selectedNodes = getNodesInside<Node>(
$nodes,
nextUserSelectRect,
$transform,
selectionMode === SelectionMode.Partial,
@@ -1,5 +1,6 @@
<script lang="ts">
import { setContext, onMount } from 'svelte';
import { ConnectionLineType } from '@reactflow/system';
import cc from 'classcat';
import { key, createStore } from '$lib/store';
@@ -23,7 +24,7 @@
export let nodeTypes: $$Props['nodeTypes'] = undefined;
export let selectionKey: $$Props['selectionKey'] = undefined;
export let deleteKey: $$Props['deleteKey'] = undefined;
export let connectionLineType: $$Props['connectionLineType'] = undefined;
export let connectionLineType: $$Props['connectionLineType'] = ConnectionLineType.Bezier;
let className: $$Props['class'] = undefined;
export { className as class };
@@ -51,27 +52,6 @@
});
});
// $: {
// const updatableProps = {
// defaultEdgeOptions,
// connectionMode,
// snapToGrid,
// snapGrid,
// nodesDraggable,
// connectOnClick,
// fitViewOnInit: fitView,
// fitViewOnInitOptions: fitViewOptions,
// };
// Object.keys(updatableProps).forEach((key) => {
// store.update((state) => ({
// ...state,
// [key]: valuesToUpdate[key],
// }));
// });
// }
$: {
store.setNodes(nodes);
}
+2
View File
@@ -3,6 +3,8 @@ export { Controls, ControlButton } from '$lib/plugins/Controls';
export { Background, BackgroundVariant } from '$lib/plugins/Background';
export { Minimap } from '$lib/plugins/Minimap';
export { default as Panel } from '$lib/container/Panel/index.svelte';
export * from '$lib/types';
export * from '$lib/utils';
export default SvelteFlow;
@@ -1,8 +1,8 @@
import { derived } from 'svelte/store';
import { getBezierPath, getSmoothStepPath, getStraightPath } from '@reactflow/edge-utils';
import { ConnectionLineType, ConnectionMode, Position } from '@reactflow/system';
import { ConnectionLineType, ConnectionMode, Position, internalsSymbol } from '@reactflow/system';
import type { SvelteFlowStoreState } from './types';
import { derived } from 'svelte/store';
const oppositePosition = {
[Position.Left]: Position.Right,
@@ -20,21 +20,21 @@ export function getConnectionPath(store: SvelteFlowStoreState) {
store.nodes,
store.transform
],
([$connection, $connectionLineType, $connectionMode, $nodes, $transform]) => {
if (!$connection.nodeId) {
([connection, connectionLineType, connectionMode, nodes, transform]) => {
if (!connection.nodeId) {
return null;
}
const fromNode = $nodes.find((n) => n.id === $connection.nodeId);
const fromNode = nodes.find((n) => n.id === connection.nodeId);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
const handleBoundsStrict = fromHandleBounds?.[$connection.handleType || 'source'] || [];
const handleBoundsStrict = fromHandleBounds?.[connection.handleType || 'source'] || [];
const handleBoundsLoose = handleBoundsStrict
? handleBoundsStrict
: fromHandleBounds?.[$connection.handleType === 'source' ? 'target' : 'source']!;
: fromHandleBounds?.[connection.handleType === 'source' ? 'target' : 'source']!;
const handleBounds =
$connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
const fromHandle = $connection.handleId
? handleBounds.find((d) => d.id === $connection.handleId)
connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
const fromHandle = connection.handleId
? handleBounds.find((d) => d.id === connection.handleId)
: handleBounds[0];
const fromHandleX = fromHandle
? fromHandle.x + fromHandle.width / 2
@@ -49,22 +49,22 @@ export function getConnectionPath(store: SvelteFlowStoreState) {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: (($connection.position?.x ?? 0) - $transform[0]) / $transform[2],
targetY: (($connection.position?.y ?? 0) - $transform[1]) / $transform[2],
targetX: ((connection.position?.x ?? 0) - transform[0]) / transform[2],
targetY: ((connection.position?.y ?? 0) - transform[1]) / transform[2],
targetPosition: toPosition
};
let path = '';
if ($connectionLineType === ConnectionLineType.Bezier) {
if (connectionLineType === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[path] = getBezierPath(pathParams);
} else if ($connectionLineType === ConnectionLineType.Step) {
} else if (connectionLineType === ConnectionLineType.Step) {
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0
});
} else if ($connectionLineType === ConnectionLineType.SmoothStep) {
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
[path] = getSmoothStepPath(pathParams);
} else {
[path] = getStraightPath(pathParams);
@@ -2,7 +2,7 @@ import { derived } from 'svelte/store';
import { Position } from '@reactflow/system';
import { getEdgePositions, getHandle, getNodeData } from '$lib/container/EdgeRenderer/utils';
import type { WrapEdgeProps } from '$lib/types';
import type { EdgeLayouted } from '$lib/types';
import type { SvelteFlowStoreState } from './types';
export function getEdgesLayouted(store: SvelteFlowStoreState) {
@@ -56,6 +56,6 @@ export function getEdgesLayouted(store: SvelteFlowStoreState) {
targetHandleId
};
})
.filter((e) => e !== null) as WrapEdgeProps[];
.filter((e) => e !== null) as EdgeLayouted[];
});
}
+5 -13
View File
@@ -1,5 +1,6 @@
import { getContext } from 'svelte';
import { get } from 'svelte/store';
import { zoomIdentity } from 'd3-zoom';
import {
type Transform,
type NodeDragItem,
@@ -7,23 +8,14 @@ import {
internalsSymbol,
type NodeOrigin,
type ViewportHelperFunctionOptions,
type Node as RFNode,
type Connection,
type XYPosition,
type CoordinateExtent
} from '@reactflow/system';
import {
fitView as fitViewUtil,
getConnectedEdges,
getD3Transition,
getDimensions,
addEdge as addEdgeUtil
} from '@reactflow/utils';
import { zoomIdentity } from 'd3-zoom';
import { fitView as fitViewUtil, getD3Transition, getDimensions } from '@reactflow/utils';
import { getHandleBounds } from '../../utils';
import { getHandleBounds, getConnectedEdges, addEdge as addEdgeUtil } from '$lib/utils';
import type { EdgeTypes, NodeTypes, Node, Edge, ConnectionData } from '$lib/types';
import { getEdgesLayouted } from './edges-layouted';
import { getConnectionPath } from './connection-path';
import { initConnectionData, initialStoreState } from './initial-store';
@@ -170,7 +162,7 @@ export function createStore({
return fitViewUtil(
{
nodes: get(store.nodes) as RFNode[],
nodes: get(store.nodes),
width: get(store.width),
height: get(store.height),
minZoom: 0.2,
@@ -227,7 +219,7 @@ export function createStore({
const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id));
if (nodesToRemove || initialHitEdges) {
const connectedEdges = getConnectedEdges(nodesToRemove as RFNode[], deletableEdges);
const connectedEdges = getConnectedEdges(nodesToRemove, deletableEdges);
const edgesToRemove = [...initialHitEdges, ...connectedEdges];
const edgeIdsToRemove = edgesToRemove.reduce<string[]>((res, edge) => {
if (!res.includes(edge.id)) {
@@ -5,15 +5,15 @@ import {
type D3SelectionInstance,
ConnectionMode,
ConnectionLineType,
type SelectionRect,
type Transform,
type NodeOrigin,
type Rect
type NodeOrigin
} from '@reactflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import InputNode from '$lib/components/nodes/InputNode.svelte';
import OutputNode from '$lib/components/nodes/OutputNode.svelte';
import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes } from '$lib/types';
import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes, EdgeLayouted } from '$lib/types';
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
@@ -29,7 +29,7 @@ export const initConnectionData = {
export const initialStoreState = {
nodes: writable<Node[]>([]),
edges: writable<Edge[]>([]),
edgesLayouted: readable<Edge[]>([]),
edgesLayouted: readable<EdgeLayouted[]>([]),
height: writable<number>(500),
width: writable<number>(500),
nodeOrigin: writable<NodeOrigin>([0.5, 0.5]),
@@ -39,7 +39,7 @@ export const initialStoreState = {
}),
id: writable<string | null>(null),
dragging: writable<boolean>(false),
selectionRect: writable<(Rect & { startX: number; startY: number }) | null>(null),
selectionRect: writable<SelectionRect | null>(null),
selectionKeyPressed: writable<boolean>(false),
multiselectionKeyPressed: writable<boolean>(false),
deleteKeyPressed: writable<boolean>(false),
@@ -6,7 +6,7 @@ import type {
NodeDragItem
} from '@reactflow/system';
import { initialStoreState } from './initial-store';
import type { initialStoreState } from './initial-store';
import type { Node, Edge, ConnectionData } from '$lib/types';
export type SvelteFlowStoreActions = {
+63 -1
View File
@@ -1,4 +1,66 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { SvelteComponentTyped } from 'svelte';
import type { EdgeProps } from '$lib/types';
import type {
BaseEdge,
BezierPathOptions,
Position,
SmoothStepPathOptions
} from '@reactflow/system';
import type { Node } from '$lib/types';
export type DefaultEdge<EdgeData = any> = BaseEdge<EdgeData> & {
label?: string;
style?: string;
class?: string;
sourceNode?: Node;
targetNode?: Node;
};
type SmoothStepEdgeType<T> = DefaultEdge<T> & {
type: 'smoothstep';
pathOptions?: SmoothStepPathOptions;
};
type BezierEdgeType<T> = DefaultEdge<T> & {
type: 'default';
pathOptions?: BezierPathOptions;
};
export type Edge<T = any> = DefaultEdge<T> | SmoothStepEdgeType<T> | BezierEdgeType<T>;
export type EdgeLayouted = Omit<Edge, 'sourceHandle' | 'targetHandle'> & {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
sourceHandleId?: string;
targetHandleId?: string;
};
export type EdgeProps = Pick<
EdgeLayouted,
| 'id'
| 'source'
| 'target'
| 'sourceX'
| 'sourceY'
| 'targetX'
| 'targetY'
| 'sourcePosition'
| 'targetPosition'
| 'animated'
| 'selected'
| 'label'
| 'interactionWidth'
>;
export type BaseEdgeProps = Pick<EdgeProps, 'interactionWidth' | 'label'> & {
path: string;
labelX?: number;
labelY?: number;
};
export type EdgeTypes = Record<string, typeof SvelteComponentTyped<EdgeProps>>;
+3 -2
View File
@@ -1,6 +1,7 @@
import type { Node, NodeTypes } from './nodes';
import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
import type { ConnectionLineType, Edge, HandleType, XYPosition } from '@reactflow/system';
import type { ConnectionLineType, HandleType, XYPosition } from '@reactflow/system';
import type { Node, NodeTypes, Edge } from '.';
export type KeyModifier = ShortcutModifierDefinition;
export type KeyDefinitionObject = { key: string; modifier?: KeyModifier };
-9
View File
@@ -1,12 +1,3 @@
export type {
Position,
XYPosition,
Edge,
BaseEdgeProps,
WrapEdgeProps,
EdgeProps
} from '@reactflow/system';
export * from './nodes';
export * from './edges';
export * from './general';
+7 -23
View File
@@ -1,33 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { SvelteComponentTyped } from 'svelte';
import type { internalsSymbol, NodeHandleBounds, Position, XYPosition } from '@reactflow/system';
import type { BaseNode } from '@reactflow/system';
// @todo: currently the helper function only like Node from '@reactflow/core'
// we need a base node type or helpes that accept Node like types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Node<NodeData = any> = {
id: string;
type: string;
data: NodeData;
position: XYPosition;
sourcePosition?: Position;
targetPosition?: Position;
positionAbsolute?: XYPosition;
width?: number;
height?: number;
selected?: boolean;
export type Node<
NodeData = any,
NodeType extends string | undefined = string | undefined
> = BaseNode<NodeData, NodeType> & {
class?: string;
style?: string;
deletable?: boolean;
// not supported yet
parentNode?: string;
// only used internally
[internalsSymbol]?: {
z?: number;
handleBounds?: NodeHandleBounds;
isParent?: boolean;
};
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+47
View File
@@ -0,0 +1,47 @@
import type { HandleElement, Position } from '@reactflow/system';
import {
isNodeBase,
isEdgeBase,
addEdgeBase,
getOutgoersBase,
getIncomersBase,
updateEdgeBase,
getConnectedEdgesBase,
getDimensions
} from '@reactflow/utils';
import type { Edge, Node } from '$lib/types';
export const isNode = isNodeBase<Node, Edge>;
export const isEdge = isEdgeBase<Node, Edge>;
export const getOutgoers = getOutgoersBase<Node, Edge>;
export const getIncomers = getIncomersBase<Node, Edge>;
export const addEdge = addEdgeBase<Edge>;
export const updateEdge = updateEdgeBase<Edge>;
export const getConnectedEdges = getConnectedEdgesBase<Node, Edge>;
export const getHandleBounds = (
selector: string,
nodeElement: HTMLDivElement,
zoom: number
): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector);
if (!handles || !handles.length) {
return null;
}
const handlesArray = Array.from(handles) as HTMLDivElement[];
const nodeBounds = nodeElement.getBoundingClientRect();
return handlesArray.map((handle): HandleElement => {
const handleBounds = handle.getBoundingClientRect();
return {
id: handle.getAttribute('data-handleid'),
position: handle.getAttribute('data-handlepos') as unknown as Position,
x: (handleBounds.left - nodeBounds.left) / zoom,
y: (handleBounds.top - nodeBounds.top) / zoom,
...getDimensions(handle)
};
});
};
-97
View File
@@ -1,97 +0,0 @@
import {
type CoordinateExtent,
type Dimensions,
type XYPosition,
type Node,
type XYZPosition,
type HandleElement,
internalsSymbol,
Position
} from '@reactflow/system';
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
export const clampPosition = (position: XYPosition = { x: 0, y: 0 }, extent: CoordinateExtent) => ({
x: clamp(position.x, extent[0][0], extent[1][0]),
y: clamp(position.y, extent[0][1], extent[1][1])
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
width: node.offsetWidth,
height: node.offsetHeight
});
type ParentNodes = Record<string, boolean>;
function calculateXYZPosition(node: Node, nodes: Node[], result: XYZPosition): XYZPosition {
if (!node.parentNode) {
return result;
}
const parentNode = nodes.find((n) => n.id === node.parentNode)!;
const parentNodePosition = parentNode.positionAbsolute!;
return calculateXYZPosition(parentNode, nodes, {
x: (result.x ?? 0) + parentNodePosition.x,
y: (result.y ?? 0) + parentNodePosition.y,
z:
(parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0)
? parentNode[internalsSymbol]?.z ?? 0
: result.z ?? 0
});
}
export function updateAbsoluteNodePositions(nodes: Node[], parentNodes?: ParentNodes) {
nodes.forEach((node) => {
if (node.parentNode) {
throw new Error(`Parent node ${node.parentNode} not found`);
}
if (node.parentNode || parentNodes?.[node.id]) {
const { x, y, z } = calculateXYZPosition(node, nodes, {
...node.position,
z: node[internalsSymbol]?.z ?? 0
});
node.positionAbsolute = {
x,
y
};
node[internalsSymbol]!.z = z;
if (parentNodes?.[node.id]) {
node[internalsSymbol]!.isParent = true;
}
}
});
}
export const getHandleBounds = (
selector: string,
nodeElement: HTMLDivElement,
zoom: number
): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector);
if (!handles || !handles.length) {
return null;
}
const handlesArray = Array.from(handles) as HTMLDivElement[];
const nodeBounds = nodeElement.getBoundingClientRect();
return handlesArray.map((handle): HandleElement => {
const handleBounds = handle.getBoundingClientRect();
return {
id: handle.getAttribute('data-handleid'),
position: handle.getAttribute('data-handlepos') as unknown as Position,
x: (handleBounds.left - nodeBounds.left) / zoom,
y: (handleBounds.top - nodeBounds.top) / zoom,
...getDimensions(handle)
};
});
};