@grida/svg-editor 1.0.0-alpha.2 → 1.0.0-alpha.21

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.
@@ -1,929 +0,0 @@
1
- import { a as capture_resize_baseline, c as is_resizable, i as apply_translate, o as capture_translate_baseline, r as apply_resize, s as compute_resize_factors, t as parse_paint } from "./paint-DuCg6Y-K.mjs";
2
- import { createTextEditor } from "@grida/text-editor/dom";
3
- import { NO_MODS, Surface, measurementToHUDDraw } from "@grida/hud";
4
- import { measure } from "@grida/cmath/_measurement";
5
- import cmath from "@grida/cmath";
6
- //#region src/text-surface.ts
7
- const SVG_NS = "http://www.w3.org/2000/svg";
8
- const XML_NS = "http://www.w3.org/XML/1998/namespace";
9
- var SvgTextSurface = class {
10
- constructor(textEl) {
11
- this.prevXmlSpace = void 0;
12
- this.last_caret_idx = -1;
13
- this.last_caret_visible = false;
14
- this.last_sel_start = -1;
15
- this.last_sel_end = -1;
16
- this.textEl = textEl;
17
- const ownerDoc = textEl.ownerDocument;
18
- const parent = textEl.parentNode;
19
- if (!parent) throw new Error("text element has no parent");
20
- const computedWhitespace = ownerDoc.defaultView?.getComputedStyle(textEl).whiteSpace;
21
- if (!(computedWhitespace === "pre" || computedWhitespace === "pre-wrap" || computedWhitespace === "break-spaces")) {
22
- this.prevXmlSpace = textEl.getAttributeNS(XML_NS, "space");
23
- textEl.setAttributeNS(XML_NS, "xml:space", "preserve");
24
- }
25
- const selection = ownerDoc.createElementNS(SVG_NS, "rect");
26
- selection.setAttribute("fill", "#2563eb");
27
- selection.setAttribute("fill-opacity", "0.25");
28
- selection.setAttribute("pointer-events", "none");
29
- selection.setAttribute("data-svg-text-edit-selection", "");
30
- selection.style.display = "none";
31
- parent.insertBefore(selection, textEl);
32
- this.selectionRect = selection;
33
- const caret = ownerDoc.createElementNS(SVG_NS, "rect");
34
- caret.setAttribute("fill", "#2563eb");
35
- caret.setAttribute("pointer-events", "none");
36
- caret.setAttribute("data-svg-text-edit-caret", "");
37
- caret.style.display = "none";
38
- parent.insertBefore(caret, textEl.nextSibling);
39
- this.caretRect = caret;
40
- }
41
- setText(text) {
42
- if (this.textEl.textContent !== text) this.textEl.textContent = text;
43
- }
44
- setCaret(index, visible) {
45
- if (index === this.last_caret_idx && visible === this.last_caret_visible) return;
46
- this.last_caret_idx = index;
47
- this.last_caret_visible = visible;
48
- if (!visible) {
49
- this.caretRect.style.display = "none";
50
- return;
51
- }
52
- const m = this.metrics();
53
- const x = this.charX(index);
54
- this.caretRect.setAttribute("x", String(x - .75));
55
- this.caretRect.setAttribute("y", String(m.top));
56
- this.caretRect.setAttribute("width", "1.5");
57
- this.caretRect.setAttribute("height", String(m.height));
58
- this.caretRect.style.display = "block";
59
- }
60
- setSelection(start, end) {
61
- if (start === this.last_sel_start && end === this.last_sel_end) return;
62
- this.last_sel_start = start;
63
- this.last_sel_end = end;
64
- if (start === end) {
65
- this.selectionRect.style.display = "none";
66
- return;
67
- }
68
- const m = this.metrics();
69
- const x1 = this.charX(start);
70
- const x2 = this.charX(end);
71
- this.selectionRect.setAttribute("x", String(Math.min(x1, x2)));
72
- this.selectionRect.setAttribute("y", String(m.top));
73
- this.selectionRect.setAttribute("width", String(Math.abs(x2 - x1)));
74
- this.selectionRect.setAttribute("height", String(m.height));
75
- this.selectionRect.style.display = "block";
76
- }
77
- dispose(keepEditMutations = false) {
78
- this.caretRect.remove();
79
- this.selectionRect.remove();
80
- if (this.prevXmlSpace !== void 0 && !keepEditMutations) if (this.prevXmlSpace === null) this.textEl.removeAttributeNS(XML_NS, "space");
81
- else this.textEl.setAttributeNS(XML_NS, "xml:space", this.prevXmlSpace);
82
- this.prevXmlSpace = void 0;
83
- }
84
- positionAtPoint(clientX, clientY) {
85
- const ctm = this.textEl.getScreenCTM();
86
- const svg = this.textEl.ownerSVGElement;
87
- if (!ctm || !svg) return 0;
88
- const pt = svg.createSVGPoint();
89
- pt.x = clientX;
90
- pt.y = clientY;
91
- const local = pt.matrixTransform(ctm.inverse());
92
- return this.localXToCharIndex(local.x);
93
- }
94
- /**
95
- * Single-line `<text>` element: there's no "previous visual line" to move
96
- * to. Cocoa single-line convention: Up/PageUp/line_start → doc start;
97
- * Down/PageDown/line_end → doc end.
98
- */
99
- positionForNavigation(_index, direction) {
100
- const text = this.textEl.textContent ?? "";
101
- switch (direction) {
102
- case "up":
103
- case "page_up":
104
- case "line_start": return 0;
105
- case "down":
106
- case "page_down":
107
- case "line_end": return text.length;
108
- }
109
- }
110
- metrics() {
111
- try {
112
- const b = this.textEl.getBBox();
113
- if (b.height > 0) return {
114
- top: b.y,
115
- height: b.height
116
- };
117
- } catch {}
118
- const fontSize = parseFloat(this.textEl.ownerDocument.defaultView?.getComputedStyle(this.textEl).fontSize ?? "16") || 16;
119
- return {
120
- top: parseFloat(this.textEl.getAttribute("y") ?? "0") - fontSize * .85,
121
- height: fontSize
122
- };
123
- }
124
- charX(i) {
125
- const text = this.textEl.textContent ?? "";
126
- const baseX = parseFloat(this.textEl.getAttribute("x") ?? "0");
127
- if (text.length === 0) return baseX;
128
- if (i <= 0) try {
129
- return this.textEl.getStartPositionOfChar(0).x;
130
- } catch {
131
- return baseX;
132
- }
133
- if (i >= text.length) try {
134
- return this.textEl.getEndPositionOfChar(text.length - 1).x;
135
- } catch {
136
- return baseX;
137
- }
138
- try {
139
- return this.textEl.getStartPositionOfChar(i).x;
140
- } catch {
141
- return baseX;
142
- }
143
- }
144
- localXToCharIndex(localX) {
145
- const text = this.textEl.textContent ?? "";
146
- if (!text) return 0;
147
- for (let i = 0; i < text.length; i++) try {
148
- const ext = this.textEl.getExtentOfChar(i);
149
- if (localX < ext.x + ext.width / 2) return i;
150
- } catch {
151
- break;
152
- }
153
- return text.length;
154
- }
155
- };
156
- //#endregion
157
- //#region src/dom.ts
158
- const ID_ATTR = "data-grida-id";
159
- /** KeyboardEvent.key values for the modifiers the surface tracks. Used to
160
- * short-circuit window-level `keydown`/`keyup` for non-modifier keystrokes. */
161
- const IS_MODIFIER_KEY = {
162
- Shift: true,
163
- Alt: true,
164
- Meta: true,
165
- Control: true
166
- };
167
- /** Sentinel placed in `text_edit` before `createTextEditor` returns, so the
168
- * surface skips render() during the in-flight mount and doesn't yank the
169
- * live `<text>` element out from under the about-to-mount text surface. */
170
- const TEXT_EDIT_PENDING = { __pending: true };
171
- /**
172
- * Attach a DOM surface to a headless editor. Returns a `SurfaceHandle` whose
173
- * `detach()` is the inverse — DOM cleared, listeners removed.
174
- *
175
- * Usage is one-shot per container: the surface owns the container's children
176
- * for its lifetime, and `detach()` restores it to empty.
177
- */
178
- function attach_dom_surface(editor, options) {
179
- const surface = new DomSurface(editor, options.container);
180
- return editor.attach(surface);
181
- }
182
- var DomSurface = class {
183
- constructor(editor, container) {
184
- this.editor = editor;
185
- this.container = container;
186
- this.svg_root = null;
187
- this.teardown = [];
188
- this.element_index = /* @__PURE__ */ new Map();
189
- this.resize_observer = null;
190
- this.active_preview = null;
191
- this.text_edit = null;
192
- this.text_edit_target = null;
193
- this.text_edit_original = "";
194
- this.editor_hover_internal = null;
195
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
196
- container.style.userSelect = "none";
197
- container.style.webkitUserSelect = "none";
198
- this.hud_canvas = container.ownerDocument.createElement("canvas");
199
- Object.assign(this.hud_canvas.style, {
200
- position: "absolute",
201
- left: "0",
202
- top: "0",
203
- pointerEvents: "none"
204
- });
205
- container.appendChild(this.hud_canvas);
206
- this.hud = new Surface(this.hud_canvas, {
207
- pick: (p) => this.hit_test(p[0], p[1]),
208
- shapeOf: (id) => this.shape_of(id),
209
- onIntent: (i) => this.commit_intent(i),
210
- style: { chromeColor: editor.style.chrome_color }
211
- });
212
- this.render();
213
- this.sync_canvas_size();
214
- this.sync_surface_selection();
215
- this.redraw();
216
- const win = container.ownerDocument.defaultView ?? window;
217
- const raf = win.requestAnimationFrame(() => {
218
- this.sync_canvas_size();
219
- this.redraw();
220
- });
221
- this.teardown.push(() => win.cancelAnimationFrame(raf));
222
- const unsub = editor.subscribe(() => {
223
- this.render();
224
- this.sync_surface_selection();
225
- this.sync_canvas_size();
226
- });
227
- this.teardown.push(unsub);
228
- if (typeof ResizeObserver !== "undefined") {
229
- this.resize_observer = new ResizeObserver(() => this.sync_canvas_size());
230
- this.resize_observer.observe(container);
231
- this.teardown.push(() => this.resize_observer?.disconnect());
232
- } else {
233
- const win = container.ownerDocument.defaultView ?? window;
234
- const fn = () => this.sync_canvas_size();
235
- win.addEventListener("resize", fn);
236
- this.teardown.push(() => win.removeEventListener("resize", fn));
237
- }
238
- this.wire_events();
239
- const internal = editor._internal;
240
- this.editor_hover_internal = internal;
241
- internal.set_content_edit_driver((id) => this.enter_content_edit(id));
242
- this.teardown.push(() => internal.set_content_edit_driver(null));
243
- internal.set_computed_resolver({
244
- computed_property: (id, name) => {
245
- const el = this.element_index.get(id);
246
- if (!el) return null;
247
- const value = getComputedStyle(el).getPropertyValue(name);
248
- return value === "" ? null : value;
249
- },
250
- computed_paint: (id, channel) => {
251
- const el = this.element_index.get(id);
252
- if (!el) return null;
253
- const computed = getComputedStyle(el).getPropertyValue(channel);
254
- if (computed === "") return null;
255
- return {
256
- computed,
257
- resolved_paint: parse_paint(computed)
258
- };
259
- }
260
- });
261
- this.teardown.push(() => internal.set_computed_resolver(null));
262
- internal.set_surface_hover_override_driver((id) => {
263
- const response = this.hud.setHoverOverride(id);
264
- if (response.hoverChanged) internal.push_surface_hover(this.hud.hover());
265
- if (response.needsRedraw) this.redraw();
266
- });
267
- this.teardown.push(() => internal.set_surface_hover_override_driver(null));
268
- }
269
- paint(_snapshot) {}
270
- hit_test(x, y) {
271
- const owner_doc = this.container.ownerDocument;
272
- const cr = this.container.getBoundingClientRect();
273
- const target = owner_doc.elementFromPoint(cr.left + x, cr.top + y);
274
- if (!(target instanceof SVGElement)) return null;
275
- const root_id = this.editor.tree().root;
276
- let cur = target;
277
- while (cur instanceof Element) {
278
- const id = cur.getAttribute(ID_ATTR);
279
- if (id) return id === root_id ? null : id;
280
- cur = cur.parentElement;
281
- }
282
- return null;
283
- }
284
- on_input(_listener) {
285
- return () => {};
286
- }
287
- dispose() {
288
- if (this.text_edit) {
289
- this.text_edit.cancel();
290
- this.text_edit = null;
291
- this.text_edit_target = null;
292
- }
293
- for (const fn of this.teardown) fn();
294
- this.teardown = [];
295
- this.hud.dispose();
296
- this.hud_canvas.remove();
297
- if (this.svg_root) this.svg_root.remove();
298
- this.svg_root = null;
299
- this.element_index.clear();
300
- this.active_preview = null;
301
- }
302
- render() {
303
- if (this.text_edit) return;
304
- const owner_doc = this.container.ownerDocument;
305
- const doc = this.editor._internal.doc;
306
- const svg_text = this.editor.serialize();
307
- const wrapper = owner_doc.createElement("div");
308
- wrapper.innerHTML = svg_text;
309
- const new_svg = wrapper.querySelector("svg");
310
- if (!(new_svg instanceof SVGSVGElement)) return;
311
- if (this.svg_root) this.svg_root.replaceWith(new_svg);
312
- else this.container.insertBefore(new_svg, this.hud_canvas);
313
- this.svg_root = new_svg;
314
- this.element_index.clear();
315
- const ids = doc.all_elements();
316
- let i = 0;
317
- const tag_walk = (el) => {
318
- if (i < ids.length) {
319
- const id = ids[i++];
320
- el.setAttribute(ID_ATTR, id);
321
- this.element_index.set(id, el);
322
- }
323
- for (let c = el.firstElementChild; c; c = c.nextElementSibling) if (c instanceof SVGElement) tag_walk(c);
324
- };
325
- tag_walk(new_svg);
326
- }
327
- sync_canvas_size() {
328
- const cr = this.container.getBoundingClientRect();
329
- this.hud.setSize(cr.width, cr.height);
330
- this.redraw();
331
- }
332
- /** Single per-frame draw entry — merges host-fed extras with surface chrome. */
333
- redraw() {
334
- this.hud.draw(this.compute_measurement_extra());
335
- }
336
- /**
337
- * Build the host-fed measurement guide for the current frame, or
338
- * `undefined` if no guide should be drawn.
339
- *
340
- * Master signal: Alt held (read from `surface.modifiers()`). Each
341
- * additional condition is a derivation, not a separate flag — this keeps
342
- * a single source of truth and lets future Alt-consumers (constrained
343
- * resize, axis-lock, …) live next to this one without re-tracking the key.
344
- */
345
- compute_measurement_extra() {
346
- if (!this.hud.modifiers().alt) return void 0;
347
- if (this.hud.gesture().kind !== "idle") return void 0;
348
- const sel = this.editor.state.selection;
349
- if (sel.length === 0) return void 0;
350
- const hover = this.hud.hover();
351
- if (!hover) return void 0;
352
- if (sel.includes(hover)) return void 0;
353
- const a_rects = sel.map((id) => this.container_box(id)).filter((r) => r !== null);
354
- if (a_rects.length === 0) return void 0;
355
- const b_rect = this.container_box(hover);
356
- if (!b_rect) return void 0;
357
- const m = measure(cmath.rect.union(a_rects), b_rect);
358
- if (!m) return void 0;
359
- return measurementToHUDDraw(m, this.editor.style.measurement_color);
360
- }
361
- sync_surface_selection() {
362
- const state = this.editor.state;
363
- if (state.mode === "edit-content") {
364
- this.hud.setSelection([]);
365
- return;
366
- }
367
- this.hud.setSelection(state.selection);
368
- }
369
- /**
370
- * Return the selection shape for a node. Vector `<line>` nodes return
371
- * `{ kind: "line", p1, p2 }` so the HUD lays out endpoint knobs; all
372
- * other nodes return `{ kind: "rect", rect }` using the container-space
373
- * bounding box.
374
- */
375
- shape_of(id) {
376
- if (this.tag_of(id) === "line") {
377
- const line = this.line_endpoints_in_container(id);
378
- if (line) return {
379
- kind: "line",
380
- p1: line.p1,
381
- p2: line.p2
382
- };
383
- }
384
- const rect = this.container_box(id);
385
- if (!rect) return null;
386
- return {
387
- kind: "rect",
388
- rect
389
- };
390
- }
391
- /**
392
- * Project an SVG `<line>`'s `x1,y1,x2,y2` from its own coordinate space
393
- * to the container's coordinate space, where the HUD operates.
394
- */
395
- line_endpoints_in_container(id) {
396
- const el = this.element_index.get(id);
397
- if (!(el instanceof SVGGraphicsElement)) return null;
398
- if (typeof el.getScreenCTM !== "function") return null;
399
- const ctm = el.getScreenCTM();
400
- if (!ctm || !this.svg_root) return null;
401
- const x1 = parseFloat(el.getAttribute("x1") ?? "0");
402
- const y1 = parseFloat(el.getAttribute("y1") ?? "0");
403
- const x2 = parseFloat(el.getAttribute("x2") ?? "0");
404
- const y2 = parseFloat(el.getAttribute("y2") ?? "0");
405
- if (!Number.isFinite(x1) || !Number.isFinite(y1)) return null;
406
- if (!Number.isFinite(x2) || !Number.isFinite(y2)) return null;
407
- const project = (px, py) => {
408
- return [ctm.a * px + ctm.c * py + ctm.e, ctm.b * px + ctm.d * py + ctm.f];
409
- };
410
- const cr = this.container.getBoundingClientRect();
411
- const [s1x, s1y] = project(x1, y1);
412
- const [s2x, s2y] = project(x2, y2);
413
- return {
414
- p1: [s1x - cr.left + this.container.scrollLeft, s1y - cr.top + this.container.scrollTop],
415
- p2: [s2x - cr.left + this.container.scrollLeft, s2y - cr.top + this.container.scrollTop]
416
- };
417
- }
418
- container_box(id) {
419
- const el = this.element_index.get(id);
420
- if (!el) return null;
421
- const ge = el;
422
- if (typeof ge.getBBox !== "function" || typeof ge.getScreenCTM !== "function") return null;
423
- let bbox;
424
- try {
425
- const b = ge.getBBox();
426
- bbox = {
427
- x: b.x,
428
- y: b.y,
429
- width: b.width,
430
- height: b.height
431
- };
432
- } catch {
433
- return null;
434
- }
435
- const ctm = ge.getScreenCTM();
436
- if (!ctm) return null;
437
- const project = (px, py) => ({
438
- x: ctm.a * px + ctm.c * py + ctm.e,
439
- y: ctm.b * px + ctm.d * py + ctm.f
440
- });
441
- const corners = [
442
- project(bbox.x, bbox.y),
443
- project(bbox.x + bbox.width, bbox.y),
444
- project(bbox.x + bbox.width, bbox.y + bbox.height),
445
- project(bbox.x, bbox.y + bbox.height)
446
- ];
447
- const xs = corners.map((c) => c.x);
448
- const ys = corners.map((c) => c.y);
449
- const left = Math.min(...xs);
450
- const top = Math.min(...ys);
451
- const right = Math.max(...xs);
452
- const bottom = Math.max(...ys);
453
- const cr = this.container.getBoundingClientRect();
454
- return {
455
- x: left - cr.left + this.container.scrollLeft,
456
- y: top - cr.top + this.container.scrollTop,
457
- width: right - left,
458
- height: bottom - top
459
- };
460
- }
461
- screen_delta_in_own_frame(id, dx, dy) {
462
- const el = this.element_index.get(id);
463
- if (!el) return {
464
- x: dx,
465
- y: dy
466
- };
467
- return this.unproject_delta(el, dx, dy);
468
- }
469
- screen_delta_in_parent_frame(id, dx, dy) {
470
- const el = this.element_index.get(id);
471
- if (!el) return {
472
- x: dx,
473
- y: dy
474
- };
475
- const parent = el.parentNode ?? el;
476
- return this.unproject_delta(parent, dx, dy);
477
- }
478
- unproject_delta(frame, dx, dy) {
479
- const ctm = typeof frame.getScreenCTM === "function" ? frame.getScreenCTM() : null;
480
- if (!ctm || !this.svg_root) return {
481
- x: dx,
482
- y: dy
483
- };
484
- const inv = ctm.inverse();
485
- const p0 = this.svg_root.createSVGPoint();
486
- p0.x = 0;
487
- p0.y = 0;
488
- const p1 = this.svg_root.createSVGPoint();
489
- p1.x = dx;
490
- p1.y = dy;
491
- const tp0 = p0.matrixTransform(inv);
492
- const tp1 = p1.matrixTransform(inv);
493
- return {
494
- x: tp1.x - tp0.x,
495
- y: tp1.y - tp0.y
496
- };
497
- }
498
- wire_events() {
499
- const owner_doc = this.container.ownerDocument;
500
- const win = owner_doc.defaultView ?? window;
501
- const on = (target, event, handler) => {
502
- target.addEventListener(event, handler);
503
- this.teardown.push(() => target.removeEventListener(event, handler));
504
- };
505
- on(this.container, "pointerdown", (e) => this.dispatch_pointer(e, "pointer_down"));
506
- on(win, "pointermove", (e) => this.dispatch_pointer(e, "pointer_move"));
507
- on(win, "pointerup", (e) => this.dispatch_pointer(e, "pointer_up"));
508
- on(owner_doc, "keydown", (e) => this.on_keydown(e));
509
- on(win, "keydown", (e) => {
510
- if (e.repeat || !IS_MODIFIER_KEY[e.key]) return;
511
- this.sync_modifiers(e);
512
- });
513
- on(win, "keyup", (e) => {
514
- if (!IS_MODIFIER_KEY[e.key]) return;
515
- this.sync_modifiers(e);
516
- });
517
- on(win, "blur", () => this.sync_modifiers(null));
518
- on(this.container, "contextmenu", (e) => e.preventDefault());
519
- }
520
- /**
521
- * Master signal for modifier-driven UX consumers (measurement, future
522
- * constrained-resize, …). Modifier changes aren't on the pointer-event
523
- * path, so derived overlays would otherwise wait for the next pointer
524
- * move; redraw eagerly. `null` means modifiers are forced clear
525
- * (blur / focus-out).
526
- */
527
- sync_modifiers(e) {
528
- const next = e ? {
529
- shift: e.shiftKey,
530
- alt: e.altKey,
531
- meta: e.metaKey,
532
- ctrl: e.ctrlKey
533
- } : NO_MODS;
534
- const prev = this.hud.modifiers();
535
- if (prev.shift === next.shift && prev.alt === next.alt && prev.meta === next.meta && prev.ctrl === next.ctrl) return;
536
- const response = this.hud.dispatch({
537
- kind: "modifiers",
538
- mods: next
539
- });
540
- this.redraw();
541
- if (response.cursorChanged) this.sync_cursor();
542
- if (response.hoverChanged) this.editor_hover_internal?.push_surface_hover(this.hud.hover());
543
- }
544
- dispatch_pointer(e, kind) {
545
- if (this.text_edit) {
546
- if (kind === "pointer_down") {
547
- const el = this.text_edit_target ? this.element_index.get(this.text_edit_target) : null;
548
- if (el && e.target instanceof Element && (e.target === el || el.contains(e.target))) this.text_edit.pointerDown(e.clientX, e.clientY, e.shiftKey);
549
- } else if (kind === "pointer_move") this.text_edit.pointerMove(e.clientX, e.clientY);
550
- else if (kind === "pointer_up") this.text_edit.pointerUp();
551
- return;
552
- }
553
- const cr = this.container.getBoundingClientRect();
554
- const x = e.clientX - cr.left;
555
- const y = e.clientY - cr.top;
556
- const mods = {
557
- shift: e.shiftKey,
558
- alt: e.altKey,
559
- meta: e.metaKey,
560
- ctrl: e.ctrlKey
561
- };
562
- const button = e.button === 0 ? "primary" : e.button === 2 ? "secondary" : "middle";
563
- let event;
564
- if (kind === "pointer_move") event = {
565
- kind,
566
- x,
567
- y,
568
- mods
569
- };
570
- else {
571
- event = {
572
- kind,
573
- x,
574
- y,
575
- button,
576
- mods
577
- };
578
- if (kind === "pointer_down") try {
579
- this.container.setPointerCapture(e.pointerId);
580
- } catch {}
581
- }
582
- const response = this.hud.dispatch(event);
583
- if (response.needsRedraw) this.redraw();
584
- if (response.cursorChanged) this.sync_cursor();
585
- if (response.hoverChanged) this.editor_hover_internal?.push_surface_hover(this.hud.hover());
586
- }
587
- sync_cursor() {
588
- const c = this.hud.cursor();
589
- let css = "default";
590
- if (typeof c === "string") css = c === "default" ? "default" : c;
591
- else if (c.kind === "resize") css = `${c.direction}-resize`;
592
- else if (c.kind === "rotate") css = "crosshair";
593
- this.container.style.cursor = css;
594
- }
595
- on_keydown(e) {
596
- if (this.text_edit) return;
597
- if (e.code === "Escape" && this.active_preview) {
598
- this.active_preview.session.discard();
599
- this.active_preview = null;
600
- }
601
- this.editor.keymap.dispatch(e);
602
- }
603
- commit_intent(intent) {
604
- switch (intent.kind) {
605
- case "select":
606
- this.editor.commands.select(intent.ids, { additive: intent.mode !== "replace" });
607
- return;
608
- case "deselect_all":
609
- this.editor.commands.deselect();
610
- return;
611
- case "translate":
612
- this.handle_translate(intent);
613
- return;
614
- case "resize":
615
- this.handle_resize(intent);
616
- return;
617
- case "rotate": return;
618
- case "marquee_select":
619
- this.handle_marquee(intent);
620
- return;
621
- case "set_endpoint":
622
- this.handle_set_endpoint(intent);
623
- return;
624
- case "enter_content_edit":
625
- this.editor.commands.select(intent.id);
626
- this.editor.enter_content_edit(intent.id);
627
- return;
628
- case "cancel_gesture":
629
- if (this.active_preview) {
630
- this.active_preview.session.discard();
631
- this.active_preview = null;
632
- }
633
- return;
634
- }
635
- }
636
- handle_translate(intent) {
637
- const ids = intent.ids;
638
- if (ids.length === 0) return;
639
- const internal = this.editor_internal();
640
- const doc = internal.doc;
641
- const emit = internal.emit;
642
- if (!this.active_preview || this.active_preview.kind !== "translate") {
643
- if (this.active_preview) this.active_preview.session.discard();
644
- const baselines = /* @__PURE__ */ new Map();
645
- for (const id of ids) baselines.set(id, capture_translate_baseline(doc, id));
646
- this.active_preview = {
647
- kind: "translate",
648
- ids,
649
- baselines,
650
- session: internal.history.preview("move")
651
- };
652
- }
653
- const baselines = this.active_preview.baselines;
654
- const deltas = /* @__PURE__ */ new Map();
655
- for (const id of ids) deltas.set(id, this.screen_delta_in_parent_frame(id, intent.dx, intent.dy));
656
- const apply = () => {
657
- for (const id of ids) {
658
- const baseline = baselines.get(id);
659
- const d = deltas.get(id);
660
- if (!baseline || !d) continue;
661
- apply_translate(doc, id, baseline, d.x, d.y);
662
- }
663
- emit();
664
- };
665
- const revert = () => {
666
- for (const id of ids) {
667
- const baseline = baselines.get(id);
668
- if (!baseline) continue;
669
- apply_translate(doc, id, baseline, 0, 0);
670
- }
671
- emit();
672
- };
673
- this.active_preview.session.set({
674
- providerId: "svg-editor",
675
- apply,
676
- revert
677
- });
678
- if (intent.phase === "commit") {
679
- this.active_preview.session.commit();
680
- this.active_preview = null;
681
- }
682
- }
683
- handle_resize(intent) {
684
- const id = intent.ids[0];
685
- if (!id || !is_resizable(this.tag_of(id))) return;
686
- const internal = this.editor_internal();
687
- const doc = internal.doc;
688
- const emit = internal.emit;
689
- if (!this.active_preview || this.active_preview.kind !== "resize" || this.active_preview.id !== id) {
690
- if (this.active_preview) this.active_preview.session.discard();
691
- const bbox = this.bbox_local(id) ?? {
692
- x: 0,
693
- y: 0,
694
- width: 0,
695
- height: 0
696
- };
697
- const initial_screen = this.container_box(id) ?? {
698
- x: 0,
699
- y: 0,
700
- width: 0,
701
- height: 0
702
- };
703
- this.active_preview = {
704
- kind: "resize",
705
- id,
706
- direction: intent.anchor,
707
- baseline: capture_resize_baseline(doc, id, bbox),
708
- initial_screen,
709
- session: internal.history.preview("resize")
710
- };
711
- }
712
- const baseline = this.active_preview.baseline;
713
- const dir = this.active_preview.direction;
714
- const initial = this.active_preview.initial_screen;
715
- const dx_screen = intent.rect.width - initial.width;
716
- const dy_screen = intent.rect.height - initial.height;
717
- const signed_dx = dir === "w" || dir === "nw" || dir === "sw" ? -dx_screen : dx_screen;
718
- const signed_dy = dir === "n" || dir === "ne" || dir === "nw" ? -dy_screen : dy_screen;
719
- const d = this.screen_delta_in_own_frame(id, signed_dx, signed_dy);
720
- const f = compute_resize_factors(baseline, dir, d.x, d.y, false);
721
- const apply = () => {
722
- apply_resize(doc, id, baseline, f.sx, f.sy, f.origin);
723
- emit();
724
- };
725
- const revert = () => {
726
- apply_resize(doc, id, baseline, 1, 1, f.origin);
727
- emit();
728
- };
729
- this.active_preview.session.set({
730
- providerId: "svg-editor",
731
- apply,
732
- revert
733
- });
734
- if (intent.phase === "commit") {
735
- this.active_preview.session.commit();
736
- this.active_preview = null;
737
- }
738
- }
739
- /**
740
- * Apply a `set_endpoint` intent — moving one endpoint of a vector
741
- * `<line>` to a new container-space position. Unprojects to the element's
742
- * own (SVG) coord space and updates the corresponding attribute.
743
- */
744
- handle_set_endpoint(intent) {
745
- const id = intent.id;
746
- if (this.tag_of(id) !== "line") return;
747
- const internal = this.editor_internal();
748
- const doc = internal.doc;
749
- const emit = internal.emit;
750
- if (!this.active_preview || this.active_preview.kind !== "endpoint" || this.active_preview.id !== id || this.active_preview.endpoint !== intent.endpoint) {
751
- if (this.active_preview) this.active_preview.session.discard();
752
- const initial = {
753
- x1: numAttr(doc, id, "x1"),
754
- y1: numAttr(doc, id, "y1"),
755
- x2: numAttr(doc, id, "x2"),
756
- y2: numAttr(doc, id, "y2")
757
- };
758
- this.active_preview = {
759
- kind: "endpoint",
760
- id,
761
- endpoint: intent.endpoint,
762
- initial,
763
- session: internal.history.preview("set-endpoint")
764
- };
765
- }
766
- const initial = this.active_preview.initial;
767
- const endpoint = this.active_preview.endpoint;
768
- const pos_own = this.container_point_in_own_frame(id, intent.pos[0], intent.pos[1]);
769
- if (!pos_own) return;
770
- const target_x = pos_own.x;
771
- const target_y = pos_own.y;
772
- const apply = () => {
773
- if (endpoint === "p1") {
774
- doc.set_attr(id, "x1", String(target_x));
775
- doc.set_attr(id, "y1", String(target_y));
776
- } else {
777
- doc.set_attr(id, "x2", String(target_x));
778
- doc.set_attr(id, "y2", String(target_y));
779
- }
780
- emit();
781
- };
782
- const revert = () => {
783
- doc.set_attr(id, "x1", String(initial.x1));
784
- doc.set_attr(id, "y1", String(initial.y1));
785
- doc.set_attr(id, "x2", String(initial.x2));
786
- doc.set_attr(id, "y2", String(initial.y2));
787
- emit();
788
- };
789
- this.active_preview.session.set({
790
- providerId: "svg-editor",
791
- apply,
792
- revert
793
- });
794
- if (intent.phase === "commit") {
795
- this.active_preview.session.commit();
796
- this.active_preview = null;
797
- }
798
- }
799
- /**
800
- * Convert a container-space point to the element's own SVG coord space.
801
- * Inverse of `line_endpoints_in_container`'s projection.
802
- */
803
- container_point_in_own_frame(id, cx, cy) {
804
- const el = this.element_index.get(id);
805
- if (!(el instanceof SVGGraphicsElement)) return null;
806
- if (typeof el.getScreenCTM !== "function") return null;
807
- const ctm = el.getScreenCTM();
808
- if (!ctm || !this.svg_root) return null;
809
- const cr = this.container.getBoundingClientRect();
810
- const inv = ctm.inverse();
811
- const p = this.svg_root.createSVGPoint();
812
- p.x = cx + cr.left - this.container.scrollLeft;
813
- p.y = cy + cr.top - this.container.scrollTop;
814
- const t = p.matrixTransform(inv);
815
- return {
816
- x: t.x,
817
- y: t.y
818
- };
819
- }
820
- handle_marquee(intent) {
821
- if (intent.phase !== "commit") return;
822
- const ids = [];
823
- for (const [id, el] of this.element_index) {
824
- if (id === this.editor.tree().root) continue;
825
- const box = this.container_box(id);
826
- if (!box) continue;
827
- if (rect_intersects(box, intent.rect)) ids.push(id);
828
- }
829
- if (ids.length === 0) {
830
- if (!intent.additive) this.editor.commands.deselect();
831
- return;
832
- }
833
- this.editor.commands.select(ids, { additive: intent.additive });
834
- }
835
- enter_content_edit(id) {
836
- if (this.text_edit) return false;
837
- const el = this.element_index.get(id);
838
- if (!(el instanceof SVGTextElement)) return false;
839
- const doc = this.editor._internal;
840
- this.text_edit_target = id;
841
- this.text_edit_original = doc.doc.text_of(id);
842
- this.text_edit = TEXT_EDIT_PENDING;
843
- this.editor.commands.set_mode("edit-content");
844
- this.sync_surface_selection();
845
- this.redraw();
846
- const text_surface = new SvgTextSurface(this.element_index.get(id) ?? el);
847
- const is_mac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
848
- let settled = false;
849
- const cleanup_after_commit_or_cancel = () => {
850
- this.text_edit = null;
851
- this.text_edit_target = null;
852
- this.editor.commands.set_mode("select");
853
- this.render();
854
- this.sync_surface_selection();
855
- this.redraw();
856
- };
857
- this.text_edit = createTextEditor({
858
- container: this.container,
859
- initialText: this.text_edit_original,
860
- layout: text_surface,
861
- surface: text_surface,
862
- isMac: is_mac,
863
- ariaLabel: "edit svg text",
864
- requiresMutationsForCommit: (text) => /\s{2,}|^\s|\s$/.test(text),
865
- callbacks: {
866
- onChange: (text) => {
867
- doc.doc.set_text(id, text);
868
- },
869
- onCommit: (final_text) => {
870
- if (settled) return;
871
- settled = true;
872
- doc.doc.set_text(id, this.text_edit_original);
873
- cleanup_after_commit_or_cancel();
874
- if (final_text !== this.text_edit_original) this.editor.commands.set_text(final_text);
875
- },
876
- onCancel: () => {
877
- if (settled) return;
878
- settled = true;
879
- doc.doc.set_text(id, this.text_edit_original);
880
- cleanup_after_commit_or_cancel();
881
- doc.emit();
882
- },
883
- onUndoFallthrough: () => {
884
- this.text_edit?.commit();
885
- this.editor.commands.undo();
886
- },
887
- onRedoFallthrough: () => {
888
- this.text_edit?.commit();
889
- this.editor.commands.redo();
890
- }
891
- }
892
- });
893
- return true;
894
- }
895
- tag_of(id) {
896
- return this.editor.tree().nodes.get(id)?.tag ?? "";
897
- }
898
- bbox_local(id) {
899
- const el = this.element_index.get(id);
900
- if (!el) return null;
901
- const ge = el;
902
- if (typeof ge.getBBox !== "function") return null;
903
- try {
904
- const b = ge.getBBox();
905
- return {
906
- x: b.x,
907
- y: b.y,
908
- width: b.width,
909
- height: b.height
910
- };
911
- } catch {
912
- return null;
913
- }
914
- }
915
- editor_internal() {
916
- return this.editor._internal;
917
- }
918
- };
919
- function numAttr(doc, id, name) {
920
- const v = doc.get_attr(id, name);
921
- if (v === null || v === "") return 0;
922
- const n = parseFloat(v);
923
- return Number.isFinite(n) ? n : 0;
924
- }
925
- function rect_intersects(a, b) {
926
- return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
927
- }
928
- //#endregion
929
- export { attach_dom_surface as t };