@node-edit-utils/core 1.2.2

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.
Files changed (102) hide show
  1. package/README.md +268 -0
  2. package/dist/index.d.ts +6 -0
  3. package/dist/lib/canvas/createCanvasObserver.d.ts +2 -0
  4. package/dist/lib/canvas/disableCanvasKeyboard.d.ts +1 -0
  5. package/dist/lib/canvas/enableCanvasKeyboard.d.ts +1 -0
  6. package/dist/lib/canvas/helpers/applyCanvasState.d.ts +1 -0
  7. package/dist/lib/canvas/helpers/getCanvasContainer.d.ts +1 -0
  8. package/dist/lib/canvas/helpers/getCanvasWindowValue.d.ts +1 -0
  9. package/dist/lib/helpers/index.d.ts +1 -0
  10. package/dist/lib/helpers/observer/connectMutationObserver.d.ts +1 -0
  11. package/dist/lib/helpers/observer/connectResizeObserver.d.ts +1 -0
  12. package/dist/lib/helpers/withRAF.d.ts +4 -0
  13. package/dist/lib/node-tools/createNodeTools.d.ts +2 -0
  14. package/dist/lib/node-tools/events/click/handleNodeClick.d.ts +1 -0
  15. package/dist/lib/node-tools/events/setupEventListener.d.ts +1 -0
  16. package/dist/lib/node-tools/highlight/clearHighlightFrame.d.ts +1 -0
  17. package/dist/lib/node-tools/highlight/createHighlightFrame.d.ts +1 -0
  18. package/dist/lib/node-tools/highlight/createTagLabel.d.ts +1 -0
  19. package/dist/lib/node-tools/highlight/createToolsContainer.d.ts +1 -0
  20. package/dist/lib/node-tools/highlight/helpers/getElementBounds.d.ts +6 -0
  21. package/dist/lib/node-tools/highlight/helpers/getHighlightFrameElement.d.ts +1 -0
  22. package/dist/lib/node-tools/highlight/highlightNode.d.ts +1 -0
  23. package/dist/lib/node-tools/highlight/refreshHighlightFrame.d.ts +1 -0
  24. package/dist/lib/node-tools/select/constants.d.ts +1 -0
  25. package/dist/lib/node-tools/select/helpers/getElementsFromPoint.d.ts +1 -0
  26. package/dist/lib/node-tools/select/helpers/targetSameCandidates.d.ts +1 -0
  27. package/dist/lib/node-tools/select/selectNode.d.ts +1 -0
  28. package/dist/lib/node-tools/text/events/setupKeydownHandler.d.ts +1 -0
  29. package/dist/lib/node-tools/text/events/setupMutationObserver.d.ts +1 -0
  30. package/dist/lib/node-tools/text/events/setupNodeListeners.d.ts +1 -0
  31. package/dist/lib/node-tools/text/helpers/hasTextContent.d.ts +1 -0
  32. package/dist/lib/node-tools/text/helpers/insertLineBreak.d.ts +1 -0
  33. package/dist/lib/node-tools/text/helpers/makeNodeEditable.d.ts +1 -0
  34. package/dist/lib/node-tools/text/helpers/makeNodeNonEditable.d.ts +1 -0
  35. package/dist/lib/node-tools/text/nodeText.d.ts +2 -0
  36. package/dist/lib/post-message/handlePostMessage.d.ts +1 -0
  37. package/dist/lib/post-message/sendPostMessage.d.ts +1 -0
  38. package/dist/lib/viewport/constants.d.ts +5 -0
  39. package/dist/lib/viewport/createViewport.d.ts +2 -0
  40. package/dist/lib/viewport/events/setupEventListener.d.ts +1 -0
  41. package/dist/lib/viewport/resize/createResizeHandle.d.ts +1 -0
  42. package/dist/lib/viewport/width/calcConstrainedWidth.d.ts +1 -0
  43. package/dist/lib/viewport/width/calcWidth.d.ts +1 -0
  44. package/dist/lib/viewport/width/updateWidth.d.ts +1 -0
  45. package/dist/lib/window/bindToWindow.d.ts +1 -0
  46. package/dist/node-edit-utils.cjs.js +588 -0
  47. package/dist/node-edit-utils.esm.js +584 -0
  48. package/dist/node-edit-utils.umd.js +594 -0
  49. package/dist/node-edit-utils.umd.min.js +1 -0
  50. package/dist/styles.css +1 -0
  51. package/dist/umd.d.ts +1 -0
  52. package/package.json +65 -0
  53. package/src/index.ts +9 -0
  54. package/src/lib/canvas/createCanvasObserver.ts +37 -0
  55. package/src/lib/canvas/disableCanvasKeyboard.ts +7 -0
  56. package/src/lib/canvas/enableCanvasKeyboard.ts +7 -0
  57. package/src/lib/canvas/helpers/applyCanvasState.ts +11 -0
  58. package/src/lib/canvas/helpers/getCanvasContainer.ts +3 -0
  59. package/src/lib/canvas/helpers/getCanvasWindowValue.ts +5 -0
  60. package/src/lib/canvas/types.d.ts +3 -0
  61. package/src/lib/helpers/index.ts +1 -0
  62. package/src/lib/helpers/observer/connectMutationObserver.ts +12 -0
  63. package/src/lib/helpers/observer/connectResizeObserver.ts +8 -0
  64. package/src/lib/helpers/withRAF.ts +39 -0
  65. package/src/lib/node-tools/createNodeTools.ts +88 -0
  66. package/src/lib/node-tools/events/click/handleNodeClick.ts +21 -0
  67. package/src/lib/node-tools/events/setupEventListener.ts +35 -0
  68. package/src/lib/node-tools/highlight/clearHighlightFrame.ts +12 -0
  69. package/src/lib/node-tools/highlight/createHighlightFrame.ts +37 -0
  70. package/src/lib/node-tools/highlight/createTagLabel.ts +7 -0
  71. package/src/lib/node-tools/highlight/createToolsContainer.ts +10 -0
  72. package/src/lib/node-tools/highlight/helpers/getElementBounds.ts +31 -0
  73. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +5 -0
  74. package/src/lib/node-tools/highlight/highlightNode.ts +23 -0
  75. package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +23 -0
  76. package/src/lib/node-tools/select/constants.ts +1 -0
  77. package/src/lib/node-tools/select/helpers/getElementsFromPoint.ts +16 -0
  78. package/src/lib/node-tools/select/helpers/targetSameCandidates.ts +2 -0
  79. package/src/lib/node-tools/select/selectNode.ts +44 -0
  80. package/src/lib/node-tools/text/events/setupKeydownHandler.ts +18 -0
  81. package/src/lib/node-tools/text/events/setupMutationObserver.ts +10 -0
  82. package/src/lib/node-tools/text/events/setupNodeListeners.ts +20 -0
  83. package/src/lib/node-tools/text/helpers/hasTextContent.ts +3 -0
  84. package/src/lib/node-tools/text/helpers/insertLineBreak.ts +17 -0
  85. package/src/lib/node-tools/text/helpers/makeNodeEditable.ts +5 -0
  86. package/src/lib/node-tools/text/helpers/makeNodeNonEditable.ts +5 -0
  87. package/src/lib/node-tools/text/nodeText.ts +67 -0
  88. package/src/lib/node-tools/text/types.d.ts +6 -0
  89. package/src/lib/node-tools/types.d.ts +12 -0
  90. package/src/lib/post-message/handlePostMessage.ts +8 -0
  91. package/src/lib/post-message/sendPostMessage.ts +11 -0
  92. package/src/lib/styles/styles.css +133 -0
  93. package/src/lib/viewport/constants.ts +6 -0
  94. package/src/lib/viewport/createViewport.ts +70 -0
  95. package/src/lib/viewport/events/setupEventListener.ts +20 -0
  96. package/src/lib/viewport/resize/createResizeHandle.ts +9 -0
  97. package/src/lib/viewport/types.d.ts +8 -0
  98. package/src/lib/viewport/width/calcConstrainedWidth.ts +6 -0
  99. package/src/lib/viewport/width/calcWidth.ts +9 -0
  100. package/src/lib/viewport/width/updateWidth.ts +3 -0
  101. package/src/lib/window/bindToWindow.ts +6 -0
  102. package/src/umd.ts +1 -0
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ import "@/lib/styles/styles.css";
2
+
3
+ export { createCanvasObserver } from "@/lib/canvas/createCanvasObserver";
4
+
5
+ export { createNodeTools } from "@/lib/node-tools/createNodeTools";
6
+ export type { NodeTools, NodeToolsRef } from "@/lib/node-tools/types";
7
+
8
+ export { createViewport } from "@/lib/viewport/createViewport";
9
+ export type { Viewport, ViewportRef } from "@/lib/viewport/types";
@@ -0,0 +1,37 @@
1
+ import { withRAFThrottle } from "../helpers/withRAF";
2
+ import { applyCanvasState } from "./helpers/applyCanvasState";
3
+ import type { CanvasObserver } from "./types";
4
+
5
+ export function createCanvasObserver(): CanvasObserver {
6
+ const transformLayer = document.querySelector(".transform-layer");
7
+
8
+ if (!transformLayer) {
9
+ return {
10
+ disconnect: () => {},
11
+ };
12
+ }
13
+
14
+ const throttledUpdate = withRAFThrottle(() => {
15
+ applyCanvasState();
16
+ });
17
+
18
+ const observer = new MutationObserver(() => {
19
+ throttledUpdate();
20
+ });
21
+
22
+ observer.observe(transformLayer, {
23
+ attributes: true,
24
+ attributeOldValue: true,
25
+ subtree: true,
26
+ childList: true,
27
+ });
28
+
29
+ function disconnect(): void {
30
+ throttledUpdate.cleanup();
31
+ observer.disconnect();
32
+ }
33
+
34
+ return {
35
+ disconnect,
36
+ };
37
+ }
@@ -0,0 +1,7 @@
1
+ import { getCanvasWindowValue } from "@/lib/canvas/helpers/getCanvasWindowValue";
2
+
3
+ export const disableCanvasKeyboard = () => {
4
+ const disable = getCanvasWindowValue(["keyboard", "disable"]);
5
+
6
+ disable?.();
7
+ };
@@ -0,0 +1,7 @@
1
+ import { getCanvasWindowValue } from "@/lib/canvas/helpers/getCanvasWindowValue";
2
+
3
+ export const enableCanvasKeyboard = () => {
4
+ const enable = getCanvasWindowValue(["keyboard", "enable"]);
5
+
6
+ enable?.();
7
+ };
@@ -0,0 +1,11 @@
1
+ import { getCanvasWindowValue } from "./getCanvasWindowValue";
2
+
3
+ export const applyCanvasState = () => {
4
+ const zoom: number = getCanvasWindowValue(["zoom", "current"]);
5
+
6
+ document.body.style.setProperty("--zoom", zoom.toFixed(5));
7
+ document.body.style.setProperty("--stroke-width", (2 / zoom).toFixed(3));
8
+
9
+ document.body.dataset.zoom = zoom.toFixed(5);
10
+ document.body.dataset.strokeWidth = (2 / zoom).toFixed(3);
11
+ };
@@ -0,0 +1,3 @@
1
+ export const getCanvasContainer = (): HTMLElement | null => {
2
+ return document.querySelector(".canvas-container");
3
+ };
@@ -0,0 +1,5 @@
1
+ export const getCanvasWindowValue = (path: string[]) => {
2
+ // biome-ignore lint/suspicious/noExplicitAny: global window extension
3
+ const canvas = (window as any).canvas;
4
+ return path.reduce((obj, prop) => obj?.[prop], canvas);
5
+ };
@@ -0,0 +1,3 @@
1
+ export interface CanvasObserver {
2
+ disconnect: () => void;
3
+ }
@@ -0,0 +1 @@
1
+ export { withRAF, withRAFThrottle } from "./withRAF";
@@ -0,0 +1,12 @@
1
+ export const connectMutationObserver = (element: HTMLElement, handler: (mutations: MutationRecord[]) => void): MutationObserver => {
2
+ const mutationObserver = new MutationObserver((mutations) => {
3
+ handler(mutations);
4
+ });
5
+ mutationObserver.observe(element, {
6
+ subtree: true,
7
+ childList: true,
8
+ characterData: true,
9
+ });
10
+
11
+ return mutationObserver;
12
+ };
@@ -0,0 +1,8 @@
1
+ export const connectResizeObserver = (element: HTMLElement, handler: (entries: ResizeObserverEntry[]) => void): ResizeObserver => {
2
+ const resizeObserver = new ResizeObserver((entries) => {
3
+ handler(entries);
4
+ });
5
+ resizeObserver.observe(element);
6
+
7
+ return resizeObserver;
8
+ };
@@ -0,0 +1,39 @@
1
+ export function withRAF(operation: () => void): () => void {
2
+ const rafId = requestAnimationFrame(() => {
3
+ operation();
4
+ });
5
+
6
+ return () => {
7
+ cancelAnimationFrame(rafId);
8
+ };
9
+ }
10
+
11
+ // biome-ignore lint/suspicious/noExplicitAny: generic constraint requires flexibility
12
+ export function withRAFThrottle<T extends (...args: any[]) => void>(func: T): T & { cleanup: () => void } {
13
+ let rafId: number | null = null;
14
+ let lastArgs: Parameters<T> | null = null;
15
+
16
+ const throttled = (...args: Parameters<T>) => {
17
+ lastArgs = args;
18
+
19
+ if (rafId === null) {
20
+ rafId = requestAnimationFrame(() => {
21
+ if (lastArgs) {
22
+ func(...lastArgs);
23
+ }
24
+ rafId = null;
25
+ lastArgs = null;
26
+ });
27
+ }
28
+ };
29
+
30
+ throttled.cleanup = () => {
31
+ if (rafId !== null) {
32
+ cancelAnimationFrame(rafId);
33
+ rafId = null;
34
+ lastArgs = null;
35
+ }
36
+ };
37
+
38
+ return throttled as T & { cleanup: () => void };
39
+ }
@@ -0,0 +1,88 @@
1
+ import { withRAFThrottle } from "../helpers";
2
+ import { connectResizeObserver } from "../helpers/observer/connectResizeObserver";
3
+ import { sendPostMessage } from "../post-message/sendPostMessage";
4
+ import { bindToWindow } from "../window/bindToWindow";
5
+ import { setupEventListener } from "./events/setupEventListener";
6
+ import { clearHighlightFrame } from "./highlight/clearHighlightFrame";
7
+ import { highlightNode } from "./highlight/highlightNode";
8
+ import { refreshHighlightFrame } from "./highlight/refreshHighlightFrame";
9
+ import { nodeText } from "./text/nodeText";
10
+ import type { NodeTools } from "./types";
11
+
12
+ export const createNodeTools = (element: HTMLElement | null): NodeTools => {
13
+ const nodeProvider = element;
14
+
15
+ let resizeObserver: ResizeObserver | null = null;
16
+ let selectedNode: HTMLElement | null = null;
17
+
18
+ const text = nodeText();
19
+ const throttledFrameRefresh = withRAFThrottle(refreshHighlightFrame);
20
+
21
+ const handleEscape = (): void => {
22
+ if (text.isEditing()) {
23
+ text.blurEditMode();
24
+ }
25
+
26
+ if (selectedNode) {
27
+ if (nodeProvider) {
28
+ clearHighlightFrame(nodeProvider);
29
+ selectedNode = null;
30
+
31
+ resizeObserver?.disconnect();
32
+ }
33
+ }
34
+ };
35
+
36
+ const selectNode = (node: HTMLElement | null): void => {
37
+ if (text.isEditing()) {
38
+ const currentEditable = text.getEditableNode();
39
+ if (currentEditable && currentEditable !== node) {
40
+ text.blurEditMode();
41
+ }
42
+ }
43
+
44
+ resizeObserver?.disconnect();
45
+
46
+ if (node && nodeProvider) {
47
+ text.enableEditMode(node, nodeProvider);
48
+
49
+ resizeObserver = connectResizeObserver(nodeProvider, () => {
50
+ throttledFrameRefresh(node, nodeProvider);
51
+ });
52
+ }
53
+
54
+ selectedNode = node;
55
+ sendPostMessage("selectedNodeChanged", node?.getAttribute("data-layer-id") ?? null);
56
+ highlightNode(node, nodeProvider as HTMLElement) ?? null;
57
+ };
58
+
59
+ // Setup event listener
60
+ const removeListeners = setupEventListener(nodeProvider, selectNode, handleEscape, text.getEditableNode);
61
+
62
+ const cleanup = (): void => {
63
+ removeListeners();
64
+ resizeObserver?.disconnect();
65
+
66
+ text.blurEditMode();
67
+ throttledFrameRefresh.cleanup();
68
+ };
69
+
70
+ const nodeTools: NodeTools = {
71
+ selectNode,
72
+ getSelectedNode: () => selectedNode,
73
+ refreshHighlightFrame: () => {
74
+ throttledFrameRefresh(selectedNode as HTMLElement, nodeProvider as HTMLElement);
75
+ },
76
+ clearSelectedNode: () => {
77
+ clearHighlightFrame(nodeProvider);
78
+ selectedNode = null;
79
+ resizeObserver?.disconnect();
80
+ },
81
+ getEditableNode: () => text.getEditableNode(),
82
+ cleanup,
83
+ };
84
+
85
+ bindToWindow("nodeTools", nodeTools);
86
+
87
+ return nodeTools;
88
+ };
@@ -0,0 +1,21 @@
1
+ import { clearHighlightFrame } from "../../highlight/clearHighlightFrame";
2
+ import { selectNode } from "../../select/selectNode";
3
+
4
+ export const handleNodeClick = (
5
+ event: MouseEvent,
6
+ nodeProvider: HTMLElement | null,
7
+ editableNode: HTMLElement | null,
8
+ onNodeSelected: (node: HTMLElement | null) => void
9
+ ): void => {
10
+ event.preventDefault();
11
+ event.stopPropagation();
12
+
13
+ if (nodeProvider && !nodeProvider.contains(event.target as Node)) {
14
+ clearHighlightFrame(nodeProvider);
15
+ onNodeSelected(null);
16
+ return;
17
+ }
18
+
19
+ const selectedNode = selectNode(event, editableNode);
20
+ onNodeSelected(selectedNode);
21
+ };
@@ -0,0 +1,35 @@
1
+ import { handlePostMessage } from "../../post-message/handlePostMessage";
2
+ import { handleNodeClick } from "./click/handleNodeClick";
3
+
4
+ export const setupEventListener = (
5
+ nodeProvider: HTMLElement | null,
6
+ onNodeSelected: (node: HTMLElement | null) => void,
7
+ onEscapePressed: () => void,
8
+ getEditableNode: () => HTMLElement | null
9
+ ): (() => void) => {
10
+ const messageHandler = (event: MessageEvent) => {
11
+ handlePostMessage(event);
12
+ };
13
+
14
+ const documentClickHandler = (event: MouseEvent) => {
15
+ handleNodeClick(event, nodeProvider, getEditableNode(), onNodeSelected);
16
+ };
17
+
18
+ const documentKeydownHandler = (event: KeyboardEvent) => {
19
+ if (event.key === "Escape") {
20
+ event.preventDefault();
21
+ event.stopPropagation();
22
+ onEscapePressed?.();
23
+ }
24
+ };
25
+
26
+ window.addEventListener("message", messageHandler);
27
+ document.addEventListener("click", documentClickHandler);
28
+ document.addEventListener("keydown", documentKeydownHandler);
29
+
30
+ return () => {
31
+ window.removeEventListener("message", messageHandler);
32
+ document.removeEventListener("click", documentClickHandler);
33
+ document.removeEventListener("keydown", documentKeydownHandler);
34
+ };
35
+ };
@@ -0,0 +1,12 @@
1
+ import { getHighlightFrameElement } from "./helpers/getHighlightFrameElement";
2
+
3
+ export const clearHighlightFrame = (nodeProvider: HTMLElement | null): void => {
4
+ if (!nodeProvider) {
5
+ return;
6
+ }
7
+
8
+ const highlightFrame = getHighlightFrameElement(nodeProvider);
9
+ if (highlightFrame) {
10
+ highlightFrame.remove();
11
+ }
12
+ };
@@ -0,0 +1,37 @@
1
+ import { getCanvasWindowValue } from "@/lib/canvas/helpers/getCanvasWindowValue";
2
+ import { getElementBounds } from "./helpers/getElementBounds";
3
+
4
+ export const createHighlightFrame = (node: HTMLElement, nodeProvider: HTMLElement): HTMLElement => {
5
+ const { top, left, width, height } = getElementBounds(node, nodeProvider);
6
+
7
+ const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
8
+
9
+ document.body.style.setProperty("--zoom", zoom.toString());
10
+ document.body.style.setProperty("--stroke-width", (2 / zoom).toFixed(3));
11
+
12
+ const frame = document.createElement("div");
13
+ frame.classList.add("highlight-frame");
14
+
15
+ frame.style.setProperty("--frame-top", `${top}px`);
16
+ frame.style.setProperty("--frame-left", `${left}px`);
17
+ frame.style.setProperty("--frame-width", `${width}px`);
18
+ frame.style.setProperty("--frame-height", `${height}px`);
19
+
20
+ // Create SVG overlay for outline
21
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
22
+ svg.classList.add("highlight-frame-svg");
23
+
24
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
25
+ rect.setAttribute("x", "0");
26
+ rect.setAttribute("y", "0");
27
+ rect.setAttribute("width", "100%");
28
+ rect.setAttribute("height", "100%");
29
+ rect.classList.add("highlight-frame-rect");
30
+
31
+ svg.appendChild(rect);
32
+ frame.appendChild(svg);
33
+
34
+ const highlightFrame = frame;
35
+
36
+ return highlightFrame;
37
+ };
@@ -0,0 +1,7 @@
1
+ export const createTagLabel = (node: HTMLElement, nodeTools: HTMLElement): void => {
2
+ const tagLabel = document.createElement("div");
3
+ tagLabel.className = "tag-label";
4
+ tagLabel.textContent = node.tagName.toLowerCase();
5
+
6
+ nodeTools.appendChild(tagLabel);
7
+ };
@@ -0,0 +1,10 @@
1
+ import { createTagLabel } from "./createTagLabel";
2
+
3
+ export const createToolsContainer = (node: HTMLElement, highlightFrame: HTMLElement): void => {
4
+ const nodeTools = document.createElement("div");
5
+
6
+ nodeTools.className = "node-tools";
7
+ highlightFrame.appendChild(nodeTools);
8
+
9
+ createTagLabel(node, nodeTools);
10
+ };
@@ -0,0 +1,31 @@
1
+ import { getCanvasWindowValue } from "@/lib/canvas/helpers/getCanvasWindowValue";
2
+
3
+ export function getElementBounds(
4
+ element: Element,
5
+ nodeProvider: HTMLElement
6
+ ): {
7
+ top: number;
8
+ left: number;
9
+ width: number;
10
+ height: number;
11
+ } {
12
+ const elementRect = element.getBoundingClientRect();
13
+ const componentRootRect = nodeProvider.getBoundingClientRect();
14
+
15
+ const relativeTop = elementRect.top - componentRootRect.top;
16
+ const relativeLeft = elementRect.left - componentRootRect.left;
17
+
18
+ const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
19
+
20
+ const top = parseFloat((relativeTop / zoom).toFixed(5));
21
+ const left = parseFloat((relativeLeft / zoom).toFixed(5));
22
+ const width = Math.max(4, parseFloat((elementRect.width / zoom).toFixed(5)));
23
+ const height = parseFloat((elementRect.height / zoom).toFixed(5));
24
+
25
+ return {
26
+ top,
27
+ left,
28
+ width,
29
+ height,
30
+ };
31
+ }
@@ -0,0 +1,5 @@
1
+ export function getHighlightFrameElement(
2
+ nodeProvider: HTMLElement
3
+ ): HTMLElement | null {
4
+ return nodeProvider.querySelector(".highlight-frame");
5
+ }
@@ -0,0 +1,23 @@
1
+ import { createHighlightFrame } from "./createHighlightFrame";
2
+ import { createToolsContainer } from "./createToolsContainer";
3
+ import { getHighlightFrameElement } from "./helpers/getHighlightFrameElement";
4
+
5
+ export const highlightNode = (node: HTMLElement | null, nodeProvider: HTMLElement): void => {
6
+ if (!node) return;
7
+
8
+ const existingHighlightFrame = getHighlightFrameElement(nodeProvider);
9
+
10
+ if (existingHighlightFrame) {
11
+ existingHighlightFrame.remove();
12
+ }
13
+
14
+ const highlightFrame = createHighlightFrame(node, nodeProvider);
15
+
16
+ if (node.contentEditable === "true") {
17
+ highlightFrame.classList.add("is-editable");
18
+ }
19
+
20
+ createToolsContainer(node, highlightFrame);
21
+
22
+ nodeProvider.appendChild(highlightFrame);
23
+ };
@@ -0,0 +1,23 @@
1
+ import { getCanvasWindowValue } from "@/lib/canvas/helpers/getCanvasWindowValue";
2
+ import { getElementBounds } from "./helpers/getElementBounds";
3
+ import { getHighlightFrameElement } from "./helpers/getHighlightFrameElement";
4
+
5
+ export const refreshHighlightFrame = (node: HTMLElement, nodeProvider: HTMLElement) => {
6
+ const frame = getHighlightFrameElement(nodeProvider);
7
+ const zoom = getCanvasWindowValue(["zoom", "current"]) ?? 1;
8
+
9
+ if (!frame) return;
10
+
11
+ if (zoom >= 0.3) {
12
+ nodeProvider.style.setProperty("--tool-opacity", `1`);
13
+ } else {
14
+ nodeProvider.style.setProperty("--tool-opacity", `0`);
15
+ }
16
+
17
+ const { top, left, width, height } = getElementBounds(node, nodeProvider);
18
+
19
+ frame.style.setProperty("--frame-top", `${top}px`);
20
+ frame.style.setProperty("--frame-left", `${left}px`);
21
+ frame.style.setProperty("--frame-width", `${width}px`);
22
+ frame.style.setProperty("--frame-height", `${height}px`);
23
+ };
@@ -0,0 +1 @@
1
+ export const IGNORED_DOM_ELEMENTS = ["path", "rect", "circle", "ellipse", "polygon", "line", "polyline", "text", "text-noci"];
@@ -0,0 +1,16 @@
1
+ export const getElementsFromPoint = (clickX: number, clickY: number): Element[] => {
2
+ const elements = document.elementsFromPoint(clickX, clickY);
3
+
4
+ return Array.from(elements).reduce(
5
+ (acc: { elements: Element[]; found: boolean }, el) => {
6
+ if (acc.found) return acc;
7
+ if (el.getAttribute("data-role") === "node-provider") {
8
+ acc.found = true;
9
+ return acc;
10
+ }
11
+ acc.elements.push(el);
12
+ return acc;
13
+ },
14
+ { elements: [], found: false }
15
+ ).elements;
16
+ };
@@ -0,0 +1,2 @@
1
+ export const targetSameCandidates = (cache: Element[], current: Element[]): boolean =>
2
+ cache.length === current.length && cache.every((el, i) => el === current[i]);
@@ -0,0 +1,44 @@
1
+ import { IGNORED_DOM_ELEMENTS } from "./constants";
2
+ import { getElementsFromPoint } from "./helpers/getElementsFromPoint";
3
+ import { targetSameCandidates } from "./helpers/targetSameCandidates";
4
+
5
+ let candidateCache: Element[] = [];
6
+ let attempt = 0;
7
+
8
+ export const selectNode = (event: MouseEvent, editableNode: HTMLElement | null): HTMLElement | null => {
9
+ let selectedNode: HTMLElement | null = null;
10
+
11
+ const clickX = event.clientX;
12
+ const clickY = event.clientY;
13
+
14
+ const clickThrough = event.metaKey || event.ctrlKey;
15
+
16
+ const candidates = getElementsFromPoint(clickX, clickY).filter(
17
+ (element) => !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase())
18
+ );
19
+
20
+ if (editableNode && candidates.includes(editableNode)) {
21
+ return editableNode;
22
+ }
23
+
24
+ if (clickThrough) {
25
+ candidateCache = [];
26
+
27
+ selectedNode = candidates[0] as HTMLElement;
28
+ return selectedNode;
29
+ }
30
+
31
+ if (targetSameCandidates(candidateCache, candidates)) {
32
+ attempt <= candidates.length && attempt++;
33
+ } else {
34
+ attempt = 0;
35
+ }
36
+
37
+ const nodeIndex = candidates.length - 1 - attempt;
38
+
39
+ selectedNode = candidates[nodeIndex] as HTMLElement;
40
+
41
+ candidateCache = candidates;
42
+
43
+ return selectedNode;
44
+ };
@@ -0,0 +1,18 @@
1
+ import { insertLineBreak } from "../helpers/insertLineBreak";
2
+
3
+ export const setupKeydownHandler = (node: HTMLElement): (() => void) => {
4
+ const keydownHandler = (event: KeyboardEvent) => {
5
+ if (event.key === "Enter") {
6
+ event.preventDefault();
7
+ event.stopPropagation();
8
+
9
+ insertLineBreak();
10
+ }
11
+ };
12
+
13
+ node.addEventListener("keydown", keydownHandler);
14
+
15
+ return () => {
16
+ node.removeEventListener("keydown", keydownHandler);
17
+ };
18
+ };
@@ -0,0 +1,10 @@
1
+ import { connectMutationObserver } from "../../../helpers/observer/connectMutationObserver";
2
+ import { refreshHighlightFrame } from "../../highlight/refreshHighlightFrame";
3
+
4
+ export const setupMutationObserver = (node: HTMLElement, nodeProvider: HTMLElement): (() => void) | undefined => {
5
+ const mutationObserver = connectMutationObserver(node, () => {
6
+ refreshHighlightFrame(node, nodeProvider);
7
+ });
8
+
9
+ return mutationObserver.disconnect;
10
+ };
@@ -0,0 +1,20 @@
1
+ import { setupKeydownHandler } from "./setupKeydownHandler";
2
+ import { setupMutationObserver } from "./setupMutationObserver";
3
+
4
+ export const setupNodeListeners = (node: HTMLElement, nodeProvider: HTMLElement | null, blur: () => void): (() => void) => {
5
+ if (!nodeProvider) {
6
+ return () => {};
7
+ }
8
+
9
+ node.addEventListener("blur", blur);
10
+
11
+ const keydownCleanup = setupKeydownHandler(node);
12
+
13
+ const mutationCleanup = setupMutationObserver(node, nodeProvider);
14
+
15
+ return () => {
16
+ node.removeEventListener("blur", blur);
17
+ keydownCleanup();
18
+ mutationCleanup?.();
19
+ };
20
+ };
@@ -0,0 +1,3 @@
1
+ export const hasTextContent = (node: HTMLElement): boolean => {
2
+ return Array.from(node.childNodes).some((child) => child.nodeType === Node.TEXT_NODE && child.textContent?.trim());
3
+ };
@@ -0,0 +1,17 @@
1
+ export const insertLineBreak = (): void => {
2
+ const selection = window.getSelection();
3
+
4
+ if (selection && selection.rangeCount > 0) {
5
+ const range = selection.getRangeAt(0);
6
+ range.deleteContents();
7
+
8
+ const br = document.createElement("br");
9
+ range.insertNode(br);
10
+
11
+ // Move cursor after the br
12
+ range.setStartAfter(br);
13
+ range.setEndAfter(br);
14
+ selection.removeAllRanges();
15
+ selection.addRange(range);
16
+ }
17
+ };
@@ -0,0 +1,5 @@
1
+ export const makeNodeEditable = (node: HTMLElement) => {
2
+ node.contentEditable = "true";
3
+ node.classList.add("is-editable");
4
+ node.style.outline = "none";
5
+ };
@@ -0,0 +1,5 @@
1
+ export const makeNodeNonEditable = (node: HTMLElement) => {
2
+ node.contentEditable = "false";
3
+ node.classList.remove("is-editable");
4
+ node.style.outline = "none";
5
+ };