@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.
- package/LICENSE +30 -0
- package/NOTICE +21 -0
- package/README.md +346 -0
- package/dist/esm/canvas-renderer.js +3226 -0
- package/dist/esm/canvas-renderer.js.map +1 -0
- package/dist/esm/core/action-registry.js +35 -0
- package/dist/esm/core/action-registry.js.map +1 -0
- package/dist/esm/core/document.js +350 -0
- package/dist/esm/core/document.js.map +1 -0
- package/dist/esm/core/history.js +136 -0
- package/dist/esm/core/history.js.map +1 -0
- package/dist/esm/core/model.js +2 -0
- package/dist/esm/core/model.js.map +1 -0
- package/dist/esm/core/state.js +73 -0
- package/dist/esm/core/state.js.map +1 -0
- package/dist/esm/core/view-state.js +70 -0
- package/dist/esm/core/view-state.js.map +1 -0
- package/dist/esm/diagram/api.js +2 -0
- package/dist/esm/diagram/api.js.map +1 -0
- package/dist/esm/diagram/catalog.js +45 -0
- package/dist/esm/diagram/catalog.js.map +1 -0
- package/dist/esm/diagram/event-emitter.js +42 -0
- package/dist/esm/diagram/event-emitter.js.map +1 -0
- package/dist/esm/diagram/palette.js +269 -0
- package/dist/esm/diagram/palette.js.map +1 -0
- package/dist/esm/diagram/stocksharp-diagram.js +784 -0
- package/dist/esm/diagram/stocksharp-diagram.js.map +1 -0
- package/dist/esm/diagram/types.js +105 -0
- package/dist/esm/diagram/types.js.map +1 -0
- package/dist/esm/embed.js +398 -0
- package/dist/esm/embed.js.map +1 -0
- package/dist/esm/i18n.js +12 -0
- package/dist/esm/i18n.js.map +1 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/ssdiagram.js +5141 -0
- package/dist/ssdiagram.js.map +7 -0
- package/dist/types/canvas-renderer.d.ts +512 -0
- package/dist/types/canvas-renderer.d.ts.map +1 -0
- package/dist/types/core/action-registry.d.ts +18 -0
- package/dist/types/core/action-registry.d.ts.map +1 -0
- package/dist/types/core/document.d.ts +12 -0
- package/dist/types/core/document.d.ts.map +1 -0
- package/dist/types/core/history.d.ts +33 -0
- package/dist/types/core/history.d.ts.map +1 -0
- package/dist/types/core/model.d.ts +85 -0
- package/dist/types/core/model.d.ts.map +1 -0
- package/dist/types/core/state.d.ts +68 -0
- package/dist/types/core/state.d.ts.map +1 -0
- package/dist/types/core/view-state.d.ts +14 -0
- package/dist/types/core/view-state.d.ts.map +1 -0
- package/dist/types/diagram/api.d.ts +213 -0
- package/dist/types/diagram/api.d.ts.map +1 -0
- package/dist/types/diagram/catalog.d.ts +19 -0
- package/dist/types/diagram/catalog.d.ts.map +1 -0
- package/dist/types/diagram/event-emitter.d.ts +9 -0
- package/dist/types/diagram/event-emitter.d.ts.map +1 -0
- package/dist/types/diagram/palette.d.ts +63 -0
- package/dist/types/diagram/palette.d.ts.map +1 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts +157 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -0
- package/dist/types/diagram/types.d.ts +171 -0
- package/dist/types/diagram/types.d.ts.map +1 -0
- package/dist/types/embed.d.ts +35 -0
- package/dist/types/embed.d.ts.map +1 -0
- package/dist/types/i18n.d.ts +167 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +126 -0
- package/sample.png +0 -0
- package/src/canvas-renderer.ts +3249 -0
- package/src/core/action-registry.ts +46 -0
- package/src/core/document.ts +387 -0
- package/src/core/history.ts +154 -0
- package/src/core/model.ts +99 -0
- package/src/core/state.ts +148 -0
- package/src/core/view-state.ts +82 -0
- package/src/diagram/api.ts +257 -0
- package/src/diagram/catalog.ts +55 -0
- package/src/diagram/event-emitter.ts +44 -0
- package/src/diagram/palette.ts +312 -0
- package/src/diagram/stocksharp-diagram.ts +945 -0
- package/src/diagram/types.ts +332 -0
- package/src/embed.ts +508 -0
- package/src/i18n.ts +210 -0
- 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
|