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.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 { className, effect, style, template, use } 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();
|
|
@@ -533,7 +535,7 @@ function createWorkerEditor(factory, timeoutMs = 2e4) {
|
|
|
533
535
|
if (typeof Worker === "undefined") throw new Error("Editing requires browser worker support.");
|
|
534
536
|
return new Worker(new URL(
|
|
535
537
|
/* @vite-ignore */
|
|
536
|
-
new URL("assets/edit-worker-
|
|
538
|
+
new URL("assets/edit-worker-CZbtTOr4.js", import.meta.url).href,
|
|
537
539
|
"" + import.meta.url
|
|
538
540
|
), { type: "module" });
|
|
539
541
|
});
|
|
@@ -789,9 +791,10 @@ var ViewerController = class {
|
|
|
789
791
|
#sourceGeneration = 0;
|
|
790
792
|
#searchIndex = null;
|
|
791
793
|
#history = null;
|
|
794
|
+
#sourceFormat = null;
|
|
792
795
|
#editId = 0;
|
|
793
796
|
#visible = documentVisibility(null);
|
|
794
|
-
constructor(parser =
|
|
797
|
+
constructor(parser = loadVisio, listenerError = (error) => {
|
|
795
798
|
if (typeof globalThis.reportError === "function") globalThis.reportError(error);
|
|
796
799
|
else console.error("Visio viewer listener failed:", error);
|
|
797
800
|
}, editor = createWorkerEditor()) {
|
|
@@ -831,6 +834,7 @@ var ViewerController = class {
|
|
|
831
834
|
++this.#sourceGeneration;
|
|
832
835
|
this.#invalidateEdit();
|
|
833
836
|
this.#history = null;
|
|
837
|
+
this.#sourceFormat = null;
|
|
834
838
|
this.#loadId++;
|
|
835
839
|
this.parser.cancel?.();
|
|
836
840
|
this.#searchIndex = null;
|
|
@@ -973,7 +977,7 @@ var ViewerController = class {
|
|
|
973
977
|
this.#change({
|
|
974
978
|
loading: true,
|
|
975
979
|
error: null,
|
|
976
|
-
edit: this.#
|
|
980
|
+
edit: this.#sourceEditState()
|
|
977
981
|
});
|
|
978
982
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
979
983
|
let document;
|
|
@@ -984,10 +988,10 @@ var ViewerController = class {
|
|
|
984
988
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
985
989
|
if (bytes.byteLength > 33554432) throw new Error("This viewer accepts files up to 32 MiB.");
|
|
986
990
|
const owned = bytes instanceof Uint8Array ? Uint8Array.from(bytes) : new Uint8Array(bytes.slice(0));
|
|
987
|
-
history = new DocumentHistory(owned);
|
|
988
991
|
document = await this.parser(Uint8Array.from(owned));
|
|
989
992
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
990
993
|
assertViewableDocument(document);
|
|
994
|
+
history = new DocumentHistory(owned);
|
|
991
995
|
visible = documentVisibility(document);
|
|
992
996
|
} catch (cause) {
|
|
993
997
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
@@ -1000,13 +1004,14 @@ var ViewerController = class {
|
|
|
1000
1004
|
throw error;
|
|
1001
1005
|
}
|
|
1002
1006
|
this.#history = history;
|
|
1007
|
+
this.#sourceFormat = document.format;
|
|
1003
1008
|
this.#searchIndex = null;
|
|
1004
1009
|
this.#visible = visible;
|
|
1005
1010
|
++this.#documentGeneration;
|
|
1006
1011
|
++this.#sourceGeneration;
|
|
1007
1012
|
this.#change({
|
|
1008
1013
|
document,
|
|
1009
|
-
edit: history.state,
|
|
1014
|
+
edit: document.format === "vsdx" ? history.state : EMPTY_EDIT_STATE,
|
|
1010
1015
|
pageIndex: 0,
|
|
1011
1016
|
loading: false,
|
|
1012
1017
|
error: null,
|
|
@@ -1023,7 +1028,7 @@ var ViewerController = class {
|
|
|
1023
1028
|
this.#invalidateEdit();
|
|
1024
1029
|
this.#change({
|
|
1025
1030
|
loading: false,
|
|
1026
|
-
edit: this.#
|
|
1031
|
+
edit: this.#sourceEditState()
|
|
1027
1032
|
});
|
|
1028
1033
|
}
|
|
1029
1034
|
destroy() {
|
|
@@ -1031,6 +1036,7 @@ var ViewerController = class {
|
|
|
1031
1036
|
this.#destroyed = true;
|
|
1032
1037
|
this.#invalidateEdit();
|
|
1033
1038
|
this.#history = null;
|
|
1039
|
+
this.#sourceFormat = null;
|
|
1034
1040
|
++this.#documentGeneration;
|
|
1035
1041
|
++this.#sourceGeneration;
|
|
1036
1042
|
++this.#loadId;
|
|
@@ -1075,11 +1081,11 @@ var ViewerController = class {
|
|
|
1075
1081
|
if (!this.#state.edit.busy) return;
|
|
1076
1082
|
this.#invalidateEdit();
|
|
1077
1083
|
this.parser.cancel?.();
|
|
1078
|
-
this.#change({ edit: this.#
|
|
1084
|
+
this.#change({ edit: this.#sourceEditState() });
|
|
1079
1085
|
}
|
|
1080
1086
|
exportVsdx() {
|
|
1081
1087
|
this.#assertAlive();
|
|
1082
|
-
if (!this.#history) throw new Error("Load a VSDX file before downloading a source-backed copy.");
|
|
1088
|
+
if (!this.#history || this.#sourceFormat !== "vsdx" || this.#state.document?.format !== "vsdx") throw new Error("Load a VSDX file before downloading a source-backed copy.");
|
|
1083
1089
|
if (this.#state.edit.busy || this.#state.loading) throw new Error("Wait for the current document operation before downloading.");
|
|
1084
1090
|
return this.#history.export();
|
|
1085
1091
|
}
|
|
@@ -1087,10 +1093,13 @@ var ViewerController = class {
|
|
|
1087
1093
|
++this.#editId;
|
|
1088
1094
|
this.editor.cancel?.();
|
|
1089
1095
|
}
|
|
1096
|
+
#sourceEditState() {
|
|
1097
|
+
return this.#sourceFormat === "vsdx" && this.#state.document?.format === "vsdx" ? this.#history?.state ?? EMPTY_EDIT_STATE : EMPTY_EDIT_STATE;
|
|
1098
|
+
}
|
|
1090
1099
|
async #mutate(kind, commands) {
|
|
1091
1100
|
this.#assertAlive();
|
|
1092
1101
|
const history = this.#history;
|
|
1093
|
-
if (!history) throw new Error("Load a VSDX file before editing. Model-only documents are read only.");
|
|
1102
|
+
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.");
|
|
1094
1103
|
if (this.#state.loading || this.#state.edit.busy) throw new Error("Another document operation is in progress.");
|
|
1095
1104
|
const target = kind === "undo" ? history.undoTarget : kind === "redo" ? history.redoTarget : void 0;
|
|
1096
1105
|
if (kind !== "edit" && !target) return;
|
|
@@ -1116,6 +1125,7 @@ var ViewerController = class {
|
|
|
1116
1125
|
} else document = await this.parser(Uint8Array.from(target.bytes));
|
|
1117
1126
|
assertCurrent();
|
|
1118
1127
|
assertViewableDocument(document);
|
|
1128
|
+
if (document.format !== "vsdx") throw new Error("A VSDX edit or history operation cannot change the source format.");
|
|
1119
1129
|
if (edited && !edited.changedParts.length) {
|
|
1120
1130
|
this.#change({ edit: history.state });
|
|
1121
1131
|
return;
|
|
@@ -1205,10 +1215,10 @@ var eventKeys = [
|
|
|
1205
1215
|
//#region src/worker-parser.ts
|
|
1206
1216
|
/** One disposable worker per document prevents old parses and long CPU tasks from occupying the UI. */
|
|
1207
1217
|
function createWorkerParser(factory, timeoutMs = 15e3) {
|
|
1208
|
-
if (!factory && typeof Worker === "undefined") return
|
|
1218
|
+
if (!factory && typeof Worker === "undefined") return loadVisio;
|
|
1209
1219
|
const create = factory ?? (() => new Worker(new URL(
|
|
1210
1220
|
/* @vite-ignore */
|
|
1211
|
-
new URL("assets/parse-worker-
|
|
1221
|
+
new URL("assets/parse-worker-BEaLqUCJ.js", import.meta.url).href,
|
|
1212
1222
|
"" + import.meta.url
|
|
1213
1223
|
), { type: "module" }));
|
|
1214
1224
|
let cancel;
|
|
@@ -2144,7 +2154,7 @@ function selection(target) {
|
|
|
2144
2154
|
}
|
|
2145
2155
|
/** Own every DOM listener and abort them together when the surface is disposed. */
|
|
2146
2156
|
function wireViewerInputs(controls, controller, fit) {
|
|
2147
|
-
const { viewport, commandRoot,
|
|
2157
|
+
const { viewport, commandRoot, zoomSlider, searchInput } = controls;
|
|
2148
2158
|
const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
2149
2159
|
const options = { signal: events.signal };
|
|
2150
2160
|
searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
|
|
@@ -2160,13 +2170,9 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2160
2170
|
controller.setSearchQuery("");
|
|
2161
2171
|
}
|
|
2162
2172
|
}, options);
|
|
2163
|
-
|
|
2173
|
+
zoomSlider.addEventListener("input", () => controller.setZoom(zoomSlider.value / 100), options);
|
|
2164
2174
|
commandRoot.addEventListener("click", (event) => {
|
|
2165
2175
|
const action = event.target?.closest?.("button")?.dataset.action;
|
|
2166
|
-
if (action === "in") controller.setZoom(controller.state.zoom * 1.25);
|
|
2167
|
-
if (action === "out") controller.setZoom(controller.state.zoom / 1.25);
|
|
2168
|
-
if (action === "fit") fit();
|
|
2169
|
-
if (action === "actual") controller.setZoom(1);
|
|
2170
2176
|
if (action === "search-next") controller.nextSearchResult();
|
|
2171
2177
|
if (action === "search-previous") controller.previousSearchResult();
|
|
2172
2178
|
}, options);
|
|
@@ -2205,7 +2211,7 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2205
2211
|
}
|
|
2206
2212
|
if (event.key === "0") {
|
|
2207
2213
|
event.preventDefault();
|
|
2208
|
-
fit();
|
|
2214
|
+
fit("page");
|
|
2209
2215
|
}
|
|
2210
2216
|
}, options);
|
|
2211
2217
|
return () => events.abort();
|
|
@@ -2309,13 +2315,7 @@ button:disabled { opacity:.4; cursor:default; }
|
|
|
2309
2315
|
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; }
|
|
2310
2316
|
button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
|
|
2311
2317
|
.toolbar { flex:none; min-width:0; border-bottom:1px solid var(--_vv-border); background:var(--_vv-surface); }
|
|
2312
|
-
.ribbon-primary { height:32px; display:flex; align-items:center; gap:6px; padding:2px 8px; }
|
|
2313
|
-
.page-picker { display:flex; min-width:0; align-items:center; gap:8px; color:var(--_vv-muted); font-size:11px; }
|
|
2314
|
-
.page-picker select { width:190px; max-width:42vw; height:26px; min-height:26px; padding:2px 6px; font-size:11px; color:var(--_vv-ink); }
|
|
2315
|
-
.page-stepper { display:flex; align-items:center; gap:2px; }
|
|
2316
|
-
.page-stepper button { width:26px; height:26px; min-height:26px; padding:0; font-size:18px; line-height:22px; background:transparent; border-color:transparent; }
|
|
2317
2318
|
.spacer { flex:1; }
|
|
2318
|
-
.local-label { color:var(--_vv-muted); font-size:10px; white-space:nowrap; }
|
|
2319
2319
|
.ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
|
|
2320
2320
|
.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; }
|
|
2321
2321
|
.ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
|
|
@@ -2325,15 +2325,6 @@ button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-acce
|
|
|
2325
2325
|
.group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
|
|
2326
2326
|
.ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
|
|
2327
2327
|
.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; }
|
|
2328
|
-
.command-icon { display:grid; place-items:center; width:26px; height:26px; color:var(--_vv-accent); }
|
|
2329
|
-
.command-icon::before { content:""; width:24px; height:24px; background:currentColor; mask:var(--command-symbol) center / contain no-repeat; }
|
|
2330
|
-
[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"); }
|
|
2331
|
-
[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"); }
|
|
2332
|
-
[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"); }
|
|
2333
|
-
[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"); }
|
|
2334
|
-
[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"); }
|
|
2335
|
-
[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"); }
|
|
2336
|
-
.ribbon-command[data-chrome="edit"] .command-icon { font-family:ui-serif,Georgia,serif; text-decoration:underline; text-decoration-thickness:1px; text-underline-offset:3px; }
|
|
2337
2328
|
.ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
|
|
2338
2329
|
.search-group { width:330px; }
|
|
2339
2330
|
.search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
|
|
@@ -2405,11 +2396,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2405
2396
|
.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; }
|
|
2406
2397
|
.status-message { display:flex; gap:16px; min-width:0; }
|
|
2407
2398
|
.status-message span { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
|
2408
|
-
.zoom-controls { display:flex; flex:none; align-items:center; gap:3px; }
|
|
2409
|
-
.zoom-controls button { min-height:24px; height:24px; padding:1px 6px; border-color:transparent; background:transparent; font-size:10px; }
|
|
2410
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:24px; padding:0; font-size:17px; line-height:20px; }
|
|
2411
|
-
.zoom-divider { width:1px; height:14px; background:var(--_vv-border); margin:0 3px; }
|
|
2412
|
-
.zoom { min-width:40px; font-size:10px; text-align:center; font-variant-numeric:tabular-nums; }
|
|
2413
2399
|
.empty { max-width:360px; margin:60px auto; padding:16px; text-align:center; color:var(--_vv-muted); font-size:12px; }
|
|
2414
2400
|
.empty strong { display:block; color:var(--_vv-ink); font-size:18px; font-weight:500; margin-bottom:8px; }
|
|
2415
2401
|
@media(max-width:1100px) { .ribbon-hint { display:none; } [data-diagnostics] { display:none; } }
|
|
@@ -2417,11 +2403,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2417
2403
|
button,select,input,textarea { min-height:44px; }
|
|
2418
2404
|
button { min-width:44px; }
|
|
2419
2405
|
summary { min-height:44px; line-height:44px; }
|
|
2420
|
-
.ribbon-primary { height:52px; padding:4px 8px; }
|
|
2421
|
-
.page-picker { flex:1; }
|
|
2422
|
-
.page-picker select { width:100%; max-width:none; min-height:44px; height:44px; font-size:12px; }
|
|
2423
|
-
.page-stepper button { width:44px; min-height:44px; height:44px; }
|
|
2424
|
-
.local-label,.ribbon-primary>.spacer { display:none; }
|
|
2425
2406
|
.ribbon-tabs { height:44px; }
|
|
2426
2407
|
.ribbon-tabs button { min-height:44px; padding:8px 16px; }
|
|
2427
2408
|
.ribbon-content { min-height:86px; }
|
|
@@ -2433,7 +2414,7 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2433
2414
|
.search-controls button { min-width:44px; font-size:0; padding:3px; }
|
|
2434
2415
|
.search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
|
|
2435
2416
|
.search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
|
|
2436
|
-
#home-panel>.ribbon-group:not(.search-group) { display:none; }
|
|
2417
|
+
#home-panel>.ribbon-group:not(.search-group):not(.undo-group) { display:none; }
|
|
2437
2418
|
.ribbon-command { min-width:90px; }
|
|
2438
2419
|
.workspace { display:block; overflow:auto; }
|
|
2439
2420
|
.page-rail { width:100%; max-height:190px; border-right:0; border-bottom:1px solid var(--_vv-border); }
|
|
@@ -2450,35 +2431,22 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2450
2431
|
.notes-strip button { min-height:44px; }
|
|
2451
2432
|
.status { flex-wrap:wrap; gap:0; padding:4px 8px; }
|
|
2452
2433
|
.status-message { width:100%; min-height:18px; }
|
|
2453
|
-
.zoom-controls { justify-content:flex-end; width:100%; }
|
|
2454
|
-
.zoom-controls button { min-height:44px; height:44px; min-width:44px; font-size:11px; }
|
|
2455
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; font-size:20px; }
|
|
2456
|
-
.zoom { font-size:11px; }
|
|
2457
2434
|
}
|
|
2458
2435
|
@media(pointer:coarse) {
|
|
2459
2436
|
button,select,input,textarea { min-height:44px; }
|
|
2460
2437
|
button { min-width:44px; }
|
|
2461
2438
|
summary,.layer-controls label { min-height:44px; line-height:44px; }
|
|
2462
|
-
.ribbon-primary { min-height:52px; height:52px; }
|
|
2463
|
-
.page-picker select,.page-stepper button { min-height:44px; height:44px; }
|
|
2464
|
-
.page-stepper button { min-width:44px; }
|
|
2465
2439
|
.ribbon-tabs { min-height:44px; height:44px; }
|
|
2466
2440
|
.ribbon-tabs button { min-height:44px; }
|
|
2467
2441
|
.ribbon-content { min-height:96px; }
|
|
2468
2442
|
.search-group { width:380px; max-width:100%; }
|
|
2469
|
-
.
|
|
2470
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; }
|
|
2443
|
+
.notes-strip button { min-height:44px; height:44px; }
|
|
2471
2444
|
.layer-controls input { min-height:20px; }
|
|
2472
2445
|
}
|
|
2473
|
-
/* Suite ribbon: tabs lead directly into command groups
|
|
2446
|
+
/* Suite ribbon: tabs lead directly into Visio command groups. */
|
|
2474
2447
|
.ribbon-tabs { height:40px; }
|
|
2475
2448
|
.ribbon-tabs button { min-height:39px; padding:8px 16px; }
|
|
2476
2449
|
.ribbon-content { min-height:104px; padding:4px 4px; }
|
|
2477
|
-
.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; }
|
|
2478
|
-
.ribbon-primary .page-picker { flex-direction:column; align-items:start; gap:5px; }
|
|
2479
|
-
.ribbon-primary .page-picker select { width:160px; max-width:100%; }
|
|
2480
|
-
.ribbon-primary .spacer { display:none; }
|
|
2481
|
-
.ribbon-primary .local-label { position:absolute; bottom:4px; left:0; width:100%; text-align:center; }
|
|
2482
2450
|
.search-group { order:2; width:320px; }
|
|
2483
2451
|
.ribbon-hint { order:3; padding:10px 18px; }
|
|
2484
2452
|
.ribbon-command { min-width:82px; }
|
|
@@ -2487,13 +2455,9 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2487
2455
|
.ribbon-tabs { height:44px; }
|
|
2488
2456
|
.ribbon-tabs button { min-height:44px; }
|
|
2489
2457
|
.ribbon-content { min-height:54px; padding:4px 0; }
|
|
2490
|
-
.ribbon-primary { min-height:44px; height:44px; width:100%; padding:0 8px; border:0; flex-wrap:nowrap; gap:4px; order:0; }
|
|
2491
|
-
.ribbon-primary .page-picker { flex-direction:row; flex:1; align-items:center; gap:8px; }
|
|
2492
|
-
.ribbon-primary .page-picker select { flex:1; width:100%; min-width:0; }
|
|
2493
2458
|
.search-group { flex:0 0 100%; width:100%; min-height:54px; }
|
|
2494
2459
|
.search-feedback,.group-label { display:none; }
|
|
2495
2460
|
.ribbon-content { overflow-x:auto; scroll-snap-type:x mandatory; }
|
|
2496
|
-
.ribbon-primary,.search-group { scroll-snap-align:start; }
|
|
2497
2461
|
.workspace { display:flex; position:relative; overflow:hidden; }
|
|
2498
2462
|
.viewport { flex:1; width:100%; height:100%; min-height:0; padding:16px 4px; }
|
|
2499
2463
|
.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; }
|
|
@@ -2522,9 +2486,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2522
2486
|
.status-message { flex:1; }
|
|
2523
2487
|
@media(max-width:760px) {
|
|
2524
2488
|
#home-panel { overflow:visible; min-height:54px; padding:4px; }
|
|
2525
|
-
.ribbon-primary { width:calc(100% - 92px); min-width:0; flex:1; padding:0 4px; }
|
|
2526
|
-
.ribbon-primary .page-picker { font-size:0; gap:0; }
|
|
2527
|
-
.ribbon-primary .page-picker select { font-size:12px; }
|
|
2528
2489
|
.ribbon-tools { display:block; flex:0 0 84px; width:84px; order:1; }
|
|
2529
2490
|
.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; }
|
|
2530
2491
|
.ribbon-tools>summary::-webkit-details-marker { display:none; }
|
|
@@ -2536,14 +2497,347 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2536
2497
|
.ribbon-tools .search-group { order:1; flex:auto; }
|
|
2537
2498
|
.ribbon-tools .search-feedback { display:block; }
|
|
2538
2499
|
.status { flex-wrap:nowrap; gap:4px; min-height:48px; }
|
|
2539
|
-
.zoom-controls { width:auto; flex:1; gap:0; }
|
|
2540
|
-
.zoom-divider { margin:0; }
|
|
2541
2500
|
.status .notes-strip { display:none; }
|
|
2542
2501
|
}
|
|
2543
2502
|
@media(pointer:coarse) { .ribbon-tabs {height:45px;min-height:45px;} .ribbon-tabs button {min-height:44px;height:44px;} }
|
|
2544
2503
|
@media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
|
|
2545
2504
|
`;
|
|
2546
2505
|
//#endregion
|
|
2506
|
+
//#region src/styles/index.ts
|
|
2507
|
+
/**
|
|
2508
|
+
* Viewer stylesheets as text for the shadow root. New styles are CSS files here; the legacy
|
|
2509
|
+
* `styles.ts` string moves into this directory when it is next changed.
|
|
2510
|
+
*/
|
|
2511
|
+
var canvasAndRibbonStyles = ["/*\n * Layout for the shared ooxml-ui ribbon, tab strip and status controls inside the viewer.\n * Viewer theme tokens feed the shared --office-* tokens, so host themes still apply.\n */\n:host {\n --office-foreground: var(--_vv-ink);\n --office-muted-foreground: var(--_vv-muted);\n --office-background: var(--_vv-surface);\n --office-surface: var(--_vv-accent-soft);\n --office-border: var(--_vv-border);\n --office-accent: var(--_vv-accent);\n --office-accent-foreground: var(--vv-accent-ink, #fff);\n --office-ring: var(--vv-focus, var(--_vv-accent));\n --office-font: 'Segoe UI', Arial, sans-serif;\n}\n.ribbon-row {\n display: flex;\n flex-wrap: nowrap;\n align-items: stretch;\n gap: 0;\n padding: 0;\n border: 0;\n background: transparent;\n}\n.ribbon-row office-ui-ribbon-group {\n min-width: 0;\n}\n.ribbon-row office-ui-button {\n --office-target-size: 28px;\n}\n.ribbon-row office-ui-button[variant='stacked']::part(button) {\n min-width: 64px;\n min-height: 62px;\n font-size: 12px;\n}\noffice-ui-button[disabled]::part(button) {\n opacity: 0.45;\n}\n.page-tabs {\n flex: none;\n}\noffice-ui-status-bar.status {\n gap: 16px;\n min-height: 29px;\n padding: 0 8px 0 12px;\n font-size: 11px;\n background: var(--_vv-surface);\n color: var(--_vv-muted);\n}\noffice-ui-status-item[value=''] {\n display: none;\n}\noffice-ui-status-item[data-page-status] {\n color: var(--_vv-ink);\n}\n.status-message {\n flex: 1;\n min-width: 0;\n}\n.viewport[data-tool='rectangle'],\n.viewport[data-tool='rectangle'] .paper [data-shape-id] {\n cursor: crosshair;\n}\n.viewport[data-tool='rectangle'] {\n touch-action: none;\n}\n.draw-preview {\n fill: var(--_vv-accent-soft);\n stroke: var(--_vv-accent);\n stroke-width: 1.5px;\n stroke-dasharray: 4 3;\n pointer-events: none;\n}\n.viewport[data-grid='true'] .paper {\n --_vv-grid-minor: rgb(0 0 0 / 7%);\n --_vv-grid-major: rgb(0 0 0 / 16%);\n background-image:\n linear-gradient(to right, var(--_vv-grid-major) 1px, transparent 1px),\n linear-gradient(to top, var(--_vv-grid-major) 1px, transparent 1px),\n linear-gradient(to right, var(--_vv-grid-minor) 1px, transparent 1px),\n linear-gradient(to top, var(--_vv-grid-minor) 1px, transparent 1px);\n background-size:\n var(--vv-inch, 96px) var(--vv-inch, 96px),\n var(--vv-inch, 96px) var(--vv-inch, 96px),\n calc(var(--vv-inch, 96px) / 4) calc(var(--vv-inch, 96px) / 4),\n calc(var(--vv-inch, 96px) / 4) calc(var(--vv-inch, 96px) / 4);\n background-position: left bottom;\n}\n:host(:fullscreen) {\n width: 100vw;\n height: 100vh;\n}\n@media (max-width: 760px) {\n .ribbon-row {\n flex: 1;\n min-width: 0;\n }\n .undo-group {\n flex: none;\n }\n .ribbon-row office-ui-button[variant='stacked']::part(button) {\n min-width: 52px;\n min-height: 44px;\n }\n .ribbon-tools .tools-content office-ui-ribbon-group {\n width: 100%;\n }\n .search-group {\n scroll-snap-align: start;\n }\n office-ui-status-bar.status {\n gap: 4px;\n min-height: 48px;\n }\n office-ui-status-bar .notes-strip,\n office-ui-status-bar .status-message,\n office-ui-status-item[data-shape-status],\n office-ui-status-item[data-page-status] {\n display: none;\n }\n .zoom-controls {\n flex: 1;\n min-width: 0;\n --office-zoom-slider-width: 0;\n }\n}\n", "/*\n * Drawing canvas focus. SVG outlines are drawn in drawing units (inches here), so a browser\n * focus ring on a shape becomes a page-sized blob. Shapes show focus with a dashed accent\n * stroke instead, only for keyboard focus; selection keeps its solid accent stroke.\n */\n.paper [data-shape-id]:focus,\n.paper [data-shape-id]:focus-visible {\n outline: none;\n}\n.paper [data-shape-id]:focus-visible > [data-geometry] {\n stroke: var(--vv-focus, var(--_vv-accent)) !important;\n stroke-width: 0.04 !important;\n stroke-dasharray: 0.08 0.05;\n}\n"].join("\n");
|
|
2512
|
+
//#endregion
|
|
2513
|
+
//#region src/ribbon-action.ts
|
|
2514
|
+
/** Event name shared by every ribbon control; detail is the typed action. */
|
|
2515
|
+
var RIBBON_ACTION_EVENT = "ribbon-action";
|
|
2516
|
+
/** Internal, non-composed event: it stays inside the viewer's shadow tree. */
|
|
2517
|
+
function emitRibbonAction(target, action) {
|
|
2518
|
+
target.dispatchEvent(new CustomEvent(RIBBON_ACTION_EVENT, {
|
|
2519
|
+
detail: action,
|
|
2520
|
+
bubbles: true
|
|
2521
|
+
}));
|
|
2522
|
+
}
|
|
2523
|
+
//#endregion
|
|
2524
|
+
//#region src/ribbon-parts.ts
|
|
2525
|
+
/**
|
|
2526
|
+
* A large stacked ribbon command. `id` is a stable name for state sync and tests; the typed
|
|
2527
|
+
* `action` is what runs. Labels, icons and shortcuts are static application strings.
|
|
2528
|
+
*/
|
|
2529
|
+
function tool(doc, id, label, icon, action, options = {}) {
|
|
2530
|
+
const el = doc.createElement("office-ui-button");
|
|
2531
|
+
el.setAttribute("variant", "stacked");
|
|
2532
|
+
el.setAttribute("command", id);
|
|
2533
|
+
el.setAttribute("icon", icon);
|
|
2534
|
+
el.setAttribute("label", label);
|
|
2535
|
+
el.setAttribute("title", options.keys ? `${label} (${options.keys[1]})` : label);
|
|
2536
|
+
if (options.keys) el.setAttribute("keyshortcuts", options.keys[0]);
|
|
2537
|
+
if (options.pressed !== void 0) el.setAttribute("pressed", String(options.pressed));
|
|
2538
|
+
if (options.controls) el.setAttribute("aria-controls", options.controls);
|
|
2539
|
+
el.addEventListener("office-command", (event) => {
|
|
2540
|
+
event.stopPropagation();
|
|
2541
|
+
emitRibbonAction(el, action);
|
|
2542
|
+
});
|
|
2543
|
+
return el;
|
|
2544
|
+
}
|
|
2545
|
+
/** A labelled ribbon group (the shared `office-ui-ribbon-group`). */
|
|
2546
|
+
function group(doc, label, children, className = "") {
|
|
2547
|
+
const el = doc.createElement("office-ui-ribbon-group");
|
|
2548
|
+
el.setAttribute("label", label);
|
|
2549
|
+
if (className) el.className = className;
|
|
2550
|
+
el.append(...children);
|
|
2551
|
+
return el;
|
|
2552
|
+
}
|
|
2553
|
+
/** A keyboard-navigable row of groups (the shared `office-ui-toolbar`). */
|
|
2554
|
+
function commandRow(doc, label, children) {
|
|
2555
|
+
const el = doc.createElement("office-ui-toolbar");
|
|
2556
|
+
el.className = "ribbon-row";
|
|
2557
|
+
el.setAttribute("aria-label", label);
|
|
2558
|
+
el.append(...children);
|
|
2559
|
+
return el;
|
|
2560
|
+
}
|
|
2561
|
+
/** Muted two-line guidance at the end of a ribbon panel. */
|
|
2562
|
+
function hint(doc, lines) {
|
|
2563
|
+
const el = doc.createElement("p");
|
|
2564
|
+
el.className = "ribbon-hint";
|
|
2565
|
+
lines.forEach((line, index) => {
|
|
2566
|
+
if (index) el.append(doc.createElement("br"));
|
|
2567
|
+
el.append(line);
|
|
2568
|
+
});
|
|
2569
|
+
return el;
|
|
2570
|
+
}
|
|
2571
|
+
//#endregion
|
|
2572
|
+
//#region src/viewer-search.ts
|
|
2573
|
+
/** Builds the Find group: a search field, previous/next result buttons and a live status. */
|
|
2574
|
+
function createSearchGroup(doc) {
|
|
2575
|
+
const group = doc.createElement("div");
|
|
2576
|
+
group.className = "ribbon-group search-group";
|
|
2577
|
+
group.setAttribute("role", "group");
|
|
2578
|
+
group.setAttribute("aria-label", "Find");
|
|
2579
|
+
const controls = doc.createElement("div");
|
|
2580
|
+
controls.className = "search-controls";
|
|
2581
|
+
controls.setAttribute("role", "search");
|
|
2582
|
+
controls.setAttribute("aria-label", "Diagram text search");
|
|
2583
|
+
const label = doc.createElement("label");
|
|
2584
|
+
label.append("Find ");
|
|
2585
|
+
const input = doc.createElement("input");
|
|
2586
|
+
input.type = "search";
|
|
2587
|
+
input.setAttribute("aria-label", "Search diagram text");
|
|
2588
|
+
input.placeholder = "Text across pages";
|
|
2589
|
+
input.maxLength = TEXT_SEARCH_LIMITS.queryCharacters;
|
|
2590
|
+
input.autocomplete = "off";
|
|
2591
|
+
input.spellcheck = false;
|
|
2592
|
+
input.setAttribute("aria-describedby", "search-status");
|
|
2593
|
+
label.append(input);
|
|
2594
|
+
const step = (action, name, text) => {
|
|
2595
|
+
const button = doc.createElement("button");
|
|
2596
|
+
button.type = "button";
|
|
2597
|
+
button.dataset.action = action;
|
|
2598
|
+
button.setAttribute("aria-label", name);
|
|
2599
|
+
button.textContent = text;
|
|
2600
|
+
return button;
|
|
2601
|
+
};
|
|
2602
|
+
const status = doc.createElement("span");
|
|
2603
|
+
status.id = "search-status";
|
|
2604
|
+
status.setAttribute("role", "status");
|
|
2605
|
+
status.setAttribute("aria-live", "polite");
|
|
2606
|
+
status.setAttribute("aria-atomic", "true");
|
|
2607
|
+
controls.append(label, step("search-previous", "Previous matching shape", "Previous"), step("search-next", "Next matching shape", "Next"), status);
|
|
2608
|
+
const caption = doc.createElement("span");
|
|
2609
|
+
caption.className = "group-label";
|
|
2610
|
+
caption.textContent = "Find in drawing";
|
|
2611
|
+
group.append(controls, caption);
|
|
2612
|
+
return group;
|
|
2613
|
+
}
|
|
2614
|
+
function searchControls(root) {
|
|
2615
|
+
return {
|
|
2616
|
+
input: root.querySelector("input[type=\"search\"]"),
|
|
2617
|
+
status: root.querySelector("#search-status"),
|
|
2618
|
+
previous: root.querySelector("[data-action=\"search-previous\"]"),
|
|
2619
|
+
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
function renderSearchControls(controls, state) {
|
|
2623
|
+
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2624
|
+
controls.input.disabled = !state.document?.pages.length;
|
|
2625
|
+
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2626
|
+
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2627
|
+
const result = state.search.results[state.search.activeIndex];
|
|
2628
|
+
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2629
|
+
}
|
|
2630
|
+
//#endregion
|
|
2631
|
+
//#region src/ribbon-home.ts
|
|
2632
|
+
/**
|
|
2633
|
+
* Visio's Home tab, limited to commands this build can honestly perform: Undo, Tools and
|
|
2634
|
+
* Editing. Clipboard, Font, Paragraph, Shape Styles and Arrange wait for core edit commands.
|
|
2635
|
+
* The compact (phone) layout folds every group but Undo into the Tools disclosure.
|
|
2636
|
+
*/
|
|
2637
|
+
function buildHomePanel(doc, panel) {
|
|
2638
|
+
const tools = doc.createElement("details");
|
|
2639
|
+
tools.className = "ribbon-tools";
|
|
2640
|
+
tools.open = true;
|
|
2641
|
+
const summary = doc.createElement("summary");
|
|
2642
|
+
const caret = doc.createElement("span");
|
|
2643
|
+
caret.setAttribute("aria-hidden", "true");
|
|
2644
|
+
caret.textContent = "⌄";
|
|
2645
|
+
summary.append("Tools ", caret);
|
|
2646
|
+
const content = doc.createElement("div");
|
|
2647
|
+
content.className = "tools-content";
|
|
2648
|
+
content.append(group(doc, "Tools", [
|
|
2649
|
+
tool(doc, "pointer", "Pointer Tool", "pointer", {
|
|
2650
|
+
type: "tool",
|
|
2651
|
+
tool: "pointer"
|
|
2652
|
+
}, {
|
|
2653
|
+
keys: ["Control+1", "Ctrl+1"],
|
|
2654
|
+
pressed: true
|
|
2655
|
+
}),
|
|
2656
|
+
tool(doc, "rectangle", "Rectangle", "rectangle", {
|
|
2657
|
+
type: "tool",
|
|
2658
|
+
tool: "rectangle"
|
|
2659
|
+
}, {
|
|
2660
|
+
keys: ["Control+8", "Ctrl+8"],
|
|
2661
|
+
pressed: false
|
|
2662
|
+
}),
|
|
2663
|
+
tool(doc, "edit", "Edit text", "pencil", {
|
|
2664
|
+
type: "reveal",
|
|
2665
|
+
panel: "edit"
|
|
2666
|
+
}, { keys: ["F2", "F2"] })
|
|
2667
|
+
]), createSearchGroup(doc), group(doc, "Editing", [
|
|
2668
|
+
tool(doc, "delete", "Delete", "trash", { type: "delete" }, { keys: ["Delete", "Del"] }),
|
|
2669
|
+
tool(doc, "layers", "Layers", "visioLayers", {
|
|
2670
|
+
type: "reveal",
|
|
2671
|
+
panel: "layers"
|
|
2672
|
+
}),
|
|
2673
|
+
tool(doc, "selection", "Shape details", "visioShapeData", {
|
|
2674
|
+
type: "reveal",
|
|
2675
|
+
panel: "selection"
|
|
2676
|
+
})
|
|
2677
|
+
]));
|
|
2678
|
+
tools.append(summary, content);
|
|
2679
|
+
panel.append(commandRow(doc, "Home commands", [group(doc, "Undo", [tool(doc, "undo", "Undo", "undo", {
|
|
2680
|
+
type: "history",
|
|
2681
|
+
key: "undo"
|
|
2682
|
+
}, { keys: ["Control+Z", "Ctrl+Z"] }), tool(doc, "redo", "Redo", "redo", {
|
|
2683
|
+
type: "history",
|
|
2684
|
+
key: "redo"
|
|
2685
|
+
}, { keys: ["Control+Y", "Ctrl+Y"] })], "undo-group"), tools]), hint(doc, ["Rectangle: drag on the page to draw.", "Editing is experimental. Keep your original file."]));
|
|
2686
|
+
}
|
|
2687
|
+
//#endregion
|
|
2688
|
+
//#region src/ribbon-view.ts
|
|
2689
|
+
/** Visio's View tab: Views, Show (grid and task panes) and Zoom. */
|
|
2690
|
+
function buildViewPanel(doc, panel) {
|
|
2691
|
+
panel.append(commandRow(doc, "View commands", [
|
|
2692
|
+
group(doc, "Views", [tool(doc, "fullscreen", "Full Screen", "fullscreen", { type: "fullscreen" }, {
|
|
2693
|
+
keys: ["F5", "F5"],
|
|
2694
|
+
pressed: false
|
|
2695
|
+
})]),
|
|
2696
|
+
group(doc, "Show", [
|
|
2697
|
+
tool(doc, "grid", "Grid", "grid", { type: "grid" }, { pressed: false }),
|
|
2698
|
+
tool(doc, "pages", "Pages pane", "visioPagesPane", {
|
|
2699
|
+
type: "pane",
|
|
2700
|
+
pane: "pages"
|
|
2701
|
+
}, {
|
|
2702
|
+
pressed: true,
|
|
2703
|
+
controls: "page-rail"
|
|
2704
|
+
}),
|
|
2705
|
+
tool(doc, "inspector", "Inspector pane", "visioInspectorPane", {
|
|
2706
|
+
type: "pane",
|
|
2707
|
+
pane: "inspector"
|
|
2708
|
+
}, {
|
|
2709
|
+
pressed: true,
|
|
2710
|
+
controls: "inspector-pane"
|
|
2711
|
+
}),
|
|
2712
|
+
tool(doc, "notes", "Review notes", "message", {
|
|
2713
|
+
type: "reveal",
|
|
2714
|
+
panel: "notes"
|
|
2715
|
+
})
|
|
2716
|
+
]),
|
|
2717
|
+
group(doc, "Zoom", [
|
|
2718
|
+
tool(doc, "zoom-fit", "Fit to Window", "fitPage", {
|
|
2719
|
+
type: "zoom",
|
|
2720
|
+
mode: "fit"
|
|
2721
|
+
}, { keys: ["Control+Shift+W", "Ctrl+Shift+W"] }),
|
|
2722
|
+
tool(doc, "page-width", "Page Width", "pageWidth", {
|
|
2723
|
+
type: "zoom",
|
|
2724
|
+
mode: "width"
|
|
2725
|
+
}),
|
|
2726
|
+
tool(doc, "actual-size", "100%", "search", {
|
|
2727
|
+
type: "zoom",
|
|
2728
|
+
mode: "actual"
|
|
2729
|
+
})
|
|
2730
|
+
])
|
|
2731
|
+
]), hint(doc, ["Ctrl+wheel or + / − to zoom.", "Ctrl+Page Up / Page Down changes page."]));
|
|
2732
|
+
}
|
|
2733
|
+
//#endregion
|
|
2734
|
+
//#region src/ribbon.ts
|
|
2735
|
+
var TABS = [[
|
|
2736
|
+
"home",
|
|
2737
|
+
"Home",
|
|
2738
|
+
buildHomePanel
|
|
2739
|
+
], [
|
|
2740
|
+
"view",
|
|
2741
|
+
"View",
|
|
2742
|
+
buildViewPanel
|
|
2743
|
+
]];
|
|
2744
|
+
/**
|
|
2745
|
+
* The Visio ribbon: a tab list and one panel per tab. Tab switching and keyboard movement
|
|
2746
|
+
* belong to the chrome; commands emit `ribbon-action` events for the router.
|
|
2747
|
+
*/
|
|
2748
|
+
function createRibbon(doc) {
|
|
2749
|
+
const toolbar = doc.createElement("div");
|
|
2750
|
+
toolbar.className = "toolbar";
|
|
2751
|
+
toolbar.setAttribute("role", "group");
|
|
2752
|
+
toolbar.setAttribute("aria-label", "Diagram controls");
|
|
2753
|
+
const tabs = doc.createElement("div");
|
|
2754
|
+
tabs.className = "ribbon-tabs";
|
|
2755
|
+
tabs.setAttribute("role", "tablist");
|
|
2756
|
+
tabs.setAttribute("aria-label", "Ribbon");
|
|
2757
|
+
toolbar.append(tabs);
|
|
2758
|
+
for (const [key, name, build] of TABS) {
|
|
2759
|
+
const selected = key === "home";
|
|
2760
|
+
const tab = doc.createElement("button");
|
|
2761
|
+
tab.type = "button";
|
|
2762
|
+
tab.id = `${key}-tab`;
|
|
2763
|
+
tab.dataset.tab = key;
|
|
2764
|
+
tab.textContent = name;
|
|
2765
|
+
tab.setAttribute("role", "tab");
|
|
2766
|
+
tab.setAttribute("aria-selected", String(selected));
|
|
2767
|
+
tab.setAttribute("aria-controls", `${key}-panel`);
|
|
2768
|
+
if (!selected) tab.tabIndex = -1;
|
|
2769
|
+
const panel = doc.createElement("div");
|
|
2770
|
+
panel.className = "ribbon-content";
|
|
2771
|
+
panel.id = `${key}-panel`;
|
|
2772
|
+
panel.setAttribute("role", "tabpanel");
|
|
2773
|
+
panel.setAttribute("aria-labelledby", tab.id);
|
|
2774
|
+
panel.hidden = !selected;
|
|
2775
|
+
build(doc, panel);
|
|
2776
|
+
tabs.append(tab);
|
|
2777
|
+
toolbar.append(panel);
|
|
2778
|
+
}
|
|
2779
|
+
return toolbar;
|
|
2780
|
+
}
|
|
2781
|
+
//#endregion
|
|
2782
|
+
//#region src/status-bar.ts
|
|
2783
|
+
/**
|
|
2784
|
+
* Visio's status bar on the shared `office-ui-status-bar`: page and shape metrics, a live
|
|
2785
|
+
* message region, compatibility notes, the host footer slot and the shared zoom slider.
|
|
2786
|
+
*/
|
|
2787
|
+
function createStatusBar(doc) {
|
|
2788
|
+
const bar = doc.createElement("office-ui-status-bar");
|
|
2789
|
+
bar.className = "status";
|
|
2790
|
+
bar.setAttribute("label", "Status bar");
|
|
2791
|
+
const item = (name) => {
|
|
2792
|
+
const el = doc.createElement("office-ui-status-item");
|
|
2793
|
+
el.setAttribute(name, "");
|
|
2794
|
+
el.setAttribute("label", "");
|
|
2795
|
+
el.setAttribute("value", "");
|
|
2796
|
+
return el;
|
|
2797
|
+
};
|
|
2798
|
+
const message = doc.createElement("div");
|
|
2799
|
+
message.className = "status-message";
|
|
2800
|
+
message.setAttribute("role", "status");
|
|
2801
|
+
const text = doc.createElement("span");
|
|
2802
|
+
text.dataset.status = "";
|
|
2803
|
+
const diagnostics = doc.createElement("span");
|
|
2804
|
+
diagnostics.dataset.diagnostics = "";
|
|
2805
|
+
message.append(text, diagnostics);
|
|
2806
|
+
const notes = doc.createElement("div");
|
|
2807
|
+
notes.className = "notes-strip";
|
|
2808
|
+
const notesButton = doc.createElement("button");
|
|
2809
|
+
notesButton.type = "button";
|
|
2810
|
+
notesButton.dataset.chrome = "notes";
|
|
2811
|
+
notesButton.setAttribute("aria-controls", "inspector-pane");
|
|
2812
|
+
notesButton.setAttribute("aria-expanded", "false");
|
|
2813
|
+
const count = doc.createElement("span");
|
|
2814
|
+
count.className = "notes-count";
|
|
2815
|
+
count.dataset.noteCount = "";
|
|
2816
|
+
notesButton.append("Notes", count);
|
|
2817
|
+
const local = doc.createElement("span");
|
|
2818
|
+
local.textContent = "Files stay in your browser";
|
|
2819
|
+
notes.append(notesButton, local);
|
|
2820
|
+
const footer = doc.createElement("slot");
|
|
2821
|
+
footer.name = "workspace-footer";
|
|
2822
|
+
const zoom = doc.createElement("office-ui-zoom-slider");
|
|
2823
|
+
zoom.slot = "end";
|
|
2824
|
+
zoom.className = "zoom-controls";
|
|
2825
|
+
zoom.setAttribute("label", "Canvas zoom");
|
|
2826
|
+
zoom.setAttribute("fit", "Fit page to current window");
|
|
2827
|
+
zoom.setAttribute("value", "100");
|
|
2828
|
+
bar.append(item("data-page-status"), item("data-shape-status"), message, notes, footer, zoom);
|
|
2829
|
+
return bar;
|
|
2830
|
+
}
|
|
2831
|
+
/** Visio's page tabs under the drawing, on the shared document tab strip. */
|
|
2832
|
+
function createPageTabs(doc) {
|
|
2833
|
+
const strip = doc.createElement("office-ui-tab-strip");
|
|
2834
|
+
strip.className = "page-tabs";
|
|
2835
|
+
strip.setAttribute("label", "Pages");
|
|
2836
|
+
strip.setAttribute("previous-label", "Previous page");
|
|
2837
|
+
strip.setAttribute("next-label", "Next page");
|
|
2838
|
+
return strip;
|
|
2839
|
+
}
|
|
2840
|
+
//#endregion
|
|
2547
2841
|
//#region src/edit-error.ts
|
|
2548
2842
|
/** Core error codes remain visible for a refused transaction; document content is plain text. */
|
|
2549
2843
|
function editErrorMessage(error) {
|
|
@@ -2831,59 +3125,13 @@ var ViewerEditControls = class {
|
|
|
2831
3125
|
}
|
|
2832
3126
|
};
|
|
2833
3127
|
//#endregion
|
|
2834
|
-
//#region src/viewer-search.ts
|
|
2835
|
-
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>`;
|
|
2836
|
-
function searchControls(root) {
|
|
2837
|
-
return {
|
|
2838
|
-
input: root.querySelector("input[type=\"search\"]"),
|
|
2839
|
-
status: root.querySelector("#search-status"),
|
|
2840
|
-
previous: root.querySelector("[data-action=\"search-previous\"]"),
|
|
2841
|
-
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2842
|
-
};
|
|
2843
|
-
}
|
|
2844
|
-
function renderSearchControls(controls, state) {
|
|
2845
|
-
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2846
|
-
controls.input.disabled = !state.document?.pages.length;
|
|
2847
|
-
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2848
|
-
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2849
|
-
const result = state.search.results[state.search.activeIndex];
|
|
2850
|
-
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2851
|
-
}
|
|
2852
|
-
//#endregion
|
|
2853
3128
|
//#region src/viewer-chrome.ts
|
|
2854
|
-
/**
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
<button type="button" id="view-tab" role="tab" aria-selected="false" aria-controls="view-panel" tabindex="-1" data-tab="view">View</button>
|
|
2861
|
-
</div>
|
|
2862
|
-
<div class="ribbon-content" id="home-panel" role="tabpanel" aria-labelledby="home-tab">
|
|
2863
|
-
<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>
|
|
2864
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2865
|
-
<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>
|
|
2866
|
-
<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>
|
|
2867
|
-
<button class="ribbon-command" type="button" data-chrome="layers"><span class="command-icon" data-icon="layers" aria-hidden="true"></span><span>Layers</span></button>
|
|
2868
|
-
</div><span class="group-label">Inspect and edit</span></div>
|
|
2869
|
-
</div></details>
|
|
2870
|
-
<div class="ribbon-primary">
|
|
2871
|
-
<label class="page-picker">Page <select aria-label="Page"></select></label>
|
|
2872
|
-
<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>
|
|
2873
|
-
<span class="spacer"></span><span class="local-label">Local workspace</span>
|
|
2874
|
-
</div>
|
|
2875
|
-
<p class="ribbon-hint">Select a shape to inspect its details.<br>Text editing is experimental.</p>
|
|
2876
|
-
</div>
|
|
2877
|
-
<div class="ribbon-content" id="view-panel" role="tabpanel" aria-labelledby="view-tab" hidden>
|
|
2878
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2879
|
-
<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>
|
|
2880
|
-
<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>
|
|
2881
|
-
<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>
|
|
2882
|
-
</div><span class="group-label">Workspace panes</span></div>
|
|
2883
|
-
<p class="ribbon-hint">Canvas shortcuts: + / − to zoom, 0 to fit.<br>Arrow keys move between focused shapes.</p>
|
|
2884
|
-
</div>
|
|
2885
|
-
</div>
|
|
2886
|
-
<div class="workspace">
|
|
3129
|
+
/**
|
|
3130
|
+
* Static workspace markup (legacy; migrate to builders when next changed). The ribbon, page
|
|
3131
|
+
* tabs and status bar are built by `createRibbon`, `createPageTabs` and `createStatusBar`.
|
|
3132
|
+
* All document labels are inserted as text.
|
|
3133
|
+
*/
|
|
3134
|
+
var viewerChromeTemplate = `<div class="workspace">
|
|
2887
3135
|
<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>
|
|
2888
3136
|
<div class="viewport" tabindex="0" role="region" aria-label="Diagram canvas"></div>
|
|
2889
3137
|
<aside id="inspector-pane" class="inspector-pane" aria-label="Drawing inspector">
|
|
@@ -2896,14 +3144,15 @@ var viewerChromeTemplate = `
|
|
|
2896
3144
|
<details class="notes inspector-card"><summary>Compatibility notes</summary><ul></ul></details>
|
|
2897
3145
|
</div>
|
|
2898
3146
|
</aside>
|
|
2899
|
-
</div
|
|
2900
|
-
<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>`;
|
|
3147
|
+
</div>`;
|
|
2901
3148
|
/** Presentation state stays local; page navigation uses the same controller as every binding. */
|
|
2902
3149
|
var ViewerChrome = class {
|
|
2903
3150
|
#root;
|
|
2904
3151
|
#controller;
|
|
2905
3152
|
#document;
|
|
2906
3153
|
#pageList;
|
|
3154
|
+
#pageTabs;
|
|
3155
|
+
#tools;
|
|
2907
3156
|
#rail;
|
|
2908
3157
|
#inspector;
|
|
2909
3158
|
#notes;
|
|
@@ -2915,6 +3164,8 @@ var ViewerChrome = class {
|
|
|
2915
3164
|
this.#root = root;
|
|
2916
3165
|
this.#controller = controller;
|
|
2917
3166
|
this.#pageList = root.querySelector(".page-list");
|
|
3167
|
+
this.#pageTabs = root.querySelector("office-ui-tab-strip");
|
|
3168
|
+
this.#tools = root.querySelector(".ribbon-tools");
|
|
2918
3169
|
this.#rail = root.querySelector(".page-rail");
|
|
2919
3170
|
this.#inspector = root.querySelector(".inspector-pane");
|
|
2920
3171
|
this.#notes = root.querySelector(".notes");
|
|
@@ -2925,7 +3176,7 @@ var ViewerChrome = class {
|
|
|
2925
3176
|
this.#responsive = view?.matchMedia?.("(max-width: 980px)");
|
|
2926
3177
|
const compact = view?.matchMedia?.("(max-width: 760px)");
|
|
2927
3178
|
this.#compact = compact;
|
|
2928
|
-
const tools = this.#
|
|
3179
|
+
const tools = this.#tools;
|
|
2929
3180
|
const toolLayout = () => {
|
|
2930
3181
|
tools.open = !compact?.matches;
|
|
2931
3182
|
};
|
|
@@ -2952,22 +3203,16 @@ var ViewerChrome = class {
|
|
|
2952
3203
|
this.#root.addEventListener("click", (event) => {
|
|
2953
3204
|
const button = event.target?.closest?.("button");
|
|
2954
3205
|
if (!button || button.disabled) return;
|
|
2955
|
-
if (compact?.matches && button.dataset.chrome) tools.open = false;
|
|
2956
3206
|
const tab = button.dataset.tab;
|
|
2957
|
-
if (tab === "home" || tab === "view") this
|
|
3207
|
+
if (tab === "home" || tab === "view") this.showTab(tab);
|
|
2958
3208
|
if (button.dataset.pageIndex !== void 0) this.#controller.setPage(Number(button.dataset.pageIndex));
|
|
2959
3209
|
const action = button.dataset.chrome;
|
|
2960
|
-
if (action === "
|
|
2961
|
-
if (action === "
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
if (action === "inspector") {
|
|
2967
|
-
this.#inspectorManuallyToggled = true;
|
|
2968
|
-
this.#setPane("inspector", this.#inspector.hidden);
|
|
2969
|
-
}
|
|
2970
|
-
if (action === "notes" || action === "selection" || action === "edit" || action === "layers") this.#reveal(action);
|
|
3210
|
+
if (action === "inspector") this.togglePane("inspector");
|
|
3211
|
+
if (action === "notes") this.reveal("notes");
|
|
3212
|
+
}, options);
|
|
3213
|
+
this.#root.addEventListener("office-tab-select", (event) => {
|
|
3214
|
+
const id = Number(event.detail.id);
|
|
3215
|
+
if (Number.isSafeInteger(id)) this.#controller.setPage(id);
|
|
2971
3216
|
}, options);
|
|
2972
3217
|
this.#root.addEventListener("keydown", (event) => {
|
|
2973
3218
|
const key = event;
|
|
@@ -2984,11 +3229,11 @@ var ViewerChrome = class {
|
|
|
2984
3229
|
if (key.key === "ArrowUp" || key.key === "ArrowDown") return;
|
|
2985
3230
|
key.preventDefault();
|
|
2986
3231
|
const tab = key.key === "Home" ? "home" : key.key === "End" ? "view" : button.dataset.tab === "home" ? "view" : "home";
|
|
2987
|
-
this
|
|
3232
|
+
this.showTab(tab);
|
|
2988
3233
|
this.#root.querySelector(`[data-tab="${tab}"]`).focus();
|
|
2989
3234
|
} else if (button.dataset.pageIndex !== void 0) {
|
|
2990
3235
|
key.preventDefault();
|
|
2991
|
-
const pages = [...this.#pageList.querySelectorAll("
|
|
3236
|
+
const pages = [...this.#pageList.querySelectorAll("[data-page-index]")];
|
|
2992
3237
|
const current = Number(button.dataset.pageIndex);
|
|
2993
3238
|
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)));
|
|
2994
3239
|
this.#controller.setPage(next);
|
|
@@ -2998,7 +3243,7 @@ var ViewerChrome = class {
|
|
|
2998
3243
|
this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
|
|
2999
3244
|
return () => events.abort();
|
|
3000
3245
|
}
|
|
3001
|
-
|
|
3246
|
+
showTab(tab) {
|
|
3002
3247
|
for (const button of this.#root.querySelectorAll("[data-tab]")) {
|
|
3003
3248
|
const selected = button.dataset.tab === tab;
|
|
3004
3249
|
button.setAttribute("aria-selected", String(selected));
|
|
@@ -3006,27 +3251,41 @@ var ViewerChrome = class {
|
|
|
3006
3251
|
}
|
|
3007
3252
|
for (const panel of this.#root.querySelectorAll("[role=\"tabpanel\"]")) panel.hidden = panel.id !== `${tab}-panel`;
|
|
3008
3253
|
}
|
|
3254
|
+
/** Show or hide a task pane; compact layouts close the Tools menu afterwards. */
|
|
3255
|
+
togglePane(pane) {
|
|
3256
|
+
if (this.#compact?.matches) this.#tools.open = false;
|
|
3257
|
+
if (pane === "pages") this.#pagesManuallyToggled = true;
|
|
3258
|
+
else this.#inspectorManuallyToggled = true;
|
|
3259
|
+
this.#setPane(pane, (pane === "pages" ? this.#rail : this.#inspector).hidden);
|
|
3260
|
+
}
|
|
3261
|
+
#command(name) {
|
|
3262
|
+
return this.#root.querySelector(`office-ui-button[command="${name}"]`);
|
|
3263
|
+
}
|
|
3009
3264
|
#setPane(pane, visible) {
|
|
3010
3265
|
if (visible && this.#compact?.matches) {
|
|
3011
3266
|
const other = pane === "pages" ? "inspector" : "pages";
|
|
3012
3267
|
(other === "pages" ? this.#rail : this.#inspector).hidden = true;
|
|
3013
|
-
this.#
|
|
3268
|
+
this.#command(other).setAttribute("pressed", "false");
|
|
3014
3269
|
}
|
|
3015
3270
|
(pane === "pages" ? this.#rail : this.#inspector).hidden = !visible;
|
|
3016
|
-
this.#
|
|
3271
|
+
this.#command(pane).setAttribute("pressed", String(visible));
|
|
3017
3272
|
this.#syncNotes();
|
|
3018
3273
|
}
|
|
3019
3274
|
#syncNotes() {
|
|
3020
3275
|
this.#root.querySelector(".notes-strip button").setAttribute("aria-expanded", String(!this.#inspector.hidden && this.#notes.open));
|
|
3021
3276
|
}
|
|
3022
|
-
|
|
3277
|
+
/** Open an inspector disclosure; text editing may focus the text field directly (F2). */
|
|
3278
|
+
reveal(kind, focusText = false) {
|
|
3279
|
+
if (this.#compact?.matches) this.#tools.open = false;
|
|
3023
3280
|
this.#inspectorManuallyToggled = true;
|
|
3024
3281
|
this.#setPane("inspector", true);
|
|
3025
3282
|
const selector = kind === "notes" ? ".notes" : kind === "edit" ? ".edit-controls" : kind === "layers" ? ".layer-controls" : ".shape-inspector";
|
|
3026
3283
|
const panel = this.#root.querySelector(selector);
|
|
3027
3284
|
if (panel.hidden) return;
|
|
3028
3285
|
panel.open = true;
|
|
3029
|
-
panel.querySelector("
|
|
3286
|
+
const text = panel.querySelector("textarea");
|
|
3287
|
+
if (focusText && text && !text.disabled) text.focus();
|
|
3288
|
+
else panel.querySelector("summary").focus();
|
|
3030
3289
|
panel.scrollIntoView?.({
|
|
3031
3290
|
block: "nearest",
|
|
3032
3291
|
inline: "nearest"
|
|
@@ -3067,14 +3326,21 @@ var ViewerChrome = class {
|
|
|
3067
3326
|
fragment.append(item);
|
|
3068
3327
|
}
|
|
3069
3328
|
this.#pageList.replaceChildren(fragment);
|
|
3329
|
+
this.#pageTabs.tabs = (state.document?.pages ?? []).map((candidate, index) => ({
|
|
3330
|
+
id: String(index),
|
|
3331
|
+
label: candidate.name,
|
|
3332
|
+
title: candidate.isBackground ? `${candidate.name} (background page)` : candidate.name
|
|
3333
|
+
}));
|
|
3070
3334
|
if (restoreFocus !== void 0) [...this.#pageList.querySelectorAll("button")].find((button) => button.dataset.pageIndex === restoreFocus)?.focus();
|
|
3071
3335
|
}
|
|
3336
|
+
this.#pageTabs.selected = page ? String(state.pageIndex) : "";
|
|
3072
3337
|
for (const button of this.#pageList.querySelectorAll("button")) {
|
|
3073
3338
|
const selected = Number(button.dataset.pageIndex) === state.pageIndex;
|
|
3074
3339
|
if (selected) button.setAttribute("aria-current", "page");
|
|
3075
3340
|
else button.removeAttribute("aria-current");
|
|
3076
3341
|
button.tabIndex = selected ? 0 : -1;
|
|
3077
3342
|
}
|
|
3343
|
+
this.#root.querySelector("[data-page-status]").setAttribute("value", page ? `Page ${state.pageIndex + 1} of ${state.document.pages.length}` : "");
|
|
3078
3344
|
const count = state.document?.pages.length ?? 0;
|
|
3079
3345
|
this.#root.querySelector("[data-page-count]").textContent = String(count);
|
|
3080
3346
|
this.#root.querySelector(".page-empty").hidden = count > 0;
|
|
@@ -3082,17 +3348,405 @@ var ViewerChrome = class {
|
|
|
3082
3348
|
this.#root.querySelector("[data-page-size]").textContent = page ? `${page.width} × ${page.height} in` : "No page";
|
|
3083
3349
|
this.#root.querySelector("[data-page-shapes]").textContent = page ? `${page.shapes.length} top-level` : "No shapes";
|
|
3084
3350
|
this.#root.querySelector(".selection-hint").hidden = !!state.selectedShape;
|
|
3085
|
-
this.#
|
|
3086
|
-
this.#
|
|
3087
|
-
this.#
|
|
3088
|
-
this.#
|
|
3089
|
-
this.#root.querySelector("
|
|
3090
|
-
for (const button of this.#root.querySelectorAll("[data-chrome=\"notes\"]")) button.disabled = noteCount === 0;
|
|
3351
|
+
this.#command("selection").disabled = !state.selectedShape;
|
|
3352
|
+
this.#command("edit").disabled = !page;
|
|
3353
|
+
this.#command("layers").disabled = this.#root.querySelector(".layer-controls").hidden;
|
|
3354
|
+
this.#command("notes").disabled = noteCount === 0;
|
|
3355
|
+
this.#root.querySelector(".notes-strip button").disabled = noteCount === 0;
|
|
3091
3356
|
this.#root.querySelector("[data-note-count]").textContent = noteCount ? ` · ${noteCount}` : "";
|
|
3092
3357
|
this.#syncNotes();
|
|
3093
3358
|
}
|
|
3094
3359
|
};
|
|
3095
3360
|
//#endregion
|
|
3361
|
+
//#region src/ribbon-router.ts
|
|
3362
|
+
/** Routes a ribbon, status-bar or shortcut action to the controller that owns it. */
|
|
3363
|
+
function routeRibbonAction(targets, action) {
|
|
3364
|
+
const { controller } = targets;
|
|
3365
|
+
const page = controller.state.document?.pages[controller.state.pageIndex];
|
|
3366
|
+
switch (action.type) {
|
|
3367
|
+
case "history": return targets.history(action.key);
|
|
3368
|
+
case "delete": return targets.deleteSelection();
|
|
3369
|
+
case "tool": return targets.setTool(action.tool);
|
|
3370
|
+
case "grid": return targets.toggleGrid();
|
|
3371
|
+
case "fullscreen": return targets.toggleFullscreen();
|
|
3372
|
+
case "pane": return targets.togglePane(action.pane);
|
|
3373
|
+
case "reveal": return targets.reveal(action.panel, action.focusText ?? false);
|
|
3374
|
+
case "search": return targets.focusSearch();
|
|
3375
|
+
case "page": return controller.setPage(controller.state.pageIndex + action.step);
|
|
3376
|
+
case "zoom":
|
|
3377
|
+
if (!page) return;
|
|
3378
|
+
if (action.mode === "actual") return controller.setZoom(1);
|
|
3379
|
+
return targets.fit(action.mode === "fit" ? "page" : "width");
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
//#endregion
|
|
3383
|
+
//#region src/viewer-draw-tool.ts
|
|
3384
|
+
/** Visio snaps new geometry to ruler subdivisions; 1/16 inch matches its default fine grid. */
|
|
3385
|
+
var SNAP = 1 / 16;
|
|
3386
|
+
var MIN_SIZE = SNAP;
|
|
3387
|
+
var snap = (value) => Math.round(value / SNAP) * SNAP;
|
|
3388
|
+
/** Next free numeric shape ID on the page tree. Core still rejects any collision. */
|
|
3389
|
+
function nextShapeId(page) {
|
|
3390
|
+
let max = 0;
|
|
3391
|
+
const pending = [...page.shapes];
|
|
3392
|
+
while (pending.length) {
|
|
3393
|
+
const shape = pending.pop();
|
|
3394
|
+
if (/^\d{1,9}$/.test(shape.id)) max = Math.max(max, Number(shape.id));
|
|
3395
|
+
pending.push(...shape.children);
|
|
3396
|
+
}
|
|
3397
|
+
return String(max + 1);
|
|
3398
|
+
}
|
|
3399
|
+
/** Page inches with a top-left origin (SVG user space) from a client point. */
|
|
3400
|
+
function pagePoint(svg, page, event) {
|
|
3401
|
+
const matrix = svg.getScreenCTM?.();
|
|
3402
|
+
if (!matrix) return void 0;
|
|
3403
|
+
const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse());
|
|
3404
|
+
return {
|
|
3405
|
+
x: snap(Math.max(0, Math.min(page.width, point.x))),
|
|
3406
|
+
y: snap(Math.max(0, Math.min(page.height, point.y)))
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
/** Drag-to-draw rectangle tool. The preview is viewer-only; the shape is created by core. */
|
|
3410
|
+
var RectangleDrawTool = class {
|
|
3411
|
+
viewport;
|
|
3412
|
+
controller;
|
|
3413
|
+
options;
|
|
3414
|
+
#drag;
|
|
3415
|
+
#request = 0;
|
|
3416
|
+
constructor(viewport, controller, options) {
|
|
3417
|
+
this.viewport = viewport;
|
|
3418
|
+
this.controller = controller;
|
|
3419
|
+
this.options = options;
|
|
3420
|
+
}
|
|
3421
|
+
get drawing() {
|
|
3422
|
+
return !!this.#drag;
|
|
3423
|
+
}
|
|
3424
|
+
wire() {
|
|
3425
|
+
const events = new ((this.viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
3426
|
+
const options = { signal: events.signal };
|
|
3427
|
+
const viewport = this.viewport;
|
|
3428
|
+
viewport.addEventListener("click", (event) => {
|
|
3429
|
+
if (this.options.active()) event.stopImmediatePropagation();
|
|
3430
|
+
}, {
|
|
3431
|
+
...options,
|
|
3432
|
+
capture: true
|
|
3433
|
+
});
|
|
3434
|
+
viewport.addEventListener("pointerdown", (event) => this.#start(event), options);
|
|
3435
|
+
viewport.addEventListener("pointermove", (event) => this.#move(event), options);
|
|
3436
|
+
viewport.addEventListener("pointerup", (event) => void this.#finish(event), options);
|
|
3437
|
+
viewport.addEventListener("pointercancel", () => this.#cancel(), options);
|
|
3438
|
+
viewport.addEventListener("keydown", (event) => {
|
|
3439
|
+
if (event.key === "Escape" && this.#drag) {
|
|
3440
|
+
event.preventDefault();
|
|
3441
|
+
this.#cancel();
|
|
3442
|
+
}
|
|
3443
|
+
}, options);
|
|
3444
|
+
return () => {
|
|
3445
|
+
++this.#request;
|
|
3446
|
+
this.#cancel();
|
|
3447
|
+
events.abort();
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
#start(event) {
|
|
3451
|
+
if (!this.options.active() || event.button !== 0 || this.#drag) return;
|
|
3452
|
+
const state = this.controller.state;
|
|
3453
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3454
|
+
const svg = this.viewport.querySelector("svg.paper");
|
|
3455
|
+
if (!page || !svg || !state.edit.sourceAvailable || state.edit.busy) return;
|
|
3456
|
+
const start = pagePoint(svg, page, event);
|
|
3457
|
+
if (!start) return;
|
|
3458
|
+
event.preventDefault();
|
|
3459
|
+
const rect = this.viewport.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
3460
|
+
rect.classList.add("draw-preview");
|
|
3461
|
+
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
3462
|
+
svg.append(rect);
|
|
3463
|
+
this.#drag = {
|
|
3464
|
+
pointer: event.pointerId,
|
|
3465
|
+
svg,
|
|
3466
|
+
page,
|
|
3467
|
+
...start,
|
|
3468
|
+
rect
|
|
3469
|
+
};
|
|
3470
|
+
this.viewport.setPointerCapture?.(event.pointerId);
|
|
3471
|
+
this.#update(start);
|
|
3472
|
+
}
|
|
3473
|
+
#move(event) {
|
|
3474
|
+
if (!this.#drag || event.pointerId !== this.#drag.pointer) return;
|
|
3475
|
+
const point = pagePoint(this.#drag.svg, this.#drag.page, event);
|
|
3476
|
+
if (point) this.#update(point);
|
|
3477
|
+
}
|
|
3478
|
+
#update(point) {
|
|
3479
|
+
const { x, y, rect } = this.#drag;
|
|
3480
|
+
rect.setAttribute("x", String(Math.min(x, point.x)));
|
|
3481
|
+
rect.setAttribute("y", String(Math.min(y, point.y)));
|
|
3482
|
+
rect.setAttribute("width", String(Math.abs(point.x - x)));
|
|
3483
|
+
rect.setAttribute("height", String(Math.abs(point.y - y)));
|
|
3484
|
+
}
|
|
3485
|
+
#cancel() {
|
|
3486
|
+
this.#drag?.rect.remove();
|
|
3487
|
+
this.#drag = void 0;
|
|
3488
|
+
}
|
|
3489
|
+
async #finish(event) {
|
|
3490
|
+
const drag = this.#drag;
|
|
3491
|
+
if (!drag || event.pointerId !== drag.pointer) return;
|
|
3492
|
+
const end = pagePoint(drag.svg, drag.page, event) ?? drag;
|
|
3493
|
+
this.#cancel();
|
|
3494
|
+
const width = Math.abs(end.x - drag.x), height = Math.abs(end.y - drag.y);
|
|
3495
|
+
if (width < MIN_SIZE || height < MIN_SIZE) {
|
|
3496
|
+
this.options.announce("Drag on the page to draw a rectangle.");
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
const state = this.controller.state;
|
|
3500
|
+
if (state.document?.pages[state.pageIndex] !== drag.page) return;
|
|
3501
|
+
const shapeId = nextShapeId(drag.page);
|
|
3502
|
+
const pageId = drag.page.id;
|
|
3503
|
+
const request = ++this.#request;
|
|
3504
|
+
try {
|
|
3505
|
+
await this.controller.applyEdits([{
|
|
3506
|
+
type: "create-rectangle",
|
|
3507
|
+
pageId,
|
|
3508
|
+
shapeId,
|
|
3509
|
+
x: (drag.x + end.x) / 2,
|
|
3510
|
+
y: drag.page.height - (drag.y + end.y) / 2,
|
|
3511
|
+
width,
|
|
3512
|
+
height
|
|
3513
|
+
}]);
|
|
3514
|
+
if (request !== this.#request) return;
|
|
3515
|
+
const created = this.controller.state.document?.pages.find((page) => page.id === pageId)?.shapes.find((shape) => shape.id === shapeId);
|
|
3516
|
+
if (created) this.controller.selectShape({
|
|
3517
|
+
id: shapeId,
|
|
3518
|
+
name: created.name,
|
|
3519
|
+
pageId
|
|
3520
|
+
});
|
|
3521
|
+
this.options.announce(`Rectangle ${shapeId} added (${+width.toFixed(4)} × ${+height.toFixed(4)} in).`);
|
|
3522
|
+
} catch (error) {
|
|
3523
|
+
if (request === this.#request && !isEditCancellation(error)) this.options.announce(editErrorMessage(error));
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
};
|
|
3527
|
+
//#endregion
|
|
3528
|
+
//#region src/viewer-commands.ts
|
|
3529
|
+
var editable = (target) => target instanceof Element && !!target.closest("input, textarea, select, [contenteditable]:not([contenteditable=\"false\"])");
|
|
3530
|
+
/**
|
|
3531
|
+
* Visio command state (tool, grid, pending edits) and keyboard shortcuts. Ribbon controls emit
|
|
3532
|
+
* typed `ribbon-action` events and shortcuts build the same actions; both go through
|
|
3533
|
+
* `routeRibbonAction`. Document mutation stays in the controller and core.
|
|
3534
|
+
*/
|
|
3535
|
+
var ViewerCommands = class {
|
|
3536
|
+
host;
|
|
3537
|
+
#tool = "pointer";
|
|
3538
|
+
#grid = false;
|
|
3539
|
+
#pending = 0;
|
|
3540
|
+
#draw;
|
|
3541
|
+
#targets;
|
|
3542
|
+
constructor(host) {
|
|
3543
|
+
this.host = host;
|
|
3544
|
+
this.#draw = new RectangleDrawTool(host.viewport, host.controller, {
|
|
3545
|
+
active: () => this.#tool === "rectangle",
|
|
3546
|
+
announce: host.announce
|
|
3547
|
+
});
|
|
3548
|
+
this.#targets = {
|
|
3549
|
+
controller: host.controller,
|
|
3550
|
+
history: (key) => this.#history(key),
|
|
3551
|
+
deleteSelection: () => this.#delete(),
|
|
3552
|
+
setTool: (tool) => this.setTool(tool),
|
|
3553
|
+
toggleGrid: () => {
|
|
3554
|
+
this.#grid = !this.#grid;
|
|
3555
|
+
this.render(host.controller.state);
|
|
3556
|
+
},
|
|
3557
|
+
toggleFullscreen: () => this.#toggleFullscreen(),
|
|
3558
|
+
togglePane: host.togglePane,
|
|
3559
|
+
reveal: host.reveal,
|
|
3560
|
+
fit: host.fit,
|
|
3561
|
+
focusSearch: host.focusSearch
|
|
3562
|
+
};
|
|
3563
|
+
}
|
|
3564
|
+
get tool() {
|
|
3565
|
+
return this.#tool;
|
|
3566
|
+
}
|
|
3567
|
+
/** Run one typed action, exactly as a ribbon control or shortcut would. */
|
|
3568
|
+
run(action) {
|
|
3569
|
+
routeRibbonAction(this.#targets, action);
|
|
3570
|
+
}
|
|
3571
|
+
wire() {
|
|
3572
|
+
const { root, viewport } = this.host;
|
|
3573
|
+
const doc = root.ownerDocument;
|
|
3574
|
+
const events = new ((doc.defaultView?.AbortController) ?? AbortController)();
|
|
3575
|
+
const options = { signal: events.signal };
|
|
3576
|
+
root.addEventListener(RIBBON_ACTION_EVENT, (event) => this.run(event.detail), options);
|
|
3577
|
+
root.addEventListener("office-command", (event) => {
|
|
3578
|
+
if (event.detail?.command === "zoom-fit") this.run({
|
|
3579
|
+
type: "zoom",
|
|
3580
|
+
mode: "fit"
|
|
3581
|
+
});
|
|
3582
|
+
}, options);
|
|
3583
|
+
root.addEventListener("keydown", (event) => this.#shortcut(event), options);
|
|
3584
|
+
viewport.addEventListener("wheel", (event) => {
|
|
3585
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
3586
|
+
event.preventDefault();
|
|
3587
|
+
const zoom = this.host.controller.state.zoom;
|
|
3588
|
+
this.host.controller.setZoom(event.deltaY < 0 ? zoom * 1.1 : zoom / 1.1);
|
|
3589
|
+
}, {
|
|
3590
|
+
...options,
|
|
3591
|
+
passive: false
|
|
3592
|
+
});
|
|
3593
|
+
viewport.addEventListener("pointerdown", () => {
|
|
3594
|
+
if (!viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
|
|
3595
|
+
}, options);
|
|
3596
|
+
viewport.addEventListener("dblclick", (event) => {
|
|
3597
|
+
if (this.#tool !== "pointer") return;
|
|
3598
|
+
if (event.target?.closest?.("[data-shape-id]")) this.run({
|
|
3599
|
+
type: "reveal",
|
|
3600
|
+
panel: "edit",
|
|
3601
|
+
focusText: true
|
|
3602
|
+
});
|
|
3603
|
+
}, options);
|
|
3604
|
+
doc.addEventListener("fullscreenchange", () => this.render(this.host.controller.state), options);
|
|
3605
|
+
const disposeDraw = this.#draw.wire();
|
|
3606
|
+
return () => {
|
|
3607
|
+
++this.#pending;
|
|
3608
|
+
events.abort();
|
|
3609
|
+
disposeDraw();
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
setTool(tool) {
|
|
3613
|
+
if (tool === "rectangle" && !this.#canEdit(this.host.controller.state)) return;
|
|
3614
|
+
this.#tool = tool;
|
|
3615
|
+
this.render(this.host.controller.state);
|
|
3616
|
+
}
|
|
3617
|
+
#canEdit(state) {
|
|
3618
|
+
return state.edit.sourceAvailable && !state.loading && !state.edit.busy;
|
|
3619
|
+
}
|
|
3620
|
+
#history(key) {
|
|
3621
|
+
const { controller } = this.host;
|
|
3622
|
+
const { edit, loading } = controller.state;
|
|
3623
|
+
if ((key === "undo" ? edit.canUndo : edit.canRedo) && !edit.busy && !loading) this.#edit(() => controller[key]());
|
|
3624
|
+
}
|
|
3625
|
+
#delete() {
|
|
3626
|
+
const state = this.host.controller.state;
|
|
3627
|
+
const shape = state.selectedShape;
|
|
3628
|
+
const pageId = shape?.pageId ?? state.document?.pages[state.pageIndex]?.id;
|
|
3629
|
+
if (!shape || pageId === void 0 || !this.#canEdit(state)) return;
|
|
3630
|
+
this.#edit(() => this.host.controller.applyEdits([{
|
|
3631
|
+
type: "delete-shape",
|
|
3632
|
+
pageId,
|
|
3633
|
+
shapeId: shape.id
|
|
3634
|
+
}]), `Deleted ${shape.name || `shape ${shape.id}`}.`);
|
|
3635
|
+
}
|
|
3636
|
+
async #edit(action, success) {
|
|
3637
|
+
const request = ++this.#pending;
|
|
3638
|
+
const { root, viewport } = this.host;
|
|
3639
|
+
const canvasFocused = viewport.contains(root.activeElement);
|
|
3640
|
+
try {
|
|
3641
|
+
await action();
|
|
3642
|
+
if (canvasFocused && !viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
|
|
3643
|
+
if (success && request === this.#pending) this.host.announce(success);
|
|
3644
|
+
} catch (error) {
|
|
3645
|
+
if (request === this.#pending && !isEditCancellation(error) && !this.host.controller.state.edit.error) this.host.announce(editErrorMessage(error));
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
#toggleFullscreen() {
|
|
3649
|
+
const host = this.host.root.host;
|
|
3650
|
+
const doc = this.host.root.ownerDocument;
|
|
3651
|
+
(doc.fullscreenElement === host ? doc.exitFullscreen?.() : host.requestFullscreen?.())?.catch?.(() => this.host.announce("Full screen is not available in this browser frame."));
|
|
3652
|
+
}
|
|
3653
|
+
/** Visio shortcuts as typed actions. Text fields keep native undo, deletion and editing keys. */
|
|
3654
|
+
#shortcutAction(event) {
|
|
3655
|
+
const state = this.host.controller.state;
|
|
3656
|
+
const control = event.ctrlKey || event.metaKey;
|
|
3657
|
+
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
|
|
3658
|
+
if (control && key === "f") return { type: "search" };
|
|
3659
|
+
if (control && event.shiftKey && key === "w") return {
|
|
3660
|
+
type: "zoom",
|
|
3661
|
+
mode: "fit"
|
|
3662
|
+
};
|
|
3663
|
+
if (control && (key === "PageDown" || key === "PageUp")) return {
|
|
3664
|
+
type: "page",
|
|
3665
|
+
step: key === "PageDown" ? 1 : -1
|
|
3666
|
+
};
|
|
3667
|
+
if (key === "F5" && !control) return { type: "fullscreen" };
|
|
3668
|
+
if (editable(event.target)) return void 0;
|
|
3669
|
+
if (control && !event.shiftKey && key === "z") return {
|
|
3670
|
+
type: "history",
|
|
3671
|
+
key: "undo"
|
|
3672
|
+
};
|
|
3673
|
+
if (control && (key === "y" || event.shiftKey && key === "z")) return {
|
|
3674
|
+
type: "history",
|
|
3675
|
+
key: "redo"
|
|
3676
|
+
};
|
|
3677
|
+
if (control && key === "1") return {
|
|
3678
|
+
type: "tool",
|
|
3679
|
+
tool: "pointer"
|
|
3680
|
+
};
|
|
3681
|
+
if (control && key === "8") return {
|
|
3682
|
+
type: "tool",
|
|
3683
|
+
tool: "rectangle"
|
|
3684
|
+
};
|
|
3685
|
+
if (!control && key === "Delete" && state.selectedShape) return { type: "delete" };
|
|
3686
|
+
if (!control && key === "F2" && state.document) return {
|
|
3687
|
+
type: "reveal",
|
|
3688
|
+
panel: "edit",
|
|
3689
|
+
focusText: true
|
|
3690
|
+
};
|
|
3691
|
+
if (key === "Escape" && this.#tool !== "pointer" && !this.#draw.drawing) return {
|
|
3692
|
+
type: "tool",
|
|
3693
|
+
tool: "pointer"
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
#shortcut(event) {
|
|
3697
|
+
if (event.defaultPrevented || event.isComposing || event.altKey) return;
|
|
3698
|
+
const action = this.#shortcutAction(event);
|
|
3699
|
+
if (!action) return;
|
|
3700
|
+
event.preventDefault();
|
|
3701
|
+
this.run(action);
|
|
3702
|
+
}
|
|
3703
|
+
render(state) {
|
|
3704
|
+
const { root, viewport } = this.host;
|
|
3705
|
+
const button = (name) => root.querySelector(`office-ui-button[command="${name}"]`);
|
|
3706
|
+
const editing = this.#canEdit(state);
|
|
3707
|
+
if (this.#tool === "rectangle" && !state.edit.sourceAvailable) this.#tool = "pointer";
|
|
3708
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3709
|
+
button("undo").disabled = !state.edit.canUndo || state.edit.busy || state.loading;
|
|
3710
|
+
button("redo").disabled = !state.edit.canRedo || state.edit.busy || state.loading;
|
|
3711
|
+
button("delete").disabled = !editing || !state.selectedShape;
|
|
3712
|
+
button("pointer").setAttribute("pressed", String(this.#tool === "pointer"));
|
|
3713
|
+
const rectangle = button("rectangle");
|
|
3714
|
+
rectangle.setAttribute("pressed", String(this.#tool === "rectangle"));
|
|
3715
|
+
rectangle.disabled = !editing || !page;
|
|
3716
|
+
rectangle.title = state.edit.sourceAvailable ? "Rectangle (Ctrl+8)" : "Rectangle (Ctrl+8): open a .vsdx file to draw. Model-only documents are read only.";
|
|
3717
|
+
button("grid").setAttribute("pressed", String(this.#grid));
|
|
3718
|
+
button("grid").disabled = !page;
|
|
3719
|
+
for (const name of [
|
|
3720
|
+
"zoom-fit",
|
|
3721
|
+
"page-width",
|
|
3722
|
+
"actual-size"
|
|
3723
|
+
]) button(name).disabled = !page;
|
|
3724
|
+
const fullscreen = button("fullscreen");
|
|
3725
|
+
const host = root.host;
|
|
3726
|
+
fullscreen.disabled = typeof host.requestFullscreen !== "function";
|
|
3727
|
+
fullscreen.setAttribute("pressed", String(root.ownerDocument.fullscreenElement === host));
|
|
3728
|
+
viewport.dataset.tool = this.#tool;
|
|
3729
|
+
viewport.dataset.grid = String(this.#grid);
|
|
3730
|
+
}
|
|
3731
|
+
};
|
|
3732
|
+
//#endregion
|
|
3733
|
+
//#region src/office-ui.ts
|
|
3734
|
+
/**
|
|
3735
|
+
* Visio-only glyphs (20x20 path data). Neutral Office glyphs come from ooxml-ui; add a glyph
|
|
3736
|
+
* there instead when another Office product could use it.
|
|
3737
|
+
*/
|
|
3738
|
+
var VISIO_ICONS = {
|
|
3739
|
+
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",
|
|
3740
|
+
visioShapeData: "M3 3.5h14v13H3ZM3 7.5h14M8 7.5v9M10.5 11h4M10.5 14h4",
|
|
3741
|
+
visioPagesPane: "M2.5 3h15v14h-15ZM7 3v14M3.8 6h1.9M3.8 9h1.9M3.8 12h1.9",
|
|
3742
|
+
visioInspectorPane: "M2.5 3h15v14h-15ZM13 3v14M14.5 6h1.5M14.5 9h1.5M14.5 12h1.5"
|
|
3743
|
+
};
|
|
3744
|
+
/** Define the shared Office controls the viewer chrome uses. Idempotent; browser only. */
|
|
3745
|
+
function registerViewerControls(registry) {
|
|
3746
|
+
for (const [name, path] of Object.entries(VISIO_ICONS)) if (!getIcon(name)) registerIcon(name, path);
|
|
3747
|
+
registerControls(registry);
|
|
3748
|
+
}
|
|
3749
|
+
//#endregion
|
|
3096
3750
|
//#region src/export-svg.ts
|
|
3097
3751
|
var MAX_SVG_EXPORT_BYTES = 16777216;
|
|
3098
3752
|
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.";
|
|
@@ -3703,8 +4357,10 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3703
4357
|
#root;
|
|
3704
4358
|
#disposeInputs;
|
|
3705
4359
|
#viewport;
|
|
3706
|
-
#
|
|
3707
|
-
#
|
|
4360
|
+
#zoomSlider;
|
|
4361
|
+
#shapeStatus;
|
|
4362
|
+
#announcement;
|
|
4363
|
+
#commands;
|
|
3708
4364
|
#status;
|
|
3709
4365
|
#diagnostics;
|
|
3710
4366
|
#toolbar;
|
|
@@ -3737,10 +4393,14 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3737
4393
|
constructor() {
|
|
3738
4394
|
super();
|
|
3739
4395
|
this.#root = this.attachShadow({ mode: "open" });
|
|
3740
|
-
|
|
4396
|
+
registerViewerControls();
|
|
4397
|
+
this.#root.innerHTML = `<style>${viewerStyles}${canvasAndRibbonStyles}</style>${viewerChromeTemplate}`;
|
|
4398
|
+
const workspace = this.#root.querySelector(".workspace");
|
|
4399
|
+
workspace.before(createRibbon(document));
|
|
4400
|
+
workspace.after(createPageTabs(document), createStatusBar(document));
|
|
3741
4401
|
this.#viewport = this.#root.querySelector(".viewport");
|
|
3742
|
-
this.#
|
|
3743
|
-
this.#
|
|
4402
|
+
this.#zoomSlider = this.#root.querySelector("office-ui-zoom-slider");
|
|
4403
|
+
this.#shapeStatus = this.#root.querySelector("[data-shape-status]");
|
|
3744
4404
|
this.#status = this.#root.querySelector("[data-status]");
|
|
3745
4405
|
this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
|
|
3746
4406
|
this.#toolbar = this.#root.querySelector(".toolbar");
|
|
@@ -3751,6 +4411,23 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3751
4411
|
this.#notesPanel = this.#root.querySelector(".notes");
|
|
3752
4412
|
this.#inspector = this.#root.querySelector(".shape-inspector");
|
|
3753
4413
|
this.#chrome = new ViewerChrome(this.#root, this.controller);
|
|
4414
|
+
this.#commands = new ViewerCommands({
|
|
4415
|
+
root: this.#root,
|
|
4416
|
+
viewport: this.#viewport,
|
|
4417
|
+
controller: this.controller,
|
|
4418
|
+
fit: (mode) => this.#fit(mode),
|
|
4419
|
+
togglePane: (pane) => this.#chrome.togglePane(pane),
|
|
4420
|
+
reveal: (panel, focusText) => this.#chrome.reveal(panel, focusText),
|
|
4421
|
+
focusSearch: () => {
|
|
4422
|
+
this.#chrome.showTab("home");
|
|
4423
|
+
this.#search.input.focus();
|
|
4424
|
+
this.#search.input.select();
|
|
4425
|
+
},
|
|
4426
|
+
announce: (message) => {
|
|
4427
|
+
this.#announcement = message;
|
|
4428
|
+
this.#status.textContent = message;
|
|
4429
|
+
}
|
|
4430
|
+
});
|
|
3754
4431
|
this.#disposeInputs = this.#wireInputs();
|
|
3755
4432
|
this.#unsubscribe = this.controller.subscribe((state) => this.#render(state));
|
|
3756
4433
|
this.#eventUnsubscribe = this.controller.onEvent((name, detail) => {
|
|
@@ -3824,13 +4501,24 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3824
4501
|
}
|
|
3825
4502
|
fit() {
|
|
3826
4503
|
this.#assertAlive();
|
|
4504
|
+
this.#fit("page");
|
|
4505
|
+
}
|
|
4506
|
+
/** Visio Fit to Window shows the whole page; Page Width fills the canvas width. */
|
|
4507
|
+
#fit(mode) {
|
|
3827
4508
|
const page = this.document?.pages[this.pageIndex];
|
|
3828
4509
|
if (!page) return;
|
|
3829
4510
|
const style = this.ownerDocument.defaultView?.getComputedStyle(this.#viewport);
|
|
3830
4511
|
const padding = (value) => Number.parseFloat(value ?? "") || 0;
|
|
3831
4512
|
const width = Math.max(1, this.#viewport.clientWidth - padding(style?.paddingLeft) - padding(style?.paddingRight));
|
|
3832
4513
|
const height = Math.max(1, this.#viewport.clientHeight - padding(style?.paddingTop) - padding(style?.paddingBottom));
|
|
3833
|
-
|
|
4514
|
+
if (mode === "page") {
|
|
4515
|
+
this.zoom = Math.min(1, width / (page.width * 96), height / (page.height * 96));
|
|
4516
|
+
return;
|
|
4517
|
+
}
|
|
4518
|
+
const scrollbar = this.#viewport.offsetWidth - this.#viewport.clientWidth;
|
|
4519
|
+
const fitted = width / (page.width * 96);
|
|
4520
|
+
const reserve = page.height * 96 * fitted > height && scrollbar <= 0 ? 17 : 0;
|
|
4521
|
+
this.zoom = Math.max(1, width - reserve - 1) / (page.width * 96);
|
|
3834
4522
|
}
|
|
3835
4523
|
setLayerVisibility(pageId, layerId, visible) {
|
|
3836
4524
|
this.#assertAlive();
|
|
@@ -3900,16 +4588,18 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3900
4588
|
}
|
|
3901
4589
|
#wireInputs() {
|
|
3902
4590
|
const disposeChrome = this.#chrome.wire();
|
|
4591
|
+
const disposeCommands = this.#commands.wire();
|
|
3903
4592
|
const disposeEdit = this.#edit.wire();
|
|
3904
4593
|
const disposeLayers = wireLayerControls(this.#layers, this.controller);
|
|
3905
4594
|
const disposeInputs = wireViewerInputs({
|
|
3906
4595
|
viewport: this.#viewport,
|
|
3907
4596
|
commandRoot: this.#root,
|
|
3908
|
-
|
|
4597
|
+
zoomSlider: this.#zoomSlider,
|
|
3909
4598
|
searchInput: this.#search.input
|
|
3910
|
-
}, this.controller, () => this
|
|
4599
|
+
}, this.controller, (mode) => this.#fit(mode));
|
|
3911
4600
|
return () => {
|
|
3912
4601
|
disposeChrome();
|
|
4602
|
+
disposeCommands();
|
|
3913
4603
|
disposeInputs();
|
|
3914
4604
|
disposeLayers();
|
|
3915
4605
|
disposeEdit();
|
|
@@ -3928,14 +4618,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3928
4618
|
this.#document = state.document;
|
|
3929
4619
|
this.#renderedPage = state.pageIndex;
|
|
3930
4620
|
this.#renderedLayerOverrides = state.layerVisibilityOverrides;
|
|
3931
|
-
this.#
|
|
3932
|
-
for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
|
|
3933
|
-
const option = document.createElement("option");
|
|
3934
|
-
option.value = String(index);
|
|
3935
|
-
option.textContent = candidate.name + (candidate.isBackground ? " (background)" : "");
|
|
3936
|
-
this.#pageSelect.append(option);
|
|
3937
|
-
}
|
|
3938
|
-
this.#pageSelect.value = String(state.pageIndex);
|
|
4621
|
+
this.#announcement = void 0;
|
|
3939
4622
|
this.#renderWarnings = [];
|
|
3940
4623
|
if (state.document && page) {
|
|
3941
4624
|
const result = renderPage(state.document, page, { layerVisibilityOverrides: state.layerVisibilityOverrides });
|
|
@@ -3947,7 +4630,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3947
4630
|
empty.className = "empty";
|
|
3948
4631
|
const heading = document.createElement("strong");
|
|
3949
4632
|
heading.textContent = "Open a Visio drawing";
|
|
3950
|
-
empty.append(heading, "Open a .vsdx file to start. Files stay in this browser.");
|
|
4633
|
+
empty.append(heading, "Open a .vsdx or supported .vsd file to start. Files stay in this browser.");
|
|
3951
4634
|
this.#viewport.replaceChildren(empty);
|
|
3952
4635
|
}
|
|
3953
4636
|
const notes = compatibilityNotes(state.document?.diagnostics ?? [], this.#renderWarnings);
|
|
@@ -3962,6 +4645,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3962
4645
|
if (svg && page) {
|
|
3963
4646
|
svg.style.width = `${page.width * 96 * state.zoom}px`;
|
|
3964
4647
|
svg.style.height = `${page.height * 96 * state.zoom}px`;
|
|
4648
|
+
svg.style.setProperty("--vv-inch", `${96 * state.zoom}px`);
|
|
3965
4649
|
}
|
|
3966
4650
|
const searchResult = state.search.results[state.search.activeIndex];
|
|
3967
4651
|
const selection = state.selectedShape;
|
|
@@ -3989,16 +4673,20 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3989
4673
|
this.#renderedSelection = selection ? { ...selection } : null;
|
|
3990
4674
|
}
|
|
3991
4675
|
if (!searchResult) this.#revealedSearchResult = void 0;
|
|
3992
|
-
this.#
|
|
3993
|
-
this.#
|
|
4676
|
+
this.#zoomSlider.value = Math.round(state.zoom * 100);
|
|
4677
|
+
this.#zoomSlider.disabled = !page;
|
|
4678
|
+
const inspected = selectedShape(state.document, state.selectedShape, state.pageIndex);
|
|
4679
|
+
const inches = (value) => `${+value.toFixed(3)} in`;
|
|
4680
|
+
this.#shapeStatus.setAttribute("value", inspected ? `Width: ${inches(inspected.width)} Height: ${inches(inspected.height)}` : "");
|
|
3994
4681
|
for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
|
|
3995
4682
|
renderSearchControls(this.#search, state);
|
|
3996
4683
|
this.#edit.render(state);
|
|
3997
4684
|
if (changed) renderLayerControls(this.#layers, state);
|
|
3998
4685
|
else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
|
|
3999
4686
|
this.#chrome.render(state, this.#notes.children.length);
|
|
4687
|
+
this.#commands.render(state);
|
|
4000
4688
|
this.#viewport.setAttribute("aria-busy", String(state.loading || state.edit.busy));
|
|
4001
|
-
this.#status.textContent = state.loading ? "Opening diagram…" : state.error?.message ?? (page ? `${page.name} · ${page.shapes.length} top-level shapes` : "No diagram open");
|
|
4689
|
+
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");
|
|
4002
4690
|
const warnings = (state.document?.diagnostics.length ?? 0) + this.#renderWarnings.length;
|
|
4003
4691
|
this.#diagnostics.textContent = warnings ? `${this.#notes.children.length} compatibility notes` : "Local-only viewing";
|
|
4004
4692
|
}
|
|
@@ -4208,6 +4896,23 @@ function viewerHandle(current) {
|
|
|
4208
4896
|
}
|
|
4209
4897
|
};
|
|
4210
4898
|
}
|
|
4899
|
+
function viewerStateSource(current) {
|
|
4900
|
+
const controller = () => {
|
|
4901
|
+
try {
|
|
4902
|
+
return current()?.controller;
|
|
4903
|
+
} catch {
|
|
4904
|
+
return;
|
|
4905
|
+
}
|
|
4906
|
+
};
|
|
4907
|
+
return {
|
|
4908
|
+
subscribe(onChange) {
|
|
4909
|
+
return controller()?.subscribe(() => onChange()) ?? (() => {});
|
|
4910
|
+
},
|
|
4911
|
+
getSnapshot() {
|
|
4912
|
+
return controller()?.state ?? null;
|
|
4913
|
+
}
|
|
4914
|
+
};
|
|
4915
|
+
}
|
|
4211
4916
|
//#endregion
|
|
4212
4917
|
//#region packages/bindings/src/solid.tsx
|
|
4213
4918
|
var _tmpl$ = /*#__PURE__*/ template(`<div>`);
|
|
@@ -4261,5 +4966,19 @@ function VisioViewer(props) {
|
|
|
4261
4966
|
return _el$;
|
|
4262
4967
|
})();
|
|
4263
4968
|
}
|
|
4969
|
+
/**
|
|
4970
|
+
* Reactive viewer state as a Solid accessor for a handle accessor (for example a signal set by
|
|
4971
|
+
* `viewerRef`). Follows handle changes and cleans up with the owning scope.
|
|
4972
|
+
*/
|
|
4973
|
+
function createVisioViewerState(handle) {
|
|
4974
|
+
const [state, setState] = createSignal(null);
|
|
4975
|
+
createEffect(() => {
|
|
4976
|
+
const current = handle();
|
|
4977
|
+
const source = viewerStateSource(() => current);
|
|
4978
|
+
setState(() => source.getSnapshot());
|
|
4979
|
+
onCleanup(source.subscribe(() => setState(() => source.getSnapshot())));
|
|
4980
|
+
});
|
|
4981
|
+
return state;
|
|
4982
|
+
}
|
|
4264
4983
|
//#endregion
|
|
4265
|
-
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 };
|
|
4984
|
+
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 };
|