@miragon/modeler-shell 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/API.md +1051 -84
- package/README.md +162 -31
- package/dist/appearance-BBaM_4bD.d.ts +323 -0
- package/dist/appearance-XX3JC3pI.d.cts +323 -0
- package/dist/{chunk-6LQOFACK.js → chunk-66HCSJNY.js} +1247 -31
- package/dist/chunk-66HCSJNY.js.map +1 -0
- package/dist/{chunk-RJRFR6N3.js → chunk-KYDG5DMX.js} +2 -2
- package/dist/{chunk-5JSZ4VGA.js → chunk-OADW3GPA.js} +240 -6
- package/dist/chunk-OADW3GPA.js.map +1 -0
- package/dist/core.cjs +260 -6
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +81 -6
- package/dist/core.d.ts +81 -6
- package/dist/core.js +43 -3
- package/dist/diagram-js.cjs +34 -2
- package/dist/diagram-js.cjs.map +1 -1
- package/dist/diagram-js.d.cts +10 -3
- package/dist/diagram-js.d.ts +10 -3
- package/dist/diagram-js.js +14 -2
- package/dist/diagram-js.js.map +1 -1
- package/dist/index.cjs +1654 -197
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -9
- package/dist/index.d.ts +18 -9
- package/dist/index.js +44 -4
- package/dist/shell.css +692 -3
- package/dist/testing-vscode-stub.js +2 -2
- package/dist/testing.cjs +2849 -550
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +75 -9
- package/dist/testing.d.ts +75 -9
- package/dist/testing.js +2744 -558
- package/dist/testing.js.map +1 -1
- package/dist/toasts-Bp0REqdq.d.cts +142 -0
- package/dist/toasts-Bp0REqdq.d.ts +142 -0
- package/dist/{types-OuFlqZp0.d.cts → types-CnRrINRs.d.cts} +170 -9
- package/dist/{types-DnZn9CPw.d.ts → types-PuDZczzy.d.ts} +170 -9
- package/dist/vscode-extension.js +2 -2
- package/dist/vscode-webview.cjs +1541 -178
- package/dist/vscode-webview.cjs.map +1 -1
- package/dist/vscode-webview.d.cts +6 -4
- package/dist/vscode-webview.d.ts +6 -4
- package/dist/vscode-webview.js +11 -2
- package/dist/vscode-webview.js.map +1 -1
- package/package.json +2 -2
- package/src/core/feedback/appearance.ts +141 -0
- package/src/core/feedback/problems.ts +23 -0
- package/src/core/feedback/toasts.ts +118 -0
- package/src/core/fit.ts +18 -3
- package/src/core/index.ts +43 -0
- package/src/core/keymap.ts +20 -0
- package/src/core/messages.ts +71 -0
- package/src/core/types.ts +24 -2
- package/src/core/web-chrome/bottom-row.ts +37 -0
- package/src/core/web-chrome/drop.ts +78 -0
- package/src/core/web-chrome/top-row.ts +66 -0
- package/src/diagram-js/service.ts +4 -1
- package/src/diagram-js/viewport.ts +19 -0
- package/src/dom/buttons.css +124 -0
- package/src/dom/feedback/appearance.ts +184 -0
- package/src/dom/feedback/feedback.css +435 -0
- package/src/dom/feedback/problems-popover.ts +287 -0
- package/src/dom/feedback/toasts.ts +431 -0
- package/src/dom/index.ts +13 -0
- package/src/dom/keys.ts +8 -0
- package/src/dom/menu.ts +28 -17
- package/src/dom/mount.ts +290 -20
- package/src/dom/problems.ts +5 -4
- package/src/dom/shell-dom.css +7 -4
- package/src/dom/types.ts +188 -6
- package/src/dom/web/drop-zone.ts +181 -0
- package/src/dom/web/empty-state.ts +144 -0
- package/src/dom/web/legal.ts +75 -0
- package/src/dom/web/menu-groups.ts +69 -0
- package/src/dom/web/primary.ts +124 -0
- package/src/dom/web/web.css +386 -0
- package/src/shell.css +3 -0
- package/src/testing/case-tools.ts +164 -0
- package/src/testing/cases.ts +209 -144
- package/src/testing/checks.ts +51 -15
- package/src/testing/facts.ts +56 -2
- package/src/testing/geometry.ts +206 -26
- package/src/testing/harness.ts +56 -7
- package/src/testing/index.ts +4 -0
- package/src/testing/page-tools.ts +24 -1
- package/src/testing/page.ts +10 -0
- package/src/testing/web-cases.ts +913 -0
- package/src/testing/web-checks.ts +1049 -0
- package/src/testing/web-facts.ts +713 -0
- package/src/vscode/webview/mount.ts +13 -1
- package/dist/chunk-5JSZ4VGA.js.map +0 -1
- package/dist/chunk-6LQOFACK.js.map +0 -1
- package/dist/keymap-CObQE6q6.d.cts +0 -151
- package/dist/keymap-DORzHnK-.d.ts +0 -151
- package/dist/types-B6NfkiUG.d.cts +0 -66
- package/dist/types-B6NfkiUG.d.ts +0 -66
- /package/dist/{chunk-RJRFR6N3.js.map → chunk-KYDG5DMX.js.map} +0 -0
package/dist/vscode-webview.cjs
CHANGED
|
@@ -60,13 +60,28 @@ var SHELL = {
|
|
|
60
60
|
layerToast: 60
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
// src/core/web-chrome/bottom-row.ts
|
|
64
|
+
var LEGAL_LINE_HEIGHT = 20;
|
|
65
|
+
function legalLineHeight(measured) {
|
|
66
|
+
if (measured === void 0 || !Number.isFinite(measured) || measured <= 0) return LEGAL_LINE_HEIGHT;
|
|
67
|
+
return Math.max(1, Math.round(measured / LEGAL_LINE_HEIGHT)) * LEGAL_LINE_HEIGHT;
|
|
68
|
+
}
|
|
69
|
+
function zoomRowRaised(input) {
|
|
70
|
+
const { containerWidth, legalRight, zoomWidth } = input;
|
|
71
|
+
if (legalRight === void 0 || zoomWidth === void 0) return false;
|
|
72
|
+
const zoomLeft = containerWidth - SHELL.edge - zoomWidth;
|
|
73
|
+
return legalRight + SHELL.gap > zoomLeft;
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
// src/core/fit.ts
|
|
64
77
|
function fitInsets(layout) {
|
|
65
|
-
const { container, host, paletteRow, problemsVisible, legendWidth, inspector } = layout;
|
|
78
|
+
const { container, host, paletteRow, problemsVisible, zoomRaised, legendWidth, inspector } = layout;
|
|
79
|
+
const raisedBottomRow = host === "web" && (problemsVisible || zoomRaised === true);
|
|
80
|
+
const wrappedLegalLines = legalLineHeight(layout.legalHeight) - LEGAL_LINE_HEIGHT;
|
|
66
81
|
const insets = {
|
|
67
82
|
top: paletteRow === "second-row" ? SHELL.fitInsetTopSecondRow : SHELL.fitInsetTop,
|
|
68
83
|
right: SHELL.fitInsetSide,
|
|
69
|
-
bottom:
|
|
84
|
+
bottom: raisedBottomRow ? SHELL.fitInsetBottomWeb + wrappedLegalLines : SHELL.fitInsetBottom,
|
|
70
85
|
left: SHELL.fitInsetSide
|
|
71
86
|
};
|
|
72
87
|
const bottomSheetOpen = inspector !== void 0 && container.width <= SHELL.narrow;
|
|
@@ -201,6 +216,7 @@ function decideKey(event, context) {
|
|
|
201
216
|
}
|
|
202
217
|
const chromeKey = matchChromeKey(event);
|
|
203
218
|
if (chromeKey !== void 0) {
|
|
219
|
+
if (context.startScreen === true && !stop) return { preventDefault: false, stopPropagation: false };
|
|
204
220
|
const blocked = composing || context.inTextField || context.modalOpen;
|
|
205
221
|
if (!blocked) return { action: chromeKey, preventDefault: true, stopPropagation: stop };
|
|
206
222
|
return { preventDefault: false, stopPropagation: chromeKey === "fit" ? false : stop };
|
|
@@ -371,7 +387,27 @@ var defaultMessages = {
|
|
|
371
387
|
emptyHint: "Pick a tool from the palette to start.",
|
|
372
388
|
insertExample: "Insert example",
|
|
373
389
|
invalidFile: (diagram) => `This file isn't a valid ${diagram} right now. The diagram shows the last valid version and is read-only until you fix the text.`,
|
|
374
|
-
keyNames: { ctrl: "Ctrl", shift: "Shift", plus: "Plus", minus: "Minus" }
|
|
390
|
+
keyNames: { ctrl: "Ctrl", shift: "Shift", plus: "Plus", minus: "Minus" },
|
|
391
|
+
share: "Share",
|
|
392
|
+
legal: "Legal",
|
|
393
|
+
copyright: (year) => `\xA9 ${year} Miragon GmbH`,
|
|
394
|
+
opensInNewTab: "Opens in a new tab",
|
|
395
|
+
newDiagram: "New diagram",
|
|
396
|
+
openFile: "Open\u2026",
|
|
397
|
+
showExample: "Show example",
|
|
398
|
+
importFile: "Import\u2026",
|
|
399
|
+
appearance: "Appearance",
|
|
400
|
+
appearanceSystem: "System",
|
|
401
|
+
appearanceLight: "Light",
|
|
402
|
+
appearanceDark: "Dark",
|
|
403
|
+
problemsTitle: "Problems",
|
|
404
|
+
problemSeverity: (severity) => severity === "error" ? "Error" : "Warning",
|
|
405
|
+
close: "Close",
|
|
406
|
+
dropFile: "Drop file here",
|
|
407
|
+
dropForbidden: (reason) => reason === "multiple" ? "Drop one file at a time" : reason === "dialog" ? "Close the dialog to drop a file" : "This modeler can't open this file type",
|
|
408
|
+
dropHint: "Or drop a file here",
|
|
409
|
+
openFileLink: "Open a file\u2026",
|
|
410
|
+
cannotOpenFile: (name, accepted) => `Couldn't open \u201C${name}\u201D. Drop a ${accepted} file.`
|
|
375
411
|
};
|
|
376
412
|
function resolveMessages(overrides) {
|
|
377
413
|
const { keyNames, ...texts } = overrides ?? {};
|
|
@@ -410,6 +446,121 @@ function placeTooltip(input) {
|
|
|
410
446
|
return { x, y };
|
|
411
447
|
}
|
|
412
448
|
|
|
449
|
+
// src/core/web-chrome/top-row.ts
|
|
450
|
+
function decideTopRow(input) {
|
|
451
|
+
const { containerWidth, paletteWidth, menuRight, primary } = input;
|
|
452
|
+
if (!primary) {
|
|
453
|
+
return {
|
|
454
|
+
paletteRow: decidePaletteRow({ containerWidth, paletteWidth, menuRight }),
|
|
455
|
+
primaryLabel: "shown"
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const paletteRowBeside = (primaryWidth) => decidePaletteRow({
|
|
459
|
+
containerWidth,
|
|
460
|
+
paletteWidth,
|
|
461
|
+
menuRight,
|
|
462
|
+
primaryLeft: containerWidth - SHELL.edge - primaryWidth
|
|
463
|
+
});
|
|
464
|
+
if (paletteRowBeside(primary.withLabel) === "top") return { paletteRow: "top", primaryLabel: "shown" };
|
|
465
|
+
if (paletteRowBeside(primary.iconOnly) === "top") return { paletteRow: "top", primaryLabel: "collapsed" };
|
|
466
|
+
const labelledLeft = containerWidth - SHELL.edge - primary.withLabel;
|
|
467
|
+
return {
|
|
468
|
+
paletteRow: "second-row",
|
|
469
|
+
primaryLabel: menuRight + PALETTE_CLEARANCE <= labelledLeft ? "shown" : "collapsed"
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/core/web-chrome/drop.ts
|
|
474
|
+
function acceptEntries(accept) {
|
|
475
|
+
const entries = accept.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry !== "");
|
|
476
|
+
return {
|
|
477
|
+
extensions: entries.filter((entry) => entry.startsWith(".")),
|
|
478
|
+
mimeTypes: entries.filter((entry) => !entry.startsWith(".") && entry.includes("/"))
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function matchesMimeType(type, mimeTypes) {
|
|
482
|
+
const normalized = type.trim().toLowerCase();
|
|
483
|
+
if (normalized === "") return false;
|
|
484
|
+
return mimeTypes.some(
|
|
485
|
+
(entry) => entry.endsWith("/*") ? normalized.startsWith(entry.slice(0, -1)) : normalized === entry
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
function dragVerdict(items, context) {
|
|
489
|
+
const files = items.filter((item) => item.kind === "file");
|
|
490
|
+
if (files.length === 0) return { state: "none" };
|
|
491
|
+
if (context.dialogOpen) return { state: "forbidden", reason: "dialog" };
|
|
492
|
+
if (files.length > 1 && !context.multiple) return { state: "forbidden", reason: "multiple" };
|
|
493
|
+
const { mimeTypes } = acceptEntries(context.accept);
|
|
494
|
+
const foreignType = (item) => item.type.trim() !== "" && !matchesMimeType(item.type, mimeTypes);
|
|
495
|
+
if (mimeTypes.length > 0 && files.every(foreignType)) return { state: "forbidden", reason: "type" };
|
|
496
|
+
return { state: "over" };
|
|
497
|
+
}
|
|
498
|
+
function acceptsFile(file, accept) {
|
|
499
|
+
const { extensions, mimeTypes } = acceptEntries(accept);
|
|
500
|
+
if (extensions.length === 0 && mimeTypes.length === 0) return true;
|
|
501
|
+
const name = file.name.toLowerCase();
|
|
502
|
+
return extensions.some((extension) => name.endsWith(extension)) || matchesMimeType(file.type, mimeTypes);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/core/feedback/toasts.ts
|
|
506
|
+
var MAX_TOASTS = 3;
|
|
507
|
+
var TOAST_DURATION_MS = {
|
|
508
|
+
success: 2800,
|
|
509
|
+
info: 2800,
|
|
510
|
+
warning: 5e3,
|
|
511
|
+
danger: 5e3
|
|
512
|
+
};
|
|
513
|
+
var TOAST_MAX_WIDTH = 480;
|
|
514
|
+
function toastLifetime(request) {
|
|
515
|
+
if (request.action) return void 0;
|
|
516
|
+
const requested = request.durationMs;
|
|
517
|
+
const longer = requested !== void 0 && Number.isFinite(requested) ? requested : 0;
|
|
518
|
+
return Math.max(TOAST_DURATION_MS[request.status], longer);
|
|
519
|
+
}
|
|
520
|
+
function toastToEvict(stack) {
|
|
521
|
+
if (stack.length < MAX_TOASTS) return void 0;
|
|
522
|
+
const evicted = stack.find((slot) => !slot.engaged && !slot.hasAction) ?? stack.find((slot) => !slot.engaged) ?? stack[0];
|
|
523
|
+
return evicted?.id;
|
|
524
|
+
}
|
|
525
|
+
function toastStackWidth(containerWidth) {
|
|
526
|
+
return Math.max(0, Math.min(TOAST_MAX_WIDTH, containerWidth - 2 * SHELL.edge));
|
|
527
|
+
}
|
|
528
|
+
function toastStackBottom(input) {
|
|
529
|
+
const stackWidth = toastStackWidth(input.containerWidth);
|
|
530
|
+
const { cardWidth } = input;
|
|
531
|
+
const width = cardWidth !== void 0 && Number.isFinite(cardWidth) ? Math.min(stackWidth, Math.max(0, cardWidth)) : stackWidth;
|
|
532
|
+
const left = (input.containerWidth - width) / 2;
|
|
533
|
+
const right = left + width;
|
|
534
|
+
let highest;
|
|
535
|
+
for (const obstacle of input.obstacles) {
|
|
536
|
+
const distance = Math.max(obstacle.left - right, left - obstacle.right);
|
|
537
|
+
if (distance >= SHELL.gap) continue;
|
|
538
|
+
highest = highest === void 0 ? obstacle.top : Math.max(highest, obstacle.top);
|
|
539
|
+
}
|
|
540
|
+
return highest === void 0 ? SHELL.edge : Math.max(SHELL.edge, highest + SHELL.gap);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/core/feedback/appearance.ts
|
|
544
|
+
var APPEARANCE_STORAGE_KEY = "miragon.darstellung";
|
|
545
|
+
function parseAppearanceChoice(stored) {
|
|
546
|
+
return stored === "light" || stored === "dark" ? stored : "system";
|
|
547
|
+
}
|
|
548
|
+
function documentMode(choice, themes, colors) {
|
|
549
|
+
if (!themes.includes("dark")) return fixedMode("light", colors.light);
|
|
550
|
+
const parsed = parseAppearanceChoice(choice);
|
|
551
|
+
if (parsed === "system") {
|
|
552
|
+
return {
|
|
553
|
+
theme: "system",
|
|
554
|
+
colorScheme: "light dark",
|
|
555
|
+
themeColor: { light: colors.light, dark: colors.dark }
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
return fixedMode(parsed, colors[parsed]);
|
|
559
|
+
}
|
|
560
|
+
function fixedMode(theme, color) {
|
|
561
|
+
return { theme, colorScheme: theme, themeColor: { light: color, dark: color } };
|
|
562
|
+
}
|
|
563
|
+
|
|
413
564
|
// src/vscode/protocol.ts
|
|
414
565
|
var APPEARANCE_MESSAGE_TYPE = "modelerShell.appearance";
|
|
415
566
|
function appearanceMessage(appearance) {
|
|
@@ -554,163 +705,134 @@ function hasVisibleFocus(element) {
|
|
|
554
705
|
}
|
|
555
706
|
}
|
|
556
707
|
|
|
557
|
-
// src/dom/
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
element.hidden = !shown;
|
|
580
|
-
},
|
|
581
|
-
place({ top, right, bottom, left }) {
|
|
582
|
-
element.style.inset = `${top}px ${right}px ${bottom}px ${left}px`;
|
|
583
|
-
},
|
|
584
|
-
destroy() {
|
|
585
|
-
element.remove();
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// src/dom/inspector.ts
|
|
591
|
-
function createInspector(ownerDocument, options, onChange) {
|
|
592
|
-
const element = createElement(ownerDocument, "aside", {
|
|
593
|
-
class: "cd-shell-inspector",
|
|
594
|
-
"data-chrome": "inspector",
|
|
595
|
-
"aria-label": options.label
|
|
596
|
-
});
|
|
597
|
-
element.hidden = true;
|
|
598
|
-
function setOpen(open) {
|
|
599
|
-
if (open === !element.hidden) return;
|
|
600
|
-
element.hidden = !open;
|
|
601
|
-
onChange(open);
|
|
708
|
+
// src/dom/feedback/appearance.ts
|
|
709
|
+
var NO_TRANSITION_CLASS2 = "cd-shell-no-transition";
|
|
710
|
+
var CONTRAST_QUERY = "(prefers-contrast: more)";
|
|
711
|
+
var SCHEME_MEDIA = {
|
|
712
|
+
light: "(prefers-color-scheme: light)",
|
|
713
|
+
dark: "(prefers-color-scheme: dark)"
|
|
714
|
+
};
|
|
715
|
+
function onMediaChange(query, listener) {
|
|
716
|
+
if (typeof query.addEventListener === "function") {
|
|
717
|
+
query.addEventListener("change", listener);
|
|
718
|
+
return () => query.removeEventListener("change", listener);
|
|
719
|
+
}
|
|
720
|
+
query.addListener(listener);
|
|
721
|
+
return () => query.removeListener(listener);
|
|
722
|
+
}
|
|
723
|
+
function connectWebAppearance(context) {
|
|
724
|
+
const { ownerDocument, themes, options } = context;
|
|
725
|
+
const darkCapable = themes.includes("dark");
|
|
726
|
+
if (darkCapable && !options) {
|
|
727
|
+
throw new Error(
|
|
728
|
+
'@miragon/modeler-shell: a web modeler whose themes include "dark" needs the appearance option ({ themeColors } from its vendored cd-tokens.generated.json)'
|
|
729
|
+
);
|
|
602
730
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
731
|
+
const html = ownerDocument.documentElement;
|
|
732
|
+
const view = ownerDocument.defaultView;
|
|
733
|
+
const cleanups = [];
|
|
734
|
+
const contrast = typeof view?.matchMedia === "function" ? view.matchMedia(CONTRAST_QUERY) : void 0;
|
|
735
|
+
if (contrast) {
|
|
736
|
+
const mirrorContrast = () => toggleAttribute(html, "data-cd-kontrast", contrast.matches && "hoch");
|
|
737
|
+
mirrorContrast();
|
|
738
|
+
cleanups.push(onMediaChange(contrast, mirrorContrast));
|
|
739
|
+
}
|
|
740
|
+
function destroy() {
|
|
741
|
+
for (const cleanup of cleanups.splice(0)) cleanup();
|
|
742
|
+
}
|
|
743
|
+
if (!darkCapable || !options) {
|
|
744
|
+
return { choice: () => "system", setChoice() {
|
|
745
|
+
}, destroy };
|
|
746
|
+
}
|
|
747
|
+
const { themeColors } = options;
|
|
748
|
+
function storage() {
|
|
749
|
+
if (options?.storage) return options.storage;
|
|
750
|
+
try {
|
|
751
|
+
return globalThis.localStorage;
|
|
752
|
+
} catch {
|
|
753
|
+
return void 0;
|
|
610
754
|
}
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// src/dom/keys.ts
|
|
615
|
-
function isTextField(target) {
|
|
616
|
-
if (!target || typeof target.closest !== "function") return false;
|
|
617
|
-
const element = target;
|
|
618
|
-
return element.isContentEditable === true || element.closest(TEXT_FIELD_SELECTOR) !== null;
|
|
619
|
-
}
|
|
620
|
-
function isShown(element) {
|
|
621
|
-
if (element.localName === "dialog" && !element.hasAttribute("open")) return false;
|
|
622
|
-
if (element.closest("[hidden]")) return false;
|
|
623
|
-
return typeof element.checkVisibility !== "function" || element.checkVisibility({ visibilityProperty: true });
|
|
624
|
-
}
|
|
625
|
-
function isModal(element) {
|
|
626
|
-
if (element.getAttribute("aria-modal") === "true") return true;
|
|
627
|
-
try {
|
|
628
|
-
return element.matches(":modal");
|
|
629
|
-
} catch {
|
|
630
|
-
return false;
|
|
631
755
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (event.key === "Escape") context.hideTooltip();
|
|
639
|
-
const dialogs = openDialogs(context.dialogScope);
|
|
640
|
-
const decision = decideKey(event, {
|
|
641
|
-
host: context.host,
|
|
642
|
-
inTextField: isTextField(event.target),
|
|
643
|
-
menuOpen: context.menuOpen(),
|
|
644
|
-
dialogOpen: dialogs.length > 0,
|
|
645
|
-
modalOpen: dialogs.some(isModal),
|
|
646
|
-
defaultPrevented: event.defaultPrevented
|
|
647
|
-
});
|
|
648
|
-
if (decision.preventDefault) event.preventDefault();
|
|
649
|
-
if (decision.stopPropagation) event.stopPropagation();
|
|
650
|
-
if (decision.action === "closeMenu") context.closeMenu();
|
|
651
|
-
else if (decision.action) context.run(decision.action);
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// src/dom/ledger.ts
|
|
656
|
-
var Ledger = class {
|
|
657
|
-
undo = [];
|
|
658
|
-
originalAttributes = /* @__PURE__ */ new Map();
|
|
659
|
-
closed = false;
|
|
660
|
-
get isClosed() {
|
|
661
|
-
return this.closed;
|
|
662
|
-
}
|
|
663
|
-
/** Runs `task` on `close()`, in reverse order of registration. */
|
|
664
|
-
defer(task) {
|
|
665
|
-
this.undo.push(task);
|
|
666
|
-
}
|
|
667
|
-
/** Adds `className` unless present; `close()` removes it only if this ledger added it. */
|
|
668
|
-
addClass(element, className) {
|
|
669
|
-
if (element.classList.contains(className)) return;
|
|
670
|
-
const hadClassAttribute = element.hasAttribute("class");
|
|
671
|
-
element.classList.add(className);
|
|
672
|
-
this.defer(() => {
|
|
673
|
-
element.classList.remove(className);
|
|
674
|
-
if (!hadClassAttribute && element.classList.length === 0) element.removeAttribute("class");
|
|
675
|
-
});
|
|
756
|
+
function readStored() {
|
|
757
|
+
try {
|
|
758
|
+
return parseAppearanceChoice(storage()?.getItem(APPEARANCE_STORAGE_KEY));
|
|
759
|
+
} catch {
|
|
760
|
+
return "system";
|
|
761
|
+
}
|
|
676
762
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
763
|
+
function store(choice) {
|
|
764
|
+
try {
|
|
765
|
+
const target = storage();
|
|
766
|
+
if (choice === "system") target?.removeItem(APPEARANCE_STORAGE_KEY);
|
|
767
|
+
else target?.setItem(APPEARANCE_STORAGE_KEY, choice);
|
|
768
|
+
} catch {
|
|
683
769
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
else element.setAttribute(name, original);
|
|
690
|
-
});
|
|
770
|
+
}
|
|
771
|
+
function nextFrame(callback) {
|
|
772
|
+
if (view && typeof view.requestAnimationFrame === "function") {
|
|
773
|
+
const frame = view.requestAnimationFrame(() => callback());
|
|
774
|
+
return () => view.cancelAnimationFrame(frame);
|
|
691
775
|
}
|
|
692
|
-
|
|
693
|
-
|
|
776
|
+
const timer = setTimeout(callback, 16);
|
|
777
|
+
return () => clearTimeout(timer);
|
|
694
778
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
779
|
+
let cancelTransitionFrame;
|
|
780
|
+
function allowTransitions() {
|
|
781
|
+
cancelTransitionFrame?.();
|
|
782
|
+
cancelTransitionFrame = void 0;
|
|
783
|
+
html.classList.remove(NO_TRANSITION_CLASS2);
|
|
699
784
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
this.undo.length = 0;
|
|
711
|
-
this.originalAttributes.clear();
|
|
785
|
+
cleanups.push(() => {
|
|
786
|
+
if (cancelTransitionFrame) allowTransitions();
|
|
787
|
+
});
|
|
788
|
+
function suspendTransitions() {
|
|
789
|
+
cancelTransitionFrame?.();
|
|
790
|
+
html.classList.add(NO_TRANSITION_CLASS2);
|
|
791
|
+
cancelTransitionFrame = nextFrame(() => {
|
|
792
|
+
cancelTransitionFrame = nextFrame(allowTransitions);
|
|
793
|
+
});
|
|
712
794
|
}
|
|
713
|
-
|
|
795
|
+
function metaFor(name, media) {
|
|
796
|
+
const normalize = (value) => (value ?? "").replace(/\s+/g, "").toLowerCase();
|
|
797
|
+
const existing = [...ownerDocument.querySelectorAll(`meta[name="${name}"]`)].find(
|
|
798
|
+
(meta) => media === void 0 ? true : normalize(meta.getAttribute("media")) === normalize(media)
|
|
799
|
+
);
|
|
800
|
+
if (existing) return existing;
|
|
801
|
+
const created = createElement(ownerDocument, "meta", { name, media });
|
|
802
|
+
(ownerDocument.head ?? html).append(created);
|
|
803
|
+
return created;
|
|
804
|
+
}
|
|
805
|
+
function apply(choice) {
|
|
806
|
+
const mode = documentMode(choice, themes, themeColors);
|
|
807
|
+
suspendTransitions();
|
|
808
|
+
html.setAttribute("data-cd-theme", mode.theme);
|
|
809
|
+
metaFor("color-scheme").setAttribute("content", mode.colorScheme);
|
|
810
|
+
metaFor("theme-color", SCHEME_MEDIA.light).setAttribute("content", mode.themeColor.light);
|
|
811
|
+
metaFor("theme-color", SCHEME_MEDIA.dark).setAttribute("content", mode.themeColor.dark);
|
|
812
|
+
}
|
|
813
|
+
let current = readStored();
|
|
814
|
+
const onStorage = (event) => {
|
|
815
|
+
if (event.key !== null && event.key !== APPEARANCE_STORAGE_KEY) return;
|
|
816
|
+
const next = event.key === null ? "system" : parseAppearanceChoice(event.newValue);
|
|
817
|
+
if (next === current) return;
|
|
818
|
+
current = next;
|
|
819
|
+
apply(next);
|
|
820
|
+
};
|
|
821
|
+
if (view) {
|
|
822
|
+
view.addEventListener("storage", onStorage);
|
|
823
|
+
cleanups.push(() => view.removeEventListener("storage", onStorage));
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
choice: () => current,
|
|
827
|
+
setChoice(choice) {
|
|
828
|
+
const next = parseAppearanceChoice(choice);
|
|
829
|
+
current = next;
|
|
830
|
+
store(next);
|
|
831
|
+
apply(next);
|
|
832
|
+
},
|
|
833
|
+
destroy
|
|
834
|
+
};
|
|
835
|
+
}
|
|
714
836
|
|
|
715
837
|
// src/dom/icons.ts
|
|
716
838
|
var ICONS = {
|
|
@@ -928,9 +1050,672 @@ function iconElement(icon, options) {
|
|
|
928
1050
|
for (const [name, value] of attributes) child.setAttribute(name, value);
|
|
929
1051
|
svg.append(child);
|
|
930
1052
|
}
|
|
931
|
-
return svg;
|
|
1053
|
+
return svg;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/dom/feedback/problems-popover.ts
|
|
1057
|
+
var PILL_ATTRIBUTES = ["aria-haspopup", "aria-expanded", "aria-controls"];
|
|
1058
|
+
function isPrintable(event) {
|
|
1059
|
+
return event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
1060
|
+
}
|
|
1061
|
+
function createProblemsPopover(context) {
|
|
1062
|
+
const { ownerDocument, root, layer, pill, id, titleId, messages } = context;
|
|
1063
|
+
const view = ownerDocument.defaultView;
|
|
1064
|
+
const list = createElement(ownerDocument, "ul", { class: "cd-shell-problems-list", role: "list" });
|
|
1065
|
+
const element = createElement(
|
|
1066
|
+
ownerDocument,
|
|
1067
|
+
"div",
|
|
1068
|
+
{
|
|
1069
|
+
class: "cd-shell-problems-popover",
|
|
1070
|
+
"data-chrome": "problems-popover",
|
|
1071
|
+
role: "dialog",
|
|
1072
|
+
id,
|
|
1073
|
+
"aria-labelledby": titleId,
|
|
1074
|
+
tabindex: "-1"
|
|
1075
|
+
},
|
|
1076
|
+
createElement(
|
|
1077
|
+
ownerDocument,
|
|
1078
|
+
"h2",
|
|
1079
|
+
{ class: "cd-shell-problems-popover-title", id: titleId },
|
|
1080
|
+
messages.problemsTitle
|
|
1081
|
+
),
|
|
1082
|
+
list
|
|
1083
|
+
);
|
|
1084
|
+
element.hidden = true;
|
|
1085
|
+
layer.append(element);
|
|
1086
|
+
pill.setAttribute("aria-haspopup", "dialog");
|
|
1087
|
+
pill.setAttribute("aria-expanded", "false");
|
|
1088
|
+
pill.setAttribute("aria-controls", id);
|
|
1089
|
+
let items = [];
|
|
1090
|
+
let rows = [];
|
|
1091
|
+
let destroyed = false;
|
|
1092
|
+
const isOpen = () => !element.hidden;
|
|
1093
|
+
function keyOf(item) {
|
|
1094
|
+
return `${item.elementId ?? ""}\0${item.severity}\0${item.message}`;
|
|
1095
|
+
}
|
|
1096
|
+
function rowElement(item) {
|
|
1097
|
+
const severity = item.severity === "error" ? "error" : "warning";
|
|
1098
|
+
const parts = [
|
|
1099
|
+
// The shape and colour of the icon carry the severity on screen, its name for screen readers.
|
|
1100
|
+
createElement(
|
|
1101
|
+
ownerDocument,
|
|
1102
|
+
"span",
|
|
1103
|
+
{ class: "cd-shell-problem-icon", role: "img", "aria-label": messages.problemSeverity(severity) },
|
|
1104
|
+
iconElement(severity === "error" ? "circle-x" : "triangle-alert")
|
|
1105
|
+
),
|
|
1106
|
+
createElement(ownerDocument, "span", { class: "cd-shell-problem-message" }, item.message),
|
|
1107
|
+
item.location ? createElement(ownerDocument, "span", { class: "cd-shell-problem-location" }, item.location) : void 0
|
|
1108
|
+
];
|
|
1109
|
+
const attributes = { class: "cd-shell-problem", "data-severity": severity };
|
|
1110
|
+
if (!item.elementId) {
|
|
1111
|
+
const text = createElement(ownerDocument, "div", { ...attributes, tabindex: "-1" }, ...parts);
|
|
1112
|
+
rows.push({ key: keyOf(item), element: text });
|
|
1113
|
+
return text;
|
|
1114
|
+
}
|
|
1115
|
+
const button = createElement(ownerDocument, "button", { type: "button", ...attributes }, ...parts);
|
|
1116
|
+
button.addEventListener("click", () => select(item));
|
|
1117
|
+
rows.push({ key: keyOf(item), element: button });
|
|
1118
|
+
return button;
|
|
1119
|
+
}
|
|
1120
|
+
function focusedRowIndex() {
|
|
1121
|
+
return rows.findIndex((row) => row.element === ownerDocument.activeElement);
|
|
1122
|
+
}
|
|
1123
|
+
function render() {
|
|
1124
|
+
const focusedIndex = focusedRowIndex();
|
|
1125
|
+
const focusedKey = rows[focusedIndex]?.key;
|
|
1126
|
+
rows = [];
|
|
1127
|
+
list.replaceChildren(...items.map((item) => createElement(ownerDocument, "li", {}, rowElement(item))));
|
|
1128
|
+
if (focusedKey === void 0 || !isOpen()) return;
|
|
1129
|
+
const kept = rows.find((row) => row.key === focusedKey) ?? rows[Math.min(focusedIndex, rows.length - 1)];
|
|
1130
|
+
if (kept) focusRowElement(kept.element);
|
|
1131
|
+
else element.focus({ preventScroll: true });
|
|
1132
|
+
}
|
|
1133
|
+
function revealInList(row) {
|
|
1134
|
+
const rowBox = row.getBoundingClientRect();
|
|
1135
|
+
const listBox = list.getBoundingClientRect();
|
|
1136
|
+
if (rowBox.top < listBox.top) list.scrollTop -= listBox.top - rowBox.top;
|
|
1137
|
+
else if (rowBox.bottom > listBox.bottom) list.scrollTop += rowBox.bottom - listBox.bottom;
|
|
1138
|
+
}
|
|
1139
|
+
function focusRowElement(row) {
|
|
1140
|
+
row.focus({ preventScroll: true });
|
|
1141
|
+
revealInList(row);
|
|
1142
|
+
}
|
|
1143
|
+
function focusCanvas() {
|
|
1144
|
+
const canvas = root.querySelector(".cd-shell-canvas svg[tabindex]");
|
|
1145
|
+
canvas?.focus({ preventScroll: true });
|
|
1146
|
+
if ((!canvas || ownerDocument.activeElement !== canvas) && !pill.hidden)
|
|
1147
|
+
pill.focus({ preventScroll: true });
|
|
1148
|
+
}
|
|
1149
|
+
function select(item) {
|
|
1150
|
+
close();
|
|
1151
|
+
focusCanvas();
|
|
1152
|
+
context.onSelectItem?.(item);
|
|
1153
|
+
}
|
|
1154
|
+
function focusRow(index) {
|
|
1155
|
+
if (rows.length === 0) return;
|
|
1156
|
+
const row = rows[(index + rows.length) % rows.length];
|
|
1157
|
+
if (row) focusRowElement(row.element);
|
|
1158
|
+
}
|
|
1159
|
+
const onOutsidePointerDown = (event) => {
|
|
1160
|
+
const target = event.target;
|
|
1161
|
+
if (target && (element.contains(target) || pill.contains(target))) return;
|
|
1162
|
+
close();
|
|
1163
|
+
};
|
|
1164
|
+
const onWindowBlur = () => close();
|
|
1165
|
+
function open() {
|
|
1166
|
+
if (destroyed || isOpen()) return;
|
|
1167
|
+
element.hidden = false;
|
|
1168
|
+
pill.setAttribute("aria-expanded", "true");
|
|
1169
|
+
ownerDocument.addEventListener("pointerdown", onOutsidePointerDown, true);
|
|
1170
|
+
view?.addEventListener("blur", onWindowBlur);
|
|
1171
|
+
list.scrollTop = 0;
|
|
1172
|
+
const first = rows[0];
|
|
1173
|
+
if (first) focusRowElement(first.element);
|
|
1174
|
+
else element.focus({ preventScroll: true });
|
|
1175
|
+
}
|
|
1176
|
+
function close(options = {}) {
|
|
1177
|
+
if (!isOpen()) return;
|
|
1178
|
+
element.hidden = true;
|
|
1179
|
+
pill.setAttribute("aria-expanded", "false");
|
|
1180
|
+
ownerDocument.removeEventListener("pointerdown", onOutsidePointerDown, true);
|
|
1181
|
+
view?.removeEventListener("blur", onWindowBlur);
|
|
1182
|
+
if (options.restoreFocus && !pill.hidden) pill.focus({ preventScroll: true });
|
|
1183
|
+
}
|
|
1184
|
+
function consume(event) {
|
|
1185
|
+
event.preventDefault();
|
|
1186
|
+
event.stopPropagation();
|
|
1187
|
+
}
|
|
1188
|
+
element.addEventListener("keydown", (event) => {
|
|
1189
|
+
if (matchChromeKey(event)) return;
|
|
1190
|
+
const index = focusedRowIndex();
|
|
1191
|
+
switch (event.key) {
|
|
1192
|
+
case "Escape":
|
|
1193
|
+
consume(event);
|
|
1194
|
+
close({ restoreFocus: true });
|
|
1195
|
+
return;
|
|
1196
|
+
case "Tab": {
|
|
1197
|
+
const first = index <= 0;
|
|
1198
|
+
const last = rows.length === 0 || index === rows.length - 1;
|
|
1199
|
+
if (event.shiftKey && first) {
|
|
1200
|
+
consume(event);
|
|
1201
|
+
close({ restoreFocus: true });
|
|
1202
|
+
} else if (!event.shiftKey && last) {
|
|
1203
|
+
event.stopPropagation();
|
|
1204
|
+
close({ restoreFocus: true });
|
|
1205
|
+
}
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
case "ArrowDown":
|
|
1209
|
+
focusRow(index + 1);
|
|
1210
|
+
break;
|
|
1211
|
+
case "ArrowUp":
|
|
1212
|
+
focusRow(index < 0 ? rows.length - 1 : index - 1);
|
|
1213
|
+
break;
|
|
1214
|
+
case "Home":
|
|
1215
|
+
focusRow(0);
|
|
1216
|
+
break;
|
|
1217
|
+
case "End":
|
|
1218
|
+
focusRow(rows.length - 1);
|
|
1219
|
+
break;
|
|
1220
|
+
case "Enter":
|
|
1221
|
+
case " ":
|
|
1222
|
+
case "Delete":
|
|
1223
|
+
case "Backspace":
|
|
1224
|
+
event.stopPropagation();
|
|
1225
|
+
return;
|
|
1226
|
+
default:
|
|
1227
|
+
if (isPrintable(event)) event.stopPropagation();
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
consume(event);
|
|
1231
|
+
});
|
|
1232
|
+
return {
|
|
1233
|
+
element,
|
|
1234
|
+
isOpen,
|
|
1235
|
+
toggle() {
|
|
1236
|
+
if (isOpen()) close({ restoreFocus: true });
|
|
1237
|
+
else open();
|
|
1238
|
+
},
|
|
1239
|
+
open,
|
|
1240
|
+
close,
|
|
1241
|
+
setItems(next) {
|
|
1242
|
+
items = next;
|
|
1243
|
+
if (!destroyed) render();
|
|
1244
|
+
},
|
|
1245
|
+
destroy() {
|
|
1246
|
+
if (destroyed) return;
|
|
1247
|
+
close();
|
|
1248
|
+
destroyed = true;
|
|
1249
|
+
for (const name of PILL_ATTRIBUTES) pill.removeAttribute(name);
|
|
1250
|
+
element.remove();
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/dom/feedback/toasts.ts
|
|
1256
|
+
var TOAST_RESUME_MIN_MS = 1500;
|
|
1257
|
+
var TOAST_LEAVE_FALLBACK_MS = 400;
|
|
1258
|
+
var LEAVE_ANIMATION = "cd-shell-toast-out";
|
|
1259
|
+
var TOAST_ANNOUNCE_DELAY_MS = 100;
|
|
1260
|
+
var INERT_HANDLE = Object.freeze({ dismiss() {
|
|
1261
|
+
} });
|
|
1262
|
+
function createToaster(context) {
|
|
1263
|
+
const { ownerDocument, messages } = context;
|
|
1264
|
+
const web = context.host === "web";
|
|
1265
|
+
const element = createElement(ownerDocument, "div", {
|
|
1266
|
+
class: "cd-shell-toasts",
|
|
1267
|
+
"data-chrome": "toasts",
|
|
1268
|
+
popover: web ? "manual" : void 0
|
|
1269
|
+
});
|
|
1270
|
+
const liveRegion = (politeness) => createElement(ownerDocument, "div", {
|
|
1271
|
+
class: "cd-shell-live-region",
|
|
1272
|
+
"data-live": politeness,
|
|
1273
|
+
role: politeness === "polite" ? "status" : "alert",
|
|
1274
|
+
"aria-live": politeness,
|
|
1275
|
+
"aria-atomic": "true"
|
|
1276
|
+
});
|
|
1277
|
+
const politeRegion = liveRegion("polite");
|
|
1278
|
+
const assertiveRegion = liveRegion("assertive");
|
|
1279
|
+
const announcer = createElement(
|
|
1280
|
+
ownerDocument,
|
|
1281
|
+
"div",
|
|
1282
|
+
{ class: "cd-shell-live" },
|
|
1283
|
+
politeRegion,
|
|
1284
|
+
assertiveRegion
|
|
1285
|
+
);
|
|
1286
|
+
const live = [];
|
|
1287
|
+
const leaveTimers = /* @__PURE__ */ new Set();
|
|
1288
|
+
const announceTimers = /* @__PURE__ */ new Map();
|
|
1289
|
+
let lastId = 0;
|
|
1290
|
+
let destroyed = false;
|
|
1291
|
+
function regionFor(request) {
|
|
1292
|
+
return request.status === "danger" ? assertiveRegion : politeRegion;
|
|
1293
|
+
}
|
|
1294
|
+
function announce(request) {
|
|
1295
|
+
const region = regionFor(request);
|
|
1296
|
+
clearTimeout(announceTimers.get(region));
|
|
1297
|
+
region.textContent = "";
|
|
1298
|
+
announceTimers.set(
|
|
1299
|
+
region,
|
|
1300
|
+
setTimeout(() => {
|
|
1301
|
+
announceTimers.delete(region);
|
|
1302
|
+
if (!destroyed) region.textContent = request.text;
|
|
1303
|
+
}, TOAST_ANNOUNCE_DELAY_MS)
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
function silence(request) {
|
|
1307
|
+
const region = regionFor(request);
|
|
1308
|
+
if (region.textContent === request.text) region.textContent = "";
|
|
1309
|
+
}
|
|
1310
|
+
function changed() {
|
|
1311
|
+
if (!destroyed) context.onChange?.();
|
|
1312
|
+
}
|
|
1313
|
+
function supportsPopover() {
|
|
1314
|
+
return web && typeof element.showPopover === "function" && typeof element.hidePopover === "function";
|
|
1315
|
+
}
|
|
1316
|
+
function popoverOpen() {
|
|
1317
|
+
try {
|
|
1318
|
+
return element.matches(":popover-open");
|
|
1319
|
+
} catch {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
function modalOpen() {
|
|
1324
|
+
try {
|
|
1325
|
+
return ownerDocument.querySelector(":modal") !== null;
|
|
1326
|
+
} catch {
|
|
1327
|
+
return true;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function raise() {
|
|
1331
|
+
if (!supportsPopover() || !element.isConnected) return;
|
|
1332
|
+
if (popoverOpen() && !modalOpen()) return;
|
|
1333
|
+
const focused = ownerDocument.activeElement;
|
|
1334
|
+
const keepFocus = focused && element.contains(focused) ? focused : void 0;
|
|
1335
|
+
try {
|
|
1336
|
+
element.hidePopover();
|
|
1337
|
+
element.showPopover();
|
|
1338
|
+
} catch {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (keepFocus && ownerDocument.activeElement !== keepFocus) keepFocus.focus({ preventScroll: true });
|
|
1342
|
+
}
|
|
1343
|
+
function lowerWhenEmpty() {
|
|
1344
|
+
if (element.childElementCount > 0 || !supportsPopover()) return;
|
|
1345
|
+
try {
|
|
1346
|
+
element.hidePopover();
|
|
1347
|
+
} catch {
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function engaged(toast) {
|
|
1351
|
+
return toast.hovered || toast.focused;
|
|
1352
|
+
}
|
|
1353
|
+
function stopTimer(toast) {
|
|
1354
|
+
clearTimeout(toast.timer);
|
|
1355
|
+
toast.timer = void 0;
|
|
1356
|
+
}
|
|
1357
|
+
function runTimer(toast) {
|
|
1358
|
+
stopTimer(toast);
|
|
1359
|
+
if (toast.remaining === void 0 || engaged(toast)) return;
|
|
1360
|
+
toast.startedAt = Date.now();
|
|
1361
|
+
toast.timer = setTimeout(() => close(toast), toast.remaining);
|
|
1362
|
+
}
|
|
1363
|
+
function pause(toast) {
|
|
1364
|
+
if (toast.timer === void 0) return;
|
|
1365
|
+
stopTimer(toast);
|
|
1366
|
+
if (toast.remaining !== void 0) {
|
|
1367
|
+
toast.remaining = Math.max(0, toast.remaining - (Date.now() - toast.startedAt));
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
function setEngagement(toast, hovered, focused) {
|
|
1371
|
+
if (!live.includes(toast)) return;
|
|
1372
|
+
const wasEngaged = engaged(toast);
|
|
1373
|
+
toast.hovered = hovered;
|
|
1374
|
+
toast.focused = focused;
|
|
1375
|
+
if (engaged(toast) === wasEngaged) return;
|
|
1376
|
+
if (wasEngaged) {
|
|
1377
|
+
if (toast.remaining !== void 0) toast.remaining = Math.max(toast.remaining, TOAST_RESUME_MIN_MS);
|
|
1378
|
+
runTimer(toast);
|
|
1379
|
+
} else {
|
|
1380
|
+
pause(toast);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
function reducedMotion() {
|
|
1384
|
+
if (ownerDocument.documentElement.classList.contains(REDUCED_MOTION_CLASS)) return true;
|
|
1385
|
+
const view = ownerDocument.defaultView;
|
|
1386
|
+
return typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1387
|
+
}
|
|
1388
|
+
function removeElement(toastElement) {
|
|
1389
|
+
toastElement.remove();
|
|
1390
|
+
lowerWhenEmpty();
|
|
1391
|
+
changed();
|
|
1392
|
+
}
|
|
1393
|
+
function leave(toastElement) {
|
|
1394
|
+
toastElement.setAttribute("data-leaving", "");
|
|
1395
|
+
toastElement.inert = true;
|
|
1396
|
+
if (reducedMotion()) {
|
|
1397
|
+
removeElement(toastElement);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
let done = false;
|
|
1401
|
+
const finish = () => {
|
|
1402
|
+
if (done) return;
|
|
1403
|
+
done = true;
|
|
1404
|
+
clearTimeout(fallback);
|
|
1405
|
+
leaveTimers.delete(fallback);
|
|
1406
|
+
removeElement(toastElement);
|
|
1407
|
+
};
|
|
1408
|
+
toastElement.addEventListener("animationend", (event) => {
|
|
1409
|
+
if (event.target === toastElement && event.animationName === LEAVE_ANIMATION) finish();
|
|
1410
|
+
});
|
|
1411
|
+
const fallback = setTimeout(finish, TOAST_LEAVE_FALLBACK_MS);
|
|
1412
|
+
leaveTimers.add(fallback);
|
|
1413
|
+
}
|
|
1414
|
+
function close(toast) {
|
|
1415
|
+
const index = live.indexOf(toast);
|
|
1416
|
+
if (index < 0) return;
|
|
1417
|
+
live.splice(index, 1);
|
|
1418
|
+
stopTimer(toast);
|
|
1419
|
+
silence(toast.request);
|
|
1420
|
+
const heldFocus = toast.element.contains(ownerDocument.activeElement);
|
|
1421
|
+
if (heldFocus) {
|
|
1422
|
+
const next = live[index] ?? live[index - 1];
|
|
1423
|
+
if (next) next.closeButton.focus({ preventScroll: true });
|
|
1424
|
+
else context.focusFallback();
|
|
1425
|
+
}
|
|
1426
|
+
leave(toast.element);
|
|
1427
|
+
}
|
|
1428
|
+
function setAction(toast, action) {
|
|
1429
|
+
const previous = toast.actionButton;
|
|
1430
|
+
const hadFocus = previous !== void 0 && ownerDocument.activeElement === previous;
|
|
1431
|
+
previous?.remove();
|
|
1432
|
+
toast.actionButton = void 0;
|
|
1433
|
+
if (action) {
|
|
1434
|
+
const button = createElement(
|
|
1435
|
+
ownerDocument,
|
|
1436
|
+
"button",
|
|
1437
|
+
{ type: "button", class: "cd-shell-toast-action cd-shell-button", "data-variant": "ghost" },
|
|
1438
|
+
action.label
|
|
1439
|
+
);
|
|
1440
|
+
button.addEventListener("click", () => {
|
|
1441
|
+
close(toast);
|
|
1442
|
+
action.onSelect();
|
|
1443
|
+
});
|
|
1444
|
+
toast.closeButton.before(button);
|
|
1445
|
+
toast.actionButton = button;
|
|
1446
|
+
}
|
|
1447
|
+
if (hadFocus) (toast.actionButton ?? toast.closeButton).focus({ preventScroll: true });
|
|
1448
|
+
}
|
|
1449
|
+
function slotOf(toast) {
|
|
1450
|
+
return { id: toast.id, hasAction: toast.request.action !== void 0, engaged: engaged(toast) };
|
|
1451
|
+
}
|
|
1452
|
+
function render(request) {
|
|
1453
|
+
const id = ++lastId;
|
|
1454
|
+
const textId = `${context.idPrefix}-${id}`;
|
|
1455
|
+
const closeButton = createElement(
|
|
1456
|
+
ownerDocument,
|
|
1457
|
+
"button",
|
|
1458
|
+
{
|
|
1459
|
+
type: "button",
|
|
1460
|
+
class: "cd-shell-toast-close",
|
|
1461
|
+
"aria-label": messages.close,
|
|
1462
|
+
"aria-describedby": textId
|
|
1463
|
+
},
|
|
1464
|
+
iconElement("x")
|
|
1465
|
+
);
|
|
1466
|
+
const toastElement = createElement(
|
|
1467
|
+
ownerDocument,
|
|
1468
|
+
"div",
|
|
1469
|
+
{ class: "cd-shell-toast", "data-status": request.status },
|
|
1470
|
+
createElement(ownerDocument, "span", { class: "cd-shell-toast-dot", "aria-hidden": "true" }),
|
|
1471
|
+
createElement(ownerDocument, "span", { class: "cd-shell-toast-text", id: textId }, request.text),
|
|
1472
|
+
closeButton
|
|
1473
|
+
);
|
|
1474
|
+
const toast = {
|
|
1475
|
+
id,
|
|
1476
|
+
element: toastElement,
|
|
1477
|
+
closeButton,
|
|
1478
|
+
handle: Object.freeze({ dismiss: () => close(toast) }),
|
|
1479
|
+
request,
|
|
1480
|
+
actionButton: void 0,
|
|
1481
|
+
hovered: false,
|
|
1482
|
+
focused: false,
|
|
1483
|
+
remaining: toastLifetime(request),
|
|
1484
|
+
startedAt: Date.now(),
|
|
1485
|
+
timer: void 0
|
|
1486
|
+
};
|
|
1487
|
+
setAction(toast, request.action);
|
|
1488
|
+
closeButton.addEventListener("click", () => close(toast));
|
|
1489
|
+
toastElement.addEventListener("mouseenter", () => setEngagement(toast, true, toast.focused));
|
|
1490
|
+
toastElement.addEventListener("mouseleave", () => setEngagement(toast, false, toast.focused));
|
|
1491
|
+
toastElement.addEventListener("focusin", () => setEngagement(toast, toast.hovered, true));
|
|
1492
|
+
toastElement.addEventListener(
|
|
1493
|
+
"focusout",
|
|
1494
|
+
(event) => setEngagement(toast, toast.hovered, toastElement.contains(event.relatedTarget))
|
|
1495
|
+
);
|
|
1496
|
+
return toast;
|
|
1497
|
+
}
|
|
1498
|
+
function show(request) {
|
|
1499
|
+
if (destroyed) return INERT_HANDLE;
|
|
1500
|
+
const same = live.find(
|
|
1501
|
+
(toast2) => toast2.request.status === request.status && toast2.request.text === request.text
|
|
1502
|
+
);
|
|
1503
|
+
if (same) {
|
|
1504
|
+
same.request = request;
|
|
1505
|
+
setAction(same, request.action);
|
|
1506
|
+
same.remaining = toastLifetime(request);
|
|
1507
|
+
runTimer(same);
|
|
1508
|
+
raise();
|
|
1509
|
+
announce(request);
|
|
1510
|
+
changed();
|
|
1511
|
+
return same.handle;
|
|
1512
|
+
}
|
|
1513
|
+
while (live.length >= MAX_TOASTS) {
|
|
1514
|
+
const evictedId = toastToEvict(live.map(slotOf));
|
|
1515
|
+
const evicted = live.find((toast2) => toast2.id === evictedId) ?? live[0];
|
|
1516
|
+
if (!evicted) break;
|
|
1517
|
+
close(evicted);
|
|
1518
|
+
}
|
|
1519
|
+
const toast = render(request);
|
|
1520
|
+
live.push(toast);
|
|
1521
|
+
element.append(toast.element);
|
|
1522
|
+
raise();
|
|
1523
|
+
runTimer(toast);
|
|
1524
|
+
announce(request);
|
|
1525
|
+
changed();
|
|
1526
|
+
return toast.handle;
|
|
1527
|
+
}
|
|
1528
|
+
return {
|
|
1529
|
+
element,
|
|
1530
|
+
announcer,
|
|
1531
|
+
show,
|
|
1532
|
+
setBottom(bottom) {
|
|
1533
|
+
element.style.bottom = `${bottom}px`;
|
|
1534
|
+
},
|
|
1535
|
+
widestCard() {
|
|
1536
|
+
let widest;
|
|
1537
|
+
for (const card of element.children) {
|
|
1538
|
+
const width = card.getBoundingClientRect().width;
|
|
1539
|
+
if (width > 0) widest = Math.max(widest ?? 0, width);
|
|
1540
|
+
}
|
|
1541
|
+
return widest;
|
|
1542
|
+
},
|
|
1543
|
+
destroy() {
|
|
1544
|
+
if (destroyed) return;
|
|
1545
|
+
destroyed = true;
|
|
1546
|
+
for (const toast of live.splice(0)) stopTimer(toast);
|
|
1547
|
+
for (const timer of leaveTimers) clearTimeout(timer);
|
|
1548
|
+
leaveTimers.clear();
|
|
1549
|
+
for (const timer of announceTimers.values()) clearTimeout(timer);
|
|
1550
|
+
announceTimers.clear();
|
|
1551
|
+
element.remove();
|
|
1552
|
+
announcer.remove();
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/dom/hint.ts
|
|
1558
|
+
function createEmptyHint(ownerDocument, messages, options) {
|
|
1559
|
+
const { onInsertExample } = options;
|
|
1560
|
+
const action = onInsertExample ? createElement(
|
|
1561
|
+
ownerDocument,
|
|
1562
|
+
"button",
|
|
1563
|
+
{ type: "button", class: "cd-shell-hint-action" },
|
|
1564
|
+
messages.insertExample
|
|
1565
|
+
) : void 0;
|
|
1566
|
+
action?.addEventListener("click", () => onInsertExample?.());
|
|
1567
|
+
const element = createElement(
|
|
1568
|
+
ownerDocument,
|
|
1569
|
+
"div",
|
|
1570
|
+
{ class: "cd-shell-hint" },
|
|
1571
|
+
createElement(ownerDocument, "p", { class: "cd-shell-hint-text" }, messages.emptyHint),
|
|
1572
|
+
action
|
|
1573
|
+
);
|
|
1574
|
+
element.hidden = true;
|
|
1575
|
+
return {
|
|
1576
|
+
element,
|
|
1577
|
+
isShown: () => !element.hidden,
|
|
1578
|
+
setShown(shown) {
|
|
1579
|
+
element.hidden = !shown;
|
|
1580
|
+
},
|
|
1581
|
+
place({ top, right, bottom, left }) {
|
|
1582
|
+
element.style.inset = `${top}px ${right}px ${bottom}px ${left}px`;
|
|
1583
|
+
},
|
|
1584
|
+
destroy() {
|
|
1585
|
+
element.remove();
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/dom/inspector.ts
|
|
1591
|
+
function createInspector(ownerDocument, options, onChange) {
|
|
1592
|
+
const element = createElement(ownerDocument, "aside", {
|
|
1593
|
+
class: "cd-shell-inspector",
|
|
1594
|
+
"data-chrome": "inspector",
|
|
1595
|
+
"aria-label": options.label
|
|
1596
|
+
});
|
|
1597
|
+
element.hidden = true;
|
|
1598
|
+
function setOpen(open) {
|
|
1599
|
+
if (open === !element.hidden) return;
|
|
1600
|
+
element.hidden = !open;
|
|
1601
|
+
onChange(open);
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
element,
|
|
1605
|
+
isOpen: () => !element.hidden,
|
|
1606
|
+
open: () => setOpen(true),
|
|
1607
|
+
close: () => setOpen(false),
|
|
1608
|
+
destroy() {
|
|
1609
|
+
element.remove();
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// src/dom/keys.ts
|
|
1615
|
+
function isTextField(target) {
|
|
1616
|
+
if (!target || typeof target.closest !== "function") return false;
|
|
1617
|
+
const element = target;
|
|
1618
|
+
return element.isContentEditable === true || element.closest(TEXT_FIELD_SELECTOR) !== null;
|
|
1619
|
+
}
|
|
1620
|
+
function isShown(element) {
|
|
1621
|
+
if (element.localName === "dialog" && !element.hasAttribute("open")) return false;
|
|
1622
|
+
if (element.closest("[hidden]")) return false;
|
|
1623
|
+
return typeof element.checkVisibility !== "function" || element.checkVisibility({ visibilityProperty: true });
|
|
1624
|
+
}
|
|
1625
|
+
function isModal(element) {
|
|
1626
|
+
if (element.getAttribute("aria-modal") === "true") return true;
|
|
1627
|
+
try {
|
|
1628
|
+
return element.matches(":modal");
|
|
1629
|
+
} catch {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
function openDialogs(scope) {
|
|
1634
|
+
return [...scope.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog')].filter(isShown);
|
|
1635
|
+
}
|
|
1636
|
+
function isModalOpen(scope) {
|
|
1637
|
+
return openDialogs(scope).some(isModal);
|
|
1638
|
+
}
|
|
1639
|
+
function createKeyListener(context) {
|
|
1640
|
+
return (event) => {
|
|
1641
|
+
if (event.key === "Escape") context.hideTooltip();
|
|
1642
|
+
const dialogs = openDialogs(context.dialogScope);
|
|
1643
|
+
const decision = decideKey(event, {
|
|
1644
|
+
host: context.host,
|
|
1645
|
+
inTextField: isTextField(event.target),
|
|
1646
|
+
menuOpen: context.menuOpen(),
|
|
1647
|
+
dialogOpen: dialogs.length > 0,
|
|
1648
|
+
modalOpen: dialogs.some(isModal),
|
|
1649
|
+
defaultPrevented: event.defaultPrevented,
|
|
1650
|
+
startScreen: context.startScreen?.() ?? false
|
|
1651
|
+
});
|
|
1652
|
+
if (decision.preventDefault) event.preventDefault();
|
|
1653
|
+
if (decision.stopPropagation) event.stopPropagation();
|
|
1654
|
+
if (decision.action === "closeMenu") context.closeMenu();
|
|
1655
|
+
else if (decision.action) context.run(decision.action);
|
|
1656
|
+
};
|
|
932
1657
|
}
|
|
933
1658
|
|
|
1659
|
+
// src/dom/ledger.ts
|
|
1660
|
+
var Ledger = class {
|
|
1661
|
+
undo = [];
|
|
1662
|
+
originalAttributes = /* @__PURE__ */ new Map();
|
|
1663
|
+
closed = false;
|
|
1664
|
+
get isClosed() {
|
|
1665
|
+
return this.closed;
|
|
1666
|
+
}
|
|
1667
|
+
/** Runs `task` on `close()`, in reverse order of registration. */
|
|
1668
|
+
defer(task) {
|
|
1669
|
+
this.undo.push(task);
|
|
1670
|
+
}
|
|
1671
|
+
/** Adds `className` unless present; `close()` removes it only if this ledger added it. */
|
|
1672
|
+
addClass(element, className) {
|
|
1673
|
+
if (element.classList.contains(className)) return;
|
|
1674
|
+
const hadClassAttribute = element.hasAttribute("class");
|
|
1675
|
+
element.classList.add(className);
|
|
1676
|
+
this.defer(() => {
|
|
1677
|
+
element.classList.remove(className);
|
|
1678
|
+
if (!hadClassAttribute && element.classList.length === 0) element.removeAttribute("class");
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
/** Sets (string) or removes (`null`) an attribute; `close()` restores the value before the first change. */
|
|
1682
|
+
attribute(element, name, value) {
|
|
1683
|
+
let originals = this.originalAttributes.get(element);
|
|
1684
|
+
if (!originals) {
|
|
1685
|
+
originals = /* @__PURE__ */ new Map();
|
|
1686
|
+
this.originalAttributes.set(element, originals);
|
|
1687
|
+
}
|
|
1688
|
+
if (!originals.has(name)) {
|
|
1689
|
+
originals.set(name, element.getAttribute(name));
|
|
1690
|
+
this.defer(() => {
|
|
1691
|
+
const original = originals.get(name) ?? null;
|
|
1692
|
+
if (original === null) element.removeAttribute(name);
|
|
1693
|
+
else element.setAttribute(name, original);
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
if (value === null) element.removeAttribute(name);
|
|
1697
|
+
else element.setAttribute(name, value);
|
|
1698
|
+
}
|
|
1699
|
+
listen(target, type, listener, options) {
|
|
1700
|
+
const handler = listener;
|
|
1701
|
+
target.addEventListener(type, handler, options);
|
|
1702
|
+
this.defer(() => target.removeEventListener(type, handler, options));
|
|
1703
|
+
}
|
|
1704
|
+
/** Runs every undo task once; later calls are no-ops. */
|
|
1705
|
+
close() {
|
|
1706
|
+
if (this.closed) return;
|
|
1707
|
+
this.closed = true;
|
|
1708
|
+
for (const task of this.undo.reverse()) {
|
|
1709
|
+
try {
|
|
1710
|
+
task();
|
|
1711
|
+
} catch {
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
this.undo.length = 0;
|
|
1715
|
+
this.originalAttributes.clear();
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
|
|
934
1719
|
// src/dom/legend.ts
|
|
935
1720
|
function createLegend(context) {
|
|
936
1721
|
const { ownerDocument, messages, tooltip, titleId, options } = context;
|
|
@@ -990,8 +1775,11 @@ function createLegend(context) {
|
|
|
990
1775
|
}
|
|
991
1776
|
|
|
992
1777
|
// src/dom/menu.ts
|
|
993
|
-
var GROUP_ORDER = ["model", "export", "help"];
|
|
994
|
-
function
|
|
1778
|
+
var GROUP_ORDER = ["file", "model", "export", "appearance", "help"];
|
|
1779
|
+
function isModelerGroup(name) {
|
|
1780
|
+
return name === "model" || name === "export" || name === "help";
|
|
1781
|
+
}
|
|
1782
|
+
function isPrintable2(event) {
|
|
995
1783
|
return event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
996
1784
|
}
|
|
997
1785
|
function createMenu(context) {
|
|
@@ -1020,8 +1808,9 @@ function createMenu(context) {
|
|
|
1020
1808
|
layer.prepend(dropdown);
|
|
1021
1809
|
const isOpen = () => !dropdown.hidden;
|
|
1022
1810
|
function entriesOf(name) {
|
|
1023
|
-
const own = groups[name] ?? [];
|
|
1024
|
-
|
|
1811
|
+
const own = isModelerGroup(name) ? groups[name] ?? [] : [];
|
|
1812
|
+
const added = context.shellEntries(name);
|
|
1813
|
+
return added.length > 0 ? [...own, ...added] : own;
|
|
1025
1814
|
}
|
|
1026
1815
|
function hasEntries() {
|
|
1027
1816
|
return GROUP_ORDER.some((name) => entriesOf(name).length > 0);
|
|
@@ -1075,14 +1864,12 @@ function createMenu(context) {
|
|
|
1075
1864
|
}
|
|
1076
1865
|
};
|
|
1077
1866
|
}
|
|
1078
|
-
const shortcut = entry.
|
|
1867
|
+
const shortcut = entry.shortcut ? createElement(ownerDocument, "span", { class: "cd-shell-menu-shortcut" }, entry.shortcut) : void 0;
|
|
1079
1868
|
const element = itemElement("menuitem", entry.label, { icon: entry.icon, end: shortcut, disabled });
|
|
1080
1869
|
element.dataset.menuId = entry.id;
|
|
1081
1870
|
element.dataset.menuGroup = group;
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
toggleAttribute(element, "aria-keyshortcuts", keyShortcuts || null);
|
|
1085
|
-
}
|
|
1871
|
+
const keyShortcuts = entry.ariaKeyShortcuts ?? (entry.shortcut ? ariaKeyShortcutsFromLabel(entry.shortcut) : void 0);
|
|
1872
|
+
toggleAttribute(element, "aria-keyshortcuts", keyShortcuts || null);
|
|
1086
1873
|
if (entry.kind === "dialog") element.setAttribute("aria-haspopup", "dialog");
|
|
1087
1874
|
if (entry.kind === "action" && entry.danger) element.dataset.danger = "";
|
|
1088
1875
|
return {
|
|
@@ -1267,7 +2054,7 @@ function createMenu(context) {
|
|
|
1267
2054
|
event.stopPropagation();
|
|
1268
2055
|
return;
|
|
1269
2056
|
default:
|
|
1270
|
-
if (!
|
|
2057
|
+
if (!isPrintable2(event)) return;
|
|
1271
2058
|
typeAhead(event.key);
|
|
1272
2059
|
}
|
|
1273
2060
|
consume(event);
|
|
@@ -1297,7 +2084,7 @@ function createMenu(context) {
|
|
|
1297
2084
|
|
|
1298
2085
|
// src/dom/problems.ts
|
|
1299
2086
|
function createProblemsPill(context) {
|
|
1300
|
-
const { ownerDocument, messages, lang, tooltip
|
|
2087
|
+
const { ownerDocument, messages, lang, tooltip } = context;
|
|
1301
2088
|
const icon = createElement(ownerDocument, "span", { class: "cd-shell-problems-icon" });
|
|
1302
2089
|
const text = createElement(ownerDocument, "span", { class: "cd-shell-problems-text" });
|
|
1303
2090
|
const element = createElement(
|
|
@@ -1308,7 +2095,7 @@ function createProblemsPill(context) {
|
|
|
1308
2095
|
text
|
|
1309
2096
|
);
|
|
1310
2097
|
element.hidden = true;
|
|
1311
|
-
element.addEventListener("click", () =>
|
|
2098
|
+
element.addEventListener("click", () => context.onActivate());
|
|
1312
2099
|
const detachTooltip = context.showTooltip ? tooltip.attach(element, { label: messages.showProblems }) : void 0;
|
|
1313
2100
|
let severity;
|
|
1314
2101
|
return {
|
|
@@ -1584,6 +2371,380 @@ function createTooltip({ root, layer, id }) {
|
|
|
1584
2371
|
};
|
|
1585
2372
|
}
|
|
1586
2373
|
|
|
2374
|
+
// src/dom/web/drop-zone.ts
|
|
2375
|
+
var DRAG_EVENTS = ["dragenter", "dragover", "dragleave", "drop"];
|
|
2376
|
+
function carriesFiles(transfer) {
|
|
2377
|
+
if (!transfer) return false;
|
|
2378
|
+
if (Array.from(transfer.types ?? []).includes("Files")) return true;
|
|
2379
|
+
return Array.from(transfer.items ?? []).some((item) => item.kind === "file");
|
|
2380
|
+
}
|
|
2381
|
+
function dragItems(transfer) {
|
|
2382
|
+
const items = Array.from(transfer.items ?? [], (item) => ({ kind: item.kind, type: item.type }));
|
|
2383
|
+
return items.length > 0 ? items : [{ kind: "file", type: "" }];
|
|
2384
|
+
}
|
|
2385
|
+
function acceptList(accept, lang) {
|
|
2386
|
+
const entries = accept.map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
2387
|
+
const extensions = entries.filter((entry) => entry.startsWith("."));
|
|
2388
|
+
const listed = extensions.length > 0 ? extensions : entries;
|
|
2389
|
+
let format;
|
|
2390
|
+
try {
|
|
2391
|
+
format = new Intl.ListFormat(lang, { type: "disjunction" });
|
|
2392
|
+
} catch {
|
|
2393
|
+
format = new Intl.ListFormat("en", { type: "disjunction" });
|
|
2394
|
+
}
|
|
2395
|
+
return format.format(listed);
|
|
2396
|
+
}
|
|
2397
|
+
function createDropZone(context) {
|
|
2398
|
+
const { ownerDocument, layer, messages, lang, options, modalOpen, toast, setState } = context;
|
|
2399
|
+
const chip = createElement(ownerDocument, "span", { class: "cd-shell-drop-zone-chip" });
|
|
2400
|
+
const element = createElement(
|
|
2401
|
+
ownerDocument,
|
|
2402
|
+
"div",
|
|
2403
|
+
{ class: "cd-shell-drop-zone", "data-chrome": "drop-zone", "aria-hidden": "true" },
|
|
2404
|
+
chip
|
|
2405
|
+
);
|
|
2406
|
+
element.hidden = true;
|
|
2407
|
+
layer.append(element);
|
|
2408
|
+
const view = ownerDocument.defaultView;
|
|
2409
|
+
let depth = 0;
|
|
2410
|
+
let current = null;
|
|
2411
|
+
function verdictFor(items) {
|
|
2412
|
+
return dragVerdict(items, {
|
|
2413
|
+
accept: options.accept,
|
|
2414
|
+
multiple: options.multiple ?? false,
|
|
2415
|
+
dialogOpen: modalOpen()
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
function show(verdict) {
|
|
2419
|
+
const next = verdict.state === "none" ? null : verdict.state;
|
|
2420
|
+
if (verdict.state === "over") chip.textContent = messages.dropFile;
|
|
2421
|
+
if (verdict.state === "forbidden") chip.textContent = messages.dropForbidden(verdict.reason);
|
|
2422
|
+
if (next === current) return;
|
|
2423
|
+
current = next;
|
|
2424
|
+
element.hidden = next === null;
|
|
2425
|
+
if (next === null) element.removeAttribute("data-state");
|
|
2426
|
+
else element.setAttribute("data-state", next);
|
|
2427
|
+
setState(next);
|
|
2428
|
+
}
|
|
2429
|
+
function track(transfer) {
|
|
2430
|
+
const verdict = verdictFor(dragItems(transfer));
|
|
2431
|
+
show(verdict);
|
|
2432
|
+
transfer.dropEffect = verdict.state === "forbidden" ? "none" : "copy";
|
|
2433
|
+
}
|
|
2434
|
+
function rejectByDefault(files) {
|
|
2435
|
+
const accepted = acceptList(options.accept, lang);
|
|
2436
|
+
for (const file of files) toast({ status: "danger", text: messages.cannotOpenFile(file.name, accepted) });
|
|
2437
|
+
}
|
|
2438
|
+
function receive(files, verdict) {
|
|
2439
|
+
if (verdict.state === "forbidden" && verdict.reason !== "type") return;
|
|
2440
|
+
const accepted = files.filter((file) => acceptsFile(file, options.accept));
|
|
2441
|
+
const rejected = files.filter((file) => !accepted.includes(file));
|
|
2442
|
+
if (accepted.length > 0) options.onDrop(accepted);
|
|
2443
|
+
if (rejected.length > 0) (options.onReject ?? rejectByDefault)(rejected);
|
|
2444
|
+
}
|
|
2445
|
+
const onDragEvent = (event) => {
|
|
2446
|
+
const transfer = event.dataTransfer;
|
|
2447
|
+
if (!carriesFiles(transfer)) return;
|
|
2448
|
+
event.preventDefault();
|
|
2449
|
+
switch (event.type) {
|
|
2450
|
+
case "dragenter":
|
|
2451
|
+
depth += 1;
|
|
2452
|
+
track(transfer);
|
|
2453
|
+
break;
|
|
2454
|
+
case "dragover":
|
|
2455
|
+
depth = Math.max(depth, 1);
|
|
2456
|
+
track(transfer);
|
|
2457
|
+
break;
|
|
2458
|
+
case "dragleave":
|
|
2459
|
+
depth = Math.max(0, depth - 1);
|
|
2460
|
+
if (depth === 0) show({ state: "none" });
|
|
2461
|
+
break;
|
|
2462
|
+
case "drop": {
|
|
2463
|
+
depth = 0;
|
|
2464
|
+
show({ state: "none" });
|
|
2465
|
+
const files = Array.from(transfer.files ?? []);
|
|
2466
|
+
const items = files.length > 0 ? files.map((file) => ({ kind: "file", type: file.type })) : dragItems(transfer);
|
|
2467
|
+
receive(files, verdictFor(items));
|
|
2468
|
+
break;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
for (const type of DRAG_EVENTS) view?.addEventListener(type, onDragEvent);
|
|
2473
|
+
return {
|
|
2474
|
+
element,
|
|
2475
|
+
state: () => current,
|
|
2476
|
+
destroy() {
|
|
2477
|
+
for (const type of DRAG_EVENTS) view?.removeEventListener(type, onDragEvent);
|
|
2478
|
+
depth = 0;
|
|
2479
|
+
if (current !== null) {
|
|
2480
|
+
current = null;
|
|
2481
|
+
setState(null);
|
|
2482
|
+
}
|
|
2483
|
+
element.remove();
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// src/dom/web/empty-state.ts
|
|
2489
|
+
var KOMET_SIZE = "64";
|
|
2490
|
+
function createEmptyState(context) {
|
|
2491
|
+
const { ownerDocument, messages, options, dropHint, open, titleId, onLeave } = context;
|
|
2492
|
+
const button = (variant, label, run) => {
|
|
2493
|
+
const element2 = createElement(
|
|
2494
|
+
ownerDocument,
|
|
2495
|
+
"button",
|
|
2496
|
+
{ type: "button", class: "cd-shell-button", "data-variant": variant },
|
|
2497
|
+
label
|
|
2498
|
+
);
|
|
2499
|
+
element2.addEventListener("click", () => {
|
|
2500
|
+
onLeave();
|
|
2501
|
+
run();
|
|
2502
|
+
});
|
|
2503
|
+
return element2;
|
|
2504
|
+
};
|
|
2505
|
+
const onShowExample = options.onShowExample;
|
|
2506
|
+
const newDiagram = button("primary", messages.newDiagram, () => options.onNewDiagram());
|
|
2507
|
+
const element = createElement(
|
|
2508
|
+
ownerDocument,
|
|
2509
|
+
"section",
|
|
2510
|
+
{ class: "cd-shell-empty-state", "data-chrome": "empty-state", "aria-labelledby": titleId },
|
|
2511
|
+
// Dragging the mark would start an image drag that some browsers carry as a file.
|
|
2512
|
+
createElement(ownerDocument, "img", {
|
|
2513
|
+
class: "cd-shell-empty-state-mark",
|
|
2514
|
+
src: options.komet,
|
|
2515
|
+
width: KOMET_SIZE,
|
|
2516
|
+
height: KOMET_SIZE,
|
|
2517
|
+
alt: "",
|
|
2518
|
+
draggable: "false"
|
|
2519
|
+
}),
|
|
2520
|
+
createElement(ownerDocument, "h1", { class: "cd-shell-empty-state-title", id: titleId }, options.title),
|
|
2521
|
+
createElement(ownerDocument, "p", { class: "cd-shell-empty-state-text" }, options.text),
|
|
2522
|
+
createElement(
|
|
2523
|
+
ownerDocument,
|
|
2524
|
+
"div",
|
|
2525
|
+
{ class: "cd-shell-empty-state-actions" },
|
|
2526
|
+
newDiagram,
|
|
2527
|
+
onShowExample && button("ghost", messages.showExample, () => onShowExample())
|
|
2528
|
+
),
|
|
2529
|
+
hintParagraph()
|
|
2530
|
+
);
|
|
2531
|
+
function hintParagraph() {
|
|
2532
|
+
const hasDropHint = dropHint !== void 0 && dropHint !== "";
|
|
2533
|
+
if (!hasDropHint && !open) return void 0;
|
|
2534
|
+
const paragraph = createElement(ownerDocument, "p", { class: "cd-shell-empty-state-hint" });
|
|
2535
|
+
if (hasDropHint) paragraph.append(dropHint);
|
|
2536
|
+
if (!open) return paragraph;
|
|
2537
|
+
const shortcut = open.shortcut?.trim();
|
|
2538
|
+
const link = createElement(
|
|
2539
|
+
ownerDocument,
|
|
2540
|
+
"button",
|
|
2541
|
+
{
|
|
2542
|
+
type: "button",
|
|
2543
|
+
class: "cd-shell-empty-state-open",
|
|
2544
|
+
"aria-keyshortcuts": shortcut ? ariaKeyShortcutsFromLabel(shortcut) : void 0,
|
|
2545
|
+
"aria-haspopup": open.dialog ? "dialog" : void 0
|
|
2546
|
+
},
|
|
2547
|
+
messages.openFileLink
|
|
2548
|
+
);
|
|
2549
|
+
link.addEventListener("click", () => open.onSelect());
|
|
2550
|
+
if (hasDropHint) {
|
|
2551
|
+
paragraph.append(" ", createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7"), " ");
|
|
2552
|
+
}
|
|
2553
|
+
const openItem = createElement(ownerDocument, "span", { class: "cd-shell-empty-state-open-item" }, link);
|
|
2554
|
+
if (shortcut) {
|
|
2555
|
+
openItem.append(
|
|
2556
|
+
" ",
|
|
2557
|
+
createElement(
|
|
2558
|
+
ownerDocument,
|
|
2559
|
+
"kbd",
|
|
2560
|
+
{ class: "cd-shell-empty-state-key", "aria-hidden": "true" },
|
|
2561
|
+
shortcut
|
|
2562
|
+
)
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
paragraph.append(openItem);
|
|
2566
|
+
return paragraph;
|
|
2567
|
+
}
|
|
2568
|
+
element.hidden = true;
|
|
2569
|
+
return {
|
|
2570
|
+
element,
|
|
2571
|
+
isShown: () => !element.hidden,
|
|
2572
|
+
setShown(shown) {
|
|
2573
|
+
element.hidden = !shown;
|
|
2574
|
+
},
|
|
2575
|
+
focus() {
|
|
2576
|
+
newDiagram.focus({ preventScroll: true });
|
|
2577
|
+
},
|
|
2578
|
+
destroy() {
|
|
2579
|
+
element.remove();
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/dom/web/legal.ts
|
|
2585
|
+
var MIRAGON_HOME = "https://www.miragon.io/";
|
|
2586
|
+
var MIRAGON_IMPRESSUM = "https://www.miragon.io/impressum/";
|
|
2587
|
+
var MIRAGON_DATENSCHUTZ = "https://www.miragon.io/datenschutz/";
|
|
2588
|
+
function createLegalLine(context) {
|
|
2589
|
+
const { ownerDocument, messages, options } = context;
|
|
2590
|
+
const year = options.year ?? (/* @__PURE__ */ new Date()).getFullYear();
|
|
2591
|
+
const newTabId = `${context.idPrefix}-new-tab`;
|
|
2592
|
+
const link = (label, href, lang) => createElement(
|
|
2593
|
+
ownerDocument,
|
|
2594
|
+
"a",
|
|
2595
|
+
{ href, lang, target: "_blank", rel: "noopener noreferrer", "aria-describedby": newTabId },
|
|
2596
|
+
label
|
|
2597
|
+
);
|
|
2598
|
+
const separator = () => createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7");
|
|
2599
|
+
const item = (anchor) => createElement(ownerDocument, "span", { class: "cd-shell-legal-item" }, separator(), anchor);
|
|
2600
|
+
const links = [
|
|
2601
|
+
link(messages.copyright(year), MIRAGON_HOME),
|
|
2602
|
+
link("Impressum", MIRAGON_IMPRESSUM, "de"),
|
|
2603
|
+
link("Datenschutz", MIRAGON_DATENSCHUTZ, "de"),
|
|
2604
|
+
...(options.links ?? []).map((extra) => link(extra.label, extra.href, extra.lang))
|
|
2605
|
+
];
|
|
2606
|
+
const element = createElement(ownerDocument, "nav", {
|
|
2607
|
+
class: "cd-shell-legal",
|
|
2608
|
+
"data-chrome": "legal",
|
|
2609
|
+
"aria-label": messages.legal
|
|
2610
|
+
});
|
|
2611
|
+
element.append(
|
|
2612
|
+
...links.map((anchor, index) => index === 0 ? anchor : item(anchor)),
|
|
2613
|
+
createElement(ownerDocument, "span", { id: newTabId, hidden: "" }, messages.opensInNewTab)
|
|
2614
|
+
);
|
|
2615
|
+
return {
|
|
2616
|
+
element,
|
|
2617
|
+
destroy() {
|
|
2618
|
+
element.remove();
|
|
2619
|
+
}
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// src/dom/web/menu-groups.ts
|
|
2624
|
+
function fileEntry(spec, action) {
|
|
2625
|
+
const common = {
|
|
2626
|
+
id: spec.id,
|
|
2627
|
+
label: spec.label,
|
|
2628
|
+
...spec.icon ? { icon: spec.icon } : {},
|
|
2629
|
+
...action.disabled ? { disabled: action.disabled } : {},
|
|
2630
|
+
...action.shortcut ? { shortcut: action.shortcut } : {},
|
|
2631
|
+
onSelect: () => action.onSelect()
|
|
2632
|
+
};
|
|
2633
|
+
return action.dialog ? { kind: "dialog", ...common } : { kind: "action", ...common };
|
|
2634
|
+
}
|
|
2635
|
+
function fileMenuEntries(options, messages) {
|
|
2636
|
+
const specs = [
|
|
2637
|
+
{ option: "newDiagram", id: "new-diagram", label: messages.newDiagram, icon: "file-plus" },
|
|
2638
|
+
{ option: "open", id: "open", label: messages.openFile, icon: "folder-open" },
|
|
2639
|
+
// §5.14 gives "Show example" no icon.
|
|
2640
|
+
{ option: "showExample", id: "show-example", label: messages.showExample, icon: void 0 },
|
|
2641
|
+
{ option: "import", id: "import", label: messages.importFile, icon: "import" }
|
|
2642
|
+
];
|
|
2643
|
+
return specs.flatMap((spec) => {
|
|
2644
|
+
const action = options[spec.option];
|
|
2645
|
+
return action ? [fileEntry(spec, action)] : [];
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
function appearanceMenuEntries(appearance, messages) {
|
|
2649
|
+
return [
|
|
2650
|
+
{
|
|
2651
|
+
kind: "radio",
|
|
2652
|
+
id: "appearance",
|
|
2653
|
+
label: messages.appearance,
|
|
2654
|
+
options: [
|
|
2655
|
+
{ value: "system", label: messages.appearanceSystem },
|
|
2656
|
+
{ value: "light", label: messages.appearanceLight },
|
|
2657
|
+
{ value: "dark", label: messages.appearanceDark }
|
|
2658
|
+
],
|
|
2659
|
+
value: () => appearance.choice(),
|
|
2660
|
+
onSelect: (value) => appearance.setChoice(parseAppearanceChoice(value))
|
|
2661
|
+
}
|
|
2662
|
+
];
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// src/dom/web/primary.ts
|
|
2666
|
+
var COLLAPSED_ATTRIBUTE = "data-collapsed";
|
|
2667
|
+
function createPrimaryAction(context) {
|
|
2668
|
+
const { ownerDocument, messages, tooltip, options } = context;
|
|
2669
|
+
const label = options.label ?? messages.share;
|
|
2670
|
+
const labelElement = createElement(ownerDocument, "span", { class: "cd-shell-primary-label" }, label);
|
|
2671
|
+
const element = createElement(
|
|
2672
|
+
ownerDocument,
|
|
2673
|
+
"button",
|
|
2674
|
+
{
|
|
2675
|
+
type: "button",
|
|
2676
|
+
class: "cd-shell-primary cd-shell-button",
|
|
2677
|
+
"data-variant": "primary",
|
|
2678
|
+
"data-chrome": "primary",
|
|
2679
|
+
"aria-label": label
|
|
2680
|
+
},
|
|
2681
|
+
iconElement(options.icon ?? "share-2"),
|
|
2682
|
+
labelElement
|
|
2683
|
+
);
|
|
2684
|
+
let collapsed = false;
|
|
2685
|
+
let disabled = false;
|
|
2686
|
+
let labelledWidth;
|
|
2687
|
+
function readDisabled() {
|
|
2688
|
+
disabled = options.disabled?.() ?? false;
|
|
2689
|
+
toggleAttribute(element, "aria-disabled", disabled ? "true" : null);
|
|
2690
|
+
}
|
|
2691
|
+
function showCollapsed(state) {
|
|
2692
|
+
labelElement.hidden = state;
|
|
2693
|
+
toggleAttribute(element, COLLAPSED_ATTRIBUTE, state ? "" : null);
|
|
2694
|
+
}
|
|
2695
|
+
function measureLabelledWidth() {
|
|
2696
|
+
if (!collapsed) return element.offsetWidth;
|
|
2697
|
+
showCollapsed(false);
|
|
2698
|
+
const width = element.offsetWidth;
|
|
2699
|
+
showCollapsed(true);
|
|
2700
|
+
return width;
|
|
2701
|
+
}
|
|
2702
|
+
function isEngaged() {
|
|
2703
|
+
if (ownerDocument.activeElement === element) return true;
|
|
2704
|
+
try {
|
|
2705
|
+
return element.matches(":hover");
|
|
2706
|
+
} catch {
|
|
2707
|
+
return false;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
const onClick = (event) => {
|
|
2711
|
+
if (disabled) {
|
|
2712
|
+
event.preventDefault();
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
options.onSelect();
|
|
2716
|
+
};
|
|
2717
|
+
element.addEventListener("click", onClick);
|
|
2718
|
+
const detachTooltip = tooltip.attach(element, () => collapsed ? { label } : null);
|
|
2719
|
+
readDisabled();
|
|
2720
|
+
return {
|
|
2721
|
+
element,
|
|
2722
|
+
isCollapsed: () => collapsed,
|
|
2723
|
+
refresh() {
|
|
2724
|
+
readDisabled();
|
|
2725
|
+
labelledWidth = void 0;
|
|
2726
|
+
},
|
|
2727
|
+
widths() {
|
|
2728
|
+
if (!collapsed || labelledWidth === void 0) {
|
|
2729
|
+
const measured = measureLabelledWidth();
|
|
2730
|
+
if (measured > 0) labelledWidth = measured;
|
|
2731
|
+
}
|
|
2732
|
+
return { withLabel: labelledWidth ?? 0, iconOnly: SHELL.row };
|
|
2733
|
+
},
|
|
2734
|
+
setCollapsed(next) {
|
|
2735
|
+
if (next === collapsed) return;
|
|
2736
|
+
collapsed = next;
|
|
2737
|
+
showCollapsed(next);
|
|
2738
|
+
if (!next && isEngaged()) tooltip.hide();
|
|
2739
|
+
},
|
|
2740
|
+
destroy() {
|
|
2741
|
+
detachTooltip();
|
|
2742
|
+
element.removeEventListener("click", onClick);
|
|
2743
|
+
element.remove();
|
|
2744
|
+
}
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
|
|
1587
2748
|
// src/dom/zoom-panel.ts
|
|
1588
2749
|
function createZoomPanel(context) {
|
|
1589
2750
|
const { ownerDocument, messages, lang, platform, tooltip } = context;
|
|
@@ -1701,6 +2862,16 @@ function nextMountIndex() {
|
|
|
1701
2862
|
return index;
|
|
1702
2863
|
}
|
|
1703
2864
|
var DEFAULT_THEMES = ["light"];
|
|
2865
|
+
var LEGAL_HEIGHT_PROPERTY = "--modeler-shell-legal-height";
|
|
2866
|
+
var INERT_TOAST = Object.freeze({ dismiss() {
|
|
2867
|
+
} });
|
|
2868
|
+
function visibleBoxIn(element, root) {
|
|
2869
|
+
if (!element || element.hidden || !element.isConnected) return void 0;
|
|
2870
|
+
if (typeof element.checkVisibility === "function" && !element.checkVisibility({ visibilityProperty: true }))
|
|
2871
|
+
return void 0;
|
|
2872
|
+
const box = boxRelativeTo(element, root);
|
|
2873
|
+
return box.width > 0 && box.height > 0 ? box : void 0;
|
|
2874
|
+
}
|
|
1704
2875
|
function languageOf(root, host, requested) {
|
|
1705
2876
|
if (requested) return requested;
|
|
1706
2877
|
const declared = host === "embedded" ? root.getAttribute("lang") : root.closest("[lang]")?.getAttribute("lang");
|
|
@@ -1729,6 +2900,7 @@ function renderShell(root, options, themes, ledger) {
|
|
|
1729
2900
|
const platform = options.platform ?? detectPlatform(globalThis.navigator);
|
|
1730
2901
|
const lang = languageOf(root, host, options.lang);
|
|
1731
2902
|
const keyScope = options.keyScope ?? (host === "embedded" ? "root" : "document");
|
|
2903
|
+
const web = host === "web";
|
|
1732
2904
|
ledger.addClass(root, "cd-shell");
|
|
1733
2905
|
ledger.attribute(root, "data-cd-shell-host", host);
|
|
1734
2906
|
ledger.attribute(root, "data-palette", "top");
|
|
@@ -1741,6 +2913,9 @@ function renderShell(root, options, themes, ledger) {
|
|
|
1741
2913
|
const setState = (name, value) => {
|
|
1742
2914
|
if (!ledger.isClosed) ledger.attribute(root, name, value);
|
|
1743
2915
|
};
|
|
2916
|
+
const appearance = web ? connectWebAppearance({ ownerDocument, themes, options: options.appearance }) : void 0;
|
|
2917
|
+
if (appearance) ledger.defer(() => appearance.destroy());
|
|
2918
|
+
const darkCapable = themes.includes("dark");
|
|
1744
2919
|
const layer = createElement(ownerDocument, "div", { class: "cd-shell-layer" });
|
|
1745
2920
|
ledger.defer(() => layer.remove());
|
|
1746
2921
|
const tooltip = createTooltip({ root, layer, id: `cd-shell-tooltip-${index}` });
|
|
@@ -1772,6 +2947,14 @@ function renderShell(root, options, themes, ledger) {
|
|
|
1772
2947
|
keepEnabledWhenReadOnly: true
|
|
1773
2948
|
} : void 0;
|
|
1774
2949
|
const shellModelEntries = legendEntry ? [legendEntry] : [];
|
|
2950
|
+
const fileEntries = web && options.file ? fileMenuEntries(options.file, messages) : [];
|
|
2951
|
+
const appearanceEntries = appearance && darkCapable ? appearanceMenuEntries(appearance, messages) : [];
|
|
2952
|
+
const shellEntries = (group) => {
|
|
2953
|
+
if (group === "file") return fileEntries;
|
|
2954
|
+
if (group === "model") return shellModelEntries;
|
|
2955
|
+
if (group === "appearance") return appearanceEntries;
|
|
2956
|
+
return [];
|
|
2957
|
+
};
|
|
1775
2958
|
const menu = createMenu({
|
|
1776
2959
|
root,
|
|
1777
2960
|
layer,
|
|
@@ -1779,55 +2962,133 @@ function renderShell(root, options, themes, ledger) {
|
|
|
1779
2962
|
messages,
|
|
1780
2963
|
tooltip,
|
|
1781
2964
|
groups: options.menu ?? {},
|
|
1782
|
-
|
|
2965
|
+
shellEntries,
|
|
1783
2966
|
isReadOnly: () => readOnly
|
|
1784
2967
|
});
|
|
1785
2968
|
ledger.defer(() => menu.destroy());
|
|
1786
2969
|
ledger.defer(tooltip.attach(menu.trigger, () => menu.isOpen() ? null : { label: messages.menu }));
|
|
2970
|
+
const primary = web && options.primary ? createPrimaryAction({ ownerDocument, messages, tooltip, options: options.primary }) : void 0;
|
|
2971
|
+
if (primary) ledger.defer(() => primary.destroy());
|
|
1787
2972
|
const canvas = options.canvas ?? createElement(ownerDocument, "div");
|
|
1788
2973
|
ledger.addClass(canvas, "cd-shell-canvas");
|
|
2974
|
+
if (web && !canvas.hasAttribute("role")) ledger.attribute(canvas, "role", "main");
|
|
1789
2975
|
if (!options.canvas) ledger.defer(() => canvas.remove());
|
|
2976
|
+
function focusCanvasOrTrigger() {
|
|
2977
|
+
const took = (target) => {
|
|
2978
|
+
target?.focus({ preventScroll: true });
|
|
2979
|
+
return target !== null && target !== void 0 && ownerDocument.activeElement === target;
|
|
2980
|
+
};
|
|
2981
|
+
if (emptyState?.isShown()) {
|
|
2982
|
+
emptyState.focus();
|
|
2983
|
+
if (emptyState.element.contains(ownerDocument.activeElement)) return;
|
|
2984
|
+
}
|
|
2985
|
+
if (took(canvas.querySelector("svg[tabindex]"))) return;
|
|
2986
|
+
if (!menu.trigger.hidden) took(menu.trigger);
|
|
2987
|
+
}
|
|
1790
2988
|
function moveFocusOutOf(island) {
|
|
1791
|
-
if (
|
|
1792
|
-
const canvasTarget = canvas.querySelector("svg[tabindex]");
|
|
1793
|
-
canvasTarget?.focus({ preventScroll: true });
|
|
1794
|
-
if (canvasTarget && ownerDocument.activeElement === canvasTarget) return;
|
|
1795
|
-
if (!menu.trigger.hidden) menu.trigger.focus({ preventScroll: true });
|
|
2989
|
+
if (island.contains(ownerDocument.activeElement)) focusCanvasOrTrigger();
|
|
1796
2990
|
}
|
|
1797
2991
|
const status = createStatusBox(ownerDocument);
|
|
1798
2992
|
ledger.defer(() => status.destroy());
|
|
1799
2993
|
const hint = options.emptyHint ? createEmptyHint(ownerDocument, messages, options.emptyHint) : void 0;
|
|
1800
2994
|
if (hint) ledger.defer(() => hint.destroy());
|
|
2995
|
+
const emptyState = web && options.emptyState ? createEmptyState({
|
|
2996
|
+
ownerDocument,
|
|
2997
|
+
messages,
|
|
2998
|
+
options: options.emptyState,
|
|
2999
|
+
dropHint: options.dropZone ? options.emptyState.dropHint ?? messages.dropHint : void 0,
|
|
3000
|
+
open: options.file?.open,
|
|
3001
|
+
titleId: `cd-shell-empty-state-title-${index}`,
|
|
3002
|
+
onLeave: () => setEmptyStateShown(false)
|
|
3003
|
+
}) : void 0;
|
|
3004
|
+
if (emptyState) ledger.defer(() => emptyState.destroy());
|
|
1801
3005
|
const inspector = options.inspector ? createInspector(ownerDocument, options.inspector, (open) => {
|
|
1802
3006
|
setState("data-inspector", open ? "open" : null);
|
|
3007
|
+
updateBottomRow();
|
|
1803
3008
|
placeHint();
|
|
1804
3009
|
}) : void 0;
|
|
1805
3010
|
if (inspector) ledger.defer(() => inspector.destroy());
|
|
1806
|
-
const
|
|
3011
|
+
const problemsOptions = options.problems;
|
|
3012
|
+
let popover;
|
|
3013
|
+
const pill = problemsOptions ? createProblemsPill({
|
|
1807
3014
|
ownerDocument,
|
|
1808
3015
|
messages,
|
|
1809
3016
|
lang,
|
|
1810
3017
|
tooltip,
|
|
1811
3018
|
showTooltip: host === "ide",
|
|
1812
|
-
|
|
3019
|
+
onActivate() {
|
|
3020
|
+
if (popover) popover.toggle();
|
|
3021
|
+
else problemsOptions.onActivate?.();
|
|
3022
|
+
}
|
|
1813
3023
|
}) : void 0;
|
|
1814
3024
|
if (pill) ledger.defer(() => pill.destroy());
|
|
3025
|
+
if (pill && (web || host === "embedded" && !problemsOptions?.onActivate)) {
|
|
3026
|
+
const created = createProblemsPopover({
|
|
3027
|
+
ownerDocument,
|
|
3028
|
+
root,
|
|
3029
|
+
layer,
|
|
3030
|
+
pill: pill.element,
|
|
3031
|
+
id: `cd-shell-problems-${index}`,
|
|
3032
|
+
titleId: `cd-shell-problems-title-${index}`,
|
|
3033
|
+
messages,
|
|
3034
|
+
onSelectItem: problemsOptions?.onSelectItem
|
|
3035
|
+
});
|
|
3036
|
+
popover = created;
|
|
3037
|
+
ledger.defer(() => created.destroy());
|
|
3038
|
+
}
|
|
3039
|
+
const legal = web ? createLegalLine({
|
|
3040
|
+
ownerDocument,
|
|
3041
|
+
messages,
|
|
3042
|
+
options: options.legal ?? {},
|
|
3043
|
+
idPrefix: `cd-shell-legal-${index}`
|
|
3044
|
+
}) : void 0;
|
|
3045
|
+
if (legal) ledger.defer(() => legal.destroy());
|
|
1815
3046
|
const zoomPanel = createZoomPanel({ ownerDocument, messages, lang, platform, tooltip });
|
|
1816
3047
|
ledger.defer(() => zoomPanel.destroy());
|
|
1817
|
-
|
|
3048
|
+
const toaster = host === "ide" ? void 0 : createToaster({
|
|
3049
|
+
ownerDocument,
|
|
3050
|
+
host,
|
|
3051
|
+
messages,
|
|
3052
|
+
idPrefix: `cd-shell-toast-${index}`,
|
|
3053
|
+
focusFallback: focusCanvasOrTrigger,
|
|
3054
|
+
onChange: () => updateBottomRow()
|
|
3055
|
+
});
|
|
3056
|
+
if (toaster) ledger.defer(() => toaster.destroy());
|
|
3057
|
+
const defined = (element) => element !== void 0;
|
|
3058
|
+
root.prepend(...[menu.trigger, primary?.element, canvas].filter(defined));
|
|
1818
3059
|
root.append(
|
|
1819
3060
|
...[
|
|
1820
3061
|
status.element,
|
|
1821
3062
|
hint?.element,
|
|
3063
|
+
emptyState?.element,
|
|
1822
3064
|
inspector?.element,
|
|
1823
3065
|
pill?.element,
|
|
1824
3066
|
legend?.element,
|
|
3067
|
+
legal?.element,
|
|
1825
3068
|
zoomPanel.element
|
|
1826
|
-
].filter(
|
|
1827
|
-
layer
|
|
3069
|
+
].filter(defined),
|
|
3070
|
+
layer,
|
|
3071
|
+
...[toaster?.announcer, toaster?.element].filter(defined)
|
|
1828
3072
|
);
|
|
3073
|
+
function showToast(request) {
|
|
3074
|
+
return ledger.isClosed || !toaster ? INERT_TOAST : toaster.show(request);
|
|
3075
|
+
}
|
|
3076
|
+
const dropZone = web && options.dropZone ? createDropZone({
|
|
3077
|
+
ownerDocument,
|
|
3078
|
+
layer,
|
|
3079
|
+
messages,
|
|
3080
|
+
lang,
|
|
3081
|
+
options: options.dropZone,
|
|
3082
|
+
modalOpen: () => isModalOpen(ownerDocument),
|
|
3083
|
+
toast: showToast,
|
|
3084
|
+
setState: (state) => setState("data-drop", state)
|
|
3085
|
+
}) : void 0;
|
|
3086
|
+
if (dropZone) ledger.defer(() => dropZone.destroy());
|
|
1829
3087
|
let palette;
|
|
1830
3088
|
let paletteRow = "top";
|
|
3089
|
+
let zoomRaised = false;
|
|
3090
|
+
let legalHeight = LEGAL_LINE_HEIGHT;
|
|
3091
|
+
if (legal) ledger.defer(() => root.style.removeProperty(LEGAL_HEIGHT_PROPERTY));
|
|
1831
3092
|
function containerSize() {
|
|
1832
3093
|
return { width: root.clientWidth, height: root.clientHeight };
|
|
1833
3094
|
}
|
|
@@ -1847,6 +3108,10 @@ function renderShell(root, options, themes, ledger) {
|
|
|
1847
3108
|
paletteRow,
|
|
1848
3109
|
problemsVisible: pill?.isShown() ?? false
|
|
1849
3110
|
};
|
|
3111
|
+
if (web) {
|
|
3112
|
+
layout.zoomRaised = zoomRaised;
|
|
3113
|
+
layout.legalHeight = legalHeight;
|
|
3114
|
+
}
|
|
1850
3115
|
if (legend?.isOpen()) layout.legendWidth = legend.element.getBoundingClientRect().width;
|
|
1851
3116
|
if (inspector?.isOpen()) {
|
|
1852
3117
|
layout.inspector = container.width > SHELL.narrow ? {} : { sheetTop: boxRelativeTo(inspector.element, root).y };
|
|
@@ -1860,18 +3125,76 @@ function renderShell(root, options, themes, ledger) {
|
|
|
1860
3125
|
if (ledger.isClosed) return;
|
|
1861
3126
|
const size = containerSize();
|
|
1862
3127
|
if (size.width === 0 || size.height === 0) return;
|
|
1863
|
-
|
|
3128
|
+
const topRow = decideTopRow({
|
|
1864
3129
|
containerWidth: size.width,
|
|
1865
3130
|
paletteWidth: paletteWidth(),
|
|
1866
|
-
menuRight: menuRight()
|
|
3131
|
+
menuRight: menuRight(),
|
|
3132
|
+
primary: primary?.widths()
|
|
1867
3133
|
});
|
|
3134
|
+
paletteRow = topRow.paletteRow;
|
|
1868
3135
|
setState("data-palette", paletteRow);
|
|
3136
|
+
if (primary) {
|
|
3137
|
+
const collapsed = topRow.primaryLabel === "collapsed";
|
|
3138
|
+
primary.setCollapsed(collapsed);
|
|
3139
|
+
setState("data-primary", collapsed ? "collapsed" : null);
|
|
3140
|
+
}
|
|
1869
3141
|
if (legend && !legend.isStarted()) legend.start(options.legend?.open ?? legendStartsOpen(size.height));
|
|
3142
|
+
updateBottomRow();
|
|
1870
3143
|
placeHint();
|
|
1871
3144
|
}
|
|
3145
|
+
function updateBottomRow() {
|
|
3146
|
+
if (ledger.isClosed) return;
|
|
3147
|
+
const size = containerSize();
|
|
3148
|
+
if (size.width === 0 || size.height === 0) return;
|
|
3149
|
+
if (legal) {
|
|
3150
|
+
const legalBox = visibleBoxIn(legal.element, root);
|
|
3151
|
+
const height = legalLineHeight(legalBox?.height);
|
|
3152
|
+
if (height !== legalHeight || !root.style.getPropertyValue(LEGAL_HEIGHT_PROPERTY)) {
|
|
3153
|
+
legalHeight = height;
|
|
3154
|
+
root.style.setProperty(LEGAL_HEIGHT_PROPERTY, `${height}px`);
|
|
3155
|
+
}
|
|
3156
|
+
zoomRaised = zoomRowRaised({
|
|
3157
|
+
containerWidth: size.width,
|
|
3158
|
+
legalRight: legalBox && legalBox.x + legalBox.width,
|
|
3159
|
+
zoomWidth: visibleBoxIn(zoomPanel.element, root)?.width
|
|
3160
|
+
});
|
|
3161
|
+
setState("data-zoom-row", zoomRaised ? "raised" : null);
|
|
3162
|
+
}
|
|
3163
|
+
if (toaster) {
|
|
3164
|
+
const obstacles = [zoomPanel.element, legal?.element, pill?.element].flatMap(
|
|
3165
|
+
(element) => {
|
|
3166
|
+
const box = visibleBoxIn(element, root);
|
|
3167
|
+
return box ? [{ left: box.x, right: box.x + box.width, top: size.height - box.y }] : [];
|
|
3168
|
+
}
|
|
3169
|
+
);
|
|
3170
|
+
toaster.setBottom(
|
|
3171
|
+
toastStackBottom({ containerWidth: size.width, obstacles, cardWidth: toaster.widestCard() })
|
|
3172
|
+
);
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
function setEmptyStateShown(shown) {
|
|
3176
|
+
if (ledger.isClosed || !emptyState || emptyState.isShown() === shown) return;
|
|
3177
|
+
const active = ownerDocument.activeElement;
|
|
3178
|
+
const hadFocus = emptyState.element.contains(active);
|
|
3179
|
+
const workHadFocus = shown && active !== null && root.contains(active) && ![emptyState.element, legal?.element, toaster?.element].some((kept) => kept?.contains(active));
|
|
3180
|
+
emptyState.setShown(shown);
|
|
3181
|
+
setState("data-empty-state", shown ? "shown" : null);
|
|
3182
|
+
ledger.attribute(canvas, "inert", shown ? "" : null);
|
|
3183
|
+
if (shown) {
|
|
3184
|
+
menu.close();
|
|
3185
|
+
popover?.close();
|
|
3186
|
+
tooltip.hide();
|
|
3187
|
+
if (workHadFocus) emptyState.focus();
|
|
3188
|
+
} else if (hadFocus) {
|
|
3189
|
+
focusCanvasOrTrigger();
|
|
3190
|
+
}
|
|
3191
|
+
updateLayout();
|
|
3192
|
+
}
|
|
1872
3193
|
const ResizeObserverClass = globalThis.ResizeObserver;
|
|
1873
3194
|
const resizeObserver = ResizeObserverClass ? new ResizeObserverClass(() => updateLayout()) : void 0;
|
|
1874
3195
|
resizeObserver?.observe(root);
|
|
3196
|
+
if (primary) resizeObserver?.observe(primary.element);
|
|
3197
|
+
if (legal) resizeObserver?.observe(legal.element);
|
|
1875
3198
|
ledger.defer(() => resizeObserver?.disconnect());
|
|
1876
3199
|
if (legend && options.legend?.open !== void 0) legend.start(options.legend.open);
|
|
1877
3200
|
updateLayout();
|
|
@@ -1880,12 +3203,16 @@ function renderShell(root, options, themes, ledger) {
|
|
|
1880
3203
|
host,
|
|
1881
3204
|
dialogScope: keyTarget,
|
|
1882
3205
|
menuOpen: () => menu.isOpen(),
|
|
3206
|
+
startScreen: () => emptyState?.isShown() ?? false,
|
|
1883
3207
|
closeMenu() {
|
|
1884
3208
|
const active = ownerDocument.activeElement;
|
|
1885
3209
|
menu.close({ restoreFocus: active === menu.trigger || menu.dropdown.contains(active) });
|
|
1886
3210
|
},
|
|
1887
3211
|
hideTooltip: () => tooltip.hide(),
|
|
1888
|
-
|
|
3212
|
+
// Behind the start screen the keys reach the browser instead: there is no diagram to zoom.
|
|
3213
|
+
run: (key) => {
|
|
3214
|
+
if (!emptyState?.isShown()) zoomPanel.run(key);
|
|
3215
|
+
}
|
|
1889
3216
|
});
|
|
1890
3217
|
keyTarget.addEventListener("keydown", onKeydown);
|
|
1891
3218
|
ledger.defer(() => keyTarget.removeEventListener("keydown", onKeydown));
|
|
@@ -1937,6 +3264,21 @@ function renderShell(root, options, themes, ledger) {
|
|
|
1937
3264
|
if (!ledger.isClosed) legend.toggle();
|
|
1938
3265
|
}
|
|
1939
3266
|
};
|
|
3267
|
+
const primaryHandle = primary && {
|
|
3268
|
+
element: primary.element,
|
|
3269
|
+
isCollapsed: () => primary.isCollapsed(),
|
|
3270
|
+
refresh: () => {
|
|
3271
|
+
if (!ledger.isClosed) primary.refresh();
|
|
3272
|
+
}
|
|
3273
|
+
};
|
|
3274
|
+
const appearanceHandle = appearance && darkCapable ? {
|
|
3275
|
+
choice: () => appearance.choice(),
|
|
3276
|
+
setChoice: (choice) => {
|
|
3277
|
+
if (ledger.isClosed) return;
|
|
3278
|
+
appearance.setChoice(choice);
|
|
3279
|
+
menu.refresh();
|
|
3280
|
+
}
|
|
3281
|
+
} : void 0;
|
|
1940
3282
|
const inspectorHandle = inspector && {
|
|
1941
3283
|
element: inspector.element,
|
|
1942
3284
|
isOpen: () => inspector.isOpen(),
|
|
@@ -1960,6 +3302,8 @@ function renderShell(root, options, themes, ledger) {
|
|
|
1960
3302
|
tooltip: tooltipHandle,
|
|
1961
3303
|
legend: legendHandle,
|
|
1962
3304
|
inspector: inspectorHandle,
|
|
3305
|
+
primary: primaryHandle,
|
|
3306
|
+
appearance: appearanceHandle,
|
|
1963
3307
|
setZoomTarget(target) {
|
|
1964
3308
|
if (!ledger.isClosed) zoomPanel.setTarget(target);
|
|
1965
3309
|
},
|
|
@@ -1988,8 +3332,15 @@ function renderShell(root, options, themes, ledger) {
|
|
|
1988
3332
|
},
|
|
1989
3333
|
setProblems(summary) {
|
|
1990
3334
|
if (ledger.isClosed || !pill) return;
|
|
3335
|
+
const listHadFocus = popover?.element.contains(ownerDocument.activeElement) ?? false;
|
|
1991
3336
|
pill.set(summary);
|
|
1992
3337
|
setState("data-problems", pill.isShown() ? "shown" : null);
|
|
3338
|
+
popover?.setItems(summary.items ?? []);
|
|
3339
|
+
if (!pill.isShown() && popover?.isOpen()) {
|
|
3340
|
+
popover.close();
|
|
3341
|
+
if (listHadFocus) focusCanvasOrTrigger();
|
|
3342
|
+
}
|
|
3343
|
+
updateBottomRow();
|
|
1993
3344
|
placeHint();
|
|
1994
3345
|
},
|
|
1995
3346
|
setStatus(next, statusOptions) {
|
|
@@ -2019,6 +3370,9 @@ function renderShell(root, options, themes, ledger) {
|
|
|
2019
3370
|
applyReadOnly();
|
|
2020
3371
|
},
|
|
2021
3372
|
isReadOnly: () => readOnly,
|
|
3373
|
+
setEmptyState: setEmptyStateShown,
|
|
3374
|
+
isEmptyStateShown: () => emptyState?.isShown() ?? false,
|
|
3375
|
+
toast: showToast,
|
|
2022
3376
|
destroy() {
|
|
2023
3377
|
ledger.close();
|
|
2024
3378
|
}
|
|
@@ -2077,6 +3431,12 @@ function withDestroyHook(shell, beforeDestroy) {
|
|
|
2077
3431
|
get inspector() {
|
|
2078
3432
|
return shell.inspector;
|
|
2079
3433
|
},
|
|
3434
|
+
get primary() {
|
|
3435
|
+
return shell.primary;
|
|
3436
|
+
},
|
|
3437
|
+
get appearance() {
|
|
3438
|
+
return shell.appearance;
|
|
3439
|
+
},
|
|
2080
3440
|
setZoomTarget: (target) => shell.setZoomTarget(target),
|
|
2081
3441
|
fitInsets: () => shell.fitInsets(),
|
|
2082
3442
|
registerPalette: (palette) => shell.registerPalette(palette),
|
|
@@ -2085,6 +3445,9 @@ function withDestroyHook(shell, beforeDestroy) {
|
|
|
2085
3445
|
setEmptyHint: (visible) => shell.setEmptyHint(visible),
|
|
2086
3446
|
setReadOnly: (readOnly, options) => shell.setReadOnly(readOnly, options),
|
|
2087
3447
|
isReadOnly: () => shell.isReadOnly(),
|
|
3448
|
+
setEmptyState: (shown) => shell.setEmptyState(shown),
|
|
3449
|
+
isEmptyStateShown: () => shell.isEmptyStateShown(),
|
|
3450
|
+
toast: (request) => shell.toast(request),
|
|
2088
3451
|
destroy() {
|
|
2089
3452
|
beforeDestroy();
|
|
2090
3453
|
shell.destroy();
|