@miragon/modeler-shell 0.0.0-stage → 0.1.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 +1902 -0
- package/LICENSE +21 -0
- package/README.md +435 -2
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/appearance-D3FUcbu6.d.cts +53 -0
- package/dist/appearance-D3FUcbu6.d.ts +53 -0
- package/dist/chunk-5JSZ4VGA.js +551 -0
- package/dist/chunk-5JSZ4VGA.js.map +1 -0
- package/dist/chunk-6LQOFACK.js +1576 -0
- package/dist/chunk-6LQOFACK.js.map +1 -0
- package/dist/chunk-N4QXFF3A.js +23 -0
- package/dist/chunk-N4QXFF3A.js.map +1 -0
- package/dist/chunk-RJRFR6N3.js +136 -0
- package/dist/chunk-RJRFR6N3.js.map +1 -0
- package/dist/core.cjs +619 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +171 -0
- package/dist/core.d.ts +171 -0
- package/dist/core.js +91 -0
- package/dist/core.js.map +1 -0
- package/dist/diagram-js.cjs +1115 -0
- package/dist/diagram-js.cjs.map +1 -0
- package/dist/diagram-js.d.cts +163 -0
- package/dist/diagram-js.d.ts +163 -0
- package/dist/diagram-js.js +867 -0
- package/dist/diagram-js.js.map +1 -0
- package/dist/index.cjs +2175 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/dist/keymap-CObQE6q6.d.cts +151 -0
- package/dist/keymap-DORzHnK-.d.ts +151 -0
- package/dist/protocol-DhYBakWL.d.cts +15 -0
- package/dist/protocol-IeW8bf4f.d.ts +15 -0
- package/dist/shell.css +914 -0
- package/dist/testing-vscode-stub.cjs +332 -0
- package/dist/testing-vscode-stub.cjs.map +1 -0
- package/dist/testing-vscode-stub.d.cts +65 -0
- package/dist/testing-vscode-stub.d.ts +65 -0
- package/dist/testing-vscode-stub.js +154 -0
- package/dist/testing-vscode-stub.js.map +1 -0
- package/dist/testing.cjs +2364 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +165 -0
- package/dist/testing.d.ts +165 -0
- package/dist/testing.js +2297 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-B6NfkiUG.d.cts +66 -0
- package/dist/types-B6NfkiUG.d.ts +66 -0
- package/dist/types-DnZn9CPw.d.ts +284 -0
- package/dist/types-OuFlqZp0.d.cts +284 -0
- package/dist/vscode-extension.cjs +215 -0
- package/dist/vscode-extension.cjs.map +1 -0
- package/dist/vscode-extension.d.cts +103 -0
- package/dist/vscode-extension.d.ts +103 -0
- package/dist/vscode-extension.js +24 -0
- package/dist/vscode-extension.js.map +1 -0
- package/dist/vscode-webview.cjs +2198 -0
- package/dist/vscode-webview.cjs.map +1 -0
- package/dist/vscode-webview.d.cts +82 -0
- package/dist/vscode-webview.d.ts +82 -0
- package/dist/vscode-webview.js +250 -0
- package/dist/vscode-webview.js.map +1 -0
- package/package.json +145 -4
- package/src/core/appearance.ts +104 -0
- package/src/core/fit.ts +205 -0
- package/src/core/index.ts +84 -0
- package/src/core/keymap.ts +275 -0
- package/src/core/layout.ts +69 -0
- package/src/core/messages.ts +88 -0
- package/src/core/sizes.ts +85 -0
- package/src/core/types.ts +84 -0
- package/src/core/zoom.ts +74 -0
- package/src/core.ts +2 -0
- package/src/diagram-js/connect.ts +75 -0
- package/src/diagram-js/index.ts +16 -0
- package/src/diagram-js/module.ts +20 -0
- package/src/diagram-js/palette-skin.ts +267 -0
- package/src/diagram-js/palette.css +269 -0
- package/src/diagram-js/service.ts +240 -0
- package/src/diagram-js/types.ts +67 -0
- package/src/diagram-js/viewport.ts +564 -0
- package/src/diagram-js.ts +2 -0
- package/src/dom/element.ts +51 -0
- package/src/dom/hint.ts +51 -0
- package/src/dom/icons.ts +309 -0
- package/src/dom/index.ts +29 -0
- package/src/dom/inspector.ts +35 -0
- package/src/dom/keys.ts +59 -0
- package/src/dom/ledger.ts +87 -0
- package/src/dom/legend.ts +85 -0
- package/src/dom/menu.ts +393 -0
- package/src/dom/mount.ts +461 -0
- package/src/dom/problems.ts +72 -0
- package/src/dom/shell-dom.css +966 -0
- package/src/dom/status.ts +72 -0
- package/src/dom/tooltip.ts +237 -0
- package/src/dom/types.ts +277 -0
- package/src/dom/zoom-panel.ts +154 -0
- package/src/index.ts +3 -0
- package/src/shell.css +6 -0
- package/src/testing/cases.ts +1012 -0
- package/src/testing/checks.ts +832 -0
- package/src/testing/facts.ts +587 -0
- package/src/testing/geometry.ts +472 -0
- package/src/testing/harness.ts +138 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/page-tools.ts +434 -0
- package/src/testing/page.ts +17 -0
- package/src/testing/vscode-stub.ts +237 -0
- package/src/testing-vscode-stub.ts +2 -0
- package/src/testing.ts +2 -0
- package/src/vscode/extension/html.ts +107 -0
- package/src/vscode/extension/index.ts +8 -0
- package/src/vscode/extension/reveal.ts +9 -0
- package/src/vscode/extension/theme-bridge.ts +85 -0
- package/src/vscode/extension/types.ts +36 -0
- package/src/vscode/protocol.ts +31 -0
- package/src/vscode/webview/appearance.ts +141 -0
- package/src/vscode/webview/index.ts +8 -0
- package/src/vscode/webview/mount.ts +76 -0
- package/src/vscode/webview/state.ts +116 -0
- package/src/vscode-extension.ts +2 -0
- package/src/vscode-webview.ts +2 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every text the shell shows or announces, in one table (guide §2, shared terms). English by
|
|
3
|
+
* default; a localized modeler overrides the whole table in its language, numbers arrive already
|
|
4
|
+
* formatted through `Intl` in that language.
|
|
5
|
+
*/
|
|
6
|
+
export interface ShellMessages {
|
|
7
|
+
/** Menu trigger `aria-label` and tooltip, dropdown `aria-label`. */
|
|
8
|
+
menu: string;
|
|
9
|
+
/** Palette `aria-label`. */
|
|
10
|
+
tools: string;
|
|
11
|
+
/** `aria-label` of the focusable canvas `<svg>` (`role="application"`); a modeler names its notation. */
|
|
12
|
+
canvas: string;
|
|
13
|
+
/** Zoom panel `aria-label`. */
|
|
14
|
+
zoom: string;
|
|
15
|
+
zoomOut: string;
|
|
16
|
+
zoomIn: string;
|
|
17
|
+
/** Tooltip of the readout, e.g. `(percent) => "Zoom to " + percent` with percent "100%". */
|
|
18
|
+
zoomToActual(percent: string): string;
|
|
19
|
+
/** `aria-label` of the readout: "Zoom to 100% (now 86%)". */
|
|
20
|
+
zoomToActualNow(percent: string, now: string): string;
|
|
21
|
+
fit: string;
|
|
22
|
+
/** Legend title and the Model-group checkbox. */
|
|
23
|
+
legend: string;
|
|
24
|
+
closeLegend: string;
|
|
25
|
+
/** Pill text: "1 problem", "2 problems". */
|
|
26
|
+
problems(count: number, formattedCount: string): string;
|
|
27
|
+
/** Pill `aria-label`: "2 problems, 1 error", "2 problems, no errors". */
|
|
28
|
+
problemsLabel(count: number, errors: number, formattedCount: string, formattedErrors: string): string;
|
|
29
|
+
/** Pill tooltip in the IDE host. */
|
|
30
|
+
showProblems: string;
|
|
31
|
+
/** Empty-file hint (guide §13.5). */
|
|
32
|
+
emptyHint: string;
|
|
33
|
+
insertExample: string;
|
|
34
|
+
/** Status box for an invalid file; `diagram` names the notation ("Event Storming board"). */
|
|
35
|
+
invalidFile(diagram: string): string;
|
|
36
|
+
/** Key names in shortcut chips outside macOS (German: Strg, Umschalt, Plus, Minus). */
|
|
37
|
+
keyNames: { ctrl: string; shift: string; plus: string; minus: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Every key optional; an `undefined` value keeps the default (a conditional override). */
|
|
41
|
+
export type ShellMessageOverrides = {
|
|
42
|
+
[Key in Exclude<keyof ShellMessages, "keyNames">]?: ShellMessages[Key] | undefined;
|
|
43
|
+
} & {
|
|
44
|
+
keyNames?: { [Key in keyof ShellMessages["keyNames"]]?: string | undefined } | undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const defaultMessages: ShellMessages = {
|
|
48
|
+
menu: "Menu",
|
|
49
|
+
tools: "Tools",
|
|
50
|
+
canvas: "Diagram",
|
|
51
|
+
zoom: "Zoom",
|
|
52
|
+
zoomOut: "Zoom out",
|
|
53
|
+
zoomIn: "Zoom in",
|
|
54
|
+
zoomToActual: (percent) => `Zoom to ${percent}`,
|
|
55
|
+
zoomToActualNow: (percent, now) => `Zoom to ${percent} (now ${now})`,
|
|
56
|
+
fit: "Fit to view",
|
|
57
|
+
legend: "Legend",
|
|
58
|
+
closeLegend: "Close legend",
|
|
59
|
+
problems: (count, formattedCount) => `${formattedCount} ${count === 1 ? "problem" : "problems"}`,
|
|
60
|
+
problemsLabel: (count, errors, formattedCount, formattedErrors) =>
|
|
61
|
+
`${formattedCount} ${count === 1 ? "problem" : "problems"}, ${
|
|
62
|
+
errors === 0 ? "no errors" : `${formattedErrors} ${errors === 1 ? "error" : "errors"}`
|
|
63
|
+
}`,
|
|
64
|
+
showProblems: "Show in Problems view",
|
|
65
|
+
emptyHint: "Pick a tool from the palette to start.",
|
|
66
|
+
insertExample: "Insert example",
|
|
67
|
+
invalidFile: (diagram) =>
|
|
68
|
+
`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.`,
|
|
69
|
+
keyNames: { ctrl: "Ctrl", shift: "Shift", plus: "Plus", minus: "Minus" },
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** `defaultMessages` with `overrides` applied (`keyNames` merged key by key). */
|
|
73
|
+
export function resolveMessages(overrides?: ShellMessageOverrides | undefined): ShellMessages {
|
|
74
|
+
const { keyNames, ...texts } = overrides ?? {};
|
|
75
|
+
return {
|
|
76
|
+
...defaultMessages,
|
|
77
|
+
...definedEntries(texts),
|
|
78
|
+
keyNames: { ...defaultMessages.keyNames, ...definedEntries(keyNames ?? {}) },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// `{ menu: undefined }` (a spread of optional fields, a conditional override) keeps the default
|
|
83
|
+
// instead of blanking the text.
|
|
84
|
+
function definedEntries<T extends object>(partial: T): { [Key in keyof T]?: Exclude<T[Key], undefined> } {
|
|
85
|
+
return Object.fromEntries(Object.entries(partial).filter(([, value]) => value !== undefined)) as {
|
|
86
|
+
[Key in keyof T]?: Exclude<T[Key], undefined>;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell dimensions of the design system (guide §5.1, `shell` group of
|
|
3
|
+
* `cd-tokens.generated.json`), keyed by a readable name. Scripts use `SHELL`; CSS uses the
|
|
4
|
+
* `var(--cd-shell-*)` variables the consumer vendors. A drift test pins `SHELL` to the token JSON.
|
|
5
|
+
*/
|
|
6
|
+
export const SHELL_TOKENS = {
|
|
7
|
+
edge: "--cd-shell-rand",
|
|
8
|
+
gap: "--cd-shell-luecke",
|
|
9
|
+
row: "--cd-shell-zeile",
|
|
10
|
+
target: "--cd-shell-ziel",
|
|
11
|
+
entry: "--cd-shell-eintrag",
|
|
12
|
+
free: "--cd-shell-frei",
|
|
13
|
+
menuMinWidth: "--cd-shell-menue-breite",
|
|
14
|
+
inspectorWidth: "--cd-shell-inspector-breite",
|
|
15
|
+
fitInsetTop: "--cd-shell-einpassen-oben",
|
|
16
|
+
fitInsetTopSecondRow: "--cd-shell-einpassen-oben-schmal",
|
|
17
|
+
fitInsetSide: "--cd-shell-einpassen-seite",
|
|
18
|
+
fitInsetBottom: "--cd-shell-einpassen-unten",
|
|
19
|
+
fitInsetBottomWeb: "--cd-shell-einpassen-unten-web",
|
|
20
|
+
narrow: "--cd-shell-schmal",
|
|
21
|
+
veryNarrow: "--cd-shell-sehr-schmal",
|
|
22
|
+
low: "--cd-shell-niedrig",
|
|
23
|
+
layerIslands: "--cd-shell-ebene-inseln",
|
|
24
|
+
layerInspector: "--cd-shell-ebene-inspector",
|
|
25
|
+
layerTopRow: "--cd-shell-ebene-reihe",
|
|
26
|
+
layerPopups: "--cd-shell-ebene-aufklapper",
|
|
27
|
+
layerDialog: "--cd-shell-ebene-dialog",
|
|
28
|
+
layerToast: "--cd-shell-ebene-toast",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export type ShellSizeName = keyof typeof SHELL_TOKENS;
|
|
32
|
+
|
|
33
|
+
export type ShellSizes = Readonly<Record<ShellSizeName, number>>;
|
|
34
|
+
|
|
35
|
+
/** Pixels (layers: plain z-index numbers), equal to `parseFloat` of the token JSON values. */
|
|
36
|
+
export const SHELL: ShellSizes = {
|
|
37
|
+
edge: 12,
|
|
38
|
+
gap: 8,
|
|
39
|
+
row: 40,
|
|
40
|
+
target: 32,
|
|
41
|
+
entry: 36,
|
|
42
|
+
free: 64,
|
|
43
|
+
menuMinWidth: 248,
|
|
44
|
+
inspectorWidth: 288,
|
|
45
|
+
fitInsetTop: 88,
|
|
46
|
+
fitInsetTopSecondRow: 140,
|
|
47
|
+
fitInsetSide: 24,
|
|
48
|
+
fitInsetBottom: 64,
|
|
49
|
+
fitInsetBottomWeb: 92,
|
|
50
|
+
narrow: 760,
|
|
51
|
+
veryNarrow: 440,
|
|
52
|
+
low: 820,
|
|
53
|
+
layerIslands: 20,
|
|
54
|
+
layerInspector: 25,
|
|
55
|
+
layerTopRow: 30,
|
|
56
|
+
layerPopups: 40,
|
|
57
|
+
layerDialog: 50,
|
|
58
|
+
layerToast: 60,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Shell sizes are pixels ("12px") or plain z-index numbers ("20"); any other unit would make
|
|
62
|
+
// parseFloat silently return a number with the wrong meaning.
|
|
63
|
+
const PIXEL_OR_PLAIN_NUMBER = /^\s*-?(?:\d+\.?\d*|\.\d+)(?:px)?\s*$/;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Reads the `shell` group of `cd-tokens.generated.json` (`{ "--cd-shell-rand": "12px", … }`) into
|
|
67
|
+
* numbers. Throws when a size of `SHELL_TOKENS` is missing or not a number.
|
|
68
|
+
*/
|
|
69
|
+
export function shellSizesFromTokens(shellGroup: Readonly<Record<string, string>>): ShellSizes {
|
|
70
|
+
const sizes = {} as Record<ShellSizeName, number>;
|
|
71
|
+
for (const name of Object.keys(SHELL_TOKENS) as ShellSizeName[]) {
|
|
72
|
+
const token = SHELL_TOKENS[name];
|
|
73
|
+
const raw: unknown = Object.hasOwn(shellGroup, token) ? shellGroup[token] : undefined;
|
|
74
|
+
if (raw === undefined) {
|
|
75
|
+
throw new Error(`@miragon/modeler-shell: the shell token ${token} is missing`);
|
|
76
|
+
}
|
|
77
|
+
if (typeof raw !== "string" || !PIXEL_OR_PLAIN_NUMBER.test(raw)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`@miragon/modeler-shell: the shell token ${token} is not a number: ${JSON.stringify(raw)}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
sizes[name] = Number.parseFloat(raw);
|
|
83
|
+
}
|
|
84
|
+
return sizes;
|
|
85
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** A point in pixels or diagram coordinates, as the context says. */
|
|
2
|
+
export interface Point {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Size {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A rectangle; content bounds and viewboxes are in diagram coordinates. */
|
|
13
|
+
export interface Bounds {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A viewbox together with the scale diagram-js shows it at (`canvas.viewbox().scale`). */
|
|
21
|
+
export interface ScaledViewbox extends Bounds {
|
|
22
|
+
scale: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Distances in pixels from the edges of the modeler's container. */
|
|
26
|
+
export interface Insets {
|
|
27
|
+
top: number;
|
|
28
|
+
right: number;
|
|
29
|
+
bottom: number;
|
|
30
|
+
left: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The host column of guide §5.2: `ide` is a VS Code webview (later IntelliJ), `web` a Miragon web
|
|
35
|
+
* app that owns its page, `embedded` a modeler inside a foreign page (designIQ, modeler-api hosts).
|
|
36
|
+
*/
|
|
37
|
+
export type ShellHost = "ide" | "web" | "embedded";
|
|
38
|
+
|
|
39
|
+
/** The `data-chrome` names of the islands (guide §5.2, §13.4). `primary` is reserved for the web app. */
|
|
40
|
+
export type ChromeIsland =
|
|
41
|
+
"menu" | "palette" | "primary" | "zoom" | "problems" | "legend" | "inspector" | "status";
|
|
42
|
+
|
|
43
|
+
export const CHROME_ISLANDS: readonly ChromeIsland[] = [
|
|
44
|
+
"menu",
|
|
45
|
+
"palette",
|
|
46
|
+
"primary",
|
|
47
|
+
"zoom",
|
|
48
|
+
"problems",
|
|
49
|
+
"legend",
|
|
50
|
+
"inspector",
|
|
51
|
+
"status",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** Where the palette sits: in the top row, or in the second row below it (guide §5.15). */
|
|
55
|
+
export type PaletteRow = "top" | "second-row";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What the zoom panel and the chrome keys act on. The diagram-js viewport implements it; the DOM
|
|
59
|
+
* shell only talks to this interface.
|
|
60
|
+
*/
|
|
61
|
+
export interface ZoomTarget {
|
|
62
|
+
/** The current zoom, 1 = 100 % (diagram-js `canvas.zoom()`, rounded to three decimals). */
|
|
63
|
+
zoom(): number;
|
|
64
|
+
/** One step on the zoom raster around the container centre (diagram-js `zoomScroll.stepZoom`). */
|
|
65
|
+
stepZoom(direction: 1 | -1): void;
|
|
66
|
+
/** 100 % around the container centre. Counts as a user move. */
|
|
67
|
+
zoomToActual(): void;
|
|
68
|
+
/** Explicit fit (button, Shift+1, command): animated unless reduced motion, makes the view automatic. */
|
|
69
|
+
fit(): void;
|
|
70
|
+
/** False while there is nothing to fit (the content bounds are empty). */
|
|
71
|
+
canFit(): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Calls `listener` after every viewbox change and whenever `canFit()` may have changed, possibly
|
|
74
|
+
* several times per frame. Returns the unsubscribe function.
|
|
75
|
+
*/
|
|
76
|
+
onChange(listener: () => void): () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The persisted view (VS Code `setState`): JSON-serializable. */
|
|
80
|
+
export interface ViewportState {
|
|
81
|
+
viewbox: Bounds;
|
|
82
|
+
/** The user zoomed or panned since the last fit; a resize no longer refits. */
|
|
83
|
+
userMoved: boolean;
|
|
84
|
+
}
|
package/src/core/zoom.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ShellMessages } from "./messages.js";
|
|
2
|
+
|
|
3
|
+
/** diagram-js `ZoomScroll` RANGE (not configurable): 20 % … 400 %. */
|
|
4
|
+
export const ZOOM_MIN = 0.2;
|
|
5
|
+
export const ZOOM_MAX = 4;
|
|
6
|
+
|
|
7
|
+
/** Zoom out is disabled at 20 % and below (`zoom <= 0.2 · 1.001`), so it never zooms in. */
|
|
8
|
+
export function canZoomOut(zoom: number): boolean {
|
|
9
|
+
return zoom > ZOOM_MIN * 1.001;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Zoom in is disabled at 400 % (`zoom >= 4 · 0.999`). */
|
|
13
|
+
export function canZoomIn(zoom: number): boolean {
|
|
14
|
+
return zoom < ZOOM_MAX * 0.999;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A zoom as a whole percentage in the UI language: `Intl.NumberFormat(lang, { style: "percent",
|
|
19
|
+
* maximumFractionDigits: 0 })`, so English "86%", German "86 %" (with U+00A0).
|
|
20
|
+
*/
|
|
21
|
+
export function formatPercent(zoom: number, lang: string): string {
|
|
22
|
+
return numberFormat(lang, "percent").format(zoom);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Any other number in the chrome ("2 problems"), via `Intl.NumberFormat(lang)`. */
|
|
26
|
+
export function formatNumber(value: number, lang: string): string {
|
|
27
|
+
return numberFormat(lang, "decimal").format(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FALLBACK_LANG = "en";
|
|
31
|
+
const formats = new Map<string, Intl.NumberFormat>();
|
|
32
|
+
|
|
33
|
+
// The readout formats on every animation frame of a zoom, so each formatter is built once. A
|
|
34
|
+
// language tag Intl rejects (an empty `lang=""`, a typo) falls back to English, the shell's default
|
|
35
|
+
// language, instead of breaking the chrome.
|
|
36
|
+
function numberFormat(lang: string, style: "percent" | "decimal"): Intl.NumberFormat {
|
|
37
|
+
const key = `${style}|${lang}`;
|
|
38
|
+
let format = formats.get(key);
|
|
39
|
+
if (!format) {
|
|
40
|
+
const options: Intl.NumberFormatOptions =
|
|
41
|
+
style === "percent" ? { style, maximumFractionDigits: 0 } : { style };
|
|
42
|
+
try {
|
|
43
|
+
format = new Intl.NumberFormat(lang, options);
|
|
44
|
+
} catch {
|
|
45
|
+
format = new Intl.NumberFormat(FALLBACK_LANG, options);
|
|
46
|
+
}
|
|
47
|
+
formats.set(key, format);
|
|
48
|
+
}
|
|
49
|
+
return format;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ZoomReadout {
|
|
53
|
+
/** "86%" */
|
|
54
|
+
text: string;
|
|
55
|
+
/** "Zoom to 100% (now 86%)" */
|
|
56
|
+
ariaLabel: string;
|
|
57
|
+
canZoomOut: boolean;
|
|
58
|
+
canZoomIn: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Everything the zoom panel shows for a zoom: readout text, its accessible name and the limits. */
|
|
62
|
+
export function zoomReadout(
|
|
63
|
+
zoom: number,
|
|
64
|
+
lang: string,
|
|
65
|
+
messages: Pick<ShellMessages, "zoomToActualNow">,
|
|
66
|
+
): ZoomReadout {
|
|
67
|
+
const text = formatPercent(zoom, lang);
|
|
68
|
+
return {
|
|
69
|
+
text,
|
|
70
|
+
ariaLabel: messages.zoomToActualNow(formatPercent(1, lang), text),
|
|
71
|
+
canZoomOut: canZoomOut(zoom),
|
|
72
|
+
canZoomIn: canZoomIn(zoom),
|
|
73
|
+
};
|
|
74
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Bounds } from "../core/index.js";
|
|
2
|
+
import type { ShellHandle } from "../dom/index.js";
|
|
3
|
+
import { shellServiceOf } from "./service.js";
|
|
4
|
+
import type { DiagramEventListener, EventBusLike, InjectorLike } from "./types.js";
|
|
5
|
+
import { DiagramViewport, type Viewport } from "./viewport.js";
|
|
6
|
+
|
|
7
|
+
export interface ConnectOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Content bounds in diagram coordinates, tight around the content: the fit insets are the only
|
|
10
|
+
* margin (guide §5.9). `undefined` = nothing to fit (empty file).
|
|
11
|
+
*/
|
|
12
|
+
contentBounds(): Bounds | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* An empty file (guide §13.5: no element, only whitespace, comments or a title), which shows the
|
|
15
|
+
* empty-file hint. Default: `contentBounds()` is empty. A notation whose bounds are never empty
|
|
16
|
+
* (Wardley's plot) passes its own test.
|
|
17
|
+
*/
|
|
18
|
+
isEmpty?: (() => boolean) | undefined;
|
|
19
|
+
prefersReducedMotion?: (() => boolean) | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DiagramJsConnection {
|
|
23
|
+
readonly viewport: Viewport;
|
|
24
|
+
/**
|
|
25
|
+
* Unbinds viewport, palette, keys and the hint sync from the shell; destroys neither the diagram
|
|
26
|
+
* nor the shell. Runs by itself on `diagram.destroy`.
|
|
27
|
+
*/
|
|
28
|
+
destroy(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** After the viewport's own listeners, so its content state is fresh when the hint reads it. */
|
|
32
|
+
const HINT_SYNC_PRIORITY = 500;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Connects a diagram built with `shellModule` to a mounted shell: creates the viewport with
|
|
36
|
+
* `shell.fitInsets`, binds it with `shell.setZoomTarget`, registers the palette
|
|
37
|
+
* (`shell.registerPalette`), keeps the empty-file hint in sync with the diagram (after every
|
|
38
|
+
* `import.done` and `elements.changed`, so the first element created on the canvas hides it) and
|
|
39
|
+
* calls `canvas.resized()`. Throws when `shellModule` is missing.
|
|
40
|
+
*/
|
|
41
|
+
export function connectDiagramJs(
|
|
42
|
+
shell: ShellHandle,
|
|
43
|
+
diagram: InjectorLike,
|
|
44
|
+
options: ConnectOptions,
|
|
45
|
+
): DiagramJsConnection {
|
|
46
|
+
const service = shellServiceOf(diagram);
|
|
47
|
+
const eventBus = diagram.get<EventBusLike>("eventBus");
|
|
48
|
+
const prefersReducedMotion = options.prefersReducedMotion?.bind(options);
|
|
49
|
+
const viewport = new DiagramViewport(diagram, {
|
|
50
|
+
contentBounds: () => options.contentBounds(),
|
|
51
|
+
insets: () => shell.fitInsets(),
|
|
52
|
+
...(prefersReducedMotion ? { prefersReducedMotion } : {}),
|
|
53
|
+
});
|
|
54
|
+
shell.setZoomTarget(viewport);
|
|
55
|
+
const detachPalette = service.attachShell(shell);
|
|
56
|
+
// The shell has moved the canvas element into its slot; diagram-js caches the old size.
|
|
57
|
+
viewport.resized();
|
|
58
|
+
|
|
59
|
+
const isEmpty = options.isEmpty ? () => options.isEmpty?.() === true : () => !viewport.canFit();
|
|
60
|
+
const syncHint: DiagramEventListener = () => shell.setEmptyHint(isEmpty());
|
|
61
|
+
eventBus.on(["import.done", "elements.changed"], HINT_SYNC_PRIORITY, syncHint);
|
|
62
|
+
|
|
63
|
+
let destroyed = false;
|
|
64
|
+
const destroy = (): void => {
|
|
65
|
+
if (destroyed) return;
|
|
66
|
+
destroyed = true;
|
|
67
|
+
eventBus.off(["import.done", "elements.changed"], syncHint);
|
|
68
|
+
eventBus.off("diagram.destroy", destroy);
|
|
69
|
+
detachPalette();
|
|
70
|
+
shell.setZoomTarget(null);
|
|
71
|
+
viewport.destroy();
|
|
72
|
+
};
|
|
73
|
+
eventBus.on("diagram.destroy", destroy);
|
|
74
|
+
return { viewport, destroy };
|
|
75
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
CanvasLike,
|
|
3
|
+
DiagramEventListener,
|
|
4
|
+
DiagramViewbox,
|
|
5
|
+
EventBusLike,
|
|
6
|
+
InjectorLike,
|
|
7
|
+
KeyboardLike,
|
|
8
|
+
ModuleDeclarationLike,
|
|
9
|
+
PaletteLike,
|
|
10
|
+
ZoomScrollLike,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
export { shellModule } from "./module.js";
|
|
13
|
+
export type { Viewport, ViewportOptions } from "./viewport.js";
|
|
14
|
+
export { createViewport } from "./viewport.js";
|
|
15
|
+
export type { ConnectOptions, DiagramJsConnection } from "./connect.js";
|
|
16
|
+
export { connectDiagramJs } from "./connect.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SHELL_SERVICE, ShellDiagramService } from "./service.js";
|
|
2
|
+
import type { ModuleDeclarationLike } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Add to the modeler's modules (`additionalModules`, after the renderer's own modules):
|
|
6
|
+
* - `paletteTooltip: ["value", null]` removes diagram-js's (or the renderer's) palette tooltip; the
|
|
7
|
+
* chrome tooltip takes over;
|
|
8
|
+
* - the palette skin: on `palette.create` it sets `data-chrome="palette"` and moves the palette
|
|
9
|
+
* before the canvas `<svg>`; after every `palette.changed` it re-applies `role="toolbar"`,
|
|
10
|
+
* `aria-label`, roving `tabindex` and `aria-pressed` (from `tool-manager.update`);
|
|
11
|
+
* - `keyboard.addListener(2000, …)` returning `true` for every chrome key, so `KeyboardBindings` and
|
|
12
|
+
* the modeler's keyboard modules (priority 1000) never act on them.
|
|
13
|
+
* It buffers the palette until `connectDiagramJs` hands it to a shell, so the diagram may be
|
|
14
|
+
* constructed before `mountShell`.
|
|
15
|
+
*/
|
|
16
|
+
export const shellModule: ModuleDeclarationLike = {
|
|
17
|
+
__init__: [SHELL_SERVICE],
|
|
18
|
+
[SHELL_SERVICE]: ["type", ShellDiagramService],
|
|
19
|
+
paletteTooltip: ["value", null],
|
|
20
|
+
};
|