@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
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// canvus/src/handlers/drag.handler.ts
|
|
3
|
+
// Handles hit-testing, selection adjustments, and dragging (movement/reparenting/cloning) of nodes.
|
|
4
|
+
// ─────────────────────────────────────────────────────────────
|
|
5
|
+
import { computeAlignmentGuides, computeSnappedPosition } from "../renderer.js";
|
|
6
|
+
import { findDropTarget } from "../drop-zone.js";
|
|
7
|
+
import { isPointInElement } from "../matrix.js";
|
|
8
|
+
/**
|
|
9
|
+
* Manages node dragging, multi-selection drags, drop zones, cloning, and alignment guides.
|
|
10
|
+
*/
|
|
11
|
+
export class DragHandler {
|
|
12
|
+
id = "drag";
|
|
13
|
+
ctx;
|
|
14
|
+
_isDragging = false;
|
|
15
|
+
_isDragCopy = false;
|
|
16
|
+
_pointerDownReadyToDrag = false;
|
|
17
|
+
_pointerDownInsideSelection = null;
|
|
18
|
+
_dragStartCanvas = null;
|
|
19
|
+
_dragStartNodes = new Map();
|
|
20
|
+
constructor(ctx) {
|
|
21
|
+
this.ctx = ctx;
|
|
22
|
+
}
|
|
23
|
+
get isDragging() {
|
|
24
|
+
return this._isDragging;
|
|
25
|
+
}
|
|
26
|
+
// ── InteractionHandler Interface ────────────────
|
|
27
|
+
claim(e, canvasPos, hitNodeId, _containerRect) {
|
|
28
|
+
if (e.button !== 0 || this.ctx.previewMode)
|
|
29
|
+
return false;
|
|
30
|
+
// Calculate isDoubleClick early to prevent handles/adjusters from intercepting double-clicks on small/nested nodes
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const targetEl = e.composedPath()[0];
|
|
33
|
+
const isSameTarget = targetEl !== null && this.ctx.lastPointerDownTarget !== null &&
|
|
34
|
+
(targetEl === this.ctx.lastPointerDownTarget || this.ctx.lastPointerDownTarget.contains(targetEl) || targetEl.contains(this.ctx.lastPointerDownTarget));
|
|
35
|
+
const isDoubleClick = (now - this.ctx.lastPointerDownTime < 350) && (hitNodeId !== null &&
|
|
36
|
+
this.ctx.lastPointerDownId !== null &&
|
|
37
|
+
(hitNodeId === this.ctx.lastPointerDownId || isSameTarget || this.ctx.tree.isAncestor(this.ctx.lastPointerDownId, hitNodeId)));
|
|
38
|
+
if (isDoubleClick)
|
|
39
|
+
return false;
|
|
40
|
+
if (hitNodeId) {
|
|
41
|
+
// Scoping/selection drill-down lock check
|
|
42
|
+
if (this.ctx.isNodeLocked(hitNodeId)) {
|
|
43
|
+
this.ctx.callbacks.onLockedNodeInteraction?.(hitNodeId);
|
|
44
|
+
return false; // Let lock callback handle it or fall through to deselect/marquee
|
|
45
|
+
}
|
|
46
|
+
// Check if we clicked inside the existing selection
|
|
47
|
+
const hasModifier = e.shiftKey || e.metaKey || e.ctrlKey;
|
|
48
|
+
let clickInsideSelection = false;
|
|
49
|
+
let targetSelectId = null;
|
|
50
|
+
if (this.ctx.selectedIds.size > 0 && !hasModifier) {
|
|
51
|
+
for (const selId of this.ctx.selectedIds) {
|
|
52
|
+
const selNode = this.ctx.tree.get(selId);
|
|
53
|
+
if (selNode?.currentRect && isPointInElement(canvasPos.x, canvasPos.y, selNode.currentRect)) {
|
|
54
|
+
clickInsideSelection = true;
|
|
55
|
+
targetSelectId = selId;
|
|
56
|
+
this._pointerDownInsideSelection = selId;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!clickInsideSelection) {
|
|
62
|
+
this._pointerDownInsideSelection = null;
|
|
63
|
+
// Resolve target node based on current entered scope
|
|
64
|
+
const isCmdClick = e.metaKey || e.ctrlKey;
|
|
65
|
+
if (isCmdClick) {
|
|
66
|
+
targetSelectId = hitNodeId;
|
|
67
|
+
this.ctx.enteredContainerId = this.ctx.tree.get(hitNodeId)?.parentId ?? null;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const resolvedId = this.ctx.findSelectableNode(hitNodeId, this.ctx.enteredContainerId);
|
|
71
|
+
if (resolvedId) {
|
|
72
|
+
targetSelectId = resolvedId;
|
|
73
|
+
const node = this.ctx.tree.get(resolvedId);
|
|
74
|
+
this.ctx.enteredContainerId = node?.parentId ?? null;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
this.ctx.enteredContainerId = null;
|
|
78
|
+
targetSelectId = this.ctx.findSelectableNode(hitNodeId, null);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (targetSelectId) {
|
|
83
|
+
// Update selection if click was not inside the existing selection
|
|
84
|
+
if (!clickInsideSelection) {
|
|
85
|
+
const prevSelection = new Set(this.ctx.selectedIds);
|
|
86
|
+
const isShift = e.shiftKey;
|
|
87
|
+
if (isShift) {
|
|
88
|
+
if (this.ctx.selectedIds.has(targetSelectId)) {
|
|
89
|
+
this.ctx.selectedIds.delete(targetSelectId);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.ctx.selectedIds.add(targetSelectId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.ctx.selectedIds.clear();
|
|
97
|
+
this.ctx.selectedIds.add(targetSelectId);
|
|
98
|
+
}
|
|
99
|
+
this.ctx.syncLazyChildren(prevSelection, this.ctx.selectedIds);
|
|
100
|
+
this.ctx.callbacks.onSelectionChange?.(this.ctx.selectedIds);
|
|
101
|
+
this.ctx.updateBreadcrumb();
|
|
102
|
+
}
|
|
103
|
+
// Initialize drag state variables
|
|
104
|
+
this._isDragging = false;
|
|
105
|
+
this._pointerDownReadyToDrag = true;
|
|
106
|
+
this._dragStartCanvas = canvasPos;
|
|
107
|
+
this._dragStartNodes.clear();
|
|
108
|
+
// Capture initial styles/rects of selected nodes
|
|
109
|
+
const topLevelIds = this.ctx.getTopLevelSelectedIds();
|
|
110
|
+
for (const selId of topLevelIds) {
|
|
111
|
+
const selNode = this.ctx.tree.get(selId);
|
|
112
|
+
if (selNode && selNode.currentRect) {
|
|
113
|
+
const contentRoot = this.ctx.mount.getContentRoot(selId);
|
|
114
|
+
let startStyles = null;
|
|
115
|
+
if (contentRoot) {
|
|
116
|
+
startStyles = {
|
|
117
|
+
"grid-column-start": contentRoot.style.gridColumnStart || null,
|
|
118
|
+
"grid-column-end": contentRoot.style.gridColumnEnd || null,
|
|
119
|
+
"grid-row-start": contentRoot.style.gridRowStart || null,
|
|
120
|
+
"grid-row-end": contentRoot.style.gridRowEnd || null,
|
|
121
|
+
"position": contentRoot.style.position || null,
|
|
122
|
+
"left": contentRoot.style.left || null,
|
|
123
|
+
"top": contentRoot.style.top || null,
|
|
124
|
+
"width": contentRoot.style.width || null,
|
|
125
|
+
"height": contentRoot.style.height || null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
this._dragStartNodes.set(selId, {
|
|
129
|
+
startPos: { x: selNode.currentRect.x, y: selNode.currentRect.y },
|
|
130
|
+
startParentId: selNode.parentId,
|
|
131
|
+
startIndex: selNode.parentId !== null ? this.ctx.tree.getChildIndex(selId) : -1,
|
|
132
|
+
startStyles,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
this.ctx.render();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
onPointerMove(e, canvasPos, _containerRect) {
|
|
143
|
+
if (this._isDragging) {
|
|
144
|
+
console.log('DEBUG WORKSPACE MOVE: viewport scale:', this.ctx.viewport.scale, 'canvasPos:', canvasPos, 'dragStartCanvas:', this._dragStartCanvas, 'clientX:', e.clientX, 'clientY:', e.clientY);
|
|
145
|
+
}
|
|
146
|
+
// ── Drag initiation ───────────────────────────
|
|
147
|
+
if (this._pointerDownReadyToDrag && this._dragStartCanvas) {
|
|
148
|
+
const dx = canvasPos.x - this._dragStartCanvas.x;
|
|
149
|
+
const dy = canvasPos.y - this._dragStartCanvas.y;
|
|
150
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
151
|
+
if (dist >= 3) {
|
|
152
|
+
if (e.altKey && this.ctx.selectedIds.size > 0) {
|
|
153
|
+
const topLevelIds = this.ctx.getTopLevelSelectedIds();
|
|
154
|
+
this.ctx.mount.setTransitionsEnabled(false);
|
|
155
|
+
const newSelectedIds = [];
|
|
156
|
+
this._dragStartNodes.clear();
|
|
157
|
+
for (const originalId of topLevelIds) {
|
|
158
|
+
const originalNode = this.ctx.tree.get(originalId);
|
|
159
|
+
if (originalNode && originalNode.currentRect) {
|
|
160
|
+
const rawMarkup = this.ctx.mount.extractHTML(originalId);
|
|
161
|
+
if (rawMarkup) {
|
|
162
|
+
const newIdVal = this.ctx.nextElementId();
|
|
163
|
+
const duplicateId = `cloned-${newIdVal}-${Date.now().toString(36)}`;
|
|
164
|
+
const parentId = originalNode.parentId;
|
|
165
|
+
const index = parentId !== null ? this.ctx.tree.getChildIndex(originalId) + 1 : undefined;
|
|
166
|
+
this.ctx.addNode({
|
|
167
|
+
id: duplicateId,
|
|
168
|
+
rawMarkup,
|
|
169
|
+
currentRect: { ...originalNode.currentRect }
|
|
170
|
+
}, parentId, index);
|
|
171
|
+
if (this.ctx.jsMarkedNodes.has(originalId)) {
|
|
172
|
+
this.ctx.markNodeHasJS(duplicateId);
|
|
173
|
+
}
|
|
174
|
+
this.ctx.callbacks.onNodeCloned?.(originalId, duplicateId);
|
|
175
|
+
newSelectedIds.push(duplicateId);
|
|
176
|
+
const duplicateContentRoot = this.ctx.mount.getContentRoot(duplicateId);
|
|
177
|
+
let startStyles = null;
|
|
178
|
+
if (duplicateContentRoot) {
|
|
179
|
+
startStyles = {
|
|
180
|
+
"grid-column-start": duplicateContentRoot.style.gridColumnStart || null,
|
|
181
|
+
"grid-column-end": duplicateContentRoot.style.gridColumnEnd || null,
|
|
182
|
+
"grid-row-start": duplicateContentRoot.style.gridRowStart || null,
|
|
183
|
+
"grid-row-end": duplicateContentRoot.style.gridRowEnd || null,
|
|
184
|
+
"position": duplicateContentRoot.style.position || null,
|
|
185
|
+
"left": duplicateContentRoot.style.left || null,
|
|
186
|
+
"top": duplicateContentRoot.style.top || null,
|
|
187
|
+
"width": duplicateContentRoot.style.width || null,
|
|
188
|
+
"height": duplicateContentRoot.style.height || null,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
this._dragStartNodes.set(duplicateId, {
|
|
192
|
+
startPos: { x: originalNode.currentRect.x, y: originalNode.currentRect.y },
|
|
193
|
+
startParentId: parentId,
|
|
194
|
+
startIndex: parentId !== null ? this.ctx.tree.getChildIndex(duplicateId) : -1,
|
|
195
|
+
startStyles,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (newSelectedIds.length > 0) {
|
|
201
|
+
const prevSelection = new Set(this.ctx.selectedIds);
|
|
202
|
+
this.ctx.selectedIds.clear();
|
|
203
|
+
for (const id of newSelectedIds) {
|
|
204
|
+
this.ctx.selectedIds.add(id);
|
|
205
|
+
}
|
|
206
|
+
this.ctx.syncLazyChildren(prevSelection, this.ctx.selectedIds);
|
|
207
|
+
this.ctx.callbacks.onSelectionChange?.(this.ctx.selectedIds);
|
|
208
|
+
this.ctx.updateBreadcrumb();
|
|
209
|
+
this._isDragCopy = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// ── Multi-node property lock check for drag ──
|
|
213
|
+
const dragTopLevelIds = this.ctx.getTopLevelSelectedIds();
|
|
214
|
+
let dragBlocked = false;
|
|
215
|
+
for (const nodeId of dragTopLevelIds) {
|
|
216
|
+
const posProps = ["left", "top"];
|
|
217
|
+
for (const prop of posProps) {
|
|
218
|
+
if (this.ctx.isPropertyLocked(nodeId, prop)) {
|
|
219
|
+
this.ctx.notifyPropertyLockInteraction(nodeId, prop);
|
|
220
|
+
dragBlocked = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (dragBlocked) {
|
|
225
|
+
this._pointerDownReadyToDrag = false;
|
|
226
|
+
this.onCancel();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this._isDragging = true;
|
|
230
|
+
this._pointerDownReadyToDrag = false;
|
|
231
|
+
this.ctx.callbacks.onInteractionChange?.("drag-node");
|
|
232
|
+
this.ctx.safeSetPointerCapture(e.pointerId);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (this._isDragging && this._dragStartCanvas && this._dragStartNodes.size > 0) {
|
|
236
|
+
const topLevelIds = this.ctx.getTopLevelSelectedIds();
|
|
237
|
+
const primaryId = this._dragStartNodes.keys().next().value;
|
|
238
|
+
const primaryStart = this._dragStartNodes.get(primaryId);
|
|
239
|
+
const dx = canvasPos.x - this._dragStartCanvas.x;
|
|
240
|
+
const dy = canvasPos.y - this._dragStartCanvas.y;
|
|
241
|
+
let snapDx = dx;
|
|
242
|
+
let snapDy = dy;
|
|
243
|
+
if (primaryStart.startParentId === null) {
|
|
244
|
+
// Absolute Root dragging
|
|
245
|
+
const newX = primaryStart.startPos.x + dx;
|
|
246
|
+
const newY = primaryStart.startPos.y + dy;
|
|
247
|
+
// Snap-to-align
|
|
248
|
+
if (this.ctx.enableSnapGuides) {
|
|
249
|
+
const primaryNode = this.ctx.tree.get(primaryId);
|
|
250
|
+
if (primaryNode && primaryNode.currentRect) {
|
|
251
|
+
const candidateRect = {
|
|
252
|
+
x: newX, y: newY,
|
|
253
|
+
width: primaryNode.currentRect.width,
|
|
254
|
+
height: primaryNode.currentRect.height,
|
|
255
|
+
};
|
|
256
|
+
const otherRects = this.ctx.getOtherRectsMultiple(topLevelIds);
|
|
257
|
+
const snapped = computeSnappedPosition(candidateRect, otherRects, this.ctx.snapThreshold);
|
|
258
|
+
snapDx = snapped.x - primaryStart.startPos.x;
|
|
259
|
+
snapDy = snapped.y - primaryStart.startPos.y;
|
|
260
|
+
const snappedRect = {
|
|
261
|
+
x: snapped.x, y: snapped.y,
|
|
262
|
+
width: primaryNode.currentRect.width,
|
|
263
|
+
height: primaryNode.currentRect.height,
|
|
264
|
+
};
|
|
265
|
+
this.ctx.guides = computeAlignmentGuides(snappedRect, otherRects, this.ctx.snapThreshold);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Apply translations on all dragged nodes
|
|
269
|
+
for (const [id, start] of this._dragStartNodes.entries()) {
|
|
270
|
+
if (start.startParentId === null) {
|
|
271
|
+
this.ctx.mount.setNodePosition(id, start.startPos.x + snapDx, start.startPos.y + snapDy);
|
|
272
|
+
this.ctx.remeasureSubtree(id);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const wrapper = this.ctx.mount.getWrapper(id);
|
|
276
|
+
if (wrapper) {
|
|
277
|
+
wrapper.style.transform = `translate3d(${snapDx}px, ${snapDy}px, 0)`;
|
|
278
|
+
}
|
|
279
|
+
const node = this.ctx.tree.get(id);
|
|
280
|
+
if (node && node.currentRect) {
|
|
281
|
+
node.currentRect = {
|
|
282
|
+
x: start.startPos.x + snapDx,
|
|
283
|
+
y: start.startPos.y + snapDy,
|
|
284
|
+
width: node.currentRect.width,
|
|
285
|
+
height: node.currentRect.height,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// Flow child dragging (visual translation)
|
|
293
|
+
for (const [id, start] of this._dragStartNodes.entries()) {
|
|
294
|
+
const wrapper = this.ctx.mount.getWrapper(id);
|
|
295
|
+
if (wrapper) {
|
|
296
|
+
wrapper.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
|
|
297
|
+
}
|
|
298
|
+
const node = this.ctx.tree.get(id);
|
|
299
|
+
if (node && node.currentRect) {
|
|
300
|
+
node.currentRect = {
|
|
301
|
+
x: start.startPos.x + dx,
|
|
302
|
+
y: start.startPos.y + dy,
|
|
303
|
+
width: node.currentRect.width,
|
|
304
|
+
height: node.currentRect.height,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Detect active drop target container & flow position based on the primary node / canvasPos
|
|
310
|
+
this.ctx.activeDropTarget = findDropTarget(primaryId, canvasPos, this.ctx.tree, (id) => this.ctx.mount.getWrapper(id), (id) => this.ctx.mount.getContentRoot(id));
|
|
311
|
+
// Notify node rect changes
|
|
312
|
+
for (const id of this.ctx.selectedIds) {
|
|
313
|
+
const node = this.ctx.tree.get(id);
|
|
314
|
+
if (node?.currentRect) {
|
|
315
|
+
this.ctx.callbacks.onNodeRectChange?.(id, node.currentRect);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
this.ctx.render();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
onPointerUp(e, canvasPos, _containerRect) {
|
|
322
|
+
if (this._isDragging) {
|
|
323
|
+
console.log('DEBUG WORKSPACE UP: viewport scale:', this.ctx.viewport.scale, 'dragStartNodes:', Array.from(this._dragStartNodes.entries()).map(([id, s]) => ({ id, startPos: s.startPos, startParentId: s.startParentId })), 'clientX:', e.clientX, 'clientY:', e.clientY);
|
|
324
|
+
}
|
|
325
|
+
let commitId = null;
|
|
326
|
+
const operations = [];
|
|
327
|
+
if (this._isDragging) {
|
|
328
|
+
if (this.ctx.selectedIds.size === 1) {
|
|
329
|
+
commitId = this.ctx.selectedIds.values().next().value;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (this._isDragging) {
|
|
333
|
+
this._isDragging = false;
|
|
334
|
+
this._dragStartCanvas = null;
|
|
335
|
+
this.ctx.mount.setTransitionsEnabled(false);
|
|
336
|
+
if (this._dragStartNodes.size > 0) {
|
|
337
|
+
if (this._isDragCopy) {
|
|
338
|
+
this._isDragCopy = false;
|
|
339
|
+
const parentsToCommit = new Set();
|
|
340
|
+
const rootsToCommit = [];
|
|
341
|
+
for (const clonedId of this._dragStartNodes.keys()) {
|
|
342
|
+
const node = this.ctx.tree.get(clonedId);
|
|
343
|
+
if (!node || !node.currentRect)
|
|
344
|
+
continue;
|
|
345
|
+
const wrapper = this.ctx.mount.getWrapper(clonedId);
|
|
346
|
+
if (wrapper) {
|
|
347
|
+
wrapper.style.transform = "";
|
|
348
|
+
}
|
|
349
|
+
const rawMarkup = this.ctx.mount.extractHTML(clonedId) || "";
|
|
350
|
+
let rect = { ...node.currentRect };
|
|
351
|
+
if (this.ctx.activeDropTarget) {
|
|
352
|
+
const { parentId, gridPlacement } = this.ctx.activeDropTarget;
|
|
353
|
+
if (gridPlacement) {
|
|
354
|
+
const gridStyles = {
|
|
355
|
+
"grid-column-start": `${gridPlacement.colStart}`,
|
|
356
|
+
"grid-column-end": `span ${gridPlacement.colSpan}`,
|
|
357
|
+
"grid-row-start": `${gridPlacement.rowStart}`,
|
|
358
|
+
"grid-row-end": `span ${gridPlacement.rowSpan}`,
|
|
359
|
+
};
|
|
360
|
+
this.ctx.setNodeStyles(clonedId, gridStyles);
|
|
361
|
+
rect = gridPlacement.rect;
|
|
362
|
+
}
|
|
363
|
+
const insertionIndex = this.ctx.activeDropTarget.insertionIndex;
|
|
364
|
+
if (node.parentId !== parentId) {
|
|
365
|
+
this.ctx.reparentNode(clonedId, parentId, insertionIndex !== undefined ? insertionIndex : 0);
|
|
366
|
+
}
|
|
367
|
+
operations.push({
|
|
368
|
+
type: "create-node",
|
|
369
|
+
nodeId: clonedId,
|
|
370
|
+
payload: { parentId, index: this.ctx.tree.getChildIndex(clonedId), rawMarkup, rect },
|
|
371
|
+
undoPayload: { parentId }
|
|
372
|
+
});
|
|
373
|
+
if (parentId) {
|
|
374
|
+
parentsToCommit.add(parentId);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
if (node.parentId !== null) {
|
|
379
|
+
this.ctx.reparentNode(clonedId, null);
|
|
380
|
+
this.ctx.mount.setNodePosition(clonedId, rect.x, rect.y);
|
|
381
|
+
}
|
|
382
|
+
operations.push({
|
|
383
|
+
type: "create-node",
|
|
384
|
+
nodeId: clonedId,
|
|
385
|
+
payload: { parentId: null, index: -1, rawMarkup, rect },
|
|
386
|
+
undoPayload: { parentId: null }
|
|
387
|
+
});
|
|
388
|
+
rootsToCommit.push(clonedId);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
this.ctx.activeDropTarget = null;
|
|
392
|
+
this._dragStartNodes.clear();
|
|
393
|
+
this.ctx.mount.setTransitionsEnabled(true);
|
|
394
|
+
if (operations.length > 0) {
|
|
395
|
+
this.ctx.callbacks.onOperationsGenerated?.(operations);
|
|
396
|
+
}
|
|
397
|
+
for (const id of this.ctx.selectedIds) {
|
|
398
|
+
this.ctx.remeasureSubtree(id);
|
|
399
|
+
const node = this.ctx.tree.get(id);
|
|
400
|
+
if (node?.currentRect) {
|
|
401
|
+
this.ctx.callbacks.onNodeRectChange?.(id, node.currentRect);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
for (const parentId of parentsToCommit) {
|
|
405
|
+
const html = this.ctx.mount.extractHTML(parentId);
|
|
406
|
+
if (html) {
|
|
407
|
+
this.ctx.callbacks.onHTMLCommit?.(parentId, html);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (const rootId of rootsToCommit) {
|
|
411
|
+
const html = this.ctx.mount.extractHTML(rootId);
|
|
412
|
+
if (html) {
|
|
413
|
+
this.ctx.callbacks.onHTMLCommit?.(rootId, html);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
this.ctx.canvas.style.pointerEvents = "none";
|
|
417
|
+
this.ctx.emitInteraction(null);
|
|
418
|
+
this.ctx.render();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (this.ctx.activeDropTarget) {
|
|
422
|
+
const { parentId, insertionIndex, gridPlacement } = this.ctx.activeDropTarget;
|
|
423
|
+
let currentInsertion = insertionIndex !== undefined ? insertionIndex : 0;
|
|
424
|
+
for (const [id, start] of this._dragStartNodes.entries()) {
|
|
425
|
+
const node = this.ctx.tree.get(id);
|
|
426
|
+
if (!node)
|
|
427
|
+
continue;
|
|
428
|
+
const oldParentId = start.startParentId;
|
|
429
|
+
const oldIndex = start.startIndex;
|
|
430
|
+
const wrapper = this.ctx.mount.getWrapper(id);
|
|
431
|
+
if (wrapper) {
|
|
432
|
+
wrapper.style.transform = "";
|
|
433
|
+
}
|
|
434
|
+
if (gridPlacement) {
|
|
435
|
+
const payloadStyles = {
|
|
436
|
+
"grid-column-start": `${gridPlacement.colStart}`,
|
|
437
|
+
"grid-column-end": `span ${gridPlacement.colSpan}`,
|
|
438
|
+
"grid-row-start": `${gridPlacement.rowStart}`,
|
|
439
|
+
"grid-row-end": `span ${gridPlacement.rowSpan}`,
|
|
440
|
+
"position": null, "left": null, "top": null, "width": null, "height": null,
|
|
441
|
+
};
|
|
442
|
+
this.ctx.mount.setNodeStyles(id, payloadStyles);
|
|
443
|
+
const undoPayloadStyles = {};
|
|
444
|
+
for (const prop of Object.keys(payloadStyles)) {
|
|
445
|
+
undoPayloadStyles[prop] = (start.startStyles && start.startStyles[prop] !== undefined) ? start.startStyles[prop] : null;
|
|
446
|
+
}
|
|
447
|
+
operations.push({
|
|
448
|
+
type: "update-style",
|
|
449
|
+
nodeId: id,
|
|
450
|
+
payload: payloadStyles,
|
|
451
|
+
undoPayload: undoPayloadStyles
|
|
452
|
+
});
|
|
453
|
+
if (parentId !== node.parentId) {
|
|
454
|
+
this.ctx.reparentNode(id, parentId, 0);
|
|
455
|
+
operations.push({
|
|
456
|
+
type: "reparent",
|
|
457
|
+
nodeId: id,
|
|
458
|
+
payload: { newParentId: parentId, index: 0 },
|
|
459
|
+
undoPayload: { newParentId: oldParentId, index: oldIndex }
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
this.ctx.remeasureSubtree(parentId);
|
|
464
|
+
const html = this.ctx.mount.extractHTML(parentId);
|
|
465
|
+
if (html) {
|
|
466
|
+
this.ctx.callbacks.onHTMLCommit?.(parentId, html);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
let styleChanged = false;
|
|
472
|
+
const payloadStyles = {};
|
|
473
|
+
const undoPayloadStyles = {};
|
|
474
|
+
for (const prop of ["grid-column-start", "grid-column-end", "grid-row-start", "grid-row-end"]) {
|
|
475
|
+
const orig = start.startStyles ? start.startStyles[prop] : null;
|
|
476
|
+
if (orig !== null) {
|
|
477
|
+
payloadStyles[prop] = null;
|
|
478
|
+
undoPayloadStyles[prop] = orig;
|
|
479
|
+
styleChanged = true;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (styleChanged) {
|
|
483
|
+
this.ctx.mount.setNodeStyles(id, payloadStyles);
|
|
484
|
+
operations.push({
|
|
485
|
+
type: "update-style",
|
|
486
|
+
nodeId: id,
|
|
487
|
+
payload: payloadStyles,
|
|
488
|
+
undoPayload: undoPayloadStyles
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (parentId === node.parentId) {
|
|
492
|
+
this.ctx.reorderChild(id, currentInsertion);
|
|
493
|
+
const newIndex = this.ctx.tree.getChildIndex(id);
|
|
494
|
+
if (newIndex !== oldIndex) {
|
|
495
|
+
operations.push({
|
|
496
|
+
type: "reorder",
|
|
497
|
+
nodeId: id,
|
|
498
|
+
payload: { index: newIndex },
|
|
499
|
+
undoPayload: { index: oldIndex }
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
currentInsertion = newIndex + 1;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
this.ctx.reparentNode(id, parentId, currentInsertion);
|
|
506
|
+
const newIndex = this.ctx.tree.getChildIndex(id);
|
|
507
|
+
operations.push({
|
|
508
|
+
type: "reparent",
|
|
509
|
+
nodeId: id,
|
|
510
|
+
payload: { newParentId: parentId, index: newIndex },
|
|
511
|
+
undoPayload: { newParentId: oldParentId, index: oldIndex }
|
|
512
|
+
});
|
|
513
|
+
currentInsertion = newIndex + 1;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
for (const [id, start] of this._dragStartNodes.entries()) {
|
|
520
|
+
const node = this.ctx.tree.get(id);
|
|
521
|
+
if (!node)
|
|
522
|
+
continue;
|
|
523
|
+
const oldParentId = start.startParentId;
|
|
524
|
+
const oldIndex = start.startIndex;
|
|
525
|
+
const oldPos = start.startPos;
|
|
526
|
+
const wrapper = this.ctx.mount.getWrapper(id);
|
|
527
|
+
if (wrapper) {
|
|
528
|
+
wrapper.style.transform = "";
|
|
529
|
+
}
|
|
530
|
+
if (node.parentId !== null) {
|
|
531
|
+
this.ctx.reparentNode(id, null);
|
|
532
|
+
if (node.currentRect) {
|
|
533
|
+
this.ctx.mount.setNodePosition(id, node.currentRect.x, node.currentRect.y);
|
|
534
|
+
this.ctx.remeasureSubtree(id);
|
|
535
|
+
}
|
|
536
|
+
operations.push({
|
|
537
|
+
type: "reparent",
|
|
538
|
+
nodeId: id,
|
|
539
|
+
payload: { newParentId: null, index: -1 },
|
|
540
|
+
undoPayload: { newParentId: oldParentId, index: oldIndex }
|
|
541
|
+
});
|
|
542
|
+
let styleChanged = false;
|
|
543
|
+
const payloadStyles = {};
|
|
544
|
+
const undoPayloadStyles = {};
|
|
545
|
+
for (const prop of ["grid-column-start", "grid-column-end", "grid-row-start", "grid-row-end"]) {
|
|
546
|
+
const orig = start.startStyles ? start.startStyles[prop] : null;
|
|
547
|
+
if (orig !== null) {
|
|
548
|
+
payloadStyles[prop] = null;
|
|
549
|
+
undoPayloadStyles[prop] = orig;
|
|
550
|
+
styleChanged = true;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (styleChanged) {
|
|
554
|
+
this.ctx.mount.setNodeStyles(id, payloadStyles);
|
|
555
|
+
operations.push({
|
|
556
|
+
type: "update-style",
|
|
557
|
+
nodeId: id,
|
|
558
|
+
payload: payloadStyles,
|
|
559
|
+
undoPayload: undoPayloadStyles
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
else if (oldParentId === null && oldPos) {
|
|
564
|
+
const newX = node.currentRect ? node.currentRect.x : oldPos.x;
|
|
565
|
+
const newY = node.currentRect ? node.currentRect.y : oldPos.y;
|
|
566
|
+
if (newX !== oldPos.x || newY !== oldPos.y) {
|
|
567
|
+
operations.push({
|
|
568
|
+
type: "update-style",
|
|
569
|
+
nodeId: id,
|
|
570
|
+
payload: { left: `${newX}px`, top: `${newY}px` },
|
|
571
|
+
undoPayload: { left: `${oldPos.x}px`, top: `${oldPos.y}px` }
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
this.ctx.activeDropTarget = null;
|
|
579
|
+
this._dragStartNodes.clear();
|
|
580
|
+
for (const id of this.ctx.selectedIds) {
|
|
581
|
+
this.ctx.remeasureSubtree(id);
|
|
582
|
+
const node = this.ctx.tree.get(id);
|
|
583
|
+
if (node?.currentRect) {
|
|
584
|
+
this.ctx.callbacks.onNodeRectChange?.(id, node.currentRect);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
this.ctx.mount.setTransitionsEnabled(true);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
// Simple click without dragging: cycle overlapping elements
|
|
591
|
+
if (this._pointerDownInsideSelection) {
|
|
592
|
+
const nodeList = this.ctx.getOrderedNodeList();
|
|
593
|
+
const hitNodes = nodeList.filter(n => {
|
|
594
|
+
if (!n.currentRect || !isPointInElement(canvasPos.x, canvasPos.y, n.currentRect)) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
const treeNode = this.ctx.tree.get(n.id);
|
|
598
|
+
return treeNode && treeNode.parentId === this.ctx.enteredContainerId;
|
|
599
|
+
});
|
|
600
|
+
if (hitNodes.length > 1) {
|
|
601
|
+
const idx = hitNodes.findIndex(n => n.id === this._pointerDownInsideSelection);
|
|
602
|
+
if (idx !== -1) {
|
|
603
|
+
const nextIdx = (idx - 1 + hitNodes.length) % hitNodes.length;
|
|
604
|
+
const nextNode = hitNodes[nextIdx];
|
|
605
|
+
if (nextNode) {
|
|
606
|
+
const nextId = nextNode.id;
|
|
607
|
+
this.ctx.selectedIds.clear();
|
|
608
|
+
this.ctx.selectedIds.add(nextId);
|
|
609
|
+
this.ctx.callbacks.onSelectionChange?.(this.ctx.selectedIds);
|
|
610
|
+
this.ctx.updateBreadcrumb();
|
|
611
|
+
this.ctx.render();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
this._pointerDownReadyToDrag = false;
|
|
618
|
+
this._pointerDownInsideSelection = null;
|
|
619
|
+
// Clear guides.
|
|
620
|
+
this.ctx.guides = [];
|
|
621
|
+
// Release pointer capture.
|
|
622
|
+
try {
|
|
623
|
+
this.ctx.container.releasePointerCapture(e.pointerId);
|
|
624
|
+
}
|
|
625
|
+
catch { }
|
|
626
|
+
if (operations.length > 0) {
|
|
627
|
+
this.ctx.callbacks.onOperationsGenerated?.(operations);
|
|
628
|
+
}
|
|
629
|
+
this.ctx.canvas.style.pointerEvents = "none";
|
|
630
|
+
this.ctx.emitInteraction(null);
|
|
631
|
+
this.ctx.render();
|
|
632
|
+
// ── Flat String Bridge ────────────────────────
|
|
633
|
+
if (commitId) {
|
|
634
|
+
const node = this.ctx.tree.get(commitId);
|
|
635
|
+
const commitTarget = (node && node.parentId !== null) ? node.parentId : commitId;
|
|
636
|
+
const html = this.ctx.mount.extractHTML(commitTarget);
|
|
637
|
+
if (html) {
|
|
638
|
+
this.ctx.callbacks.onHTMLCommit?.(commitTarget, html);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
onCancel() {
|
|
643
|
+
this._isDragging = false;
|
|
644
|
+
this._pointerDownReadyToDrag = false;
|
|
645
|
+
this._pointerDownInsideSelection = null;
|
|
646
|
+
this._dragStartCanvas = null;
|
|
647
|
+
// Reset styles
|
|
648
|
+
for (const [id, start] of this._dragStartNodes.entries()) {
|
|
649
|
+
const wrapper = this.ctx.mount.getWrapper(id);
|
|
650
|
+
if (wrapper) {
|
|
651
|
+
wrapper.style.transform = "";
|
|
652
|
+
}
|
|
653
|
+
const node = this.ctx.tree.get(id);
|
|
654
|
+
if (node && start.startPos) {
|
|
655
|
+
if (start.startParentId === null) {
|
|
656
|
+
this.ctx.mount.setNodePosition(id, start.startPos.x, start.startPos.y);
|
|
657
|
+
this.ctx.remeasureSubtree(id);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
this._dragStartNodes.clear();
|
|
662
|
+
this.ctx.activeDropTarget = null;
|
|
663
|
+
this.ctx.guides = [];
|
|
664
|
+
this.ctx.container.style.cursor = "default";
|
|
665
|
+
this.ctx.emitInteraction(null);
|
|
666
|
+
this.ctx.render();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
//# sourceMappingURL=drag.handler.js.map
|