@pixerate/editor-svelte 0.3.12 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canvas/index.d.ts +345 -0
- package/dist/canvas/index.js +672 -0
- package/dist/canvas/index.js.map +1 -0
- package/package.json +24 -1
- package/src/canvas/README.md +167 -0
- package/src/canvas/__tests__/canvas-clipboard.test.ts +154 -0
- package/src/canvas/__tests__/canvas-docking.test.ts +136 -0
- package/src/canvas/__tests__/canvas-graph.test.ts +159 -0
- package/src/canvas/__tests__/canvas-interactions.test.ts +86 -0
- package/src/canvas/__tests__/canvas-layout.test.ts +75 -0
- package/src/canvas/__tests__/canvas-shortcuts.test.ts +59 -0
- package/src/canvas/createCanvasClipboard.svelte.ts +219 -0
- package/src/canvas/createCanvasDocking.svelte.ts +145 -0
- package/src/canvas/createCanvasGraph.svelte.ts +330 -0
- package/src/canvas/createCanvasInteractions.svelte.ts +112 -0
- package/src/canvas/createCanvasLayout.ts +120 -0
- package/src/canvas/createCanvasShortcuts.ts +99 -0
- package/src/canvas/index.d.ts +345 -0
- package/src/canvas/index.ts +46 -0
- package/src/canvas/types.ts +96 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/canvas/createCanvasGraph.svelte.ts","../../src/canvas/createCanvasLayout.ts","../../src/canvas/createCanvasClipboard.svelte.ts","../../src/canvas/createCanvasDocking.svelte.ts","../../src/canvas/createCanvasShortcuts.ts","../../src/canvas/createCanvasInteractions.svelte.ts"],"sourcesContent":["import { cubicOut } from \"svelte/easing\";\nexport function calculateGraphDifferences(currentState, baselineState) {\n const currentNodes = currentState.nodes ?? [];\n const currentEdges = currentState.edges ?? [];\n const baselineNodes = baselineState.nodes ?? [];\n const baselineEdges = baselineState.edges ?? [];\n const baselineNodeMap = new Map(baselineNodes.map((n) => [n.id, n]));\n const baselineEdgeMap = new Map(baselineEdges.map((e) => [e.id, e]));\n const currentNodeMap = new Map(currentNodes.map((n) => [n.id, n]));\n const currentEdgeMap = new Map(currentEdges.map((e) => [e.id, e]));\n const removedNodes = baselineNodes.filter((oldNode) => !currentNodeMap.has(oldNode.id));\n const removedEdges = baselineEdges.filter((oldEdge) => !currentEdgeMap.has(oldEdge.id));\n const changedNodes = currentNodes.filter((newNode) => {\n const oldNode = baselineNodeMap.get(newNode.id);\n if (!oldNode) return true;\n return JSON.stringify(newNode) !== JSON.stringify(oldNode);\n });\n const changedEdges = currentEdges.filter((newEdge) => {\n const oldEdge = baselineEdgeMap.get(newEdge.id);\n if (!oldEdge) return true;\n return JSON.stringify(newEdge) !== JSON.stringify(oldEdge);\n });\n return { changedNodes, changedEdges, removedNodes, removedEdges };\n}\nexport function replaceNodeInGraph(nodes, edges, oldNode, newNode, keepEdges = false) {\n const updatedNodes = nodes.map((n) => n.id === oldNode.id ? newNode : n);\n if (!keepEdges) {\n return { nodes: updatedNodes, edges };\n }\n const updatedEdges = edges.map((edge) => {\n if (edge.source === oldNode.id || edge.target === oldNode.id) {\n return {\n ...edge,\n id: crypto.randomUUID(),\n source: edge.source === oldNode.id ? newNode.id : edge.source,\n target: edge.target === oldNode.id ? newNode.id : edge.target\n };\n }\n return edge;\n });\n return {\n nodes: updatedNodes,\n edges: updatedEdges\n };\n}\nexport function createCanvasGraph(options = {}) {\n let nodes = $state.raw(options.initialNodes ?? []);\n let edges = $state.raw(options.initialEdges ?? []);\n function setNodes(newNodes, triggerCallback = true) {\n nodes = newNodes;\n if (triggerCallback) {\n options.onNodesChange?.(nodes);\n }\n }\n function setEdges(newEdges, triggerCallback = true) {\n edges = newEdges;\n if (triggerCallback) {\n options.onEdgesChange?.(edges);\n }\n }\n function animateNodePosition(node, targetPosition, duration = 400, easing = cubicOut) {\n const delay = node.data?.animationDelay ?? 0;\n const startPosition = { ...node.position };\n const startTime = Date.now() + delay;\n function step() {\n const currentTime = Date.now();\n if (currentTime < startTime) {\n if (typeof requestAnimationFrame !== \"undefined\") {\n requestAnimationFrame(step);\n }\n return;\n }\n const progress = Math.min((currentTime - startTime) / duration, 1);\n const eased = easing(progress);\n const currentPos = {\n x: startPosition.x + (targetPosition.x - startPosition.x) * eased,\n y: startPosition.y + (targetPosition.y - startPosition.y) * eased\n };\n const targetNode = nodes.find((n) => n.id === node.id);\n if (targetNode) {\n targetNode.position = currentPos;\n }\n if (progress < 1) {\n if (typeof requestAnimationFrame !== \"undefined\") {\n requestAnimationFrame(step);\n }\n } else {\n setNodes([...nodes]);\n }\n }\n step();\n }\n function addNodeOrNodes(nodeOrNodesToAdd, animateFromPosition, save = true) {\n const toAdd = Array.isArray(nodeOrNodesToAdd) ? nodeOrNodesToAdd : [nodeOrNodesToAdd];\n const existingMap = new Map(nodes.map((n) => [n.id, n]));\n toAdd.forEach((node) => {\n if (!existingMap.has(node.id)) {\n existingMap.set(node.id, node);\n }\n });\n setNodes(Array.from(existingMap.values()));\n if (animateFromPosition) {\n toAdd.forEach((node) => {\n const target = { ...node.position };\n node.position = { ...animateFromPosition };\n animateNodePosition(node, target);\n });\n }\n if (save) options.onSave?.();\n }\n function addEdge(edgeToAdd, save = true) {\n setEdges([...edges, edgeToAdd]);\n if (save) options.onSave?.();\n }\n function addEdges(edgesToAdd, save = true) {\n setEdges([...edges, ...edgesToAdd]);\n if (save) options.onSave?.();\n }\n function removeNodes(nodesToRemove, save = true) {\n const idsToRemove = new Set(nodesToRemove.map((n) => n.id));\n setNodes(nodes.filter((n) => !idsToRemove.has(n.id)));\n if (save) options.onSave?.();\n }\n function removeEdges(edgesToRemove, save = true) {\n const idsToRemove = new Set(edgesToRemove.map((e) => e.id));\n setEdges(edges.filter((e) => !idsToRemove.has(e.id)));\n if (save) options.onSave?.();\n }\n function replaceNode(oldNode, newNode, save = true, keepEdges = false) {\n const result = replaceNodeInGraph(nodes, edges, oldNode, newNode, keepEdges);\n setNodes(result.nodes);\n if (result.edges !== edges) {\n setEdges(result.edges);\n }\n if (save) options.onSave?.();\n }\n function addConnectedNodes(params) {\n const {\n nodesToAdd,\n inboundNodes = [],\n outboundNodes = [],\n animatedEdges = false,\n animateFromPosition,\n inboundEdgeData = {},\n outboundEdgeData = {},\n getEdgeLabel\n } = params;\n const defaultCreateEdge = (sourceId, targetId, opt) => {\n if (options.createEdge) {\n return options.createEdge(sourceId, targetId, opt);\n }\n return {\n id: crypto.randomUUID(),\n type: \"default\",\n source: sourceId,\n target: targetId,\n label: opt?.label ?? \"\",\n animated: opt?.animated ?? false,\n data: opt?.data ?? {}\n };\n };\n nodesToAdd.forEach((addedNode) => {\n addNodeOrNodes(addedNode, animateFromPosition, false);\n inboundNodes.forEach((sourceNode) => {\n const label = getEdgeLabel ? getEdgeLabel(sourceNode, addedNode) : \"\";\n addEdge(\n defaultCreateEdge(sourceNode.id, addedNode.id, {\n label,\n animated: animatedEdges,\n data: inboundEdgeData\n }),\n false\n );\n });\n outboundNodes.forEach((targetNode) => {\n const label = getEdgeLabel ? getEdgeLabel(addedNode, targetNode) : \"\";\n addEdge(\n defaultCreateEdge(addedNode.id, targetNode.id, {\n label,\n animated: animatedEdges,\n data: outboundEdgeData\n }),\n false\n );\n });\n });\n options.onSave?.();\n }\n function getDifferences(baseline) {\n return calculateGraphDifferences({ nodes, edges }, baseline);\n }\n return {\n get nodes() {\n return nodes;\n },\n set nodes(val) {\n setNodes(val);\n },\n get edges() {\n return edges;\n },\n set edges(val) {\n setEdges(val);\n },\n setNodes,\n setEdges,\n addNodeOrNodes,\n addEdge,\n addEdges,\n removeNodes,\n removeEdges,\n replaceNode,\n addConnectedNodes,\n animateNodePosition,\n getDifferences\n };\n}\n","import dagre from '@dagrejs/dagre';\nimport { Position, type XYPosition, type Rect } from '@xyflow/svelte';\nimport type { CanvasNode, CanvasEdge, CanvasLayoutOptions } from './types.js';\n\n/**\n * Calculates upper-left flow coordinate for an element to be centered in the viewport.\n */\nexport function getCenteredNodePosition(\n\twidth = 0,\n\theight = 0,\n\tscreenToFlowPosition: (position: XYPosition) => XYPosition,\n\tscreenDimensions?: { width: number; height: number }\n): XYPosition {\n\tconst rect = screenDimensions ?? (typeof document !== 'undefined'\n\t\t? { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }\n\t\t: { width: 1920, height: 1080 });\n\n\tconst flowPos = screenToFlowPosition({ x: rect.width / 2, y: rect.height / 2 });\n\treturn {\n\t\tx: flowPos.x - width / 2,\n\t\ty: flowPos.y - height / 2\n\t};\n}\n\n/**\n * Updates positions of nodes so their collective bounding box is centered in the current viewport.\n */\nexport function centerNodes<TNode extends CanvasNode = CanvasNode>(\n\tnodes: TNode[],\n\tscreenToFlowPosition: (position: XYPosition) => XYPosition,\n\tgetNodesBounds: (nodes: TNode[]) => Rect,\n\tscreenDimensions?: { width: number; height: number }\n): TNode[] {\n\tif (nodes.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst bounds = getNodesBounds(nodes);\n\tconst boundingBoxOrigin = { x: bounds.x, y: bounds.y };\n\n\tconst centeredPosition = getCenteredNodePosition(\n\t\tbounds.width,\n\t\tbounds.height,\n\t\tscreenToFlowPosition,\n\t\tscreenDimensions\n\t);\n\n\tconst deltaX = centeredPosition.x - boundingBoxOrigin.x;\n\tconst deltaY = centeredPosition.y - boundingBoxOrigin.y;\n\n\treturn nodes.map((node) => ({\n\t\t...node,\n\t\tposition: {\n\t\t\tx: node.position.x + deltaX,\n\t\t\ty: node.position.y + deltaY\n\t\t}\n\t}));\n}\n\n/**\n * Automatically calculates directed hierarchical layout of nodes and edges using Dagre.\n */\nexport function getLayoutedNodes<\n\tTNode extends CanvasNode = CanvasNode,\n\tTEdge extends CanvasEdge = CanvasEdge\n>(\n\tnodes: TNode[],\n\tedges: TEdge[],\n\tgetNodesBounds: (nodes: TNode[]) => Rect,\n\toptions: CanvasLayoutOptions = {}\n): TNode[] {\n\tif (nodes.length === 0) return [];\n\n\tconst {\n\t\tdirection = 'LR',\n\t\tranksep = 100,\n\t\tnodesep = 25,\n\t\tdefaultNodeWidth = 200,\n\t\tdefaultNodeHeight = 200\n\t} = options;\n\n\tconst isHorizontal = direction === 'LR' || direction === 'RL';\n\tconst bounds = getNodesBounds(nodes);\n\n\tconst dagreGraph = new dagre.graphlib.Graph();\n\tdagreGraph.setDefaultEdgeLabel(() => ({}));\n\tdagreGraph.setGraph({ rankdir: direction, ranksep, nodesep });\n\n\tconst nodeIds = new Set(nodes.map((node) => node.id));\n\n\tnodes.forEach((node) => {\n\t\tconst nodeWidth = node.measured?.width ?? (node as any).width ?? defaultNodeWidth;\n\t\tconst nodeHeight = node.measured?.height ?? (node as any).height ?? defaultNodeHeight;\n\t\tdagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });\n\t});\n\n\tedges.forEach((edge) => {\n\t\tif (nodeIds.has(edge.source) && nodeIds.has(edge.target)) {\n\t\t\tdagreGraph.setEdge(edge.source, edge.target);\n\t\t}\n\t});\n\n\tdagre.layout(dagreGraph);\n\n\treturn nodes.map((node) => {\n\t\tconst nodeWithPosition = dagreGraph.node(node.id);\n\t\tconst nodeWidth = node.measured?.width ?? (node as any).width ?? defaultNodeWidth;\n\t\tconst nodeHeight = node.measured?.height ?? (node as any).height ?? defaultNodeHeight;\n\n\t\treturn {\n\t\t\t...node,\n\t\t\ttargetPosition: isHorizontal ? Position.Left : Position.Top,\n\t\t\tsourcePosition: isHorizontal ? Position.Right : Position.Bottom,\n\t\t\tposition: {\n\t\t\t\tx: bounds.x + (nodeWithPosition?.x ?? 0) - nodeWidth / 2,\n\t\t\t\ty: bounds.y + (nodeWithPosition?.y ?? 0) - nodeHeight / 2\n\t\t\t}\n\t\t};\n\t});\n}\n","import { tick } from \"svelte\";\nexport function isEventFromTextInput(event) {\n const target = event.target;\n if (!target) return false;\n if (target.tagName === \"INPUT\" || target.tagName === \"TEXTAREA\" || target.tagName === \"SELECT\" || target.isContentEditable || target.closest?.('[contenteditable=\"true\"]') || target.closest?.(\".ProseMirror\")) {\n return true;\n }\n return false;\n}\nexport function isValidCanvasNode(obj) {\n return typeof obj === \"object\" && obj !== null && \"id\" in obj && typeof obj.id === \"string\" && \"position\" in obj && typeof obj.position === \"object\" && obj.position !== null && \"x\" in obj.position && \"y\" in obj.position;\n}\nexport function isValidUrl(url) {\n try {\n const parsed = new URL(url);\n return parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n } catch {\n return false;\n }\n}\nexport function createCanvasClipboard(options = {}) {\n function copySelectedNodes(selectedNodes, connectedEdges, event) {\n if (event && isEventFromTextInput(event)) {\n return false;\n }\n if (selectedNodes.length === 0) return false;\n const payload = {\n version: 1,\n nodes: selectedNodes,\n edges: connectedEdges\n };\n const serialized = JSON.stringify(payload);\n if (event?.clipboardData) {\n event.clipboardData.setData(\"application/json\", serialized);\n event.clipboardData.setData(\"text/plain\", serialized);\n event.preventDefault();\n } else if (typeof navigator !== \"undefined\" && navigator.clipboard) {\n navigator.clipboard.writeText(serialized).catch(() => {\n });\n }\n return true;\n }\n function cutSelectedNodes(selectedNodes, connectedEdges, onRemove, event) {\n if (event && isEventFromTextInput(event)) {\n return false;\n }\n const copied = copySelectedNodes(selectedNodes, connectedEdges, event);\n if (copied && onRemove) {\n onRemove(selectedNodes);\n }\n return copied;\n }\n async function handlePasteEvent(event) {\n if (isEventFromTextInput(event)) {\n return false;\n }\n const clipboardData = event.clipboardData;\n if (!clipboardData) return false;\n const files = clipboardData.files;\n if (files && files.length > 0) {\n const imageFile = Array.from(files).find((f) => f.type.startsWith(\"image/\"));\n if (imageFile && options.onPasteImage) {\n event.preventDefault();\n await options.onPasteImage(imageFile);\n return true;\n }\n }\n const rawJson = clipboardData.getData(\"application/json\") || clipboardData.getData(\"text/plain\");\n if (rawJson) {\n try {\n const parsed = JSON.parse(rawJson);\n let rawNodes;\n let rawEdges;\n if (Array.isArray(parsed) && parsed.every(isValidCanvasNode)) {\n rawNodes = parsed;\n } else if (typeof parsed === \"object\" && parsed !== null && Array.isArray(parsed.nodes) && parsed.nodes.every(isValidCanvasNode)) {\n rawNodes = parsed.nodes;\n rawEdges = parsed.edges;\n }\n if (rawNodes && rawNodes.length > 0) {\n event.preventDefault();\n const idMap = /* @__PURE__ */ new Map();\n const newNodes = rawNodes.map((node) => {\n const newId = crypto.randomUUID();\n idMap.set(node.id, newId);\n return {\n ...node,\n id: newId,\n selected: true,\n position: {\n x: node.position.x + 40,\n y: node.position.y + 40\n }\n };\n });\n const newEdges = rawEdges?.map((edge) => ({\n ...edge,\n id: crypto.randomUUID(),\n source: idMap.get(edge.source) ?? edge.source,\n target: idMap.get(edge.target) ?? edge.target\n }));\n if (options.onPasteNodes) {\n await options.onPasteNodes(newNodes, newEdges);\n await tick();\n }\n return true;\n }\n } catch {\n }\n }\n const text = clipboardData.getData(\"text/plain\")?.trim();\n if (text) {\n if (isValidUrl(text) && options.onPasteUrl) {\n event.preventDefault();\n options.onPasteUrl(text);\n return true;\n } else if (options.onPasteText) {\n event.preventDefault();\n options.onPasteText(text);\n return true;\n }\n }\n return false;\n }\n return {\n copySelectedNodes,\n cutSelectedNodes,\n handlePasteEvent\n };\n}\n","export function createCanvasDocking(options = {}) {\n let draggedNode = $state.raw(null);\n let draggedInitialPosition = $state.raw(null);\n let connectingHandle = $state.raw(\"\");\n let connectingNode = $state.raw(null);\n let currentHoverTargets = $state.raw(/* @__PURE__ */ new Set());\n const isDockTarget = options.isDockTarget ?? ((target) => target.type === \"boardNode\");\n function handleNodeDragStart(params) {\n draggedNode = params.targetNode;\n draggedInitialPosition = params.targetNode?.position ? { ...params.targetNode.position } : null;\n currentHoverTargets = /* @__PURE__ */ new Set();\n }\n function handleNodeDrag(params, getIntersectingNodes) {\n draggedNode = params.targetNode;\n if (!draggedNode) return;\n const intersecting = getIntersectingNodes(draggedNode);\n const currentDockTargets = intersecting.filter(isDockTarget);\n const newHoverSet = new Set(currentDockTargets.map((t) => t.id));\n currentHoverTargets.forEach((targetId) => {\n if (!newHoverSet.has(targetId)) {\n const leftNode = intersecting.find((n) => n.id === targetId) ?? { id: targetId };\n options.onDockLeave?.(leftNode, draggedNode);\n }\n });\n currentDockTargets.forEach((target) => {\n if (!currentHoverTargets.has(target.id)) {\n options.onDockHover?.(target, draggedNode);\n }\n });\n currentHoverTargets = newHoverSet;\n }\n function handleNodeDragStop(params, getIntersectingNodes, updateNodePosition) {\n draggedNode = params.targetNode;\n if (draggedNode) {\n const intersecting = getIntersectingNodes(draggedNode);\n const dockTarget = intersecting.find(isDockTarget);\n if (dockTarget) {\n options.onDockDrop?.(dockTarget, draggedNode);\n if (options.restorePositionOnDock !== false && draggedInitialPosition && updateNodePosition) {\n updateNodePosition(draggedNode.id, draggedInitialPosition);\n }\n }\n }\n draggedNode = null;\n draggedInitialPosition = null;\n currentHoverTargets = /* @__PURE__ */ new Set();\n options.onSave?.();\n }\n function handleConnectStart(params, getNode) {\n connectingHandle = params.handleType ?? \"\";\n connectingNode = params.nodeId ? getNode(params.nodeId) ?? null : null;\n }\n function handleConnectEnd() {\n connectingHandle = \"\";\n connectingNode = null;\n }\n function reset() {\n draggedNode = null;\n draggedInitialPosition = null;\n connectingHandle = \"\";\n connectingNode = null;\n currentHoverTargets = /* @__PURE__ */ new Set();\n }\n return {\n get draggedNode() {\n return draggedNode;\n },\n get draggedInitialPosition() {\n return draggedInitialPosition;\n },\n get connectingHandle() {\n return connectingHandle;\n },\n get connectingNode() {\n return connectingNode;\n },\n get isDragging() {\n return draggedNode !== null;\n },\n handleNodeDragStart,\n handleNodeDrag,\n handleNodeDragStop,\n handleConnectStart,\n handleConnectEnd,\n reset\n };\n}\n","import { isEventFromTextInput } from './createCanvasClipboard.svelte.js';\nimport type { CanvasNode, CanvasShortcutsOptions } from './types.js';\n\n/**\n * Attaches keyboard shortcut handlers for canvas interaction.\n * Automatically ignores shortcuts when the user is typing into text inputs, textareas, or rich editors.\n *\n * @returns A cleanup function to remove event listeners.\n */\nexport function createCanvasShortcuts<TNode extends CanvasNode = CanvasNode>(\n\toptions: CanvasShortcutsOptions<TNode>\n): () => void {\n\tconst target = options.targetElement ?? (typeof window !== 'undefined' ? window : null);\n\tif (!target) return () => {};\n\n\tfunction handleKeyDown(event: Event) {\n\t\tconst keyboardEvent = event as KeyboardEvent;\n\t\tif (isEventFromTextInput(keyboardEvent)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst isMod = keyboardEvent.metaKey || keyboardEvent.ctrlKey;\n\t\tconst isShift = keyboardEvent.shiftKey;\n\t\tconst key = keyboardEvent.key;\n\n\t\tconst selectedNodes = options.getSelectedNodes ? options.getSelectedNodes() : [];\n\n\t\tif (key === 'Backspace' || key === 'Delete') {\n\t\t\tif (options.onDelete && selectedNodes.length > 0) {\n\t\t\t\tkeyboardEvent.preventDefault();\n\t\t\t\toptions.onDelete(selectedNodes);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (isMod) {\n\t\t\tconst lowerKey = key.toLowerCase();\n\n\t\t\tif (lowerKey === 'c' && !isShift) {\n\t\t\t\tif (options.onCopy) {\n\t\t\t\t\t// We let the native copy event fire or call onCopy\n\t\t\t\t\toptions.onCopy();\n\t\t\t\t}\n\t\t\t} else if (lowerKey === 'x' && !isShift) {\n\t\t\t\tif (options.onCut) {\n\t\t\t\t\toptions.onCut();\n\t\t\t\t}\n\t\t\t} else if (lowerKey === 'd' && !isShift) {\n\t\t\t\tif (options.onDuplicate && selectedNodes.length > 0) {\n\t\t\t\t\tkeyboardEvent.preventDefault();\n\t\t\t\t\toptions.onDuplicate(selectedNodes);\n\t\t\t\t}\n\t\t\t} else if (lowerKey === 'a' && !isShift) {\n\t\t\t\tif (options.onSelectAll) {\n\t\t\t\t\tkeyboardEvent.preventDefault();\n\t\t\t\t\toptions.onSelectAll();\n\t\t\t\t}\n\t\t\t} else if (lowerKey === 'g') {\n\t\t\t\tif (isShift) {\n\t\t\t\t\tif (options.onUngroup && selectedNodes.length > 0) {\n\t\t\t\t\t\tkeyboardEvent.preventDefault();\n\t\t\t\t\t\toptions.onUngroup(selectedNodes);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (options.onGroup && selectedNodes.length > 0) {\n\t\t\t\t\t\tkeyboardEvent.preventDefault();\n\t\t\t\t\t\toptions.onGroup(selectedNodes);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (key === '=' || key === '+') {\n\t\t\t\tif (options.onZoomIn) {\n\t\t\t\t\tkeyboardEvent.preventDefault();\n\t\t\t\t\toptions.onZoomIn();\n\t\t\t\t}\n\t\t\t} else if (key === '-') {\n\t\t\t\tif (options.onZoomOut) {\n\t\t\t\t\tkeyboardEvent.preventDefault();\n\t\t\t\t\toptions.onZoomOut();\n\t\t\t\t}\n\t\t\t} else if (key === '0') {\n\t\t\t\tif (options.onFitView) {\n\t\t\t\t\tkeyboardEvent.preventDefault();\n\t\t\t\t\toptions.onFitView();\n\t\t\t\t}\n\t\t\t} else if (lowerKey === 'f') {\n\t\t\t\tif (options.onSearch) {\n\t\t\t\t\tkeyboardEvent.preventDefault();\n\t\t\t\t\toptions.onSearch();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ttarget.addEventListener('keydown', handleKeyDown as EventListener);\n\n\treturn () => {\n\t\ttarget.removeEventListener('keydown', handleKeyDown as EventListener);\n\t};\n}\n","import { isEventFromTextInput } from \"./createCanvasClipboard.svelte.js\";\nexport function createCanvasInteractions(options = {}) {\n let isSpacebarPanning = $state.raw(false);\n let trackpadDetected = $state.raw(false);\n let toggledPanMode = false;\n function handleKeyDown(event) {\n if (event.key === \" \" || event.code === \"Space\") {\n if (isSpacebarPanning || isEventFromTextInput(event)) return;\n isSpacebarPanning = true;\n toggledPanMode = true;\n options.onPanModeChange?.(true);\n if (typeof document !== \"undefined\") {\n document.body.classList.add(\"cursor-grab\");\n }\n }\n }\n function handleKeyUp(event) {\n if (event.key === \" \" || event.code === \"Space\") {\n if (!isSpacebarPanning && !toggledPanMode) return;\n isSpacebarPanning = false;\n toggledPanMode = false;\n options.onPanModeChange?.(false);\n if (typeof document !== \"undefined\") {\n document.body.classList.remove(\"cursor-grab\");\n }\n }\n }\n function handleWheel(event) {\n if (event.ctrlKey && !trackpadDetected) {\n trackpadDetected = true;\n options.onTrackpadDetected?.();\n }\n }\n function attachListeners(target) {\n const el = target ?? options.targetElement ?? (typeof window !== \"undefined\" ? window : null);\n if (!el) return () => {\n };\n el.addEventListener(\"keydown\", handleKeyDown);\n el.addEventListener(\"keyup\", handleKeyUp);\n el.addEventListener(\"wheel\", handleWheel, { passive: true });\n return () => {\n el.removeEventListener(\"keydown\", handleKeyDown);\n el.removeEventListener(\"keyup\", handleKeyUp);\n el.removeEventListener(\"wheel\", handleWheel);\n if (typeof document !== \"undefined\") {\n document.body.classList.remove(\"cursor-grab\");\n }\n };\n }\n return {\n get isSpacebarPanning() {\n return isSpacebarPanning;\n },\n get trackpadDetected() {\n return trackpadDetected;\n },\n attachListeners,\n destroy: attachListeners()\n };\n}\nexport const defaultSvelteFlowPreset = {\n snapGrid: [25, 25],\n panOnDrag: [0, 1, 2, 3, 4],\n panOnScroll: false,\n selectionOnDrag: false,\n colorMode: \"system\",\n multiSelectionKey: [\"Shift\", \"Meta\"]\n};\nexport const miroCompatiblePreset = {\n ...defaultSvelteFlowPreset,\n panOnDrag: [1, 2],\n selectionOnDrag: true\n};\nexport function restorePanelPointerEvents(panelSelector = \".svelte-flow__panel\") {\n if (typeof document === \"undefined\") return;\n const panels = document.querySelectorAll(panelSelector);\n panels.forEach((panel) => {\n panel.style.pointerEvents = \"auto\";\n });\n}\n"],"mappings":";;AAAA,SAAS,gBAAgB;AAClB,SAAS,0BAA0B,cAAc,eAAe;AACrE,QAAM,eAAe,aAAa,SAAK,CAAA;AACvC,QAAM,eAAe,aAAa,SAAK,CAAA;AACvC,QAAM,gBAAgB,cAAc,SAAK,CAAA;AACzC,QAAM,gBAAgB,cAAc,SAAK,CAAA;AACzC,QAAM,kBAAkB,IAAI,IAAI,cAAc,IAAG,CAAE,MAAC,CAAM,EAAE,IAAI,CAAC,CAAA,CAAA;AACjE,QAAM,kBAAkB,IAAI,IAAI,cAAc,IAAG,CAAE,MAAC,CAAM,EAAE,IAAI,CAAC,CAAA,CAAA;AACjE,QAAM,iBAAiB,IAAI,IAAI,aAAa,IAAG,CAAE,MAAC,CAAM,EAAE,IAAI,CAAC,CAAA,CAAA;AAC/D,QAAM,iBAAiB,IAAI,IAAI,aAAa,IAAG,CAAE,MAAC,CAAM,EAAE,IAAI,CAAC,CAAA,CAAA;AAC/D,QAAM,eAAe,cAAc,OAAM,CAAE,YAAO,CAAM,eAAe,IAAI,QAAQ,EAAE,CAAA;AACrF,QAAM,eAAe,cAAc,OAAM,CAAE,YAAO,CAAM,eAAe,IAAI,QAAQ,EAAE,CAAA;AACrF,QAAM,eAAe,aAAa,OAAM,CAAE,YAAY;AACpD,UAAM,UAAU,gBAAgB,IAAI,QAAQ,EAAE;AAC9C,QAAE,CAAG,QAAS,QAAO;AACrB,WAAO,KAAK,UAAU,OAAO,MAAM,KAAK,UAAU,OAAO;EAC3D,CAAC;AACD,QAAM,eAAe,aAAa,OAAM,CAAE,YAAY;AACpD,UAAM,UAAU,gBAAgB,IAAI,QAAQ,EAAE;AAC9C,QAAE,CAAG,QAAS,QAAO;AACrB,WAAO,KAAK,UAAU,OAAO,MAAM,KAAK,UAAU,OAAO;EAC3D,CAAC;AACD,SAAM,EAAG,cAAc,cAAc,cAAc,aAAY;AACjE;AACO,SAAS,mBAAmB,OAAO,OAAO,SAAS,SAAS,YAAY,OAAO;AACpF,QAAM,eAAe,MAAM,IAAG,CAAE,MAAM,EAAE,OAAO,QAAQ,KAAK,UAAU,CAAC;AACvE,MAAE,CAAG,WAAW;AACd,WAAM,EAAG,OAAO,cAAc,MAAK;EACrC;AACA,QAAM,eAAe,MAAM,IAAG,CAAE,SAAS;AACvC,QAAI,KAAK,WAAW,QAAQ,MAAM,KAAK,WAAW,QAAQ,IAAI;AAC5D,aAAM;WACD;QACH,IAAI,OAAO,WAAU;QACrB,QAAQ,KAAK,WAAW,QAAQ,KAAK,QAAQ,KAAK,KAAK;QACvD,QAAQ,KAAK,WAAW,QAAQ,KAAK,QAAQ,KAAK,KAAK;;IAE3D;AACA,WAAO;EACT,CAAC;AACD,SAAM,EACJ,OAAO,cACP,OAAO,aAAY;AAEvB;AACO,SAAS,kBAAkB,UAAO,CAAA,GAAO;AAC9C,MAAI,QAAQ,QAAW,QAAQ,gBAAY,CAAA,CAAA;AAC3C,MAAI,QAAQ,QAAW,QAAQ,gBAAY,CAAA,CAAA;AAC3C,WAAS,SAAS,UAAU,kBAAkB,MAAM;UAClD,OAAQ,QAAQ;AAChB,QAAI,iBAAiB;AACnB,cAAQ,gBAAa,MAAG,KAAK,CAAA;IAC/B;EACF;AACA,WAAS,SAAS,UAAU,kBAAkB,MAAM;UAClD,OAAQ,QAAQ;AAChB,QAAI,iBAAiB;AACnB,cAAQ,gBAAa,MAAG,KAAK,CAAA;IAC/B;EACF;AACA,WAAS,oBAAoB,MAAM,gBAAgB,WAAW,KAAK,SAAS,UAAU;AACpF,UAAM,QAAQ,KAAK,MAAM,kBAAkB;AAC3C,UAAM,gBAAa,EAAA,GAAQ,KAAK,SAAQ;AACxC,UAAM,YAAY,KAAK,IAAG,IAAK;AAC/B,aAAS,OAAO;AACd,YAAM,cAAc,KAAK,IAAG;AAC5B,UAAI,cAAc,WAAW;AAC3B,YAAE,OAAS,0BAA0B,aAAa;AAChD,gCAAsB,IAAI;QAC5B;AACA;MACF;AACA,YAAM,WAAW,KAAK,KAAK,cAAc,aAAa,UAAU,CAAC;AACjE,YAAM,QAAQ,OAAO,QAAQ;AAC7B,YAAM,aAAU;QACd,GAAG,cAAc,KAAK,eAAe,IAAI,cAAc,KAAK;QAC5D,GAAG,cAAc,KAAK,eAAe,IAAI,cAAc,KAAK;;AAE9D,YAAM,aAAU,MAAG,KAAK,EAAC,KAAI,CAAE,MAAM,EAAE,OAAO,KAAK,EAAE;AACrD,UAAI,YAAY;AACd,mBAAW,WAAW;MACxB;AACA,UAAI,WAAW,GAAG;AAChB,YAAE,OAAS,0BAA0B,aAAa;AAChD,gCAAsB,IAAI;QAC5B;MACF,OAAO;AACL,iBAAQ,CAAA,GAAA,MAAK,KAAK,CAAA,CAAA;MACpB;IACF;AACA,SAAI;EACN;AACA,WAAS,eAAe,kBAAkB,qBAAqB,OAAO,MAAM;AAC1E,UAAM,QAAQ,MAAM,QAAQ,gBAAgB,IAAI,mBAAgB,CAAI,gBAAgB;AACpF,UAAM,cAAc,IAAI,IAAG,MAAC,KAAK,EAAC,IAAG,CAAE,MAAC,CAAM,EAAE,IAAI,CAAC,CAAA,CAAA;AACrD,UAAM,QAAO,CAAE,SAAS;AACtB,UAAE,CAAG,YAAY,IAAI,KAAK,EAAE,GAAG;AAC7B,oBAAY,IAAI,KAAK,IAAI,IAAI;MAC/B;IACF,CAAC;AACD,aAAS,MAAM,KAAK,YAAY,OAAM,CAAA,CAAA;AACtC,QAAI,qBAAqB;AACvB,YAAM,QAAO,CAAE,SAAS;AACtB,cAAM,SAAM,EAAA,GAAQ,KAAK,SAAQ;AACjC,aAAK,WAAQ,EAAA,GAAQ,oBAAmB;AACxC,4BAAoB,MAAM,MAAM;MAClC,CAAC;IACH;AACA,QAAI,KAAM,SAAQ,SAAM;EAC1B;AACA,WAAS,QAAQ,WAAW,OAAO,MAAM;AACvC,aAAQ,CAAA,GAAA,MAAK,KAAK,GAAE,SAAS,CAAA;AAC7B,QAAI,KAAM,SAAQ,SAAM;EAC1B;AACA,WAAS,SAAS,YAAY,OAAO,MAAM;AACzC,aAAQ,CAAA,GAAA,MAAK,KAAK,GAAA,GAAK,UAAU,CAAA;AACjC,QAAI,KAAM,SAAQ,SAAM;EAC1B;AACA,WAAS,YAAY,eAAe,OAAO,MAAM;AAC/C,UAAM,cAAc,IAAI,IAAI,cAAc,IAAG,CAAE,MAAM,EAAE,EAAE,CAAA;AACzD,aAAQ,MAAC,KAAK,EAAC,OAAM,CAAE,MAAC,CAAM,YAAY,IAAI,EAAE,EAAE,CAAA,CAAA;AAClD,QAAI,KAAM,SAAQ,SAAM;EAC1B;AACA,WAAS,YAAY,eAAe,OAAO,MAAM;AAC/C,UAAM,cAAc,IAAI,IAAI,cAAc,IAAG,CAAE,MAAM,EAAE,EAAE,CAAA;AACzD,aAAQ,MAAC,KAAK,EAAC,OAAM,CAAE,MAAC,CAAM,YAAY,IAAI,EAAE,EAAE,CAAA,CAAA;AAClD,QAAI,KAAM,SAAQ,SAAM;EAC1B;AACA,WAAS,YAAY,SAAS,SAAS,OAAO,MAAM,YAAY,OAAO;AACrE,UAAM,SAAS,mBAAkB,MAAC,KAAK,GAAA,MAAE,KAAK,GAAE,SAAS,SAAS,SAAS;AAC3E,aAAS,OAAO,KAAK;AACrB,QAAI,OAAO,UAAK,MAAK,KAAK,GAAE;AAC1B,eAAS,OAAO,KAAK;IACvB;AACA,QAAI,KAAM,SAAQ,SAAM;EAC1B;AACA,WAAS,kBAAkB,QAAQ;AACjC,UAAM;MACJ;MACA,eAAY,CAAA;MACZ,gBAAa,CAAA;MACb,gBAAgB;MAChB;MACA,kBAAe,CAAA;MACf,mBAAgB,CAAA;MAChB;QACE;AACJ,UAAM,oBAAiB,CAAI,UAAU,UAAU,QAAQ;AACrD,UAAI,QAAQ,YAAY;AACtB,eAAO,QAAQ,WAAW,UAAU,UAAU,GAAG;MACnD;AACA,aAAM;QACJ,IAAI,OAAO,WAAU;QACrB,MAAM;QACN,QAAQ;QACR,QAAQ;QACR,OAAO,KAAK,SAAS;QACrB,UAAU,KAAK,YAAY;QAC3B,MAAM,KAAK,QAAI,CAAA;;IAEnB;AACA,eAAW,QAAO,CAAE,cAAc;AAChC,qBAAe,WAAW,qBAAqB,KAAK;AACpD,mBAAa,QAAO,CAAE,eAAe;AACnC,cAAM,QAAQ,eAAe,aAAa,YAAY,SAAS,IAAI;AACnE,gBACE,kBAAkB,WAAW,IAAI,UAAU,IAAE,EAC3C,OACA,UAAU,eACV,MAAM,gBAAe,CAAA,GAEvB,KAAK;MAET,CAAC;AACD,oBAAc,QAAO,CAAE,eAAe;AACpC,cAAM,QAAQ,eAAe,aAAa,WAAW,UAAU,IAAI;AACnE,gBACE,kBAAkB,UAAU,IAAI,WAAW,IAAE,EAC3C,OACA,UAAU,eACV,MAAM,iBAAgB,CAAA,GAExB,KAAK;MAET,CAAC;IACH,CAAC;AACD,YAAQ,SAAM;EAChB;AACA,WAAS,eAAe,UAAU;AAChC,WAAO,0BAAyB,EAAG,OAAK,MAAL,KAAK,GAAE,OAAK,MAAL,KAAK,EAAA,GAAI,QAAQ;EAC7D;AACA,SAAM;IACJ,IAAI,QAAQ;AACV,aAAM,MAAC,KAAK;IACd;IACA,IAAI,MAAM,KAAK;AACb,eAAS,GAAG;IACd;IACA,IAAI,QAAQ;AACV,aAAM,MAAC,KAAK;IACd;IACA,IAAI,MAAM,KAAK;AACb,eAAS,GAAG;IACd;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;AAEJ;;;ACxNA,OAAO,WAAW;AAClB,SAAS,gBAA4C;AAM9C,SAAS,wBACf,QAAQ,GACR,SAAS,GACT,sBACA,kBACa;AACb,QAAM,OAAO,qBAAqB,OAAO,aAAa,cACnD,EAAE,OAAO,SAAS,gBAAgB,aAAa,QAAQ,SAAS,gBAAgB,aAAa,IAC7F,EAAE,OAAO,MAAM,QAAQ,KAAK;AAE/B,QAAM,UAAU,qBAAqB,EAAE,GAAG,KAAK,QAAQ,GAAG,GAAG,KAAK,SAAS,EAAE,CAAC;AAC9E,SAAO;AAAA,IACN,GAAG,QAAQ,IAAI,QAAQ;AAAA,IACvB,GAAG,QAAQ,IAAI,SAAS;AAAA,EACzB;AACD;AAKO,SAAS,YACf,OACA,sBACA,gBACA,kBACU;AACV,MAAI,MAAM,WAAW,GAAG;AACvB,WAAO,CAAC;AAAA,EACT;AAEA,QAAM,SAAS,eAAe,KAAK;AACnC,QAAM,oBAAoB,EAAE,GAAG,OAAO,GAAG,GAAG,OAAO,EAAE;AAErD,QAAM,mBAAmB;AAAA,IACxB,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA;AAAA,EACD;AAEA,QAAM,SAAS,iBAAiB,IAAI,kBAAkB;AACtD,QAAM,SAAS,iBAAiB,IAAI,kBAAkB;AAEtD,SAAO,MAAM,IAAI,CAAC,UAAU;AAAA,IAC3B,GAAG;AAAA,IACH,UAAU;AAAA,MACT,GAAG,KAAK,SAAS,IAAI;AAAA,MACrB,GAAG,KAAK,SAAS,IAAI;AAAA,IACtB;AAAA,EACD,EAAE;AACH;AAKO,SAAS,iBAIf,OACA,OACA,gBACA,UAA+B,CAAC,GACtB;AACV,MAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAEhC,QAAM;AAAA,IACL,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,UAAU;AAAA,IACV,mBAAmB;AAAA,IACnB,oBAAoB;AAAA,EACrB,IAAI;AAEJ,QAAM,eAAe,cAAc,QAAQ,cAAc;AACzD,QAAM,SAAS,eAAe,KAAK;AAEnC,QAAM,aAAa,IAAI,MAAM,SAAS,MAAM;AAC5C,aAAW,oBAAoB,OAAO,CAAC,EAAE;AACzC,aAAW,SAAS,EAAE,SAAS,WAAW,SAAS,QAAQ,CAAC;AAE5D,QAAM,UAAU,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AAEpD,QAAM,QAAQ,CAAC,SAAS;AACvB,UAAM,YAAY,KAAK,UAAU,SAAU,KAAa,SAAS;AACjE,UAAM,aAAa,KAAK,UAAU,UAAW,KAAa,UAAU;AACpE,eAAW,QAAQ,KAAK,IAAI,EAAE,OAAO,WAAW,QAAQ,WAAW,CAAC;AAAA,EACrE,CAAC;AAED,QAAM,QAAQ,CAAC,SAAS;AACvB,QAAI,QAAQ,IAAI,KAAK,MAAM,KAAK,QAAQ,IAAI,KAAK,MAAM,GAAG;AACzD,iBAAW,QAAQ,KAAK,QAAQ,KAAK,MAAM;AAAA,IAC5C;AAAA,EACD,CAAC;AAED,QAAM,OAAO,UAAU;AAEvB,SAAO,MAAM,IAAI,CAAC,SAAS;AAC1B,UAAM,mBAAmB,WAAW,KAAK,KAAK,EAAE;AAChD,UAAM,YAAY,KAAK,UAAU,SAAU,KAAa,SAAS;AACjE,UAAM,aAAa,KAAK,UAAU,UAAW,KAAa,UAAU;AAEpE,WAAO;AAAA,MACN,GAAG;AAAA,MACH,gBAAgB,eAAe,SAAS,OAAO,SAAS;AAAA,MACxD,gBAAgB,eAAe,SAAS,QAAQ,SAAS;AAAA,MACzD,UAAU;AAAA,QACT,GAAG,OAAO,KAAK,kBAAkB,KAAK,KAAK,YAAY;AAAA,QACvD,GAAG,OAAO,KAAK,kBAAkB,KAAK,KAAK,aAAa;AAAA,MACzD;AAAA,IACD;AAAA,EACD,CAAC;AACF;;;;ACvHA,SAAS,YAAY;AACd,SAAS,qBAAqB,OAAO;AAC1C,QAAM,SAAS,MAAM;AACrB,MAAE,CAAG,OAAQ,QAAO;AACpB,MAAI,OAAO,YAAY,WAAW,OAAO,YAAY,cAAc,OAAO,YAAY,YAAY,OAAO,qBAAqB,OAAO,UAAU,0BAA0B,KAAK,OAAO,UAAU,cAAc,GAAG;AAC9M,WAAO;EACT;AACA,SAAO;AACT;AACO,SAAS,kBAAkB,KAAK;AACrC,SAAM,OAAQ,QAAQ,YAAY,QAAQ,QAAQ,QAAQ,OAAG,OAAW,IAAI,OAAO,YAAY,cAAc,OAAG,OAAW,IAAI,aAAa,YAAY,IAAI,aAAa,QAAQ,OAAO,IAAI,YAAY,OAAO,IAAI;AACrN;AACO,SAAS,WAAW,KAAK;AAC9B,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,WAAO,OAAO,aAAa,WAAW,OAAO,aAAa;EAC5D,QAAQ;AACN,WAAO;EACT;AACF;AACO,SAAS,sBAAsB,UAAO,CAAA,GAAO;AAClD,WAAS,kBAAkB,eAAe,gBAAgB,OAAO;AAC/D,QAAI,SAAS,qBAAqB,KAAK,GAAG;AACxC,aAAO;IACT;AACA,QAAI,cAAc,WAAW,EAAG,QAAO;AACvC,UAAM,UAAO,EACX,SAAS,GACT,OAAO,eACP,OAAO,eAAc;AAEvB,UAAM,aAAa,KAAK,UAAU,OAAO;AACzC,QAAI,OAAO,eAAe;AACxB,YAAM,cAAc,QAAQ,oBAAoB,UAAU;AAC1D,YAAM,cAAc,QAAQ,cAAc,UAAU;AACpD,YAAM,eAAc;IACtB,WAAS,OAAS,cAAc,eAAe,UAAU,WAAW;AAClE,gBAAU,UAAU,UAAU,UAAU,EAAE,MAAK,MAAO;MACtD,CAAC;IACH;AACA,WAAO;EACT;AACA,WAAS,iBAAiB,eAAe,gBAAgB,UAAU,OAAO;AACxE,QAAI,SAAS,qBAAqB,KAAK,GAAG;AACxC,aAAO;IACT;AACA,UAAM,SAAS,kBAAkB,eAAe,gBAAgB,KAAK;AACrE,QAAI,UAAU,UAAU;AACtB,eAAS,aAAa;IACxB;AACA,WAAO;EACT;AACA,iBAAe,iBAAiB,OAAO;AACrC,QAAI,qBAAqB,KAAK,GAAG;AAC/B,aAAO;IACT;AACA,UAAM,gBAAgB,MAAM;AAC5B,QAAE,CAAG,cAAe,QAAO;AAC3B,UAAM,QAAQ,cAAc;AAC5B,QAAI,SAAS,MAAM,SAAS,GAAG;AAC7B,YAAM,YAAY,MAAM,KAAK,KAAK,EAAE,KAAI,CAAE,MAAM,EAAE,KAAK,WAAW,QAAQ,CAAA;AAC1E,UAAI,aAAa,QAAQ,cAAc;AACrC,cAAM,eAAc;AACpB,cAAM,QAAQ,aAAa,SAAS;AACpC,eAAO;MACT;IACF;AACA,UAAM,UAAU,cAAc,QAAQ,kBAAkB,KAAK,cAAc,QAAQ,YAAY;AAC/F,QAAI,SAAS;AACX,UAAI;AACF,cAAM,SAAS,KAAK,MAAM,OAAO;AACjC,YAAI;AACJ,YAAI;AACJ,YAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,MAAM,iBAAiB,GAAG;AAC5D,qBAAW;QACb,WAAS,OAAS,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,KAAK,KAAK,OAAO,MAAM,MAAM,iBAAiB,GAAG;AAChI,qBAAW,OAAO;AAClB,qBAAW,OAAO;QACpB;AACA,YAAI,YAAY,SAAS,SAAS,GAAG;AACnC,gBAAM,eAAc;AACpB,gBAAM,QAAwB,oBAAI,IAAG;AACrC,gBAAM,WAAW,SAAS,IAAG,CAAE,SAAS;AACtC,kBAAM,QAAQ,OAAO,WAAU;AAC/B,kBAAM,IAAI,KAAK,IAAI,KAAK;AACxB,mBAAM;iBACD;cACH,IAAI;cACJ,UAAU;cACV,UAAQ,EACN,GAAG,KAAK,SAAS,IAAI,IACrB,GAAG,KAAK,SAAS,IAAI,GAAE;;UAG7B,CAAC;AACD,gBAAM,WAAW,UAAU,IAAG,CAAE,UAAI;eAC/B;YACH,IAAI,OAAO,WAAU;YACrB,QAAQ,MAAM,IAAI,KAAK,MAAM,KAAK,KAAK;YACvC,QAAQ,MAAM,IAAI,KAAK,MAAM,KAAK,KAAK;;AAEzC,cAAI,QAAQ,cAAc;AACxB,kBAAM,QAAQ,aAAa,UAAU,QAAQ;AAC7C,kBAAM,KAAI;UACZ;AACA,iBAAO;QACT;MACF,QAAQ;MACR;IACF;AACA,UAAM,OAAO,cAAc,QAAQ,YAAY,GAAG,KAAI;AACtD,QAAI,MAAM;AACR,UAAI,WAAW,IAAI,KAAK,QAAQ,YAAY;AAC1C,cAAM,eAAc;AACpB,gBAAQ,WAAW,IAAI;AACvB,eAAO;MACT,WAAW,QAAQ,aAAa;AAC9B,cAAM,eAAc;AACpB,gBAAQ,YAAY,IAAI;AACxB,eAAO;MACT;IACF;AACA,WAAO;EACT;AACA,SAAM,EACJ,mBACA,kBACA,iBAAgB;AAEpB;;;;ACjIO,SAAS,oBAAoB,UAAO,CAAA,GAAO;AAChD,MAAI,cAAc,SAAW,IAAI;AACjC,MAAI,yBAAyB,SAAW,IAAI;AAC5C,MAAI,mBAAmB,SAAW,EAAE;AACpC,MAAI,iBAAiB,SAAW,IAAI;AACpC,MAAI,sBAAsB,SAA2B,oBAAI,IAAG,CAAA;AAC5D,QAAM,eAAe,QAAQ,iBAAY,CAAM,WAAW,OAAO,SAAS;AAC1E,WAAS,oBAAoB,QAAQ;WACnC,aAAc,OAAO,UAAU;WAC/B,wBAAyB,OAAO,YAAY,WAAQ,EAAA,GAAQ,OAAO,WAAW,SAAQ,IAAK,IAAI;WAC/F,qBAAsC,oBAAI,IAAG,CAAA;EAC/C;AACA,WAAS,eAAe,QAAQ,sBAAsB;WACpD,aAAc,OAAO,UAAU;AAC/B,QAAE,CAAA,OAAG,WAAW,EAAE;AAClB,UAAM,eAAe,qBAAoB,OAAC,WAAW,CAAA;AACrD,UAAM,qBAAqB,aAAa,OAAO,YAAY;AAC3D,UAAM,cAAc,IAAI,IAAI,mBAAmB,IAAG,CAAE,MAAM,EAAE,EAAE,CAAA;WAC9D,mBAAmB,EAAC,QAAO,CAAE,aAAa;AACxC,UAAE,CAAG,YAAY,IAAI,QAAQ,GAAG;AAC9B,cAAM,WAAW,aAAa,KAAI,CAAE,MAAM,EAAE,OAAO,QAAQ,KAAA,EAAO,IAAI,SAAQ;AAC9E,gBAAQ,cAAc,UAAQ,OAAE,WAAW,CAAA;MAC7C;IACF,CAAC;AACD,uBAAmB,QAAO,CAAE,WAAW;AACrC,UAAE,CAAA,OAAG,mBAAmB,EAAC,IAAI,OAAO,EAAE,GAAG;AACvC,gBAAQ,cAAc,QAAM,OAAE,WAAW,CAAA;MAC3C;IACF,CAAC;WACD,qBAAsB,WAAW;EACnC;AACA,WAAS,mBAAmB,QAAQ,sBAAsB,oBAAoB;WAC5E,aAAc,OAAO,UAAU;AAC/B,QAAE,OAAE,WAAW,GAAE;AACf,YAAM,eAAe,qBAAoB,OAAC,WAAW,CAAA;AACrD,YAAM,aAAa,aAAa,KAAK,YAAY;AACjD,UAAI,YAAY;AACd,gBAAQ,aAAa,YAAU,OAAE,WAAW,CAAA;AAC5C,YAAI,QAAQ,0BAA0B,SAAK,OAAI,sBAAsB,KAAI,oBAAoB;AAC3F,6BAAkB,OAAC,WAAW,EAAC,IAAE,OAAE,sBAAsB,CAAA;QAC3D;MACF;IACF;WACA,aAAc,IAAI;WAClB,wBAAyB,IAAI;WAC7B,qBAAsC,oBAAI,IAAG,CAAA;AAC7C,YAAQ,SAAM;EAChB;AACA,WAAS,mBAAmB,QAAQ,SAAS;WAC3C,kBAAmB,OAAO,cAAc,EAAE;WAC1C,gBAAiB,OAAO,SAAS,QAAQ,OAAO,MAAM,KAAK,OAAO,IAAI;EACxE;AACA,WAAS,mBAAmB;WAC1B,kBAAmB,EAAE;WACrB,gBAAiB,IAAI;EACvB;AACA,WAAS,QAAQ;WACf,aAAc,IAAI;WAClB,wBAAyB,IAAI;WAC7B,kBAAmB,EAAE;WACrB,gBAAiB,IAAI;WACrB,qBAAsC,oBAAI,IAAG,CAAA;EAC/C;AACA,SAAM;IACJ,IAAI,cAAc;AAChB,aAAM,OAAC,WAAW;IACpB;IACA,IAAI,yBAAyB;AAC3B,aAAM,OAAC,sBAAsB;IAC/B;IACA,IAAI,mBAAmB;AACrB,aAAM,OAAC,gBAAgB;IACzB;IACA,IAAI,iBAAiB;AACnB,aAAM,OAAC,cAAc;IACvB;IACA,IAAI,aAAa;AACf,aAAM,OAAC,WAAW,MAAK;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;;AAEJ;;;AC7EO,SAAS,sBACf,SACa;AACb,QAAM,SAAS,QAAQ,kBAAkB,OAAO,WAAW,cAAc,SAAS;AAClF,MAAI,CAAC,OAAQ,QAAO,MAAM;AAAA,EAAC;AAE3B,WAAS,cAAc,OAAc;AACpC,UAAM,gBAAgB;AACtB,QAAI,qBAAqB,aAAa,GAAG;AACxC;AAAA,IACD;AAEA,UAAM,QAAQ,cAAc,WAAW,cAAc;AACrD,UAAM,UAAU,cAAc;AAC9B,UAAM,MAAM,cAAc;AAE1B,UAAM,gBAAgB,QAAQ,mBAAmB,QAAQ,iBAAiB,IAAI,CAAC;AAE/E,QAAI,QAAQ,eAAe,QAAQ,UAAU;AAC5C,UAAI,QAAQ,YAAY,cAAc,SAAS,GAAG;AACjD,sBAAc,eAAe;AAC7B,gBAAQ,SAAS,aAAa;AAAA,MAC/B;AACA;AAAA,IACD;AAEA,QAAI,OAAO;AACV,YAAM,WAAW,IAAI,YAAY;AAEjC,UAAI,aAAa,OAAO,CAAC,SAAS;AACjC,YAAI,QAAQ,QAAQ;AAEnB,kBAAQ,OAAO;AAAA,QAChB;AAAA,MACD,WAAW,aAAa,OAAO,CAAC,SAAS;AACxC,YAAI,QAAQ,OAAO;AAClB,kBAAQ,MAAM;AAAA,QACf;AAAA,MACD,WAAW,aAAa,OAAO,CAAC,SAAS;AACxC,YAAI,QAAQ,eAAe,cAAc,SAAS,GAAG;AACpD,wBAAc,eAAe;AAC7B,kBAAQ,YAAY,aAAa;AAAA,QAClC;AAAA,MACD,WAAW,aAAa,OAAO,CAAC,SAAS;AACxC,YAAI,QAAQ,aAAa;AACxB,wBAAc,eAAe;AAC7B,kBAAQ,YAAY;AAAA,QACrB;AAAA,MACD,WAAW,aAAa,KAAK;AAC5B,YAAI,SAAS;AACZ,cAAI,QAAQ,aAAa,cAAc,SAAS,GAAG;AAClD,0BAAc,eAAe;AAC7B,oBAAQ,UAAU,aAAa;AAAA,UAChC;AAAA,QACD,OAAO;AACN,cAAI,QAAQ,WAAW,cAAc,SAAS,GAAG;AAChD,0BAAc,eAAe;AAC7B,oBAAQ,QAAQ,aAAa;AAAA,UAC9B;AAAA,QACD;AAAA,MACD,WAAW,QAAQ,OAAO,QAAQ,KAAK;AACtC,YAAI,QAAQ,UAAU;AACrB,wBAAc,eAAe;AAC7B,kBAAQ,SAAS;AAAA,QAClB;AAAA,MACD,WAAW,QAAQ,KAAK;AACvB,YAAI,QAAQ,WAAW;AACtB,wBAAc,eAAe;AAC7B,kBAAQ,UAAU;AAAA,QACnB;AAAA,MACD,WAAW,QAAQ,KAAK;AACvB,YAAI,QAAQ,WAAW;AACtB,wBAAc,eAAe;AAC7B,kBAAQ,UAAU;AAAA,QACnB;AAAA,MACD,WAAW,aAAa,KAAK;AAC5B,YAAI,QAAQ,UAAU;AACrB,wBAAc,eAAe;AAC7B,kBAAQ,SAAS;AAAA,QAClB;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAEA,SAAO,iBAAiB,WAAW,aAA8B;AAEjE,SAAO,MAAM;AACZ,WAAO,oBAAoB,WAAW,aAA8B;AAAA,EACrE;AACD;A;;;ACjGO,SAAS,yBAAyB,UAAO,CAAA,GAAO;AACrD,MAAI,oBAAoB,SAAW,KAAK;AACxC,MAAI,mBAAmB,SAAW,KAAK;AACvC,MAAI,iBAAiB;AACrB,WAAS,cAAc,OAAO;AAC5B,QAAI,MAAM,QAAQ,OAAO,MAAM,SAAS,SAAS;AAC/C,UAAE,OAAE,iBAAiB,KAAI,qBAAqB,KAAK,EAAG;aACtD,mBAAoB,IAAI;AACxB,uBAAiB;AACjB,cAAQ,kBAAkB,IAAI;AAC9B,UAAE,OAAS,aAAa,aAAa;AACnC,iBAAS,KAAK,UAAU,IAAI,aAAa;MAC3C;IACF;EACF;AACA,WAAS,YAAY,OAAO;AAC1B,QAAI,MAAM,QAAQ,OAAO,MAAM,SAAS,SAAS;AAC/C,UAAE,CAAA,OAAG,iBAAiB,KAAA,CAAK,eAAgB;aAC3C,mBAAoB,KAAK;AACzB,uBAAiB;AACjB,cAAQ,kBAAkB,KAAK;AAC/B,UAAE,OAAS,aAAa,aAAa;AACnC,iBAAS,KAAK,UAAU,OAAO,aAAa;MAC9C;IACF;EACF;AACA,WAAS,YAAY,OAAO;AAC1B,QAAI,MAAM,WAAO,CAAA,OAAK,gBAAgB,GAAE;aACtC,kBAAmB,IAAI;AACvB,cAAQ,qBAAkB;IAC5B;EACF;AACA,WAAS,gBAAgB,QAAQ;AAC/B,UAAM,KAAK,UAAU,QAAQ,kBAAa,OAAY,WAAW,cAAc,SAAS;AACxF,QAAE,CAAG,GAAI,QAAM,MAAO;IACtB;AACA,OAAG,iBAAiB,WAAW,aAAa;AAC5C,OAAG,iBAAiB,SAAS,WAAW;AACxC,OAAG,iBAAiB,SAAS,aAAW,EAAI,SAAS,KAAI,CAAA;AACzD,WAAM,MAAO;AACX,SAAG,oBAAoB,WAAW,aAAa;AAC/C,SAAG,oBAAoB,SAAS,WAAW;AAC3C,SAAG,oBAAoB,SAAS,WAAW;AAC3C,UAAE,OAAS,aAAa,aAAa;AACnC,iBAAS,KAAK,UAAU,OAAO,aAAa;MAC9C;IACF;EACF;AACA,SAAM;IACJ,IAAI,oBAAoB;AACtB,aAAM,OAAC,iBAAiB;IAC1B;IACA,IAAI,mBAAmB;AACrB,aAAM,OAAC,gBAAgB;IACzB;IACA;IACA,SAAS,gBAAe;;AAE5B;AACO,IAAM,0BAAuB;EAClC,UAAQ,CAAG,IAAI,EAAE;EACjB,WAAS,CAAG,GAAG,GAAG,GAAG,GAAG,CAAC;EACzB,aAAa;EACb,iBAAiB;EACjB,WAAW;EACX,mBAAiB,CAAG,SAAS,MAAM;;AAE9B,IAAM,uBAAoB;KAC5B;EACH,WAAS,CAAG,GAAG,CAAC;EAChB,iBAAiB;;AAEZ,SAAS,0BAA0B,gBAAgB,uBAAuB;AAC/E,MAAE,OAAS,aAAa,YAAa;AACrC,QAAM,SAAS,SAAS,iBAAiB,aAAa;AACtD,SAAO,QAAO,CAAE,UAAU;AACxB,UAAM,MAAM,gBAAgB;EAC9B,CAAC;AACH;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,19 @@
|
|
|
20
20
|
"svelte": "./dist/index.js",
|
|
21
21
|
"import": "./dist/index.js",
|
|
22
22
|
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./canvas": {
|
|
25
|
+
"types": "./dist/canvas/index.d.ts",
|
|
26
|
+
"svelte": "./dist/canvas/index.js",
|
|
27
|
+
"import": "./dist/canvas/index.js",
|
|
28
|
+
"default": "./dist/canvas/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"typesVersions": {
|
|
32
|
+
"*": {
|
|
33
|
+
"canvas": [
|
|
34
|
+
"./dist/canvas/index.d.ts"
|
|
35
|
+
]
|
|
23
36
|
}
|
|
24
37
|
},
|
|
25
38
|
"files": [
|
|
@@ -27,6 +40,7 @@
|
|
|
27
40
|
"src"
|
|
28
41
|
],
|
|
29
42
|
"dependencies": {
|
|
43
|
+
"@dagrejs/dagre": "^1.1.4",
|
|
30
44
|
"@tiptap/core": "^2.11.5",
|
|
31
45
|
"@tiptap/extension-bubble-menu": "^2.11.5",
|
|
32
46
|
"@tiptap/extension-placeholder": "^2.11.5",
|
|
@@ -35,9 +49,18 @@
|
|
|
35
49
|
"@pixerate/editor": "0.4.5"
|
|
36
50
|
},
|
|
37
51
|
"peerDependencies": {
|
|
52
|
+
"@xyflow/svelte": "^1.6.0",
|
|
38
53
|
"svelte": "^4.0.0 || ^5.0.0"
|
|
39
54
|
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"@xyflow/svelte": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
40
60
|
"devDependencies": {
|
|
61
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
|
62
|
+
"@types/dagre": "^0.7.52",
|
|
63
|
+
"@xyflow/svelte": "^1.6.6",
|
|
41
64
|
"esbuild-svelte": "^0.9.5",
|
|
42
65
|
"svelte": "^5.0.0",
|
|
43
66
|
"svelte-preprocess": "^6.0.5",
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# @pixerate/editor-svelte/canvas
|
|
2
|
+
|
|
3
|
+
A headless, unstyled Svelte 5 runes architecture for building interactive graph, flow, and diagramming canvas experiences on top of SvelteFlow (`@xyflow/svelte`).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy & Core Principles
|
|
8
|
+
|
|
9
|
+
1. **Headless & UI-Agnostic**: Contains zero assumptions about styles, colors, DOM presentation, or application-specific AI pipelines. Pure business logic, coordinate mathematics, and interaction controllers.
|
|
10
|
+
2. **Svelte 5 Runes First**: Implemented using `$state.raw`, `$derived`, and modern functional runes instead of `.svelte` component-as-controller wrappers.
|
|
11
|
+
3. **High Performance**: Eliminates expensive per-frame array clones during animations and node dragging, avoiding requestAnimationFrame memory churn.
|
|
12
|
+
4. **Decoupled Architecture**: Independent modules for graph state, clipboard serialization, Dagre auto-layout, docking/intersections, and hotkeys that can be used together or individually.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Module Overview
|
|
17
|
+
|
|
18
|
+
| Module | Purpose |
|
|
19
|
+
| :--- | :--- |
|
|
20
|
+
| [`createCanvasGraph.svelte.ts`](./createCanvasGraph.svelte.ts) | Reactive graph state management, node deduplication, edge-preserving replacement, and diff calculation. |
|
|
21
|
+
| [`createCanvasClipboard.svelte.ts`](./createCanvasClipboard.svelte.ts) | Serialized JSON clipboard management, UUID remapping, position offset, URL and text pasting. |
|
|
22
|
+
| [`createCanvasDocking.svelte.ts`](./createCanvasDocking.svelte.ts) | Drag-to-dock intersection tracking, dock target hover/leave/drop lifecycle, and connection state. |
|
|
23
|
+
| [`createCanvasLayout.ts`](./createCanvasLayout.ts) | Dagre hierarchical auto-layout, viewport centering, and bounding box math. |
|
|
24
|
+
| [`createCanvasShortcuts.ts`](./createCanvasShortcuts.ts) | Global keyboard shortcut binding with automatic suppression during active text editing. |
|
|
25
|
+
| [`createCanvasInteractions.svelte.ts`](./createCanvasInteractions.svelte.ts) | Spacebar panning toggle, trackpad pinch detection, and SvelteFlow interaction presets. |
|
|
26
|
+
| [`types.ts`](./types.ts) | Shared generic types (`CanvasNode`, `CanvasEdge`, `GraphDifferences`, `CanvasLayoutOptions`, etc.). |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Quickstart & Usage
|
|
31
|
+
|
|
32
|
+
### 1. Basic Setup
|
|
33
|
+
|
|
34
|
+
```svelte
|
|
35
|
+
<script lang="ts">
|
|
36
|
+
import { SvelteFlow, useSvelteFlow } from '@xyflow/svelte';
|
|
37
|
+
import '@xyflow/svelte/dist/style.css';
|
|
38
|
+
import {
|
|
39
|
+
createCanvasGraph,
|
|
40
|
+
createCanvasClipboard,
|
|
41
|
+
createCanvasShortcuts,
|
|
42
|
+
createCanvasInteractions,
|
|
43
|
+
defaultSvelteFlowPreset
|
|
44
|
+
} from '@pixerate/editor-svelte/canvas';
|
|
45
|
+
|
|
46
|
+
// 1. Initialize reactive graph controller
|
|
47
|
+
const graph = createCanvasGraph({
|
|
48
|
+
initialNodes: [
|
|
49
|
+
{ id: '1', position: { x: 100, y: 100 }, data: { label: 'Start' } }
|
|
50
|
+
],
|
|
51
|
+
initialEdges: [],
|
|
52
|
+
onSave: () => console.log('Graph changed, autosaving...')
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 2. Setup clipboard handler
|
|
56
|
+
const clipboard = createCanvasClipboard({
|
|
57
|
+
onPasteNodes: (nodes, edges) => {
|
|
58
|
+
graph.addNodeOrNodes(nodes);
|
|
59
|
+
if (edges) graph.addEdges(edges);
|
|
60
|
+
},
|
|
61
|
+
onPasteUrl: (url) => {
|
|
62
|
+
console.log('Pasted URL:', url);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 3. Attach keyboard shortcuts
|
|
67
|
+
$effect(() => {
|
|
68
|
+
return createCanvasShortcuts({
|
|
69
|
+
getSelectedNodes: () => graph.nodes.filter((n) => n.selected),
|
|
70
|
+
onDelete: (selected) => graph.removeNodes(selected),
|
|
71
|
+
onDuplicate: (selected) => {
|
|
72
|
+
const clones = selected.map((n) => ({
|
|
73
|
+
...n,
|
|
74
|
+
id: crypto.randomUUID(),
|
|
75
|
+
position: { x: n.position.x + 40, y: n.position.y + 40 }
|
|
76
|
+
}));
|
|
77
|
+
graph.addNodeOrNodes(clones);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div class="w-full h-screen" onpaste={(e) => clipboard.handlePasteEvent(e)}>
|
|
84
|
+
<SvelteFlow
|
|
85
|
+
bind:nodes={graph.nodes}
|
|
86
|
+
bind:edges={graph.edges}
|
|
87
|
+
{...defaultSvelteFlowPreset}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
### `createCanvasGraph<TNode, TEdge>(options?)`
|
|
97
|
+
|
|
98
|
+
Creates a reactive graph controller managing nodes and edges.
|
|
99
|
+
|
|
100
|
+
#### Parameters (`options`):
|
|
101
|
+
- `initialNodes?: TNode[]`: Initial node list.
|
|
102
|
+
- `initialEdges?: TEdge[]`: Initial edge list.
|
|
103
|
+
- `onNodesChange?: (nodes: TNode[]) => void`: Callback fired when nodes change.
|
|
104
|
+
- `onEdgesChange?: (edges: TEdge[]) => void`: Callback fired when edges change.
|
|
105
|
+
- `onSave?: () => void`: Optional persistence trigger.
|
|
106
|
+
- `createEdge?: (sourceId, targetId, options?) => TEdge`: Edge factory function.
|
|
107
|
+
|
|
108
|
+
#### Returns:
|
|
109
|
+
- `nodes`: Getter/setter for active nodes array.
|
|
110
|
+
- `edges`: Getter/setter for active edges array.
|
|
111
|
+
- `addNodeOrNodes(nodeOrNodes, animateFromPosition?, options?)`: Adds nodes with deduplication and optional positioning animation.
|
|
112
|
+
- `addEdge(edge, options?)` / `addEdges(edges, options?)`: Adds edges.
|
|
113
|
+
- `removeNodes(nodesToRemove, options?)` / `removeEdges(edgesToRemove, options?)`: Removes entities by ID.
|
|
114
|
+
- `replaceNode(oldNode, newNode, save?, keepEdges?)`: Replaces a node, optionally updating connected edge endpoints.
|
|
115
|
+
- `addConnectedNodes(params)`: Connects incoming and outgoing nodes to a batch of added nodes.
|
|
116
|
+
- `getDifferences(baseline)`: Returns `{ changedNodes, changedEdges, removedNodes, removedEdges }`.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### `createCanvasLayout`
|
|
121
|
+
|
|
122
|
+
Pure geometry and Dagre layout calculations:
|
|
123
|
+
|
|
124
|
+
- `getLayoutedNodes(nodes, edges, getNodesBounds, options?)`: Automatically lays out graph nodes hierarchically (`'LR'`, `'TB'`, `'RL'`, or `'BT'`).
|
|
125
|
+
- `getCenteredNodePosition(width, height, screenToFlowPosition, screenDimensions?)`: Computes top-left coordinate to center an element in view.
|
|
126
|
+
- `centerNodes(nodes, screenToFlowPosition, getNodesBounds, screenDimensions?)`: Translates a collection of nodes so their bounding box is centered in the current viewport.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### `createCanvasClipboard(options?)`
|
|
131
|
+
|
|
132
|
+
Headless clipboard interaction:
|
|
133
|
+
|
|
134
|
+
- `copySelectedNodes(nodes, edges?, event?)`: Serializes nodes to clipboard as JSON.
|
|
135
|
+
- `cutSelectedNodes(nodes, edges?, onRemove?, event?)`: Copies and calls deletion callback.
|
|
136
|
+
- `handlePasteEvent(event)`: Intelligently handles paste events, disambiguating images, serialized nodes, URLs, and plain text.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### `createCanvasDocking(options?)`
|
|
141
|
+
|
|
142
|
+
Drag-and-dock interaction tracking:
|
|
143
|
+
|
|
144
|
+
- `handleNodeDragStart({ targetNode, event })`: Starts drag tracking.
|
|
145
|
+
- `handleNodeDrag({ targetNode, event }, getIntersectingNodes)`: Computes intersections, firing `onDockHover` and `onDockLeave`.
|
|
146
|
+
- `handleNodeDragStop({ targetNode, event }, getIntersectingNodes, updatePosition?)`: Fires `onDockDrop` and cleans up state.
|
|
147
|
+
- `handleConnectStart` / `handleConnectEnd`: Tracks connection handle dragging.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### `createCanvasShortcuts(options)`
|
|
152
|
+
|
|
153
|
+
Attaches standard canvas hotkeys (`Meta/Ctrl+C`, `Meta/Ctrl+X`, `Meta/Ctrl+V`, `Delete`, `Meta/Ctrl+D`, `Meta/Ctrl+A`, `Meta/Ctrl+G`, zoom controls):
|
|
154
|
+
|
|
155
|
+
- Automatically respects active text inputs (`input`, `textarea`, `select`, `contenteditable`, and `.ProseMirror`).
|
|
156
|
+
- Returns a cleanup function `() => void`.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### `createCanvasInteractions(options?)`
|
|
161
|
+
|
|
162
|
+
Manages viewport interaction modes:
|
|
163
|
+
|
|
164
|
+
- `isSpacebarPanning`: Reactive flag active while Spacebar is held down.
|
|
165
|
+
- `trackpadDetected`: Reactive flag set when trackpad pinch-to-zoom is detected.
|
|
166
|
+
- Presets: `defaultSvelteFlowPreset`, `miroCompatiblePreset`.
|
|
167
|
+
- Helper: `restorePanelPointerEvents()` restores panel clickability after drag-selection.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createCanvasClipboard,
|
|
4
|
+
isValidCanvasNode,
|
|
5
|
+
isValidUrl,
|
|
6
|
+
isEventFromTextInput
|
|
7
|
+
} from '../createCanvasClipboard.svelte.js';
|
|
8
|
+
import type { CanvasNode, CanvasEdge } from '../types.js';
|
|
9
|
+
|
|
10
|
+
describe('createCanvasClipboard & Clipboard Utilities', () => {
|
|
11
|
+
it('validates canvas nodes with isValidCanvasNode', () => {
|
|
12
|
+
expect(isValidCanvasNode({ id: '1', position: { x: 10, y: 20 } })).toBe(true);
|
|
13
|
+
expect(isValidCanvasNode({ id: 123, position: { x: 10, y: 20 } })).toBe(false);
|
|
14
|
+
expect(isValidCanvasNode({ id: '1' })).toBe(false);
|
|
15
|
+
expect(isValidCanvasNode(null)).toBe(false);
|
|
16
|
+
expect(isValidCanvasNode('not a node')).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('validates URLs with isValidUrl', () => {
|
|
20
|
+
expect(isValidUrl('https://example.com')).toBe(true);
|
|
21
|
+
expect(isValidUrl('http://localhost:5173/canvas')).toBe(true);
|
|
22
|
+
expect(isValidUrl('not a url')).toBe(false);
|
|
23
|
+
expect(isValidUrl('ftp://example.com')).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('detects text input targets with isEventFromTextInput', () => {
|
|
27
|
+
const inputEl = { tagName: 'INPUT' } as unknown as HTMLElement;
|
|
28
|
+
const textareaEl = { tagName: 'TEXTAREA' } as unknown as HTMLElement;
|
|
29
|
+
const contentEditableEl = { isContentEditable: true, tagName: 'DIV' } as unknown as HTMLElement;
|
|
30
|
+
const canvasEl = { tagName: 'DIV', isContentEditable: false } as unknown as HTMLElement;
|
|
31
|
+
|
|
32
|
+
expect(isEventFromTextInput({ target: inputEl })).toBe(true);
|
|
33
|
+
expect(isEventFromTextInput({ target: textareaEl })).toBe(true);
|
|
34
|
+
expect(isEventFromTextInput({ target: contentEditableEl })).toBe(true);
|
|
35
|
+
expect(isEventFromTextInput({ target: canvasEl })).toBe(false);
|
|
36
|
+
expect(isEventFromTextInput({ target: null })).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('copies selected nodes into clipboardData JSON payload', () => {
|
|
40
|
+
const clipboard = createCanvasClipboard();
|
|
41
|
+
const mockClipboardData = {
|
|
42
|
+
setData: vi.fn()
|
|
43
|
+
};
|
|
44
|
+
const mockEvent = {
|
|
45
|
+
target: { tagName: 'DIV' },
|
|
46
|
+
clipboardData: mockClipboardData,
|
|
47
|
+
preventDefault: vi.fn()
|
|
48
|
+
} as unknown as ClipboardEvent;
|
|
49
|
+
|
|
50
|
+
const selectedNodes: CanvasNode[] = [{ id: 'n1', position: { x: 10, y: 20 }, data: {} }];
|
|
51
|
+
const connectedEdges: CanvasEdge[] = [{ id: 'e1', source: 'n1', target: 'n2' }];
|
|
52
|
+
|
|
53
|
+
const copied = clipboard.copySelectedNodes(selectedNodes, connectedEdges, mockEvent);
|
|
54
|
+
expect(copied).toBe(true);
|
|
55
|
+
expect(mockClipboardData.setData).toHaveBeenCalledWith(
|
|
56
|
+
'application/json',
|
|
57
|
+
expect.stringContaining('"id":"n1"')
|
|
58
|
+
);
|
|
59
|
+
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('cuts selected nodes by calling onRemove callback', () => {
|
|
63
|
+
const clipboard = createCanvasClipboard();
|
|
64
|
+
const onRemove = vi.fn();
|
|
65
|
+
const mockEvent = {
|
|
66
|
+
target: { tagName: 'DIV' },
|
|
67
|
+
clipboardData: { setData: vi.fn() },
|
|
68
|
+
preventDefault: vi.fn()
|
|
69
|
+
} as unknown as ClipboardEvent;
|
|
70
|
+
|
|
71
|
+
const selectedNodes: CanvasNode[] = [{ id: 'n1', position: { x: 0, y: 0 }, data: {} }];
|
|
72
|
+
|
|
73
|
+
const cut = clipboard.cutSelectedNodes(selectedNodes, undefined, onRemove, mockEvent);
|
|
74
|
+
expect(cut).toBe(true);
|
|
75
|
+
expect(onRemove).toHaveBeenCalledWith(selectedNodes);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('handles paste of serialized node JSON with UUID remapping and offset', async () => {
|
|
79
|
+
const onPasteNodes = vi.fn();
|
|
80
|
+
const clipboard = createCanvasClipboard({ onPasteNodes });
|
|
81
|
+
|
|
82
|
+
const originalNodes: CanvasNode[] = [
|
|
83
|
+
{ id: 'orig-1', position: { x: 100, y: 100 }, data: { label: 'Node 1' } },
|
|
84
|
+
{ id: 'orig-2', position: { x: 200, y: 200 }, data: { label: 'Node 2' } }
|
|
85
|
+
];
|
|
86
|
+
const originalEdges: CanvasEdge[] = [{ id: 'e-orig', source: 'orig-1', target: 'orig-2' }];
|
|
87
|
+
|
|
88
|
+
const serializedPayload = JSON.stringify({
|
|
89
|
+
version: 1,
|
|
90
|
+
nodes: originalNodes,
|
|
91
|
+
edges: originalEdges
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const mockEvent = {
|
|
95
|
+
target: { tagName: 'DIV' },
|
|
96
|
+
clipboardData: {
|
|
97
|
+
getData: (format: string) => (format === 'application/json' ? serializedPayload : ''),
|
|
98
|
+
files: []
|
|
99
|
+
},
|
|
100
|
+
preventDefault: vi.fn()
|
|
101
|
+
} as unknown as ClipboardEvent;
|
|
102
|
+
|
|
103
|
+
const handled = await clipboard.handlePasteEvent(mockEvent);
|
|
104
|
+
expect(handled).toBe(true);
|
|
105
|
+
expect(onPasteNodes).toHaveBeenCalledTimes(1);
|
|
106
|
+
|
|
107
|
+
const [pastedNodes, pastedEdges] = onPasteNodes.mock.calls[0];
|
|
108
|
+
expect(pastedNodes).toHaveLength(2);
|
|
109
|
+
expect(pastedNodes[0].id).not.toBe('orig-1');
|
|
110
|
+
expect(pastedNodes[0].position.x).toBe(140); // 100 + 40
|
|
111
|
+
expect(pastedNodes[0].position.y).toBe(140); // 100 + 40
|
|
112
|
+
|
|
113
|
+
// Check edge remapping
|
|
114
|
+
expect(pastedEdges).toHaveLength(1);
|
|
115
|
+
expect(pastedEdges[0].source).toBe(pastedNodes[0].id);
|
|
116
|
+
expect(pastedEdges[0].target).toBe(pastedNodes[1].id);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('handles paste of plain text URLs', async () => {
|
|
120
|
+
const onPasteUrl = vi.fn();
|
|
121
|
+
const clipboard = createCanvasClipboard({ onPasteUrl });
|
|
122
|
+
|
|
123
|
+
const mockEvent = {
|
|
124
|
+
target: { tagName: 'DIV' },
|
|
125
|
+
clipboardData: {
|
|
126
|
+
getData: (format: string) => (format === 'text/plain' ? 'https://example.com/asset' : ''),
|
|
127
|
+
files: []
|
|
128
|
+
},
|
|
129
|
+
preventDefault: vi.fn()
|
|
130
|
+
} as unknown as ClipboardEvent;
|
|
131
|
+
|
|
132
|
+
const handled = await clipboard.handlePasteEvent(mockEvent);
|
|
133
|
+
expect(handled).toBe(true);
|
|
134
|
+
expect(onPasteUrl).toHaveBeenCalledWith('https://example.com/asset');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('handles paste of plain text', async () => {
|
|
138
|
+
const onPasteText = vi.fn();
|
|
139
|
+
const clipboard = createCanvasClipboard({ onPasteText });
|
|
140
|
+
|
|
141
|
+
const mockEvent = {
|
|
142
|
+
target: { tagName: 'DIV' },
|
|
143
|
+
clipboardData: {
|
|
144
|
+
getData: (format: string) => (format === 'text/plain' ? 'Sample text content' : ''),
|
|
145
|
+
files: []
|
|
146
|
+
},
|
|
147
|
+
preventDefault: vi.fn()
|
|
148
|
+
} as unknown as ClipboardEvent;
|
|
149
|
+
|
|
150
|
+
const handled = await clipboard.handlePasteEvent(mockEvent);
|
|
151
|
+
expect(handled).toBe(true);
|
|
152
|
+
expect(onPasteText).toHaveBeenCalledWith('Sample text content');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createCanvasDocking } from '../createCanvasDocking.svelte.js';
|
|
3
|
+
import type { CanvasNode } from '../types.js';
|
|
4
|
+
|
|
5
|
+
describe('createCanvasDocking', () => {
|
|
6
|
+
const createSampleNode = (id: string, type = 'default', x = 0, y = 0): CanvasNode => ({
|
|
7
|
+
id,
|
|
8
|
+
type,
|
|
9
|
+
position: { x, y },
|
|
10
|
+
data: {}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('initializes with null drag and connect states', () => {
|
|
14
|
+
const docking = createCanvasDocking();
|
|
15
|
+
|
|
16
|
+
expect(docking.isDragging).toBe(false);
|
|
17
|
+
expect(docking.draggedNode).toBeNull();
|
|
18
|
+
expect(docking.draggedInitialPosition).toBeNull();
|
|
19
|
+
expect(docking.connectingHandle).toBe('');
|
|
20
|
+
expect(docking.connectingNode).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('tracks drag start, records initial position, and sets isDragging', () => {
|
|
24
|
+
const docking = createCanvasDocking();
|
|
25
|
+
const node = createSampleNode('n1', 'default', 100, 200);
|
|
26
|
+
|
|
27
|
+
docking.handleNodeDragStart({ targetNode: node });
|
|
28
|
+
|
|
29
|
+
expect(docking.isDragging).toBe(true);
|
|
30
|
+
expect(docking.draggedNode).toBe(node);
|
|
31
|
+
expect(docking.draggedInitialPosition).toEqual({ x: 100, y: 200 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('fires onDockHover and onDockLeave as intersecting dock targets change', () => {
|
|
35
|
+
const onDockHover = vi.fn();
|
|
36
|
+
const onDockLeave = vi.fn();
|
|
37
|
+
const docking = createCanvasDocking({
|
|
38
|
+
isDockTarget: (n) => n.type === 'dockArea',
|
|
39
|
+
onDockHover,
|
|
40
|
+
onDockLeave
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const draggedNode = createSampleNode('dragged', 'item', 50, 50);
|
|
44
|
+
const dockNode1 = createSampleNode('dock-1', 'dockArea', 0, 0);
|
|
45
|
+
const dockNode2 = createSampleNode('dock-2', 'dockArea', 200, 200);
|
|
46
|
+
const normalNode = createSampleNode('normal', 'item', 50, 50);
|
|
47
|
+
|
|
48
|
+
docking.handleNodeDragStart({ targetNode: draggedNode });
|
|
49
|
+
|
|
50
|
+
// Drag intersecting with dockNode1 and a non-dock normalNode
|
|
51
|
+
docking.handleNodeDrag(
|
|
52
|
+
{ targetNode: draggedNode },
|
|
53
|
+
() => [dockNode1, normalNode]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(onDockHover).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(onDockHover).toHaveBeenCalledWith(dockNode1, draggedNode);
|
|
58
|
+
expect(onDockLeave).not.toHaveBeenCalled();
|
|
59
|
+
|
|
60
|
+
// Drag moves over dockNode2, leaving dockNode1
|
|
61
|
+
docking.handleNodeDrag(
|
|
62
|
+
{ targetNode: draggedNode },
|
|
63
|
+
() => [dockNode2]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(onDockLeave).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(onDockLeave).toHaveBeenCalledWith(expect.objectContaining({ id: 'dock-1' }), draggedNode);
|
|
68
|
+
expect(onDockHover).toHaveBeenCalledTimes(2);
|
|
69
|
+
expect(onDockHover).toHaveBeenLastCalledWith(dockNode2, draggedNode);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('fires onDockDrop and restores position on dock stop', () => {
|
|
73
|
+
const onDockDrop = vi.fn();
|
|
74
|
+
const onSave = vi.fn();
|
|
75
|
+
const updateNodePosition = vi.fn();
|
|
76
|
+
|
|
77
|
+
const docking = createCanvasDocking({
|
|
78
|
+
isDockTarget: (n) => n.type === 'dockArea',
|
|
79
|
+
restorePositionOnDock: true,
|
|
80
|
+
onDockDrop,
|
|
81
|
+
onSave
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const draggedNode = createSampleNode('dragged', 'item', 100, 100);
|
|
85
|
+
const dockNode = createSampleNode('dock', 'dockArea', 0, 0);
|
|
86
|
+
|
|
87
|
+
docking.handleNodeDragStart({ targetNode: draggedNode });
|
|
88
|
+
|
|
89
|
+
// Node was dragged to new position
|
|
90
|
+
draggedNode.position = { x: 300, y: 300 };
|
|
91
|
+
|
|
92
|
+
docking.handleNodeDragStop(
|
|
93
|
+
{ targetNode: draggedNode },
|
|
94
|
+
() => [dockNode],
|
|
95
|
+
updateNodePosition
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(onDockDrop).toHaveBeenCalledWith(dockNode, draggedNode);
|
|
99
|
+
expect(updateNodePosition).toHaveBeenCalledWith('dragged', { x: 100, y: 100 });
|
|
100
|
+
expect(onSave).toHaveBeenCalled();
|
|
101
|
+
expect(docking.isDragging).toBe(false);
|
|
102
|
+
expect(docking.draggedNode).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('tracks handle connect start and end', () => {
|
|
106
|
+
const docking = createCanvasDocking();
|
|
107
|
+
const node = createSampleNode('source-node');
|
|
108
|
+
const getNode = (id: string) => (id === 'source-node' ? node : undefined);
|
|
109
|
+
|
|
110
|
+
docking.handleConnectStart(
|
|
111
|
+
{ nodeId: 'source-node', handleId: 'h1', handleType: 'source' },
|
|
112
|
+
getNode
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(docking.connectingHandle).toBe('source');
|
|
116
|
+
expect(docking.connectingNode).toBe(node);
|
|
117
|
+
|
|
118
|
+
docking.handleConnectEnd();
|
|
119
|
+
|
|
120
|
+
expect(docking.connectingHandle).toBe('');
|
|
121
|
+
expect(docking.connectingNode).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('resets all state cleanly', () => {
|
|
125
|
+
const docking = createCanvasDocking();
|
|
126
|
+
const node = createSampleNode('test');
|
|
127
|
+
|
|
128
|
+
docking.handleNodeDragStart({ targetNode: node });
|
|
129
|
+
expect(docking.isDragging).toBe(true);
|
|
130
|
+
|
|
131
|
+
docking.reset();
|
|
132
|
+
expect(docking.isDragging).toBe(false);
|
|
133
|
+
expect(docking.draggedNode).toBeNull();
|
|
134
|
+
expect(docking.draggedInitialPosition).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
});
|