@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
|
@@ -158,6 +158,17 @@ type EditorState = {
|
|
|
158
158
|
* `structure_version` so a drag doesn't invalidate the tree view.
|
|
159
159
|
*/
|
|
160
160
|
readonly structure_version: number;
|
|
161
|
+
/**
|
|
162
|
+
* Bumps once per `editor.load(svg)` call. Distinct from
|
|
163
|
+
* `structure_version` (which bumps on edits too). Starts at 0; the
|
|
164
|
+
* constructor's initial SVG does NOT count as a load. Use this when
|
|
165
|
+
* you want to react to "a new document was loaded" — e.g. refit
|
|
166
|
+
* camera to the new root, reset host-side UI state, clear per-file
|
|
167
|
+
* scratch — without firing on text edits, reorders, or deletes.
|
|
168
|
+
*
|
|
169
|
+
* Monotonic, never resets.
|
|
170
|
+
*/
|
|
171
|
+
readonly load_version: number;
|
|
161
172
|
};
|
|
162
173
|
type Unsubscribe = () => void;
|
|
163
174
|
type ReorderDirection = "bring_forward" | "send_backward" | "bring_to_front" | "send_to_back";
|
|
@@ -186,6 +197,22 @@ type CameraOptions = {
|
|
|
186
197
|
resolve_bounds: BoundsResolver;
|
|
187
198
|
initial?: cmath.Transform;
|
|
188
199
|
};
|
|
200
|
+
/**
|
|
201
|
+
* Camera viewport constraint. Discriminated union with `type` so future
|
|
202
|
+
* variants (`'contain'`, `'pan-region'`) can be added without breaking
|
|
203
|
+
* existing call sites — each future variant has its own payload shape.
|
|
204
|
+
*
|
|
205
|
+
* v1.1 ships only `'cover'`. CSS analogy: `object-fit: cover` — the
|
|
206
|
+
* bounds rect covers the viewport edge-to-edge. Zoom is lower-bounded
|
|
207
|
+
* at fit-with-padding; pan is clamped so the bounds always covers the
|
|
208
|
+
* viewport. Use for slide / page / kiosk UX where the user should
|
|
209
|
+
* never see past the artwork.
|
|
210
|
+
*/
|
|
211
|
+
type CameraConstraints = {
|
|
212
|
+
/** Bounds cover viewport (viewport ⊆ bounds). Keynote / slide UX. */type: "cover"; /** World-space rect, or `"<root>"` to resolve via BoundsResolver. */
|
|
213
|
+
bounds: Rect | "<root>"; /** Screen-pixel breathing room between bounds and viewport edge. */
|
|
214
|
+
padding?: number;
|
|
215
|
+
};
|
|
189
216
|
/**
|
|
190
217
|
* Surface-scoped pan/zoom state.
|
|
191
218
|
*
|
|
@@ -199,7 +226,19 @@ declare class Camera {
|
|
|
199
226
|
private viewport_h;
|
|
200
227
|
private listeners;
|
|
201
228
|
private resolve_bounds;
|
|
229
|
+
private _constraints;
|
|
202
230
|
constructor(opts: CameraOptions);
|
|
231
|
+
/**
|
|
232
|
+
* Current viewport constraint, or `null` for free pan/zoom. Set with
|
|
233
|
+
* `camera.constraints = { type: 'cover', bounds: '<root>', padding: 80 }`
|
|
234
|
+
* to clamp zoom + pan; assign `null` to clear.
|
|
235
|
+
*
|
|
236
|
+
* Constraints are applied synchronously inside `set_transform` (and
|
|
237
|
+
* `_set_viewport_size`), so every public mutation respects them
|
|
238
|
+
* automatically — the host never needs to subscribe-and-clamp itself.
|
|
239
|
+
*/
|
|
240
|
+
get constraints(): CameraConstraints | null;
|
|
241
|
+
set constraints(c: CameraConstraints | null);
|
|
203
242
|
/** Underlying 2D affine. World→screen. */
|
|
204
243
|
get transform(): cmath.Transform;
|
|
205
244
|
/** Uniform scale factor. 1 = 100 %. */
|
|
@@ -227,6 +266,10 @@ declare class Camera {
|
|
|
227
266
|
* makes external constraint loops (e.g. "subscribe → compute clamped →
|
|
228
267
|
* set_transform") terminate: the clamp re-emits the same transform on
|
|
229
268
|
* the second pass, set_transform short-circuits, no recursion.
|
|
269
|
+
*
|
|
270
|
+
* When `camera.constraints` is non-null, the input transform is clamped
|
|
271
|
+
* synchronously before being stored — every public mutation respects the
|
|
272
|
+
* constraint automatically.
|
|
230
273
|
*/
|
|
231
274
|
set_transform(t: cmath.Transform): void;
|
|
232
275
|
/** Viewport size in screen pixels. Read by host code computing constraints. */
|
|
@@ -262,114 +305,20 @@ declare class Camera {
|
|
|
262
305
|
screen_to_world(p: Vec2): Vec2;
|
|
263
306
|
/** Convert a world-space point to screen-space. */
|
|
264
307
|
world_to_screen(p: Vec2): Vec2;
|
|
265
|
-
private notify;
|
|
266
|
-
}
|
|
267
|
-
//#endregion
|
|
268
|
-
//#region src/commands/registry.d.ts
|
|
269
|
-
/**
|
|
270
|
-
* Command registry.
|
|
271
|
-
*
|
|
272
|
-
* A passive id-keyed registry of handlers. Built so that:
|
|
273
|
-
*
|
|
274
|
-
* - keybindings (in `src/keymap`) can address commands by stable id;
|
|
275
|
-
* - new commands can be added in ONE place (`src/commands/defaults.ts`)
|
|
276
|
-
* without growing the public surface of the editor;
|
|
277
|
-
* - "one key, many meanings" can be expressed via chain semantics: a
|
|
278
|
-
* handler returns `true` if it consumed, `false`/`void` otherwise,
|
|
279
|
-
* and the dispatcher tries the next candidate in the chain.
|
|
280
|
-
*
|
|
281
|
-
* Handlers are plain closures — they capture whatever editor reference
|
|
282
|
-
* they need. The registry itself stays unaware of the editor's type,
|
|
283
|
-
* which avoids a circular type dependency between editor and registry.
|
|
284
|
-
*/
|
|
285
|
-
/** Stable, dotted id for a command, e.g. `"history.undo"`. */
|
|
286
|
-
type CommandId = string;
|
|
287
|
-
/**
|
|
288
|
-
* A command handler.
|
|
289
|
-
*
|
|
290
|
-
* Return `true` if the handler consumed the invocation. Return `false`
|
|
291
|
-
* or `undefined` to signal "did not apply" — the dispatcher will try
|
|
292
|
-
* the next candidate registered for the same key.
|
|
293
|
-
*
|
|
294
|
-
* Handlers are closures: they capture their editor reference. No
|
|
295
|
-
* editor parameter is passed — keep handlers self-contained.
|
|
296
|
-
*/
|
|
297
|
-
type CommandHandler = (args?: unknown) => boolean | void;
|
|
298
|
-
declare class CommandRegistry {
|
|
299
|
-
private readonly map;
|
|
300
308
|
/**
|
|
301
|
-
*
|
|
302
|
-
*
|
|
309
|
+
* Apply the current constraint (if any) to a candidate transform.
|
|
310
|
+
* Pure: returns the clamped result, never mutates state. Returns the
|
|
311
|
+
* input unchanged when constraints are null / bounds are unresolvable /
|
|
312
|
+
* viewport is 0.
|
|
303
313
|
*/
|
|
304
|
-
|
|
314
|
+
private apply_constraints;
|
|
305
315
|
/**
|
|
306
|
-
*
|
|
307
|
-
* `
|
|
308
|
-
*
|
|
316
|
+
* Re-clamp the stored transform against the current constraint. Called
|
|
317
|
+
* from the `constraints` setter; `_set_viewport_size` has its own
|
|
318
|
+
* notify-inclusive path.
|
|
309
319
|
*/
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
/** All registered ids, for debugging / introspection. */
|
|
313
|
-
ids(): readonly CommandId[];
|
|
314
|
-
}
|
|
315
|
-
//#endregion
|
|
316
|
-
//#region src/keymap/keymap.d.ts
|
|
317
|
-
type KeymapBinding = {
|
|
318
|
-
/** Declarative key combination. Build with `kb()` / `c()` / `seq()`. */keybinding: Keybinding; /** Command id to invoke on match. */
|
|
319
|
-
command: CommandId; /** Forwarded as the `args` parameter to the command handler. */
|
|
320
|
-
args?: unknown; /** Higher priorities run first in the chain. Default 0. */
|
|
321
|
-
priority?: number;
|
|
322
|
-
/**
|
|
323
|
-
* Reserved for V2; not honored by the V1 dispatcher. When added, this
|
|
324
|
-
* will be evaluated before the handler runs; if false, the binding is
|
|
325
|
-
* skipped without invoking the handler.
|
|
326
|
-
*/
|
|
327
|
-
when?: (ctx: unknown) => boolean;
|
|
328
|
-
};
|
|
329
|
-
declare class Keymap {
|
|
330
|
-
private readonly commands;
|
|
331
|
-
private readonly platformGetter;
|
|
332
|
-
/**
|
|
333
|
-
* Bindings bucketed by canonical chunk-key hash, computed per
|
|
334
|
-
* `@grida/keybinding`'s `chunkKey`. Each list is the chain for that
|
|
335
|
-
* key, sorted in dispatch order (priority desc, then registration
|
|
336
|
-
* order).
|
|
337
|
-
*/
|
|
338
|
-
private readonly buckets;
|
|
339
|
-
/** Insert order, so ties on priority are deterministic. */
|
|
340
|
-
private seq;
|
|
341
|
-
constructor(commands: CommandRegistry, platformGetter?: () => Platform);
|
|
342
|
-
/**
|
|
343
|
-
* Bind a key combination to a command. Returns an unbind function.
|
|
344
|
-
* The same `Keybinding` can be bound to multiple commands — they will
|
|
345
|
-
* all be tried in chain order on dispatch.
|
|
346
|
-
*/
|
|
347
|
-
bind(binding: KeymapBinding): () => void;
|
|
348
|
-
/**
|
|
349
|
-
* Remove bindings matching the spec. If both filters are passed, only
|
|
350
|
-
* bindings that match BOTH are removed.
|
|
351
|
-
*/
|
|
352
|
-
unbind(spec: {
|
|
353
|
-
keybinding?: Keybinding;
|
|
354
|
-
command?: CommandId;
|
|
355
|
-
}): void;
|
|
356
|
-
/** All registered bindings, for introspection. Order is not guaranteed. */
|
|
357
|
-
bindings(): readonly KeymapBinding[];
|
|
358
|
-
/**
|
|
359
|
-
* Match the event against bound chunks, then run candidates in chain
|
|
360
|
-
* order. Returns `true` and calls `preventDefault()` on the first
|
|
361
|
-
* handler that consumes; returns `false` if nothing matched or all
|
|
362
|
-
* matches fell through.
|
|
363
|
-
*/
|
|
364
|
-
dispatch(event: KeyboardEvent): boolean;
|
|
365
|
-
/**
|
|
366
|
-
* Compute the set of canonical hashes a `Keybinding` lights up. A
|
|
367
|
-
* binding with aliases (multiple sequences) contributes one hash per
|
|
368
|
-
* single-chunk alias; multi-chunk sequences (chords) are skipped in
|
|
369
|
-
* V1.
|
|
370
|
-
*/
|
|
371
|
-
private chunkKeysFor;
|
|
372
|
-
private has_safe_mod;
|
|
320
|
+
private reenforce;
|
|
321
|
+
private notify;
|
|
373
322
|
}
|
|
374
323
|
//#endregion
|
|
375
324
|
//#region src/core/parser.d.ts
|
|
@@ -501,6 +450,19 @@ declare class SvgDocument implements DocumentEvents {
|
|
|
501
450
|
property: string;
|
|
502
451
|
value: string;
|
|
503
452
|
}>;
|
|
453
|
+
/**
|
|
454
|
+
* Whether `id` can be opened in the flat-string text editor.
|
|
455
|
+
*
|
|
456
|
+
* v1 contract: the editor only operates on a *single flat text run*. That
|
|
457
|
+
* means the target must be a `<text>` or `<tspan>` whose direct children
|
|
458
|
+
* are all text nodes (or it has no children). A `<text>` containing a
|
|
459
|
+
* `<tspan>` is *not* honestly editable — `text_of` would drop the tspan
|
|
460
|
+
* content from the editor's view, and a flat-text write would leave the
|
|
461
|
+
* tspan dangling. Tspan-as-target is fine and well-defined when it's a
|
|
462
|
+
* leaf; only the host decides whether to route double-click to a tspan
|
|
463
|
+
* or its parent text.
|
|
464
|
+
*/
|
|
465
|
+
is_text_edit_target(id: NodeId): boolean;
|
|
504
466
|
text_of(id: NodeId): string;
|
|
505
467
|
/** Replace all direct text children with a single text node carrying `value`. */
|
|
506
468
|
set_text(id: NodeId, value: string): void;
|
|
@@ -530,6 +492,113 @@ type Defs = {
|
|
|
530
492
|
gradients: GradientsApi;
|
|
531
493
|
};
|
|
532
494
|
//#endregion
|
|
495
|
+
//#region src/commands/registry.d.ts
|
|
496
|
+
/**
|
|
497
|
+
* Command registry.
|
|
498
|
+
*
|
|
499
|
+
* A passive id-keyed registry of handlers. Built so that:
|
|
500
|
+
*
|
|
501
|
+
* - keybindings (in `src/keymap`) can address commands by stable id;
|
|
502
|
+
* - new commands can be added in ONE place (`src/commands/defaults.ts`)
|
|
503
|
+
* without growing the public surface of the editor;
|
|
504
|
+
* - "one key, many meanings" can be expressed via chain semantics: a
|
|
505
|
+
* handler returns `true` if it consumed, `false`/`void` otherwise,
|
|
506
|
+
* and the dispatcher tries the next candidate in the chain.
|
|
507
|
+
*
|
|
508
|
+
* Handlers are plain closures — they capture whatever editor reference
|
|
509
|
+
* they need. The registry itself stays unaware of the editor's type,
|
|
510
|
+
* which avoids a circular type dependency between editor and registry.
|
|
511
|
+
*/
|
|
512
|
+
/** Stable, dotted id for a command, e.g. `"history.undo"`. */
|
|
513
|
+
type CommandId = string;
|
|
514
|
+
/**
|
|
515
|
+
* A command handler.
|
|
516
|
+
*
|
|
517
|
+
* Return `true` if the handler consumed the invocation. Return `false`
|
|
518
|
+
* or `undefined` to signal "did not apply" — the dispatcher will try
|
|
519
|
+
* the next candidate registered for the same key.
|
|
520
|
+
*
|
|
521
|
+
* Handlers are closures: they capture their editor reference. No
|
|
522
|
+
* editor parameter is passed — keep handlers self-contained.
|
|
523
|
+
*/
|
|
524
|
+
type CommandHandler = (args?: unknown) => boolean | void;
|
|
525
|
+
declare class CommandRegistry {
|
|
526
|
+
private readonly map;
|
|
527
|
+
/**
|
|
528
|
+
* Register a command. Returns an unregister function. Re-registering
|
|
529
|
+
* the same id replaces the previous handler (last writer wins).
|
|
530
|
+
*/
|
|
531
|
+
register(id: CommandId, handler: CommandHandler): () => void;
|
|
532
|
+
/**
|
|
533
|
+
* Invoke a command by id. Returns `true` if the handler consumed,
|
|
534
|
+
* `false` otherwise (including unknown ids and handlers that returned
|
|
535
|
+
* `false`/`undefined`).
|
|
536
|
+
*/
|
|
537
|
+
invoke(id: CommandId, args?: unknown): boolean;
|
|
538
|
+
has(id: CommandId): boolean;
|
|
539
|
+
/** All registered ids, for debugging / introspection. */
|
|
540
|
+
ids(): readonly CommandId[];
|
|
541
|
+
}
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/keymap/keymap.d.ts
|
|
544
|
+
type KeymapBinding = {
|
|
545
|
+
/** Declarative key combination. Build with `kb()` / `c()` / `seq()`. */keybinding: Keybinding; /** Command id to invoke on match. */
|
|
546
|
+
command: CommandId; /** Forwarded as the `args` parameter to the command handler. */
|
|
547
|
+
args?: unknown; /** Higher priorities run first in the chain. Default 0. */
|
|
548
|
+
priority?: number;
|
|
549
|
+
/**
|
|
550
|
+
* Reserved for V2; not honored by the V1 dispatcher. When added, this
|
|
551
|
+
* will be evaluated before the handler runs; if false, the binding is
|
|
552
|
+
* skipped without invoking the handler.
|
|
553
|
+
*/
|
|
554
|
+
when?: (ctx: unknown) => boolean;
|
|
555
|
+
};
|
|
556
|
+
declare class Keymap {
|
|
557
|
+
private readonly commands;
|
|
558
|
+
private readonly platformGetter;
|
|
559
|
+
/**
|
|
560
|
+
* Bindings bucketed by canonical chunk-key hash, computed per
|
|
561
|
+
* `@grida/keybinding`'s `chunkKey`. Each list is the chain for that
|
|
562
|
+
* key, sorted in dispatch order (priority desc, then registration
|
|
563
|
+
* order).
|
|
564
|
+
*/
|
|
565
|
+
private readonly buckets;
|
|
566
|
+
/** Insert order, so ties on priority are deterministic. */
|
|
567
|
+
private seq;
|
|
568
|
+
constructor(commands: CommandRegistry, platformGetter?: () => Platform);
|
|
569
|
+
/**
|
|
570
|
+
* Bind a key combination to a command. Returns an unbind function.
|
|
571
|
+
* The same `Keybinding` can be bound to multiple commands — they will
|
|
572
|
+
* all be tried in chain order on dispatch.
|
|
573
|
+
*/
|
|
574
|
+
bind(binding: KeymapBinding): () => void;
|
|
575
|
+
/**
|
|
576
|
+
* Remove bindings matching the spec. If both filters are passed, only
|
|
577
|
+
* bindings that match BOTH are removed.
|
|
578
|
+
*/
|
|
579
|
+
unbind(spec: {
|
|
580
|
+
keybinding?: Keybinding;
|
|
581
|
+
command?: CommandId;
|
|
582
|
+
}): void;
|
|
583
|
+
/** All registered bindings, for introspection. Order is not guaranteed. */
|
|
584
|
+
bindings(): readonly KeymapBinding[];
|
|
585
|
+
/**
|
|
586
|
+
* Match the event against bound chunks, then run candidates in chain
|
|
587
|
+
* order. Returns `true` and calls `preventDefault()` on the first
|
|
588
|
+
* handler that consumes; returns `false` if nothing matched or all
|
|
589
|
+
* matches fell through.
|
|
590
|
+
*/
|
|
591
|
+
dispatch(event: KeyboardEvent): boolean;
|
|
592
|
+
/**
|
|
593
|
+
* Compute the set of canonical hashes a `Keybinding` lights up. A
|
|
594
|
+
* binding with aliases (multiple sequences) contributes one hash per
|
|
595
|
+
* single-chunk alias; multi-chunk sequences (chords) are skipped in
|
|
596
|
+
* V1.
|
|
597
|
+
*/
|
|
598
|
+
private chunkKeysFor;
|
|
599
|
+
private has_safe_mod;
|
|
600
|
+
}
|
|
601
|
+
//#endregion
|
|
533
602
|
//#region src/core/properties.d.ts
|
|
534
603
|
declare function read_property(doc: SvgDocument, id: NodeId, property: string): PropertyValue;
|
|
535
604
|
//#endregion
|
|
@@ -619,9 +688,18 @@ type Surface = {
|
|
|
619
688
|
type SurfaceHandle = {
|
|
620
689
|
detach(): void;
|
|
621
690
|
};
|
|
691
|
+
/**
|
|
692
|
+
* Mode for `commands.select`. Matches the HUD's `SelectMode` vocabulary so
|
|
693
|
+
* intents can be threaded through without lossy collapsing.
|
|
694
|
+
*
|
|
695
|
+
* - `"replace"` (default) — set selection to `ids`, discarding the previous set.
|
|
696
|
+
* - `"add"` — union: each id in `ids` is added; existing members stay.
|
|
697
|
+
* - `"toggle"` — flip each id's membership (present → removed; absent → added).
|
|
698
|
+
*/
|
|
699
|
+
type SelectMode = "replace" | "add" | "toggle";
|
|
622
700
|
type Commands = {
|
|
623
701
|
select(target: NodeId | ReadonlyArray<NodeId>, opts?: {
|
|
624
|
-
|
|
702
|
+
mode?: SelectMode;
|
|
625
703
|
}): void;
|
|
626
704
|
deselect(): void;
|
|
627
705
|
enter_scope(group: NodeId): void;
|
|
@@ -759,4 +837,4 @@ declare function createSvgEditor(opts: CreateSvgEditorOptions): {
|
|
|
759
837
|
keymap: Keymap;
|
|
760
838
|
};
|
|
761
839
|
//#endregion
|
|
762
|
-
export {
|
|
840
|
+
export { InvalidComputedValue as A, Provenance as B, EditorState as C, GradientDefinition as D, FontResolver as E, PaintFallback as F, Unsubscribe as G, RadialGradientDefinition as H, PaintPreviewSession as I, Vec2 as K, PaintValue as L, Mode as M, NodeId as N, GradientEntry as O, Paint as P, PreviewSession as R, DEFAULT_STYLE as S, FileIOProvider as T, Rect as U, Providers as V, ReorderDirection as W, Camera as _, SelectMode as a, ClipboardProvider as b, createSvgEditor as c, GestureId as d, Gestures as f, BoundsResolver as g, CommandId as h, DomComputedResolver as i, LinearGradientDefinition as j, GradientStop as k, GestureBinding as l, CommandHandler as m, CreateSvgEditorOptions as n, SurfaceHandle as o, KeymapBinding as p, DomComputedPaint as r, SvgEditor as s, Commands as t, GestureContext as u, CameraConstraints as v, EditorStyle as w, Color as x, CameraOptions as y, PropertyValue as z };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require("./dom-
|
|
2
|
-
const require_paint = require("./paint-
|
|
1
|
+
require("./dom-DyJy1H6Q.js");
|
|
2
|
+
const require_paint = require("./paint-cTjePy5e.js");
|
|
3
3
|
let _grida_history = require("@grida/history");
|
|
4
4
|
let _grida_keybinding = require("@grida/keybinding");
|
|
5
5
|
//#region src/commands/registry.ts
|
|
@@ -490,7 +490,7 @@ var GradientsRegistry = class {
|
|
|
490
490
|
};
|
|
491
491
|
}
|
|
492
492
|
write_gradient(node, def) {
|
|
493
|
-
for (const c of
|
|
493
|
+
for (const c of this.doc.children_of(node).slice()) this.doc.remove(c);
|
|
494
494
|
const set_num = (name, v) => {
|
|
495
495
|
this.doc.set_attr(node, name, v === void 0 ? null : String(v));
|
|
496
496
|
};
|
|
@@ -731,7 +731,6 @@ function parse_attrs(src, from) {
|
|
|
731
731
|
const c = src[i];
|
|
732
732
|
if (c === "/") {
|
|
733
733
|
if (src[i + 1] !== ">") throw new Error("expected '/>' at " + i);
|
|
734
|
-
pre + "";
|
|
735
734
|
return {
|
|
736
735
|
attrs,
|
|
737
736
|
end_index: i + 2,
|
|
@@ -1013,6 +1012,25 @@ var SvgDocument = class SvgDocument {
|
|
|
1013
1012
|
if (!style) return [];
|
|
1014
1013
|
return parse_inline_style(style);
|
|
1015
1014
|
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Whether `id` can be opened in the flat-string text editor.
|
|
1017
|
+
*
|
|
1018
|
+
* v1 contract: the editor only operates on a *single flat text run*. That
|
|
1019
|
+
* means the target must be a `<text>` or `<tspan>` whose direct children
|
|
1020
|
+
* are all text nodes (or it has no children). A `<text>` containing a
|
|
1021
|
+
* `<tspan>` is *not* honestly editable — `text_of` would drop the tspan
|
|
1022
|
+
* content from the editor's view, and a flat-text write would leave the
|
|
1023
|
+
* tspan dangling. Tspan-as-target is fine and well-defined when it's a
|
|
1024
|
+
* leaf; only the host decides whether to route double-click to a tspan
|
|
1025
|
+
* or its parent text.
|
|
1026
|
+
*/
|
|
1027
|
+
is_text_edit_target(id) {
|
|
1028
|
+
const n = this.nodes.get(id);
|
|
1029
|
+
if (!n || n.kind !== "element") return false;
|
|
1030
|
+
if (n.local !== "text" && n.local !== "tspan") return false;
|
|
1031
|
+
for (const c of n.children) if (this.nodes.get(c)?.kind !== "text") return false;
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1016
1034
|
text_of(id) {
|
|
1017
1035
|
const n = this.nodes.get(id);
|
|
1018
1036
|
if (!n || n.kind !== "element") return "";
|
|
@@ -1319,9 +1337,16 @@ function createSvgEditor(opts) {
|
|
|
1319
1337
|
let doc_version = 0;
|
|
1320
1338
|
/** doc_version at the last load()/serialize(); compared to derive `dirty`. */
|
|
1321
1339
|
let baseline_doc_version = 0;
|
|
1340
|
+
/**
|
|
1341
|
+
* Bumps once per `editor.load(svg)` call. The constructor's initial parse
|
|
1342
|
+
* does NOT count — it's the "factory" state. Hosts subscribe via
|
|
1343
|
+
* `subscribe_with_selector(s => s.load_version, ...)` to react to fresh
|
|
1344
|
+
* document loads without firing on every edit.
|
|
1345
|
+
*/
|
|
1346
|
+
let load_version = 0;
|
|
1322
1347
|
let style = {
|
|
1323
1348
|
...DEFAULT_STYLE,
|
|
1324
|
-
...opts.style
|
|
1349
|
+
...opts.style
|
|
1325
1350
|
};
|
|
1326
1351
|
const providers = opts.providers ?? {};
|
|
1327
1352
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -1336,7 +1361,8 @@ function createSvgEditor(opts) {
|
|
|
1336
1361
|
can_undo: history.stack.canUndo,
|
|
1337
1362
|
can_redo: history.stack.canRedo,
|
|
1338
1363
|
version,
|
|
1339
|
-
structure_version: doc.structure_version
|
|
1364
|
+
structure_version: doc.structure_version,
|
|
1365
|
+
load_version
|
|
1340
1366
|
});
|
|
1341
1367
|
}
|
|
1342
1368
|
function emit() {
|
|
@@ -1373,11 +1399,16 @@ function createSvgEditor(opts) {
|
|
|
1373
1399
|
}
|
|
1374
1400
|
function select(target, opts) {
|
|
1375
1401
|
const ids = typeof target === "string" ? [target] : [...target];
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
}
|
|
1402
|
+
const mode = opts?.mode ?? "replace";
|
|
1403
|
+
if (mode === "replace") {
|
|
1404
|
+
set_selection(ids);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const next = new Set(selection);
|
|
1408
|
+
if (mode === "add") for (const id of ids) next.add(id);
|
|
1409
|
+
else for (const id of ids) if (next.has(id)) next.delete(id);
|
|
1410
|
+
else next.add(id);
|
|
1411
|
+
set_selection([...next]);
|
|
1381
1412
|
}
|
|
1382
1413
|
function deselect() {
|
|
1383
1414
|
set_selection([]);
|
|
@@ -1590,7 +1621,7 @@ function createSvgEditor(opts) {
|
|
|
1590
1621
|
function set_text(value) {
|
|
1591
1622
|
if (selection.length !== 1) return;
|
|
1592
1623
|
const target = selection[0];
|
|
1593
|
-
if (doc.
|
|
1624
|
+
if (!doc.is_text_edit_target(target)) return;
|
|
1594
1625
|
const original = doc.text_of(target);
|
|
1595
1626
|
if (original === value) return;
|
|
1596
1627
|
const apply = () => {
|
|
@@ -1633,7 +1664,7 @@ function createSvgEditor(opts) {
|
|
|
1633
1664
|
function enter_content_edit(target) {
|
|
1634
1665
|
const id = target ?? (selection.length === 1 ? selection[0] : null);
|
|
1635
1666
|
if (!id) return false;
|
|
1636
|
-
if (doc.
|
|
1667
|
+
if (!doc.is_text_edit_target(id)) return false;
|
|
1637
1668
|
if (!content_edit_driver) return false;
|
|
1638
1669
|
return content_edit_driver(id);
|
|
1639
1670
|
}
|
|
@@ -1644,6 +1675,7 @@ function createSvgEditor(opts) {
|
|
|
1644
1675
|
mode = "select";
|
|
1645
1676
|
history.clear();
|
|
1646
1677
|
baseline_doc_version = doc_version;
|
|
1678
|
+
load_version++;
|
|
1647
1679
|
emit();
|
|
1648
1680
|
}
|
|
1649
1681
|
function serialize_svg() {
|