@stocksharp/diagram 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -1
- package/dist/esm/canvas-renderer.js +178 -31
- package/dist/esm/canvas-renderer.js.map +1 -1
- package/dist/esm/color.js +110 -0
- package/dist/esm/color.js.map +1 -1
- package/dist/esm/core/document.js +11 -0
- package/dist/esm/core/document.js.map +1 -1
- package/dist/esm/core/model.js.map +1 -1
- package/dist/esm/diagram/stocksharp-diagram.js +23 -3
- package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
- package/dist/esm/embed.js +104 -20
- package/dist/esm/embed.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/ssdiagram-headless.js +247 -26
- package/dist/ssdiagram-headless.js.map +2 -2
- package/dist/ssdiagram.js +352 -48
- package/dist/ssdiagram.js.map +3 -3
- package/dist/types/canvas-renderer.d.ts +26 -3
- package/dist/types/canvas-renderer.d.ts.map +1 -1
- package/dist/types/color.d.ts +9 -0
- package/dist/types/color.d.ts.map +1 -1
- package/dist/types/core/model.d.ts +7 -0
- package/dist/types/core/model.d.ts.map +1 -1
- package/dist/types/diagram/api.d.ts +6 -0
- package/dist/types/diagram/api.d.ts.map +1 -1
- package/dist/types/diagram/stocksharp-diagram.d.ts +11 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
- package/dist/types/embed.d.ts +21 -0
- package/dist/types/embed.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/canvas-renderer.ts +182 -30
- package/src/color.ts +130 -0
- package/src/core/document.ts +12 -0
- package/src/core/model.ts +7 -0
- package/src/diagram/api.ts +6 -0
- package/src/diagram/stocksharp-diagram.ts +27 -3
- package/src/embed.ts +147 -29
- package/src/index.ts +2 -0
package/dist/ssdiagram.js
CHANGED
|
@@ -53,6 +53,7 @@ var SSDiagram = (() => {
|
|
|
53
53
|
parseDiagramViewState: () => parseDiagramViewState,
|
|
54
54
|
renderAll: () => renderAll,
|
|
55
55
|
renderDiagramSvg: () => renderDiagramSvg,
|
|
56
|
+
renderDocument: () => renderDocument,
|
|
56
57
|
renderFromInline: () => renderFromInline,
|
|
57
58
|
renderFromSource: () => renderFromSource,
|
|
58
59
|
renderScheme: () => renderScheme,
|
|
@@ -162,10 +163,12 @@ var SSDiagram = (() => {
|
|
|
162
163
|
function normalizeNode(value, path) {
|
|
163
164
|
const input = requireStructure(value, path);
|
|
164
165
|
const id = requireIdentifier(input.id, `${path}.id`);
|
|
166
|
+
const subtitle = requireOptionalText(input.subtitle, `${path}.subtitle`);
|
|
165
167
|
return {
|
|
166
168
|
id,
|
|
167
169
|
typeId: requireIdentifier(input.typeId ?? id, `${path}.typeId`),
|
|
168
170
|
name: requireString(input.name, `${path}.name`),
|
|
171
|
+
...subtitle === "" ? {} : { subtitle },
|
|
169
172
|
description: requireString(input.description ?? "", `${path}.description`),
|
|
170
173
|
groupName: requireString(input.groupName ?? "Common", `${path}.groupName`),
|
|
171
174
|
x: requireFiniteNumber(input.x ?? 0, `${path}.x`),
|
|
@@ -216,14 +219,19 @@ var SSDiagram = (() => {
|
|
|
216
219
|
}
|
|
217
220
|
function normalizeLink(value, path) {
|
|
218
221
|
const input = requireStructure(value, path);
|
|
222
|
+
const label2 = requireOptionalText(input.label, `${path}.label`);
|
|
219
223
|
return {
|
|
220
224
|
id: requireIdentifier(input.id, `${path}.id`),
|
|
221
225
|
from: normalizeEndpoint(input.from, `${path}.from`),
|
|
222
226
|
to: normalizeEndpoint(input.to, `${path}.to`),
|
|
223
227
|
style: requireLinkStyle(input.style, `${path}.style`),
|
|
228
|
+
...label2 === "" ? {} : { label: label2 },
|
|
224
229
|
metadata: cloneJsonObject(input.metadata ?? {}, `${path}.metadata`)
|
|
225
230
|
};
|
|
226
231
|
}
|
|
232
|
+
function requireOptionalText(value, path) {
|
|
233
|
+
return value === void 0 || value === null ? "" : requireString(value, path);
|
|
234
|
+
}
|
|
227
235
|
function requireLinkStyle(value, path) {
|
|
228
236
|
if (value === void 0 || value === null) return "solid";
|
|
229
237
|
if (value !== "solid" && value !== "dashed") {
|
|
@@ -263,6 +271,7 @@ var SSDiagram = (() => {
|
|
|
263
271
|
id: requireString(node.id, `${path}.id`),
|
|
264
272
|
typeId: requireString(node.typeId, `${path}.typeId`),
|
|
265
273
|
name: requireString(node.name, `${path}.name`),
|
|
274
|
+
subtitle: node.subtitle,
|
|
266
275
|
description: requireString(node.description, `${path}.description`),
|
|
267
276
|
groupName: requireString(node.groupName, `${path}.groupName`),
|
|
268
277
|
x: requireNumber(node.x, `${path}.x`),
|
|
@@ -318,6 +327,7 @@ var SSDiagram = (() => {
|
|
|
318
327
|
from: parseEndpoint(link.from, `${path}.from`),
|
|
319
328
|
to: parseEndpoint(link.to, `${path}.to`),
|
|
320
329
|
style: link.style === void 0 ? void 0 : requireLinkStyle(link.style, `${path}.style`),
|
|
330
|
+
label: link.label,
|
|
321
331
|
metadata: cloneJsonObject(link.metadata, `${path}.metadata`)
|
|
322
332
|
}, path);
|
|
323
333
|
}
|
|
@@ -873,6 +883,82 @@ var SSDiagram = (() => {
|
|
|
873
883
|
return DARK_OUTLINE;
|
|
874
884
|
return contrast(background, relativeLuminance(LIGHT_OUTLINE)) > contrast(background, relativeLuminance(DARK_OUTLINE)) ? LIGHT_OUTLINE : DARK_OUTLINE;
|
|
875
885
|
}
|
|
886
|
+
function toHsl(color) {
|
|
887
|
+
const hsl = color.trim().match(/^hsl\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*\)$/i);
|
|
888
|
+
if (hsl !== null)
|
|
889
|
+
return [+hsl[1], +hsl[2] / 100, +hsl[3] / 100];
|
|
890
|
+
const rgb = parseRgb(color);
|
|
891
|
+
if (rgb === null)
|
|
892
|
+
return null;
|
|
893
|
+
const [r, g, b] = rgb.map((channel) => channel / 255);
|
|
894
|
+
const max = Math.max(r, g, b);
|
|
895
|
+
const min = Math.min(r, g, b);
|
|
896
|
+
const lightness = (max + min) / 2;
|
|
897
|
+
if (max === min)
|
|
898
|
+
return [0, 0, lightness];
|
|
899
|
+
const span = max - min;
|
|
900
|
+
const saturation = lightness > 0.5 ? span / (2 - max - min) : span / (max + min);
|
|
901
|
+
const hue = max === r ? (g - b) / span + (g < b ? 6 : 0) : max === g ? (b - r) / span + 2 : (r - g) / span + 4;
|
|
902
|
+
return [hue * 60, saturation, lightness];
|
|
903
|
+
}
|
|
904
|
+
var DARK_CANVAS_FLOOR = 0.42;
|
|
905
|
+
var LIGHT_CANVAS_CEILING = 0.45;
|
|
906
|
+
function legibleOn(color, background) {
|
|
907
|
+
const hsl = toHsl(color);
|
|
908
|
+
if (hsl === null)
|
|
909
|
+
return color;
|
|
910
|
+
const canvas = relativeLuminance(background);
|
|
911
|
+
const [hue, saturation, lightness] = hsl;
|
|
912
|
+
const moved = canvas < 0 ? lightness : canvas > 0.18 ? Math.min(lightness, LIGHT_CANVAS_CEILING) : Math.max(lightness, DARK_CANVAS_FLOOR);
|
|
913
|
+
if (moved === lightness && /^hsl/i.test(color.trim()))
|
|
914
|
+
return color;
|
|
915
|
+
return `hsl(${Math.round(hue * 10) / 10}, ${Math.round(saturation * 1e3) / 10}%, ${Math.round(moved * 1e3) / 10}%)`;
|
|
916
|
+
}
|
|
917
|
+
function cappedLightness(color, maxLightness) {
|
|
918
|
+
const hsl = toHsl(color);
|
|
919
|
+
if (hsl === null)
|
|
920
|
+
return color;
|
|
921
|
+
const [hue, saturation, lightness] = hsl;
|
|
922
|
+
if (lightness <= maxLightness)
|
|
923
|
+
return color;
|
|
924
|
+
return `hsl(${Math.round(hue * 10) / 10}, ${Math.round(saturation * 1e3) / 10}%, ${Math.round(maxLightness * 1e3) / 10}%)`;
|
|
925
|
+
}
|
|
926
|
+
function resolveCssVariables(value, lookup) {
|
|
927
|
+
return resolveAt(value, lookup, 0);
|
|
928
|
+
}
|
|
929
|
+
function resolveAt(value, lookup, depth) {
|
|
930
|
+
const start = value.indexOf("var(");
|
|
931
|
+
if (start < 0)
|
|
932
|
+
return value;
|
|
933
|
+
if (depth > 8)
|
|
934
|
+
return null;
|
|
935
|
+
let level = 0;
|
|
936
|
+
let end = -1;
|
|
937
|
+
for (let i = start + 3; i < value.length; i += 1) {
|
|
938
|
+
if (value[i] === "(")
|
|
939
|
+
level += 1;
|
|
940
|
+
else if (value[i] === ")" && --level === 0) {
|
|
941
|
+
end = i;
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (end < 0)
|
|
946
|
+
return null;
|
|
947
|
+
const inner = value.slice(start + 4, end);
|
|
948
|
+
const comma = inner.indexOf(",");
|
|
949
|
+
const name = (comma < 0 ? inner : inner.slice(0, comma)).trim();
|
|
950
|
+
const fallback = comma < 0 ? "" : inner.slice(comma + 1).trim();
|
|
951
|
+
let replacement = name.startsWith("--") ? lookup(name).trim() : "";
|
|
952
|
+
if (replacement === "")
|
|
953
|
+
replacement = fallback;
|
|
954
|
+
if (replacement === "")
|
|
955
|
+
return null;
|
|
956
|
+
const resolved = resolveAt(replacement, lookup, depth + 1);
|
|
957
|
+
const rest = resolveAt(value.slice(end + 1), lookup, depth);
|
|
958
|
+
if (resolved === null || rest === null)
|
|
959
|
+
return null;
|
|
960
|
+
return value.slice(0, start) + resolved + rest;
|
|
961
|
+
}
|
|
876
962
|
|
|
877
963
|
// src/core/history.ts
|
|
878
964
|
var DiagramCommandHistory = class {
|
|
@@ -1162,6 +1248,7 @@ var SSDiagram = (() => {
|
|
|
1162
1248
|
this.id = id;
|
|
1163
1249
|
this.typeId = init.typeId ?? id;
|
|
1164
1250
|
this.name = init.name;
|
|
1251
|
+
this.subtitle = init.subtitle ?? "";
|
|
1165
1252
|
this.description = init.description ?? "";
|
|
1166
1253
|
this.groupName = init.groupName ?? "Common";
|
|
1167
1254
|
this.color = init.color ?? "#d7d7d7";
|
|
@@ -1187,6 +1274,7 @@ var SSDiagram = (() => {
|
|
|
1187
1274
|
id: this.id,
|
|
1188
1275
|
typeId: this.typeId,
|
|
1189
1276
|
name: this.name,
|
|
1277
|
+
...this.subtitle === "" ? {} : { subtitle: this.subtitle },
|
|
1190
1278
|
description: this.description,
|
|
1191
1279
|
groupName: this.groupName,
|
|
1192
1280
|
color: this.color,
|
|
@@ -1208,12 +1296,13 @@ var SSDiagram = (() => {
|
|
|
1208
1296
|
}
|
|
1209
1297
|
};
|
|
1210
1298
|
var LinkModel = class {
|
|
1211
|
-
constructor(from, fromPort, to, toPort, id = "", metadata, style = "solid") {
|
|
1299
|
+
constructor(from, fromPort, to, toPort, id = "", metadata, style = "solid", label2 = "") {
|
|
1212
1300
|
this.from = from;
|
|
1213
1301
|
this.fromPort = fromPort;
|
|
1214
1302
|
this.to = to;
|
|
1215
1303
|
this.toPort = toPort;
|
|
1216
1304
|
this.style = style;
|
|
1305
|
+
this.label = label2;
|
|
1217
1306
|
this.id = id;
|
|
1218
1307
|
this.metadata = copyJsonObject(metadata);
|
|
1219
1308
|
}
|
|
@@ -1228,6 +1317,7 @@ var SSDiagram = (() => {
|
|
|
1228
1317
|
to: this.to,
|
|
1229
1318
|
toPort: this.toPort,
|
|
1230
1319
|
style: this.style,
|
|
1320
|
+
...this.label === "" ? {} : { label: this.label },
|
|
1231
1321
|
metadata: copyJsonObject(this.metadata)
|
|
1232
1322
|
};
|
|
1233
1323
|
}
|
|
@@ -1248,6 +1338,15 @@ var SSDiagram = (() => {
|
|
|
1248
1338
|
var INTRO_RISE = 70;
|
|
1249
1339
|
var ERROR_FLASH_MS = 1100;
|
|
1250
1340
|
var ERROR_RED = "#f6465d";
|
|
1341
|
+
var DEFAULT_NODE_COLOR = "#d7d7d7";
|
|
1342
|
+
var DEFAULT_NODE_BORDER = "#8c8c8c";
|
|
1343
|
+
var DEFAULT_ZONE_TINT = "#8a8f98";
|
|
1344
|
+
var TITLE_FONT = "600 12px Segoe UI, Tahoma, sans-serif";
|
|
1345
|
+
var SUBTITLE_FONT = "11px Segoe UI, Tahoma, sans-serif";
|
|
1346
|
+
var SUBTITLE_MIN_H = 62;
|
|
1347
|
+
var LINK_LABEL_FONT = "600 10px Segoe UI, Tahoma, sans-serif";
|
|
1348
|
+
var ZONE_CAPTION_TOP = 9;
|
|
1349
|
+
var ZONE_CAPTION_H = 14;
|
|
1251
1350
|
var ERROR_BACKGROUND = "#7d2632";
|
|
1252
1351
|
var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
1253
1352
|
function hashHue(s) {
|
|
@@ -1256,9 +1355,7 @@ var SSDiagram = (() => {
|
|
|
1256
1355
|
return (h % 360 + 360) % 360;
|
|
1257
1356
|
}
|
|
1258
1357
|
function clampLightness(color, maxL) {
|
|
1259
|
-
|
|
1260
|
-
if (m === null) return color;
|
|
1261
|
-
return `hsl(${m[1]}, ${m[2]}%, ${Math.min(+m[3], maxL * 100)}%)`;
|
|
1358
|
+
return cappedLightness(color, maxL);
|
|
1262
1359
|
}
|
|
1263
1360
|
function colorLuminance(color) {
|
|
1264
1361
|
if (color === void 0) return 0;
|
|
@@ -1347,10 +1444,14 @@ var SSDiagram = (() => {
|
|
|
1347
1444
|
this.ovGeo = null;
|
|
1348
1445
|
this.validator = null;
|
|
1349
1446
|
this.handlers = /* @__PURE__ */ new Map();
|
|
1447
|
+
// Computed style of the host, read at most once per frame: colours written as var(--token)
|
|
1448
|
+
// resolve against it, so a frame drawn after a theme switch picks up the new values.
|
|
1449
|
+
this.hostStyle = null;
|
|
1350
1450
|
this.opts = opts;
|
|
1351
1451
|
if (opts.typeColors !== void 0) this.setTypeColorsInternal(opts.typeColors);
|
|
1352
1452
|
this.host = opts.host;
|
|
1353
1453
|
this.gridSnapEnabled = opts.gridSnap ?? false;
|
|
1454
|
+
this.pageScroll = opts.pageScroll ?? false;
|
|
1354
1455
|
this.gridSize = this.normalizeGridSize(opts.gridSize ?? DEFAULT_GRID_SIZE);
|
|
1355
1456
|
this.history = new DiagramCommandHistory(() => {
|
|
1356
1457
|
this.emit("undoStackChanged", { canUndo: this.canUndo(), canRedo: this.canRedo() });
|
|
@@ -1359,7 +1460,7 @@ var SSDiagram = (() => {
|
|
|
1359
1460
|
this.canvas.style.display = "block";
|
|
1360
1461
|
this.canvas.tabIndex = 0;
|
|
1361
1462
|
this.canvas.style.outline = "none";
|
|
1362
|
-
this.
|
|
1463
|
+
this.applyTouchAction();
|
|
1363
1464
|
this.canvas.style.userSelect = "none";
|
|
1364
1465
|
this.canvas.style.webkitUserSelect = "none";
|
|
1365
1466
|
this.canvas.style.webkitTouchCallout = "none";
|
|
@@ -1413,6 +1514,19 @@ var SSDiagram = (() => {
|
|
|
1413
1514
|
this.gridSize = nextSize;
|
|
1414
1515
|
this.scheduleDraw();
|
|
1415
1516
|
}
|
|
1517
|
+
/** See DiagramOptions.pageScroll. */
|
|
1518
|
+
setPageScroll(enabled) {
|
|
1519
|
+
this.pageScroll = enabled;
|
|
1520
|
+
this.applyTouchAction();
|
|
1521
|
+
}
|
|
1522
|
+
isPageScrollEnabled() {
|
|
1523
|
+
return this.pageScroll;
|
|
1524
|
+
}
|
|
1525
|
+
// pan-y hands a vertical swipe to the browser, which scrolls the page; a horizontal one and a
|
|
1526
|
+
// two-finger pinch still reach the canvas.
|
|
1527
|
+
applyTouchAction() {
|
|
1528
|
+
this.canvas.style.touchAction = this.pageScroll ? "pan-y" : "none";
|
|
1529
|
+
}
|
|
1416
1530
|
getGridSnap() {
|
|
1417
1531
|
return { enabled: this.gridSnapEnabled, size: this.gridSize };
|
|
1418
1532
|
}
|
|
@@ -1628,7 +1742,7 @@ var SSDiagram = (() => {
|
|
|
1628
1742
|
this.emit("linkValidation", { fromNode: fn, from: fp, toNode: tn, to: tp, ...validation });
|
|
1629
1743
|
if (!validation.allowed) return false;
|
|
1630
1744
|
}
|
|
1631
|
-
const link = new LinkModel(init.from, init.fromPort, init.to, init.toPort, init.id ?? this.nextLinkId(), init.metadata, init.style ?? "solid");
|
|
1745
|
+
const link = new LinkModel(init.from, init.fromPort, init.to, init.toPort, init.id ?? this.nextLinkId(), init.metadata, init.style ?? "solid", init.label ?? "");
|
|
1632
1746
|
if (this.links.some((l) => l.id === link.id)) return false;
|
|
1633
1747
|
this.links.splice(clamp(Math.trunc(index), 0, this.links.length), 0, link);
|
|
1634
1748
|
this.emit("linkAdded", { link });
|
|
@@ -1900,6 +2014,7 @@ var SSDiagram = (() => {
|
|
|
1900
2014
|
updateNode(nodeId, patch) {
|
|
1901
2015
|
return this.updateNodeState(nodeId, "update node", (node) => {
|
|
1902
2016
|
if (patch.name !== void 0) node.name = patch.name;
|
|
2017
|
+
if (patch.subtitle !== void 0) node.subtitle = patch.subtitle;
|
|
1903
2018
|
if (patch.description !== void 0) node.description = patch.description;
|
|
1904
2019
|
if (patch.color !== void 0) node.color = patch.color;
|
|
1905
2020
|
if (patch.border !== void 0) node.border = patch.border;
|
|
@@ -1945,6 +2060,7 @@ var SSDiagram = (() => {
|
|
|
1945
2060
|
const restored = new NodeModel(snapshot, nodeId);
|
|
1946
2061
|
node.typeId = restored.typeId;
|
|
1947
2062
|
node.name = restored.name;
|
|
2063
|
+
node.subtitle = restored.subtitle;
|
|
1948
2064
|
node.description = restored.description;
|
|
1949
2065
|
node.groupName = restored.groupName;
|
|
1950
2066
|
node.color = restored.color;
|
|
@@ -1975,6 +2091,10 @@ var SSDiagram = (() => {
|
|
|
1975
2091
|
return true;
|
|
1976
2092
|
}
|
|
1977
2093
|
load(nodes, links) {
|
|
2094
|
+
this.loadModel(nodes, links, []);
|
|
2095
|
+
}
|
|
2096
|
+
// Zones are placed before the view is fitted, so the first frame frames them too.
|
|
2097
|
+
loadModel(nodes, links, zones) {
|
|
1978
2098
|
this.clear();
|
|
1979
2099
|
this.idSeq = 1;
|
|
1980
2100
|
this.linkSeq = 1;
|
|
@@ -2001,9 +2121,10 @@ var SSDiagram = (() => {
|
|
|
2001
2121
|
if (this.links.some((existing) => existing.id === id)) {
|
|
2002
2122
|
throw new Error(`ssdiagram: duplicate link id "${id}"`);
|
|
2003
2123
|
}
|
|
2004
|
-
const model = new LinkModel(link.from, link.fromPort, link.to, link.toPort, id, link.metadata, link.style ?? "solid");
|
|
2124
|
+
const model = new LinkModel(link.from, link.fromPort, link.to, link.toPort, id, link.metadata, link.style ?? "solid", link.label ?? "");
|
|
2005
2125
|
if (!this.links.some((existing) => existing.key() === model.key())) this.links.push(model);
|
|
2006
2126
|
}
|
|
2127
|
+
this.zones = zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) }));
|
|
2007
2128
|
for (const node of this.nodes) {
|
|
2008
2129
|
if (node.loadError.length === 0) continue;
|
|
2009
2130
|
const state = createDiagramNodeRuntimeState();
|
|
@@ -2026,13 +2147,14 @@ var SSDiagram = (() => {
|
|
|
2026
2147
|
to: l.to,
|
|
2027
2148
|
toPort: l.toPort,
|
|
2028
2149
|
style: l.style,
|
|
2150
|
+
...l.label === "" ? {} : { label: l.label },
|
|
2029
2151
|
metadata: copyJsonObject(l.metadata)
|
|
2030
2152
|
}))
|
|
2031
2153
|
};
|
|
2032
2154
|
}
|
|
2033
2155
|
loadDocument(source) {
|
|
2034
2156
|
const document2 = parseDiagramDocument(source);
|
|
2035
|
-
this.
|
|
2157
|
+
this.loadModel(
|
|
2036
2158
|
document2.nodes,
|
|
2037
2159
|
document2.links.map((link) => ({
|
|
2038
2160
|
id: link.id,
|
|
@@ -2041,10 +2163,11 @@ var SSDiagram = (() => {
|
|
|
2041
2163
|
to: link.to.nodeId,
|
|
2042
2164
|
toPort: link.to.portId,
|
|
2043
2165
|
style: link.style,
|
|
2166
|
+
label: link.label,
|
|
2044
2167
|
metadata: link.metadata
|
|
2045
|
-
}))
|
|
2168
|
+
})),
|
|
2169
|
+
document2.zones
|
|
2046
2170
|
);
|
|
2047
|
-
this.zones = document2.zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) }));
|
|
2048
2171
|
this.documentMetadata = copyJsonObject(document2.metadata);
|
|
2049
2172
|
}
|
|
2050
2173
|
saveDocument() {
|
|
@@ -2055,6 +2178,7 @@ var SSDiagram = (() => {
|
|
|
2055
2178
|
from: { nodeId: link.from, portId: link.fromPort },
|
|
2056
2179
|
to: { nodeId: link.to, portId: link.toPort },
|
|
2057
2180
|
style: link.style,
|
|
2181
|
+
label: link.label,
|
|
2058
2182
|
metadata: link.metadata
|
|
2059
2183
|
})),
|
|
2060
2184
|
zones: this.zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) })),
|
|
@@ -2481,8 +2605,10 @@ var SSDiagram = (() => {
|
|
|
2481
2605
|
if (t2.overviewViewportFill !== void 0) this.opts.overviewViewportFill = t2.overviewViewportFill;
|
|
2482
2606
|
this.scheduleDraw();
|
|
2483
2607
|
}
|
|
2608
|
+
// Zones count: fitting or exporting only the nodes cut off the places they sit in, captions
|
|
2609
|
+
// included. A caption that is taller than its zone still has to fit.
|
|
2484
2610
|
graphBounds() {
|
|
2485
|
-
if (this.nodes.length === 0) return null;
|
|
2611
|
+
if (this.nodes.length === 0 && this.zones.length === 0) return null;
|
|
2486
2612
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2487
2613
|
for (const n2 of this.nodes) {
|
|
2488
2614
|
minX = Math.min(minX, n2.x);
|
|
@@ -2490,6 +2616,13 @@ var SSDiagram = (() => {
|
|
|
2490
2616
|
maxX = Math.max(maxX, n2.x + n2.w);
|
|
2491
2617
|
maxY = Math.max(maxY, n2.y + n2.h);
|
|
2492
2618
|
}
|
|
2619
|
+
for (const zone of this.zones) {
|
|
2620
|
+
const bottom = zone.name.length > 0 ? Math.max(zone.height, ZONE_CAPTION_TOP + ZONE_CAPTION_H) : zone.height;
|
|
2621
|
+
minX = Math.min(minX, zone.x - 1);
|
|
2622
|
+
minY = Math.min(minY, zone.y - 1);
|
|
2623
|
+
maxX = Math.max(maxX, zone.x + zone.width + 1);
|
|
2624
|
+
maxY = Math.max(maxY, zone.y + bottom + 1);
|
|
2625
|
+
}
|
|
2493
2626
|
return { minX, minY, maxX, maxY };
|
|
2494
2627
|
}
|
|
2495
2628
|
resize(w, h) {
|
|
@@ -2541,11 +2674,16 @@ var SSDiagram = (() => {
|
|
|
2541
2674
|
// ---- geometry ---------------------------------------------------
|
|
2542
2675
|
layoutNode(n2) {
|
|
2543
2676
|
const ctx = this.ctx;
|
|
2544
|
-
ctx.font =
|
|
2677
|
+
ctx.font = TITLE_FONT;
|
|
2545
2678
|
const titleW = ctx.measureText(n2.name).width;
|
|
2679
|
+
let subtitleW = 0;
|
|
2680
|
+
if (n2.subtitle.length > 0) {
|
|
2681
|
+
ctx.font = SUBTITLE_FONT;
|
|
2682
|
+
subtitleW = ctx.measureText(n2.subtitle).width;
|
|
2683
|
+
}
|
|
2546
2684
|
const rows = Math.max(n2.inPorts.length, n2.outPorts.length, 1);
|
|
2547
|
-
n2.w = Math.max(170, Math.ceil(titleW) + 28);
|
|
2548
|
-
n2.h = Math.max(48, rows * PORT_ROW_H + 16);
|
|
2685
|
+
n2.w = Math.max(170, Math.ceil(titleW) + 28, Math.ceil(subtitleW) + 28);
|
|
2686
|
+
n2.h = Math.max(n2.subtitle.length > 0 ? SUBTITLE_MIN_H : 48, rows * PORT_ROW_H + 16);
|
|
2549
2687
|
const off = PORT_SQ / 2;
|
|
2550
2688
|
const place = (ports, x) => {
|
|
2551
2689
|
const stack = ports.some(isGrowAnchor) ? [...ports.filter((p) => !isGrowAnchor(p)), ...ports.filter(isGrowAnchor)] : ports;
|
|
@@ -2587,6 +2725,15 @@ var SSDiagram = (() => {
|
|
|
2587
2725
|
if (p.direction !== "in" || p.type === "" || !isWildcardPortType(p.type)) return p.type;
|
|
2588
2726
|
return this.portFeed.get(p) ?? p.type;
|
|
2589
2727
|
}
|
|
2728
|
+
/// A type's colour as a wire.
|
|
2729
|
+
///
|
|
2730
|
+
/// The same colour a socket of that type is filled with, moved into the band the canvas
|
|
2731
|
+
/// leaves room for. A socket can afford the host's exact colour because it is a filled
|
|
2732
|
+
/// square with an outline; a wire is a thin line with nothing behind it, and one drawn in
|
|
2733
|
+
/// #000000 on a dark canvas is not there at all.
|
|
2734
|
+
linkColor(type) {
|
|
2735
|
+
return legibleOn(this.portColor(type), this.opts.background ?? "#1b1b1f");
|
|
2736
|
+
}
|
|
2590
2737
|
portColor(type) {
|
|
2591
2738
|
const maxL = this.opts.linkMaxLightness;
|
|
2592
2739
|
const light = maxL !== void 0 && maxL < 0.5;
|
|
@@ -2895,6 +3042,7 @@ var SSDiagram = (() => {
|
|
|
2895
3042
|
// The clipboard is built from saveDocument(), so it carries the style;
|
|
2896
3043
|
// rebuilding without it silently turned every pasted dashed link solid.
|
|
2897
3044
|
style: l.style,
|
|
3045
|
+
label: l.label,
|
|
2898
3046
|
metadata: l.metadata
|
|
2899
3047
|
});
|
|
2900
3048
|
}
|
|
@@ -3280,6 +3428,7 @@ var SSDiagram = (() => {
|
|
|
3280
3428
|
this.listen(window, "pointerup", finish);
|
|
3281
3429
|
this.listen(window, "pointercancel", finish);
|
|
3282
3430
|
this.listen(this.canvas, "wheel", (e) => {
|
|
3431
|
+
if (this.pageScroll && !e.ctrlKey && !e.metaKey) return;
|
|
3283
3432
|
e.preventDefault();
|
|
3284
3433
|
const [sx, sy] = localXY(e);
|
|
3285
3434
|
const [wx, wy] = this.toWorld(sx, sy);
|
|
@@ -3425,6 +3574,7 @@ var SSDiagram = (() => {
|
|
|
3425
3574
|
}
|
|
3426
3575
|
draw(options = SCREEN_RENDER_OPTIONS) {
|
|
3427
3576
|
const ctx = this.ctx;
|
|
3577
|
+
this.hostStyle = null;
|
|
3428
3578
|
ctx.clearRect(0, 0, this.width, this.height);
|
|
3429
3579
|
const background = this.opts.background ?? "#1b1b1f";
|
|
3430
3580
|
ctx.fillStyle = background;
|
|
@@ -3452,19 +3602,21 @@ var SSDiagram = (() => {
|
|
|
3452
3602
|
);
|
|
3453
3603
|
this.drawZones();
|
|
3454
3604
|
const prior = [];
|
|
3605
|
+
const labels = [];
|
|
3455
3606
|
const hoveredNode = this.hoverPort?.node ?? this.hoverNode;
|
|
3456
3607
|
for (const l of this.links) {
|
|
3457
3608
|
const a = this.endpoint(l, "from");
|
|
3458
3609
|
const b = this.endpoint(l, "to");
|
|
3459
3610
|
if (a === null || b === null) continue;
|
|
3460
3611
|
const fp = this.nodes.find((n2) => n2.id === l.from)?.outPorts.find((p) => p.id === l.fromPort);
|
|
3461
|
-
const baseColor = (this.opts.typedLinkColors ?? false) && fp !== void 0 ? this.
|
|
3612
|
+
const baseColor = (this.opts.typedLinkColors ?? false) && fp !== void 0 ? this.linkColor(fp.type) : this.portColor("");
|
|
3462
3613
|
const state = options.selection && l === this.selectedLink ? "sel" : options.transient && (l === this.hoveredLink || hoveredNode !== null && (l.from === hoveredNode.id || l.to === hoveredNode.id)) ? "hov" : "norm";
|
|
3463
3614
|
const color = state === "sel" ? this.selectionColor() : state === "hov" ? this.linkHoverColor() : baseColor;
|
|
3464
3615
|
const width = state === "sel" ? 3 : state === "hov" ? 2.6 : 2;
|
|
3465
3616
|
const pts = this.routeLink(a, b, /* @__PURE__ */ new Set([l.from, l.to]));
|
|
3466
3617
|
this.strokeRoute(pts, color, width, prior, l.style);
|
|
3467
3618
|
this.drawArrow(pts, color);
|
|
3619
|
+
if (l.label.length > 0) labels.push({ pts, text: l.label, color });
|
|
3468
3620
|
for (let k = 1; k < pts.length; k += 1) {
|
|
3469
3621
|
const [x1, y1] = pts[k - 1];
|
|
3470
3622
|
const [x2, y2] = pts[k];
|
|
@@ -3472,6 +3624,7 @@ var SSDiagram = (() => {
|
|
|
3472
3624
|
else if (y1 === y2 && x1 !== x2) prior.push({ h: true, c: y1, a: Math.min(x1, x2), b: Math.max(x1, x2) });
|
|
3473
3625
|
}
|
|
3474
3626
|
}
|
|
3627
|
+
for (const label2 of labels) this.drawLinkLabel(label2.pts, label2.text, label2.color, background);
|
|
3475
3628
|
if (options.transient && (this.linking !== null || this.relinking !== null)) this.drawPendingLink();
|
|
3476
3629
|
this.portFeed = this.feedTypes();
|
|
3477
3630
|
for (const n2 of this.nodes) this.drawNode(n2, options.selection && this.selectedNodes.has(n2), options);
|
|
@@ -3626,8 +3779,9 @@ ${runtime.error}`;
|
|
|
3626
3779
|
drawZones() {
|
|
3627
3780
|
if (this.zones.length === 0) return;
|
|
3628
3781
|
const ctx = this.ctx;
|
|
3782
|
+
const defaultTint = this.resolveColor(this.opts.zoneColor ?? DEFAULT_ZONE_TINT, DEFAULT_ZONE_TINT);
|
|
3629
3783
|
for (const zone of this.zones) {
|
|
3630
|
-
const tint = zone.color.length > 0 ? zone.color :
|
|
3784
|
+
const tint = zone.color.length > 0 ? this.resolveColor(zone.color, defaultTint) : defaultTint;
|
|
3631
3785
|
ctx.save();
|
|
3632
3786
|
roundRect(ctx, zone.x, zone.y, zone.width, zone.height, 10);
|
|
3633
3787
|
ctx.globalAlpha = 0.12;
|
|
@@ -3643,10 +3797,10 @@ ${runtime.error}`;
|
|
|
3643
3797
|
ctx.save();
|
|
3644
3798
|
ctx.globalAlpha = 0.75;
|
|
3645
3799
|
ctx.fillStyle = tint;
|
|
3646
|
-
ctx.font =
|
|
3800
|
+
ctx.font = TITLE_FONT;
|
|
3647
3801
|
ctx.textAlign = "left";
|
|
3648
3802
|
ctx.textBaseline = "top";
|
|
3649
|
-
ctx.fillText(zone.name, zone.x + 12, zone.y +
|
|
3803
|
+
ctx.fillText(zone.name, zone.x + 12, zone.y + ZONE_CAPTION_TOP, Math.max(0, zone.width - 24));
|
|
3650
3804
|
ctx.restore();
|
|
3651
3805
|
}
|
|
3652
3806
|
}
|
|
@@ -3691,11 +3845,12 @@ ${runtime.error}`;
|
|
|
3691
3845
|
const hasRuntimeError = options.runtime && n2.runtimeError.length > 0;
|
|
3692
3846
|
const runtime = options.runtime ? getJsonKey(this.runtimeState.nodes, n2.id) : void 0;
|
|
3693
3847
|
const active = options.runtime && (this.runtimeState.activeNodeId === n2.id || runtime?.active === true);
|
|
3848
|
+
const fill = this.resolveColor(n2.color, DEFAULT_NODE_COLOR);
|
|
3694
3849
|
roundRect(ctx, n2.x, n2.y, n2.w, n2.h, 6);
|
|
3695
|
-
ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" :
|
|
3850
|
+
ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" : fill;
|
|
3696
3851
|
ctx.fill();
|
|
3697
3852
|
ctx.lineWidth = selected ? 2 : 1.5;
|
|
3698
|
-
ctx.strokeStyle = selected ? this.selectionColor() : n2.border;
|
|
3853
|
+
ctx.strokeStyle = selected ? this.selectionColor() : this.resolveColor(n2.border, DEFAULT_NODE_BORDER);
|
|
3699
3854
|
ctx.stroke();
|
|
3700
3855
|
if (hasLoadError || hasRuntimeError) {
|
|
3701
3856
|
let alpha = 1;
|
|
@@ -3728,11 +3883,22 @@ ${runtime.error}`;
|
|
|
3728
3883
|
}
|
|
3729
3884
|
}
|
|
3730
3885
|
const titleShift = n2.icon ? iconW + 4 : 0;
|
|
3731
|
-
|
|
3732
|
-
|
|
3886
|
+
const textX = n2.x + titleShift + (n2.w - titleShift) / 2;
|
|
3887
|
+
const textWidth = n2.w - titleShift - 14;
|
|
3888
|
+
ctx.fillStyle = hasLoadError ? "#ffffff" : readableTextOn(active ? "#ffd1dc" : fill);
|
|
3889
|
+
ctx.font = TITLE_FONT;
|
|
3733
3890
|
ctx.textAlign = "center";
|
|
3734
3891
|
ctx.textBaseline = "middle";
|
|
3735
|
-
|
|
3892
|
+
if (n2.subtitle.length === 0) {
|
|
3893
|
+
ctx.fillText(n2.name, textX, n2.y + n2.h / 2, textWidth);
|
|
3894
|
+
} else {
|
|
3895
|
+
ctx.fillText(n2.name, textX, n2.y + n2.h / 2 - 8, textWidth);
|
|
3896
|
+
ctx.save();
|
|
3897
|
+
ctx.globalAlpha *= 0.72;
|
|
3898
|
+
ctx.font = SUBTITLE_FONT;
|
|
3899
|
+
ctx.fillText(n2.subtitle, textX, n2.y + n2.h / 2 + 9, textWidth);
|
|
3900
|
+
ctx.restore();
|
|
3901
|
+
}
|
|
3736
3902
|
for (const p of n2.inPorts) this.drawPort(n2, p, options);
|
|
3737
3903
|
for (const p of n2.outPorts) this.drawPort(n2, p, options);
|
|
3738
3904
|
}
|
|
@@ -3849,6 +4015,45 @@ ${runtime.error}`;
|
|
|
3849
4015
|
ctx.fill();
|
|
3850
4016
|
ctx.restore();
|
|
3851
4017
|
}
|
|
4018
|
+
// A plate of the canvas colour under the text, edged in the wire's colour: the label reads the same
|
|
4019
|
+
// whatever the wire crosses, and still says which wire it belongs to.
|
|
4020
|
+
drawLinkLabel(pts, text, color, background) {
|
|
4021
|
+
const [x, y] = pathMidpoint(pts);
|
|
4022
|
+
const ctx = this.ctx;
|
|
4023
|
+
ctx.save();
|
|
4024
|
+
ctx.font = LINK_LABEL_FONT;
|
|
4025
|
+
const w = Math.ceil(ctx.measureText(text).width) + 12;
|
|
4026
|
+
const h = 16;
|
|
4027
|
+
roundRect(ctx, x - w / 2, y - h / 2, w, h, h / 2);
|
|
4028
|
+
ctx.fillStyle = background;
|
|
4029
|
+
ctx.fill();
|
|
4030
|
+
ctx.lineWidth = 1;
|
|
4031
|
+
ctx.strokeStyle = color;
|
|
4032
|
+
ctx.stroke();
|
|
4033
|
+
ctx.fillStyle = readableTextOn(background);
|
|
4034
|
+
ctx.textAlign = "center";
|
|
4035
|
+
ctx.textBaseline = "middle";
|
|
4036
|
+
ctx.fillText(text, x, y);
|
|
4037
|
+
ctx.restore();
|
|
4038
|
+
}
|
|
4039
|
+
// Scheme colours may name a custom property of the page, var(--token), which a canvas cannot
|
|
4040
|
+
// paint. They resolve against the host element; one that resolves to nothing and has no
|
|
4041
|
+
// fallback of its own gets the renderer's default for that slot.
|
|
4042
|
+
resolveColor(value, fallback) {
|
|
4043
|
+
if (!value.includes("var(")) return value;
|
|
4044
|
+
return resolveCssVariables(value, (name) => this.hostProperty(name)) ?? fallback;
|
|
4045
|
+
}
|
|
4046
|
+
hostProperty(name) {
|
|
4047
|
+
if (this.hostStyle === null) {
|
|
4048
|
+
if (typeof getComputedStyle !== "function") return "";
|
|
4049
|
+
try {
|
|
4050
|
+
this.hostStyle = getComputedStyle(this.host);
|
|
4051
|
+
} catch {
|
|
4052
|
+
return "";
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
return this.hostStyle.getPropertyValue(name);
|
|
4056
|
+
}
|
|
3852
4057
|
drawSelectedLinkEndpoints() {
|
|
3853
4058
|
if (this.selectedLink === null) return;
|
|
3854
4059
|
const ctx = this.ctx;
|
|
@@ -3906,7 +4111,7 @@ ${runtime.error}`;
|
|
|
3906
4111
|
}
|
|
3907
4112
|
if (snapped !== null) excludedNodes.add(snapped.node.id);
|
|
3908
4113
|
const points = this.routeLink(a, b, excludedNodes);
|
|
3909
|
-
ctx.strokeStyle = this.
|
|
4114
|
+
ctx.strokeStyle = this.linkColor(colorType);
|
|
3910
4115
|
if (snapped !== null) {
|
|
3911
4116
|
ctx.setLineDash([]);
|
|
3912
4117
|
ctx.lineWidth = 2.6;
|
|
@@ -3964,9 +4169,9 @@ ${runtime.error}`;
|
|
|
3964
4169
|
ctx.stroke();
|
|
3965
4170
|
ctx.clip();
|
|
3966
4171
|
for (const n2 of this.nodes) {
|
|
3967
|
-
ctx.fillStyle = n2.color;
|
|
4172
|
+
ctx.fillStyle = this.resolveColor(n2.color, DEFAULT_NODE_COLOR);
|
|
3968
4173
|
ctx.fillRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
|
|
3969
|
-
ctx.strokeStyle = n2.border;
|
|
4174
|
+
ctx.strokeStyle = this.resolveColor(n2.border, DEFAULT_NODE_BORDER);
|
|
3970
4175
|
ctx.lineWidth = 1;
|
|
3971
4176
|
ctx.strokeRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
|
|
3972
4177
|
}
|
|
@@ -4007,6 +4212,23 @@ ${runtime.error}`;
|
|
|
4007
4212
|
ctx.arcTo(x, y, x + w, y, rr);
|
|
4008
4213
|
ctx.closePath();
|
|
4009
4214
|
}
|
|
4215
|
+
function pathMidpoint(pts) {
|
|
4216
|
+
let total = 0;
|
|
4217
|
+
for (let i = 1; i < pts.length; i += 1) total += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
|
|
4218
|
+
let remaining = total / 2;
|
|
4219
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
4220
|
+
const [x1, y1] = pts[i - 1];
|
|
4221
|
+
const [x2, y2] = pts[i];
|
|
4222
|
+
const length = Math.hypot(x2 - x1, y2 - y1);
|
|
4223
|
+
if (length > 0 && remaining <= length) {
|
|
4224
|
+
const t2 = remaining / length;
|
|
4225
|
+
return [x1 + (x2 - x1) * t2, y1 + (y2 - y1) * t2];
|
|
4226
|
+
}
|
|
4227
|
+
remaining -= length;
|
|
4228
|
+
}
|
|
4229
|
+
const last = pts[pts.length - 1];
|
|
4230
|
+
return [last[0], last[1]];
|
|
4231
|
+
}
|
|
4010
4232
|
function route(a, b) {
|
|
4011
4233
|
const [ax, ay] = a;
|
|
4012
4234
|
const [bx, by] = b;
|
|
@@ -4991,6 +5213,7 @@ ${runtime.error}`;
|
|
|
4991
5213
|
this.fullscreen = false;
|
|
4992
5214
|
this.fullscreenButtonVisible = true;
|
|
4993
5215
|
this.downloadButtonVisible = false;
|
|
5216
|
+
this.exportEnabled = true;
|
|
4994
5217
|
this.downloadLabel = null;
|
|
4995
5218
|
// null means the host never named the button, so its wording comes from the shared bundle.
|
|
4996
5219
|
// Keeping the two apart is what lets an explicit label win while an untouched control still
|
|
@@ -5007,7 +5230,8 @@ ${runtime.error}`;
|
|
|
5007
5230
|
host: this.div,
|
|
5008
5231
|
typeColors: this.portTypeColors(),
|
|
5009
5232
|
gridSnap: options.gridSnap ?? true,
|
|
5010
|
-
gridSize: options.gridSize
|
|
5233
|
+
gridSize: options.gridSize,
|
|
5234
|
+
pageScroll: options.pageScroll ?? false
|
|
5011
5235
|
});
|
|
5012
5236
|
this.fullscreenButtonVisible = options.showFullscreenButton ?? true;
|
|
5013
5237
|
this.fullscreenLabels = options.fullscreenLabels ?? null;
|
|
@@ -5085,6 +5309,13 @@ ${runtime.error}`;
|
|
|
5085
5309
|
getGridSnap() {
|
|
5086
5310
|
return this.canvas.getGridSnap();
|
|
5087
5311
|
}
|
|
5312
|
+
/** See DiagramOptions.pageScroll. */
|
|
5313
|
+
setPageScroll(enabled) {
|
|
5314
|
+
this.canvas.setPageScroll(enabled);
|
|
5315
|
+
}
|
|
5316
|
+
isPageScrollEnabled() {
|
|
5317
|
+
return this.canvas.isPageScrollEnabled();
|
|
5318
|
+
}
|
|
5088
5319
|
nudgeSelection(dx, dy) {
|
|
5089
5320
|
return this.canvas.nudgeSelection(dx, dy);
|
|
5090
5321
|
}
|
|
@@ -5266,6 +5497,17 @@ ${runtime.error}`;
|
|
|
5266
5497
|
this.downloadButton.hidden = false;
|
|
5267
5498
|
this.downloadButton.style.display = "inline-flex";
|
|
5268
5499
|
}
|
|
5500
|
+
/**
|
|
5501
|
+
* Turns the export commands of the context menu on or off. Off, the menu offers them greyed out and
|
|
5502
|
+
* `executeContextCommand` refuses them, for a page that does not want its diagram saved. The
|
|
5503
|
+
* download button is separate: hide it with `setDownloadButtonVisible(false)`.
|
|
5504
|
+
*/
|
|
5505
|
+
setExportEnabled(enabled) {
|
|
5506
|
+
this.exportEnabled = enabled;
|
|
5507
|
+
}
|
|
5508
|
+
isExportEnabled() {
|
|
5509
|
+
return this.exportEnabled;
|
|
5510
|
+
}
|
|
5269
5511
|
isDownloadButtonVisible() {
|
|
5270
5512
|
return this.downloadButtonVisible;
|
|
5271
5513
|
}
|
|
@@ -5767,15 +6009,15 @@ ${runtime.error}`;
|
|
|
5767
6009
|
// picture, and an empty diagram has none to give. They stay on in read-only mode,
|
|
5768
6010
|
// which forbids changing the diagram, not looking at it.
|
|
5769
6011
|
exportDocument: {
|
|
5770
|
-
canExecute: ({ document: document2 }) => document2.nodes.length > 0,
|
|
6012
|
+
canExecute: ({ document: document2 }) => this.exportEnabled && document2.nodes.length > 0,
|
|
5771
6013
|
execute: () => this.emit("exportRequested", { format: "document" })
|
|
5772
6014
|
},
|
|
5773
6015
|
exportPng: {
|
|
5774
|
-
canExecute: ({ document: document2 }) => document2.nodes.length > 0,
|
|
6016
|
+
canExecute: ({ document: document2 }) => this.exportEnabled && document2.nodes.length > 0,
|
|
5775
6017
|
execute: () => this.emit("exportRequested", { format: "png" })
|
|
5776
6018
|
},
|
|
5777
6019
|
exportSvg: {
|
|
5778
|
-
canExecute: ({ document: document2 }) => document2.nodes.length > 0,
|
|
6020
|
+
canExecute: ({ document: document2 }) => this.exportEnabled && document2.nodes.length > 0,
|
|
5779
6021
|
execute: () => this.emit("exportRequested", { format: "svg" })
|
|
5780
6022
|
},
|
|
5781
6023
|
// Always available: it shows the overview as readily as it hides it, and an empty
|
|
@@ -6411,17 +6653,62 @@ ${runtime.error}`;
|
|
|
6411
6653
|
if (renderRevisions.get(div) !== revision)
|
|
6412
6654
|
return null;
|
|
6413
6655
|
const catalog = buildCatalog(palette);
|
|
6656
|
+
return mountDiagram(div, catalog, options, false, (diagram) => {
|
|
6657
|
+
const missingTypeTemplate = div.dataset.diagramMissingElement ?? t("embedMissingElement", 'Element type "{typeId}" is missing from the palette.');
|
|
6658
|
+
const converted = toDiagramNodes(
|
|
6659
|
+
scheme,
|
|
6660
|
+
catalog,
|
|
6661
|
+
(typeId) => missingTypeTemplate.replace("{typeId}", typeId || "(empty)")
|
|
6662
|
+
);
|
|
6663
|
+
diagram.load(
|
|
6664
|
+
converted.nodes,
|
|
6665
|
+
scheme.links.map((link) => new Link({
|
|
6666
|
+
outNode: link.from,
|
|
6667
|
+
outPort: link.fromPort,
|
|
6668
|
+
inNode: link.to,
|
|
6669
|
+
inPort: link.toPort
|
|
6670
|
+
})),
|
|
6671
|
+
{ nodeErrors: converted.nodeErrors }
|
|
6672
|
+
);
|
|
6673
|
+
});
|
|
6674
|
+
}
|
|
6675
|
+
function renderDocument(div, source, options = {}) {
|
|
6676
|
+
const parsed = readDocument(source);
|
|
6677
|
+
beginRender(div);
|
|
6678
|
+
return mountDocument(div, parsed, options);
|
|
6679
|
+
}
|
|
6680
|
+
function readDocument(source) {
|
|
6681
|
+
let value = source;
|
|
6682
|
+
if (typeof source === "string") {
|
|
6683
|
+
try {
|
|
6684
|
+
value = JSON.parse(source);
|
|
6685
|
+
} catch (error) {
|
|
6686
|
+
throw new DiagramDocumentError(error instanceof Error ? error.message : "invalid JSON");
|
|
6687
|
+
}
|
|
6688
|
+
}
|
|
6689
|
+
if (!isRecord(value))
|
|
6690
|
+
throw new DiagramDocumentError("expected an object");
|
|
6691
|
+
if (value.version !== void 0 && value.version !== DIAGRAM_DOCUMENT_VERSION)
|
|
6692
|
+
throw new DiagramDocumentError(`unsupported document version ${String(value.version)}`, "$.version");
|
|
6693
|
+
return createDiagramDocument(value);
|
|
6694
|
+
}
|
|
6695
|
+
function mountDocument(div, source, options) {
|
|
6696
|
+
return mountDiagram(div, new StockSharpCatalog(), options, true, (diagram) => diagram.loadDocument(source));
|
|
6697
|
+
}
|
|
6698
|
+
function mountDiagram(div, catalog, options, pageScrollByDefault, populate) {
|
|
6414
6699
|
disposeActiveRender(div);
|
|
6415
6700
|
div.classList.remove("ss-diagram-error");
|
|
6416
6701
|
div.replaceChildren();
|
|
6417
6702
|
const [enterLabel, exitLabel] = (div.dataset.diagramFullscreen ?? "").split("|");
|
|
6418
6703
|
const downloadLabel = div.dataset.diagramDownload;
|
|
6419
|
-
const
|
|
6704
|
+
const exportAllowed = options.allowExport !== false && div.dataset.diagramExport !== "off";
|
|
6705
|
+
const showDownloadButton = exportAllowed && options.onExportRequested !== void 0;
|
|
6420
6706
|
let diagram;
|
|
6421
6707
|
try {
|
|
6422
6708
|
diagram = new StockSharpDiagram({
|
|
6423
6709
|
div,
|
|
6424
6710
|
catalog,
|
|
6711
|
+
pageScroll: options.pageScroll ?? pageScrollByDefault,
|
|
6425
6712
|
...enterLabel && exitLabel ? { fullscreenLabels: { enter: enterLabel, exit: exitLabel } } : {},
|
|
6426
6713
|
...showDownloadButton ? { showDownloadButton: true } : {},
|
|
6427
6714
|
...downloadLabel ? { downloadLabel } : {}
|
|
@@ -6471,7 +6758,9 @@ ${runtime.error}`;
|
|
|
6471
6758
|
activeRenders.set(div, handle);
|
|
6472
6759
|
connectedRenders.add(handle);
|
|
6473
6760
|
ensureDisconnectedHostObserver();
|
|
6474
|
-
if (
|
|
6761
|
+
if (!exportAllowed) {
|
|
6762
|
+
diagram.setExportEnabled(false);
|
|
6763
|
+
} else if (options.onExportRequested !== void 0) {
|
|
6475
6764
|
const notifyHost = options.onExportRequested;
|
|
6476
6765
|
cleanups.push(diagram.on("exportRequested", (request) => notifyHost(request, handle)));
|
|
6477
6766
|
}
|
|
@@ -6498,22 +6787,7 @@ ${runtime.error}`;
|
|
|
6498
6787
|
diagram.setReadOnly(true);
|
|
6499
6788
|
diagram.setLinkValidator(() => true);
|
|
6500
6789
|
diagram.setOverviewVisible(false);
|
|
6501
|
-
|
|
6502
|
-
const converted = toDiagramNodes(
|
|
6503
|
-
scheme,
|
|
6504
|
-
catalog,
|
|
6505
|
-
(typeId) => missingTypeTemplate.replace("{typeId}", typeId || "(empty)")
|
|
6506
|
-
);
|
|
6507
|
-
diagram.load(
|
|
6508
|
-
converted.nodes,
|
|
6509
|
-
scheme.links.map((link) => new Link({
|
|
6510
|
-
outNode: link.from,
|
|
6511
|
-
outPort: link.fromPort,
|
|
6512
|
-
inNode: link.to,
|
|
6513
|
-
inPort: link.toPort
|
|
6514
|
-
})),
|
|
6515
|
-
{ nodeErrors: converted.nodeErrors }
|
|
6516
|
-
);
|
|
6790
|
+
populate(diagram);
|
|
6517
6791
|
const fit = () => {
|
|
6518
6792
|
if (!div.isConnected) {
|
|
6519
6793
|
handle.destroy();
|
|
@@ -6610,6 +6884,8 @@ ${runtime.error}`;
|
|
|
6610
6884
|
note(div, errors.load, revision);
|
|
6611
6885
|
return null;
|
|
6612
6886
|
}
|
|
6887
|
+
if (holdsDocument(div, raw))
|
|
6888
|
+
return drawDocumentAtRevision(div, raw, revision, options, errors);
|
|
6613
6889
|
const scheme = parseRawScheme(raw);
|
|
6614
6890
|
if (scheme.nodes.length === 0) {
|
|
6615
6891
|
note(div, errors.empty, revision);
|
|
@@ -6632,6 +6908,8 @@ ${runtime.error}`;
|
|
|
6632
6908
|
note(div, errors.empty, revision);
|
|
6633
6909
|
return null;
|
|
6634
6910
|
}
|
|
6911
|
+
if (holdsDocument(div, raw))
|
|
6912
|
+
return drawDocumentAtRevision(div, raw, revision, options, errors);
|
|
6635
6913
|
const scheme = parseRawScheme(raw);
|
|
6636
6914
|
if (scheme.nodes.length === 0) {
|
|
6637
6915
|
note(div, errors.empty, revision);
|
|
@@ -6644,6 +6922,32 @@ ${runtime.error}`;
|
|
|
6644
6922
|
return null;
|
|
6645
6923
|
}
|
|
6646
6924
|
}
|
|
6925
|
+
function holdsDocument(div, raw) {
|
|
6926
|
+
if (div.dataset.diagramKind === "document")
|
|
6927
|
+
return true;
|
|
6928
|
+
return isRecord(raw) && typeof raw.version === "number" && Array.isArray(raw.nodes);
|
|
6929
|
+
}
|
|
6930
|
+
function drawDocumentAtRevision(div, raw, revision, options, errors) {
|
|
6931
|
+
if (renderRevisions.get(div) !== revision)
|
|
6932
|
+
return null;
|
|
6933
|
+
let parsed;
|
|
6934
|
+
try {
|
|
6935
|
+
parsed = readDocument(raw);
|
|
6936
|
+
} catch {
|
|
6937
|
+
note(div, errors.empty, revision);
|
|
6938
|
+
return null;
|
|
6939
|
+
}
|
|
6940
|
+
if (parsed.nodes.length === 0 && parsed.zones.length === 0) {
|
|
6941
|
+
note(div, errors.empty, revision);
|
|
6942
|
+
return null;
|
|
6943
|
+
}
|
|
6944
|
+
try {
|
|
6945
|
+
return mountDocument(div, parsed, options);
|
|
6946
|
+
} catch {
|
|
6947
|
+
note(div, errors.draw, revision);
|
|
6948
|
+
return null;
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6647
6951
|
function renderAll(root = document, options = {}) {
|
|
6648
6952
|
root.querySelectorAll(".ss-diagram-host").forEach((host) => {
|
|
6649
6953
|
if (host.dataset.rendered === "1")
|