From 5dc276af52994f83ecd96e86ddcf8dd1a339ac80 Mon Sep 17 00:00:00 2001 From: Braks <78412429+bcakmakoglu@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:46:48 +0200 Subject: [PATCH] docs(examples): helper lines (#1915) feat: add optional chaining to prevent types errors and add an example of helper lines (#1904) * docs: code errors in "Updating Node Data" (#1900) fix(docs): Example of fixing code errors in "Updating Node Data" * fix: add optional chaining to prevent errors in link filtering * feat: add an example of helper lines * refactor: In the HelperLines example, replace the observer with dimensions --------- Co-authored-by: Charles Lee <114982593+rookie-orange@users.noreply.github.com> Co-authored-by: J --- docs/examples/helper-lines/App.vue | 46 +++++ docs/examples/helper-lines/HelperLines.vue | 72 ++++++++ docs/examples/helper-lines/index.ts | 5 + docs/examples/helper-lines/initialElements.ts | 36 ++++ docs/examples/helper-lines/style.css | 14 ++ docs/examples/helper-lines/utils.ts | 163 ++++++++++++++++++ docs/examples/index.ts | 14 ++ docs/src/.vitepress/config.mts | 3 +- docs/src/examples/helper-lines.md | 7 + 9 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 docs/examples/helper-lines/App.vue create mode 100644 docs/examples/helper-lines/HelperLines.vue create mode 100644 docs/examples/helper-lines/index.ts create mode 100644 docs/examples/helper-lines/initialElements.ts create mode 100644 docs/examples/helper-lines/style.css create mode 100644 docs/examples/helper-lines/utils.ts create mode 100644 docs/src/examples/helper-lines.md diff --git a/docs/examples/helper-lines/App.vue b/docs/examples/helper-lines/App.vue new file mode 100644 index 00000000..900cef92 --- /dev/null +++ b/docs/examples/helper-lines/App.vue @@ -0,0 +1,46 @@ + + + diff --git a/docs/examples/helper-lines/HelperLines.vue b/docs/examples/helper-lines/HelperLines.vue new file mode 100644 index 00000000..184b44ce --- /dev/null +++ b/docs/examples/helper-lines/HelperLines.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/docs/examples/helper-lines/index.ts b/docs/examples/helper-lines/index.ts new file mode 100644 index 00000000..76ebcd16 --- /dev/null +++ b/docs/examples/helper-lines/index.ts @@ -0,0 +1,5 @@ +export { default as HelperLinesApp } from './App.vue?raw' +export { default as HelperLinesUtils } from './utils.ts?raw' +export { default as HelperLinesInitialElements } from './initialElements.ts?raw' +export { default as HelperLinesStyle } from './style.css?inline' +export { default as HelperLinesComponent } from './HelperLines.vue?raw' diff --git a/docs/examples/helper-lines/initialElements.ts b/docs/examples/helper-lines/initialElements.ts new file mode 100644 index 00000000..ebbf3fca --- /dev/null +++ b/docs/examples/helper-lines/initialElements.ts @@ -0,0 +1,36 @@ +import type { Edge, Node } from '@vue-flow/core' + +export const initialNodes: Node[] = [ + { + id: '1', + position: { x: 0, y: 0 }, + style: { width: '200px', height: '100px', backgroundColor: '#D6E4F0' }, + data: { label: 'Move me around' }, + }, + { + id: '2', + position: { x: 400, y: 300 }, + style: { width: '220px', height: '400px', backgroundColor: '#5CD882' }, + data: { label: 'Move me around' }, + }, + { + id: '3', + position: { x: -55, y: 220 }, + style: { width: '125px', height: '220px', backgroundColor: '#F6E5E5' }, + data: { label: 'Move me around' }, + }, + { + id: '4', + position: { x: 250, y: -200 }, + style: { width: '180px', height: '180px', backgroundColor: '#FF0071' }, + data: { label: 'Move me around' }, + }, + { + id: '5', + position: { x: -120, y: 500 }, + style: { width: '300px', height: '120px', backgroundColor: '#FBC2EB' }, + data: { label: 'Move me around' }, + }, +] + +export const initialEdges: Edge[] = [] diff --git a/docs/examples/helper-lines/style.css b/docs/examples/helper-lines/style.css new file mode 100644 index 00000000..e489f7f7 --- /dev/null +++ b/docs/examples/helper-lines/style.css @@ -0,0 +1,14 @@ +.vue-flow__node { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + padding: 10px; + font-size: 12px; + font-weight: 700; + flex-direction: column; +} + +.vue-flow__handle { + visibility: hidden; +} diff --git a/docs/examples/helper-lines/utils.ts b/docs/examples/helper-lines/utils.ts new file mode 100644 index 00000000..b01742f1 --- /dev/null +++ b/docs/examples/helper-lines/utils.ts @@ -0,0 +1,163 @@ +import type { GraphNode, NodePositionChange, XYPosition } from '@vue-flow/core' + +interface GetHelperLinesResult { + horizontal?: number + vertical?: number + snapPosition: Partial +} + +// this utility function can be called with a position change (inside onNodesChange) +// it checks all other nodes and calculated the helper line positions and the position where the current node should snap to +export function getHelperLines(change: NodePositionChange, nodes: GraphNode[], distance = 5): GetHelperLinesResult { + const defaultResult = { + horizontal: undefined, + vertical: undefined, + snapPosition: { x: undefined, y: undefined }, + } + const nodeA = nodes.find((node) => node.id === change.id) + + if (!nodeA || !change.position) { + return defaultResult + } + + const nodeABounds = { + left: change.position.x, + right: change.position.x + ((nodeA.dimensions.width as number) ?? 0), + top: change.position.y, + bottom: change.position.y + ((nodeA.dimensions.height as number) ?? 0), + width: (nodeA.dimensions.width as number) ?? 0, + height: (nodeA.dimensions.height as number) ?? 0, + } + + let horizontalDistance = distance + let verticalDistance = distance + + return nodes + .filter((node) => node.id !== nodeA.id) + .reduce((result, nodeB) => { + const nodeBBounds = { + left: nodeB.position.x, + right: nodeB.position.x + ((nodeB.dimensions.width as number) ?? 0), + top: nodeB.position.y, + bottom: nodeB.position.y + ((nodeB.dimensions.height as number) ?? 0), + width: nodeB.width ?? 0, + height: nodeB.height ?? 0, + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________| + // | + // | + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // |___________| + const distanceLeftLeft = Math.abs(nodeABounds.left - nodeBBounds.left) + + if (distanceLeftLeft < verticalDistance) { + result.snapPosition.x = nodeBBounds.left + result.vertical = nodeBBounds.left + verticalDistance = distanceLeftLeft + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________| + // | + // | + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // |___________| + const distanceRightRight = Math.abs(nodeABounds.right - nodeBBounds.right) + + if (distanceRightRight < verticalDistance) { + result.snapPosition.x = nodeBBounds.right - nodeABounds.width + result.vertical = nodeBBounds.right + verticalDistance = distanceRightRight + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________| + // | + // | + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // |___________| + const distanceLeftRight = Math.abs(nodeABounds.left - nodeBBounds.right) + + if (distanceLeftRight < verticalDistance) { + result.snapPosition.x = nodeBBounds.right + result.vertical = nodeBBounds.right + verticalDistance = distanceLeftRight + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________| + // | + // | + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // |___________| + const distanceRightLeft = Math.abs(nodeABounds.right - nodeBBounds.left) + + if (distanceRightLeft < verticalDistance) { + result.snapPosition.x = nodeBBounds.left - nodeABounds.width + result.vertical = nodeBBounds.left + verticalDistance = distanceRightLeft + } + + // |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾| + // | A | | B | + // |___________| |___________| + const distanceTopTop = Math.abs(nodeABounds.top - nodeBBounds.top) + + if (distanceTopTop < horizontalDistance) { + result.snapPosition.y = nodeBBounds.top + result.horizontal = nodeBBounds.top + horizontalDistance = distanceTopTop + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________|_________________ + // | | + // | B | + // |___________| + const distanceBottomTop = Math.abs(nodeABounds.bottom - nodeBBounds.top) + + if (distanceBottomTop < horizontalDistance) { + result.snapPosition.y = nodeBBounds.top - nodeABounds.height + result.horizontal = nodeBBounds.top + horizontalDistance = distanceBottomTop + } + + // |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾| + // | A | | B | + // |___________|_____|___________| + const distanceBottomBottom = Math.abs(nodeABounds.bottom - nodeBBounds.bottom) + + if (distanceBottomBottom < horizontalDistance) { + result.snapPosition.y = nodeBBounds.bottom - nodeABounds.height + result.horizontal = nodeBBounds.bottom + horizontalDistance = distanceBottomBottom + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // | | + // |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + // | A | + // |___________| + const distanceTopBottom = Math.abs(nodeABounds.top - nodeBBounds.bottom) + + if (distanceTopBottom < horizontalDistance) { + result.snapPosition.y = nodeBBounds.bottom + result.horizontal = nodeBBounds.bottom + horizontalDistance = distanceTopBottom + } + + return result + }, defaultResult) +} diff --git a/docs/examples/index.ts b/docs/examples/index.ts index 3aaa09f3..59552fcc 100644 --- a/docs/examples/index.ts +++ b/docs/examples/index.ts @@ -24,6 +24,13 @@ import { LoopbackApp, LoopbackCSS, LoopbackEdge } from './loopback' import { MathApp, MathCSS, MathElements, MathIcon, MathOperatorNode, MathResultNode, MathValueNode } from './math' import { ConfirmApp, ConfirmDialog, useDialog } from './confirm-delete' import { EdgeMarkersApp, EdgeMarkersCSS, EdgeMarkersEdge, EdgeMarkersMarker } from './edge-markers' +import { + HelperLinesApp, + HelperLinesComponent, + HelperLinesInitialElements, + HelperLinesStyle, + HelperLinesUtils, +} from './helper-lines' export const exampleImports = { basic: { @@ -176,4 +183,11 @@ export const exampleImports = { 'CustomMarker.vue': EdgeMarkersMarker, 'style.css': EdgeMarkersCSS, }, + helperLines: { + 'App.vue': HelperLinesApp, + 'HelperLines.vue': HelperLinesComponent, + 'utils.ts': HelperLinesUtils, + 'initialElements.ts': HelperLinesInitialElements, + 'style.css': HelperLinesStyle, + }, } diff --git a/docs/src/.vitepress/config.mts b/docs/src/.vitepress/config.mts index d64eca04..675f50b6 100644 --- a/docs/src/.vitepress/config.mts +++ b/docs/src/.vitepress/config.mts @@ -31,7 +31,7 @@ function typedocSidebarEntries() { if (module === 'variables') { children = children.filter((child) => { - return child.link.includes('default') + return child.link?.includes('default') }) } @@ -218,6 +218,7 @@ export default defineConfigWithTheme({ { text: 'Viewport Transition', link: '/examples/transition' }, { text: 'Teleport Nodes', link: '/examples/teleport' }, { text: 'Stress Test', link: '/examples/stress' }, + { text: 'Helper Lines', link: '/examples/helper-lines' }, ], }, { diff --git a/docs/src/examples/helper-lines.md b/docs/src/examples/helper-lines.md new file mode 100644 index 00000000..e63babc7 --- /dev/null +++ b/docs/src/examples/helper-lines.md @@ -0,0 +1,7 @@ +# Helper Lines + +Visual guides and snapping for nodes while dragging. + +
+ +