@stocksharp/diagram 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 (87) hide show
  1. package/LICENSE +30 -0
  2. package/NOTICE +21 -0
  3. package/README.md +346 -0
  4. package/dist/esm/canvas-renderer.js +3226 -0
  5. package/dist/esm/canvas-renderer.js.map +1 -0
  6. package/dist/esm/core/action-registry.js +35 -0
  7. package/dist/esm/core/action-registry.js.map +1 -0
  8. package/dist/esm/core/document.js +350 -0
  9. package/dist/esm/core/document.js.map +1 -0
  10. package/dist/esm/core/history.js +136 -0
  11. package/dist/esm/core/history.js.map +1 -0
  12. package/dist/esm/core/model.js +2 -0
  13. package/dist/esm/core/model.js.map +1 -0
  14. package/dist/esm/core/state.js +73 -0
  15. package/dist/esm/core/state.js.map +1 -0
  16. package/dist/esm/core/view-state.js +70 -0
  17. package/dist/esm/core/view-state.js.map +1 -0
  18. package/dist/esm/diagram/api.js +2 -0
  19. package/dist/esm/diagram/api.js.map +1 -0
  20. package/dist/esm/diagram/catalog.js +45 -0
  21. package/dist/esm/diagram/catalog.js.map +1 -0
  22. package/dist/esm/diagram/event-emitter.js +42 -0
  23. package/dist/esm/diagram/event-emitter.js.map +1 -0
  24. package/dist/esm/diagram/palette.js +269 -0
  25. package/dist/esm/diagram/palette.js.map +1 -0
  26. package/dist/esm/diagram/stocksharp-diagram.js +784 -0
  27. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -0
  28. package/dist/esm/diagram/types.js +105 -0
  29. package/dist/esm/diagram/types.js.map +1 -0
  30. package/dist/esm/embed.js +398 -0
  31. package/dist/esm/embed.js.map +1 -0
  32. package/dist/esm/i18n.js +12 -0
  33. package/dist/esm/i18n.js.map +1 -0
  34. package/dist/esm/index.js +12 -0
  35. package/dist/esm/index.js.map +1 -0
  36. package/dist/ssdiagram.js +5141 -0
  37. package/dist/ssdiagram.js.map +7 -0
  38. package/dist/types/canvas-renderer.d.ts +512 -0
  39. package/dist/types/canvas-renderer.d.ts.map +1 -0
  40. package/dist/types/core/action-registry.d.ts +18 -0
  41. package/dist/types/core/action-registry.d.ts.map +1 -0
  42. package/dist/types/core/document.d.ts +12 -0
  43. package/dist/types/core/document.d.ts.map +1 -0
  44. package/dist/types/core/history.d.ts +33 -0
  45. package/dist/types/core/history.d.ts.map +1 -0
  46. package/dist/types/core/model.d.ts +85 -0
  47. package/dist/types/core/model.d.ts.map +1 -0
  48. package/dist/types/core/state.d.ts +68 -0
  49. package/dist/types/core/state.d.ts.map +1 -0
  50. package/dist/types/core/view-state.d.ts +14 -0
  51. package/dist/types/core/view-state.d.ts.map +1 -0
  52. package/dist/types/diagram/api.d.ts +213 -0
  53. package/dist/types/diagram/api.d.ts.map +1 -0
  54. package/dist/types/diagram/catalog.d.ts +19 -0
  55. package/dist/types/diagram/catalog.d.ts.map +1 -0
  56. package/dist/types/diagram/event-emitter.d.ts +9 -0
  57. package/dist/types/diagram/event-emitter.d.ts.map +1 -0
  58. package/dist/types/diagram/palette.d.ts +63 -0
  59. package/dist/types/diagram/palette.d.ts.map +1 -0
  60. package/dist/types/diagram/stocksharp-diagram.d.ts +157 -0
  61. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -0
  62. package/dist/types/diagram/types.d.ts +171 -0
  63. package/dist/types/diagram/types.d.ts.map +1 -0
  64. package/dist/types/embed.d.ts +35 -0
  65. package/dist/types/embed.d.ts.map +1 -0
  66. package/dist/types/i18n.d.ts +167 -0
  67. package/dist/types/i18n.d.ts.map +1 -0
  68. package/dist/types/index.d.ts +22 -0
  69. package/dist/types/index.d.ts.map +1 -0
  70. package/package.json +126 -0
  71. package/sample.png +0 -0
  72. package/src/canvas-renderer.ts +3249 -0
  73. package/src/core/action-registry.ts +46 -0
  74. package/src/core/document.ts +387 -0
  75. package/src/core/history.ts +154 -0
  76. package/src/core/model.ts +99 -0
  77. package/src/core/state.ts +148 -0
  78. package/src/core/view-state.ts +82 -0
  79. package/src/diagram/api.ts +257 -0
  80. package/src/diagram/catalog.ts +55 -0
  81. package/src/diagram/event-emitter.ts +44 -0
  82. package/src/diagram/palette.ts +312 -0
  83. package/src/diagram/stocksharp-diagram.ts +945 -0
  84. package/src/diagram/types.ts +332 -0
  85. package/src/embed.ts +508 -0
  86. package/src/i18n.ts +210 -0
  87. package/src/index.ts +171 -0
@@ -0,0 +1,148 @@
1
+ export type DiagramPortDirection = 'in' | 'out';
2
+ export type DiagramNodeErrorKind = 'runtime' | 'load';
3
+ export type DiagramGlobalErrorKind = 'invalid' | 'load' | 'locked' | 'encrypted';
4
+
5
+ export interface DiagramErrorState<TKind extends string> {
6
+ kind: TKind;
7
+ message: string;
8
+ /** Incrementing this value requests a fresh renderer animation. */
9
+ pulse: number;
10
+ }
11
+
12
+ export interface DiagramPortRuntimeState {
13
+ active: boolean;
14
+ selected: boolean;
15
+ breakpoint: boolean;
16
+ breakpointActive: boolean;
17
+ value: string | null;
18
+ error: string | null;
19
+ }
20
+
21
+ export interface DiagramNodePortRuntimeState {
22
+ in: Record<string, DiagramPortRuntimeState>;
23
+ out: Record<string, DiagramPortRuntimeState>;
24
+ }
25
+
26
+ export interface DiagramNodeRuntimeState {
27
+ active: boolean;
28
+ error: DiagramErrorState<DiagramNodeErrorKind> | null;
29
+ ports: DiagramNodePortRuntimeState;
30
+ }
31
+
32
+ export interface DiagramRuntimeState {
33
+ activeNodeId: string | null;
34
+ nodes: Record<string, DiagramNodeRuntimeState>;
35
+ globalError: DiagramErrorState<DiagramGlobalErrorKind> | null;
36
+ }
37
+
38
+ export interface DiagramViewState {
39
+ zoom: number;
40
+ panX: number;
41
+ panY: number;
42
+ overviewVisible: boolean;
43
+ }
44
+
45
+ export interface DiagramSelectedPort {
46
+ nodeId: string;
47
+ portId: string;
48
+ direction: DiagramPortDirection;
49
+ }
50
+
51
+ export interface DiagramSelection {
52
+ nodeIds: string[];
53
+ linkIds: string[];
54
+ port: DiagramSelectedPort | null;
55
+ primaryNodeId: string | null;
56
+ primaryLinkId: string | null;
57
+ }
58
+
59
+ export interface DiagramInteractionPermissions {
60
+ select: boolean;
61
+ inspect: boolean;
62
+ copy: boolean;
63
+ moveNodes: boolean;
64
+ createLinks: boolean;
65
+ deleteSelection: boolean;
66
+ paste: boolean;
67
+ history: boolean;
68
+ }
69
+
70
+ export function createDiagramRuntimeState(): DiagramRuntimeState {
71
+ return { activeNodeId: null, nodes: {}, globalError: null };
72
+ }
73
+
74
+ export function cloneDiagramRuntimeState(state: DiagramRuntimeState): DiagramRuntimeState {
75
+ return {
76
+ activeNodeId: state.activeNodeId,
77
+ globalError: state.globalError === null ? null : { ...state.globalError },
78
+ nodes: Object.fromEntries(Object.entries(state.nodes).map(([nodeId, node]) => [nodeId, {
79
+ active: node.active,
80
+ error: node.error === null ? null : { ...node.error },
81
+ ports: {
82
+ in: Object.fromEntries(Object.entries(node.ports.in)
83
+ .map(([portId, port]) => [portId, { ...port }])),
84
+ out: Object.fromEntries(Object.entries(node.ports.out)
85
+ .map(([portId, port]) => [portId, { ...port }])),
86
+ },
87
+ }])),
88
+ };
89
+ }
90
+
91
+ export function createDiagramViewState(): DiagramViewState {
92
+ return { zoom: 1, panX: 0, panY: 0, overviewVisible: true };
93
+ }
94
+
95
+ export function createDiagramSelection(): DiagramSelection {
96
+ return {
97
+ nodeIds: [],
98
+ linkIds: [],
99
+ port: null,
100
+ primaryNodeId: null,
101
+ primaryLinkId: null,
102
+ };
103
+ }
104
+
105
+ export function createDiagramPortRuntimeState(): DiagramPortRuntimeState {
106
+ return {
107
+ active: false,
108
+ selected: false,
109
+ breakpoint: false,
110
+ breakpointActive: false,
111
+ value: null,
112
+ error: null,
113
+ };
114
+ }
115
+
116
+ export function createEditableDiagramPermissions(): DiagramInteractionPermissions {
117
+ return {
118
+ select: true,
119
+ inspect: true,
120
+ copy: true,
121
+ moveNodes: true,
122
+ createLinks: true,
123
+ deleteSelection: true,
124
+ paste: true,
125
+ history: true,
126
+ };
127
+ }
128
+
129
+ export function createReadOnlyDiagramPermissions(): DiagramInteractionPermissions {
130
+ return {
131
+ select: true,
132
+ inspect: true,
133
+ copy: true,
134
+ moveNodes: false,
135
+ createLinks: false,
136
+ deleteSelection: false,
137
+ paste: false,
138
+ history: false,
139
+ };
140
+ }
141
+
142
+ export function createDiagramNodeRuntimeState(): DiagramNodeRuntimeState {
143
+ return {
144
+ active: false,
145
+ error: null,
146
+ ports: { in: {}, out: {} },
147
+ };
148
+ }
@@ -0,0 +1,82 @@
1
+ import { createDiagramViewState, type DiagramViewState } from './state.js';
2
+
3
+ export const DIAGRAM_VIEW_STATE_VERSION = 1 as const;
4
+
5
+ export interface DiagramViewStateDocument {
6
+ version: typeof DIAGRAM_VIEW_STATE_VERSION;
7
+ view: DiagramViewState;
8
+ }
9
+
10
+ export class DiagramViewStateError extends Error {
11
+ constructor(message: string, readonly path: string = '$') {
12
+ super(`${path}: ${message}`);
13
+ this.name = 'DiagramViewStateError';
14
+ }
15
+ }
16
+
17
+ export function createDiagramViewStateDocument(
18
+ view: DiagramViewState = createDiagramViewState(),
19
+ ): DiagramViewStateDocument {
20
+ return {
21
+ version: DIAGRAM_VIEW_STATE_VERSION,
22
+ view: normalizeView(view),
23
+ };
24
+ }
25
+
26
+ export function parseDiagramViewState(source: string | unknown): DiagramViewState {
27
+ let value: unknown = source;
28
+ if (typeof source === 'string') {
29
+ try {
30
+ value = JSON.parse(source) as unknown;
31
+ } catch (error) {
32
+ const reason = error instanceof Error ? error.message : 'invalid JSON';
33
+ throw new DiagramViewStateError(reason);
34
+ }
35
+ }
36
+
37
+ const root = requireObject(value, '$');
38
+ const version = requireFiniteNumber(root.version, '$.version');
39
+ if (version !== DIAGRAM_VIEW_STATE_VERSION) {
40
+ throw new DiagramViewStateError(`unsupported view-state version ${version}`, '$.version');
41
+ }
42
+ return normalizeView(requireObject(root.view, '$.view'));
43
+ }
44
+
45
+ export function serializeDiagramViewState(view: DiagramViewState, space?: number): string {
46
+ return JSON.stringify(createDiagramViewStateDocument(view), null, space);
47
+ }
48
+
49
+ function normalizeView(value: unknown): DiagramViewState {
50
+ const view = requireObject(value, '$.view');
51
+ const zoom = requireFiniteNumber(view.zoom, '$.view.zoom');
52
+ if (zoom <= 0) throw new DiagramViewStateError('expected a positive number', '$.view.zoom');
53
+ return {
54
+ zoom,
55
+ panX: requireFiniteNumber(view.panX, '$.view.panX'),
56
+ panY: requireFiniteNumber(view.panY, '$.view.panY'),
57
+ overviewVisible: requireBoolean(view.overviewVisible, '$.view.overviewVisible'),
58
+ };
59
+ }
60
+
61
+ function requireObject(value: unknown, path: string): Record<string, unknown> {
62
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
63
+ throw new DiagramViewStateError('expected an object', path);
64
+ }
65
+ const prototype = Object.getPrototypeOf(value) as object | null;
66
+ if (prototype !== Object.prototype && prototype !== null) {
67
+ throw new DiagramViewStateError('expected a plain object', path);
68
+ }
69
+ return value as Record<string, unknown>;
70
+ }
71
+
72
+ function requireFiniteNumber(value: unknown, path: string): number {
73
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
74
+ throw new DiagramViewStateError('expected a finite number', path);
75
+ }
76
+ return value;
77
+ }
78
+
79
+ function requireBoolean(value: unknown, path: string): boolean {
80
+ if (typeof value !== 'boolean') throw new DiagramViewStateError('expected a boolean', path);
81
+ return value;
82
+ }
@@ -0,0 +1,257 @@
1
+ import type { DiagramDocument } from '../core/model.js';
2
+ import type {
3
+ DiagramRuntimeState,
4
+ DiagramSelection,
5
+ DiagramViewState,
6
+ } from '../core/state.js';
7
+ import type { DiagramNode, Link, Port, PortDirection } from './types.js';
8
+
9
+ export interface DiagramOptions {
10
+ div: HTMLElement;
11
+ catalog: import('./catalog.js').StockSharpCatalog;
12
+ /** Show the built-in top-right fullscreen request button. Defaults to true. */
13
+ showFullscreenButton?: boolean;
14
+ overviewContainer?: HTMLElement | null;
15
+ zoomLabel?: HTMLElement | null;
16
+ /** Optional system clipboard adapter. Pass null to force memory-only clipboard. */
17
+ clipboard?: DiagramClipboard | null;
18
+ /** Snap pointer-dragged nodes to the grid. Defaults to true. */
19
+ gridSnap?: boolean;
20
+ /** Positive world-space grid step. Defaults to 28. */
21
+ gridSize?: number;
22
+ }
23
+
24
+ export interface DiagramGridSettings {
25
+ enabled: boolean;
26
+ size: number;
27
+ }
28
+
29
+ export interface DiagramPoint {
30
+ x: number;
31
+ y: number;
32
+ }
33
+
34
+ export interface DiagramNodeBounds extends DiagramPoint {
35
+ width: number;
36
+ height: number;
37
+ }
38
+
39
+ export type DiagramScreenshotScope = 'viewport' | 'content';
40
+
41
+ export interface DiagramScreenshotOptions {
42
+ /** Current viewport or the complete graph bounds. Defaults to viewport. */
43
+ scope?: DiagramScreenshotScope;
44
+ /** World-to-CSS-pixel scale for content export. Defaults to 1. */
45
+ scale?: number;
46
+ /** CSS-pixel padding around content export. Defaults to 32. */
47
+ padding?: number;
48
+ /** Output pixel density. Defaults to the current device pixel ratio. */
49
+ pixelRatio?: number;
50
+ /** Optional export-only background override. */
51
+ background?: string;
52
+ /** Defaults to true. */
53
+ includeGrid?: boolean;
54
+ /** Defaults to the current value for viewport and false for content. */
55
+ includeOverview?: boolean;
56
+ /** Defaults to true for viewport and false for content. */
57
+ includeSelection?: boolean;
58
+ /** Include debugger and error state. Defaults to true. */
59
+ includeRuntimeState?: boolean;
60
+ }
61
+
62
+ export interface DiagramClipboard {
63
+ readText(): Promise<string>;
64
+ writeText(value: string): Promise<void>;
65
+ }
66
+
67
+ export interface DiagramThemeOptions {
68
+ diagramBackground?: string;
69
+ overviewBackground?: string;
70
+ gridColor?: string;
71
+ linkMaxLightness?: number;
72
+ overviewBorderColor?: string;
73
+ overviewViewportColor?: string;
74
+ overviewViewportFill?: string;
75
+ }
76
+
77
+ export type NodeErrorKind = 'runtime' | 'load';
78
+
79
+ export interface NodeErrorOptions {
80
+ kind?: NodeErrorKind;
81
+ animate?: boolean;
82
+ }
83
+
84
+ export interface DiagramLoadOptions {
85
+ /** Transient per-node errors discovered while restoring a scheme. */
86
+ nodeErrors?: Readonly<Record<string, string>>;
87
+ }
88
+
89
+ export interface DocumentLoadFailedPayload {
90
+ message: string;
91
+ error: Error;
92
+ }
93
+
94
+ export interface FullscreenChangedPayload {
95
+ fullscreen: boolean;
96
+ }
97
+
98
+ /** Requested host layout state. The component never enters fullscreen itself. */
99
+ export interface FullscreenRequestedPayload {
100
+ fullscreen: boolean;
101
+ }
102
+
103
+ export type ContextCommand = 'undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'open' | 'delete' | 'properties' | 'help';
104
+
105
+ export interface ContextCommandPayload {
106
+ command: ContextCommand;
107
+ nodes: DiagramNode[];
108
+ links: Link[];
109
+ }
110
+
111
+ export interface ContextCommandState {
112
+ command: ContextCommand;
113
+ enabled: boolean;
114
+ }
115
+
116
+ export interface ContextMenuRequestedPayload {
117
+ x: number;
118
+ y: number;
119
+ node: DiagramNode | null;
120
+ link: Link | null;
121
+ port: Port | null;
122
+ portDirection: PortDirection | null;
123
+ commands: ContextCommandState[];
124
+ }
125
+
126
+ export interface NodeSelectedPayload {
127
+ node: DiagramNode;
128
+ selected: boolean;
129
+ }
130
+
131
+ export interface NodeHoverPayload {
132
+ node: DiagramNode;
133
+ hovering: boolean;
134
+ }
135
+
136
+ export interface PortSelectedPayload {
137
+ node: DiagramNode;
138
+ port: Port;
139
+ direction: PortDirection;
140
+ }
141
+
142
+ export interface PortHoverPayload extends PortSelectedPayload {
143
+ hovering: boolean;
144
+ }
145
+
146
+ export type PortClickAction = 'leftClick' | 'rightClick';
147
+
148
+ export interface PortClickedPayload extends PortSelectedPayload {
149
+ action: PortClickAction;
150
+ ctrlKey: boolean;
151
+ shiftKey: boolean;
152
+ altKey: boolean;
153
+ metaKey: boolean;
154
+ }
155
+
156
+ export interface LinkSelectedPayload {
157
+ link: Link;
158
+ selected: boolean;
159
+ }
160
+
161
+ export interface LinkHoverPayload {
162
+ link: Link;
163
+ hovering: boolean;
164
+ }
165
+
166
+ export interface NodeMovedPayload {
167
+ node: DiagramNode;
168
+ }
169
+
170
+ export interface NodeChangePayload {
171
+ nodes: DiagramNode[];
172
+ }
173
+
174
+ export interface LinkChangePayload {
175
+ links: Link[];
176
+ }
177
+
178
+ export interface LinkRelinkedPayload {
179
+ link: Link;
180
+ previous: Link;
181
+ }
182
+
183
+ export interface LoadFinishedPayload {
184
+ nodes: DiagramNode[];
185
+ links: Link[];
186
+ }
187
+
188
+ export interface LinkValidationPayload {
189
+ fromNode: DiagramNode;
190
+ fromPort: Port;
191
+ toNode: DiagramNode;
192
+ toPort: Port;
193
+ allowed: boolean;
194
+ reason: LinkValidationReason;
195
+ }
196
+
197
+ export type LinkValidationReason =
198
+ | 'allowed'
199
+ | 'missing-link'
200
+ | 'missing-node'
201
+ | 'missing-port'
202
+ | 'same-node'
203
+ | 'invalid-direction'
204
+ | 'incompatible-type'
205
+ | 'duplicate-link'
206
+ | 'source-limit'
207
+ | 'target-limit'
208
+ | 'host-rejected';
209
+
210
+ export interface LinkValidationResult {
211
+ allowed: boolean;
212
+ reason: LinkValidationReason;
213
+ }
214
+
215
+ export interface LinkValidatorArgs {
216
+ fromNode: DiagramNode;
217
+ fromPort: Port;
218
+ toNode: DiagramNode;
219
+ toPort: Port;
220
+ }
221
+
222
+ export type LinkValidator = (args: LinkValidatorArgs) => boolean;
223
+
224
+ export interface DiagramEvents extends Record<string, unknown> {
225
+ nodeAdded: NodeChangePayload;
226
+ nodeRemoved: NodeChangePayload;
227
+ linkAdded: LinkChangePayload;
228
+ linkRemoved: LinkChangePayload;
229
+ linkRelinked: LinkRelinkedPayload;
230
+ nodeMoved: NodeMovedPayload;
231
+ nodeSelected: NodeSelectedPayload;
232
+ nodeHover: NodeHoverPayload;
233
+ portSelected: PortSelectedPayload;
234
+ portClicked: PortClickedPayload;
235
+ portHover: PortHoverPayload;
236
+ linkSelected: LinkSelectedPayload;
237
+ linkHover: LinkHoverPayload;
238
+ linkValidation: LinkValidationPayload;
239
+ loadFinished: LoadFinishedPayload;
240
+ contextCommand: ContextCommandPayload;
241
+ contextMenuRequested: ContextMenuRequestedPayload;
242
+ undoRequested: NodeChangePayload & LinkChangePayload;
243
+ redoRequested: NodeChangePayload & LinkChangePayload;
244
+ nodeEdit: NodeChangePayload;
245
+ nodeProperties: NodeChangePayload;
246
+ nodeOpen: NodeChangePayload;
247
+ nodeHelp: NodeChangePayload;
248
+ zoomChanged: DiagramViewState;
249
+ viewChanged: DiagramViewState;
250
+ selectionChanged: DiagramSelection;
251
+ runtimeStateChanged: { state: DiagramRuntimeState };
252
+ undoStackChanged: { canUndo: boolean; canRedo: boolean };
253
+ documentLoaded: { document: DiagramDocument };
254
+ documentLoadFailed: DocumentLoadFailedPayload;
255
+ fullscreenRequested: FullscreenRequestedPayload;
256
+ fullscreenChanged: FullscreenChangedPayload;
257
+ }
@@ -0,0 +1,55 @@
1
+ import { EventEmitter } from './event-emitter.js';
2
+ import { Node, NodeInit, PortType, PortTypeInit } from './types.js';
3
+
4
+ export interface CatalogEvents extends Record<string, unknown> {
5
+ portTypesChanged: PortType[];
6
+ nodeTypesChanged: Node[];
7
+ }
8
+
9
+ export class StockSharpCatalog extends EventEmitter<CatalogEvents> {
10
+ private readonly portTypes = new Map<string, PortType>();
11
+ private readonly nodeTypes = new Map<string, Node>();
12
+
13
+ addPortType(portType: PortType | PortTypeInit): void {
14
+ const pt = portType instanceof PortType ? portType : new PortType(portType);
15
+ this.portTypes.set(pt.name, pt);
16
+ this.emit('portTypesChanged', this.getPortTypes());
17
+ }
18
+
19
+ removePortType(name: string): void {
20
+ this.portTypes.delete(name);
21
+ this.emit('portTypesChanged', this.getPortTypes());
22
+ }
23
+
24
+ getPortType(name: string): PortType | null {
25
+ return this.portTypes.get(name) ?? null;
26
+ }
27
+
28
+ getPortTypes(): PortType[] {
29
+ return Array.from(this.portTypes.values());
30
+ }
31
+
32
+ // Keyed by lower-cased typeId: element GUIDs are case-insensitive,
33
+ // but the seeded palette uses upper-case while desktop-schema imports
34
+ // serialize lower-case. A case-sensitive Map.get would miss every
35
+ // imported node ("Element type is missing from the palette"). Node.id
36
+ // keeps its original casing so save/load round-trips unchanged.
37
+ addNodeType(node: Node | NodeInit): void {
38
+ const n = node instanceof Node ? node : new Node(node);
39
+ this.nodeTypes.set(n.id.toLowerCase(), n);
40
+ this.emit('nodeTypesChanged', this.getNodeTypes());
41
+ }
42
+
43
+ removeNodeType(id: string): void {
44
+ this.nodeTypes.delete(id.toLowerCase());
45
+ this.emit('nodeTypesChanged', this.getNodeTypes());
46
+ }
47
+
48
+ getNodeType(id: string): Node | null {
49
+ return this.nodeTypes.get(id.toLowerCase()) ?? null;
50
+ }
51
+
52
+ getNodeTypes(): Node[] {
53
+ return Array.from(this.nodeTypes.values());
54
+ }
55
+ }
@@ -0,0 +1,44 @@
1
+ export type EventHandler<T> = (payload: T) => void;
2
+
3
+ export class EventEmitter<TEvents extends Record<string, unknown>> {
4
+ private readonly handlers = new Map<keyof TEvents, Set<EventHandler<unknown>>>();
5
+
6
+ on<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): () => void {
7
+ let set = this.handlers.get(event);
8
+ if (set === undefined) {
9
+ set = new Set();
10
+ this.handlers.set(event, set);
11
+ }
12
+ set.add(handler as EventHandler<unknown>);
13
+ return () => this.off(event, handler);
14
+ }
15
+
16
+ off<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): void {
17
+ const set = this.handlers.get(event);
18
+ if (set === undefined) {
19
+ return;
20
+ }
21
+ set.delete(handler as EventHandler<unknown>);
22
+ if (set.size === 0) {
23
+ this.handlers.delete(event);
24
+ }
25
+ }
26
+
27
+ protected emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void {
28
+ const set = this.handlers.get(event);
29
+ if (set === undefined) {
30
+ return;
31
+ }
32
+ for (const handler of set) {
33
+ try {
34
+ (handler as EventHandler<TEvents[K]>)(payload);
35
+ } catch (err) {
36
+ console.error(err);
37
+ }
38
+ }
39
+ }
40
+
41
+ protected clearEventHandlers(): void {
42
+ this.handlers.clear();
43
+ }
44
+ }