@node-edit-utils/core 2.3.3 → 2.3.4

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 (101) hide show
  1. package/dist/lib/viewport/label/getViewportLabelOverlay.d.ts +1 -0
  2. package/dist/lib/viewport/label/index.d.ts +5 -3
  3. package/dist/lib/viewport/label/isViewportDragging.d.ts +2 -0
  4. package/dist/lib/viewport/label/refreshViewportLabel.d.ts +8 -0
  5. package/dist/lib/viewport/label/removeViewportLabel.d.ts +5 -0
  6. package/dist/lib/viewport/label/setupViewportDrag.d.ts +1 -0
  7. package/dist/node-edit-utils.cjs.js +100 -62
  8. package/dist/node-edit-utils.esm.js +100 -62
  9. package/dist/node-edit-utils.umd.js +100 -62
  10. package/dist/node-edit-utils.umd.min.js +1 -1
  11. package/dist/styles.css +1 -1
  12. package/package.json +7 -2
  13. package/src/lib/canvas/createCanvasObserver.test.ts +242 -0
  14. package/src/lib/canvas/disableCanvasKeyboard.test.ts +53 -0
  15. package/src/lib/canvas/disableCanvasKeyboard.ts +1 -1
  16. package/src/lib/canvas/disableCanvasTextMode.test.ts +53 -0
  17. package/src/lib/canvas/disableCanvasTextMode.ts +1 -1
  18. package/src/lib/canvas/enableCanvasKeyboard.test.ts +53 -0
  19. package/src/lib/canvas/enableCanvasKeyboard.ts +1 -1
  20. package/src/lib/canvas/enableCanvasTextMode.test.ts +53 -0
  21. package/src/lib/canvas/enableCanvasTextMode.ts +1 -1
  22. package/src/lib/canvas/helpers/applyCanvasState.test.ts +119 -0
  23. package/src/lib/canvas/helpers/applyCanvasState.ts +1 -1
  24. package/src/lib/canvas/helpers/getCanvasContainer.test.ts +62 -0
  25. package/src/lib/canvas/helpers/getCanvasContainerOrBody.test.ts +51 -0
  26. package/src/lib/canvas/helpers/getCanvasWindowValue.test.ts +116 -0
  27. package/src/lib/helpers/adjustForZoom.test.ts +65 -0
  28. package/src/lib/helpers/createDragHandler.test.ts +325 -0
  29. package/src/lib/helpers/getNodeProvider.test.ts +71 -0
  30. package/src/lib/helpers/getNodeTools.test.ts +50 -0
  31. package/src/lib/helpers/getViewportDimensions.test.ts +93 -0
  32. package/src/lib/helpers/observer/connectMutationObserver.test.ts +127 -0
  33. package/src/lib/helpers/observer/connectResizeObserver.test.ts +147 -0
  34. package/src/lib/helpers/parseTransform.test.ts +117 -0
  35. package/src/lib/helpers/toggleClass.test.ts +71 -0
  36. package/src/lib/helpers/withRAF.test.ts +439 -0
  37. package/src/lib/node-tools/createNodeTools.test.ts +373 -0
  38. package/src/lib/node-tools/events/click/handleNodeClick.test.ts +109 -0
  39. package/src/lib/node-tools/events/setupEventListener.test.ts +136 -0
  40. package/src/lib/node-tools/highlight/clearHighlightFrame.test.ts +88 -0
  41. package/src/lib/node-tools/highlight/createCornerHandles.test.ts +150 -0
  42. package/src/lib/node-tools/highlight/createHighlightFrame.test.ts +237 -0
  43. package/src/lib/node-tools/highlight/createTagLabel.test.ts +135 -0
  44. package/src/lib/node-tools/highlight/createToolsContainer.test.ts +97 -0
  45. package/src/lib/node-tools/highlight/helpers/getElementBounds.test.ts +158 -0
  46. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.test.ts +78 -0
  47. package/src/lib/node-tools/highlight/helpers/getScreenBounds.test.ts +133 -0
  48. package/src/lib/node-tools/highlight/highlightNode.test.ts +213 -0
  49. package/src/lib/node-tools/highlight/refreshHighlightFrame.test.ts +323 -0
  50. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.test.ts +110 -0
  51. package/src/lib/node-tools/select/helpers/getElementsFromPoint.test.ts +109 -0
  52. package/src/lib/node-tools/select/helpers/isInsideComponent.test.ts +81 -0
  53. package/src/lib/node-tools/select/helpers/isInsideViewport.test.ts +82 -0
  54. package/src/lib/node-tools/select/helpers/targetSameCandidates.test.ts +81 -0
  55. package/src/lib/node-tools/select/selectNode.test.ts +238 -0
  56. package/src/lib/node-tools/text/events/setupKeydownHandler.test.ts +91 -0
  57. package/src/lib/node-tools/text/events/setupMutationObserver.test.ts +213 -0
  58. package/src/lib/node-tools/text/events/setupNodeListeners.test.ts +133 -0
  59. package/src/lib/node-tools/text/helpers/enterTextEditMode.test.ts +50 -0
  60. package/src/lib/node-tools/text/helpers/handleTextChange.test.ts +201 -0
  61. package/src/lib/node-tools/text/helpers/hasTextContent.test.ts +101 -0
  62. package/src/lib/node-tools/text/helpers/insertLineBreak.test.ts +96 -0
  63. package/src/lib/node-tools/text/helpers/makeNodeEditable.test.ts +56 -0
  64. package/src/lib/node-tools/text/helpers/makeNodeNonEditable.test.ts +57 -0
  65. package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.test.ts +61 -0
  66. package/src/lib/node-tools/text/nodeText.test.ts +233 -0
  67. package/src/lib/post-message/processPostMessage.test.ts +218 -0
  68. package/src/lib/post-message/sendPostMessage.test.ts +120 -0
  69. package/src/lib/styles/styles.css +2 -2
  70. package/src/lib/viewport/createViewport.test.ts +267 -0
  71. package/src/lib/viewport/createViewport.ts +7 -4
  72. package/src/lib/viewport/events/setupEventListener.test.ts +103 -0
  73. package/src/lib/viewport/label/getViewportLabelOverlay.test.ts +77 -0
  74. package/src/lib/viewport/label/{getViewportLabelsOverlay.ts → getViewportLabelOverlay.ts} +2 -1
  75. package/src/lib/viewport/label/helpers/getLabelPosition.test.ts +51 -0
  76. package/src/lib/viewport/label/helpers/getTransformValues.test.ts +59 -0
  77. package/src/lib/viewport/label/helpers/getZoomValue.test.ts +53 -0
  78. package/src/lib/viewport/label/helpers/selectFirstViewportNode.test.ts +105 -0
  79. package/src/lib/viewport/label/helpers/selectFirstViewportNode.ts +8 -0
  80. package/src/lib/viewport/label/index.ts +5 -3
  81. package/src/lib/viewport/label/isViewportDragging.test.ts +35 -0
  82. package/src/lib/viewport/label/isViewportDragging.ts +9 -0
  83. package/src/lib/viewport/label/refreshViewportLabel.test.ts +105 -0
  84. package/src/lib/viewport/label/refreshViewportLabel.ts +50 -0
  85. package/src/lib/viewport/label/refreshViewportLabels.test.ts +107 -0
  86. package/src/lib/viewport/label/refreshViewportLabels.ts +17 -50
  87. package/src/lib/viewport/label/removeViewportLabel.test.ts +67 -0
  88. package/src/lib/viewport/label/removeViewportLabel.ts +20 -0
  89. package/src/lib/viewport/label/setupViewportDrag.test.ts +249 -0
  90. package/src/lib/viewport/label/{setupViewportLabelDrag.ts → setupViewportDrag.ts} +14 -14
  91. package/src/lib/viewport/resize/createResizeHandle.test.ts +37 -0
  92. package/src/lib/viewport/resize/createResizePresets.test.ts +75 -0
  93. package/src/lib/viewport/resize/updateActivePreset.test.ts +92 -0
  94. package/src/lib/viewport/width/calcConstrainedWidth.test.ts +47 -0
  95. package/src/lib/viewport/width/calcWidth.test.ts +68 -0
  96. package/src/lib/viewport/width/updateWidth.test.ts +78 -0
  97. package/src/lib/window/bindToWindow.test.ts +166 -0
  98. package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +0 -1
  99. package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +0 -2
  100. package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +0 -1
  101. package/src/lib/viewport/label/isViewportLabelDragging.ts +0 -9
@@ -0,0 +1,105 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import * as getNodeToolsModule from "../../../helpers/getNodeTools";
3
+ import type { NodeTools } from "../../../node-tools/types";
4
+ import * as sendPostMessageModule from "../../../post-message/sendPostMessage";
5
+ import { selectFirstViewportNode } from "./selectFirstViewportNode";
6
+
7
+ vi.mock("../../../helpers/getNodeTools");
8
+ vi.mock("../../../post-message/sendPostMessage");
9
+
10
+ describe("selectFirstViewportNode", () => {
11
+ let viewportElement: HTMLElement;
12
+ let firstChild: HTMLElement;
13
+ let mockNodeTools: {
14
+ selectNode: ReturnType<typeof vi.fn>;
15
+ getSelectedNode: ReturnType<typeof vi.fn>;
16
+ };
17
+
18
+ beforeEach(() => {
19
+ viewportElement = document.createElement("div");
20
+ firstChild = document.createElement("div");
21
+ firstChild.setAttribute("data-node-id", "test-node-1");
22
+ viewportElement.appendChild(firstChild);
23
+ document.body.appendChild(viewportElement);
24
+
25
+ mockNodeTools = {
26
+ selectNode: vi.fn(),
27
+ getSelectedNode: vi.fn(),
28
+ };
29
+
30
+ vi.mocked(getNodeToolsModule.getNodeTools).mockReturnValue(mockNodeTools as Partial<NodeTools> as NodeTools);
31
+ vi.mocked(sendPostMessageModule.sendPostMessage).mockImplementation(() => {});
32
+ });
33
+
34
+ afterEach(() => {
35
+ document.body.removeChild(viewportElement);
36
+ vi.clearAllMocks();
37
+ });
38
+
39
+ it("should select first child node when nodeTools is available", () => {
40
+ mockNodeTools.getSelectedNode.mockReturnValue(null);
41
+
42
+ selectFirstViewportNode(viewportElement);
43
+
44
+ expect(mockNodeTools.selectNode).toHaveBeenCalledWith(firstChild);
45
+ });
46
+
47
+ it("should skip resize-handle element", () => {
48
+ const resizeHandle = document.createElement("div");
49
+ resizeHandle.classList.add("resize-handle");
50
+ viewportElement.insertBefore(resizeHandle, firstChild);
51
+
52
+ mockNodeTools.getSelectedNode.mockReturnValue(null);
53
+
54
+ selectFirstViewportNode(viewportElement);
55
+
56
+ expect(mockNodeTools.selectNode).toHaveBeenCalledWith(firstChild);
57
+ expect(mockNodeTools.selectNode).not.toHaveBeenCalledWith(resizeHandle);
58
+ });
59
+
60
+ it("should send postMessage when node was already selected", () => {
61
+ mockNodeTools.getSelectedNode.mockReturnValue(firstChild);
62
+
63
+ selectFirstViewportNode(viewportElement);
64
+
65
+ expect(mockNodeTools.selectNode).toHaveBeenCalledWith(firstChild);
66
+ expect(sendPostMessageModule.sendPostMessage).toHaveBeenCalledWith("selectedNodeChanged", "test-node-1");
67
+ });
68
+
69
+ it("should not send postMessage when node was not already selected", () => {
70
+ mockNodeTools.getSelectedNode.mockReturnValue(null);
71
+
72
+ selectFirstViewportNode(viewportElement);
73
+
74
+ expect(mockNodeTools.selectNode).toHaveBeenCalledWith(firstChild);
75
+ expect(sendPostMessageModule.sendPostMessage).not.toHaveBeenCalled();
76
+ });
77
+
78
+ it("should do nothing when nodeTools is not available", () => {
79
+ vi.mocked(getNodeToolsModule.getNodeTools).mockReturnValue(undefined);
80
+
81
+ selectFirstViewportNode(viewportElement);
82
+
83
+ expect(mockNodeTools.selectNode).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it("should do nothing when there are no children", () => {
87
+ const emptyViewport = document.createElement("div");
88
+ document.body.appendChild(emptyViewport);
89
+
90
+ selectFirstViewportNode(emptyViewport);
91
+
92
+ expect(mockNodeTools.selectNode).not.toHaveBeenCalled();
93
+
94
+ document.body.removeChild(emptyViewport);
95
+ });
96
+
97
+ it("should handle null data-node-id attribute", () => {
98
+ firstChild.removeAttribute("data-node-id");
99
+ mockNodeTools.getSelectedNode.mockReturnValue(firstChild);
100
+
101
+ selectFirstViewportNode(viewportElement);
102
+
103
+ expect(sendPostMessageModule.sendPostMessage).toHaveBeenCalledWith("selectedNodeChanged", null);
104
+ });
105
+ });
@@ -1,4 +1,5 @@
1
1
  import { getNodeTools } from "../../../helpers/getNodeTools";
2
+ import { sendPostMessage } from "../../../post-message/sendPostMessage";
2
3
 
3
4
  /**
4
5
  * Selects the first child node inside a viewport element.
@@ -12,7 +13,14 @@ export const selectFirstViewportNode = (viewportElement: HTMLElement): void => {
12
13
  if (firstChild) {
13
14
  const nodeTools = getNodeTools();
14
15
  if (nodeTools?.selectNode) {
16
+ const wasAlreadySelected = nodeTools.getSelectedNode() === firstChild;
15
17
  nodeTools.selectNode(firstChild);
18
+
19
+ // Always emit postMessage when selecting via viewport label click,
20
+ // even if the node was already selected (to match behavior of direct node clicks)
21
+ if (wasAlreadySelected) {
22
+ sendPostMessage("selectedNodeChanged", firstChild.getAttribute("data-node-id") ?? null);
23
+ }
16
24
  }
17
25
  }
18
26
  };
@@ -1,4 +1,6 @@
1
- export { getViewportLabelsOverlay } from "./getViewportLabelsOverlay";
2
- export { isViewportLabelDragging } from "./isViewportLabelDragging";
1
+ export { getViewportLabelOverlay } from "./getViewportLabelOverlay";
2
+ export { isViewportDragging } from "./isViewportDragging";
3
+ export { refreshViewportLabel } from "./refreshViewportLabel";
3
4
  export { refreshViewportLabels } from "./refreshViewportLabels";
4
- export { setupViewportLabelDrag } from "./setupViewportLabelDrag";
5
+ export { removeViewportLabel } from "./removeViewportLabel";
6
+ export { setupViewportDrag } from "./setupViewportDrag";
@@ -0,0 +1,35 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { isViewportDragging, setViewportDragging } from "./isViewportDragging";
3
+
4
+ describe("isViewportDragging", () => {
5
+ beforeEach(() => {
6
+ // Reset dragging state before each test
7
+ setViewportDragging(false);
8
+ });
9
+
10
+ it("should return false initially", () => {
11
+ expect(isViewportDragging()).toBe(false);
12
+ });
13
+
14
+ it("should return true after setting dragging to true", () => {
15
+ setViewportDragging(true);
16
+ expect(isViewportDragging()).toBe(true);
17
+ });
18
+
19
+ it("should return false after setting dragging to false", () => {
20
+ setViewportDragging(true);
21
+ setViewportDragging(false);
22
+ expect(isViewportDragging()).toBe(false);
23
+ });
24
+
25
+ it("should maintain state across multiple calls", () => {
26
+ setViewportDragging(true);
27
+ expect(isViewportDragging()).toBe(true);
28
+ expect(isViewportDragging()).toBe(true);
29
+ expect(isViewportDragging()).toBe(true);
30
+
31
+ setViewportDragging(false);
32
+ expect(isViewportDragging()).toBe(false);
33
+ expect(isViewportDragging()).toBe(false);
34
+ });
35
+ });
@@ -0,0 +1,9 @@
1
+ // Global flag to prevent refreshViewportLabels during drag
2
+ let globalIsDragging = false;
3
+
4
+ export const isViewportDragging = (): boolean => globalIsDragging;
5
+
6
+ export const setViewportDragging = (isDragging: boolean): void => {
7
+ globalIsDragging = isDragging;
8
+ };
9
+
@@ -0,0 +1,105 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import * as getScreenBoundsModule from "../../node-tools/highlight/helpers/getScreenBounds";
3
+ import * as getViewportLabelOverlayModule from "./getViewportLabelOverlay";
4
+ import { refreshViewportLabel } from "./refreshViewportLabel";
5
+ import * as setupViewportDragModule from "./setupViewportDrag";
6
+
7
+ vi.mock("./getViewportLabelOverlay");
8
+ vi.mock("../../node-tools/highlight/helpers/getScreenBounds");
9
+ vi.mock("./setupViewportDrag");
10
+
11
+ describe("refreshViewportLabel", () => {
12
+ let overlay: SVGSVGElement;
13
+ let viewportElement: HTMLElement;
14
+
15
+ beforeEach(() => {
16
+ overlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
17
+ overlay.classList.add("viewport-labels-overlay");
18
+ document.body.appendChild(overlay);
19
+
20
+ viewportElement = document.createElement("div");
21
+ viewportElement.classList.add("viewport");
22
+ viewportElement.setAttribute("data-viewport-name", "test-viewport");
23
+ document.body.appendChild(viewportElement);
24
+
25
+ vi.mocked(getViewportLabelOverlayModule.getViewportLabelOverlay).mockReturnValue(overlay);
26
+ vi.mocked(getScreenBoundsModule.getScreenBounds).mockReturnValue({
27
+ top: 100,
28
+ left: 200,
29
+ width: 400,
30
+ height: 300,
31
+ });
32
+ vi.mocked(setupViewportDragModule.setupViewportDrag).mockReturnValue(() => {});
33
+ });
34
+
35
+ afterEach(() => {
36
+ document.body.removeChild(overlay);
37
+ document.body.removeChild(viewportElement);
38
+ vi.clearAllMocks();
39
+ });
40
+
41
+ it("should do nothing when viewport name is not set", () => {
42
+ viewportElement.removeAttribute("data-viewport-name");
43
+
44
+ refreshViewportLabel(viewportElement);
45
+
46
+ expect(getViewportLabelOverlayModule.getViewportLabelOverlay).not.toHaveBeenCalled();
47
+ });
48
+
49
+ it("should create new label group when it doesn't exist", () => {
50
+ refreshViewportLabel(viewportElement);
51
+
52
+ const group = overlay.querySelector(`[data-viewport-name="test-viewport"]`) as SVGGElement | null;
53
+ expect(group).not.toBeNull();
54
+ expect(group?.classList.contains("viewport-label-group")).toBe(true);
55
+ });
56
+
57
+ it("should create text element with correct attributes", () => {
58
+ refreshViewportLabel(viewportElement);
59
+
60
+ const group = overlay.querySelector(`[data-viewport-name="test-viewport"]`) as SVGGElement | null;
61
+ const text = group?.querySelector("text");
62
+ expect(text).not.toBeNull();
63
+ expect(text?.classList.contains("viewport-label-text")).toBe(true);
64
+ expect(text?.getAttribute("x")).toBe("0");
65
+ expect(text?.getAttribute("y")).toBe("-8");
66
+ expect(text?.getAttribute("vector-effect")).toBe("non-scaling-stroke");
67
+ expect(text?.getAttribute("pointer-events")).toBe("auto");
68
+ expect(text?.textContent).toBe("test-viewport");
69
+ });
70
+
71
+ it("should set up drag functionality when creating new label", () => {
72
+ refreshViewportLabel(viewportElement);
73
+
74
+ const group = overlay.querySelector(`[data-viewport-name="test-viewport"]`) as SVGGElement | null;
75
+ const text = group?.querySelector("text") as SVGTextElement | null;
76
+
77
+ expect(setupViewportDragModule.setupViewportDrag).toHaveBeenCalledWith(text, viewportElement, "test-viewport");
78
+ });
79
+
80
+ it("should update label position when group already exists", () => {
81
+ // Create existing group
82
+ const existingGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
83
+ existingGroup.setAttribute("data-viewport-name", "test-viewport");
84
+ existingGroup.setAttribute("transform", "translate(0, 0)");
85
+ overlay.appendChild(existingGroup);
86
+
87
+ vi.mocked(getScreenBoundsModule.getScreenBounds).mockReturnValue({
88
+ top: 150,
89
+ left: 250,
90
+ width: 400,
91
+ height: 300,
92
+ });
93
+
94
+ refreshViewportLabel(viewportElement);
95
+
96
+ expect(existingGroup.getAttribute("transform")).toBe("translate(250, 150)");
97
+ expect(setupViewportDragModule.setupViewportDrag).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it("should use screen bounds to position label", () => {
101
+ refreshViewportLabel(viewportElement);
102
+
103
+ expect(getScreenBoundsModule.getScreenBounds).toHaveBeenCalledWith(viewportElement);
104
+ });
105
+ });
@@ -0,0 +1,50 @@
1
+ import { getScreenBounds } from "../../node-tools/highlight/helpers/getScreenBounds";
2
+ import { getViewportLabelOverlay } from "./getViewportLabelOverlay";
3
+ import { setupViewportDrag } from "./setupViewportDrag";
4
+
5
+ /**
6
+ * Refreshes (updates) a viewport label for a single viewport element.
7
+ * Creates the label if it doesn't exist, or updates its position if it does.
8
+ * Similar to refreshHighlightFrame - updates existing elements rather than recreating.
9
+ *
10
+ * @param viewportElement - The viewport element to refresh the label for
11
+ */
12
+ export const refreshViewportLabel = (viewportElement: HTMLElement): void => {
13
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
14
+
15
+ if (!viewportName) {
16
+ return;
17
+ }
18
+
19
+ const overlay = getViewportLabelOverlay();
20
+ const bounds = getScreenBounds(viewportElement);
21
+
22
+ // Get existing label group or create if it doesn't exist
23
+ let group = overlay.querySelector(`[data-viewport-name="${viewportName}"]`) as SVGGElement | null;
24
+
25
+ if (!group) {
26
+ // Create group for this viewport label
27
+ group = document.createElementNS("http://www.w3.org/2000/svg", "g");
28
+ group.classList.add("viewport-label-group");
29
+ group.setAttribute("data-viewport-name", viewportName);
30
+
31
+ // Create text element
32
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
33
+ text.classList.add("viewport-label-text");
34
+ text.setAttribute("x", "0");
35
+ text.setAttribute("y", "-8");
36
+ text.setAttribute("vector-effect", "non-scaling-stroke");
37
+ text.setAttribute("pointer-events", "auto");
38
+ text.textContent = viewportName;
39
+
40
+ group.appendChild(text);
41
+ overlay.appendChild(group);
42
+
43
+ // Setup drag functionality only when creating new label
44
+ setupViewportDrag(text, viewportElement, viewportName);
45
+ }
46
+
47
+ // Update label position (this is the refresh part - updates existing label)
48
+ group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
49
+ };
50
+
@@ -0,0 +1,107 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import * as getViewportDimensionsModule from "../../helpers/getViewportDimensions";
3
+ import * as getViewportLabelOverlayModule from "./getViewportLabelOverlay";
4
+ import * as isViewportDraggingModule from "./isViewportDragging";
5
+ import * as refreshViewportLabelModule from "./refreshViewportLabel";
6
+ import { refreshViewportLabels } from "./refreshViewportLabels";
7
+
8
+ vi.mock("./getViewportLabelOverlay");
9
+ vi.mock("../../helpers/getViewportDimensions");
10
+ vi.mock("./refreshViewportLabel");
11
+ vi.mock("./isViewportDragging");
12
+
13
+ describe("refreshViewportLabels", () => {
14
+ let overlay: SVGSVGElement;
15
+
16
+ beforeEach(() => {
17
+ overlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
18
+ overlay.classList.add("viewport-labels-overlay");
19
+ document.body.appendChild(overlay);
20
+
21
+ vi.mocked(getViewportLabelOverlayModule.getViewportLabelOverlay).mockReturnValue(overlay);
22
+ vi.mocked(getViewportDimensionsModule.getViewportDimensions).mockReturnValue({
23
+ width: 1920,
24
+ height: 1080,
25
+ });
26
+ vi.mocked(isViewportDraggingModule.isViewportDragging).mockReturnValue(false);
27
+ vi.mocked(refreshViewportLabelModule.refreshViewportLabel).mockImplementation(() => {});
28
+ });
29
+
30
+ afterEach(() => {
31
+ document.body.removeChild(overlay);
32
+ vi.clearAllMocks();
33
+ });
34
+
35
+ it("should skip refresh when viewport is being dragged", () => {
36
+ vi.mocked(isViewportDraggingModule.isViewportDragging).mockReturnValue(true);
37
+
38
+ refreshViewportLabels();
39
+
40
+ expect(refreshViewportLabelModule.refreshViewportLabel).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it("should update SVG dimensions", () => {
44
+ refreshViewportLabels();
45
+
46
+ expect(overlay.getAttribute("width")).toBe("1920");
47
+ expect(overlay.getAttribute("height")).toBe("1080");
48
+ });
49
+
50
+ it("should refresh labels for all viewports with data-viewport-name", () => {
51
+ const viewport1 = document.createElement("div");
52
+ viewport1.classList.add("viewport");
53
+ viewport1.setAttribute("data-viewport-name", "viewport-1");
54
+ document.body.appendChild(viewport1);
55
+
56
+ const viewport2 = document.createElement("div");
57
+ viewport2.classList.add("viewport");
58
+ viewport2.setAttribute("data-viewport-name", "viewport-2");
59
+ document.body.appendChild(viewport2);
60
+
61
+ const viewport3 = document.createElement("div");
62
+ viewport3.classList.add("viewport");
63
+ // No data-viewport-name
64
+ document.body.appendChild(viewport3);
65
+
66
+ refreshViewportLabels();
67
+
68
+ expect(refreshViewportLabelModule.refreshViewportLabel).toHaveBeenCalledTimes(2);
69
+ expect(refreshViewportLabelModule.refreshViewportLabel).toHaveBeenCalledWith(viewport1);
70
+ expect(refreshViewportLabelModule.refreshViewportLabel).toHaveBeenCalledWith(viewport2);
71
+
72
+ document.body.removeChild(viewport1);
73
+ document.body.removeChild(viewport2);
74
+ document.body.removeChild(viewport3);
75
+ });
76
+
77
+ it("should remove labels for viewports that no longer exist", () => {
78
+ const existingGroup1 = document.createElementNS("http://www.w3.org/2000/svg", "g");
79
+ existingGroup1.classList.add("viewport-label-group");
80
+ existingGroup1.setAttribute("data-viewport-name", "viewport-1");
81
+ overlay.appendChild(existingGroup1);
82
+
83
+ const existingGroup2 = document.createElementNS("http://www.w3.org/2000/svg", "g");
84
+ existingGroup2.classList.add("viewport-label-group");
85
+ existingGroup2.setAttribute("data-viewport-name", "viewport-2");
86
+ overlay.appendChild(existingGroup2);
87
+
88
+ // Only viewport-1 exists in DOM
89
+ const viewport1 = document.createElement("div");
90
+ viewport1.classList.add("viewport");
91
+ viewport1.setAttribute("data-viewport-name", "viewport-1");
92
+ document.body.appendChild(viewport1);
93
+
94
+ refreshViewportLabels();
95
+
96
+ expect(overlay.querySelector(`[data-viewport-name="viewport-1"]`)).not.toBeNull();
97
+ expect(overlay.querySelector(`[data-viewport-name="viewport-2"]`)).toBeNull();
98
+
99
+ document.body.removeChild(viewport1);
100
+ });
101
+
102
+ it("should handle empty viewport list", () => {
103
+ refreshViewportLabels();
104
+
105
+ expect(refreshViewportLabelModule.refreshViewportLabel).not.toHaveBeenCalled();
106
+ });
107
+ });
@@ -1,69 +1,36 @@
1
1
  import { getViewportDimensions } from "../../helpers/getViewportDimensions";
2
- import { getScreenBounds } from "../../node-tools/highlight/helpers/getScreenBounds";
3
- import { getViewportLabelsOverlay } from "./getViewportLabelsOverlay";
4
- import { isViewportLabelDragging } from "./isViewportLabelDragging";
5
- import { setupViewportLabelDrag } from "./setupViewportLabelDrag";
6
-
7
- // Store cleanup functions for drag listeners
8
- const dragCleanupFunctions = new Map<string, () => void>();
2
+ import { getViewportLabelOverlay } from "./getViewportLabelOverlay";
3
+ import { isViewportDragging } from "./isViewportDragging";
4
+ import { refreshViewportLabel } from "./refreshViewportLabel";
9
5
 
10
6
  export const refreshViewportLabels = (): void => {
11
7
  // Skip refresh if a viewport label is being dragged
12
- if (isViewportLabelDragging()) {
8
+ if (isViewportDragging()) {
13
9
  return;
14
10
  }
15
11
 
16
- const overlay = getViewportLabelsOverlay();
12
+ const overlay = getViewportLabelOverlay();
17
13
 
18
- // Update SVG dimensions to match current viewport
14
+ // Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
19
15
  const { width: viewportWidth, height: viewportHeight } = getViewportDimensions();
20
16
  overlay.setAttribute("width", viewportWidth.toString());
21
17
  overlay.setAttribute("height", viewportHeight.toString());
22
18
 
23
- // Find all viewports with names
19
+ // Find all viewports with names and refresh each label
24
20
  const viewports = document.querySelectorAll(".viewport[data-viewport-name]");
25
-
26
- // Clean up existing drag listeners
27
- dragCleanupFunctions.forEach((cleanup) => {
28
- cleanup();
21
+ viewports.forEach((viewport) => {
22
+ refreshViewportLabel(viewport as HTMLElement);
29
23
  });
30
- dragCleanupFunctions.clear();
31
24
 
32
- // Remove existing label groups
25
+ // Remove labels for viewports that no longer exist
33
26
  const existingGroups = overlay.querySelectorAll(".viewport-label-group");
34
27
  existingGroups.forEach((group) => {
35
- group.remove();
36
- });
37
-
38
- // Create/update labels for each viewport
39
- viewports.forEach((viewport) => {
40
- const viewportElement = viewport as HTMLElement;
41
- const viewportName = viewportElement.getAttribute("data-viewport-name");
42
-
43
- if (!viewportName) return;
44
-
45
- const bounds = getScreenBounds(viewportElement);
46
-
47
- // Create group for this viewport label
48
- const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
49
- group.classList.add("viewport-label-group");
50
- group.setAttribute("data-viewport-name", viewportName);
51
- group.setAttribute("transform", `translate(${bounds.left}, ${bounds.top})`);
52
-
53
- // Create text element
54
- const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
55
- text.classList.add("viewport-label-text");
56
- text.setAttribute("x", "0");
57
- text.setAttribute("y", "-8");
58
- text.setAttribute("vector-effect", "non-scaling-stroke");
59
- text.setAttribute("pointer-events", "auto");
60
- text.textContent = viewportName;
61
-
62
- group.appendChild(text);
63
- overlay.appendChild(group);
64
-
65
- // Setup drag functionality for this label
66
- const cleanup = setupViewportLabelDrag(text, viewportElement, viewportName);
67
- dragCleanupFunctions.set(viewportName, cleanup);
28
+ const viewportName = group.getAttribute("data-viewport-name");
29
+ if (viewportName) {
30
+ const viewportExists = Array.from(viewports).some((viewport) => viewport.getAttribute("data-viewport-name") === viewportName);
31
+ if (!viewportExists) {
32
+ group.remove();
33
+ }
34
+ }
68
35
  });
69
36
  };
@@ -0,0 +1,67 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import * as getViewportLabelOverlayModule from "./getViewportLabelOverlay";
3
+ import { removeViewportLabel } from "./removeViewportLabel";
4
+
5
+ vi.mock("./getViewportLabelOverlay");
6
+
7
+ describe("removeViewportLabel", () => {
8
+ let overlay: SVGSVGElement;
9
+ let viewportElement: HTMLElement;
10
+
11
+ beforeEach(() => {
12
+ overlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
13
+ overlay.classList.add("viewport-labels-overlay");
14
+ document.body.appendChild(overlay);
15
+
16
+ viewportElement = document.createElement("div");
17
+ viewportElement.setAttribute("data-viewport-name", "test-viewport");
18
+ document.body.appendChild(viewportElement);
19
+
20
+ vi.mocked(getViewportLabelOverlayModule.getViewportLabelOverlay).mockReturnValue(overlay);
21
+ });
22
+
23
+ afterEach(() => {
24
+ document.body.removeChild(overlay);
25
+ document.body.removeChild(viewportElement);
26
+ vi.clearAllMocks();
27
+ });
28
+
29
+ it("should remove label group when viewport name exists", () => {
30
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
31
+ group.setAttribute("data-viewport-name", "test-viewport");
32
+ overlay.appendChild(group);
33
+
34
+ removeViewportLabel(viewportElement);
35
+
36
+ expect(overlay.querySelector(`[data-viewport-name="test-viewport"]`)).toBeNull();
37
+ });
38
+
39
+ it("should do nothing when viewport name doesn't exist", () => {
40
+ viewportElement.removeAttribute("data-viewport-name");
41
+
42
+ expect(() => {
43
+ removeViewportLabel(viewportElement);
44
+ }).not.toThrow();
45
+ });
46
+
47
+ it("should do nothing when label group doesn't exist", () => {
48
+ removeViewportLabel(viewportElement);
49
+
50
+ expect(overlay.querySelector(`[data-viewport-name="test-viewport"]`)).toBeNull();
51
+ });
52
+
53
+ it("should only remove the matching label group", () => {
54
+ const group1 = document.createElementNS("http://www.w3.org/2000/svg", "g");
55
+ group1.setAttribute("data-viewport-name", "test-viewport");
56
+ overlay.appendChild(group1);
57
+
58
+ const group2 = document.createElementNS("http://www.w3.org/2000/svg", "g");
59
+ group2.setAttribute("data-viewport-name", "other-viewport");
60
+ overlay.appendChild(group2);
61
+
62
+ removeViewportLabel(viewportElement);
63
+
64
+ expect(overlay.querySelector(`[data-viewport-name="test-viewport"]`)).toBeNull();
65
+ expect(overlay.querySelector(`[data-viewport-name="other-viewport"]`)).toBe(group2);
66
+ });
67
+ });
@@ -0,0 +1,20 @@
1
+ import { getViewportLabelOverlay } from "./getViewportLabelOverlay";
2
+
3
+ /**
4
+ * Removes a viewport label for a single viewport element.
5
+ * @param viewportElement - The viewport element to remove the label for
6
+ */
7
+ export const removeViewportLabel = (viewportElement: HTMLElement): void => {
8
+ const viewportName = viewportElement.getAttribute("data-viewport-name");
9
+
10
+ if (!viewportName) {
11
+ return;
12
+ }
13
+
14
+ const overlay = getViewportLabelOverlay();
15
+ const labelGroup = overlay.querySelector(`[data-viewport-name="${viewportName}"]`);
16
+
17
+ if (labelGroup) {
18
+ labelGroup.remove();
19
+ }
20
+ };