@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,784 @@
1
+ import { parseDiagramDocument, serializeDiagramDocument, } from '../core/document.js';
2
+ import { parseDiagramViewState, serializeDiagramViewState, } from '../core/view-state.js';
3
+ import { DiagramActionRegistry } from '../core/action-registry.js';
4
+ import { Diagram as CanvasDiagram, } from '../canvas-renderer.js';
5
+ import { EventEmitter } from './event-emitter.js';
6
+ import { DiagramNode, Link, Node, Port, } from './types.js';
7
+ export class StockSharpDiagram extends EventEmitter {
8
+ constructor(options) {
9
+ super();
10
+ this.disposables = [];
11
+ this.idCounter = 1;
12
+ this.undoEnabled = true;
13
+ this.redoEnabled = true;
14
+ this.helpEnabled = true;
15
+ this.loading = false;
16
+ this.destroyed = false;
17
+ this.fullscreen = false;
18
+ this.fullscreenButtonVisible = true;
19
+ this.linkValidator = null;
20
+ this.contextActions = new DiagramActionRegistry();
21
+ this.div = options.div;
22
+ this.catalog = options.catalog;
23
+ this.overviewContainer = options.overviewContainer ?? null;
24
+ this.zoomLabel = options.zoomLabel ?? null;
25
+ this.clipboard = this.resolveClipboard(options.clipboard);
26
+ this.canvas = new CanvasDiagram({
27
+ host: this.div,
28
+ typeColors: this.portTypeColors(),
29
+ gridSnap: options.gridSnap ?? true,
30
+ gridSize: options.gridSize,
31
+ });
32
+ this.fullscreenButtonVisible = options.showFullscreenButton ?? true;
33
+ this.prepareFullscreenButtonHost();
34
+ this.fullscreenButton = this.createFullscreenButton();
35
+ this.updateFullscreenButton();
36
+ this.registerContextActions();
37
+ this.bindCanvasEvents();
38
+ this.disposables.push(this.catalog.on('portTypesChanged', () => this.applySocketTheme()));
39
+ this.updateZoomLabel();
40
+ }
41
+ setLinkValidator(validator) {
42
+ this.linkValidator = validator;
43
+ this.canvas.setLinkValidator(validator === null ? null : ({ fromNode, fromPort, toNode, toPort }) => validator({
44
+ fromNode: this.fromCanvasNode(fromNode),
45
+ fromPort: this.fromCanvasPort(fromPort),
46
+ toNode: this.fromCanvasNode(toNode),
47
+ toPort: this.fromCanvasPort(toPort),
48
+ }));
49
+ }
50
+ addDiagramNode(node) {
51
+ return this.canvas.addDiagramNode(this.toCanvasNode(node));
52
+ }
53
+ dropNodeFromPalette(typeId, clientX, clientY) {
54
+ const rect = this.div.getBoundingClientRect();
55
+ const [x, y] = this.canvas.viewToWorld(clientX - rect.left, clientY - rect.top);
56
+ const definition = this.catalog.getNodeType(typeId) ?? new Node({ id: typeId, name: typeId });
57
+ const id = this.generateNodeId(definition.id);
58
+ const node = new DiagramNode({
59
+ id,
60
+ typeId: definition.id,
61
+ name: definition.name,
62
+ description: definition.description,
63
+ groupName: definition.groupName,
64
+ inPorts: definition.inPorts.map((port) => port.clone()),
65
+ outPorts: definition.outPorts.map((port) => port.clone()),
66
+ icon: definition.icon,
67
+ parameters: definition.parameters.map((parameter) => ({ ...parameter, options: [...parameter.options] })),
68
+ openAction: definition.openAction,
69
+ x,
70
+ y,
71
+ });
72
+ this.canvas.addDiagramNode(this.toCanvasNode(node));
73
+ this.canvas.selectNodeById(id);
74
+ return id;
75
+ }
76
+ removeDiagramNode(nodeId) {
77
+ this.canvas.removeDiagramNode(nodeId);
78
+ }
79
+ moveNode(nodeId, x, y) {
80
+ this.canvas.moveNode(nodeId, x, y);
81
+ }
82
+ getNodeBounds(nodeId) {
83
+ const node = this.canvas.findNode(nodeId);
84
+ return node === undefined
85
+ ? null
86
+ : { x: node.x, y: node.y, width: node.w, height: node.h };
87
+ }
88
+ getPortPosition(nodeId, direction, portId) {
89
+ const node = this.canvas.findNode(nodeId);
90
+ const port = direction === 'in'
91
+ ? node?.inPorts.find((candidate) => candidate.id === portId)
92
+ : node?.outPorts.find((candidate) => candidate.id === portId);
93
+ return port === undefined ? null : { x: port.cx, y: port.cy };
94
+ }
95
+ worldToView(x, y) {
96
+ const [viewX, viewY] = this.canvas.worldToView(x, y);
97
+ return { x: viewX, y: viewY };
98
+ }
99
+ setGridSnap(enabled, size) {
100
+ this.canvas.setGridSnap(enabled, size);
101
+ }
102
+ getGridSnap() {
103
+ return this.canvas.getGridSnap();
104
+ }
105
+ nudgeSelection(dx, dy) {
106
+ return this.canvas.nudgeSelection(dx, dy);
107
+ }
108
+ addLink(link) {
109
+ return this.canvas.addLink(this.toCanvasLink(link));
110
+ }
111
+ validateLink(link, excludeLinkId) {
112
+ return this.canvas.validateLink(this.toCanvasLink(link), excludeLinkId);
113
+ }
114
+ relink(linkId, link) {
115
+ return this.canvas.relink(linkId, this.toCanvasLink(link));
116
+ }
117
+ removeLink(link) {
118
+ this.canvas.removeLink(this.toCanvasLink(link));
119
+ }
120
+ addPort(nodeId, direction, port) {
121
+ this.canvas.addPort(nodeId, direction, this.toCanvasPort(port));
122
+ }
123
+ removePort(nodeId, direction, portId) {
124
+ this.canvas.removePort(nodeId, direction, portId);
125
+ }
126
+ updatePortType(nodeId, direction, portId, type) {
127
+ this.canvas.updatePortType(nodeId, direction, portId, type);
128
+ }
129
+ updatePort(nodeId, direction, portId, patch) {
130
+ return this.canvas.updatePort(nodeId, direction, portId, patch);
131
+ }
132
+ setNodePorts(nodeId, inPorts, outPorts) {
133
+ const current = this.canvas.findNode(nodeId);
134
+ if (current === undefined)
135
+ return;
136
+ const convert = (port) => ({
137
+ id: port.key,
138
+ name: port.name,
139
+ description: port.description,
140
+ type: port.type,
141
+ maxLinks: port.maxLinks,
142
+ availableTypes: [...(port.availableTypes ?? [])],
143
+ isDynamic: port.isDynamic ?? false,
144
+ dynamicMode: port.dynamicMode ?? '',
145
+ });
146
+ const nextIn = inPorts.map(convert);
147
+ const nextOut = outPorts.map(convert);
148
+ for (const sibling of current.inPorts.filter((port) => port.isSibling)) {
149
+ if (!nextIn.some((port) => port.id === sibling.id))
150
+ nextIn.push(sibling.toInit());
151
+ }
152
+ for (const sibling of current.outPorts.filter((port) => port.isSibling)) {
153
+ if (!nextOut.some((port) => port.id === sibling.id))
154
+ nextOut.push(sibling.toInit());
155
+ }
156
+ this.canvas.setNodePorts(nodeId, nextIn, nextOut);
157
+ }
158
+ updateNode(nodeId, patch) {
159
+ this.canvas.updateNode(nodeId, patch);
160
+ }
161
+ setNodeMessage(nodeId, message) {
162
+ this.canvas.updateNode(nodeId, { message });
163
+ }
164
+ setNodeError(nodeId, message, options = {}) {
165
+ return this.canvas.setNodeError(nodeId, message, options);
166
+ }
167
+ clearNodeError(nodeId, kind) {
168
+ return this.canvas.clearNodeError(nodeId, kind);
169
+ }
170
+ getRuntimeState() {
171
+ return this.canvas.getRuntimeState();
172
+ }
173
+ setRuntimeState(state) {
174
+ this.canvas.setRuntimeState(state);
175
+ }
176
+ clearRuntimeState() {
177
+ this.canvas.clearRuntimeState();
178
+ }
179
+ setActiveNode(nodeId) {
180
+ return this.canvas.setActiveNode(nodeId);
181
+ }
182
+ setPortRuntimeState(nodeId, direction, portId, patch) {
183
+ return this.canvas.setPortRuntimeState(nodeId, direction, portId, patch);
184
+ }
185
+ setGlobalError(message, kind = 'invalid') {
186
+ this.canvas.setGlobalError(message, kind);
187
+ }
188
+ setNodeParamValue(nodeId, paramName, value) {
189
+ this.canvas.setNodeParamValue(nodeId, paramName, value);
190
+ }
191
+ setNodeName(nodeId, value) {
192
+ this.canvas.updateNode(nodeId, { name: value });
193
+ }
194
+ /** Groups host-driven document edits into one undo/redo operation. */
195
+ transaction(label, action) {
196
+ return this.canvas.withTransaction(label, action);
197
+ }
198
+ setShowNodeMessages(show) {
199
+ this.canvas.setShowNodeMessages(show);
200
+ }
201
+ setReadOnly(readonly) {
202
+ this.canvas.setReadOnly(readonly);
203
+ }
204
+ getInteractionPermissions() {
205
+ return this.canvas.getInteractionPermissions();
206
+ }
207
+ setInteractionPermissions(patch) {
208
+ this.canvas.setInteractionPermissions(patch);
209
+ }
210
+ applySocketTheme() {
211
+ this.canvas.setTypeColors(this.portTypeColors());
212
+ }
213
+ applyOverviewTheme() {
214
+ this.canvas.requestRedraw();
215
+ }
216
+ setOverviewVisible(visible) {
217
+ this.canvas.setOverviewVisible(visible);
218
+ this.overviewContainer?.classList.toggle('hidden', !visible);
219
+ }
220
+ zoomToFit() {
221
+ this.canvas.zoomToFit();
222
+ }
223
+ setZoom(scale) {
224
+ this.canvas.setZoom(scale);
225
+ }
226
+ getViewState() {
227
+ return this.canvas.getViewState();
228
+ }
229
+ setViewState(state) {
230
+ this.canvas.setViewState(state);
231
+ this.overviewContainer?.classList.toggle('hidden', !state.overviewVisible);
232
+ }
233
+ /** Serializes only viewport preferences; strategy data stays in saveDocument(). */
234
+ saveViewState(space) {
235
+ return serializeDiagramViewState(this.canvas.getViewState(), space);
236
+ }
237
+ /** Restores a versioned viewport snapshot produced by saveViewState(). */
238
+ loadViewState(source) {
239
+ this.setViewState(parseDiagramViewState(source));
240
+ }
241
+ /** Detached PNG-ready canvas; use scope: 'content' for the complete scheme. */
242
+ takeScreenshot(options = {}) {
243
+ return this.canvas.takeScreenshot(options);
244
+ }
245
+ getSelection() {
246
+ return this.canvas.getSelection();
247
+ }
248
+ selectNodes(nodeIds) {
249
+ this.canvas.selectNodesById(nodeIds);
250
+ }
251
+ selectLink(linkId) {
252
+ this.canvas.selectLinkById(linkId);
253
+ }
254
+ selectPort(nodeId, direction, portId) {
255
+ this.canvas.selectPortById(nodeId, direction, portId);
256
+ }
257
+ resize(width, height) {
258
+ this.canvas.resize(width, height);
259
+ }
260
+ isFullscreen() {
261
+ return this.fullscreen;
262
+ }
263
+ setFullscreenButtonVisible(visible) {
264
+ this.fullscreenButtonVisible = visible;
265
+ this.fullscreenButton.hidden = !visible;
266
+ this.fullscreenButton.style.display = visible ? 'inline-flex' : 'none';
267
+ }
268
+ isFullscreenButtonVisible() {
269
+ return this.fullscreenButtonVisible;
270
+ }
271
+ /** Updates only the control's state after the host changed its own layout. */
272
+ setFullscreenState(fullscreen) {
273
+ if (this.fullscreen === fullscreen)
274
+ return;
275
+ this.fullscreen = fullscreen;
276
+ this.updateFullscreenButton();
277
+ this.emit('fullscreenChanged', { fullscreen });
278
+ }
279
+ setTheme(options) {
280
+ const { diagramBackground, overviewBackground, gridColor, linkMaxLightness, overviewBorderColor, overviewViewportColor, overviewViewportFill, } = options;
281
+ if (diagramBackground !== undefined) {
282
+ this.div.style.background = diagramBackground;
283
+ if (this.div.parentElement !== null)
284
+ this.div.parentElement.style.background = diagramBackground;
285
+ }
286
+ if (this.overviewContainer !== null && overviewBackground !== undefined) {
287
+ this.overviewContainer.style.background = overviewBackground;
288
+ }
289
+ this.canvas.setTheme({
290
+ background: diagramBackground,
291
+ gridColor,
292
+ linkMaxLightness,
293
+ overviewBackground,
294
+ overviewBorderColor,
295
+ overviewViewportColor,
296
+ overviewViewportFill,
297
+ });
298
+ }
299
+ enableUndo(enabled) { this.undoEnabled = enabled; }
300
+ enableRedo(enabled) { this.redoEnabled = enabled; }
301
+ enableHelp(enabled) { this.helpEnabled = enabled; }
302
+ canUndo() { return this.undoEnabled && this.canvas.canUndo(); }
303
+ canRedo() { return this.redoEnabled && this.canvas.canRedo(); }
304
+ undo() {
305
+ if (!this.canUndo())
306
+ return;
307
+ this.canvas.undo();
308
+ const snapshot = this.save();
309
+ this.emit('undoRequested', snapshot);
310
+ }
311
+ redo() {
312
+ if (!this.canRedo())
313
+ return;
314
+ this.canvas.redo();
315
+ const snapshot = this.save();
316
+ this.emit('redoRequested', snapshot);
317
+ }
318
+ cutSelection() { this.canvas.cutSelection(); }
319
+ copySelection() { this.canvas.copySelection(); }
320
+ pasteSelection() { this.canvas.pasteSelection(); }
321
+ deleteSelection() { this.canvas.deleteSelection(); }
322
+ async copySelectionToClipboard() {
323
+ const document = this.canvas.copySelectionDocument();
324
+ if (document === null)
325
+ return false;
326
+ if (this.clipboard !== null) {
327
+ try {
328
+ await this.clipboard.writeText(serializeDiagramDocument(document));
329
+ }
330
+ catch {
331
+ // The in-memory document remains available as a safe fallback.
332
+ }
333
+ }
334
+ return true;
335
+ }
336
+ async pasteSelectionFromClipboard() {
337
+ if (!this.canvas.getInteractionPermissions().paste)
338
+ return false;
339
+ if (this.clipboard !== null) {
340
+ try {
341
+ const text = await this.clipboard.readText();
342
+ const document = parseDiagramDocument(text);
343
+ return this.canvas.pasteDocument(document).length > 0;
344
+ }
345
+ catch {
346
+ // Fall through to the component's last valid in-memory copy.
347
+ }
348
+ }
349
+ const fallback = this.canvas.getClipboardDocument();
350
+ return fallback !== null && this.canvas.pasteDocument(fallback).length > 0;
351
+ }
352
+ getContextCommands() {
353
+ return this.contextActions.states(this.contextActionContext()).map(({ id, enabled }) => ({
354
+ command: id,
355
+ enabled,
356
+ }));
357
+ }
358
+ executeContextCommand(command) {
359
+ const context = this.contextActionContext();
360
+ if (!this.contextActions.execute(command, context))
361
+ return false;
362
+ this.emit('contextCommand', { command, nodes: context.nodes, links: context.links });
363
+ return true;
364
+ }
365
+ clear() {
366
+ this.loading = true;
367
+ try {
368
+ this.canvas.load([], []);
369
+ }
370
+ finally {
371
+ this.loading = false;
372
+ }
373
+ this.emit('runtimeStateChanged', { state: this.canvas.getRuntimeState() });
374
+ }
375
+ load(nodes, links, options = {}) {
376
+ this.loading = true;
377
+ try {
378
+ this.canvas.load(nodes.map((node) => this.toCanvasNode(node)), links.map((link) => this.toCanvasLink(link)));
379
+ for (const [nodeId, message] of Object.entries(options.nodeErrors ?? {})) {
380
+ this.canvas.setNodeError(nodeId, message, { kind: 'load', animate: false });
381
+ }
382
+ }
383
+ finally {
384
+ this.loading = false;
385
+ }
386
+ this.emit('runtimeStateChanged', { state: this.canvas.getRuntimeState() });
387
+ const snapshot = this.save();
388
+ this.emit('loadFinished', snapshot);
389
+ }
390
+ save() {
391
+ const snapshot = this.canvas.save();
392
+ return {
393
+ nodes: snapshot.nodes.map((node) => this.fromCanvasInit(node)),
394
+ links: snapshot.links.map((link) => this.fromCanvasLink(link)),
395
+ };
396
+ }
397
+ loadDocument(document, options = {}) {
398
+ let failure = null;
399
+ this.loading = true;
400
+ try {
401
+ this.canvas.loadDocument(document);
402
+ for (const [nodeId, message] of Object.entries(options.nodeErrors ?? {})) {
403
+ this.canvas.setNodeError(nodeId, message, { kind: 'load', animate: false });
404
+ }
405
+ }
406
+ catch (error) {
407
+ failure = error instanceof Error ? error : new Error(String(error));
408
+ this.canvas.setGlobalError(failure.message, 'load');
409
+ }
410
+ finally {
411
+ this.loading = false;
412
+ }
413
+ this.emit('runtimeStateChanged', { state: this.canvas.getRuntimeState() });
414
+ if (failure !== null) {
415
+ this.emit('documentLoadFailed', { message: failure.message, error: failure });
416
+ throw failure;
417
+ }
418
+ const saved = this.canvas.saveDocument();
419
+ this.emit('documentLoaded', { document: saved });
420
+ const snapshot = this.save();
421
+ this.emit('loadFinished', snapshot);
422
+ }
423
+ saveDocument() {
424
+ return this.canvas.saveDocument();
425
+ }
426
+ destroy() {
427
+ if (this.destroyed)
428
+ return;
429
+ this.destroyed = true;
430
+ for (const dispose of this.disposables.splice(0).reverse())
431
+ dispose();
432
+ this.canvas.destroy();
433
+ }
434
+ prepareFullscreenButtonHost() {
435
+ const previous = this.div.style.position;
436
+ const position = typeof getComputedStyle === 'function'
437
+ ? getComputedStyle(this.div).position
438
+ : previous;
439
+ if (position !== '' && position !== 'static' && position !== undefined)
440
+ return;
441
+ this.div.style.position = 'relative';
442
+ this.disposables.push(() => {
443
+ if (this.div.style.position === 'relative')
444
+ this.div.style.position = previous;
445
+ });
446
+ }
447
+ createFullscreenButton() {
448
+ const owner = this.div.ownerDocument ?? document;
449
+ const button = owner.createElement('button');
450
+ button.type = 'button';
451
+ button.className = 'ssdiagram-fullscreen-button';
452
+ button.setAttribute('data-ssdiagram-fullscreen-button', '');
453
+ Object.assign(button.style, {
454
+ position: 'absolute',
455
+ top: '8px',
456
+ right: '8px',
457
+ zIndex: '4',
458
+ display: 'inline-flex',
459
+ alignItems: 'center',
460
+ justifyContent: 'center',
461
+ width: '30px',
462
+ height: '30px',
463
+ padding: '0',
464
+ border: '1px solid var(--ssdiagram-control-border, var(--t-border, #3a4250))',
465
+ borderRadius: '5px',
466
+ background: 'var(--ssdiagram-control-background, var(--t-surface, rgba(18, 21, 28, 0.9)))',
467
+ color: 'var(--ssdiagram-control-color, var(--t-text, #eaecef))',
468
+ boxSizing: 'border-box',
469
+ cursor: 'pointer',
470
+ opacity: '0.9',
471
+ });
472
+ const click = () => {
473
+ this.emit('fullscreenRequested', { fullscreen: !this.fullscreen });
474
+ };
475
+ button.addEventListener('click', click);
476
+ this.div.appendChild(button);
477
+ this.disposables.push(() => {
478
+ button.removeEventListener('click', click);
479
+ button.remove();
480
+ });
481
+ button.hidden = !this.fullscreenButtonVisible;
482
+ button.style.display = this.fullscreenButtonVisible ? 'inline-flex' : 'none';
483
+ return button;
484
+ }
485
+ updateFullscreenButton() {
486
+ const fullscreen = this.fullscreen;
487
+ const label = fullscreen ? 'Exit fullscreen' : 'Enter fullscreen';
488
+ const path = fullscreen
489
+ ? 'M4 9h5V4M20 9h-5V4M4 15h5v5M20 15h-5v5'
490
+ : 'M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5';
491
+ this.fullscreenButton.className = fullscreen
492
+ ? 'ssdiagram-fullscreen-button is-active'
493
+ : 'ssdiagram-fullscreen-button';
494
+ this.fullscreenButton.title = label;
495
+ this.fullscreenButton.setAttribute('aria-label', label);
496
+ this.fullscreenButton.setAttribute('aria-pressed', String(fullscreen));
497
+ this.fullscreenButton.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" `
498
+ + 'fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" '
499
+ + `stroke-linejoin="round"><path d="${path}"/></svg>`;
500
+ }
501
+ bindCanvasEvents() {
502
+ this.disposables.push(this.canvas.on('nodeAdded', ({ node }) => {
503
+ if (!this.loading)
504
+ this.emit('nodeAdded', { nodes: [this.fromCanvasNode(node)] });
505
+ }), this.canvas.on('nodeRemoved', ({ node }) => {
506
+ if (!this.loading)
507
+ this.emit('nodeRemoved', { nodes: [this.fromCanvasNode(node)] });
508
+ }), this.canvas.on('nodeMoved', ({ node }) => {
509
+ if (!this.loading)
510
+ this.emit('nodeMoved', { node: this.fromCanvasNode(node) });
511
+ }), this.canvas.on('nodeChanged', ({ node }) => {
512
+ if (!this.loading)
513
+ this.emit('nodeEdit', { nodes: [this.fromCanvasNode(node)] });
514
+ }), this.canvas.on('linkAdded', ({ link }) => {
515
+ if (!this.loading)
516
+ this.emit('linkAdded', { links: [this.fromCanvasLink(link)] });
517
+ }), this.canvas.on('linkRemoved', ({ link }) => {
518
+ if (!this.loading)
519
+ this.emit('linkRemoved', { links: [this.fromCanvasLink(link)] });
520
+ }), this.canvas.on('linkRelinked', ({ link, previous }) => {
521
+ if (!this.loading)
522
+ this.emit('linkRelinked', {
523
+ link: this.fromCanvasLink(link),
524
+ previous: this.fromCanvasLink(previous),
525
+ });
526
+ }), this.canvas.on('nodeSelected', ({ node, selected }) => {
527
+ if (node !== null)
528
+ this.emit('nodeSelected', { node: this.fromCanvasNode(node), selected });
529
+ }), this.canvas.on('linkSelected', ({ link, selected }) => {
530
+ if (link !== null)
531
+ this.emit('linkSelected', { link: this.fromCanvasLink(link), selected });
532
+ }), this.canvas.on('selectionChanged', (selection) => this.emit('selectionChanged', selection)), this.canvas.on('runtimeStateChanged', ({ state }) => {
533
+ if (!this.loading)
534
+ this.emit('runtimeStateChanged', { state });
535
+ }), this.canvas.on('nodeHover', ({ node, hovering }) => {
536
+ this.emit('nodeHover', { node: this.fromCanvasNode(node), hovering });
537
+ }), this.canvas.on('portSelected', ({ node, port }) => {
538
+ this.emit('portSelected', {
539
+ node: this.fromCanvasNode(node),
540
+ port: this.fromCanvasPort(port),
541
+ direction: port.direction,
542
+ });
543
+ }), this.canvas.on('portClicked', ({ node, port, action, ctrlKey, shiftKey, altKey, metaKey }) => {
544
+ this.emit('portClicked', {
545
+ node: this.fromCanvasNode(node),
546
+ port: this.fromCanvasPort(port),
547
+ direction: port.direction,
548
+ action,
549
+ ctrlKey,
550
+ shiftKey,
551
+ altKey,
552
+ metaKey,
553
+ });
554
+ }), this.canvas.on('portHover', ({ node, port, hovering }) => {
555
+ this.emit('portHover', {
556
+ node: this.fromCanvasNode(node),
557
+ port: this.fromCanvasPort(port),
558
+ direction: port.direction,
559
+ hovering,
560
+ });
561
+ }), this.canvas.on('linkHover', ({ link, hovering }) => {
562
+ this.emit('linkHover', { link: this.fromCanvasLink(link), hovering });
563
+ }), this.canvas.on('linkValidation', ({ fromNode, from, toNode, to, allowed, reason }) => {
564
+ this.emit('linkValidation', {
565
+ fromNode: this.fromCanvasNode(fromNode),
566
+ fromPort: this.fromCanvasPort(from),
567
+ toNode: this.fromCanvasNode(toNode),
568
+ toPort: this.fromCanvasPort(to),
569
+ allowed,
570
+ reason,
571
+ });
572
+ }), this.canvas.on('nodeOpen', ({ node }) => {
573
+ this.emit('nodeOpen', { nodes: [this.fromCanvasNode(node)] });
574
+ }), this.canvas.on('zoomChanged', () => {
575
+ this.updateZoomLabel();
576
+ this.emit('zoomChanged', this.canvas.getViewState());
577
+ }), this.canvas.on('viewChanged', (state) => {
578
+ this.overviewContainer?.classList.toggle('hidden', !state.overviewVisible);
579
+ this.emit('viewChanged', state);
580
+ }), this.canvas.on('undoStackChanged', (state) => this.emit('undoStackChanged', state)), this.canvas.on('contextMenu', ({ x, y, node, link, port }) => {
581
+ this.emit('contextMenuRequested', {
582
+ x,
583
+ y,
584
+ node: node === null ? null : this.fromCanvasNode(node),
585
+ link: link === null ? null : this.fromCanvasLink(link),
586
+ port: port === null ? null : this.fromCanvasPort(port.port),
587
+ portDirection: port?.port.direction ?? null,
588
+ commands: this.getContextCommands(),
589
+ });
590
+ }));
591
+ }
592
+ registerContextActions() {
593
+ const permissions = () => this.canvas.getInteractionPermissions();
594
+ this.contextActions.register({
595
+ id: 'undo',
596
+ canExecute: () => this.canUndo(),
597
+ execute: () => this.undo(),
598
+ });
599
+ this.contextActions.register({
600
+ id: 'redo',
601
+ canExecute: () => this.canRedo(),
602
+ execute: () => this.redo(),
603
+ });
604
+ this.contextActions.register({
605
+ id: 'cut',
606
+ canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy && permissions().deleteSelection,
607
+ execute: () => this.cutSelection(),
608
+ });
609
+ this.contextActions.register({
610
+ id: 'copy',
611
+ canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy,
612
+ execute: () => this.copySelection(),
613
+ });
614
+ this.contextActions.register({
615
+ id: 'paste',
616
+ canExecute: () => this.canvas.hasClipboard() && permissions().paste,
617
+ execute: () => this.pasteSelection(),
618
+ });
619
+ this.contextActions.register({
620
+ id: 'open',
621
+ canExecute: ({ nodes }) => nodes.length === 1 && nodes[0].openAction.length > 0,
622
+ execute: ({ nodes }) => this.emit('nodeOpen', { nodes }),
623
+ });
624
+ this.contextActions.register({
625
+ id: 'delete',
626
+ canExecute: ({ selection }) => permissions().deleteSelection
627
+ && (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
628
+ execute: () => this.canvas.deleteSelection(),
629
+ });
630
+ this.contextActions.register({
631
+ id: 'properties',
632
+ canExecute: ({ nodes }) => nodes.length > 0,
633
+ execute: ({ nodes }) => this.emit('nodeProperties', { nodes }),
634
+ });
635
+ this.contextActions.register({
636
+ id: 'help',
637
+ canExecute: ({ nodes }) => this.helpEnabled && nodes.length > 0,
638
+ execute: ({ nodes }) => this.emit('nodeHelp', { nodes }),
639
+ });
640
+ }
641
+ contextActionContext() {
642
+ const selection = this.canvas.getSelection();
643
+ const document = this.canvas.saveDocument();
644
+ const nodeIds = new Set(selection.nodeIds);
645
+ const linkIds = new Set(selection.linkIds);
646
+ return {
647
+ selection,
648
+ nodes: document.nodes
649
+ .filter((node) => nodeIds.has(node.id))
650
+ .map((node) => this.fromCanvasInit(node)),
651
+ links: document.links
652
+ .filter((link) => linkIds.has(link.id))
653
+ .map((link) => new Link({
654
+ id: link.id,
655
+ outNode: link.from.nodeId,
656
+ outPort: link.from.portId,
657
+ inNode: link.to.nodeId,
658
+ inPort: link.to.portId,
659
+ metadata: link.metadata,
660
+ })),
661
+ };
662
+ }
663
+ toCanvasNode(node) {
664
+ return {
665
+ id: node.id,
666
+ typeId: node.typeId,
667
+ name: node.name,
668
+ description: node.description,
669
+ groupName: node.groupName,
670
+ color: node.color,
671
+ border: node.border,
672
+ icon: node.icon,
673
+ openAction: node.openAction,
674
+ message: node.message,
675
+ isPlaceholder: node.isPlaceholder,
676
+ parameters: node.parameters.map((parameter) => ({ ...parameter, options: [...parameter.options] })),
677
+ paramValues: { ...node.paramValues },
678
+ x: node.x,
679
+ y: node.y,
680
+ inPorts: node.inPorts.map((port) => this.toCanvasPort(port)),
681
+ outPorts: node.outPorts.map((port) => this.toCanvasPort(port)),
682
+ };
683
+ }
684
+ fromCanvasNode(node) {
685
+ return this.fromCanvasInit(node.toInit(false));
686
+ }
687
+ fromCanvasInit(node) {
688
+ return new DiagramNode({
689
+ id: node.id,
690
+ typeId: node.typeId,
691
+ name: node.name,
692
+ description: node.description,
693
+ groupName: node.groupName,
694
+ color: node.color,
695
+ border: node.border,
696
+ icon: node.icon,
697
+ openAction: node.openAction,
698
+ message: node.message,
699
+ isPlaceholder: node.isPlaceholder,
700
+ parameters: (node.parameters ?? []).map((parameter) => ({ ...parameter, options: [...parameter.options] })),
701
+ paramValues: { ...(node.paramValues ?? {}) },
702
+ x: node.x,
703
+ y: node.y,
704
+ inPorts: (node.inPorts ?? []).map((port) => this.fromCanvasPortInit(port)),
705
+ outPorts: (node.outPorts ?? []).map((port) => this.fromCanvasPortInit(port)),
706
+ });
707
+ }
708
+ toCanvasPort(port) {
709
+ return {
710
+ id: port.id,
711
+ name: port.name,
712
+ description: port.description,
713
+ type: port.type,
714
+ maxLinks: port.maxLinks,
715
+ availableTypes: [...port.availableTypes],
716
+ isDynamic: port.isDynamic,
717
+ dynamicMode: port.dynamicMode,
718
+ isSibling: port.isSibling,
719
+ };
720
+ }
721
+ fromCanvasPort(port) {
722
+ return this.fromCanvasPortInit(port.toInit());
723
+ }
724
+ fromCanvasPortInit(port) {
725
+ return new Port({
726
+ id: port.id,
727
+ name: port.name,
728
+ description: port.description,
729
+ type: port.type,
730
+ maxLinks: port.maxLinks,
731
+ availableTypes: [...(port.availableTypes ?? [])],
732
+ isDynamic: port.isDynamic,
733
+ dynamicMode: port.dynamicMode,
734
+ isSibling: port.isSibling,
735
+ });
736
+ }
737
+ toCanvasLink(link) {
738
+ const idOf = (value) => typeof value === 'string' ? value : value.id;
739
+ return {
740
+ id: link.id || undefined,
741
+ from: idOf(link.outNode),
742
+ fromPort: idOf(link.outPort),
743
+ to: idOf(link.inNode),
744
+ toPort: idOf(link.inPort),
745
+ metadata: link.metadata,
746
+ };
747
+ }
748
+ fromCanvasLink(link) {
749
+ return new Link({
750
+ id: link.id,
751
+ outNode: link.from,
752
+ outPort: link.fromPort,
753
+ inNode: link.to,
754
+ inPort: link.toPort,
755
+ metadata: link.metadata,
756
+ });
757
+ }
758
+ portTypeColors() {
759
+ return Object.fromEntries(this.catalog.getPortTypes().map((portType) => [portType.name, portType.color]));
760
+ }
761
+ updateZoomLabel() {
762
+ if (this.zoomLabel !== null)
763
+ this.zoomLabel.textContent = `${Math.round(this.canvas.getViewState().zoom * 100)}%`;
764
+ }
765
+ resolveClipboard(explicit) {
766
+ if (explicit !== undefined)
767
+ return explicit;
768
+ if (typeof navigator === 'undefined' || navigator.clipboard === undefined)
769
+ return null;
770
+ return {
771
+ readText: () => navigator.clipboard.readText(),
772
+ writeText: (value) => navigator.clipboard.writeText(value),
773
+ };
774
+ }
775
+ generateNodeId(prefix) {
776
+ const safePrefix = prefix.replace(/[^a-zA-Z0-9_-]/g, '') || 'node';
777
+ let id;
778
+ do
779
+ id = `${safePrefix}_${this.idCounter++}`;
780
+ while (this.canvas.findNode(id) !== undefined);
781
+ return id;
782
+ }
783
+ }
784
+ //# sourceMappingURL=stocksharp-diagram.js.map