@stocksharp/diagram 1.2.1 → 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
@@ -1104,6 +1104,9 @@ var SSDiagram = (() => {
1104
1104
  const normalized = normalizePortType(type);
1105
1105
  return normalized === "" || normalized === "*" || normalized === "any" || normalized === "anydata" || normalized === "any data" || normalized === "object" || normalized === "system.object" || normalized.startsWith("system.object,");
1106
1106
  }
1107
+ function isGrowAnchor(port) {
1108
+ return port.isDynamic && !port.isSibling;
1109
+ }
1107
1110
  function arePortTypesCompatible(fromType, toType, availableTypes) {
1108
1111
  if (isWildcardPortType(fromType) || isWildcardPortType(toType)) return true;
1109
1112
  const normalizedFrom = normalizePortType(fromType);
@@ -1311,6 +1314,8 @@ var SSDiagram = (() => {
1311
1314
  this.hoverPort = null;
1312
1315
  this.hoverNode = null;
1313
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();
1314
1319
  this.tipTimer = null;
1315
1320
  this.tipShow = false;
1316
1321
  this.tipTarget = null;
@@ -1353,8 +1358,20 @@ var SSDiagram = (() => {
1353
1358
  if (ctx === null) throw new Error("ssdiagram: 2d context unavailable");
1354
1359
  this.ctx = ctx;
1355
1360
  this.resize(this.host.clientWidth || 800, this.host.clientHeight || 480);
1361
+ this.observeHostSize();
1356
1362
  this.bind();
1357
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
+ }
1358
1375
  // ---- events -----------------------------------------------------
1359
1376
  on(ev, h) {
1360
1377
  let set = this.handlers.get(ev);
@@ -2521,8 +2538,9 @@ var SSDiagram = (() => {
2521
2538
  n2.h = Math.max(48, rows * PORT_ROW_H + 16);
2522
2539
  const off = PORT_SQ / 2;
2523
2540
  const place = (ports, x) => {
2524
- const startY = n2.y + (n2.h - ports.length * PORT_ROW_H) / 2 + PORT_ROW_H / 2;
2525
- 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) => {
2526
2544
  p.cx = x;
2527
2545
  p.cy = startY + i * PORT_ROW_H;
2528
2546
  });
@@ -2539,6 +2557,26 @@ var SSDiagram = (() => {
2539
2557
  toWorld(sx, sy) {
2540
2558
  return [(sx - this.offX) / this.scale, (sy - this.offY) / this.scale];
2541
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
+ }
2542
2580
  portColor(type) {
2543
2581
  const maxL = this.opts.linkMaxLightness;
2544
2582
  const light = maxL !== void 0 && maxL < 0.5;
@@ -2879,10 +2917,36 @@ var SSDiagram = (() => {
2879
2917
  }
2880
2918
  this.lpStart = null;
2881
2919
  }
2882
- // Hit-test at screen coords and emit a contextMenu event. Cancels
2883
- // any partial drag/rubber/link gesture that may have started once
2884
- // 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.
2885
2948
  fireContextMenu(sx, sy, pageX, pageY) {
2949
+ this.commitNodeDrag();
2886
2950
  const [wx, wy] = this.toWorld(sx, sy);
2887
2951
  const port = this.portAt(wx, wy);
2888
2952
  const node = port?.node ?? this.nodeAt(wx, wy);
@@ -2899,11 +2963,14 @@ var SSDiagram = (() => {
2899
2963
  this.dragNode = null;
2900
2964
  this.dragStart = [];
2901
2965
  this.rubber = null;
2902
- this.panning = false;
2903
2966
  this.linking = null;
2904
2967
  this.relinking = null;
2905
2968
  this.relinkCandidate = null;
2906
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);
2907
2974
  this.clearHover();
2908
2975
  this.emit("contextMenu", { x: pageX, y: pageY, link, node, port });
2909
2976
  }
@@ -3145,26 +3212,7 @@ var SSDiagram = (() => {
3145
3212
  const finish = (e) => {
3146
3213
  this.cancelLongPress();
3147
3214
  this.relinkCandidate = null;
3148
- if (this.dragNode !== null) {
3149
- 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);
3150
- const moved = new Set(moves.map((m) => m.id));
3151
- for (const it of this.dragStart) {
3152
- if (moved.has(it.n.id)) this.emit("nodeMoved", { node: it.n });
3153
- }
3154
- this.dragNode = null;
3155
- this.dragStart = [];
3156
- if (moves.length > 0) {
3157
- this.record({
3158
- do: () => {
3159
- for (const m of moves) this.doMoveNode(m.id, m.toX, m.toY);
3160
- },
3161
- undo: () => {
3162
- for (const m of moves) this.doMoveNode(m.id, m.fromX, m.fromY);
3163
- },
3164
- label: "drag"
3165
- });
3166
- }
3167
- }
3215
+ this.commitNodeDrag();
3168
3216
  if (this.rubber !== null) {
3169
3217
  const rx0 = Math.min(this.rubber.x0, this.rubber.x);
3170
3218
  const ry0 = Math.min(this.rubber.y0, this.rubber.y);
@@ -3248,12 +3296,16 @@ var SSDiagram = (() => {
3248
3296
  }
3249
3297
  this.zoomToFit();
3250
3298
  });
3251
- this.listen(this.canvas, "contextmenu", (e) => {
3299
+ const openContextMenu = (e) => {
3252
3300
  e.preventDefault();
3253
3301
  if (!this.permissions.inspect) return;
3254
3302
  const [sx, sy] = localXY(e);
3255
3303
  this.cancelLongPress();
3256
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);
3257
3309
  });
3258
3310
  let pinchDist = 0;
3259
3311
  let pinchScale = 1;
@@ -3409,6 +3461,7 @@ var SSDiagram = (() => {
3409
3461
  }
3410
3462
  }
3411
3463
  if (options.transient && (this.linking !== null || this.relinking !== null)) this.drawPendingLink();
3464
+ this.portFeed = this.feedTypes();
3412
3465
  for (const n2 of this.nodes) this.drawNode(n2, options.selection && this.selectedNodes.has(n2), options);
3413
3466
  if (options.selection && this.permissions.createLinks && this.relinking === null)
3414
3467
  this.drawSelectedLinkEndpoints();
@@ -3686,7 +3739,7 @@ ${runtime.error}`;
3686
3739
  ctx.fill();
3687
3740
  }
3688
3741
  roundRect(ctx, x, y, s, s, r);
3689
- ctx.fillStyle = this.portColor(p.type);
3742
+ ctx.fillStyle = this.portColor(this.portFillType(p));
3690
3743
  ctx.fill();
3691
3744
  ctx.lineWidth = magnet ? 1.5 : 1;
3692
3745
  ctx.strokeStyle = magnet ? "#ffffff" : "rgba(12,12,16,0.55)";
@@ -4528,6 +4581,7 @@ ${runtime.error}`;
4528
4581
  });
4529
4582
  this.on(root, "contextmenu", (event) => {
4530
4583
  event.preventDefault();
4584
+ event.stopPropagation();
4531
4585
  });
4532
4586
  this.container.appendChild(root);
4533
4587
  this.root = root;
@@ -5359,6 +5413,16 @@ ${runtime.error}`;
5359
5413
  isContextMenuEnabled() {
5360
5414
  return this.contextMenu !== null;
5361
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
+ }
5362
5426
  getContextCommands() {
5363
5427
  const context = this.contextActionContext();
5364
5428
  const state = (command) => {