@stocksharp/diagram 1.2.0 → 1.2.2

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/dist/ssdiagram.js CHANGED
@@ -796,6 +796,76 @@ var SSDiagram = (() => {
796
796
  }
797
797
  };
798
798
 
799
+ // src/color.ts
800
+ var CSS_COLOR_KEYWORDS = {
801
+ white: [255, 255, 255],
802
+ black: [0, 0, 0],
803
+ transparent: [255, 255, 255]
804
+ };
805
+ function parseRgb(color) {
806
+ const value = color.trim().toLowerCase();
807
+ if (value === "")
808
+ return null;
809
+ const keyword = CSS_COLOR_KEYWORDS[value];
810
+ if (keyword !== void 0)
811
+ return [...keyword];
812
+ const long = value.match(/^#?([0-9a-f]{6})$/);
813
+ if (long !== null) {
814
+ const n2 = parseInt(long[1], 16);
815
+ return [n2 >> 16 & 255, n2 >> 8 & 255, n2 & 255];
816
+ }
817
+ const short = value.match(/^#?([0-9a-f]{3})([0-9a-f])?$/);
818
+ if (short !== null) {
819
+ const [r, g, b] = [...short[1]].map((digit) => parseInt(digit + digit, 16));
820
+ return [r, g, b];
821
+ }
822
+ const long8 = value.match(/^#?([0-9a-f]{6})[0-9a-f]{2}$/);
823
+ if (long8 !== null) {
824
+ const n2 = parseInt(long8[1], 16);
825
+ return [n2 >> 16 & 255, n2 >> 8 & 255, n2 & 255];
826
+ }
827
+ const rgb = value.match(/^rgba?\(([^)]+)\)$/);
828
+ if (rgb !== null) {
829
+ const parts = rgb[1].split(/[\s,/]+/).filter((part) => part !== "");
830
+ if (parts.length >= 3) {
831
+ const [r, g, b] = parts.slice(0, 3).map((part) => part.endsWith("%") ? parseFloat(part) / 100 * 255 : parseFloat(part));
832
+ if ([r, g, b].every((channel) => Number.isFinite(channel)))
833
+ return [r, g, b];
834
+ }
835
+ }
836
+ return null;
837
+ }
838
+ function luminance(color) {
839
+ const rgb = parseRgb(color);
840
+ if (rgb === null)
841
+ return -1;
842
+ const [r, g, b] = rgb;
843
+ return 0.299 * r + 0.587 * g + 0.114 * b;
844
+ }
845
+ var DARK_TEXT = "#1b1b1b";
846
+ var LIGHT_TEXT = "#f5f5f5";
847
+ function toLinear(channel) {
848
+ const v = channel / 255;
849
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
850
+ }
851
+ function relativeLuminance(color) {
852
+ const rgb = parseRgb(color);
853
+ if (rgb === null)
854
+ return -1;
855
+ const [r, g, b] = rgb;
856
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
857
+ }
858
+ function contrast(a, b) {
859
+ const [lighter, darker] = a > b ? [a, b] : [b, a];
860
+ return (lighter + 0.05) / (darker + 0.05);
861
+ }
862
+ function readableTextOn(fill) {
863
+ const background = relativeLuminance(fill);
864
+ if (background < 0)
865
+ return DARK_TEXT;
866
+ return contrast(background, relativeLuminance(LIGHT_TEXT)) > contrast(background, relativeLuminance(DARK_TEXT)) ? LIGHT_TEXT : DARK_TEXT;
867
+ }
868
+
799
869
  // src/core/history.ts
800
870
  var DiagramCommandHistory = class {
801
871
  constructor(listener) {
@@ -1034,6 +1104,9 @@ var SSDiagram = (() => {
1034
1104
  const normalized = normalizePortType(type);
1035
1105
  return normalized === "" || normalized === "*" || normalized === "any" || normalized === "anydata" || normalized === "any data" || normalized === "object" || normalized === "system.object" || normalized.startsWith("system.object,");
1036
1106
  }
1107
+ function isGrowAnchor(port) {
1108
+ return port.isDynamic && !port.isSibling;
1109
+ }
1037
1110
  function arePortTypesCompatible(fromType, toType, availableTypes) {
1038
1111
  if (isWildcardPortType(fromType) || isWildcardPortType(toType)) return true;
1039
1112
  const normalizedFrom = normalizePortType(fromType);
@@ -1241,6 +1314,8 @@ var SSDiagram = (() => {
1241
1314
  this.hoverPort = null;
1242
1315
  this.hoverNode = null;
1243
1316
  this.hoveredLink = null;
1317
+ // Type feeding each wired input, keyed by node and port, rebuilt for every frame.
1318
+ this.portFeed = /* @__PURE__ */ new Map();
1244
1319
  this.tipTimer = null;
1245
1320
  this.tipShow = false;
1246
1321
  this.tipTarget = null;
@@ -1283,8 +1358,20 @@ var SSDiagram = (() => {
1283
1358
  if (ctx === null) throw new Error("ssdiagram: 2d context unavailable");
1284
1359
  this.ctx = ctx;
1285
1360
  this.resize(this.host.clientWidth || 800, this.host.clientHeight || 480);
1361
+ this.observeHostSize();
1286
1362
  this.bind();
1287
1363
  }
1364
+ /// The control creates the canvas, so it keeps it the size of the element it was mounted in.
1365
+ /// A host that lays its panels out after construction - a docking shell, a tab that opens
1366
+ /// hidden - measures nothing at that point, and the fallback size above would then outlive
1367
+ /// the layout: a strip of the panel that draws nothing and answers no clicks. resize()
1368
+ /// ignores anything smaller than two pixels, so a panel switched away from keeps its view.
1369
+ observeHostSize() {
1370
+ if (typeof ResizeObserver === "undefined") return;
1371
+ const observer = new ResizeObserver(() => this.resize(this.host.clientWidth, this.host.clientHeight));
1372
+ observer.observe(this.host);
1373
+ this.domDisposables.push(() => observer.disconnect());
1374
+ }
1288
1375
  // ---- events -----------------------------------------------------
1289
1376
  on(ev, h) {
1290
1377
  let set = this.handlers.get(ev);
@@ -2451,8 +2538,9 @@ var SSDiagram = (() => {
2451
2538
  n2.h = Math.max(48, rows * PORT_ROW_H + 16);
2452
2539
  const off = PORT_SQ / 2;
2453
2540
  const place = (ports, x) => {
2454
- const startY = n2.y + (n2.h - ports.length * PORT_ROW_H) / 2 + PORT_ROW_H / 2;
2455
- ports.forEach((p, i) => {
2541
+ const stack = ports.some(isGrowAnchor) ? [...ports.filter((p) => !isGrowAnchor(p)), ...ports.filter(isGrowAnchor)] : ports;
2542
+ const startY = n2.y + (n2.h - stack.length * PORT_ROW_H) / 2 + PORT_ROW_H / 2;
2543
+ stack.forEach((p, i) => {
2456
2544
  p.cx = x;
2457
2545
  p.cy = startY + i * PORT_ROW_H;
2458
2546
  });
@@ -2469,6 +2557,26 @@ var SSDiagram = (() => {
2469
2557
  toWorld(sx, sy) {
2470
2558
  return [(sx - this.offX) / this.scale, (sy - this.offY) / this.scale];
2471
2559
  }
2560
+ /// Type of the output wired into each connected input. First link wins, which is the one
2561
+ /// drawn first, so a socket accepting several links agrees with the wire on top.
2562
+ feedTypes() {
2563
+ const feed = /* @__PURE__ */ new Map();
2564
+ for (const l of this.links) {
2565
+ const from = this.findNode(l.from)?.outPorts.find((p) => p.id === l.fromPort);
2566
+ if (from === void 0 || isWildcardPortType(from.type)) continue;
2567
+ const to = this.findNode(l.to)?.inPorts.find((p) => p.id === l.toPort);
2568
+ if (to === void 0 || feed.has(to)) continue;
2569
+ feed.set(to, from.type);
2570
+ }
2571
+ return feed;
2572
+ }
2573
+ /// Colour type of a socket: its own, unless it is a wildcard input with something wired in.
2574
+ /// A wildcard promises to take whatever arrives, so once something has arrived the socket
2575
+ /// shows what that is. An untyped socket ('') promises nothing and keeps its neutral fill.
2576
+ portFillType(p) {
2577
+ if (p.direction !== "in" || p.type === "" || !isWildcardPortType(p.type)) return p.type;
2578
+ return this.portFeed.get(p) ?? p.type;
2579
+ }
2472
2580
  portColor(type) {
2473
2581
  const maxL = this.opts.linkMaxLightness;
2474
2582
  const light = maxL !== void 0 && maxL < 0.5;
@@ -2809,10 +2917,36 @@ var SSDiagram = (() => {
2809
2917
  }
2810
2918
  this.lpStart = null;
2811
2919
  }
2812
- // Hit-test at screen coords and emit a contextMenu event. Cancels
2813
- // any partial drag/rubber/link gesture that may have started once
2814
- // the menu opens we don't want a half-drag racing it.
2920
+ /// Ends a node drag the way releasing the pointer does: the whole multi-node move becomes
2921
+ /// one undo step, and every node that actually moved is announced. Nodes are moved in place
2922
+ /// as the pointer travels, so a caller that clears the drag without coming through here
2923
+ /// leaves them where the drag put them with nothing in the history and the host unaware.
2924
+ commitNodeDrag() {
2925
+ if (this.dragNode === null) return;
2926
+ const moves = this.dragStart.map((it) => ({ id: it.n.id, fromX: it.x, fromY: it.y, toX: it.n.x, toY: it.n.y })).filter((m) => m.fromX !== m.toX || m.fromY !== m.toY);
2927
+ const moved = new Set(moves.map((m) => m.id));
2928
+ for (const it of this.dragStart) {
2929
+ if (moved.has(it.n.id)) this.emit("nodeMoved", { node: it.n });
2930
+ }
2931
+ this.dragNode = null;
2932
+ this.dragStart = [];
2933
+ if (moves.length > 0) {
2934
+ this.record({
2935
+ do: () => {
2936
+ for (const m of moves) this.doMoveNode(m.id, m.toX, m.toY);
2937
+ },
2938
+ undo: () => {
2939
+ for (const m of moves) this.doMoveNode(m.id, m.fromX, m.fromY);
2940
+ },
2941
+ label: "drag"
2942
+ });
2943
+ }
2944
+ }
2945
+ // Hit-test at screen coords and emit a contextMenu event. Ends whatever gesture was in
2946
+ // flight — once the menu opens we don't want a half-drag racing it — and a node drag ends
2947
+ // properly rather than being dropped, because its nodes have already moved.
2815
2948
  fireContextMenu(sx, sy, pageX, pageY) {
2949
+ this.commitNodeDrag();
2816
2950
  const [wx, wy] = this.toWorld(sx, sy);
2817
2951
  const port = this.portAt(wx, wy);
2818
2952
  const node = port?.node ?? this.nodeAt(wx, wy);
@@ -2829,11 +2963,14 @@ var SSDiagram = (() => {
2829
2963
  this.dragNode = null;
2830
2964
  this.dragStart = [];
2831
2965
  this.rubber = null;
2832
- this.panning = false;
2833
2966
  this.linking = null;
2834
2967
  this.relinking = null;
2835
2968
  this.relinkCandidate = null;
2836
2969
  this.linkSnap = null;
2970
+ const viewportMoved = this.panning || this.ovDragging;
2971
+ this.panning = false;
2972
+ this.ovDragging = false;
2973
+ if (viewportMoved) this.emitViewChanged(false);
2837
2974
  this.clearHover();
2838
2975
  this.emit("contextMenu", { x: pageX, y: pageY, link, node, port });
2839
2976
  }
@@ -3075,26 +3212,7 @@ var SSDiagram = (() => {
3075
3212
  const finish = (e) => {
3076
3213
  this.cancelLongPress();
3077
3214
  this.relinkCandidate = null;
3078
- if (this.dragNode !== null) {
3079
- const moves = this.dragStart.map((it) => ({ id: it.n.id, fromX: it.x, fromY: it.y, toX: it.n.x, toY: it.n.y })).filter((m) => m.fromX !== m.toX || m.fromY !== m.toY);
3080
- const moved = new Set(moves.map((m) => m.id));
3081
- for (const it of this.dragStart) {
3082
- if (moved.has(it.n.id)) this.emit("nodeMoved", { node: it.n });
3083
- }
3084
- this.dragNode = null;
3085
- this.dragStart = [];
3086
- if (moves.length > 0) {
3087
- this.record({
3088
- do: () => {
3089
- for (const m of moves) this.doMoveNode(m.id, m.toX, m.toY);
3090
- },
3091
- undo: () => {
3092
- for (const m of moves) this.doMoveNode(m.id, m.fromX, m.fromY);
3093
- },
3094
- label: "drag"
3095
- });
3096
- }
3097
- }
3215
+ this.commitNodeDrag();
3098
3216
  if (this.rubber !== null) {
3099
3217
  const rx0 = Math.min(this.rubber.x0, this.rubber.x);
3100
3218
  const ry0 = Math.min(this.rubber.y0, this.rubber.y);
@@ -3178,12 +3296,16 @@ var SSDiagram = (() => {
3178
3296
  }
3179
3297
  this.zoomToFit();
3180
3298
  });
3181
- this.listen(this.canvas, "contextmenu", (e) => {
3299
+ const openContextMenu = (e) => {
3182
3300
  e.preventDefault();
3183
3301
  if (!this.permissions.inspect) return;
3184
3302
  const [sx, sy] = localXY(e);
3185
3303
  this.cancelLongPress();
3186
3304
  this.fireContextMenu(sx, sy, e.clientX, e.clientY);
3305
+ };
3306
+ this.listen(this.canvas, "contextmenu", openContextMenu);
3307
+ this.listen(this.host, "contextmenu", (e) => {
3308
+ if (e.target !== this.canvas) openContextMenu(e);
3187
3309
  });
3188
3310
  let pinchDist = 0;
3189
3311
  let pinchScale = 1;
@@ -3339,6 +3461,7 @@ var SSDiagram = (() => {
3339
3461
  }
3340
3462
  }
3341
3463
  if (options.transient && (this.linking !== null || this.relinking !== null)) this.drawPendingLink();
3464
+ this.portFeed = this.feedTypes();
3342
3465
  for (const n2 of this.nodes) this.drawNode(n2, options.selection && this.selectedNodes.has(n2), options);
3343
3466
  if (options.selection && this.permissions.createLinks && this.relinking === null)
3344
3467
  this.drawSelectedLinkEndpoints();
@@ -3593,7 +3716,7 @@ ${runtime.error}`;
3593
3716
  }
3594
3717
  }
3595
3718
  const titleShift = n2.icon ? iconW + 4 : 0;
3596
- ctx.fillStyle = hasLoadError ? "#ffffff" : "#1b1b1b";
3719
+ ctx.fillStyle = hasLoadError ? "#ffffff" : readableTextOn(active ? "#ffd1dc" : n2.color);
3597
3720
  ctx.font = "600 12px Segoe UI, Tahoma, sans-serif";
3598
3721
  ctx.textAlign = "center";
3599
3722
  ctx.textBaseline = "middle";
@@ -3616,7 +3739,7 @@ ${runtime.error}`;
3616
3739
  ctx.fill();
3617
3740
  }
3618
3741
  roundRect(ctx, x, y, s, s, r);
3619
- ctx.fillStyle = this.portColor(p.type);
3742
+ ctx.fillStyle = this.portColor(this.portFillType(p));
3620
3743
  ctx.fill();
3621
3744
  ctx.lineWidth = magnet ? 1.5 : 1;
3622
3745
  ctx.strokeStyle = magnet ? "#ffffff" : "rgba(12,12,16,0.55)";
@@ -4458,6 +4581,7 @@ ${runtime.error}`;
4458
4581
  });
4459
4582
  this.on(root, "contextmenu", (event) => {
4460
4583
  event.preventDefault();
4584
+ event.stopPropagation();
4461
4585
  });
4462
4586
  this.container.appendChild(root);
4463
4587
  this.root = root;
@@ -5289,6 +5413,16 @@ ${runtime.error}`;
5289
5413
  isContextMenuEnabled() {
5290
5414
  return this.contextMenu !== null;
5291
5415
  }
5416
+ /**
5417
+ * Whether the built-in menu is on screen right now.
5418
+ *
5419
+ * The menu closes itself on Escape. A host that binds the same key - to leave a fullscreen
5420
+ * layout, to close its own dialog - needs to know the key was already spoken for, or one
5421
+ * press does both.
5422
+ */
5423
+ isContextMenuOpen() {
5424
+ return this.contextMenu?.isOpen ?? false;
5425
+ }
5292
5426
  getContextCommands() {
5293
5427
  const context = this.contextActionContext();
5294
5428
  const state = (command) => {
@@ -6079,44 +6213,6 @@ ${runtime.error}`;
6079
6213
  function iconUrl(name) {
6080
6214
  return name ? `/diagram-icons/${name}.svg` : "";
6081
6215
  }
6082
- var CSS_COLOR_KEYWORDS = {
6083
- white: [255, 255, 255],
6084
- black: [0, 0, 0],
6085
- transparent: [255, 255, 255]
6086
- };
6087
- function luminance(color) {
6088
- const value = color.trim().toLowerCase();
6089
- if (value === "")
6090
- return -1;
6091
- const keyword = CSS_COLOR_KEYWORDS[value];
6092
- if (keyword !== void 0)
6093
- return 0.299 * keyword[0] + 0.587 * keyword[1] + 0.114 * keyword[2];
6094
- const long = value.match(/^#?([0-9a-f]{6})$/);
6095
- if (long !== null) {
6096
- const n2 = parseInt(long[1], 16);
6097
- return 0.299 * (n2 >> 16 & 255) + 0.587 * (n2 >> 8 & 255) + 0.114 * (n2 & 255);
6098
- }
6099
- const short = value.match(/^#?([0-9a-f]{3})([0-9a-f])?$/);
6100
- if (short !== null) {
6101
- const [r, g, b] = [...short[1]].map((digit) => parseInt(digit + digit, 16));
6102
- return 0.299 * r + 0.587 * g + 0.114 * b;
6103
- }
6104
- const long8 = value.match(/^#?([0-9a-f]{6})[0-9a-f]{2}$/);
6105
- if (long8 !== null) {
6106
- const n2 = parseInt(long8[1], 16);
6107
- return 0.299 * (n2 >> 16 & 255) + 0.587 * (n2 >> 8 & 255) + 0.114 * (n2 & 255);
6108
- }
6109
- const rgb = value.match(/^rgba?\(([^)]+)\)$/);
6110
- if (rgb !== null) {
6111
- const parts = rgb[1].split(/[\s,/]+/).filter((part) => part !== "");
6112
- if (parts.length >= 3) {
6113
- const [r, g, b] = parts.slice(0, 3).map((part) => part.endsWith("%") ? parseFloat(part) / 100 * 255 : parseFloat(part));
6114
- if ([r, g, b].every((channel) => Number.isFinite(channel)))
6115
- return 0.299 * r + 0.587 * g + 0.114 * b;
6116
- }
6117
- }
6118
- return -1;
6119
- }
6120
6216
  function makePort(p) {
6121
6217
  return new Port({
6122
6218
  id: p.key,