@stocksharp/trading-controls 1.4.0 → 1.6.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.
@@ -922,6 +922,12 @@ var SSTradingControls = (() => {
922
922
  /// The rendered filter controls, by column key, so a stored filter can be put
923
923
  /// back into them without rebuilding the row under the user's cursor.
924
924
  __publicField(this, "_filterEls");
925
+ // How many pages of `renderLimit` rows a paint may show: one, and one more each time
926
+ // the scroller nears its end. It never shrinks, so a repaint does not throw a reader
927
+ // halfway down back to the top.
928
+ __publicField(this, "_pages");
929
+ // Whether the last paint stopped at its limit with rows still unpainted.
930
+ __publicField(this, "_truncated");
925
931
  /// Suppresses onStateChange while setState applies a stored arrangement.
926
932
  __publicField(this, "_restoring");
927
933
  for (const col of options.columns) {
@@ -954,6 +960,8 @@ var SSTradingControls = (() => {
954
960
  );
955
961
  this._filterEls = /* @__PURE__ */ new Map();
956
962
  this._restoring = false;
963
+ this._pages = 1;
964
+ this._truncated = false;
957
965
  const accessors = {};
958
966
  for (const col of options.columns) {
959
967
  if (col.value) accessors[col.key] = col.value;
@@ -963,6 +971,7 @@ var SSTradingControls = (() => {
963
971
  this._paint();
964
972
  if (options.contextMenu) this._bindContextMenu();
965
973
  if ((options.selection || "none") !== "none") this._bindCopyShortcut();
974
+ if (options.scroller) this._bindScroller(options.scroller);
966
975
  }
967
976
  /** Every declared column in display order, hidden ones included. */
968
977
  columnKeys() {
@@ -1525,6 +1534,7 @@ var SSTradingControls = (() => {
1525
1534
  }
1526
1535
  render() {
1527
1536
  this._paint();
1537
+ this._fillScroller();
1528
1538
  if (this.options.afterRender) this.options.afterRender();
1529
1539
  }
1530
1540
  _paint() {
@@ -1532,7 +1542,8 @@ var SSTradingControls = (() => {
1532
1542
  this._cellEls.clear();
1533
1543
  const pinned = this.options.pinnedRows ? this.options.pinnedRows() : [];
1534
1544
  const sorted = this.sortedRows();
1535
- const limit = this.options.renderLimit != null ? this.options.renderLimit : Infinity;
1545
+ const limit = this.options.renderLimit != null ? this.options.renderLimit * this._pages : Infinity;
1546
+ this._truncated = false;
1536
1547
  const children = [];
1537
1548
  for (const row of pinned) {
1538
1549
  if (row.place === GridPinnedPlacements.Top) children.push(this._pinnedRow(row));
@@ -1540,17 +1551,26 @@ var SSTradingControls = (() => {
1540
1551
  let painted = 0;
1541
1552
  if (this._group === null) {
1542
1553
  for (const row of sorted) {
1543
- if (painted >= limit) break;
1554
+ if (painted >= limit) {
1555
+ this._truncated = true;
1556
+ break;
1557
+ }
1544
1558
  children.push(this._bodyRow(row));
1545
1559
  painted++;
1546
1560
  }
1547
1561
  } else {
1548
1562
  for (const [key, group] of this._groups(sorted)) {
1549
- if (painted >= limit) break;
1563
+ if (painted >= limit) {
1564
+ this._truncated = true;
1565
+ break;
1566
+ }
1550
1567
  children.push(this._groupRow(key, group.label, group.rows.length));
1551
1568
  if (this._collapsed.has(key)) continue;
1552
1569
  for (const row of group.rows) {
1553
- if (painted >= limit) break;
1570
+ if (painted >= limit) {
1571
+ this._truncated = true;
1572
+ break;
1573
+ }
1554
1574
  children.push(this._bodyRow(row));
1555
1575
  painted++;
1556
1576
  }
@@ -1566,6 +1586,29 @@ var SSTradingControls = (() => {
1566
1586
  if (this._flashKnown !== null || keys.length > 0) this._flashKnown = new Set(keys);
1567
1587
  }
1568
1588
  }
1589
+ _bindScroller(scroller) {
1590
+ const onScroll = () => {
1591
+ if (!this._canPage(scroller)) return;
1592
+ this._pages++;
1593
+ this.render();
1594
+ };
1595
+ scroller.addEventListener("scroll", onScroll);
1596
+ this._release.push(() => scroller.removeEventListener("scroll", onScroll));
1597
+ }
1598
+ // A page that leaves the reader nothing to scroll would hold the rest out of reach, so the
1599
+ // next is painted at once, until the scroller holds a screen past the one in view.
1600
+ _fillScroller() {
1601
+ const scroller = this.options.scroller;
1602
+ while (scroller && this._canPage(scroller)) {
1603
+ this._pages++;
1604
+ this._paint();
1605
+ }
1606
+ }
1607
+ // Whether the last paint left rows out while the scroller is within a screen of its end. A
1608
+ // scroller with no height is not laid out - a panel in a hidden tab - and pages nothing.
1609
+ _canPage(scroller) {
1610
+ return this._truncated && (this.options.renderLimit ?? 0) > 0 && scroller.clientHeight > 0 && scroller.scrollTop + scroller.clientHeight * 2 >= scroller.scrollHeight;
1611
+ }
1569
1612
  /**
1570
1613
  * The rendered row with this key, or null when it is not on screen (filtered
1571
1614
  * out, or past `renderLimit`).
@@ -2792,8 +2835,9 @@ var SSTradingControls = (() => {
2792
2835
  emptyText: this._host.t("No instruments"),
2793
2836
  rowClass: (i) => i.symbol === this.currentSymbol ? "wl-row wl-current" : "wl-row",
2794
2837
  bindRow: (tr, i) => tr.addEventListener("click", () => this._deps.onSelect(i.symbol)),
2795
- // Paint a screenful, export and subscribe over the whole filtered set.
2796
- renderLimit: _WatchlistWidget.RENDER_CAP,
2838
+ // Paint a page at a time as the list is scrolled; export over the whole filtered set.
2839
+ renderLimit: _WatchlistWidget.RENDER_PAGE,
2840
+ scroller: this.rootEl.querySelector(".watchlist-pane"),
2797
2841
  // A header click re-renders through the grid, so this is the only
2798
2842
  // place that sees every change to the visible set — including a sort
2799
2843
  // the widget was never told about.
@@ -2993,7 +3037,7 @@ var SSTradingControls = (() => {
2993
3037
  return cell;
2994
3038
  }
2995
3039
  // Export the current filtered+sorted view to .xlsx — every matching
2996
- // instrument, without the RENDER_CAP truncation applied to the DOM.
3040
+ // instrument, painted or not.
2997
3041
  _export() {
2998
3042
  this._grid?.download("watchlist", this._host.t("Markets"));
2999
3043
  }
@@ -3116,7 +3160,8 @@ var SSTradingControls = (() => {
3116
3160
  // day is scratch, not a setting worth syncing.
3117
3161
  _WatchlistWidget.BASELINE_KEY_PREFIX = "wlBaseline:";
3118
3162
  _WatchlistWidget.VISIBLE_CAP = 30;
3119
- _WatchlistWidget.RENDER_CAP = 300;
3163
+ // Rows painted at a time: scrolling the list towards its end paints the next page.
3164
+ _WatchlistWidget.RENDER_PAGE = 300;
3120
3165
  _WatchlistWidget.TYPE = ControlTypes.Watchlist;
3121
3166
  var WatchlistWidget = _WatchlistWidget;
3122
3167
 
@@ -6806,7 +6851,7 @@ var SSTradingControls = (() => {
6806
6851
  var _LogMonitorWidget = class _LogMonitorWidget {
6807
6852
  static create(hostEl, state, deps) {
6808
6853
  const host = assertHost(deps?.host, "LogMonitorWidget");
6809
- const root = _LogMonitorWidget._buildRoot(host);
6854
+ const root = _LogMonitorWidget._buildRoot(host, deps?.chrome !== false, deps?.sources !== false);
6810
6855
  root.id = makePanelId(_LogMonitorWidget.TYPE);
6811
6856
  hostEl.appendChild(root);
6812
6857
  return new _LogMonitorWidget(root, state || {}, deps);
@@ -6814,13 +6859,14 @@ var SSTradingControls = (() => {
6814
6859
  // The panel's markup: a source tree, a toolbar of level toggles and a text box, and the
6815
6860
  // message table. The level toggles are buttons rather than checkboxes because they are read
6816
6861
  // as a row of states, and each carries its letter so the toolbar and the column agree.
6817
- static _buildRoot(host) {
6862
+ static _buildRoot(host, chrome, sources) {
6818
6863
  const title = host.t("LogMonitor");
6819
- return makePanelRoot("log-monitor-panel", title, [
6820
- makeElement("div", "panel-header", {}, [
6821
- makeElement("span", "", {}, [title]),
6822
- makeIconButton("bt-icon-btn bt-icon-cancel panel-close-btn", host.t("ClosePanel"), "bi-x", { type: "button" })
6823
- ]),
6864
+ const header = chrome ? [makeElement("div", "panel-header", {}, [
6865
+ makeElement("span", "", {}, [title]),
6866
+ makeIconButton("bt-icon-btn bt-icon-cancel panel-close-btn", host.t("ClosePanel"), "bi-x", { type: "button" })
6867
+ ])] : [];
6868
+ return makePanelRoot(`log-monitor-panel${sources ? "" : " log-sources-off"}`, title, [
6869
+ ...header,
6824
6870
  makeElement("div", "panel-body log-monitor-body", {}, [
6825
6871
  makeElement("div", "log-sources", { role: "tree", "aria-label": host.t("LogSources") }, []),
6826
6872
  makeElement("div", "log-messages", {}, [
@@ -6905,7 +6951,16 @@ var SSTradingControls = (() => {
6905
6951
  rowKey: (m) => String(m.id),
6906
6952
  emptyText: this._host.t("NoLogMessages"),
6907
6953
  rowClass: (m) => `log-row log-row-${m.level}`,
6908
- contextMenu: makeGridMenu(this._host),
6954
+ contextMenu: {
6955
+ ...makeGridMenu(this._host),
6956
+ // The tree is worth its width when several things write to the log and worth
6957
+ // nothing when one does, and which of those is true changes with the session.
6958
+ // So it is a choice the reader makes here, next to the other column choices.
6959
+ items: (_context, defaults) => [
6960
+ ...defaults,
6961
+ { label: this._host.t("LogSources"), checked: this.sourcesShown(), run: () => this.showSources(!this.sourcesShown()) }
6962
+ ]
6963
+ },
6909
6964
  selection: "multi"
6910
6965
  }) : null;
6911
6966
  this._host.register(this);
@@ -6938,6 +6993,15 @@ var SSTradingControls = (() => {
6938
6993
  this._messages = [];
6939
6994
  this._render();
6940
6995
  }
6996
+ /// Whether the source tree is currently drawn.
6997
+ sourcesShown() {
6998
+ return !this.rootEl.classList.contains("log-sources-off");
6999
+ }
7000
+ /// Show or hide the source tree. Hiding it does not drop the filter that is set: the
7001
+ /// messages stay filtered by whatever was selected, which is what the reader last asked for.
7002
+ showSources(on) {
7003
+ this.rootEl.classList.toggle("log-sources-off", !on);
7004
+ }
6941
7005
  /// Show one source's subtree, or everything when given null.
6942
7006
  select(sourceId) {
6943
7007
  this._selected = sourceId;
@@ -7188,7 +7252,9 @@ var SSTradingControls = (() => {
7188
7252
  var _TradeFeedWidget = class _TradeFeedWidget {
7189
7253
  static create(hostEl, state, deps) {
7190
7254
  const host = assertHost(deps?.host, "TradeFeedWidget");
7191
- const root = _TradeFeedWidget._buildRoot(host);
7255
+ if (typeof deps.ownTradesTab !== "boolean")
7256
+ throw new Error('TradeFeedWidget: dep "ownTradesTab" is required');
7257
+ const root = _TradeFeedWidget._buildRoot(host, deps.ownTradesTab);
7192
7258
  root.id = makePanelId(_TradeFeedWidget.TYPE);
7193
7259
  hostEl.appendChild(root);
7194
7260
  return new _TradeFeedWidget(root, state || {}, deps);
@@ -7200,7 +7266,7 @@ var SSTradingControls = (() => {
7200
7266
  // pins another instrument to THIS feed, the one beside it asks the host for
7201
7267
  // another feed panel. The tooltips are what tell them apart, so neither is
7202
7268
  // an icon on its own.
7203
- static _buildRoot(host) {
7269
+ static _buildRoot(host, ownTradesTab) {
7204
7270
  const addInstrument = host.t("AddInstrument");
7205
7271
  const marketTrades = host.t("MarketTrades");
7206
7272
  return makePanelRoot("tradefeed-panel tf-tab-market tf-view-list", host.t("TradeFeed"), [
@@ -7214,10 +7280,12 @@ var SSTradingControls = (() => {
7214
7280
  ]),
7215
7281
  makeIconButton("bt-icon-btn bt-icon-cancel panel-close-btn", host.t("ClosePanel"), "bi-x", { type: "button" })
7216
7282
  ]),
7217
- makeElement("div", "tf-tabs", { role: "tablist", "aria-label": host.t("Trade feed tabs") }, [
7218
- makeElement("button", "tf-tab active", { type: "button", role: "tab", "aria-selected": "true", "data-tab": "market" }, [marketTrades]),
7219
- makeElement("button", "tf-tab", { type: "button", role: "tab", "aria-selected": "false", "data-tab": "my" }, [host.t("My trades")])
7220
- ]),
7283
+ ...ownTradesTab ? [
7284
+ makeElement("div", "tf-tabs", { role: "tablist", "aria-label": host.t("Trade feed tabs") }, [
7285
+ makeElement("button", "tf-tab active", { type: "button", role: "tab", "aria-selected": "true", "data-tab": "market" }, [marketTrades]),
7286
+ makeElement("button", "tf-tab", { type: "button", role: "tab", "aria-selected": "false", "data-tab": "my" }, [host.t("My trades")])
7287
+ ])
7288
+ ] : [],
7221
7289
  makeElement("div", "tf-extras", { role: "list", "aria-label": host.t("ExtraInstruments"), hidden: "" }, []),
7222
7290
  // Each tape is a DataGrid over the shared table skin: the <thead>
7223
7291
  // is left empty for the grid to fill, which is also what makes the
@@ -7228,12 +7296,14 @@ var SSTradingControls = (() => {
7228
7296
  makeElement("tbody", "", {}, [])
7229
7297
  ])
7230
7298
  ]),
7231
- makeElement("div", "tradefeed-content tf-my", {}, [
7232
- makeElement("table", "terminal-table tradefeed-table", { role: "table", "aria-label": host.t("My trades") }, [
7233
- makeElement("thead", "", {}, []),
7234
- makeElement("tbody", "", {}, [])
7299
+ ...ownTradesTab ? [
7300
+ makeElement("div", "tradefeed-content tf-my", {}, [
7301
+ makeElement("table", "terminal-table tradefeed-table", { role: "table", "aria-label": host.t("My trades") }, [
7302
+ makeElement("thead", "", {}, []),
7303
+ makeElement("tbody", "", {}, [])
7304
+ ])
7235
7305
  ])
7236
- ]),
7306
+ ] : [],
7237
7307
  makeElement("canvas", "tradefeed-bubbles tf-bubbles", { "aria-hidden": "true" }, []),
7238
7308
  makeElement("div", "tf-bubble-tooltip", { role: "tooltip", hidden: "" }, [])
7239
7309
  ]);