visio-solid-viewer 0.1.1 → 0.3.0
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/dist/assets/{edit-worker-DRgXgRud.js → edit-worker-CZbtTOr4.js} +473 -137
- package/dist/assets/{parse-worker-B8EBQQpD.js → parse-worker-BEaLqUCJ.js} +7287 -82
- package/dist/index.js +891 -172
- package/dist/index.server.js +889 -170
- package/dist/types/packages/bindings/src/common.d.ts +11 -0
- package/dist/types/packages/bindings/src/solid.d.ts +8 -3
- package/dist/types/src/office-ui.d.ts +2 -0
- package/dist/types/src/ribbon-action.d.ts +34 -0
- package/dist/types/src/ribbon-home.d.ts +6 -0
- package/dist/types/src/ribbon-parts.d.ts +20 -0
- package/dist/types/src/ribbon-router.d.ts +17 -0
- package/dist/types/src/ribbon-view.d.ts +2 -0
- package/dist/types/src/ribbon.d.ts +6 -0
- package/dist/types/src/status-bar.d.ts +7 -0
- package/dist/types/src/styles/index.d.ts +5 -0
- package/dist/types/src/styles.d.ts +1 -1
- package/dist/types/src/viewer-chrome.d.ts +11 -2
- package/dist/types/src/viewer-commands.d.ts +31 -0
- package/dist/types/src/viewer-draw-tool.d.ts +17 -0
- package/dist/types/src/viewer-input.d.ts +4 -2
- package/dist/types/src/viewer-search.d.ts +2 -1
- package/package.json +4 -3
package/dist/index.server.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { VISIO_RASTER_IMAGE_LIMITS, VISIO_VISIBILITY_LIMITS, getVisioPageLayers, inspectVisioRasterImage,
|
|
1
|
+
import { VISIO_RASTER_IMAGE_LIMITS, VISIO_VISIBILITY_LIMITS, getVisioPageLayers, inspectVisioRasterImage, loadVisio, resolveVisioPageVisibility, validateVisioForeignVector } from "ooxml-core/visio";
|
|
2
|
+
import { registerControls } from "ooxml-ui/controls";
|
|
3
|
+
import { getIcon, registerIcon } from "ooxml-ui/icons";
|
|
2
4
|
import { escape, ssr, ssrAttribute, ssrHydrationKey, ssrStyle } from "solid-js/web";
|
|
3
5
|
import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
|
|
4
6
|
export * from "visio-core";
|
|
@@ -271,7 +273,7 @@ function member(value, choices, label) {
|
|
|
271
273
|
}
|
|
272
274
|
/** Defensive display limits also protect callers supplying their own typed scenes. */
|
|
273
275
|
function assertViewableDocument(model) {
|
|
274
|
-
if (model.format !== "vsdx" || !Array.isArray(model.pages) || model.pages.length > 256 || !Array.isArray(model.diagnostics) || model.diagnostics.length > 2e3) throw new Error("The scene has an invalid page list.");
|
|
276
|
+
if (model.format !== "vsdx" && model.format !== "vsd" || !Array.isArray(model.pages) || model.pages.length > 256 || !Array.isArray(model.diagnostics) || model.diagnostics.length > 2e3) throw new Error("The scene has an invalid page list.");
|
|
275
277
|
const seen = /* @__PURE__ */ new Set();
|
|
276
278
|
let pathBytes = 0, textBytes = 0, runCount = 0, imageBytes = 0, geometryCount = 0, uniquePixels = 0, drawnPixels = 0, layerCount = 0, paragraphCount = 0, gradientStops = 0;
|
|
277
279
|
const imageResources = /* @__PURE__ */ new Map();
|
|
@@ -785,9 +787,10 @@ var ViewerController = class {
|
|
|
785
787
|
#sourceGeneration = 0;
|
|
786
788
|
#searchIndex = null;
|
|
787
789
|
#history = null;
|
|
790
|
+
#sourceFormat = null;
|
|
788
791
|
#editId = 0;
|
|
789
792
|
#visible = documentVisibility(null);
|
|
790
|
-
constructor(parser =
|
|
793
|
+
constructor(parser = loadVisio, listenerError = (error) => {
|
|
791
794
|
if (typeof globalThis.reportError === "function") globalThis.reportError(error);
|
|
792
795
|
else console.error("Visio viewer listener failed:", error);
|
|
793
796
|
}, editor = createWorkerEditor()) {
|
|
@@ -827,6 +830,7 @@ var ViewerController = class {
|
|
|
827
830
|
++this.#sourceGeneration;
|
|
828
831
|
this.#invalidateEdit();
|
|
829
832
|
this.#history = null;
|
|
833
|
+
this.#sourceFormat = null;
|
|
830
834
|
this.#loadId++;
|
|
831
835
|
this.parser.cancel?.();
|
|
832
836
|
this.#searchIndex = null;
|
|
@@ -969,7 +973,7 @@ var ViewerController = class {
|
|
|
969
973
|
this.#change({
|
|
970
974
|
loading: true,
|
|
971
975
|
error: null,
|
|
972
|
-
edit: this.#
|
|
976
|
+
edit: this.#sourceEditState()
|
|
973
977
|
});
|
|
974
978
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
975
979
|
let document;
|
|
@@ -980,10 +984,10 @@ var ViewerController = class {
|
|
|
980
984
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
981
985
|
if (bytes.byteLength > 33554432) throw new Error("This viewer accepts files up to 32 MiB.");
|
|
982
986
|
const owned = bytes instanceof Uint8Array ? Uint8Array.from(bytes) : new Uint8Array(bytes.slice(0));
|
|
983
|
-
history = new DocumentHistory(owned);
|
|
984
987
|
document = await this.parser(Uint8Array.from(owned));
|
|
985
988
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
986
989
|
assertViewableDocument(document);
|
|
990
|
+
history = new DocumentHistory(owned);
|
|
987
991
|
visible = documentVisibility(document);
|
|
988
992
|
} catch (cause) {
|
|
989
993
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
@@ -996,13 +1000,14 @@ var ViewerController = class {
|
|
|
996
1000
|
throw error;
|
|
997
1001
|
}
|
|
998
1002
|
this.#history = history;
|
|
1003
|
+
this.#sourceFormat = document.format;
|
|
999
1004
|
this.#searchIndex = null;
|
|
1000
1005
|
this.#visible = visible;
|
|
1001
1006
|
++this.#documentGeneration;
|
|
1002
1007
|
++this.#sourceGeneration;
|
|
1003
1008
|
this.#change({
|
|
1004
1009
|
document,
|
|
1005
|
-
edit: history.state,
|
|
1010
|
+
edit: document.format === "vsdx" ? history.state : EMPTY_EDIT_STATE,
|
|
1006
1011
|
pageIndex: 0,
|
|
1007
1012
|
loading: false,
|
|
1008
1013
|
error: null,
|
|
@@ -1019,7 +1024,7 @@ var ViewerController = class {
|
|
|
1019
1024
|
this.#invalidateEdit();
|
|
1020
1025
|
this.#change({
|
|
1021
1026
|
loading: false,
|
|
1022
|
-
edit: this.#
|
|
1027
|
+
edit: this.#sourceEditState()
|
|
1023
1028
|
});
|
|
1024
1029
|
}
|
|
1025
1030
|
destroy() {
|
|
@@ -1027,6 +1032,7 @@ var ViewerController = class {
|
|
|
1027
1032
|
this.#destroyed = true;
|
|
1028
1033
|
this.#invalidateEdit();
|
|
1029
1034
|
this.#history = null;
|
|
1035
|
+
this.#sourceFormat = null;
|
|
1030
1036
|
++this.#documentGeneration;
|
|
1031
1037
|
++this.#sourceGeneration;
|
|
1032
1038
|
++this.#loadId;
|
|
@@ -1071,11 +1077,11 @@ var ViewerController = class {
|
|
|
1071
1077
|
if (!this.#state.edit.busy) return;
|
|
1072
1078
|
this.#invalidateEdit();
|
|
1073
1079
|
this.parser.cancel?.();
|
|
1074
|
-
this.#change({ edit: this.#
|
|
1080
|
+
this.#change({ edit: this.#sourceEditState() });
|
|
1075
1081
|
}
|
|
1076
1082
|
exportVsdx() {
|
|
1077
1083
|
this.#assertAlive();
|
|
1078
|
-
if (!this.#history) throw new Error("Load a VSDX file before downloading a source-backed copy.");
|
|
1084
|
+
if (!this.#history || this.#sourceFormat !== "vsdx" || this.#state.document?.format !== "vsdx") throw new Error("Load a VSDX file before downloading a source-backed copy.");
|
|
1079
1085
|
if (this.#state.edit.busy || this.#state.loading) throw new Error("Wait for the current document operation before downloading.");
|
|
1080
1086
|
return this.#history.export();
|
|
1081
1087
|
}
|
|
@@ -1083,10 +1089,13 @@ var ViewerController = class {
|
|
|
1083
1089
|
++this.#editId;
|
|
1084
1090
|
this.editor.cancel?.();
|
|
1085
1091
|
}
|
|
1092
|
+
#sourceEditState() {
|
|
1093
|
+
return this.#sourceFormat === "vsdx" && this.#state.document?.format === "vsdx" ? this.#history?.state ?? EMPTY_EDIT_STATE : EMPTY_EDIT_STATE;
|
|
1094
|
+
}
|
|
1086
1095
|
async #mutate(kind, commands) {
|
|
1087
1096
|
this.#assertAlive();
|
|
1088
1097
|
const history = this.#history;
|
|
1089
|
-
if (!history) throw new Error("Load a VSDX file before editing. Model-only documents are read only.");
|
|
1098
|
+
if (!history || this.#sourceFormat !== "vsdx" || this.#state.document?.format !== "vsdx") throw new Error("Load a VSDX file before editing. Model-only documents are read only.");
|
|
1090
1099
|
if (this.#state.loading || this.#state.edit.busy) throw new Error("Another document operation is in progress.");
|
|
1091
1100
|
const target = kind === "undo" ? history.undoTarget : kind === "redo" ? history.redoTarget : void 0;
|
|
1092
1101
|
if (kind !== "edit" && !target) return;
|
|
@@ -1112,6 +1121,7 @@ var ViewerController = class {
|
|
|
1112
1121
|
} else document = await this.parser(Uint8Array.from(target.bytes));
|
|
1113
1122
|
assertCurrent();
|
|
1114
1123
|
assertViewableDocument(document);
|
|
1124
|
+
if (document.format !== "vsdx") throw new Error("A VSDX edit or history operation cannot change the source format.");
|
|
1115
1125
|
if (edited && !edited.changedParts.length) {
|
|
1116
1126
|
this.#change({ edit: history.state });
|
|
1117
1127
|
return;
|
|
@@ -1201,7 +1211,7 @@ var eventKeys = [
|
|
|
1201
1211
|
//#region src/worker-parser.ts
|
|
1202
1212
|
/** One disposable worker per document prevents old parses and long CPU tasks from occupying the UI. */
|
|
1203
1213
|
function createWorkerParser(factory, timeoutMs = 15e3) {
|
|
1204
|
-
if (!factory && typeof Worker === "undefined") return
|
|
1214
|
+
if (!factory && typeof Worker === "undefined") return loadVisio;
|
|
1205
1215
|
const create = factory ?? (() => new Worker(new URL("./parse-worker.js", import.meta.url), { type: "module" }));
|
|
1206
1216
|
let cancel;
|
|
1207
1217
|
const parse = (bytes) => {
|
|
@@ -2136,7 +2146,7 @@ function selection(target) {
|
|
|
2136
2146
|
}
|
|
2137
2147
|
/** Own every DOM listener and abort them together when the surface is disposed. */
|
|
2138
2148
|
function wireViewerInputs(controls, controller, fit) {
|
|
2139
|
-
const { viewport, commandRoot,
|
|
2149
|
+
const { viewport, commandRoot, zoomSlider, searchInput } = controls;
|
|
2140
2150
|
const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
2141
2151
|
const options = { signal: events.signal };
|
|
2142
2152
|
searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
|
|
@@ -2152,13 +2162,9 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2152
2162
|
controller.setSearchQuery("");
|
|
2153
2163
|
}
|
|
2154
2164
|
}, options);
|
|
2155
|
-
|
|
2165
|
+
zoomSlider.addEventListener("input", () => controller.setZoom(zoomSlider.value / 100), options);
|
|
2156
2166
|
commandRoot.addEventListener("click", (event) => {
|
|
2157
2167
|
const action = event.target?.closest?.("button")?.dataset.action;
|
|
2158
|
-
if (action === "in") controller.setZoom(controller.state.zoom * 1.25);
|
|
2159
|
-
if (action === "out") controller.setZoom(controller.state.zoom / 1.25);
|
|
2160
|
-
if (action === "fit") fit();
|
|
2161
|
-
if (action === "actual") controller.setZoom(1);
|
|
2162
2168
|
if (action === "search-next") controller.nextSearchResult();
|
|
2163
2169
|
if (action === "search-previous") controller.previousSearchResult();
|
|
2164
2170
|
}, options);
|
|
@@ -2197,7 +2203,7 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2197
2203
|
}
|
|
2198
2204
|
if (event.key === "0") {
|
|
2199
2205
|
event.preventDefault();
|
|
2200
|
-
fit();
|
|
2206
|
+
fit("page");
|
|
2201
2207
|
}
|
|
2202
2208
|
}, options);
|
|
2203
2209
|
return () => events.abort();
|
|
@@ -2301,13 +2307,7 @@ button:disabled { opacity:.4; cursor:default; }
|
|
|
2301
2307
|
button:focus-visible,select:focus-visible,input:focus-visible,textarea:focus-visible,summary:focus-visible,[tabindex]:focus-visible { outline:2px solid var(--vv-focus,var(--_vv-accent)); outline-offset:2px; }
|
|
2302
2308
|
button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
|
|
2303
2309
|
.toolbar { flex:none; min-width:0; border-bottom:1px solid var(--_vv-border); background:var(--_vv-surface); }
|
|
2304
|
-
.ribbon-primary { height:32px; display:flex; align-items:center; gap:6px; padding:2px 8px; }
|
|
2305
|
-
.page-picker { display:flex; min-width:0; align-items:center; gap:8px; color:var(--_vv-muted); font-size:11px; }
|
|
2306
|
-
.page-picker select { width:190px; max-width:42vw; height:26px; min-height:26px; padding:2px 6px; font-size:11px; color:var(--_vv-ink); }
|
|
2307
|
-
.page-stepper { display:flex; align-items:center; gap:2px; }
|
|
2308
|
-
.page-stepper button { width:26px; height:26px; min-height:26px; padding:0; font-size:18px; line-height:22px; background:transparent; border-color:transparent; }
|
|
2309
2310
|
.spacer { flex:1; }
|
|
2310
|
-
.local-label { color:var(--_vv-muted); font-size:10px; white-space:nowrap; }
|
|
2311
2311
|
.ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
|
|
2312
2312
|
.ribbon-tabs button { position:relative; min-height:34px; padding:7px 14px; border:0; border-radius:0; background:transparent; color:var(--_vv-muted); font-size:12px; font-weight:500; }
|
|
2313
2313
|
.ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
|
|
@@ -2317,15 +2317,6 @@ button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-acce
|
|
|
2317
2317
|
.group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
|
|
2318
2318
|
.ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
|
|
2319
2319
|
.ribbon-command { display:flex; flex-direction:column; justify-content:center; align-items:center; gap:2px; min-width:70px; padding:2px 6px; border-color:transparent; background:transparent; font-size:11px; white-space:nowrap; }
|
|
2320
|
-
.command-icon { display:grid; place-items:center; width:26px; height:26px; color:var(--_vv-accent); }
|
|
2321
|
-
.command-icon::before { content:""; width:24px; height:24px; background:currentColor; mask:var(--command-symbol) center / contain no-repeat; }
|
|
2322
|
-
[data-icon="selection"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22m5%203%2013%209-6%201-3%206z%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2323
|
-
[data-icon="edit"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%205h16M12%205v14M8%2019h8%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2324
|
-
[data-icon="layers"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22m3%208%209-5%209%205-9%205zM3%2012l9%205%209-5M3%2016l9%205%209-5%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2325
|
-
[data-icon="pages"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%225%22%20y%3D%223%22%20width%3D%2214%22%20height%3D%2218%22%20rx%3D%221%22%2F%3E%3Cpath%20d%3D%22M8%208h8M8%2012h8M8%2016h5%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2326
|
-
[data-icon="inspector"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%223%22%20y%3D%224%22%20width%3D%2218%22%20height%3D%2216%22%20rx%3D%221%22%2F%3E%3Cpath%20d%3D%22M14%204v16M17%208h1M17%2012h1M17%2016h1%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2327
|
-
[data-icon="notes"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%204h16v12H9l-5%204z%22%2F%3E%3Cpath%20d%3D%22M8%208h8M8%2012h5%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2328
|
-
.ribbon-command[data-chrome="edit"] .command-icon { font-family:ui-serif,Georgia,serif; text-decoration:underline; text-decoration-thickness:1px; text-underline-offset:3px; }
|
|
2329
2320
|
.ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
|
|
2330
2321
|
.search-group { width:330px; }
|
|
2331
2322
|
.search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
|
|
@@ -2397,11 +2388,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2397
2388
|
.status { flex:none; display:flex; align-items:center; justify-content:space-between; gap:12px; min-width:0; min-height:29px; padding:0 8px 0 12px; border-top:1px solid var(--_vv-border); color:var(--_vv-muted); background:var(--_vv-surface); font-size:10px; }
|
|
2398
2389
|
.status-message { display:flex; gap:16px; min-width:0; }
|
|
2399
2390
|
.status-message span { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
|
2400
|
-
.zoom-controls { display:flex; flex:none; align-items:center; gap:3px; }
|
|
2401
|
-
.zoom-controls button { min-height:24px; height:24px; padding:1px 6px; border-color:transparent; background:transparent; font-size:10px; }
|
|
2402
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:24px; padding:0; font-size:17px; line-height:20px; }
|
|
2403
|
-
.zoom-divider { width:1px; height:14px; background:var(--_vv-border); margin:0 3px; }
|
|
2404
|
-
.zoom { min-width:40px; font-size:10px; text-align:center; font-variant-numeric:tabular-nums; }
|
|
2405
2391
|
.empty { max-width:360px; margin:60px auto; padding:16px; text-align:center; color:var(--_vv-muted); font-size:12px; }
|
|
2406
2392
|
.empty strong { display:block; color:var(--_vv-ink); font-size:18px; font-weight:500; margin-bottom:8px; }
|
|
2407
2393
|
@media(max-width:1100px) { .ribbon-hint { display:none; } [data-diagnostics] { display:none; } }
|
|
@@ -2409,11 +2395,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2409
2395
|
button,select,input,textarea { min-height:44px; }
|
|
2410
2396
|
button { min-width:44px; }
|
|
2411
2397
|
summary { min-height:44px; line-height:44px; }
|
|
2412
|
-
.ribbon-primary { height:52px; padding:4px 8px; }
|
|
2413
|
-
.page-picker { flex:1; }
|
|
2414
|
-
.page-picker select { width:100%; max-width:none; min-height:44px; height:44px; font-size:12px; }
|
|
2415
|
-
.page-stepper button { width:44px; min-height:44px; height:44px; }
|
|
2416
|
-
.local-label,.ribbon-primary>.spacer { display:none; }
|
|
2417
2398
|
.ribbon-tabs { height:44px; }
|
|
2418
2399
|
.ribbon-tabs button { min-height:44px; padding:8px 16px; }
|
|
2419
2400
|
.ribbon-content { min-height:86px; }
|
|
@@ -2425,7 +2406,7 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2425
2406
|
.search-controls button { min-width:44px; font-size:0; padding:3px; }
|
|
2426
2407
|
.search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
|
|
2427
2408
|
.search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
|
|
2428
|
-
#home-panel>.ribbon-group:not(.search-group) { display:none; }
|
|
2409
|
+
#home-panel>.ribbon-group:not(.search-group):not(.undo-group) { display:none; }
|
|
2429
2410
|
.ribbon-command { min-width:90px; }
|
|
2430
2411
|
.workspace { display:block; overflow:auto; }
|
|
2431
2412
|
.page-rail { width:100%; max-height:190px; border-right:0; border-bottom:1px solid var(--_vv-border); }
|
|
@@ -2442,35 +2423,22 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2442
2423
|
.notes-strip button { min-height:44px; }
|
|
2443
2424
|
.status { flex-wrap:wrap; gap:0; padding:4px 8px; }
|
|
2444
2425
|
.status-message { width:100%; min-height:18px; }
|
|
2445
|
-
.zoom-controls { justify-content:flex-end; width:100%; }
|
|
2446
|
-
.zoom-controls button { min-height:44px; height:44px; min-width:44px; font-size:11px; }
|
|
2447
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; font-size:20px; }
|
|
2448
|
-
.zoom { font-size:11px; }
|
|
2449
2426
|
}
|
|
2450
2427
|
@media(pointer:coarse) {
|
|
2451
2428
|
button,select,input,textarea { min-height:44px; }
|
|
2452
2429
|
button { min-width:44px; }
|
|
2453
2430
|
summary,.layer-controls label { min-height:44px; line-height:44px; }
|
|
2454
|
-
.ribbon-primary { min-height:52px; height:52px; }
|
|
2455
|
-
.page-picker select,.page-stepper button { min-height:44px; height:44px; }
|
|
2456
|
-
.page-stepper button { min-width:44px; }
|
|
2457
2431
|
.ribbon-tabs { min-height:44px; height:44px; }
|
|
2458
2432
|
.ribbon-tabs button { min-height:44px; }
|
|
2459
2433
|
.ribbon-content { min-height:96px; }
|
|
2460
2434
|
.search-group { width:380px; max-width:100%; }
|
|
2461
|
-
.
|
|
2462
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; }
|
|
2435
|
+
.notes-strip button { min-height:44px; height:44px; }
|
|
2463
2436
|
.layer-controls input { min-height:20px; }
|
|
2464
2437
|
}
|
|
2465
|
-
/* Suite ribbon: tabs lead directly into command groups
|
|
2438
|
+
/* Suite ribbon: tabs lead directly into Visio command groups. */
|
|
2466
2439
|
.ribbon-tabs { height:40px; }
|
|
2467
2440
|
.ribbon-tabs button { min-height:39px; padding:8px 16px; }
|
|
2468
2441
|
.ribbon-content { min-height:104px; padding:4px 4px; }
|
|
2469
|
-
.ribbon-primary { height:auto; min-height:92px; width:238px; flex:none; flex-wrap:wrap; justify-content:center; gap:2px; padding:10px 12px 18px; border-right:1px solid var(--_vv-border); position:relative; order:1; }
|
|
2470
|
-
.ribbon-primary .page-picker { flex-direction:column; align-items:start; gap:5px; }
|
|
2471
|
-
.ribbon-primary .page-picker select { width:160px; max-width:100%; }
|
|
2472
|
-
.ribbon-primary .spacer { display:none; }
|
|
2473
|
-
.ribbon-primary .local-label { position:absolute; bottom:4px; left:0; width:100%; text-align:center; }
|
|
2474
2442
|
.search-group { order:2; width:320px; }
|
|
2475
2443
|
.ribbon-hint { order:3; padding:10px 18px; }
|
|
2476
2444
|
.ribbon-command { min-width:82px; }
|
|
@@ -2479,13 +2447,9 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2479
2447
|
.ribbon-tabs { height:44px; }
|
|
2480
2448
|
.ribbon-tabs button { min-height:44px; }
|
|
2481
2449
|
.ribbon-content { min-height:54px; padding:4px 0; }
|
|
2482
|
-
.ribbon-primary { min-height:44px; height:44px; width:100%; padding:0 8px; border:0; flex-wrap:nowrap; gap:4px; order:0; }
|
|
2483
|
-
.ribbon-primary .page-picker { flex-direction:row; flex:1; align-items:center; gap:8px; }
|
|
2484
|
-
.ribbon-primary .page-picker select { flex:1; width:100%; min-width:0; }
|
|
2485
2450
|
.search-group { flex:0 0 100%; width:100%; min-height:54px; }
|
|
2486
2451
|
.search-feedback,.group-label { display:none; }
|
|
2487
2452
|
.ribbon-content { overflow-x:auto; scroll-snap-type:x mandatory; }
|
|
2488
|
-
.ribbon-primary,.search-group { scroll-snap-align:start; }
|
|
2489
2453
|
.workspace { display:flex; position:relative; overflow:hidden; }
|
|
2490
2454
|
.viewport { flex:1; width:100%; height:100%; min-height:0; padding:16px 4px; }
|
|
2491
2455
|
.inspector-pane { position:absolute; inset:0 0 0 auto; width:min(320px,100%); z-index:4; overflow:auto; border-left:1px solid var(--_vv-border); box-shadow:-8px 0 24px #0002; }
|
|
@@ -2514,9 +2478,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2514
2478
|
.status-message { flex:1; }
|
|
2515
2479
|
@media(max-width:760px) {
|
|
2516
2480
|
#home-panel { overflow:visible; min-height:54px; padding:4px; }
|
|
2517
|
-
.ribbon-primary { width:calc(100% - 92px); min-width:0; flex:1; padding:0 4px; }
|
|
2518
|
-
.ribbon-primary .page-picker { font-size:0; gap:0; }
|
|
2519
|
-
.ribbon-primary .page-picker select { font-size:12px; }
|
|
2520
2481
|
.ribbon-tools { display:block; flex:0 0 84px; width:84px; order:1; }
|
|
2521
2482
|
.ribbon-tools>summary { display:flex; min-height:44px; align-items:center; justify-content:center; gap:6px; padding:0 10px; border:1px solid var(--_vv-border); background:var(--_vv-surface); border-radius:4px; font-size:12px; cursor:pointer; list-style:none; }
|
|
2522
2483
|
.ribbon-tools>summary::-webkit-details-marker { display:none; }
|
|
@@ -2528,14 +2489,347 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2528
2489
|
.ribbon-tools .search-group { order:1; flex:auto; }
|
|
2529
2490
|
.ribbon-tools .search-feedback { display:block; }
|
|
2530
2491
|
.status { flex-wrap:nowrap; gap:4px; min-height:48px; }
|
|
2531
|
-
.zoom-controls { width:auto; flex:1; gap:0; }
|
|
2532
|
-
.zoom-divider { margin:0; }
|
|
2533
2492
|
.status .notes-strip { display:none; }
|
|
2534
2493
|
}
|
|
2535
2494
|
@media(pointer:coarse) { .ribbon-tabs {height:45px;min-height:45px;} .ribbon-tabs button {min-height:44px;height:44px;} }
|
|
2536
2495
|
@media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
|
|
2537
2496
|
`;
|
|
2538
2497
|
//#endregion
|
|
2498
|
+
//#region src/styles/index.ts
|
|
2499
|
+
/**
|
|
2500
|
+
* Viewer stylesheets as text for the shadow root. New styles are CSS files here; the legacy
|
|
2501
|
+
* `styles.ts` string moves into this directory when it is next changed.
|
|
2502
|
+
*/
|
|
2503
|
+
var canvasAndRibbonStyles = [":host{--office-foreground:var(--_vv-ink);--office-muted-foreground:var(--_vv-muted);--office-background:var(--_vv-surface);--office-surface:var(--_vv-accent-soft);--office-border:var(--_vv-border);--office-accent:var(--_vv-accent);--office-accent-foreground:var(--vv-accent-ink,#fff);--office-ring:var(--vv-focus,var(--_vv-accent));--office-font:\"Segoe UI\", Arial, sans-serif}.ribbon-row{background:0 0;border:0;flex-wrap:nowrap;align-items:stretch;gap:0;padding:0;display:flex}.ribbon-row office-ui-ribbon-group{min-width:0}.ribbon-row office-ui-button{--office-target-size:28px}.ribbon-row office-ui-button[variant=stacked]::part(button){min-width:64px;min-height:62px;font-size:12px}office-ui-button[disabled]::part(button){opacity:.45}.page-tabs{flex:none}office-ui-status-bar.status{background:var(--_vv-surface);min-height:29px;color:var(--_vv-muted);gap:16px;padding:0 8px 0 12px;font-size:11px}office-ui-status-item[value=\"\"]{display:none}office-ui-status-item[data-page-status]{color:var(--_vv-ink)}.status-message{flex:1;min-width:0}.viewport[data-tool=rectangle],.viewport[data-tool=rectangle] .paper [data-shape-id]{cursor:crosshair}.viewport[data-tool=rectangle]{touch-action:none}.draw-preview{fill:var(--_vv-accent-soft);stroke:var(--_vv-accent);stroke-width:1.5px;stroke-dasharray:4 3;pointer-events:none}.viewport[data-grid=true] .paper{--_vv-grid-minor:#00000012;--_vv-grid-major:#00000029;background-image:linear-gradient(to right, var(--_vv-grid-major) 1px, transparent 1px), linear-gradient(to top, var(--_vv-grid-major) 1px, transparent 1px), linear-gradient(to right, var(--_vv-grid-minor) 1px, transparent 1px), linear-gradient(to top, var(--_vv-grid-minor) 1px, transparent 1px);background-size:var(--vv-inch,96px) var(--vv-inch,96px), var(--vv-inch,96px) var(--vv-inch,96px), calc(var(--vv-inch,96px) / 4) calc(var(--vv-inch,96px) / 4), calc(var(--vv-inch,96px) / 4) calc(var(--vv-inch,96px) / 4);background-position:0 100%}:host(:fullscreen){width:100vw;height:100vh}@media (width<=760px){.ribbon-row{flex:1;min-width:0}.undo-group{flex:none}.ribbon-row office-ui-button[variant=stacked]::part(button){min-width:52px;min-height:44px}.ribbon-tools .tools-content office-ui-ribbon-group{width:100%}.search-group{scroll-snap-align:start}office-ui-status-bar.status{gap:4px;min-height:48px}office-ui-status-bar .notes-strip,office-ui-status-bar .status-message,office-ui-status-item[data-shape-status],office-ui-status-item[data-page-status]{display:none}.zoom-controls{--office-zoom-slider-width:0;flex:1;min-width:0}}", ".paper [data-shape-id]:focus,.paper [data-shape-id]:focus-visible{outline:none}.paper [data-shape-id]:focus-visible>[data-geometry]{stroke-dasharray:.08 .05;stroke:var(--vv-focus,var(--_vv-accent))!important;stroke-width:.04px!important}"].join("\n");
|
|
2504
|
+
//#endregion
|
|
2505
|
+
//#region src/ribbon-action.ts
|
|
2506
|
+
/** Event name shared by every ribbon control; detail is the typed action. */
|
|
2507
|
+
var RIBBON_ACTION_EVENT = "ribbon-action";
|
|
2508
|
+
/** Internal, non-composed event: it stays inside the viewer's shadow tree. */
|
|
2509
|
+
function emitRibbonAction(target, action) {
|
|
2510
|
+
target.dispatchEvent(new CustomEvent(RIBBON_ACTION_EVENT, {
|
|
2511
|
+
detail: action,
|
|
2512
|
+
bubbles: true
|
|
2513
|
+
}));
|
|
2514
|
+
}
|
|
2515
|
+
//#endregion
|
|
2516
|
+
//#region src/ribbon-parts.ts
|
|
2517
|
+
/**
|
|
2518
|
+
* A large stacked ribbon command. `id` is a stable name for state sync and tests; the typed
|
|
2519
|
+
* `action` is what runs. Labels, icons and shortcuts are static application strings.
|
|
2520
|
+
*/
|
|
2521
|
+
function tool(doc, id, label, icon, action, options = {}) {
|
|
2522
|
+
const el = doc.createElement("office-ui-button");
|
|
2523
|
+
el.setAttribute("variant", "stacked");
|
|
2524
|
+
el.setAttribute("command", id);
|
|
2525
|
+
el.setAttribute("icon", icon);
|
|
2526
|
+
el.setAttribute("label", label);
|
|
2527
|
+
el.setAttribute("title", options.keys ? `${label} (${options.keys[1]})` : label);
|
|
2528
|
+
if (options.keys) el.setAttribute("keyshortcuts", options.keys[0]);
|
|
2529
|
+
if (options.pressed !== void 0) el.setAttribute("pressed", String(options.pressed));
|
|
2530
|
+
if (options.controls) el.setAttribute("aria-controls", options.controls);
|
|
2531
|
+
el.addEventListener("office-command", (event) => {
|
|
2532
|
+
event.stopPropagation();
|
|
2533
|
+
emitRibbonAction(el, action);
|
|
2534
|
+
});
|
|
2535
|
+
return el;
|
|
2536
|
+
}
|
|
2537
|
+
/** A labelled ribbon group (the shared `office-ui-ribbon-group`). */
|
|
2538
|
+
function group(doc, label, children, className = "") {
|
|
2539
|
+
const el = doc.createElement("office-ui-ribbon-group");
|
|
2540
|
+
el.setAttribute("label", label);
|
|
2541
|
+
if (className) el.className = className;
|
|
2542
|
+
el.append(...children);
|
|
2543
|
+
return el;
|
|
2544
|
+
}
|
|
2545
|
+
/** A keyboard-navigable row of groups (the shared `office-ui-toolbar`). */
|
|
2546
|
+
function commandRow(doc, label, children) {
|
|
2547
|
+
const el = doc.createElement("office-ui-toolbar");
|
|
2548
|
+
el.className = "ribbon-row";
|
|
2549
|
+
el.setAttribute("aria-label", label);
|
|
2550
|
+
el.append(...children);
|
|
2551
|
+
return el;
|
|
2552
|
+
}
|
|
2553
|
+
/** Muted two-line guidance at the end of a ribbon panel. */
|
|
2554
|
+
function hint(doc, lines) {
|
|
2555
|
+
const el = doc.createElement("p");
|
|
2556
|
+
el.className = "ribbon-hint";
|
|
2557
|
+
lines.forEach((line, index) => {
|
|
2558
|
+
if (index) el.append(doc.createElement("br"));
|
|
2559
|
+
el.append(line);
|
|
2560
|
+
});
|
|
2561
|
+
return el;
|
|
2562
|
+
}
|
|
2563
|
+
//#endregion
|
|
2564
|
+
//#region src/viewer-search.ts
|
|
2565
|
+
/** Builds the Find group: a search field, previous/next result buttons and a live status. */
|
|
2566
|
+
function createSearchGroup(doc) {
|
|
2567
|
+
const group = doc.createElement("div");
|
|
2568
|
+
group.className = "ribbon-group search-group";
|
|
2569
|
+
group.setAttribute("role", "group");
|
|
2570
|
+
group.setAttribute("aria-label", "Find");
|
|
2571
|
+
const controls = doc.createElement("div");
|
|
2572
|
+
controls.className = "search-controls";
|
|
2573
|
+
controls.setAttribute("role", "search");
|
|
2574
|
+
controls.setAttribute("aria-label", "Diagram text search");
|
|
2575
|
+
const label = doc.createElement("label");
|
|
2576
|
+
label.append("Find ");
|
|
2577
|
+
const input = doc.createElement("input");
|
|
2578
|
+
input.type = "search";
|
|
2579
|
+
input.setAttribute("aria-label", "Search diagram text");
|
|
2580
|
+
input.placeholder = "Text across pages";
|
|
2581
|
+
input.maxLength = TEXT_SEARCH_LIMITS.queryCharacters;
|
|
2582
|
+
input.autocomplete = "off";
|
|
2583
|
+
input.spellcheck = false;
|
|
2584
|
+
input.setAttribute("aria-describedby", "search-status");
|
|
2585
|
+
label.append(input);
|
|
2586
|
+
const step = (action, name, text) => {
|
|
2587
|
+
const button = doc.createElement("button");
|
|
2588
|
+
button.type = "button";
|
|
2589
|
+
button.dataset.action = action;
|
|
2590
|
+
button.setAttribute("aria-label", name);
|
|
2591
|
+
button.textContent = text;
|
|
2592
|
+
return button;
|
|
2593
|
+
};
|
|
2594
|
+
const status = doc.createElement("span");
|
|
2595
|
+
status.id = "search-status";
|
|
2596
|
+
status.setAttribute("role", "status");
|
|
2597
|
+
status.setAttribute("aria-live", "polite");
|
|
2598
|
+
status.setAttribute("aria-atomic", "true");
|
|
2599
|
+
controls.append(label, step("search-previous", "Previous matching shape", "Previous"), step("search-next", "Next matching shape", "Next"), status);
|
|
2600
|
+
const caption = doc.createElement("span");
|
|
2601
|
+
caption.className = "group-label";
|
|
2602
|
+
caption.textContent = "Find in drawing";
|
|
2603
|
+
group.append(controls, caption);
|
|
2604
|
+
return group;
|
|
2605
|
+
}
|
|
2606
|
+
function searchControls(root) {
|
|
2607
|
+
return {
|
|
2608
|
+
input: root.querySelector("input[type=\"search\"]"),
|
|
2609
|
+
status: root.querySelector("#search-status"),
|
|
2610
|
+
previous: root.querySelector("[data-action=\"search-previous\"]"),
|
|
2611
|
+
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
function renderSearchControls(controls, state) {
|
|
2615
|
+
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2616
|
+
controls.input.disabled = !state.document?.pages.length;
|
|
2617
|
+
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2618
|
+
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2619
|
+
const result = state.search.results[state.search.activeIndex];
|
|
2620
|
+
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2621
|
+
}
|
|
2622
|
+
//#endregion
|
|
2623
|
+
//#region src/ribbon-home.ts
|
|
2624
|
+
/**
|
|
2625
|
+
* Visio's Home tab, limited to commands this build can honestly perform: Undo, Tools and
|
|
2626
|
+
* Editing. Clipboard, Font, Paragraph, Shape Styles and Arrange wait for core edit commands.
|
|
2627
|
+
* The compact (phone) layout folds every group but Undo into the Tools disclosure.
|
|
2628
|
+
*/
|
|
2629
|
+
function buildHomePanel(doc, panel) {
|
|
2630
|
+
const tools = doc.createElement("details");
|
|
2631
|
+
tools.className = "ribbon-tools";
|
|
2632
|
+
tools.open = true;
|
|
2633
|
+
const summary = doc.createElement("summary");
|
|
2634
|
+
const caret = doc.createElement("span");
|
|
2635
|
+
caret.setAttribute("aria-hidden", "true");
|
|
2636
|
+
caret.textContent = "⌄";
|
|
2637
|
+
summary.append("Tools ", caret);
|
|
2638
|
+
const content = doc.createElement("div");
|
|
2639
|
+
content.className = "tools-content";
|
|
2640
|
+
content.append(group(doc, "Tools", [
|
|
2641
|
+
tool(doc, "pointer", "Pointer Tool", "pointer", {
|
|
2642
|
+
type: "tool",
|
|
2643
|
+
tool: "pointer"
|
|
2644
|
+
}, {
|
|
2645
|
+
keys: ["Control+1", "Ctrl+1"],
|
|
2646
|
+
pressed: true
|
|
2647
|
+
}),
|
|
2648
|
+
tool(doc, "rectangle", "Rectangle", "rectangle", {
|
|
2649
|
+
type: "tool",
|
|
2650
|
+
tool: "rectangle"
|
|
2651
|
+
}, {
|
|
2652
|
+
keys: ["Control+8", "Ctrl+8"],
|
|
2653
|
+
pressed: false
|
|
2654
|
+
}),
|
|
2655
|
+
tool(doc, "edit", "Edit text", "pencil", {
|
|
2656
|
+
type: "reveal",
|
|
2657
|
+
panel: "edit"
|
|
2658
|
+
}, { keys: ["F2", "F2"] })
|
|
2659
|
+
]), createSearchGroup(doc), group(doc, "Editing", [
|
|
2660
|
+
tool(doc, "delete", "Delete", "trash", { type: "delete" }, { keys: ["Delete", "Del"] }),
|
|
2661
|
+
tool(doc, "layers", "Layers", "visioLayers", {
|
|
2662
|
+
type: "reveal",
|
|
2663
|
+
panel: "layers"
|
|
2664
|
+
}),
|
|
2665
|
+
tool(doc, "selection", "Shape details", "visioShapeData", {
|
|
2666
|
+
type: "reveal",
|
|
2667
|
+
panel: "selection"
|
|
2668
|
+
})
|
|
2669
|
+
]));
|
|
2670
|
+
tools.append(summary, content);
|
|
2671
|
+
panel.append(commandRow(doc, "Home commands", [group(doc, "Undo", [tool(doc, "undo", "Undo", "undo", {
|
|
2672
|
+
type: "history",
|
|
2673
|
+
key: "undo"
|
|
2674
|
+
}, { keys: ["Control+Z", "Ctrl+Z"] }), tool(doc, "redo", "Redo", "redo", {
|
|
2675
|
+
type: "history",
|
|
2676
|
+
key: "redo"
|
|
2677
|
+
}, { keys: ["Control+Y", "Ctrl+Y"] })], "undo-group"), tools]), hint(doc, ["Rectangle: drag on the page to draw.", "Editing is experimental. Keep your original file."]));
|
|
2678
|
+
}
|
|
2679
|
+
//#endregion
|
|
2680
|
+
//#region src/ribbon-view.ts
|
|
2681
|
+
/** Visio's View tab: Views, Show (grid and task panes) and Zoom. */
|
|
2682
|
+
function buildViewPanel(doc, panel) {
|
|
2683
|
+
panel.append(commandRow(doc, "View commands", [
|
|
2684
|
+
group(doc, "Views", [tool(doc, "fullscreen", "Full Screen", "fullscreen", { type: "fullscreen" }, {
|
|
2685
|
+
keys: ["F5", "F5"],
|
|
2686
|
+
pressed: false
|
|
2687
|
+
})]),
|
|
2688
|
+
group(doc, "Show", [
|
|
2689
|
+
tool(doc, "grid", "Grid", "grid", { type: "grid" }, { pressed: false }),
|
|
2690
|
+
tool(doc, "pages", "Pages pane", "visioPagesPane", {
|
|
2691
|
+
type: "pane",
|
|
2692
|
+
pane: "pages"
|
|
2693
|
+
}, {
|
|
2694
|
+
pressed: true,
|
|
2695
|
+
controls: "page-rail"
|
|
2696
|
+
}),
|
|
2697
|
+
tool(doc, "inspector", "Inspector pane", "visioInspectorPane", {
|
|
2698
|
+
type: "pane",
|
|
2699
|
+
pane: "inspector"
|
|
2700
|
+
}, {
|
|
2701
|
+
pressed: true,
|
|
2702
|
+
controls: "inspector-pane"
|
|
2703
|
+
}),
|
|
2704
|
+
tool(doc, "notes", "Review notes", "message", {
|
|
2705
|
+
type: "reveal",
|
|
2706
|
+
panel: "notes"
|
|
2707
|
+
})
|
|
2708
|
+
]),
|
|
2709
|
+
group(doc, "Zoom", [
|
|
2710
|
+
tool(doc, "zoom-fit", "Fit to Window", "fitPage", {
|
|
2711
|
+
type: "zoom",
|
|
2712
|
+
mode: "fit"
|
|
2713
|
+
}, { keys: ["Control+Shift+W", "Ctrl+Shift+W"] }),
|
|
2714
|
+
tool(doc, "page-width", "Page Width", "pageWidth", {
|
|
2715
|
+
type: "zoom",
|
|
2716
|
+
mode: "width"
|
|
2717
|
+
}),
|
|
2718
|
+
tool(doc, "actual-size", "100%", "search", {
|
|
2719
|
+
type: "zoom",
|
|
2720
|
+
mode: "actual"
|
|
2721
|
+
})
|
|
2722
|
+
])
|
|
2723
|
+
]), hint(doc, ["Ctrl+wheel or + / − to zoom.", "Ctrl+Page Up / Page Down changes page."]));
|
|
2724
|
+
}
|
|
2725
|
+
//#endregion
|
|
2726
|
+
//#region src/ribbon.ts
|
|
2727
|
+
var TABS = [[
|
|
2728
|
+
"home",
|
|
2729
|
+
"Home",
|
|
2730
|
+
buildHomePanel
|
|
2731
|
+
], [
|
|
2732
|
+
"view",
|
|
2733
|
+
"View",
|
|
2734
|
+
buildViewPanel
|
|
2735
|
+
]];
|
|
2736
|
+
/**
|
|
2737
|
+
* The Visio ribbon: a tab list and one panel per tab. Tab switching and keyboard movement
|
|
2738
|
+
* belong to the chrome; commands emit `ribbon-action` events for the router.
|
|
2739
|
+
*/
|
|
2740
|
+
function createRibbon(doc) {
|
|
2741
|
+
const toolbar = doc.createElement("div");
|
|
2742
|
+
toolbar.className = "toolbar";
|
|
2743
|
+
toolbar.setAttribute("role", "group");
|
|
2744
|
+
toolbar.setAttribute("aria-label", "Diagram controls");
|
|
2745
|
+
const tabs = doc.createElement("div");
|
|
2746
|
+
tabs.className = "ribbon-tabs";
|
|
2747
|
+
tabs.setAttribute("role", "tablist");
|
|
2748
|
+
tabs.setAttribute("aria-label", "Ribbon");
|
|
2749
|
+
toolbar.append(tabs);
|
|
2750
|
+
for (const [key, name, build] of TABS) {
|
|
2751
|
+
const selected = key === "home";
|
|
2752
|
+
const tab = doc.createElement("button");
|
|
2753
|
+
tab.type = "button";
|
|
2754
|
+
tab.id = `${key}-tab`;
|
|
2755
|
+
tab.dataset.tab = key;
|
|
2756
|
+
tab.textContent = name;
|
|
2757
|
+
tab.setAttribute("role", "tab");
|
|
2758
|
+
tab.setAttribute("aria-selected", String(selected));
|
|
2759
|
+
tab.setAttribute("aria-controls", `${key}-panel`);
|
|
2760
|
+
if (!selected) tab.tabIndex = -1;
|
|
2761
|
+
const panel = doc.createElement("div");
|
|
2762
|
+
panel.className = "ribbon-content";
|
|
2763
|
+
panel.id = `${key}-panel`;
|
|
2764
|
+
panel.setAttribute("role", "tabpanel");
|
|
2765
|
+
panel.setAttribute("aria-labelledby", tab.id);
|
|
2766
|
+
panel.hidden = !selected;
|
|
2767
|
+
build(doc, panel);
|
|
2768
|
+
tabs.append(tab);
|
|
2769
|
+
toolbar.append(panel);
|
|
2770
|
+
}
|
|
2771
|
+
return toolbar;
|
|
2772
|
+
}
|
|
2773
|
+
//#endregion
|
|
2774
|
+
//#region src/status-bar.ts
|
|
2775
|
+
/**
|
|
2776
|
+
* Visio's status bar on the shared `office-ui-status-bar`: page and shape metrics, a live
|
|
2777
|
+
* message region, compatibility notes, the host footer slot and the shared zoom slider.
|
|
2778
|
+
*/
|
|
2779
|
+
function createStatusBar(doc) {
|
|
2780
|
+
const bar = doc.createElement("office-ui-status-bar");
|
|
2781
|
+
bar.className = "status";
|
|
2782
|
+
bar.setAttribute("label", "Status bar");
|
|
2783
|
+
const item = (name) => {
|
|
2784
|
+
const el = doc.createElement("office-ui-status-item");
|
|
2785
|
+
el.setAttribute(name, "");
|
|
2786
|
+
el.setAttribute("label", "");
|
|
2787
|
+
el.setAttribute("value", "");
|
|
2788
|
+
return el;
|
|
2789
|
+
};
|
|
2790
|
+
const message = doc.createElement("div");
|
|
2791
|
+
message.className = "status-message";
|
|
2792
|
+
message.setAttribute("role", "status");
|
|
2793
|
+
const text = doc.createElement("span");
|
|
2794
|
+
text.dataset.status = "";
|
|
2795
|
+
const diagnostics = doc.createElement("span");
|
|
2796
|
+
diagnostics.dataset.diagnostics = "";
|
|
2797
|
+
message.append(text, diagnostics);
|
|
2798
|
+
const notes = doc.createElement("div");
|
|
2799
|
+
notes.className = "notes-strip";
|
|
2800
|
+
const notesButton = doc.createElement("button");
|
|
2801
|
+
notesButton.type = "button";
|
|
2802
|
+
notesButton.dataset.chrome = "notes";
|
|
2803
|
+
notesButton.setAttribute("aria-controls", "inspector-pane");
|
|
2804
|
+
notesButton.setAttribute("aria-expanded", "false");
|
|
2805
|
+
const count = doc.createElement("span");
|
|
2806
|
+
count.className = "notes-count";
|
|
2807
|
+
count.dataset.noteCount = "";
|
|
2808
|
+
notesButton.append("Notes", count);
|
|
2809
|
+
const local = doc.createElement("span");
|
|
2810
|
+
local.textContent = "Files stay in your browser";
|
|
2811
|
+
notes.append(notesButton, local);
|
|
2812
|
+
const footer = doc.createElement("slot");
|
|
2813
|
+
footer.name = "workspace-footer";
|
|
2814
|
+
const zoom = doc.createElement("office-ui-zoom-slider");
|
|
2815
|
+
zoom.slot = "end";
|
|
2816
|
+
zoom.className = "zoom-controls";
|
|
2817
|
+
zoom.setAttribute("label", "Canvas zoom");
|
|
2818
|
+
zoom.setAttribute("fit", "Fit page to current window");
|
|
2819
|
+
zoom.setAttribute("value", "100");
|
|
2820
|
+
bar.append(item("data-page-status"), item("data-shape-status"), message, notes, footer, zoom);
|
|
2821
|
+
return bar;
|
|
2822
|
+
}
|
|
2823
|
+
/** Visio's page tabs under the drawing, on the shared document tab strip. */
|
|
2824
|
+
function createPageTabs(doc) {
|
|
2825
|
+
const strip = doc.createElement("office-ui-tab-strip");
|
|
2826
|
+
strip.className = "page-tabs";
|
|
2827
|
+
strip.setAttribute("label", "Pages");
|
|
2828
|
+
strip.setAttribute("previous-label", "Previous page");
|
|
2829
|
+
strip.setAttribute("next-label", "Next page");
|
|
2830
|
+
return strip;
|
|
2831
|
+
}
|
|
2832
|
+
//#endregion
|
|
2539
2833
|
//#region src/edit-error.ts
|
|
2540
2834
|
/** Core error codes remain visible for a refused transaction; document content is plain text. */
|
|
2541
2835
|
function editErrorMessage(error) {
|
|
@@ -2823,59 +3117,13 @@ var ViewerEditControls = class {
|
|
|
2823
3117
|
}
|
|
2824
3118
|
};
|
|
2825
3119
|
//#endregion
|
|
2826
|
-
//#region src/viewer-search.ts
|
|
2827
|
-
var searchTemplate = `<div class="search-controls" role="search" aria-label="Diagram text search"><label>Find <input type="search" aria-label="Search diagram text" placeholder="Text across pages" maxlength="${TEXT_SEARCH_LIMITS.queryCharacters}" autocomplete="off" spellcheck="false" aria-describedby="search-status"></label><button type="button" data-action="search-previous" aria-label="Previous matching shape">Previous</button><button type="button" data-action="search-next" aria-label="Next matching shape">Next</button><span id="search-status" role="status" aria-live="polite" aria-atomic="true"></span></div>`;
|
|
2828
|
-
function searchControls(root) {
|
|
2829
|
-
return {
|
|
2830
|
-
input: root.querySelector("input[type=\"search\"]"),
|
|
2831
|
-
status: root.querySelector("#search-status"),
|
|
2832
|
-
previous: root.querySelector("[data-action=\"search-previous\"]"),
|
|
2833
|
-
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2834
|
-
};
|
|
2835
|
-
}
|
|
2836
|
-
function renderSearchControls(controls, state) {
|
|
2837
|
-
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2838
|
-
controls.input.disabled = !state.document?.pages.length;
|
|
2839
|
-
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2840
|
-
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2841
|
-
const result = state.search.results[state.search.activeIndex];
|
|
2842
|
-
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2843
|
-
}
|
|
2844
|
-
//#endregion
|
|
2845
3120
|
//#region src/viewer-chrome.ts
|
|
2846
|
-
/**
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
<button type="button" id="view-tab" role="tab" aria-selected="false" aria-controls="view-panel" tabindex="-1" data-tab="view">View</button>
|
|
2853
|
-
</div>
|
|
2854
|
-
<div class="ribbon-content" id="home-panel" role="tabpanel" aria-labelledby="home-tab">
|
|
2855
|
-
<details class="ribbon-tools" open><summary>Tools <span aria-hidden="true">⌄</span></summary><div class="tools-content"><div class="ribbon-group search-group">${searchTemplate}<span class="group-label">Find in drawing</span></div>
|
|
2856
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2857
|
-
<button class="ribbon-command" type="button" data-chrome="selection"><span class="command-icon" data-icon="selection" aria-hidden="true"></span><span>Shape details</span></button>
|
|
2858
|
-
<button class="ribbon-command" type="button" data-chrome="edit"><span class="command-icon" data-icon="edit" aria-hidden="true"></span><span>Edit text</span></button>
|
|
2859
|
-
<button class="ribbon-command" type="button" data-chrome="layers"><span class="command-icon" data-icon="layers" aria-hidden="true"></span><span>Layers</span></button>
|
|
2860
|
-
</div><span class="group-label">Inspect and edit</span></div>
|
|
2861
|
-
</div></details>
|
|
2862
|
-
<div class="ribbon-primary">
|
|
2863
|
-
<label class="page-picker">Page <select aria-label="Page"></select></label>
|
|
2864
|
-
<div class="page-stepper"><button type="button" data-chrome="previous-page" aria-label="Previous page">‹</button><button type="button" data-chrome="next-page" aria-label="Next page">›</button></div>
|
|
2865
|
-
<span class="spacer"></span><span class="local-label">Local workspace</span>
|
|
2866
|
-
</div>
|
|
2867
|
-
<p class="ribbon-hint">Select a shape to inspect its details.<br>Text editing is experimental.</p>
|
|
2868
|
-
</div>
|
|
2869
|
-
<div class="ribbon-content" id="view-panel" role="tabpanel" aria-labelledby="view-tab" hidden>
|
|
2870
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2871
|
-
<button class="ribbon-command" type="button" data-chrome="pages" aria-pressed="true" aria-controls="page-rail"><span class="command-icon" data-icon="pages" aria-hidden="true"></span><span>Pages pane</span></button>
|
|
2872
|
-
<button class="ribbon-command" type="button" data-chrome="inspector" aria-pressed="true" aria-controls="inspector-pane"><span class="command-icon" data-icon="inspector" aria-hidden="true"></span><span>Inspector pane</span></button>
|
|
2873
|
-
<button class="ribbon-command" type="button" data-chrome="notes"><span class="command-icon" data-icon="notes" aria-hidden="true"></span><span>Review notes</span></button>
|
|
2874
|
-
</div><span class="group-label">Workspace panes</span></div>
|
|
2875
|
-
<p class="ribbon-hint">Canvas shortcuts: + / − to zoom, 0 to fit.<br>Arrow keys move between focused shapes.</p>
|
|
2876
|
-
</div>
|
|
2877
|
-
</div>
|
|
2878
|
-
<div class="workspace">
|
|
3121
|
+
/**
|
|
3122
|
+
* Static workspace markup (legacy; migrate to builders when next changed). The ribbon, page
|
|
3123
|
+
* tabs and status bar are built by `createRibbon`, `createPageTabs` and `createStatusBar`.
|
|
3124
|
+
* All document labels are inserted as text.
|
|
3125
|
+
*/
|
|
3126
|
+
var viewerChromeTemplate = `<div class="workspace">
|
|
2879
3127
|
<nav id="page-rail" class="page-rail" aria-label="Diagram pages"><div class="pane-heading"><span>Pages</span><span data-page-count>0</span></div><ol class="page-list"></ol><p class="page-empty">Open a drawing to see its pages.</p></nav>
|
|
2880
3128
|
<div class="viewport" tabindex="0" role="region" aria-label="Diagram canvas"></div>
|
|
2881
3129
|
<aside id="inspector-pane" class="inspector-pane" aria-label="Drawing inspector">
|
|
@@ -2888,14 +3136,15 @@ var viewerChromeTemplate = `
|
|
|
2888
3136
|
<details class="notes inspector-card"><summary>Compatibility notes</summary><ul></ul></details>
|
|
2889
3137
|
</div>
|
|
2890
3138
|
</aside>
|
|
2891
|
-
</div
|
|
2892
|
-
<div class="status"><div class="status-message" role="status"><span data-status></span><span data-diagnostics></span></div><div class="notes-strip"><button type="button" data-chrome="notes" aria-controls="inspector-pane" aria-expanded="false">Notes<span class="notes-count" data-note-count></span></button><span>Files stay in your browser</span></div><slot name="workspace-footer"></slot><div class="zoom-controls" role="group" aria-label="Canvas zoom"><button type="button" data-action="fit">Fit page</button><button type="button" data-action="actual">100%</button><span class="zoom-divider"></span><button type="button" data-action="out" aria-label="Zoom out">−</button><output class="zoom" aria-label="Zoom level">100%</output><button type="button" data-action="in" aria-label="Zoom in">+</button></div></div>`;
|
|
3139
|
+
</div>`;
|
|
2893
3140
|
/** Presentation state stays local; page navigation uses the same controller as every binding. */
|
|
2894
3141
|
var ViewerChrome = class {
|
|
2895
3142
|
#root;
|
|
2896
3143
|
#controller;
|
|
2897
3144
|
#document;
|
|
2898
3145
|
#pageList;
|
|
3146
|
+
#pageTabs;
|
|
3147
|
+
#tools;
|
|
2899
3148
|
#rail;
|
|
2900
3149
|
#inspector;
|
|
2901
3150
|
#notes;
|
|
@@ -2907,6 +3156,8 @@ var ViewerChrome = class {
|
|
|
2907
3156
|
this.#root = root;
|
|
2908
3157
|
this.#controller = controller;
|
|
2909
3158
|
this.#pageList = root.querySelector(".page-list");
|
|
3159
|
+
this.#pageTabs = root.querySelector("office-ui-tab-strip");
|
|
3160
|
+
this.#tools = root.querySelector(".ribbon-tools");
|
|
2910
3161
|
this.#rail = root.querySelector(".page-rail");
|
|
2911
3162
|
this.#inspector = root.querySelector(".inspector-pane");
|
|
2912
3163
|
this.#notes = root.querySelector(".notes");
|
|
@@ -2917,7 +3168,7 @@ var ViewerChrome = class {
|
|
|
2917
3168
|
this.#responsive = view?.matchMedia?.("(max-width: 980px)");
|
|
2918
3169
|
const compact = view?.matchMedia?.("(max-width: 760px)");
|
|
2919
3170
|
this.#compact = compact;
|
|
2920
|
-
const tools = this.#
|
|
3171
|
+
const tools = this.#tools;
|
|
2921
3172
|
const toolLayout = () => {
|
|
2922
3173
|
tools.open = !compact?.matches;
|
|
2923
3174
|
};
|
|
@@ -2944,22 +3195,16 @@ var ViewerChrome = class {
|
|
|
2944
3195
|
this.#root.addEventListener("click", (event) => {
|
|
2945
3196
|
const button = event.target?.closest?.("button");
|
|
2946
3197
|
if (!button || button.disabled) return;
|
|
2947
|
-
if (compact?.matches && button.dataset.chrome) tools.open = false;
|
|
2948
3198
|
const tab = button.dataset.tab;
|
|
2949
|
-
if (tab === "home" || tab === "view") this
|
|
3199
|
+
if (tab === "home" || tab === "view") this.showTab(tab);
|
|
2950
3200
|
if (button.dataset.pageIndex !== void 0) this.#controller.setPage(Number(button.dataset.pageIndex));
|
|
2951
3201
|
const action = button.dataset.chrome;
|
|
2952
|
-
if (action === "
|
|
2953
|
-
if (action === "
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
if (action === "inspector") {
|
|
2959
|
-
this.#inspectorManuallyToggled = true;
|
|
2960
|
-
this.#setPane("inspector", this.#inspector.hidden);
|
|
2961
|
-
}
|
|
2962
|
-
if (action === "notes" || action === "selection" || action === "edit" || action === "layers") this.#reveal(action);
|
|
3202
|
+
if (action === "inspector") this.togglePane("inspector");
|
|
3203
|
+
if (action === "notes") this.reveal("notes");
|
|
3204
|
+
}, options);
|
|
3205
|
+
this.#root.addEventListener("office-tab-select", (event) => {
|
|
3206
|
+
const id = Number(event.detail.id);
|
|
3207
|
+
if (Number.isSafeInteger(id)) this.#controller.setPage(id);
|
|
2963
3208
|
}, options);
|
|
2964
3209
|
this.#root.addEventListener("keydown", (event) => {
|
|
2965
3210
|
const key = event;
|
|
@@ -2976,11 +3221,11 @@ var ViewerChrome = class {
|
|
|
2976
3221
|
if (key.key === "ArrowUp" || key.key === "ArrowDown") return;
|
|
2977
3222
|
key.preventDefault();
|
|
2978
3223
|
const tab = key.key === "Home" ? "home" : key.key === "End" ? "view" : button.dataset.tab === "home" ? "view" : "home";
|
|
2979
|
-
this
|
|
3224
|
+
this.showTab(tab);
|
|
2980
3225
|
this.#root.querySelector(`[data-tab="${tab}"]`).focus();
|
|
2981
3226
|
} else if (button.dataset.pageIndex !== void 0) {
|
|
2982
3227
|
key.preventDefault();
|
|
2983
|
-
const pages = [...this.#pageList.querySelectorAll("
|
|
3228
|
+
const pages = [...this.#pageList.querySelectorAll("[data-page-index]")];
|
|
2984
3229
|
const current = Number(button.dataset.pageIndex);
|
|
2985
3230
|
const next = key.key === "Home" ? 0 : key.key === "End" ? pages.length - 1 : Math.max(0, Math.min(pages.length - 1, current + (["ArrowRight", "ArrowDown"].includes(key.key) ? 1 : -1)));
|
|
2986
3231
|
this.#controller.setPage(next);
|
|
@@ -2990,7 +3235,7 @@ var ViewerChrome = class {
|
|
|
2990
3235
|
this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
|
|
2991
3236
|
return () => events.abort();
|
|
2992
3237
|
}
|
|
2993
|
-
|
|
3238
|
+
showTab(tab) {
|
|
2994
3239
|
for (const button of this.#root.querySelectorAll("[data-tab]")) {
|
|
2995
3240
|
const selected = button.dataset.tab === tab;
|
|
2996
3241
|
button.setAttribute("aria-selected", String(selected));
|
|
@@ -2998,27 +3243,41 @@ var ViewerChrome = class {
|
|
|
2998
3243
|
}
|
|
2999
3244
|
for (const panel of this.#root.querySelectorAll("[role=\"tabpanel\"]")) panel.hidden = panel.id !== `${tab}-panel`;
|
|
3000
3245
|
}
|
|
3246
|
+
/** Show or hide a task pane; compact layouts close the Tools menu afterwards. */
|
|
3247
|
+
togglePane(pane) {
|
|
3248
|
+
if (this.#compact?.matches) this.#tools.open = false;
|
|
3249
|
+
if (pane === "pages") this.#pagesManuallyToggled = true;
|
|
3250
|
+
else this.#inspectorManuallyToggled = true;
|
|
3251
|
+
this.#setPane(pane, (pane === "pages" ? this.#rail : this.#inspector).hidden);
|
|
3252
|
+
}
|
|
3253
|
+
#command(name) {
|
|
3254
|
+
return this.#root.querySelector(`office-ui-button[command="${name}"]`);
|
|
3255
|
+
}
|
|
3001
3256
|
#setPane(pane, visible) {
|
|
3002
3257
|
if (visible && this.#compact?.matches) {
|
|
3003
3258
|
const other = pane === "pages" ? "inspector" : "pages";
|
|
3004
3259
|
(other === "pages" ? this.#rail : this.#inspector).hidden = true;
|
|
3005
|
-
this.#
|
|
3260
|
+
this.#command(other).setAttribute("pressed", "false");
|
|
3006
3261
|
}
|
|
3007
3262
|
(pane === "pages" ? this.#rail : this.#inspector).hidden = !visible;
|
|
3008
|
-
this.#
|
|
3263
|
+
this.#command(pane).setAttribute("pressed", String(visible));
|
|
3009
3264
|
this.#syncNotes();
|
|
3010
3265
|
}
|
|
3011
3266
|
#syncNotes() {
|
|
3012
3267
|
this.#root.querySelector(".notes-strip button").setAttribute("aria-expanded", String(!this.#inspector.hidden && this.#notes.open));
|
|
3013
3268
|
}
|
|
3014
|
-
|
|
3269
|
+
/** Open an inspector disclosure; text editing may focus the text field directly (F2). */
|
|
3270
|
+
reveal(kind, focusText = false) {
|
|
3271
|
+
if (this.#compact?.matches) this.#tools.open = false;
|
|
3015
3272
|
this.#inspectorManuallyToggled = true;
|
|
3016
3273
|
this.#setPane("inspector", true);
|
|
3017
3274
|
const selector = kind === "notes" ? ".notes" : kind === "edit" ? ".edit-controls" : kind === "layers" ? ".layer-controls" : ".shape-inspector";
|
|
3018
3275
|
const panel = this.#root.querySelector(selector);
|
|
3019
3276
|
if (panel.hidden) return;
|
|
3020
3277
|
panel.open = true;
|
|
3021
|
-
panel.querySelector("
|
|
3278
|
+
const text = panel.querySelector("textarea");
|
|
3279
|
+
if (focusText && text && !text.disabled) text.focus();
|
|
3280
|
+
else panel.querySelector("summary").focus();
|
|
3022
3281
|
panel.scrollIntoView?.({
|
|
3023
3282
|
block: "nearest",
|
|
3024
3283
|
inline: "nearest"
|
|
@@ -3059,14 +3318,21 @@ var ViewerChrome = class {
|
|
|
3059
3318
|
fragment.append(item);
|
|
3060
3319
|
}
|
|
3061
3320
|
this.#pageList.replaceChildren(fragment);
|
|
3321
|
+
this.#pageTabs.tabs = (state.document?.pages ?? []).map((candidate, index) => ({
|
|
3322
|
+
id: String(index),
|
|
3323
|
+
label: candidate.name,
|
|
3324
|
+
title: candidate.isBackground ? `${candidate.name} (background page)` : candidate.name
|
|
3325
|
+
}));
|
|
3062
3326
|
if (restoreFocus !== void 0) [...this.#pageList.querySelectorAll("button")].find((button) => button.dataset.pageIndex === restoreFocus)?.focus();
|
|
3063
3327
|
}
|
|
3328
|
+
this.#pageTabs.selected = page ? String(state.pageIndex) : "";
|
|
3064
3329
|
for (const button of this.#pageList.querySelectorAll("button")) {
|
|
3065
3330
|
const selected = Number(button.dataset.pageIndex) === state.pageIndex;
|
|
3066
3331
|
if (selected) button.setAttribute("aria-current", "page");
|
|
3067
3332
|
else button.removeAttribute("aria-current");
|
|
3068
3333
|
button.tabIndex = selected ? 0 : -1;
|
|
3069
3334
|
}
|
|
3335
|
+
this.#root.querySelector("[data-page-status]").setAttribute("value", page ? `Page ${state.pageIndex + 1} of ${state.document.pages.length}` : "");
|
|
3070
3336
|
const count = state.document?.pages.length ?? 0;
|
|
3071
3337
|
this.#root.querySelector("[data-page-count]").textContent = String(count);
|
|
3072
3338
|
this.#root.querySelector(".page-empty").hidden = count > 0;
|
|
@@ -3074,17 +3340,405 @@ var ViewerChrome = class {
|
|
|
3074
3340
|
this.#root.querySelector("[data-page-size]").textContent = page ? `${page.width} × ${page.height} in` : "No page";
|
|
3075
3341
|
this.#root.querySelector("[data-page-shapes]").textContent = page ? `${page.shapes.length} top-level` : "No shapes";
|
|
3076
3342
|
this.#root.querySelector(".selection-hint").hidden = !!state.selectedShape;
|
|
3077
|
-
this.#
|
|
3078
|
-
this.#
|
|
3079
|
-
this.#
|
|
3080
|
-
this.#
|
|
3081
|
-
this.#root.querySelector("
|
|
3082
|
-
for (const button of this.#root.querySelectorAll("[data-chrome=\"notes\"]")) button.disabled = noteCount === 0;
|
|
3343
|
+
this.#command("selection").disabled = !state.selectedShape;
|
|
3344
|
+
this.#command("edit").disabled = !page;
|
|
3345
|
+
this.#command("layers").disabled = this.#root.querySelector(".layer-controls").hidden;
|
|
3346
|
+
this.#command("notes").disabled = noteCount === 0;
|
|
3347
|
+
this.#root.querySelector(".notes-strip button").disabled = noteCount === 0;
|
|
3083
3348
|
this.#root.querySelector("[data-note-count]").textContent = noteCount ? ` · ${noteCount}` : "";
|
|
3084
3349
|
this.#syncNotes();
|
|
3085
3350
|
}
|
|
3086
3351
|
};
|
|
3087
3352
|
//#endregion
|
|
3353
|
+
//#region src/ribbon-router.ts
|
|
3354
|
+
/** Routes a ribbon, status-bar or shortcut action to the controller that owns it. */
|
|
3355
|
+
function routeRibbonAction(targets, action) {
|
|
3356
|
+
const { controller } = targets;
|
|
3357
|
+
const page = controller.state.document?.pages[controller.state.pageIndex];
|
|
3358
|
+
switch (action.type) {
|
|
3359
|
+
case "history": return targets.history(action.key);
|
|
3360
|
+
case "delete": return targets.deleteSelection();
|
|
3361
|
+
case "tool": return targets.setTool(action.tool);
|
|
3362
|
+
case "grid": return targets.toggleGrid();
|
|
3363
|
+
case "fullscreen": return targets.toggleFullscreen();
|
|
3364
|
+
case "pane": return targets.togglePane(action.pane);
|
|
3365
|
+
case "reveal": return targets.reveal(action.panel, action.focusText ?? false);
|
|
3366
|
+
case "search": return targets.focusSearch();
|
|
3367
|
+
case "page": return controller.setPage(controller.state.pageIndex + action.step);
|
|
3368
|
+
case "zoom":
|
|
3369
|
+
if (!page) return;
|
|
3370
|
+
if (action.mode === "actual") return controller.setZoom(1);
|
|
3371
|
+
return targets.fit(action.mode === "fit" ? "page" : "width");
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
//#endregion
|
|
3375
|
+
//#region src/viewer-draw-tool.ts
|
|
3376
|
+
/** Visio snaps new geometry to ruler subdivisions; 1/16 inch matches its default fine grid. */
|
|
3377
|
+
var SNAP = 1 / 16;
|
|
3378
|
+
var MIN_SIZE = SNAP;
|
|
3379
|
+
var snap = (value) => Math.round(value / SNAP) * SNAP;
|
|
3380
|
+
/** Next free numeric shape ID on the page tree. Core still rejects any collision. */
|
|
3381
|
+
function nextShapeId(page) {
|
|
3382
|
+
let max = 0;
|
|
3383
|
+
const pending = [...page.shapes];
|
|
3384
|
+
while (pending.length) {
|
|
3385
|
+
const shape = pending.pop();
|
|
3386
|
+
if (/^\d{1,9}$/.test(shape.id)) max = Math.max(max, Number(shape.id));
|
|
3387
|
+
pending.push(...shape.children);
|
|
3388
|
+
}
|
|
3389
|
+
return String(max + 1);
|
|
3390
|
+
}
|
|
3391
|
+
/** Page inches with a top-left origin (SVG user space) from a client point. */
|
|
3392
|
+
function pagePoint(svg, page, event) {
|
|
3393
|
+
const matrix = svg.getScreenCTM?.();
|
|
3394
|
+
if (!matrix) return void 0;
|
|
3395
|
+
const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse());
|
|
3396
|
+
return {
|
|
3397
|
+
x: snap(Math.max(0, Math.min(page.width, point.x))),
|
|
3398
|
+
y: snap(Math.max(0, Math.min(page.height, point.y)))
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
/** Drag-to-draw rectangle tool. The preview is viewer-only; the shape is created by core. */
|
|
3402
|
+
var RectangleDrawTool = class {
|
|
3403
|
+
viewport;
|
|
3404
|
+
controller;
|
|
3405
|
+
options;
|
|
3406
|
+
#drag;
|
|
3407
|
+
#request = 0;
|
|
3408
|
+
constructor(viewport, controller, options) {
|
|
3409
|
+
this.viewport = viewport;
|
|
3410
|
+
this.controller = controller;
|
|
3411
|
+
this.options = options;
|
|
3412
|
+
}
|
|
3413
|
+
get drawing() {
|
|
3414
|
+
return !!this.#drag;
|
|
3415
|
+
}
|
|
3416
|
+
wire() {
|
|
3417
|
+
const events = new ((this.viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
3418
|
+
const options = { signal: events.signal };
|
|
3419
|
+
const viewport = this.viewport;
|
|
3420
|
+
viewport.addEventListener("click", (event) => {
|
|
3421
|
+
if (this.options.active()) event.stopImmediatePropagation();
|
|
3422
|
+
}, {
|
|
3423
|
+
...options,
|
|
3424
|
+
capture: true
|
|
3425
|
+
});
|
|
3426
|
+
viewport.addEventListener("pointerdown", (event) => this.#start(event), options);
|
|
3427
|
+
viewport.addEventListener("pointermove", (event) => this.#move(event), options);
|
|
3428
|
+
viewport.addEventListener("pointerup", (event) => void this.#finish(event), options);
|
|
3429
|
+
viewport.addEventListener("pointercancel", () => this.#cancel(), options);
|
|
3430
|
+
viewport.addEventListener("keydown", (event) => {
|
|
3431
|
+
if (event.key === "Escape" && this.#drag) {
|
|
3432
|
+
event.preventDefault();
|
|
3433
|
+
this.#cancel();
|
|
3434
|
+
}
|
|
3435
|
+
}, options);
|
|
3436
|
+
return () => {
|
|
3437
|
+
++this.#request;
|
|
3438
|
+
this.#cancel();
|
|
3439
|
+
events.abort();
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
#start(event) {
|
|
3443
|
+
if (!this.options.active() || event.button !== 0 || this.#drag) return;
|
|
3444
|
+
const state = this.controller.state;
|
|
3445
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3446
|
+
const svg = this.viewport.querySelector("svg.paper");
|
|
3447
|
+
if (!page || !svg || !state.edit.sourceAvailable || state.edit.busy) return;
|
|
3448
|
+
const start = pagePoint(svg, page, event);
|
|
3449
|
+
if (!start) return;
|
|
3450
|
+
event.preventDefault();
|
|
3451
|
+
const rect = this.viewport.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
3452
|
+
rect.classList.add("draw-preview");
|
|
3453
|
+
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
3454
|
+
svg.append(rect);
|
|
3455
|
+
this.#drag = {
|
|
3456
|
+
pointer: event.pointerId,
|
|
3457
|
+
svg,
|
|
3458
|
+
page,
|
|
3459
|
+
...start,
|
|
3460
|
+
rect
|
|
3461
|
+
};
|
|
3462
|
+
this.viewport.setPointerCapture?.(event.pointerId);
|
|
3463
|
+
this.#update(start);
|
|
3464
|
+
}
|
|
3465
|
+
#move(event) {
|
|
3466
|
+
if (!this.#drag || event.pointerId !== this.#drag.pointer) return;
|
|
3467
|
+
const point = pagePoint(this.#drag.svg, this.#drag.page, event);
|
|
3468
|
+
if (point) this.#update(point);
|
|
3469
|
+
}
|
|
3470
|
+
#update(point) {
|
|
3471
|
+
const { x, y, rect } = this.#drag;
|
|
3472
|
+
rect.setAttribute("x", String(Math.min(x, point.x)));
|
|
3473
|
+
rect.setAttribute("y", String(Math.min(y, point.y)));
|
|
3474
|
+
rect.setAttribute("width", String(Math.abs(point.x - x)));
|
|
3475
|
+
rect.setAttribute("height", String(Math.abs(point.y - y)));
|
|
3476
|
+
}
|
|
3477
|
+
#cancel() {
|
|
3478
|
+
this.#drag?.rect.remove();
|
|
3479
|
+
this.#drag = void 0;
|
|
3480
|
+
}
|
|
3481
|
+
async #finish(event) {
|
|
3482
|
+
const drag = this.#drag;
|
|
3483
|
+
if (!drag || event.pointerId !== drag.pointer) return;
|
|
3484
|
+
const end = pagePoint(drag.svg, drag.page, event) ?? drag;
|
|
3485
|
+
this.#cancel();
|
|
3486
|
+
const width = Math.abs(end.x - drag.x), height = Math.abs(end.y - drag.y);
|
|
3487
|
+
if (width < MIN_SIZE || height < MIN_SIZE) {
|
|
3488
|
+
this.options.announce("Drag on the page to draw a rectangle.");
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
const state = this.controller.state;
|
|
3492
|
+
if (state.document?.pages[state.pageIndex] !== drag.page) return;
|
|
3493
|
+
const shapeId = nextShapeId(drag.page);
|
|
3494
|
+
const pageId = drag.page.id;
|
|
3495
|
+
const request = ++this.#request;
|
|
3496
|
+
try {
|
|
3497
|
+
await this.controller.applyEdits([{
|
|
3498
|
+
type: "create-rectangle",
|
|
3499
|
+
pageId,
|
|
3500
|
+
shapeId,
|
|
3501
|
+
x: (drag.x + end.x) / 2,
|
|
3502
|
+
y: drag.page.height - (drag.y + end.y) / 2,
|
|
3503
|
+
width,
|
|
3504
|
+
height
|
|
3505
|
+
}]);
|
|
3506
|
+
if (request !== this.#request) return;
|
|
3507
|
+
const created = this.controller.state.document?.pages.find((page) => page.id === pageId)?.shapes.find((shape) => shape.id === shapeId);
|
|
3508
|
+
if (created) this.controller.selectShape({
|
|
3509
|
+
id: shapeId,
|
|
3510
|
+
name: created.name,
|
|
3511
|
+
pageId
|
|
3512
|
+
});
|
|
3513
|
+
this.options.announce(`Rectangle ${shapeId} added (${+width.toFixed(4)} × ${+height.toFixed(4)} in).`);
|
|
3514
|
+
} catch (error) {
|
|
3515
|
+
if (request === this.#request && !isEditCancellation(error)) this.options.announce(editErrorMessage(error));
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
};
|
|
3519
|
+
//#endregion
|
|
3520
|
+
//#region src/viewer-commands.ts
|
|
3521
|
+
var editable = (target) => target instanceof Element && !!target.closest("input, textarea, select, [contenteditable]:not([contenteditable=\"false\"])");
|
|
3522
|
+
/**
|
|
3523
|
+
* Visio command state (tool, grid, pending edits) and keyboard shortcuts. Ribbon controls emit
|
|
3524
|
+
* typed `ribbon-action` events and shortcuts build the same actions; both go through
|
|
3525
|
+
* `routeRibbonAction`. Document mutation stays in the controller and core.
|
|
3526
|
+
*/
|
|
3527
|
+
var ViewerCommands = class {
|
|
3528
|
+
host;
|
|
3529
|
+
#tool = "pointer";
|
|
3530
|
+
#grid = false;
|
|
3531
|
+
#pending = 0;
|
|
3532
|
+
#draw;
|
|
3533
|
+
#targets;
|
|
3534
|
+
constructor(host) {
|
|
3535
|
+
this.host = host;
|
|
3536
|
+
this.#draw = new RectangleDrawTool(host.viewport, host.controller, {
|
|
3537
|
+
active: () => this.#tool === "rectangle",
|
|
3538
|
+
announce: host.announce
|
|
3539
|
+
});
|
|
3540
|
+
this.#targets = {
|
|
3541
|
+
controller: host.controller,
|
|
3542
|
+
history: (key) => this.#history(key),
|
|
3543
|
+
deleteSelection: () => this.#delete(),
|
|
3544
|
+
setTool: (tool) => this.setTool(tool),
|
|
3545
|
+
toggleGrid: () => {
|
|
3546
|
+
this.#grid = !this.#grid;
|
|
3547
|
+
this.render(host.controller.state);
|
|
3548
|
+
},
|
|
3549
|
+
toggleFullscreen: () => this.#toggleFullscreen(),
|
|
3550
|
+
togglePane: host.togglePane,
|
|
3551
|
+
reveal: host.reveal,
|
|
3552
|
+
fit: host.fit,
|
|
3553
|
+
focusSearch: host.focusSearch
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
get tool() {
|
|
3557
|
+
return this.#tool;
|
|
3558
|
+
}
|
|
3559
|
+
/** Run one typed action, exactly as a ribbon control or shortcut would. */
|
|
3560
|
+
run(action) {
|
|
3561
|
+
routeRibbonAction(this.#targets, action);
|
|
3562
|
+
}
|
|
3563
|
+
wire() {
|
|
3564
|
+
const { root, viewport } = this.host;
|
|
3565
|
+
const doc = root.ownerDocument;
|
|
3566
|
+
const events = new ((doc.defaultView?.AbortController) ?? AbortController)();
|
|
3567
|
+
const options = { signal: events.signal };
|
|
3568
|
+
root.addEventListener(RIBBON_ACTION_EVENT, (event) => this.run(event.detail), options);
|
|
3569
|
+
root.addEventListener("office-command", (event) => {
|
|
3570
|
+
if (event.detail?.command === "zoom-fit") this.run({
|
|
3571
|
+
type: "zoom",
|
|
3572
|
+
mode: "fit"
|
|
3573
|
+
});
|
|
3574
|
+
}, options);
|
|
3575
|
+
root.addEventListener("keydown", (event) => this.#shortcut(event), options);
|
|
3576
|
+
viewport.addEventListener("wheel", (event) => {
|
|
3577
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
3578
|
+
event.preventDefault();
|
|
3579
|
+
const zoom = this.host.controller.state.zoom;
|
|
3580
|
+
this.host.controller.setZoom(event.deltaY < 0 ? zoom * 1.1 : zoom / 1.1);
|
|
3581
|
+
}, {
|
|
3582
|
+
...options,
|
|
3583
|
+
passive: false
|
|
3584
|
+
});
|
|
3585
|
+
viewport.addEventListener("pointerdown", () => {
|
|
3586
|
+
if (!viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
|
|
3587
|
+
}, options);
|
|
3588
|
+
viewport.addEventListener("dblclick", (event) => {
|
|
3589
|
+
if (this.#tool !== "pointer") return;
|
|
3590
|
+
if (event.target?.closest?.("[data-shape-id]")) this.run({
|
|
3591
|
+
type: "reveal",
|
|
3592
|
+
panel: "edit",
|
|
3593
|
+
focusText: true
|
|
3594
|
+
});
|
|
3595
|
+
}, options);
|
|
3596
|
+
doc.addEventListener("fullscreenchange", () => this.render(this.host.controller.state), options);
|
|
3597
|
+
const disposeDraw = this.#draw.wire();
|
|
3598
|
+
return () => {
|
|
3599
|
+
++this.#pending;
|
|
3600
|
+
events.abort();
|
|
3601
|
+
disposeDraw();
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
setTool(tool) {
|
|
3605
|
+
if (tool === "rectangle" && !this.#canEdit(this.host.controller.state)) return;
|
|
3606
|
+
this.#tool = tool;
|
|
3607
|
+
this.render(this.host.controller.state);
|
|
3608
|
+
}
|
|
3609
|
+
#canEdit(state) {
|
|
3610
|
+
return state.edit.sourceAvailable && !state.loading && !state.edit.busy;
|
|
3611
|
+
}
|
|
3612
|
+
#history(key) {
|
|
3613
|
+
const { controller } = this.host;
|
|
3614
|
+
const { edit, loading } = controller.state;
|
|
3615
|
+
if ((key === "undo" ? edit.canUndo : edit.canRedo) && !edit.busy && !loading) this.#edit(() => controller[key]());
|
|
3616
|
+
}
|
|
3617
|
+
#delete() {
|
|
3618
|
+
const state = this.host.controller.state;
|
|
3619
|
+
const shape = state.selectedShape;
|
|
3620
|
+
const pageId = shape?.pageId ?? state.document?.pages[state.pageIndex]?.id;
|
|
3621
|
+
if (!shape || pageId === void 0 || !this.#canEdit(state)) return;
|
|
3622
|
+
this.#edit(() => this.host.controller.applyEdits([{
|
|
3623
|
+
type: "delete-shape",
|
|
3624
|
+
pageId,
|
|
3625
|
+
shapeId: shape.id
|
|
3626
|
+
}]), `Deleted ${shape.name || `shape ${shape.id}`}.`);
|
|
3627
|
+
}
|
|
3628
|
+
async #edit(action, success) {
|
|
3629
|
+
const request = ++this.#pending;
|
|
3630
|
+
const { root, viewport } = this.host;
|
|
3631
|
+
const canvasFocused = viewport.contains(root.activeElement);
|
|
3632
|
+
try {
|
|
3633
|
+
await action();
|
|
3634
|
+
if (canvasFocused && !viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
|
|
3635
|
+
if (success && request === this.#pending) this.host.announce(success);
|
|
3636
|
+
} catch (error) {
|
|
3637
|
+
if (request === this.#pending && !isEditCancellation(error) && !this.host.controller.state.edit.error) this.host.announce(editErrorMessage(error));
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
#toggleFullscreen() {
|
|
3641
|
+
const host = this.host.root.host;
|
|
3642
|
+
const doc = this.host.root.ownerDocument;
|
|
3643
|
+
(doc.fullscreenElement === host ? doc.exitFullscreen?.() : host.requestFullscreen?.())?.catch?.(() => this.host.announce("Full screen is not available in this browser frame."));
|
|
3644
|
+
}
|
|
3645
|
+
/** Visio shortcuts as typed actions. Text fields keep native undo, deletion and editing keys. */
|
|
3646
|
+
#shortcutAction(event) {
|
|
3647
|
+
const state = this.host.controller.state;
|
|
3648
|
+
const control = event.ctrlKey || event.metaKey;
|
|
3649
|
+
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
|
|
3650
|
+
if (control && key === "f") return { type: "search" };
|
|
3651
|
+
if (control && event.shiftKey && key === "w") return {
|
|
3652
|
+
type: "zoom",
|
|
3653
|
+
mode: "fit"
|
|
3654
|
+
};
|
|
3655
|
+
if (control && (key === "PageDown" || key === "PageUp")) return {
|
|
3656
|
+
type: "page",
|
|
3657
|
+
step: key === "PageDown" ? 1 : -1
|
|
3658
|
+
};
|
|
3659
|
+
if (key === "F5" && !control) return { type: "fullscreen" };
|
|
3660
|
+
if (editable(event.target)) return void 0;
|
|
3661
|
+
if (control && !event.shiftKey && key === "z") return {
|
|
3662
|
+
type: "history",
|
|
3663
|
+
key: "undo"
|
|
3664
|
+
};
|
|
3665
|
+
if (control && (key === "y" || event.shiftKey && key === "z")) return {
|
|
3666
|
+
type: "history",
|
|
3667
|
+
key: "redo"
|
|
3668
|
+
};
|
|
3669
|
+
if (control && key === "1") return {
|
|
3670
|
+
type: "tool",
|
|
3671
|
+
tool: "pointer"
|
|
3672
|
+
};
|
|
3673
|
+
if (control && key === "8") return {
|
|
3674
|
+
type: "tool",
|
|
3675
|
+
tool: "rectangle"
|
|
3676
|
+
};
|
|
3677
|
+
if (!control && key === "Delete" && state.selectedShape) return { type: "delete" };
|
|
3678
|
+
if (!control && key === "F2" && state.document) return {
|
|
3679
|
+
type: "reveal",
|
|
3680
|
+
panel: "edit",
|
|
3681
|
+
focusText: true
|
|
3682
|
+
};
|
|
3683
|
+
if (key === "Escape" && this.#tool !== "pointer" && !this.#draw.drawing) return {
|
|
3684
|
+
type: "tool",
|
|
3685
|
+
tool: "pointer"
|
|
3686
|
+
};
|
|
3687
|
+
}
|
|
3688
|
+
#shortcut(event) {
|
|
3689
|
+
if (event.defaultPrevented || event.isComposing || event.altKey) return;
|
|
3690
|
+
const action = this.#shortcutAction(event);
|
|
3691
|
+
if (!action) return;
|
|
3692
|
+
event.preventDefault();
|
|
3693
|
+
this.run(action);
|
|
3694
|
+
}
|
|
3695
|
+
render(state) {
|
|
3696
|
+
const { root, viewport } = this.host;
|
|
3697
|
+
const button = (name) => root.querySelector(`office-ui-button[command="${name}"]`);
|
|
3698
|
+
const editing = this.#canEdit(state);
|
|
3699
|
+
if (this.#tool === "rectangle" && !state.edit.sourceAvailable) this.#tool = "pointer";
|
|
3700
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3701
|
+
button("undo").disabled = !state.edit.canUndo || state.edit.busy || state.loading;
|
|
3702
|
+
button("redo").disabled = !state.edit.canRedo || state.edit.busy || state.loading;
|
|
3703
|
+
button("delete").disabled = !editing || !state.selectedShape;
|
|
3704
|
+
button("pointer").setAttribute("pressed", String(this.#tool === "pointer"));
|
|
3705
|
+
const rectangle = button("rectangle");
|
|
3706
|
+
rectangle.setAttribute("pressed", String(this.#tool === "rectangle"));
|
|
3707
|
+
rectangle.disabled = !editing || !page;
|
|
3708
|
+
rectangle.title = state.edit.sourceAvailable ? "Rectangle (Ctrl+8)" : "Rectangle (Ctrl+8): open a .vsdx file to draw. Model-only documents are read only.";
|
|
3709
|
+
button("grid").setAttribute("pressed", String(this.#grid));
|
|
3710
|
+
button("grid").disabled = !page;
|
|
3711
|
+
for (const name of [
|
|
3712
|
+
"zoom-fit",
|
|
3713
|
+
"page-width",
|
|
3714
|
+
"actual-size"
|
|
3715
|
+
]) button(name).disabled = !page;
|
|
3716
|
+
const fullscreen = button("fullscreen");
|
|
3717
|
+
const host = root.host;
|
|
3718
|
+
fullscreen.disabled = typeof host.requestFullscreen !== "function";
|
|
3719
|
+
fullscreen.setAttribute("pressed", String(root.ownerDocument.fullscreenElement === host));
|
|
3720
|
+
viewport.dataset.tool = this.#tool;
|
|
3721
|
+
viewport.dataset.grid = String(this.#grid);
|
|
3722
|
+
}
|
|
3723
|
+
};
|
|
3724
|
+
//#endregion
|
|
3725
|
+
//#region src/office-ui.ts
|
|
3726
|
+
/**
|
|
3727
|
+
* Visio-only glyphs (20x20 path data). Neutral Office glyphs come from ooxml-ui; add a glyph
|
|
3728
|
+
* there instead when another Office product could use it.
|
|
3729
|
+
*/
|
|
3730
|
+
var VISIO_ICONS = {
|
|
3731
|
+
visioLayers: "m2.5 7 7.5-4 7.5 4-7.5 4ZM2.5 10.5l7.5 4 7.5-4M2.5 14l7.5 4 7.5-4",
|
|
3732
|
+
visioShapeData: "M3 3.5h14v13H3ZM3 7.5h14M8 7.5v9M10.5 11h4M10.5 14h4",
|
|
3733
|
+
visioPagesPane: "M2.5 3h15v14h-15ZM7 3v14M3.8 6h1.9M3.8 9h1.9M3.8 12h1.9",
|
|
3734
|
+
visioInspectorPane: "M2.5 3h15v14h-15ZM13 3v14M14.5 6h1.5M14.5 9h1.5M14.5 12h1.5"
|
|
3735
|
+
};
|
|
3736
|
+
/** Define the shared Office controls the viewer chrome uses. Idempotent; browser only. */
|
|
3737
|
+
function registerViewerControls(registry) {
|
|
3738
|
+
for (const [name, path] of Object.entries(VISIO_ICONS)) if (!getIcon(name)) registerIcon(name, path);
|
|
3739
|
+
registerControls(registry);
|
|
3740
|
+
}
|
|
3741
|
+
//#endregion
|
|
3088
3742
|
//#region src/export-svg.ts
|
|
3089
3743
|
var MAX_SVG_EXPORT_BYTES = 16777216;
|
|
3090
3744
|
var APPROXIMATION = "Static SVG export is a rendered approximation, not an editable VSDX round-trip or a guarantee of Microsoft Visio fidelity. Fonts are not embedded.";
|
|
@@ -3695,8 +4349,10 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3695
4349
|
#root;
|
|
3696
4350
|
#disposeInputs;
|
|
3697
4351
|
#viewport;
|
|
3698
|
-
#
|
|
3699
|
-
#
|
|
4352
|
+
#zoomSlider;
|
|
4353
|
+
#shapeStatus;
|
|
4354
|
+
#announcement;
|
|
4355
|
+
#commands;
|
|
3700
4356
|
#status;
|
|
3701
4357
|
#diagnostics;
|
|
3702
4358
|
#toolbar;
|
|
@@ -3729,10 +4385,14 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3729
4385
|
constructor() {
|
|
3730
4386
|
super();
|
|
3731
4387
|
this.#root = this.attachShadow({ mode: "open" });
|
|
3732
|
-
|
|
4388
|
+
registerViewerControls();
|
|
4389
|
+
this.#root.innerHTML = `<style>${viewerStyles}${canvasAndRibbonStyles}</style>${viewerChromeTemplate}`;
|
|
4390
|
+
const workspace = this.#root.querySelector(".workspace");
|
|
4391
|
+
workspace.before(createRibbon(document));
|
|
4392
|
+
workspace.after(createPageTabs(document), createStatusBar(document));
|
|
3733
4393
|
this.#viewport = this.#root.querySelector(".viewport");
|
|
3734
|
-
this.#
|
|
3735
|
-
this.#
|
|
4394
|
+
this.#zoomSlider = this.#root.querySelector("office-ui-zoom-slider");
|
|
4395
|
+
this.#shapeStatus = this.#root.querySelector("[data-shape-status]");
|
|
3736
4396
|
this.#status = this.#root.querySelector("[data-status]");
|
|
3737
4397
|
this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
|
|
3738
4398
|
this.#toolbar = this.#root.querySelector(".toolbar");
|
|
@@ -3743,6 +4403,23 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3743
4403
|
this.#notesPanel = this.#root.querySelector(".notes");
|
|
3744
4404
|
this.#inspector = this.#root.querySelector(".shape-inspector");
|
|
3745
4405
|
this.#chrome = new ViewerChrome(this.#root, this.controller);
|
|
4406
|
+
this.#commands = new ViewerCommands({
|
|
4407
|
+
root: this.#root,
|
|
4408
|
+
viewport: this.#viewport,
|
|
4409
|
+
controller: this.controller,
|
|
4410
|
+
fit: (mode) => this.#fit(mode),
|
|
4411
|
+
togglePane: (pane) => this.#chrome.togglePane(pane),
|
|
4412
|
+
reveal: (panel, focusText) => this.#chrome.reveal(panel, focusText),
|
|
4413
|
+
focusSearch: () => {
|
|
4414
|
+
this.#chrome.showTab("home");
|
|
4415
|
+
this.#search.input.focus();
|
|
4416
|
+
this.#search.input.select();
|
|
4417
|
+
},
|
|
4418
|
+
announce: (message) => {
|
|
4419
|
+
this.#announcement = message;
|
|
4420
|
+
this.#status.textContent = message;
|
|
4421
|
+
}
|
|
4422
|
+
});
|
|
3746
4423
|
this.#disposeInputs = this.#wireInputs();
|
|
3747
4424
|
this.#unsubscribe = this.controller.subscribe((state) => this.#render(state));
|
|
3748
4425
|
this.#eventUnsubscribe = this.controller.onEvent((name, detail) => {
|
|
@@ -3816,13 +4493,24 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3816
4493
|
}
|
|
3817
4494
|
fit() {
|
|
3818
4495
|
this.#assertAlive();
|
|
4496
|
+
this.#fit("page");
|
|
4497
|
+
}
|
|
4498
|
+
/** Visio Fit to Window shows the whole page; Page Width fills the canvas width. */
|
|
4499
|
+
#fit(mode) {
|
|
3819
4500
|
const page = this.document?.pages[this.pageIndex];
|
|
3820
4501
|
if (!page) return;
|
|
3821
4502
|
const style = this.ownerDocument.defaultView?.getComputedStyle(this.#viewport);
|
|
3822
4503
|
const padding = (value) => Number.parseFloat(value ?? "") || 0;
|
|
3823
4504
|
const width = Math.max(1, this.#viewport.clientWidth - padding(style?.paddingLeft) - padding(style?.paddingRight));
|
|
3824
4505
|
const height = Math.max(1, this.#viewport.clientHeight - padding(style?.paddingTop) - padding(style?.paddingBottom));
|
|
3825
|
-
|
|
4506
|
+
if (mode === "page") {
|
|
4507
|
+
this.zoom = Math.min(1, width / (page.width * 96), height / (page.height * 96));
|
|
4508
|
+
return;
|
|
4509
|
+
}
|
|
4510
|
+
const scrollbar = this.#viewport.offsetWidth - this.#viewport.clientWidth;
|
|
4511
|
+
const fitted = width / (page.width * 96);
|
|
4512
|
+
const reserve = page.height * 96 * fitted > height && scrollbar <= 0 ? 17 : 0;
|
|
4513
|
+
this.zoom = Math.max(1, width - reserve - 1) / (page.width * 96);
|
|
3826
4514
|
}
|
|
3827
4515
|
setLayerVisibility(pageId, layerId, visible) {
|
|
3828
4516
|
this.#assertAlive();
|
|
@@ -3892,16 +4580,18 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3892
4580
|
}
|
|
3893
4581
|
#wireInputs() {
|
|
3894
4582
|
const disposeChrome = this.#chrome.wire();
|
|
4583
|
+
const disposeCommands = this.#commands.wire();
|
|
3895
4584
|
const disposeEdit = this.#edit.wire();
|
|
3896
4585
|
const disposeLayers = wireLayerControls(this.#layers, this.controller);
|
|
3897
4586
|
const disposeInputs = wireViewerInputs({
|
|
3898
4587
|
viewport: this.#viewport,
|
|
3899
4588
|
commandRoot: this.#root,
|
|
3900
|
-
|
|
4589
|
+
zoomSlider: this.#zoomSlider,
|
|
3901
4590
|
searchInput: this.#search.input
|
|
3902
|
-
}, this.controller, () => this
|
|
4591
|
+
}, this.controller, (mode) => this.#fit(mode));
|
|
3903
4592
|
return () => {
|
|
3904
4593
|
disposeChrome();
|
|
4594
|
+
disposeCommands();
|
|
3905
4595
|
disposeInputs();
|
|
3906
4596
|
disposeLayers();
|
|
3907
4597
|
disposeEdit();
|
|
@@ -3920,14 +4610,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3920
4610
|
this.#document = state.document;
|
|
3921
4611
|
this.#renderedPage = state.pageIndex;
|
|
3922
4612
|
this.#renderedLayerOverrides = state.layerVisibilityOverrides;
|
|
3923
|
-
this.#
|
|
3924
|
-
for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
|
|
3925
|
-
const option = document.createElement("option");
|
|
3926
|
-
option.value = String(index);
|
|
3927
|
-
option.textContent = candidate.name + (candidate.isBackground ? " (background)" : "");
|
|
3928
|
-
this.#pageSelect.append(option);
|
|
3929
|
-
}
|
|
3930
|
-
this.#pageSelect.value = String(state.pageIndex);
|
|
4613
|
+
this.#announcement = void 0;
|
|
3931
4614
|
this.#renderWarnings = [];
|
|
3932
4615
|
if (state.document && page) {
|
|
3933
4616
|
const result = renderPage(state.document, page, { layerVisibilityOverrides: state.layerVisibilityOverrides });
|
|
@@ -3939,7 +4622,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3939
4622
|
empty.className = "empty";
|
|
3940
4623
|
const heading = document.createElement("strong");
|
|
3941
4624
|
heading.textContent = "Open a Visio drawing";
|
|
3942
|
-
empty.append(heading, "Open a .vsdx file to start. Files stay in this browser.");
|
|
4625
|
+
empty.append(heading, "Open a .vsdx or supported .vsd file to start. Files stay in this browser.");
|
|
3943
4626
|
this.#viewport.replaceChildren(empty);
|
|
3944
4627
|
}
|
|
3945
4628
|
const notes = compatibilityNotes(state.document?.diagnostics ?? [], this.#renderWarnings);
|
|
@@ -3954,6 +4637,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3954
4637
|
if (svg && page) {
|
|
3955
4638
|
svg.style.width = `${page.width * 96 * state.zoom}px`;
|
|
3956
4639
|
svg.style.height = `${page.height * 96 * state.zoom}px`;
|
|
4640
|
+
svg.style.setProperty("--vv-inch", `${96 * state.zoom}px`);
|
|
3957
4641
|
}
|
|
3958
4642
|
const searchResult = state.search.results[state.search.activeIndex];
|
|
3959
4643
|
const selection = state.selectedShape;
|
|
@@ -3981,16 +4665,20 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3981
4665
|
this.#renderedSelection = selection ? { ...selection } : null;
|
|
3982
4666
|
}
|
|
3983
4667
|
if (!searchResult) this.#revealedSearchResult = void 0;
|
|
3984
|
-
this.#
|
|
3985
|
-
this.#
|
|
4668
|
+
this.#zoomSlider.value = Math.round(state.zoom * 100);
|
|
4669
|
+
this.#zoomSlider.disabled = !page;
|
|
4670
|
+
const inspected = selectedShape(state.document, state.selectedShape, state.pageIndex);
|
|
4671
|
+
const inches = (value) => `${+value.toFixed(3)} in`;
|
|
4672
|
+
this.#shapeStatus.setAttribute("value", inspected ? `Width: ${inches(inspected.width)} Height: ${inches(inspected.height)}` : "");
|
|
3986
4673
|
for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
|
|
3987
4674
|
renderSearchControls(this.#search, state);
|
|
3988
4675
|
this.#edit.render(state);
|
|
3989
4676
|
if (changed) renderLayerControls(this.#layers, state);
|
|
3990
4677
|
else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
|
|
3991
4678
|
this.#chrome.render(state, this.#notes.children.length);
|
|
4679
|
+
this.#commands.render(state);
|
|
3992
4680
|
this.#viewport.setAttribute("aria-busy", String(state.loading || state.edit.busy));
|
|
3993
|
-
this.#status.textContent = state.loading ? "Opening diagram…" : state.error?.message ?? (page ? `${page.name} · ${page.shapes.length} top-level shapes` : "No diagram open");
|
|
4681
|
+
this.#status.textContent = state.loading ? "Opening diagram…" : state.edit.busy ? "Updating diagram…" : state.error?.message ?? (state.edit.error ? `Edit rejected: ${editErrorMessage(state.edit.error)}` : void 0) ?? this.#announcement ?? (page ? `${page.name} · ${page.shapes.length} top-level shapes` : "No diagram open");
|
|
3994
4682
|
const warnings = (state.document?.diagnostics.length ?? 0) + this.#renderWarnings.length;
|
|
3995
4683
|
this.#diagnostics.textContent = warnings ? `${this.#notes.children.length} compatibility notes` : "Local-only viewing";
|
|
3996
4684
|
}
|
|
@@ -4200,6 +4888,23 @@ function viewerHandle(current) {
|
|
|
4200
4888
|
}
|
|
4201
4889
|
};
|
|
4202
4890
|
}
|
|
4891
|
+
function viewerStateSource(current) {
|
|
4892
|
+
const controller = () => {
|
|
4893
|
+
try {
|
|
4894
|
+
return current()?.controller;
|
|
4895
|
+
} catch {
|
|
4896
|
+
return;
|
|
4897
|
+
}
|
|
4898
|
+
};
|
|
4899
|
+
return {
|
|
4900
|
+
subscribe(onChange) {
|
|
4901
|
+
return controller()?.subscribe(() => onChange()) ?? (() => {});
|
|
4902
|
+
},
|
|
4903
|
+
getSnapshot() {
|
|
4904
|
+
return controller()?.state ?? null;
|
|
4905
|
+
}
|
|
4906
|
+
};
|
|
4907
|
+
}
|
|
4203
4908
|
//#endregion
|
|
4204
4909
|
//#region packages/bindings/src/solid.tsx
|
|
4205
4910
|
var _tmpl$ = [
|
|
@@ -4243,5 +4948,19 @@ function VisioViewer(props) {
|
|
|
4243
4948
|
});
|
|
4244
4949
|
return ssr(_tmpl$, ssrHydrationKey() + ssrAttribute("class", escape(props.class, true), false), ssrStyle(props.style));
|
|
4245
4950
|
}
|
|
4951
|
+
/**
|
|
4952
|
+
* Reactive viewer state as a Solid accessor for a handle accessor (for example a signal set by
|
|
4953
|
+
* `viewerRef`). Follows handle changes and cleans up with the owning scope.
|
|
4954
|
+
*/
|
|
4955
|
+
function createVisioViewerState(handle) {
|
|
4956
|
+
const [state, setState] = createSignal(null);
|
|
4957
|
+
createEffect(() => {
|
|
4958
|
+
const current = handle();
|
|
4959
|
+
const source = viewerStateSource(() => current);
|
|
4960
|
+
setState(() => source.getSnapshot());
|
|
4961
|
+
onCleanup(source.subscribe(() => setState(() => source.getSnapshot())));
|
|
4962
|
+
});
|
|
4963
|
+
return state;
|
|
4964
|
+
}
|
|
4246
4965
|
//#endregion
|
|
4247
|
-
export { MAX_SVG_EXPORT_BYTES, PRINT_SNAPSHOT_LIMITS, TEXT_SEARCH_LIMITS, VIEWER_LAYER_LIMITS, ViewerController, VisioViewer, VisioViewerElement, compatibilityNotes, compatibilityText, createPrintSnapshot, createWorkerParser, eventKeys, exportPageSvg, mountViewer, propertyKeys, registerVisioViewer, renderPage };
|
|
4966
|
+
export { MAX_SVG_EXPORT_BYTES, PRINT_SNAPSHOT_LIMITS, TEXT_SEARCH_LIMITS, VIEWER_LAYER_LIMITS, ViewerController, VisioViewer, VisioViewerElement, compatibilityNotes, compatibilityText, createPrintSnapshot, createVisioViewerState, createWorkerParser, eventKeys, exportPageSvg, mountViewer, propertyKeys, registerVisioViewer, renderPage };
|