visio-svelte-viewer 0.8.0 → 1.0.1

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 CHANGED
@@ -45,22 +45,34 @@ npm install visio-svelte-viewer
45
45
 
46
46
  The shared properties are `document` (a parsed `VisioDocument`, or `null` to clear),
47
47
  `pageIndex` (zero-based), `zoom` (`1` means 100%), `showToolbar`, `ribbonAddIns` (tabs the host adds
48
- after Help) and `events`.
49
- The callback map uses `document-load`, `document-change`, `document-error`,
50
- `page-change`, `zoom-change` and `shape-select`. File loading uses the imperative
48
+ after Help), `canEdit` (editing permission; the viewer is editable unless you pass `false`),
49
+ `collaboration` (shares the open drawing in a room while set; `null` leaves it) and `events`.
50
+ The callback map uses `document-load`, `document-change` (`{ document, dirty, kind }` with kind
51
+ `edit`, `undo`, `redo`, `remote` or `save`), `document-error`, `page-change`, `zoom-change`,
52
+ `shape-select`, `selection-change`, `office-ribbon-add-in` and `collaboration-change`. File loading uses the imperative
51
53
  `load(File | Blob | Uint8Array | ArrayBuffer)` handle; `document` is not a file URL.
52
54
  Provide a height for the viewer host. Mount and load files in the browser.
53
55
 
54
56
  The handle supports `fit()`, `replacePlainText(pageId, shapeId, text)`,
55
57
  `applyEdits(edits)`, `undo()`, `redo()` and `exportVsdx()`.
56
58
  `exportVsdx()` returns `{ bytes, dirty, diagnostics }`; saving or downloading those
57
- bytes is the application's responsibility. Load original bytes through `load()`
59
+ bytes is the application's responsibility, and `handle.controller.markClean()` declares them saved.
60
+ Sharing state is on the handle's element: `collaborationState`, `reconnectCollaboration()` and
61
+ `stopCollaboration()`. Load original bytes through `load()`
58
62
  to enable source-backed editing and export. Handles are usable after mount.
59
63
 
60
64
  Every viewer package includes the same browser UI and re-exports the
61
65
  `visio-core` document API. Parsing and editing belong to `ooxml-core/visio`;
62
66
  framework bindings only manage lifecycle, properties and events.
63
67
 
68
+ ## Compose your own UI
69
+
70
+ To build a custom diagram UI instead of mounting the whole viewer, use the building blocks: `createViewerBuildingBlocks`, `DiagramCanvas`, `Toolbar`, `PageNavigation`, `Inspector` and `ShapesPane`. The factory returns readable stores
71
+ for the flat `toolbarProps` (page, zoom, selection, search, enabled flags and callbacks), `mode`,
72
+ `loading`/`error` and `markClean()`. Hosts pass `canEdit` (read-only until `true`) and optional
73
+ `onOpenSettings`/`onOpenShareDialog`/`onOpenPageSetup`/`onOpenFile` callbacks for dialogs they own.
74
+ See [composable building blocks](https://github.com/ChristopherVR/ooxml/blob/main/viewers/visio/docs/building-blocks.md).
75
+
64
76
  ## Limitations
65
77
 
66
78
  Rendering and editing are beta features. Supported text and geometry edits are
@@ -70,7 +82,7 @@ Files stay in the browser unless your application sends them elsewhere.
70
82
 
71
83
  ## Documentation
72
84
 
73
- [Viewer guide](https://christophervr.github.io/ooxml/visio/docs/) |
85
+ [Viewer guide](https://christophervr.github.io/ooxml/visio/) |
74
86
  [Demo](https://christophervr.github.io/ooxml/visio/demo/) |
75
87
  [Source](https://github.com/ChristopherVR/ooxml/tree/main/viewers/visio)
76
88
 
@@ -1,4 +1,4 @@
1
1
  import type { Component } from 'svelte';
2
- import type { ViewerController, DiagramToolbarProps } from './runtime.js';
2
+ import type { ViewerController, DiagramToolbarProps, ToolbarGroupId } from './runtime.js';
3
3
  declare const component: Component<{ mount(node: HTMLElement | null): void; class?: string }>;
4
4
  export default component;
@@ -1,4 +1,4 @@
1
1
  import type { Component } from 'svelte';
2
- import type { ViewerController, DiagramToolbarProps } from './runtime.js';
2
+ import type { ViewerController, DiagramToolbarProps, ToolbarGroupId } from './runtime.js';
3
3
  declare const component: Component<{ controller: ViewerController }>;
4
4
  export default component;
@@ -1,4 +1,4 @@
1
1
  import type { Component } from 'svelte';
2
- import type { ViewerController, DiagramToolbarProps } from './runtime.js';
2
+ import type { ViewerController, DiagramToolbarProps, ToolbarGroupId } from './runtime.js';
3
3
  declare const component: Component<{ toolbarProps: DiagramToolbarProps }>;
4
4
  export default component;
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { buildingBlock, type ViewerController } from './runtime.js';
3
+ let { controller, locale }: { controller: ViewerController; locale?: string | undefined } = $props();
4
+ let host: HTMLDivElement;
5
+ $effect(() => buildingBlock('mountDiagramShapes')(host, controller, { locale }));
6
+ </script>
7
+ <div bind:this={host}></div>
@@ -0,0 +1,4 @@
1
+ import type { Component } from 'svelte';
2
+ import type { ViewerController, DiagramToolbarProps, ToolbarGroupId } from './runtime.js';
3
+ declare const component: Component<{ controller: ViewerController; locale?: string | undefined }>;
4
+ export default component;
@@ -1,11 +1,28 @@
1
1
  <script lang="ts">
2
2
  import { prepareDiagramBlock } from './runtime.js';
3
+ import { toolbarGroups, type ToolbarGroupId } from './runtime.js';
3
4
  function theme(host: HTMLElement) { prepareDiagramBlock(host, 'toolbar'); }
4
5
  import type { DiagramToolbarProps } from './runtime.js';
5
- let { toolbarProps }: { toolbarProps: DiagramToolbarProps } = $props();
6
+ let { toolbarProps, include }: { toolbarProps: DiagramToolbarProps; include?: readonly ToolbarGroupId[] } = $props();
7
+ const groups = $derived(toolbarGroups(toolbarProps, include));
6
8
  </script>
7
9
  <div role="toolbar" use:theme>
8
- <button type="button" disabled={!toolbarProps.canUndo} onclick={() => void toolbarProps.onUndo()}>{toolbarProps.labels.Undo}</button>
9
- <button type="button" disabled={!toolbarProps.canRedo} onclick={() => void toolbarProps.onRedo()}>{toolbarProps.labels.Redo}</button>
10
- <button type="button" onclick={toolbarProps.onFit}>{toolbarProps.labels['Fit to Window']}</button>
10
+ {#each groups as group (group.id)}
11
+ <span data-group={group.id}>
12
+ {#each group.controls as control (control.id)}
13
+ {#if control.kind === 'text'}
14
+ <span data-control={control.id}>{control.text}</span>
15
+ {:else if control.kind === 'button'}
16
+ <button type="button" data-control={control.id} disabled={control.disabled} aria-pressed={control.pressed} onclick={() => control.run()}>{control.label}</button>
17
+ {:else}
18
+ <span data-control="search">
19
+ <input type="search" aria-label={control.label} placeholder={control.label} value={control.query} oninput={(event) => control.change(event.currentTarget.value)} />
20
+ <span aria-live="polite">{control.summary}</span>
21
+ <button type="button" data-control={control.previous.id} disabled={control.previous.disabled} onclick={() => control.previous.run()}>{control.previous.label}</button>
22
+ <button type="button" data-control={control.next.id} disabled={control.next.disabled} onclick={() => control.next.run()}>{control.next.label}</button>
23
+ </span>
24
+ {/if}
25
+ {/each}
26
+ </span>
27
+ {/each}
11
28
  </div>
@@ -1,4 +1,4 @@
1
1
  import type { Component } from 'svelte';
2
- import type { ViewerController, DiagramToolbarProps } from './runtime.js';
3
- declare const component: Component<{ toolbarProps: DiagramToolbarProps }>;
2
+ import type { ViewerController, DiagramToolbarProps, ToolbarGroupId } from './runtime.js';
3
+ declare const component: Component<{ toolbarProps: DiagramToolbarProps; include?: readonly ToolbarGroupId[] }>;
4
4
  export default component;
@@ -3,8 +3,8 @@
3
3
  import { readonly, writable } from 'svelte/store';
4
4
  import { mountFrameworkViewer, viewerHandle, viewerOptions, viewerStateSource, type ViewerState, type MountedViewer, type ViewerProps, type VsdxSource, type VisioEdit, type VisioShapeSelection, type SvgExportOptions, type CurrentPagePrintSnapshotOptions } from './runtime.js';
5
5
  type Props = ViewerProps & { class?: string; style?: string; 'aria-label'?: string };
6
- let { document, pageIndex, zoom, showToolbar, canEdit, ribbonAddIns, events, class: className, style, 'aria-label': ariaLabel }: Props = $props();
7
- const options = $derived({ document, pageIndex, zoom, showToolbar, canEdit, ribbonAddIns, events } satisfies Required<ViewerProps>);
6
+ let { document, pageIndex, zoom, showToolbar, canEdit, ribbonAddIns, collaboration, events, class: className, style, 'aria-label': ariaLabel }: Props = $props();
7
+ const options = $derived({ document, pageIndex, zoom, showToolbar, canEdit, ribbonAddIns, collaboration, events } satisfies Required<ViewerProps>);
8
8
  let binding: MountedViewer | undefined;
9
9
  const handle = viewerHandle(() => binding);
10
10
  // The component owns its state store, so subscribers made before mount still see updates.
package/dist/index.d.ts CHANGED
@@ -6,3 +6,4 @@ export { default as DiagramCanvas } from './DiagramCanvas.svelte';
6
6
  export { default as Toolbar } from './Toolbar.svelte';
7
7
  export { default as PageNavigation } from './PageNavigation.svelte';
8
8
  export { default as Inspector } from './Inspector.svelte';
9
+ export { default as ShapesPane } from './ShapesPane.svelte';
package/dist/index.js CHANGED
@@ -149,7 +149,7 @@ function viewerStateSource(current) {
149
149
  }
150
150
  //#endregion
151
151
  //#region packages/bindings/src/VisioViewer.svelte
152
- var root$4 = $.from_html(`<div></div>`);
152
+ var root$5 = $.from_html(`<div></div>`);
153
153
  function VisioViewer($$anchor, $$props) {
154
154
  $.push($$props, true);
155
155
  const options = $.derived(() => ({
@@ -159,6 +159,7 @@ function VisioViewer($$anchor, $$props) {
159
159
  showToolbar: $$props.showToolbar,
160
160
  canEdit: $$props.canEdit,
161
161
  ribbonAddIns: $$props.ribbonAddIns,
162
+ collaboration: $$props.collaboration,
162
163
  events: $$props.events
163
164
  }));
164
165
  let binding;
@@ -273,7 +274,7 @@ function VisioViewer($$anchor, $$props) {
273
274
  exportSvg,
274
275
  createPrintSnapshot
275
276
  };
276
- var div = root$4();
277
+ var div = root$5();
277
278
  $.action(div, ($$node, $$action_arg) => attach?.($$node, $$action_arg), () => $.get(options));
278
279
  $.template_effect(() => {
279
280
  $.set_class(div, 1, $.clsx($$props.class));
@@ -290,10 +291,15 @@ function buildingBlock(key) {
290
291
  if (typeof value !== "function") throw new Error("Update ooxml-ui to use Visio building blocks.");
291
292
  return value;
292
293
  }
294
+ /** The mode a host sees: 'edit' only when permission is granted and the file can be saved back. */
295
+ function effectiveMode(state) {
296
+ return buildingBlock("diagramMode")(state);
297
+ }
293
298
  /** Framework-neutral ownership only; each adapter supplies its own reactive state. */
294
- function createBuildingBlockSession() {
299
+ function createBuildingBlockSession(initial = {}) {
295
300
  const controller = new ViewerController(createWorkerParser());
296
- let options = {};
301
+ controller.canEdit = initial.canEdit === true;
302
+ let options = initial;
297
303
  let source;
298
304
  let host = null;
299
305
  let disposeCanvas;
@@ -317,13 +323,23 @@ function createBuildingBlockSession() {
317
323
  host = node;
318
324
  if (node) disposeCanvas = buildingBlock("mountDiagramCanvas")(node, controller);
319
325
  },
320
- toolbar(locale = options.locale) {
321
- return buildingBlock("diagramToolbarProps")(controller, fit, locale);
326
+ /** Flat control props; pass the framework's latest options so callbacks stay current. */
327
+ toolbar(latest = options) {
328
+ return buildingBlock("diagramToolbarProps")(controller, fit, latest.locale, {
329
+ onOpenFile: latest.onOpenFile,
330
+ onOpenPageSetup: latest.onOpenPageSetup,
331
+ onOpenSettings: latest.onOpenSettings,
332
+ onOpenShareDialog: latest.onOpenShareDialog
333
+ });
334
+ },
335
+ /** Acknowledge a host save: clears dirty and emits the dirty change. */
336
+ markClean() {
337
+ controller.markClean();
322
338
  },
323
339
  update(next) {
324
340
  if (destroyed) return;
325
341
  options = next;
326
- controller.canEdit = next.canEdit !== false;
342
+ controller.canEdit = next.canEdit === true;
327
343
  if (source === next.content) return;
328
344
  source = next.content;
329
345
  controller.cancelLoad();
@@ -357,9 +373,9 @@ function prepareDiagramBlock(host, kind) {
357
373
  function createViewerBuildingBlocks(options = {}) {
358
374
  const source = "subscribe" in options ? options : void 0;
359
375
  const initial = source ? get(source) : options;
360
- const session = createBuildingBlockSession();
376
+ const session = createBuildingBlockSession(initial);
361
377
  const state = writable(session.controller.state);
362
- const locale = writable(initial.locale);
378
+ const latest = writable(initial);
363
379
  const stop = session.controller.subscribe((value) => state.set(value));
364
380
  let stopOptions = () => {};
365
381
  onDestroy(() => {
@@ -368,7 +384,7 @@ function createViewerBuildingBlocks(options = {}) {
368
384
  session.destroy();
369
385
  });
370
386
  const update = (next) => {
371
- locale.set(next.locale);
387
+ latest.set(next);
372
388
  session.update(next);
373
389
  };
374
390
  if (source) stopOptions = source.subscribe(update);
@@ -376,18 +392,25 @@ function createViewerBuildingBlocks(options = {}) {
376
392
  return {
377
393
  controller: session.controller,
378
394
  state: { subscribe: state.subscribe },
379
- toolbarProps: derived([state, locale], ([, language]) => session.toolbar(language)),
395
+ toolbarProps: derived([state, latest], ([, next]) => session.toolbar(next)),
380
396
  loading: derived(state, (value) => value.loading),
381
397
  error: derived(state, (value) => value.error ?? value.edit.error),
382
- mode: derived(state, (value) => value.canEdit ? "edit" : "view"),
398
+ /** Effective permission: host `canEdit` and a drawing that can be saved back. */
399
+ mode: derived(state, (value) => effectiveMode(value)),
383
400
  canvasProps: { mount: session.mount },
384
401
  inspectorProps: { controller: session.controller },
402
+ shapesProps: derived(latest, (next) => ({
403
+ controller: session.controller,
404
+ locale: next.locale
405
+ })),
406
+ /** Acknowledge a host save: clears dirty and fires `onDirtyChange(false)`. */
407
+ markClean: () => session.markClean(),
385
408
  update
386
409
  };
387
410
  }
388
411
  //#endregion
389
412
  //#region packages/svelte/src/DiagramCanvas.svelte
390
- var root$3 = $.from_html(`<div></div>`);
413
+ var root$4 = $.from_html(`<div></div>`);
391
414
  function DiagramCanvas($$anchor, $$props) {
392
415
  $.push($$props, true);
393
416
  let host;
@@ -396,48 +419,180 @@ function DiagramCanvas($$anchor, $$props) {
396
419
  current(host);
397
420
  return () => current(null);
398
421
  });
399
- var div = root$3();
422
+ var div = root$4();
400
423
  $.bind_this(div, ($$value) => host = $$value, () => host);
401
424
  $.template_effect(() => $.set_class(div, 1, $.clsx($$props.class)));
402
425
  $.append($$anchor, div);
403
426
  $.pop();
404
427
  }
405
428
  //#endregion
429
+ //#region packages/bindings/src/toolbar-controls.ts
430
+ var TOOLBAR_GROUPS = [
431
+ "file",
432
+ "history",
433
+ "zoom",
434
+ "selection",
435
+ "clipboard",
436
+ "tools",
437
+ "search"
438
+ ];
439
+ var fire = (action) => () => {
440
+ Promise.resolve().then(action).catch(() => {});
441
+ };
442
+ var button = (id, label, enabled, action, pressed) => ({
443
+ kind: "button",
444
+ id,
445
+ label,
446
+ disabled: !enabled,
447
+ ...pressed === void 0 ? {} : { pressed },
448
+ run: fire(action)
449
+ });
450
+ /**
451
+ * What the library Toolbar shows, as plain data every framework maps onto its own elements.
452
+ * Every control reflects `toolbarProps`: edit actions are disabled while the diagram is read-only,
453
+ * viewing actions (zoom, fit, search) never are, and a host dialog button appears only when the
454
+ * host supplied its callback.
455
+ */
456
+ function toolbarGroups(props, include = TOOLBAR_GROUPS) {
457
+ const { labels } = props;
458
+ const has = props.pages.length > 0;
459
+ const groups = [];
460
+ const add = (id, controls) => {
461
+ if (include.includes(id) && controls.length) groups.push({
462
+ id,
463
+ controls
464
+ });
465
+ };
466
+ add("file", [
467
+ ...props.onOpenFile ? [button("open", labels.Open, true, props.onOpenFile)] : [],
468
+ ...props.onOpenPageSetup ? [button("page-setup", labels["Page Setup"], has, props.onOpenPageSetup)] : [],
469
+ ...props.onOpenShareDialog ? [button("share", labels.Share, has, props.onOpenShareDialog)] : [],
470
+ ...props.onOpenSettings ? [button("settings", labels.Options, true, props.onOpenSettings)] : []
471
+ ]);
472
+ add("history", [button("undo", labels.Undo, props.canUndo, props.onUndo), button("redo", labels.Redo, props.canRedo, props.onRedo)]);
473
+ add("zoom", [
474
+ button("zoom-out", labels["Zoom Out"], props.canZoomOut, props.onZoomOut),
475
+ {
476
+ kind: "text",
477
+ id: "zoom",
478
+ text: `${Math.round(props.zoom * 100)}%`
479
+ },
480
+ button("zoom-in", labels["Zoom In"], props.canZoomIn, props.onZoomIn),
481
+ button("fit", labels["Fit to Window"], has, props.onFit)
482
+ ]);
483
+ add("selection", [
484
+ button("duplicate", labels.Duplicate, props.canDuplicate, props.onDuplicate),
485
+ button("group", labels.Group, props.canGroup, props.onGroup),
486
+ button("ungroup", labels.Ungroup, props.canUngroup, props.onUngroup),
487
+ button("delete", labels.Delete, props.canDelete, props.onDelete)
488
+ ]);
489
+ add("clipboard", [
490
+ button("cut", labels.Cut, props.canCut, props.onCut),
491
+ button("copy", labels.Copy, props.canCopy, props.onCopy),
492
+ button("paste", labels.Paste, props.canPaste, props.onPaste)
493
+ ]);
494
+ add("tools", props.tools.map((tool) => button(`tool-${tool.id}`, tool.label, tool.id === "pointer" || props.canDraw, () => props.onToolChange(tool.id), tool.active)));
495
+ const { search } = props;
496
+ add("search", [{
497
+ kind: "search",
498
+ id: "search",
499
+ label: labels.Find,
500
+ query: search.query,
501
+ summary: search.query ? search.resultCount ? `${search.activeIndex >= 0 ? search.activeIndex + 1 : "-"} / ${search.resultCount}` : "0" : "",
502
+ previous: button("search-previous", labels["Previous Result"], search.resultCount > 0, props.onPreviousSearchResult),
503
+ next: button("search-next", labels["Next Result"], search.resultCount > 0, props.onNextSearchResult),
504
+ change: (query) => props.onSearchChange(query)
505
+ }]);
506
+ return groups;
507
+ }
508
+ //#endregion
406
509
  //#region packages/svelte/src/Toolbar.svelte
407
- var root$2 = $.from_html(`<div role="toolbar"><button type="button"> </button> <button type="button"> </button> <button type="button"> </button></div>`);
510
+ var root$3 = $.from_html(`<span> </span>`);
511
+ var root_1$1 = $.from_html(`<button type="button"> </button>`);
512
+ var root_2 = $.from_html(`<span data-control="search"><input type="search"/> <span aria-live="polite"> </span> <button type="button"> </button> <button type="button"> </button></span>`);
513
+ var root_3 = $.from_html(`<span></span>`);
514
+ var root_4 = $.from_html(`<div role="toolbar"></div>`);
408
515
  function Toolbar($$anchor, $$props) {
409
516
  $.push($$props, true);
410
517
  function theme(host) {
411
518
  prepareDiagramBlock(host, "toolbar");
412
519
  }
413
- var div = root$2();
414
- var button = $.child(div);
415
- var text = $.only_child(button, true);
416
- var button_1 = $.sibling(button, 2);
417
- var text_1 = $.only_child(button_1, true);
418
- var button_2 = $.sibling(button_1, 2);
419
- var text_2 = $.only_child(button_2, true);
520
+ const groups = $.derived(() => toolbarGroups($$props.toolbarProps, $$props.include));
521
+ var div = root_4();
522
+ $.each(div, 21, () => $.get(groups), (group) => group.id, ($$anchor, group) => {
523
+ var span = root_3();
524
+ $.each(span, 21, () => $.get(group).controls, (control) => control.id, ($$anchor, control) => {
525
+ var fragment = $.comment();
526
+ var node = $.first_child(fragment);
527
+ var consequent = ($$anchor) => {
528
+ var span_1 = root$3();
529
+ var text = $.only_child(span_1, true);
530
+ $.template_effect(() => {
531
+ $.set_attribute(span_1, "data-control", $.get(control).id);
532
+ $.set_text(text, $.get(control).text);
533
+ });
534
+ $.append($$anchor, span_1);
535
+ };
536
+ var consequent_1 = ($$anchor) => {
537
+ var button = root_1$1();
538
+ var text_1 = $.only_child(button, true);
539
+ $.template_effect(() => {
540
+ $.set_attribute(button, "data-control", $.get(control).id);
541
+ button.disabled = $.get(control).disabled;
542
+ $.set_attribute(button, "aria-pressed", $.get(control).pressed);
543
+ $.set_text(text_1, $.get(control).label);
544
+ });
545
+ $.delegated("click", button, () => $.get(control).run());
546
+ $.append($$anchor, button);
547
+ };
548
+ var alternate = ($$anchor) => {
549
+ var span_2 = root_2();
550
+ var input = $.child(span_2);
551
+ $.remove_input_defaults(input);
552
+ var span_3 = $.sibling(input, 2);
553
+ var text_2 = $.only_child(span_3, true);
554
+ var button_1 = $.sibling(span_3, 2);
555
+ var text_3 = $.only_child(button_1, true);
556
+ var button_2 = $.sibling(button_1, 2);
557
+ var text_4 = $.only_child(button_2, true);
558
+ $.reset(span_2);
559
+ $.template_effect(() => {
560
+ $.set_attribute(input, "aria-label", $.get(control).label);
561
+ $.set_attribute(input, "placeholder", $.get(control).label);
562
+ $.set_value(input, $.get(control).query);
563
+ $.set_text(text_2, $.get(control).summary);
564
+ $.set_attribute(button_1, "data-control", $.get(control).previous.id);
565
+ button_1.disabled = $.get(control).previous.disabled;
566
+ $.set_text(text_3, $.get(control).previous.label);
567
+ $.set_attribute(button_2, "data-control", $.get(control).next.id);
568
+ button_2.disabled = $.get(control).next.disabled;
569
+ $.set_text(text_4, $.get(control).next.label);
570
+ });
571
+ $.delegated("input", input, (event) => $.get(control).change(event.currentTarget.value));
572
+ $.delegated("click", button_1, () => $.get(control).previous.run());
573
+ $.delegated("click", button_2, () => $.get(control).next.run());
574
+ $.append($$anchor, span_2);
575
+ };
576
+ $.if(node, ($$render) => {
577
+ if ($.get(control).kind === "text") $$render(consequent);
578
+ else if ($.get(control).kind === "button") $$render(consequent_1, 1);
579
+ else $$render(alternate, -1);
580
+ });
581
+ $.append($$anchor, fragment);
582
+ });
583
+ $.reset(span);
584
+ $.template_effect(() => $.set_attribute(span, "data-group", $.get(group).id));
585
+ $.append($$anchor, span);
586
+ });
420
587
  $.reset(div);
421
588
  $.action(div, ($$node) => theme?.($$node));
422
- $.template_effect(() => {
423
- button.disabled = !$$props.toolbarProps.canUndo;
424
- $.set_text(text, $$props.toolbarProps.labels.Undo);
425
- button_1.disabled = !$$props.toolbarProps.canRedo;
426
- $.set_text(text_1, $$props.toolbarProps.labels.Redo);
427
- $.set_text(text_2, $$props.toolbarProps.labels["Fit to Window"]);
428
- });
429
- $.delegated("click", button, () => void $$props.toolbarProps.onUndo());
430
- $.delegated("click", button_1, () => void $$props.toolbarProps.onRedo());
431
- $.delegated("click", button_2, function(...$$args) {
432
- $$props.toolbarProps.onFit?.apply(this, $$args);
433
- });
434
589
  $.append($$anchor, div);
435
590
  $.pop();
436
591
  }
437
- $.delegate(["click"]);
592
+ $.delegate(["click", "input"]);
438
593
  //#endregion
439
594
  //#region packages/svelte/src/PageNavigation.svelte
440
- var root$1 = $.from_html(`<option> </option>`);
595
+ var root$2 = $.from_html(`<option> </option>`);
441
596
  var root_1 = $.from_html(`<select></select>`);
442
597
  function PageNavigation($$anchor, $$props) {
443
598
  $.push($$props, true);
@@ -446,7 +601,7 @@ function PageNavigation($$anchor, $$props) {
446
601
  }
447
602
  var select = root_1();
448
603
  $.each(select, 23, () => $$props.toolbarProps.pages, (page) => page.id, ($$anchor, page, index) => {
449
- var option = root$1();
604
+ var option = root$2();
450
605
  var text = $.only_child(option, true);
451
606
  var option_value = {};
452
607
  $.template_effect(() => {
@@ -470,15 +625,27 @@ function PageNavigation($$anchor, $$props) {
470
625
  $.delegate(["change"]);
471
626
  //#endregion
472
627
  //#region packages/svelte/src/Inspector.svelte
473
- var root = $.from_html(`<div></div>`);
628
+ var root$1 = $.from_html(`<div></div>`);
474
629
  function Inspector($$anchor, $$props) {
475
630
  $.push($$props, true);
476
631
  let host;
477
632
  $.user_effect(() => buildingBlock("mountDiagramInspector")(host, $$props.controller));
633
+ var div = root$1();
634
+ $.bind_this(div, ($$value) => host = $$value, () => host);
635
+ $.append($$anchor, div);
636
+ $.pop();
637
+ }
638
+ //#endregion
639
+ //#region packages/svelte/src/ShapesPane.svelte
640
+ var root = $.from_html(`<div></div>`);
641
+ function ShapesPane($$anchor, $$props) {
642
+ $.push($$props, true);
643
+ let host;
644
+ $.user_effect(() => buildingBlock("mountDiagramShapes")(host, $$props.controller, { locale: $$props.locale }));
478
645
  var div = root();
479
646
  $.bind_this(div, ($$value) => host = $$value, () => host);
480
647
  $.append($$anchor, div);
481
648
  $.pop();
482
649
  }
483
650
  //#endregion
484
- export { DiagramCanvas, Inspector, PageNavigation, Toolbar, VisioViewer, VisioViewer as default, createViewerBuildingBlocks };
651
+ export { DiagramCanvas, Inspector, PageNavigation, ShapesPane, Toolbar, VisioViewer, VisioViewer as default, createViewerBuildingBlocks };
@@ -87,7 +87,7 @@ function viewerHandle(current) {
87
87
  //#region packages/bindings/src/VisioViewer.svelte
88
88
  function VisioViewer($$renderer, $$props) {
89
89
  $$renderer.component(($$renderer) => {
90
- let { document, pageIndex, zoom, showToolbar, canEdit, ribbonAddIns, events, class: className, style, "aria-label": ariaLabel } = $$props;
90
+ let { document, pageIndex, zoom, showToolbar, canEdit, ribbonAddIns, collaboration, events, class: className, style, "aria-label": ariaLabel } = $$props;
91
91
  $.derived(() => ({
92
92
  document,
93
93
  pageIndex,
@@ -95,6 +95,7 @@ function VisioViewer($$renderer, $$props) {
95
95
  showToolbar,
96
96
  canEdit,
97
97
  ribbonAddIns,
98
+ collaboration,
98
99
  events
99
100
  }));
100
101
  let binding;
@@ -201,10 +202,15 @@ function buildingBlock(key) {
201
202
  if (typeof value !== "function") throw new Error("Update ooxml-ui to use Visio building blocks.");
202
203
  return value;
203
204
  }
205
+ /** The mode a host sees: 'edit' only when permission is granted and the file can be saved back. */
206
+ function effectiveMode(state) {
207
+ return buildingBlock("diagramMode")(state);
208
+ }
204
209
  /** Framework-neutral ownership only; each adapter supplies its own reactive state. */
205
- function createBuildingBlockSession() {
210
+ function createBuildingBlockSession(initial = {}) {
206
211
  const controller = new ViewerController(createWorkerParser());
207
- let options = {};
212
+ controller.canEdit = initial.canEdit === true;
213
+ let options = initial;
208
214
  let source;
209
215
  let host = null;
210
216
  let disposeCanvas;
@@ -228,13 +234,23 @@ function createBuildingBlockSession() {
228
234
  host = node;
229
235
  if (node) disposeCanvas = buildingBlock("mountDiagramCanvas")(node, controller);
230
236
  },
231
- toolbar(locale = options.locale) {
232
- return buildingBlock("diagramToolbarProps")(controller, fit, locale);
237
+ /** Flat control props; pass the framework's latest options so callbacks stay current. */
238
+ toolbar(latest = options) {
239
+ return buildingBlock("diagramToolbarProps")(controller, fit, latest.locale, {
240
+ onOpenFile: latest.onOpenFile,
241
+ onOpenPageSetup: latest.onOpenPageSetup,
242
+ onOpenSettings: latest.onOpenSettings,
243
+ onOpenShareDialog: latest.onOpenShareDialog
244
+ });
245
+ },
246
+ /** Acknowledge a host save: clears dirty and emits the dirty change. */
247
+ markClean() {
248
+ controller.markClean();
233
249
  },
234
250
  update(next) {
235
251
  if (destroyed) return;
236
252
  options = next;
237
- controller.canEdit = next.canEdit !== false;
253
+ controller.canEdit = next.canEdit === true;
238
254
  if (source === next.content) return;
239
255
  source = next.content;
240
256
  controller.cancelLoad();
@@ -261,9 +277,9 @@ function createBuildingBlockSession() {
261
277
  function createViewerBuildingBlocks(options = {}) {
262
278
  const source = "subscribe" in options ? options : void 0;
263
279
  const initial = source ? get(source) : options;
264
- const session = createBuildingBlockSession();
280
+ const session = createBuildingBlockSession(initial);
265
281
  const state = writable(session.controller.state);
266
- const locale = writable(initial.locale);
282
+ const latest = writable(initial);
267
283
  const stop = session.controller.subscribe((value) => state.set(value));
268
284
  let stopOptions = () => {};
269
285
  onDestroy(() => {
@@ -272,7 +288,7 @@ function createViewerBuildingBlocks(options = {}) {
272
288
  session.destroy();
273
289
  });
274
290
  const update = (next) => {
275
- locale.set(next.locale);
291
+ latest.set(next);
276
292
  session.update(next);
277
293
  };
278
294
  if (source) stopOptions = source.subscribe(update);
@@ -280,12 +296,19 @@ function createViewerBuildingBlocks(options = {}) {
280
296
  return {
281
297
  controller: session.controller,
282
298
  state: { subscribe: state.subscribe },
283
- toolbarProps: derived([state, locale], ([, language]) => session.toolbar(language)),
299
+ toolbarProps: derived([state, latest], ([, next]) => session.toolbar(next)),
284
300
  loading: derived(state, (value) => value.loading),
285
301
  error: derived(state, (value) => value.error ?? value.edit.error),
286
- mode: derived(state, (value) => value.canEdit ? "edit" : "view"),
302
+ /** Effective permission: host `canEdit` and a drawing that can be saved back. */
303
+ mode: derived(state, (value) => effectiveMode(value)),
287
304
  canvasProps: { mount: session.mount },
288
305
  inspectorProps: { controller: session.controller },
306
+ shapesProps: derived(latest, (next) => ({
307
+ controller: session.controller,
308
+ locale: next.locale
309
+ })),
310
+ /** Acknowledge a host save: clears dirty and fires `onDirtyChange(false)`. */
311
+ markClean: () => session.markClean(),
289
312
  update
290
313
  };
291
314
  }
@@ -298,11 +321,107 @@ function DiagramCanvas($$renderer, $$props) {
298
321
  });
299
322
  }
300
323
  //#endregion
324
+ //#region packages/bindings/src/toolbar-controls.ts
325
+ var TOOLBAR_GROUPS = [
326
+ "file",
327
+ "history",
328
+ "zoom",
329
+ "selection",
330
+ "clipboard",
331
+ "tools",
332
+ "search"
333
+ ];
334
+ var fire = (action) => () => {
335
+ Promise.resolve().then(action).catch(() => {});
336
+ };
337
+ var button = (id, label, enabled, action, pressed) => ({
338
+ kind: "button",
339
+ id,
340
+ label,
341
+ disabled: !enabled,
342
+ ...pressed === void 0 ? {} : { pressed },
343
+ run: fire(action)
344
+ });
345
+ /**
346
+ * What the library Toolbar shows, as plain data every framework maps onto its own elements.
347
+ * Every control reflects `toolbarProps`: edit actions are disabled while the diagram is read-only,
348
+ * viewing actions (zoom, fit, search) never are, and a host dialog button appears only when the
349
+ * host supplied its callback.
350
+ */
351
+ function toolbarGroups(props, include = TOOLBAR_GROUPS) {
352
+ const { labels } = props;
353
+ const has = props.pages.length > 0;
354
+ const groups = [];
355
+ const add = (id, controls) => {
356
+ if (include.includes(id) && controls.length) groups.push({
357
+ id,
358
+ controls
359
+ });
360
+ };
361
+ add("file", [
362
+ ...props.onOpenFile ? [button("open", labels.Open, true, props.onOpenFile)] : [],
363
+ ...props.onOpenPageSetup ? [button("page-setup", labels["Page Setup"], has, props.onOpenPageSetup)] : [],
364
+ ...props.onOpenShareDialog ? [button("share", labels.Share, has, props.onOpenShareDialog)] : [],
365
+ ...props.onOpenSettings ? [button("settings", labels.Options, true, props.onOpenSettings)] : []
366
+ ]);
367
+ add("history", [button("undo", labels.Undo, props.canUndo, props.onUndo), button("redo", labels.Redo, props.canRedo, props.onRedo)]);
368
+ add("zoom", [
369
+ button("zoom-out", labels["Zoom Out"], props.canZoomOut, props.onZoomOut),
370
+ {
371
+ kind: "text",
372
+ id: "zoom",
373
+ text: `${Math.round(props.zoom * 100)}%`
374
+ },
375
+ button("zoom-in", labels["Zoom In"], props.canZoomIn, props.onZoomIn),
376
+ button("fit", labels["Fit to Window"], has, props.onFit)
377
+ ]);
378
+ add("selection", [
379
+ button("duplicate", labels.Duplicate, props.canDuplicate, props.onDuplicate),
380
+ button("group", labels.Group, props.canGroup, props.onGroup),
381
+ button("ungroup", labels.Ungroup, props.canUngroup, props.onUngroup),
382
+ button("delete", labels.Delete, props.canDelete, props.onDelete)
383
+ ]);
384
+ add("clipboard", [
385
+ button("cut", labels.Cut, props.canCut, props.onCut),
386
+ button("copy", labels.Copy, props.canCopy, props.onCopy),
387
+ button("paste", labels.Paste, props.canPaste, props.onPaste)
388
+ ]);
389
+ add("tools", props.tools.map((tool) => button(`tool-${tool.id}`, tool.label, tool.id === "pointer" || props.canDraw, () => props.onToolChange(tool.id), tool.active)));
390
+ const { search } = props;
391
+ add("search", [{
392
+ kind: "search",
393
+ id: "search",
394
+ label: labels.Find,
395
+ query: search.query,
396
+ summary: search.query ? search.resultCount ? `${search.activeIndex >= 0 ? search.activeIndex + 1 : "-"} / ${search.resultCount}` : "0" : "",
397
+ previous: button("search-previous", labels["Previous Result"], search.resultCount > 0, props.onPreviousSearchResult),
398
+ next: button("search-next", labels["Next Result"], search.resultCount > 0, props.onNextSearchResult),
399
+ change: (query) => props.onSearchChange(query)
400
+ }]);
401
+ return groups;
402
+ }
403
+ //#endregion
301
404
  //#region packages/svelte/src/Toolbar.svelte
302
405
  function Toolbar($$renderer, $$props) {
303
406
  $$renderer.component(($$renderer) => {
304
- let { toolbarProps } = $$props;
305
- $$renderer.push(`<div role="toolbar"><button type="button"${$.attr("disabled", !toolbarProps.canUndo, true)}>${$.escape(toolbarProps.labels.Undo)}</button> <button type="button"${$.attr("disabled", !toolbarProps.canRedo, true)}>${$.escape(toolbarProps.labels.Redo)}</button> <button type="button">${$.escape(toolbarProps.labels["Fit to Window"])}</button></div>`);
407
+ let { toolbarProps, include } = $$props;
408
+ const groups = $.derived(() => toolbarGroups(toolbarProps, include));
409
+ $$renderer.push(`<div role="toolbar"><!--[-->`);
410
+ const each_array = $.ensure_array_like(groups());
411
+ for (let $$index_1 = 0, $$length = each_array.length; $$index_1 < $$length; $$index_1++) {
412
+ let group = each_array[$$index_1];
413
+ $$renderer.push(`<span${$.attr("data-group", group.id)}><!--[-->`);
414
+ const each_array_1 = $.ensure_array_like(group.controls);
415
+ for (let $$index = 0, $$length = each_array_1.length; $$index < $$length; $$index++) {
416
+ let control = each_array_1[$$index];
417
+ if (control.kind === "text") $$renderer.push(`<!--[0--><span${$.attr("data-control", control.id)}>${$.escape(control.text)}</span>`);
418
+ else if (control.kind === "button") $$renderer.push(`<!--[1--><button type="button"${$.attr("data-control", control.id)}${$.attr("disabled", control.disabled, true)}${$.attr("aria-pressed", control.pressed)}>${$.escape(control.label)}</button>`);
419
+ else $$renderer.push(`<!--[-1--><span data-control="search"><input type="search"${$.attr("aria-label", control.label)}${$.attr("placeholder", control.label)}${$.attr("value", control.query)}/> <span aria-live="polite">${$.escape(control.summary)}</span> <button type="button"${$.attr("data-control", control.previous.id)}${$.attr("disabled", control.previous.disabled, true)}>${$.escape(control.previous.label)}</button> <button type="button"${$.attr("data-control", control.next.id)}${$.attr("disabled", control.next.disabled, true)}>${$.escape(control.next.label)}</button></span>`);
420
+ $$renderer.push(`<!--]-->`);
421
+ }
422
+ $$renderer.push(`<!--]--></span>`);
423
+ }
424
+ $$renderer.push(`<!--]--></div>`);
306
425
  });
307
426
  }
308
427
  //#endregion
@@ -336,4 +455,12 @@ function Inspector($$renderer, $$props) {
336
455
  });
337
456
  }
338
457
  //#endregion
339
- export { DiagramCanvas, Inspector, PageNavigation, Toolbar, VisioViewer, VisioViewer as default, createViewerBuildingBlocks };
458
+ //#region packages/svelte/src/ShapesPane.svelte
459
+ function ShapesPane($$renderer, $$props) {
460
+ $$renderer.component(($$renderer) => {
461
+ let { controller, locale } = $$props;
462
+ $$renderer.push(`<div></div>`);
463
+ });
464
+ }
465
+ //#endregion
466
+ export { DiagramCanvas, Inspector, PageNavigation, ShapesPane, Toolbar, VisioViewer, VisioViewer as default, createViewerBuildingBlocks };
@@ -5,3 +5,4 @@ export { default as DiagramCanvas } from './DiagramCanvas.svelte';
5
5
  export { default as Toolbar } from './Toolbar.svelte';
6
6
  export { default as PageNavigation } from './PageNavigation.svelte';
7
7
  export { default as Inspector } from './Inspector.svelte';
8
+ export { default as ShapesPane } from './ShapesPane.svelte';
package/dist/runtime.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './types/packages/bindings/src/common.js';
2
2
  export * from './types/packages/bindings/src/building-blocks.js';
3
+ export * from './types/packages/bindings/src/toolbar-controls.js';
3
4
  export * from './types/packages/svelte/src/building-blocks.js';
4
5
  export * from './types/src/index.js';
5
6
  export * from 'visio-core';
package/dist/runtime.js CHANGED
@@ -168,10 +168,15 @@ function buildingBlock(key) {
168
168
  if (typeof value !== "function") throw new Error("Update ooxml-ui to use Visio building blocks.");
169
169
  return value;
170
170
  }
171
+ /** The mode a host sees: 'edit' only when permission is granted and the file can be saved back. */
172
+ function effectiveMode(state) {
173
+ return buildingBlock("diagramMode")(state);
174
+ }
171
175
  /** Framework-neutral ownership only; each adapter supplies its own reactive state. */
172
- function createBuildingBlockSession() {
176
+ function createBuildingBlockSession(initial = {}) {
173
177
  const controller = new ViewerController$1(createWorkerParser());
174
- let options = {};
178
+ controller.canEdit = initial.canEdit === true;
179
+ let options = initial;
175
180
  let source;
176
181
  let host = null;
177
182
  let disposeCanvas;
@@ -195,13 +200,23 @@ function createBuildingBlockSession() {
195
200
  host = node;
196
201
  if (node) disposeCanvas = buildingBlock("mountDiagramCanvas")(node, controller);
197
202
  },
198
- toolbar(locale = options.locale) {
199
- return buildingBlock("diagramToolbarProps")(controller, fit, locale);
203
+ /** Flat control props; pass the framework's latest options so callbacks stay current. */
204
+ toolbar(latest = options) {
205
+ return buildingBlock("diagramToolbarProps")(controller, fit, latest.locale, {
206
+ onOpenFile: latest.onOpenFile,
207
+ onOpenPageSetup: latest.onOpenPageSetup,
208
+ onOpenSettings: latest.onOpenSettings,
209
+ onOpenShareDialog: latest.onOpenShareDialog
210
+ });
211
+ },
212
+ /** Acknowledge a host save: clears dirty and emits the dirty change. */
213
+ markClean() {
214
+ controller.markClean();
200
215
  },
201
216
  update(next) {
202
217
  if (destroyed) return;
203
218
  options = next;
204
- controller.canEdit = next.canEdit !== false;
219
+ controller.canEdit = next.canEdit === true;
205
220
  if (source === next.content) return;
206
221
  source = next.content;
207
222
  controller.cancelLoad();
@@ -230,14 +245,94 @@ function prepareDiagramBlock(host, kind) {
230
245
  prepare(host, kind, "visio");
231
246
  }
232
247
  //#endregion
248
+ //#region packages/bindings/src/toolbar-controls.ts
249
+ var TOOLBAR_GROUPS = [
250
+ "file",
251
+ "history",
252
+ "zoom",
253
+ "selection",
254
+ "clipboard",
255
+ "tools",
256
+ "search"
257
+ ];
258
+ var fire = (action) => () => {
259
+ Promise.resolve().then(action).catch(() => {});
260
+ };
261
+ var button = (id, label, enabled, action, pressed) => ({
262
+ kind: "button",
263
+ id,
264
+ label,
265
+ disabled: !enabled,
266
+ ...pressed === void 0 ? {} : { pressed },
267
+ run: fire(action)
268
+ });
269
+ /**
270
+ * What the library Toolbar shows, as plain data every framework maps onto its own elements.
271
+ * Every control reflects `toolbarProps`: edit actions are disabled while the diagram is read-only,
272
+ * viewing actions (zoom, fit, search) never are, and a host dialog button appears only when the
273
+ * host supplied its callback.
274
+ */
275
+ function toolbarGroups(props, include = TOOLBAR_GROUPS) {
276
+ const { labels } = props;
277
+ const has = props.pages.length > 0;
278
+ const groups = [];
279
+ const add = (id, controls) => {
280
+ if (include.includes(id) && controls.length) groups.push({
281
+ id,
282
+ controls
283
+ });
284
+ };
285
+ add("file", [
286
+ ...props.onOpenFile ? [button("open", labels.Open, true, props.onOpenFile)] : [],
287
+ ...props.onOpenPageSetup ? [button("page-setup", labels["Page Setup"], has, props.onOpenPageSetup)] : [],
288
+ ...props.onOpenShareDialog ? [button("share", labels.Share, has, props.onOpenShareDialog)] : [],
289
+ ...props.onOpenSettings ? [button("settings", labels.Options, true, props.onOpenSettings)] : []
290
+ ]);
291
+ add("history", [button("undo", labels.Undo, props.canUndo, props.onUndo), button("redo", labels.Redo, props.canRedo, props.onRedo)]);
292
+ add("zoom", [
293
+ button("zoom-out", labels["Zoom Out"], props.canZoomOut, props.onZoomOut),
294
+ {
295
+ kind: "text",
296
+ id: "zoom",
297
+ text: `${Math.round(props.zoom * 100)}%`
298
+ },
299
+ button("zoom-in", labels["Zoom In"], props.canZoomIn, props.onZoomIn),
300
+ button("fit", labels["Fit to Window"], has, props.onFit)
301
+ ]);
302
+ add("selection", [
303
+ button("duplicate", labels.Duplicate, props.canDuplicate, props.onDuplicate),
304
+ button("group", labels.Group, props.canGroup, props.onGroup),
305
+ button("ungroup", labels.Ungroup, props.canUngroup, props.onUngroup),
306
+ button("delete", labels.Delete, props.canDelete, props.onDelete)
307
+ ]);
308
+ add("clipboard", [
309
+ button("cut", labels.Cut, props.canCut, props.onCut),
310
+ button("copy", labels.Copy, props.canCopy, props.onCopy),
311
+ button("paste", labels.Paste, props.canPaste, props.onPaste)
312
+ ]);
313
+ add("tools", props.tools.map((tool) => button(`tool-${tool.id}`, tool.label, tool.id === "pointer" || props.canDraw, () => props.onToolChange(tool.id), tool.active)));
314
+ const { search } = props;
315
+ add("search", [{
316
+ kind: "search",
317
+ id: "search",
318
+ label: labels.Find,
319
+ query: search.query,
320
+ summary: search.query ? search.resultCount ? `${search.activeIndex >= 0 ? search.activeIndex + 1 : "-"} / ${search.resultCount}` : "0" : "",
321
+ previous: button("search-previous", labels["Previous Result"], search.resultCount > 0, props.onPreviousSearchResult),
322
+ next: button("search-next", labels["Next Result"], search.resultCount > 0, props.onNextSearchResult),
323
+ change: (query) => props.onSearchChange(query)
324
+ }]);
325
+ return groups;
326
+ }
327
+ //#endregion
233
328
  //#region packages/svelte/src/building-blocks.ts
234
329
  /** Call during component initialization. Reactive option changes use blocks.update(options). */
235
330
  function createViewerBuildingBlocks(options = {}) {
236
331
  const source = "subscribe" in options ? options : void 0;
237
332
  const initial = source ? get(source) : options;
238
- const session = createBuildingBlockSession();
333
+ const session = createBuildingBlockSession(initial);
239
334
  const state = writable(session.controller.state);
240
- const locale = writable(initial.locale);
335
+ const latest = writable(initial);
241
336
  const stop = session.controller.subscribe((value) => state.set(value));
242
337
  let stopOptions = () => {};
243
338
  onDestroy(() => {
@@ -246,7 +341,7 @@ function createViewerBuildingBlocks(options = {}) {
246
341
  session.destroy();
247
342
  });
248
343
  const update = (next) => {
249
- locale.set(next.locale);
344
+ latest.set(next);
250
345
  session.update(next);
251
346
  };
252
347
  if (source) stopOptions = source.subscribe(update);
@@ -254,14 +349,21 @@ function createViewerBuildingBlocks(options = {}) {
254
349
  return {
255
350
  controller: session.controller,
256
351
  state: { subscribe: state.subscribe },
257
- toolbarProps: derived([state, locale], ([, language]) => session.toolbar(language)),
352
+ toolbarProps: derived([state, latest], ([, next]) => session.toolbar(next)),
258
353
  loading: derived(state, (value) => value.loading),
259
354
  error: derived(state, (value) => value.error ?? value.edit.error),
260
- mode: derived(state, (value) => value.canEdit ? "edit" : "view"),
355
+ /** Effective permission: host `canEdit` and a drawing that can be saved back. */
356
+ mode: derived(state, (value) => effectiveMode(value)),
261
357
  canvasProps: { mount: session.mount },
262
358
  inspectorProps: { controller: session.controller },
359
+ shapesProps: derived(latest, (next) => ({
360
+ controller: session.controller,
361
+ locale: next.locale
362
+ })),
363
+ /** Acknowledge a host save: clears dirty and fires `onDirtyChange(false)`. */
364
+ markClean: () => session.markClean(),
263
365
  update
264
366
  };
265
367
  }
266
368
  //#endregion
267
- export { ViewerController, buildingBlock, createBuildingBlockSession, createViewerBuildingBlocks, eventKeys, mountFrameworkViewer, mountViewer, prepareDiagramBlock, propertyKeys, viewerHandle, viewerOptions, viewerStateSource, withEventEmitter };
369
+ export { TOOLBAR_GROUPS, ViewerController, buildingBlock, createBuildingBlockSession, createViewerBuildingBlocks, effectiveMode, eventKeys, mountFrameworkViewer, mountViewer, prepareDiagramBlock, propertyKeys, toolbarGroups, viewerHandle, viewerOptions, viewerStateSource, withEventEmitter };
@@ -1,23 +1,33 @@
1
1
  import { ViewerController, type VsdxSource } from 'ooxml-ui/visio';
2
2
  import * as ui from 'ooxml-ui/visio';
3
- export interface BuildingBlockOptions {
3
+ /** Host-owned dialogs, as in the PowerPoint building blocks; omitted ones report not-enabled. */
4
+ export interface BuildingBlockHostCallbacks {
5
+ onOpenFile?: (() => void) | undefined;
6
+ onOpenPageSetup?: (() => void) | undefined;
7
+ onOpenSettings?: (() => void) | undefined;
8
+ onOpenShareDialog?: (() => void) | undefined;
9
+ }
10
+ export interface BuildingBlockOptions extends BuildingBlockHostCallbacks {
4
11
  content?: VsdxSource | undefined;
12
+ /** Host edit permission. Read-only until it is `true`, so nothing is editable before the host says so. */
5
13
  canEdit?: boolean | undefined;
6
14
  locale?: string | undefined;
7
15
  onDocumentChange?: ((document: NonNullable<ViewerController['state']['document']>) => void) | undefined;
8
16
  onDirtyChange?: ((dirty: boolean) => void) | undefined;
9
17
  onDocumentError?: ((error: Error) => void) | undefined;
10
18
  }
11
- type Api = Pick<typeof ui, 'mountDiagramCanvas' | 'fitDiagramCanvas' | 'mountDiagramInspector' | 'diagramToolbarProps'>;
19
+ type Api = Pick<typeof ui, 'mountDiagramCanvas' | 'fitDiagramCanvas' | 'mountDiagramInspector' | 'mountDiagramShapes' | 'diagramToolbarProps' | 'diagramMode'>;
12
20
  export declare function buildingBlock<K extends keyof Api>(key: K): Api[K];
21
+ /** The mode a host sees: 'edit' only when permission is granted and the file can be saved back. */
22
+ export declare function effectiveMode(state: ViewerController['state']): 'edit' | 'view';
13
23
  /** Framework-neutral ownership only; each adapter supplies its own reactive state. */
14
- export declare function createBuildingBlockSession(): {
24
+ export declare function createBuildingBlockSession(initial?: BuildingBlockOptions): {
15
25
  controller: ViewerController;
16
26
  mount(node: HTMLElement | null): void;
17
- toolbar(locale?: string | undefined): {
18
- labels: {
19
- [k: string]: string;
20
- };
27
+ /** Flat control props; pass the framework's latest options so callbacks stay current. */
28
+ toolbar(latest?: BuildingBlockOptions): {
29
+ labels: Readonly<Record<"Arc" | "Connector" | "Copy" | "Cut" | "Delete" | "Duplicate" | "Ellipse" | "Find" | "Fit to Window" | "Freeform" | "Group" | "Line" | "Next Result" | "Open" | "Options" | "Page" | "Page Setup" | "Paste" | "Pencil" | "Pointer Tool" | "Previous Result" | "Rectangle" | "Redo" | "Shapes" | "Share" | "Text" | "Undo" | "Ungroup" | "Zoom In" | "Zoom Out", string>>;
30
+ mode: ui.DiagramMode;
21
31
  canEdit: boolean;
22
32
  pageIndex: number;
23
33
  pages: {
@@ -31,23 +41,55 @@ export declare function createBuildingBlockSession(): {
31
41
  canDuplicate: boolean;
32
42
  canGroup: boolean;
33
43
  canUngroup: boolean;
44
+ canDelete: boolean;
45
+ canCut: boolean;
46
+ canCopy: boolean;
47
+ canPaste: boolean;
48
+ canDraw: boolean;
49
+ canZoomIn: boolean;
50
+ canZoomOut: boolean;
51
+ tool: ui.DiagramTool;
52
+ tools: ui.DiagramToolSummary[];
53
+ search: {
54
+ query: string;
55
+ resultCount: number;
56
+ activeIndex: number;
57
+ truncated: boolean;
58
+ };
34
59
  onPageChange: (index: number) => void;
35
60
  onZoomChange: (zoom: number) => void;
61
+ onZoomIn: () => void;
62
+ onZoomOut: () => void;
36
63
  onFit: () => void;
37
64
  onUndo: () => Promise<void>;
38
65
  onRedo: () => Promise<void>;
39
66
  onDuplicate: () => Promise<void>;
40
67
  onGroup: () => Promise<void>;
41
68
  onUngroup: () => Promise<void>;
69
+ onDelete: () => Promise<void>;
70
+ onCut: () => Promise<void>;
71
+ onCopy: () => Promise<void>;
72
+ onPaste: () => Promise<void>;
73
+ onToolChange: (tool: ui.DiagramTool) => void;
42
74
  onTextChange: (pageId: string, shapeId: string, text: string) => Promise<void>;
43
75
  onSearchChange: (query: string) => void;
44
76
  onNextSearchResult: () => void;
45
77
  onPreviousSearchResult: () => void;
78
+ canOpenFile: boolean;
79
+ canOpenPageSetup: boolean;
80
+ canOpenSettings: boolean;
81
+ canOpenShareDialog: boolean;
82
+ onOpenFile?: () => void;
83
+ onOpenPageSetup?: () => void;
84
+ onOpenSettings?: () => void;
85
+ onOpenShareDialog?: () => void;
46
86
  };
87
+ /** Acknowledge a host save: clears dirty and emits the dirty change. */
88
+ markClean(): void;
47
89
  update(next: BuildingBlockOptions): void;
48
90
  destroy(): void;
49
91
  };
50
92
  export type BuildingBlockSession = ReturnType<typeof createBuildingBlockSession>;
51
- export type { DiagramToolbarProps, ViewerController } from 'ooxml-ui/visio';
93
+ export type { DiagramToolbarProps, DiagramToolbarLabels, DiagramToolbarHostCallbacks, DiagramMode, DiagramTool, ViewerController, } from 'ooxml-ui/visio';
52
94
  /** Resolve the new optional-control theme API only when that control is mounted. */
53
95
  export declare function prepareDiagramBlock(host: HTMLElement | null, kind: 'toolbar' | 'tabs'): void;
@@ -0,0 +1,41 @@
1
+ import type { DiagramToolbarProps } from 'ooxml-ui/visio';
2
+ /** The groups of the library toolbar; a host picks the ones it wants with `include`. */
3
+ export type ToolbarGroupId = 'file' | 'history' | 'zoom' | 'selection' | 'clipboard' | 'tools' | 'search';
4
+ export declare const TOOLBAR_GROUPS: readonly ToolbarGroupId[];
5
+ export interface ToolbarButton {
6
+ readonly kind: 'button';
7
+ readonly id: string;
8
+ readonly label: string;
9
+ readonly disabled: boolean;
10
+ /** Set for toggles such as the drawing tools. */
11
+ readonly pressed?: boolean;
12
+ run(): void;
13
+ }
14
+ export interface ToolbarText {
15
+ readonly kind: 'text';
16
+ readonly id: string;
17
+ readonly text: string;
18
+ }
19
+ export interface ToolbarSearch {
20
+ readonly kind: 'search';
21
+ readonly id: 'search';
22
+ readonly label: string;
23
+ readonly query: string;
24
+ /** Result summary for assistive technology, e.g. "2 of 5". */
25
+ readonly summary: string;
26
+ readonly previous: ToolbarButton;
27
+ readonly next: ToolbarButton;
28
+ change(query: string): void;
29
+ }
30
+ export type ToolbarControl = ToolbarButton | ToolbarText | ToolbarSearch;
31
+ export interface ToolbarGroup {
32
+ readonly id: ToolbarGroupId;
33
+ readonly controls: readonly ToolbarControl[];
34
+ }
35
+ /**
36
+ * What the library Toolbar shows, as plain data every framework maps onto its own elements.
37
+ * Every control reflects `toolbarProps`: edit actions are disabled while the diagram is read-only,
38
+ * viewing actions (zoom, fit, search) never are, and a host dialog button appears only when the
39
+ * host supplied its callback.
40
+ */
41
+ export declare function toolbarGroups(props: DiagramToolbarProps, include?: readonly ToolbarGroupId[]): ToolbarGroup[];
@@ -7,9 +7,8 @@ export declare function createViewerBuildingBlocks(options?: BuildingBlockOption
7
7
  subscribe: (this: void, run: import("svelte/store").Subscriber<import("ooxml-ui/visio").ViewerState>, invalidate?: () => void) => import("svelte/store").Unsubscriber;
8
8
  };
9
9
  toolbarProps: Readable<{
10
- labels: {
11
- [k: string]: string;
12
- };
10
+ labels: Readonly<Record<"Arc" | "Connector" | "Copy" | "Cut" | "Delete" | "Duplicate" | "Ellipse" | "Find" | "Fit to Window" | "Freeform" | "Group" | "Line" | "Next Result" | "Open" | "Options" | "Page" | "Page Setup" | "Paste" | "Pencil" | "Pointer Tool" | "Previous Result" | "Rectangle" | "Redo" | "Shapes" | "Share" | "Text" | "Undo" | "Ungroup" | "Zoom In" | "Zoom Out", string>>;
11
+ mode: import("ooxml-ui/visio").DiagramMode;
13
12
  canEdit: boolean;
14
13
  pageIndex: number;
15
14
  pages: {
@@ -23,21 +22,52 @@ export declare function createViewerBuildingBlocks(options?: BuildingBlockOption
23
22
  canDuplicate: boolean;
24
23
  canGroup: boolean;
25
24
  canUngroup: boolean;
25
+ canDelete: boolean;
26
+ canCut: boolean;
27
+ canCopy: boolean;
28
+ canPaste: boolean;
29
+ canDraw: boolean;
30
+ canZoomIn: boolean;
31
+ canZoomOut: boolean;
32
+ tool: import("ooxml-ui/visio").DiagramTool;
33
+ tools: import("ooxml-ui/visio").DiagramToolSummary[];
34
+ search: {
35
+ query: string;
36
+ resultCount: number;
37
+ activeIndex: number;
38
+ truncated: boolean;
39
+ };
26
40
  onPageChange: (index: number) => void;
27
41
  onZoomChange: (zoom: number) => void;
42
+ onZoomIn: () => void;
43
+ onZoomOut: () => void;
28
44
  onFit: () => void;
29
45
  onUndo: () => Promise<void>;
30
46
  onRedo: () => Promise<void>;
31
47
  onDuplicate: () => Promise<void>;
32
48
  onGroup: () => Promise<void>;
33
49
  onUngroup: () => Promise<void>;
50
+ onDelete: () => Promise<void>;
51
+ onCut: () => Promise<void>;
52
+ onCopy: () => Promise<void>;
53
+ onPaste: () => Promise<void>;
54
+ onToolChange: (tool: import("ooxml-ui/visio").DiagramTool) => void;
34
55
  onTextChange: (pageId: string, shapeId: string, text: string) => Promise<void>;
35
56
  onSearchChange: (query: string) => void;
36
57
  onNextSearchResult: () => void;
37
58
  onPreviousSearchResult: () => void;
59
+ canOpenFile: boolean;
60
+ canOpenPageSetup: boolean;
61
+ canOpenSettings: boolean;
62
+ canOpenShareDialog: boolean;
63
+ onOpenFile?: () => void;
64
+ onOpenPageSetup?: () => void;
65
+ onOpenSettings?: () => void;
66
+ onOpenShareDialog?: () => void;
38
67
  }>;
39
68
  loading: Readable<boolean>;
40
69
  error: Readable<Error | null>;
70
+ /** Effective permission: host `canEdit` and a drawing that can be saved back. */
41
71
  mode: Readable<"edit" | "view">;
42
72
  canvasProps: {
43
73
  mount: (node: HTMLElement | null) => void;
@@ -45,6 +75,13 @@ export declare function createViewerBuildingBlocks(options?: BuildingBlockOption
45
75
  inspectorProps: {
46
76
  controller: import("ooxml-ui/visio").ViewerController;
47
77
  };
78
+ shapesProps: Readable<{
79
+ controller: import("ooxml-ui/visio").ViewerController;
80
+ locale: string | undefined;
81
+ }>;
82
+ /** Acknowledge a host save: clears dirty and fires `onDirtyChange(false)`. */
83
+ markClean: () => void;
48
84
  update: (next: BuildingBlockOptions) => void;
49
85
  };
50
- export type { BuildingBlockOptions, DiagramToolbarProps } from '../../bindings/src/building-blocks.js';
86
+ export type { BuildingBlockOptions, BuildingBlockHostCallbacks, DiagramToolbarProps, } from '../../bindings/src/building-blocks.js';
87
+ export type { ToolbarGroupId } from '../../bindings/src/toolbar-controls.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visio-svelte-viewer",
3
- "version": "0.8.0",
3
+ "version": "1.0.1",
4
4
  "description": "Local-first Visio viewer for svelte",
5
5
  "homepage": "https://christophervr.github.io/ooxml/visio/",
6
6
  "bugs": {
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "emf-converter": "4.21.0",
41
- "ooxml-core": "^1.20.0",
42
- "ooxml-ui": "^1.19.0",
43
- "visio-core": "^0.5.0"
41
+ "ooxml-core": "^1.23.0",
42
+ "ooxml-ui": "^1.22.0",
43
+ "visio-core": "^1.0.1"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "svelte": "^5.0.0"