@grida/svg-editor 1.0.0-alpha.3 → 1.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -107
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/dom-CMXNUMjP.d.mts +48 -0
- package/dist/{dom-BlJZWpR_.js → dom-DyJy1H6Q.js} +115 -9
- package/dist/dom-eIgcZ4JC.d.ts +48 -0
- package/dist/{dom-D-5D_3o0.mjs → dom-l5Y1Wf8C.mjs} +100 -9
- package/dist/dom.d.mts +1 -47
- package/dist/dom.d.ts +1 -47
- package/dist/dom.js +1 -1
- package/dist/dom.mjs +1 -1
- package/dist/{editor-DP36h-SE.mjs → editor-CRflVqEz.mjs} +44 -12
- package/dist/{editor-Da446SPO.d.ts → editor-DdgqLDC9.d.ts} +184 -106
- package/dist/{editor-Eon0043Z.js → editor-Ds47eN37.js} +45 -13
- package/dist/{editor-DSADZszj.d.mts → editor-KRAmUodY.d.mts} +184 -106
- package/dist/index-CHiXYO9-.d.ts +1 -0
- package/dist/index-ThDLM4Am.d.mts +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{paint-CVLZazOa.js → paint-cTjePy5e.js} +1 -1
- package/dist/presets.d.mts +46 -0
- package/dist/presets.d.ts +46 -0
- package/dist/presets.js +52 -0
- package/dist/presets.mjs +47 -0
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/react.mjs +2 -2
- package/package.json +7 -2
- /package/dist/{paint-BTKvRItP.mjs → paint-Cfiw4g_J.mjs} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as capture_resize_baseline, c as is_resizable, i as apply_translate, l as is_text_input_focused, o as capture_translate_baseline, r as apply_resize, s as compute_resize_factors, t as parse_paint } from "./paint-
|
|
1
|
+
import { a as capture_resize_baseline, c as is_resizable, i as apply_translate, l as is_text_input_focused, o as capture_translate_baseline, r as apply_resize, s as compute_resize_factors, t as parse_paint } from "./paint-Cfiw4g_J.mjs";
|
|
2
2
|
import { createTextEditor } from "@grida/text-editor/dom";
|
|
3
3
|
import { NO_MODS, Surface, measurementToHUDDraw } from "@grida/hud";
|
|
4
4
|
import { measure } from "@grida/cmath/_measurement";
|
|
@@ -16,9 +16,26 @@ var Camera = class {
|
|
|
16
16
|
this.viewport_w = 0;
|
|
17
17
|
this.viewport_h = 0;
|
|
18
18
|
this.listeners = /* @__PURE__ */ new Set();
|
|
19
|
+
this._constraints = null;
|
|
19
20
|
this._transform = opts.initial ?? cmath.transform.identity;
|
|
20
21
|
this.resolve_bounds = opts.resolve_bounds;
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Current viewport constraint, or `null` for free pan/zoom. Set with
|
|
25
|
+
* `camera.constraints = { type: 'cover', bounds: '<root>', padding: 80 }`
|
|
26
|
+
* to clamp zoom + pan; assign `null` to clear.
|
|
27
|
+
*
|
|
28
|
+
* Constraints are applied synchronously inside `set_transform` (and
|
|
29
|
+
* `_set_viewport_size`), so every public mutation respects them
|
|
30
|
+
* automatically — the host never needs to subscribe-and-clamp itself.
|
|
31
|
+
*/
|
|
32
|
+
get constraints() {
|
|
33
|
+
return this._constraints;
|
|
34
|
+
}
|
|
35
|
+
set constraints(c) {
|
|
36
|
+
this._constraints = c;
|
|
37
|
+
if (c) this.reenforce();
|
|
38
|
+
}
|
|
22
39
|
/** Underlying 2D affine. World→screen. */
|
|
23
40
|
get transform() {
|
|
24
41
|
return this._transform;
|
|
@@ -117,10 +134,15 @@ var Camera = class {
|
|
|
117
134
|
* makes external constraint loops (e.g. "subscribe → compute clamped →
|
|
118
135
|
* set_transform") terminate: the clamp re-emits the same transform on
|
|
119
136
|
* the second pass, set_transform short-circuits, no recursion.
|
|
137
|
+
*
|
|
138
|
+
* When `camera.constraints` is non-null, the input transform is clamped
|
|
139
|
+
* synchronously before being stored — every public mutation respects the
|
|
140
|
+
* constraint automatically.
|
|
120
141
|
*/
|
|
121
142
|
set_transform(t) {
|
|
122
|
-
|
|
123
|
-
this._transform
|
|
143
|
+
const next = this.apply_constraints(t);
|
|
144
|
+
if (transform_equal(this._transform, next)) return;
|
|
145
|
+
this._transform = next;
|
|
124
146
|
this.notify();
|
|
125
147
|
}
|
|
126
148
|
/** Viewport size in screen pixels. Read by host code computing constraints. */
|
|
@@ -174,6 +196,10 @@ var Camera = class {
|
|
|
174
196
|
if (w === this.viewport_w && h === this.viewport_h) return;
|
|
175
197
|
this.viewport_w = w;
|
|
176
198
|
this.viewport_h = h;
|
|
199
|
+
if (this._constraints) {
|
|
200
|
+
const next = this.apply_constraints(this._transform);
|
|
201
|
+
if (!transform_equal(this._transform, next)) this._transform = next;
|
|
202
|
+
}
|
|
177
203
|
this.notify();
|
|
178
204
|
}
|
|
179
205
|
/** Convert a screen-space point to world-space. */
|
|
@@ -193,10 +219,71 @@ var Camera = class {
|
|
|
193
219
|
y: sy
|
|
194
220
|
};
|
|
195
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Apply the current constraint (if any) to a candidate transform.
|
|
224
|
+
* Pure: returns the clamped result, never mutates state. Returns the
|
|
225
|
+
* input unchanged when constraints are null / bounds are unresolvable /
|
|
226
|
+
* viewport is 0.
|
|
227
|
+
*/
|
|
228
|
+
apply_constraints(t) {
|
|
229
|
+
if (!this._constraints) return t;
|
|
230
|
+
if (this.viewport_w <= 0 || this.viewport_h <= 0) return t;
|
|
231
|
+
switch (this._constraints.type) {
|
|
232
|
+
case "cover": return clamp_cover(t, this._constraints, this.viewport_w, this.viewport_h, this.resolve_bounds);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Re-clamp the stored transform against the current constraint. Called
|
|
237
|
+
* from the `constraints` setter; `_set_viewport_size` has its own
|
|
238
|
+
* notify-inclusive path.
|
|
239
|
+
*/
|
|
240
|
+
reenforce() {
|
|
241
|
+
if (!this._constraints) return;
|
|
242
|
+
const next = this.apply_constraints(this._transform);
|
|
243
|
+
if (transform_equal(this._transform, next)) return;
|
|
244
|
+
this._transform = next;
|
|
245
|
+
this.notify();
|
|
246
|
+
}
|
|
196
247
|
notify() {
|
|
197
248
|
for (const cb of this.listeners) cb();
|
|
198
249
|
}
|
|
199
250
|
};
|
|
251
|
+
/**
|
|
252
|
+
* Clamp a transform under a `'cover'` constraint:
|
|
253
|
+
* - Zoom lower-bounded at fit-with-padding (the slide always fills the
|
|
254
|
+
* viewport edge-to-edge).
|
|
255
|
+
* - When at min-zoom the slide is locked centered (bounds smaller than
|
|
256
|
+
* viewport on the constrained axis is impossible above min_zoom; below
|
|
257
|
+
* is impossible because zoom is clamped up).
|
|
258
|
+
* - When zoomed in, pan is clamped so the slide always covers the viewport
|
|
259
|
+
* (no black bars).
|
|
260
|
+
*
|
|
261
|
+
* Returns the input transform unchanged when bounds can't be resolved or
|
|
262
|
+
* are degenerate.
|
|
263
|
+
*/
|
|
264
|
+
function clamp_cover(t, c, vp_w, vp_h, resolve) {
|
|
265
|
+
const bounds = typeof c.bounds === "string" ? resolve(c.bounds) : c.bounds;
|
|
266
|
+
if (!bounds || bounds.width <= 0 || bounds.height <= 0) return t;
|
|
267
|
+
const padding = c.padding ?? 0;
|
|
268
|
+
const eff_w = vp_w - 2 * padding;
|
|
269
|
+
const eff_h = vp_h - 2 * padding;
|
|
270
|
+
if (eff_w <= 0 || eff_h <= 0) return t;
|
|
271
|
+
const min_zoom = Math.min(eff_w / bounds.width, eff_h / bounds.height);
|
|
272
|
+
const s = Math.max(t[0][0], min_zoom);
|
|
273
|
+
const sw = s * bounds.width;
|
|
274
|
+
const sh = s * bounds.height;
|
|
275
|
+
const tx = sw > vp_w ? cmath.clamp(t[0][2], vp_w - s * (bounds.x + bounds.width), -s * bounds.x) : (vp_w - sw) / 2 - s * bounds.x;
|
|
276
|
+
const ty = sh > vp_h ? cmath.clamp(t[1][2], vp_h - s * (bounds.y + bounds.height), -s * bounds.y) : (vp_h - sh) / 2 - s * bounds.y;
|
|
277
|
+
return [[
|
|
278
|
+
s,
|
|
279
|
+
0,
|
|
280
|
+
tx
|
|
281
|
+
], [
|
|
282
|
+
0,
|
|
283
|
+
s,
|
|
284
|
+
ty
|
|
285
|
+
]];
|
|
286
|
+
}
|
|
200
287
|
function transform_equal(a, b) {
|
|
201
288
|
return a[0][0] === b[0][0] && a[0][1] === b[0][1] && a[0][2] === b[0][2] && a[1][0] === b[1][0] && a[1][1] === b[1][1] && a[1][2] === b[1][2];
|
|
202
289
|
}
|
|
@@ -445,7 +532,9 @@ var SvgTextSurface = class {
|
|
|
445
532
|
this.last_sel_end = -1;
|
|
446
533
|
this.textEl = textEl;
|
|
447
534
|
const ownerDoc = textEl.ownerDocument;
|
|
448
|
-
|
|
535
|
+
let mountAnchor = textEl;
|
|
536
|
+
while (mountAnchor.parentElement instanceof SVGElement && (mountAnchor.localName === "tspan" || mountAnchor.localName === "textPath")) mountAnchor = mountAnchor.parentElement;
|
|
537
|
+
const parent = mountAnchor.parentNode;
|
|
449
538
|
if (!parent) throw new Error("text element has no parent");
|
|
450
539
|
const computedWhitespace = ownerDoc.defaultView?.getComputedStyle(textEl).whiteSpace;
|
|
451
540
|
if (!(computedWhitespace === "pre" || computedWhitespace === "pre-wrap" || computedWhitespace === "break-spaces")) {
|
|
@@ -460,14 +549,14 @@ var SvgTextSurface = class {
|
|
|
460
549
|
selection.setAttribute("pointer-events", "none");
|
|
461
550
|
selection.setAttribute("data-svg-text-edit-selection", "");
|
|
462
551
|
selection.style.display = "none";
|
|
463
|
-
parent.insertBefore(selection,
|
|
552
|
+
parent.insertBefore(selection, mountAnchor);
|
|
464
553
|
this.selectionRect = selection;
|
|
465
554
|
const caret = ownerDoc.createElementNS(SVG_NS, "rect");
|
|
466
555
|
caret.setAttribute("fill", "#2563eb");
|
|
467
556
|
caret.setAttribute("pointer-events", "none");
|
|
468
557
|
caret.setAttribute("data-svg-text-edit-caret", "");
|
|
469
558
|
caret.style.display = "none";
|
|
470
|
-
parent.insertBefore(caret,
|
|
559
|
+
parent.insertBefore(caret, mountAnchor.nextSibling);
|
|
471
560
|
this.caretRect = caret;
|
|
472
561
|
}
|
|
473
562
|
setText(text) {
|
|
@@ -1210,7 +1299,7 @@ var DomSurface = class {
|
|
|
1210
1299
|
commit_intent(intent) {
|
|
1211
1300
|
switch (intent.kind) {
|
|
1212
1301
|
case "select":
|
|
1213
|
-
this.editor.commands.select(intent.ids, {
|
|
1302
|
+
this.editor.commands.select(intent.ids, { mode: intent.mode });
|
|
1214
1303
|
return;
|
|
1215
1304
|
case "deselect_all":
|
|
1216
1305
|
this.editor.commands.deselect();
|
|
@@ -1437,13 +1526,15 @@ var DomSurface = class {
|
|
|
1437
1526
|
if (!intent.additive) this.editor.commands.deselect();
|
|
1438
1527
|
return;
|
|
1439
1528
|
}
|
|
1440
|
-
this.editor.commands.select(ids, {
|
|
1529
|
+
this.editor.commands.select(ids, { mode: intent.additive ? "add" : "replace" });
|
|
1441
1530
|
}
|
|
1442
1531
|
enter_content_edit(id) {
|
|
1443
1532
|
if (this.text_edit) return false;
|
|
1444
1533
|
const el = this.element_index.get(id);
|
|
1445
|
-
if (!(el instanceof
|
|
1534
|
+
if (!(el instanceof SVGElement)) return false;
|
|
1446
1535
|
const doc = this.editor._internal;
|
|
1536
|
+
if (!doc.doc.is_text_edit_target(id)) return false;
|
|
1537
|
+
if (!(el instanceof SVGTextContentElement)) return false;
|
|
1447
1538
|
this.text_edit_target = id;
|
|
1448
1539
|
this.text_edit_original = doc.doc.text_of(id);
|
|
1449
1540
|
this.text_edit = TEXT_EDIT_PENDING;
|
package/dist/dom.d.mts
CHANGED
|
@@ -1,48 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import cmath from "@grida/cmath";
|
|
3
|
-
|
|
4
|
-
//#region src/dom.d.ts
|
|
5
|
-
type DomSurfaceOptions = {
|
|
6
|
-
/** Mount the SVG inside this container. */container: HTMLElement;
|
|
7
|
-
/**
|
|
8
|
-
* Install the default gesture set (wheel-pan/zoom, space-drag, middle-mouse,
|
|
9
|
-
* keyboard zoom). Default `true`. Pass `false` to start blank and bind à la
|
|
10
|
-
* carte via `handle.gestures.bind(...)`.
|
|
11
|
-
*/
|
|
12
|
-
gestures?: boolean;
|
|
13
|
-
/**
|
|
14
|
-
* Auto-fit the document into the viewport on initial attach. Default
|
|
15
|
-
* `false`. Mirrors Excalidraw's `initialData.scrollToContent`.
|
|
16
|
-
* Subsequent `editor.load()` calls do NOT re-fit — call
|
|
17
|
-
* `handle.camera.fit("<root>")` yourself if you want that behavior.
|
|
18
|
-
*/
|
|
19
|
-
fit?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Initial camera transform. Default `cmath.transform.identity`. Ignored
|
|
22
|
-
* when `fit: true`.
|
|
23
|
-
*/
|
|
24
|
-
initial_camera?: cmath.Transform;
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Surface handle for the DOM surface. Extends the editor's core
|
|
28
|
-
* `SurfaceHandle` with the viewport-scoped concerns: pan/zoom (`camera`)
|
|
29
|
-
* and pointer/wheel/keyboard gesture bindings (`gestures`).
|
|
30
|
-
*
|
|
31
|
-
* Camera + gestures are **surface-scoped**: detaching the surface drops
|
|
32
|
-
* both. They never appear on the headless `SvgEditor`.
|
|
33
|
-
*/
|
|
34
|
-
type DomSurfaceHandle = SurfaceHandle & {
|
|
35
|
-
camera: Camera;
|
|
36
|
-
gestures: Gestures;
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Attach a DOM surface to a headless editor. Returns a `DomSurfaceHandle`
|
|
40
|
-
* whose `detach()` is the inverse — DOM cleared, listeners removed,
|
|
41
|
-
* gestures uninstalled.
|
|
42
|
-
*
|
|
43
|
-
* Usage is one-shot per container: the surface owns the container's children
|
|
44
|
-
* for its lifetime, and `detach()` restores it to empty.
|
|
45
|
-
*/
|
|
46
|
-
declare function attach_dom_surface(editor: SvgEditor, options: DomSurfaceOptions): DomSurfaceHandle;
|
|
47
|
-
//#endregion
|
|
1
|
+
import { n as DomSurfaceOptions, r as attach_dom_surface, t as DomSurfaceHandle } from "./dom-CMXNUMjP.mjs";
|
|
48
2
|
export { DomSurfaceHandle, DomSurfaceOptions, attach_dom_surface };
|
package/dist/dom.d.ts
CHANGED
|
@@ -1,48 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import cmath from "@grida/cmath";
|
|
3
|
-
|
|
4
|
-
//#region src/dom.d.ts
|
|
5
|
-
type DomSurfaceOptions = {
|
|
6
|
-
/** Mount the SVG inside this container. */container: HTMLElement;
|
|
7
|
-
/**
|
|
8
|
-
* Install the default gesture set (wheel-pan/zoom, space-drag, middle-mouse,
|
|
9
|
-
* keyboard zoom). Default `true`. Pass `false` to start blank and bind à la
|
|
10
|
-
* carte via `handle.gestures.bind(...)`.
|
|
11
|
-
*/
|
|
12
|
-
gestures?: boolean;
|
|
13
|
-
/**
|
|
14
|
-
* Auto-fit the document into the viewport on initial attach. Default
|
|
15
|
-
* `false`. Mirrors Excalidraw's `initialData.scrollToContent`.
|
|
16
|
-
* Subsequent `editor.load()` calls do NOT re-fit — call
|
|
17
|
-
* `handle.camera.fit("<root>")` yourself if you want that behavior.
|
|
18
|
-
*/
|
|
19
|
-
fit?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Initial camera transform. Default `cmath.transform.identity`. Ignored
|
|
22
|
-
* when `fit: true`.
|
|
23
|
-
*/
|
|
24
|
-
initial_camera?: cmath.Transform;
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Surface handle for the DOM surface. Extends the editor's core
|
|
28
|
-
* `SurfaceHandle` with the viewport-scoped concerns: pan/zoom (`camera`)
|
|
29
|
-
* and pointer/wheel/keyboard gesture bindings (`gestures`).
|
|
30
|
-
*
|
|
31
|
-
* Camera + gestures are **surface-scoped**: detaching the surface drops
|
|
32
|
-
* both. They never appear on the headless `SvgEditor`.
|
|
33
|
-
*/
|
|
34
|
-
type DomSurfaceHandle = SurfaceHandle & {
|
|
35
|
-
camera: Camera;
|
|
36
|
-
gestures: Gestures;
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Attach a DOM surface to a headless editor. Returns a `DomSurfaceHandle`
|
|
40
|
-
* whose `detach()` is the inverse — DOM cleared, listeners removed,
|
|
41
|
-
* gestures uninstalled.
|
|
42
|
-
*
|
|
43
|
-
* Usage is one-shot per container: the surface owns the container's children
|
|
44
|
-
* for its lifetime, and `detach()` restores it to empty.
|
|
45
|
-
*/
|
|
46
|
-
declare function attach_dom_surface(editor: SvgEditor, options: DomSurfaceOptions): DomSurfaceHandle;
|
|
47
|
-
//#endregion
|
|
1
|
+
import { n as DomSurfaceOptions, r as attach_dom_surface, t as DomSurfaceHandle } from "./dom-eIgcZ4JC.js";
|
|
48
2
|
export { DomSurfaceHandle, DomSurfaceOptions, attach_dom_surface };
|
package/dist/dom.js
CHANGED
package/dist/dom.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as attach_dom_surface } from "./dom-
|
|
1
|
+
import { t as attach_dom_surface } from "./dom-l5Y1Wf8C.mjs";
|
|
2
2
|
export { attach_dom_surface };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as apply_translate, l as is_text_input_focused, n as serialize_paint, o as capture_translate_baseline, t as parse_paint } from "./paint-
|
|
1
|
+
import { i as apply_translate, l as is_text_input_focused, n as serialize_paint, o as capture_translate_baseline, t as parse_paint } from "./paint-Cfiw4g_J.mjs";
|
|
2
2
|
import { HistoryImpl } from "@grida/history";
|
|
3
3
|
import { KeyCode, M, chunkKey, eventToChunk, getKeyboardOS, kb, keybindingsToKeyCodes } from "@grida/keybinding";
|
|
4
4
|
//#region src/commands/registry.ts
|
|
@@ -489,7 +489,7 @@ var GradientsRegistry = class {
|
|
|
489
489
|
};
|
|
490
490
|
}
|
|
491
491
|
write_gradient(node, def) {
|
|
492
|
-
for (const c of
|
|
492
|
+
for (const c of this.doc.children_of(node).slice()) this.doc.remove(c);
|
|
493
493
|
const set_num = (name, v) => {
|
|
494
494
|
this.doc.set_attr(node, name, v === void 0 ? null : String(v));
|
|
495
495
|
};
|
|
@@ -730,7 +730,6 @@ function parse_attrs(src, from) {
|
|
|
730
730
|
const c = src[i];
|
|
731
731
|
if (c === "/") {
|
|
732
732
|
if (src[i + 1] !== ">") throw new Error("expected '/>' at " + i);
|
|
733
|
-
pre + "";
|
|
734
733
|
return {
|
|
735
734
|
attrs,
|
|
736
735
|
end_index: i + 2,
|
|
@@ -1012,6 +1011,25 @@ var SvgDocument = class SvgDocument {
|
|
|
1012
1011
|
if (!style) return [];
|
|
1013
1012
|
return parse_inline_style(style);
|
|
1014
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Whether `id` can be opened in the flat-string text editor.
|
|
1016
|
+
*
|
|
1017
|
+
* v1 contract: the editor only operates on a *single flat text run*. That
|
|
1018
|
+
* means the target must be a `<text>` or `<tspan>` whose direct children
|
|
1019
|
+
* are all text nodes (or it has no children). A `<text>` containing a
|
|
1020
|
+
* `<tspan>` is *not* honestly editable — `text_of` would drop the tspan
|
|
1021
|
+
* content from the editor's view, and a flat-text write would leave the
|
|
1022
|
+
* tspan dangling. Tspan-as-target is fine and well-defined when it's a
|
|
1023
|
+
* leaf; only the host decides whether to route double-click to a tspan
|
|
1024
|
+
* or its parent text.
|
|
1025
|
+
*/
|
|
1026
|
+
is_text_edit_target(id) {
|
|
1027
|
+
const n = this.nodes.get(id);
|
|
1028
|
+
if (!n || n.kind !== "element") return false;
|
|
1029
|
+
if (n.local !== "text" && n.local !== "tspan") return false;
|
|
1030
|
+
for (const c of n.children) if (this.nodes.get(c)?.kind !== "text") return false;
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1015
1033
|
text_of(id) {
|
|
1016
1034
|
const n = this.nodes.get(id);
|
|
1017
1035
|
if (!n || n.kind !== "element") return "";
|
|
@@ -1318,9 +1336,16 @@ function createSvgEditor(opts) {
|
|
|
1318
1336
|
let doc_version = 0;
|
|
1319
1337
|
/** doc_version at the last load()/serialize(); compared to derive `dirty`. */
|
|
1320
1338
|
let baseline_doc_version = 0;
|
|
1339
|
+
/**
|
|
1340
|
+
* Bumps once per `editor.load(svg)` call. The constructor's initial parse
|
|
1341
|
+
* does NOT count — it's the "factory" state. Hosts subscribe via
|
|
1342
|
+
* `subscribe_with_selector(s => s.load_version, ...)` to react to fresh
|
|
1343
|
+
* document loads without firing on every edit.
|
|
1344
|
+
*/
|
|
1345
|
+
let load_version = 0;
|
|
1321
1346
|
let style = {
|
|
1322
1347
|
...DEFAULT_STYLE,
|
|
1323
|
-
...opts.style
|
|
1348
|
+
...opts.style
|
|
1324
1349
|
};
|
|
1325
1350
|
const providers = opts.providers ?? {};
|
|
1326
1351
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -1335,7 +1360,8 @@ function createSvgEditor(opts) {
|
|
|
1335
1360
|
can_undo: history.stack.canUndo,
|
|
1336
1361
|
can_redo: history.stack.canRedo,
|
|
1337
1362
|
version,
|
|
1338
|
-
structure_version: doc.structure_version
|
|
1363
|
+
structure_version: doc.structure_version,
|
|
1364
|
+
load_version
|
|
1339
1365
|
});
|
|
1340
1366
|
}
|
|
1341
1367
|
function emit() {
|
|
@@ -1372,11 +1398,16 @@ function createSvgEditor(opts) {
|
|
|
1372
1398
|
}
|
|
1373
1399
|
function select(target, opts) {
|
|
1374
1400
|
const ids = typeof target === "string" ? [target] : [...target];
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1401
|
+
const mode = opts?.mode ?? "replace";
|
|
1402
|
+
if (mode === "replace") {
|
|
1403
|
+
set_selection(ids);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const next = new Set(selection);
|
|
1407
|
+
if (mode === "add") for (const id of ids) next.add(id);
|
|
1408
|
+
else for (const id of ids) if (next.has(id)) next.delete(id);
|
|
1409
|
+
else next.add(id);
|
|
1410
|
+
set_selection([...next]);
|
|
1380
1411
|
}
|
|
1381
1412
|
function deselect() {
|
|
1382
1413
|
set_selection([]);
|
|
@@ -1589,7 +1620,7 @@ function createSvgEditor(opts) {
|
|
|
1589
1620
|
function set_text(value) {
|
|
1590
1621
|
if (selection.length !== 1) return;
|
|
1591
1622
|
const target = selection[0];
|
|
1592
|
-
if (doc.
|
|
1623
|
+
if (!doc.is_text_edit_target(target)) return;
|
|
1593
1624
|
const original = doc.text_of(target);
|
|
1594
1625
|
if (original === value) return;
|
|
1595
1626
|
const apply = () => {
|
|
@@ -1632,7 +1663,7 @@ function createSvgEditor(opts) {
|
|
|
1632
1663
|
function enter_content_edit(target) {
|
|
1633
1664
|
const id = target ?? (selection.length === 1 ? selection[0] : null);
|
|
1634
1665
|
if (!id) return false;
|
|
1635
|
-
if (doc.
|
|
1666
|
+
if (!doc.is_text_edit_target(id)) return false;
|
|
1636
1667
|
if (!content_edit_driver) return false;
|
|
1637
1668
|
return content_edit_driver(id);
|
|
1638
1669
|
}
|
|
@@ -1643,6 +1674,7 @@ function createSvgEditor(opts) {
|
|
|
1643
1674
|
mode = "select";
|
|
1644
1675
|
history.clear();
|
|
1645
1676
|
baseline_doc_version = doc_version;
|
|
1677
|
+
load_version++;
|
|
1646
1678
|
emit();
|
|
1647
1679
|
}
|
|
1648
1680
|
function serialize_svg() {
|