@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.
Files changed (126) hide show
  1. package/API.md +1902 -0
  2. package/LICENSE +21 -0
  3. package/README.md +435 -2
  4. package/THIRD_PARTY_NOTICES.md +52 -0
  5. package/dist/appearance-D3FUcbu6.d.cts +53 -0
  6. package/dist/appearance-D3FUcbu6.d.ts +53 -0
  7. package/dist/chunk-5JSZ4VGA.js +551 -0
  8. package/dist/chunk-5JSZ4VGA.js.map +1 -0
  9. package/dist/chunk-6LQOFACK.js +1576 -0
  10. package/dist/chunk-6LQOFACK.js.map +1 -0
  11. package/dist/chunk-N4QXFF3A.js +23 -0
  12. package/dist/chunk-N4QXFF3A.js.map +1 -0
  13. package/dist/chunk-RJRFR6N3.js +136 -0
  14. package/dist/chunk-RJRFR6N3.js.map +1 -0
  15. package/dist/core.cjs +619 -0
  16. package/dist/core.cjs.map +1 -0
  17. package/dist/core.d.cts +171 -0
  18. package/dist/core.d.ts +171 -0
  19. package/dist/core.js +91 -0
  20. package/dist/core.js.map +1 -0
  21. package/dist/diagram-js.cjs +1115 -0
  22. package/dist/diagram-js.cjs.map +1 -0
  23. package/dist/diagram-js.d.cts +163 -0
  24. package/dist/diagram-js.d.ts +163 -0
  25. package/dist/diagram-js.js +867 -0
  26. package/dist/diagram-js.js.map +1 -0
  27. package/dist/index.cjs +2175 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +20 -0
  30. package/dist/index.d.ts +20 -0
  31. package/dist/index.js +103 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/keymap-CObQE6q6.d.cts +151 -0
  34. package/dist/keymap-DORzHnK-.d.ts +151 -0
  35. package/dist/protocol-DhYBakWL.d.cts +15 -0
  36. package/dist/protocol-IeW8bf4f.d.ts +15 -0
  37. package/dist/shell.css +914 -0
  38. package/dist/testing-vscode-stub.cjs +332 -0
  39. package/dist/testing-vscode-stub.cjs.map +1 -0
  40. package/dist/testing-vscode-stub.d.cts +65 -0
  41. package/dist/testing-vscode-stub.d.ts +65 -0
  42. package/dist/testing-vscode-stub.js +154 -0
  43. package/dist/testing-vscode-stub.js.map +1 -0
  44. package/dist/testing.cjs +2364 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +165 -0
  47. package/dist/testing.d.ts +165 -0
  48. package/dist/testing.js +2297 -0
  49. package/dist/testing.js.map +1 -0
  50. package/dist/types-B6NfkiUG.d.cts +66 -0
  51. package/dist/types-B6NfkiUG.d.ts +66 -0
  52. package/dist/types-DnZn9CPw.d.ts +284 -0
  53. package/dist/types-OuFlqZp0.d.cts +284 -0
  54. package/dist/vscode-extension.cjs +215 -0
  55. package/dist/vscode-extension.cjs.map +1 -0
  56. package/dist/vscode-extension.d.cts +103 -0
  57. package/dist/vscode-extension.d.ts +103 -0
  58. package/dist/vscode-extension.js +24 -0
  59. package/dist/vscode-extension.js.map +1 -0
  60. package/dist/vscode-webview.cjs +2198 -0
  61. package/dist/vscode-webview.cjs.map +1 -0
  62. package/dist/vscode-webview.d.cts +82 -0
  63. package/dist/vscode-webview.d.ts +82 -0
  64. package/dist/vscode-webview.js +250 -0
  65. package/dist/vscode-webview.js.map +1 -0
  66. package/package.json +145 -4
  67. package/src/core/appearance.ts +104 -0
  68. package/src/core/fit.ts +205 -0
  69. package/src/core/index.ts +84 -0
  70. package/src/core/keymap.ts +275 -0
  71. package/src/core/layout.ts +69 -0
  72. package/src/core/messages.ts +88 -0
  73. package/src/core/sizes.ts +85 -0
  74. package/src/core/types.ts +84 -0
  75. package/src/core/zoom.ts +74 -0
  76. package/src/core.ts +2 -0
  77. package/src/diagram-js/connect.ts +75 -0
  78. package/src/diagram-js/index.ts +16 -0
  79. package/src/diagram-js/module.ts +20 -0
  80. package/src/diagram-js/palette-skin.ts +267 -0
  81. package/src/diagram-js/palette.css +269 -0
  82. package/src/diagram-js/service.ts +240 -0
  83. package/src/diagram-js/types.ts +67 -0
  84. package/src/diagram-js/viewport.ts +564 -0
  85. package/src/diagram-js.ts +2 -0
  86. package/src/dom/element.ts +51 -0
  87. package/src/dom/hint.ts +51 -0
  88. package/src/dom/icons.ts +309 -0
  89. package/src/dom/index.ts +29 -0
  90. package/src/dom/inspector.ts +35 -0
  91. package/src/dom/keys.ts +59 -0
  92. package/src/dom/ledger.ts +87 -0
  93. package/src/dom/legend.ts +85 -0
  94. package/src/dom/menu.ts +393 -0
  95. package/src/dom/mount.ts +461 -0
  96. package/src/dom/problems.ts +72 -0
  97. package/src/dom/shell-dom.css +966 -0
  98. package/src/dom/status.ts +72 -0
  99. package/src/dom/tooltip.ts +237 -0
  100. package/src/dom/types.ts +277 -0
  101. package/src/dom/zoom-panel.ts +154 -0
  102. package/src/index.ts +3 -0
  103. package/src/shell.css +6 -0
  104. package/src/testing/cases.ts +1012 -0
  105. package/src/testing/checks.ts +832 -0
  106. package/src/testing/facts.ts +587 -0
  107. package/src/testing/geometry.ts +472 -0
  108. package/src/testing/harness.ts +138 -0
  109. package/src/testing/index.ts +15 -0
  110. package/src/testing/page-tools.ts +434 -0
  111. package/src/testing/page.ts +17 -0
  112. package/src/testing/vscode-stub.ts +237 -0
  113. package/src/testing-vscode-stub.ts +2 -0
  114. package/src/testing.ts +2 -0
  115. package/src/vscode/extension/html.ts +107 -0
  116. package/src/vscode/extension/index.ts +8 -0
  117. package/src/vscode/extension/reveal.ts +9 -0
  118. package/src/vscode/extension/theme-bridge.ts +85 -0
  119. package/src/vscode/extension/types.ts +36 -0
  120. package/src/vscode/protocol.ts +31 -0
  121. package/src/vscode/webview/appearance.ts +141 -0
  122. package/src/vscode/webview/index.ts +8 -0
  123. package/src/vscode/webview/mount.ts +76 -0
  124. package/src/vscode/webview/state.ts +116 -0
  125. package/src/vscode-extension.ts +2 -0
  126. package/src/vscode-webview.ts +2 -0
@@ -0,0 +1,240 @@
1
+ import { TEXT_FIELD_SELECTOR, defaultMessages, isUndoRedoKey, matchChromeKey } from "../core/index.js";
2
+ import type { ShellHandle } from "../dom/index.js";
3
+ import { PaletteSkin, type PaletteEntryLike } from "./palette-skin.js";
4
+ import type {
5
+ CanvasLike,
6
+ DiagramEventListener,
7
+ EventBusLike,
8
+ InjectorLike,
9
+ KeyboardLike,
10
+ PaletteLike,
11
+ } from "./types.js";
12
+
13
+ /** The didi name of the service `shellModule` registers. */
14
+ export const SHELL_SERVICE = "modelerShell";
15
+
16
+ /** Above `KeyboardBindings` and the modelers' keyboard modules (default priority 1000). */
17
+ export const KEYBOARD_PRIORITY = 2000;
18
+
19
+ /** Ahead of every default-priority `palette.trigger` listener, so the read-only veto comes first. */
20
+ const TRIGGER_PRIORITY = 2000;
21
+
22
+ /** A palette without a shell is editable. */
23
+ const EDITABLE = { locked: false, shown: false } as const;
24
+
25
+ /** What the shell sets on the canvas `<svg>` while a shell is attached, restored on detach. */
26
+ const CANVAS_ATTRIBUTES = ["role", "aria-label"] as const;
27
+
28
+ function isTextField(target: EventTarget | null): boolean {
29
+ if (!target || typeof (target as Element).closest !== "function") return false;
30
+ return (
31
+ (target as HTMLElement).isContentEditable === true ||
32
+ (target as Element).closest(TEXT_FIELD_SELECTOR) !== null
33
+ );
34
+ }
35
+
36
+ /** didi's injector, which resolves a missing service to `null` with `strict: false`. */
37
+ interface DidiInjectorLike {
38
+ get(name: string, strict?: boolean): unknown;
39
+ }
40
+
41
+ interface KeyboardEventContext {
42
+ keyEvent?: KeyboardEvent;
43
+ }
44
+
45
+ interface PaletteTriggerEvent {
46
+ entry?: PaletteEntryLike;
47
+ event?: Event;
48
+ }
49
+
50
+ /**
51
+ * The diagram-side half of `shellModule`: the palette skin, the chrome-key guard on the diagram-js
52
+ * keyboard and the buffer that holds the palette until `connectDiagramJs` hands it to a shell.
53
+ */
54
+ export class ShellDiagramService {
55
+ static $inject = ["eventBus", "injector"];
56
+
57
+ private svg: Element | undefined;
58
+ /** The canvas `<svg>`'s own `role` and `aria-label` before the shell named it. */
59
+ private canvasOriginals: Map<string, string | null> | undefined;
60
+ private skin: PaletteSkin | undefined;
61
+ private activeTool: string | null = null;
62
+ private shell: ShellHandle | undefined;
63
+ private unregisterPalette: (() => void) | undefined;
64
+ private readonly subscriptions: Array<readonly [string, DiagramEventListener]> = [];
65
+ private destroyed = false;
66
+
67
+ constructor(
68
+ private readonly eventBus: EventBusLike,
69
+ private readonly injector: DidiInjectorLike,
70
+ ) {
71
+ this.subscribe("canvas.init", (event: { svg?: Element }) => {
72
+ this.svg = event.svg;
73
+ this.nameCanvas();
74
+ });
75
+ this.subscribe("palette.create", (event: { container: HTMLElement }) =>
76
+ this.onPaletteCreate(event.container),
77
+ );
78
+ this.subscribe("palette.changed", () => this.skin?.refresh());
79
+ this.subscribe("tool-manager.update", (event: { tool?: string | null }) => {
80
+ this.activeTool = event.tool ?? null;
81
+ this.skin?.setActiveTool(this.activeTool);
82
+ });
83
+ this.subscribe("palette.trigger", this.onPaletteTrigger, TRIGGER_PRIORITY);
84
+ this.subscribe("diagram.destroy", () => this.destroy());
85
+
86
+ const keyboard = injector.get("keyboard", false) as KeyboardLike | null;
87
+ if (keyboard) {
88
+ keyboard.addListener(KEYBOARD_PRIORITY, this.onKeyboardKeydown);
89
+ this.subscriptions.push(["keyboard.keydown", this.onKeyboardKeydown]);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Hands the palette (now or once diagram-js creates it) to `shell`; returns the detach
95
+ * function. A later call replaces the earlier shell.
96
+ */
97
+ attachShell(shell: ShellHandle): () => void {
98
+ this.detachShell();
99
+ if (this.destroyed) return () => undefined;
100
+ this.shell = shell;
101
+ this.skin?.setLabel(shell.messages.tools);
102
+ this.nameCanvas();
103
+ this.registerPalette();
104
+ return () => {
105
+ if (this.shell === shell) this.detachShell();
106
+ };
107
+ }
108
+
109
+ private registerPalette(): void {
110
+ const { shell, skin } = this;
111
+ if (!shell || !skin) return;
112
+ this.unregisterPalette?.();
113
+ this.unregisterPalette = shell.registerPalette({
114
+ element: skin.element,
115
+ entryTooltip: (entry) => skin.entryTooltip(entry),
116
+ setReadOnly: (state) => skin.setReadOnly(state),
117
+ });
118
+ }
119
+
120
+ private detachShell(): void {
121
+ const unregister = this.unregisterPalette;
122
+ this.unregisterPalette = undefined;
123
+ this.shell = undefined;
124
+ unregister?.();
125
+ this.unnameCanvas();
126
+ this.skin?.setReadOnly(EDITABLE);
127
+ this.skin?.setLabel(defaultMessages.tools);
128
+ }
129
+
130
+ /**
131
+ * The focusable canvas is an application with a name (WCAG 4.1.2): screen readers switch to focus
132
+ * mode, so the canvas's single-key commands reach it. A role or name the modeler set stays.
133
+ */
134
+ private nameCanvas(): void {
135
+ const { svg, shell } = this;
136
+ if (!svg || !shell || this.canvasOriginals) return;
137
+ this.canvasOriginals = new Map(CANVAS_ATTRIBUTES.map((name) => [name, svg.getAttribute(name)]));
138
+ if (!svg.hasAttribute("role")) svg.setAttribute("role", "application");
139
+ if (!svg.hasAttribute("aria-label") && !svg.hasAttribute("aria-labelledby")) {
140
+ svg.setAttribute("aria-label", shell.messages.canvas);
141
+ }
142
+ }
143
+
144
+ private unnameCanvas(): void {
145
+ const { svg, canvasOriginals } = this;
146
+ this.canvasOriginals = undefined;
147
+ if (!svg || !canvasOriginals) return;
148
+ for (const [name, value] of canvasOriginals) {
149
+ if (value === null) svg.removeAttribute(name);
150
+ else svg.setAttribute(name, value);
151
+ }
152
+ }
153
+
154
+ private onPaletteCreate(container: HTMLElement): void {
155
+ this.skin?.destroy();
156
+ const palette = this.injector.get("palette", false) as PaletteLike | null;
157
+ const skin = new PaletteSkin(container, () => palette?.getEntries() ?? {});
158
+ this.skin = skin;
159
+ skin.setActiveTool(this.activeTool);
160
+ if (this.shell) skin.setLabel(this.shell.messages.tools);
161
+ this.moveBeforeCanvas(container);
162
+ this.registerPalette();
163
+ }
164
+
165
+ // Palette#_init appends the palette after the <svg>; in front of it the palette comes first in
166
+ // the tab order (guide §5.14).
167
+ private moveBeforeCanvas(container: HTMLElement): void {
168
+ const parent = container.parentElement ?? this.canvasContainer();
169
+ if (!parent) return;
170
+ const svg =
171
+ this.svg?.parentNode === parent
172
+ ? this.svg
173
+ : Array.from(parent.children).find((child) => child.localName === "svg");
174
+ if (svg) parent.insertBefore(container, svg);
175
+ }
176
+
177
+ private canvasContainer(): HTMLElement | undefined {
178
+ const canvas = this.injector.get("canvas", false) as CanvasLike | null;
179
+ return canvas?.getContainer();
180
+ }
181
+
182
+ private readonly onPaletteTrigger = (event: PaletteTriggerEvent): false | undefined => {
183
+ if (!event.entry || !this.skin?.vetoes(event.entry, event.event)) return undefined;
184
+ // A vetoed dragstart would otherwise still start the browser's native drag of the entry.
185
+ event.event?.preventDefault();
186
+ return false;
187
+ };
188
+
189
+ /**
190
+ * `true` stops the diagram-js keyboard (and makes it call `preventDefault`), `false` stops it
191
+ * without: a chrome key in a text field inside the `<svg>` stays the field's (Shift+1 types "!"),
192
+ * and in the IDE host undo and redo stay the document's (guide §13.5), so they reach VS Code
193
+ * instead of the canvas `commandStack`.
194
+ */
195
+ private readonly onKeyboardKeydown = (context: KeyboardEventContext): boolean | undefined => {
196
+ const event = context.keyEvent;
197
+ if (!event) return undefined;
198
+ if (matchChromeKey(event)) return !isTextField(event.target);
199
+ if (this.shell?.host === "ide" && isUndoRedoKey(event)) return false;
200
+ return undefined;
201
+ };
202
+
203
+ private subscribe<E>(name: string, listener: (event: E) => unknown, priority?: number): void {
204
+ const handler = listener as DiagramEventListener;
205
+ if (priority === undefined) this.eventBus.on(name, handler);
206
+ else this.eventBus.on(name, priority, handler);
207
+ this.subscriptions.push([name, handler]);
208
+ }
209
+
210
+ private destroy(): void {
211
+ if (this.destroyed) return;
212
+ this.destroyed = true;
213
+ this.detachShell();
214
+ this.skin?.destroy();
215
+ this.skin = undefined;
216
+ this.svg = undefined;
217
+ for (const [name, listener] of this.subscriptions.splice(0)) this.eventBus.off(name, listener);
218
+ }
219
+ }
220
+
221
+ /** The service of `shellModule` in `diagram`; throws when the modeler was built without it. */
222
+ export function shellServiceOf(diagram: InjectorLike): ShellDiagramService {
223
+ let service: unknown;
224
+ try {
225
+ service = diagram.get<unknown>(SHELL_SERVICE);
226
+ } catch {
227
+ service = undefined;
228
+ }
229
+ if (!isShellService(service)) {
230
+ throw new Error(
231
+ "@miragon/modeler-shell: connectDiagramJs needs shellModule in the modeler's modules (additionalModules: [shellModule])",
232
+ );
233
+ }
234
+ return service;
235
+ }
236
+
237
+ // Duck-typed rather than `instanceof`, so a shellModule from the CJS build connects to the ESM one.
238
+ function isShellService(value: unknown): value is ShellDiagramService {
239
+ return typeof (value as Partial<ShellDiagramService> | null)?.attachShell === "function";
240
+ }
@@ -0,0 +1,67 @@
1
+ import type { Bounds, Point, ScaledViewbox, Size } from "../core/index.js";
2
+
3
+ /**
4
+ * diagram-js services, typed structurally: the shell never imports diagram-js (not even its
5
+ * types), so it works with every diagram-js 15.28+ build the modeler bundles.
6
+ */
7
+
8
+ /** Anything with diagram-js's `get`: a `Diagram`, a renderer's `Modeler`, a didi injector. */
9
+ export interface InjectorLike {
10
+ get<T>(name: string): T;
11
+ }
12
+
13
+ /** A didi module declaration, assignable to didi's `ModuleDeclaration`. */
14
+ export interface ModuleDeclarationLike {
15
+ __init__?: string[];
16
+ __depends__?: ModuleDeclarationLike[];
17
+ [name: string]: unknown;
18
+ }
19
+
20
+ export type DiagramEventListener = (event: never) => unknown;
21
+
22
+ /** `eventBus` */
23
+ export interface EventBusLike {
24
+ on(events: string | string[], priority: number, listener: DiagramEventListener): void;
25
+ on(events: string | string[], listener: DiagramEventListener): void;
26
+ off(events: string | string[], listener?: DiagramEventListener): void;
27
+ }
28
+
29
+ /** `canvas.viewbox()` */
30
+ export interface DiagramViewbox extends ScaledViewbox {
31
+ inner: Bounds;
32
+ outer: Size;
33
+ }
34
+
35
+ /** `canvas` */
36
+ export interface CanvasLike {
37
+ getContainer(): HTMLElement;
38
+ getSize(): Size;
39
+ viewbox(): DiagramViewbox;
40
+ viewbox(box: Bounds): void;
41
+ zoom(): number;
42
+ zoom(scale: number, center?: Point): number;
43
+ resized(): void;
44
+ }
45
+
46
+ /** `zoomScroll` */
47
+ export interface ZoomScrollLike {
48
+ stepZoom(delta: number, position?: Point): void;
49
+ }
50
+
51
+ /** `keyboard` (listeners are removed through `eventBus.off("keyboard.keydown", …)`) */
52
+ export interface KeyboardLike {
53
+ addListener(priority: number, listener: (context: { keyEvent: KeyboardEvent }) => unknown): void;
54
+ }
55
+
56
+ /** `palette` */
57
+ export interface PaletteLike {
58
+ getEntries(): Record<
59
+ string,
60
+ {
61
+ title?: string | undefined;
62
+ shortcut?: string | undefined;
63
+ ariaKeyShortcuts?: string | undefined;
64
+ group?: string | undefined;
65
+ }
66
+ >;
67
+ }