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