visio-angular-viewer 0.1.0 → 0.2.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/README.md +80 -4
- package/dist/assets/{edit-worker-DnTSJ65K.js → edit-worker-BcXrV_St.js} +204 -32
- package/dist/assets/{parse-worker-CvtlZkVJ.js → parse-worker-B8EBQQpD.js} +1 -1
- package/dist/index.js +1021 -206
- package/dist/third-party/Inter-OFL.txt +92 -0
- 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
1
|
import { VISIO_RASTER_IMAGE_LIMITS, VISIO_VISIBILITY_LIMITS, getVisioPageLayers, inspectVisioRasterImage, parseVsdx, resolveVisioPageVisibility, validateVisioForeignVector } from "ooxml-core/visio";
|
|
2
|
-
import {
|
|
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;
|
|
@@ -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-BcXrV_St.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,
|
|
@@ -1183,7 +1187,7 @@ var ViewerController = class {
|
|
|
1183
1187
|
}
|
|
1184
1188
|
};
|
|
1185
1189
|
//#endregion
|
|
1186
|
-
//#region
|
|
1190
|
+
//#region src/contract.ts
|
|
1187
1191
|
var propertyKeys = [
|
|
1188
1192
|
"document",
|
|
1189
1193
|
"pageIndex",
|
|
@@ -1199,13 +1203,13 @@ var eventKeys = [
|
|
|
1199
1203
|
"shape-select"
|
|
1200
1204
|
];
|
|
1201
1205
|
//#endregion
|
|
1202
|
-
//#region
|
|
1206
|
+
//#region src/worker-parser.ts
|
|
1203
1207
|
/** One disposable worker per document prevents old parses and long CPU tasks from occupying the UI. */
|
|
1204
1208
|
function createWorkerParser(factory, timeoutMs = 15e3) {
|
|
1205
1209
|
if (!factory && typeof Worker === "undefined") return parseVsdx;
|
|
1206
1210
|
const create = factory ?? (() => new Worker(new URL(
|
|
1207
1211
|
/* @vite-ignore */
|
|
1208
|
-
new URL("assets/parse-worker-
|
|
1212
|
+
new URL("assets/parse-worker-B8EBQQpD.js", import.meta.url).href,
|
|
1209
1213
|
"" + import.meta.url
|
|
1210
1214
|
), { type: "module" }));
|
|
1211
1215
|
let cancel;
|
|
@@ -1252,7 +1256,7 @@ function createWorkerParser(factory, timeoutMs = 15e3) {
|
|
|
1252
1256
|
return parse;
|
|
1253
1257
|
}
|
|
1254
1258
|
//#endregion
|
|
1255
|
-
//#region
|
|
1259
|
+
//#region src/render-fill.ts
|
|
1256
1260
|
var gradientId = 0;
|
|
1257
1261
|
function fillPaint(style, defs) {
|
|
1258
1262
|
if (!style.fillGradient) return safeColor(style.fill, "#fff");
|
|
@@ -1274,7 +1278,7 @@ function fillPaint(style, defs) {
|
|
|
1274
1278
|
return `url(#${gradient.id})`;
|
|
1275
1279
|
}
|
|
1276
1280
|
//#endregion
|
|
1277
|
-
//#region
|
|
1281
|
+
//#region src/render-image.ts
|
|
1278
1282
|
var clipId$1 = 0;
|
|
1279
1283
|
/** URLs are created only from bounded embedded raster bytes, never from document strings. */
|
|
1280
1284
|
function renderImage(shape, resources) {
|
|
@@ -1307,7 +1311,7 @@ function renderImage(shape, resources) {
|
|
|
1307
1311
|
return group;
|
|
1308
1312
|
}
|
|
1309
1313
|
//#endregion
|
|
1310
|
-
//#region
|
|
1314
|
+
//#region src/render-foreign-vector.ts
|
|
1311
1315
|
var NS$1 = "http://www.w3.org/2000/svg";
|
|
1312
1316
|
var nextResource = 0;
|
|
1313
1317
|
var pathData = (commands) => commands.map(({ command, values }) => `${command}${values.length ? " " + values.join(" ") : ""}`).join(" ");
|
|
@@ -1373,7 +1377,7 @@ function renderForeignVector(source, owner = document, budget = new ForeignVecto
|
|
|
1373
1377
|
return root;
|
|
1374
1378
|
}
|
|
1375
1379
|
//#endregion
|
|
1376
|
-
//#region
|
|
1380
|
+
//#region src/render-foreign-vector-shape.ts
|
|
1377
1381
|
var clipId = 0;
|
|
1378
1382
|
/** Core supplies shape-local placement; converter coordinates have a downward Y axis. */
|
|
1379
1383
|
function renderForeignVectorShape(shape, budget) {
|
|
@@ -1397,10 +1401,11 @@ function renderForeignVectorShape(shape, budget) {
|
|
|
1397
1401
|
return group;
|
|
1398
1402
|
}
|
|
1399
1403
|
//#endregion
|
|
1400
|
-
//#region
|
|
1404
|
+
//#region src/render-resources.ts
|
|
1401
1405
|
var MAX_EMBEDDED_BYTES = 16777216;
|
|
1402
1406
|
/** One reference per shared core image resource, with explicit per-render disposal. */
|
|
1403
1407
|
var RenderResources = class {
|
|
1408
|
+
definitions;
|
|
1404
1409
|
#images = /* @__PURE__ */ new Map();
|
|
1405
1410
|
#disposed = false;
|
|
1406
1411
|
#embeddedBytes = 0;
|
|
@@ -1454,7 +1459,7 @@ function base64(bytes) {
|
|
|
1454
1459
|
return chunks.join("");
|
|
1455
1460
|
}
|
|
1456
1461
|
//#endregion
|
|
1457
|
-
//#region
|
|
1462
|
+
//#region src/arrowheads.ts
|
|
1458
1463
|
var markerId = 0;
|
|
1459
1464
|
/** Common Visio line/triangle and concave arrow styles. Sizes remain a documented visual approximation. */
|
|
1460
1465
|
function applyArrowheads(path, style, defs, warnings) {
|
|
@@ -1498,7 +1503,7 @@ function applyArrowheads(path, style, defs, warnings) {
|
|
|
1498
1503
|
}
|
|
1499
1504
|
}
|
|
1500
1505
|
//#endregion
|
|
1501
|
-
//#region
|
|
1506
|
+
//#region src/text-layout.ts
|
|
1502
1507
|
var DEFAULT_LIMITS = {
|
|
1503
1508
|
maxCharacters: 25e4,
|
|
1504
1509
|
maxMeasurements: 5e4,
|
|
@@ -1518,6 +1523,9 @@ var TextLayoutBudgetError = class extends Error {
|
|
|
1518
1523
|
};
|
|
1519
1524
|
/** One bounded measurement cache and work budget per render; shape scopes share both. */
|
|
1520
1525
|
var TextLayoutBudget = class TextLayoutBudget {
|
|
1526
|
+
limits;
|
|
1527
|
+
cache;
|
|
1528
|
+
parent;
|
|
1521
1529
|
#characters = 0;
|
|
1522
1530
|
#measurements = 0;
|
|
1523
1531
|
#lines = 0;
|
|
@@ -1687,7 +1695,7 @@ function wrapText(runs, width, measure = measureBrowserText, firstLineIndent = 0
|
|
|
1687
1695
|
return lines;
|
|
1688
1696
|
}
|
|
1689
1697
|
//#endregion
|
|
1690
|
-
//#region
|
|
1698
|
+
//#region src/paragraph-layout.ts
|
|
1691
1699
|
function layoutParagraphs(text, measure = measureBrowserText, budget = createTextLayoutBudget()) {
|
|
1692
1700
|
const scope = budget.forShape();
|
|
1693
1701
|
const runs = text.runs.length ? text.runs : [{
|
|
@@ -1785,7 +1793,7 @@ function sliceRuns(runs, offsets, start, end) {
|
|
|
1785
1793
|
return result;
|
|
1786
1794
|
}
|
|
1787
1795
|
//#endregion
|
|
1788
|
-
//#region
|
|
1796
|
+
//#region src/render-text.ts
|
|
1789
1797
|
/** Browser-font layout is approximate until compared with the licensed Visio reference corpus. */
|
|
1790
1798
|
function renderText(text, warnings, budget) {
|
|
1791
1799
|
const group = svgElement("g");
|
|
@@ -1863,7 +1871,7 @@ function renderText(text, warnings, budget) {
|
|
|
1863
1871
|
return group;
|
|
1864
1872
|
}
|
|
1865
1873
|
//#endregion
|
|
1866
|
-
//#region
|
|
1874
|
+
//#region src/render-svg.ts
|
|
1867
1875
|
var NS = "http://www.w3.org/2000/svg";
|
|
1868
1876
|
function svgElement(name) {
|
|
1869
1877
|
return document.createElementNS(NS, name);
|
|
@@ -1996,7 +2004,7 @@ function drawOwn(shape, group, context) {
|
|
|
1996
2004
|
if (shape.kind === "foreign" && !shape.image && !shape.foreignVector) warnings.add("Embedded or foreign objects are not rendered in this build.");
|
|
1997
2005
|
}
|
|
1998
2006
|
//#endregion
|
|
1999
|
-
//#region
|
|
2007
|
+
//#region src/shape-inspector.ts
|
|
2000
2008
|
function selectedShape(document, selection, pageIndex) {
|
|
2001
2009
|
if (!document || !selection) return void 0;
|
|
2002
2010
|
const stack = [...(selection.pageId ? document.pages.find((page) => page.id === selection.pageId) : document.pages[pageIndex])?.shapes ?? []];
|
|
@@ -2094,7 +2102,7 @@ function shapeDetails(shape) {
|
|
|
2094
2102
|
return fragment;
|
|
2095
2103
|
}
|
|
2096
2104
|
//#endregion
|
|
2097
|
-
//#region
|
|
2105
|
+
//#region src/diagnostics.ts
|
|
2098
2106
|
/** Presentation-only grouping keeps repeated per-shape diagnostics readable without losing counts. */
|
|
2099
2107
|
function compatibilityNotes(diagnostics, renderWarnings = []) {
|
|
2100
2108
|
const notes = /* @__PURE__ */ new Map();
|
|
@@ -2126,7 +2134,7 @@ function compatibilityText(note) {
|
|
|
2126
2134
|
return note.message + (note.count > 1 ? ` (${note.count} occurrences)` : "");
|
|
2127
2135
|
}
|
|
2128
2136
|
//#endregion
|
|
2129
|
-
//#region
|
|
2137
|
+
//#region src/viewer-input.ts
|
|
2130
2138
|
var targetShape = (event) => event.target?.closest?.("[data-shape-id]");
|
|
2131
2139
|
function selection(target) {
|
|
2132
2140
|
return target ? {
|
|
@@ -2137,7 +2145,7 @@ function selection(target) {
|
|
|
2137
2145
|
}
|
|
2138
2146
|
/** Own every DOM listener and abort them together when the surface is disposed. */
|
|
2139
2147
|
function wireViewerInputs(controls, controller, fit) {
|
|
2140
|
-
const { viewport, commandRoot,
|
|
2148
|
+
const { viewport, commandRoot, zoomSlider, searchInput } = controls;
|
|
2141
2149
|
const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
2142
2150
|
const options = { signal: events.signal };
|
|
2143
2151
|
searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
|
|
@@ -2153,13 +2161,9 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2153
2161
|
controller.setSearchQuery("");
|
|
2154
2162
|
}
|
|
2155
2163
|
}, options);
|
|
2156
|
-
|
|
2164
|
+
zoomSlider.addEventListener("input", () => controller.setZoom(zoomSlider.value / 100), options);
|
|
2157
2165
|
commandRoot.addEventListener("click", (event) => {
|
|
2158
2166
|
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
2167
|
if (action === "search-next") controller.nextSearchResult();
|
|
2164
2168
|
if (action === "search-previous") controller.previousSearchResult();
|
|
2165
2169
|
}, options);
|
|
@@ -2198,13 +2202,13 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2198
2202
|
}
|
|
2199
2203
|
if (event.key === "0") {
|
|
2200
2204
|
event.preventDefault();
|
|
2201
|
-
fit();
|
|
2205
|
+
fit("page");
|
|
2202
2206
|
}
|
|
2203
2207
|
}, options);
|
|
2204
2208
|
return () => events.abort();
|
|
2205
2209
|
}
|
|
2206
2210
|
//#endregion
|
|
2207
|
-
//#region
|
|
2211
|
+
//#region src/viewer-layer-controls.ts
|
|
2208
2212
|
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
2213
|
/** Static controls and text-only labels. Document strings never become selectors or markup. */
|
|
2210
2214
|
function renderLayerControls(panel, state) {
|
|
@@ -2282,7 +2286,7 @@ function wireLayerControls(panel, controller) {
|
|
|
2282
2286
|
return () => events.abort();
|
|
2283
2287
|
}
|
|
2284
2288
|
//#endregion
|
|
2285
|
-
//#region
|
|
2289
|
+
//#region src/styles.ts
|
|
2286
2290
|
/** Inherited public tokens use local fallback aliases so host themes are never shadowed. */
|
|
2287
2291
|
var viewerStyles = `
|
|
2288
2292
|
:host {
|
|
@@ -2302,13 +2306,7 @@ button:disabled { opacity:.4; cursor:default; }
|
|
|
2302
2306
|
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
2307
|
button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
|
|
2304
2308
|
.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
2309
|
.spacer { flex:1; }
|
|
2311
|
-
.local-label { color:var(--_vv-muted); font-size:10px; white-space:nowrap; }
|
|
2312
2310
|
.ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
|
|
2313
2311
|
.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
2312
|
.ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
|
|
@@ -2318,15 +2316,6 @@ button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-acce
|
|
|
2318
2316
|
.group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
|
|
2319
2317
|
.ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
|
|
2320
2318
|
.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
2319
|
.ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
|
|
2331
2320
|
.search-group { width:330px; }
|
|
2332
2321
|
.search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
|
|
@@ -2398,11 +2387,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2398
2387
|
.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
2388
|
.status-message { display:flex; gap:16px; min-width:0; }
|
|
2400
2389
|
.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
2390
|
.empty { max-width:360px; margin:60px auto; padding:16px; text-align:center; color:var(--_vv-muted); font-size:12px; }
|
|
2407
2391
|
.empty strong { display:block; color:var(--_vv-ink); font-size:18px; font-weight:500; margin-bottom:8px; }
|
|
2408
2392
|
@media(max-width:1100px) { .ribbon-hint { display:none; } [data-diagnostics] { display:none; } }
|
|
@@ -2410,11 +2394,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2410
2394
|
button,select,input,textarea { min-height:44px; }
|
|
2411
2395
|
button { min-width:44px; }
|
|
2412
2396
|
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
2397
|
.ribbon-tabs { height:44px; }
|
|
2419
2398
|
.ribbon-tabs button { min-height:44px; padding:8px 16px; }
|
|
2420
2399
|
.ribbon-content { min-height:86px; }
|
|
@@ -2426,7 +2405,7 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2426
2405
|
.search-controls button { min-width:44px; font-size:0; padding:3px; }
|
|
2427
2406
|
.search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
|
|
2428
2407
|
.search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
|
|
2429
|
-
#home-panel>.ribbon-group:not(.search-group) { display:none; }
|
|
2408
|
+
#home-panel>.ribbon-group:not(.search-group):not(.undo-group) { display:none; }
|
|
2430
2409
|
.ribbon-command { min-width:90px; }
|
|
2431
2410
|
.workspace { display:block; overflow:auto; }
|
|
2432
2411
|
.page-rail { width:100%; max-height:190px; border-right:0; border-bottom:1px solid var(--_vv-border); }
|
|
@@ -2443,30 +2422,414 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2443
2422
|
.notes-strip button { min-height:44px; }
|
|
2444
2423
|
.status { flex-wrap:wrap; gap:0; padding:4px 8px; }
|
|
2445
2424
|
.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
2425
|
}
|
|
2451
2426
|
@media(pointer:coarse) {
|
|
2452
2427
|
button,select,input,textarea { min-height:44px; }
|
|
2453
2428
|
button { min-width:44px; }
|
|
2454
2429
|
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
2430
|
.ribbon-tabs { min-height:44px; height:44px; }
|
|
2459
2431
|
.ribbon-tabs button { min-height:44px; }
|
|
2460
2432
|
.ribbon-content { min-height:96px; }
|
|
2461
2433
|
.search-group { width:380px; max-width:100%; }
|
|
2462
|
-
.
|
|
2463
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; }
|
|
2434
|
+
.notes-strip button { min-height:44px; height:44px; }
|
|
2464
2435
|
.layer-controls input { min-height:20px; }
|
|
2465
2436
|
}
|
|
2437
|
+
/* Suite ribbon: tabs lead directly into Visio command groups. */
|
|
2438
|
+
.ribbon-tabs { height:40px; }
|
|
2439
|
+
.ribbon-tabs button { min-height:39px; padding:8px 16px; }
|
|
2440
|
+
.ribbon-content { min-height:104px; padding:4px 4px; }
|
|
2441
|
+
.search-group { order:2; width:320px; }
|
|
2442
|
+
.ribbon-hint { order:3; padding:10px 18px; }
|
|
2443
|
+
.ribbon-command { min-width:82px; }
|
|
2444
|
+
.pane-close { display:none; }
|
|
2445
|
+
@media(max-width:760px) {
|
|
2446
|
+
.ribbon-tabs { height:44px; }
|
|
2447
|
+
.ribbon-tabs button { min-height:44px; }
|
|
2448
|
+
.ribbon-content { min-height:54px; padding:4px 0; }
|
|
2449
|
+
.search-group { flex:0 0 100%; width:100%; min-height:54px; }
|
|
2450
|
+
.search-feedback,.group-label { display:none; }
|
|
2451
|
+
.ribbon-content { overflow-x:auto; scroll-snap-type:x mandatory; }
|
|
2452
|
+
.workspace { display:flex; position:relative; overflow:hidden; }
|
|
2453
|
+
.viewport { flex:1; width:100%; height:100%; min-height:0; padding:16px 4px; }
|
|
2454
|
+
.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; }
|
|
2455
|
+
.pane-close { display:block; width:44px; height:44px; padding:0; font-size:22px; background:transparent; border:0; }
|
|
2456
|
+
.inspector-kind { margin-left:auto; }
|
|
2457
|
+
.page-rail { position:absolute; inset:0 auto 0 0; width:180px; max-height:none; z-index:3; border-right:1px solid var(--_vv-border); background:var(--_vv-secondary); }
|
|
2458
|
+
.page-list { flex-direction:column; }
|
|
2459
|
+
.page-list li { flex:none; }
|
|
2460
|
+
.notes-strip { display:none; }
|
|
2461
|
+
.status { min-height:48px; padding:2px 8px; }
|
|
2462
|
+
.status-message { display:none; }
|
|
2463
|
+
}
|
|
2464
|
+
.toolbar { position:relative; }
|
|
2465
|
+
.ribbon-tools { display:flex; flex:none; order:0; }
|
|
2466
|
+
.tools-content { display:flex; align-items:stretch; }
|
|
2467
|
+
.ribbon-tools>summary { display:none; }
|
|
2468
|
+
.ribbon-command { font-size:12px; min-width:88px; }
|
|
2469
|
+
.ribbon-hint { font-size:12px; line-height:20px; flex:1; }
|
|
2470
|
+
.group-label { font-size:11px; line-height:14px; }
|
|
2471
|
+
.inspector-card,.edit-controls,.layer-controls { font-size:12px; line-height:18px; }
|
|
2472
|
+
.selection-hint,.document-card dl { font-size:12px; }
|
|
2473
|
+
.pane-heading { font-size:12px; }
|
|
2474
|
+
.inspector-pane .pane-heading { position:sticky; top:0; z-index:1; background:var(--_vv-secondary); flex:none; }
|
|
2475
|
+
.status .notes-strip { border:0; padding:0 8px; background:transparent; min-height:28px; }
|
|
2476
|
+
.status .notes-strip>span { display:none; }
|
|
2477
|
+
.status-message { flex:1; }
|
|
2478
|
+
@media(max-width:760px) {
|
|
2479
|
+
#home-panel { overflow:visible; min-height:54px; padding:4px; }
|
|
2480
|
+
.ribbon-tools { display:block; flex:0 0 84px; width:84px; order:1; }
|
|
2481
|
+
.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; }
|
|
2482
|
+
.ribbon-tools>summary::-webkit-details-marker { display:none; }
|
|
2483
|
+
.ribbon-tools .tools-content { display:flex; position:absolute; top:100%; left:4px; right:4px; z-index:6; flex-direction:column; padding:12px; gap:12px; border:1px solid var(--_vv-border); border-radius:4px; background:var(--_vv-surface); box-shadow:0 12px 28px #0003; max-height:60vh; overflow:auto; }
|
|
2484
|
+
.ribbon-tools:not([open])>.tools-content { display:none; }
|
|
2485
|
+
.ribbon-tools .ribbon-group { display:flex; width:100%; min-height:64px; border:0; padding:0; }
|
|
2486
|
+
.ribbon-tools .ribbon-actions { justify-content:space-between; }
|
|
2487
|
+
.ribbon-tools .ribbon-command { min-width:88px; font-size:12px; }
|
|
2488
|
+
.ribbon-tools .search-group { order:1; flex:auto; }
|
|
2489
|
+
.ribbon-tools .search-feedback { display:block; }
|
|
2490
|
+
.status { flex-wrap:nowrap; gap:4px; min-height:48px; }
|
|
2491
|
+
.status .notes-strip { display:none; }
|
|
2492
|
+
}
|
|
2493
|
+
@media(pointer:coarse) { .ribbon-tabs {height:45px;min-height:45px;} .ribbon-tabs button {min-height:44px;height:44px;} }
|
|
2466
2494
|
@media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
|
|
2467
2495
|
`;
|
|
2468
2496
|
//#endregion
|
|
2469
|
-
//#region
|
|
2497
|
+
//#region src/styles/index.ts
|
|
2498
|
+
/**
|
|
2499
|
+
* Viewer stylesheets as text for the shadow root. New styles are CSS files here; the legacy
|
|
2500
|
+
* `styles.ts` string moves into this directory when it is next changed.
|
|
2501
|
+
*/
|
|
2502
|
+
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");
|
|
2503
|
+
//#endregion
|
|
2504
|
+
//#region src/ribbon-action.ts
|
|
2505
|
+
/** Event name shared by every ribbon control; detail is the typed action. */
|
|
2506
|
+
var RIBBON_ACTION_EVENT = "ribbon-action";
|
|
2507
|
+
/** Internal, non-composed event: it stays inside the viewer's shadow tree. */
|
|
2508
|
+
function emitRibbonAction(target, action) {
|
|
2509
|
+
target.dispatchEvent(new CustomEvent(RIBBON_ACTION_EVENT, {
|
|
2510
|
+
detail: action,
|
|
2511
|
+
bubbles: true
|
|
2512
|
+
}));
|
|
2513
|
+
}
|
|
2514
|
+
//#endregion
|
|
2515
|
+
//#region src/ribbon-parts.ts
|
|
2516
|
+
/**
|
|
2517
|
+
* A large stacked ribbon command. `id` is a stable name for state sync and tests; the typed
|
|
2518
|
+
* `action` is what runs. Labels, icons and shortcuts are static application strings.
|
|
2519
|
+
*/
|
|
2520
|
+
function tool(doc, id, label, icon, action, options = {}) {
|
|
2521
|
+
const el = doc.createElement("office-ui-button");
|
|
2522
|
+
el.setAttribute("variant", "stacked");
|
|
2523
|
+
el.setAttribute("command", id);
|
|
2524
|
+
el.setAttribute("icon", icon);
|
|
2525
|
+
el.setAttribute("label", label);
|
|
2526
|
+
el.setAttribute("title", options.keys ? `${label} (${options.keys[1]})` : label);
|
|
2527
|
+
if (options.keys) el.setAttribute("keyshortcuts", options.keys[0]);
|
|
2528
|
+
if (options.pressed !== void 0) el.setAttribute("pressed", String(options.pressed));
|
|
2529
|
+
if (options.controls) el.setAttribute("aria-controls", options.controls);
|
|
2530
|
+
el.addEventListener("office-command", (event) => {
|
|
2531
|
+
event.stopPropagation();
|
|
2532
|
+
emitRibbonAction(el, action);
|
|
2533
|
+
});
|
|
2534
|
+
return el;
|
|
2535
|
+
}
|
|
2536
|
+
/** A labelled ribbon group (the shared `office-ui-ribbon-group`). */
|
|
2537
|
+
function group(doc, label, children, className = "") {
|
|
2538
|
+
const el = doc.createElement("office-ui-ribbon-group");
|
|
2539
|
+
el.setAttribute("label", label);
|
|
2540
|
+
if (className) el.className = className;
|
|
2541
|
+
el.append(...children);
|
|
2542
|
+
return el;
|
|
2543
|
+
}
|
|
2544
|
+
/** A keyboard-navigable row of groups (the shared `office-ui-toolbar`). */
|
|
2545
|
+
function commandRow(doc, label, children) {
|
|
2546
|
+
const el = doc.createElement("office-ui-toolbar");
|
|
2547
|
+
el.className = "ribbon-row";
|
|
2548
|
+
el.setAttribute("aria-label", label);
|
|
2549
|
+
el.append(...children);
|
|
2550
|
+
return el;
|
|
2551
|
+
}
|
|
2552
|
+
/** Muted two-line guidance at the end of a ribbon panel. */
|
|
2553
|
+
function hint(doc, lines) {
|
|
2554
|
+
const el = doc.createElement("p");
|
|
2555
|
+
el.className = "ribbon-hint";
|
|
2556
|
+
lines.forEach((line, index) => {
|
|
2557
|
+
if (index) el.append(doc.createElement("br"));
|
|
2558
|
+
el.append(line);
|
|
2559
|
+
});
|
|
2560
|
+
return el;
|
|
2561
|
+
}
|
|
2562
|
+
//#endregion
|
|
2563
|
+
//#region src/viewer-search.ts
|
|
2564
|
+
/** Builds the Find group: a search field, previous/next result buttons and a live status. */
|
|
2565
|
+
function createSearchGroup(doc) {
|
|
2566
|
+
const group = doc.createElement("div");
|
|
2567
|
+
group.className = "ribbon-group search-group";
|
|
2568
|
+
group.setAttribute("role", "group");
|
|
2569
|
+
group.setAttribute("aria-label", "Find");
|
|
2570
|
+
const controls = doc.createElement("div");
|
|
2571
|
+
controls.className = "search-controls";
|
|
2572
|
+
controls.setAttribute("role", "search");
|
|
2573
|
+
controls.setAttribute("aria-label", "Diagram text search");
|
|
2574
|
+
const label = doc.createElement("label");
|
|
2575
|
+
label.append("Find ");
|
|
2576
|
+
const input = doc.createElement("input");
|
|
2577
|
+
input.type = "search";
|
|
2578
|
+
input.setAttribute("aria-label", "Search diagram text");
|
|
2579
|
+
input.placeholder = "Text across pages";
|
|
2580
|
+
input.maxLength = TEXT_SEARCH_LIMITS.queryCharacters;
|
|
2581
|
+
input.autocomplete = "off";
|
|
2582
|
+
input.spellcheck = false;
|
|
2583
|
+
input.setAttribute("aria-describedby", "search-status");
|
|
2584
|
+
label.append(input);
|
|
2585
|
+
const step = (action, name, text) => {
|
|
2586
|
+
const button = doc.createElement("button");
|
|
2587
|
+
button.type = "button";
|
|
2588
|
+
button.dataset.action = action;
|
|
2589
|
+
button.setAttribute("aria-label", name);
|
|
2590
|
+
button.textContent = text;
|
|
2591
|
+
return button;
|
|
2592
|
+
};
|
|
2593
|
+
const status = doc.createElement("span");
|
|
2594
|
+
status.id = "search-status";
|
|
2595
|
+
status.setAttribute("role", "status");
|
|
2596
|
+
status.setAttribute("aria-live", "polite");
|
|
2597
|
+
status.setAttribute("aria-atomic", "true");
|
|
2598
|
+
controls.append(label, step("search-previous", "Previous matching shape", "Previous"), step("search-next", "Next matching shape", "Next"), status);
|
|
2599
|
+
const caption = doc.createElement("span");
|
|
2600
|
+
caption.className = "group-label";
|
|
2601
|
+
caption.textContent = "Find in drawing";
|
|
2602
|
+
group.append(controls, caption);
|
|
2603
|
+
return group;
|
|
2604
|
+
}
|
|
2605
|
+
function searchControls(root) {
|
|
2606
|
+
return {
|
|
2607
|
+
input: root.querySelector("input[type=\"search\"]"),
|
|
2608
|
+
status: root.querySelector("#search-status"),
|
|
2609
|
+
previous: root.querySelector("[data-action=\"search-previous\"]"),
|
|
2610
|
+
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
function renderSearchControls(controls, state) {
|
|
2614
|
+
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2615
|
+
controls.input.disabled = !state.document?.pages.length;
|
|
2616
|
+
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2617
|
+
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2618
|
+
const result = state.search.results[state.search.activeIndex];
|
|
2619
|
+
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2620
|
+
}
|
|
2621
|
+
//#endregion
|
|
2622
|
+
//#region src/ribbon-home.ts
|
|
2623
|
+
/**
|
|
2624
|
+
* Visio's Home tab, limited to commands this build can honestly perform: Undo, Tools and
|
|
2625
|
+
* Editing. Clipboard, Font, Paragraph, Shape Styles and Arrange wait for core edit commands.
|
|
2626
|
+
* The compact (phone) layout folds every group but Undo into the Tools disclosure.
|
|
2627
|
+
*/
|
|
2628
|
+
function buildHomePanel(doc, panel) {
|
|
2629
|
+
const tools = doc.createElement("details");
|
|
2630
|
+
tools.className = "ribbon-tools";
|
|
2631
|
+
tools.open = true;
|
|
2632
|
+
const summary = doc.createElement("summary");
|
|
2633
|
+
const caret = doc.createElement("span");
|
|
2634
|
+
caret.setAttribute("aria-hidden", "true");
|
|
2635
|
+
caret.textContent = "⌄";
|
|
2636
|
+
summary.append("Tools ", caret);
|
|
2637
|
+
const content = doc.createElement("div");
|
|
2638
|
+
content.className = "tools-content";
|
|
2639
|
+
content.append(group(doc, "Tools", [
|
|
2640
|
+
tool(doc, "pointer", "Pointer Tool", "pointer", {
|
|
2641
|
+
type: "tool",
|
|
2642
|
+
tool: "pointer"
|
|
2643
|
+
}, {
|
|
2644
|
+
keys: ["Control+1", "Ctrl+1"],
|
|
2645
|
+
pressed: true
|
|
2646
|
+
}),
|
|
2647
|
+
tool(doc, "rectangle", "Rectangle", "rectangle", {
|
|
2648
|
+
type: "tool",
|
|
2649
|
+
tool: "rectangle"
|
|
2650
|
+
}, {
|
|
2651
|
+
keys: ["Control+8", "Ctrl+8"],
|
|
2652
|
+
pressed: false
|
|
2653
|
+
}),
|
|
2654
|
+
tool(doc, "edit", "Edit text", "pencil", {
|
|
2655
|
+
type: "reveal",
|
|
2656
|
+
panel: "edit"
|
|
2657
|
+
}, { keys: ["F2", "F2"] })
|
|
2658
|
+
]), createSearchGroup(doc), group(doc, "Editing", [
|
|
2659
|
+
tool(doc, "delete", "Delete", "trash", { type: "delete" }, { keys: ["Delete", "Del"] }),
|
|
2660
|
+
tool(doc, "layers", "Layers", "visioLayers", {
|
|
2661
|
+
type: "reveal",
|
|
2662
|
+
panel: "layers"
|
|
2663
|
+
}),
|
|
2664
|
+
tool(doc, "selection", "Shape details", "visioShapeData", {
|
|
2665
|
+
type: "reveal",
|
|
2666
|
+
panel: "selection"
|
|
2667
|
+
})
|
|
2668
|
+
]));
|
|
2669
|
+
tools.append(summary, content);
|
|
2670
|
+
panel.append(commandRow(doc, "Home commands", [group(doc, "Undo", [tool(doc, "undo", "Undo", "undo", {
|
|
2671
|
+
type: "history",
|
|
2672
|
+
key: "undo"
|
|
2673
|
+
}, { keys: ["Control+Z", "Ctrl+Z"] }), tool(doc, "redo", "Redo", "redo", {
|
|
2674
|
+
type: "history",
|
|
2675
|
+
key: "redo"
|
|
2676
|
+
}, { keys: ["Control+Y", "Ctrl+Y"] })], "undo-group"), tools]), hint(doc, ["Rectangle: drag on the page to draw.", "Editing is experimental. Keep your original file."]));
|
|
2677
|
+
}
|
|
2678
|
+
//#endregion
|
|
2679
|
+
//#region src/ribbon-view.ts
|
|
2680
|
+
/** Visio's View tab: Views, Show (grid and task panes) and Zoom. */
|
|
2681
|
+
function buildViewPanel(doc, panel) {
|
|
2682
|
+
panel.append(commandRow(doc, "View commands", [
|
|
2683
|
+
group(doc, "Views", [tool(doc, "fullscreen", "Full Screen", "fullscreen", { type: "fullscreen" }, {
|
|
2684
|
+
keys: ["F5", "F5"],
|
|
2685
|
+
pressed: false
|
|
2686
|
+
})]),
|
|
2687
|
+
group(doc, "Show", [
|
|
2688
|
+
tool(doc, "grid", "Grid", "grid", { type: "grid" }, { pressed: false }),
|
|
2689
|
+
tool(doc, "pages", "Pages pane", "visioPagesPane", {
|
|
2690
|
+
type: "pane",
|
|
2691
|
+
pane: "pages"
|
|
2692
|
+
}, {
|
|
2693
|
+
pressed: true,
|
|
2694
|
+
controls: "page-rail"
|
|
2695
|
+
}),
|
|
2696
|
+
tool(doc, "inspector", "Inspector pane", "visioInspectorPane", {
|
|
2697
|
+
type: "pane",
|
|
2698
|
+
pane: "inspector"
|
|
2699
|
+
}, {
|
|
2700
|
+
pressed: true,
|
|
2701
|
+
controls: "inspector-pane"
|
|
2702
|
+
}),
|
|
2703
|
+
tool(doc, "notes", "Review notes", "message", {
|
|
2704
|
+
type: "reveal",
|
|
2705
|
+
panel: "notes"
|
|
2706
|
+
})
|
|
2707
|
+
]),
|
|
2708
|
+
group(doc, "Zoom", [
|
|
2709
|
+
tool(doc, "zoom-fit", "Fit to Window", "fitPage", {
|
|
2710
|
+
type: "zoom",
|
|
2711
|
+
mode: "fit"
|
|
2712
|
+
}, { keys: ["Control+Shift+W", "Ctrl+Shift+W"] }),
|
|
2713
|
+
tool(doc, "page-width", "Page Width", "pageWidth", {
|
|
2714
|
+
type: "zoom",
|
|
2715
|
+
mode: "width"
|
|
2716
|
+
}),
|
|
2717
|
+
tool(doc, "actual-size", "100%", "search", {
|
|
2718
|
+
type: "zoom",
|
|
2719
|
+
mode: "actual"
|
|
2720
|
+
})
|
|
2721
|
+
])
|
|
2722
|
+
]), hint(doc, ["Ctrl+wheel or + / − to zoom.", "Ctrl+Page Up / Page Down changes page."]));
|
|
2723
|
+
}
|
|
2724
|
+
//#endregion
|
|
2725
|
+
//#region src/ribbon.ts
|
|
2726
|
+
var TABS = [[
|
|
2727
|
+
"home",
|
|
2728
|
+
"Home",
|
|
2729
|
+
buildHomePanel
|
|
2730
|
+
], [
|
|
2731
|
+
"view",
|
|
2732
|
+
"View",
|
|
2733
|
+
buildViewPanel
|
|
2734
|
+
]];
|
|
2735
|
+
/**
|
|
2736
|
+
* The Visio ribbon: a tab list and one panel per tab. Tab switching and keyboard movement
|
|
2737
|
+
* belong to the chrome; commands emit `ribbon-action` events for the router.
|
|
2738
|
+
*/
|
|
2739
|
+
function createRibbon(doc) {
|
|
2740
|
+
const toolbar = doc.createElement("div");
|
|
2741
|
+
toolbar.className = "toolbar";
|
|
2742
|
+
toolbar.setAttribute("role", "group");
|
|
2743
|
+
toolbar.setAttribute("aria-label", "Diagram controls");
|
|
2744
|
+
const tabs = doc.createElement("div");
|
|
2745
|
+
tabs.className = "ribbon-tabs";
|
|
2746
|
+
tabs.setAttribute("role", "tablist");
|
|
2747
|
+
tabs.setAttribute("aria-label", "Ribbon");
|
|
2748
|
+
toolbar.append(tabs);
|
|
2749
|
+
for (const [key, name, build] of TABS) {
|
|
2750
|
+
const selected = key === "home";
|
|
2751
|
+
const tab = doc.createElement("button");
|
|
2752
|
+
tab.type = "button";
|
|
2753
|
+
tab.id = `${key}-tab`;
|
|
2754
|
+
tab.dataset.tab = key;
|
|
2755
|
+
tab.textContent = name;
|
|
2756
|
+
tab.setAttribute("role", "tab");
|
|
2757
|
+
tab.setAttribute("aria-selected", String(selected));
|
|
2758
|
+
tab.setAttribute("aria-controls", `${key}-panel`);
|
|
2759
|
+
if (!selected) tab.tabIndex = -1;
|
|
2760
|
+
const panel = doc.createElement("div");
|
|
2761
|
+
panel.className = "ribbon-content";
|
|
2762
|
+
panel.id = `${key}-panel`;
|
|
2763
|
+
panel.setAttribute("role", "tabpanel");
|
|
2764
|
+
panel.setAttribute("aria-labelledby", tab.id);
|
|
2765
|
+
panel.hidden = !selected;
|
|
2766
|
+
build(doc, panel);
|
|
2767
|
+
tabs.append(tab);
|
|
2768
|
+
toolbar.append(panel);
|
|
2769
|
+
}
|
|
2770
|
+
return toolbar;
|
|
2771
|
+
}
|
|
2772
|
+
//#endregion
|
|
2773
|
+
//#region src/status-bar.ts
|
|
2774
|
+
/**
|
|
2775
|
+
* Visio's status bar on the shared `office-ui-status-bar`: page and shape metrics, a live
|
|
2776
|
+
* message region, compatibility notes, the host footer slot and the shared zoom slider.
|
|
2777
|
+
*/
|
|
2778
|
+
function createStatusBar(doc) {
|
|
2779
|
+
const bar = doc.createElement("office-ui-status-bar");
|
|
2780
|
+
bar.className = "status";
|
|
2781
|
+
bar.setAttribute("label", "Status bar");
|
|
2782
|
+
const item = (name) => {
|
|
2783
|
+
const el = doc.createElement("office-ui-status-item");
|
|
2784
|
+
el.setAttribute(name, "");
|
|
2785
|
+
el.setAttribute("label", "");
|
|
2786
|
+
el.setAttribute("value", "");
|
|
2787
|
+
return el;
|
|
2788
|
+
};
|
|
2789
|
+
const message = doc.createElement("div");
|
|
2790
|
+
message.className = "status-message";
|
|
2791
|
+
message.setAttribute("role", "status");
|
|
2792
|
+
const text = doc.createElement("span");
|
|
2793
|
+
text.dataset.status = "";
|
|
2794
|
+
const diagnostics = doc.createElement("span");
|
|
2795
|
+
diagnostics.dataset.diagnostics = "";
|
|
2796
|
+
message.append(text, diagnostics);
|
|
2797
|
+
const notes = doc.createElement("div");
|
|
2798
|
+
notes.className = "notes-strip";
|
|
2799
|
+
const notesButton = doc.createElement("button");
|
|
2800
|
+
notesButton.type = "button";
|
|
2801
|
+
notesButton.dataset.chrome = "notes";
|
|
2802
|
+
notesButton.setAttribute("aria-controls", "inspector-pane");
|
|
2803
|
+
notesButton.setAttribute("aria-expanded", "false");
|
|
2804
|
+
const count = doc.createElement("span");
|
|
2805
|
+
count.className = "notes-count";
|
|
2806
|
+
count.dataset.noteCount = "";
|
|
2807
|
+
notesButton.append("Notes", count);
|
|
2808
|
+
const local = doc.createElement("span");
|
|
2809
|
+
local.textContent = "Files stay in your browser";
|
|
2810
|
+
notes.append(notesButton, local);
|
|
2811
|
+
const footer = doc.createElement("slot");
|
|
2812
|
+
footer.name = "workspace-footer";
|
|
2813
|
+
const zoom = doc.createElement("office-ui-zoom-slider");
|
|
2814
|
+
zoom.slot = "end";
|
|
2815
|
+
zoom.className = "zoom-controls";
|
|
2816
|
+
zoom.setAttribute("label", "Canvas zoom");
|
|
2817
|
+
zoom.setAttribute("fit", "Fit page to current window");
|
|
2818
|
+
zoom.setAttribute("value", "100");
|
|
2819
|
+
bar.append(item("data-page-status"), item("data-shape-status"), message, notes, footer, zoom);
|
|
2820
|
+
return bar;
|
|
2821
|
+
}
|
|
2822
|
+
/** Visio's page tabs under the drawing, on the shared document tab strip. */
|
|
2823
|
+
function createPageTabs(doc) {
|
|
2824
|
+
const strip = doc.createElement("office-ui-tab-strip");
|
|
2825
|
+
strip.className = "page-tabs";
|
|
2826
|
+
strip.setAttribute("label", "Pages");
|
|
2827
|
+
strip.setAttribute("previous-label", "Previous page");
|
|
2828
|
+
strip.setAttribute("next-label", "Next page");
|
|
2829
|
+
return strip;
|
|
2830
|
+
}
|
|
2831
|
+
//#endregion
|
|
2832
|
+
//#region src/edit-error.ts
|
|
2470
2833
|
/** Core error codes remain visible for a refused transaction; document content is plain text. */
|
|
2471
2834
|
function editErrorMessage(error) {
|
|
2472
2835
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2478,10 +2841,13 @@ function isEditCancellation(error) {
|
|
|
2478
2841
|
return !!error && typeof error === "object" && "name" in error && error.name === "AbortError";
|
|
2479
2842
|
}
|
|
2480
2843
|
//#endregion
|
|
2481
|
-
//#region
|
|
2844
|
+
//#region src/viewer-geometry-controls.ts
|
|
2482
2845
|
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>`;
|
|
2483
2846
|
/** Collect explicit coordinates; never infer a Visio pin from rendered SVG transforms. */
|
|
2484
2847
|
var ViewerGeometryControls = class {
|
|
2848
|
+
panel;
|
|
2849
|
+
controller;
|
|
2850
|
+
draftChanged;
|
|
2485
2851
|
#fields;
|
|
2486
2852
|
#buttons;
|
|
2487
2853
|
#error;
|
|
@@ -2602,10 +2968,12 @@ var ViewerGeometryControls = class {
|
|
|
2602
2968
|
}
|
|
2603
2969
|
};
|
|
2604
2970
|
//#endregion
|
|
2605
|
-
//#region
|
|
2971
|
+
//#region src/viewer-edit-controls.ts
|
|
2606
2972
|
/** Static markup only. Document text is assigned exclusively through value/textContent. */
|
|
2607
2973
|
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>`;
|
|
2608
2974
|
var ViewerEditControls = class {
|
|
2975
|
+
panel;
|
|
2976
|
+
input;
|
|
2609
2977
|
#target;
|
|
2610
2978
|
#status;
|
|
2611
2979
|
#error;
|
|
@@ -2748,61 +3116,17 @@ var ViewerEditControls = class {
|
|
|
2748
3116
|
}
|
|
2749
3117
|
};
|
|
2750
3118
|
//#endregion
|
|
2751
|
-
//#region
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2759
|
-
};
|
|
2760
|
-
}
|
|
2761
|
-
function renderSearchControls(controls, state) {
|
|
2762
|
-
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2763
|
-
controls.input.disabled = !state.document?.pages.length;
|
|
2764
|
-
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2765
|
-
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2766
|
-
const result = state.search.results[state.search.activeIndex];
|
|
2767
|
-
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2768
|
-
}
|
|
2769
|
-
//#endregion
|
|
2770
|
-
//#region .package-build/output/src/viewer-chrome.js
|
|
2771
|
-
/** Only static application markup belongs here. All document labels are inserted as text. */
|
|
2772
|
-
var viewerChromeTemplate = `
|
|
2773
|
-
<div class="toolbar" role="group" aria-label="Diagram controls">
|
|
2774
|
-
<div class="ribbon-primary">
|
|
2775
|
-
<label class="page-picker">Page <select aria-label="Page"></select></label>
|
|
2776
|
-
<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>
|
|
2777
|
-
<span class="spacer"></span><span class="local-label">Local workspace</span>
|
|
2778
|
-
</div>
|
|
2779
|
-
<div class="ribbon-tabs" role="tablist" aria-label="Ribbon">
|
|
2780
|
-
<button type="button" id="home-tab" role="tab" aria-selected="true" aria-controls="home-panel" data-tab="home">Home</button>
|
|
2781
|
-
<button type="button" id="view-tab" role="tab" aria-selected="false" aria-controls="view-panel" tabindex="-1" data-tab="view">View</button>
|
|
2782
|
-
</div>
|
|
2783
|
-
<div class="ribbon-content" id="home-panel" role="tabpanel" aria-labelledby="home-tab">
|
|
2784
|
-
<div class="ribbon-group search-group">${searchTemplate}<span class="group-label">Find in drawing</span></div>
|
|
2785
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2786
|
-
<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>
|
|
2787
|
-
<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>
|
|
2788
|
-
<button class="ribbon-command" type="button" data-chrome="layers"><span class="command-icon" data-icon="layers" aria-hidden="true"></span><span>Layers</span></button>
|
|
2789
|
-
</div><span class="group-label">Inspect and edit</span></div>
|
|
2790
|
-
<p class="ribbon-hint">Select a shape to inspect its details.<br>Text editing is experimental.</p>
|
|
2791
|
-
</div>
|
|
2792
|
-
<div class="ribbon-content" id="view-panel" role="tabpanel" aria-labelledby="view-tab" hidden>
|
|
2793
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2794
|
-
<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>
|
|
2795
|
-
<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>
|
|
2796
|
-
<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>
|
|
2797
|
-
</div><span class="group-label">Workspace panes</span></div>
|
|
2798
|
-
<p class="ribbon-hint">Canvas shortcuts: + / − to zoom, 0 to fit.<br>Arrow keys move between focused shapes.</p>
|
|
2799
|
-
</div>
|
|
2800
|
-
</div>
|
|
2801
|
-
<div class="workspace">
|
|
3119
|
+
//#region src/viewer-chrome.ts
|
|
3120
|
+
/**
|
|
3121
|
+
* Static workspace markup (legacy; migrate to builders when next changed). The ribbon, page
|
|
3122
|
+
* tabs and status bar are built by `createRibbon`, `createPageTabs` and `createStatusBar`.
|
|
3123
|
+
* All document labels are inserted as text.
|
|
3124
|
+
*/
|
|
3125
|
+
var viewerChromeTemplate = `<div class="workspace">
|
|
2802
3126
|
<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>
|
|
2803
3127
|
<div class="viewport" tabindex="0" role="region" aria-label="Diagram canvas"></div>
|
|
2804
3128
|
<aside id="inspector-pane" class="inspector-pane" aria-label="Drawing inspector">
|
|
2805
|
-
<div class="pane-heading"><span>Inspector</span><span class="inspector-kind">Drawing</span></div>
|
|
3129
|
+
<div class="pane-heading"><span>Inspector</span><span class="inspector-kind">Drawing</span><button class="pane-close" type="button" data-chrome="inspector" aria-label="Close inspector">×</button></div>
|
|
2806
3130
|
<div class="inspector-body">
|
|
2807
3131
|
<section class="inspector-card document-card" aria-label="Current page"><h2>Current page</h2><p class="current-page-name" data-page-name>No diagram open</p><dl><dt>Size</dt><dd data-page-size>No page</dd><dt>Shapes</dt><dd data-page-shapes>No shapes</dd></dl></section>
|
|
2808
3132
|
<p class="selection-hint">Select a shape on the canvas to see its data and links.</p>
|
|
@@ -2811,24 +3135,28 @@ var viewerChromeTemplate = `
|
|
|
2811
3135
|
<details class="notes inspector-card"><summary>Compatibility notes</summary><ul></ul></details>
|
|
2812
3136
|
</div>
|
|
2813
3137
|
</aside>
|
|
2814
|
-
</div
|
|
2815
|
-
<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>
|
|
2816
|
-
<div class="status"><div class="status-message" role="status"><span data-status></span><span data-diagnostics></span></div><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>`;
|
|
3138
|
+
</div>`;
|
|
2817
3139
|
/** Presentation state stays local; page navigation uses the same controller as every binding. */
|
|
2818
3140
|
var ViewerChrome = class {
|
|
2819
3141
|
#root;
|
|
2820
3142
|
#controller;
|
|
2821
3143
|
#document;
|
|
2822
3144
|
#pageList;
|
|
3145
|
+
#pageTabs;
|
|
3146
|
+
#tools;
|
|
2823
3147
|
#rail;
|
|
2824
3148
|
#inspector;
|
|
2825
3149
|
#notes;
|
|
2826
3150
|
#responsive;
|
|
3151
|
+
#compact;
|
|
2827
3152
|
#pagesManuallyToggled = false;
|
|
3153
|
+
#inspectorManuallyToggled = false;
|
|
2828
3154
|
constructor(root, controller) {
|
|
2829
3155
|
this.#root = root;
|
|
2830
3156
|
this.#controller = controller;
|
|
2831
3157
|
this.#pageList = root.querySelector(".page-list");
|
|
3158
|
+
this.#pageTabs = root.querySelector("office-ui-tab-strip");
|
|
3159
|
+
this.#tools = root.querySelector(".ribbon-tools");
|
|
2832
3160
|
this.#rail = root.querySelector(".page-rail");
|
|
2833
3161
|
this.#inspector = root.querySelector(".inspector-pane");
|
|
2834
3162
|
this.#notes = root.querySelector(".notes");
|
|
@@ -2837,26 +3165,45 @@ var ViewerChrome = class {
|
|
|
2837
3165
|
const view = this.#root.ownerDocument.defaultView;
|
|
2838
3166
|
const events = new ((view?.AbortController) ?? AbortController)(), options = { signal: events.signal };
|
|
2839
3167
|
this.#responsive = view?.matchMedia?.("(max-width: 980px)");
|
|
3168
|
+
const compact = view?.matchMedia?.("(max-width: 760px)");
|
|
3169
|
+
this.#compact = compact;
|
|
3170
|
+
const tools = this.#tools;
|
|
3171
|
+
const toolLayout = () => {
|
|
3172
|
+
tools.open = !compact?.matches;
|
|
3173
|
+
};
|
|
3174
|
+
toolLayout();
|
|
3175
|
+
compact?.addEventListener("change", toolLayout, options);
|
|
3176
|
+
this.#root.addEventListener("keydown", (event) => {
|
|
3177
|
+
if (event.key === "Escape" && compact?.matches && tools.open) {
|
|
3178
|
+
event.preventDefault();
|
|
3179
|
+
event.stopPropagation();
|
|
3180
|
+
tools.open = false;
|
|
3181
|
+
tools.querySelector("summary").focus();
|
|
3182
|
+
}
|
|
3183
|
+
}, {
|
|
3184
|
+
...options,
|
|
3185
|
+
capture: true
|
|
3186
|
+
});
|
|
2840
3187
|
const responsive = () => {
|
|
2841
3188
|
if (!this.#pagesManuallyToggled) this.#setPane("pages", !this.#responsive?.matches);
|
|
3189
|
+
if (!this.#inspectorManuallyToggled) this.#setPane("inspector", !compact?.matches);
|
|
2842
3190
|
};
|
|
2843
3191
|
responsive();
|
|
2844
3192
|
this.#responsive?.addEventListener("change", responsive, options);
|
|
3193
|
+
compact?.addEventListener("change", responsive, options);
|
|
2845
3194
|
this.#root.addEventListener("click", (event) => {
|
|
2846
3195
|
const button = event.target?.closest?.("button");
|
|
2847
3196
|
if (!button || button.disabled) return;
|
|
2848
3197
|
const tab = button.dataset.tab;
|
|
2849
|
-
if (tab === "home" || tab === "view") this
|
|
3198
|
+
if (tab === "home" || tab === "view") this.showTab(tab);
|
|
2850
3199
|
if (button.dataset.pageIndex !== void 0) this.#controller.setPage(Number(button.dataset.pageIndex));
|
|
2851
3200
|
const action = button.dataset.chrome;
|
|
2852
|
-
if (action === "
|
|
2853
|
-
if (action === "
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
if (action === "inspector") this.#setPane("inspector", this.#inspector.hidden);
|
|
2859
|
-
if (action === "notes" || action === "selection" || action === "edit" || action === "layers") this.#reveal(action);
|
|
3201
|
+
if (action === "inspector") this.togglePane("inspector");
|
|
3202
|
+
if (action === "notes") this.reveal("notes");
|
|
3203
|
+
}, options);
|
|
3204
|
+
this.#root.addEventListener("office-tab-select", (event) => {
|
|
3205
|
+
const id = Number(event.detail.id);
|
|
3206
|
+
if (Number.isSafeInteger(id)) this.#controller.setPage(id);
|
|
2860
3207
|
}, options);
|
|
2861
3208
|
this.#root.addEventListener("keydown", (event) => {
|
|
2862
3209
|
const key = event;
|
|
@@ -2873,11 +3220,11 @@ var ViewerChrome = class {
|
|
|
2873
3220
|
if (key.key === "ArrowUp" || key.key === "ArrowDown") return;
|
|
2874
3221
|
key.preventDefault();
|
|
2875
3222
|
const tab = key.key === "Home" ? "home" : key.key === "End" ? "view" : button.dataset.tab === "home" ? "view" : "home";
|
|
2876
|
-
this
|
|
3223
|
+
this.showTab(tab);
|
|
2877
3224
|
this.#root.querySelector(`[data-tab="${tab}"]`).focus();
|
|
2878
3225
|
} else if (button.dataset.pageIndex !== void 0) {
|
|
2879
3226
|
key.preventDefault();
|
|
2880
|
-
const pages = [...this.#pageList.querySelectorAll("
|
|
3227
|
+
const pages = [...this.#pageList.querySelectorAll("[data-page-index]")];
|
|
2881
3228
|
const current = Number(button.dataset.pageIndex);
|
|
2882
3229
|
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)));
|
|
2883
3230
|
this.#controller.setPage(next);
|
|
@@ -2887,7 +3234,7 @@ var ViewerChrome = class {
|
|
|
2887
3234
|
this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
|
|
2888
3235
|
return () => events.abort();
|
|
2889
3236
|
}
|
|
2890
|
-
|
|
3237
|
+
showTab(tab) {
|
|
2891
3238
|
for (const button of this.#root.querySelectorAll("[data-tab]")) {
|
|
2892
3239
|
const selected = button.dataset.tab === tab;
|
|
2893
3240
|
button.setAttribute("aria-selected", String(selected));
|
|
@@ -2895,21 +3242,41 @@ var ViewerChrome = class {
|
|
|
2895
3242
|
}
|
|
2896
3243
|
for (const panel of this.#root.querySelectorAll("[role=\"tabpanel\"]")) panel.hidden = panel.id !== `${tab}-panel`;
|
|
2897
3244
|
}
|
|
3245
|
+
/** Show or hide a task pane; compact layouts close the Tools menu afterwards. */
|
|
3246
|
+
togglePane(pane) {
|
|
3247
|
+
if (this.#compact?.matches) this.#tools.open = false;
|
|
3248
|
+
if (pane === "pages") this.#pagesManuallyToggled = true;
|
|
3249
|
+
else this.#inspectorManuallyToggled = true;
|
|
3250
|
+
this.#setPane(pane, (pane === "pages" ? this.#rail : this.#inspector).hidden);
|
|
3251
|
+
}
|
|
3252
|
+
#command(name) {
|
|
3253
|
+
return this.#root.querySelector(`office-ui-button[command="${name}"]`);
|
|
3254
|
+
}
|
|
2898
3255
|
#setPane(pane, visible) {
|
|
3256
|
+
if (visible && this.#compact?.matches) {
|
|
3257
|
+
const other = pane === "pages" ? "inspector" : "pages";
|
|
3258
|
+
(other === "pages" ? this.#rail : this.#inspector).hidden = true;
|
|
3259
|
+
this.#command(other).setAttribute("pressed", "false");
|
|
3260
|
+
}
|
|
2899
3261
|
(pane === "pages" ? this.#rail : this.#inspector).hidden = !visible;
|
|
2900
|
-
this.#
|
|
3262
|
+
this.#command(pane).setAttribute("pressed", String(visible));
|
|
2901
3263
|
this.#syncNotes();
|
|
2902
3264
|
}
|
|
2903
3265
|
#syncNotes() {
|
|
2904
3266
|
this.#root.querySelector(".notes-strip button").setAttribute("aria-expanded", String(!this.#inspector.hidden && this.#notes.open));
|
|
2905
3267
|
}
|
|
2906
|
-
|
|
3268
|
+
/** Open an inspector disclosure; text editing may focus the text field directly (F2). */
|
|
3269
|
+
reveal(kind, focusText = false) {
|
|
3270
|
+
if (this.#compact?.matches) this.#tools.open = false;
|
|
3271
|
+
this.#inspectorManuallyToggled = true;
|
|
2907
3272
|
this.#setPane("inspector", true);
|
|
2908
3273
|
const selector = kind === "notes" ? ".notes" : kind === "edit" ? ".edit-controls" : kind === "layers" ? ".layer-controls" : ".shape-inspector";
|
|
2909
3274
|
const panel = this.#root.querySelector(selector);
|
|
2910
3275
|
if (panel.hidden) return;
|
|
2911
3276
|
panel.open = true;
|
|
2912
|
-
panel.querySelector("
|
|
3277
|
+
const text = panel.querySelector("textarea");
|
|
3278
|
+
if (focusText && text && !text.disabled) text.focus();
|
|
3279
|
+
else panel.querySelector("summary").focus();
|
|
2913
3280
|
panel.scrollIntoView?.({
|
|
2914
3281
|
block: "nearest",
|
|
2915
3282
|
inline: "nearest"
|
|
@@ -2950,14 +3317,21 @@ var ViewerChrome = class {
|
|
|
2950
3317
|
fragment.append(item);
|
|
2951
3318
|
}
|
|
2952
3319
|
this.#pageList.replaceChildren(fragment);
|
|
3320
|
+
this.#pageTabs.tabs = (state.document?.pages ?? []).map((candidate, index) => ({
|
|
3321
|
+
id: String(index),
|
|
3322
|
+
label: candidate.name,
|
|
3323
|
+
title: candidate.isBackground ? `${candidate.name} (background page)` : candidate.name
|
|
3324
|
+
}));
|
|
2953
3325
|
if (restoreFocus !== void 0) [...this.#pageList.querySelectorAll("button")].find((button) => button.dataset.pageIndex === restoreFocus)?.focus();
|
|
2954
3326
|
}
|
|
3327
|
+
this.#pageTabs.selected = page ? String(state.pageIndex) : "";
|
|
2955
3328
|
for (const button of this.#pageList.querySelectorAll("button")) {
|
|
2956
3329
|
const selected = Number(button.dataset.pageIndex) === state.pageIndex;
|
|
2957
3330
|
if (selected) button.setAttribute("aria-current", "page");
|
|
2958
3331
|
else button.removeAttribute("aria-current");
|
|
2959
3332
|
button.tabIndex = selected ? 0 : -1;
|
|
2960
3333
|
}
|
|
3334
|
+
this.#root.querySelector("[data-page-status]").setAttribute("value", page ? `Page ${state.pageIndex + 1} of ${state.document.pages.length}` : "");
|
|
2961
3335
|
const count = state.document?.pages.length ?? 0;
|
|
2962
3336
|
this.#root.querySelector("[data-page-count]").textContent = String(count);
|
|
2963
3337
|
this.#root.querySelector(".page-empty").hidden = count > 0;
|
|
@@ -2965,18 +3339,406 @@ var ViewerChrome = class {
|
|
|
2965
3339
|
this.#root.querySelector("[data-page-size]").textContent = page ? `${page.width} × ${page.height} in` : "No page";
|
|
2966
3340
|
this.#root.querySelector("[data-page-shapes]").textContent = page ? `${page.shapes.length} top-level` : "No shapes";
|
|
2967
3341
|
this.#root.querySelector(".selection-hint").hidden = !!state.selectedShape;
|
|
2968
|
-
this.#
|
|
2969
|
-
this.#
|
|
2970
|
-
this.#
|
|
2971
|
-
this.#
|
|
2972
|
-
this.#root.querySelector("
|
|
2973
|
-
for (const button of this.#root.querySelectorAll("[data-chrome=\"notes\"]")) button.disabled = noteCount === 0;
|
|
3342
|
+
this.#command("selection").disabled = !state.selectedShape;
|
|
3343
|
+
this.#command("edit").disabled = !page;
|
|
3344
|
+
this.#command("layers").disabled = this.#root.querySelector(".layer-controls").hidden;
|
|
3345
|
+
this.#command("notes").disabled = noteCount === 0;
|
|
3346
|
+
this.#root.querySelector(".notes-strip button").disabled = noteCount === 0;
|
|
2974
3347
|
this.#root.querySelector("[data-note-count]").textContent = noteCount ? ` · ${noteCount}` : "";
|
|
2975
3348
|
this.#syncNotes();
|
|
2976
3349
|
}
|
|
2977
3350
|
};
|
|
2978
3351
|
//#endregion
|
|
2979
|
-
//#region
|
|
3352
|
+
//#region src/ribbon-router.ts
|
|
3353
|
+
/** Routes a ribbon, status-bar or shortcut action to the controller that owns it. */
|
|
3354
|
+
function routeRibbonAction(targets, action) {
|
|
3355
|
+
const { controller } = targets;
|
|
3356
|
+
const page = controller.state.document?.pages[controller.state.pageIndex];
|
|
3357
|
+
switch (action.type) {
|
|
3358
|
+
case "history": return targets.history(action.key);
|
|
3359
|
+
case "delete": return targets.deleteSelection();
|
|
3360
|
+
case "tool": return targets.setTool(action.tool);
|
|
3361
|
+
case "grid": return targets.toggleGrid();
|
|
3362
|
+
case "fullscreen": return targets.toggleFullscreen();
|
|
3363
|
+
case "pane": return targets.togglePane(action.pane);
|
|
3364
|
+
case "reveal": return targets.reveal(action.panel, action.focusText ?? false);
|
|
3365
|
+
case "search": return targets.focusSearch();
|
|
3366
|
+
case "page": return controller.setPage(controller.state.pageIndex + action.step);
|
|
3367
|
+
case "zoom":
|
|
3368
|
+
if (!page) return;
|
|
3369
|
+
if (action.mode === "actual") return controller.setZoom(1);
|
|
3370
|
+
return targets.fit(action.mode === "fit" ? "page" : "width");
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
//#endregion
|
|
3374
|
+
//#region src/viewer-draw-tool.ts
|
|
3375
|
+
/** Visio snaps new geometry to ruler subdivisions; 1/16 inch matches its default fine grid. */
|
|
3376
|
+
var SNAP = 1 / 16;
|
|
3377
|
+
var MIN_SIZE = SNAP;
|
|
3378
|
+
var snap = (value) => Math.round(value / SNAP) * SNAP;
|
|
3379
|
+
/** Next free numeric shape ID on the page tree. Core still rejects any collision. */
|
|
3380
|
+
function nextShapeId(page) {
|
|
3381
|
+
let max = 0;
|
|
3382
|
+
const pending = [...page.shapes];
|
|
3383
|
+
while (pending.length) {
|
|
3384
|
+
const shape = pending.pop();
|
|
3385
|
+
if (/^\d{1,9}$/.test(shape.id)) max = Math.max(max, Number(shape.id));
|
|
3386
|
+
pending.push(...shape.children);
|
|
3387
|
+
}
|
|
3388
|
+
return String(max + 1);
|
|
3389
|
+
}
|
|
3390
|
+
/** Page inches with a top-left origin (SVG user space) from a client point. */
|
|
3391
|
+
function pagePoint(svg, page, event) {
|
|
3392
|
+
const matrix = svg.getScreenCTM?.();
|
|
3393
|
+
if (!matrix) return void 0;
|
|
3394
|
+
const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse());
|
|
3395
|
+
return {
|
|
3396
|
+
x: snap(Math.max(0, Math.min(page.width, point.x))),
|
|
3397
|
+
y: snap(Math.max(0, Math.min(page.height, point.y)))
|
|
3398
|
+
};
|
|
3399
|
+
}
|
|
3400
|
+
/** Drag-to-draw rectangle tool. The preview is viewer-only; the shape is created by core. */
|
|
3401
|
+
var RectangleDrawTool = class {
|
|
3402
|
+
viewport;
|
|
3403
|
+
controller;
|
|
3404
|
+
options;
|
|
3405
|
+
#drag;
|
|
3406
|
+
#request = 0;
|
|
3407
|
+
constructor(viewport, controller, options) {
|
|
3408
|
+
this.viewport = viewport;
|
|
3409
|
+
this.controller = controller;
|
|
3410
|
+
this.options = options;
|
|
3411
|
+
}
|
|
3412
|
+
get drawing() {
|
|
3413
|
+
return !!this.#drag;
|
|
3414
|
+
}
|
|
3415
|
+
wire() {
|
|
3416
|
+
const events = new ((this.viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
3417
|
+
const options = { signal: events.signal };
|
|
3418
|
+
const viewport = this.viewport;
|
|
3419
|
+
viewport.addEventListener("click", (event) => {
|
|
3420
|
+
if (this.options.active()) event.stopImmediatePropagation();
|
|
3421
|
+
}, {
|
|
3422
|
+
...options,
|
|
3423
|
+
capture: true
|
|
3424
|
+
});
|
|
3425
|
+
viewport.addEventListener("pointerdown", (event) => this.#start(event), options);
|
|
3426
|
+
viewport.addEventListener("pointermove", (event) => this.#move(event), options);
|
|
3427
|
+
viewport.addEventListener("pointerup", (event) => void this.#finish(event), options);
|
|
3428
|
+
viewport.addEventListener("pointercancel", () => this.#cancel(), options);
|
|
3429
|
+
viewport.addEventListener("keydown", (event) => {
|
|
3430
|
+
if (event.key === "Escape" && this.#drag) {
|
|
3431
|
+
event.preventDefault();
|
|
3432
|
+
this.#cancel();
|
|
3433
|
+
}
|
|
3434
|
+
}, options);
|
|
3435
|
+
return () => {
|
|
3436
|
+
++this.#request;
|
|
3437
|
+
this.#cancel();
|
|
3438
|
+
events.abort();
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
#start(event) {
|
|
3442
|
+
if (!this.options.active() || event.button !== 0 || this.#drag) return;
|
|
3443
|
+
const state = this.controller.state;
|
|
3444
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3445
|
+
const svg = this.viewport.querySelector("svg.paper");
|
|
3446
|
+
if (!page || !svg || !state.edit.sourceAvailable || state.edit.busy) return;
|
|
3447
|
+
const start = pagePoint(svg, page, event);
|
|
3448
|
+
if (!start) return;
|
|
3449
|
+
event.preventDefault();
|
|
3450
|
+
const rect = this.viewport.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
3451
|
+
rect.classList.add("draw-preview");
|
|
3452
|
+
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
3453
|
+
svg.append(rect);
|
|
3454
|
+
this.#drag = {
|
|
3455
|
+
pointer: event.pointerId,
|
|
3456
|
+
svg,
|
|
3457
|
+
page,
|
|
3458
|
+
...start,
|
|
3459
|
+
rect
|
|
3460
|
+
};
|
|
3461
|
+
this.viewport.setPointerCapture?.(event.pointerId);
|
|
3462
|
+
this.#update(start);
|
|
3463
|
+
}
|
|
3464
|
+
#move(event) {
|
|
3465
|
+
if (!this.#drag || event.pointerId !== this.#drag.pointer) return;
|
|
3466
|
+
const point = pagePoint(this.#drag.svg, this.#drag.page, event);
|
|
3467
|
+
if (point) this.#update(point);
|
|
3468
|
+
}
|
|
3469
|
+
#update(point) {
|
|
3470
|
+
const { x, y, rect } = this.#drag;
|
|
3471
|
+
rect.setAttribute("x", String(Math.min(x, point.x)));
|
|
3472
|
+
rect.setAttribute("y", String(Math.min(y, point.y)));
|
|
3473
|
+
rect.setAttribute("width", String(Math.abs(point.x - x)));
|
|
3474
|
+
rect.setAttribute("height", String(Math.abs(point.y - y)));
|
|
3475
|
+
}
|
|
3476
|
+
#cancel() {
|
|
3477
|
+
this.#drag?.rect.remove();
|
|
3478
|
+
this.#drag = void 0;
|
|
3479
|
+
}
|
|
3480
|
+
async #finish(event) {
|
|
3481
|
+
const drag = this.#drag;
|
|
3482
|
+
if (!drag || event.pointerId !== drag.pointer) return;
|
|
3483
|
+
const end = pagePoint(drag.svg, drag.page, event) ?? drag;
|
|
3484
|
+
this.#cancel();
|
|
3485
|
+
const width = Math.abs(end.x - drag.x), height = Math.abs(end.y - drag.y);
|
|
3486
|
+
if (width < MIN_SIZE || height < MIN_SIZE) {
|
|
3487
|
+
this.options.announce("Drag on the page to draw a rectangle.");
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
const state = this.controller.state;
|
|
3491
|
+
if (state.document?.pages[state.pageIndex] !== drag.page) return;
|
|
3492
|
+
const shapeId = nextShapeId(drag.page);
|
|
3493
|
+
const pageId = drag.page.id;
|
|
3494
|
+
const request = ++this.#request;
|
|
3495
|
+
try {
|
|
3496
|
+
await this.controller.applyEdits([{
|
|
3497
|
+
type: "create-rectangle",
|
|
3498
|
+
pageId,
|
|
3499
|
+
shapeId,
|
|
3500
|
+
x: (drag.x + end.x) / 2,
|
|
3501
|
+
y: drag.page.height - (drag.y + end.y) / 2,
|
|
3502
|
+
width,
|
|
3503
|
+
height
|
|
3504
|
+
}]);
|
|
3505
|
+
if (request !== this.#request) return;
|
|
3506
|
+
const created = this.controller.state.document?.pages.find((page) => page.id === pageId)?.shapes.find((shape) => shape.id === shapeId);
|
|
3507
|
+
if (created) this.controller.selectShape({
|
|
3508
|
+
id: shapeId,
|
|
3509
|
+
name: created.name,
|
|
3510
|
+
pageId
|
|
3511
|
+
});
|
|
3512
|
+
this.options.announce(`Rectangle ${shapeId} added (${+width.toFixed(4)} × ${+height.toFixed(4)} in).`);
|
|
3513
|
+
} catch (error) {
|
|
3514
|
+
if (request === this.#request && !isEditCancellation(error)) this.options.announce(editErrorMessage(error));
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
};
|
|
3518
|
+
//#endregion
|
|
3519
|
+
//#region src/viewer-commands.ts
|
|
3520
|
+
var editable = (target) => target instanceof Element && !!target.closest("input, textarea, select, [contenteditable]:not([contenteditable=\"false\"])");
|
|
3521
|
+
/**
|
|
3522
|
+
* Visio command state (tool, grid, pending edits) and keyboard shortcuts. Ribbon controls emit
|
|
3523
|
+
* typed `ribbon-action` events and shortcuts build the same actions; both go through
|
|
3524
|
+
* `routeRibbonAction`. Document mutation stays in the controller and core.
|
|
3525
|
+
*/
|
|
3526
|
+
var ViewerCommands = class {
|
|
3527
|
+
host;
|
|
3528
|
+
#tool = "pointer";
|
|
3529
|
+
#grid = false;
|
|
3530
|
+
#pending = 0;
|
|
3531
|
+
#draw;
|
|
3532
|
+
#targets;
|
|
3533
|
+
constructor(host) {
|
|
3534
|
+
this.host = host;
|
|
3535
|
+
this.#draw = new RectangleDrawTool(host.viewport, host.controller, {
|
|
3536
|
+
active: () => this.#tool === "rectangle",
|
|
3537
|
+
announce: host.announce
|
|
3538
|
+
});
|
|
3539
|
+
this.#targets = {
|
|
3540
|
+
controller: host.controller,
|
|
3541
|
+
history: (key) => this.#history(key),
|
|
3542
|
+
deleteSelection: () => this.#delete(),
|
|
3543
|
+
setTool: (tool) => this.setTool(tool),
|
|
3544
|
+
toggleGrid: () => {
|
|
3545
|
+
this.#grid = !this.#grid;
|
|
3546
|
+
this.render(host.controller.state);
|
|
3547
|
+
},
|
|
3548
|
+
toggleFullscreen: () => this.#toggleFullscreen(),
|
|
3549
|
+
togglePane: host.togglePane,
|
|
3550
|
+
reveal: host.reveal,
|
|
3551
|
+
fit: host.fit,
|
|
3552
|
+
focusSearch: host.focusSearch
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
get tool() {
|
|
3556
|
+
return this.#tool;
|
|
3557
|
+
}
|
|
3558
|
+
/** Run one typed action, exactly as a ribbon control or shortcut would. */
|
|
3559
|
+
run(action) {
|
|
3560
|
+
routeRibbonAction(this.#targets, action);
|
|
3561
|
+
}
|
|
3562
|
+
wire() {
|
|
3563
|
+
const { root, viewport } = this.host;
|
|
3564
|
+
const doc = root.ownerDocument;
|
|
3565
|
+
const events = new ((doc.defaultView?.AbortController) ?? AbortController)();
|
|
3566
|
+
const options = { signal: events.signal };
|
|
3567
|
+
root.addEventListener(RIBBON_ACTION_EVENT, (event) => this.run(event.detail), options);
|
|
3568
|
+
root.addEventListener("office-command", (event) => {
|
|
3569
|
+
if (event.detail?.command === "zoom-fit") this.run({
|
|
3570
|
+
type: "zoom",
|
|
3571
|
+
mode: "fit"
|
|
3572
|
+
});
|
|
3573
|
+
}, options);
|
|
3574
|
+
root.addEventListener("keydown", (event) => this.#shortcut(event), options);
|
|
3575
|
+
viewport.addEventListener("wheel", (event) => {
|
|
3576
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
3577
|
+
event.preventDefault();
|
|
3578
|
+
const zoom = this.host.controller.state.zoom;
|
|
3579
|
+
this.host.controller.setZoom(event.deltaY < 0 ? zoom * 1.1 : zoom / 1.1);
|
|
3580
|
+
}, {
|
|
3581
|
+
...options,
|
|
3582
|
+
passive: false
|
|
3583
|
+
});
|
|
3584
|
+
viewport.addEventListener("pointerdown", () => {
|
|
3585
|
+
if (!viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
|
|
3586
|
+
}, options);
|
|
3587
|
+
viewport.addEventListener("dblclick", (event) => {
|
|
3588
|
+
if (this.#tool !== "pointer") return;
|
|
3589
|
+
if (event.target?.closest?.("[data-shape-id]")) this.run({
|
|
3590
|
+
type: "reveal",
|
|
3591
|
+
panel: "edit",
|
|
3592
|
+
focusText: true
|
|
3593
|
+
});
|
|
3594
|
+
}, options);
|
|
3595
|
+
doc.addEventListener("fullscreenchange", () => this.render(this.host.controller.state), options);
|
|
3596
|
+
const disposeDraw = this.#draw.wire();
|
|
3597
|
+
return () => {
|
|
3598
|
+
++this.#pending;
|
|
3599
|
+
events.abort();
|
|
3600
|
+
disposeDraw();
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
setTool(tool) {
|
|
3604
|
+
if (tool === "rectangle" && !this.#canEdit(this.host.controller.state)) return;
|
|
3605
|
+
this.#tool = tool;
|
|
3606
|
+
this.render(this.host.controller.state);
|
|
3607
|
+
}
|
|
3608
|
+
#canEdit(state) {
|
|
3609
|
+
return state.edit.sourceAvailable && !state.loading && !state.edit.busy;
|
|
3610
|
+
}
|
|
3611
|
+
#history(key) {
|
|
3612
|
+
const { controller } = this.host;
|
|
3613
|
+
const { edit, loading } = controller.state;
|
|
3614
|
+
if ((key === "undo" ? edit.canUndo : edit.canRedo) && !edit.busy && !loading) this.#edit(() => controller[key]());
|
|
3615
|
+
}
|
|
3616
|
+
#delete() {
|
|
3617
|
+
const state = this.host.controller.state;
|
|
3618
|
+
const shape = state.selectedShape;
|
|
3619
|
+
const pageId = shape?.pageId ?? state.document?.pages[state.pageIndex]?.id;
|
|
3620
|
+
if (!shape || pageId === void 0 || !this.#canEdit(state)) return;
|
|
3621
|
+
this.#edit(() => this.host.controller.applyEdits([{
|
|
3622
|
+
type: "delete-shape",
|
|
3623
|
+
pageId,
|
|
3624
|
+
shapeId: shape.id
|
|
3625
|
+
}]), `Deleted ${shape.name || `shape ${shape.id}`}.`);
|
|
3626
|
+
}
|
|
3627
|
+
async #edit(action, success) {
|
|
3628
|
+
const request = ++this.#pending;
|
|
3629
|
+
const { root, viewport } = this.host;
|
|
3630
|
+
const canvasFocused = viewport.contains(root.activeElement);
|
|
3631
|
+
try {
|
|
3632
|
+
await action();
|
|
3633
|
+
if (canvasFocused && !viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
|
|
3634
|
+
if (success && request === this.#pending) this.host.announce(success);
|
|
3635
|
+
} catch (error) {
|
|
3636
|
+
if (request === this.#pending && !isEditCancellation(error) && !this.host.controller.state.edit.error) this.host.announce(editErrorMessage(error));
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
#toggleFullscreen() {
|
|
3640
|
+
const host = this.host.root.host;
|
|
3641
|
+
const doc = this.host.root.ownerDocument;
|
|
3642
|
+
(doc.fullscreenElement === host ? doc.exitFullscreen?.() : host.requestFullscreen?.())?.catch?.(() => this.host.announce("Full screen is not available in this browser frame."));
|
|
3643
|
+
}
|
|
3644
|
+
/** Visio shortcuts as typed actions. Text fields keep native undo, deletion and editing keys. */
|
|
3645
|
+
#shortcutAction(event) {
|
|
3646
|
+
const state = this.host.controller.state;
|
|
3647
|
+
const control = event.ctrlKey || event.metaKey;
|
|
3648
|
+
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
|
|
3649
|
+
if (control && key === "f") return { type: "search" };
|
|
3650
|
+
if (control && event.shiftKey && key === "w") return {
|
|
3651
|
+
type: "zoom",
|
|
3652
|
+
mode: "fit"
|
|
3653
|
+
};
|
|
3654
|
+
if (control && (key === "PageDown" || key === "PageUp")) return {
|
|
3655
|
+
type: "page",
|
|
3656
|
+
step: key === "PageDown" ? 1 : -1
|
|
3657
|
+
};
|
|
3658
|
+
if (key === "F5" && !control) return { type: "fullscreen" };
|
|
3659
|
+
if (editable(event.target)) return void 0;
|
|
3660
|
+
if (control && !event.shiftKey && key === "z") return {
|
|
3661
|
+
type: "history",
|
|
3662
|
+
key: "undo"
|
|
3663
|
+
};
|
|
3664
|
+
if (control && (key === "y" || event.shiftKey && key === "z")) return {
|
|
3665
|
+
type: "history",
|
|
3666
|
+
key: "redo"
|
|
3667
|
+
};
|
|
3668
|
+
if (control && key === "1") return {
|
|
3669
|
+
type: "tool",
|
|
3670
|
+
tool: "pointer"
|
|
3671
|
+
};
|
|
3672
|
+
if (control && key === "8") return {
|
|
3673
|
+
type: "tool",
|
|
3674
|
+
tool: "rectangle"
|
|
3675
|
+
};
|
|
3676
|
+
if (!control && key === "Delete" && state.selectedShape) return { type: "delete" };
|
|
3677
|
+
if (!control && key === "F2" && state.document) return {
|
|
3678
|
+
type: "reveal",
|
|
3679
|
+
panel: "edit",
|
|
3680
|
+
focusText: true
|
|
3681
|
+
};
|
|
3682
|
+
if (key === "Escape" && this.#tool !== "pointer" && !this.#draw.drawing) return {
|
|
3683
|
+
type: "tool",
|
|
3684
|
+
tool: "pointer"
|
|
3685
|
+
};
|
|
3686
|
+
}
|
|
3687
|
+
#shortcut(event) {
|
|
3688
|
+
if (event.defaultPrevented || event.isComposing || event.altKey) return;
|
|
3689
|
+
const action = this.#shortcutAction(event);
|
|
3690
|
+
if (!action) return;
|
|
3691
|
+
event.preventDefault();
|
|
3692
|
+
this.run(action);
|
|
3693
|
+
}
|
|
3694
|
+
render(state) {
|
|
3695
|
+
const { root, viewport } = this.host;
|
|
3696
|
+
const button = (name) => root.querySelector(`office-ui-button[command="${name}"]`);
|
|
3697
|
+
const editing = this.#canEdit(state);
|
|
3698
|
+
if (this.#tool === "rectangle" && !state.edit.sourceAvailable) this.#tool = "pointer";
|
|
3699
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3700
|
+
button("undo").disabled = !state.edit.canUndo || state.edit.busy || state.loading;
|
|
3701
|
+
button("redo").disabled = !state.edit.canRedo || state.edit.busy || state.loading;
|
|
3702
|
+
button("delete").disabled = !editing || !state.selectedShape;
|
|
3703
|
+
button("pointer").setAttribute("pressed", String(this.#tool === "pointer"));
|
|
3704
|
+
const rectangle = button("rectangle");
|
|
3705
|
+
rectangle.setAttribute("pressed", String(this.#tool === "rectangle"));
|
|
3706
|
+
rectangle.disabled = !editing || !page;
|
|
3707
|
+
rectangle.title = state.edit.sourceAvailable ? "Rectangle (Ctrl+8)" : "Rectangle (Ctrl+8): open a .vsdx file to draw. Model-only documents are read only.";
|
|
3708
|
+
button("grid").setAttribute("pressed", String(this.#grid));
|
|
3709
|
+
button("grid").disabled = !page;
|
|
3710
|
+
for (const name of [
|
|
3711
|
+
"zoom-fit",
|
|
3712
|
+
"page-width",
|
|
3713
|
+
"actual-size"
|
|
3714
|
+
]) button(name).disabled = !page;
|
|
3715
|
+
const fullscreen = button("fullscreen");
|
|
3716
|
+
const host = root.host;
|
|
3717
|
+
fullscreen.disabled = typeof host.requestFullscreen !== "function";
|
|
3718
|
+
fullscreen.setAttribute("pressed", String(root.ownerDocument.fullscreenElement === host));
|
|
3719
|
+
viewport.dataset.tool = this.#tool;
|
|
3720
|
+
viewport.dataset.grid = String(this.#grid);
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
//#endregion
|
|
3724
|
+
//#region src/office-ui.ts
|
|
3725
|
+
/**
|
|
3726
|
+
* Visio-only glyphs (20x20 path data). Neutral Office glyphs come from ooxml-ui; add a glyph
|
|
3727
|
+
* there instead when another Office product could use it.
|
|
3728
|
+
*/
|
|
3729
|
+
var VISIO_ICONS = {
|
|
3730
|
+
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",
|
|
3731
|
+
visioShapeData: "M3 3.5h14v13H3ZM3 7.5h14M8 7.5v9M10.5 11h4M10.5 14h4",
|
|
3732
|
+
visioPagesPane: "M2.5 3h15v14h-15ZM7 3v14M3.8 6h1.9M3.8 9h1.9M3.8 12h1.9",
|
|
3733
|
+
visioInspectorPane: "M2.5 3h15v14h-15ZM13 3v14M14.5 6h1.5M14.5 9h1.5M14.5 12h1.5"
|
|
3734
|
+
};
|
|
3735
|
+
/** Define the shared Office controls the viewer chrome uses. Idempotent; browser only. */
|
|
3736
|
+
function registerViewerControls(registry) {
|
|
3737
|
+
for (const [name, path] of Object.entries(VISIO_ICONS)) if (!getIcon(name)) registerIcon(name, path);
|
|
3738
|
+
registerControls(registry);
|
|
3739
|
+
}
|
|
3740
|
+
//#endregion
|
|
3741
|
+
//#region src/export-svg.ts
|
|
2980
3742
|
var MAX_SVG_EXPORT_BYTES = 16777216;
|
|
2981
3743
|
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.";
|
|
2982
3744
|
var INVALID_XML = "Invalid XML characters were replaced with the Unicode replacement character in the SVG export.";
|
|
@@ -3146,7 +3908,7 @@ function assertSerializedBudget(root, maxBytes) {
|
|
|
3146
3908
|
}
|
|
3147
3909
|
}
|
|
3148
3910
|
//#endregion
|
|
3149
|
-
//#region
|
|
3911
|
+
//#region src/snapshot-scene.ts
|
|
3150
3912
|
/** Copy only named scalar fields, never arbitrary host properties or their getters. */
|
|
3151
3913
|
function fields(source, keys) {
|
|
3152
3914
|
const result = {};
|
|
@@ -3362,7 +4124,7 @@ function copySnapshotScene(model) {
|
|
|
3362
4124
|
};
|
|
3363
4125
|
}
|
|
3364
4126
|
//#endregion
|
|
3365
|
-
//#region
|
|
4127
|
+
//#region src/print-snapshot.ts
|
|
3366
4128
|
/** Application guardrails, not printer capabilities or Microsoft Visio limits. */
|
|
3367
4129
|
var PRINT_SNAPSHOT_LIMITS = Object.freeze({
|
|
3368
4130
|
maxPages: 32,
|
|
@@ -3579,14 +4341,17 @@ function validationCost(model) {
|
|
|
3579
4341
|
};
|
|
3580
4342
|
}
|
|
3581
4343
|
//#endregion
|
|
3582
|
-
//#region
|
|
4344
|
+
//#region src/viewer-element.ts
|
|
3583
4345
|
var BaseElement = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
|
|
3584
4346
|
var VisioViewerElement = class extends BaseElement {
|
|
4347
|
+
controller = new ViewerController(createWorkerParser());
|
|
3585
4348
|
#root;
|
|
3586
4349
|
#disposeInputs;
|
|
3587
4350
|
#viewport;
|
|
3588
|
-
#
|
|
3589
|
-
#
|
|
4351
|
+
#zoomSlider;
|
|
4352
|
+
#shapeStatus;
|
|
4353
|
+
#announcement;
|
|
4354
|
+
#commands;
|
|
3590
4355
|
#status;
|
|
3591
4356
|
#diagnostics;
|
|
3592
4357
|
#toolbar;
|
|
@@ -3600,38 +4365,33 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3600
4365
|
#notesPanel;
|
|
3601
4366
|
#inspector;
|
|
3602
4367
|
#inspectedShape;
|
|
3603
|
-
#renderedSelection;
|
|
4368
|
+
#renderedSelection = null;
|
|
3604
4369
|
#unsubscribe;
|
|
3605
4370
|
#eventUnsubscribe;
|
|
3606
|
-
#document;
|
|
3607
|
-
#renderedPage;
|
|
3608
|
-
#disposed;
|
|
3609
|
-
#suspended;
|
|
3610
|
-
#renderWarnings;
|
|
3611
|
-
#disposeRenderer;
|
|
4371
|
+
#document = null;
|
|
4372
|
+
#renderedPage = -1;
|
|
4373
|
+
#disposed = false;
|
|
4374
|
+
#suspended = false;
|
|
4375
|
+
#renderWarnings = [];
|
|
4376
|
+
#disposeRenderer = () => {};
|
|
3612
4377
|
#fontEvents;
|
|
3613
|
-
#fontsChanged
|
|
3614
|
-
|
|
3615
|
-
super();
|
|
3616
|
-
this.controller = new ViewerController(createWorkerParser());
|
|
3617
|
-
this.#renderedSelection = null;
|
|
4378
|
+
#fontsChanged = () => {
|
|
4379
|
+
if (this.#disposed) return;
|
|
3618
4380
|
this.#document = null;
|
|
3619
4381
|
this.#renderedPage = -1;
|
|
3620
|
-
this.#
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
this.#fontsChanged = () => {
|
|
3625
|
-
if (this.#disposed) return;
|
|
3626
|
-
this.#document = null;
|
|
3627
|
-
this.#renderedPage = -1;
|
|
3628
|
-
this.#render(this.controller.state);
|
|
3629
|
-
};
|
|
4382
|
+
this.#render(this.controller.state);
|
|
4383
|
+
};
|
|
4384
|
+
constructor() {
|
|
4385
|
+
super();
|
|
3630
4386
|
this.#root = this.attachShadow({ mode: "open" });
|
|
3631
|
-
|
|
4387
|
+
registerViewerControls();
|
|
4388
|
+
this.#root.innerHTML = `<style>${viewerStyles}${canvasAndRibbonStyles}</style>${viewerChromeTemplate}`;
|
|
4389
|
+
const workspace = this.#root.querySelector(".workspace");
|
|
4390
|
+
workspace.before(createRibbon(document));
|
|
4391
|
+
workspace.after(createPageTabs(document), createStatusBar(document));
|
|
3632
4392
|
this.#viewport = this.#root.querySelector(".viewport");
|
|
3633
|
-
this.#
|
|
3634
|
-
this.#
|
|
4393
|
+
this.#zoomSlider = this.#root.querySelector("office-ui-zoom-slider");
|
|
4394
|
+
this.#shapeStatus = this.#root.querySelector("[data-shape-status]");
|
|
3635
4395
|
this.#status = this.#root.querySelector("[data-status]");
|
|
3636
4396
|
this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
|
|
3637
4397
|
this.#toolbar = this.#root.querySelector(".toolbar");
|
|
@@ -3642,6 +4402,23 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3642
4402
|
this.#notesPanel = this.#root.querySelector(".notes");
|
|
3643
4403
|
this.#inspector = this.#root.querySelector(".shape-inspector");
|
|
3644
4404
|
this.#chrome = new ViewerChrome(this.#root, this.controller);
|
|
4405
|
+
this.#commands = new ViewerCommands({
|
|
4406
|
+
root: this.#root,
|
|
4407
|
+
viewport: this.#viewport,
|
|
4408
|
+
controller: this.controller,
|
|
4409
|
+
fit: (mode) => this.#fit(mode),
|
|
4410
|
+
togglePane: (pane) => this.#chrome.togglePane(pane),
|
|
4411
|
+
reveal: (panel, focusText) => this.#chrome.reveal(panel, focusText),
|
|
4412
|
+
focusSearch: () => {
|
|
4413
|
+
this.#chrome.showTab("home");
|
|
4414
|
+
this.#search.input.focus();
|
|
4415
|
+
this.#search.input.select();
|
|
4416
|
+
},
|
|
4417
|
+
announce: (message) => {
|
|
4418
|
+
this.#announcement = message;
|
|
4419
|
+
this.#status.textContent = message;
|
|
4420
|
+
}
|
|
4421
|
+
});
|
|
3645
4422
|
this.#disposeInputs = this.#wireInputs();
|
|
3646
4423
|
this.#unsubscribe = this.controller.subscribe((state) => this.#render(state));
|
|
3647
4424
|
this.#eventUnsubscribe = this.controller.onEvent((name, detail) => {
|
|
@@ -3715,13 +4492,24 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3715
4492
|
}
|
|
3716
4493
|
fit() {
|
|
3717
4494
|
this.#assertAlive();
|
|
4495
|
+
this.#fit("page");
|
|
4496
|
+
}
|
|
4497
|
+
/** Visio Fit to Window shows the whole page; Page Width fills the canvas width. */
|
|
4498
|
+
#fit(mode) {
|
|
3718
4499
|
const page = this.document?.pages[this.pageIndex];
|
|
3719
4500
|
if (!page) return;
|
|
3720
4501
|
const style = this.ownerDocument.defaultView?.getComputedStyle(this.#viewport);
|
|
3721
4502
|
const padding = (value) => Number.parseFloat(value ?? "") || 0;
|
|
3722
4503
|
const width = Math.max(1, this.#viewport.clientWidth - padding(style?.paddingLeft) - padding(style?.paddingRight));
|
|
3723
4504
|
const height = Math.max(1, this.#viewport.clientHeight - padding(style?.paddingTop) - padding(style?.paddingBottom));
|
|
3724
|
-
|
|
4505
|
+
if (mode === "page") {
|
|
4506
|
+
this.zoom = Math.min(1, width / (page.width * 96), height / (page.height * 96));
|
|
4507
|
+
return;
|
|
4508
|
+
}
|
|
4509
|
+
const scrollbar = this.#viewport.offsetWidth - this.#viewport.clientWidth;
|
|
4510
|
+
const fitted = width / (page.width * 96);
|
|
4511
|
+
const reserve = page.height * 96 * fitted > height && scrollbar <= 0 ? 17 : 0;
|
|
4512
|
+
this.zoom = Math.max(1, width - reserve - 1) / (page.width * 96);
|
|
3725
4513
|
}
|
|
3726
4514
|
setLayerVisibility(pageId, layerId, visible) {
|
|
3727
4515
|
this.#assertAlive();
|
|
@@ -3791,16 +4579,18 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3791
4579
|
}
|
|
3792
4580
|
#wireInputs() {
|
|
3793
4581
|
const disposeChrome = this.#chrome.wire();
|
|
4582
|
+
const disposeCommands = this.#commands.wire();
|
|
3794
4583
|
const disposeEdit = this.#edit.wire();
|
|
3795
4584
|
const disposeLayers = wireLayerControls(this.#layers, this.controller);
|
|
3796
4585
|
const disposeInputs = wireViewerInputs({
|
|
3797
4586
|
viewport: this.#viewport,
|
|
3798
4587
|
commandRoot: this.#root,
|
|
3799
|
-
|
|
4588
|
+
zoomSlider: this.#zoomSlider,
|
|
3800
4589
|
searchInput: this.#search.input
|
|
3801
|
-
}, this.controller, () => this
|
|
4590
|
+
}, this.controller, (mode) => this.#fit(mode));
|
|
3802
4591
|
return () => {
|
|
3803
4592
|
disposeChrome();
|
|
4593
|
+
disposeCommands();
|
|
3804
4594
|
disposeInputs();
|
|
3805
4595
|
disposeLayers();
|
|
3806
4596
|
disposeEdit();
|
|
@@ -3819,14 +4609,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3819
4609
|
this.#document = state.document;
|
|
3820
4610
|
this.#renderedPage = state.pageIndex;
|
|
3821
4611
|
this.#renderedLayerOverrides = state.layerVisibilityOverrides;
|
|
3822
|
-
this.#
|
|
3823
|
-
for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
|
|
3824
|
-
const option = document.createElement("option");
|
|
3825
|
-
option.value = String(index);
|
|
3826
|
-
option.textContent = candidate.name + (candidate.isBackground ? " (background)" : "");
|
|
3827
|
-
this.#pageSelect.append(option);
|
|
3828
|
-
}
|
|
3829
|
-
this.#pageSelect.value = String(state.pageIndex);
|
|
4612
|
+
this.#announcement = void 0;
|
|
3830
4613
|
this.#renderWarnings = [];
|
|
3831
4614
|
if (state.document && page) {
|
|
3832
4615
|
const result = renderPage(state.document, page, { layerVisibilityOverrides: state.layerVisibilityOverrides });
|
|
@@ -3853,6 +4636,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3853
4636
|
if (svg && page) {
|
|
3854
4637
|
svg.style.width = `${page.width * 96 * state.zoom}px`;
|
|
3855
4638
|
svg.style.height = `${page.height * 96 * state.zoom}px`;
|
|
4639
|
+
svg.style.setProperty("--vv-inch", `${96 * state.zoom}px`);
|
|
3856
4640
|
}
|
|
3857
4641
|
const searchResult = state.search.results[state.search.activeIndex];
|
|
3858
4642
|
const selection = state.selectedShape;
|
|
@@ -3880,16 +4664,20 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3880
4664
|
this.#renderedSelection = selection ? { ...selection } : null;
|
|
3881
4665
|
}
|
|
3882
4666
|
if (!searchResult) this.#revealedSearchResult = void 0;
|
|
3883
|
-
this.#
|
|
3884
|
-
this.#
|
|
4667
|
+
this.#zoomSlider.value = Math.round(state.zoom * 100);
|
|
4668
|
+
this.#zoomSlider.disabled = !page;
|
|
4669
|
+
const inspected = selectedShape(state.document, state.selectedShape, state.pageIndex);
|
|
4670
|
+
const inches = (value) => `${+value.toFixed(3)} in`;
|
|
4671
|
+
this.#shapeStatus.setAttribute("value", inspected ? `Width: ${inches(inspected.width)} Height: ${inches(inspected.height)}` : "");
|
|
3885
4672
|
for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
|
|
3886
4673
|
renderSearchControls(this.#search, state);
|
|
3887
4674
|
this.#edit.render(state);
|
|
3888
4675
|
if (changed) renderLayerControls(this.#layers, state);
|
|
3889
4676
|
else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
|
|
3890
4677
|
this.#chrome.render(state, this.#notes.children.length);
|
|
4678
|
+
this.#commands.render(state);
|
|
3891
4679
|
this.#viewport.setAttribute("aria-busy", String(state.loading || state.edit.busy));
|
|
3892
|
-
this.#status.textContent = state.loading ? "Opening diagram…" : state.error?.message ?? (page ? `${page.name} · ${page.shapes.length} top-level shapes` : "No diagram open");
|
|
4680
|
+
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");
|
|
3893
4681
|
const warnings = (state.document?.diagnostics.length ?? 0) + this.#renderWarnings.length;
|
|
3894
4682
|
this.#diagnostics.textContent = warnings ? `${this.#notes.children.length} compatibility notes` : "Local-only viewing";
|
|
3895
4683
|
}
|
|
@@ -3899,7 +4687,7 @@ function registerVisioViewer() {
|
|
|
3899
4687
|
if (!customElements.get("visio-viewer")) customElements.define("visio-viewer", VisioViewerElement);
|
|
3900
4688
|
}
|
|
3901
4689
|
//#endregion
|
|
3902
|
-
//#region
|
|
4690
|
+
//#region src/binding.ts
|
|
3903
4691
|
/** One client-only lifecycle adapter; framework wrappers forward to this implementation. */
|
|
3904
4692
|
function mountViewer(container, initial = {}) {
|
|
3905
4693
|
registerVisioViewer();
|
|
@@ -4000,7 +4788,7 @@ function mountViewer(container, initial = {}) {
|
|
|
4000
4788
|
};
|
|
4001
4789
|
}
|
|
4002
4790
|
//#endregion
|
|
4003
|
-
//#region
|
|
4791
|
+
//#region packages/bindings/src/common.ts
|
|
4004
4792
|
/** One mapping for every property and event, shared by every framework adapter. */
|
|
4005
4793
|
function viewerOptions(props) {
|
|
4006
4794
|
const result = { events: props.events ?? {} };
|
|
@@ -4099,13 +4887,30 @@ function viewerHandle(current) {
|
|
|
4099
4887
|
}
|
|
4100
4888
|
};
|
|
4101
4889
|
}
|
|
4890
|
+
function viewerStateSource(current) {
|
|
4891
|
+
const controller = () => {
|
|
4892
|
+
try {
|
|
4893
|
+
return current()?.controller;
|
|
4894
|
+
} catch {
|
|
4895
|
+
return;
|
|
4896
|
+
}
|
|
4897
|
+
};
|
|
4898
|
+
return {
|
|
4899
|
+
subscribe(onChange) {
|
|
4900
|
+
return controller()?.subscribe(() => onChange()) ?? (() => {});
|
|
4901
|
+
},
|
|
4902
|
+
getSnapshot() {
|
|
4903
|
+
return controller()?.state ?? null;
|
|
4904
|
+
}
|
|
4905
|
+
};
|
|
4906
|
+
}
|
|
4102
4907
|
/** Native framework emits and callback-map props share one complete event inventory. */
|
|
4103
4908
|
function withEventEmitter(props, emit) {
|
|
4104
4909
|
const options = viewerOptions(props);
|
|
4105
4910
|
const callbacks = {};
|
|
4106
4911
|
for (const name of eventKeys) Object.assign(callbacks, { [name]: (value) => {
|
|
4107
4912
|
try {
|
|
4108
|
-
props.events?.[name]?.(value);
|
|
4913
|
+
(props.events?.[name])?.(value);
|
|
4109
4914
|
} finally {
|
|
4110
4915
|
emit(name, value);
|
|
4111
4916
|
}
|
|
@@ -4116,14 +4921,15 @@ function withEventEmitter(props, emit) {
|
|
|
4116
4921
|
};
|
|
4117
4922
|
}
|
|
4118
4923
|
//#endregion
|
|
4119
|
-
//#region .
|
|
4120
|
-
|
|
4924
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
|
|
4925
|
+
function __decorate(decorators, target, key, desc) {
|
|
4121
4926
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4122
4927
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4123
4928
|
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;
|
|
4124
4929
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
4125
|
-
}
|
|
4126
|
-
|
|
4930
|
+
}
|
|
4931
|
+
//#endregion
|
|
4932
|
+
//#region packages/bindings/src/angular.ts
|
|
4127
4933
|
var VisioViewerComponent = class VisioViewerComponent {
|
|
4128
4934
|
constructor() {
|
|
4129
4935
|
this.documentLoad = new EventEmitter();
|
|
@@ -4134,6 +4940,9 @@ var VisioViewerComponent = class VisioViewerComponent {
|
|
|
4134
4940
|
this.shapeSelect = new EventEmitter();
|
|
4135
4941
|
this.host = inject(ElementRef);
|
|
4136
4942
|
this.handle = viewerHandle(() => this.binding);
|
|
4943
|
+
this.stopState = () => {};
|
|
4944
|
+
this.viewerState = signal(null);
|
|
4945
|
+
this.state = this.viewerState.asReadonly();
|
|
4137
4946
|
this.outputs = {
|
|
4138
4947
|
"document-load": this.documentLoad,
|
|
4139
4948
|
"document-change": this.documentChange,
|
|
@@ -4150,11 +4959,17 @@ var VisioViewerComponent = class VisioViewerComponent {
|
|
|
4150
4959
|
}
|
|
4151
4960
|
ngAfterViewInit() {
|
|
4152
4961
|
this.binding = mountFrameworkViewer(this.host.nativeElement, this.options());
|
|
4962
|
+
const source = viewerStateSource(() => this.handle);
|
|
4963
|
+
this.viewerState.set(source.getSnapshot());
|
|
4964
|
+
this.stopState = source.subscribe(() => this.viewerState.set(source.getSnapshot()));
|
|
4153
4965
|
}
|
|
4154
4966
|
ngOnChanges() {
|
|
4155
4967
|
this.binding?.update(this.options());
|
|
4156
4968
|
}
|
|
4157
4969
|
ngOnDestroy() {
|
|
4970
|
+
this.stopState();
|
|
4971
|
+
this.stopState = () => {};
|
|
4972
|
+
this.viewerState.set(null);
|
|
4158
4973
|
const mounted = this.binding;
|
|
4159
4974
|
this.binding = void 0;
|
|
4160
4975
|
mounted?.destroy();
|