@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.
- package/README.md +268 -0
- package/dist/index.d.ts +6 -0
- package/dist/lib/canvas/createCanvasObserver.d.ts +2 -0
- package/dist/lib/canvas/disableCanvasKeyboard.d.ts +1 -0
- package/dist/lib/canvas/enableCanvasKeyboard.d.ts +1 -0
- package/dist/lib/canvas/helpers/applyCanvasState.d.ts +1 -0
- package/dist/lib/canvas/helpers/getCanvasContainer.d.ts +1 -0
- package/dist/lib/canvas/helpers/getCanvasWindowValue.d.ts +1 -0
- package/dist/lib/helpers/index.d.ts +1 -0
- package/dist/lib/helpers/observer/connectMutationObserver.d.ts +1 -0
- package/dist/lib/helpers/observer/connectResizeObserver.d.ts +1 -0
- package/dist/lib/helpers/withRAF.d.ts +4 -0
- package/dist/lib/node-tools/createNodeTools.d.ts +2 -0
- package/dist/lib/node-tools/events/click/handleNodeClick.d.ts +1 -0
- package/dist/lib/node-tools/events/setupEventListener.d.ts +1 -0
- package/dist/lib/node-tools/highlight/clearHighlightFrame.d.ts +1 -0
- package/dist/lib/node-tools/highlight/createHighlightFrame.d.ts +1 -0
- package/dist/lib/node-tools/highlight/createTagLabel.d.ts +1 -0
- package/dist/lib/node-tools/highlight/createToolsContainer.d.ts +1 -0
- package/dist/lib/node-tools/highlight/helpers/getElementBounds.d.ts +6 -0
- package/dist/lib/node-tools/highlight/helpers/getHighlightFrameElement.d.ts +1 -0
- package/dist/lib/node-tools/highlight/highlightNode.d.ts +1 -0
- package/dist/lib/node-tools/highlight/refreshHighlightFrame.d.ts +1 -0
- package/dist/lib/node-tools/select/constants.d.ts +1 -0
- package/dist/lib/node-tools/select/helpers/getElementsFromPoint.d.ts +1 -0
- package/dist/lib/node-tools/select/helpers/targetSameCandidates.d.ts +1 -0
- package/dist/lib/node-tools/select/selectNode.d.ts +1 -0
- package/dist/lib/node-tools/text/events/setupKeydownHandler.d.ts +1 -0
- package/dist/lib/node-tools/text/events/setupMutationObserver.d.ts +1 -0
- package/dist/lib/node-tools/text/events/setupNodeListeners.d.ts +1 -0
- package/dist/lib/node-tools/text/helpers/hasTextContent.d.ts +1 -0
- package/dist/lib/node-tools/text/helpers/insertLineBreak.d.ts +1 -0
- package/dist/lib/node-tools/text/helpers/makeNodeEditable.d.ts +1 -0
- package/dist/lib/node-tools/text/helpers/makeNodeNonEditable.d.ts +1 -0
- package/dist/lib/node-tools/text/nodeText.d.ts +2 -0
- package/dist/lib/post-message/handlePostMessage.d.ts +1 -0
- package/dist/lib/post-message/sendPostMessage.d.ts +1 -0
- package/dist/lib/viewport/constants.d.ts +5 -0
- package/dist/lib/viewport/createViewport.d.ts +2 -0
- package/dist/lib/viewport/events/setupEventListener.d.ts +1 -0
- package/dist/lib/viewport/resize/createResizeHandle.d.ts +1 -0
- package/dist/lib/viewport/width/calcConstrainedWidth.d.ts +1 -0
- package/dist/lib/viewport/width/calcWidth.d.ts +1 -0
- package/dist/lib/viewport/width/updateWidth.d.ts +1 -0
- package/dist/lib/window/bindToWindow.d.ts +1 -0
- package/dist/node-edit-utils.cjs.js +588 -0
- package/dist/node-edit-utils.esm.js +584 -0
- package/dist/node-edit-utils.umd.js +594 -0
- package/dist/node-edit-utils.umd.min.js +1 -0
- package/dist/styles.css +1 -0
- package/dist/umd.d.ts +1 -0
- package/package.json +65 -0
- package/src/index.ts +9 -0
- package/src/lib/canvas/createCanvasObserver.ts +37 -0
- package/src/lib/canvas/disableCanvasKeyboard.ts +7 -0
- package/src/lib/canvas/enableCanvasKeyboard.ts +7 -0
- package/src/lib/canvas/helpers/applyCanvasState.ts +11 -0
- package/src/lib/canvas/helpers/getCanvasContainer.ts +3 -0
- package/src/lib/canvas/helpers/getCanvasWindowValue.ts +5 -0
- package/src/lib/canvas/types.d.ts +3 -0
- package/src/lib/helpers/index.ts +1 -0
- package/src/lib/helpers/observer/connectMutationObserver.ts +12 -0
- package/src/lib/helpers/observer/connectResizeObserver.ts +8 -0
- package/src/lib/helpers/withRAF.ts +39 -0
- package/src/lib/node-tools/createNodeTools.ts +88 -0
- package/src/lib/node-tools/events/click/handleNodeClick.ts +21 -0
- package/src/lib/node-tools/events/setupEventListener.ts +35 -0
- package/src/lib/node-tools/highlight/clearHighlightFrame.ts +12 -0
- package/src/lib/node-tools/highlight/createHighlightFrame.ts +37 -0
- package/src/lib/node-tools/highlight/createTagLabel.ts +7 -0
- package/src/lib/node-tools/highlight/createToolsContainer.ts +10 -0
- package/src/lib/node-tools/highlight/helpers/getElementBounds.ts +31 -0
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +5 -0
- package/src/lib/node-tools/highlight/highlightNode.ts +23 -0
- package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +23 -0
- package/src/lib/node-tools/select/constants.ts +1 -0
- package/src/lib/node-tools/select/helpers/getElementsFromPoint.ts +16 -0
- package/src/lib/node-tools/select/helpers/targetSameCandidates.ts +2 -0
- package/src/lib/node-tools/select/selectNode.ts +44 -0
- package/src/lib/node-tools/text/events/setupKeydownHandler.ts +18 -0
- package/src/lib/node-tools/text/events/setupMutationObserver.ts +10 -0
- package/src/lib/node-tools/text/events/setupNodeListeners.ts +20 -0
- package/src/lib/node-tools/text/helpers/hasTextContent.ts +3 -0
- package/src/lib/node-tools/text/helpers/insertLineBreak.ts +17 -0
- package/src/lib/node-tools/text/helpers/makeNodeEditable.ts +5 -0
- package/src/lib/node-tools/text/helpers/makeNodeNonEditable.ts +5 -0
- package/src/lib/node-tools/text/nodeText.ts +67 -0
- package/src/lib/node-tools/text/types.d.ts +6 -0
- package/src/lib/node-tools/types.d.ts +12 -0
- package/src/lib/post-message/handlePostMessage.ts +8 -0
- package/src/lib/post-message/sendPostMessage.ts +11 -0
- package/src/lib/styles/styles.css +133 -0
- package/src/lib/viewport/constants.ts +6 -0
- package/src/lib/viewport/createViewport.ts +70 -0
- package/src/lib/viewport/events/setupEventListener.ts +20 -0
- package/src/lib/viewport/resize/createResizeHandle.ts +9 -0
- package/src/lib/viewport/types.d.ts +8 -0
- package/src/lib/viewport/width/calcConstrainedWidth.ts +6 -0
- package/src/lib/viewport/width/calcWidth.ts +9 -0
- package/src/lib/viewport/width/updateWidth.ts +3 -0
- package/src/lib/window/bindToWindow.ts +6 -0
- 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,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 @@
|
|
|
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,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,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,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,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
|
+
};
|