@stocksharp/diagram 0.4.1 → 0.6.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 (45) hide show
  1. package/README.md +105 -2
  2. package/dist/esm/canvas-renderer.js +116 -36
  3. package/dist/esm/canvas-renderer.js.map +1 -1
  4. package/dist/esm/diagram/context-menu.js +319 -0
  5. package/dist/esm/diagram/context-menu.js.map +1 -0
  6. package/dist/esm/diagram/stocksharp-diagram.js +121 -9
  7. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
  8. package/dist/esm/draw-surface.js +2 -0
  9. package/dist/esm/draw-surface.js.map +1 -0
  10. package/dist/esm/embed.js +7 -4
  11. package/dist/esm/embed.js.map +1 -1
  12. package/dist/esm/i18n.js +15 -4
  13. package/dist/esm/i18n.js.map +1 -1
  14. package/dist/esm/index.js.map +1 -1
  15. package/dist/esm/svg-surface.js +341 -0
  16. package/dist/esm/svg-surface.js.map +1 -0
  17. package/dist/ssdiagram.js +978 -188
  18. package/dist/ssdiagram.js.map +4 -4
  19. package/dist/types/canvas-renderer.d.ts +13 -0
  20. package/dist/types/canvas-renderer.d.ts.map +1 -1
  21. package/dist/types/diagram/api.d.ts +42 -2
  22. package/dist/types/diagram/api.d.ts.map +1 -1
  23. package/dist/types/diagram/context-menu.d.ts +33 -0
  24. package/dist/types/diagram/context-menu.d.ts.map +1 -0
  25. package/dist/types/diagram/stocksharp-diagram.d.ts +18 -5
  26. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
  27. package/dist/types/draw-surface.d.ts +52 -0
  28. package/dist/types/draw-surface.d.ts.map +1 -0
  29. package/dist/types/embed.d.ts.map +1 -1
  30. package/dist/types/i18n.d.ts +12 -0
  31. package/dist/types/i18n.d.ts.map +1 -1
  32. package/dist/types/index.d.ts +2 -1
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/dist/types/svg-surface.d.ts +74 -0
  35. package/dist/types/svg-surface.d.ts.map +1 -0
  36. package/package.json +1 -1
  37. package/src/canvas-renderer.ts +120 -27
  38. package/src/diagram/api.ts +53 -2
  39. package/src/diagram/context-menu.ts +363 -0
  40. package/src/diagram/stocksharp-diagram.ts +134 -10
  41. package/src/draw-surface.ts +61 -0
  42. package/src/embed.ts +7 -4
  43. package/src/i18n.ts +41 -6
  44. package/src/index.ts +10 -0
  45. package/src/svg-surface.ts +430 -0
@@ -0,0 +1,319 @@
1
+ import { t } from '../i18n.js';
2
+ /**
3
+ * The control's own context menu.
4
+ *
5
+ * It exists because right-click already suppresses the browser's menu whether or not anyone
6
+ * is listening. A control that takes the native menu away owes one back, so this is on by
7
+ * default; a host with its own menu turns it off (`showContextMenu: false`) and keeps using
8
+ * `contextMenuRequested`, which is emitted either way.
9
+ *
10
+ * Styling follows the fullscreen button: inline styles driven by `--ssdiagram-*` custom
11
+ * properties with sensible fallbacks, so the menu is usable with no stylesheet at all and
12
+ * retintable without one. Class names are stable for hosts that would rather use CSS.
13
+ */
14
+ const ROOT_CLASS = 'ssdiagram-context-menu';
15
+ const ITEM_CLASS = 'ssdiagram-context-menu-item';
16
+ const SUBMENU_CLASS = 'ssdiagram-context-menu-submenu';
17
+ const SEPARATOR_CLASS = 'ssdiagram-context-menu-separator';
18
+ const LABEL_CLASS = 'ssdiagram-context-menu-label';
19
+ const ARROW_CLASS = 'ssdiagram-context-menu-arrow';
20
+ const CHECK_CLASS = 'ssdiagram-context-menu-check';
21
+ /** A rule below these entries, so related commands read as blocks rather than one long list. */
22
+ const SEPARATE_AFTER = new Set(['redo', 'paste', 'delete', 'overview']);
23
+ function label(id) {
24
+ switch (id) {
25
+ case 'undo': return t('undo', 'Undo');
26
+ case 'redo': return t('redo', 'Redo');
27
+ case 'cut': return t('cut', 'Cut');
28
+ case 'copy': return t('copy', 'Copy');
29
+ case 'paste': return t('paste', 'Paste');
30
+ case 'open': return t('ctxOpen', 'Open');
31
+ case 'delete': return t('ctxDelete', 'Delete');
32
+ case 'export': return t('ctxExportAs', 'Export as');
33
+ case 'exportDocument': return t('ctxExportDocument', 'Scheme');
34
+ case 'exportPng': return t('ctxExportPng', 'PNG image');
35
+ case 'exportSvg': return t('ctxExportSvg', 'SVG image');
36
+ case 'overview': return t('ctxOverview', 'Overview');
37
+ case 'properties': return t('properties', 'Properties');
38
+ case 'help': return t('ctxHelp', 'Help');
39
+ }
40
+ }
41
+ const PANEL_STYLE = {
42
+ position: 'fixed',
43
+ zIndex: '2147483000',
44
+ minWidth: '176px',
45
+ // Translations run longer than English -- German and Russian routinely half again. Without
46
+ // a cap the panel just grows, and on a narrow viewport it leaves the screen entirely, which
47
+ // no amount of repositioning can recover.
48
+ maxWidth: 'min(320px, calc(100vw - 16px))',
49
+ padding: '4px',
50
+ border: '1px solid var(--ssdiagram-menu-border, var(--ssdiagram-control-border, var(--t-border, #3a4250)))',
51
+ borderRadius: '7px',
52
+ background: 'var(--ssdiagram-menu-background, var(--ssdiagram-control-background, var(--t-panel, #181a20)))',
53
+ color: 'var(--ssdiagram-menu-color, var(--ssdiagram-control-color, var(--t-text, #eaecef)))',
54
+ boxShadow: '0 12px 32px rgba(0, 0, 0, 0.45)',
55
+ font: 'var(--ssdiagram-menu-font, 12.5px/1.35 system-ui, -apple-system, "Segoe UI", sans-serif)',
56
+ boxSizing: 'border-box',
57
+ userSelect: 'none',
58
+ };
59
+ const ITEM_STYLE = {
60
+ display: 'flex',
61
+ alignItems: 'center',
62
+ gap: '10px',
63
+ width: '100%',
64
+ padding: '6px 10px',
65
+ border: '0',
66
+ borderRadius: '4px',
67
+ background: 'transparent',
68
+ color: 'inherit',
69
+ font: 'inherit',
70
+ // Logical, not physical: on an RTL page these follow the writing direction instead of
71
+ // pinning the text left and truncating from the wrong end.
72
+ textAlign: 'start',
73
+ overflowWrap: 'anywhere',
74
+ cursor: 'pointer',
75
+ boxSizing: 'border-box',
76
+ };
77
+ const HOVER_BACKGROUND = 'var(--ssdiagram-menu-hover, var(--t-hover, rgba(255, 255, 255, 0.08)))';
78
+ const DISABLED_COLOR = 'var(--ssdiagram-menu-disabled-color, var(--t-text-dim, #848e9c))';
79
+ export class ContextMenuView {
80
+ constructor(options) {
81
+ this.root = null;
82
+ this.submenu = null;
83
+ this.submenuTriggers = new Map();
84
+ this.detach = [];
85
+ this.container = options.container;
86
+ this.execute = options.execute;
87
+ }
88
+ get isOpen() {
89
+ return this.root !== null;
90
+ }
91
+ show(x, y, items) {
92
+ this.hide();
93
+ const owner = this.container.ownerDocument ?? document;
94
+ const root = this.panel(owner);
95
+ root.className = ROOT_CLASS;
96
+ root.setAttribute('role', 'menu');
97
+ const gutter = items.some((item) => !('group' in item) && item.checked !== undefined);
98
+ items.forEach((item, index) => {
99
+ if ('group' in item)
100
+ root.appendChild(this.groupEntry(owner, item, gutter));
101
+ else
102
+ root.appendChild(this.commandEntry(owner, item.command, item.enabled, true, item.checked, gutter));
103
+ const id = 'group' in item ? item.group : item.command;
104
+ if (SEPARATE_AFTER.has(id) && index < items.length - 1)
105
+ root.appendChild(this.separator(owner));
106
+ });
107
+ this.container.appendChild(root);
108
+ this.root = root;
109
+ this.place(root, x, y);
110
+ this.watchForDismissal(owner);
111
+ }
112
+ hide() {
113
+ for (const dispose of this.detach.splice(0))
114
+ dispose();
115
+ this.submenu = null;
116
+ this.submenuTriggers.clear();
117
+ this.root?.remove();
118
+ this.root = null;
119
+ }
120
+ destroy() {
121
+ this.hide();
122
+ }
123
+ panel(owner) {
124
+ const panel = owner.createElement('div');
125
+ Object.assign(panel.style, PANEL_STYLE);
126
+ return panel;
127
+ }
128
+ separator(owner) {
129
+ const rule = owner.createElement('div');
130
+ rule.className = SEPARATOR_CLASS;
131
+ Object.assign(rule.style, {
132
+ height: '1px',
133
+ margin: '4px 6px',
134
+ background: 'var(--ssdiagram-menu-border, var(--t-border, #3a4250))',
135
+ });
136
+ return rule;
137
+ }
138
+ item(owner, text, enabled, arrow, checked, gutter) {
139
+ const button = owner.createElement('button');
140
+ button.type = 'button';
141
+ button.className = ITEM_CLASS;
142
+ button.setAttribute('role', checked === undefined ? 'menuitem' : 'menuitemcheckbox');
143
+ if (checked !== undefined)
144
+ button.setAttribute('aria-checked', String(checked));
145
+ Object.assign(button.style, ITEM_STYLE);
146
+ if (!enabled) {
147
+ button.disabled = true;
148
+ button.style.color = DISABLED_COLOR;
149
+ button.style.cursor = 'default';
150
+ }
151
+ // Every item in a panel that has a checkable entry gets the same gutter, so the
152
+ // captions stay on one line rather than one row sitting further in than the rest.
153
+ if (gutter) {
154
+ const tick = owner.createElement('span');
155
+ tick.className = CHECK_CLASS;
156
+ tick.textContent = checked === true ? '✓' : '';
157
+ tick.setAttribute('aria-hidden', 'true');
158
+ Object.assign(tick.style, { flex: 'none', width: '12px', textAlign: 'center' });
159
+ button.appendChild(tick);
160
+ }
161
+ const caption = owner.createElement('span');
162
+ caption.className = LABEL_CLASS;
163
+ caption.textContent = text;
164
+ caption.style.flex = '1';
165
+ button.appendChild(caption);
166
+ if (arrow) {
167
+ const chevron = owner.createElement('span');
168
+ chevron.className = ARROW_CLASS;
169
+ // Points the way the submenu actually opens, which on an RTL page is the other way.
170
+ chevron.textContent = this.rtl() ? '‹' : '›';
171
+ chevron.setAttribute('aria-hidden', 'true');
172
+ Object.assign(chevron.style, { flex: 'none', opacity: '0.7', fontSize: '14px' });
173
+ button.appendChild(chevron);
174
+ }
175
+ return button;
176
+ }
177
+ // `closesSubmenu` separates the two places an item can live. A top-level item shuts any
178
+ // open submenu when the pointer arrives, the way menus everywhere behave. An item inside
179
+ // the submenu must not: reaching it means the pointer has entered that very panel, and
180
+ // closing it there would pull it out from under the pointer and drop the click on the
181
+ // canvas behind.
182
+ commandEntry(owner, command, enabled, closesSubmenu, checked, gutter) {
183
+ const button = this.item(owner, label(command), enabled, false, checked, gutter);
184
+ this.on(button, 'pointerenter', () => {
185
+ if (closesSubmenu)
186
+ this.closeSubmenu();
187
+ if (enabled)
188
+ button.style.background = HOVER_BACKGROUND;
189
+ });
190
+ this.on(button, 'pointerleave', () => { button.style.background = 'transparent'; });
191
+ this.on(button, 'click', () => {
192
+ if (!enabled)
193
+ return;
194
+ // Hide first: a command may open a dialog, and a menu left on top of it looks stuck.
195
+ this.hide();
196
+ this.execute(command);
197
+ });
198
+ return button;
199
+ }
200
+ groupEntry(owner, group, gutter) {
201
+ const host = owner.createElement('div');
202
+ host.style.position = 'relative';
203
+ const button = this.item(owner, label(group.group), group.enabled, true, undefined, gutter);
204
+ button.setAttribute('aria-haspopup', 'menu');
205
+ button.setAttribute('aria-expanded', 'false');
206
+ host.appendChild(button);
207
+ const panel = this.panel(owner);
208
+ panel.className = SUBMENU_CLASS;
209
+ panel.setAttribute('role', 'menu');
210
+ panel.setAttribute('aria-label', label(group.group));
211
+ // insetInlineStart, not left: the submenu opens towards the end of the line, which
212
+ // is the right on an LTR page and the left on an RTL one.
213
+ Object.assign(panel.style, { position: 'absolute', insetInlineStart: '100%', top: '-5px', display: 'none' });
214
+ const childGutter = group.commands.some((child) => child.checked !== undefined);
215
+ for (const child of group.commands) {
216
+ panel.appendChild(this.commandEntry(owner, child.command, child.enabled, false, child.checked, childGutter));
217
+ }
218
+ host.appendChild(panel);
219
+ const open = () => {
220
+ if (!group.enabled)
221
+ return;
222
+ button.style.background = HOVER_BACKGROUND;
223
+ button.setAttribute('aria-expanded', 'true');
224
+ panel.style.display = 'block';
225
+ this.submenu = panel;
226
+ this.keepOnScreen(panel);
227
+ };
228
+ this.on(button, 'pointerenter', open);
229
+ // Tap opens it too: a touch device never sends pointerenter on its own.
230
+ this.on(button, 'click', open);
231
+ this.on(host, 'pointerleave', () => {
232
+ if (this.submenu !== panel)
233
+ return;
234
+ button.style.background = 'transparent';
235
+ button.setAttribute('aria-expanded', 'false');
236
+ panel.style.display = 'none';
237
+ this.submenu = null;
238
+ });
239
+ this.submenuTriggers.set(panel, button);
240
+ return host;
241
+ }
242
+ closeSubmenu() {
243
+ if (this.submenu === null)
244
+ return;
245
+ this.submenu.style.display = 'none';
246
+ this.submenuTriggers.get(this.submenu)?.setAttribute('aria-expanded', 'false');
247
+ this.submenu = null;
248
+ }
249
+ // Reads the resolved writing direction so the submenu's side and its arrow agree with it.
250
+ rtl() {
251
+ if (typeof getComputedStyle !== 'function')
252
+ return false;
253
+ try {
254
+ return getComputedStyle(this.container).direction === 'rtl';
255
+ }
256
+ catch {
257
+ // A detached or fake container has no computed style; left-to-right is the safe read.
258
+ return false;
259
+ }
260
+ }
261
+ /** Keeps the panel inside the viewport, flipping it left when it would run off the edge. */
262
+ keepOnScreen(panel) {
263
+ if (typeof panel.getBoundingClientRect !== 'function' || typeof window === 'undefined')
264
+ return;
265
+ const box = panel.getBoundingClientRect();
266
+ if (box.right > window.innerWidth - 4 || box.left < 4) {
267
+ panel.style.insetInlineStart = 'auto';
268
+ panel.style.insetInlineEnd = '100%';
269
+ }
270
+ if (box.bottom > window.innerHeight - 4) {
271
+ panel.style.top = 'auto';
272
+ panel.style.bottom = '-5px';
273
+ }
274
+ }
275
+ place(root, x, y) {
276
+ root.style.left = `${x}px`;
277
+ root.style.top = `${y}px`;
278
+ if (typeof root.getBoundingClientRect !== 'function' || typeof window === 'undefined')
279
+ return;
280
+ const box = root.getBoundingClientRect();
281
+ // Flip rather than clamp: a menu shoved back onto the screen would cover the very
282
+ // thing that was right-clicked.
283
+ if (box.right > window.innerWidth - 4)
284
+ root.style.left = `${Math.max(4, x - box.width)}px`;
285
+ if (box.bottom > window.innerHeight - 4)
286
+ root.style.top = `${Math.max(4, y - box.height)}px`;
287
+ }
288
+ watchForDismissal(owner) {
289
+ const dismiss = () => this.hide();
290
+ const outside = (event) => {
291
+ // Duck-typed rather than `target instanceof Node`: this runs in whatever document
292
+ // the host mounted us in, and a cross-document Node fails that check.
293
+ const root = this.root;
294
+ const target = event.target;
295
+ if (root !== null && target !== null && typeof root.contains === 'function' && root.contains(target))
296
+ return;
297
+ this.hide();
298
+ };
299
+ this.on(owner, 'pointerdown', outside, true);
300
+ this.on(owner, 'keydown', (event) => {
301
+ if (event.key === 'Escape')
302
+ this.hide();
303
+ });
304
+ if (typeof window !== 'undefined') {
305
+ // Scroll and resize move the diagram out from under the menu, which would otherwise
306
+ // hang in place pointing at nothing.
307
+ this.on(window, 'blur', dismiss);
308
+ this.on(window, 'resize', dismiss);
309
+ this.on(window, 'scroll', dismiss, true);
310
+ }
311
+ }
312
+ on(target, type, listener, capture = false) {
313
+ if (typeof target.addEventListener !== 'function')
314
+ return;
315
+ target.addEventListener(type, listener, capture);
316
+ this.detach.push(() => target.removeEventListener?.(type, listener, capture));
317
+ }
318
+ }
319
+ //# sourceMappingURL=context-menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-menu.js","sourceRoot":"","sources":["../../../src/diagram/context-menu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAG/B;;;;;;;;;;;GAWG;AAEH,MAAM,UAAU,GAAG,wBAAwB,CAAC;AAC5C,MAAM,UAAU,GAAG,6BAA6B,CAAC;AACjD,MAAM,aAAa,GAAG,gCAAgC,CAAC;AACvD,MAAM,eAAe,GAAG,kCAAkC,CAAC;AAC3D,MAAM,WAAW,GAAG,8BAA8B,CAAC;AACnD,MAAM,WAAW,GAAG,8BAA8B,CAAC;AACnD,MAAM,WAAW,GAAG,8BAA8B,CAAC;AAEnD,gGAAgG;AAChG,MAAM,cAAc,GAAwB,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;AAE7F,SAAS,KAAK,CAAC,EAAwC;IACnD,QAAQ,EAAE,EAAE,CAAC;QACT,KAAK,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,KAAK,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,KAAK,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACnC,KAAK,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,KAAK,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACzC,KAAK,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACzC,KAAK,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QAC/C,KAAK,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;QACpD,KAAK,gBAAgB,CAAC,CAAC,OAAO,CAAC,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;QAC/D,KAAK,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;QACxD,KAAK,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;QACxD,KAAK,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QACrD,KAAK,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QACxD,KAAK,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;AACL,CAAC;AAED,MAAM,WAAW,GAAiC;IAC9C,QAAQ,EAAE,OAAO;IACjB,MAAM,EAAE,YAAY;IACpB,QAAQ,EAAE,OAAO;IACjB,2FAA2F;IAC3F,4FAA4F;IAC5F,0CAA0C;IAC1C,QAAQ,EAAE,gCAAgC;IAC1C,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,mGAAmG;IAC3G,YAAY,EAAE,KAAK;IACnB,UAAU,EAAE,gGAAgG;IAC5G,KAAK,EAAE,qFAAqF;IAC5F,SAAS,EAAE,iCAAiC;IAC5C,IAAI,EAAE,0FAA0F;IAChG,SAAS,EAAE,YAAY;IACvB,UAAU,EAAE,MAAM;CACrB,CAAC;AAEF,MAAM,UAAU,GAAiC;IAC7C,OAAO,EAAE,MAAM;IACf,UAAU,EAAE,QAAQ;IACpB,GAAG,EAAE,MAAM;IACX,KAAK,EAAE,MAAM;IACb,OAAO,EAAE,UAAU;IACnB,MAAM,EAAE,GAAG;IACX,YAAY,EAAE,KAAK;IACnB,UAAU,EAAE,aAAa;IACzB,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE,SAAS;IACf,sFAAsF;IACtF,2DAA2D;IAC3D,SAAS,EAAE,OAAO;IAClB,YAAY,EAAE,UAAU;IACxB,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,YAAY;CAC1B,CAAC;AAEF,MAAM,gBAAgB,GAAG,wEAAwE,CAAC;AAClG,MAAM,cAAc,GAAG,kEAAkE,CAAC;AAS1F,MAAM,OAAO,eAAe;IAQxB,YAAY,OAA+B;QALnC,SAAI,GAAuB,IAAI,CAAC;QAChC,YAAO,GAAuB,IAAI,CAAC;QAC1B,oBAAe,GAAG,IAAI,GAAG,EAA4B,CAAC;QACtD,WAAM,GAAsB,EAAE,CAAC;QAG5C,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACnC,CAAC;IAED,IAAI,MAAM;QACN,OAAO,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,CAAS,EAAE,CAAS,EAAE,KAAsC;QAC7D,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,IAAI,QAAQ,CAAC;QACvD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC;QAC5B,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAElC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC;QACtF,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;YAC1B,IAAI,OAAO,IAAI,IAAI;gBAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;;gBACvE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;YACxG,MAAM,EAAE,GAAG,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;YACvD,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC;gBAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QACpG,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACvB,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IAED,IAAI;QACA,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,OAAO,EAAE,CAAC;QACvD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;IAED,OAAO;QACH,IAAI,CAAC,IAAI,EAAE,CAAC;IAChB,CAAC;IAEO,KAAK,CAAC,KAAe;QACzB,MAAM,KAAK,GAAG,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QACxC,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,SAAS,CAAC,KAAe;QAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,CAAC,SAAS,GAAG,eAAe,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE;YACtB,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,SAAS;YACjB,UAAU,EAAE,wDAAwD;SACvE,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IAChB,CAAC;IAEO,IAAI,CACR,KAAe,EACf,IAAY,EACZ,OAAgB,EAChB,KAAc,EACd,OAA4B,EAC5B,MAAe;QAEf,MAAM,MAAM,GAAG,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAC;QACvB,MAAM,CAAC,SAAS,GAAG,UAAU,CAAC;QAC9B,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC;QACrF,IAAI,OAAO,KAAK,SAAS;YAAE,MAAM,CAAC,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QAChF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QACxC,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC;YACvB,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,cAAc,CAAC;YACpC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC;QACpC,CAAC;QAED,gFAAgF;QAChF,kFAAkF;QAClF,IAAI,MAAM,EAAE,CAAC;YACT,MAAM,IAAI,GAAG,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YACzC,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC;YAC7B,IAAI,CAAC,WAAW,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/C,IAAI,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;YACzC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;YAChF,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAC5C,OAAO,CAAC,SAAS,GAAG,WAAW,CAAC;QAChC,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC;QACzB,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAE5B,IAAI,KAAK,EAAE,CAAC;YACR,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YAC5C,OAAO,CAAC,SAAS,GAAG,WAAW,CAAC;YAChC,oFAAoF;YACpF,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;YAC7C,OAAO,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;YAC5C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YACjF,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,wFAAwF;IACxF,yFAAyF;IACzF,uFAAuF;IACvF,sFAAsF;IACtF,iBAAiB;IACT,YAAY,CAChB,KAAe,EACf,OAAuB,EACvB,OAAgB,EAChB,aAAsB,EACtB,OAA4B,EAC5B,MAAe;QAEf,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACjF,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE;YACjC,IAAI,aAAa;gBAAE,IAAI,CAAC,YAAY,EAAE,CAAC;YACvC,IAAI,OAAO;gBAAE,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAC;QAC5D,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;QACpF,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE;YAC1B,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,qFAAqF;YACrF,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAClB,CAAC;IAEO,UAAU,CACd,KAAe,EACf,KAA8I,EAC9I,MAAe;QAEf,MAAM,IAAI,GAAG,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,UAAU,CAAC;QAEjC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QAC5F,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;QAC7C,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAC9C,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAEzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAChC,KAAK,CAAC,SAAS,GAAG,aAAa,CAAC;QAChC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACnC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACrD,mFAAmF;QACnF,0DAA0D;QAC1D,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QAC7G,MAAM,WAAW,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC;QAChF,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACjC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;QACjH,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAExB,MAAM,IAAI,GAAG,GAAS,EAAE;YACpB,IAAI,CAAC,KAAK,CAAC,OAAO;gBAAE,OAAO;YAC3B,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAC;YAC3C,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;YAC7C,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;YAC9B,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;YACrB,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC,CAAC;QACF,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC;QACtC,wEAAwE;QACxE,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QAC/B,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE;YAC/B,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK;gBAAE,OAAO;YACnC,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,aAAa,CAAC;YACxC,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;YAC9C,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;YAC7B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACxB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACxC,OAAO,IAAI,CAAC;IAChB,CAAC;IAEO,YAAY;QAChB,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI;YAAE,OAAO;QAClC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QACpC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAC/E,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;IACxB,CAAC;IAED,0FAA0F;IAClF,GAAG;QACP,IAAI,OAAO,gBAAgB,KAAK,UAAU;YAAE,OAAO,KAAK,CAAC;QACzD,IAAI,CAAC;YACD,OAAO,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,SAAS,KAAK,KAAK,CAAC;QAChE,CAAC;QAAC,MAAM,CAAC;YACL,sFAAsF;YACtF,OAAO,KAAK,CAAC;QACjB,CAAC;IACL,CAAC;IAED,4FAA4F;IACpF,YAAY,CAAC,KAAkB;QACnC,IAAI,OAAO,KAAK,CAAC,qBAAqB,KAAK,UAAU,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO;QAC/F,MAAM,GAAG,GAAG,KAAK,CAAC,qBAAqB,EAAE,CAAC;QAC1C,IAAI,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,UAAU,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACpD,KAAK,CAAC,KAAK,CAAC,gBAAgB,GAAG,MAAM,CAAC;YACtC,KAAK,CAAC,KAAK,CAAC,cAAc,GAAG,MAAM,CAAC;QACxC,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,WAAW,GAAG,CAAC,EAAE,CAAC;YACtC,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC;YACzB,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;QAChC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,IAAiB,EAAE,CAAS,EAAE,CAAS;QACjD,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QAC3B,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC;QAC1B,IAAI,OAAO,IAAI,CAAC,qBAAqB,KAAK,UAAU,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO;QAC9F,MAAM,GAAG,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;QACzC,kFAAkF;QAClF,gCAAgC;QAChC,IAAI,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,UAAU,GAAG,CAAC;YAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;QAC3F,IAAI,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,WAAW,GAAG,CAAC;YAAE,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IACjG,CAAC;IAEO,iBAAiB,CAAC,KAAe;QACrC,MAAM,OAAO,GAAG,GAAS,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACxC,MAAM,OAAO,GAAG,CAAC,KAAY,EAAQ,EAAE;YACnC,kFAAkF;YAClF,sEAAsE;YACtE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACvB,MAAM,MAAM,GAAG,KAAK,CAAC,MAAqB,CAAC;YAC3C,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,UAAU,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAAE,OAAO;YAC7G,IAAI,CAAC,IAAI,EAAE,CAAC;QAChB,CAAC,CAAC;QACF,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QAC7C,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,CAAC,KAAY,EAAE,EAAE;YACvC,IAAK,KAAuB,CAAC,GAAG,KAAK,QAAQ;gBAAE,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/D,CAAC,CAAC,CAAC;QACH,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YAChC,oFAAoF;YACpF,qCAAqC;YACrC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;YACjC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;YACnC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QAC7C,CAAC;IACL,CAAC;IAEO,EAAE,CACN,MAA2L,EAC3L,IAAY,EACZ,QAAuB,EACvB,OAAO,GAAG,KAAK;QAEf,IAAI,OAAO,MAAM,CAAC,gBAAgB,KAAK,UAAU;YAAE,OAAO;QAC1D,MAAM,CAAC,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QACjD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAClF,CAAC;CACJ"}
@@ -2,8 +2,30 @@ import { parseDiagramDocument, serializeDiagramDocument, } from '../core/documen
2
2
  import { parseDiagramViewState, serializeDiagramViewState, } from '../core/view-state.js';
3
3
  import { DiagramActionRegistry } from '../core/action-registry.js';
4
4
  import { Diagram as CanvasDiagram, } from '../canvas-renderer.js';
5
+ import { ContextMenuView } from './context-menu.js';
6
+ import { t } from '../i18n.js';
5
7
  import { EventEmitter } from './event-emitter.js';
6
8
  import { DiagramNode, Link, Node, Port, } from './types.js';
9
+ /**
10
+ * Menu order, and the only place the nesting is declared. The registry is flat because
11
+ * every command must be runnable by id, so the shape of the menu cannot live there.
12
+ * A command missing from this list would be executable and never offered; the test suite
13
+ * walks both and refuses to let the two drift.
14
+ */
15
+ const CONTEXT_MENU = [
16
+ 'undo', 'redo',
17
+ 'cut', 'copy', 'paste',
18
+ 'open',
19
+ 'delete',
20
+ { group: 'export', commands: ['exportDocument', 'exportPng', 'exportSvg'] },
21
+ 'overview',
22
+ 'properties',
23
+ 'help',
24
+ ];
25
+ /** Commands that toggle rather than do, so the menu reports their state instead of just enabling them. */
26
+ const CONTEXT_CHECKED = {
27
+ overview: (diagram) => diagram.getViewState().overviewVisible,
28
+ };
7
29
  export class StockSharpDiagram extends EventEmitter {
8
30
  constructor(options) {
9
31
  super();
@@ -16,8 +38,12 @@ export class StockSharpDiagram extends EventEmitter {
16
38
  this.destroyed = false;
17
39
  this.fullscreen = false;
18
40
  this.fullscreenButtonVisible = true;
19
- this.fullscreenLabels = { enter: 'Enter fullscreen', exit: 'Exit fullscreen' };
41
+ // null means the host never named the button, so its wording comes from the shared bundle.
42
+ // Keeping the two apart is what lets an explicit label win while an untouched control still
43
+ // follows the page's language instead of being stuck in English.
44
+ this.fullscreenLabels = null;
20
45
  this.contextActions = new DiagramActionRegistry();
46
+ this.contextMenu = null;
21
47
  this.div = options.div;
22
48
  this.catalog = options.catalog;
23
49
  this.overviewContainer = options.overviewContainer ?? null;
@@ -30,10 +56,11 @@ export class StockSharpDiagram extends EventEmitter {
30
56
  gridSize: options.gridSize,
31
57
  });
32
58
  this.fullscreenButtonVisible = options.showFullscreenButton ?? true;
33
- this.fullscreenLabels = options.fullscreenLabels ?? this.fullscreenLabels;
59
+ this.fullscreenLabels = options.fullscreenLabels ?? null;
34
60
  this.prepareFullscreenButtonHost();
35
61
  this.fullscreenButton = this.createFullscreenButton();
36
62
  this.updateFullscreenButton();
63
+ this.setContextMenuEnabled(options.showContextMenu ?? true);
37
64
  this.registerContextActions();
38
65
  this.bindCanvasEvents();
39
66
  this.disposables.push(this.catalog.on('portTypesChanged', () => this.applySocketTheme()));
@@ -250,6 +277,10 @@ export class StockSharpDiagram extends EventEmitter {
250
277
  takeScreenshot(options = {}) {
251
278
  return this.canvas.takeScreenshot(options);
252
279
  }
280
+ /** The same picture as takeScreenshot, as an SVG document. */
281
+ takeSvg(options = {}) {
282
+ return this.canvas.takeSvg(options);
283
+ }
253
284
  getSelection() {
254
285
  return this.canvas.getSelection();
255
286
  }
@@ -281,8 +312,22 @@ export class StockSharpDiagram extends EventEmitter {
281
312
  this.fullscreenLabels = labels;
282
313
  this.updateFullscreenButton();
283
314
  }
315
+ /**
316
+ * Re-reads the shared bundle for text the control renders itself. Call it after changing
317
+ * the page language: the context menu is rebuilt on every open and needs nothing, but the
318
+ * fullscreen button is drawn once and would otherwise keep the old wording.
319
+ */
320
+ refreshLabels() {
321
+ this.updateFullscreenButton();
322
+ }
284
323
  getFullscreenLabels() {
285
- return { ...this.fullscreenLabels };
324
+ return { ...this.resolveFullscreenLabels() };
325
+ }
326
+ resolveFullscreenLabels() {
327
+ return this.fullscreenLabels ?? {
328
+ enter: t('fullscreenEnter', 'Enter fullscreen'),
329
+ exit: t('fullscreenExit', 'Exit fullscreen'),
330
+ };
286
331
  }
287
332
  /** Updates only the control's state after the host changed its own layout. */
288
333
  setFullscreenState(fullscreen) {
@@ -366,11 +411,46 @@ export class StockSharpDiagram extends EventEmitter {
366
411
  const fallback = this.canvas.getClipboardDocument();
367
412
  return fallback !== null && this.canvas.pasteDocument(fallback).length > 0;
368
413
  }
414
+ /**
415
+ * Turns the built-in menu on or off. Off leaves `contextMenuRequested` untouched, so a
416
+ * host that draws its own is only trading one menu for another, never losing the event.
417
+ */
418
+ setContextMenuEnabled(enabled) {
419
+ if (enabled === (this.contextMenu !== null))
420
+ return;
421
+ if (!enabled) {
422
+ this.contextMenu?.destroy();
423
+ this.contextMenu = null;
424
+ return;
425
+ }
426
+ this.contextMenu = new ContextMenuView({
427
+ container: this.div,
428
+ // Through the public entry point, so the built-in menu is exactly as privileged
429
+ // as a host's own -- same guards, same contextCommand event.
430
+ execute: (command) => { this.executeContextCommand(command); },
431
+ });
432
+ }
433
+ isContextMenuEnabled() {
434
+ return this.contextMenu !== null;
435
+ }
369
436
  getContextCommands() {
370
- return this.contextActions.states(this.contextActionContext()).map(({ id, enabled }) => ({
371
- command: id,
372
- enabled,
373
- }));
437
+ const context = this.contextActionContext();
438
+ const state = (command) => {
439
+ const checked = CONTEXT_CHECKED[command];
440
+ return {
441
+ command,
442
+ enabled: this.contextActions.canExecute(command, context),
443
+ ...(checked === undefined ? {} : { checked: checked(this) }),
444
+ };
445
+ };
446
+ return CONTEXT_MENU.map((entry) => {
447
+ if (typeof entry === 'string')
448
+ return state(entry);
449
+ const commands = entry.commands.map(state);
450
+ // A submenu that opens onto nothing but greyed items is itself dead, and
451
+ // saying so here spares every host from working the same thing out again.
452
+ return { group: entry.group, enabled: commands.some(({ enabled }) => enabled), commands };
453
+ });
374
454
  }
375
455
  executeContextCommand(command) {
376
456
  const context = this.contextActionContext();
@@ -444,6 +524,8 @@ export class StockSharpDiagram extends EventEmitter {
444
524
  if (this.destroyed)
445
525
  return;
446
526
  this.destroyed = true;
527
+ this.contextMenu?.destroy();
528
+ this.contextMenu = null;
447
529
  for (const dispose of this.disposables.splice(0).reverse())
448
530
  dispose();
449
531
  this.canvas.destroy();
@@ -501,7 +583,8 @@ export class StockSharpDiagram extends EventEmitter {
501
583
  }
502
584
  updateFullscreenButton() {
503
585
  const fullscreen = this.fullscreen;
504
- const label = fullscreen ? this.fullscreenLabels.exit : this.fullscreenLabels.enter;
586
+ const labels = this.resolveFullscreenLabels();
587
+ const label = fullscreen ? labels.exit : labels.enter;
505
588
  const path = fullscreen
506
589
  ? 'M4 9h5V4M20 9h-5V4M4 15h5v5M20 15h-5v5'
507
590
  : 'M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5';
@@ -595,6 +678,7 @@ export class StockSharpDiagram extends EventEmitter {
595
678
  this.overviewContainer?.classList.toggle('hidden', !state.overviewVisible);
596
679
  this.emit('viewChanged', state);
597
680
  }), this.canvas.on('undoStackChanged', (state) => this.emit('undoStackChanged', state)), this.canvas.on('contextMenu', ({ x, y, node, link, port }) => {
681
+ const commands = this.getContextCommands();
598
682
  this.emit('contextMenuRequested', {
599
683
  x,
600
684
  y,
@@ -602,8 +686,11 @@ export class StockSharpDiagram extends EventEmitter {
602
686
  link: link === null ? null : this.fromCanvasLink(link),
603
687
  port: port === null ? null : this.fromCanvasPort(port.port),
604
688
  portDirection: port?.port.direction ?? null,
605
- commands: this.getContextCommands(),
689
+ commands,
606
690
  });
691
+ // After the event, so a host that draws its own menu has already been told
692
+ // and can have switched this one off in the handler.
693
+ this.contextMenu?.show(x, y, commands);
607
694
  }));
608
695
  }
609
696
  registerContextActions() {
@@ -641,6 +728,30 @@ export class StockSharpDiagram extends EventEmitter {
641
728
  && (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
642
729
  execute: () => this.canvas.deleteSelection(),
643
730
  },
731
+ // Export asks rather than does. Where the result goes -- a download, an upload,
732
+ // a clipboard, a print preview -- is the host's decision, and so is which options
733
+ // to render with, so the control reports the request and stops there.
734
+ // Availability follows the diagram, not the selection: all three export the whole
735
+ // picture, and an empty diagram has none to give. They stay on in read-only mode,
736
+ // which forbids changing the diagram, not looking at it.
737
+ exportDocument: {
738
+ canExecute: ({ document }) => document.nodes.length > 0,
739
+ execute: () => this.emit('exportRequested', { format: 'document' }),
740
+ },
741
+ exportPng: {
742
+ canExecute: ({ document }) => document.nodes.length > 0,
743
+ execute: () => this.emit('exportRequested', { format: 'png' }),
744
+ },
745
+ exportSvg: {
746
+ canExecute: ({ document }) => document.nodes.length > 0,
747
+ execute: () => this.emit('exportRequested', { format: 'svg' }),
748
+ },
749
+ // Always available: it shows the overview as readily as it hides it, and an empty
750
+ // diagram is exactly when someone may want the panel out of the way.
751
+ overview: {
752
+ canExecute: () => true,
753
+ execute: () => this.setOverviewVisible(!this.getViewState().overviewVisible),
754
+ },
644
755
  properties: {
645
756
  canExecute: ({ nodes }) => nodes.length > 0,
646
757
  execute: ({ nodes }) => this.emit('nodeProperties', { nodes }),
@@ -658,6 +769,7 @@ export class StockSharpDiagram extends EventEmitter {
658
769
  const linkIds = new Set(selection.linkIds);
659
770
  return {
660
771
  selection,
772
+ document,
661
773
  nodes: document.nodes
662
774
  .filter((node) => nodeIds.has(node.id))
663
775
  .map((node) => this.fromCanvasInit(node)),