@canvus/core 0.1.1 → 0.1.3
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/LICENSE +21 -0
- package/README.md +32 -1
- package/dist/drop-zone.d.ts.map +1 -1
- package/dist/drop-zone.js +16 -0
- package/dist/drop-zone.js.map +1 -1
- package/dist/handlers/clipboard.handler.d.ts +22 -0
- package/dist/handlers/clipboard.handler.d.ts.map +1 -0
- package/dist/handlers/clipboard.handler.js +349 -0
- package/dist/handlers/clipboard.handler.js.map +1 -0
- package/dist/handlers/command.handler.d.ts +18 -0
- package/dist/handlers/command.handler.d.ts.map +1 -0
- package/dist/handlers/command.handler.js +430 -0
- package/dist/handlers/command.handler.js.map +1 -0
- package/dist/handlers/drag.handler.d.ts +22 -0
- package/dist/handlers/drag.handler.d.ts.map +1 -0
- package/dist/handlers/drag.handler.js +669 -0
- package/dist/handlers/drag.handler.js.map +1 -0
- package/dist/handlers/draw.handler.d.ts +37 -0
- package/dist/handlers/draw.handler.d.ts.map +1 -0
- package/dist/handlers/draw.handler.js +210 -0
- package/dist/handlers/draw.handler.js.map +1 -0
- package/dist/handlers/index.d.ts +10 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +12 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/pan.handler.d.ts +34 -0
- package/dist/handlers/pan.handler.d.ts.map +1 -0
- package/dist/handlers/pan.handler.js +95 -0
- package/dist/handlers/pan.handler.js.map +1 -0
- package/dist/handlers/resize.handler.d.ts +26 -0
- package/dist/handlers/resize.handler.d.ts.map +1 -0
- package/dist/handlers/resize.handler.js +487 -0
- package/dist/handlers/resize.handler.js.map +1 -0
- package/dist/handlers/selection.handler.d.ts +22 -0
- package/dist/handlers/selection.handler.d.ts.map +1 -0
- package/dist/handlers/selection.handler.js +259 -0
- package/dist/handlers/selection.handler.js.map +1 -0
- package/dist/handlers/spacing.handler.d.ts +29 -0
- package/dist/handlers/spacing.handler.d.ts.map +1 -0
- package/dist/handlers/spacing.handler.js +326 -0
- package/dist/handlers/spacing.handler.js.map +1 -0
- package/dist/handlers/types.d.ts +204 -0
- package/dist/handlers/types.d.ts.map +1 -0
- package/dist/handlers/types.js +10 -0
- package/dist/handlers/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/shadow-mount.d.ts.map +1 -1
- package/dist/shadow-mount.js +51 -2
- package/dist/shadow-mount.js.map +1 -1
- package/dist/workspace.d.ts +149 -68
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +349 -2208
- package/dist/workspace.js.map +1 -1
- package/package.json +4 -1
package/dist/workspace.js
CHANGED
|
@@ -14,60 +14,19 @@
|
|
|
14
14
|
// callbacks; everything else is handled internally.
|
|
15
15
|
// ─────────────────────────────────────────────────────────────
|
|
16
16
|
import { createDefaultViewport, resolveNode } from "./types.js";
|
|
17
|
-
import { applyPan, applyWheelZoom, hitTestElements, screenToCanvas,
|
|
17
|
+
import { applyPan, applyWheelZoom, hitTestElements, screenToCanvas, isPointInElement, } from "./matrix.js";
|
|
18
18
|
import { ShadowMount } from "./shadow-mount.js";
|
|
19
|
-
import { NodeTree
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const affectsLeft = anchor === "nw" || anchor === "w" || anchor === "sw";
|
|
31
|
-
const affectsRight = anchor === "ne" || anchor === "e" || anchor === "se";
|
|
32
|
-
const affectsTop = anchor === "nw" || anchor === "n" || anchor === "ne";
|
|
33
|
-
const affectsBottom = anchor === "sw" || anchor === "s" || anchor === "se";
|
|
34
|
-
if (symmetrical) {
|
|
35
|
-
const centerX = start.x + start.width / 2;
|
|
36
|
-
const centerY = start.y + start.height / 2;
|
|
37
|
-
if (affectsRight) {
|
|
38
|
-
width = Math.max(minSize, start.width + 2 * dx);
|
|
39
|
-
}
|
|
40
|
-
else if (affectsLeft) {
|
|
41
|
-
width = Math.max(minSize, start.width - 2 * dx);
|
|
42
|
-
}
|
|
43
|
-
if (affectsBottom) {
|
|
44
|
-
height = Math.max(minSize, start.height + 2 * dy);
|
|
45
|
-
}
|
|
46
|
-
else if (affectsTop) {
|
|
47
|
-
height = Math.max(minSize, start.height - 2 * dy);
|
|
48
|
-
}
|
|
49
|
-
x = centerX - width / 2;
|
|
50
|
-
y = centerY - height / 2;
|
|
51
|
-
return { x, y, width, height };
|
|
52
|
-
}
|
|
53
|
-
if (affectsRight) {
|
|
54
|
-
width = Math.max(minSize, width + dx);
|
|
55
|
-
}
|
|
56
|
-
if (affectsLeft) {
|
|
57
|
-
const newWidth = Math.max(minSize, width - dx);
|
|
58
|
-
x = x + (width - newWidth); // Shift origin to compensate.
|
|
59
|
-
width = newWidth;
|
|
60
|
-
}
|
|
61
|
-
if (affectsBottom) {
|
|
62
|
-
height = Math.max(minSize, height + dy);
|
|
63
|
-
}
|
|
64
|
-
if (affectsTop) {
|
|
65
|
-
const newHeight = Math.max(minSize, height - dy);
|
|
66
|
-
y = y + (height - newHeight);
|
|
67
|
-
height = newHeight;
|
|
68
|
-
}
|
|
69
|
-
return { x, y, width, height };
|
|
70
|
-
}
|
|
19
|
+
import { NodeTree } from "./tree.js";
|
|
20
|
+
import { OverlayRenderer, anchorCursor, isContainerNode, } from "./renderer.js";
|
|
21
|
+
import { detectLayout, getLayoutLabel, parseGridTracks } from "./layout.js";
|
|
22
|
+
import { PanHandler } from "./handlers/pan.handler.js";
|
|
23
|
+
import { DrawHandler } from "./handlers/draw.handler.js";
|
|
24
|
+
import { ClipboardHandler } from "./handlers/clipboard.handler.js";
|
|
25
|
+
import { CommandHandler } from "./handlers/command.handler.js";
|
|
26
|
+
import { SpacingHandler } from "./handlers/spacing.handler.js";
|
|
27
|
+
import { ResizeHandler, getLockedPropertiesForAnchor } from "./handlers/resize.handler.js";
|
|
28
|
+
import { DragHandler } from "./handlers/drag.handler.js";
|
|
29
|
+
import { SelectionHandler } from "./handlers/selection.handler.js";
|
|
71
30
|
// ── Workspace Class ─────────────────────────────────────────
|
|
72
31
|
/**
|
|
73
32
|
* The top-level orchestration engine for a Canvus workspace.
|
|
@@ -124,7 +83,6 @@ export class Workspace {
|
|
|
124
83
|
active: new Set(),
|
|
125
84
|
focus: new Set()
|
|
126
85
|
};
|
|
127
|
-
activeAnchor = null;
|
|
128
86
|
guides = [];
|
|
129
87
|
// ── Scoped Selection Scope ──────────────────────
|
|
130
88
|
enteredContainerId = null;
|
|
@@ -134,50 +92,60 @@ export class Workspace {
|
|
|
134
92
|
editAllowedOnDblClick = false;
|
|
135
93
|
// ── Drag & Drop State ───────────────────────────
|
|
136
94
|
activeDropTarget = null;
|
|
137
|
-
pointerDownInsideSelection = null;
|
|
138
95
|
// ── Interaction State Machine ───────────────────
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
activeAdjusterType = null;
|
|
156
|
-
adjusterStartValue = 0;
|
|
157
|
-
adjusterStartValueStr = null;
|
|
158
|
-
dragStartCanvas = null;
|
|
96
|
+
get hoveredAdjusterType() {
|
|
97
|
+
return this.spacingHandler ? this.spacingHandler.hoveredAdjusterType : null;
|
|
98
|
+
}
|
|
99
|
+
set hoveredAdjusterType(value) {
|
|
100
|
+
if (this.spacingHandler) {
|
|
101
|
+
this.spacingHandler.hoveredAdjusterType = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
get hoveredRadiusCorner() {
|
|
105
|
+
return this.spacingHandler ? this.spacingHandler.hoveredRadiusCorner : null;
|
|
106
|
+
}
|
|
107
|
+
set hoveredRadiusCorner(value) {
|
|
108
|
+
if (this.spacingHandler) {
|
|
109
|
+
this.spacingHandler.hoveredRadiusCorner = value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
159
112
|
lastCanvasPos = null;
|
|
160
|
-
resizeStartRect = null;
|
|
161
|
-
dragStartStyles = null;
|
|
162
113
|
disposed = false;
|
|
163
114
|
renderRequested = false;
|
|
164
115
|
previewMode = false;
|
|
165
116
|
/** Set of node IDs explicitly marked as containing JavaScript behavior. */
|
|
166
117
|
jsMarkedNodes = new Set();
|
|
118
|
+
/** Set of node IDs explicitly locked by the host. Locked nodes are non-interactive. */
|
|
119
|
+
lockedNodes = new Set();
|
|
167
120
|
/** Set of node IDs that were lazily registered (children discovered on selection). */
|
|
168
121
|
lazyRegisteredIds = new Set();
|
|
169
122
|
lazyChildCounter = 0;
|
|
170
|
-
// ──
|
|
171
|
-
|
|
172
|
-
drawingTag = "div";
|
|
173
|
-
drawingTextTag = "p";
|
|
174
|
-
isDrawingNode = false;
|
|
175
|
-
drawStartCanvas = null;
|
|
176
|
-
drawCurrentCanvas = null;
|
|
123
|
+
// ── Shared Counter ──────────────────────────────
|
|
124
|
+
/** Monotonic counter for generating unique element IDs (shared across draw, clone, paste). */
|
|
177
125
|
newElementCounter = 0;
|
|
178
|
-
// ──
|
|
179
|
-
|
|
180
|
-
|
|
126
|
+
// ── Handler Architecture ────────────────────────
|
|
127
|
+
/** Registered pointer-gesture handlers in priority order. */
|
|
128
|
+
interactionHandlers = [];
|
|
129
|
+
/** Registered keyboard handlers in priority order. */
|
|
130
|
+
keyboardHandlers = [];
|
|
131
|
+
/** The handler that currently owns the active pointer gesture. */
|
|
132
|
+
activeHandler = null;
|
|
133
|
+
/** Pan handler instance (for direct space-key delegation). */
|
|
134
|
+
panHandler;
|
|
135
|
+
/** Draw handler instance (for public API delegation). */
|
|
136
|
+
drawHandler;
|
|
137
|
+
/** Clipboard handler instance (for public API delegation). */
|
|
138
|
+
clipboardHandler;
|
|
139
|
+
/** Command handler instance (for public API delegation). */
|
|
140
|
+
commandHandler;
|
|
141
|
+
/** Spacing handler instance (for interaction delegation). */
|
|
142
|
+
spacingHandler;
|
|
143
|
+
/** Resize handler instance (for interaction delegation). */
|
|
144
|
+
resizeHandler;
|
|
145
|
+
/** Drag handler instance (for interaction delegation). */
|
|
146
|
+
dragHandler;
|
|
147
|
+
/** Selection handler instance (for interaction delegation). */
|
|
148
|
+
selectionHandler;
|
|
181
149
|
// ── Bound Event Handlers (for cleanup) ──────────
|
|
182
150
|
onWheel;
|
|
183
151
|
onPointerDown;
|
|
@@ -254,6 +222,25 @@ export class Workspace {
|
|
|
254
222
|
window.addEventListener("resize", this.onWindowResize);
|
|
255
223
|
// ── Initial Sizing ────────────────────────────
|
|
256
224
|
this.handleResize();
|
|
225
|
+
// ── Register Handlers ─────────────────────────
|
|
226
|
+
// Handlers receive `this` as WorkspaceContext. The Workspace
|
|
227
|
+
// class implements the context interface implicitly.
|
|
228
|
+
this.panHandler = new PanHandler(this);
|
|
229
|
+
this.registerInteractionHandler(this.panHandler, 0); // Highest priority
|
|
230
|
+
this.drawHandler = new DrawHandler(this);
|
|
231
|
+
this.registerInteractionHandler(this.drawHandler, 1); // After pan
|
|
232
|
+
this.clipboardHandler = new ClipboardHandler(this);
|
|
233
|
+
this.registerKeyboardHandler(this.clipboardHandler);
|
|
234
|
+
this.commandHandler = new CommandHandler(this);
|
|
235
|
+
this.registerKeyboardHandler(this.commandHandler);
|
|
236
|
+
this.resizeHandler = new ResizeHandler(this);
|
|
237
|
+
this.registerInteractionHandler(this.resizeHandler, 2);
|
|
238
|
+
this.spacingHandler = new SpacingHandler(this);
|
|
239
|
+
this.registerInteractionHandler(this.spacingHandler, 3);
|
|
240
|
+
this.dragHandler = new DragHandler(this);
|
|
241
|
+
this.registerInteractionHandler(this.dragHandler, 4);
|
|
242
|
+
this.selectionHandler = new SelectionHandler(this);
|
|
243
|
+
this.registerInteractionHandler(this.selectionHandler, 5);
|
|
257
244
|
}
|
|
258
245
|
// ── Public API: Node Management ─────────────────
|
|
259
246
|
/**
|
|
@@ -291,6 +278,7 @@ export class Workspace {
|
|
|
291
278
|
if (resolved.parentId !== null) {
|
|
292
279
|
this.remeasureSubtree(resolved.parentId);
|
|
293
280
|
}
|
|
281
|
+
this.callbacks.onNodeAdded?.(resolved.id);
|
|
294
282
|
this.render();
|
|
295
283
|
return resolved.currentRect ?? rect;
|
|
296
284
|
}
|
|
@@ -301,11 +289,13 @@ export class Workspace {
|
|
|
301
289
|
for (const did of descendantIds) {
|
|
302
290
|
this.mount.removeNode(did);
|
|
303
291
|
this.selectedIds.delete(did);
|
|
292
|
+
this.callbacks.onNodeRemoved?.(did);
|
|
304
293
|
}
|
|
305
294
|
const removed = this.mount.removeNode(id);
|
|
306
295
|
if (removed) {
|
|
307
296
|
this.tree.removeNode(id); // Also removes descendants from tree.
|
|
308
297
|
this.selectedIds.delete(id);
|
|
298
|
+
this.callbacks.onNodeRemoved?.(id);
|
|
309
299
|
this.render();
|
|
310
300
|
}
|
|
311
301
|
return removed;
|
|
@@ -465,6 +455,23 @@ export class Workspace {
|
|
|
465
455
|
getSelectedIds() {
|
|
466
456
|
return this.selectedIds;
|
|
467
457
|
}
|
|
458
|
+
// ── Public API: Drawing Tools ───────────────
|
|
459
|
+
/** Sets the active drawing tool (box, text, or null to return to selection/idle mode). */
|
|
460
|
+
setActiveTool(tool) {
|
|
461
|
+
this.drawHandler.setActiveTool(tool);
|
|
462
|
+
}
|
|
463
|
+
/** Returns the currently active drawing tool. */
|
|
464
|
+
getActiveTool() {
|
|
465
|
+
return this.drawHandler.getActiveTool();
|
|
466
|
+
}
|
|
467
|
+
/** Customizes the HTML tag type for box or text drawing. */
|
|
468
|
+
setDrawingTag(tag) {
|
|
469
|
+
this.drawHandler.setDrawingTag(tag);
|
|
470
|
+
}
|
|
471
|
+
/** Returns the active drawing tag based on the selected tool. */
|
|
472
|
+
getDrawingTag() {
|
|
473
|
+
return this.drawHandler.getDrawingTag();
|
|
474
|
+
}
|
|
468
475
|
// ── Public API: Viewport ────────────────────────
|
|
469
476
|
/** Returns the current viewport transform. */
|
|
470
477
|
getViewport() {
|
|
@@ -495,15 +502,16 @@ export class Workspace {
|
|
|
495
502
|
this.canvas.style.pointerEvents = "none";
|
|
496
503
|
// Clear selection, hover, and active interactions.
|
|
497
504
|
if (enabled) {
|
|
505
|
+
if (this.activeHandler) {
|
|
506
|
+
this.activeHandler.onCancel?.();
|
|
507
|
+
this.activeHandler = null;
|
|
508
|
+
}
|
|
498
509
|
this.selectedIds.clear();
|
|
499
510
|
this.clearDynamicHover();
|
|
500
511
|
this.hoveredId = null;
|
|
501
512
|
this.activeDropTarget = null;
|
|
502
|
-
this.
|
|
503
|
-
this.
|
|
504
|
-
this.isResizing = false;
|
|
505
|
-
this.isMarqueeSelecting = false;
|
|
506
|
-
this.pointerDownReadyToDrag = false;
|
|
513
|
+
this.dragHandler.onCancel();
|
|
514
|
+
this.selectionHandler.onCancel();
|
|
507
515
|
this.callbacks.onSelectionChange?.(this.selectedIds);
|
|
508
516
|
this.callbacks.onInteractionChange?.(null);
|
|
509
517
|
}
|
|
@@ -513,332 +521,27 @@ export class Workspace {
|
|
|
513
521
|
isPreviewMode() {
|
|
514
522
|
return this.previewMode;
|
|
515
523
|
}
|
|
516
|
-
//
|
|
517
|
-
/** Sets the active drawing tool (box, text, or null to return to selection/idle mode). */
|
|
518
|
-
setActiveTool(tool) {
|
|
519
|
-
this.activeTool = tool;
|
|
520
|
-
this.container.style.cursor = tool ? "crosshair" : "default";
|
|
521
|
-
if (tool !== null) {
|
|
522
|
-
this.deselectAll();
|
|
523
|
-
}
|
|
524
|
-
this.callbacks.onInteractionChange?.(tool ? `draw-${tool}` : null);
|
|
525
|
-
this.render();
|
|
526
|
-
}
|
|
527
|
-
/** Returns the currently active drawing tool. */
|
|
528
|
-
getActiveTool() {
|
|
529
|
-
return this.activeTool;
|
|
530
|
-
}
|
|
531
|
-
/** Customizes the HTML tag type for box or text drawing. */
|
|
532
|
-
setDrawingTag(tag) {
|
|
533
|
-
const lower = tag.toLowerCase().trim();
|
|
534
|
-
const textTags = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "span", "a", "strong", "em", "li", "ul", "ol"];
|
|
535
|
-
if (textTags.includes(lower)) {
|
|
536
|
-
this.drawingTextTag = lower;
|
|
537
|
-
}
|
|
538
|
-
else {
|
|
539
|
-
this.drawingTag = lower;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
/** Returns the active drawing tag based on the selected tool. */
|
|
543
|
-
getDrawingTag() {
|
|
544
|
-
return this.activeTool === "text" ? this.drawingTextTag : this.drawingTag;
|
|
545
|
-
}
|
|
524
|
+
// (Drawing tool API moved — see setActiveTool/getActiveTool/setDrawingTag/getDrawingTag above)
|
|
546
525
|
// ── Public API: Clipboard Operations ────────────
|
|
547
526
|
/** Deletes the currently selected node from the workspace. */
|
|
548
527
|
deleteSelectedNode() {
|
|
549
|
-
|
|
550
|
-
if (topLevelIds.length === 0)
|
|
551
|
-
return;
|
|
552
|
-
this.mount.setTransitionsEnabled(false);
|
|
553
|
-
const ops = [];
|
|
554
|
-
const parentsToRemeasure = new Set();
|
|
555
|
-
for (const id of topLevelIds) {
|
|
556
|
-
const node = this.tree.get(id);
|
|
557
|
-
if (!node)
|
|
558
|
-
continue;
|
|
559
|
-
const parentId = node.parentId;
|
|
560
|
-
const rawMarkup = node.rawMarkup;
|
|
561
|
-
const rect = node.currentRect;
|
|
562
|
-
const removed = this.removeNode(id);
|
|
563
|
-
if (removed) {
|
|
564
|
-
ops.push({
|
|
565
|
-
type: "delete-node",
|
|
566
|
-
nodeId: id,
|
|
567
|
-
payload: { parentId },
|
|
568
|
-
undoPayload: { parentId, rawMarkup, rect }
|
|
569
|
-
});
|
|
570
|
-
if (parentId) {
|
|
571
|
-
parentsToRemeasure.add(parentId);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (ops.length > 0) {
|
|
576
|
-
this.callbacks.onOperationsGenerated?.(ops);
|
|
577
|
-
// Commit HTML for affected parent containers or root
|
|
578
|
-
for (const parentId of parentsToRemeasure) {
|
|
579
|
-
this.remeasureSubtree(parentId);
|
|
580
|
-
const html = this.mount.extractHTML(parentId);
|
|
581
|
-
if (html) {
|
|
582
|
-
this.callbacks.onHTMLCommit?.(parentId, html);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
// If any deleted node was a root node, commit HTML for it
|
|
586
|
-
for (const op of ops) {
|
|
587
|
-
if (!op.payload.parentId) {
|
|
588
|
-
this.callbacks.onHTMLCommit?.(op.nodeId, "");
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
this.deselectAll();
|
|
592
|
-
}
|
|
593
|
-
this.mount.setTransitionsEnabled(true);
|
|
594
|
-
this.render();
|
|
528
|
+
this.clipboardHandler.deleteSelectedNode();
|
|
595
529
|
}
|
|
596
530
|
/** Duplicates the selected node right next to it as a sibling. */
|
|
597
531
|
duplicateSelectedNode() {
|
|
598
|
-
|
|
599
|
-
if (topLevelIds.length === 0)
|
|
600
|
-
return;
|
|
601
|
-
this.mount.setTransitionsEnabled(false);
|
|
602
|
-
const newSelectedIds = [];
|
|
603
|
-
const ops = [];
|
|
604
|
-
const parentsToCommit = new Set();
|
|
605
|
-
const rootsToCommit = [];
|
|
606
|
-
for (const originalId of topLevelIds) {
|
|
607
|
-
const originalNode = this.tree.get(originalId);
|
|
608
|
-
if (!originalNode)
|
|
609
|
-
continue;
|
|
610
|
-
const rawMarkup = this.mount.extractHTML(originalId);
|
|
611
|
-
if (!rawMarkup)
|
|
612
|
-
continue;
|
|
613
|
-
this.newElementCounter++;
|
|
614
|
-
const duplicateId = `cloned-${this.newElementCounter}-${Date.now().toString(36)}`;
|
|
615
|
-
const parentId = originalNode.parentId;
|
|
616
|
-
let rect = originalNode.currentRect ? { ...originalNode.currentRect } : null;
|
|
617
|
-
let index;
|
|
618
|
-
if (parentId !== null) {
|
|
619
|
-
index = this.tree.getChildIndex(originalId) + 1;
|
|
620
|
-
}
|
|
621
|
-
else if (rect) {
|
|
622
|
-
rect.x += 20;
|
|
623
|
-
rect.y += 20;
|
|
624
|
-
}
|
|
625
|
-
this.addNode({
|
|
626
|
-
id: duplicateId,
|
|
627
|
-
rawMarkup,
|
|
628
|
-
currentRect: rect
|
|
629
|
-
}, parentId, index);
|
|
630
|
-
if (this.jsMarkedNodes.has(originalId)) {
|
|
631
|
-
this.markNodeHasJS(duplicateId);
|
|
632
|
-
}
|
|
633
|
-
newSelectedIds.push(duplicateId);
|
|
634
|
-
const finalIndex = parentId !== null ? this.tree.getChildIndex(duplicateId) : -1;
|
|
635
|
-
ops.push({
|
|
636
|
-
type: "create-node",
|
|
637
|
-
nodeId: duplicateId,
|
|
638
|
-
payload: { parentId, index: finalIndex, rawMarkup, rect },
|
|
639
|
-
undoPayload: { parentId }
|
|
640
|
-
});
|
|
641
|
-
if (parentId) {
|
|
642
|
-
parentsToCommit.add(parentId);
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
rootsToCommit.push(duplicateId);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
if (ops.length > 0) {
|
|
649
|
-
this.selectedIds.clear();
|
|
650
|
-
for (const id of newSelectedIds) {
|
|
651
|
-
this.selectedIds.add(id);
|
|
652
|
-
}
|
|
653
|
-
this.callbacks.onSelectionChange?.(this.selectedIds);
|
|
654
|
-
this.updateBreadcrumb();
|
|
655
|
-
this.callbacks.onOperationsGenerated?.(ops);
|
|
656
|
-
for (const parentId of parentsToCommit) {
|
|
657
|
-
const html = this.mount.extractHTML(parentId);
|
|
658
|
-
if (html) {
|
|
659
|
-
this.callbacks.onHTMLCommit?.(parentId, html);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
for (const rootId of rootsToCommit) {
|
|
663
|
-
const html = this.mount.extractHTML(rootId);
|
|
664
|
-
if (html) {
|
|
665
|
-
this.callbacks.onHTMLCommit?.(rootId, html);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
this.mount.setTransitionsEnabled(true);
|
|
670
|
-
this.render();
|
|
532
|
+
this.clipboardHandler.duplicateSelectedNode();
|
|
671
533
|
}
|
|
672
534
|
/** Copies the selected node to the internal clipboard. */
|
|
673
535
|
copySelectedNode() {
|
|
674
|
-
|
|
675
|
-
if (topLevelIds.length === 0)
|
|
676
|
-
return;
|
|
677
|
-
this.clipboardItems = [];
|
|
678
|
-
for (const id of topLevelIds) {
|
|
679
|
-
const node = this.tree.get(id);
|
|
680
|
-
const markup = this.mount.extractHTML(id);
|
|
681
|
-
if (node && markup) {
|
|
682
|
-
this.clipboardItems.push({
|
|
683
|
-
rawMarkup: markup,
|
|
684
|
-
rect: node.currentRect ? { ...node.currentRect } : null,
|
|
685
|
-
hasJS: this.jsMarkedNodes.has(id),
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
}
|
|
536
|
+
this.clipboardHandler.copySelectedNode();
|
|
689
537
|
}
|
|
690
538
|
/** Cuts the selected node to the clipboard, removing it from the canvas. */
|
|
691
539
|
cutSelectedNode() {
|
|
692
|
-
this.
|
|
693
|
-
this.deleteSelectedNode();
|
|
540
|
+
this.clipboardHandler.cutSelectedNode();
|
|
694
541
|
}
|
|
695
542
|
/** Pastes the node currently in the clipboard into the canvas. */
|
|
696
543
|
pasteNode() {
|
|
697
|
-
|
|
698
|
-
return;
|
|
699
|
-
this.mount.setTransitionsEnabled(false);
|
|
700
|
-
const newSelectedIds = [];
|
|
701
|
-
const ops = [];
|
|
702
|
-
const parentsToCommit = new Set();
|
|
703
|
-
const rootsToCommit = [];
|
|
704
|
-
const targets = this.selectedIds.size > 0 ? this.getTopLevelSelectedIds() : [];
|
|
705
|
-
if (targets.length === 0) {
|
|
706
|
-
// Paste all items at root level
|
|
707
|
-
for (const item of this.clipboardItems) {
|
|
708
|
-
this.newElementCounter++;
|
|
709
|
-
const id = `pasted-${this.newElementCounter}-${Date.now().toString(36)}`;
|
|
710
|
-
let rect;
|
|
711
|
-
if (item.rect) {
|
|
712
|
-
rect = {
|
|
713
|
-
x: item.rect.x + 20,
|
|
714
|
-
y: item.rect.y + 20,
|
|
715
|
-
width: item.rect.width,
|
|
716
|
-
height: item.rect.height,
|
|
717
|
-
};
|
|
718
|
-
item.rect = {
|
|
719
|
-
x: item.rect.x + 20,
|
|
720
|
-
y: item.rect.y + 20,
|
|
721
|
-
width: item.rect.width,
|
|
722
|
-
height: item.rect.height,
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
else {
|
|
726
|
-
rect = { x: 100, y: 100, width: 120, height: 120 };
|
|
727
|
-
}
|
|
728
|
-
this.addNode({
|
|
729
|
-
id,
|
|
730
|
-
rawMarkup: item.rawMarkup,
|
|
731
|
-
currentRect: rect,
|
|
732
|
-
}, null);
|
|
733
|
-
if (item.hasJS) {
|
|
734
|
-
this.markNodeHasJS(id);
|
|
735
|
-
}
|
|
736
|
-
newSelectedIds.push(id);
|
|
737
|
-
ops.push({
|
|
738
|
-
type: "create-node",
|
|
739
|
-
nodeId: id,
|
|
740
|
-
payload: { parentId: null, index: undefined, rawMarkup: item.rawMarkup, rect },
|
|
741
|
-
undoPayload: { parentId: null }
|
|
742
|
-
});
|
|
743
|
-
rootsToCommit.push(id);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
else {
|
|
747
|
-
// Paste next to or inside each target
|
|
748
|
-
for (const targetId of targets) {
|
|
749
|
-
const targetNode = this.tree.get(targetId);
|
|
750
|
-
if (!targetNode)
|
|
751
|
-
continue;
|
|
752
|
-
const isContainer = this.tree.isContainer(targetId);
|
|
753
|
-
const parentId = isContainer ? targetId : targetNode.parentId;
|
|
754
|
-
let startIndex = isContainer ? 0 : this.tree.getChildIndex(targetId) + 1;
|
|
755
|
-
for (const item of this.clipboardItems) {
|
|
756
|
-
this.newElementCounter++;
|
|
757
|
-
const id = `pasted-${this.newElementCounter}-${Date.now().toString(36)}`;
|
|
758
|
-
let rect;
|
|
759
|
-
if (parentId === null) {
|
|
760
|
-
if (item.rect) {
|
|
761
|
-
rect = {
|
|
762
|
-
x: item.rect.x + 20,
|
|
763
|
-
y: item.rect.y + 20,
|
|
764
|
-
width: item.rect.width,
|
|
765
|
-
height: item.rect.height,
|
|
766
|
-
};
|
|
767
|
-
item.rect = {
|
|
768
|
-
x: item.rect.x + 20,
|
|
769
|
-
y: item.rect.y + 20,
|
|
770
|
-
width: item.rect.width,
|
|
771
|
-
height: item.rect.height,
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
else {
|
|
775
|
-
rect = { x: 100, y: 100, width: 120, height: 120 };
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
else {
|
|
779
|
-
if (item.rect) {
|
|
780
|
-
rect = {
|
|
781
|
-
x: 0,
|
|
782
|
-
y: 0,
|
|
783
|
-
width: item.rect.width,
|
|
784
|
-
height: item.rect.height,
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
else {
|
|
788
|
-
rect = { x: 0, y: 0, width: 120, height: 120 };
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
this.addNode({
|
|
792
|
-
id,
|
|
793
|
-
rawMarkup: item.rawMarkup,
|
|
794
|
-
currentRect: rect,
|
|
795
|
-
}, parentId, startIndex);
|
|
796
|
-
if (parentId !== null) {
|
|
797
|
-
startIndex++;
|
|
798
|
-
}
|
|
799
|
-
if (item.hasJS) {
|
|
800
|
-
this.markNodeHasJS(id);
|
|
801
|
-
}
|
|
802
|
-
newSelectedIds.push(id);
|
|
803
|
-
const finalIndex = parentId !== null ? this.tree.getChildIndex(id) : -1;
|
|
804
|
-
ops.push({
|
|
805
|
-
type: "create-node",
|
|
806
|
-
nodeId: id,
|
|
807
|
-
payload: { parentId, index: finalIndex, rawMarkup: item.rawMarkup, rect },
|
|
808
|
-
undoPayload: { parentId }
|
|
809
|
-
});
|
|
810
|
-
if (parentId) {
|
|
811
|
-
parentsToCommit.add(parentId);
|
|
812
|
-
}
|
|
813
|
-
else {
|
|
814
|
-
rootsToCommit.push(id);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
if (ops.length > 0) {
|
|
820
|
-
this.selectedIds.clear();
|
|
821
|
-
for (const id of newSelectedIds) {
|
|
822
|
-
this.selectedIds.add(id);
|
|
823
|
-
}
|
|
824
|
-
this.callbacks.onSelectionChange?.(this.selectedIds);
|
|
825
|
-
this.updateBreadcrumb();
|
|
826
|
-
this.callbacks.onOperationsGenerated?.(ops);
|
|
827
|
-
for (const parentId of parentsToCommit) {
|
|
828
|
-
const html = this.mount.extractHTML(parentId);
|
|
829
|
-
if (html) {
|
|
830
|
-
this.callbacks.onHTMLCommit?.(parentId, html);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
for (const rootId of rootsToCommit) {
|
|
834
|
-
const html = this.mount.extractHTML(rootId);
|
|
835
|
-
if (html) {
|
|
836
|
-
this.callbacks.onHTMLCommit?.(rootId, html);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
this.mount.setTransitionsEnabled(true);
|
|
841
|
-
this.render();
|
|
544
|
+
this.clipboardHandler.pasteNode();
|
|
842
545
|
}
|
|
843
546
|
// ── Public API: State Forcing ───────────────────
|
|
844
547
|
/** Forces a pseudo-class state (hover, active, focus) on the specified node element. */
|
|
@@ -876,6 +579,87 @@ export class Workspace {
|
|
|
876
579
|
hasJSMark(nodeId) {
|
|
877
580
|
return this.jsMarkedNodes.has(nodeId);
|
|
878
581
|
}
|
|
582
|
+
// ── Public API: Layer Locking ───────────────────
|
|
583
|
+
/**
|
|
584
|
+
* Locks a node, making it non-interactive on the canvas.
|
|
585
|
+
* Locked nodes cannot be selected, dragged, resized, or hovered
|
|
586
|
+
* via user pointer/keyboard interaction. If the node is currently
|
|
587
|
+
* selected, it will be deselected. Locking a parent node also
|
|
588
|
+
* locks all of its descendants.
|
|
589
|
+
*/
|
|
590
|
+
lockNode(nodeId) {
|
|
591
|
+
this.lockedNodes.add(nodeId);
|
|
592
|
+
// Deselect this node and any locked descendants
|
|
593
|
+
let changed = false;
|
|
594
|
+
for (const selId of [...this.selectedIds]) {
|
|
595
|
+
if (this.isNodeLocked(selId)) {
|
|
596
|
+
this.selectedIds.delete(selId);
|
|
597
|
+
changed = true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (changed) {
|
|
601
|
+
this.callbacks.onSelectionChange?.(this.selectedIds);
|
|
602
|
+
this.updateBreadcrumb();
|
|
603
|
+
}
|
|
604
|
+
this.render();
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Unlocks a previously locked node, restoring interactivity.
|
|
608
|
+
*/
|
|
609
|
+
unlockNode(nodeId) {
|
|
610
|
+
this.lockedNodes.delete(nodeId);
|
|
611
|
+
this.render();
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Returns whether a node is currently locked (directly or via a locked ancestor).
|
|
615
|
+
*/
|
|
616
|
+
isNodeLocked(nodeId) {
|
|
617
|
+
if (this.lockedNodes.has(nodeId))
|
|
618
|
+
return true;
|
|
619
|
+
// Walk up the tree checking ancestors
|
|
620
|
+
let currentId = this.tree.get(nodeId)?.parentId ?? null;
|
|
621
|
+
while (currentId !== null) {
|
|
622
|
+
if (this.lockedNodes.has(currentId))
|
|
623
|
+
return true;
|
|
624
|
+
currentId = this.tree.get(currentId)?.parentId ?? null;
|
|
625
|
+
}
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Returns the set of directly locked node IDs.
|
|
630
|
+
* Does not include nodes that are only locked via ancestor inheritance.
|
|
631
|
+
*/
|
|
632
|
+
getLockedNodeIds() {
|
|
633
|
+
return this.lockedNodes;
|
|
634
|
+
}
|
|
635
|
+
// ── Public API: Property Lock Queries ───────────
|
|
636
|
+
/**
|
|
637
|
+
* Checks whether a CSS property on a node is locked by the host.
|
|
638
|
+
* Delegates to the `isPropertyLocked` callback if provided.
|
|
639
|
+
* Returns `false` (unlocked) when no callback is registered.
|
|
640
|
+
*/
|
|
641
|
+
isPropertyLocked(nodeId, property) {
|
|
642
|
+
return this.callbacks.isPropertyLocked?.(nodeId, property) ?? false;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Notifies the host that the user attempted to adjust a locked property.
|
|
646
|
+
* Reads the current computed value from the node's content root and
|
|
647
|
+
* fires the `onPropertyLockInteraction` callback.
|
|
648
|
+
* No-op when the callback is not registered.
|
|
649
|
+
*/
|
|
650
|
+
notifyPropertyLockInteraction(nodeId, property) {
|
|
651
|
+
if (!this.callbacks.onPropertyLockInteraction)
|
|
652
|
+
return;
|
|
653
|
+
const contentRoot = this.mount.getContentRoot(nodeId);
|
|
654
|
+
let currentValue = "";
|
|
655
|
+
if (contentRoot) {
|
|
656
|
+
currentValue =
|
|
657
|
+
contentRoot.style.getPropertyValue(property) ||
|
|
658
|
+
window.getComputedStyle(contentRoot).getPropertyValue(property) ||
|
|
659
|
+
"";
|
|
660
|
+
}
|
|
661
|
+
this.callbacks.onPropertyLockInteraction(nodeId, property, currentValue);
|
|
662
|
+
}
|
|
879
663
|
// ── Public API: Synthetic Interaction ───────────
|
|
880
664
|
/** Dispatches a synthetic pointer/mouse event (e.g. mouseenter, mouseleave, click) to a node. */
|
|
881
665
|
dispatchInteractionEvent(nodeId, eventName) {
|
|
@@ -1132,6 +916,48 @@ export class Workspace {
|
|
|
1132
916
|
injectCSSLink(href) {
|
|
1133
917
|
return this.mount.injectStylesheetLink(href);
|
|
1134
918
|
}
|
|
919
|
+
// ── Handler Architecture API ─────────────────────
|
|
920
|
+
/**
|
|
921
|
+
* Registers a pointer-gesture handler at the specified priority position.
|
|
922
|
+
* Lower index = higher priority (checked first on pointerdown).
|
|
923
|
+
* If no index is given, the handler is appended (lowest priority).
|
|
924
|
+
*/
|
|
925
|
+
registerInteractionHandler(handler, index) {
|
|
926
|
+
if (index !== undefined) {
|
|
927
|
+
this.interactionHandlers.splice(index, 0, handler);
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
this.interactionHandlers.push(handler);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Registers a keyboard handler at the specified priority position.
|
|
935
|
+
* Lower index = higher priority (checked first on keydown).
|
|
936
|
+
* If no index is given, the handler is appended (lowest priority).
|
|
937
|
+
*/
|
|
938
|
+
registerKeyboardHandler(handler, index) {
|
|
939
|
+
if (index !== undefined) {
|
|
940
|
+
this.keyboardHandlers.splice(index, 0, handler);
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
this.keyboardHandlers.push(handler);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Emit an interaction mode change to the host.
|
|
948
|
+
* Enriches the existing `onInteractionChange` callback with
|
|
949
|
+
* optional `InteractionDetail` for richer host observability.
|
|
950
|
+
*/
|
|
951
|
+
emitInteraction(mode, _detail) {
|
|
952
|
+
this.callbacks.onInteractionChange?.(mode);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Increment and return a unique counter for generating element IDs.
|
|
956
|
+
* Used by handlers that create new nodes (DrawHandler, ClipboardHandler, etc.).
|
|
957
|
+
*/
|
|
958
|
+
nextElementId() {
|
|
959
|
+
return ++this.newElementCounter;
|
|
960
|
+
}
|
|
1135
961
|
// ── Disposal ────────────────────────────────────
|
|
1136
962
|
/** Tears down the workspace completely. */
|
|
1137
963
|
dispose() {
|
|
@@ -1156,11 +982,26 @@ export class Workspace {
|
|
|
1156
982
|
this.selectedIds.clear();
|
|
1157
983
|
}
|
|
1158
984
|
// ── Event Handlers ──────────────────────────────
|
|
1159
|
-
/**
|
|
985
|
+
/**
|
|
986
|
+
* Handles wheel events with Figma-style behavior:
|
|
987
|
+
* - **Trackpad two-finger scroll** → pans the canvas
|
|
988
|
+
* - **Trackpad pinch-to-zoom** → zooms (browsers report this with ctrlKey=true)
|
|
989
|
+
* - **Ctrl + mouse wheel** → zooms
|
|
990
|
+
*/
|
|
1160
991
|
handleWheel(e) {
|
|
1161
992
|
e.preventDefault();
|
|
1162
993
|
const rect = this.getContainerRect();
|
|
1163
|
-
|
|
994
|
+
// Browsers report trackpad pinch gestures as wheel events with ctrlKey=true.
|
|
995
|
+
// Regular two-finger scrolling does NOT set ctrlKey.
|
|
996
|
+
const isPinchOrCtrlWheel = e.ctrlKey || e.metaKey;
|
|
997
|
+
if (isPinchOrCtrlWheel) {
|
|
998
|
+
// Pinch-to-zoom or Ctrl+scroll → zoom anchored at cursor
|
|
999
|
+
this.viewport = applyWheelZoom(e.clientX, e.clientY, e.deltaY, this.viewport, rect, true);
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
// Regular two-finger scroll → pan the canvas
|
|
1003
|
+
this.viewport = applyPan(-e.deltaX, -e.deltaY, this.viewport);
|
|
1004
|
+
}
|
|
1164
1005
|
this.mount.applyViewportTransform(this.viewport);
|
|
1165
1006
|
this.callbacks.onViewportChange?.(this.viewport);
|
|
1166
1007
|
this.render();
|
|
@@ -1170,321 +1011,21 @@ export class Workspace {
|
|
|
1170
1011
|
const rect = this.getContainerRect();
|
|
1171
1012
|
const canvasPos = screenToCanvas(e.clientX, e.clientY, this.viewport, rect);
|
|
1172
1013
|
console.log('DEBUG WORKSPACE DOWN: viewport scale:', this.viewport.scale, 'canvasPos:', canvasPos, 'clientX:', e.clientX, 'clientY:', e.clientY);
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
this.pointerDownInsideSelection = null;
|
|
1187
|
-
// ── Drawing Tool Interception ─────────────────
|
|
1188
|
-
if (this.activeTool !== null && e.button === 0) {
|
|
1189
|
-
this.isDrawingNode = true;
|
|
1190
|
-
this.drawStartCanvas = canvasPos;
|
|
1191
|
-
this.drawCurrentCanvas = canvasPos;
|
|
1192
|
-
this.activeDropTarget = null;
|
|
1193
|
-
this.guides = [];
|
|
1194
|
-
this.safeSetPointerCapture(e.pointerId);
|
|
1195
|
-
this.callbacks.onInteractionChange?.("draw-node");
|
|
1196
|
-
this.render();
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
// ── Space + pointer = Pan ─────────────────────
|
|
1200
|
-
if (this.spaceDown || e.button === 1) {
|
|
1201
|
-
if (e.button === 1) {
|
|
1202
|
-
e.preventDefault();
|
|
1203
|
-
}
|
|
1204
|
-
this.isPanning = true;
|
|
1205
|
-
this.container.classList.add("canvus-panning");
|
|
1206
|
-
this.safeSetPointerCapture(e.pointerId);
|
|
1207
|
-
this.callbacks.onInteractionChange?.("pan");
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
// Calculate isDoubleClick early to prevent handles/adjusters from intercepting double-clicks on small/nested nodes
|
|
1211
|
-
const nodeList = this.getOrderedNodeList();
|
|
1212
|
-
const hitId = hitTestElements(canvasPos.x, canvasPos.y, nodeList);
|
|
1213
|
-
const targetEl = e.composedPath()[0];
|
|
1214
|
-
const now = Date.now();
|
|
1215
|
-
const isSameTarget = targetEl !== null && this.lastPointerDownTarget !== null &&
|
|
1216
|
-
(targetEl === this.lastPointerDownTarget || this.lastPointerDownTarget.contains(targetEl) || targetEl.contains(this.lastPointerDownTarget));
|
|
1217
|
-
const isDoubleClick = (now - this.lastPointerDownTime < 350) && (hitId !== null &&
|
|
1218
|
-
this.lastPointerDownId !== null &&
|
|
1219
|
-
(hitId === this.lastPointerDownId || isSameTarget || this.tree.isAncestor(this.lastPointerDownId, hitId)));
|
|
1220
|
-
this.lastPointerDownTime = now;
|
|
1221
|
-
this.lastPointerDownId = hitId;
|
|
1222
|
-
this.lastPointerDownTarget = targetEl;
|
|
1223
|
-
// ── Handle hit-test (resize) ──────────────────
|
|
1224
|
-
if (!isDoubleClick && this.selectedIds.size === 1) {
|
|
1225
|
-
const selId = this.selectedIds.values().next().value;
|
|
1226
|
-
const selNode = this.tree.get(selId);
|
|
1227
|
-
if (selNode?.currentRect) {
|
|
1228
|
-
const localX = e.clientX - rect.x;
|
|
1229
|
-
const localY = e.clientY - rect.y;
|
|
1230
|
-
const anchor = this.renderer.hitTestHandle(localX, localY, selNode.currentRect, this.viewport);
|
|
1231
|
-
if (anchor) {
|
|
1232
|
-
this.isResizing = true;
|
|
1233
|
-
this.activeAnchor = anchor;
|
|
1234
|
-
this.dragStartCanvas = canvasPos;
|
|
1235
|
-
this.resizeStartRect = { ...selNode.currentRect };
|
|
1236
|
-
const contentRoot = this.mount.getContentRoot(selId);
|
|
1237
|
-
if (contentRoot) {
|
|
1238
|
-
this.dragStartStyles = {
|
|
1239
|
-
"grid-column-start": contentRoot.style.gridColumnStart || null,
|
|
1240
|
-
"grid-column-end": contentRoot.style.gridColumnEnd || null,
|
|
1241
|
-
"grid-row-start": contentRoot.style.gridRowStart || null,
|
|
1242
|
-
"grid-row-end": contentRoot.style.gridRowEnd || null,
|
|
1243
|
-
"position": contentRoot.style.position || null,
|
|
1244
|
-
"left": contentRoot.style.left || null,
|
|
1245
|
-
"top": contentRoot.style.top || null,
|
|
1246
|
-
"width": contentRoot.style.width || null,
|
|
1247
|
-
"height": contentRoot.style.height || null,
|
|
1248
|
-
};
|
|
1249
|
-
}
|
|
1250
|
-
this.render();
|
|
1014
|
+
// ── Handler Dispatch: try registered handlers first ──────
|
|
1015
|
+
if (this.interactionHandlers.length > 0) {
|
|
1016
|
+
const nodeList = this.getOrderedNodeList();
|
|
1017
|
+
const hitId = hitTestElements(canvasPos.x, canvasPos.y, nodeList);
|
|
1018
|
+
for (const handler of this.interactionHandlers) {
|
|
1019
|
+
if (handler.claim(e, canvasPos, hitId, rect)) {
|
|
1020
|
+
this.activeHandler = handler;
|
|
1021
|
+
const isDoubleClick = (Date.now() - this.lastPointerDownTime < 350);
|
|
1022
|
+
this.lastPointerDownTime = isDoubleClick ? 0 : Date.now();
|
|
1023
|
+
this.lastPointerDownId = isDoubleClick ? null : hitId;
|
|
1024
|
+
this.lastPointerDownTarget = isDoubleClick ? null : (e.composedPath()[0] || null);
|
|
1251
1025
|
return;
|
|
1252
1026
|
}
|
|
1253
1027
|
}
|
|
1254
1028
|
}
|
|
1255
|
-
// ── Corner Radius handles hit-test ────────────
|
|
1256
|
-
if (!isDoubleClick && this.selectedIds.size > 0) {
|
|
1257
|
-
const localX = e.clientX - rect.x;
|
|
1258
|
-
const localY = e.clientY - rect.y;
|
|
1259
|
-
let hitRadiusCorner = null;
|
|
1260
|
-
let targetNodeId = null;
|
|
1261
|
-
for (const selId of this.selectedIds) {
|
|
1262
|
-
const selNode = this.tree.get(selId);
|
|
1263
|
-
if (selNode && isContainerNode(selNode) && selNode.currentRect) {
|
|
1264
|
-
const hit = this.hitTestRadiusHandle(localX, localY, selNode.currentRect, this.viewport);
|
|
1265
|
-
if (hit) {
|
|
1266
|
-
hitRadiusCorner = hit;
|
|
1267
|
-
targetNodeId = selId;
|
|
1268
|
-
break;
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
if (hitRadiusCorner && targetNodeId) {
|
|
1273
|
-
this.isAdjustingRadius = true;
|
|
1274
|
-
this.activeRadiusCorner = hitRadiusCorner;
|
|
1275
|
-
this.radiusTargetNodeId = targetNodeId;
|
|
1276
|
-
this.radiusStartValues.clear();
|
|
1277
|
-
for (const selId of this.selectedIds) {
|
|
1278
|
-
const selNode = this.tree.get(selId);
|
|
1279
|
-
if (selNode && isContainerNode(selNode)) {
|
|
1280
|
-
const contentRoot = this.mount.getContentRoot(selId);
|
|
1281
|
-
let initialRadiusStr = "0px";
|
|
1282
|
-
if (contentRoot) {
|
|
1283
|
-
initialRadiusStr = contentRoot.style.borderRadius || window.getComputedStyle(contentRoot).borderRadius || "0px";
|
|
1284
|
-
}
|
|
1285
|
-
this.radiusStartValues.set(selId, initialRadiusStr);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
this.dragStartCanvas = canvasPos;
|
|
1289
|
-
this.safeSetPointerCapture(e.pointerId);
|
|
1290
|
-
this.callbacks.onInteractionChange?.("resize-radius");
|
|
1291
|
-
this.render();
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
// ── Spacing Adjusters hit-test ────────────────
|
|
1296
|
-
if (!isDoubleClick && this.selectedIds.size === 1) {
|
|
1297
|
-
const selId = this.selectedIds.values().next().value;
|
|
1298
|
-
const adjusters = this.computeSpacingAdjusters(selId);
|
|
1299
|
-
const hitAdjuster = adjusters.find(adj => canvasPos.x >= adj.rect.x &&
|
|
1300
|
-
canvasPos.x <= adj.rect.x + adj.rect.width &&
|
|
1301
|
-
canvasPos.y >= adj.rect.y &&
|
|
1302
|
-
canvasPos.y <= adj.rect.y + adj.rect.height);
|
|
1303
|
-
if (hitAdjuster) {
|
|
1304
|
-
this.activeAdjusterType = hitAdjuster.type;
|
|
1305
|
-
this.adjusterStartValue = hitAdjuster.value;
|
|
1306
|
-
const contentRoot = this.mount.getContentRoot(selId);
|
|
1307
|
-
this.adjusterStartValueStr = contentRoot ? (contentRoot.style.getPropertyValue(hitAdjuster.type) || null) : null;
|
|
1308
|
-
this.dragStartCanvas = canvasPos;
|
|
1309
|
-
this.render();
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
// ── Node hit-test (select + drag) ─────────────
|
|
1314
|
-
let targetSelectId = null;
|
|
1315
|
-
let clickInsideSelection = false;
|
|
1316
|
-
const hasModifier = e.shiftKey || e.metaKey || e.ctrlKey;
|
|
1317
|
-
if (this.selectedIds.size > 0 && !hasModifier && !isDoubleClick) {
|
|
1318
|
-
for (const selId of this.selectedIds) {
|
|
1319
|
-
const selNode = this.tree.get(selId);
|
|
1320
|
-
if (selNode?.currentRect && isPointInElement(canvasPos.x, canvasPos.y, selNode.currentRect)) {
|
|
1321
|
-
clickInsideSelection = true;
|
|
1322
|
-
targetSelectId = selId;
|
|
1323
|
-
this.pointerDownInsideSelection = selId;
|
|
1324
|
-
break;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
if (!clickInsideSelection) {
|
|
1329
|
-
this.pointerDownInsideSelection = null;
|
|
1330
|
-
if (hitId) {
|
|
1331
|
-
const isCmdClick = e.metaKey || e.ctrlKey;
|
|
1332
|
-
if (isCmdClick) {
|
|
1333
|
-
// Cmd+Click: deep select the hit element directly
|
|
1334
|
-
targetSelectId = hitId;
|
|
1335
|
-
this.enteredContainerId = this.tree.get(hitId)?.parentId ?? null;
|
|
1336
|
-
}
|
|
1337
|
-
else if (isDoubleClick) {
|
|
1338
|
-
// Double click: Figma-like drill down
|
|
1339
|
-
const path = this.tree.getPath(hitId);
|
|
1340
|
-
let foundSelectedIdx = -1;
|
|
1341
|
-
for (let i = 0; i < path.length; i++) {
|
|
1342
|
-
if (this.selectedIds.has(path[i].id)) {
|
|
1343
|
-
foundSelectedIdx = i;
|
|
1344
|
-
break;
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
if (foundSelectedIdx !== -1 && foundSelectedIdx < path.length - 1) {
|
|
1348
|
-
// Drill down one level
|
|
1349
|
-
const nextParent = path[foundSelectedIdx];
|
|
1350
|
-
const nextSelect = path[foundSelectedIdx + 1];
|
|
1351
|
-
this.enteredContainerId = nextParent.id;
|
|
1352
|
-
targetSelectId = nextSelect.id;
|
|
1353
|
-
}
|
|
1354
|
-
else if (foundSelectedIdx === path.length - 1) {
|
|
1355
|
-
// Leaf is already selected: keep selection on leaf to trigger text editing
|
|
1356
|
-
targetSelectId = path[path.length - 1].id;
|
|
1357
|
-
this.enteredContainerId = path[path.length - 2]?.id ?? null;
|
|
1358
|
-
}
|
|
1359
|
-
else {
|
|
1360
|
-
// Nothing in the path is selected
|
|
1361
|
-
if (path.length > 0) {
|
|
1362
|
-
targetSelectId = path[0].id;
|
|
1363
|
-
this.enteredContainerId = null;
|
|
1364
|
-
}
|
|
1365
|
-
else {
|
|
1366
|
-
targetSelectId = hitId;
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
else {
|
|
1371
|
-
// Single Click: resolve based on current entered scope
|
|
1372
|
-
const resolvedId = this.findSelectableNode(hitId, this.enteredContainerId);
|
|
1373
|
-
if (resolvedId) {
|
|
1374
|
-
targetSelectId = resolvedId;
|
|
1375
|
-
const node = this.tree.get(resolvedId);
|
|
1376
|
-
this.enteredContainerId = node?.parentId ?? null;
|
|
1377
|
-
}
|
|
1378
|
-
else {
|
|
1379
|
-
// Clicked outside currently entered container: exit scope, select root ancestor
|
|
1380
|
-
this.enteredContainerId = null;
|
|
1381
|
-
targetSelectId = this.findSelectableNode(hitId, null);
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
if (isDoubleClick && targetSelectId && this.selectedIds.has(targetSelectId)) {
|
|
1386
|
-
this.editAllowedOnDblClick = true;
|
|
1387
|
-
}
|
|
1388
|
-
else {
|
|
1389
|
-
this.editAllowedOnDblClick = false;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
if (targetSelectId) {
|
|
1393
|
-
if (!clickInsideSelection) {
|
|
1394
|
-
const prevSelection = new Set(this.selectedIds);
|
|
1395
|
-
const isShift = e.shiftKey;
|
|
1396
|
-
if (isShift) {
|
|
1397
|
-
if (this.selectedIds.has(targetSelectId)) {
|
|
1398
|
-
this.selectedIds.delete(targetSelectId);
|
|
1399
|
-
}
|
|
1400
|
-
else {
|
|
1401
|
-
this.selectedIds.add(targetSelectId);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
else {
|
|
1405
|
-
this.selectedIds.clear();
|
|
1406
|
-
this.selectedIds.add(targetSelectId);
|
|
1407
|
-
}
|
|
1408
|
-
this.syncLazyChildren(prevSelection, this.selectedIds);
|
|
1409
|
-
this.callbacks.onSelectionChange?.(this.selectedIds);
|
|
1410
|
-
this.updateBreadcrumb();
|
|
1411
|
-
}
|
|
1412
|
-
this.isDragging = false;
|
|
1413
|
-
this.pointerDownReadyToDrag = true;
|
|
1414
|
-
this.dragStartCanvas = canvasPos;
|
|
1415
|
-
this.dragStartNodes.clear();
|
|
1416
|
-
const topLevelIds = this.getTopLevelSelectedIds();
|
|
1417
|
-
for (const selId of topLevelIds) {
|
|
1418
|
-
const selNode = this.tree.get(selId);
|
|
1419
|
-
if (selNode && selNode.currentRect) {
|
|
1420
|
-
const contentRoot = this.mount.getContentRoot(selId);
|
|
1421
|
-
let startStyles = null;
|
|
1422
|
-
if (contentRoot) {
|
|
1423
|
-
startStyles = {
|
|
1424
|
-
"grid-column-start": contentRoot.style.gridColumnStart || null,
|
|
1425
|
-
"grid-column-end": contentRoot.style.gridColumnEnd || null,
|
|
1426
|
-
"grid-row-start": contentRoot.style.gridRowStart || null,
|
|
1427
|
-
"grid-row-end": contentRoot.style.gridRowEnd || null,
|
|
1428
|
-
"position": contentRoot.style.position || null,
|
|
1429
|
-
"left": contentRoot.style.left || null,
|
|
1430
|
-
"top": contentRoot.style.top || null,
|
|
1431
|
-
"width": contentRoot.style.width || null,
|
|
1432
|
-
"height": contentRoot.style.height || null,
|
|
1433
|
-
};
|
|
1434
|
-
}
|
|
1435
|
-
this.dragStartNodes.set(selId, {
|
|
1436
|
-
startPos: { x: selNode.currentRect.x, y: selNode.currentRect.y },
|
|
1437
|
-
startParentId: selNode.parentId,
|
|
1438
|
-
startIndex: this.tree.getChildIndex(selId),
|
|
1439
|
-
startStyles,
|
|
1440
|
-
});
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
let primaryId = targetSelectId;
|
|
1444
|
-
if (!topLevelIds.includes(targetSelectId)) {
|
|
1445
|
-
const path = this.tree.getPath(targetSelectId);
|
|
1446
|
-
for (const node of path) {
|
|
1447
|
-
if (topLevelIds.includes(node.id)) {
|
|
1448
|
-
primaryId = node.id;
|
|
1449
|
-
break;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
const contentRoot = this.mount.getContentRoot(primaryId);
|
|
1454
|
-
if (contentRoot) {
|
|
1455
|
-
this.dragStartStyles = {
|
|
1456
|
-
"grid-column-start": contentRoot.style.gridColumnStart || null,
|
|
1457
|
-
"grid-column-end": contentRoot.style.gridColumnEnd || null,
|
|
1458
|
-
"grid-row-start": contentRoot.style.gridRowStart || null,
|
|
1459
|
-
"grid-row-end": contentRoot.style.gridRowEnd || null,
|
|
1460
|
-
"position": contentRoot.style.position || null,
|
|
1461
|
-
"left": contentRoot.style.left || null,
|
|
1462
|
-
"top": contentRoot.style.top || null,
|
|
1463
|
-
"width": contentRoot.style.width || null,
|
|
1464
|
-
"height": contentRoot.style.height || null,
|
|
1465
|
-
};
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
else {
|
|
1469
|
-
// Click on empty space — start marquee selection
|
|
1470
|
-
const isShift = e.shiftKey;
|
|
1471
|
-
if (!isShift) {
|
|
1472
|
-
const prevSelection = new Set(this.selectedIds);
|
|
1473
|
-
this.selectedIds.clear();
|
|
1474
|
-
this.enteredContainerId = null;
|
|
1475
|
-
this.guides = [];
|
|
1476
|
-
this.syncLazyChildren(prevSelection, this.selectedIds);
|
|
1477
|
-
this.callbacks.onSelectionChange?.(this.selectedIds);
|
|
1478
|
-
this.updateBreadcrumb();
|
|
1479
|
-
}
|
|
1480
|
-
this.preMarqueeSelectedIds = new Set(this.selectedIds);
|
|
1481
|
-
this.isMarqueeSelecting = true;
|
|
1482
|
-
this.marqueeStartCanvas = canvasPos;
|
|
1483
|
-
this.marqueeCurrentCanvas = canvasPos;
|
|
1484
|
-
this.safeSetPointerCapture(e.pointerId);
|
|
1485
|
-
this.callbacks.onInteractionChange?.("select-marquee");
|
|
1486
|
-
}
|
|
1487
|
-
this.render();
|
|
1488
1029
|
}
|
|
1489
1030
|
/**
|
|
1490
1031
|
* The core **Synchronous Reflow Loop**.
|
|
@@ -1502,257 +1043,17 @@ export class Workspace {
|
|
|
1502
1043
|
const rect = this.getContainerRect();
|
|
1503
1044
|
const canvasPos = screenToCanvas(e.clientX, e.clientY, this.viewport, rect);
|
|
1504
1045
|
this.lastCanvasPos = canvasPos;
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
if (this.previewMode) {
|
|
1509
|
-
if (this.isPanning) {
|
|
1510
|
-
this.viewport = applyPan(e.movementX, e.movementY, this.viewport);
|
|
1511
|
-
this.mount.applyViewportTransform(this.viewport);
|
|
1512
|
-
this.callbacks.onViewportChange?.(this.viewport);
|
|
1513
|
-
this.render();
|
|
1514
|
-
}
|
|
1515
|
-
return;
|
|
1516
|
-
}
|
|
1517
|
-
// ── Drawing Tool Dragging ─────────────────────
|
|
1518
|
-
if (this.isDrawingNode && this.drawStartCanvas) {
|
|
1519
|
-
this.drawCurrentCanvas = canvasPos;
|
|
1520
|
-
// Dynamically resolve target container and placement index to show guidelines preview
|
|
1521
|
-
this.activeDropTarget = findDropTarget("__new_node__", canvasPos, this.tree, (id) => this.mount.getWrapper(id), (id) => this.mount.getContentRoot(id));
|
|
1522
|
-
this.render();
|
|
1523
|
-
return;
|
|
1524
|
-
}
|
|
1525
|
-
// ── Corner Radius Adjusting ───────────────────
|
|
1526
|
-
if (this.isAdjustingRadius && this.dragStartCanvas && this.radiusTargetNodeId) {
|
|
1527
|
-
const targetNode = this.tree.get(this.radiusTargetNodeId);
|
|
1528
|
-
if (targetNode && targetNode.currentRect) {
|
|
1529
|
-
this.safeSetPointerCapture(e.pointerId);
|
|
1530
|
-
this.container.style.cursor = "pointer";
|
|
1531
|
-
this.canvas.style.pointerEvents = "auto";
|
|
1532
|
-
this.callbacks.onInteractionChange?.("resize-radius");
|
|
1533
|
-
const bounds = targetNode.currentRect;
|
|
1534
|
-
const s = this.viewport.scale;
|
|
1535
|
-
const ox = this.viewport.offsetX;
|
|
1536
|
-
const oy = this.viewport.offsetY;
|
|
1537
|
-
const left = bounds.x * s + ox;
|
|
1538
|
-
const top = bounds.y * s + oy;
|
|
1539
|
-
const right = (bounds.x + bounds.width) * s + ox;
|
|
1540
|
-
const bottom = (bounds.y + bounds.height) * s + oy;
|
|
1541
|
-
let dragX = 0;
|
|
1542
|
-
let dragY = 0;
|
|
1543
|
-
if (this.activeRadiusCorner === "tl") {
|
|
1544
|
-
dragX = e.clientX - rect.x - left;
|
|
1545
|
-
dragY = e.clientY - rect.y - top;
|
|
1546
|
-
}
|
|
1547
|
-
else if (this.activeRadiusCorner === "tr") {
|
|
1548
|
-
dragX = right - (e.clientX - rect.x);
|
|
1549
|
-
dragY = e.clientY - rect.y - top;
|
|
1550
|
-
}
|
|
1551
|
-
else if (this.activeRadiusCorner === "bl") {
|
|
1552
|
-
dragX = e.clientX - rect.x - left;
|
|
1553
|
-
dragY = bottom - (e.clientY - rect.y);
|
|
1554
|
-
}
|
|
1555
|
-
else if (this.activeRadiusCorner === "br") {
|
|
1556
|
-
dragX = right - (e.clientX - rect.x);
|
|
1557
|
-
dragY = bottom - (e.clientY - rect.y);
|
|
1558
|
-
}
|
|
1559
|
-
const dragDistScreen = (dragX + dragY) / 2;
|
|
1560
|
-
const dragDistCanvas = dragDistScreen / s;
|
|
1561
|
-
// Apply to all selected containers
|
|
1562
|
-
for (const selId of this.selectedIds) {
|
|
1563
|
-
const selNode = this.tree.get(selId);
|
|
1564
|
-
if (selNode && isContainerNode(selNode) && selNode.currentRect) {
|
|
1565
|
-
const maxRadius = Math.min(selNode.currentRect.width, selNode.currentRect.height) / 2;
|
|
1566
|
-
const newRadius = Math.max(0, Math.min(maxRadius, Math.round(dragDistCanvas)));
|
|
1567
|
-
this.mount.setNodeStyle(selId, "border-radius", `${newRadius}px`);
|
|
1568
|
-
this.remeasureSubtree(selId);
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
this.render();
|
|
1572
|
-
}
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
// ── Spacing Adjusters Dragging ────────────────
|
|
1576
|
-
if (this.activeAdjusterType && this.dragStartCanvas) {
|
|
1577
|
-
const selId = this.selectedIds.values().next().value;
|
|
1578
|
-
const node = this.tree.get(selId);
|
|
1579
|
-
if (!node)
|
|
1580
|
-
return;
|
|
1581
|
-
this.safeSetPointerCapture(e.pointerId);
|
|
1582
|
-
this.canvas.style.pointerEvents = "auto";
|
|
1583
|
-
this.callbacks.onInteractionChange?.("adjust-spacing");
|
|
1584
|
-
const isVertical = this.activeAdjusterType.includes("top") || this.activeAdjusterType.includes("bottom");
|
|
1585
|
-
this.container.style.cursor = isVertical ? "ns-resize" : "ew-resize";
|
|
1586
|
-
this.canvas.style.pointerEvents = "auto";
|
|
1587
|
-
const dx = canvasPos.x - this.dragStartCanvas.x;
|
|
1588
|
-
const dy = canvasPos.y - this.dragStartCanvas.y;
|
|
1589
|
-
let delta = 0;
|
|
1590
|
-
switch (this.activeAdjusterType) {
|
|
1591
|
-
case "padding-top":
|
|
1592
|
-
delta = dy;
|
|
1593
|
-
break;
|
|
1594
|
-
case "padding-bottom":
|
|
1595
|
-
delta = dy;
|
|
1596
|
-
break;
|
|
1597
|
-
case "padding-left":
|
|
1598
|
-
delta = dx;
|
|
1599
|
-
break;
|
|
1600
|
-
case "padding-right":
|
|
1601
|
-
delta = -dx;
|
|
1602
|
-
break;
|
|
1603
|
-
case "margin-top":
|
|
1604
|
-
delta = -dy;
|
|
1605
|
-
break;
|
|
1606
|
-
case "margin-bottom":
|
|
1607
|
-
delta = dy;
|
|
1608
|
-
break;
|
|
1609
|
-
case "margin-left":
|
|
1610
|
-
delta = -dx;
|
|
1611
|
-
break;
|
|
1612
|
-
case "margin-right":
|
|
1613
|
-
delta = -dx;
|
|
1614
|
-
break;
|
|
1615
|
-
}
|
|
1616
|
-
const contentRoot = this.mount.getContentRoot(selId);
|
|
1617
|
-
const internalScale = contentRoot ? this.mount.getElementScale(contentRoot) : 1;
|
|
1618
|
-
const safeScale = internalScale && !isNaN(internalScale) ? internalScale : 1;
|
|
1619
|
-
const newValue = Math.max(0, Math.round(this.adjusterStartValue + delta / safeScale));
|
|
1620
|
-
// Style surgery - direct DOM mutation
|
|
1621
|
-
this.mount.setNodeStyle(selId, this.activeAdjusterType, `${newValue}px`);
|
|
1622
|
-
// Synchronous reflow + measurement
|
|
1623
|
-
this.remeasureSubtree(selId);
|
|
1624
|
-
if (node.parentId) {
|
|
1625
|
-
this.remeasureSubtree(node.parentId);
|
|
1626
|
-
}
|
|
1627
|
-
this.render();
|
|
1046
|
+
// ── Handler Dispatch: route to active handler ────────────
|
|
1047
|
+
if (this.activeHandler) {
|
|
1048
|
+
this.activeHandler.onPointerMove?.(e, canvasPos, rect);
|
|
1628
1049
|
return;
|
|
1629
1050
|
}
|
|
1630
|
-
|
|
1631
|
-
if (this.isMarqueeSelecting && this.marqueeStartCanvas) {
|
|
1632
|
-
this.marqueeCurrentCanvas = canvasPos;
|
|
1633
|
-
const mRect = this.getMarqueeRect();
|
|
1634
|
-
// Find all selectable nodes inside or intersecting the marquee rect
|
|
1635
|
-
const selectableNodes = this.getOrderedNodeList();
|
|
1636
|
-
const currentMarqueeSelection = new Set();
|
|
1637
|
-
for (const node of selectableNodes) {
|
|
1638
|
-
if (!node.currentRect)
|
|
1639
|
-
continue;
|
|
1640
|
-
const treeNode = this.tree.get(node.id);
|
|
1641
|
-
if (!treeNode)
|
|
1642
|
-
continue;
|
|
1643
|
-
// Scoping constraint
|
|
1644
|
-
if (this.enteredContainerId !== null) {
|
|
1645
|
-
if (treeNode.parentId !== this.enteredContainerId)
|
|
1646
|
-
continue;
|
|
1647
|
-
}
|
|
1648
|
-
else {
|
|
1649
|
-
if (treeNode.parentId !== null)
|
|
1650
|
-
continue;
|
|
1651
|
-
}
|
|
1652
|
-
if (rectsIntersect(node.currentRect, mRect)) {
|
|
1653
|
-
currentMarqueeSelection.add(node.id);
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
this.selectedIds.clear();
|
|
1657
|
-
if (e.shiftKey) {
|
|
1658
|
-
for (const id of this.preMarqueeSelectedIds) {
|
|
1659
|
-
this.selectedIds.add(id);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
for (const id of currentMarqueeSelection) {
|
|
1663
|
-
this.selectedIds.add(id);
|
|
1664
|
-
}
|
|
1665
|
-
this.callbacks.onSelectionChange?.(this.selectedIds);
|
|
1666
|
-
this.updateBreadcrumb();
|
|
1667
|
-
this.render();
|
|
1051
|
+
if (this.previewMode) {
|
|
1668
1052
|
return;
|
|
1669
1053
|
}
|
|
1670
|
-
//
|
|
1671
|
-
if (this.pointerDownReadyToDrag && this.dragStartCanvas) {
|
|
1672
|
-
const dx = canvasPos.x - this.dragStartCanvas.x;
|
|
1673
|
-
const dy = canvasPos.y - this.dragStartCanvas.y;
|
|
1674
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1675
|
-
if (dist >= 3) {
|
|
1676
|
-
if (e.altKey && this.selectedIds.size > 0) {
|
|
1677
|
-
const topLevelIds = this.getTopLevelSelectedIds();
|
|
1678
|
-
this.mount.setTransitionsEnabled(false);
|
|
1679
|
-
const newSelectedIds = [];
|
|
1680
|
-
this.dragStartNodes.clear();
|
|
1681
|
-
for (const originalId of topLevelIds) {
|
|
1682
|
-
const originalNode = this.tree.get(originalId);
|
|
1683
|
-
if (originalNode && originalNode.currentRect) {
|
|
1684
|
-
const rawMarkup = this.mount.extractHTML(originalId);
|
|
1685
|
-
if (rawMarkup) {
|
|
1686
|
-
this.newElementCounter++;
|
|
1687
|
-
const duplicateId = `cloned-${this.newElementCounter}-${Date.now().toString(36)}`;
|
|
1688
|
-
const parentId = originalNode.parentId;
|
|
1689
|
-
const index = parentId !== null ? this.tree.getChildIndex(originalId) + 1 : undefined;
|
|
1690
|
-
this.addNode({
|
|
1691
|
-
id: duplicateId,
|
|
1692
|
-
rawMarkup,
|
|
1693
|
-
currentRect: { ...originalNode.currentRect }
|
|
1694
|
-
}, parentId, index);
|
|
1695
|
-
if (this.jsMarkedNodes.has(originalId)) {
|
|
1696
|
-
this.markNodeHasJS(duplicateId);
|
|
1697
|
-
}
|
|
1698
|
-
newSelectedIds.push(duplicateId);
|
|
1699
|
-
const duplicateContentRoot = this.mount.getContentRoot(duplicateId);
|
|
1700
|
-
let startStyles = null;
|
|
1701
|
-
if (duplicateContentRoot) {
|
|
1702
|
-
startStyles = {
|
|
1703
|
-
"grid-column-start": duplicateContentRoot.style.gridColumnStart || null,
|
|
1704
|
-
"grid-column-end": duplicateContentRoot.style.gridColumnEnd || null,
|
|
1705
|
-
"grid-row-start": duplicateContentRoot.style.gridRowStart || null,
|
|
1706
|
-
"grid-row-end": duplicateContentRoot.style.gridRowEnd || null,
|
|
1707
|
-
"position": duplicateContentRoot.style.position || null,
|
|
1708
|
-
"left": duplicateContentRoot.style.left || null,
|
|
1709
|
-
"top": duplicateContentRoot.style.top || null,
|
|
1710
|
-
"width": duplicateContentRoot.style.width || null,
|
|
1711
|
-
"height": duplicateContentRoot.style.height || null,
|
|
1712
|
-
};
|
|
1713
|
-
}
|
|
1714
|
-
this.dragStartNodes.set(duplicateId, {
|
|
1715
|
-
startPos: { x: originalNode.currentRect.x, y: originalNode.currentRect.y },
|
|
1716
|
-
startParentId: parentId,
|
|
1717
|
-
startIndex: parentId !== null ? this.tree.getChildIndex(duplicateId) : -1,
|
|
1718
|
-
startStyles,
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
if (newSelectedIds.length > 0) {
|
|
1724
|
-
this.selectedIds.clear();
|
|
1725
|
-
for (const id of newSelectedIds) {
|
|
1726
|
-
this.selectedIds.add(id);
|
|
1727
|
-
}
|
|
1728
|
-
this.callbacks.onSelectionChange?.(this.selectedIds);
|
|
1729
|
-
this.updateBreadcrumb();
|
|
1730
|
-
const primaryId = newSelectedIds[0];
|
|
1731
|
-
const contentRoot = this.mount.getContentRoot(primaryId);
|
|
1732
|
-
if (contentRoot) {
|
|
1733
|
-
this.dragStartStyles = {
|
|
1734
|
-
"grid-column-start": contentRoot.style.gridColumnStart || null,
|
|
1735
|
-
"grid-column-end": contentRoot.style.gridColumnEnd || null,
|
|
1736
|
-
"grid-row-start": contentRoot.style.gridRowStart || null,
|
|
1737
|
-
"grid-row-end": contentRoot.style.gridRowEnd || null,
|
|
1738
|
-
"position": contentRoot.style.position || null,
|
|
1739
|
-
"left": contentRoot.style.left || null,
|
|
1740
|
-
"top": contentRoot.style.top || null,
|
|
1741
|
-
"width": contentRoot.style.width || null,
|
|
1742
|
-
"height": contentRoot.style.height || null,
|
|
1743
|
-
};
|
|
1744
|
-
}
|
|
1745
|
-
this.isDragCopy = true;
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
this.isDragging = true;
|
|
1749
|
-
this.pointerDownReadyToDrag = false;
|
|
1750
|
-
this.callbacks.onInteractionChange?.("drag-node");
|
|
1751
|
-
this.safeSetPointerCapture(e.pointerId);
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1054
|
+
// (Drawing tool dragging now handled by DrawHandler via dispatch loop)
|
|
1754
1055
|
// ── Hover tracking ────────────────────────────
|
|
1755
|
-
if (!this.
|
|
1056
|
+
if (!this.panHandler.isActive && !this.dragHandler.isDragging && !this.resizeHandler.isResizing && !this.spacingHandler.isAdjustingRadius) {
|
|
1756
1057
|
this.updateHover(e.metaKey || e.ctrlKey);
|
|
1757
1058
|
// Handle hover cursor for multiple elements.
|
|
1758
1059
|
let hoveredSelectedId = null;
|
|
@@ -1773,14 +1074,19 @@ export class Workspace {
|
|
|
1773
1074
|
}
|
|
1774
1075
|
if (hitRadiusCorner) {
|
|
1775
1076
|
this.hoveredRadiusCorner = hitRadiusCorner;
|
|
1776
|
-
|
|
1077
|
+
// ── Lock check: suppress radius cursor if locked ──
|
|
1078
|
+
const radiusLocked = this.isPropertyLocked(hoveredSelectedId, "border-radius");
|
|
1079
|
+
this.container.style.cursor = radiusLocked ? "default" : "pointer";
|
|
1777
1080
|
this.hoveredAdjusterType = null;
|
|
1778
1081
|
}
|
|
1779
1082
|
else {
|
|
1780
1083
|
this.hoveredRadiusCorner = null;
|
|
1781
1084
|
const anchor = this.renderer.hitTestHandle(localX, localY, selNode.currentRect, this.viewport);
|
|
1782
1085
|
if (anchor) {
|
|
1783
|
-
|
|
1086
|
+
// ── Lock check: suppress resize cursor if locked ──
|
|
1087
|
+
const affectedProps = getLockedPropertiesForAnchor(anchor);
|
|
1088
|
+
const anyLocked = affectedProps.some(p => this.isPropertyLocked(hoveredSelectedId, p));
|
|
1089
|
+
this.container.style.cursor = anyLocked ? "default" : anchorCursor(anchor);
|
|
1784
1090
|
this.hoveredAdjusterType = null;
|
|
1785
1091
|
}
|
|
1786
1092
|
else {
|
|
@@ -1791,249 +1097,37 @@ export class Workspace {
|
|
|
1791
1097
|
canvasPos.y >= adj.rect.y &&
|
|
1792
1098
|
canvasPos.y <= adj.rect.y + adj.rect.height);
|
|
1793
1099
|
if (hoveredAdj) {
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
else {
|
|
1806
|
-
this.hoveredRadiusCorner = null;
|
|
1807
|
-
this.hoveredAdjusterType = null;
|
|
1808
|
-
this.container.style.cursor = "default";
|
|
1809
|
-
}
|
|
1810
|
-
this.render();
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
// ── Pan ────────────────────────────────────────
|
|
1814
|
-
if (this.isPanning) {
|
|
1815
|
-
this.viewport = applyPan(e.movementX, e.movementY, this.viewport);
|
|
1816
|
-
this.mount.applyViewportTransform(this.viewport);
|
|
1817
|
-
this.callbacks.onViewportChange?.(this.viewport);
|
|
1818
|
-
this.render();
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
// ── Resize (Synchronous Reflow Loop) ──────────
|
|
1822
|
-
if (this.isResizing && this.activeAnchor && this.dragStartCanvas && this.resizeStartRect) {
|
|
1823
|
-
const selId = this.selectedIds.values().next().value;
|
|
1824
|
-
const node = this.tree.get(selId);
|
|
1825
|
-
if (!node)
|
|
1826
|
-
return;
|
|
1827
|
-
this.safeSetPointerCapture(e.pointerId);
|
|
1828
|
-
this.container.style.cursor = anchorCursor(this.activeAnchor);
|
|
1829
|
-
this.canvas.style.pointerEvents = "auto";
|
|
1830
|
-
this.callbacks.onInteractionChange?.("resize-node");
|
|
1831
|
-
const dx = canvasPos.x - this.dragStartCanvas.x;
|
|
1832
|
-
const dy = canvasPos.y - this.dragStartCanvas.y;
|
|
1833
|
-
const wrapper = this.mount.getWrapper(selId);
|
|
1834
|
-
let parentIsGrid = false;
|
|
1835
|
-
let gridInfo = null;
|
|
1836
|
-
let parentRect = null;
|
|
1837
|
-
let padLeft = 0;
|
|
1838
|
-
let padTop = 0;
|
|
1839
|
-
if (node.parentId !== null) {
|
|
1840
|
-
const parentContent = this.mount.getContentRoot(node.parentId);
|
|
1841
|
-
if (parentContent) {
|
|
1842
|
-
gridInfo = detectLayout(parentContent);
|
|
1843
|
-
if (gridInfo.mode === "grid" || gridInfo.mode === "inline-grid") {
|
|
1844
|
-
parentIsGrid = true;
|
|
1845
|
-
const parentNode = this.tree.get(node.parentId);
|
|
1846
|
-
parentRect = parentNode?.currentRect ?? null;
|
|
1847
|
-
const cs = getComputedStyle(parentContent);
|
|
1848
|
-
padLeft = parseFloat(cs.paddingLeft) || 0;
|
|
1849
|
-
padTop = parseFloat(cs.paddingTop) || 0;
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
if (parentIsGrid && gridInfo && parentRect && wrapper) {
|
|
1854
|
-
const colTracks = parseGridTracks(gridInfo.gridTemplateColumns || "", gridInfo.gap.column);
|
|
1855
|
-
const rowTracks = parseGridTracks(gridInfo.gridTemplateRows || "", gridInfo.gap.row);
|
|
1856
|
-
const contentRoot = this.mount.getContentRoot(selId);
|
|
1857
|
-
if (contentRoot) {
|
|
1858
|
-
const colStart = getGridStart(contentRoot, "column");
|
|
1859
|
-
const rowStart = getGridStart(contentRoot, "row");
|
|
1860
|
-
const colSpan = getGridSpan(contentRoot, "column");
|
|
1861
|
-
const rowSpan = getGridSpan(contentRoot, "row");
|
|
1862
|
-
const cx = canvasPos.x - parentRect.x - padLeft;
|
|
1863
|
-
const cy = canvasPos.y - parentRect.y - padTop;
|
|
1864
|
-
let newColStart = colStart;
|
|
1865
|
-
let newColSpan = colSpan;
|
|
1866
|
-
let newRowStart = rowStart;
|
|
1867
|
-
let newRowSpan = rowSpan;
|
|
1868
|
-
console.log('DEBUG WORKSPACE RESIZE GRID templateRows:', gridInfo.gridTemplateRows, 'templateCols:', gridInfo.gridTemplateColumns);
|
|
1869
|
-
console.log('DEBUG WORKSPACE RESIZE GRID: colTracks:', JSON.stringify(colTracks), 'rowTracks:', JSON.stringify(rowTracks), 'colStart:', colStart, 'colSpan:', colSpan, 'rowStart:', rowStart, 'rowSpan:', rowSpan, 'cx:', cx, 'cy:', cy);
|
|
1870
|
-
const anchor = this.activeAnchor;
|
|
1871
|
-
// West / East column resizing
|
|
1872
|
-
if (anchor.includes("w")) {
|
|
1873
|
-
const colEndIndex = colStart + colSpan;
|
|
1874
|
-
for (let i = 0; i < colTracks.length; i++) {
|
|
1875
|
-
const c = colTracks[i];
|
|
1876
|
-
if (cx <= c.start + c.size + gridInfo.gap.column / 2) {
|
|
1877
|
-
newColStart = Math.min(i + 1, colEndIndex - 1);
|
|
1878
|
-
newColSpan = colEndIndex - newColStart;
|
|
1879
|
-
break;
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
else if (anchor.includes("e")) {
|
|
1884
|
-
for (let i = 0; i < colTracks.length; i++) {
|
|
1885
|
-
const c = colTracks[i];
|
|
1886
|
-
if (cx <= c.start + c.size + gridInfo.gap.column / 2) {
|
|
1887
|
-
newColSpan = Math.max(1, (i + 1) - colStart + 1);
|
|
1888
|
-
break;
|
|
1889
|
-
}
|
|
1890
|
-
newColSpan = Math.max(1, (i + 1) - colStart + 1);
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
// North / South row resizing
|
|
1894
|
-
if (anchor.includes("n")) {
|
|
1895
|
-
const rowEndIndex = rowStart + rowSpan;
|
|
1896
|
-
for (let i = 0; i < rowTracks.length; i++) {
|
|
1897
|
-
const r = rowTracks[i];
|
|
1898
|
-
if (cy <= r.start + r.size + gridInfo.gap.row / 2) {
|
|
1899
|
-
newRowStart = Math.min(i + 1, rowEndIndex - 1);
|
|
1900
|
-
newRowSpan = rowEndIndex - newRowStart;
|
|
1901
|
-
break;
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
else if (anchor.includes("s")) {
|
|
1906
|
-
for (let i = 0; i < rowTracks.length; i++) {
|
|
1907
|
-
const r = rowTracks[i];
|
|
1908
|
-
if (cy <= r.start + r.size + gridInfo.gap.row / 2) {
|
|
1909
|
-
newRowSpan = Math.max(1, (i + 1) - rowStart + 1);
|
|
1910
|
-
break;
|
|
1100
|
+
// ── Lock check: suppress adjuster cursor if locked ──
|
|
1101
|
+
if (this.isPropertyLocked(hoveredSelectedId, hoveredAdj.type)) {
|
|
1102
|
+
this.hoveredAdjusterType = null;
|
|
1103
|
+
this.container.style.cursor = "default";
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
this.hoveredAdjusterType = hoveredAdj.type;
|
|
1107
|
+
const isVertical = hoveredAdj.type.includes("top") || hoveredAdj.type.includes("bottom");
|
|
1108
|
+
this.container.style.cursor = isVertical ? "ns-resize" : "ew-resize";
|
|
1911
1109
|
}
|
|
1912
|
-
newRowSpan = Math.max(1, (i + 1) - rowStart + 1);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
console.log('DEBUG WORKSPACE RESIZE GRID result:', 'colStart:', newColStart, 'colSpan:', newColSpan, 'rowStart:', newRowStart, 'rowSpan:', newRowSpan);
|
|
1916
|
-
this.mount.setNodeStyles(selId, {
|
|
1917
|
-
"grid-column-start": `${newColStart}`,
|
|
1918
|
-
"grid-column-end": `span ${newColSpan}`,
|
|
1919
|
-
"grid-row-start": `${newRowStart}`,
|
|
1920
|
-
"grid-row-end": `span ${newRowSpan}`,
|
|
1921
|
-
});
|
|
1922
|
-
this.remeasureSubtree(selId);
|
|
1923
|
-
if (node.parentId) {
|
|
1924
|
-
this.remeasureSubtree(node.parentId);
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
else {
|
|
1929
|
-
// 1. Compute new rect from anchor delta.
|
|
1930
|
-
const newRect = computeResizedRect(this.resizeStartRect, this.activeAnchor, dx, dy, this.minResizeSize, e.altKey);
|
|
1931
|
-
// 2. Style surgery — direct DOM mutation.
|
|
1932
|
-
this.mount.setNodeRect(selId, newRect);
|
|
1933
|
-
// 3. Synchronous reflow + measurement.
|
|
1934
|
-
// Reading dimensions forces the browser to reflow NOW.
|
|
1935
|
-
this.remeasureSubtree(selId);
|
|
1936
|
-
}
|
|
1937
|
-
// 4. Compute alignment guides.
|
|
1938
|
-
if (this.enableSnapGuides && node.currentRect) {
|
|
1939
|
-
const otherRects = this.getOtherRects(selId);
|
|
1940
|
-
this.guides = computeAlignmentGuides(node.currentRect, otherRects, this.snapThreshold);
|
|
1941
|
-
}
|
|
1942
|
-
// 5. Notify.
|
|
1943
|
-
if (node.currentRect) {
|
|
1944
|
-
this.callbacks.onNodeRectChange?.(selId, node.currentRect);
|
|
1945
|
-
}
|
|
1946
|
-
// 6. Render overlay.
|
|
1947
|
-
this.container.style.cursor = anchorCursor(this.activeAnchor);
|
|
1948
|
-
this.canvas.style.pointerEvents = "auto";
|
|
1949
|
-
this.render();
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
// ── Drag (Synchronous Reflow Loop) ────────────
|
|
1953
|
-
if (this.isDragging && this.dragStartCanvas && this.dragStartNodes.size > 0) {
|
|
1954
|
-
const topLevelIds = this.getTopLevelSelectedIds();
|
|
1955
|
-
const primaryId = this.dragStartNodes.keys().next().value;
|
|
1956
|
-
const primaryStart = this.dragStartNodes.get(primaryId);
|
|
1957
|
-
const dx = canvasPos.x - this.dragStartCanvas.x;
|
|
1958
|
-
const dy = canvasPos.y - this.dragStartCanvas.y;
|
|
1959
|
-
let snapDx = dx;
|
|
1960
|
-
let snapDy = dy;
|
|
1961
|
-
if (primaryStart.startParentId === null) {
|
|
1962
|
-
// Absolute Root dragging
|
|
1963
|
-
let newX = primaryStart.startPos.x + dx;
|
|
1964
|
-
let newY = primaryStart.startPos.y + dy;
|
|
1965
|
-
// Snap-to-align
|
|
1966
|
-
if (this.enableSnapGuides) {
|
|
1967
|
-
const primaryNode = this.tree.get(primaryId);
|
|
1968
|
-
if (primaryNode && primaryNode.currentRect) {
|
|
1969
|
-
const candidateRect = {
|
|
1970
|
-
x: newX, y: newY,
|
|
1971
|
-
width: primaryNode.currentRect.width,
|
|
1972
|
-
height: primaryNode.currentRect.height,
|
|
1973
|
-
};
|
|
1974
|
-
const otherRects = this.getOtherRectsMultiple(topLevelIds);
|
|
1975
|
-
const snapped = computeSnappedPosition(candidateRect, otherRects, this.snapThreshold);
|
|
1976
|
-
snapDx = snapped.x - primaryStart.startPos.x;
|
|
1977
|
-
snapDy = snapped.y - primaryStart.startPos.y;
|
|
1978
|
-
const snappedRect = {
|
|
1979
|
-
x: snapped.x, y: snapped.y,
|
|
1980
|
-
width: primaryNode.currentRect.width,
|
|
1981
|
-
height: primaryNode.currentRect.height,
|
|
1982
|
-
};
|
|
1983
|
-
this.guides = computeAlignmentGuides(snappedRect, otherRects, this.snapThreshold);
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
// Apply translations on all dragged nodes
|
|
1987
|
-
for (const [id, start] of this.dragStartNodes.entries()) {
|
|
1988
|
-
if (start.startParentId === null) {
|
|
1989
|
-
this.mount.setNodePosition(id, start.startPos.x + snapDx, start.startPos.y + snapDy);
|
|
1990
|
-
this.remeasureSubtree(id);
|
|
1991
|
-
}
|
|
1992
|
-
else {
|
|
1993
|
-
const wrapper = this.mount.getWrapper(id);
|
|
1994
|
-
if (wrapper) {
|
|
1995
|
-
wrapper.style.transform = `translate3d(${snapDx}px, ${snapDy}px, 0)`;
|
|
1996
1110
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
x: start.startPos.x + snapDx,
|
|
2001
|
-
y: start.startPos.y + snapDy,
|
|
2002
|
-
width: node.currentRect.width,
|
|
2003
|
-
height: node.currentRect.height,
|
|
2004
|
-
};
|
|
1111
|
+
else {
|
|
1112
|
+
this.hoveredAdjusterType = null;
|
|
1113
|
+
this.container.style.cursor = "default";
|
|
2005
1114
|
}
|
|
2006
1115
|
}
|
|
2007
1116
|
}
|
|
2008
1117
|
}
|
|
2009
1118
|
else {
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
if (wrapper) {
|
|
2014
|
-
wrapper.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
|
|
2015
|
-
}
|
|
2016
|
-
const node = this.tree.get(id);
|
|
2017
|
-
if (node && node.currentRect) {
|
|
2018
|
-
node.currentRect = {
|
|
2019
|
-
x: start.startPos.x + dx,
|
|
2020
|
-
y: start.startPos.y + dy,
|
|
2021
|
-
width: node.currentRect.width,
|
|
2022
|
-
height: node.currentRect.height,
|
|
2023
|
-
};
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
// Detect active drop target container & flow position based on the primary node / canvasPos
|
|
2028
|
-
this.activeDropTarget = findDropTarget(primaryId, canvasPos, this.tree, (id) => this.mount.getWrapper(id), (id) => this.mount.getContentRoot(id));
|
|
2029
|
-
// Notify node rect changes
|
|
2030
|
-
for (const id of this.selectedIds) {
|
|
2031
|
-
const node = this.tree.get(id);
|
|
2032
|
-
if (node?.currentRect) {
|
|
2033
|
-
this.callbacks.onNodeRectChange?.(id, node.currentRect);
|
|
2034
|
-
}
|
|
1119
|
+
this.hoveredRadiusCorner = null;
|
|
1120
|
+
this.hoveredAdjusterType = null;
|
|
1121
|
+
this.container.style.cursor = "default";
|
|
2035
1122
|
}
|
|
2036
1123
|
this.render();
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
// ── Pan ────────────────────────────────────────
|
|
1127
|
+
// (Pan is now handled by PanHandler via dispatch loop.
|
|
1128
|
+
// This block is kept as a guard in case of stale state.)
|
|
1129
|
+
if (this.panHandler.isActive) {
|
|
1130
|
+
return;
|
|
2037
1131
|
}
|
|
2038
1132
|
}
|
|
2039
1133
|
/**
|
|
@@ -2043,533 +1137,18 @@ export class Workspace {
|
|
|
2043
1137
|
* extracts the clean HTML and fires `onHTMLCommit`.
|
|
2044
1138
|
*/
|
|
2045
1139
|
handlePointerUp(e) {
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
this.container.classList.remove("canvus-panning");
|
|
2053
|
-
}
|
|
1140
|
+
// ── Handler Dispatch: route to active handler ────────────
|
|
1141
|
+
if (this.activeHandler) {
|
|
1142
|
+
const rect = this.getContainerRect();
|
|
1143
|
+
const canvasPos = screenToCanvas(e.clientX, e.clientY, this.viewport, rect);
|
|
1144
|
+
this.activeHandler.onPointerUp?.(e, canvasPos, rect);
|
|
1145
|
+
this.activeHandler = null;
|
|
2054
1146
|
return;
|
|
2055
1147
|
}
|
|
2056
|
-
|
|
2057
|
-
if (this.isDrawingNode && this.drawStartCanvas && this.drawCurrentCanvas) {
|
|
2058
|
-
this.isDrawingNode = false;
|
|
2059
|
-
const start = this.drawStartCanvas;
|
|
2060
|
-
const end = this.drawCurrentCanvas;
|
|
2061
|
-
this.drawStartCanvas = null;
|
|
2062
|
-
this.drawCurrentCanvas = null;
|
|
2063
|
-
try {
|
|
2064
|
-
this.container.releasePointerCapture(e.pointerId);
|
|
2065
|
-
}
|
|
2066
|
-
catch { }
|
|
2067
|
-
// Calculate drawn dimensions
|
|
2068
|
-
let x = Math.min(start.x, end.x);
|
|
2069
|
-
let y = Math.min(start.y, end.y);
|
|
2070
|
-
let width = Math.abs(start.x - end.x);
|
|
2071
|
-
let height = Math.abs(start.y - end.y);
|
|
2072
|
-
// Apply defaults if users did a simple click-to-draw instead of drag-to-draw
|
|
2073
|
-
if (width < 8 && height < 8) {
|
|
2074
|
-
if (this.activeTool === "box") {
|
|
2075
|
-
width = 120;
|
|
2076
|
-
height = 120;
|
|
2077
|
-
}
|
|
2078
|
-
else {
|
|
2079
|
-
width = 180;
|
|
2080
|
-
height = 40; // Let text height be auto/placeholder size
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
const parentTarget = this.activeDropTarget;
|
|
2084
|
-
this.activeDropTarget = null;
|
|
2085
|
-
// Determine final placement
|
|
2086
|
-
let parentId = parentTarget?.parentId ?? null;
|
|
2087
|
-
let index = parentTarget?.insertionIndex;
|
|
2088
|
-
this.newElementCounter++;
|
|
2089
|
-
const id = `${this.activeTool || "node"}-${this.newElementCounter}-${Date.now().toString(36)}`;
|
|
2090
|
-
let rawMarkup = "";
|
|
2091
|
-
if (this.activeTool === "box") {
|
|
2092
|
-
const tag = this.drawingTag;
|
|
2093
|
-
rawMarkup = `<${tag} style="background:rgba(99, 102, 241, 0.05);border:1.5px dashed #6366f1;border-radius:8px;box-sizing:border-box;width:100%;height:100%;min-width:40px;min-height:40px;"></${tag}>`;
|
|
2094
|
-
}
|
|
2095
|
-
else {
|
|
2096
|
-
const tag = this.drawingTextTag;
|
|
2097
|
-
let fontSize = "16px";
|
|
2098
|
-
let fontWeight = "400";
|
|
2099
|
-
if (tag.match(/^h[1-6]$/)) {
|
|
2100
|
-
fontWeight = "700";
|
|
2101
|
-
if (tag === "h1")
|
|
2102
|
-
fontSize = "28px";
|
|
2103
|
-
else if (tag === "h2")
|
|
2104
|
-
fontSize = "24px";
|
|
2105
|
-
else if (tag === "h3")
|
|
2106
|
-
fontSize = "20px";
|
|
2107
|
-
else
|
|
2108
|
-
fontSize = "18px";
|
|
2109
|
-
}
|
|
2110
|
-
rawMarkup = `<${tag} style="margin:0;font-family:sans-serif;font-size:${fontSize};font-weight:${fontWeight};color:#e8e8f0;line-height:1.5;outline:none;min-width:100px;">Double-click to edit text</${tag}>`;
|
|
2111
|
-
}
|
|
2112
|
-
let rect = { x, y, width, height };
|
|
2113
|
-
// Temporary disable transitions during mount
|
|
2114
|
-
this.mount.setTransitionsEnabled(false);
|
|
2115
|
-
// Perform addition
|
|
2116
|
-
if (parentId !== null && parentTarget?.gridPlacement) {
|
|
2117
|
-
const gp = parentTarget.gridPlacement;
|
|
2118
|
-
// Construct grid position styles directly
|
|
2119
|
-
const gridStyles = {
|
|
2120
|
-
"grid-column-start": `${gp.colStart}`,
|
|
2121
|
-
"grid-column-end": `span ${gp.colSpan}`,
|
|
2122
|
-
"grid-row-start": `${gp.rowStart}`,
|
|
2123
|
-
"grid-row-end": `span ${gp.rowSpan}`,
|
|
2124
|
-
};
|
|
2125
|
-
this.addNode({ id, rawMarkup, currentRect: gp.rect }, parentId, 0);
|
|
2126
|
-
this.setNodeStyles(id, gridStyles);
|
|
2127
|
-
rect = gp.rect;
|
|
2128
|
-
}
|
|
2129
|
-
else {
|
|
2130
|
-
this.addNode({ id, rawMarkup, currentRect: rect }, parentId, index);
|
|
2131
|
-
}
|
|
2132
|
-
this.selectNode(id);
|
|
2133
|
-
// Operations
|
|
2134
|
-
this.callbacks.onOperationsGenerated?.([{
|
|
2135
|
-
type: "create-node",
|
|
2136
|
-
nodeId: id,
|
|
2137
|
-
payload: { parentId, index, rawMarkup, rect },
|
|
2138
|
-
undoPayload: { parentId }
|
|
2139
|
-
}]);
|
|
2140
|
-
// HTML commit
|
|
2141
|
-
const commitTarget = parentId ?? id;
|
|
2142
|
-
const html = this.mount.extractHTML(commitTarget);
|
|
2143
|
-
if (html) {
|
|
2144
|
-
this.callbacks.onHTMLCommit?.(commitTarget, html);
|
|
2145
|
-
}
|
|
2146
|
-
// Clear active tool (resets back to selection/idle mode)
|
|
2147
|
-
this.setActiveTool(null);
|
|
2148
|
-
this.mount.setTransitionsEnabled(true);
|
|
2149
|
-
this.render();
|
|
1148
|
+
if (this.previewMode) {
|
|
2150
1149
|
return;
|
|
2151
1150
|
}
|
|
2152
|
-
//
|
|
2153
|
-
let commitId = null;
|
|
2154
|
-
const operations = [];
|
|
2155
|
-
if (this.isDragging || this.isResizing || this.isAdjustingRadius) {
|
|
2156
|
-
if (this.selectedIds.size === 1) {
|
|
2157
|
-
commitId = this.selectedIds.values().next().value;
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
if (this.activeAdjusterType) {
|
|
2161
|
-
if (this.selectedIds.size === 1) {
|
|
2162
|
-
const selId = this.selectedIds.values().next().value;
|
|
2163
|
-
const contentRoot = this.mount.getContentRoot(selId);
|
|
2164
|
-
if (contentRoot && this.activeAdjusterType) {
|
|
2165
|
-
const finalValueStr = contentRoot.style.getPropertyValue(this.activeAdjusterType) || null;
|
|
2166
|
-
if (finalValueStr !== this.adjusterStartValueStr) {
|
|
2167
|
-
operations.push({
|
|
2168
|
-
type: "update-style",
|
|
2169
|
-
nodeId: selId,
|
|
2170
|
-
payload: { [this.activeAdjusterType]: finalValueStr },
|
|
2171
|
-
undoPayload: { [this.activeAdjusterType]: this.adjusterStartValueStr }
|
|
2172
|
-
});
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
const node = this.tree.get(selId);
|
|
2176
|
-
commitId = (node && node.parentId !== null) ? node.parentId : selId;
|
|
2177
|
-
}
|
|
2178
|
-
this.activeAdjusterType = null;
|
|
2179
|
-
this.dragStartCanvas = null;
|
|
2180
|
-
this.container.style.cursor = "default";
|
|
2181
|
-
this.adjusterStartValueStr = null;
|
|
2182
|
-
}
|
|
2183
|
-
if (this.isAdjustingRadius) {
|
|
2184
|
-
const parentsToCommit = new Set();
|
|
2185
|
-
for (const selId of this.selectedIds) {
|
|
2186
|
-
const selNode = this.tree.get(selId);
|
|
2187
|
-
if (selNode && isContainerNode(selNode)) {
|
|
2188
|
-
const contentRoot = this.mount.getContentRoot(selId);
|
|
2189
|
-
if (contentRoot) {
|
|
2190
|
-
const finalRadiusStr = contentRoot.style.borderRadius || "";
|
|
2191
|
-
const initialRadiusStr = this.radiusStartValues.get(selId) || "0px";
|
|
2192
|
-
if (finalRadiusStr !== initialRadiusStr) {
|
|
2193
|
-
operations.push({
|
|
2194
|
-
type: "update-style",
|
|
2195
|
-
nodeId: selId,
|
|
2196
|
-
payload: { "border-radius": finalRadiusStr },
|
|
2197
|
-
undoPayload: { "border-radius": initialRadiusStr }
|
|
2198
|
-
});
|
|
2199
|
-
if (selNode.parentId) {
|
|
2200
|
-
parentsToCommit.add(selNode.parentId);
|
|
2201
|
-
}
|
|
2202
|
-
else {
|
|
2203
|
-
parentsToCommit.add(selId);
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
for (const commitId of parentsToCommit) {
|
|
2210
|
-
const html = this.mount.extractHTML(commitId);
|
|
2211
|
-
if (html) {
|
|
2212
|
-
this.callbacks.onHTMLCommit?.(commitId, html);
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
this.isAdjustingRadius = false;
|
|
2216
|
-
this.activeRadiusCorner = null;
|
|
2217
|
-
this.radiusTargetNodeId = null;
|
|
2218
|
-
this.radiusStartValues.clear();
|
|
2219
|
-
this.dragStartCanvas = null;
|
|
2220
|
-
this.container.style.cursor = "default";
|
|
2221
|
-
}
|
|
2222
|
-
if (this.isMarqueeSelecting) {
|
|
2223
|
-
this.isMarqueeSelecting = false;
|
|
2224
|
-
this.marqueeStartCanvas = null;
|
|
2225
|
-
this.marqueeCurrentCanvas = null;
|
|
2226
|
-
this.preMarqueeSelectedIds.clear();
|
|
2227
|
-
}
|
|
2228
|
-
// Reset interaction state.
|
|
2229
|
-
if (this.isPanning) {
|
|
2230
|
-
this.isPanning = false;
|
|
2231
|
-
this.container.classList.remove("canvus-panning");
|
|
2232
|
-
}
|
|
2233
|
-
if (this.isDragging) {
|
|
2234
|
-
this.isDragging = false;
|
|
2235
|
-
this.dragStartCanvas = null;
|
|
2236
|
-
this.mount.setTransitionsEnabled(false);
|
|
2237
|
-
if (this.dragStartNodes.size > 0) {
|
|
2238
|
-
if (this.isDragCopy) {
|
|
2239
|
-
this.isDragCopy = false;
|
|
2240
|
-
const parentsToCommit = new Set();
|
|
2241
|
-
const rootsToCommit = [];
|
|
2242
|
-
for (const clonedId of this.dragStartNodes.keys()) {
|
|
2243
|
-
const node = this.tree.get(clonedId);
|
|
2244
|
-
if (!node || !node.currentRect)
|
|
2245
|
-
continue;
|
|
2246
|
-
const wrapper = this.mount.getWrapper(clonedId);
|
|
2247
|
-
if (wrapper) {
|
|
2248
|
-
wrapper.style.transform = "";
|
|
2249
|
-
}
|
|
2250
|
-
const rawMarkup = this.mount.extractHTML(clonedId) || "";
|
|
2251
|
-
let rect = { ...node.currentRect };
|
|
2252
|
-
if (this.activeDropTarget) {
|
|
2253
|
-
const { parentId, gridPlacement } = this.activeDropTarget;
|
|
2254
|
-
if (gridPlacement) {
|
|
2255
|
-
const gridStyles = {
|
|
2256
|
-
"grid-column-start": `${gridPlacement.colStart}`,
|
|
2257
|
-
"grid-column-end": `span ${gridPlacement.colSpan}`,
|
|
2258
|
-
"grid-row-start": `${gridPlacement.rowStart}`,
|
|
2259
|
-
"grid-row-end": `span ${gridPlacement.rowSpan}`,
|
|
2260
|
-
};
|
|
2261
|
-
this.setNodeStyles(clonedId, gridStyles);
|
|
2262
|
-
rect = gridPlacement.rect;
|
|
2263
|
-
}
|
|
2264
|
-
const insertionIndex = this.activeDropTarget.insertionIndex;
|
|
2265
|
-
if (node.parentId !== parentId) {
|
|
2266
|
-
this.reparentNode(clonedId, parentId, insertionIndex !== undefined ? insertionIndex : 0);
|
|
2267
|
-
}
|
|
2268
|
-
operations.push({
|
|
2269
|
-
type: "create-node",
|
|
2270
|
-
nodeId: clonedId,
|
|
2271
|
-
payload: { parentId, index: this.tree.getChildIndex(clonedId), rawMarkup, rect },
|
|
2272
|
-
undoPayload: { parentId }
|
|
2273
|
-
});
|
|
2274
|
-
if (parentId) {
|
|
2275
|
-
parentsToCommit.add(parentId);
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
else {
|
|
2279
|
-
if (node.parentId !== null) {
|
|
2280
|
-
this.reparentNode(clonedId, null);
|
|
2281
|
-
this.mount.setNodePosition(clonedId, rect.x, rect.y);
|
|
2282
|
-
}
|
|
2283
|
-
operations.push({
|
|
2284
|
-
type: "create-node",
|
|
2285
|
-
nodeId: clonedId,
|
|
2286
|
-
payload: { parentId: null, index: -1, rawMarkup, rect },
|
|
2287
|
-
undoPayload: { parentId: null }
|
|
2288
|
-
});
|
|
2289
|
-
rootsToCommit.push(clonedId);
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
this.activeDropTarget = null;
|
|
2293
|
-
this.dragStartNodes.clear();
|
|
2294
|
-
this.dragStartStyles = null;
|
|
2295
|
-
this.mount.setTransitionsEnabled(true);
|
|
2296
|
-
if (operations.length > 0) {
|
|
2297
|
-
this.callbacks.onOperationsGenerated?.(operations);
|
|
2298
|
-
}
|
|
2299
|
-
for (const id of this.selectedIds) {
|
|
2300
|
-
this.remeasureSubtree(id);
|
|
2301
|
-
const node = this.tree.get(id);
|
|
2302
|
-
if (node?.currentRect) {
|
|
2303
|
-
this.callbacks.onNodeRectChange?.(id, node.currentRect);
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
for (const parentId of parentsToCommit) {
|
|
2307
|
-
const html = this.mount.extractHTML(parentId);
|
|
2308
|
-
if (html) {
|
|
2309
|
-
this.callbacks.onHTMLCommit?.(parentId, html);
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
for (const rootId of rootsToCommit) {
|
|
2313
|
-
const html = this.mount.extractHTML(rootId);
|
|
2314
|
-
if (html) {
|
|
2315
|
-
this.callbacks.onHTMLCommit?.(rootId, html);
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
this.canvas.style.pointerEvents = "none";
|
|
2319
|
-
this.callbacks.onInteractionChange?.(null);
|
|
2320
|
-
this.render();
|
|
2321
|
-
return;
|
|
2322
|
-
}
|
|
2323
|
-
if (this.activeDropTarget) {
|
|
2324
|
-
const { parentId, insertionIndex, gridPlacement } = this.activeDropTarget;
|
|
2325
|
-
let currentInsertion = insertionIndex !== undefined ? insertionIndex : 0;
|
|
2326
|
-
for (const [id, start] of this.dragStartNodes.entries()) {
|
|
2327
|
-
const node = this.tree.get(id);
|
|
2328
|
-
if (!node)
|
|
2329
|
-
continue;
|
|
2330
|
-
const oldParentId = start.startParentId;
|
|
2331
|
-
const oldIndex = start.startIndex;
|
|
2332
|
-
const wrapper = this.mount.getWrapper(id);
|
|
2333
|
-
if (wrapper) {
|
|
2334
|
-
wrapper.style.transform = "";
|
|
2335
|
-
}
|
|
2336
|
-
if (gridPlacement) {
|
|
2337
|
-
const payloadStyles = {
|
|
2338
|
-
"grid-column-start": `${gridPlacement.colStart}`,
|
|
2339
|
-
"grid-column-end": `span ${gridPlacement.colSpan}`,
|
|
2340
|
-
"grid-row-start": `${gridPlacement.rowStart}`,
|
|
2341
|
-
"grid-row-end": `span ${gridPlacement.rowSpan}`,
|
|
2342
|
-
"position": null, "left": null, "top": null, "width": null, "height": null,
|
|
2343
|
-
};
|
|
2344
|
-
this.mount.setNodeStyles(id, payloadStyles);
|
|
2345
|
-
const undoPayloadStyles = {};
|
|
2346
|
-
for (const prop of Object.keys(payloadStyles)) {
|
|
2347
|
-
undoPayloadStyles[prop] = (start.startStyles && start.startStyles[prop] !== undefined) ? start.startStyles[prop] : null;
|
|
2348
|
-
}
|
|
2349
|
-
operations.push({
|
|
2350
|
-
type: "update-style",
|
|
2351
|
-
nodeId: id,
|
|
2352
|
-
payload: payloadStyles,
|
|
2353
|
-
undoPayload: undoPayloadStyles
|
|
2354
|
-
});
|
|
2355
|
-
if (parentId !== node.parentId) {
|
|
2356
|
-
this.reparentNode(id, parentId, 0);
|
|
2357
|
-
operations.push({
|
|
2358
|
-
type: "reparent",
|
|
2359
|
-
nodeId: id,
|
|
2360
|
-
payload: { newParentId: parentId, index: 0 },
|
|
2361
|
-
undoPayload: { newParentId: oldParentId, index: oldIndex }
|
|
2362
|
-
});
|
|
2363
|
-
}
|
|
2364
|
-
else {
|
|
2365
|
-
this.remeasureSubtree(parentId);
|
|
2366
|
-
const html = this.mount.extractHTML(parentId);
|
|
2367
|
-
if (html) {
|
|
2368
|
-
this.callbacks.onHTMLCommit?.(parentId, html);
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
2372
|
-
else {
|
|
2373
|
-
let styleChanged = false;
|
|
2374
|
-
const payloadStyles = {};
|
|
2375
|
-
const undoPayloadStyles = {};
|
|
2376
|
-
for (const prop of ["grid-column-start", "grid-column-end", "grid-row-start", "grid-row-end"]) {
|
|
2377
|
-
const orig = start.startStyles ? start.startStyles[prop] : null;
|
|
2378
|
-
if (orig !== null) {
|
|
2379
|
-
payloadStyles[prop] = null;
|
|
2380
|
-
undoPayloadStyles[prop] = orig;
|
|
2381
|
-
styleChanged = true;
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
if (styleChanged) {
|
|
2385
|
-
this.mount.setNodeStyles(id, payloadStyles);
|
|
2386
|
-
operations.push({
|
|
2387
|
-
type: "update-style",
|
|
2388
|
-
nodeId: id,
|
|
2389
|
-
payload: payloadStyles,
|
|
2390
|
-
undoPayload: undoPayloadStyles
|
|
2391
|
-
});
|
|
2392
|
-
}
|
|
2393
|
-
if (parentId === node.parentId) {
|
|
2394
|
-
this.reorderChild(id, currentInsertion);
|
|
2395
|
-
const newIndex = this.tree.getChildIndex(id);
|
|
2396
|
-
if (newIndex !== oldIndex) {
|
|
2397
|
-
operations.push({
|
|
2398
|
-
type: "reorder",
|
|
2399
|
-
nodeId: id,
|
|
2400
|
-
payload: { index: newIndex },
|
|
2401
|
-
undoPayload: { index: oldIndex }
|
|
2402
|
-
});
|
|
2403
|
-
}
|
|
2404
|
-
currentInsertion = newIndex + 1;
|
|
2405
|
-
}
|
|
2406
|
-
else {
|
|
2407
|
-
this.reparentNode(id, parentId, currentInsertion);
|
|
2408
|
-
const newIndex = this.tree.getChildIndex(id);
|
|
2409
|
-
operations.push({
|
|
2410
|
-
type: "reparent",
|
|
2411
|
-
nodeId: id,
|
|
2412
|
-
payload: { newParentId: parentId, index: newIndex },
|
|
2413
|
-
undoPayload: { newParentId: oldParentId, index: oldIndex }
|
|
2414
|
-
});
|
|
2415
|
-
currentInsertion = newIndex + 1;
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
else {
|
|
2421
|
-
for (const [id, start] of this.dragStartNodes.entries()) {
|
|
2422
|
-
const node = this.tree.get(id);
|
|
2423
|
-
if (!node)
|
|
2424
|
-
continue;
|
|
2425
|
-
const oldParentId = start.startParentId;
|
|
2426
|
-
const oldIndex = start.startIndex;
|
|
2427
|
-
const oldPos = start.startPos;
|
|
2428
|
-
const wrapper = this.mount.getWrapper(id);
|
|
2429
|
-
if (wrapper) {
|
|
2430
|
-
wrapper.style.transform = "";
|
|
2431
|
-
}
|
|
2432
|
-
if (node.parentId !== null) {
|
|
2433
|
-
this.reparentNode(id, null);
|
|
2434
|
-
if (node.currentRect) {
|
|
2435
|
-
this.mount.setNodePosition(id, node.currentRect.x, node.currentRect.y);
|
|
2436
|
-
this.remeasureSubtree(id);
|
|
2437
|
-
}
|
|
2438
|
-
operations.push({
|
|
2439
|
-
type: "reparent",
|
|
2440
|
-
nodeId: id,
|
|
2441
|
-
payload: { newParentId: null, index: -1 },
|
|
2442
|
-
undoPayload: { newParentId: oldParentId, index: oldIndex }
|
|
2443
|
-
});
|
|
2444
|
-
let styleChanged = false;
|
|
2445
|
-
const payloadStyles = {};
|
|
2446
|
-
const undoPayloadStyles = {};
|
|
2447
|
-
for (const prop of ["grid-column-start", "grid-column-end", "grid-row-start", "grid-row-end"]) {
|
|
2448
|
-
const orig = start.startStyles ? start.startStyles[prop] : null;
|
|
2449
|
-
if (orig !== null) {
|
|
2450
|
-
payloadStyles[prop] = null;
|
|
2451
|
-
undoPayloadStyles[prop] = orig;
|
|
2452
|
-
styleChanged = true;
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
if (styleChanged) {
|
|
2456
|
-
this.mount.setNodeStyles(id, payloadStyles);
|
|
2457
|
-
operations.push({
|
|
2458
|
-
type: "update-style",
|
|
2459
|
-
nodeId: id,
|
|
2460
|
-
payload: payloadStyles,
|
|
2461
|
-
undoPayload: undoPayloadStyles
|
|
2462
|
-
});
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
else if (oldParentId === null && oldPos) {
|
|
2466
|
-
const newX = node.currentRect ? node.currentRect.x : oldPos.x;
|
|
2467
|
-
const newY = node.currentRect ? node.currentRect.y : oldPos.y;
|
|
2468
|
-
if (newX !== oldPos.x || newY !== oldPos.y) {
|
|
2469
|
-
operations.push({
|
|
2470
|
-
type: "update-style",
|
|
2471
|
-
nodeId: id,
|
|
2472
|
-
payload: { left: `${newX}px`, top: `${newY}px` },
|
|
2473
|
-
undoPayload: { left: `${oldPos.x}px`, top: `${oldPos.y}px` }
|
|
2474
|
-
});
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
this.activeDropTarget = null;
|
|
2481
|
-
this.dragStartStyles = null;
|
|
2482
|
-
this.dragStartNodes.clear();
|
|
2483
|
-
for (const id of this.selectedIds) {
|
|
2484
|
-
this.remeasureSubtree(id);
|
|
2485
|
-
const node = this.tree.get(id);
|
|
2486
|
-
if (node?.currentRect) {
|
|
2487
|
-
this.callbacks.onNodeRectChange?.(id, node.currentRect);
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
this.mount.setTransitionsEnabled(true);
|
|
2491
|
-
}
|
|
2492
|
-
this.pointerDownReadyToDrag = false;
|
|
2493
|
-
if (this.isResizing) {
|
|
2494
|
-
this.isResizing = false;
|
|
2495
|
-
this.activeAnchor = null;
|
|
2496
|
-
this.dragStartCanvas = null;
|
|
2497
|
-
if (commitId && this.resizeStartRect) {
|
|
2498
|
-
const node = this.tree.get(commitId);
|
|
2499
|
-
if (node?.currentRect) {
|
|
2500
|
-
let parentIsGrid = false;
|
|
2501
|
-
if (node.parentId !== null) {
|
|
2502
|
-
const parentContent = this.mount.getContentRoot(node.parentId);
|
|
2503
|
-
if (parentContent) {
|
|
2504
|
-
const info = detectLayout(parentContent);
|
|
2505
|
-
parentIsGrid = info.mode === "grid" || info.mode === "inline-grid";
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
if (parentIsGrid) {
|
|
2509
|
-
const contentRoot = this.mount.getContentRoot(commitId);
|
|
2510
|
-
if (contentRoot && this.dragStartStyles) {
|
|
2511
|
-
const payload = {};
|
|
2512
|
-
const undoPayload = {};
|
|
2513
|
-
let styleChanged = false;
|
|
2514
|
-
const styleProps = [
|
|
2515
|
-
"grid-column-start",
|
|
2516
|
-
"grid-column-end",
|
|
2517
|
-
"grid-row-start",
|
|
2518
|
-
"grid-row-end",
|
|
2519
|
-
];
|
|
2520
|
-
for (const prop of styleProps) {
|
|
2521
|
-
const val = contentRoot.style.getPropertyValue(prop) || null;
|
|
2522
|
-
const origVal = this.dragStartStyles[prop] || null;
|
|
2523
|
-
if (val !== origVal) {
|
|
2524
|
-
payload[prop] = val;
|
|
2525
|
-
undoPayload[prop] = origVal;
|
|
2526
|
-
styleChanged = true;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
if (styleChanged) {
|
|
2530
|
-
operations.push({
|
|
2531
|
-
type: "update-style",
|
|
2532
|
-
nodeId: commitId,
|
|
2533
|
-
payload,
|
|
2534
|
-
undoPayload
|
|
2535
|
-
});
|
|
2536
|
-
}
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
else {
|
|
2540
|
-
const finalRect = node.currentRect;
|
|
2541
|
-
const startRect = this.resizeStartRect;
|
|
2542
|
-
if (finalRect.width !== startRect.width || finalRect.height !== startRect.height ||
|
|
2543
|
-
finalRect.x !== startRect.x || finalRect.y !== startRect.y) {
|
|
2544
|
-
const payload = {
|
|
2545
|
-
width: `${finalRect.width}px`,
|
|
2546
|
-
height: `${finalRect.height}px`
|
|
2547
|
-
};
|
|
2548
|
-
const undoPayload = {
|
|
2549
|
-
width: `${startRect.width}px`,
|
|
2550
|
-
height: `${startRect.height}px`
|
|
2551
|
-
};
|
|
2552
|
-
if (node.parentId === null) {
|
|
2553
|
-
payload.left = `${finalRect.x}px`;
|
|
2554
|
-
payload.top = `${finalRect.y}px`;
|
|
2555
|
-
undoPayload.left = `${startRect.x}px`;
|
|
2556
|
-
undoPayload.top = `${startRect.y}px`;
|
|
2557
|
-
}
|
|
2558
|
-
operations.push({
|
|
2559
|
-
type: "update-style",
|
|
2560
|
-
nodeId: commitId,
|
|
2561
|
-
payload,
|
|
2562
|
-
undoPayload
|
|
2563
|
-
});
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
this.callbacks.onNodeRectChange?.(commitId, node.currentRect);
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
this.resizeStartRect = null;
|
|
2570
|
-
this.dragStartStyles = null;
|
|
2571
|
-
}
|
|
2572
|
-
// Clear guides.
|
|
1151
|
+
// (Drawing tool completion now handled by DrawHandler via dispatch loop)
|
|
2573
1152
|
this.guides = [];
|
|
2574
1153
|
// Release pointer capture.
|
|
2575
1154
|
try {
|
|
@@ -2578,62 +1157,22 @@ export class Workspace {
|
|
|
2578
1157
|
catch {
|
|
2579
1158
|
// Ignore if capture was already released or lost
|
|
2580
1159
|
}
|
|
2581
|
-
if (operations.length > 0) {
|
|
2582
|
-
this.callbacks.onOperationsGenerated?.(operations);
|
|
2583
|
-
}
|
|
2584
1160
|
this.canvas.style.pointerEvents = "none";
|
|
2585
1161
|
this.callbacks.onInteractionChange?.(null);
|
|
2586
1162
|
this.render();
|
|
2587
|
-
// ── Flat String Bridge ────────────────────────
|
|
2588
|
-
// Extract clean HTML and fire commit callback.
|
|
2589
|
-
if (commitId) {
|
|
2590
|
-
const node = this.tree.get(commitId);
|
|
2591
|
-
const commitTarget = (node && node.parentId !== null) ? node.parentId : commitId;
|
|
2592
|
-
const html = this.mount.extractHTML(commitTarget);
|
|
2593
|
-
if (html) {
|
|
2594
|
-
this.callbacks.onHTMLCommit?.(commitTarget, html);
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
// Cycle overlapping elements on simple click inside selection
|
|
2598
|
-
if (!this.isDragging && !this.isResizing && !this.isPanning && this.pointerDownInsideSelection) {
|
|
2599
|
-
const rect = this.getContainerRect();
|
|
2600
|
-
const canvasPos = screenToCanvas(e.clientX, e.clientY, this.viewport, rect);
|
|
2601
|
-
const nodeList = this.getOrderedNodeList();
|
|
2602
|
-
// Find all selectable nodes under the cursor in the current selection scope
|
|
2603
|
-
const hitNodes = nodeList.filter(n => {
|
|
2604
|
-
if (!n.currentRect || !isPointInElement(canvasPos.x, canvasPos.y, n.currentRect)) {
|
|
2605
|
-
return false;
|
|
2606
|
-
}
|
|
2607
|
-
const treeNode = this.tree.get(n.id);
|
|
2608
|
-
return treeNode && treeNode.parentId === this.enteredContainerId;
|
|
2609
|
-
});
|
|
2610
|
-
if (hitNodes.length > 1) {
|
|
2611
|
-
const idx = hitNodes.findIndex(n => n.id === this.pointerDownInsideSelection);
|
|
2612
|
-
if (idx !== -1) {
|
|
2613
|
-
const nextIdx = (idx - 1 + hitNodes.length) % hitNodes.length;
|
|
2614
|
-
const nextNode = hitNodes[nextIdx];
|
|
2615
|
-
if (nextNode) {
|
|
2616
|
-
const nextId = nextNode.id;
|
|
2617
|
-
this.selectedIds.clear();
|
|
2618
|
-
this.selectedIds.add(nextId);
|
|
2619
|
-
this.callbacks.onSelectionChange?.(this.selectedIds);
|
|
2620
|
-
this.updateBreadcrumb();
|
|
2621
|
-
this.render();
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
this.pointerDownInsideSelection = null;
|
|
2627
1163
|
}
|
|
2628
|
-
/** Spacebar tracking for pan mode. */
|
|
2629
1164
|
handleKeyDown(e) {
|
|
2630
1165
|
const target = e.composedPath()[0] || null;
|
|
2631
1166
|
if (isEditableTarget(target))
|
|
2632
1167
|
return;
|
|
1168
|
+
for (const handler of this.keyboardHandlers) {
|
|
1169
|
+
if (handler.onKeyDown?.(e)) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
2633
1173
|
if (e.code === "Space" && !e.repeat) {
|
|
2634
1174
|
e.preventDefault();
|
|
2635
|
-
this.
|
|
2636
|
-
this.container.classList.add("canvus-panning");
|
|
1175
|
+
this.panHandler.onSpaceDown();
|
|
2637
1176
|
}
|
|
2638
1177
|
else if (e.code === "Escape") {
|
|
2639
1178
|
this.handleEscapeKey();
|
|
@@ -2641,326 +1180,23 @@ export class Workspace {
|
|
|
2641
1180
|
else if (e.key === "Meta" || e.key === "Control") {
|
|
2642
1181
|
this.updateHover(true);
|
|
2643
1182
|
}
|
|
2644
|
-
else if (e.key === "Delete" || e.key === "Backspace") {
|
|
2645
|
-
if (e.metaKey || e.ctrlKey) {
|
|
2646
|
-
e.preventDefault();
|
|
2647
|
-
this.ungroupSelectedOrParent();
|
|
2648
|
-
}
|
|
2649
|
-
else {
|
|
2650
|
-
this.deleteSelectedNode();
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "d") {
|
|
2654
|
-
e.preventDefault();
|
|
2655
|
-
this.duplicateSelectedNode();
|
|
2656
|
-
}
|
|
2657
|
-
else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c") {
|
|
2658
|
-
this.copySelectedNode();
|
|
2659
|
-
}
|
|
2660
|
-
else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "x") {
|
|
2661
|
-
this.cutSelectedNode();
|
|
2662
|
-
}
|
|
2663
|
-
else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "v") {
|
|
2664
|
-
this.pasteNode();
|
|
2665
|
-
}
|
|
2666
|
-
else if (e.shiftKey && e.key.toLowerCase() === "a") {
|
|
2667
|
-
e.preventDefault();
|
|
2668
|
-
this.wrapSelectedInFlex();
|
|
2669
|
-
}
|
|
2670
|
-
else if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
|
2671
|
-
if (this.selectedIds.size > 0) {
|
|
2672
|
-
e.preventDefault();
|
|
2673
|
-
this.nudgeOrReorderSelected(e.key, e.shiftKey);
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
}
|
|
2677
|
-
nudgeOrReorderSelected(key, shiftKey) {
|
|
2678
|
-
const topLevelIds = this.getTopLevelSelectedIds();
|
|
2679
|
-
if (topLevelIds.length === 0)
|
|
2680
|
-
return;
|
|
2681
|
-
const rootNodes = [];
|
|
2682
|
-
const groupedByParent = new Map();
|
|
2683
|
-
for (const id of topLevelIds) {
|
|
2684
|
-
const node = this.tree.get(id);
|
|
2685
|
-
if (!node)
|
|
2686
|
-
continue;
|
|
2687
|
-
if (node.parentId === null) {
|
|
2688
|
-
rootNodes.push(node);
|
|
2689
|
-
}
|
|
2690
|
-
else {
|
|
2691
|
-
if (!groupedByParent.has(node.parentId)) {
|
|
2692
|
-
groupedByParent.set(node.parentId, []);
|
|
2693
|
-
}
|
|
2694
|
-
groupedByParent.get(node.parentId).push(node);
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
this.mount.setTransitionsEnabled(false);
|
|
2698
|
-
const ops = [];
|
|
2699
|
-
// ── Absolute Nudging (Root Nodes) ─────────────
|
|
2700
|
-
if (rootNodes.length > 0) {
|
|
2701
|
-
const nudgeAmount = shiftKey ? 10 : 1;
|
|
2702
|
-
for (const node of rootNodes) {
|
|
2703
|
-
const currentX = node.currentRect ? node.currentRect.x : 0;
|
|
2704
|
-
const currentY = node.currentRect ? node.currentRect.y : 0;
|
|
2705
|
-
let newX = currentX;
|
|
2706
|
-
let newY = currentY;
|
|
2707
|
-
if (key === "ArrowLeft")
|
|
2708
|
-
newX -= nudgeAmount;
|
|
2709
|
-
if (key === "ArrowRight")
|
|
2710
|
-
newX += nudgeAmount;
|
|
2711
|
-
if (key === "ArrowUp")
|
|
2712
|
-
newY -= nudgeAmount;
|
|
2713
|
-
if (key === "ArrowDown")
|
|
2714
|
-
newY += nudgeAmount;
|
|
2715
|
-
if (newX !== currentX || newY !== currentY) {
|
|
2716
|
-
const payload = { left: `${newX}px`, top: `${newY}px` };
|
|
2717
|
-
const undoPayload = { left: `${currentX}px`, top: `${currentY}px` };
|
|
2718
|
-
this.setNodeStyles(node.id, payload);
|
|
2719
|
-
ops.push({
|
|
2720
|
-
type: "update-style",
|
|
2721
|
-
nodeId: node.id,
|
|
2722
|
-
payload,
|
|
2723
|
-
undoPayload
|
|
2724
|
-
});
|
|
2725
|
-
if (node.currentRect) {
|
|
2726
|
-
this.callbacks.onNodeRectChange?.(node.id, node.currentRect);
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
// ── Flow Child Reordering (Grouped by Parent) ──
|
|
2732
|
-
for (const [parentId, nodes] of groupedByParent.entries()) {
|
|
2733
|
-
const parentContent = this.mount.getContentRoot(parentId);
|
|
2734
|
-
if (!parentContent)
|
|
2735
|
-
continue;
|
|
2736
|
-
const layoutInfo = detectLayout(parentContent);
|
|
2737
|
-
const flowAxis = getFlowAxis(layoutInfo); // "x" or "y"
|
|
2738
|
-
const siblings = this.tree.getChildren(parentId);
|
|
2739
|
-
const maxIndex = siblings.length - 1;
|
|
2740
|
-
let direction = 0;
|
|
2741
|
-
if (layoutInfo.mode === "grid" || layoutInfo.mode === "inline-grid") {
|
|
2742
|
-
if (key === "ArrowLeft" || key === "ArrowUp")
|
|
2743
|
-
direction = -1;
|
|
2744
|
-
else if (key === "ArrowRight" || key === "ArrowDown")
|
|
2745
|
-
direction = 1;
|
|
2746
|
-
}
|
|
2747
|
-
else if (flowAxis === "x") {
|
|
2748
|
-
if (key === "ArrowLeft")
|
|
2749
|
-
direction = -1;
|
|
2750
|
-
else if (key === "ArrowRight")
|
|
2751
|
-
direction = 1;
|
|
2752
|
-
}
|
|
2753
|
-
else {
|
|
2754
|
-
if (key === "ArrowUp")
|
|
2755
|
-
direction = -1;
|
|
2756
|
-
else if (key === "ArrowDown")
|
|
2757
|
-
direction = 1;
|
|
2758
|
-
}
|
|
2759
|
-
if (direction !== 0) {
|
|
2760
|
-
const sortedNodes = nodes.slice().sort((a, b) => {
|
|
2761
|
-
return this.tree.getChildIndex(a.id) - this.tree.getChildIndex(b.id);
|
|
2762
|
-
});
|
|
2763
|
-
if (direction === -1) {
|
|
2764
|
-
for (const node of sortedNodes) {
|
|
2765
|
-
const currentIndex = this.tree.getChildIndex(node.id);
|
|
2766
|
-
const oldIndex = currentIndex;
|
|
2767
|
-
const newIndex = Math.max(0, currentIndex - 1);
|
|
2768
|
-
if (newIndex !== currentIndex) {
|
|
2769
|
-
this.reorderChild(node.id, newIndex);
|
|
2770
|
-
ops.push({
|
|
2771
|
-
type: "reorder",
|
|
2772
|
-
nodeId: node.id,
|
|
2773
|
-
payload: { index: newIndex },
|
|
2774
|
-
undoPayload: { index: oldIndex }
|
|
2775
|
-
});
|
|
2776
|
-
}
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
else {
|
|
2780
|
-
for (let i = sortedNodes.length - 1; i >= 0; i--) {
|
|
2781
|
-
const node = sortedNodes[i];
|
|
2782
|
-
const currentIndex = this.tree.getChildIndex(node.id);
|
|
2783
|
-
const oldIndex = currentIndex;
|
|
2784
|
-
const newIndex = Math.min(maxIndex, currentIndex + 1);
|
|
2785
|
-
if (newIndex !== currentIndex) {
|
|
2786
|
-
this.reorderChild(node.id, newIndex);
|
|
2787
|
-
ops.push({
|
|
2788
|
-
type: "reorder",
|
|
2789
|
-
nodeId: node.id,
|
|
2790
|
-
payload: { index: newIndex },
|
|
2791
|
-
undoPayload: { index: oldIndex }
|
|
2792
|
-
});
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
const html = this.mount.extractHTML(parentId);
|
|
2797
|
-
if (html) {
|
|
2798
|
-
this.callbacks.onHTMLCommit?.(parentId, html);
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
if (ops.length > 0) {
|
|
2803
|
-
this.callbacks.onOperationsGenerated?.(ops);
|
|
2804
|
-
}
|
|
2805
|
-
this.mount.setTransitionsEnabled(true);
|
|
2806
|
-
this.render();
|
|
2807
|
-
}
|
|
2808
|
-
ungroupSelectedOrParent() {
|
|
2809
|
-
const targetContainers = new Set();
|
|
2810
|
-
for (const id of this.selectedIds) {
|
|
2811
|
-
const node = this.tree.get(id);
|
|
2812
|
-
if (!node)
|
|
2813
|
-
continue;
|
|
2814
|
-
if (this.tree.isContainer(id)) {
|
|
2815
|
-
if (node.parentId !== null) {
|
|
2816
|
-
targetContainers.add(id);
|
|
2817
|
-
}
|
|
2818
|
-
}
|
|
2819
|
-
else {
|
|
2820
|
-
if (node.parentId !== null) {
|
|
2821
|
-
targetContainers.add(node.parentId);
|
|
2822
|
-
}
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
if (targetContainers.size === 0)
|
|
2826
|
-
return;
|
|
2827
|
-
this.mount.setTransitionsEnabled(false);
|
|
2828
|
-
const ops = [];
|
|
2829
|
-
const parentsToCommit = new Set();
|
|
2830
|
-
const rootsToCommit = new Set();
|
|
2831
|
-
for (const containerId of targetContainers) {
|
|
2832
|
-
const containerNode = this.tree.get(containerId);
|
|
2833
|
-
if (!containerNode)
|
|
2834
|
-
continue;
|
|
2835
|
-
const parentId = containerNode.parentId;
|
|
2836
|
-
const index = parentId !== null ? this.tree.getChildIndex(containerId) : -1;
|
|
2837
|
-
const children = this.tree.getChildren(containerId);
|
|
2838
|
-
let childIndexOffset = 0;
|
|
2839
|
-
for (const child of children) {
|
|
2840
|
-
const childId = child.id;
|
|
2841
|
-
const oldParentId = containerId;
|
|
2842
|
-
const oldIndex = this.tree.getChildIndex(childId);
|
|
2843
|
-
const newIndex = parentId !== null ? index + childIndexOffset : undefined;
|
|
2844
|
-
this.mount.reparentNodeDOM(childId, parentId, newIndex);
|
|
2845
|
-
this.tree.reparentNode(childId, parentId, newIndex);
|
|
2846
|
-
this.remeasureSubtree(childId);
|
|
2847
|
-
ops.push({
|
|
2848
|
-
type: "reparent",
|
|
2849
|
-
nodeId: childId,
|
|
2850
|
-
payload: { newParentId: parentId, index: newIndex !== undefined ? this.tree.getChildIndex(childId) : undefined },
|
|
2851
|
-
undoPayload: { newParentId: oldParentId, index: oldIndex }
|
|
2852
|
-
});
|
|
2853
|
-
childIndexOffset++;
|
|
2854
|
-
}
|
|
2855
|
-
const rawMarkup = this.mount.extractHTML(containerId);
|
|
2856
|
-
const rect = containerNode.currentRect;
|
|
2857
|
-
this.removeNode(containerId);
|
|
2858
|
-
ops.push({
|
|
2859
|
-
type: "delete-node",
|
|
2860
|
-
nodeId: containerId,
|
|
2861
|
-
payload: { parentId },
|
|
2862
|
-
undoPayload: { parentId, rawMarkup, rect }
|
|
2863
|
-
});
|
|
2864
|
-
if (parentId) {
|
|
2865
|
-
parentsToCommit.add(parentId);
|
|
2866
|
-
this.remeasureSubtree(parentId);
|
|
2867
|
-
}
|
|
2868
|
-
else {
|
|
2869
|
-
rootsToCommit.add(containerId);
|
|
2870
|
-
}
|
|
2871
|
-
}
|
|
2872
|
-
if (ops.length > 0) {
|
|
2873
|
-
this.deselectAll();
|
|
2874
|
-
this.callbacks.onOperationsGenerated?.(ops);
|
|
2875
|
-
for (const parentId of parentsToCommit) {
|
|
2876
|
-
const html = this.mount.extractHTML(parentId);
|
|
2877
|
-
if (html) {
|
|
2878
|
-
this.callbacks.onHTMLCommit?.(parentId, html);
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
for (const rootId of rootsToCommit) {
|
|
2882
|
-
this.callbacks.onHTMLCommit?.(rootId, "");
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
this.mount.setTransitionsEnabled(true);
|
|
2886
|
-
this.render();
|
|
2887
1183
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
return;
|
|
2892
|
-
this.mount.setTransitionsEnabled(false);
|
|
2893
|
-
const firstId = topLevelIds[0];
|
|
2894
|
-
const firstNode = this.tree.get(firstId);
|
|
2895
|
-
if (!firstNode) {
|
|
2896
|
-
this.mount.setTransitionsEnabled(true);
|
|
2897
|
-
return;
|
|
2898
|
-
}
|
|
2899
|
-
const parentId = firstNode.parentId;
|
|
2900
|
-
const index = parentId !== null ? this.tree.getChildIndex(firstId) : -1;
|
|
2901
|
-
const nodesToWrap = topLevelIds.map(id => this.tree.get(id)).filter((n) => n !== undefined);
|
|
2902
|
-
const bounds = computeAggregateBounds(nodesToWrap);
|
|
2903
|
-
this.newElementCounter++;
|
|
2904
|
-
const wrapperId = `flex-wrapper-${this.newElementCounter}-${Date.now().toString(36)}`;
|
|
2905
|
-
const rawMarkup = `<div style="display: flex; justify-content: center; align-items: center; gap: 10px; flex-direction: row; box-sizing: border-box;"></div>`;
|
|
2906
|
-
let rect = bounds ? { ...bounds } : null;
|
|
2907
|
-
this.addNode({
|
|
2908
|
-
id: wrapperId,
|
|
2909
|
-
rawMarkup,
|
|
2910
|
-
currentRect: rect
|
|
2911
|
-
}, parentId, index === -1 ? undefined : index);
|
|
2912
|
-
const ops = [];
|
|
2913
|
-
ops.push({
|
|
2914
|
-
type: "create-node",
|
|
2915
|
-
nodeId: wrapperId,
|
|
2916
|
-
payload: { parentId, index: index === -1 ? undefined : this.tree.getChildIndex(wrapperId), rawMarkup, rect },
|
|
2917
|
-
undoPayload: { parentId }
|
|
2918
|
-
});
|
|
2919
|
-
let childIdx = 0;
|
|
2920
|
-
for (const nodeId of topLevelIds) {
|
|
2921
|
-
const node = this.tree.get(nodeId);
|
|
2922
|
-
if (!node)
|
|
2923
|
-
continue;
|
|
2924
|
-
const oldParentId = node.parentId;
|
|
2925
|
-
const oldIndex = this.tree.getChildIndex(nodeId);
|
|
2926
|
-
this.mount.reparentNodeDOM(nodeId, wrapperId, childIdx);
|
|
2927
|
-
this.tree.reparentNode(nodeId, wrapperId, childIdx);
|
|
2928
|
-
this.remeasureSubtree(nodeId);
|
|
2929
|
-
ops.push({
|
|
2930
|
-
type: "reparent",
|
|
2931
|
-
nodeId: nodeId,
|
|
2932
|
-
payload: { newParentId: wrapperId, index: childIdx },
|
|
2933
|
-
undoPayload: { newParentId: oldParentId, index: oldIndex }
|
|
2934
|
-
});
|
|
2935
|
-
childIdx++;
|
|
2936
|
-
}
|
|
2937
|
-
this.remeasureSubtree(wrapperId);
|
|
2938
|
-
if (parentId) {
|
|
2939
|
-
this.remeasureSubtree(parentId);
|
|
2940
|
-
}
|
|
2941
|
-
this.selectedIds.clear();
|
|
2942
|
-
this.selectedIds.add(wrapperId);
|
|
2943
|
-
this.callbacks.onSelectionChange?.(this.selectedIds);
|
|
2944
|
-
this.updateBreadcrumb();
|
|
2945
|
-
this.callbacks.onOperationsGenerated?.(ops);
|
|
2946
|
-
const commitTarget = parentId ?? wrapperId;
|
|
2947
|
-
const html = this.mount.extractHTML(commitTarget);
|
|
2948
|
-
if (html) {
|
|
2949
|
-
this.callbacks.onHTMLCommit?.(commitTarget, html);
|
|
2950
|
-
}
|
|
2951
|
-
this.mount.setTransitionsEnabled(true);
|
|
2952
|
-
this.render();
|
|
1184
|
+
/** Registers a custom keyboard command shortcut. */
|
|
1185
|
+
registerCommand(cmd) {
|
|
1186
|
+
this.commandHandler.registerCommand(cmd);
|
|
2953
1187
|
}
|
|
2954
1188
|
handleKeyUp(e) {
|
|
2955
1189
|
const target = e.composedPath()[0] || null;
|
|
2956
1190
|
if (isEditableTarget(target))
|
|
2957
1191
|
return;
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
this.container.classList.remove("canvus-panning");
|
|
1192
|
+
for (const handler of this.keyboardHandlers) {
|
|
1193
|
+
if (handler.onKeyUp?.(e)) {
|
|
1194
|
+
return;
|
|
2962
1195
|
}
|
|
2963
1196
|
}
|
|
1197
|
+
if (e.code === "Space") {
|
|
1198
|
+
this.panHandler.onSpaceUp();
|
|
1199
|
+
}
|
|
2964
1200
|
else if (e.key === "Meta" || e.key === "Control") {
|
|
2965
1201
|
this.updateHover(e.metaKey || e.ctrlKey);
|
|
2966
1202
|
}
|
|
@@ -3015,9 +1251,15 @@ export class Workspace {
|
|
|
3015
1251
|
if (!node)
|
|
3016
1252
|
return;
|
|
3017
1253
|
const wrapper = this.mount.getWrapper(nodeId);
|
|
3018
|
-
const contentRoot = this.mount.getContentRoot(nodeId);
|
|
1254
|
+
const contentRoot = this.mount.getContentRoot(nodeId) || wrapper;
|
|
3019
1255
|
if (!wrapper || !contentRoot)
|
|
3020
1256
|
return;
|
|
1257
|
+
// Disallow inline text editing for React nodes (marked with data-canvus-react)
|
|
1258
|
+
const isReact = contentRoot.hasAttribute("data-canvus-react") || contentRoot.querySelector("[data-canvus-react]") !== null;
|
|
1259
|
+
if (isReact) {
|
|
1260
|
+
this.editAllowedOnDblClick = false;
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
3021
1263
|
const path = getDOMPath(contentRoot, targetEl);
|
|
3022
1264
|
const originalHTML = targetEl.innerHTML;
|
|
3023
1265
|
// Option B: Custom Editor Mount Escape Hatch
|
|
@@ -3210,7 +1452,7 @@ export class Workspace {
|
|
|
3210
1452
|
}
|
|
3211
1453
|
// Compute spacing adjusters if a single node is selected
|
|
3212
1454
|
let spacingAdjusters;
|
|
3213
|
-
if (this.selectedIds.size === 1 && !this.isMarqueeSelecting) {
|
|
1455
|
+
if (this.selectedIds.size === 1 && !this.selectionHandler.isMarqueeSelecting) {
|
|
3214
1456
|
const selId = this.selectedIds.values().next().value;
|
|
3215
1457
|
spacingAdjusters = this.computeSpacingAdjusters(selId);
|
|
3216
1458
|
}
|
|
@@ -3219,18 +1461,18 @@ export class Workspace {
|
|
|
3219
1461
|
nodes: this.getOrderedNodeList(),
|
|
3220
1462
|
selectedIds: this.selectedIds,
|
|
3221
1463
|
hoveredId: this.hoveredId,
|
|
3222
|
-
activeAnchor: this.activeAnchor,
|
|
1464
|
+
activeAnchor: this.resizeHandler.activeAnchor,
|
|
3223
1465
|
guides: this.guides,
|
|
3224
1466
|
layoutBadges: layoutBadges.length > 0 ? layoutBadges : undefined,
|
|
3225
1467
|
gridOverlays: gridOverlays.length > 0 ? gridOverlays : undefined,
|
|
3226
1468
|
activeDropTarget: this.activeDropTarget,
|
|
3227
1469
|
marqueeRect: this.getMarqueeRect(),
|
|
3228
1470
|
spacingAdjusters,
|
|
3229
|
-
draggedNodeId: this.isDragging && this.selectedIds.size === 1 ? this.selectedIds.values().next().value : null,
|
|
3230
|
-
resizedNodeId: this.isResizing && this.selectedIds.size === 1 ? this.selectedIds.values().next().value : null,
|
|
3231
|
-
drawingRect: this.getDrawingRect(),
|
|
3232
|
-
drawingTag: this.
|
|
3233
|
-
activeRadiusCorner: this.isAdjustingRadius ? this.activeRadiusCorner : this.hoveredRadiusCorner,
|
|
1471
|
+
draggedNodeId: this.dragHandler.isDragging && this.selectedIds.size === 1 ? this.selectedIds.values().next().value : null,
|
|
1472
|
+
resizedNodeId: this.resizeHandler.isResizing && this.selectedIds.size === 1 ? this.selectedIds.values().next().value : null,
|
|
1473
|
+
drawingRect: this.drawHandler.getDrawingRect(),
|
|
1474
|
+
drawingTag: this.drawHandler.isDrawing ? this.drawHandler.getDrawingTag() : null,
|
|
1475
|
+
activeRadiusCorner: this.spacingHandler.isAdjustingRadius ? this.spacingHandler.activeRadiusCorner : this.hoveredRadiusCorner,
|
|
3234
1476
|
});
|
|
3235
1477
|
}
|
|
3236
1478
|
// ── Private Helpers ─────────────────────────────
|
|
@@ -3285,7 +1527,16 @@ export class Workspace {
|
|
|
3285
1527
|
if (!tag || tag === "script" || tag === "style" || tag === "link")
|
|
3286
1528
|
continue;
|
|
3287
1529
|
// Use existing id or generate a stable one
|
|
3288
|
-
|
|
1530
|
+
let existingId = child.getAttribute("data-canvus-id") || child.getAttribute("id");
|
|
1531
|
+
if (existingId) {
|
|
1532
|
+
const hasNodeInTree = !!this.tree.get(existingId);
|
|
1533
|
+
const trackedWrapper = this.mount.getWrapper(existingId);
|
|
1534
|
+
const trackedContentRoot = this.mount.getContentRoot(existingId);
|
|
1535
|
+
// If the ID is tracked but points to a different DOM element, we have an ID conflict.
|
|
1536
|
+
if (hasNodeInTree && trackedWrapper !== child && trackedContentRoot !== child) {
|
|
1537
|
+
existingId = null;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
3289
1540
|
const id = existingId || `${parentId}__child-${++this.lazyChildCounter}`;
|
|
3290
1541
|
if (!existingId) {
|
|
3291
1542
|
child.setAttribute("id", id);
|
|
@@ -3509,7 +1760,7 @@ export class Workspace {
|
|
|
3509
1760
|
}
|
|
3510
1761
|
/** Updates the hovered node ID based on current pointer position and Cmd/Ctrl modifier. */
|
|
3511
1762
|
updateHover(isCmdPressed) {
|
|
3512
|
-
if (!this.lastCanvasPos || this.
|
|
1763
|
+
if (!this.lastCanvasPos || this.panHandler.isActive || this.dragHandler.isDragging || this.resizeHandler.isResizing) {
|
|
3513
1764
|
this.clearDynamicHover();
|
|
3514
1765
|
this.hoveredId = null;
|
|
3515
1766
|
return;
|
|
@@ -3518,7 +1769,11 @@ export class Workspace {
|
|
|
3518
1769
|
const hitId = hitTestElements(this.lastCanvasPos.x, this.lastCanvasPos.y, nodeList);
|
|
3519
1770
|
let nextHoveredId = null;
|
|
3520
1771
|
if (hitId) {
|
|
3521
|
-
|
|
1772
|
+
// Skip hover on locked nodes
|
|
1773
|
+
if (this.isNodeLocked(hitId)) {
|
|
1774
|
+
nextHoveredId = null;
|
|
1775
|
+
}
|
|
1776
|
+
else if (isCmdPressed) {
|
|
3522
1777
|
nextHoveredId = hitId;
|
|
3523
1778
|
}
|
|
3524
1779
|
else {
|
|
@@ -3591,27 +1846,9 @@ export class Workspace {
|
|
|
3591
1846
|
}
|
|
3592
1847
|
}
|
|
3593
1848
|
}
|
|
3594
|
-
getDrawingRect
|
|
3595
|
-
if (!this.isDrawingNode || !this.drawStartCanvas || !this.drawCurrentCanvas) {
|
|
3596
|
-
return null;
|
|
3597
|
-
}
|
|
3598
|
-
return {
|
|
3599
|
-
x: Math.min(this.drawStartCanvas.x, this.drawCurrentCanvas.x),
|
|
3600
|
-
y: Math.min(this.drawStartCanvas.y, this.drawCurrentCanvas.y),
|
|
3601
|
-
width: Math.abs(this.drawStartCanvas.x - this.drawCurrentCanvas.x),
|
|
3602
|
-
height: Math.abs(this.drawStartCanvas.y - this.drawCurrentCanvas.y),
|
|
3603
|
-
};
|
|
3604
|
-
}
|
|
1849
|
+
// (getDrawingRect moved to DrawHandler)
|
|
3605
1850
|
getMarqueeRect() {
|
|
3606
|
-
|
|
3607
|
-
return null;
|
|
3608
|
-
}
|
|
3609
|
-
return {
|
|
3610
|
-
x: Math.min(this.marqueeStartCanvas.x, this.marqueeCurrentCanvas.x),
|
|
3611
|
-
y: Math.min(this.marqueeStartCanvas.y, this.marqueeCurrentCanvas.y),
|
|
3612
|
-
width: Math.abs(this.marqueeStartCanvas.x - this.marqueeCurrentCanvas.x),
|
|
3613
|
-
height: Math.abs(this.marqueeStartCanvas.y - this.marqueeCurrentCanvas.y),
|
|
3614
|
-
};
|
|
1851
|
+
return this.selectionHandler ? this.selectionHandler.getMarqueeRect() : null;
|
|
3615
1852
|
}
|
|
3616
1853
|
computeSpacingAdjusters(id) {
|
|
3617
1854
|
const node = this.tree.get(id);
|
|
@@ -3636,14 +1873,14 @@ export class Workspace {
|
|
|
3636
1873
|
const thickness = 10;
|
|
3637
1874
|
const adjusters = [];
|
|
3638
1875
|
const addAdjuster = (type, rect, visualRect, value) => {
|
|
3639
|
-
if (value > 0 || this.activeAdjusterType === type) {
|
|
1876
|
+
if (value > 0 || this.spacingHandler.activeAdjusterType === type) {
|
|
3640
1877
|
adjusters.push({
|
|
3641
1878
|
type,
|
|
3642
1879
|
rect,
|
|
3643
1880
|
visualRect,
|
|
3644
1881
|
value,
|
|
3645
1882
|
isHovered: this.hoveredAdjusterType === type,
|
|
3646
|
-
isActive: this.activeAdjusterType === type,
|
|
1883
|
+
isActive: this.spacingHandler.activeAdjusterType === type,
|
|
3647
1884
|
});
|
|
3648
1885
|
}
|
|
3649
1886
|
};
|
|
@@ -3823,100 +2060,4 @@ function isEditableTarget(target) {
|
|
|
3823
2060
|
tagName === "SELECT" ||
|
|
3824
2061
|
isContentEditable);
|
|
3825
2062
|
}
|
|
3826
|
-
// ── Layout Grid Helpers ─────────────────────────────────────
|
|
3827
|
-
function getGridStart(element, dimension) {
|
|
3828
|
-
const cs = getComputedStyle(element);
|
|
3829
|
-
const startVal = cs.getPropertyValue(`grid-${dimension}-start`);
|
|
3830
|
-
const val = cs.getPropertyValue(`grid-${dimension}`);
|
|
3831
|
-
const startNum = parseInt(startVal, 10);
|
|
3832
|
-
if (!isNaN(startNum))
|
|
3833
|
-
return startNum;
|
|
3834
|
-
if (val) {
|
|
3835
|
-
const match = val.match(/^\s*(\d+)/);
|
|
3836
|
-
if (match && match[1]) {
|
|
3837
|
-
return parseInt(match[1], 10);
|
|
3838
|
-
}
|
|
3839
|
-
}
|
|
3840
|
-
return getRealGridStart(element, dimension);
|
|
3841
|
-
}
|
|
3842
|
-
function getRealGridStart(element, dimension) {
|
|
3843
|
-
const parent = element.parentElement;
|
|
3844
|
-
if (!parent)
|
|
3845
|
-
return 1;
|
|
3846
|
-
let current = parent;
|
|
3847
|
-
let offset = 0;
|
|
3848
|
-
let gap = 0;
|
|
3849
|
-
let tracks = [];
|
|
3850
|
-
let definingGrid = null;
|
|
3851
|
-
while (current) {
|
|
3852
|
-
const cs = getComputedStyle(current);
|
|
3853
|
-
const display = cs.display;
|
|
3854
|
-
if (display.includes("grid")) {
|
|
3855
|
-
const template = cs.getPropertyValue(`grid-template-${dimension}s`);
|
|
3856
|
-
if (template && !template.includes("subgrid")) {
|
|
3857
|
-
definingGrid = current;
|
|
3858
|
-
gap = parseFloat(cs.getPropertyValue(`${dimension}-gap`)) || 0;
|
|
3859
|
-
tracks = parseGridTracks(template, gap);
|
|
3860
|
-
break;
|
|
3861
|
-
}
|
|
3862
|
-
}
|
|
3863
|
-
const nextParent = current.parentElement;
|
|
3864
|
-
if (!nextParent)
|
|
3865
|
-
break;
|
|
3866
|
-
const currentRect = current.getBoundingClientRect();
|
|
3867
|
-
const parentRect = nextParent.getBoundingClientRect();
|
|
3868
|
-
const pcs = getComputedStyle(nextParent);
|
|
3869
|
-
const padLeft = parseFloat(pcs.paddingLeft) || 0;
|
|
3870
|
-
const padTop = parseFloat(pcs.paddingTop) || 0;
|
|
3871
|
-
offset += (dimension === "column")
|
|
3872
|
-
? (currentRect.left - parentRect.left - padLeft)
|
|
3873
|
-
: (currentRect.top - parentRect.top - padTop);
|
|
3874
|
-
current = nextParent;
|
|
3875
|
-
}
|
|
3876
|
-
if (!definingGrid || tracks.length === 0)
|
|
3877
|
-
return 1;
|
|
3878
|
-
const elRect = element.getBoundingClientRect();
|
|
3879
|
-
const defRect = definingGrid.getBoundingClientRect();
|
|
3880
|
-
const defStyle = getComputedStyle(definingGrid);
|
|
3881
|
-
const defPadLeft = parseFloat(defStyle.paddingLeft) || 0;
|
|
3882
|
-
const defPadTop = parseFloat(defStyle.paddingTop) || 0;
|
|
3883
|
-
const elOffset = (dimension === "column")
|
|
3884
|
-
? (elRect.left - defRect.left - defPadLeft)
|
|
3885
|
-
: (elRect.top - defRect.top - defPadTop);
|
|
3886
|
-
const cellIndex = getCellIndexAtOffset(elOffset, tracks, gap);
|
|
3887
|
-
if (parent !== definingGrid) {
|
|
3888
|
-
const parentRect = parent.getBoundingClientRect();
|
|
3889
|
-
const parentOffset = (dimension === "column")
|
|
3890
|
-
? (parentRect.left - defRect.left - defPadLeft)
|
|
3891
|
-
: (parentRect.top - defRect.top - defPadTop);
|
|
3892
|
-
const parentCellIndex = getCellIndexAtOffset(parentOffset, tracks, gap);
|
|
3893
|
-
return Math.max(1, cellIndex - parentCellIndex + 1);
|
|
3894
|
-
}
|
|
3895
|
-
return cellIndex;
|
|
3896
|
-
}
|
|
3897
|
-
function getCellIndexAtOffset(offset, tracks, gap) {
|
|
3898
|
-
for (let i = 0; i < tracks.length; i++) {
|
|
3899
|
-
const t = tracks[i];
|
|
3900
|
-
if (offset <= t.start + t.size + gap / 2) {
|
|
3901
|
-
return i + 1;
|
|
3902
|
-
}
|
|
3903
|
-
}
|
|
3904
|
-
return tracks.length;
|
|
3905
|
-
}
|
|
3906
|
-
function getGridSpan(element, dimension) {
|
|
3907
|
-
const cs = getComputedStyle(element);
|
|
3908
|
-
const startVal = cs.getPropertyValue(`grid-${dimension}-start`);
|
|
3909
|
-
const endVal = cs.getPropertyValue(`grid-${dimension}-end`);
|
|
3910
|
-
const val = cs.getPropertyValue(`grid-${dimension}`);
|
|
3911
|
-
const spanMatch = (startVal + " " + endVal + " " + val).match(/span\s+(\d+)/i);
|
|
3912
|
-
if (spanMatch && spanMatch[1]) {
|
|
3913
|
-
return parseInt(spanMatch[1], 10);
|
|
3914
|
-
}
|
|
3915
|
-
const startNum = parseInt(startVal, 10);
|
|
3916
|
-
const endNum = parseInt(endVal, 10);
|
|
3917
|
-
if (!isNaN(startNum) && !isNaN(endNum) && endNum > startNum) {
|
|
3918
|
-
return endNum - startNum;
|
|
3919
|
-
}
|
|
3920
|
-
return 1;
|
|
3921
|
-
}
|
|
3922
2063
|
//# sourceMappingURL=workspace.js.map
|