@node-edit-utils/core 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/lib/node-tools/select/helpers/isInsideViewport.d.ts +1 -0
  2. package/dist/lib/viewport/constants.d.ts +2 -2
  3. package/dist/lib/viewport/createViewport.d.ts +1 -1
  4. package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +1 -0
  5. package/dist/lib/viewport/label/helpers/getLabelPosition.d.ts +4 -0
  6. package/dist/lib/viewport/label/helpers/getTransformValues.d.ts +4 -0
  7. package/dist/lib/viewport/label/helpers/getZoomValue.d.ts +1 -0
  8. package/dist/lib/viewport/label/index.d.ts +4 -0
  9. package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +2 -0
  10. package/dist/lib/viewport/label/refreshViewportLabels.d.ts +1 -0
  11. package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +1 -0
  12. package/dist/node-edit-utils.cjs.js +288 -63
  13. package/dist/node-edit-utils.esm.js +288 -63
  14. package/dist/node-edit-utils.umd.js +288 -63
  15. package/dist/node-edit-utils.umd.min.js +1 -1
  16. package/dist/styles.css +1 -1
  17. package/package.json +1 -1
  18. package/src/lib/canvas/createCanvasObserver.ts +14 -0
  19. package/src/lib/node-tools/createNodeTools.ts +12 -5
  20. package/src/lib/node-tools/select/helpers/isInsideViewport.ts +19 -0
  21. package/src/lib/node-tools/select/selectNode.ts +13 -1
  22. package/src/lib/node-tools/text/events/setupMutationObserver.ts +1 -0
  23. package/src/lib/styles/styles.css +48 -1
  24. package/src/lib/viewport/constants.ts +2 -2
  25. package/src/lib/viewport/createViewport.ts +26 -7
  26. package/src/lib/viewport/events/setupEventListener.ts +9 -0
  27. package/src/lib/viewport/label/getViewportLabelsOverlay.ts +33 -0
  28. package/src/lib/viewport/label/helpers/getLabelPosition.ts +8 -0
  29. package/src/lib/viewport/label/helpers/getTransformValues.ts +8 -0
  30. package/src/lib/viewport/label/helpers/getZoomValue.ts +4 -0
  31. package/src/lib/viewport/label/index.ts +4 -0
  32. package/src/lib/viewport/label/isViewportLabelDragging.ts +9 -0
  33. package/src/lib/viewport/label/refreshViewportLabels.ts +69 -0
  34. package/src/lib/viewport/label/setupViewportLabelDrag.ts +98 -0
@@ -71,6 +71,7 @@ export const createNodeTools = (element: HTMLElement | null, canvasName: string
71
71
  checkNodeExists();
72
72
  if (!document.contains(node)) return;
73
73
 
74
+ console.log("refreshHighlightFrame in mutationObserver 2");
74
75
  refreshHighlightFrame(node, nodeProvider, canvasName);
75
76
  updateHighlightFrameVisibility(node);
76
77
  });
@@ -78,8 +79,8 @@ export const createNodeTools = (element: HTMLElement | null, canvasName: string
78
79
  mutationObserver.observe(node, {
79
80
  attributes: true,
80
81
  characterData: true,
81
- childList: true,
82
- subtree: true,
82
+ //childList: true,
83
+ //subtree: true,
83
84
  });
84
85
 
85
86
  // Also observe parent node to catch when this node is removed
@@ -111,6 +112,7 @@ export const createNodeTools = (element: HTMLElement | null, canvasName: string
111
112
  if (!document.contains(node)) return; // Exit early if node was removed
112
113
 
113
114
  refreshHighlightFrame(node, nodeProvider, canvasName);
115
+ console.log("refreshHighlightFrame in resizeObserver");
114
116
  updateHighlightFrameVisibility(node);
115
117
  });
116
118
  }
@@ -119,9 +121,14 @@ export const createNodeTools = (element: HTMLElement | null, canvasName: string
119
121
  sendPostMessage("selectedNodeChanged", node?.getAttribute("data-node-id") ?? null);
120
122
  highlightNode(node) ?? null;
121
123
 
122
- if (node && nodeProvider) {
123
- updateHighlightFrameVisibility(node);
124
- updateHighlightFrameVisibility(node);
124
+ if (node) {
125
+ highlightNode(node);
126
+ if (nodeProvider) {
127
+ updateHighlightFrameVisibility(node);
128
+ updateHighlightFrameVisibility(node);
129
+ }
130
+ } else {
131
+ clearHighlightFrame();
125
132
  }
126
133
  };
127
134
 
@@ -0,0 +1,19 @@
1
+ export const isInsideViewport = (element: Element): boolean => {
2
+ let current: Element | null = element;
3
+
4
+ while (current) {
5
+ if (current.classList.contains("viewport")) {
6
+ return true;
7
+ }
8
+
9
+ // Stop at node-provider to avoid checking beyond the editable area
10
+ if (current.getAttribute("data-role") === "node-provider") {
11
+ break;
12
+ }
13
+
14
+ current = current.parentElement;
15
+ }
16
+
17
+ return false;
18
+ };
19
+
@@ -3,6 +3,7 @@ import type { NodeText } from "../text/types";
3
3
  import { IGNORED_DOM_ELEMENTS } from "./constants";
4
4
  import { getElementsFromPoint } from "./helpers/getElementsFromPoint";
5
5
  import { isInsideComponent } from "./helpers/isInsideComponent";
6
+ import { isInsideViewport } from "./helpers/isInsideViewport";
6
7
  import { targetSameCandidates } from "./helpers/targetSameCandidates";
7
8
 
8
9
  let candidateCache: Element[] = [];
@@ -22,9 +23,20 @@ export const selectNode = (event: MouseEvent, nodeProvider: HTMLElement | null,
22
23
  (element) =>
23
24
  !IGNORED_DOM_ELEMENTS.includes(element.tagName.toLowerCase()) &&
24
25
  !element.classList.contains("select-none") &&
25
- !isInsideComponent(element)
26
+ !element.classList.contains("content-layer") &&
27
+ !element.classList.contains("resize-handle") &&
28
+ !element.classList.contains("resize-presets") &&
29
+ !isInsideComponent(element) &&
30
+ isInsideViewport(element)
26
31
  );
27
32
 
33
+ console.log("candidates", candidates);
34
+
35
+ if (candidates.length === 0) {
36
+ lastSelectedNode = null;
37
+ return null;
38
+ }
39
+
28
40
  const editableNode = text.getEditableNode();
29
41
  if (editableNode && candidates.includes(editableNode)) {
30
42
  selectedNode = editableNode;
@@ -50,6 +50,7 @@ export const setupMutationObserver = (
50
50
  // Accumulate mutations instead of replacing
51
51
  pendingMutations.push(...mutations);
52
52
  scheduleProcess();
53
+ console.log("refreshHighlightFrame in mutationObserver");
53
54
  refreshHighlightFrame(node, nodeProvider, canvasName);
54
55
  });
55
56
 
@@ -71,6 +71,51 @@
71
71
  will-change: transform;
72
72
  }
73
73
 
74
+ .viewport-labels-overlay {
75
+ position: absolute;
76
+ inset: 0;
77
+ width: 100vw;
78
+ height: 100vh;
79
+ pointer-events: none;
80
+ z-index: var(--z-index-highlight);
81
+ overflow: visible;
82
+ contain: layout style paint;
83
+ will-change: transform;
84
+ }
85
+
86
+ .viewport-label-text {
87
+ font-size: 0.6875rem;
88
+ font-weight: 500;
89
+ font-family: var(--font-family-primary);
90
+ letter-spacing: var(--letter-spacing);
91
+ fill: oklch(0.5 0 0);
92
+ pointer-events: auto;
93
+ user-select: none;
94
+ -webkit-user-select: none;
95
+ -moz-user-select: none;
96
+ -ms-user-select: none;
97
+ }
98
+
99
+ @media (prefers-color-scheme: dark) {
100
+ .viewport-label-text {
101
+ fill: oklch(0.3 0 0);
102
+ }
103
+
104
+ .viewport-label-text:hover {
105
+ fill: oklch(0 0 0);
106
+ }
107
+ }
108
+
109
+ @media (prefers-color-scheme: light) {
110
+ .viewport-label-text {
111
+ fill: oklch(0.7 0 0);
112
+ }
113
+
114
+ .viewport-label-text:hover {
115
+ fill: oklch(0.3 0 0);
116
+ }
117
+ }
118
+
74
119
  .highlight-frame-rect {
75
120
  fill: none;
76
121
  stroke: var(--primary-color);
@@ -152,7 +197,9 @@
152
197
  }
153
198
 
154
199
  .viewport {
155
- position: relative;
200
+ position: absolute;
201
+ top: 0;
202
+ left: 0;
156
203
  width: var(--container-width);
157
204
  }
158
205
 
@@ -1,8 +1,8 @@
1
1
  export const DEFAULT_WIDTH = 400;
2
2
 
3
3
  export const RESIZE_CONFIG = {
4
- minWidth: 320,
5
- maxWidth: 1680,
4
+ minWidth: 4,
5
+ maxWidth: 2560,
6
6
  } as const;
7
7
 
8
8
  export const RESIZE_PRESETS = [
@@ -1,14 +1,15 @@
1
1
  import { getCanvasContainer } from "../canvas/helpers/getCanvasContainer";
2
- import { withRAFThrottle } from "../helpers";
2
+ import { refreshHighlightFrame } from "../node-tools/highlight/refreshHighlightFrame";
3
3
  import { DEFAULT_WIDTH } from "./constants";
4
4
  import { setupEventListener } from "./events/setupEventListener";
5
+ import { refreshViewportLabels } from "./label/refreshViewportLabels";
5
6
  import { createResizeHandle } from "./resize/createResizeHandle";
6
7
  import { createResizePresets } from "./resize/createResizePresets";
7
8
  import type { Viewport } from "./types";
8
9
  import { calcWidth } from "./width/calcWidth";
9
10
  import { updateWidth } from "./width/updateWidth";
10
11
 
11
- export const createViewport = (container: HTMLElement): Viewport => {
12
+ export const createViewport = (container: HTMLElement, initialWidth?: number): Viewport => {
12
13
  const canvas: HTMLElement | null = getCanvasContainer();
13
14
 
14
15
  // Remove any existing resize handle to prevent duplicates
@@ -18,7 +19,8 @@ export const createViewport = (container: HTMLElement): Viewport => {
18
19
  }
19
20
 
20
21
  const resizeHandle = createResizeHandle(container);
21
- container.style.setProperty("--container-width", `${DEFAULT_WIDTH}px`);
22
+ const width = initialWidth ?? DEFAULT_WIDTH;
23
+ container.style.setProperty("--container-width", `${width}px`);
22
24
 
23
25
  createResizePresets(resizeHandle, container, updateWidth);
24
26
 
@@ -46,8 +48,6 @@ export const createViewport = (container: HTMLElement): Viewport => {
46
48
  updateWidth(container, width);
47
49
  };
48
50
 
49
- const throttledHandleResize = withRAFThrottle(handleResize);
50
-
51
51
  const stopResize = (event: MouseEvent): void => {
52
52
  event.preventDefault();
53
53
  event.stopPropagation();
@@ -60,21 +60,40 @@ export const createViewport = (container: HTMLElement): Viewport => {
60
60
  };
61
61
 
62
62
  const blurResize = (): void => {
63
+ if (canvas) {
64
+ canvas.style.cursor = "default";
65
+ }
66
+
63
67
  isDragging = false;
64
68
  };
65
69
 
66
- const removeListeners = setupEventListener(resizeHandle, startResize, throttledHandleResize, stopResize, blurResize);
70
+ const removeListeners = setupEventListener(resizeHandle, startResize, handleResize, stopResize, blurResize);
71
+
72
+ // Refresh viewport labels when viewport is created
73
+ refreshViewportLabels();
67
74
 
68
75
  const cleanup = (): void => {
69
76
  isDragging = false;
70
- throttledHandleResize?.cleanup();
71
77
  removeListeners();
72
78
  resizeHandle.remove();
79
+ // Refresh labels after cleanup to remove this viewport's label if needed
80
+ refreshViewportLabels();
73
81
  };
74
82
 
75
83
  return {
76
84
  setWidth: (width: number): void => {
77
85
  updateWidth(container, width);
86
+ refreshViewportLabels();
87
+
88
+ // Refresh highlight frame when viewport width changes to update node positions
89
+ // biome-ignore lint/suspicious/noExplicitAny: global window extension
90
+ const nodeTools = (window as any).nodeTools;
91
+ const selectedNode = nodeTools?.getSelectedNode?.();
92
+ const nodeProvider = document.querySelector('[data-role="node-provider"]') as HTMLElement | null;
93
+
94
+ if (selectedNode && nodeProvider) {
95
+ refreshHighlightFrame(selectedNode, nodeProvider);
96
+ }
78
97
  },
79
98
  cleanup,
80
99
  };
@@ -5,9 +5,17 @@ export const setupEventListener = (
5
5
  stopResize: (event: MouseEvent) => void,
6
6
  blurResize: () => void
7
7
  ): (() => void) => {
8
+ const handleMouseLeave = (event: MouseEvent): void => {
9
+ // Check if mouse is leaving the window/document
10
+ if (!event.relatedTarget && (event.target === document || event.target === document.documentElement)) {
11
+ blurResize();
12
+ }
13
+ };
14
+
8
15
  resizeHandle.addEventListener("mousedown", startResize);
9
16
  document.addEventListener("mousemove", handleResize);
10
17
  document.addEventListener("mouseup", stopResize);
18
+ document.addEventListener("mouseleave", handleMouseLeave);
11
19
 
12
20
  window.addEventListener("blur", blurResize);
13
21
 
@@ -15,6 +23,7 @@ export const setupEventListener = (
15
23
  resizeHandle.removeEventListener("mousedown", startResize);
16
24
  document.removeEventListener("mousemove", handleResize);
17
25
  document.removeEventListener("mouseup", stopResize);
26
+ document.removeEventListener("mouseleave", handleMouseLeave);
18
27
  window.removeEventListener("blur", blurResize);
19
28
  };
20
29
  };
@@ -0,0 +1,33 @@
1
+ import { getCanvasContainer } from "../../canvas/helpers/getCanvasContainer";
2
+
3
+ export const getViewportLabelsOverlay = (): SVGSVGElement => {
4
+ const canvasContainer = getCanvasContainer();
5
+ const container = canvasContainer || document.body;
6
+
7
+ // Check if overlay already exists
8
+ let overlay = container.querySelector(".viewport-labels-overlay") as SVGSVGElement | null;
9
+
10
+ if (!overlay) {
11
+ // Create new SVG overlay
12
+ overlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
13
+ overlay.classList.add("viewport-labels-overlay");
14
+
15
+ // Set fixed positioning
16
+ overlay.style.position = "absolute";
17
+ overlay.style.top = "0";
18
+ overlay.style.left = "0";
19
+ overlay.style.width = "100vw";
20
+ overlay.style.height = "100vh";
21
+ overlay.style.pointerEvents = "none";
22
+ overlay.style.zIndex = "500";
23
+
24
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
25
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
26
+ overlay.setAttribute("width", viewportWidth.toString());
27
+ overlay.setAttribute("height", viewportHeight.toString());
28
+
29
+ container.appendChild(overlay);
30
+ }
31
+
32
+ return overlay;
33
+ };
@@ -0,0 +1,8 @@
1
+ export const getLabelPosition = (labelGroup: SVGGElement): { x: number; y: number } => {
2
+ const transform = labelGroup.getAttribute("transform");
3
+ const match = transform?.match(/translate\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)/);
4
+ if (match) {
5
+ return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
6
+ }
7
+ return { x: 0, y: 0 };
8
+ };
@@ -0,0 +1,8 @@
1
+ export const getTransformValues = (element: HTMLElement): { x: number; y: number } => {
2
+ const style = element.style.transform;
3
+ const match = style.match(/translate3d\((-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px\)/);
4
+ if (match) {
5
+ return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
6
+ }
7
+ return { x: 0, y: 0 };
8
+ };
@@ -0,0 +1,4 @@
1
+ export const getZoomValue = (): number => {
2
+ const zoomValue = getComputedStyle(document.body).getPropertyValue("--zoom").trim();
3
+ return zoomValue ? parseFloat(zoomValue) : 1;
4
+ };
@@ -0,0 +1,4 @@
1
+ export { getViewportLabelsOverlay } from "./getViewportLabelsOverlay";
2
+ export { isViewportLabelDragging } from "./isViewportLabelDragging";
3
+ export { refreshViewportLabels } from "./refreshViewportLabels";
4
+ export { setupViewportLabelDrag } from "./setupViewportLabelDrag";
@@ -0,0 +1,9 @@
1
+ // Global flag to prevent refreshViewportLabels during drag
2
+ let globalIsDragging = false;
3
+
4
+ export const isViewportLabelDragging = (): boolean => globalIsDragging;
5
+
6
+ export const setViewportLabelDragging = (isDragging: boolean): void => {
7
+ globalIsDragging = isDragging;
8
+ };
9
+
@@ -0,0 +1,69 @@
1
+ import { getScreenBounds } from "../../node-tools/highlight/helpers/getScreenBounds";
2
+ import { getViewportLabelsOverlay } from "./getViewportLabelsOverlay";
3
+ import { isViewportLabelDragging } from "./isViewportLabelDragging";
4
+ import { setupViewportLabelDrag } from "./setupViewportLabelDrag";
5
+
6
+ // Store cleanup functions for drag listeners
7
+ const dragCleanupFunctions = new Map<string, () => void>();
8
+
9
+ export const refreshViewportLabels = (): void => {
10
+ // Skip refresh if a viewport label is being dragged
11
+ if (isViewportLabelDragging()) {
12
+ return;
13
+ }
14
+
15
+ const overlay = getViewportLabelsOverlay();
16
+
17
+ // Update SVG dimensions to match current viewport
18
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
19
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
20
+ overlay.setAttribute("width", viewportWidth.toString());
21
+ overlay.setAttribute("height", viewportHeight.toString());
22
+
23
+ // Find all viewports with names
24
+ const viewports = document.querySelectorAll(".viewport[data-viewport-name]");
25
+
26
+ // Clean up existing drag listeners
27
+ dragCleanupFunctions.forEach((cleanup) => {
28
+ cleanup();
29
+ });
30
+ dragCleanupFunctions.clear();
31
+
32
+ // Remove existing label groups
33
+ const existingGroups = overlay.querySelectorAll(".viewport-label-group");
34
+ 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);
68
+ });
69
+ };
@@ -0,0 +1,98 @@
1
+ import { sendPostMessage } from "../../post-message/sendPostMessage";
2
+ import { getLabelPosition } from "./helpers/getLabelPosition";
3
+ import { getTransformValues } from "./helpers/getTransformValues";
4
+ import { getZoomValue } from "./helpers/getZoomValue";
5
+ import { setViewportLabelDragging } from "./isViewportLabelDragging";
6
+
7
+ export const setupViewportLabelDrag = (labelElement: SVGTextElement, viewportElement: HTMLElement, viewportName: string): (() => void) => {
8
+ let isDragging = false;
9
+ let startX = 0;
10
+ let startY = 0;
11
+ let initialTransform = { x: 0, y: 0 };
12
+ let initialLabelPosition = { x: 0, y: 0 };
13
+
14
+ // Get the parent group element that contains the label
15
+ const labelGroup = labelElement.parentElement as unknown as SVGGElement;
16
+
17
+ const startDrag = (event: MouseEvent): void => {
18
+ event.preventDefault();
19
+ event.stopPropagation();
20
+
21
+ isDragging = true;
22
+ setViewportLabelDragging(true);
23
+ startX = event.clientX;
24
+ startY = event.clientY;
25
+ initialTransform = getTransformValues(viewportElement);
26
+ initialLabelPosition = getLabelPosition(labelGroup);
27
+ };
28
+
29
+ const handleDrag = (event: MouseEvent): void => {
30
+ if (!isDragging) return;
31
+
32
+ const zoom = getZoomValue();
33
+
34
+ // Calculate mouse delta
35
+ const rawDeltaX = event.clientX - startX;
36
+ const rawDeltaY = event.clientY - startY;
37
+
38
+ // Adjust delta for zoom level
39
+ const deltaX = rawDeltaX / zoom;
40
+ const deltaY = rawDeltaY / zoom;
41
+
42
+ const newX = initialTransform.x + deltaX;
43
+ const newY = initialTransform.y + deltaY;
44
+
45
+ // Update label position with raw delta (labels are in screen space)
46
+ const newLabelX = initialLabelPosition.x + rawDeltaX;
47
+ const newLabelY = initialLabelPosition.y + rawDeltaY;
48
+ labelGroup.setAttribute("transform", `translate(${newLabelX}, ${newLabelY})`);
49
+
50
+ // Update viewport position with zoom-adjusted delta
51
+ viewportElement.style.transform = `translate3d(${newX}px, ${newY}px, 0)`;
52
+ };
53
+
54
+ const stopDrag = (event: MouseEvent): void => {
55
+ if (!isDragging) return;
56
+
57
+ event.preventDefault();
58
+ event.stopPropagation();
59
+
60
+ isDragging = false;
61
+ setViewportLabelDragging(false);
62
+
63
+ const finalTransform = getTransformValues(viewportElement);
64
+
65
+ // Trigger refresh after drag completes to update highlight frame and labels
66
+ // biome-ignore lint/suspicious/noExplicitAny: global window extension
67
+ const nodeTools = (window as any).nodeTools;
68
+ if (nodeTools?.refreshHighlightFrame) {
69
+ nodeTools.refreshHighlightFrame();
70
+ }
71
+
72
+ // Notify parent about the new position
73
+ sendPostMessage("viewport-position-changed", {
74
+ viewportName,
75
+ x: finalTransform.x,
76
+ y: finalTransform.y,
77
+ });
78
+ };
79
+
80
+ const cancelDrag = (): void => {
81
+ isDragging = false;
82
+ setViewportLabelDragging(false);
83
+ };
84
+
85
+ // Attach event listeners
86
+ labelElement.addEventListener("mousedown", startDrag);
87
+ document.addEventListener("mousemove", handleDrag);
88
+ document.addEventListener("mouseup", stopDrag);
89
+ window.addEventListener("blur", cancelDrag);
90
+
91
+ // Return cleanup function
92
+ return () => {
93
+ labelElement.removeEventListener("mousedown", startDrag);
94
+ document.removeEventListener("mousemove", handleDrag);
95
+ document.removeEventListener("mouseup", stopDrag);
96
+ window.removeEventListener("blur", cancelDrag);
97
+ };
98
+ };