visio-angular-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-DnTSJ65K.js → edit-worker-CZbtTOr4.js} +474 -138
- package/dist/assets/{parse-worker-CvtlZkVJ.js → parse-worker-BEaLqUCJ.js} +7288 -83
- package/dist/index.js +957 -239
- package/dist/types/packages/bindings/src/angular.d.ts +6 -2
- package/dist/types/packages/bindings/src/common.d.ts +11 -0
- 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,7 +1,9 @@
|
|
|
1
|
-
import { VISIO_RASTER_IMAGE_LIMITS, VISIO_VISIBILITY_LIMITS, getVisioPageLayers, inspectVisioRasterImage,
|
|
2
|
-
import {
|
|
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";
|
|
4
|
+
import { Component, ElementRef, EventEmitter, Input, Output, inject, signal } from "@angular/core";
|
|
3
5
|
export * from "visio-core";
|
|
4
|
-
//#region
|
|
6
|
+
//#region src/document-history.ts
|
|
5
7
|
var EMPTY_EDIT_STATE = Object.freeze({
|
|
6
8
|
sourceAvailable: false,
|
|
7
9
|
busy: false,
|
|
@@ -18,6 +20,7 @@ var EDIT_HISTORY_LIMITS = Object.freeze({
|
|
|
18
20
|
});
|
|
19
21
|
/** Private controller-owned bytes; transient worker copies and models are separate from this budget. */
|
|
20
22
|
var DocumentHistory = class {
|
|
23
|
+
original;
|
|
21
24
|
#entries;
|
|
22
25
|
#index = 0;
|
|
23
26
|
#truncated = false;
|
|
@@ -81,7 +84,7 @@ var DocumentHistory = class {
|
|
|
81
84
|
}
|
|
82
85
|
};
|
|
83
86
|
//#endregion
|
|
84
|
-
//#region
|
|
87
|
+
//#region src/edit-commands.ts
|
|
85
88
|
/** Bound cloning and strip arbitrary host properties. Semantic validation belongs to core. */
|
|
86
89
|
function snapshotEdits(edits) {
|
|
87
90
|
if (!Array.isArray(edits) || edits.length > 1e3) throw new Error("At most 1000 edits are accepted per operation.");
|
|
@@ -143,7 +146,7 @@ function snapshotEdits(edits) {
|
|
|
143
146
|
});
|
|
144
147
|
}
|
|
145
148
|
//#endregion
|
|
146
|
-
//#region
|
|
149
|
+
//#region src/scene-details.ts
|
|
147
150
|
/** Runtime model checks prevent implicit DOM stringification of host-provided objects. */
|
|
148
151
|
function assertShapeDetails(shape, budget) {
|
|
149
152
|
const data = shape.shapeData ?? [], links = shape.hyperlinks ?? [];
|
|
@@ -210,7 +213,7 @@ function assertShapeDetails(shape, budget) {
|
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
//#endregion
|
|
213
|
-
//#region
|
|
216
|
+
//#region src/foreign-vector-budget.ts
|
|
214
217
|
/** UI allocation limits, charged for every instance, including repeated shared resources. */
|
|
215
218
|
var FOREIGN_VECTOR_SCENE_LIMITS = Object.freeze({
|
|
216
219
|
maxNodes: 1e5,
|
|
@@ -247,11 +250,9 @@ function inspectForeignVectorResource(source) {
|
|
|
247
250
|
}
|
|
248
251
|
/** Invocation-local budget. Never trusts frozen host objects or caches across calls. */
|
|
249
252
|
var ForeignVectorBudget = class {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
this.operands = 0;
|
|
254
|
-
}
|
|
253
|
+
nodes = 0;
|
|
254
|
+
commands = 0;
|
|
255
|
+
operands = 0;
|
|
255
256
|
take(source) {
|
|
256
257
|
const resource = inspectForeignVectorResource(source);
|
|
257
258
|
this.nodes += resource.nodes;
|
|
@@ -271,7 +272,7 @@ function member(value, choices, label) {
|
|
|
271
272
|
}
|
|
272
273
|
/** Defensive display limits also protect callers supplying their own typed scenes. */
|
|
273
274
|
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.");
|
|
275
|
+
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
276
|
const seen = /* @__PURE__ */ new Set();
|
|
276
277
|
let pathBytes = 0, textBytes = 0, runCount = 0, imageBytes = 0, geometryCount = 0, uniquePixels = 0, drawnPixels = 0, layerCount = 0, paragraphCount = 0, gradientStops = 0;
|
|
277
278
|
const imageResources = /* @__PURE__ */ new Map();
|
|
@@ -527,13 +528,13 @@ function assertViewableDocument(model) {
|
|
|
527
528
|
}
|
|
528
529
|
}
|
|
529
530
|
//#endregion
|
|
530
|
-
//#region
|
|
531
|
+
//#region src/worker-editor.ts
|
|
531
532
|
function createWorkerEditor(factory, timeoutMs = 2e4) {
|
|
532
533
|
const create = factory ?? (() => {
|
|
533
534
|
if (typeof Worker === "undefined") throw new Error("Editing requires browser worker support.");
|
|
534
535
|
return new Worker(new URL(
|
|
535
536
|
/* @vite-ignore */
|
|
536
|
-
new URL("assets/edit-worker-
|
|
537
|
+
new URL("assets/edit-worker-CZbtTOr4.js", import.meta.url).href,
|
|
537
538
|
"" + import.meta.url
|
|
538
539
|
), { type: "module" });
|
|
539
540
|
});
|
|
@@ -602,7 +603,7 @@ function createWorkerEditor(factory, timeoutMs = 2e4) {
|
|
|
602
603
|
return edit;
|
|
603
604
|
}
|
|
604
605
|
//#endregion
|
|
605
|
-
//#region
|
|
606
|
+
//#region src/viewer-layers.ts
|
|
606
607
|
/** UI limits. Visibility semantics and shape traversal remain in ooxml-core. */
|
|
607
608
|
var VIEWER_LAYER_LIMITS = Object.freeze({
|
|
608
609
|
overrides: VISIO_VISIBILITY_LIMITS.maxLayers,
|
|
@@ -659,7 +660,7 @@ function hasVisibleShapeContent(shape, visible, cache = /* @__PURE__ */ new Weak
|
|
|
659
660
|
return renderable;
|
|
660
661
|
}
|
|
661
662
|
//#endregion
|
|
662
|
-
//#region
|
|
663
|
+
//#region src/document-text-search.ts
|
|
663
664
|
/** UTF-16 character bounds, applied before copying or case conversion. */
|
|
664
665
|
var TEXT_SEARCH_LIMITS = Object.freeze({
|
|
665
666
|
queryCharacters: 256,
|
|
@@ -764,8 +765,11 @@ function textSearchStatus(search) {
|
|
|
764
765
|
return (search.activeIndex < 0 ? `${count}${search.resultsTruncated ? "+" : ""} matching shapes` : `${search.activeIndex + 1} of ${count}${search.resultsTruncated ? "+" : ""} matching shapes`) + (search.indexTruncated ? " · Partial text index" : "");
|
|
765
766
|
}
|
|
766
767
|
//#endregion
|
|
767
|
-
//#region
|
|
768
|
+
//#region src/controller.ts
|
|
768
769
|
var ViewerController = class {
|
|
770
|
+
parser;
|
|
771
|
+
listenerError;
|
|
772
|
+
editor;
|
|
769
773
|
#state = Object.freeze({
|
|
770
774
|
document: null,
|
|
771
775
|
edit: EMPTY_EDIT_STATE,
|
|
@@ -786,9 +790,10 @@ var ViewerController = class {
|
|
|
786
790
|
#sourceGeneration = 0;
|
|
787
791
|
#searchIndex = null;
|
|
788
792
|
#history = null;
|
|
793
|
+
#sourceFormat = null;
|
|
789
794
|
#editId = 0;
|
|
790
795
|
#visible = documentVisibility(null);
|
|
791
|
-
constructor(parser =
|
|
796
|
+
constructor(parser = loadVisio, listenerError = (error) => {
|
|
792
797
|
if (typeof globalThis.reportError === "function") globalThis.reportError(error);
|
|
793
798
|
else console.error("Visio viewer listener failed:", error);
|
|
794
799
|
}, editor = createWorkerEditor()) {
|
|
@@ -828,6 +833,7 @@ var ViewerController = class {
|
|
|
828
833
|
++this.#sourceGeneration;
|
|
829
834
|
this.#invalidateEdit();
|
|
830
835
|
this.#history = null;
|
|
836
|
+
this.#sourceFormat = null;
|
|
831
837
|
this.#loadId++;
|
|
832
838
|
this.parser.cancel?.();
|
|
833
839
|
this.#searchIndex = null;
|
|
@@ -970,7 +976,7 @@ var ViewerController = class {
|
|
|
970
976
|
this.#change({
|
|
971
977
|
loading: true,
|
|
972
978
|
error: null,
|
|
973
|
-
edit: this.#
|
|
979
|
+
edit: this.#sourceEditState()
|
|
974
980
|
});
|
|
975
981
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
976
982
|
let document;
|
|
@@ -981,10 +987,10 @@ var ViewerController = class {
|
|
|
981
987
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
982
988
|
if (bytes.byteLength > 33554432) throw new Error("This viewer accepts files up to 32 MiB.");
|
|
983
989
|
const owned = bytes instanceof Uint8Array ? Uint8Array.from(bytes) : new Uint8Array(bytes.slice(0));
|
|
984
|
-
history = new DocumentHistory(owned);
|
|
985
990
|
document = await this.parser(Uint8Array.from(owned));
|
|
986
991
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
987
992
|
assertViewableDocument(document);
|
|
993
|
+
history = new DocumentHistory(owned);
|
|
988
994
|
visible = documentVisibility(document);
|
|
989
995
|
} catch (cause) {
|
|
990
996
|
if (this.#destroyed || id !== this.#loadId) return;
|
|
@@ -997,13 +1003,14 @@ var ViewerController = class {
|
|
|
997
1003
|
throw error;
|
|
998
1004
|
}
|
|
999
1005
|
this.#history = history;
|
|
1006
|
+
this.#sourceFormat = document.format;
|
|
1000
1007
|
this.#searchIndex = null;
|
|
1001
1008
|
this.#visible = visible;
|
|
1002
1009
|
++this.#documentGeneration;
|
|
1003
1010
|
++this.#sourceGeneration;
|
|
1004
1011
|
this.#change({
|
|
1005
1012
|
document,
|
|
1006
|
-
edit: history.state,
|
|
1013
|
+
edit: document.format === "vsdx" ? history.state : EMPTY_EDIT_STATE,
|
|
1007
1014
|
pageIndex: 0,
|
|
1008
1015
|
loading: false,
|
|
1009
1016
|
error: null,
|
|
@@ -1020,7 +1027,7 @@ var ViewerController = class {
|
|
|
1020
1027
|
this.#invalidateEdit();
|
|
1021
1028
|
this.#change({
|
|
1022
1029
|
loading: false,
|
|
1023
|
-
edit: this.#
|
|
1030
|
+
edit: this.#sourceEditState()
|
|
1024
1031
|
});
|
|
1025
1032
|
}
|
|
1026
1033
|
destroy() {
|
|
@@ -1028,6 +1035,7 @@ var ViewerController = class {
|
|
|
1028
1035
|
this.#destroyed = true;
|
|
1029
1036
|
this.#invalidateEdit();
|
|
1030
1037
|
this.#history = null;
|
|
1038
|
+
this.#sourceFormat = null;
|
|
1031
1039
|
++this.#documentGeneration;
|
|
1032
1040
|
++this.#sourceGeneration;
|
|
1033
1041
|
++this.#loadId;
|
|
@@ -1072,11 +1080,11 @@ var ViewerController = class {
|
|
|
1072
1080
|
if (!this.#state.edit.busy) return;
|
|
1073
1081
|
this.#invalidateEdit();
|
|
1074
1082
|
this.parser.cancel?.();
|
|
1075
|
-
this.#change({ edit: this.#
|
|
1083
|
+
this.#change({ edit: this.#sourceEditState() });
|
|
1076
1084
|
}
|
|
1077
1085
|
exportVsdx() {
|
|
1078
1086
|
this.#assertAlive();
|
|
1079
|
-
if (!this.#history) throw new Error("Load a VSDX file before downloading a source-backed copy.");
|
|
1087
|
+
if (!this.#history || this.#sourceFormat !== "vsdx" || this.#state.document?.format !== "vsdx") throw new Error("Load a VSDX file before downloading a source-backed copy.");
|
|
1080
1088
|
if (this.#state.edit.busy || this.#state.loading) throw new Error("Wait for the current document operation before downloading.");
|
|
1081
1089
|
return this.#history.export();
|
|
1082
1090
|
}
|
|
@@ -1084,10 +1092,13 @@ var ViewerController = class {
|
|
|
1084
1092
|
++this.#editId;
|
|
1085
1093
|
this.editor.cancel?.();
|
|
1086
1094
|
}
|
|
1095
|
+
#sourceEditState() {
|
|
1096
|
+
return this.#sourceFormat === "vsdx" && this.#state.document?.format === "vsdx" ? this.#history?.state ?? EMPTY_EDIT_STATE : EMPTY_EDIT_STATE;
|
|
1097
|
+
}
|
|
1087
1098
|
async #mutate(kind, commands) {
|
|
1088
1099
|
this.#assertAlive();
|
|
1089
1100
|
const history = this.#history;
|
|
1090
|
-
if (!history) throw new Error("Load a VSDX file before editing. Model-only documents are read only.");
|
|
1101
|
+
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.");
|
|
1091
1102
|
if (this.#state.loading || this.#state.edit.busy) throw new Error("Another document operation is in progress.");
|
|
1092
1103
|
const target = kind === "undo" ? history.undoTarget : kind === "redo" ? history.redoTarget : void 0;
|
|
1093
1104
|
if (kind !== "edit" && !target) return;
|
|
@@ -1113,6 +1124,7 @@ var ViewerController = class {
|
|
|
1113
1124
|
} else document = await this.parser(Uint8Array.from(target.bytes));
|
|
1114
1125
|
assertCurrent();
|
|
1115
1126
|
assertViewableDocument(document);
|
|
1127
|
+
if (document.format !== "vsdx") throw new Error("A VSDX edit or history operation cannot change the source format.");
|
|
1116
1128
|
if (edited && !edited.changedParts.length) {
|
|
1117
1129
|
this.#change({ edit: history.state });
|
|
1118
1130
|
return;
|
|
@@ -1183,7 +1195,7 @@ var ViewerController = class {
|
|
|
1183
1195
|
}
|
|
1184
1196
|
};
|
|
1185
1197
|
//#endregion
|
|
1186
|
-
//#region
|
|
1198
|
+
//#region src/contract.ts
|
|
1187
1199
|
var propertyKeys = [
|
|
1188
1200
|
"document",
|
|
1189
1201
|
"pageIndex",
|
|
@@ -1199,13 +1211,13 @@ var eventKeys = [
|
|
|
1199
1211
|
"shape-select"
|
|
1200
1212
|
];
|
|
1201
1213
|
//#endregion
|
|
1202
|
-
//#region
|
|
1214
|
+
//#region src/worker-parser.ts
|
|
1203
1215
|
/** One disposable worker per document prevents old parses and long CPU tasks from occupying the UI. */
|
|
1204
1216
|
function createWorkerParser(factory, timeoutMs = 15e3) {
|
|
1205
|
-
if (!factory && typeof Worker === "undefined") return
|
|
1217
|
+
if (!factory && typeof Worker === "undefined") return loadVisio;
|
|
1206
1218
|
const create = factory ?? (() => new Worker(new URL(
|
|
1207
1219
|
/* @vite-ignore */
|
|
1208
|
-
new URL("assets/parse-worker-
|
|
1220
|
+
new URL("assets/parse-worker-BEaLqUCJ.js", import.meta.url).href,
|
|
1209
1221
|
"" + import.meta.url
|
|
1210
1222
|
), { type: "module" }));
|
|
1211
1223
|
let cancel;
|
|
@@ -1252,7 +1264,7 @@ function createWorkerParser(factory, timeoutMs = 15e3) {
|
|
|
1252
1264
|
return parse;
|
|
1253
1265
|
}
|
|
1254
1266
|
//#endregion
|
|
1255
|
-
//#region
|
|
1267
|
+
//#region src/render-fill.ts
|
|
1256
1268
|
var gradientId = 0;
|
|
1257
1269
|
function fillPaint(style, defs) {
|
|
1258
1270
|
if (!style.fillGradient) return safeColor(style.fill, "#fff");
|
|
@@ -1274,7 +1286,7 @@ function fillPaint(style, defs) {
|
|
|
1274
1286
|
return `url(#${gradient.id})`;
|
|
1275
1287
|
}
|
|
1276
1288
|
//#endregion
|
|
1277
|
-
//#region
|
|
1289
|
+
//#region src/render-image.ts
|
|
1278
1290
|
var clipId$1 = 0;
|
|
1279
1291
|
/** URLs are created only from bounded embedded raster bytes, never from document strings. */
|
|
1280
1292
|
function renderImage(shape, resources) {
|
|
@@ -1307,7 +1319,7 @@ function renderImage(shape, resources) {
|
|
|
1307
1319
|
return group;
|
|
1308
1320
|
}
|
|
1309
1321
|
//#endregion
|
|
1310
|
-
//#region
|
|
1322
|
+
//#region src/render-foreign-vector.ts
|
|
1311
1323
|
var NS$1 = "http://www.w3.org/2000/svg";
|
|
1312
1324
|
var nextResource = 0;
|
|
1313
1325
|
var pathData = (commands) => commands.map(({ command, values }) => `${command}${values.length ? " " + values.join(" ") : ""}`).join(" ");
|
|
@@ -1373,7 +1385,7 @@ function renderForeignVector(source, owner = document, budget = new ForeignVecto
|
|
|
1373
1385
|
return root;
|
|
1374
1386
|
}
|
|
1375
1387
|
//#endregion
|
|
1376
|
-
//#region
|
|
1388
|
+
//#region src/render-foreign-vector-shape.ts
|
|
1377
1389
|
var clipId = 0;
|
|
1378
1390
|
/** Core supplies shape-local placement; converter coordinates have a downward Y axis. */
|
|
1379
1391
|
function renderForeignVectorShape(shape, budget) {
|
|
@@ -1397,10 +1409,11 @@ function renderForeignVectorShape(shape, budget) {
|
|
|
1397
1409
|
return group;
|
|
1398
1410
|
}
|
|
1399
1411
|
//#endregion
|
|
1400
|
-
//#region
|
|
1412
|
+
//#region src/render-resources.ts
|
|
1401
1413
|
var MAX_EMBEDDED_BYTES = 16777216;
|
|
1402
1414
|
/** One reference per shared core image resource, with explicit per-render disposal. */
|
|
1403
1415
|
var RenderResources = class {
|
|
1416
|
+
definitions;
|
|
1404
1417
|
#images = /* @__PURE__ */ new Map();
|
|
1405
1418
|
#disposed = false;
|
|
1406
1419
|
#embeddedBytes = 0;
|
|
@@ -1454,7 +1467,7 @@ function base64(bytes) {
|
|
|
1454
1467
|
return chunks.join("");
|
|
1455
1468
|
}
|
|
1456
1469
|
//#endregion
|
|
1457
|
-
//#region
|
|
1470
|
+
//#region src/arrowheads.ts
|
|
1458
1471
|
var markerId = 0;
|
|
1459
1472
|
/** Common Visio line/triangle and concave arrow styles. Sizes remain a documented visual approximation. */
|
|
1460
1473
|
function applyArrowheads(path, style, defs, warnings) {
|
|
@@ -1498,7 +1511,7 @@ function applyArrowheads(path, style, defs, warnings) {
|
|
|
1498
1511
|
}
|
|
1499
1512
|
}
|
|
1500
1513
|
//#endregion
|
|
1501
|
-
//#region
|
|
1514
|
+
//#region src/text-layout.ts
|
|
1502
1515
|
var DEFAULT_LIMITS = {
|
|
1503
1516
|
maxCharacters: 25e4,
|
|
1504
1517
|
maxMeasurements: 5e4,
|
|
@@ -1518,6 +1531,9 @@ var TextLayoutBudgetError = class extends Error {
|
|
|
1518
1531
|
};
|
|
1519
1532
|
/** One bounded measurement cache and work budget per render; shape scopes share both. */
|
|
1520
1533
|
var TextLayoutBudget = class TextLayoutBudget {
|
|
1534
|
+
limits;
|
|
1535
|
+
cache;
|
|
1536
|
+
parent;
|
|
1521
1537
|
#characters = 0;
|
|
1522
1538
|
#measurements = 0;
|
|
1523
1539
|
#lines = 0;
|
|
@@ -1687,7 +1703,7 @@ function wrapText(runs, width, measure = measureBrowserText, firstLineIndent = 0
|
|
|
1687
1703
|
return lines;
|
|
1688
1704
|
}
|
|
1689
1705
|
//#endregion
|
|
1690
|
-
//#region
|
|
1706
|
+
//#region src/paragraph-layout.ts
|
|
1691
1707
|
function layoutParagraphs(text, measure = measureBrowserText, budget = createTextLayoutBudget()) {
|
|
1692
1708
|
const scope = budget.forShape();
|
|
1693
1709
|
const runs = text.runs.length ? text.runs : [{
|
|
@@ -1785,7 +1801,7 @@ function sliceRuns(runs, offsets, start, end) {
|
|
|
1785
1801
|
return result;
|
|
1786
1802
|
}
|
|
1787
1803
|
//#endregion
|
|
1788
|
-
//#region
|
|
1804
|
+
//#region src/render-text.ts
|
|
1789
1805
|
/** Browser-font layout is approximate until compared with the licensed Visio reference corpus. */
|
|
1790
1806
|
function renderText(text, warnings, budget) {
|
|
1791
1807
|
const group = svgElement("g");
|
|
@@ -1863,7 +1879,7 @@ function renderText(text, warnings, budget) {
|
|
|
1863
1879
|
return group;
|
|
1864
1880
|
}
|
|
1865
1881
|
//#endregion
|
|
1866
|
-
//#region
|
|
1882
|
+
//#region src/render-svg.ts
|
|
1867
1883
|
var NS = "http://www.w3.org/2000/svg";
|
|
1868
1884
|
function svgElement(name) {
|
|
1869
1885
|
return document.createElementNS(NS, name);
|
|
@@ -1996,7 +2012,7 @@ function drawOwn(shape, group, context) {
|
|
|
1996
2012
|
if (shape.kind === "foreign" && !shape.image && !shape.foreignVector) warnings.add("Embedded or foreign objects are not rendered in this build.");
|
|
1997
2013
|
}
|
|
1998
2014
|
//#endregion
|
|
1999
|
-
//#region
|
|
2015
|
+
//#region src/shape-inspector.ts
|
|
2000
2016
|
function selectedShape(document, selection, pageIndex) {
|
|
2001
2017
|
if (!document || !selection) return void 0;
|
|
2002
2018
|
const stack = [...(selection.pageId ? document.pages.find((page) => page.id === selection.pageId) : document.pages[pageIndex])?.shapes ?? []];
|
|
@@ -2094,7 +2110,7 @@ function shapeDetails(shape) {
|
|
|
2094
2110
|
return fragment;
|
|
2095
2111
|
}
|
|
2096
2112
|
//#endregion
|
|
2097
|
-
//#region
|
|
2113
|
+
//#region src/diagnostics.ts
|
|
2098
2114
|
/** Presentation-only grouping keeps repeated per-shape diagnostics readable without losing counts. */
|
|
2099
2115
|
function compatibilityNotes(diagnostics, renderWarnings = []) {
|
|
2100
2116
|
const notes = /* @__PURE__ */ new Map();
|
|
@@ -2126,7 +2142,7 @@ function compatibilityText(note) {
|
|
|
2126
2142
|
return note.message + (note.count > 1 ? ` (${note.count} occurrences)` : "");
|
|
2127
2143
|
}
|
|
2128
2144
|
//#endregion
|
|
2129
|
-
//#region
|
|
2145
|
+
//#region src/viewer-input.ts
|
|
2130
2146
|
var targetShape = (event) => event.target?.closest?.("[data-shape-id]");
|
|
2131
2147
|
function selection(target) {
|
|
2132
2148
|
return target ? {
|
|
@@ -2137,7 +2153,7 @@ function selection(target) {
|
|
|
2137
2153
|
}
|
|
2138
2154
|
/** Own every DOM listener and abort them together when the surface is disposed. */
|
|
2139
2155
|
function wireViewerInputs(controls, controller, fit) {
|
|
2140
|
-
const { viewport, commandRoot,
|
|
2156
|
+
const { viewport, commandRoot, zoomSlider, searchInput } = controls;
|
|
2141
2157
|
const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
2142
2158
|
const options = { signal: events.signal };
|
|
2143
2159
|
searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
|
|
@@ -2153,13 +2169,9 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2153
2169
|
controller.setSearchQuery("");
|
|
2154
2170
|
}
|
|
2155
2171
|
}, options);
|
|
2156
|
-
|
|
2172
|
+
zoomSlider.addEventListener("input", () => controller.setZoom(zoomSlider.value / 100), options);
|
|
2157
2173
|
commandRoot.addEventListener("click", (event) => {
|
|
2158
2174
|
const action = event.target?.closest?.("button")?.dataset.action;
|
|
2159
|
-
if (action === "in") controller.setZoom(controller.state.zoom * 1.25);
|
|
2160
|
-
if (action === "out") controller.setZoom(controller.state.zoom / 1.25);
|
|
2161
|
-
if (action === "fit") fit();
|
|
2162
|
-
if (action === "actual") controller.setZoom(1);
|
|
2163
2175
|
if (action === "search-next") controller.nextSearchResult();
|
|
2164
2176
|
if (action === "search-previous") controller.previousSearchResult();
|
|
2165
2177
|
}, options);
|
|
@@ -2198,13 +2210,13 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2198
2210
|
}
|
|
2199
2211
|
if (event.key === "0") {
|
|
2200
2212
|
event.preventDefault();
|
|
2201
|
-
fit();
|
|
2213
|
+
fit("page");
|
|
2202
2214
|
}
|
|
2203
2215
|
}, options);
|
|
2204
2216
|
return () => events.abort();
|
|
2205
2217
|
}
|
|
2206
2218
|
//#endregion
|
|
2207
|
-
//#region
|
|
2219
|
+
//#region src/viewer-layer-controls.ts
|
|
2208
2220
|
var layerControlsTemplate = `<details class="layer-controls" hidden><summary>Layers</summary><p>Display changes only. SVG and print snapshots use saved visibility. Guides, NoShow and unknown hidden reasons remain hidden.</p><div data-layer-list></div><p data-layer-status role="status"></p><button type="button" data-layer-reset="all">Reset all layers</button></details>`;
|
|
2209
2221
|
/** Static controls and text-only labels. Document strings never become selectors or markup. */
|
|
2210
2222
|
function renderLayerControls(panel, state) {
|
|
@@ -2282,7 +2294,7 @@ function wireLayerControls(panel, controller) {
|
|
|
2282
2294
|
return () => events.abort();
|
|
2283
2295
|
}
|
|
2284
2296
|
//#endregion
|
|
2285
|
-
//#region
|
|
2297
|
+
//#region src/styles.ts
|
|
2286
2298
|
/** Inherited public tokens use local fallback aliases so host themes are never shadowed. */
|
|
2287
2299
|
var viewerStyles = `
|
|
2288
2300
|
:host {
|
|
@@ -2302,13 +2314,7 @@ button:disabled { opacity:.4; cursor:default; }
|
|
|
2302
2314
|
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; }
|
|
2303
2315
|
button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
|
|
2304
2316
|
.toolbar { flex:none; min-width:0; border-bottom:1px solid var(--_vv-border); background:var(--_vv-surface); }
|
|
2305
|
-
.ribbon-primary { height:32px; display:flex; align-items:center; gap:6px; padding:2px 8px; }
|
|
2306
|
-
.page-picker { display:flex; min-width:0; align-items:center; gap:8px; color:var(--_vv-muted); font-size:11px; }
|
|
2307
|
-
.page-picker select { width:190px; max-width:42vw; height:26px; min-height:26px; padding:2px 6px; font-size:11px; color:var(--_vv-ink); }
|
|
2308
|
-
.page-stepper { display:flex; align-items:center; gap:2px; }
|
|
2309
|
-
.page-stepper button { width:26px; height:26px; min-height:26px; padding:0; font-size:18px; line-height:22px; background:transparent; border-color:transparent; }
|
|
2310
2317
|
.spacer { flex:1; }
|
|
2311
|
-
.local-label { color:var(--_vv-muted); font-size:10px; white-space:nowrap; }
|
|
2312
2318
|
.ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
|
|
2313
2319
|
.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; }
|
|
2314
2320
|
.ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
|
|
@@ -2318,15 +2324,6 @@ button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-acce
|
|
|
2318
2324
|
.group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
|
|
2319
2325
|
.ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
|
|
2320
2326
|
.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; }
|
|
2321
|
-
.command-icon { display:grid; place-items:center; width:26px; height:26px; color:var(--_vv-accent); }
|
|
2322
|
-
.command-icon::before { content:""; width:24px; height:24px; background:currentColor; mask:var(--command-symbol) center / contain no-repeat; }
|
|
2323
|
-
[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"); }
|
|
2324
|
-
[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"); }
|
|
2325
|
-
[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"); }
|
|
2326
|
-
[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"); }
|
|
2327
|
-
[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"); }
|
|
2328
|
-
[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"); }
|
|
2329
|
-
.ribbon-command[data-chrome="edit"] .command-icon { font-family:ui-serif,Georgia,serif; text-decoration:underline; text-decoration-thickness:1px; text-underline-offset:3px; }
|
|
2330
2327
|
.ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
|
|
2331
2328
|
.search-group { width:330px; }
|
|
2332
2329
|
.search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
|
|
@@ -2398,11 +2395,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2398
2395
|
.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; }
|
|
2399
2396
|
.status-message { display:flex; gap:16px; min-width:0; }
|
|
2400
2397
|
.status-message span { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
|
2401
|
-
.zoom-controls { display:flex; flex:none; align-items:center; gap:3px; }
|
|
2402
|
-
.zoom-controls button { min-height:24px; height:24px; padding:1px 6px; border-color:transparent; background:transparent; font-size:10px; }
|
|
2403
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:24px; padding:0; font-size:17px; line-height:20px; }
|
|
2404
|
-
.zoom-divider { width:1px; height:14px; background:var(--_vv-border); margin:0 3px; }
|
|
2405
|
-
.zoom { min-width:40px; font-size:10px; text-align:center; font-variant-numeric:tabular-nums; }
|
|
2406
2398
|
.empty { max-width:360px; margin:60px auto; padding:16px; text-align:center; color:var(--_vv-muted); font-size:12px; }
|
|
2407
2399
|
.empty strong { display:block; color:var(--_vv-ink); font-size:18px; font-weight:500; margin-bottom:8px; }
|
|
2408
2400
|
@media(max-width:1100px) { .ribbon-hint { display:none; } [data-diagnostics] { display:none; } }
|
|
@@ -2410,11 +2402,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2410
2402
|
button,select,input,textarea { min-height:44px; }
|
|
2411
2403
|
button { min-width:44px; }
|
|
2412
2404
|
summary { min-height:44px; line-height:44px; }
|
|
2413
|
-
.ribbon-primary { height:52px; padding:4px 8px; }
|
|
2414
|
-
.page-picker { flex:1; }
|
|
2415
|
-
.page-picker select { width:100%; max-width:none; min-height:44px; height:44px; font-size:12px; }
|
|
2416
|
-
.page-stepper button { width:44px; min-height:44px; height:44px; }
|
|
2417
|
-
.local-label,.ribbon-primary>.spacer { display:none; }
|
|
2418
2405
|
.ribbon-tabs { height:44px; }
|
|
2419
2406
|
.ribbon-tabs button { min-height:44px; padding:8px 16px; }
|
|
2420
2407
|
.ribbon-content { min-height:86px; }
|
|
@@ -2426,7 +2413,7 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2426
2413
|
.search-controls button { min-width:44px; font-size:0; padding:3px; }
|
|
2427
2414
|
.search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
|
|
2428
2415
|
.search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
|
|
2429
|
-
#home-panel>.ribbon-group:not(.search-group) { display:none; }
|
|
2416
|
+
#home-panel>.ribbon-group:not(.search-group):not(.undo-group) { display:none; }
|
|
2430
2417
|
.ribbon-command { min-width:90px; }
|
|
2431
2418
|
.workspace { display:block; overflow:auto; }
|
|
2432
2419
|
.page-rail { width:100%; max-height:190px; border-right:0; border-bottom:1px solid var(--_vv-border); }
|
|
@@ -2443,35 +2430,22 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2443
2430
|
.notes-strip button { min-height:44px; }
|
|
2444
2431
|
.status { flex-wrap:wrap; gap:0; padding:4px 8px; }
|
|
2445
2432
|
.status-message { width:100%; min-height:18px; }
|
|
2446
|
-
.zoom-controls { justify-content:flex-end; width:100%; }
|
|
2447
|
-
.zoom-controls button { min-height:44px; height:44px; min-width:44px; font-size:11px; }
|
|
2448
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; font-size:20px; }
|
|
2449
|
-
.zoom { font-size:11px; }
|
|
2450
2433
|
}
|
|
2451
2434
|
@media(pointer:coarse) {
|
|
2452
2435
|
button,select,input,textarea { min-height:44px; }
|
|
2453
2436
|
button { min-width:44px; }
|
|
2454
2437
|
summary,.layer-controls label { min-height:44px; line-height:44px; }
|
|
2455
|
-
.ribbon-primary { min-height:52px; height:52px; }
|
|
2456
|
-
.page-picker select,.page-stepper button { min-height:44px; height:44px; }
|
|
2457
|
-
.page-stepper button { min-width:44px; }
|
|
2458
2438
|
.ribbon-tabs { min-height:44px; height:44px; }
|
|
2459
2439
|
.ribbon-tabs button { min-height:44px; }
|
|
2460
2440
|
.ribbon-content { min-height:96px; }
|
|
2461
2441
|
.search-group { width:380px; max-width:100%; }
|
|
2462
|
-
.
|
|
2463
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; }
|
|
2442
|
+
.notes-strip button { min-height:44px; height:44px; }
|
|
2464
2443
|
.layer-controls input { min-height:20px; }
|
|
2465
2444
|
}
|
|
2466
|
-
/* Suite ribbon: tabs lead directly into command groups
|
|
2445
|
+
/* Suite ribbon: tabs lead directly into Visio command groups. */
|
|
2467
2446
|
.ribbon-tabs { height:40px; }
|
|
2468
2447
|
.ribbon-tabs button { min-height:39px; padding:8px 16px; }
|
|
2469
2448
|
.ribbon-content { min-height:104px; padding:4px 4px; }
|
|
2470
|
-
.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; }
|
|
2471
|
-
.ribbon-primary .page-picker { flex-direction:column; align-items:start; gap:5px; }
|
|
2472
|
-
.ribbon-primary .page-picker select { width:160px; max-width:100%; }
|
|
2473
|
-
.ribbon-primary .spacer { display:none; }
|
|
2474
|
-
.ribbon-primary .local-label { position:absolute; bottom:4px; left:0; width:100%; text-align:center; }
|
|
2475
2449
|
.search-group { order:2; width:320px; }
|
|
2476
2450
|
.ribbon-hint { order:3; padding:10px 18px; }
|
|
2477
2451
|
.ribbon-command { min-width:82px; }
|
|
@@ -2480,13 +2454,9 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2480
2454
|
.ribbon-tabs { height:44px; }
|
|
2481
2455
|
.ribbon-tabs button { min-height:44px; }
|
|
2482
2456
|
.ribbon-content { min-height:54px; padding:4px 0; }
|
|
2483
|
-
.ribbon-primary { min-height:44px; height:44px; width:100%; padding:0 8px; border:0; flex-wrap:nowrap; gap:4px; order:0; }
|
|
2484
|
-
.ribbon-primary .page-picker { flex-direction:row; flex:1; align-items:center; gap:8px; }
|
|
2485
|
-
.ribbon-primary .page-picker select { flex:1; width:100%; min-width:0; }
|
|
2486
2457
|
.search-group { flex:0 0 100%; width:100%; min-height:54px; }
|
|
2487
2458
|
.search-feedback,.group-label { display:none; }
|
|
2488
2459
|
.ribbon-content { overflow-x:auto; scroll-snap-type:x mandatory; }
|
|
2489
|
-
.ribbon-primary,.search-group { scroll-snap-align:start; }
|
|
2490
2460
|
.workspace { display:flex; position:relative; overflow:hidden; }
|
|
2491
2461
|
.viewport { flex:1; width:100%; height:100%; min-height:0; padding:16px 4px; }
|
|
2492
2462
|
.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; }
|
|
@@ -2515,9 +2485,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2515
2485
|
.status-message { flex:1; }
|
|
2516
2486
|
@media(max-width:760px) {
|
|
2517
2487
|
#home-panel { overflow:visible; min-height:54px; padding:4px; }
|
|
2518
|
-
.ribbon-primary { width:calc(100% - 92px); min-width:0; flex:1; padding:0 4px; }
|
|
2519
|
-
.ribbon-primary .page-picker { font-size:0; gap:0; }
|
|
2520
|
-
.ribbon-primary .page-picker select { font-size:12px; }
|
|
2521
2488
|
.ribbon-tools { display:block; flex:0 0 84px; width:84px; order:1; }
|
|
2522
2489
|
.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; }
|
|
2523
2490
|
.ribbon-tools>summary::-webkit-details-marker { display:none; }
|
|
@@ -2529,15 +2496,348 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2529
2496
|
.ribbon-tools .search-group { order:1; flex:auto; }
|
|
2530
2497
|
.ribbon-tools .search-feedback { display:block; }
|
|
2531
2498
|
.status { flex-wrap:nowrap; gap:4px; min-height:48px; }
|
|
2532
|
-
.zoom-controls { width:auto; flex:1; gap:0; }
|
|
2533
|
-
.zoom-divider { margin:0; }
|
|
2534
2499
|
.status .notes-strip { display:none; }
|
|
2535
2500
|
}
|
|
2536
2501
|
@media(pointer:coarse) { .ribbon-tabs {height:45px;min-height:45px;} .ribbon-tabs button {min-height:44px;height:44px;} }
|
|
2537
2502
|
@media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
|
|
2538
2503
|
`;
|
|
2539
2504
|
//#endregion
|
|
2540
|
-
//#region
|
|
2505
|
+
//#region src/styles/index.ts
|
|
2506
|
+
/**
|
|
2507
|
+
* Viewer stylesheets as text for the shadow root. New styles are CSS files here; the legacy
|
|
2508
|
+
* `styles.ts` string moves into this directory when it is next changed.
|
|
2509
|
+
*/
|
|
2510
|
+
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");
|
|
2511
|
+
//#endregion
|
|
2512
|
+
//#region src/ribbon-action.ts
|
|
2513
|
+
/** Event name shared by every ribbon control; detail is the typed action. */
|
|
2514
|
+
var RIBBON_ACTION_EVENT = "ribbon-action";
|
|
2515
|
+
/** Internal, non-composed event: it stays inside the viewer's shadow tree. */
|
|
2516
|
+
function emitRibbonAction(target, action) {
|
|
2517
|
+
target.dispatchEvent(new CustomEvent(RIBBON_ACTION_EVENT, {
|
|
2518
|
+
detail: action,
|
|
2519
|
+
bubbles: true
|
|
2520
|
+
}));
|
|
2521
|
+
}
|
|
2522
|
+
//#endregion
|
|
2523
|
+
//#region src/ribbon-parts.ts
|
|
2524
|
+
/**
|
|
2525
|
+
* A large stacked ribbon command. `id` is a stable name for state sync and tests; the typed
|
|
2526
|
+
* `action` is what runs. Labels, icons and shortcuts are static application strings.
|
|
2527
|
+
*/
|
|
2528
|
+
function tool(doc, id, label, icon, action, options = {}) {
|
|
2529
|
+
const el = doc.createElement("office-ui-button");
|
|
2530
|
+
el.setAttribute("variant", "stacked");
|
|
2531
|
+
el.setAttribute("command", id);
|
|
2532
|
+
el.setAttribute("icon", icon);
|
|
2533
|
+
el.setAttribute("label", label);
|
|
2534
|
+
el.setAttribute("title", options.keys ? `${label} (${options.keys[1]})` : label);
|
|
2535
|
+
if (options.keys) el.setAttribute("keyshortcuts", options.keys[0]);
|
|
2536
|
+
if (options.pressed !== void 0) el.setAttribute("pressed", String(options.pressed));
|
|
2537
|
+
if (options.controls) el.setAttribute("aria-controls", options.controls);
|
|
2538
|
+
el.addEventListener("office-command", (event) => {
|
|
2539
|
+
event.stopPropagation();
|
|
2540
|
+
emitRibbonAction(el, action);
|
|
2541
|
+
});
|
|
2542
|
+
return el;
|
|
2543
|
+
}
|
|
2544
|
+
/** A labelled ribbon group (the shared `office-ui-ribbon-group`). */
|
|
2545
|
+
function group(doc, label, children, className = "") {
|
|
2546
|
+
const el = doc.createElement("office-ui-ribbon-group");
|
|
2547
|
+
el.setAttribute("label", label);
|
|
2548
|
+
if (className) el.className = className;
|
|
2549
|
+
el.append(...children);
|
|
2550
|
+
return el;
|
|
2551
|
+
}
|
|
2552
|
+
/** A keyboard-navigable row of groups (the shared `office-ui-toolbar`). */
|
|
2553
|
+
function commandRow(doc, label, children) {
|
|
2554
|
+
const el = doc.createElement("office-ui-toolbar");
|
|
2555
|
+
el.className = "ribbon-row";
|
|
2556
|
+
el.setAttribute("aria-label", label);
|
|
2557
|
+
el.append(...children);
|
|
2558
|
+
return el;
|
|
2559
|
+
}
|
|
2560
|
+
/** Muted two-line guidance at the end of a ribbon panel. */
|
|
2561
|
+
function hint(doc, lines) {
|
|
2562
|
+
const el = doc.createElement("p");
|
|
2563
|
+
el.className = "ribbon-hint";
|
|
2564
|
+
lines.forEach((line, index) => {
|
|
2565
|
+
if (index) el.append(doc.createElement("br"));
|
|
2566
|
+
el.append(line);
|
|
2567
|
+
});
|
|
2568
|
+
return el;
|
|
2569
|
+
}
|
|
2570
|
+
//#endregion
|
|
2571
|
+
//#region src/viewer-search.ts
|
|
2572
|
+
/** Builds the Find group: a search field, previous/next result buttons and a live status. */
|
|
2573
|
+
function createSearchGroup(doc) {
|
|
2574
|
+
const group = doc.createElement("div");
|
|
2575
|
+
group.className = "ribbon-group search-group";
|
|
2576
|
+
group.setAttribute("role", "group");
|
|
2577
|
+
group.setAttribute("aria-label", "Find");
|
|
2578
|
+
const controls = doc.createElement("div");
|
|
2579
|
+
controls.className = "search-controls";
|
|
2580
|
+
controls.setAttribute("role", "search");
|
|
2581
|
+
controls.setAttribute("aria-label", "Diagram text search");
|
|
2582
|
+
const label = doc.createElement("label");
|
|
2583
|
+
label.append("Find ");
|
|
2584
|
+
const input = doc.createElement("input");
|
|
2585
|
+
input.type = "search";
|
|
2586
|
+
input.setAttribute("aria-label", "Search diagram text");
|
|
2587
|
+
input.placeholder = "Text across pages";
|
|
2588
|
+
input.maxLength = TEXT_SEARCH_LIMITS.queryCharacters;
|
|
2589
|
+
input.autocomplete = "off";
|
|
2590
|
+
input.spellcheck = false;
|
|
2591
|
+
input.setAttribute("aria-describedby", "search-status");
|
|
2592
|
+
label.append(input);
|
|
2593
|
+
const step = (action, name, text) => {
|
|
2594
|
+
const button = doc.createElement("button");
|
|
2595
|
+
button.type = "button";
|
|
2596
|
+
button.dataset.action = action;
|
|
2597
|
+
button.setAttribute("aria-label", name);
|
|
2598
|
+
button.textContent = text;
|
|
2599
|
+
return button;
|
|
2600
|
+
};
|
|
2601
|
+
const status = doc.createElement("span");
|
|
2602
|
+
status.id = "search-status";
|
|
2603
|
+
status.setAttribute("role", "status");
|
|
2604
|
+
status.setAttribute("aria-live", "polite");
|
|
2605
|
+
status.setAttribute("aria-atomic", "true");
|
|
2606
|
+
controls.append(label, step("search-previous", "Previous matching shape", "Previous"), step("search-next", "Next matching shape", "Next"), status);
|
|
2607
|
+
const caption = doc.createElement("span");
|
|
2608
|
+
caption.className = "group-label";
|
|
2609
|
+
caption.textContent = "Find in drawing";
|
|
2610
|
+
group.append(controls, caption);
|
|
2611
|
+
return group;
|
|
2612
|
+
}
|
|
2613
|
+
function searchControls(root) {
|
|
2614
|
+
return {
|
|
2615
|
+
input: root.querySelector("input[type=\"search\"]"),
|
|
2616
|
+
status: root.querySelector("#search-status"),
|
|
2617
|
+
previous: root.querySelector("[data-action=\"search-previous\"]"),
|
|
2618
|
+
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
function renderSearchControls(controls, state) {
|
|
2622
|
+
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2623
|
+
controls.input.disabled = !state.document?.pages.length;
|
|
2624
|
+
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2625
|
+
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2626
|
+
const result = state.search.results[state.search.activeIndex];
|
|
2627
|
+
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2628
|
+
}
|
|
2629
|
+
//#endregion
|
|
2630
|
+
//#region src/ribbon-home.ts
|
|
2631
|
+
/**
|
|
2632
|
+
* Visio's Home tab, limited to commands this build can honestly perform: Undo, Tools and
|
|
2633
|
+
* Editing. Clipboard, Font, Paragraph, Shape Styles and Arrange wait for core edit commands.
|
|
2634
|
+
* The compact (phone) layout folds every group but Undo into the Tools disclosure.
|
|
2635
|
+
*/
|
|
2636
|
+
function buildHomePanel(doc, panel) {
|
|
2637
|
+
const tools = doc.createElement("details");
|
|
2638
|
+
tools.className = "ribbon-tools";
|
|
2639
|
+
tools.open = true;
|
|
2640
|
+
const summary = doc.createElement("summary");
|
|
2641
|
+
const caret = doc.createElement("span");
|
|
2642
|
+
caret.setAttribute("aria-hidden", "true");
|
|
2643
|
+
caret.textContent = "⌄";
|
|
2644
|
+
summary.append("Tools ", caret);
|
|
2645
|
+
const content = doc.createElement("div");
|
|
2646
|
+
content.className = "tools-content";
|
|
2647
|
+
content.append(group(doc, "Tools", [
|
|
2648
|
+
tool(doc, "pointer", "Pointer Tool", "pointer", {
|
|
2649
|
+
type: "tool",
|
|
2650
|
+
tool: "pointer"
|
|
2651
|
+
}, {
|
|
2652
|
+
keys: ["Control+1", "Ctrl+1"],
|
|
2653
|
+
pressed: true
|
|
2654
|
+
}),
|
|
2655
|
+
tool(doc, "rectangle", "Rectangle", "rectangle", {
|
|
2656
|
+
type: "tool",
|
|
2657
|
+
tool: "rectangle"
|
|
2658
|
+
}, {
|
|
2659
|
+
keys: ["Control+8", "Ctrl+8"],
|
|
2660
|
+
pressed: false
|
|
2661
|
+
}),
|
|
2662
|
+
tool(doc, "edit", "Edit text", "pencil", {
|
|
2663
|
+
type: "reveal",
|
|
2664
|
+
panel: "edit"
|
|
2665
|
+
}, { keys: ["F2", "F2"] })
|
|
2666
|
+
]), createSearchGroup(doc), group(doc, "Editing", [
|
|
2667
|
+
tool(doc, "delete", "Delete", "trash", { type: "delete" }, { keys: ["Delete", "Del"] }),
|
|
2668
|
+
tool(doc, "layers", "Layers", "visioLayers", {
|
|
2669
|
+
type: "reveal",
|
|
2670
|
+
panel: "layers"
|
|
2671
|
+
}),
|
|
2672
|
+
tool(doc, "selection", "Shape details", "visioShapeData", {
|
|
2673
|
+
type: "reveal",
|
|
2674
|
+
panel: "selection"
|
|
2675
|
+
})
|
|
2676
|
+
]));
|
|
2677
|
+
tools.append(summary, content);
|
|
2678
|
+
panel.append(commandRow(doc, "Home commands", [group(doc, "Undo", [tool(doc, "undo", "Undo", "undo", {
|
|
2679
|
+
type: "history",
|
|
2680
|
+
key: "undo"
|
|
2681
|
+
}, { keys: ["Control+Z", "Ctrl+Z"] }), tool(doc, "redo", "Redo", "redo", {
|
|
2682
|
+
type: "history",
|
|
2683
|
+
key: "redo"
|
|
2684
|
+
}, { keys: ["Control+Y", "Ctrl+Y"] })], "undo-group"), tools]), hint(doc, ["Rectangle: drag on the page to draw.", "Editing is experimental. Keep your original file."]));
|
|
2685
|
+
}
|
|
2686
|
+
//#endregion
|
|
2687
|
+
//#region src/ribbon-view.ts
|
|
2688
|
+
/** Visio's View tab: Views, Show (grid and task panes) and Zoom. */
|
|
2689
|
+
function buildViewPanel(doc, panel) {
|
|
2690
|
+
panel.append(commandRow(doc, "View commands", [
|
|
2691
|
+
group(doc, "Views", [tool(doc, "fullscreen", "Full Screen", "fullscreen", { type: "fullscreen" }, {
|
|
2692
|
+
keys: ["F5", "F5"],
|
|
2693
|
+
pressed: false
|
|
2694
|
+
})]),
|
|
2695
|
+
group(doc, "Show", [
|
|
2696
|
+
tool(doc, "grid", "Grid", "grid", { type: "grid" }, { pressed: false }),
|
|
2697
|
+
tool(doc, "pages", "Pages pane", "visioPagesPane", {
|
|
2698
|
+
type: "pane",
|
|
2699
|
+
pane: "pages"
|
|
2700
|
+
}, {
|
|
2701
|
+
pressed: true,
|
|
2702
|
+
controls: "page-rail"
|
|
2703
|
+
}),
|
|
2704
|
+
tool(doc, "inspector", "Inspector pane", "visioInspectorPane", {
|
|
2705
|
+
type: "pane",
|
|
2706
|
+
pane: "inspector"
|
|
2707
|
+
}, {
|
|
2708
|
+
pressed: true,
|
|
2709
|
+
controls: "inspector-pane"
|
|
2710
|
+
}),
|
|
2711
|
+
tool(doc, "notes", "Review notes", "message", {
|
|
2712
|
+
type: "reveal",
|
|
2713
|
+
panel: "notes"
|
|
2714
|
+
})
|
|
2715
|
+
]),
|
|
2716
|
+
group(doc, "Zoom", [
|
|
2717
|
+
tool(doc, "zoom-fit", "Fit to Window", "fitPage", {
|
|
2718
|
+
type: "zoom",
|
|
2719
|
+
mode: "fit"
|
|
2720
|
+
}, { keys: ["Control+Shift+W", "Ctrl+Shift+W"] }),
|
|
2721
|
+
tool(doc, "page-width", "Page Width", "pageWidth", {
|
|
2722
|
+
type: "zoom",
|
|
2723
|
+
mode: "width"
|
|
2724
|
+
}),
|
|
2725
|
+
tool(doc, "actual-size", "100%", "search", {
|
|
2726
|
+
type: "zoom",
|
|
2727
|
+
mode: "actual"
|
|
2728
|
+
})
|
|
2729
|
+
])
|
|
2730
|
+
]), hint(doc, ["Ctrl+wheel or + / − to zoom.", "Ctrl+Page Up / Page Down changes page."]));
|
|
2731
|
+
}
|
|
2732
|
+
//#endregion
|
|
2733
|
+
//#region src/ribbon.ts
|
|
2734
|
+
var TABS = [[
|
|
2735
|
+
"home",
|
|
2736
|
+
"Home",
|
|
2737
|
+
buildHomePanel
|
|
2738
|
+
], [
|
|
2739
|
+
"view",
|
|
2740
|
+
"View",
|
|
2741
|
+
buildViewPanel
|
|
2742
|
+
]];
|
|
2743
|
+
/**
|
|
2744
|
+
* The Visio ribbon: a tab list and one panel per tab. Tab switching and keyboard movement
|
|
2745
|
+
* belong to the chrome; commands emit `ribbon-action` events for the router.
|
|
2746
|
+
*/
|
|
2747
|
+
function createRibbon(doc) {
|
|
2748
|
+
const toolbar = doc.createElement("div");
|
|
2749
|
+
toolbar.className = "toolbar";
|
|
2750
|
+
toolbar.setAttribute("role", "group");
|
|
2751
|
+
toolbar.setAttribute("aria-label", "Diagram controls");
|
|
2752
|
+
const tabs = doc.createElement("div");
|
|
2753
|
+
tabs.className = "ribbon-tabs";
|
|
2754
|
+
tabs.setAttribute("role", "tablist");
|
|
2755
|
+
tabs.setAttribute("aria-label", "Ribbon");
|
|
2756
|
+
toolbar.append(tabs);
|
|
2757
|
+
for (const [key, name, build] of TABS) {
|
|
2758
|
+
const selected = key === "home";
|
|
2759
|
+
const tab = doc.createElement("button");
|
|
2760
|
+
tab.type = "button";
|
|
2761
|
+
tab.id = `${key}-tab`;
|
|
2762
|
+
tab.dataset.tab = key;
|
|
2763
|
+
tab.textContent = name;
|
|
2764
|
+
tab.setAttribute("role", "tab");
|
|
2765
|
+
tab.setAttribute("aria-selected", String(selected));
|
|
2766
|
+
tab.setAttribute("aria-controls", `${key}-panel`);
|
|
2767
|
+
if (!selected) tab.tabIndex = -1;
|
|
2768
|
+
const panel = doc.createElement("div");
|
|
2769
|
+
panel.className = "ribbon-content";
|
|
2770
|
+
panel.id = `${key}-panel`;
|
|
2771
|
+
panel.setAttribute("role", "tabpanel");
|
|
2772
|
+
panel.setAttribute("aria-labelledby", tab.id);
|
|
2773
|
+
panel.hidden = !selected;
|
|
2774
|
+
build(doc, panel);
|
|
2775
|
+
tabs.append(tab);
|
|
2776
|
+
toolbar.append(panel);
|
|
2777
|
+
}
|
|
2778
|
+
return toolbar;
|
|
2779
|
+
}
|
|
2780
|
+
//#endregion
|
|
2781
|
+
//#region src/status-bar.ts
|
|
2782
|
+
/**
|
|
2783
|
+
* Visio's status bar on the shared `office-ui-status-bar`: page and shape metrics, a live
|
|
2784
|
+
* message region, compatibility notes, the host footer slot and the shared zoom slider.
|
|
2785
|
+
*/
|
|
2786
|
+
function createStatusBar(doc) {
|
|
2787
|
+
const bar = doc.createElement("office-ui-status-bar");
|
|
2788
|
+
bar.className = "status";
|
|
2789
|
+
bar.setAttribute("label", "Status bar");
|
|
2790
|
+
const item = (name) => {
|
|
2791
|
+
const el = doc.createElement("office-ui-status-item");
|
|
2792
|
+
el.setAttribute(name, "");
|
|
2793
|
+
el.setAttribute("label", "");
|
|
2794
|
+
el.setAttribute("value", "");
|
|
2795
|
+
return el;
|
|
2796
|
+
};
|
|
2797
|
+
const message = doc.createElement("div");
|
|
2798
|
+
message.className = "status-message";
|
|
2799
|
+
message.setAttribute("role", "status");
|
|
2800
|
+
const text = doc.createElement("span");
|
|
2801
|
+
text.dataset.status = "";
|
|
2802
|
+
const diagnostics = doc.createElement("span");
|
|
2803
|
+
diagnostics.dataset.diagnostics = "";
|
|
2804
|
+
message.append(text, diagnostics);
|
|
2805
|
+
const notes = doc.createElement("div");
|
|
2806
|
+
notes.className = "notes-strip";
|
|
2807
|
+
const notesButton = doc.createElement("button");
|
|
2808
|
+
notesButton.type = "button";
|
|
2809
|
+
notesButton.dataset.chrome = "notes";
|
|
2810
|
+
notesButton.setAttribute("aria-controls", "inspector-pane");
|
|
2811
|
+
notesButton.setAttribute("aria-expanded", "false");
|
|
2812
|
+
const count = doc.createElement("span");
|
|
2813
|
+
count.className = "notes-count";
|
|
2814
|
+
count.dataset.noteCount = "";
|
|
2815
|
+
notesButton.append("Notes", count);
|
|
2816
|
+
const local = doc.createElement("span");
|
|
2817
|
+
local.textContent = "Files stay in your browser";
|
|
2818
|
+
notes.append(notesButton, local);
|
|
2819
|
+
const footer = doc.createElement("slot");
|
|
2820
|
+
footer.name = "workspace-footer";
|
|
2821
|
+
const zoom = doc.createElement("office-ui-zoom-slider");
|
|
2822
|
+
zoom.slot = "end";
|
|
2823
|
+
zoom.className = "zoom-controls";
|
|
2824
|
+
zoom.setAttribute("label", "Canvas zoom");
|
|
2825
|
+
zoom.setAttribute("fit", "Fit page to current window");
|
|
2826
|
+
zoom.setAttribute("value", "100");
|
|
2827
|
+
bar.append(item("data-page-status"), item("data-shape-status"), message, notes, footer, zoom);
|
|
2828
|
+
return bar;
|
|
2829
|
+
}
|
|
2830
|
+
/** Visio's page tabs under the drawing, on the shared document tab strip. */
|
|
2831
|
+
function createPageTabs(doc) {
|
|
2832
|
+
const strip = doc.createElement("office-ui-tab-strip");
|
|
2833
|
+
strip.className = "page-tabs";
|
|
2834
|
+
strip.setAttribute("label", "Pages");
|
|
2835
|
+
strip.setAttribute("previous-label", "Previous page");
|
|
2836
|
+
strip.setAttribute("next-label", "Next page");
|
|
2837
|
+
return strip;
|
|
2838
|
+
}
|
|
2839
|
+
//#endregion
|
|
2840
|
+
//#region src/edit-error.ts
|
|
2541
2841
|
/** Core error codes remain visible for a refused transaction; document content is plain text. */
|
|
2542
2842
|
function editErrorMessage(error) {
|
|
2543
2843
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2549,10 +2849,13 @@ function isEditCancellation(error) {
|
|
|
2549
2849
|
return !!error && typeof error === "object" && "name" in error && error.name === "AbortError";
|
|
2550
2850
|
}
|
|
2551
2851
|
//#endregion
|
|
2552
|
-
//#region
|
|
2852
|
+
//#region src/viewer-geometry-controls.ts
|
|
2553
2853
|
var geometryControlsTemplate = `<fieldset data-geometry><legend>Geometry (experimental)</legend><p>Pin X/Y use drawing inches, bottom-left origin, Y up. Resize holds the rotation pin fixed. Core rejects unsupported formulas, protected cells, unsafe group/master dependencies and referenced deletion. Native Visio reopen fidelity is unverified.</p><label>New rectangle ID <input data-geometry-field="id" type="text" maxlength="256"></label><label>Rectangle text (optional) <input data-geometry-field="text" type="text" maxlength="1000000"></label><label>Pin X (inches) <input data-geometry-field="x" type="number" step="any"></label><label>Pin Y (inches) <input data-geometry-field="y" type="number" step="any"></label><label>Width (inches) <input data-geometry-field="width" type="number" min="0" step="any"></label><label>Height (inches) <input data-geometry-field="height" type="number" min="0" step="any"></label><div class="edit-actions"><button type="button" data-geometry-action="create-rectangle">Create rectangle</button><button type="button" data-geometry-action="move-shape">Move selected</button><button type="button" data-geometry-action="resize-shape">Resize selected</button><button type="button" data-geometry-action="delete-shape">Delete selected</button></div><p data-geometry-error role="alert" hidden></p></fieldset>`;
|
|
2554
2854
|
/** Collect explicit coordinates; never infer a Visio pin from rendered SVG transforms. */
|
|
2555
2855
|
var ViewerGeometryControls = class {
|
|
2856
|
+
panel;
|
|
2857
|
+
controller;
|
|
2858
|
+
draftChanged;
|
|
2556
2859
|
#fields;
|
|
2557
2860
|
#buttons;
|
|
2558
2861
|
#error;
|
|
@@ -2673,10 +2976,12 @@ var ViewerGeometryControls = class {
|
|
|
2673
2976
|
}
|
|
2674
2977
|
};
|
|
2675
2978
|
//#endregion
|
|
2676
|
-
//#region
|
|
2979
|
+
//#region src/viewer-edit-controls.ts
|
|
2677
2980
|
/** Static markup only. Document text is assigned exclusively through value/textContent. */
|
|
2678
2981
|
var editControlsTemplate = `<details class="edit-controls"><summary>Edit diagram (experimental)</summary><p id="edit-warning">Experimental source-backed editing. Unsupported targets are rejected. Native Visio compatibility is not verified. Keep your original file.</p><p data-edit-target></p><label for="edit-text">Selected shape text</label><textarea id="edit-text" rows="3" aria-describedby="edit-warning edit-target edit-status"></textarea><div class="edit-actions"><button type="button" data-edit="apply">Apply text</button><button type="button" data-edit="cancel">Cancel</button><button type="button" data-edit="undo">Undo</button><button type="button" data-edit="redo">Redo</button></div><p id="edit-status" role="status" aria-live="polite"></p><p data-edit-error role="alert" hidden></p><ul data-edit-diagnostics></ul>${geometryControlsTemplate}</details>`;
|
|
2679
2982
|
var ViewerEditControls = class {
|
|
2983
|
+
panel;
|
|
2984
|
+
input;
|
|
2680
2985
|
#target;
|
|
2681
2986
|
#status;
|
|
2682
2987
|
#error;
|
|
@@ -2819,59 +3124,13 @@ var ViewerEditControls = class {
|
|
|
2819
3124
|
}
|
|
2820
3125
|
};
|
|
2821
3126
|
//#endregion
|
|
2822
|
-
//#region
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2830
|
-
};
|
|
2831
|
-
}
|
|
2832
|
-
function renderSearchControls(controls, state) {
|
|
2833
|
-
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2834
|
-
controls.input.disabled = !state.document?.pages.length;
|
|
2835
|
-
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2836
|
-
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2837
|
-
const result = state.search.results[state.search.activeIndex];
|
|
2838
|
-
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2839
|
-
}
|
|
2840
|
-
//#endregion
|
|
2841
|
-
//#region .package-build/output/src/viewer-chrome.js
|
|
2842
|
-
/** Only static application markup belongs here. All document labels are inserted as text. */
|
|
2843
|
-
var viewerChromeTemplate = `
|
|
2844
|
-
<div class="toolbar" role="group" aria-label="Diagram controls">
|
|
2845
|
-
|
|
2846
|
-
<div class="ribbon-tabs" role="tablist" aria-label="Ribbon">
|
|
2847
|
-
<button type="button" id="home-tab" role="tab" aria-selected="true" aria-controls="home-panel" data-tab="home">Home</button>
|
|
2848
|
-
<button type="button" id="view-tab" role="tab" aria-selected="false" aria-controls="view-panel" tabindex="-1" data-tab="view">View</button>
|
|
2849
|
-
</div>
|
|
2850
|
-
<div class="ribbon-content" id="home-panel" role="tabpanel" aria-labelledby="home-tab">
|
|
2851
|
-
<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>
|
|
2852
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2853
|
-
<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>
|
|
2854
|
-
<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>
|
|
2855
|
-
<button class="ribbon-command" type="button" data-chrome="layers"><span class="command-icon" data-icon="layers" aria-hidden="true"></span><span>Layers</span></button>
|
|
2856
|
-
</div><span class="group-label">Inspect and edit</span></div>
|
|
2857
|
-
</div></details>
|
|
2858
|
-
<div class="ribbon-primary">
|
|
2859
|
-
<label class="page-picker">Page <select aria-label="Page"></select></label>
|
|
2860
|
-
<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>
|
|
2861
|
-
<span class="spacer"></span><span class="local-label">Local workspace</span>
|
|
2862
|
-
</div>
|
|
2863
|
-
<p class="ribbon-hint">Select a shape to inspect its details.<br>Text editing is experimental.</p>
|
|
2864
|
-
</div>
|
|
2865
|
-
<div class="ribbon-content" id="view-panel" role="tabpanel" aria-labelledby="view-tab" hidden>
|
|
2866
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2867
|
-
<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>
|
|
2868
|
-
<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>
|
|
2869
|
-
<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>
|
|
2870
|
-
</div><span class="group-label">Workspace panes</span></div>
|
|
2871
|
-
<p class="ribbon-hint">Canvas shortcuts: + / − to zoom, 0 to fit.<br>Arrow keys move between focused shapes.</p>
|
|
2872
|
-
</div>
|
|
2873
|
-
</div>
|
|
2874
|
-
<div class="workspace">
|
|
3127
|
+
//#region src/viewer-chrome.ts
|
|
3128
|
+
/**
|
|
3129
|
+
* Static workspace markup (legacy; migrate to builders when next changed). The ribbon, page
|
|
3130
|
+
* tabs and status bar are built by `createRibbon`, `createPageTabs` and `createStatusBar`.
|
|
3131
|
+
* All document labels are inserted as text.
|
|
3132
|
+
*/
|
|
3133
|
+
var viewerChromeTemplate = `<div class="workspace">
|
|
2875
3134
|
<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>
|
|
2876
3135
|
<div class="viewport" tabindex="0" role="region" aria-label="Diagram canvas"></div>
|
|
2877
3136
|
<aside id="inspector-pane" class="inspector-pane" aria-label="Drawing inspector">
|
|
@@ -2884,14 +3143,15 @@ var viewerChromeTemplate = `
|
|
|
2884
3143
|
<details class="notes inspector-card"><summary>Compatibility notes</summary><ul></ul></details>
|
|
2885
3144
|
</div>
|
|
2886
3145
|
</aside>
|
|
2887
|
-
</div
|
|
2888
|
-
<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>`;
|
|
3146
|
+
</div>`;
|
|
2889
3147
|
/** Presentation state stays local; page navigation uses the same controller as every binding. */
|
|
2890
3148
|
var ViewerChrome = class {
|
|
2891
3149
|
#root;
|
|
2892
3150
|
#controller;
|
|
2893
3151
|
#document;
|
|
2894
3152
|
#pageList;
|
|
3153
|
+
#pageTabs;
|
|
3154
|
+
#tools;
|
|
2895
3155
|
#rail;
|
|
2896
3156
|
#inspector;
|
|
2897
3157
|
#notes;
|
|
@@ -2903,6 +3163,8 @@ var ViewerChrome = class {
|
|
|
2903
3163
|
this.#root = root;
|
|
2904
3164
|
this.#controller = controller;
|
|
2905
3165
|
this.#pageList = root.querySelector(".page-list");
|
|
3166
|
+
this.#pageTabs = root.querySelector("office-ui-tab-strip");
|
|
3167
|
+
this.#tools = root.querySelector(".ribbon-tools");
|
|
2906
3168
|
this.#rail = root.querySelector(".page-rail");
|
|
2907
3169
|
this.#inspector = root.querySelector(".inspector-pane");
|
|
2908
3170
|
this.#notes = root.querySelector(".notes");
|
|
@@ -2913,7 +3175,7 @@ var ViewerChrome = class {
|
|
|
2913
3175
|
this.#responsive = view?.matchMedia?.("(max-width: 980px)");
|
|
2914
3176
|
const compact = view?.matchMedia?.("(max-width: 760px)");
|
|
2915
3177
|
this.#compact = compact;
|
|
2916
|
-
const tools = this.#
|
|
3178
|
+
const tools = this.#tools;
|
|
2917
3179
|
const toolLayout = () => {
|
|
2918
3180
|
tools.open = !compact?.matches;
|
|
2919
3181
|
};
|
|
@@ -2940,22 +3202,16 @@ var ViewerChrome = class {
|
|
|
2940
3202
|
this.#root.addEventListener("click", (event) => {
|
|
2941
3203
|
const button = event.target?.closest?.("button");
|
|
2942
3204
|
if (!button || button.disabled) return;
|
|
2943
|
-
if (compact?.matches && button.dataset.chrome) tools.open = false;
|
|
2944
3205
|
const tab = button.dataset.tab;
|
|
2945
|
-
if (tab === "home" || tab === "view") this
|
|
3206
|
+
if (tab === "home" || tab === "view") this.showTab(tab);
|
|
2946
3207
|
if (button.dataset.pageIndex !== void 0) this.#controller.setPage(Number(button.dataset.pageIndex));
|
|
2947
3208
|
const action = button.dataset.chrome;
|
|
2948
|
-
if (action === "
|
|
2949
|
-
if (action === "
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
if (action === "inspector") {
|
|
2955
|
-
this.#inspectorManuallyToggled = true;
|
|
2956
|
-
this.#setPane("inspector", this.#inspector.hidden);
|
|
2957
|
-
}
|
|
2958
|
-
if (action === "notes" || action === "selection" || action === "edit" || action === "layers") this.#reveal(action);
|
|
3209
|
+
if (action === "inspector") this.togglePane("inspector");
|
|
3210
|
+
if (action === "notes") this.reveal("notes");
|
|
3211
|
+
}, options);
|
|
3212
|
+
this.#root.addEventListener("office-tab-select", (event) => {
|
|
3213
|
+
const id = Number(event.detail.id);
|
|
3214
|
+
if (Number.isSafeInteger(id)) this.#controller.setPage(id);
|
|
2959
3215
|
}, options);
|
|
2960
3216
|
this.#root.addEventListener("keydown", (event) => {
|
|
2961
3217
|
const key = event;
|
|
@@ -2972,11 +3228,11 @@ var ViewerChrome = class {
|
|
|
2972
3228
|
if (key.key === "ArrowUp" || key.key === "ArrowDown") return;
|
|
2973
3229
|
key.preventDefault();
|
|
2974
3230
|
const tab = key.key === "Home" ? "home" : key.key === "End" ? "view" : button.dataset.tab === "home" ? "view" : "home";
|
|
2975
|
-
this
|
|
3231
|
+
this.showTab(tab);
|
|
2976
3232
|
this.#root.querySelector(`[data-tab="${tab}"]`).focus();
|
|
2977
3233
|
} else if (button.dataset.pageIndex !== void 0) {
|
|
2978
3234
|
key.preventDefault();
|
|
2979
|
-
const pages = [...this.#pageList.querySelectorAll("
|
|
3235
|
+
const pages = [...this.#pageList.querySelectorAll("[data-page-index]")];
|
|
2980
3236
|
const current = Number(button.dataset.pageIndex);
|
|
2981
3237
|
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)));
|
|
2982
3238
|
this.#controller.setPage(next);
|
|
@@ -2986,7 +3242,7 @@ var ViewerChrome = class {
|
|
|
2986
3242
|
this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
|
|
2987
3243
|
return () => events.abort();
|
|
2988
3244
|
}
|
|
2989
|
-
|
|
3245
|
+
showTab(tab) {
|
|
2990
3246
|
for (const button of this.#root.querySelectorAll("[data-tab]")) {
|
|
2991
3247
|
const selected = button.dataset.tab === tab;
|
|
2992
3248
|
button.setAttribute("aria-selected", String(selected));
|
|
@@ -2994,27 +3250,41 @@ var ViewerChrome = class {
|
|
|
2994
3250
|
}
|
|
2995
3251
|
for (const panel of this.#root.querySelectorAll("[role=\"tabpanel\"]")) panel.hidden = panel.id !== `${tab}-panel`;
|
|
2996
3252
|
}
|
|
3253
|
+
/** Show or hide a task pane; compact layouts close the Tools menu afterwards. */
|
|
3254
|
+
togglePane(pane) {
|
|
3255
|
+
if (this.#compact?.matches) this.#tools.open = false;
|
|
3256
|
+
if (pane === "pages") this.#pagesManuallyToggled = true;
|
|
3257
|
+
else this.#inspectorManuallyToggled = true;
|
|
3258
|
+
this.#setPane(pane, (pane === "pages" ? this.#rail : this.#inspector).hidden);
|
|
3259
|
+
}
|
|
3260
|
+
#command(name) {
|
|
3261
|
+
return this.#root.querySelector(`office-ui-button[command="${name}"]`);
|
|
3262
|
+
}
|
|
2997
3263
|
#setPane(pane, visible) {
|
|
2998
3264
|
if (visible && this.#compact?.matches) {
|
|
2999
3265
|
const other = pane === "pages" ? "inspector" : "pages";
|
|
3000
3266
|
(other === "pages" ? this.#rail : this.#inspector).hidden = true;
|
|
3001
|
-
this.#
|
|
3267
|
+
this.#command(other).setAttribute("pressed", "false");
|
|
3002
3268
|
}
|
|
3003
3269
|
(pane === "pages" ? this.#rail : this.#inspector).hidden = !visible;
|
|
3004
|
-
this.#
|
|
3270
|
+
this.#command(pane).setAttribute("pressed", String(visible));
|
|
3005
3271
|
this.#syncNotes();
|
|
3006
3272
|
}
|
|
3007
3273
|
#syncNotes() {
|
|
3008
3274
|
this.#root.querySelector(".notes-strip button").setAttribute("aria-expanded", String(!this.#inspector.hidden && this.#notes.open));
|
|
3009
3275
|
}
|
|
3010
|
-
|
|
3276
|
+
/** Open an inspector disclosure; text editing may focus the text field directly (F2). */
|
|
3277
|
+
reveal(kind, focusText = false) {
|
|
3278
|
+
if (this.#compact?.matches) this.#tools.open = false;
|
|
3011
3279
|
this.#inspectorManuallyToggled = true;
|
|
3012
3280
|
this.#setPane("inspector", true);
|
|
3013
3281
|
const selector = kind === "notes" ? ".notes" : kind === "edit" ? ".edit-controls" : kind === "layers" ? ".layer-controls" : ".shape-inspector";
|
|
3014
3282
|
const panel = this.#root.querySelector(selector);
|
|
3015
3283
|
if (panel.hidden) return;
|
|
3016
3284
|
panel.open = true;
|
|
3017
|
-
panel.querySelector("
|
|
3285
|
+
const text = panel.querySelector("textarea");
|
|
3286
|
+
if (focusText && text && !text.disabled) text.focus();
|
|
3287
|
+
else panel.querySelector("summary").focus();
|
|
3018
3288
|
panel.scrollIntoView?.({
|
|
3019
3289
|
block: "nearest",
|
|
3020
3290
|
inline: "nearest"
|
|
@@ -3055,14 +3325,21 @@ var ViewerChrome = class {
|
|
|
3055
3325
|
fragment.append(item);
|
|
3056
3326
|
}
|
|
3057
3327
|
this.#pageList.replaceChildren(fragment);
|
|
3328
|
+
this.#pageTabs.tabs = (state.document?.pages ?? []).map((candidate, index) => ({
|
|
3329
|
+
id: String(index),
|
|
3330
|
+
label: candidate.name,
|
|
3331
|
+
title: candidate.isBackground ? `${candidate.name} (background page)` : candidate.name
|
|
3332
|
+
}));
|
|
3058
3333
|
if (restoreFocus !== void 0) [...this.#pageList.querySelectorAll("button")].find((button) => button.dataset.pageIndex === restoreFocus)?.focus();
|
|
3059
3334
|
}
|
|
3335
|
+
this.#pageTabs.selected = page ? String(state.pageIndex) : "";
|
|
3060
3336
|
for (const button of this.#pageList.querySelectorAll("button")) {
|
|
3061
3337
|
const selected = Number(button.dataset.pageIndex) === state.pageIndex;
|
|
3062
3338
|
if (selected) button.setAttribute("aria-current", "page");
|
|
3063
3339
|
else button.removeAttribute("aria-current");
|
|
3064
3340
|
button.tabIndex = selected ? 0 : -1;
|
|
3065
3341
|
}
|
|
3342
|
+
this.#root.querySelector("[data-page-status]").setAttribute("value", page ? `Page ${state.pageIndex + 1} of ${state.document.pages.length}` : "");
|
|
3066
3343
|
const count = state.document?.pages.length ?? 0;
|
|
3067
3344
|
this.#root.querySelector("[data-page-count]").textContent = String(count);
|
|
3068
3345
|
this.#root.querySelector(".page-empty").hidden = count > 0;
|
|
@@ -3070,18 +3347,406 @@ var ViewerChrome = class {
|
|
|
3070
3347
|
this.#root.querySelector("[data-page-size]").textContent = page ? `${page.width} × ${page.height} in` : "No page";
|
|
3071
3348
|
this.#root.querySelector("[data-page-shapes]").textContent = page ? `${page.shapes.length} top-level` : "No shapes";
|
|
3072
3349
|
this.#root.querySelector(".selection-hint").hidden = !!state.selectedShape;
|
|
3073
|
-
this.#
|
|
3074
|
-
this.#
|
|
3075
|
-
this.#
|
|
3076
|
-
this.#
|
|
3077
|
-
this.#root.querySelector("
|
|
3078
|
-
for (const button of this.#root.querySelectorAll("[data-chrome=\"notes\"]")) button.disabled = noteCount === 0;
|
|
3350
|
+
this.#command("selection").disabled = !state.selectedShape;
|
|
3351
|
+
this.#command("edit").disabled = !page;
|
|
3352
|
+
this.#command("layers").disabled = this.#root.querySelector(".layer-controls").hidden;
|
|
3353
|
+
this.#command("notes").disabled = noteCount === 0;
|
|
3354
|
+
this.#root.querySelector(".notes-strip button").disabled = noteCount === 0;
|
|
3079
3355
|
this.#root.querySelector("[data-note-count]").textContent = noteCount ? ` · ${noteCount}` : "";
|
|
3080
3356
|
this.#syncNotes();
|
|
3081
3357
|
}
|
|
3082
3358
|
};
|
|
3083
3359
|
//#endregion
|
|
3084
|
-
//#region
|
|
3360
|
+
//#region src/ribbon-router.ts
|
|
3361
|
+
/** Routes a ribbon, status-bar or shortcut action to the controller that owns it. */
|
|
3362
|
+
function routeRibbonAction(targets, action) {
|
|
3363
|
+
const { controller } = targets;
|
|
3364
|
+
const page = controller.state.document?.pages[controller.state.pageIndex];
|
|
3365
|
+
switch (action.type) {
|
|
3366
|
+
case "history": return targets.history(action.key);
|
|
3367
|
+
case "delete": return targets.deleteSelection();
|
|
3368
|
+
case "tool": return targets.setTool(action.tool);
|
|
3369
|
+
case "grid": return targets.toggleGrid();
|
|
3370
|
+
case "fullscreen": return targets.toggleFullscreen();
|
|
3371
|
+
case "pane": return targets.togglePane(action.pane);
|
|
3372
|
+
case "reveal": return targets.reveal(action.panel, action.focusText ?? false);
|
|
3373
|
+
case "search": return targets.focusSearch();
|
|
3374
|
+
case "page": return controller.setPage(controller.state.pageIndex + action.step);
|
|
3375
|
+
case "zoom":
|
|
3376
|
+
if (!page) return;
|
|
3377
|
+
if (action.mode === "actual") return controller.setZoom(1);
|
|
3378
|
+
return targets.fit(action.mode === "fit" ? "page" : "width");
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
//#endregion
|
|
3382
|
+
//#region src/viewer-draw-tool.ts
|
|
3383
|
+
/** Visio snaps new geometry to ruler subdivisions; 1/16 inch matches its default fine grid. */
|
|
3384
|
+
var SNAP = 1 / 16;
|
|
3385
|
+
var MIN_SIZE = SNAP;
|
|
3386
|
+
var snap = (value) => Math.round(value / SNAP) * SNAP;
|
|
3387
|
+
/** Next free numeric shape ID on the page tree. Core still rejects any collision. */
|
|
3388
|
+
function nextShapeId(page) {
|
|
3389
|
+
let max = 0;
|
|
3390
|
+
const pending = [...page.shapes];
|
|
3391
|
+
while (pending.length) {
|
|
3392
|
+
const shape = pending.pop();
|
|
3393
|
+
if (/^\d{1,9}$/.test(shape.id)) max = Math.max(max, Number(shape.id));
|
|
3394
|
+
pending.push(...shape.children);
|
|
3395
|
+
}
|
|
3396
|
+
return String(max + 1);
|
|
3397
|
+
}
|
|
3398
|
+
/** Page inches with a top-left origin (SVG user space) from a client point. */
|
|
3399
|
+
function pagePoint(svg, page, event) {
|
|
3400
|
+
const matrix = svg.getScreenCTM?.();
|
|
3401
|
+
if (!matrix) return void 0;
|
|
3402
|
+
const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse());
|
|
3403
|
+
return {
|
|
3404
|
+
x: snap(Math.max(0, Math.min(page.width, point.x))),
|
|
3405
|
+
y: snap(Math.max(0, Math.min(page.height, point.y)))
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3408
|
+
/** Drag-to-draw rectangle tool. The preview is viewer-only; the shape is created by core. */
|
|
3409
|
+
var RectangleDrawTool = class {
|
|
3410
|
+
viewport;
|
|
3411
|
+
controller;
|
|
3412
|
+
options;
|
|
3413
|
+
#drag;
|
|
3414
|
+
#request = 0;
|
|
3415
|
+
constructor(viewport, controller, options) {
|
|
3416
|
+
this.viewport = viewport;
|
|
3417
|
+
this.controller = controller;
|
|
3418
|
+
this.options = options;
|
|
3419
|
+
}
|
|
3420
|
+
get drawing() {
|
|
3421
|
+
return !!this.#drag;
|
|
3422
|
+
}
|
|
3423
|
+
wire() {
|
|
3424
|
+
const events = new ((this.viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
3425
|
+
const options = { signal: events.signal };
|
|
3426
|
+
const viewport = this.viewport;
|
|
3427
|
+
viewport.addEventListener("click", (event) => {
|
|
3428
|
+
if (this.options.active()) event.stopImmediatePropagation();
|
|
3429
|
+
}, {
|
|
3430
|
+
...options,
|
|
3431
|
+
capture: true
|
|
3432
|
+
});
|
|
3433
|
+
viewport.addEventListener("pointerdown", (event) => this.#start(event), options);
|
|
3434
|
+
viewport.addEventListener("pointermove", (event) => this.#move(event), options);
|
|
3435
|
+
viewport.addEventListener("pointerup", (event) => void this.#finish(event), options);
|
|
3436
|
+
viewport.addEventListener("pointercancel", () => this.#cancel(), options);
|
|
3437
|
+
viewport.addEventListener("keydown", (event) => {
|
|
3438
|
+
if (event.key === "Escape" && this.#drag) {
|
|
3439
|
+
event.preventDefault();
|
|
3440
|
+
this.#cancel();
|
|
3441
|
+
}
|
|
3442
|
+
}, options);
|
|
3443
|
+
return () => {
|
|
3444
|
+
++this.#request;
|
|
3445
|
+
this.#cancel();
|
|
3446
|
+
events.abort();
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
#start(event) {
|
|
3450
|
+
if (!this.options.active() || event.button !== 0 || this.#drag) return;
|
|
3451
|
+
const state = this.controller.state;
|
|
3452
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3453
|
+
const svg = this.viewport.querySelector("svg.paper");
|
|
3454
|
+
if (!page || !svg || !state.edit.sourceAvailable || state.edit.busy) return;
|
|
3455
|
+
const start = pagePoint(svg, page, event);
|
|
3456
|
+
if (!start) return;
|
|
3457
|
+
event.preventDefault();
|
|
3458
|
+
const rect = this.viewport.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
3459
|
+
rect.classList.add("draw-preview");
|
|
3460
|
+
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
3461
|
+
svg.append(rect);
|
|
3462
|
+
this.#drag = {
|
|
3463
|
+
pointer: event.pointerId,
|
|
3464
|
+
svg,
|
|
3465
|
+
page,
|
|
3466
|
+
...start,
|
|
3467
|
+
rect
|
|
3468
|
+
};
|
|
3469
|
+
this.viewport.setPointerCapture?.(event.pointerId);
|
|
3470
|
+
this.#update(start);
|
|
3471
|
+
}
|
|
3472
|
+
#move(event) {
|
|
3473
|
+
if (!this.#drag || event.pointerId !== this.#drag.pointer) return;
|
|
3474
|
+
const point = pagePoint(this.#drag.svg, this.#drag.page, event);
|
|
3475
|
+
if (point) this.#update(point);
|
|
3476
|
+
}
|
|
3477
|
+
#update(point) {
|
|
3478
|
+
const { x, y, rect } = this.#drag;
|
|
3479
|
+
rect.setAttribute("x", String(Math.min(x, point.x)));
|
|
3480
|
+
rect.setAttribute("y", String(Math.min(y, point.y)));
|
|
3481
|
+
rect.setAttribute("width", String(Math.abs(point.x - x)));
|
|
3482
|
+
rect.setAttribute("height", String(Math.abs(point.y - y)));
|
|
3483
|
+
}
|
|
3484
|
+
#cancel() {
|
|
3485
|
+
this.#drag?.rect.remove();
|
|
3486
|
+
this.#drag = void 0;
|
|
3487
|
+
}
|
|
3488
|
+
async #finish(event) {
|
|
3489
|
+
const drag = this.#drag;
|
|
3490
|
+
if (!drag || event.pointerId !== drag.pointer) return;
|
|
3491
|
+
const end = pagePoint(drag.svg, drag.page, event) ?? drag;
|
|
3492
|
+
this.#cancel();
|
|
3493
|
+
const width = Math.abs(end.x - drag.x), height = Math.abs(end.y - drag.y);
|
|
3494
|
+
if (width < MIN_SIZE || height < MIN_SIZE) {
|
|
3495
|
+
this.options.announce("Drag on the page to draw a rectangle.");
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
const state = this.controller.state;
|
|
3499
|
+
if (state.document?.pages[state.pageIndex] !== drag.page) return;
|
|
3500
|
+
const shapeId = nextShapeId(drag.page);
|
|
3501
|
+
const pageId = drag.page.id;
|
|
3502
|
+
const request = ++this.#request;
|
|
3503
|
+
try {
|
|
3504
|
+
await this.controller.applyEdits([{
|
|
3505
|
+
type: "create-rectangle",
|
|
3506
|
+
pageId,
|
|
3507
|
+
shapeId,
|
|
3508
|
+
x: (drag.x + end.x) / 2,
|
|
3509
|
+
y: drag.page.height - (drag.y + end.y) / 2,
|
|
3510
|
+
width,
|
|
3511
|
+
height
|
|
3512
|
+
}]);
|
|
3513
|
+
if (request !== this.#request) return;
|
|
3514
|
+
const created = this.controller.state.document?.pages.find((page) => page.id === pageId)?.shapes.find((shape) => shape.id === shapeId);
|
|
3515
|
+
if (created) this.controller.selectShape({
|
|
3516
|
+
id: shapeId,
|
|
3517
|
+
name: created.name,
|
|
3518
|
+
pageId
|
|
3519
|
+
});
|
|
3520
|
+
this.options.announce(`Rectangle ${shapeId} added (${+width.toFixed(4)} × ${+height.toFixed(4)} in).`);
|
|
3521
|
+
} catch (error) {
|
|
3522
|
+
if (request === this.#request && !isEditCancellation(error)) this.options.announce(editErrorMessage(error));
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
};
|
|
3526
|
+
//#endregion
|
|
3527
|
+
//#region src/viewer-commands.ts
|
|
3528
|
+
var editable = (target) => target instanceof Element && !!target.closest("input, textarea, select, [contenteditable]:not([contenteditable=\"false\"])");
|
|
3529
|
+
/**
|
|
3530
|
+
* Visio command state (tool, grid, pending edits) and keyboard shortcuts. Ribbon controls emit
|
|
3531
|
+
* typed `ribbon-action` events and shortcuts build the same actions; both go through
|
|
3532
|
+
* `routeRibbonAction`. Document mutation stays in the controller and core.
|
|
3533
|
+
*/
|
|
3534
|
+
var ViewerCommands = class {
|
|
3535
|
+
host;
|
|
3536
|
+
#tool = "pointer";
|
|
3537
|
+
#grid = false;
|
|
3538
|
+
#pending = 0;
|
|
3539
|
+
#draw;
|
|
3540
|
+
#targets;
|
|
3541
|
+
constructor(host) {
|
|
3542
|
+
this.host = host;
|
|
3543
|
+
this.#draw = new RectangleDrawTool(host.viewport, host.controller, {
|
|
3544
|
+
active: () => this.#tool === "rectangle",
|
|
3545
|
+
announce: host.announce
|
|
3546
|
+
});
|
|
3547
|
+
this.#targets = {
|
|
3548
|
+
controller: host.controller,
|
|
3549
|
+
history: (key) => this.#history(key),
|
|
3550
|
+
deleteSelection: () => this.#delete(),
|
|
3551
|
+
setTool: (tool) => this.setTool(tool),
|
|
3552
|
+
toggleGrid: () => {
|
|
3553
|
+
this.#grid = !this.#grid;
|
|
3554
|
+
this.render(host.controller.state);
|
|
3555
|
+
},
|
|
3556
|
+
toggleFullscreen: () => this.#toggleFullscreen(),
|
|
3557
|
+
togglePane: host.togglePane,
|
|
3558
|
+
reveal: host.reveal,
|
|
3559
|
+
fit: host.fit,
|
|
3560
|
+
focusSearch: host.focusSearch
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
get tool() {
|
|
3564
|
+
return this.#tool;
|
|
3565
|
+
}
|
|
3566
|
+
/** Run one typed action, exactly as a ribbon control or shortcut would. */
|
|
3567
|
+
run(action) {
|
|
3568
|
+
routeRibbonAction(this.#targets, action);
|
|
3569
|
+
}
|
|
3570
|
+
wire() {
|
|
3571
|
+
const { root, viewport } = this.host;
|
|
3572
|
+
const doc = root.ownerDocument;
|
|
3573
|
+
const events = new ((doc.defaultView?.AbortController) ?? AbortController)();
|
|
3574
|
+
const options = { signal: events.signal };
|
|
3575
|
+
root.addEventListener(RIBBON_ACTION_EVENT, (event) => this.run(event.detail), options);
|
|
3576
|
+
root.addEventListener("office-command", (event) => {
|
|
3577
|
+
if (event.detail?.command === "zoom-fit") this.run({
|
|
3578
|
+
type: "zoom",
|
|
3579
|
+
mode: "fit"
|
|
3580
|
+
});
|
|
3581
|
+
}, options);
|
|
3582
|
+
root.addEventListener("keydown", (event) => this.#shortcut(event), options);
|
|
3583
|
+
viewport.addEventListener("wheel", (event) => {
|
|
3584
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
3585
|
+
event.preventDefault();
|
|
3586
|
+
const zoom = this.host.controller.state.zoom;
|
|
3587
|
+
this.host.controller.setZoom(event.deltaY < 0 ? zoom * 1.1 : zoom / 1.1);
|
|
3588
|
+
}, {
|
|
3589
|
+
...options,
|
|
3590
|
+
passive: false
|
|
3591
|
+
});
|
|
3592
|
+
viewport.addEventListener("pointerdown", () => {
|
|
3593
|
+
if (!viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
|
|
3594
|
+
}, options);
|
|
3595
|
+
viewport.addEventListener("dblclick", (event) => {
|
|
3596
|
+
if (this.#tool !== "pointer") return;
|
|
3597
|
+
if (event.target?.closest?.("[data-shape-id]")) this.run({
|
|
3598
|
+
type: "reveal",
|
|
3599
|
+
panel: "edit",
|
|
3600
|
+
focusText: true
|
|
3601
|
+
});
|
|
3602
|
+
}, options);
|
|
3603
|
+
doc.addEventListener("fullscreenchange", () => this.render(this.host.controller.state), options);
|
|
3604
|
+
const disposeDraw = this.#draw.wire();
|
|
3605
|
+
return () => {
|
|
3606
|
+
++this.#pending;
|
|
3607
|
+
events.abort();
|
|
3608
|
+
disposeDraw();
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
setTool(tool) {
|
|
3612
|
+
if (tool === "rectangle" && !this.#canEdit(this.host.controller.state)) return;
|
|
3613
|
+
this.#tool = tool;
|
|
3614
|
+
this.render(this.host.controller.state);
|
|
3615
|
+
}
|
|
3616
|
+
#canEdit(state) {
|
|
3617
|
+
return state.edit.sourceAvailable && !state.loading && !state.edit.busy;
|
|
3618
|
+
}
|
|
3619
|
+
#history(key) {
|
|
3620
|
+
const { controller } = this.host;
|
|
3621
|
+
const { edit, loading } = controller.state;
|
|
3622
|
+
if ((key === "undo" ? edit.canUndo : edit.canRedo) && !edit.busy && !loading) this.#edit(() => controller[key]());
|
|
3623
|
+
}
|
|
3624
|
+
#delete() {
|
|
3625
|
+
const state = this.host.controller.state;
|
|
3626
|
+
const shape = state.selectedShape;
|
|
3627
|
+
const pageId = shape?.pageId ?? state.document?.pages[state.pageIndex]?.id;
|
|
3628
|
+
if (!shape || pageId === void 0 || !this.#canEdit(state)) return;
|
|
3629
|
+
this.#edit(() => this.host.controller.applyEdits([{
|
|
3630
|
+
type: "delete-shape",
|
|
3631
|
+
pageId,
|
|
3632
|
+
shapeId: shape.id
|
|
3633
|
+
}]), `Deleted ${shape.name || `shape ${shape.id}`}.`);
|
|
3634
|
+
}
|
|
3635
|
+
async #edit(action, success) {
|
|
3636
|
+
const request = ++this.#pending;
|
|
3637
|
+
const { root, viewport } = this.host;
|
|
3638
|
+
const canvasFocused = viewport.contains(root.activeElement);
|
|
3639
|
+
try {
|
|
3640
|
+
await action();
|
|
3641
|
+
if (canvasFocused && !viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
|
|
3642
|
+
if (success && request === this.#pending) this.host.announce(success);
|
|
3643
|
+
} catch (error) {
|
|
3644
|
+
if (request === this.#pending && !isEditCancellation(error) && !this.host.controller.state.edit.error) this.host.announce(editErrorMessage(error));
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
#toggleFullscreen() {
|
|
3648
|
+
const host = this.host.root.host;
|
|
3649
|
+
const doc = this.host.root.ownerDocument;
|
|
3650
|
+
(doc.fullscreenElement === host ? doc.exitFullscreen?.() : host.requestFullscreen?.())?.catch?.(() => this.host.announce("Full screen is not available in this browser frame."));
|
|
3651
|
+
}
|
|
3652
|
+
/** Visio shortcuts as typed actions. Text fields keep native undo, deletion and editing keys. */
|
|
3653
|
+
#shortcutAction(event) {
|
|
3654
|
+
const state = this.host.controller.state;
|
|
3655
|
+
const control = event.ctrlKey || event.metaKey;
|
|
3656
|
+
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
|
|
3657
|
+
if (control && key === "f") return { type: "search" };
|
|
3658
|
+
if (control && event.shiftKey && key === "w") return {
|
|
3659
|
+
type: "zoom",
|
|
3660
|
+
mode: "fit"
|
|
3661
|
+
};
|
|
3662
|
+
if (control && (key === "PageDown" || key === "PageUp")) return {
|
|
3663
|
+
type: "page",
|
|
3664
|
+
step: key === "PageDown" ? 1 : -1
|
|
3665
|
+
};
|
|
3666
|
+
if (key === "F5" && !control) return { type: "fullscreen" };
|
|
3667
|
+
if (editable(event.target)) return void 0;
|
|
3668
|
+
if (control && !event.shiftKey && key === "z") return {
|
|
3669
|
+
type: "history",
|
|
3670
|
+
key: "undo"
|
|
3671
|
+
};
|
|
3672
|
+
if (control && (key === "y" || event.shiftKey && key === "z")) return {
|
|
3673
|
+
type: "history",
|
|
3674
|
+
key: "redo"
|
|
3675
|
+
};
|
|
3676
|
+
if (control && key === "1") return {
|
|
3677
|
+
type: "tool",
|
|
3678
|
+
tool: "pointer"
|
|
3679
|
+
};
|
|
3680
|
+
if (control && key === "8") return {
|
|
3681
|
+
type: "tool",
|
|
3682
|
+
tool: "rectangle"
|
|
3683
|
+
};
|
|
3684
|
+
if (!control && key === "Delete" && state.selectedShape) return { type: "delete" };
|
|
3685
|
+
if (!control && key === "F2" && state.document) return {
|
|
3686
|
+
type: "reveal",
|
|
3687
|
+
panel: "edit",
|
|
3688
|
+
focusText: true
|
|
3689
|
+
};
|
|
3690
|
+
if (key === "Escape" && this.#tool !== "pointer" && !this.#draw.drawing) return {
|
|
3691
|
+
type: "tool",
|
|
3692
|
+
tool: "pointer"
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
#shortcut(event) {
|
|
3696
|
+
if (event.defaultPrevented || event.isComposing || event.altKey) return;
|
|
3697
|
+
const action = this.#shortcutAction(event);
|
|
3698
|
+
if (!action) return;
|
|
3699
|
+
event.preventDefault();
|
|
3700
|
+
this.run(action);
|
|
3701
|
+
}
|
|
3702
|
+
render(state) {
|
|
3703
|
+
const { root, viewport } = this.host;
|
|
3704
|
+
const button = (name) => root.querySelector(`office-ui-button[command="${name}"]`);
|
|
3705
|
+
const editing = this.#canEdit(state);
|
|
3706
|
+
if (this.#tool === "rectangle" && !state.edit.sourceAvailable) this.#tool = "pointer";
|
|
3707
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3708
|
+
button("undo").disabled = !state.edit.canUndo || state.edit.busy || state.loading;
|
|
3709
|
+
button("redo").disabled = !state.edit.canRedo || state.edit.busy || state.loading;
|
|
3710
|
+
button("delete").disabled = !editing || !state.selectedShape;
|
|
3711
|
+
button("pointer").setAttribute("pressed", String(this.#tool === "pointer"));
|
|
3712
|
+
const rectangle = button("rectangle");
|
|
3713
|
+
rectangle.setAttribute("pressed", String(this.#tool === "rectangle"));
|
|
3714
|
+
rectangle.disabled = !editing || !page;
|
|
3715
|
+
rectangle.title = state.edit.sourceAvailable ? "Rectangle (Ctrl+8)" : "Rectangle (Ctrl+8): open a .vsdx file to draw. Model-only documents are read only.";
|
|
3716
|
+
button("grid").setAttribute("pressed", String(this.#grid));
|
|
3717
|
+
button("grid").disabled = !page;
|
|
3718
|
+
for (const name of [
|
|
3719
|
+
"zoom-fit",
|
|
3720
|
+
"page-width",
|
|
3721
|
+
"actual-size"
|
|
3722
|
+
]) button(name).disabled = !page;
|
|
3723
|
+
const fullscreen = button("fullscreen");
|
|
3724
|
+
const host = root.host;
|
|
3725
|
+
fullscreen.disabled = typeof host.requestFullscreen !== "function";
|
|
3726
|
+
fullscreen.setAttribute("pressed", String(root.ownerDocument.fullscreenElement === host));
|
|
3727
|
+
viewport.dataset.tool = this.#tool;
|
|
3728
|
+
viewport.dataset.grid = String(this.#grid);
|
|
3729
|
+
}
|
|
3730
|
+
};
|
|
3731
|
+
//#endregion
|
|
3732
|
+
//#region src/office-ui.ts
|
|
3733
|
+
/**
|
|
3734
|
+
* Visio-only glyphs (20x20 path data). Neutral Office glyphs come from ooxml-ui; add a glyph
|
|
3735
|
+
* there instead when another Office product could use it.
|
|
3736
|
+
*/
|
|
3737
|
+
var VISIO_ICONS = {
|
|
3738
|
+
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",
|
|
3739
|
+
visioShapeData: "M3 3.5h14v13H3ZM3 7.5h14M8 7.5v9M10.5 11h4M10.5 14h4",
|
|
3740
|
+
visioPagesPane: "M2.5 3h15v14h-15ZM7 3v14M3.8 6h1.9M3.8 9h1.9M3.8 12h1.9",
|
|
3741
|
+
visioInspectorPane: "M2.5 3h15v14h-15ZM13 3v14M14.5 6h1.5M14.5 9h1.5M14.5 12h1.5"
|
|
3742
|
+
};
|
|
3743
|
+
/** Define the shared Office controls the viewer chrome uses. Idempotent; browser only. */
|
|
3744
|
+
function registerViewerControls(registry) {
|
|
3745
|
+
for (const [name, path] of Object.entries(VISIO_ICONS)) if (!getIcon(name)) registerIcon(name, path);
|
|
3746
|
+
registerControls(registry);
|
|
3747
|
+
}
|
|
3748
|
+
//#endregion
|
|
3749
|
+
//#region src/export-svg.ts
|
|
3085
3750
|
var MAX_SVG_EXPORT_BYTES = 16777216;
|
|
3086
3751
|
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.";
|
|
3087
3752
|
var INVALID_XML = "Invalid XML characters were replaced with the Unicode replacement character in the SVG export.";
|
|
@@ -3251,7 +3916,7 @@ function assertSerializedBudget(root, maxBytes) {
|
|
|
3251
3916
|
}
|
|
3252
3917
|
}
|
|
3253
3918
|
//#endregion
|
|
3254
|
-
//#region
|
|
3919
|
+
//#region src/snapshot-scene.ts
|
|
3255
3920
|
/** Copy only named scalar fields, never arbitrary host properties or their getters. */
|
|
3256
3921
|
function fields(source, keys) {
|
|
3257
3922
|
const result = {};
|
|
@@ -3467,7 +4132,7 @@ function copySnapshotScene(model) {
|
|
|
3467
4132
|
};
|
|
3468
4133
|
}
|
|
3469
4134
|
//#endregion
|
|
3470
|
-
//#region
|
|
4135
|
+
//#region src/print-snapshot.ts
|
|
3471
4136
|
/** Application guardrails, not printer capabilities or Microsoft Visio limits. */
|
|
3472
4137
|
var PRINT_SNAPSHOT_LIMITS = Object.freeze({
|
|
3473
4138
|
maxPages: 32,
|
|
@@ -3684,14 +4349,17 @@ function validationCost(model) {
|
|
|
3684
4349
|
};
|
|
3685
4350
|
}
|
|
3686
4351
|
//#endregion
|
|
3687
|
-
//#region
|
|
4352
|
+
//#region src/viewer-element.ts
|
|
3688
4353
|
var BaseElement = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
|
|
3689
4354
|
var VisioViewerElement = class extends BaseElement {
|
|
4355
|
+
controller = new ViewerController(createWorkerParser());
|
|
3690
4356
|
#root;
|
|
3691
4357
|
#disposeInputs;
|
|
3692
4358
|
#viewport;
|
|
3693
|
-
#
|
|
3694
|
-
#
|
|
4359
|
+
#zoomSlider;
|
|
4360
|
+
#shapeStatus;
|
|
4361
|
+
#announcement;
|
|
4362
|
+
#commands;
|
|
3695
4363
|
#status;
|
|
3696
4364
|
#diagnostics;
|
|
3697
4365
|
#toolbar;
|
|
@@ -3705,38 +4373,33 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3705
4373
|
#notesPanel;
|
|
3706
4374
|
#inspector;
|
|
3707
4375
|
#inspectedShape;
|
|
3708
|
-
#renderedSelection;
|
|
4376
|
+
#renderedSelection = null;
|
|
3709
4377
|
#unsubscribe;
|
|
3710
4378
|
#eventUnsubscribe;
|
|
3711
|
-
#document;
|
|
3712
|
-
#renderedPage;
|
|
3713
|
-
#disposed;
|
|
3714
|
-
#suspended;
|
|
3715
|
-
#renderWarnings;
|
|
3716
|
-
#disposeRenderer;
|
|
4379
|
+
#document = null;
|
|
4380
|
+
#renderedPage = -1;
|
|
4381
|
+
#disposed = false;
|
|
4382
|
+
#suspended = false;
|
|
4383
|
+
#renderWarnings = [];
|
|
4384
|
+
#disposeRenderer = () => {};
|
|
3717
4385
|
#fontEvents;
|
|
3718
|
-
#fontsChanged
|
|
3719
|
-
|
|
3720
|
-
super();
|
|
3721
|
-
this.controller = new ViewerController(createWorkerParser());
|
|
3722
|
-
this.#renderedSelection = null;
|
|
4386
|
+
#fontsChanged = () => {
|
|
4387
|
+
if (this.#disposed) return;
|
|
3723
4388
|
this.#document = null;
|
|
3724
4389
|
this.#renderedPage = -1;
|
|
3725
|
-
this.#
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
this.#fontsChanged = () => {
|
|
3730
|
-
if (this.#disposed) return;
|
|
3731
|
-
this.#document = null;
|
|
3732
|
-
this.#renderedPage = -1;
|
|
3733
|
-
this.#render(this.controller.state);
|
|
3734
|
-
};
|
|
4390
|
+
this.#render(this.controller.state);
|
|
4391
|
+
};
|
|
4392
|
+
constructor() {
|
|
4393
|
+
super();
|
|
3735
4394
|
this.#root = this.attachShadow({ mode: "open" });
|
|
3736
|
-
|
|
4395
|
+
registerViewerControls();
|
|
4396
|
+
this.#root.innerHTML = `<style>${viewerStyles}${canvasAndRibbonStyles}</style>${viewerChromeTemplate}`;
|
|
4397
|
+
const workspace = this.#root.querySelector(".workspace");
|
|
4398
|
+
workspace.before(createRibbon(document));
|
|
4399
|
+
workspace.after(createPageTabs(document), createStatusBar(document));
|
|
3737
4400
|
this.#viewport = this.#root.querySelector(".viewport");
|
|
3738
|
-
this.#
|
|
3739
|
-
this.#
|
|
4401
|
+
this.#zoomSlider = this.#root.querySelector("office-ui-zoom-slider");
|
|
4402
|
+
this.#shapeStatus = this.#root.querySelector("[data-shape-status]");
|
|
3740
4403
|
this.#status = this.#root.querySelector("[data-status]");
|
|
3741
4404
|
this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
|
|
3742
4405
|
this.#toolbar = this.#root.querySelector(".toolbar");
|
|
@@ -3747,6 +4410,23 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3747
4410
|
this.#notesPanel = this.#root.querySelector(".notes");
|
|
3748
4411
|
this.#inspector = this.#root.querySelector(".shape-inspector");
|
|
3749
4412
|
this.#chrome = new ViewerChrome(this.#root, this.controller);
|
|
4413
|
+
this.#commands = new ViewerCommands({
|
|
4414
|
+
root: this.#root,
|
|
4415
|
+
viewport: this.#viewport,
|
|
4416
|
+
controller: this.controller,
|
|
4417
|
+
fit: (mode) => this.#fit(mode),
|
|
4418
|
+
togglePane: (pane) => this.#chrome.togglePane(pane),
|
|
4419
|
+
reveal: (panel, focusText) => this.#chrome.reveal(panel, focusText),
|
|
4420
|
+
focusSearch: () => {
|
|
4421
|
+
this.#chrome.showTab("home");
|
|
4422
|
+
this.#search.input.focus();
|
|
4423
|
+
this.#search.input.select();
|
|
4424
|
+
},
|
|
4425
|
+
announce: (message) => {
|
|
4426
|
+
this.#announcement = message;
|
|
4427
|
+
this.#status.textContent = message;
|
|
4428
|
+
}
|
|
4429
|
+
});
|
|
3750
4430
|
this.#disposeInputs = this.#wireInputs();
|
|
3751
4431
|
this.#unsubscribe = this.controller.subscribe((state) => this.#render(state));
|
|
3752
4432
|
this.#eventUnsubscribe = this.controller.onEvent((name, detail) => {
|
|
@@ -3820,13 +4500,24 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3820
4500
|
}
|
|
3821
4501
|
fit() {
|
|
3822
4502
|
this.#assertAlive();
|
|
4503
|
+
this.#fit("page");
|
|
4504
|
+
}
|
|
4505
|
+
/** Visio Fit to Window shows the whole page; Page Width fills the canvas width. */
|
|
4506
|
+
#fit(mode) {
|
|
3823
4507
|
const page = this.document?.pages[this.pageIndex];
|
|
3824
4508
|
if (!page) return;
|
|
3825
4509
|
const style = this.ownerDocument.defaultView?.getComputedStyle(this.#viewport);
|
|
3826
4510
|
const padding = (value) => Number.parseFloat(value ?? "") || 0;
|
|
3827
4511
|
const width = Math.max(1, this.#viewport.clientWidth - padding(style?.paddingLeft) - padding(style?.paddingRight));
|
|
3828
4512
|
const height = Math.max(1, this.#viewport.clientHeight - padding(style?.paddingTop) - padding(style?.paddingBottom));
|
|
3829
|
-
|
|
4513
|
+
if (mode === "page") {
|
|
4514
|
+
this.zoom = Math.min(1, width / (page.width * 96), height / (page.height * 96));
|
|
4515
|
+
return;
|
|
4516
|
+
}
|
|
4517
|
+
const scrollbar = this.#viewport.offsetWidth - this.#viewport.clientWidth;
|
|
4518
|
+
const fitted = width / (page.width * 96);
|
|
4519
|
+
const reserve = page.height * 96 * fitted > height && scrollbar <= 0 ? 17 : 0;
|
|
4520
|
+
this.zoom = Math.max(1, width - reserve - 1) / (page.width * 96);
|
|
3830
4521
|
}
|
|
3831
4522
|
setLayerVisibility(pageId, layerId, visible) {
|
|
3832
4523
|
this.#assertAlive();
|
|
@@ -3896,16 +4587,18 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3896
4587
|
}
|
|
3897
4588
|
#wireInputs() {
|
|
3898
4589
|
const disposeChrome = this.#chrome.wire();
|
|
4590
|
+
const disposeCommands = this.#commands.wire();
|
|
3899
4591
|
const disposeEdit = this.#edit.wire();
|
|
3900
4592
|
const disposeLayers = wireLayerControls(this.#layers, this.controller);
|
|
3901
4593
|
const disposeInputs = wireViewerInputs({
|
|
3902
4594
|
viewport: this.#viewport,
|
|
3903
4595
|
commandRoot: this.#root,
|
|
3904
|
-
|
|
4596
|
+
zoomSlider: this.#zoomSlider,
|
|
3905
4597
|
searchInput: this.#search.input
|
|
3906
|
-
}, this.controller, () => this
|
|
4598
|
+
}, this.controller, (mode) => this.#fit(mode));
|
|
3907
4599
|
return () => {
|
|
3908
4600
|
disposeChrome();
|
|
4601
|
+
disposeCommands();
|
|
3909
4602
|
disposeInputs();
|
|
3910
4603
|
disposeLayers();
|
|
3911
4604
|
disposeEdit();
|
|
@@ -3924,14 +4617,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3924
4617
|
this.#document = state.document;
|
|
3925
4618
|
this.#renderedPage = state.pageIndex;
|
|
3926
4619
|
this.#renderedLayerOverrides = state.layerVisibilityOverrides;
|
|
3927
|
-
this.#
|
|
3928
|
-
for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
|
|
3929
|
-
const option = document.createElement("option");
|
|
3930
|
-
option.value = String(index);
|
|
3931
|
-
option.textContent = candidate.name + (candidate.isBackground ? " (background)" : "");
|
|
3932
|
-
this.#pageSelect.append(option);
|
|
3933
|
-
}
|
|
3934
|
-
this.#pageSelect.value = String(state.pageIndex);
|
|
4620
|
+
this.#announcement = void 0;
|
|
3935
4621
|
this.#renderWarnings = [];
|
|
3936
4622
|
if (state.document && page) {
|
|
3937
4623
|
const result = renderPage(state.document, page, { layerVisibilityOverrides: state.layerVisibilityOverrides });
|
|
@@ -3943,7 +4629,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3943
4629
|
empty.className = "empty";
|
|
3944
4630
|
const heading = document.createElement("strong");
|
|
3945
4631
|
heading.textContent = "Open a Visio drawing";
|
|
3946
|
-
empty.append(heading, "Open a .vsdx file to start. Files stay in this browser.");
|
|
4632
|
+
empty.append(heading, "Open a .vsdx or supported .vsd file to start. Files stay in this browser.");
|
|
3947
4633
|
this.#viewport.replaceChildren(empty);
|
|
3948
4634
|
}
|
|
3949
4635
|
const notes = compatibilityNotes(state.document?.diagnostics ?? [], this.#renderWarnings);
|
|
@@ -3958,6 +4644,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3958
4644
|
if (svg && page) {
|
|
3959
4645
|
svg.style.width = `${page.width * 96 * state.zoom}px`;
|
|
3960
4646
|
svg.style.height = `${page.height * 96 * state.zoom}px`;
|
|
4647
|
+
svg.style.setProperty("--vv-inch", `${96 * state.zoom}px`);
|
|
3961
4648
|
}
|
|
3962
4649
|
const searchResult = state.search.results[state.search.activeIndex];
|
|
3963
4650
|
const selection = state.selectedShape;
|
|
@@ -3985,16 +4672,20 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3985
4672
|
this.#renderedSelection = selection ? { ...selection } : null;
|
|
3986
4673
|
}
|
|
3987
4674
|
if (!searchResult) this.#revealedSearchResult = void 0;
|
|
3988
|
-
this.#
|
|
3989
|
-
this.#
|
|
4675
|
+
this.#zoomSlider.value = Math.round(state.zoom * 100);
|
|
4676
|
+
this.#zoomSlider.disabled = !page;
|
|
4677
|
+
const inspected = selectedShape(state.document, state.selectedShape, state.pageIndex);
|
|
4678
|
+
const inches = (value) => `${+value.toFixed(3)} in`;
|
|
4679
|
+
this.#shapeStatus.setAttribute("value", inspected ? `Width: ${inches(inspected.width)} Height: ${inches(inspected.height)}` : "");
|
|
3990
4680
|
for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
|
|
3991
4681
|
renderSearchControls(this.#search, state);
|
|
3992
4682
|
this.#edit.render(state);
|
|
3993
4683
|
if (changed) renderLayerControls(this.#layers, state);
|
|
3994
4684
|
else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
|
|
3995
4685
|
this.#chrome.render(state, this.#notes.children.length);
|
|
4686
|
+
this.#commands.render(state);
|
|
3996
4687
|
this.#viewport.setAttribute("aria-busy", String(state.loading || state.edit.busy));
|
|
3997
|
-
this.#status.textContent = state.loading ? "Opening diagram…" : state.error?.message ?? (page ? `${page.name} · ${page.shapes.length} top-level shapes` : "No diagram open");
|
|
4688
|
+
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");
|
|
3998
4689
|
const warnings = (state.document?.diagnostics.length ?? 0) + this.#renderWarnings.length;
|
|
3999
4690
|
this.#diagnostics.textContent = warnings ? `${this.#notes.children.length} compatibility notes` : "Local-only viewing";
|
|
4000
4691
|
}
|
|
@@ -4004,7 +4695,7 @@ function registerVisioViewer() {
|
|
|
4004
4695
|
if (!customElements.get("visio-viewer")) customElements.define("visio-viewer", VisioViewerElement);
|
|
4005
4696
|
}
|
|
4006
4697
|
//#endregion
|
|
4007
|
-
//#region
|
|
4698
|
+
//#region src/binding.ts
|
|
4008
4699
|
/** One client-only lifecycle adapter; framework wrappers forward to this implementation. */
|
|
4009
4700
|
function mountViewer(container, initial = {}) {
|
|
4010
4701
|
registerVisioViewer();
|
|
@@ -4105,7 +4796,7 @@ function mountViewer(container, initial = {}) {
|
|
|
4105
4796
|
};
|
|
4106
4797
|
}
|
|
4107
4798
|
//#endregion
|
|
4108
|
-
//#region
|
|
4799
|
+
//#region packages/bindings/src/common.ts
|
|
4109
4800
|
/** One mapping for every property and event, shared by every framework adapter. */
|
|
4110
4801
|
function viewerOptions(props) {
|
|
4111
4802
|
const result = { events: props.events ?? {} };
|
|
@@ -4204,13 +4895,30 @@ function viewerHandle(current) {
|
|
|
4204
4895
|
}
|
|
4205
4896
|
};
|
|
4206
4897
|
}
|
|
4898
|
+
function viewerStateSource(current) {
|
|
4899
|
+
const controller = () => {
|
|
4900
|
+
try {
|
|
4901
|
+
return current()?.controller;
|
|
4902
|
+
} catch {
|
|
4903
|
+
return;
|
|
4904
|
+
}
|
|
4905
|
+
};
|
|
4906
|
+
return {
|
|
4907
|
+
subscribe(onChange) {
|
|
4908
|
+
return controller()?.subscribe(() => onChange()) ?? (() => {});
|
|
4909
|
+
},
|
|
4910
|
+
getSnapshot() {
|
|
4911
|
+
return controller()?.state ?? null;
|
|
4912
|
+
}
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4207
4915
|
/** Native framework emits and callback-map props share one complete event inventory. */
|
|
4208
4916
|
function withEventEmitter(props, emit) {
|
|
4209
4917
|
const options = viewerOptions(props);
|
|
4210
4918
|
const callbacks = {};
|
|
4211
4919
|
for (const name of eventKeys) Object.assign(callbacks, { [name]: (value) => {
|
|
4212
4920
|
try {
|
|
4213
|
-
props.events?.[name]?.(value);
|
|
4921
|
+
(props.events?.[name])?.(value);
|
|
4214
4922
|
} finally {
|
|
4215
4923
|
emit(name, value);
|
|
4216
4924
|
}
|
|
@@ -4221,14 +4929,15 @@ function withEventEmitter(props, emit) {
|
|
|
4221
4929
|
};
|
|
4222
4930
|
}
|
|
4223
4931
|
//#endregion
|
|
4224
|
-
//#region .
|
|
4225
|
-
|
|
4932
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
|
|
4933
|
+
function __decorate(decorators, target, key, desc) {
|
|
4226
4934
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4227
4935
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4228
4936
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
4229
4937
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
4230
|
-
}
|
|
4231
|
-
|
|
4938
|
+
}
|
|
4939
|
+
//#endregion
|
|
4940
|
+
//#region packages/bindings/src/angular.ts
|
|
4232
4941
|
var VisioViewerComponent = class VisioViewerComponent {
|
|
4233
4942
|
constructor() {
|
|
4234
4943
|
this.documentLoad = new EventEmitter();
|
|
@@ -4239,6 +4948,9 @@ var VisioViewerComponent = class VisioViewerComponent {
|
|
|
4239
4948
|
this.shapeSelect = new EventEmitter();
|
|
4240
4949
|
this.host = inject(ElementRef);
|
|
4241
4950
|
this.handle = viewerHandle(() => this.binding);
|
|
4951
|
+
this.stopState = () => {};
|
|
4952
|
+
this.viewerState = signal(null);
|
|
4953
|
+
this.state = this.viewerState.asReadonly();
|
|
4242
4954
|
this.outputs = {
|
|
4243
4955
|
"document-load": this.documentLoad,
|
|
4244
4956
|
"document-change": this.documentChange,
|
|
@@ -4255,11 +4967,17 @@ var VisioViewerComponent = class VisioViewerComponent {
|
|
|
4255
4967
|
}
|
|
4256
4968
|
ngAfterViewInit() {
|
|
4257
4969
|
this.binding = mountFrameworkViewer(this.host.nativeElement, this.options());
|
|
4970
|
+
const source = viewerStateSource(() => this.handle);
|
|
4971
|
+
this.viewerState.set(source.getSnapshot());
|
|
4972
|
+
this.stopState = source.subscribe(() => this.viewerState.set(source.getSnapshot()));
|
|
4258
4973
|
}
|
|
4259
4974
|
ngOnChanges() {
|
|
4260
4975
|
this.binding?.update(this.options());
|
|
4261
4976
|
}
|
|
4262
4977
|
ngOnDestroy() {
|
|
4978
|
+
this.stopState();
|
|
4979
|
+
this.stopState = () => {};
|
|
4980
|
+
this.viewerState.set(null);
|
|
4263
4981
|
const mounted = this.binding;
|
|
4264
4982
|
this.binding = void 0;
|
|
4265
4983
|
mounted?.destroy();
|