visio-react-viewer 0.1.1 → 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/dist/assets/{edit-worker-DRgXgRud.js → edit-worker-BcXrV_St.js} +203 -31
- package/dist/index.js +865 -160
- package/dist/types/packages/bindings/src/common.d.ts +11 -0
- package/dist/types/packages/bindings/src/react.d.ts +8 -3
- package/dist/types/src/office-ui.d.ts +2 -0
- package/dist/types/src/ribbon-action.d.ts +34 -0
- package/dist/types/src/ribbon-home.d.ts +6 -0
- package/dist/types/src/ribbon-parts.d.ts +20 -0
- package/dist/types/src/ribbon-router.d.ts +17 -0
- package/dist/types/src/ribbon-view.d.ts +2 -0
- package/dist/types/src/ribbon.d.ts +6 -0
- package/dist/types/src/status-bar.d.ts +7 -0
- package/dist/types/src/styles/index.d.ts +5 -0
- package/dist/types/src/styles.d.ts +1 -1
- package/dist/types/src/viewer-chrome.d.ts +11 -2
- package/dist/types/src/viewer-commands.d.ts +31 -0
- package/dist/types/src/viewer-draw-tool.d.ts +17 -0
- package/dist/types/src/viewer-input.d.ts +4 -2
- package/dist/types/src/viewer-search.d.ts +2 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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 { createElement, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useSyncExternalStore } from "react";
|
|
3
5
|
export * from "visio-core";
|
|
4
6
|
//#region src/document-history.ts
|
|
5
7
|
var EMPTY_EDIT_STATE = Object.freeze({
|
|
@@ -532,7 +534,7 @@ function createWorkerEditor(factory, timeoutMs = 2e4) {
|
|
|
532
534
|
if (typeof Worker === "undefined") throw new Error("Editing requires browser worker support.");
|
|
533
535
|
return new Worker(new URL(
|
|
534
536
|
/* @vite-ignore */
|
|
535
|
-
new URL("assets/edit-worker-
|
|
537
|
+
new URL("assets/edit-worker-BcXrV_St.js", import.meta.url).href,
|
|
536
538
|
"" + import.meta.url
|
|
537
539
|
), { type: "module" });
|
|
538
540
|
});
|
|
@@ -2143,7 +2145,7 @@ function selection(target) {
|
|
|
2143
2145
|
}
|
|
2144
2146
|
/** Own every DOM listener and abort them together when the surface is disposed. */
|
|
2145
2147
|
function wireViewerInputs(controls, controller, fit) {
|
|
2146
|
-
const { viewport, commandRoot,
|
|
2148
|
+
const { viewport, commandRoot, zoomSlider, searchInput } = controls;
|
|
2147
2149
|
const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
2148
2150
|
const options = { signal: events.signal };
|
|
2149
2151
|
searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
|
|
@@ -2159,13 +2161,9 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2159
2161
|
controller.setSearchQuery("");
|
|
2160
2162
|
}
|
|
2161
2163
|
}, options);
|
|
2162
|
-
|
|
2164
|
+
zoomSlider.addEventListener("input", () => controller.setZoom(zoomSlider.value / 100), options);
|
|
2163
2165
|
commandRoot.addEventListener("click", (event) => {
|
|
2164
2166
|
const action = event.target?.closest?.("button")?.dataset.action;
|
|
2165
|
-
if (action === "in") controller.setZoom(controller.state.zoom * 1.25);
|
|
2166
|
-
if (action === "out") controller.setZoom(controller.state.zoom / 1.25);
|
|
2167
|
-
if (action === "fit") fit();
|
|
2168
|
-
if (action === "actual") controller.setZoom(1);
|
|
2169
2167
|
if (action === "search-next") controller.nextSearchResult();
|
|
2170
2168
|
if (action === "search-previous") controller.previousSearchResult();
|
|
2171
2169
|
}, options);
|
|
@@ -2204,7 +2202,7 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2204
2202
|
}
|
|
2205
2203
|
if (event.key === "0") {
|
|
2206
2204
|
event.preventDefault();
|
|
2207
|
-
fit();
|
|
2205
|
+
fit("page");
|
|
2208
2206
|
}
|
|
2209
2207
|
}, options);
|
|
2210
2208
|
return () => events.abort();
|
|
@@ -2308,13 +2306,7 @@ button:disabled { opacity:.4; cursor:default; }
|
|
|
2308
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; }
|
|
2309
2307
|
button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
|
|
2310
2308
|
.toolbar { flex:none; min-width:0; border-bottom:1px solid var(--_vv-border); background:var(--_vv-surface); }
|
|
2311
|
-
.ribbon-primary { height:32px; display:flex; align-items:center; gap:6px; padding:2px 8px; }
|
|
2312
|
-
.page-picker { display:flex; min-width:0; align-items:center; gap:8px; color:var(--_vv-muted); font-size:11px; }
|
|
2313
|
-
.page-picker select { width:190px; max-width:42vw; height:26px; min-height:26px; padding:2px 6px; font-size:11px; color:var(--_vv-ink); }
|
|
2314
|
-
.page-stepper { display:flex; align-items:center; gap:2px; }
|
|
2315
|
-
.page-stepper button { width:26px; height:26px; min-height:26px; padding:0; font-size:18px; line-height:22px; background:transparent; border-color:transparent; }
|
|
2316
2309
|
.spacer { flex:1; }
|
|
2317
|
-
.local-label { color:var(--_vv-muted); font-size:10px; white-space:nowrap; }
|
|
2318
2310
|
.ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
|
|
2319
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; }
|
|
2320
2312
|
.ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
|
|
@@ -2324,15 +2316,6 @@ button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-acce
|
|
|
2324
2316
|
.group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
|
|
2325
2317
|
.ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
|
|
2326
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; }
|
|
2327
|
-
.command-icon { display:grid; place-items:center; width:26px; height:26px; color:var(--_vv-accent); }
|
|
2328
|
-
.command-icon::before { content:""; width:24px; height:24px; background:currentColor; mask:var(--command-symbol) center / contain no-repeat; }
|
|
2329
|
-
[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"); }
|
|
2330
|
-
[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"); }
|
|
2331
|
-
[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"); }
|
|
2332
|
-
[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"); }
|
|
2333
|
-
[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"); }
|
|
2334
|
-
[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"); }
|
|
2335
|
-
.ribbon-command[data-chrome="edit"] .command-icon { font-family:ui-serif,Georgia,serif; text-decoration:underline; text-decoration-thickness:1px; text-underline-offset:3px; }
|
|
2336
2319
|
.ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
|
|
2337
2320
|
.search-group { width:330px; }
|
|
2338
2321
|
.search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
|
|
@@ -2404,11 +2387,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2404
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; }
|
|
2405
2388
|
.status-message { display:flex; gap:16px; min-width:0; }
|
|
2406
2389
|
.status-message span { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
|
2407
|
-
.zoom-controls { display:flex; flex:none; align-items:center; gap:3px; }
|
|
2408
|
-
.zoom-controls button { min-height:24px; height:24px; padding:1px 6px; border-color:transparent; background:transparent; font-size:10px; }
|
|
2409
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:24px; padding:0; font-size:17px; line-height:20px; }
|
|
2410
|
-
.zoom-divider { width:1px; height:14px; background:var(--_vv-border); margin:0 3px; }
|
|
2411
|
-
.zoom { min-width:40px; font-size:10px; text-align:center; font-variant-numeric:tabular-nums; }
|
|
2412
2390
|
.empty { max-width:360px; margin:60px auto; padding:16px; text-align:center; color:var(--_vv-muted); font-size:12px; }
|
|
2413
2391
|
.empty strong { display:block; color:var(--_vv-ink); font-size:18px; font-weight:500; margin-bottom:8px; }
|
|
2414
2392
|
@media(max-width:1100px) { .ribbon-hint { display:none; } [data-diagnostics] { display:none; } }
|
|
@@ -2416,11 +2394,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2416
2394
|
button,select,input,textarea { min-height:44px; }
|
|
2417
2395
|
button { min-width:44px; }
|
|
2418
2396
|
summary { min-height:44px; line-height:44px; }
|
|
2419
|
-
.ribbon-primary { height:52px; padding:4px 8px; }
|
|
2420
|
-
.page-picker { flex:1; }
|
|
2421
|
-
.page-picker select { width:100%; max-width:none; min-height:44px; height:44px; font-size:12px; }
|
|
2422
|
-
.page-stepper button { width:44px; min-height:44px; height:44px; }
|
|
2423
|
-
.local-label,.ribbon-primary>.spacer { display:none; }
|
|
2424
2397
|
.ribbon-tabs { height:44px; }
|
|
2425
2398
|
.ribbon-tabs button { min-height:44px; padding:8px 16px; }
|
|
2426
2399
|
.ribbon-content { min-height:86px; }
|
|
@@ -2432,7 +2405,7 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2432
2405
|
.search-controls button { min-width:44px; font-size:0; padding:3px; }
|
|
2433
2406
|
.search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
|
|
2434
2407
|
.search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
|
|
2435
|
-
#home-panel>.ribbon-group:not(.search-group) { display:none; }
|
|
2408
|
+
#home-panel>.ribbon-group:not(.search-group):not(.undo-group) { display:none; }
|
|
2436
2409
|
.ribbon-command { min-width:90px; }
|
|
2437
2410
|
.workspace { display:block; overflow:auto; }
|
|
2438
2411
|
.page-rail { width:100%; max-height:190px; border-right:0; border-bottom:1px solid var(--_vv-border); }
|
|
@@ -2449,35 +2422,22 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2449
2422
|
.notes-strip button { min-height:44px; }
|
|
2450
2423
|
.status { flex-wrap:wrap; gap:0; padding:4px 8px; }
|
|
2451
2424
|
.status-message { width:100%; min-height:18px; }
|
|
2452
|
-
.zoom-controls { justify-content:flex-end; width:100%; }
|
|
2453
|
-
.zoom-controls button { min-height:44px; height:44px; min-width:44px; font-size:11px; }
|
|
2454
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; font-size:20px; }
|
|
2455
|
-
.zoom { font-size:11px; }
|
|
2456
2425
|
}
|
|
2457
2426
|
@media(pointer:coarse) {
|
|
2458
2427
|
button,select,input,textarea { min-height:44px; }
|
|
2459
2428
|
button { min-width:44px; }
|
|
2460
2429
|
summary,.layer-controls label { min-height:44px; line-height:44px; }
|
|
2461
|
-
.ribbon-primary { min-height:52px; height:52px; }
|
|
2462
|
-
.page-picker select,.page-stepper button { min-height:44px; height:44px; }
|
|
2463
|
-
.page-stepper button { min-width:44px; }
|
|
2464
2430
|
.ribbon-tabs { min-height:44px; height:44px; }
|
|
2465
2431
|
.ribbon-tabs button { min-height:44px; }
|
|
2466
2432
|
.ribbon-content { min-height:96px; }
|
|
2467
2433
|
.search-group { width:380px; max-width:100%; }
|
|
2468
|
-
.
|
|
2469
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; }
|
|
2434
|
+
.notes-strip button { min-height:44px; height:44px; }
|
|
2470
2435
|
.layer-controls input { min-height:20px; }
|
|
2471
2436
|
}
|
|
2472
|
-
/* Suite ribbon: tabs lead directly into command groups
|
|
2437
|
+
/* Suite ribbon: tabs lead directly into Visio command groups. */
|
|
2473
2438
|
.ribbon-tabs { height:40px; }
|
|
2474
2439
|
.ribbon-tabs button { min-height:39px; padding:8px 16px; }
|
|
2475
2440
|
.ribbon-content { min-height:104px; padding:4px 4px; }
|
|
2476
|
-
.ribbon-primary { height:auto; min-height:92px; width:238px; flex:none; flex-wrap:wrap; justify-content:center; gap:2px; padding:10px 12px 18px; border-right:1px solid var(--_vv-border); position:relative; order:1; }
|
|
2477
|
-
.ribbon-primary .page-picker { flex-direction:column; align-items:start; gap:5px; }
|
|
2478
|
-
.ribbon-primary .page-picker select { width:160px; max-width:100%; }
|
|
2479
|
-
.ribbon-primary .spacer { display:none; }
|
|
2480
|
-
.ribbon-primary .local-label { position:absolute; bottom:4px; left:0; width:100%; text-align:center; }
|
|
2481
2441
|
.search-group { order:2; width:320px; }
|
|
2482
2442
|
.ribbon-hint { order:3; padding:10px 18px; }
|
|
2483
2443
|
.ribbon-command { min-width:82px; }
|
|
@@ -2486,13 +2446,9 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2486
2446
|
.ribbon-tabs { height:44px; }
|
|
2487
2447
|
.ribbon-tabs button { min-height:44px; }
|
|
2488
2448
|
.ribbon-content { min-height:54px; padding:4px 0; }
|
|
2489
|
-
.ribbon-primary { min-height:44px; height:44px; width:100%; padding:0 8px; border:0; flex-wrap:nowrap; gap:4px; order:0; }
|
|
2490
|
-
.ribbon-primary .page-picker { flex-direction:row; flex:1; align-items:center; gap:8px; }
|
|
2491
|
-
.ribbon-primary .page-picker select { flex:1; width:100%; min-width:0; }
|
|
2492
2449
|
.search-group { flex:0 0 100%; width:100%; min-height:54px; }
|
|
2493
2450
|
.search-feedback,.group-label { display:none; }
|
|
2494
2451
|
.ribbon-content { overflow-x:auto; scroll-snap-type:x mandatory; }
|
|
2495
|
-
.ribbon-primary,.search-group { scroll-snap-align:start; }
|
|
2496
2452
|
.workspace { display:flex; position:relative; overflow:hidden; }
|
|
2497
2453
|
.viewport { flex:1; width:100%; height:100%; min-height:0; padding:16px 4px; }
|
|
2498
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; }
|
|
@@ -2521,9 +2477,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2521
2477
|
.status-message { flex:1; }
|
|
2522
2478
|
@media(max-width:760px) {
|
|
2523
2479
|
#home-panel { overflow:visible; min-height:54px; padding:4px; }
|
|
2524
|
-
.ribbon-primary { width:calc(100% - 92px); min-width:0; flex:1; padding:0 4px; }
|
|
2525
|
-
.ribbon-primary .page-picker { font-size:0; gap:0; }
|
|
2526
|
-
.ribbon-primary .page-picker select { font-size:12px; }
|
|
2527
2480
|
.ribbon-tools { display:block; flex:0 0 84px; width:84px; order:1; }
|
|
2528
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; }
|
|
2529
2482
|
.ribbon-tools>summary::-webkit-details-marker { display:none; }
|
|
@@ -2535,14 +2488,347 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2535
2488
|
.ribbon-tools .search-group { order:1; flex:auto; }
|
|
2536
2489
|
.ribbon-tools .search-feedback { display:block; }
|
|
2537
2490
|
.status { flex-wrap:nowrap; gap:4px; min-height:48px; }
|
|
2538
|
-
.zoom-controls { width:auto; flex:1; gap:0; }
|
|
2539
|
-
.zoom-divider { margin:0; }
|
|
2540
2491
|
.status .notes-strip { display:none; }
|
|
2541
2492
|
}
|
|
2542
2493
|
@media(pointer:coarse) { .ribbon-tabs {height:45px;min-height:45px;} .ribbon-tabs button {min-height:44px;height:44px;} }
|
|
2543
2494
|
@media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
|
|
2544
2495
|
`;
|
|
2545
2496
|
//#endregion
|
|
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
|
|
2546
2832
|
//#region src/edit-error.ts
|
|
2547
2833
|
/** Core error codes remain visible for a refused transaction; document content is plain text. */
|
|
2548
2834
|
function editErrorMessage(error) {
|
|
@@ -2830,59 +3116,13 @@ var ViewerEditControls = class {
|
|
|
2830
3116
|
}
|
|
2831
3117
|
};
|
|
2832
3118
|
//#endregion
|
|
2833
|
-
//#region src/viewer-search.ts
|
|
2834
|
-
var searchTemplate = `<div class="search-controls" role="search" aria-label="Diagram text search"><label>Find <input type="search" aria-label="Search diagram text" placeholder="Text across pages" maxlength="${TEXT_SEARCH_LIMITS.queryCharacters}" autocomplete="off" spellcheck="false" aria-describedby="search-status"></label><button type="button" data-action="search-previous" aria-label="Previous matching shape">Previous</button><button type="button" data-action="search-next" aria-label="Next matching shape">Next</button><span id="search-status" role="status" aria-live="polite" aria-atomic="true"></span></div>`;
|
|
2835
|
-
function searchControls(root) {
|
|
2836
|
-
return {
|
|
2837
|
-
input: root.querySelector("input[type=\"search\"]"),
|
|
2838
|
-
status: root.querySelector("#search-status"),
|
|
2839
|
-
previous: root.querySelector("[data-action=\"search-previous\"]"),
|
|
2840
|
-
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2841
|
-
};
|
|
2842
|
-
}
|
|
2843
|
-
function renderSearchControls(controls, state) {
|
|
2844
|
-
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2845
|
-
controls.input.disabled = !state.document?.pages.length;
|
|
2846
|
-
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2847
|
-
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2848
|
-
const result = state.search.results[state.search.activeIndex];
|
|
2849
|
-
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2850
|
-
}
|
|
2851
|
-
//#endregion
|
|
2852
3119
|
//#region src/viewer-chrome.ts
|
|
2853
|
-
/**
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
<button type="button" id="view-tab" role="tab" aria-selected="false" aria-controls="view-panel" tabindex="-1" data-tab="view">View</button>
|
|
2860
|
-
</div>
|
|
2861
|
-
<div class="ribbon-content" id="home-panel" role="tabpanel" aria-labelledby="home-tab">
|
|
2862
|
-
<details class="ribbon-tools" open><summary>Tools <span aria-hidden="true">⌄</span></summary><div class="tools-content"><div class="ribbon-group search-group">${searchTemplate}<span class="group-label">Find in drawing</span></div>
|
|
2863
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2864
|
-
<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>
|
|
2865
|
-
<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>
|
|
2866
|
-
<button class="ribbon-command" type="button" data-chrome="layers"><span class="command-icon" data-icon="layers" aria-hidden="true"></span><span>Layers</span></button>
|
|
2867
|
-
</div><span class="group-label">Inspect and edit</span></div>
|
|
2868
|
-
</div></details>
|
|
2869
|
-
<div class="ribbon-primary">
|
|
2870
|
-
<label class="page-picker">Page <select aria-label="Page"></select></label>
|
|
2871
|
-
<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>
|
|
2872
|
-
<span class="spacer"></span><span class="local-label">Local workspace</span>
|
|
2873
|
-
</div>
|
|
2874
|
-
<p class="ribbon-hint">Select a shape to inspect its details.<br>Text editing is experimental.</p>
|
|
2875
|
-
</div>
|
|
2876
|
-
<div class="ribbon-content" id="view-panel" role="tabpanel" aria-labelledby="view-tab" hidden>
|
|
2877
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2878
|
-
<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>
|
|
2879
|
-
<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>
|
|
2880
|
-
<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>
|
|
2881
|
-
</div><span class="group-label">Workspace panes</span></div>
|
|
2882
|
-
<p class="ribbon-hint">Canvas shortcuts: + / − to zoom, 0 to fit.<br>Arrow keys move between focused shapes.</p>
|
|
2883
|
-
</div>
|
|
2884
|
-
</div>
|
|
2885
|
-
<div class="workspace">
|
|
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">
|
|
2886
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>
|
|
2887
3127
|
<div class="viewport" tabindex="0" role="region" aria-label="Diagram canvas"></div>
|
|
2888
3128
|
<aside id="inspector-pane" class="inspector-pane" aria-label="Drawing inspector">
|
|
@@ -2895,14 +3135,15 @@ var viewerChromeTemplate = `
|
|
|
2895
3135
|
<details class="notes inspector-card"><summary>Compatibility notes</summary><ul></ul></details>
|
|
2896
3136
|
</div>
|
|
2897
3137
|
</aside>
|
|
2898
|
-
</div
|
|
2899
|
-
<div class="status"><div class="status-message" role="status"><span data-status></span><span data-diagnostics></span></div><div class="notes-strip"><button type="button" data-chrome="notes" aria-controls="inspector-pane" aria-expanded="false">Notes<span class="notes-count" data-note-count></span></button><span>Files stay in your browser</span></div><slot name="workspace-footer"></slot><div class="zoom-controls" role="group" aria-label="Canvas zoom"><button type="button" data-action="fit">Fit page</button><button type="button" data-action="actual">100%</button><span class="zoom-divider"></span><button type="button" data-action="out" aria-label="Zoom out">−</button><output class="zoom" aria-label="Zoom level">100%</output><button type="button" data-action="in" aria-label="Zoom in">+</button></div></div>`;
|
|
3138
|
+
</div>`;
|
|
2900
3139
|
/** Presentation state stays local; page navigation uses the same controller as every binding. */
|
|
2901
3140
|
var ViewerChrome = class {
|
|
2902
3141
|
#root;
|
|
2903
3142
|
#controller;
|
|
2904
3143
|
#document;
|
|
2905
3144
|
#pageList;
|
|
3145
|
+
#pageTabs;
|
|
3146
|
+
#tools;
|
|
2906
3147
|
#rail;
|
|
2907
3148
|
#inspector;
|
|
2908
3149
|
#notes;
|
|
@@ -2914,6 +3155,8 @@ var ViewerChrome = class {
|
|
|
2914
3155
|
this.#root = root;
|
|
2915
3156
|
this.#controller = controller;
|
|
2916
3157
|
this.#pageList = root.querySelector(".page-list");
|
|
3158
|
+
this.#pageTabs = root.querySelector("office-ui-tab-strip");
|
|
3159
|
+
this.#tools = root.querySelector(".ribbon-tools");
|
|
2917
3160
|
this.#rail = root.querySelector(".page-rail");
|
|
2918
3161
|
this.#inspector = root.querySelector(".inspector-pane");
|
|
2919
3162
|
this.#notes = root.querySelector(".notes");
|
|
@@ -2924,7 +3167,7 @@ var ViewerChrome = class {
|
|
|
2924
3167
|
this.#responsive = view?.matchMedia?.("(max-width: 980px)");
|
|
2925
3168
|
const compact = view?.matchMedia?.("(max-width: 760px)");
|
|
2926
3169
|
this.#compact = compact;
|
|
2927
|
-
const tools = this.#
|
|
3170
|
+
const tools = this.#tools;
|
|
2928
3171
|
const toolLayout = () => {
|
|
2929
3172
|
tools.open = !compact?.matches;
|
|
2930
3173
|
};
|
|
@@ -2951,22 +3194,16 @@ var ViewerChrome = class {
|
|
|
2951
3194
|
this.#root.addEventListener("click", (event) => {
|
|
2952
3195
|
const button = event.target?.closest?.("button");
|
|
2953
3196
|
if (!button || button.disabled) return;
|
|
2954
|
-
if (compact?.matches && button.dataset.chrome) tools.open = false;
|
|
2955
3197
|
const tab = button.dataset.tab;
|
|
2956
|
-
if (tab === "home" || tab === "view") this
|
|
3198
|
+
if (tab === "home" || tab === "view") this.showTab(tab);
|
|
2957
3199
|
if (button.dataset.pageIndex !== void 0) this.#controller.setPage(Number(button.dataset.pageIndex));
|
|
2958
3200
|
const action = button.dataset.chrome;
|
|
2959
|
-
if (action === "
|
|
2960
|
-
if (action === "
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
if (action === "inspector") {
|
|
2966
|
-
this.#inspectorManuallyToggled = true;
|
|
2967
|
-
this.#setPane("inspector", this.#inspector.hidden);
|
|
2968
|
-
}
|
|
2969
|
-
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);
|
|
2970
3207
|
}, options);
|
|
2971
3208
|
this.#root.addEventListener("keydown", (event) => {
|
|
2972
3209
|
const key = event;
|
|
@@ -2983,11 +3220,11 @@ var ViewerChrome = class {
|
|
|
2983
3220
|
if (key.key === "ArrowUp" || key.key === "ArrowDown") return;
|
|
2984
3221
|
key.preventDefault();
|
|
2985
3222
|
const tab = key.key === "Home" ? "home" : key.key === "End" ? "view" : button.dataset.tab === "home" ? "view" : "home";
|
|
2986
|
-
this
|
|
3223
|
+
this.showTab(tab);
|
|
2987
3224
|
this.#root.querySelector(`[data-tab="${tab}"]`).focus();
|
|
2988
3225
|
} else if (button.dataset.pageIndex !== void 0) {
|
|
2989
3226
|
key.preventDefault();
|
|
2990
|
-
const pages = [...this.#pageList.querySelectorAll("
|
|
3227
|
+
const pages = [...this.#pageList.querySelectorAll("[data-page-index]")];
|
|
2991
3228
|
const current = Number(button.dataset.pageIndex);
|
|
2992
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)));
|
|
2993
3230
|
this.#controller.setPage(next);
|
|
@@ -2997,7 +3234,7 @@ var ViewerChrome = class {
|
|
|
2997
3234
|
this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
|
|
2998
3235
|
return () => events.abort();
|
|
2999
3236
|
}
|
|
3000
|
-
|
|
3237
|
+
showTab(tab) {
|
|
3001
3238
|
for (const button of this.#root.querySelectorAll("[data-tab]")) {
|
|
3002
3239
|
const selected = button.dataset.tab === tab;
|
|
3003
3240
|
button.setAttribute("aria-selected", String(selected));
|
|
@@ -3005,27 +3242,41 @@ var ViewerChrome = class {
|
|
|
3005
3242
|
}
|
|
3006
3243
|
for (const panel of this.#root.querySelectorAll("[role=\"tabpanel\"]")) panel.hidden = panel.id !== `${tab}-panel`;
|
|
3007
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
|
+
}
|
|
3008
3255
|
#setPane(pane, visible) {
|
|
3009
3256
|
if (visible && this.#compact?.matches) {
|
|
3010
3257
|
const other = pane === "pages" ? "inspector" : "pages";
|
|
3011
3258
|
(other === "pages" ? this.#rail : this.#inspector).hidden = true;
|
|
3012
|
-
this.#
|
|
3259
|
+
this.#command(other).setAttribute("pressed", "false");
|
|
3013
3260
|
}
|
|
3014
3261
|
(pane === "pages" ? this.#rail : this.#inspector).hidden = !visible;
|
|
3015
|
-
this.#
|
|
3262
|
+
this.#command(pane).setAttribute("pressed", String(visible));
|
|
3016
3263
|
this.#syncNotes();
|
|
3017
3264
|
}
|
|
3018
3265
|
#syncNotes() {
|
|
3019
3266
|
this.#root.querySelector(".notes-strip button").setAttribute("aria-expanded", String(!this.#inspector.hidden && this.#notes.open));
|
|
3020
3267
|
}
|
|
3021
|
-
|
|
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;
|
|
3022
3271
|
this.#inspectorManuallyToggled = true;
|
|
3023
3272
|
this.#setPane("inspector", true);
|
|
3024
3273
|
const selector = kind === "notes" ? ".notes" : kind === "edit" ? ".edit-controls" : kind === "layers" ? ".layer-controls" : ".shape-inspector";
|
|
3025
3274
|
const panel = this.#root.querySelector(selector);
|
|
3026
3275
|
if (panel.hidden) return;
|
|
3027
3276
|
panel.open = true;
|
|
3028
|
-
panel.querySelector("
|
|
3277
|
+
const text = panel.querySelector("textarea");
|
|
3278
|
+
if (focusText && text && !text.disabled) text.focus();
|
|
3279
|
+
else panel.querySelector("summary").focus();
|
|
3029
3280
|
panel.scrollIntoView?.({
|
|
3030
3281
|
block: "nearest",
|
|
3031
3282
|
inline: "nearest"
|
|
@@ -3066,14 +3317,21 @@ var ViewerChrome = class {
|
|
|
3066
3317
|
fragment.append(item);
|
|
3067
3318
|
}
|
|
3068
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
|
+
}));
|
|
3069
3325
|
if (restoreFocus !== void 0) [...this.#pageList.querySelectorAll("button")].find((button) => button.dataset.pageIndex === restoreFocus)?.focus();
|
|
3070
3326
|
}
|
|
3327
|
+
this.#pageTabs.selected = page ? String(state.pageIndex) : "";
|
|
3071
3328
|
for (const button of this.#pageList.querySelectorAll("button")) {
|
|
3072
3329
|
const selected = Number(button.dataset.pageIndex) === state.pageIndex;
|
|
3073
3330
|
if (selected) button.setAttribute("aria-current", "page");
|
|
3074
3331
|
else button.removeAttribute("aria-current");
|
|
3075
3332
|
button.tabIndex = selected ? 0 : -1;
|
|
3076
3333
|
}
|
|
3334
|
+
this.#root.querySelector("[data-page-status]").setAttribute("value", page ? `Page ${state.pageIndex + 1} of ${state.document.pages.length}` : "");
|
|
3077
3335
|
const count = state.document?.pages.length ?? 0;
|
|
3078
3336
|
this.#root.querySelector("[data-page-count]").textContent = String(count);
|
|
3079
3337
|
this.#root.querySelector(".page-empty").hidden = count > 0;
|
|
@@ -3081,17 +3339,405 @@ var ViewerChrome = class {
|
|
|
3081
3339
|
this.#root.querySelector("[data-page-size]").textContent = page ? `${page.width} × ${page.height} in` : "No page";
|
|
3082
3340
|
this.#root.querySelector("[data-page-shapes]").textContent = page ? `${page.shapes.length} top-level` : "No shapes";
|
|
3083
3341
|
this.#root.querySelector(".selection-hint").hidden = !!state.selectedShape;
|
|
3084
|
-
this.#
|
|
3085
|
-
this.#
|
|
3086
|
-
this.#
|
|
3087
|
-
this.#
|
|
3088
|
-
this.#root.querySelector("
|
|
3089
|
-
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;
|
|
3090
3347
|
this.#root.querySelector("[data-note-count]").textContent = noteCount ? ` · ${noteCount}` : "";
|
|
3091
3348
|
this.#syncNotes();
|
|
3092
3349
|
}
|
|
3093
3350
|
};
|
|
3094
3351
|
//#endregion
|
|
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
|
|
3095
3741
|
//#region src/export-svg.ts
|
|
3096
3742
|
var MAX_SVG_EXPORT_BYTES = 16777216;
|
|
3097
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.";
|
|
@@ -3702,8 +4348,10 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3702
4348
|
#root;
|
|
3703
4349
|
#disposeInputs;
|
|
3704
4350
|
#viewport;
|
|
3705
|
-
#
|
|
3706
|
-
#
|
|
4351
|
+
#zoomSlider;
|
|
4352
|
+
#shapeStatus;
|
|
4353
|
+
#announcement;
|
|
4354
|
+
#commands;
|
|
3707
4355
|
#status;
|
|
3708
4356
|
#diagnostics;
|
|
3709
4357
|
#toolbar;
|
|
@@ -3736,10 +4384,14 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3736
4384
|
constructor() {
|
|
3737
4385
|
super();
|
|
3738
4386
|
this.#root = this.attachShadow({ mode: "open" });
|
|
3739
|
-
|
|
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));
|
|
3740
4392
|
this.#viewport = this.#root.querySelector(".viewport");
|
|
3741
|
-
this.#
|
|
3742
|
-
this.#
|
|
4393
|
+
this.#zoomSlider = this.#root.querySelector("office-ui-zoom-slider");
|
|
4394
|
+
this.#shapeStatus = this.#root.querySelector("[data-shape-status]");
|
|
3743
4395
|
this.#status = this.#root.querySelector("[data-status]");
|
|
3744
4396
|
this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
|
|
3745
4397
|
this.#toolbar = this.#root.querySelector(".toolbar");
|
|
@@ -3750,6 +4402,23 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3750
4402
|
this.#notesPanel = this.#root.querySelector(".notes");
|
|
3751
4403
|
this.#inspector = this.#root.querySelector(".shape-inspector");
|
|
3752
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
|
+
});
|
|
3753
4422
|
this.#disposeInputs = this.#wireInputs();
|
|
3754
4423
|
this.#unsubscribe = this.controller.subscribe((state) => this.#render(state));
|
|
3755
4424
|
this.#eventUnsubscribe = this.controller.onEvent((name, detail) => {
|
|
@@ -3823,13 +4492,24 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3823
4492
|
}
|
|
3824
4493
|
fit() {
|
|
3825
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) {
|
|
3826
4499
|
const page = this.document?.pages[this.pageIndex];
|
|
3827
4500
|
if (!page) return;
|
|
3828
4501
|
const style = this.ownerDocument.defaultView?.getComputedStyle(this.#viewport);
|
|
3829
4502
|
const padding = (value) => Number.parseFloat(value ?? "") || 0;
|
|
3830
4503
|
const width = Math.max(1, this.#viewport.clientWidth - padding(style?.paddingLeft) - padding(style?.paddingRight));
|
|
3831
4504
|
const height = Math.max(1, this.#viewport.clientHeight - padding(style?.paddingTop) - padding(style?.paddingBottom));
|
|
3832
|
-
|
|
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);
|
|
3833
4513
|
}
|
|
3834
4514
|
setLayerVisibility(pageId, layerId, visible) {
|
|
3835
4515
|
this.#assertAlive();
|
|
@@ -3899,16 +4579,18 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3899
4579
|
}
|
|
3900
4580
|
#wireInputs() {
|
|
3901
4581
|
const disposeChrome = this.#chrome.wire();
|
|
4582
|
+
const disposeCommands = this.#commands.wire();
|
|
3902
4583
|
const disposeEdit = this.#edit.wire();
|
|
3903
4584
|
const disposeLayers = wireLayerControls(this.#layers, this.controller);
|
|
3904
4585
|
const disposeInputs = wireViewerInputs({
|
|
3905
4586
|
viewport: this.#viewport,
|
|
3906
4587
|
commandRoot: this.#root,
|
|
3907
|
-
|
|
4588
|
+
zoomSlider: this.#zoomSlider,
|
|
3908
4589
|
searchInput: this.#search.input
|
|
3909
|
-
}, this.controller, () => this
|
|
4590
|
+
}, this.controller, (mode) => this.#fit(mode));
|
|
3910
4591
|
return () => {
|
|
3911
4592
|
disposeChrome();
|
|
4593
|
+
disposeCommands();
|
|
3912
4594
|
disposeInputs();
|
|
3913
4595
|
disposeLayers();
|
|
3914
4596
|
disposeEdit();
|
|
@@ -3927,14 +4609,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3927
4609
|
this.#document = state.document;
|
|
3928
4610
|
this.#renderedPage = state.pageIndex;
|
|
3929
4611
|
this.#renderedLayerOverrides = state.layerVisibilityOverrides;
|
|
3930
|
-
this.#
|
|
3931
|
-
for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
|
|
3932
|
-
const option = document.createElement("option");
|
|
3933
|
-
option.value = String(index);
|
|
3934
|
-
option.textContent = candidate.name + (candidate.isBackground ? " (background)" : "");
|
|
3935
|
-
this.#pageSelect.append(option);
|
|
3936
|
-
}
|
|
3937
|
-
this.#pageSelect.value = String(state.pageIndex);
|
|
4612
|
+
this.#announcement = void 0;
|
|
3938
4613
|
this.#renderWarnings = [];
|
|
3939
4614
|
if (state.document && page) {
|
|
3940
4615
|
const result = renderPage(state.document, page, { layerVisibilityOverrides: state.layerVisibilityOverrides });
|
|
@@ -3961,6 +4636,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3961
4636
|
if (svg && page) {
|
|
3962
4637
|
svg.style.width = `${page.width * 96 * state.zoom}px`;
|
|
3963
4638
|
svg.style.height = `${page.height * 96 * state.zoom}px`;
|
|
4639
|
+
svg.style.setProperty("--vv-inch", `${96 * state.zoom}px`);
|
|
3964
4640
|
}
|
|
3965
4641
|
const searchResult = state.search.results[state.search.activeIndex];
|
|
3966
4642
|
const selection = state.selectedShape;
|
|
@@ -3988,16 +4664,20 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3988
4664
|
this.#renderedSelection = selection ? { ...selection } : null;
|
|
3989
4665
|
}
|
|
3990
4666
|
if (!searchResult) this.#revealedSearchResult = void 0;
|
|
3991
|
-
this.#
|
|
3992
|
-
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)}` : "");
|
|
3993
4672
|
for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
|
|
3994
4673
|
renderSearchControls(this.#search, state);
|
|
3995
4674
|
this.#edit.render(state);
|
|
3996
4675
|
if (changed) renderLayerControls(this.#layers, state);
|
|
3997
4676
|
else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
|
|
3998
4677
|
this.#chrome.render(state, this.#notes.children.length);
|
|
4678
|
+
this.#commands.render(state);
|
|
3999
4679
|
this.#viewport.setAttribute("aria-busy", String(state.loading || state.edit.busy));
|
|
4000
|
-
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");
|
|
4001
4681
|
const warnings = (state.document?.diagnostics.length ?? 0) + this.#renderWarnings.length;
|
|
4002
4682
|
this.#diagnostics.textContent = warnings ? `${this.#notes.children.length} compatibility notes` : "Local-only viewing";
|
|
4003
4683
|
}
|
|
@@ -4207,6 +4887,23 @@ function viewerHandle(current) {
|
|
|
4207
4887
|
}
|
|
4208
4888
|
};
|
|
4209
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
|
+
}
|
|
4210
4907
|
//#endregion
|
|
4211
4908
|
//#region packages/bindings/src/react.ts
|
|
4212
4909
|
/** React owns only the host and lifecycle; the shared element owns all viewer behavior. */
|
|
@@ -4232,5 +4929,13 @@ var VisioViewer = forwardRef(function VisioViewer(props, ref) {
|
|
|
4232
4929
|
"aria-label": props["aria-label"]
|
|
4233
4930
|
});
|
|
4234
4931
|
});
|
|
4932
|
+
/**
|
|
4933
|
+
* Reactive viewer state (page, zoom, selection, search, edit and undo state) for the `ref`
|
|
4934
|
+
* given to `<VisioViewer>`, through React's external-store hook. `null` until mounted.
|
|
4935
|
+
*/
|
|
4936
|
+
function useVisioViewerState(ref) {
|
|
4937
|
+
const source = useMemo(() => viewerStateSource(() => ref.current), [ref]);
|
|
4938
|
+
return useSyncExternalStore(source.subscribe, source.getSnapshot, () => null);
|
|
4939
|
+
}
|
|
4235
4940
|
//#endregion
|
|
4236
|
-
export { MAX_SVG_EXPORT_BYTES, PRINT_SNAPSHOT_LIMITS, TEXT_SEARCH_LIMITS, VIEWER_LAYER_LIMITS, ViewerController, VisioViewer, VisioViewerElement, compatibilityNotes, compatibilityText, createPrintSnapshot, createWorkerParser, eventKeys, exportPageSvg, mountViewer, propertyKeys, registerVisioViewer, renderPage };
|
|
4941
|
+
export { MAX_SVG_EXPORT_BYTES, PRINT_SNAPSHOT_LIMITS, TEXT_SEARCH_LIMITS, VIEWER_LAYER_LIMITS, ViewerController, VisioViewer, VisioViewerElement, compatibilityNotes, compatibilityText, createPrintSnapshot, createWorkerParser, eventKeys, exportPageSvg, mountViewer, propertyKeys, registerVisioViewer, renderPage, useVisioViewerState };
|