@node-edit-utils/core 2.3.2 → 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.
- package/dist/lib/canvas/helpers/getCanvasContainerOrBody.d.ts +1 -0
- package/dist/lib/canvas/helpers/getCanvasWindowValue.d.ts +1 -1
- package/dist/lib/helpers/adjustForZoom.d.ts +1 -0
- package/dist/lib/helpers/createDragHandler.d.ts +69 -0
- package/dist/lib/helpers/getNodeProvider.d.ts +1 -0
- package/dist/lib/helpers/getNodeTools.d.ts +2 -0
- package/dist/lib/helpers/getViewportDimensions.d.ts +4 -0
- package/dist/lib/helpers/index.d.ts +9 -1
- package/dist/lib/helpers/parseTransform.d.ts +8 -0
- package/dist/lib/helpers/toggleClass.d.ts +1 -0
- package/dist/lib/viewport/label/getViewportLabelOverlay.d.ts +1 -0
- package/dist/lib/viewport/label/helpers/selectFirstViewportNode.d.ts +5 -0
- package/dist/lib/viewport/label/index.d.ts +5 -3
- package/dist/lib/viewport/label/isViewportDragging.d.ts +2 -0
- package/dist/lib/viewport/label/refreshViewportLabel.d.ts +8 -0
- package/dist/lib/viewport/label/removeViewportLabel.d.ts +5 -0
- package/dist/lib/viewport/label/setupViewportDrag.d.ts +1 -0
- package/dist/node-edit-utils.cjs.js +342 -280
- package/dist/node-edit-utils.esm.js +342 -280
- package/dist/node-edit-utils.umd.js +342 -280
- package/dist/node-edit-utils.umd.min.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -2
- package/src/lib/canvas/createCanvasObserver.test.ts +242 -0
- package/src/lib/canvas/createCanvasObserver.ts +2 -2
- package/src/lib/canvas/disableCanvasKeyboard.test.ts +53 -0
- package/src/lib/canvas/disableCanvasKeyboard.ts +1 -1
- package/src/lib/canvas/disableCanvasTextMode.test.ts +53 -0
- package/src/lib/canvas/disableCanvasTextMode.ts +1 -1
- package/src/lib/canvas/enableCanvasKeyboard.test.ts +53 -0
- package/src/lib/canvas/enableCanvasKeyboard.ts +1 -1
- package/src/lib/canvas/enableCanvasTextMode.test.ts +53 -0
- package/src/lib/canvas/enableCanvasTextMode.ts +1 -1
- package/src/lib/canvas/helpers/applyCanvasState.test.ts +119 -0
- package/src/lib/canvas/helpers/applyCanvasState.ts +1 -1
- package/src/lib/canvas/helpers/getCanvasContainer.test.ts +62 -0
- package/src/lib/canvas/helpers/getCanvasContainerOrBody.test.ts +51 -0
- package/src/lib/canvas/helpers/getCanvasContainerOrBody.ts +6 -0
- package/src/lib/canvas/helpers/getCanvasWindowValue.test.ts +116 -0
- package/src/lib/canvas/helpers/getCanvasWindowValue.ts +2 -3
- package/src/lib/helpers/adjustForZoom.test.ts +65 -0
- package/src/lib/helpers/adjustForZoom.ts +4 -0
- package/src/lib/helpers/createDragHandler.test.ts +325 -0
- package/src/lib/helpers/createDragHandler.ts +171 -0
- package/src/lib/helpers/getNodeProvider.test.ts +71 -0
- package/src/lib/helpers/getNodeProvider.ts +4 -0
- package/src/lib/helpers/getNodeTools.test.ts +50 -0
- package/src/lib/helpers/getNodeTools.ts +6 -0
- package/src/lib/helpers/getViewportDimensions.test.ts +93 -0
- package/src/lib/helpers/getViewportDimensions.ts +7 -0
- package/src/lib/helpers/index.ts +9 -1
- package/src/lib/helpers/observer/connectMutationObserver.test.ts +127 -0
- package/src/lib/helpers/observer/connectResizeObserver.test.ts +147 -0
- package/src/lib/helpers/parseTransform.test.ts +117 -0
- package/src/lib/helpers/parseTransform.ts +9 -0
- package/src/lib/helpers/toggleClass.test.ts +71 -0
- package/src/lib/helpers/toggleClass.ts +9 -0
- package/src/lib/helpers/withRAF.test.ts +439 -0
- package/src/lib/node-tools/createNodeTools.test.ts +373 -0
- package/src/lib/node-tools/createNodeTools.ts +0 -1
- package/src/lib/node-tools/events/click/handleNodeClick.test.ts +109 -0
- package/src/lib/node-tools/events/setupEventListener.test.ts +136 -0
- package/src/lib/node-tools/highlight/clearHighlightFrame.test.ts +88 -0
- package/src/lib/node-tools/highlight/clearHighlightFrame.ts +2 -3
- package/src/lib/node-tools/highlight/createCornerHandles.test.ts +150 -0
- package/src/lib/node-tools/highlight/createHighlightFrame.test.ts +237 -0
- package/src/lib/node-tools/highlight/createHighlightFrame.ts +5 -9
- package/src/lib/node-tools/highlight/createTagLabel.test.ts +135 -0
- package/src/lib/node-tools/highlight/createToolsContainer.test.ts +97 -0
- package/src/lib/node-tools/highlight/createToolsContainer.ts +3 -6
- package/src/lib/node-tools/highlight/helpers/getElementBounds.test.ts +158 -0
- package/src/lib/node-tools/highlight/helpers/getElementBounds.ts +6 -5
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.test.ts +78 -0
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +2 -3
- package/src/lib/node-tools/highlight/helpers/getScreenBounds.test.ts +133 -0
- package/src/lib/node-tools/highlight/highlightNode.test.ts +213 -0
- package/src/lib/node-tools/highlight/highlightNode.ts +7 -15
- package/src/lib/node-tools/highlight/refreshHighlightFrame.test.ts +323 -0
- package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +12 -42
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.test.ts +110 -0
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +2 -3
- package/src/lib/node-tools/select/helpers/getElementsFromPoint.test.ts +109 -0
- package/src/lib/node-tools/select/helpers/isInsideComponent.test.ts +81 -0
- package/src/lib/node-tools/select/helpers/isInsideViewport.test.ts +82 -0
- package/src/lib/node-tools/select/helpers/targetSameCandidates.test.ts +81 -0
- package/src/lib/node-tools/select/selectNode.test.ts +238 -0
- package/src/lib/node-tools/text/events/setupKeydownHandler.test.ts +91 -0
- package/src/lib/node-tools/text/events/setupMutationObserver.test.ts +213 -0
- package/src/lib/node-tools/text/events/setupNodeListeners.test.ts +133 -0
- package/src/lib/node-tools/text/helpers/enterTextEditMode.test.ts +50 -0
- package/src/lib/node-tools/text/helpers/handleTextChange.test.ts +201 -0
- package/src/lib/node-tools/text/helpers/hasTextContent.test.ts +101 -0
- package/src/lib/node-tools/text/helpers/insertLineBreak.test.ts +96 -0
- package/src/lib/node-tools/text/helpers/makeNodeEditable.test.ts +56 -0
- package/src/lib/node-tools/text/helpers/makeNodeNonEditable.test.ts +57 -0
- package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.test.ts +61 -0
- package/src/lib/node-tools/text/nodeText.test.ts +233 -0
- package/src/lib/post-message/processPostMessage.test.ts +218 -0
- package/src/lib/post-message/sendPostMessage.test.ts +120 -0
- package/src/lib/styles/styles.css +3 -3
- package/src/lib/viewport/createViewport.test.ts +267 -0
- package/src/lib/viewport/createViewport.ts +51 -51
- package/src/lib/viewport/events/setupEventListener.test.ts +103 -0
- package/src/lib/viewport/label/getViewportLabelOverlay.test.ts +77 -0
- package/src/lib/viewport/label/{getViewportLabelsOverlay.ts → getViewportLabelOverlay.ts} +6 -6
- package/src/lib/viewport/label/helpers/getLabelPosition.test.ts +51 -0
- package/src/lib/viewport/label/helpers/getLabelPosition.ts +3 -5
- package/src/lib/viewport/label/helpers/getTransformValues.test.ts +59 -0
- package/src/lib/viewport/label/helpers/getTransformValues.ts +3 -5
- package/src/lib/viewport/label/helpers/getZoomValue.test.ts +53 -0
- package/src/lib/viewport/label/helpers/selectFirstViewportNode.test.ts +105 -0
- package/src/lib/viewport/label/helpers/selectFirstViewportNode.ts +26 -0
- package/src/lib/viewport/label/index.ts +5 -3
- package/src/lib/viewport/label/isViewportDragging.test.ts +35 -0
- package/src/lib/viewport/label/isViewportDragging.ts +9 -0
- package/src/lib/viewport/label/refreshViewportLabel.test.ts +105 -0
- package/src/lib/viewport/label/refreshViewportLabel.ts +50 -0
- package/src/lib/viewport/label/refreshViewportLabels.test.ts +107 -0
- package/src/lib/viewport/label/refreshViewportLabels.ts +19 -52
- package/src/lib/viewport/label/removeViewportLabel.test.ts +67 -0
- package/src/lib/viewport/label/removeViewportLabel.ts +20 -0
- package/src/lib/viewport/label/setupViewportDrag.test.ts +249 -0
- package/src/lib/viewport/label/setupViewportDrag.ts +70 -0
- package/src/lib/viewport/resize/createResizeHandle.test.ts +37 -0
- package/src/lib/viewport/resize/createResizePresets.test.ts +75 -0
- package/src/lib/viewport/resize/updateActivePreset.test.ts +92 -0
- package/src/lib/viewport/width/calcConstrainedWidth.test.ts +47 -0
- package/src/lib/viewport/width/calcWidth.test.ts +68 -0
- package/src/lib/viewport/width/updateWidth.test.ts +78 -0
- package/src/lib/window/bindToWindow.test.ts +166 -0
- package/src/lib/window/bindToWindow.ts +1 -2
- package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +0 -1
- package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +0 -2
- package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +0 -1
- package/src/lib/viewport/label/isViewportLabelDragging.ts +0 -9
- package/src/lib/viewport/label/setupViewportLabelDrag.ts +0 -98
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getTransformValues } from "./getTransformValues";
|
|
3
|
+
|
|
4
|
+
describe("getTransformValues", () => {
|
|
5
|
+
it("should parse translate3d transform with positive values", () => {
|
|
6
|
+
const element = document.createElement("div");
|
|
7
|
+
element.style.transform = "translate3d(100px, 200px, 0px)";
|
|
8
|
+
|
|
9
|
+
const result = getTransformValues(element);
|
|
10
|
+
expect(result).toEqual({ x: 100, y: 200 });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should parse translate3d transform with negative values", () => {
|
|
14
|
+
const element = document.createElement("div");
|
|
15
|
+
element.style.transform = "translate3d(-50px, -100px, 0px)";
|
|
16
|
+
|
|
17
|
+
const result = getTransformValues(element);
|
|
18
|
+
expect(result).toEqual({ x: -50, y: -100 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should parse translate3d transform with decimal values", () => {
|
|
22
|
+
const element = document.createElement("div");
|
|
23
|
+
element.style.transform = "translate3d(123.45px, 678.90px, 0px)";
|
|
24
|
+
|
|
25
|
+
const result = getTransformValues(element);
|
|
26
|
+
expect(result).toEqual({ x: 123.45, y: 678.9 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should return { x: 0, y: 0 } when transform is empty", () => {
|
|
30
|
+
const element = document.createElement("div");
|
|
31
|
+
|
|
32
|
+
const result = getTransformValues(element);
|
|
33
|
+
expect(result).toEqual({ x: 0, y: 0 });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should return { x: 0, y: 0 } when transform doesn't match pattern", () => {
|
|
37
|
+
const element = document.createElement("div");
|
|
38
|
+
element.style.transform = "rotate(45deg)";
|
|
39
|
+
|
|
40
|
+
const result = getTransformValues(element);
|
|
41
|
+
expect(result).toEqual({ x: 0, y: 0 });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should handle transform with spaces", () => {
|
|
45
|
+
const element = document.createElement("div");
|
|
46
|
+
element.style.transform = "translate3d(100px, 200px, 0px)";
|
|
47
|
+
|
|
48
|
+
const result = getTransformValues(element);
|
|
49
|
+
expect(result).toEqual({ x: 100, y: 200 });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should handle non-zero z value", () => {
|
|
53
|
+
const element = document.createElement("div");
|
|
54
|
+
element.style.transform = "translate3d(100px, 200px, 50px)";
|
|
55
|
+
|
|
56
|
+
const result = getTransformValues(element);
|
|
57
|
+
expect(result).toEqual({ x: 100, y: 200 });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
import { parseTransform3d } from "../../../helpers/parseTransform";
|
|
2
|
+
|
|
1
3
|
export const getTransformValues = (element: HTMLElement): { x: number; y: number } => {
|
|
2
4
|
const style = element.style.transform;
|
|
3
|
-
|
|
4
|
-
if (match) {
|
|
5
|
-
return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
|
|
6
|
-
}
|
|
7
|
-
return { x: 0, y: 0 };
|
|
5
|
+
return parseTransform3d(style);
|
|
8
6
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { getZoomValue } from "./getZoomValue";
|
|
3
|
+
|
|
4
|
+
describe("getZoomValue", () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
// Reset CSS custom property
|
|
7
|
+
document.body.style.setProperty("--zoom", "");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should return 1 when --zoom is not set", () => {
|
|
11
|
+
document.body.style.removeProperty("--zoom");
|
|
12
|
+
const result = getZoomValue();
|
|
13
|
+
expect(result).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should return parsed float value from --zoom CSS variable", () => {
|
|
17
|
+
document.body.style.setProperty("--zoom", "1.5");
|
|
18
|
+
const result = getZoomValue();
|
|
19
|
+
expect(result).toBe(1.5);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should handle integer zoom values", () => {
|
|
23
|
+
document.body.style.setProperty("--zoom", "2");
|
|
24
|
+
const result = getZoomValue();
|
|
25
|
+
expect(result).toBe(2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should handle decimal zoom values", () => {
|
|
29
|
+
document.body.style.setProperty("--zoom", "0.75");
|
|
30
|
+
const result = getZoomValue();
|
|
31
|
+
expect(result).toBe(0.75);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should trim whitespace from zoom value", () => {
|
|
35
|
+
// Note: CSS custom properties don't typically have whitespace, but we test the trim logic
|
|
36
|
+
// We'll mock getComputedStyle to return a value with whitespace
|
|
37
|
+
const mockGetComputedStyle = vi.spyOn(window, "getComputedStyle");
|
|
38
|
+
mockGetComputedStyle.mockReturnValue({
|
|
39
|
+
getPropertyValue: () => " 1.5 ",
|
|
40
|
+
} as unknown as CSSStyleDeclaration);
|
|
41
|
+
|
|
42
|
+
const result = getZoomValue();
|
|
43
|
+
expect(result).toBe(1.5);
|
|
44
|
+
|
|
45
|
+
mockGetComputedStyle.mockRestore();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should return 1 when --zoom is empty string", () => {
|
|
49
|
+
document.body.style.setProperty("--zoom", "");
|
|
50
|
+
const result = getZoomValue();
|
|
51
|
+
expect(result).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getNodeTools } from "../../../helpers/getNodeTools";
|
|
2
|
+
import { sendPostMessage } from "../../../post-message/sendPostMessage";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Selects the first child node inside a viewport element.
|
|
6
|
+
* Skips the resize-handle element if present.
|
|
7
|
+
*/
|
|
8
|
+
export const selectFirstViewportNode = (viewportElement: HTMLElement): void => {
|
|
9
|
+
const firstChild = Array.from(viewportElement.children).find((child) => !child.classList.contains("resize-handle")) as
|
|
10
|
+
| HTMLElement
|
|
11
|
+
| undefined;
|
|
12
|
+
|
|
13
|
+
if (firstChild) {
|
|
14
|
+
const nodeTools = getNodeTools();
|
|
15
|
+
if (nodeTools?.selectNode) {
|
|
16
|
+
const wasAlreadySelected = nodeTools.getSelectedNode() === firstChild;
|
|
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
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
1
|
+
export { getViewportLabelOverlay } from "./getViewportLabelOverlay";
|
|
2
|
+
export { isViewportDragging } from "./isViewportDragging";
|
|
3
|
+
export { refreshViewportLabel } from "./refreshViewportLabel";
|
|
3
4
|
export { refreshViewportLabels } from "./refreshViewportLabels";
|
|
4
|
-
export {
|
|
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
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
// Store cleanup functions for drag listeners
|
|
7
|
-
const dragCleanupFunctions = new Map<string, () => void>();
|
|
1
|
+
import { getViewportDimensions } from "../../helpers/getViewportDimensions";
|
|
2
|
+
import { getViewportLabelOverlay } from "./getViewportLabelOverlay";
|
|
3
|
+
import { isViewportDragging } from "./isViewportDragging";
|
|
4
|
+
import { refreshViewportLabel } from "./refreshViewportLabel";
|
|
8
5
|
|
|
9
6
|
export const refreshViewportLabels = (): void => {
|
|
10
7
|
// Skip refresh if a viewport label is being dragged
|
|
11
|
-
if (
|
|
8
|
+
if (isViewportDragging()) {
|
|
12
9
|
return;
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
const overlay =
|
|
12
|
+
const overlay = getViewportLabelOverlay();
|
|
16
13
|
|
|
17
|
-
// Update SVG dimensions to match current viewport
|
|
18
|
-
const viewportWidth
|
|
19
|
-
const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
|
|
14
|
+
// Update SVG dimensions to match current viewport (handles window resize and ensures coordinate system is correct)
|
|
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
|
-
|
|
27
|
-
dragCleanupFunctions.forEach((cleanup) => {
|
|
28
|
-
cleanup();
|
|
21
|
+
viewports.forEach((viewport) => {
|
|
22
|
+
refreshViewportLabel(viewport as HTMLElement);
|
|
29
23
|
});
|
|
30
|
-
dragCleanupFunctions.clear();
|
|
31
24
|
|
|
32
|
-
// Remove
|
|
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.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
};
|