visio-solid-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 +84 -4
- package/dist/assets/{edit-worker-DRgXgRud.js → edit-worker-BcXrV_St.js} +203 -31
- package/dist/index.js +954 -138
- package/dist/index.server.js +953 -137
- package/dist/third-party/Inter-OFL.txt +92 -0
- package/dist/types/packages/bindings/src/common.d.ts +11 -0
- package/dist/types/packages/bindings/src/solid.d.ts +8 -3
- package/dist/types/src/office-ui.d.ts +2 -0
- package/dist/types/src/ribbon-action.d.ts +34 -0
- package/dist/types/src/ribbon-home.d.ts +6 -0
- package/dist/types/src/ribbon-parts.d.ts +20 -0
- package/dist/types/src/ribbon-router.d.ts +17 -0
- package/dist/types/src/ribbon-view.d.ts +2 -0
- package/dist/types/src/ribbon.d.ts +6 -0
- package/dist/types/src/status-bar.d.ts +7 -0
- package/dist/types/src/styles/index.d.ts +5 -0
- package/dist/types/src/styles.d.ts +1 -1
- package/dist/types/src/viewer-chrome.d.ts +11 -2
- package/dist/types/src/viewer-commands.d.ts +31 -0
- package/dist/types/src/viewer-draw-tool.d.ts +17 -0
- package/dist/types/src/viewer-input.d.ts +4 -2
- package/dist/types/src/viewer-search.d.ts +2 -1
- package/package.json +4 -3
package/dist/index.server.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { VISIO_RASTER_IMAGE_LIMITS, VISIO_VISIBILITY_LIMITS, getVisioPageLayers, inspectVisioRasterImage, parseVsdx, resolveVisioPageVisibility, validateVisioForeignVector } from "ooxml-core/visio";
|
|
2
|
+
import { registerControls } from "ooxml-ui/controls";
|
|
3
|
+
import { getIcon, registerIcon } from "ooxml-ui/icons";
|
|
2
4
|
import { escape, ssr, ssrAttribute, ssrHydrationKey, ssrStyle } from "solid-js/web";
|
|
3
5
|
import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
|
|
4
6
|
export * from "visio-core";
|
|
@@ -2136,7 +2138,7 @@ function selection(target) {
|
|
|
2136
2138
|
}
|
|
2137
2139
|
/** Own every DOM listener and abort them together when the surface is disposed. */
|
|
2138
2140
|
function wireViewerInputs(controls, controller, fit) {
|
|
2139
|
-
const { viewport, commandRoot,
|
|
2141
|
+
const { viewport, commandRoot, zoomSlider, searchInput } = controls;
|
|
2140
2142
|
const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
2141
2143
|
const options = { signal: events.signal };
|
|
2142
2144
|
searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
|
|
@@ -2152,13 +2154,9 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2152
2154
|
controller.setSearchQuery("");
|
|
2153
2155
|
}
|
|
2154
2156
|
}, options);
|
|
2155
|
-
|
|
2157
|
+
zoomSlider.addEventListener("input", () => controller.setZoom(zoomSlider.value / 100), options);
|
|
2156
2158
|
commandRoot.addEventListener("click", (event) => {
|
|
2157
2159
|
const action = event.target?.closest?.("button")?.dataset.action;
|
|
2158
|
-
if (action === "in") controller.setZoom(controller.state.zoom * 1.25);
|
|
2159
|
-
if (action === "out") controller.setZoom(controller.state.zoom / 1.25);
|
|
2160
|
-
if (action === "fit") fit();
|
|
2161
|
-
if (action === "actual") controller.setZoom(1);
|
|
2162
2160
|
if (action === "search-next") controller.nextSearchResult();
|
|
2163
2161
|
if (action === "search-previous") controller.previousSearchResult();
|
|
2164
2162
|
}, options);
|
|
@@ -2197,7 +2195,7 @@ function wireViewerInputs(controls, controller, fit) {
|
|
|
2197
2195
|
}
|
|
2198
2196
|
if (event.key === "0") {
|
|
2199
2197
|
event.preventDefault();
|
|
2200
|
-
fit();
|
|
2198
|
+
fit("page");
|
|
2201
2199
|
}
|
|
2202
2200
|
}, options);
|
|
2203
2201
|
return () => events.abort();
|
|
@@ -2301,13 +2299,7 @@ button:disabled { opacity:.4; cursor:default; }
|
|
|
2301
2299
|
button:focus-visible,select:focus-visible,input:focus-visible,textarea:focus-visible,summary:focus-visible,[tabindex]:focus-visible { outline:2px solid var(--vv-focus,var(--_vv-accent)); outline-offset:2px; }
|
|
2302
2300
|
button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
|
|
2303
2301
|
.toolbar { flex:none; min-width:0; border-bottom:1px solid var(--_vv-border); background:var(--_vv-surface); }
|
|
2304
|
-
.ribbon-primary { height:32px; display:flex; align-items:center; gap:6px; padding:2px 8px; }
|
|
2305
|
-
.page-picker { display:flex; min-width:0; align-items:center; gap:8px; color:var(--_vv-muted); font-size:11px; }
|
|
2306
|
-
.page-picker select { width:190px; max-width:42vw; height:26px; min-height:26px; padding:2px 6px; font-size:11px; color:var(--_vv-ink); }
|
|
2307
|
-
.page-stepper { display:flex; align-items:center; gap:2px; }
|
|
2308
|
-
.page-stepper button { width:26px; height:26px; min-height:26px; padding:0; font-size:18px; line-height:22px; background:transparent; border-color:transparent; }
|
|
2309
2302
|
.spacer { flex:1; }
|
|
2310
|
-
.local-label { color:var(--_vv-muted); font-size:10px; white-space:nowrap; }
|
|
2311
2303
|
.ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
|
|
2312
2304
|
.ribbon-tabs button { position:relative; min-height:34px; padding:7px 14px; border:0; border-radius:0; background:transparent; color:var(--_vv-muted); font-size:12px; font-weight:500; }
|
|
2313
2305
|
.ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
|
|
@@ -2317,15 +2309,6 @@ button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-acce
|
|
|
2317
2309
|
.group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
|
|
2318
2310
|
.ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
|
|
2319
2311
|
.ribbon-command { display:flex; flex-direction:column; justify-content:center; align-items:center; gap:2px; min-width:70px; padding:2px 6px; border-color:transparent; background:transparent; font-size:11px; white-space:nowrap; }
|
|
2320
|
-
.command-icon { display:grid; place-items:center; width:26px; height:26px; color:var(--_vv-accent); }
|
|
2321
|
-
.command-icon::before { content:""; width:24px; height:24px; background:currentColor; mask:var(--command-symbol) center / contain no-repeat; }
|
|
2322
|
-
[data-icon="selection"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22m5%203%2013%209-6%201-3%206z%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2323
|
-
[data-icon="edit"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%205h16M12%205v14M8%2019h8%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2324
|
-
[data-icon="layers"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22m3%208%209-5%209%205-9%205zM3%2012l9%205%209-5M3%2016l9%205%209-5%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2325
|
-
[data-icon="pages"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%225%22%20y%3D%223%22%20width%3D%2214%22%20height%3D%2218%22%20rx%3D%221%22%2F%3E%3Cpath%20d%3D%22M8%208h8M8%2012h8M8%2016h5%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2326
|
-
[data-icon="inspector"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%223%22%20y%3D%224%22%20width%3D%2218%22%20height%3D%2216%22%20rx%3D%221%22%2F%3E%3Cpath%20d%3D%22M14%204v16M17%208h1M17%2012h1M17%2016h1%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2327
|
-
[data-icon="notes"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%204h16v12H9l-5%204z%22%2F%3E%3Cpath%20d%3D%22M8%208h8M8%2012h5%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2328
|
-
.ribbon-command[data-chrome="edit"] .command-icon { font-family:ui-serif,Georgia,serif; text-decoration:underline; text-decoration-thickness:1px; text-underline-offset:3px; }
|
|
2329
2312
|
.ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
|
|
2330
2313
|
.search-group { width:330px; }
|
|
2331
2314
|
.search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
|
|
@@ -2397,11 +2380,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2397
2380
|
.status { flex:none; display:flex; align-items:center; justify-content:space-between; gap:12px; min-width:0; min-height:29px; padding:0 8px 0 12px; border-top:1px solid var(--_vv-border); color:var(--_vv-muted); background:var(--_vv-surface); font-size:10px; }
|
|
2398
2381
|
.status-message { display:flex; gap:16px; min-width:0; }
|
|
2399
2382
|
.status-message span { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
|
2400
|
-
.zoom-controls { display:flex; flex:none; align-items:center; gap:3px; }
|
|
2401
|
-
.zoom-controls button { min-height:24px; height:24px; padding:1px 6px; border-color:transparent; background:transparent; font-size:10px; }
|
|
2402
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:24px; padding:0; font-size:17px; line-height:20px; }
|
|
2403
|
-
.zoom-divider { width:1px; height:14px; background:var(--_vv-border); margin:0 3px; }
|
|
2404
|
-
.zoom { min-width:40px; font-size:10px; text-align:center; font-variant-numeric:tabular-nums; }
|
|
2405
2383
|
.empty { max-width:360px; margin:60px auto; padding:16px; text-align:center; color:var(--_vv-muted); font-size:12px; }
|
|
2406
2384
|
.empty strong { display:block; color:var(--_vv-ink); font-size:18px; font-weight:500; margin-bottom:8px; }
|
|
2407
2385
|
@media(max-width:1100px) { .ribbon-hint { display:none; } [data-diagnostics] { display:none; } }
|
|
@@ -2409,11 +2387,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2409
2387
|
button,select,input,textarea { min-height:44px; }
|
|
2410
2388
|
button { min-width:44px; }
|
|
2411
2389
|
summary { min-height:44px; line-height:44px; }
|
|
2412
|
-
.ribbon-primary { height:52px; padding:4px 8px; }
|
|
2413
|
-
.page-picker { flex:1; }
|
|
2414
|
-
.page-picker select { width:100%; max-width:none; min-height:44px; height:44px; font-size:12px; }
|
|
2415
|
-
.page-stepper button { width:44px; min-height:44px; height:44px; }
|
|
2416
|
-
.local-label,.ribbon-primary>.spacer { display:none; }
|
|
2417
2390
|
.ribbon-tabs { height:44px; }
|
|
2418
2391
|
.ribbon-tabs button { min-height:44px; padding:8px 16px; }
|
|
2419
2392
|
.ribbon-content { min-height:86px; }
|
|
@@ -2425,7 +2398,7 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2425
2398
|
.search-controls button { min-width:44px; font-size:0; padding:3px; }
|
|
2426
2399
|
.search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
|
|
2427
2400
|
.search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
|
|
2428
|
-
#home-panel>.ribbon-group:not(.search-group) { display:none; }
|
|
2401
|
+
#home-panel>.ribbon-group:not(.search-group):not(.undo-group) { display:none; }
|
|
2429
2402
|
.ribbon-command { min-width:90px; }
|
|
2430
2403
|
.workspace { display:block; overflow:auto; }
|
|
2431
2404
|
.page-rail { width:100%; max-height:190px; border-right:0; border-bottom:1px solid var(--_vv-border); }
|
|
@@ -2442,29 +2415,413 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
|
2442
2415
|
.notes-strip button { min-height:44px; }
|
|
2443
2416
|
.status { flex-wrap:wrap; gap:0; padding:4px 8px; }
|
|
2444
2417
|
.status-message { width:100%; min-height:18px; }
|
|
2445
|
-
.zoom-controls { justify-content:flex-end; width:100%; }
|
|
2446
|
-
.zoom-controls button { min-height:44px; height:44px; min-width:44px; font-size:11px; }
|
|
2447
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; font-size:20px; }
|
|
2448
|
-
.zoom { font-size:11px; }
|
|
2449
2418
|
}
|
|
2450
2419
|
@media(pointer:coarse) {
|
|
2451
2420
|
button,select,input,textarea { min-height:44px; }
|
|
2452
2421
|
button { min-width:44px; }
|
|
2453
2422
|
summary,.layer-controls label { min-height:44px; line-height:44px; }
|
|
2454
|
-
.ribbon-primary { min-height:52px; height:52px; }
|
|
2455
|
-
.page-picker select,.page-stepper button { min-height:44px; height:44px; }
|
|
2456
|
-
.page-stepper button { min-width:44px; }
|
|
2457
2423
|
.ribbon-tabs { min-height:44px; height:44px; }
|
|
2458
2424
|
.ribbon-tabs button { min-height:44px; }
|
|
2459
2425
|
.ribbon-content { min-height:96px; }
|
|
2460
2426
|
.search-group { width:380px; max-width:100%; }
|
|
2461
|
-
.
|
|
2462
|
-
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; }
|
|
2427
|
+
.notes-strip button { min-height:44px; height:44px; }
|
|
2463
2428
|
.layer-controls input { min-height:20px; }
|
|
2464
2429
|
}
|
|
2430
|
+
/* Suite ribbon: tabs lead directly into Visio command groups. */
|
|
2431
|
+
.ribbon-tabs { height:40px; }
|
|
2432
|
+
.ribbon-tabs button { min-height:39px; padding:8px 16px; }
|
|
2433
|
+
.ribbon-content { min-height:104px; padding:4px 4px; }
|
|
2434
|
+
.search-group { order:2; width:320px; }
|
|
2435
|
+
.ribbon-hint { order:3; padding:10px 18px; }
|
|
2436
|
+
.ribbon-command { min-width:82px; }
|
|
2437
|
+
.pane-close { display:none; }
|
|
2438
|
+
@media(max-width:760px) {
|
|
2439
|
+
.ribbon-tabs { height:44px; }
|
|
2440
|
+
.ribbon-tabs button { min-height:44px; }
|
|
2441
|
+
.ribbon-content { min-height:54px; padding:4px 0; }
|
|
2442
|
+
.search-group { flex:0 0 100%; width:100%; min-height:54px; }
|
|
2443
|
+
.search-feedback,.group-label { display:none; }
|
|
2444
|
+
.ribbon-content { overflow-x:auto; scroll-snap-type:x mandatory; }
|
|
2445
|
+
.workspace { display:flex; position:relative; overflow:hidden; }
|
|
2446
|
+
.viewport { flex:1; width:100%; height:100%; min-height:0; padding:16px 4px; }
|
|
2447
|
+
.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; }
|
|
2448
|
+
.pane-close { display:block; width:44px; height:44px; padding:0; font-size:22px; background:transparent; border:0; }
|
|
2449
|
+
.inspector-kind { margin-left:auto; }
|
|
2450
|
+
.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); }
|
|
2451
|
+
.page-list { flex-direction:column; }
|
|
2452
|
+
.page-list li { flex:none; }
|
|
2453
|
+
.notes-strip { display:none; }
|
|
2454
|
+
.status { min-height:48px; padding:2px 8px; }
|
|
2455
|
+
.status-message { display:none; }
|
|
2456
|
+
}
|
|
2457
|
+
.toolbar { position:relative; }
|
|
2458
|
+
.ribbon-tools { display:flex; flex:none; order:0; }
|
|
2459
|
+
.tools-content { display:flex; align-items:stretch; }
|
|
2460
|
+
.ribbon-tools>summary { display:none; }
|
|
2461
|
+
.ribbon-command { font-size:12px; min-width:88px; }
|
|
2462
|
+
.ribbon-hint { font-size:12px; line-height:20px; flex:1; }
|
|
2463
|
+
.group-label { font-size:11px; line-height:14px; }
|
|
2464
|
+
.inspector-card,.edit-controls,.layer-controls { font-size:12px; line-height:18px; }
|
|
2465
|
+
.selection-hint,.document-card dl { font-size:12px; }
|
|
2466
|
+
.pane-heading { font-size:12px; }
|
|
2467
|
+
.inspector-pane .pane-heading { position:sticky; top:0; z-index:1; background:var(--_vv-secondary); flex:none; }
|
|
2468
|
+
.status .notes-strip { border:0; padding:0 8px; background:transparent; min-height:28px; }
|
|
2469
|
+
.status .notes-strip>span { display:none; }
|
|
2470
|
+
.status-message { flex:1; }
|
|
2471
|
+
@media(max-width:760px) {
|
|
2472
|
+
#home-panel { overflow:visible; min-height:54px; padding:4px; }
|
|
2473
|
+
.ribbon-tools { display:block; flex:0 0 84px; width:84px; order:1; }
|
|
2474
|
+
.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; }
|
|
2475
|
+
.ribbon-tools>summary::-webkit-details-marker { display:none; }
|
|
2476
|
+
.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; }
|
|
2477
|
+
.ribbon-tools:not([open])>.tools-content { display:none; }
|
|
2478
|
+
.ribbon-tools .ribbon-group { display:flex; width:100%; min-height:64px; border:0; padding:0; }
|
|
2479
|
+
.ribbon-tools .ribbon-actions { justify-content:space-between; }
|
|
2480
|
+
.ribbon-tools .ribbon-command { min-width:88px; font-size:12px; }
|
|
2481
|
+
.ribbon-tools .search-group { order:1; flex:auto; }
|
|
2482
|
+
.ribbon-tools .search-feedback { display:block; }
|
|
2483
|
+
.status { flex-wrap:nowrap; gap:4px; min-height:48px; }
|
|
2484
|
+
.status .notes-strip { display:none; }
|
|
2485
|
+
}
|
|
2486
|
+
@media(pointer:coarse) { .ribbon-tabs {height:45px;min-height:45px;} .ribbon-tabs button {min-height:44px;height:44px;} }
|
|
2465
2487
|
@media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
|
|
2466
2488
|
`;
|
|
2467
2489
|
//#endregion
|
|
2490
|
+
//#region src/styles/index.ts
|
|
2491
|
+
/**
|
|
2492
|
+
* Viewer stylesheets as text for the shadow root. New styles are CSS files here; the legacy
|
|
2493
|
+
* `styles.ts` string moves into this directory when it is next changed.
|
|
2494
|
+
*/
|
|
2495
|
+
var canvasAndRibbonStyles = [":host{--office-foreground:var(--_vv-ink);--office-muted-foreground:var(--_vv-muted);--office-background:var(--_vv-surface);--office-surface:var(--_vv-accent-soft);--office-border:var(--_vv-border);--office-accent:var(--_vv-accent);--office-accent-foreground:var(--vv-accent-ink,#fff);--office-ring:var(--vv-focus,var(--_vv-accent));--office-font:\"Segoe UI\", Arial, sans-serif}.ribbon-row{background:0 0;border:0;flex-wrap:nowrap;align-items:stretch;gap:0;padding:0;display:flex}.ribbon-row office-ui-ribbon-group{min-width:0}.ribbon-row office-ui-button{--office-target-size:28px}.ribbon-row office-ui-button[variant=stacked]::part(button){min-width:64px;min-height:62px;font-size:12px}office-ui-button[disabled]::part(button){opacity:.45}.page-tabs{flex:none}office-ui-status-bar.status{background:var(--_vv-surface);min-height:29px;color:var(--_vv-muted);gap:16px;padding:0 8px 0 12px;font-size:11px}office-ui-status-item[value=\"\"]{display:none}office-ui-status-item[data-page-status]{color:var(--_vv-ink)}.status-message{flex:1;min-width:0}.viewport[data-tool=rectangle],.viewport[data-tool=rectangle] .paper [data-shape-id]{cursor:crosshair}.viewport[data-tool=rectangle]{touch-action:none}.draw-preview{fill:var(--_vv-accent-soft);stroke:var(--_vv-accent);stroke-width:1.5px;stroke-dasharray:4 3;pointer-events:none}.viewport[data-grid=true] .paper{--_vv-grid-minor:#00000012;--_vv-grid-major:#00000029;background-image:linear-gradient(to right, var(--_vv-grid-major) 1px, transparent 1px), linear-gradient(to top, var(--_vv-grid-major) 1px, transparent 1px), linear-gradient(to right, var(--_vv-grid-minor) 1px, transparent 1px), linear-gradient(to top, var(--_vv-grid-minor) 1px, transparent 1px);background-size:var(--vv-inch,96px) var(--vv-inch,96px), var(--vv-inch,96px) var(--vv-inch,96px), calc(var(--vv-inch,96px) / 4) calc(var(--vv-inch,96px) / 4), calc(var(--vv-inch,96px) / 4) calc(var(--vv-inch,96px) / 4);background-position:0 100%}:host(:fullscreen){width:100vw;height:100vh}@media (width<=760px){.ribbon-row{flex:1;min-width:0}.undo-group{flex:none}.ribbon-row office-ui-button[variant=stacked]::part(button){min-width:52px;min-height:44px}.ribbon-tools .tools-content office-ui-ribbon-group{width:100%}.search-group{scroll-snap-align:start}office-ui-status-bar.status{gap:4px;min-height:48px}office-ui-status-bar .notes-strip,office-ui-status-bar .status-message,office-ui-status-item[data-shape-status],office-ui-status-item[data-page-status]{display:none}.zoom-controls{--office-zoom-slider-width:0;flex:1;min-width:0}}", ".paper [data-shape-id]:focus,.paper [data-shape-id]:focus-visible{outline:none}.paper [data-shape-id]:focus-visible>[data-geometry]{stroke-dasharray:.08 .05;stroke:var(--vv-focus,var(--_vv-accent))!important;stroke-width:.04px!important}"].join("\n");
|
|
2496
|
+
//#endregion
|
|
2497
|
+
//#region src/ribbon-action.ts
|
|
2498
|
+
/** Event name shared by every ribbon control; detail is the typed action. */
|
|
2499
|
+
var RIBBON_ACTION_EVENT = "ribbon-action";
|
|
2500
|
+
/** Internal, non-composed event: it stays inside the viewer's shadow tree. */
|
|
2501
|
+
function emitRibbonAction(target, action) {
|
|
2502
|
+
target.dispatchEvent(new CustomEvent(RIBBON_ACTION_EVENT, {
|
|
2503
|
+
detail: action,
|
|
2504
|
+
bubbles: true
|
|
2505
|
+
}));
|
|
2506
|
+
}
|
|
2507
|
+
//#endregion
|
|
2508
|
+
//#region src/ribbon-parts.ts
|
|
2509
|
+
/**
|
|
2510
|
+
* A large stacked ribbon command. `id` is a stable name for state sync and tests; the typed
|
|
2511
|
+
* `action` is what runs. Labels, icons and shortcuts are static application strings.
|
|
2512
|
+
*/
|
|
2513
|
+
function tool(doc, id, label, icon, action, options = {}) {
|
|
2514
|
+
const el = doc.createElement("office-ui-button");
|
|
2515
|
+
el.setAttribute("variant", "stacked");
|
|
2516
|
+
el.setAttribute("command", id);
|
|
2517
|
+
el.setAttribute("icon", icon);
|
|
2518
|
+
el.setAttribute("label", label);
|
|
2519
|
+
el.setAttribute("title", options.keys ? `${label} (${options.keys[1]})` : label);
|
|
2520
|
+
if (options.keys) el.setAttribute("keyshortcuts", options.keys[0]);
|
|
2521
|
+
if (options.pressed !== void 0) el.setAttribute("pressed", String(options.pressed));
|
|
2522
|
+
if (options.controls) el.setAttribute("aria-controls", options.controls);
|
|
2523
|
+
el.addEventListener("office-command", (event) => {
|
|
2524
|
+
event.stopPropagation();
|
|
2525
|
+
emitRibbonAction(el, action);
|
|
2526
|
+
});
|
|
2527
|
+
return el;
|
|
2528
|
+
}
|
|
2529
|
+
/** A labelled ribbon group (the shared `office-ui-ribbon-group`). */
|
|
2530
|
+
function group(doc, label, children, className = "") {
|
|
2531
|
+
const el = doc.createElement("office-ui-ribbon-group");
|
|
2532
|
+
el.setAttribute("label", label);
|
|
2533
|
+
if (className) el.className = className;
|
|
2534
|
+
el.append(...children);
|
|
2535
|
+
return el;
|
|
2536
|
+
}
|
|
2537
|
+
/** A keyboard-navigable row of groups (the shared `office-ui-toolbar`). */
|
|
2538
|
+
function commandRow(doc, label, children) {
|
|
2539
|
+
const el = doc.createElement("office-ui-toolbar");
|
|
2540
|
+
el.className = "ribbon-row";
|
|
2541
|
+
el.setAttribute("aria-label", label);
|
|
2542
|
+
el.append(...children);
|
|
2543
|
+
return el;
|
|
2544
|
+
}
|
|
2545
|
+
/** Muted two-line guidance at the end of a ribbon panel. */
|
|
2546
|
+
function hint(doc, lines) {
|
|
2547
|
+
const el = doc.createElement("p");
|
|
2548
|
+
el.className = "ribbon-hint";
|
|
2549
|
+
lines.forEach((line, index) => {
|
|
2550
|
+
if (index) el.append(doc.createElement("br"));
|
|
2551
|
+
el.append(line);
|
|
2552
|
+
});
|
|
2553
|
+
return el;
|
|
2554
|
+
}
|
|
2555
|
+
//#endregion
|
|
2556
|
+
//#region src/viewer-search.ts
|
|
2557
|
+
/** Builds the Find group: a search field, previous/next result buttons and a live status. */
|
|
2558
|
+
function createSearchGroup(doc) {
|
|
2559
|
+
const group = doc.createElement("div");
|
|
2560
|
+
group.className = "ribbon-group search-group";
|
|
2561
|
+
group.setAttribute("role", "group");
|
|
2562
|
+
group.setAttribute("aria-label", "Find");
|
|
2563
|
+
const controls = doc.createElement("div");
|
|
2564
|
+
controls.className = "search-controls";
|
|
2565
|
+
controls.setAttribute("role", "search");
|
|
2566
|
+
controls.setAttribute("aria-label", "Diagram text search");
|
|
2567
|
+
const label = doc.createElement("label");
|
|
2568
|
+
label.append("Find ");
|
|
2569
|
+
const input = doc.createElement("input");
|
|
2570
|
+
input.type = "search";
|
|
2571
|
+
input.setAttribute("aria-label", "Search diagram text");
|
|
2572
|
+
input.placeholder = "Text across pages";
|
|
2573
|
+
input.maxLength = TEXT_SEARCH_LIMITS.queryCharacters;
|
|
2574
|
+
input.autocomplete = "off";
|
|
2575
|
+
input.spellcheck = false;
|
|
2576
|
+
input.setAttribute("aria-describedby", "search-status");
|
|
2577
|
+
label.append(input);
|
|
2578
|
+
const step = (action, name, text) => {
|
|
2579
|
+
const button = doc.createElement("button");
|
|
2580
|
+
button.type = "button";
|
|
2581
|
+
button.dataset.action = action;
|
|
2582
|
+
button.setAttribute("aria-label", name);
|
|
2583
|
+
button.textContent = text;
|
|
2584
|
+
return button;
|
|
2585
|
+
};
|
|
2586
|
+
const status = doc.createElement("span");
|
|
2587
|
+
status.id = "search-status";
|
|
2588
|
+
status.setAttribute("role", "status");
|
|
2589
|
+
status.setAttribute("aria-live", "polite");
|
|
2590
|
+
status.setAttribute("aria-atomic", "true");
|
|
2591
|
+
controls.append(label, step("search-previous", "Previous matching shape", "Previous"), step("search-next", "Next matching shape", "Next"), status);
|
|
2592
|
+
const caption = doc.createElement("span");
|
|
2593
|
+
caption.className = "group-label";
|
|
2594
|
+
caption.textContent = "Find in drawing";
|
|
2595
|
+
group.append(controls, caption);
|
|
2596
|
+
return group;
|
|
2597
|
+
}
|
|
2598
|
+
function searchControls(root) {
|
|
2599
|
+
return {
|
|
2600
|
+
input: root.querySelector("input[type=\"search\"]"),
|
|
2601
|
+
status: root.querySelector("#search-status"),
|
|
2602
|
+
previous: root.querySelector("[data-action=\"search-previous\"]"),
|
|
2603
|
+
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
function renderSearchControls(controls, state) {
|
|
2607
|
+
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2608
|
+
controls.input.disabled = !state.document?.pages.length;
|
|
2609
|
+
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2610
|
+
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2611
|
+
const result = state.search.results[state.search.activeIndex];
|
|
2612
|
+
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2613
|
+
}
|
|
2614
|
+
//#endregion
|
|
2615
|
+
//#region src/ribbon-home.ts
|
|
2616
|
+
/**
|
|
2617
|
+
* Visio's Home tab, limited to commands this build can honestly perform: Undo, Tools and
|
|
2618
|
+
* Editing. Clipboard, Font, Paragraph, Shape Styles and Arrange wait for core edit commands.
|
|
2619
|
+
* The compact (phone) layout folds every group but Undo into the Tools disclosure.
|
|
2620
|
+
*/
|
|
2621
|
+
function buildHomePanel(doc, panel) {
|
|
2622
|
+
const tools = doc.createElement("details");
|
|
2623
|
+
tools.className = "ribbon-tools";
|
|
2624
|
+
tools.open = true;
|
|
2625
|
+
const summary = doc.createElement("summary");
|
|
2626
|
+
const caret = doc.createElement("span");
|
|
2627
|
+
caret.setAttribute("aria-hidden", "true");
|
|
2628
|
+
caret.textContent = "⌄";
|
|
2629
|
+
summary.append("Tools ", caret);
|
|
2630
|
+
const content = doc.createElement("div");
|
|
2631
|
+
content.className = "tools-content";
|
|
2632
|
+
content.append(group(doc, "Tools", [
|
|
2633
|
+
tool(doc, "pointer", "Pointer Tool", "pointer", {
|
|
2634
|
+
type: "tool",
|
|
2635
|
+
tool: "pointer"
|
|
2636
|
+
}, {
|
|
2637
|
+
keys: ["Control+1", "Ctrl+1"],
|
|
2638
|
+
pressed: true
|
|
2639
|
+
}),
|
|
2640
|
+
tool(doc, "rectangle", "Rectangle", "rectangle", {
|
|
2641
|
+
type: "tool",
|
|
2642
|
+
tool: "rectangle"
|
|
2643
|
+
}, {
|
|
2644
|
+
keys: ["Control+8", "Ctrl+8"],
|
|
2645
|
+
pressed: false
|
|
2646
|
+
}),
|
|
2647
|
+
tool(doc, "edit", "Edit text", "pencil", {
|
|
2648
|
+
type: "reveal",
|
|
2649
|
+
panel: "edit"
|
|
2650
|
+
}, { keys: ["F2", "F2"] })
|
|
2651
|
+
]), createSearchGroup(doc), group(doc, "Editing", [
|
|
2652
|
+
tool(doc, "delete", "Delete", "trash", { type: "delete" }, { keys: ["Delete", "Del"] }),
|
|
2653
|
+
tool(doc, "layers", "Layers", "visioLayers", {
|
|
2654
|
+
type: "reveal",
|
|
2655
|
+
panel: "layers"
|
|
2656
|
+
}),
|
|
2657
|
+
tool(doc, "selection", "Shape details", "visioShapeData", {
|
|
2658
|
+
type: "reveal",
|
|
2659
|
+
panel: "selection"
|
|
2660
|
+
})
|
|
2661
|
+
]));
|
|
2662
|
+
tools.append(summary, content);
|
|
2663
|
+
panel.append(commandRow(doc, "Home commands", [group(doc, "Undo", [tool(doc, "undo", "Undo", "undo", {
|
|
2664
|
+
type: "history",
|
|
2665
|
+
key: "undo"
|
|
2666
|
+
}, { keys: ["Control+Z", "Ctrl+Z"] }), tool(doc, "redo", "Redo", "redo", {
|
|
2667
|
+
type: "history",
|
|
2668
|
+
key: "redo"
|
|
2669
|
+
}, { keys: ["Control+Y", "Ctrl+Y"] })], "undo-group"), tools]), hint(doc, ["Rectangle: drag on the page to draw.", "Editing is experimental. Keep your original file."]));
|
|
2670
|
+
}
|
|
2671
|
+
//#endregion
|
|
2672
|
+
//#region src/ribbon-view.ts
|
|
2673
|
+
/** Visio's View tab: Views, Show (grid and task panes) and Zoom. */
|
|
2674
|
+
function buildViewPanel(doc, panel) {
|
|
2675
|
+
panel.append(commandRow(doc, "View commands", [
|
|
2676
|
+
group(doc, "Views", [tool(doc, "fullscreen", "Full Screen", "fullscreen", { type: "fullscreen" }, {
|
|
2677
|
+
keys: ["F5", "F5"],
|
|
2678
|
+
pressed: false
|
|
2679
|
+
})]),
|
|
2680
|
+
group(doc, "Show", [
|
|
2681
|
+
tool(doc, "grid", "Grid", "grid", { type: "grid" }, { pressed: false }),
|
|
2682
|
+
tool(doc, "pages", "Pages pane", "visioPagesPane", {
|
|
2683
|
+
type: "pane",
|
|
2684
|
+
pane: "pages"
|
|
2685
|
+
}, {
|
|
2686
|
+
pressed: true,
|
|
2687
|
+
controls: "page-rail"
|
|
2688
|
+
}),
|
|
2689
|
+
tool(doc, "inspector", "Inspector pane", "visioInspectorPane", {
|
|
2690
|
+
type: "pane",
|
|
2691
|
+
pane: "inspector"
|
|
2692
|
+
}, {
|
|
2693
|
+
pressed: true,
|
|
2694
|
+
controls: "inspector-pane"
|
|
2695
|
+
}),
|
|
2696
|
+
tool(doc, "notes", "Review notes", "message", {
|
|
2697
|
+
type: "reveal",
|
|
2698
|
+
panel: "notes"
|
|
2699
|
+
})
|
|
2700
|
+
]),
|
|
2701
|
+
group(doc, "Zoom", [
|
|
2702
|
+
tool(doc, "zoom-fit", "Fit to Window", "fitPage", {
|
|
2703
|
+
type: "zoom",
|
|
2704
|
+
mode: "fit"
|
|
2705
|
+
}, { keys: ["Control+Shift+W", "Ctrl+Shift+W"] }),
|
|
2706
|
+
tool(doc, "page-width", "Page Width", "pageWidth", {
|
|
2707
|
+
type: "zoom",
|
|
2708
|
+
mode: "width"
|
|
2709
|
+
}),
|
|
2710
|
+
tool(doc, "actual-size", "100%", "search", {
|
|
2711
|
+
type: "zoom",
|
|
2712
|
+
mode: "actual"
|
|
2713
|
+
})
|
|
2714
|
+
])
|
|
2715
|
+
]), hint(doc, ["Ctrl+wheel or + / − to zoom.", "Ctrl+Page Up / Page Down changes page."]));
|
|
2716
|
+
}
|
|
2717
|
+
//#endregion
|
|
2718
|
+
//#region src/ribbon.ts
|
|
2719
|
+
var TABS = [[
|
|
2720
|
+
"home",
|
|
2721
|
+
"Home",
|
|
2722
|
+
buildHomePanel
|
|
2723
|
+
], [
|
|
2724
|
+
"view",
|
|
2725
|
+
"View",
|
|
2726
|
+
buildViewPanel
|
|
2727
|
+
]];
|
|
2728
|
+
/**
|
|
2729
|
+
* The Visio ribbon: a tab list and one panel per tab. Tab switching and keyboard movement
|
|
2730
|
+
* belong to the chrome; commands emit `ribbon-action` events for the router.
|
|
2731
|
+
*/
|
|
2732
|
+
function createRibbon(doc) {
|
|
2733
|
+
const toolbar = doc.createElement("div");
|
|
2734
|
+
toolbar.className = "toolbar";
|
|
2735
|
+
toolbar.setAttribute("role", "group");
|
|
2736
|
+
toolbar.setAttribute("aria-label", "Diagram controls");
|
|
2737
|
+
const tabs = doc.createElement("div");
|
|
2738
|
+
tabs.className = "ribbon-tabs";
|
|
2739
|
+
tabs.setAttribute("role", "tablist");
|
|
2740
|
+
tabs.setAttribute("aria-label", "Ribbon");
|
|
2741
|
+
toolbar.append(tabs);
|
|
2742
|
+
for (const [key, name, build] of TABS) {
|
|
2743
|
+
const selected = key === "home";
|
|
2744
|
+
const tab = doc.createElement("button");
|
|
2745
|
+
tab.type = "button";
|
|
2746
|
+
tab.id = `${key}-tab`;
|
|
2747
|
+
tab.dataset.tab = key;
|
|
2748
|
+
tab.textContent = name;
|
|
2749
|
+
tab.setAttribute("role", "tab");
|
|
2750
|
+
tab.setAttribute("aria-selected", String(selected));
|
|
2751
|
+
tab.setAttribute("aria-controls", `${key}-panel`);
|
|
2752
|
+
if (!selected) tab.tabIndex = -1;
|
|
2753
|
+
const panel = doc.createElement("div");
|
|
2754
|
+
panel.className = "ribbon-content";
|
|
2755
|
+
panel.id = `${key}-panel`;
|
|
2756
|
+
panel.setAttribute("role", "tabpanel");
|
|
2757
|
+
panel.setAttribute("aria-labelledby", tab.id);
|
|
2758
|
+
panel.hidden = !selected;
|
|
2759
|
+
build(doc, panel);
|
|
2760
|
+
tabs.append(tab);
|
|
2761
|
+
toolbar.append(panel);
|
|
2762
|
+
}
|
|
2763
|
+
return toolbar;
|
|
2764
|
+
}
|
|
2765
|
+
//#endregion
|
|
2766
|
+
//#region src/status-bar.ts
|
|
2767
|
+
/**
|
|
2768
|
+
* Visio's status bar on the shared `office-ui-status-bar`: page and shape metrics, a live
|
|
2769
|
+
* message region, compatibility notes, the host footer slot and the shared zoom slider.
|
|
2770
|
+
*/
|
|
2771
|
+
function createStatusBar(doc) {
|
|
2772
|
+
const bar = doc.createElement("office-ui-status-bar");
|
|
2773
|
+
bar.className = "status";
|
|
2774
|
+
bar.setAttribute("label", "Status bar");
|
|
2775
|
+
const item = (name) => {
|
|
2776
|
+
const el = doc.createElement("office-ui-status-item");
|
|
2777
|
+
el.setAttribute(name, "");
|
|
2778
|
+
el.setAttribute("label", "");
|
|
2779
|
+
el.setAttribute("value", "");
|
|
2780
|
+
return el;
|
|
2781
|
+
};
|
|
2782
|
+
const message = doc.createElement("div");
|
|
2783
|
+
message.className = "status-message";
|
|
2784
|
+
message.setAttribute("role", "status");
|
|
2785
|
+
const text = doc.createElement("span");
|
|
2786
|
+
text.dataset.status = "";
|
|
2787
|
+
const diagnostics = doc.createElement("span");
|
|
2788
|
+
diagnostics.dataset.diagnostics = "";
|
|
2789
|
+
message.append(text, diagnostics);
|
|
2790
|
+
const notes = doc.createElement("div");
|
|
2791
|
+
notes.className = "notes-strip";
|
|
2792
|
+
const notesButton = doc.createElement("button");
|
|
2793
|
+
notesButton.type = "button";
|
|
2794
|
+
notesButton.dataset.chrome = "notes";
|
|
2795
|
+
notesButton.setAttribute("aria-controls", "inspector-pane");
|
|
2796
|
+
notesButton.setAttribute("aria-expanded", "false");
|
|
2797
|
+
const count = doc.createElement("span");
|
|
2798
|
+
count.className = "notes-count";
|
|
2799
|
+
count.dataset.noteCount = "";
|
|
2800
|
+
notesButton.append("Notes", count);
|
|
2801
|
+
const local = doc.createElement("span");
|
|
2802
|
+
local.textContent = "Files stay in your browser";
|
|
2803
|
+
notes.append(notesButton, local);
|
|
2804
|
+
const footer = doc.createElement("slot");
|
|
2805
|
+
footer.name = "workspace-footer";
|
|
2806
|
+
const zoom = doc.createElement("office-ui-zoom-slider");
|
|
2807
|
+
zoom.slot = "end";
|
|
2808
|
+
zoom.className = "zoom-controls";
|
|
2809
|
+
zoom.setAttribute("label", "Canvas zoom");
|
|
2810
|
+
zoom.setAttribute("fit", "Fit page to current window");
|
|
2811
|
+
zoom.setAttribute("value", "100");
|
|
2812
|
+
bar.append(item("data-page-status"), item("data-shape-status"), message, notes, footer, zoom);
|
|
2813
|
+
return bar;
|
|
2814
|
+
}
|
|
2815
|
+
/** Visio's page tabs under the drawing, on the shared document tab strip. */
|
|
2816
|
+
function createPageTabs(doc) {
|
|
2817
|
+
const strip = doc.createElement("office-ui-tab-strip");
|
|
2818
|
+
strip.className = "page-tabs";
|
|
2819
|
+
strip.setAttribute("label", "Pages");
|
|
2820
|
+
strip.setAttribute("previous-label", "Previous page");
|
|
2821
|
+
strip.setAttribute("next-label", "Next page");
|
|
2822
|
+
return strip;
|
|
2823
|
+
}
|
|
2824
|
+
//#endregion
|
|
2468
2825
|
//#region src/edit-error.ts
|
|
2469
2826
|
/** Core error codes remain visible for a refused transaction; document content is plain text. */
|
|
2470
2827
|
function editErrorMessage(error) {
|
|
@@ -2752,61 +3109,17 @@ var ViewerEditControls = class {
|
|
|
2752
3109
|
}
|
|
2753
3110
|
};
|
|
2754
3111
|
//#endregion
|
|
2755
|
-
//#region src/viewer-search.ts
|
|
2756
|
-
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>`;
|
|
2757
|
-
function searchControls(root) {
|
|
2758
|
-
return {
|
|
2759
|
-
input: root.querySelector("input[type=\"search\"]"),
|
|
2760
|
-
status: root.querySelector("#search-status"),
|
|
2761
|
-
previous: root.querySelector("[data-action=\"search-previous\"]"),
|
|
2762
|
-
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2763
|
-
};
|
|
2764
|
-
}
|
|
2765
|
-
function renderSearchControls(controls, state) {
|
|
2766
|
-
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2767
|
-
controls.input.disabled = !state.document?.pages.length;
|
|
2768
|
-
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2769
|
-
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2770
|
-
const result = state.search.results[state.search.activeIndex];
|
|
2771
|
-
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2772
|
-
}
|
|
2773
|
-
//#endregion
|
|
2774
3112
|
//#region src/viewer-chrome.ts
|
|
2775
|
-
/**
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
<span class="spacer"></span><span class="local-label">Local workspace</span>
|
|
2782
|
-
</div>
|
|
2783
|
-
<div class="ribbon-tabs" role="tablist" aria-label="Ribbon">
|
|
2784
|
-
<button type="button" id="home-tab" role="tab" aria-selected="true" aria-controls="home-panel" data-tab="home">Home</button>
|
|
2785
|
-
<button type="button" id="view-tab" role="tab" aria-selected="false" aria-controls="view-panel" tabindex="-1" data-tab="view">View</button>
|
|
2786
|
-
</div>
|
|
2787
|
-
<div class="ribbon-content" id="home-panel" role="tabpanel" aria-labelledby="home-tab">
|
|
2788
|
-
<div class="ribbon-group search-group">${searchTemplate}<span class="group-label">Find in drawing</span></div>
|
|
2789
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2790
|
-
<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>
|
|
2791
|
-
<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>
|
|
2792
|
-
<button class="ribbon-command" type="button" data-chrome="layers"><span class="command-icon" data-icon="layers" aria-hidden="true"></span><span>Layers</span></button>
|
|
2793
|
-
</div><span class="group-label">Inspect and edit</span></div>
|
|
2794
|
-
<p class="ribbon-hint">Select a shape to inspect its details.<br>Text editing is experimental.</p>
|
|
2795
|
-
</div>
|
|
2796
|
-
<div class="ribbon-content" id="view-panel" role="tabpanel" aria-labelledby="view-tab" hidden>
|
|
2797
|
-
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2798
|
-
<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>
|
|
2799
|
-
<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>
|
|
2800
|
-
<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>
|
|
2801
|
-
</div><span class="group-label">Workspace panes</span></div>
|
|
2802
|
-
<p class="ribbon-hint">Canvas shortcuts: + / − to zoom, 0 to fit.<br>Arrow keys move between focused shapes.</p>
|
|
2803
|
-
</div>
|
|
2804
|
-
</div>
|
|
2805
|
-
<div class="workspace">
|
|
3113
|
+
/**
|
|
3114
|
+
* Static workspace markup (legacy; migrate to builders when next changed). The ribbon, page
|
|
3115
|
+
* tabs and status bar are built by `createRibbon`, `createPageTabs` and `createStatusBar`.
|
|
3116
|
+
* All document labels are inserted as text.
|
|
3117
|
+
*/
|
|
3118
|
+
var viewerChromeTemplate = `<div class="workspace">
|
|
2806
3119
|
<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>
|
|
2807
3120
|
<div class="viewport" tabindex="0" role="region" aria-label="Diagram canvas"></div>
|
|
2808
3121
|
<aside id="inspector-pane" class="inspector-pane" aria-label="Drawing inspector">
|
|
2809
|
-
<div class="pane-heading"><span>Inspector</span><span class="inspector-kind">Drawing</span></div>
|
|
3122
|
+
<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>
|
|
2810
3123
|
<div class="inspector-body">
|
|
2811
3124
|
<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>
|
|
2812
3125
|
<p class="selection-hint">Select a shape on the canvas to see its data and links.</p>
|
|
@@ -2815,24 +3128,28 @@ var viewerChromeTemplate = `
|
|
|
2815
3128
|
<details class="notes inspector-card"><summary>Compatibility notes</summary><ul></ul></details>
|
|
2816
3129
|
</div>
|
|
2817
3130
|
</aside>
|
|
2818
|
-
</div
|
|
2819
|
-
<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>
|
|
2820
|
-
<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>`;
|
|
3131
|
+
</div>`;
|
|
2821
3132
|
/** Presentation state stays local; page navigation uses the same controller as every binding. */
|
|
2822
3133
|
var ViewerChrome = class {
|
|
2823
3134
|
#root;
|
|
2824
3135
|
#controller;
|
|
2825
3136
|
#document;
|
|
2826
3137
|
#pageList;
|
|
3138
|
+
#pageTabs;
|
|
3139
|
+
#tools;
|
|
2827
3140
|
#rail;
|
|
2828
3141
|
#inspector;
|
|
2829
3142
|
#notes;
|
|
2830
3143
|
#responsive;
|
|
3144
|
+
#compact;
|
|
2831
3145
|
#pagesManuallyToggled = false;
|
|
3146
|
+
#inspectorManuallyToggled = false;
|
|
2832
3147
|
constructor(root, controller) {
|
|
2833
3148
|
this.#root = root;
|
|
2834
3149
|
this.#controller = controller;
|
|
2835
3150
|
this.#pageList = root.querySelector(".page-list");
|
|
3151
|
+
this.#pageTabs = root.querySelector("office-ui-tab-strip");
|
|
3152
|
+
this.#tools = root.querySelector(".ribbon-tools");
|
|
2836
3153
|
this.#rail = root.querySelector(".page-rail");
|
|
2837
3154
|
this.#inspector = root.querySelector(".inspector-pane");
|
|
2838
3155
|
this.#notes = root.querySelector(".notes");
|
|
@@ -2841,26 +3158,45 @@ var ViewerChrome = class {
|
|
|
2841
3158
|
const view = this.#root.ownerDocument.defaultView;
|
|
2842
3159
|
const events = new ((view?.AbortController) ?? AbortController)(), options = { signal: events.signal };
|
|
2843
3160
|
this.#responsive = view?.matchMedia?.("(max-width: 980px)");
|
|
3161
|
+
const compact = view?.matchMedia?.("(max-width: 760px)");
|
|
3162
|
+
this.#compact = compact;
|
|
3163
|
+
const tools = this.#tools;
|
|
3164
|
+
const toolLayout = () => {
|
|
3165
|
+
tools.open = !compact?.matches;
|
|
3166
|
+
};
|
|
3167
|
+
toolLayout();
|
|
3168
|
+
compact?.addEventListener("change", toolLayout, options);
|
|
3169
|
+
this.#root.addEventListener("keydown", (event) => {
|
|
3170
|
+
if (event.key === "Escape" && compact?.matches && tools.open) {
|
|
3171
|
+
event.preventDefault();
|
|
3172
|
+
event.stopPropagation();
|
|
3173
|
+
tools.open = false;
|
|
3174
|
+
tools.querySelector("summary").focus();
|
|
3175
|
+
}
|
|
3176
|
+
}, {
|
|
3177
|
+
...options,
|
|
3178
|
+
capture: true
|
|
3179
|
+
});
|
|
2844
3180
|
const responsive = () => {
|
|
2845
3181
|
if (!this.#pagesManuallyToggled) this.#setPane("pages", !this.#responsive?.matches);
|
|
3182
|
+
if (!this.#inspectorManuallyToggled) this.#setPane("inspector", !compact?.matches);
|
|
2846
3183
|
};
|
|
2847
3184
|
responsive();
|
|
2848
3185
|
this.#responsive?.addEventListener("change", responsive, options);
|
|
3186
|
+
compact?.addEventListener("change", responsive, options);
|
|
2849
3187
|
this.#root.addEventListener("click", (event) => {
|
|
2850
3188
|
const button = event.target?.closest?.("button");
|
|
2851
3189
|
if (!button || button.disabled) return;
|
|
2852
3190
|
const tab = button.dataset.tab;
|
|
2853
|
-
if (tab === "home" || tab === "view") this
|
|
3191
|
+
if (tab === "home" || tab === "view") this.showTab(tab);
|
|
2854
3192
|
if (button.dataset.pageIndex !== void 0) this.#controller.setPage(Number(button.dataset.pageIndex));
|
|
2855
3193
|
const action = button.dataset.chrome;
|
|
2856
|
-
if (action === "
|
|
2857
|
-
if (action === "
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
if (action === "inspector") this.#setPane("inspector", this.#inspector.hidden);
|
|
2863
|
-
if (action === "notes" || action === "selection" || action === "edit" || action === "layers") this.#reveal(action);
|
|
3194
|
+
if (action === "inspector") this.togglePane("inspector");
|
|
3195
|
+
if (action === "notes") this.reveal("notes");
|
|
3196
|
+
}, options);
|
|
3197
|
+
this.#root.addEventListener("office-tab-select", (event) => {
|
|
3198
|
+
const id = Number(event.detail.id);
|
|
3199
|
+
if (Number.isSafeInteger(id)) this.#controller.setPage(id);
|
|
2864
3200
|
}, options);
|
|
2865
3201
|
this.#root.addEventListener("keydown", (event) => {
|
|
2866
3202
|
const key = event;
|
|
@@ -2877,11 +3213,11 @@ var ViewerChrome = class {
|
|
|
2877
3213
|
if (key.key === "ArrowUp" || key.key === "ArrowDown") return;
|
|
2878
3214
|
key.preventDefault();
|
|
2879
3215
|
const tab = key.key === "Home" ? "home" : key.key === "End" ? "view" : button.dataset.tab === "home" ? "view" : "home";
|
|
2880
|
-
this
|
|
3216
|
+
this.showTab(tab);
|
|
2881
3217
|
this.#root.querySelector(`[data-tab="${tab}"]`).focus();
|
|
2882
3218
|
} else if (button.dataset.pageIndex !== void 0) {
|
|
2883
3219
|
key.preventDefault();
|
|
2884
|
-
const pages = [...this.#pageList.querySelectorAll("
|
|
3220
|
+
const pages = [...this.#pageList.querySelectorAll("[data-page-index]")];
|
|
2885
3221
|
const current = Number(button.dataset.pageIndex);
|
|
2886
3222
|
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)));
|
|
2887
3223
|
this.#controller.setPage(next);
|
|
@@ -2891,7 +3227,7 @@ var ViewerChrome = class {
|
|
|
2891
3227
|
this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
|
|
2892
3228
|
return () => events.abort();
|
|
2893
3229
|
}
|
|
2894
|
-
|
|
3230
|
+
showTab(tab) {
|
|
2895
3231
|
for (const button of this.#root.querySelectorAll("[data-tab]")) {
|
|
2896
3232
|
const selected = button.dataset.tab === tab;
|
|
2897
3233
|
button.setAttribute("aria-selected", String(selected));
|
|
@@ -2899,21 +3235,41 @@ var ViewerChrome = class {
|
|
|
2899
3235
|
}
|
|
2900
3236
|
for (const panel of this.#root.querySelectorAll("[role=\"tabpanel\"]")) panel.hidden = panel.id !== `${tab}-panel`;
|
|
2901
3237
|
}
|
|
3238
|
+
/** Show or hide a task pane; compact layouts close the Tools menu afterwards. */
|
|
3239
|
+
togglePane(pane) {
|
|
3240
|
+
if (this.#compact?.matches) this.#tools.open = false;
|
|
3241
|
+
if (pane === "pages") this.#pagesManuallyToggled = true;
|
|
3242
|
+
else this.#inspectorManuallyToggled = true;
|
|
3243
|
+
this.#setPane(pane, (pane === "pages" ? this.#rail : this.#inspector).hidden);
|
|
3244
|
+
}
|
|
3245
|
+
#command(name) {
|
|
3246
|
+
return this.#root.querySelector(`office-ui-button[command="${name}"]`);
|
|
3247
|
+
}
|
|
2902
3248
|
#setPane(pane, visible) {
|
|
3249
|
+
if (visible && this.#compact?.matches) {
|
|
3250
|
+
const other = pane === "pages" ? "inspector" : "pages";
|
|
3251
|
+
(other === "pages" ? this.#rail : this.#inspector).hidden = true;
|
|
3252
|
+
this.#command(other).setAttribute("pressed", "false");
|
|
3253
|
+
}
|
|
2903
3254
|
(pane === "pages" ? this.#rail : this.#inspector).hidden = !visible;
|
|
2904
|
-
this.#
|
|
3255
|
+
this.#command(pane).setAttribute("pressed", String(visible));
|
|
2905
3256
|
this.#syncNotes();
|
|
2906
3257
|
}
|
|
2907
3258
|
#syncNotes() {
|
|
2908
3259
|
this.#root.querySelector(".notes-strip button").setAttribute("aria-expanded", String(!this.#inspector.hidden && this.#notes.open));
|
|
2909
3260
|
}
|
|
2910
|
-
|
|
3261
|
+
/** Open an inspector disclosure; text editing may focus the text field directly (F2). */
|
|
3262
|
+
reveal(kind, focusText = false) {
|
|
3263
|
+
if (this.#compact?.matches) this.#tools.open = false;
|
|
3264
|
+
this.#inspectorManuallyToggled = true;
|
|
2911
3265
|
this.#setPane("inspector", true);
|
|
2912
3266
|
const selector = kind === "notes" ? ".notes" : kind === "edit" ? ".edit-controls" : kind === "layers" ? ".layer-controls" : ".shape-inspector";
|
|
2913
3267
|
const panel = this.#root.querySelector(selector);
|
|
2914
3268
|
if (panel.hidden) return;
|
|
2915
3269
|
panel.open = true;
|
|
2916
|
-
panel.querySelector("
|
|
3270
|
+
const text = panel.querySelector("textarea");
|
|
3271
|
+
if (focusText && text && !text.disabled) text.focus();
|
|
3272
|
+
else panel.querySelector("summary").focus();
|
|
2917
3273
|
panel.scrollIntoView?.({
|
|
2918
3274
|
block: "nearest",
|
|
2919
3275
|
inline: "nearest"
|
|
@@ -2954,14 +3310,21 @@ var ViewerChrome = class {
|
|
|
2954
3310
|
fragment.append(item);
|
|
2955
3311
|
}
|
|
2956
3312
|
this.#pageList.replaceChildren(fragment);
|
|
3313
|
+
this.#pageTabs.tabs = (state.document?.pages ?? []).map((candidate, index) => ({
|
|
3314
|
+
id: String(index),
|
|
3315
|
+
label: candidate.name,
|
|
3316
|
+
title: candidate.isBackground ? `${candidate.name} (background page)` : candidate.name
|
|
3317
|
+
}));
|
|
2957
3318
|
if (restoreFocus !== void 0) [...this.#pageList.querySelectorAll("button")].find((button) => button.dataset.pageIndex === restoreFocus)?.focus();
|
|
2958
3319
|
}
|
|
3320
|
+
this.#pageTabs.selected = page ? String(state.pageIndex) : "";
|
|
2959
3321
|
for (const button of this.#pageList.querySelectorAll("button")) {
|
|
2960
3322
|
const selected = Number(button.dataset.pageIndex) === state.pageIndex;
|
|
2961
3323
|
if (selected) button.setAttribute("aria-current", "page");
|
|
2962
3324
|
else button.removeAttribute("aria-current");
|
|
2963
3325
|
button.tabIndex = selected ? 0 : -1;
|
|
2964
3326
|
}
|
|
3327
|
+
this.#root.querySelector("[data-page-status]").setAttribute("value", page ? `Page ${state.pageIndex + 1} of ${state.document.pages.length}` : "");
|
|
2965
3328
|
const count = state.document?.pages.length ?? 0;
|
|
2966
3329
|
this.#root.querySelector("[data-page-count]").textContent = String(count);
|
|
2967
3330
|
this.#root.querySelector(".page-empty").hidden = count > 0;
|
|
@@ -2969,17 +3332,405 @@ var ViewerChrome = class {
|
|
|
2969
3332
|
this.#root.querySelector("[data-page-size]").textContent = page ? `${page.width} × ${page.height} in` : "No page";
|
|
2970
3333
|
this.#root.querySelector("[data-page-shapes]").textContent = page ? `${page.shapes.length} top-level` : "No shapes";
|
|
2971
3334
|
this.#root.querySelector(".selection-hint").hidden = !!state.selectedShape;
|
|
2972
|
-
this.#
|
|
2973
|
-
this.#
|
|
2974
|
-
this.#
|
|
2975
|
-
this.#
|
|
2976
|
-
this.#root.querySelector("
|
|
2977
|
-
for (const button of this.#root.querySelectorAll("[data-chrome=\"notes\"]")) button.disabled = noteCount === 0;
|
|
3335
|
+
this.#command("selection").disabled = !state.selectedShape;
|
|
3336
|
+
this.#command("edit").disabled = !page;
|
|
3337
|
+
this.#command("layers").disabled = this.#root.querySelector(".layer-controls").hidden;
|
|
3338
|
+
this.#command("notes").disabled = noteCount === 0;
|
|
3339
|
+
this.#root.querySelector(".notes-strip button").disabled = noteCount === 0;
|
|
2978
3340
|
this.#root.querySelector("[data-note-count]").textContent = noteCount ? ` · ${noteCount}` : "";
|
|
2979
3341
|
this.#syncNotes();
|
|
2980
3342
|
}
|
|
2981
3343
|
};
|
|
2982
3344
|
//#endregion
|
|
3345
|
+
//#region src/ribbon-router.ts
|
|
3346
|
+
/** Routes a ribbon, status-bar or shortcut action to the controller that owns it. */
|
|
3347
|
+
function routeRibbonAction(targets, action) {
|
|
3348
|
+
const { controller } = targets;
|
|
3349
|
+
const page = controller.state.document?.pages[controller.state.pageIndex];
|
|
3350
|
+
switch (action.type) {
|
|
3351
|
+
case "history": return targets.history(action.key);
|
|
3352
|
+
case "delete": return targets.deleteSelection();
|
|
3353
|
+
case "tool": return targets.setTool(action.tool);
|
|
3354
|
+
case "grid": return targets.toggleGrid();
|
|
3355
|
+
case "fullscreen": return targets.toggleFullscreen();
|
|
3356
|
+
case "pane": return targets.togglePane(action.pane);
|
|
3357
|
+
case "reveal": return targets.reveal(action.panel, action.focusText ?? false);
|
|
3358
|
+
case "search": return targets.focusSearch();
|
|
3359
|
+
case "page": return controller.setPage(controller.state.pageIndex + action.step);
|
|
3360
|
+
case "zoom":
|
|
3361
|
+
if (!page) return;
|
|
3362
|
+
if (action.mode === "actual") return controller.setZoom(1);
|
|
3363
|
+
return targets.fit(action.mode === "fit" ? "page" : "width");
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
//#endregion
|
|
3367
|
+
//#region src/viewer-draw-tool.ts
|
|
3368
|
+
/** Visio snaps new geometry to ruler subdivisions; 1/16 inch matches its default fine grid. */
|
|
3369
|
+
var SNAP = 1 / 16;
|
|
3370
|
+
var MIN_SIZE = SNAP;
|
|
3371
|
+
var snap = (value) => Math.round(value / SNAP) * SNAP;
|
|
3372
|
+
/** Next free numeric shape ID on the page tree. Core still rejects any collision. */
|
|
3373
|
+
function nextShapeId(page) {
|
|
3374
|
+
let max = 0;
|
|
3375
|
+
const pending = [...page.shapes];
|
|
3376
|
+
while (pending.length) {
|
|
3377
|
+
const shape = pending.pop();
|
|
3378
|
+
if (/^\d{1,9}$/.test(shape.id)) max = Math.max(max, Number(shape.id));
|
|
3379
|
+
pending.push(...shape.children);
|
|
3380
|
+
}
|
|
3381
|
+
return String(max + 1);
|
|
3382
|
+
}
|
|
3383
|
+
/** Page inches with a top-left origin (SVG user space) from a client point. */
|
|
3384
|
+
function pagePoint(svg, page, event) {
|
|
3385
|
+
const matrix = svg.getScreenCTM?.();
|
|
3386
|
+
if (!matrix) return void 0;
|
|
3387
|
+
const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse());
|
|
3388
|
+
return {
|
|
3389
|
+
x: snap(Math.max(0, Math.min(page.width, point.x))),
|
|
3390
|
+
y: snap(Math.max(0, Math.min(page.height, point.y)))
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
/** Drag-to-draw rectangle tool. The preview is viewer-only; the shape is created by core. */
|
|
3394
|
+
var RectangleDrawTool = class {
|
|
3395
|
+
viewport;
|
|
3396
|
+
controller;
|
|
3397
|
+
options;
|
|
3398
|
+
#drag;
|
|
3399
|
+
#request = 0;
|
|
3400
|
+
constructor(viewport, controller, options) {
|
|
3401
|
+
this.viewport = viewport;
|
|
3402
|
+
this.controller = controller;
|
|
3403
|
+
this.options = options;
|
|
3404
|
+
}
|
|
3405
|
+
get drawing() {
|
|
3406
|
+
return !!this.#drag;
|
|
3407
|
+
}
|
|
3408
|
+
wire() {
|
|
3409
|
+
const events = new ((this.viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
3410
|
+
const options = { signal: events.signal };
|
|
3411
|
+
const viewport = this.viewport;
|
|
3412
|
+
viewport.addEventListener("click", (event) => {
|
|
3413
|
+
if (this.options.active()) event.stopImmediatePropagation();
|
|
3414
|
+
}, {
|
|
3415
|
+
...options,
|
|
3416
|
+
capture: true
|
|
3417
|
+
});
|
|
3418
|
+
viewport.addEventListener("pointerdown", (event) => this.#start(event), options);
|
|
3419
|
+
viewport.addEventListener("pointermove", (event) => this.#move(event), options);
|
|
3420
|
+
viewport.addEventListener("pointerup", (event) => void this.#finish(event), options);
|
|
3421
|
+
viewport.addEventListener("pointercancel", () => this.#cancel(), options);
|
|
3422
|
+
viewport.addEventListener("keydown", (event) => {
|
|
3423
|
+
if (event.key === "Escape" && this.#drag) {
|
|
3424
|
+
event.preventDefault();
|
|
3425
|
+
this.#cancel();
|
|
3426
|
+
}
|
|
3427
|
+
}, options);
|
|
3428
|
+
return () => {
|
|
3429
|
+
++this.#request;
|
|
3430
|
+
this.#cancel();
|
|
3431
|
+
events.abort();
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
#start(event) {
|
|
3435
|
+
if (!this.options.active() || event.button !== 0 || this.#drag) return;
|
|
3436
|
+
const state = this.controller.state;
|
|
3437
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3438
|
+
const svg = this.viewport.querySelector("svg.paper");
|
|
3439
|
+
if (!page || !svg || !state.edit.sourceAvailable || state.edit.busy) return;
|
|
3440
|
+
const start = pagePoint(svg, page, event);
|
|
3441
|
+
if (!start) return;
|
|
3442
|
+
event.preventDefault();
|
|
3443
|
+
const rect = this.viewport.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
3444
|
+
rect.classList.add("draw-preview");
|
|
3445
|
+
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
3446
|
+
svg.append(rect);
|
|
3447
|
+
this.#drag = {
|
|
3448
|
+
pointer: event.pointerId,
|
|
3449
|
+
svg,
|
|
3450
|
+
page,
|
|
3451
|
+
...start,
|
|
3452
|
+
rect
|
|
3453
|
+
};
|
|
3454
|
+
this.viewport.setPointerCapture?.(event.pointerId);
|
|
3455
|
+
this.#update(start);
|
|
3456
|
+
}
|
|
3457
|
+
#move(event) {
|
|
3458
|
+
if (!this.#drag || event.pointerId !== this.#drag.pointer) return;
|
|
3459
|
+
const point = pagePoint(this.#drag.svg, this.#drag.page, event);
|
|
3460
|
+
if (point) this.#update(point);
|
|
3461
|
+
}
|
|
3462
|
+
#update(point) {
|
|
3463
|
+
const { x, y, rect } = this.#drag;
|
|
3464
|
+
rect.setAttribute("x", String(Math.min(x, point.x)));
|
|
3465
|
+
rect.setAttribute("y", String(Math.min(y, point.y)));
|
|
3466
|
+
rect.setAttribute("width", String(Math.abs(point.x - x)));
|
|
3467
|
+
rect.setAttribute("height", String(Math.abs(point.y - y)));
|
|
3468
|
+
}
|
|
3469
|
+
#cancel() {
|
|
3470
|
+
this.#drag?.rect.remove();
|
|
3471
|
+
this.#drag = void 0;
|
|
3472
|
+
}
|
|
3473
|
+
async #finish(event) {
|
|
3474
|
+
const drag = this.#drag;
|
|
3475
|
+
if (!drag || event.pointerId !== drag.pointer) return;
|
|
3476
|
+
const end = pagePoint(drag.svg, drag.page, event) ?? drag;
|
|
3477
|
+
this.#cancel();
|
|
3478
|
+
const width = Math.abs(end.x - drag.x), height = Math.abs(end.y - drag.y);
|
|
3479
|
+
if (width < MIN_SIZE || height < MIN_SIZE) {
|
|
3480
|
+
this.options.announce("Drag on the page to draw a rectangle.");
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
const state = this.controller.state;
|
|
3484
|
+
if (state.document?.pages[state.pageIndex] !== drag.page) return;
|
|
3485
|
+
const shapeId = nextShapeId(drag.page);
|
|
3486
|
+
const pageId = drag.page.id;
|
|
3487
|
+
const request = ++this.#request;
|
|
3488
|
+
try {
|
|
3489
|
+
await this.controller.applyEdits([{
|
|
3490
|
+
type: "create-rectangle",
|
|
3491
|
+
pageId,
|
|
3492
|
+
shapeId,
|
|
3493
|
+
x: (drag.x + end.x) / 2,
|
|
3494
|
+
y: drag.page.height - (drag.y + end.y) / 2,
|
|
3495
|
+
width,
|
|
3496
|
+
height
|
|
3497
|
+
}]);
|
|
3498
|
+
if (request !== this.#request) return;
|
|
3499
|
+
const created = this.controller.state.document?.pages.find((page) => page.id === pageId)?.shapes.find((shape) => shape.id === shapeId);
|
|
3500
|
+
if (created) this.controller.selectShape({
|
|
3501
|
+
id: shapeId,
|
|
3502
|
+
name: created.name,
|
|
3503
|
+
pageId
|
|
3504
|
+
});
|
|
3505
|
+
this.options.announce(`Rectangle ${shapeId} added (${+width.toFixed(4)} × ${+height.toFixed(4)} in).`);
|
|
3506
|
+
} catch (error) {
|
|
3507
|
+
if (request === this.#request && !isEditCancellation(error)) this.options.announce(editErrorMessage(error));
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
};
|
|
3511
|
+
//#endregion
|
|
3512
|
+
//#region src/viewer-commands.ts
|
|
3513
|
+
var editable = (target) => target instanceof Element && !!target.closest("input, textarea, select, [contenteditable]:not([contenteditable=\"false\"])");
|
|
3514
|
+
/**
|
|
3515
|
+
* Visio command state (tool, grid, pending edits) and keyboard shortcuts. Ribbon controls emit
|
|
3516
|
+
* typed `ribbon-action` events and shortcuts build the same actions; both go through
|
|
3517
|
+
* `routeRibbonAction`. Document mutation stays in the controller and core.
|
|
3518
|
+
*/
|
|
3519
|
+
var ViewerCommands = class {
|
|
3520
|
+
host;
|
|
3521
|
+
#tool = "pointer";
|
|
3522
|
+
#grid = false;
|
|
3523
|
+
#pending = 0;
|
|
3524
|
+
#draw;
|
|
3525
|
+
#targets;
|
|
3526
|
+
constructor(host) {
|
|
3527
|
+
this.host = host;
|
|
3528
|
+
this.#draw = new RectangleDrawTool(host.viewport, host.controller, {
|
|
3529
|
+
active: () => this.#tool === "rectangle",
|
|
3530
|
+
announce: host.announce
|
|
3531
|
+
});
|
|
3532
|
+
this.#targets = {
|
|
3533
|
+
controller: host.controller,
|
|
3534
|
+
history: (key) => this.#history(key),
|
|
3535
|
+
deleteSelection: () => this.#delete(),
|
|
3536
|
+
setTool: (tool) => this.setTool(tool),
|
|
3537
|
+
toggleGrid: () => {
|
|
3538
|
+
this.#grid = !this.#grid;
|
|
3539
|
+
this.render(host.controller.state);
|
|
3540
|
+
},
|
|
3541
|
+
toggleFullscreen: () => this.#toggleFullscreen(),
|
|
3542
|
+
togglePane: host.togglePane,
|
|
3543
|
+
reveal: host.reveal,
|
|
3544
|
+
fit: host.fit,
|
|
3545
|
+
focusSearch: host.focusSearch
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
get tool() {
|
|
3549
|
+
return this.#tool;
|
|
3550
|
+
}
|
|
3551
|
+
/** Run one typed action, exactly as a ribbon control or shortcut would. */
|
|
3552
|
+
run(action) {
|
|
3553
|
+
routeRibbonAction(this.#targets, action);
|
|
3554
|
+
}
|
|
3555
|
+
wire() {
|
|
3556
|
+
const { root, viewport } = this.host;
|
|
3557
|
+
const doc = root.ownerDocument;
|
|
3558
|
+
const events = new ((doc.defaultView?.AbortController) ?? AbortController)();
|
|
3559
|
+
const options = { signal: events.signal };
|
|
3560
|
+
root.addEventListener(RIBBON_ACTION_EVENT, (event) => this.run(event.detail), options);
|
|
3561
|
+
root.addEventListener("office-command", (event) => {
|
|
3562
|
+
if (event.detail?.command === "zoom-fit") this.run({
|
|
3563
|
+
type: "zoom",
|
|
3564
|
+
mode: "fit"
|
|
3565
|
+
});
|
|
3566
|
+
}, options);
|
|
3567
|
+
root.addEventListener("keydown", (event) => this.#shortcut(event), options);
|
|
3568
|
+
viewport.addEventListener("wheel", (event) => {
|
|
3569
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
3570
|
+
event.preventDefault();
|
|
3571
|
+
const zoom = this.host.controller.state.zoom;
|
|
3572
|
+
this.host.controller.setZoom(event.deltaY < 0 ? zoom * 1.1 : zoom / 1.1);
|
|
3573
|
+
}, {
|
|
3574
|
+
...options,
|
|
3575
|
+
passive: false
|
|
3576
|
+
});
|
|
3577
|
+
viewport.addEventListener("pointerdown", () => {
|
|
3578
|
+
if (!viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
|
|
3579
|
+
}, options);
|
|
3580
|
+
viewport.addEventListener("dblclick", (event) => {
|
|
3581
|
+
if (this.#tool !== "pointer") return;
|
|
3582
|
+
if (event.target?.closest?.("[data-shape-id]")) this.run({
|
|
3583
|
+
type: "reveal",
|
|
3584
|
+
panel: "edit",
|
|
3585
|
+
focusText: true
|
|
3586
|
+
});
|
|
3587
|
+
}, options);
|
|
3588
|
+
doc.addEventListener("fullscreenchange", () => this.render(this.host.controller.state), options);
|
|
3589
|
+
const disposeDraw = this.#draw.wire();
|
|
3590
|
+
return () => {
|
|
3591
|
+
++this.#pending;
|
|
3592
|
+
events.abort();
|
|
3593
|
+
disposeDraw();
|
|
3594
|
+
};
|
|
3595
|
+
}
|
|
3596
|
+
setTool(tool) {
|
|
3597
|
+
if (tool === "rectangle" && !this.#canEdit(this.host.controller.state)) return;
|
|
3598
|
+
this.#tool = tool;
|
|
3599
|
+
this.render(this.host.controller.state);
|
|
3600
|
+
}
|
|
3601
|
+
#canEdit(state) {
|
|
3602
|
+
return state.edit.sourceAvailable && !state.loading && !state.edit.busy;
|
|
3603
|
+
}
|
|
3604
|
+
#history(key) {
|
|
3605
|
+
const { controller } = this.host;
|
|
3606
|
+
const { edit, loading } = controller.state;
|
|
3607
|
+
if ((key === "undo" ? edit.canUndo : edit.canRedo) && !edit.busy && !loading) this.#edit(() => controller[key]());
|
|
3608
|
+
}
|
|
3609
|
+
#delete() {
|
|
3610
|
+
const state = this.host.controller.state;
|
|
3611
|
+
const shape = state.selectedShape;
|
|
3612
|
+
const pageId = shape?.pageId ?? state.document?.pages[state.pageIndex]?.id;
|
|
3613
|
+
if (!shape || pageId === void 0 || !this.#canEdit(state)) return;
|
|
3614
|
+
this.#edit(() => this.host.controller.applyEdits([{
|
|
3615
|
+
type: "delete-shape",
|
|
3616
|
+
pageId,
|
|
3617
|
+
shapeId: shape.id
|
|
3618
|
+
}]), `Deleted ${shape.name || `shape ${shape.id}`}.`);
|
|
3619
|
+
}
|
|
3620
|
+
async #edit(action, success) {
|
|
3621
|
+
const request = ++this.#pending;
|
|
3622
|
+
const { root, viewport } = this.host;
|
|
3623
|
+
const canvasFocused = viewport.contains(root.activeElement);
|
|
3624
|
+
try {
|
|
3625
|
+
await action();
|
|
3626
|
+
if (canvasFocused && !viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
|
|
3627
|
+
if (success && request === this.#pending) this.host.announce(success);
|
|
3628
|
+
} catch (error) {
|
|
3629
|
+
if (request === this.#pending && !isEditCancellation(error) && !this.host.controller.state.edit.error) this.host.announce(editErrorMessage(error));
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
#toggleFullscreen() {
|
|
3633
|
+
const host = this.host.root.host;
|
|
3634
|
+
const doc = this.host.root.ownerDocument;
|
|
3635
|
+
(doc.fullscreenElement === host ? doc.exitFullscreen?.() : host.requestFullscreen?.())?.catch?.(() => this.host.announce("Full screen is not available in this browser frame."));
|
|
3636
|
+
}
|
|
3637
|
+
/** Visio shortcuts as typed actions. Text fields keep native undo, deletion and editing keys. */
|
|
3638
|
+
#shortcutAction(event) {
|
|
3639
|
+
const state = this.host.controller.state;
|
|
3640
|
+
const control = event.ctrlKey || event.metaKey;
|
|
3641
|
+
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
|
|
3642
|
+
if (control && key === "f") return { type: "search" };
|
|
3643
|
+
if (control && event.shiftKey && key === "w") return {
|
|
3644
|
+
type: "zoom",
|
|
3645
|
+
mode: "fit"
|
|
3646
|
+
};
|
|
3647
|
+
if (control && (key === "PageDown" || key === "PageUp")) return {
|
|
3648
|
+
type: "page",
|
|
3649
|
+
step: key === "PageDown" ? 1 : -1
|
|
3650
|
+
};
|
|
3651
|
+
if (key === "F5" && !control) return { type: "fullscreen" };
|
|
3652
|
+
if (editable(event.target)) return void 0;
|
|
3653
|
+
if (control && !event.shiftKey && key === "z") return {
|
|
3654
|
+
type: "history",
|
|
3655
|
+
key: "undo"
|
|
3656
|
+
};
|
|
3657
|
+
if (control && (key === "y" || event.shiftKey && key === "z")) return {
|
|
3658
|
+
type: "history",
|
|
3659
|
+
key: "redo"
|
|
3660
|
+
};
|
|
3661
|
+
if (control && key === "1") return {
|
|
3662
|
+
type: "tool",
|
|
3663
|
+
tool: "pointer"
|
|
3664
|
+
};
|
|
3665
|
+
if (control && key === "8") return {
|
|
3666
|
+
type: "tool",
|
|
3667
|
+
tool: "rectangle"
|
|
3668
|
+
};
|
|
3669
|
+
if (!control && key === "Delete" && state.selectedShape) return { type: "delete" };
|
|
3670
|
+
if (!control && key === "F2" && state.document) return {
|
|
3671
|
+
type: "reveal",
|
|
3672
|
+
panel: "edit",
|
|
3673
|
+
focusText: true
|
|
3674
|
+
};
|
|
3675
|
+
if (key === "Escape" && this.#tool !== "pointer" && !this.#draw.drawing) return {
|
|
3676
|
+
type: "tool",
|
|
3677
|
+
tool: "pointer"
|
|
3678
|
+
};
|
|
3679
|
+
}
|
|
3680
|
+
#shortcut(event) {
|
|
3681
|
+
if (event.defaultPrevented || event.isComposing || event.altKey) return;
|
|
3682
|
+
const action = this.#shortcutAction(event);
|
|
3683
|
+
if (!action) return;
|
|
3684
|
+
event.preventDefault();
|
|
3685
|
+
this.run(action);
|
|
3686
|
+
}
|
|
3687
|
+
render(state) {
|
|
3688
|
+
const { root, viewport } = this.host;
|
|
3689
|
+
const button = (name) => root.querySelector(`office-ui-button[command="${name}"]`);
|
|
3690
|
+
const editing = this.#canEdit(state);
|
|
3691
|
+
if (this.#tool === "rectangle" && !state.edit.sourceAvailable) this.#tool = "pointer";
|
|
3692
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3693
|
+
button("undo").disabled = !state.edit.canUndo || state.edit.busy || state.loading;
|
|
3694
|
+
button("redo").disabled = !state.edit.canRedo || state.edit.busy || state.loading;
|
|
3695
|
+
button("delete").disabled = !editing || !state.selectedShape;
|
|
3696
|
+
button("pointer").setAttribute("pressed", String(this.#tool === "pointer"));
|
|
3697
|
+
const rectangle = button("rectangle");
|
|
3698
|
+
rectangle.setAttribute("pressed", String(this.#tool === "rectangle"));
|
|
3699
|
+
rectangle.disabled = !editing || !page;
|
|
3700
|
+
rectangle.title = state.edit.sourceAvailable ? "Rectangle (Ctrl+8)" : "Rectangle (Ctrl+8): open a .vsdx file to draw. Model-only documents are read only.";
|
|
3701
|
+
button("grid").setAttribute("pressed", String(this.#grid));
|
|
3702
|
+
button("grid").disabled = !page;
|
|
3703
|
+
for (const name of [
|
|
3704
|
+
"zoom-fit",
|
|
3705
|
+
"page-width",
|
|
3706
|
+
"actual-size"
|
|
3707
|
+
]) button(name).disabled = !page;
|
|
3708
|
+
const fullscreen = button("fullscreen");
|
|
3709
|
+
const host = root.host;
|
|
3710
|
+
fullscreen.disabled = typeof host.requestFullscreen !== "function";
|
|
3711
|
+
fullscreen.setAttribute("pressed", String(root.ownerDocument.fullscreenElement === host));
|
|
3712
|
+
viewport.dataset.tool = this.#tool;
|
|
3713
|
+
viewport.dataset.grid = String(this.#grid);
|
|
3714
|
+
}
|
|
3715
|
+
};
|
|
3716
|
+
//#endregion
|
|
3717
|
+
//#region src/office-ui.ts
|
|
3718
|
+
/**
|
|
3719
|
+
* Visio-only glyphs (20x20 path data). Neutral Office glyphs come from ooxml-ui; add a glyph
|
|
3720
|
+
* there instead when another Office product could use it.
|
|
3721
|
+
*/
|
|
3722
|
+
var VISIO_ICONS = {
|
|
3723
|
+
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",
|
|
3724
|
+
visioShapeData: "M3 3.5h14v13H3ZM3 7.5h14M8 7.5v9M10.5 11h4M10.5 14h4",
|
|
3725
|
+
visioPagesPane: "M2.5 3h15v14h-15ZM7 3v14M3.8 6h1.9M3.8 9h1.9M3.8 12h1.9",
|
|
3726
|
+
visioInspectorPane: "M2.5 3h15v14h-15ZM13 3v14M14.5 6h1.5M14.5 9h1.5M14.5 12h1.5"
|
|
3727
|
+
};
|
|
3728
|
+
/** Define the shared Office controls the viewer chrome uses. Idempotent; browser only. */
|
|
3729
|
+
function registerViewerControls(registry) {
|
|
3730
|
+
for (const [name, path] of Object.entries(VISIO_ICONS)) if (!getIcon(name)) registerIcon(name, path);
|
|
3731
|
+
registerControls(registry);
|
|
3732
|
+
}
|
|
3733
|
+
//#endregion
|
|
2983
3734
|
//#region src/export-svg.ts
|
|
2984
3735
|
var MAX_SVG_EXPORT_BYTES = 16777216;
|
|
2985
3736
|
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.";
|
|
@@ -3590,8 +4341,10 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3590
4341
|
#root;
|
|
3591
4342
|
#disposeInputs;
|
|
3592
4343
|
#viewport;
|
|
3593
|
-
#
|
|
3594
|
-
#
|
|
4344
|
+
#zoomSlider;
|
|
4345
|
+
#shapeStatus;
|
|
4346
|
+
#announcement;
|
|
4347
|
+
#commands;
|
|
3595
4348
|
#status;
|
|
3596
4349
|
#diagnostics;
|
|
3597
4350
|
#toolbar;
|
|
@@ -3624,10 +4377,14 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3624
4377
|
constructor() {
|
|
3625
4378
|
super();
|
|
3626
4379
|
this.#root = this.attachShadow({ mode: "open" });
|
|
3627
|
-
|
|
4380
|
+
registerViewerControls();
|
|
4381
|
+
this.#root.innerHTML = `<style>${viewerStyles}${canvasAndRibbonStyles}</style>${viewerChromeTemplate}`;
|
|
4382
|
+
const workspace = this.#root.querySelector(".workspace");
|
|
4383
|
+
workspace.before(createRibbon(document));
|
|
4384
|
+
workspace.after(createPageTabs(document), createStatusBar(document));
|
|
3628
4385
|
this.#viewport = this.#root.querySelector(".viewport");
|
|
3629
|
-
this.#
|
|
3630
|
-
this.#
|
|
4386
|
+
this.#zoomSlider = this.#root.querySelector("office-ui-zoom-slider");
|
|
4387
|
+
this.#shapeStatus = this.#root.querySelector("[data-shape-status]");
|
|
3631
4388
|
this.#status = this.#root.querySelector("[data-status]");
|
|
3632
4389
|
this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
|
|
3633
4390
|
this.#toolbar = this.#root.querySelector(".toolbar");
|
|
@@ -3638,6 +4395,23 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3638
4395
|
this.#notesPanel = this.#root.querySelector(".notes");
|
|
3639
4396
|
this.#inspector = this.#root.querySelector(".shape-inspector");
|
|
3640
4397
|
this.#chrome = new ViewerChrome(this.#root, this.controller);
|
|
4398
|
+
this.#commands = new ViewerCommands({
|
|
4399
|
+
root: this.#root,
|
|
4400
|
+
viewport: this.#viewport,
|
|
4401
|
+
controller: this.controller,
|
|
4402
|
+
fit: (mode) => this.#fit(mode),
|
|
4403
|
+
togglePane: (pane) => this.#chrome.togglePane(pane),
|
|
4404
|
+
reveal: (panel, focusText) => this.#chrome.reveal(panel, focusText),
|
|
4405
|
+
focusSearch: () => {
|
|
4406
|
+
this.#chrome.showTab("home");
|
|
4407
|
+
this.#search.input.focus();
|
|
4408
|
+
this.#search.input.select();
|
|
4409
|
+
},
|
|
4410
|
+
announce: (message) => {
|
|
4411
|
+
this.#announcement = message;
|
|
4412
|
+
this.#status.textContent = message;
|
|
4413
|
+
}
|
|
4414
|
+
});
|
|
3641
4415
|
this.#disposeInputs = this.#wireInputs();
|
|
3642
4416
|
this.#unsubscribe = this.controller.subscribe((state) => this.#render(state));
|
|
3643
4417
|
this.#eventUnsubscribe = this.controller.onEvent((name, detail) => {
|
|
@@ -3711,13 +4485,24 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3711
4485
|
}
|
|
3712
4486
|
fit() {
|
|
3713
4487
|
this.#assertAlive();
|
|
4488
|
+
this.#fit("page");
|
|
4489
|
+
}
|
|
4490
|
+
/** Visio Fit to Window shows the whole page; Page Width fills the canvas width. */
|
|
4491
|
+
#fit(mode) {
|
|
3714
4492
|
const page = this.document?.pages[this.pageIndex];
|
|
3715
4493
|
if (!page) return;
|
|
3716
4494
|
const style = this.ownerDocument.defaultView?.getComputedStyle(this.#viewport);
|
|
3717
4495
|
const padding = (value) => Number.parseFloat(value ?? "") || 0;
|
|
3718
4496
|
const width = Math.max(1, this.#viewport.clientWidth - padding(style?.paddingLeft) - padding(style?.paddingRight));
|
|
3719
4497
|
const height = Math.max(1, this.#viewport.clientHeight - padding(style?.paddingTop) - padding(style?.paddingBottom));
|
|
3720
|
-
|
|
4498
|
+
if (mode === "page") {
|
|
4499
|
+
this.zoom = Math.min(1, width / (page.width * 96), height / (page.height * 96));
|
|
4500
|
+
return;
|
|
4501
|
+
}
|
|
4502
|
+
const scrollbar = this.#viewport.offsetWidth - this.#viewport.clientWidth;
|
|
4503
|
+
const fitted = width / (page.width * 96);
|
|
4504
|
+
const reserve = page.height * 96 * fitted > height && scrollbar <= 0 ? 17 : 0;
|
|
4505
|
+
this.zoom = Math.max(1, width - reserve - 1) / (page.width * 96);
|
|
3721
4506
|
}
|
|
3722
4507
|
setLayerVisibility(pageId, layerId, visible) {
|
|
3723
4508
|
this.#assertAlive();
|
|
@@ -3787,16 +4572,18 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3787
4572
|
}
|
|
3788
4573
|
#wireInputs() {
|
|
3789
4574
|
const disposeChrome = this.#chrome.wire();
|
|
4575
|
+
const disposeCommands = this.#commands.wire();
|
|
3790
4576
|
const disposeEdit = this.#edit.wire();
|
|
3791
4577
|
const disposeLayers = wireLayerControls(this.#layers, this.controller);
|
|
3792
4578
|
const disposeInputs = wireViewerInputs({
|
|
3793
4579
|
viewport: this.#viewport,
|
|
3794
4580
|
commandRoot: this.#root,
|
|
3795
|
-
|
|
4581
|
+
zoomSlider: this.#zoomSlider,
|
|
3796
4582
|
searchInput: this.#search.input
|
|
3797
|
-
}, this.controller, () => this
|
|
4583
|
+
}, this.controller, (mode) => this.#fit(mode));
|
|
3798
4584
|
return () => {
|
|
3799
4585
|
disposeChrome();
|
|
4586
|
+
disposeCommands();
|
|
3800
4587
|
disposeInputs();
|
|
3801
4588
|
disposeLayers();
|
|
3802
4589
|
disposeEdit();
|
|
@@ -3815,14 +4602,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3815
4602
|
this.#document = state.document;
|
|
3816
4603
|
this.#renderedPage = state.pageIndex;
|
|
3817
4604
|
this.#renderedLayerOverrides = state.layerVisibilityOverrides;
|
|
3818
|
-
this.#
|
|
3819
|
-
for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
|
|
3820
|
-
const option = document.createElement("option");
|
|
3821
|
-
option.value = String(index);
|
|
3822
|
-
option.textContent = candidate.name + (candidate.isBackground ? " (background)" : "");
|
|
3823
|
-
this.#pageSelect.append(option);
|
|
3824
|
-
}
|
|
3825
|
-
this.#pageSelect.value = String(state.pageIndex);
|
|
4605
|
+
this.#announcement = void 0;
|
|
3826
4606
|
this.#renderWarnings = [];
|
|
3827
4607
|
if (state.document && page) {
|
|
3828
4608
|
const result = renderPage(state.document, page, { layerVisibilityOverrides: state.layerVisibilityOverrides });
|
|
@@ -3849,6 +4629,7 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3849
4629
|
if (svg && page) {
|
|
3850
4630
|
svg.style.width = `${page.width * 96 * state.zoom}px`;
|
|
3851
4631
|
svg.style.height = `${page.height * 96 * state.zoom}px`;
|
|
4632
|
+
svg.style.setProperty("--vv-inch", `${96 * state.zoom}px`);
|
|
3852
4633
|
}
|
|
3853
4634
|
const searchResult = state.search.results[state.search.activeIndex];
|
|
3854
4635
|
const selection = state.selectedShape;
|
|
@@ -3876,16 +4657,20 @@ var VisioViewerElement = class extends BaseElement {
|
|
|
3876
4657
|
this.#renderedSelection = selection ? { ...selection } : null;
|
|
3877
4658
|
}
|
|
3878
4659
|
if (!searchResult) this.#revealedSearchResult = void 0;
|
|
3879
|
-
this.#
|
|
3880
|
-
this.#
|
|
4660
|
+
this.#zoomSlider.value = Math.round(state.zoom * 100);
|
|
4661
|
+
this.#zoomSlider.disabled = !page;
|
|
4662
|
+
const inspected = selectedShape(state.document, state.selectedShape, state.pageIndex);
|
|
4663
|
+
const inches = (value) => `${+value.toFixed(3)} in`;
|
|
4664
|
+
this.#shapeStatus.setAttribute("value", inspected ? `Width: ${inches(inspected.width)} Height: ${inches(inspected.height)}` : "");
|
|
3881
4665
|
for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
|
|
3882
4666
|
renderSearchControls(this.#search, state);
|
|
3883
4667
|
this.#edit.render(state);
|
|
3884
4668
|
if (changed) renderLayerControls(this.#layers, state);
|
|
3885
4669
|
else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
|
|
3886
4670
|
this.#chrome.render(state, this.#notes.children.length);
|
|
4671
|
+
this.#commands.render(state);
|
|
3887
4672
|
this.#viewport.setAttribute("aria-busy", String(state.loading || state.edit.busy));
|
|
3888
|
-
this.#status.textContent = state.loading ? "Opening diagram…" : state.error?.message ?? (page ? `${page.name} · ${page.shapes.length} top-level shapes` : "No diagram open");
|
|
4673
|
+
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");
|
|
3889
4674
|
const warnings = (state.document?.diagnostics.length ?? 0) + this.#renderWarnings.length;
|
|
3890
4675
|
this.#diagnostics.textContent = warnings ? `${this.#notes.children.length} compatibility notes` : "Local-only viewing";
|
|
3891
4676
|
}
|
|
@@ -4095,6 +4880,23 @@ function viewerHandle(current) {
|
|
|
4095
4880
|
}
|
|
4096
4881
|
};
|
|
4097
4882
|
}
|
|
4883
|
+
function viewerStateSource(current) {
|
|
4884
|
+
const controller = () => {
|
|
4885
|
+
try {
|
|
4886
|
+
return current()?.controller;
|
|
4887
|
+
} catch {
|
|
4888
|
+
return;
|
|
4889
|
+
}
|
|
4890
|
+
};
|
|
4891
|
+
return {
|
|
4892
|
+
subscribe(onChange) {
|
|
4893
|
+
return controller()?.subscribe(() => onChange()) ?? (() => {});
|
|
4894
|
+
},
|
|
4895
|
+
getSnapshot() {
|
|
4896
|
+
return controller()?.state ?? null;
|
|
4897
|
+
}
|
|
4898
|
+
};
|
|
4899
|
+
}
|
|
4098
4900
|
//#endregion
|
|
4099
4901
|
//#region packages/bindings/src/solid.tsx
|
|
4100
4902
|
var _tmpl$ = [
|
|
@@ -4138,5 +4940,19 @@ function VisioViewer(props) {
|
|
|
4138
4940
|
});
|
|
4139
4941
|
return ssr(_tmpl$, ssrHydrationKey() + ssrAttribute("class", escape(props.class, true), false), ssrStyle(props.style));
|
|
4140
4942
|
}
|
|
4943
|
+
/**
|
|
4944
|
+
* Reactive viewer state as a Solid accessor for a handle accessor (for example a signal set by
|
|
4945
|
+
* `viewerRef`). Follows handle changes and cleans up with the owning scope.
|
|
4946
|
+
*/
|
|
4947
|
+
function createVisioViewerState(handle) {
|
|
4948
|
+
const [state, setState] = createSignal(null);
|
|
4949
|
+
createEffect(() => {
|
|
4950
|
+
const current = handle();
|
|
4951
|
+
const source = viewerStateSource(() => current);
|
|
4952
|
+
setState(() => source.getSnapshot());
|
|
4953
|
+
onCleanup(source.subscribe(() => setState(() => source.getSnapshot())));
|
|
4954
|
+
});
|
|
4955
|
+
return state;
|
|
4956
|
+
}
|
|
4141
4957
|
//#endregion
|
|
4142
|
-
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 };
|
|
4958
|
+
export { MAX_SVG_EXPORT_BYTES, PRINT_SNAPSHOT_LIMITS, TEXT_SEARCH_LIMITS, VIEWER_LAYER_LIMITS, ViewerController, VisioViewer, VisioViewerElement, compatibilityNotes, compatibilityText, createPrintSnapshot, createVisioViewerState, createWorkerParser, eventKeys, exportPageSvg, mountViewer, propertyKeys, registerVisioViewer, renderPage };
|