@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.
- package/README.md +105 -2
- package/dist/esm/canvas-renderer.js +116 -36
- package/dist/esm/canvas-renderer.js.map +1 -1
- package/dist/esm/diagram/context-menu.js +319 -0
- package/dist/esm/diagram/context-menu.js.map +1 -0
- package/dist/esm/diagram/stocksharp-diagram.js +121 -9
- package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
- package/dist/esm/draw-surface.js +2 -0
- package/dist/esm/draw-surface.js.map +1 -0
- package/dist/esm/embed.js +7 -4
- package/dist/esm/embed.js.map +1 -1
- package/dist/esm/i18n.js +15 -4
- package/dist/esm/i18n.js.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/svg-surface.js +341 -0
- package/dist/esm/svg-surface.js.map +1 -0
- package/dist/ssdiagram.js +978 -188
- package/dist/ssdiagram.js.map +4 -4
- package/dist/types/canvas-renderer.d.ts +13 -0
- package/dist/types/canvas-renderer.d.ts.map +1 -1
- package/dist/types/diagram/api.d.ts +42 -2
- package/dist/types/diagram/api.d.ts.map +1 -1
- package/dist/types/diagram/context-menu.d.ts +33 -0
- package/dist/types/diagram/context-menu.d.ts.map +1 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts +18 -5
- package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
- package/dist/types/draw-surface.d.ts +52 -0
- package/dist/types/draw-surface.d.ts.map +1 -0
- package/dist/types/embed.d.ts.map +1 -1
- package/dist/types/i18n.d.ts +12 -0
- package/dist/types/i18n.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/svg-surface.d.ts +74 -0
- package/dist/types/svg-surface.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/canvas-renderer.ts +120 -27
- package/src/diagram/api.ts +53 -2
- package/src/diagram/context-menu.ts +363 -0
- package/src/diagram/stocksharp-diagram.ts +134 -10
- package/src/draw-surface.ts +61 -0
- package/src/embed.ts +7 -4
- package/src/i18n.ts +41 -6
- package/src/index.ts +10 -0
- package/src/svg-surface.ts +430 -0
|
@@ -27,11 +27,16 @@ import {
|
|
|
27
27
|
type PortModel,
|
|
28
28
|
} from '../canvas-renderer.js';
|
|
29
29
|
import { StockSharpCatalog } from './catalog.js';
|
|
30
|
+
import { ContextMenuView } from './context-menu.js';
|
|
31
|
+
import { t } from '../i18n.js';
|
|
30
32
|
import { EventEmitter } from './event-emitter.js';
|
|
31
33
|
import type {
|
|
32
34
|
DiagramEvents,
|
|
33
35
|
DiagramClipboard,
|
|
34
36
|
ContextCommand,
|
|
37
|
+
ContextCommandGroup,
|
|
38
|
+
ContextCommandState,
|
|
39
|
+
ContextMenuItemState,
|
|
35
40
|
DiagramLoadOptions,
|
|
36
41
|
DiagramFullscreenLabels,
|
|
37
42
|
DiagramGridSettings,
|
|
@@ -58,8 +63,34 @@ interface ContextActionContext {
|
|
|
58
63
|
selection: DiagramSelection;
|
|
59
64
|
nodes: DiagramNode[];
|
|
60
65
|
links: Link[];
|
|
66
|
+
/** The whole diagram, not the selection: export offers the picture as a whole. */
|
|
67
|
+
document: DiagramDocument;
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
type ContextMenuEntry = ContextCommand | { group: ContextCommandGroup; commands: readonly ContextCommand[] };
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Menu order, and the only place the nesting is declared. The registry is flat because
|
|
74
|
+
* every command must be runnable by id, so the shape of the menu cannot live there.
|
|
75
|
+
* A command missing from this list would be executable and never offered; the test suite
|
|
76
|
+
* walks both and refuses to let the two drift.
|
|
77
|
+
*/
|
|
78
|
+
const CONTEXT_MENU: readonly ContextMenuEntry[] = [
|
|
79
|
+
'undo', 'redo',
|
|
80
|
+
'cut', 'copy', 'paste',
|
|
81
|
+
'open',
|
|
82
|
+
'delete',
|
|
83
|
+
{ group: 'export', commands: ['exportDocument', 'exportPng', 'exportSvg'] },
|
|
84
|
+
'overview',
|
|
85
|
+
'properties',
|
|
86
|
+
'help',
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/** Commands that toggle rather than do, so the menu reports their state instead of just enabling them. */
|
|
90
|
+
const CONTEXT_CHECKED: Partial<Record<ContextCommand, (diagram: StockSharpDiagram) => boolean>> = {
|
|
91
|
+
overview: (diagram) => diagram.getViewState().overviewVisible,
|
|
92
|
+
};
|
|
93
|
+
|
|
63
94
|
export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
64
95
|
private readonly div: HTMLElement;
|
|
65
96
|
private readonly catalog: StockSharpCatalog;
|
|
@@ -77,8 +108,12 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
77
108
|
private destroyed = false;
|
|
78
109
|
private fullscreen = false;
|
|
79
110
|
private fullscreenButtonVisible = true;
|
|
80
|
-
|
|
111
|
+
// null means the host never named the button, so its wording comes from the shared bundle.
|
|
112
|
+
// Keeping the two apart is what lets an explicit label win while an untouched control still
|
|
113
|
+
// follows the page's language instead of being stuck in English.
|
|
114
|
+
private fullscreenLabels: DiagramFullscreenLabels | null = null;
|
|
81
115
|
private readonly contextActions = new DiagramActionRegistry<ContextCommand, ContextActionContext>();
|
|
116
|
+
private contextMenu: ContextMenuView | null = null;
|
|
82
117
|
|
|
83
118
|
constructor(options: DiagramOptions) {
|
|
84
119
|
super();
|
|
@@ -94,10 +129,11 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
94
129
|
gridSize: options.gridSize,
|
|
95
130
|
});
|
|
96
131
|
this.fullscreenButtonVisible = options.showFullscreenButton ?? true;
|
|
97
|
-
this.fullscreenLabels = options.fullscreenLabels ??
|
|
132
|
+
this.fullscreenLabels = options.fullscreenLabels ?? null;
|
|
98
133
|
this.prepareFullscreenButtonHost();
|
|
99
134
|
this.fullscreenButton = this.createFullscreenButton();
|
|
100
135
|
this.updateFullscreenButton();
|
|
136
|
+
this.setContextMenuEnabled(options.showContextMenu ?? true);
|
|
101
137
|
this.registerContextActions();
|
|
102
138
|
this.bindCanvasEvents();
|
|
103
139
|
this.disposables.push(this.catalog.on('portTypesChanged', () => this.applySocketTheme()));
|
|
@@ -372,6 +408,11 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
372
408
|
return this.canvas.takeScreenshot(options);
|
|
373
409
|
}
|
|
374
410
|
|
|
411
|
+
/** The same picture as takeScreenshot, as an SVG document. */
|
|
412
|
+
takeSvg(options: DiagramScreenshotOptions = {}): string {
|
|
413
|
+
return this.canvas.takeSvg(options);
|
|
414
|
+
}
|
|
415
|
+
|
|
375
416
|
getSelection(): DiagramSnapshot<DiagramSelection> {
|
|
376
417
|
return this.canvas.getSelection();
|
|
377
418
|
}
|
|
@@ -412,8 +453,24 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
412
453
|
this.updateFullscreenButton();
|
|
413
454
|
}
|
|
414
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Re-reads the shared bundle for text the control renders itself. Call it after changing
|
|
458
|
+
* the page language: the context menu is rebuilt on every open and needs nothing, but the
|
|
459
|
+
* fullscreen button is drawn once and would otherwise keep the old wording.
|
|
460
|
+
*/
|
|
461
|
+
refreshLabels(): void {
|
|
462
|
+
this.updateFullscreenButton();
|
|
463
|
+
}
|
|
464
|
+
|
|
415
465
|
getFullscreenLabels(): DiagramSnapshot<DiagramFullscreenLabels> {
|
|
416
|
-
return { ...this.
|
|
466
|
+
return { ...this.resolveFullscreenLabels() };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private resolveFullscreenLabels(): DiagramFullscreenLabels {
|
|
470
|
+
return this.fullscreenLabels ?? {
|
|
471
|
+
enter: t('fullscreenEnter', 'Enter fullscreen'),
|
|
472
|
+
exit: t('fullscreenExit', 'Exit fullscreen'),
|
|
473
|
+
};
|
|
417
474
|
}
|
|
418
475
|
|
|
419
476
|
/** Updates only the control's state after the host changed its own layout. */
|
|
@@ -509,11 +566,46 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
509
566
|
return fallback !== null && this.canvas.pasteDocument(fallback).length > 0;
|
|
510
567
|
}
|
|
511
568
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
569
|
+
/**
|
|
570
|
+
* Turns the built-in menu on or off. Off leaves `contextMenuRequested` untouched, so a
|
|
571
|
+
* host that draws its own is only trading one menu for another, never losing the event.
|
|
572
|
+
*/
|
|
573
|
+
setContextMenuEnabled(enabled: boolean): void {
|
|
574
|
+
if (enabled === (this.contextMenu !== null)) return;
|
|
575
|
+
if (!enabled) {
|
|
576
|
+
this.contextMenu?.destroy();
|
|
577
|
+
this.contextMenu = null;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
this.contextMenu = new ContextMenuView({
|
|
581
|
+
container: this.div,
|
|
582
|
+
// Through the public entry point, so the built-in menu is exactly as privileged
|
|
583
|
+
// as a host's own -- same guards, same contextCommand event.
|
|
584
|
+
execute: (command) => { this.executeContextCommand(command); },
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
isContextMenuEnabled(): boolean {
|
|
589
|
+
return this.contextMenu !== null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
getContextCommands(): ContextMenuItemState[] {
|
|
593
|
+
const context = this.contextActionContext();
|
|
594
|
+
const state = (command: ContextCommand): ContextCommandState => {
|
|
595
|
+
const checked = CONTEXT_CHECKED[command];
|
|
596
|
+
return {
|
|
597
|
+
command,
|
|
598
|
+
enabled: this.contextActions.canExecute(command, context),
|
|
599
|
+
...(checked === undefined ? {} : { checked: checked(this) }),
|
|
600
|
+
};
|
|
601
|
+
};
|
|
602
|
+
return CONTEXT_MENU.map((entry) => {
|
|
603
|
+
if (typeof entry === 'string') return state(entry);
|
|
604
|
+
const commands = entry.commands.map(state);
|
|
605
|
+
// A submenu that opens onto nothing but greyed items is itself dead, and
|
|
606
|
+
// saying so here spares every host from working the same thing out again.
|
|
607
|
+
return { group: entry.group, enabled: commands.some(({ enabled }) => enabled), commands };
|
|
608
|
+
});
|
|
517
609
|
}
|
|
518
610
|
|
|
519
611
|
executeContextCommand(command: ContextCommand): boolean {
|
|
@@ -588,6 +680,8 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
588
680
|
destroy(): void {
|
|
589
681
|
if (this.destroyed) return;
|
|
590
682
|
this.destroyed = true;
|
|
683
|
+
this.contextMenu?.destroy();
|
|
684
|
+
this.contextMenu = null;
|
|
591
685
|
for (const dispose of this.disposables.splice(0).reverse()) dispose();
|
|
592
686
|
this.canvas.destroy();
|
|
593
687
|
}
|
|
@@ -645,7 +739,8 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
645
739
|
|
|
646
740
|
private updateFullscreenButton(): void {
|
|
647
741
|
const fullscreen = this.fullscreen;
|
|
648
|
-
const
|
|
742
|
+
const labels = this.resolveFullscreenLabels();
|
|
743
|
+
const label = fullscreen ? labels.exit : labels.enter;
|
|
649
744
|
const path = fullscreen
|
|
650
745
|
? 'M4 9h5V4M20 9h-5V4M4 15h5v5M20 15h-5v5'
|
|
651
746
|
: 'M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5';
|
|
@@ -752,6 +847,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
752
847
|
}),
|
|
753
848
|
this.canvas.on('undoStackChanged', (state) => this.emit('undoStackChanged', state)),
|
|
754
849
|
this.canvas.on('contextMenu', ({ x, y, node, link, port }) => {
|
|
850
|
+
const commands = this.getContextCommands();
|
|
755
851
|
this.emit('contextMenuRequested', {
|
|
756
852
|
x,
|
|
757
853
|
y,
|
|
@@ -759,8 +855,11 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
759
855
|
link: link === null ? null : this.fromCanvasLink(link),
|
|
760
856
|
port: port === null ? null : this.fromCanvasPort(port.port),
|
|
761
857
|
portDirection: port?.port.direction ?? null,
|
|
762
|
-
commands
|
|
858
|
+
commands,
|
|
763
859
|
});
|
|
860
|
+
// After the event, so a host that draws its own menu has already been told
|
|
861
|
+
// and can have switched this one off in the handler.
|
|
862
|
+
this.contextMenu?.show(x, y, commands);
|
|
764
863
|
}),
|
|
765
864
|
);
|
|
766
865
|
}
|
|
@@ -800,6 +899,30 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
800
899
|
&& (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
|
|
801
900
|
execute: () => this.canvas.deleteSelection(),
|
|
802
901
|
},
|
|
902
|
+
// Export asks rather than does. Where the result goes -- a download, an upload,
|
|
903
|
+
// a clipboard, a print preview -- is the host's decision, and so is which options
|
|
904
|
+
// to render with, so the control reports the request and stops there.
|
|
905
|
+
// Availability follows the diagram, not the selection: all three export the whole
|
|
906
|
+
// picture, and an empty diagram has none to give. They stay on in read-only mode,
|
|
907
|
+
// which forbids changing the diagram, not looking at it.
|
|
908
|
+
exportDocument: {
|
|
909
|
+
canExecute: ({ document }) => document.nodes.length > 0,
|
|
910
|
+
execute: () => this.emit('exportRequested', { format: 'document' }),
|
|
911
|
+
},
|
|
912
|
+
exportPng: {
|
|
913
|
+
canExecute: ({ document }) => document.nodes.length > 0,
|
|
914
|
+
execute: () => this.emit('exportRequested', { format: 'png' }),
|
|
915
|
+
},
|
|
916
|
+
exportSvg: {
|
|
917
|
+
canExecute: ({ document }) => document.nodes.length > 0,
|
|
918
|
+
execute: () => this.emit('exportRequested', { format: 'svg' }),
|
|
919
|
+
},
|
|
920
|
+
// Always available: it shows the overview as readily as it hides it, and an empty
|
|
921
|
+
// diagram is exactly when someone may want the panel out of the way.
|
|
922
|
+
overview: {
|
|
923
|
+
canExecute: () => true,
|
|
924
|
+
execute: () => this.setOverviewVisible(!this.getViewState().overviewVisible),
|
|
925
|
+
},
|
|
803
926
|
properties: {
|
|
804
927
|
canExecute: ({ nodes }) => nodes.length > 0,
|
|
805
928
|
execute: ({ nodes }) => this.emit('nodeProperties', { nodes }),
|
|
@@ -818,6 +941,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
|
|
|
818
941
|
const linkIds = new Set(selection.linkIds);
|
|
819
942
|
return {
|
|
820
943
|
selection,
|
|
944
|
+
document,
|
|
821
945
|
nodes: document.nodes
|
|
822
946
|
.filter((node) => nodeIds.has(node.id))
|
|
823
947
|
.map((node) => this.fromCanvasInit(node)),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drawing operations the renderer actually uses, and nothing more.
|
|
3
|
+
*
|
|
4
|
+
* The renderer paints straight onto a 2D context, which means the picture only ever exists as
|
|
5
|
+
* pixels: a screenshot can be taken, but there is no vector form to export. Naming the surface it
|
|
6
|
+
* draws against is what makes a second backend possible without a second renderer -- one that emits
|
|
7
|
+
* SVG. Two renderers would be two truths about how a node looks, and they would drift.
|
|
8
|
+
*
|
|
9
|
+
* The member list is deliberately a subset of CanvasRenderingContext2D with identical names and
|
|
10
|
+
* semantics. TypeScript matches types structurally, so the browser's own context satisfies this
|
|
11
|
+
* without a wrapper: the on-screen path keeps calling the real thing directly, at no cost and with
|
|
12
|
+
* no behaviour to re-verify.
|
|
13
|
+
*/
|
|
14
|
+
export interface DrawSurface {
|
|
15
|
+
// ----- state -----
|
|
16
|
+
fillStyle: string | CanvasGradient | CanvasPattern;
|
|
17
|
+
strokeStyle: string | CanvasGradient | CanvasPattern;
|
|
18
|
+
lineWidth: number;
|
|
19
|
+
lineJoin: CanvasLineJoin;
|
|
20
|
+
lineCap: CanvasLineCap;
|
|
21
|
+
globalAlpha: number;
|
|
22
|
+
font: string;
|
|
23
|
+
textAlign: CanvasTextAlign;
|
|
24
|
+
textBaseline: CanvasTextBaseline;
|
|
25
|
+
|
|
26
|
+
save(): void;
|
|
27
|
+
restore(): void;
|
|
28
|
+
setLineDash(segments: readonly number[]): void;
|
|
29
|
+
|
|
30
|
+
// ----- transform -----
|
|
31
|
+
setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void;
|
|
32
|
+
translate(x: number, y: number): void;
|
|
33
|
+
rotate(angle: number): void;
|
|
34
|
+
|
|
35
|
+
// ----- paths -----
|
|
36
|
+
beginPath(): void;
|
|
37
|
+
closePath(): void;
|
|
38
|
+
moveTo(x: number, y: number): void;
|
|
39
|
+
lineTo(x: number, y: number): void;
|
|
40
|
+
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;
|
|
41
|
+
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void;
|
|
42
|
+
fill(): void;
|
|
43
|
+
stroke(): void;
|
|
44
|
+
clip(): void;
|
|
45
|
+
|
|
46
|
+
// ----- rectangles -----
|
|
47
|
+
clearRect(x: number, y: number, w: number, h: number): void;
|
|
48
|
+
fillRect(x: number, y: number, w: number, h: number): void;
|
|
49
|
+
strokeRect(x: number, y: number, w: number, h: number): void;
|
|
50
|
+
|
|
51
|
+
// ----- text -----
|
|
52
|
+
fillText(text: string, x: number, y: number, maxWidth?: number): void;
|
|
53
|
+
/**
|
|
54
|
+
* Only `width` is read by the renderer. An SVG backend has no font engine of its own and
|
|
55
|
+
* borrows a real 2D context for this, so text lands in exactly the same place in both outputs.
|
|
56
|
+
*/
|
|
57
|
+
measureText(text: string): { width: number };
|
|
58
|
+
|
|
59
|
+
// ----- images -----
|
|
60
|
+
drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
|
|
61
|
+
}
|
package/src/embed.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t } from './i18n.js';
|
|
1
2
|
// Shared read-only diagram embed layer. Rendering, palette loading and theming
|
|
2
3
|
// live next to the diagram engine so web applications do not copy that logic.
|
|
3
4
|
//
|
|
@@ -407,7 +408,7 @@ async function renderSchemeAtRevision(
|
|
|
407
408
|
diagram.setLinkValidator(() => true);
|
|
408
409
|
diagram.setOverviewVisible(false);
|
|
409
410
|
const missingTypeTemplate = div.dataset.diagramMissingElement
|
|
410
|
-
?? 'Element type "{typeId}" is missing from the palette.';
|
|
411
|
+
?? t('embedMissingElement', 'Element type "{typeId}" is missing from the palette.');
|
|
411
412
|
const converted = toDiagramNodes(
|
|
412
413
|
scheme,
|
|
413
414
|
catalog,
|
|
@@ -508,10 +509,12 @@ interface EmbedErrorTexts {
|
|
|
508
509
|
// default would never fire and the note would render blank.
|
|
509
510
|
function errorTexts(div: HTMLElement): EmbedErrorTexts {
|
|
510
511
|
const parts = (div.dataset.diagramErrors ?? '').split('|');
|
|
512
|
+
// The attribute stays the override; without it the wording comes from the shared bundle, so
|
|
513
|
+
// a page that already translated the menu does not have to repeat itself per embed.
|
|
511
514
|
return {
|
|
512
|
-
load: parts[0] || 'Diagram source could not be loaded.',
|
|
513
|
-
empty: parts[1] || 'Diagram is empty or malformed.',
|
|
514
|
-
draw: parts[2] || 'Diagram could not be rendered.',
|
|
515
|
+
load: parts[0] || t('embedErrorLoad', 'Diagram source could not be loaded.'),
|
|
516
|
+
empty: parts[1] || t('embedErrorEmpty', 'Diagram is empty or malformed.'),
|
|
517
|
+
draw: parts[2] || t('embedErrorDraw', 'Diagram could not be rendered.'),
|
|
515
518
|
};
|
|
516
519
|
}
|
|
517
520
|
|
package/src/i18n.ts
CHANGED
|
@@ -95,8 +95,35 @@ export interface DesignerI18n {
|
|
|
95
95
|
copy?: string;
|
|
96
96
|
paste?: string;
|
|
97
97
|
ctxOpen?: string;
|
|
98
|
+
// Deletes the selected nodes and links. Deliberately not the solution explorer's
|
|
99
|
+
// `delete`, which removes a whole strategy: same English word, but the object differs,
|
|
100
|
+
// and a language that declines it -- or a host that writes "Delete strategy..." -- cannot
|
|
101
|
+
// serve both from one key.
|
|
102
|
+
ctxDelete?: string;
|
|
103
|
+
// Submenu title, and deliberately not `ctxExport` -- that one is the solution
|
|
104
|
+
// explorer's "export this strategy", a different action that a host may well word
|
|
105
|
+
// differently. Reads as its own item too: "Export as > SVG image".
|
|
106
|
+
ctxExportAs?: string;
|
|
107
|
+
ctxExportDocument?: string;
|
|
108
|
+
ctxExportPng?: string;
|
|
109
|
+
ctxExportSvg?: string;
|
|
110
|
+
ctxOverview?: string;
|
|
98
111
|
properties?: string;
|
|
99
112
|
ctxHelp?: string;
|
|
113
|
+
|
|
114
|
+
// Everything else the control renders by itself. Each of these also has a per-instance
|
|
115
|
+
// override -- the fullscreenLabels option, or a data-diagram-* attribute on an embedded
|
|
116
|
+
// host -- and that override wins; these are the defaults when none was given.
|
|
117
|
+
fullscreenEnter?: string;
|
|
118
|
+
fullscreenExit?: string;
|
|
119
|
+
embedErrorLoad?: string;
|
|
120
|
+
embedErrorEmpty?: string;
|
|
121
|
+
embedErrorDraw?: string;
|
|
122
|
+
embedMissingElement?: string;
|
|
123
|
+
|
|
124
|
+
// Not the diagram menu: it has no collapse/expand command. Kept for hosts already
|
|
125
|
+
// shipping them, but moved out of the block above so nobody translates them expecting
|
|
126
|
+
// to see them in the context menu.
|
|
100
127
|
collapse?: string;
|
|
101
128
|
expand?: string;
|
|
102
129
|
|
|
@@ -198,13 +225,21 @@ export interface DesignerI18n {
|
|
|
198
225
|
opterr_plotly_missing?: string;
|
|
199
226
|
}
|
|
200
227
|
|
|
201
|
-
const i18n: DesignerI18n =
|
|
202
|
-
typeof window === 'undefined'
|
|
203
|
-
? {}
|
|
204
|
-
: (window as unknown as { __designerI18n?: DesignerI18n }).__designerI18n ?? {};
|
|
205
|
-
|
|
206
228
|
/// Localized string for `key`, or `fallback` (English) when the host
|
|
207
229
|
/// didn't provide it.
|
|
230
|
+
///
|
|
231
|
+
/// The bundle is read on every call rather than captured when this module loads. Capturing
|
|
232
|
+
/// it meant the host had to assign `window.__designerI18n` before the bundle evaluated --
|
|
233
|
+
/// the opposite of the usual order, and impossible for a lazily loaded chunk -- and a host
|
|
234
|
+
/// that assigned it afterwards silently got English with nothing to explain why. Reading it
|
|
235
|
+
/// live also makes assigning and mutating the object behave the same, and lets the language
|
|
236
|
+
/// change mid-session: the only thing reading these keys is the context menu, and it is
|
|
237
|
+
/// built fresh on every right-click.
|
|
238
|
+
///
|
|
239
|
+
/// An empty string counts as missing. Half-finished translation exports routinely emit one,
|
|
240
|
+
/// and a blank menu entry is worse than an English one.
|
|
208
241
|
export function t(key: keyof DesignerI18n, fallback: string): string {
|
|
209
|
-
|
|
242
|
+
if (typeof window === 'undefined') return fallback;
|
|
243
|
+
const value = (window as unknown as { __designerI18n?: DesignerI18n }).__designerI18n?.[key];
|
|
244
|
+
return value === undefined || value === '' ? fallback : value;
|
|
210
245
|
}
|
package/src/index.ts
CHANGED
|
@@ -85,11 +85,21 @@ export {
|
|
|
85
85
|
StockSharpDiagram,
|
|
86
86
|
} from './diagram/stocksharp-diagram.js';
|
|
87
87
|
|
|
88
|
+
// The menu resolves its labels through this, so a host needs the type to write a bundle the
|
|
89
|
+
// compiler can check. Without it the key names get hand-copied, and a typo shows up only as an
|
|
90
|
+
// English label on screen.
|
|
91
|
+
export type { DesignerI18n } from './i18n.js';
|
|
92
|
+
|
|
88
93
|
export type {
|
|
89
94
|
ContextCommand,
|
|
95
|
+
ContextCommandGroup,
|
|
96
|
+
ContextCommandGroupState,
|
|
90
97
|
ContextCommandPayload,
|
|
91
98
|
ContextCommandState,
|
|
99
|
+
ContextMenuItemState,
|
|
92
100
|
ContextMenuRequestedPayload,
|
|
101
|
+
ExportFormat,
|
|
102
|
+
ExportRequestedPayload,
|
|
93
103
|
DiagramEvents,
|
|
94
104
|
DiagramClipboard,
|
|
95
105
|
DiagramGridSettings,
|