@stocksharp/trading-controls 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.
Files changed (59) hide show
  1. package/LICENSE +30 -0
  2. package/NOTICE +21 -0
  3. package/README.md +240 -0
  4. package/dist/esm/active-orders-widget.js +382 -0
  5. package/dist/esm/active-orders-widget.js.map +1 -0
  6. package/dist/esm/control-types.js +23 -0
  7. package/dist/esm/control-types.js.map +1 -0
  8. package/dist/esm/dom.js +55 -0
  9. package/dist/esm/dom.js.map +1 -0
  10. package/dist/esm/formatters.js +69 -0
  11. package/dist/esm/formatters.js.map +1 -0
  12. package/dist/esm/index.js +12 -0
  13. package/dist/esm/index.js.map +1 -0
  14. package/dist/esm/positions-widget.js +258 -0
  15. package/dist/esm/positions-widget.js.map +1 -0
  16. package/dist/esm/trade-history-widget.js +197 -0
  17. package/dist/esm/trade-history-widget.js.map +1 -0
  18. package/dist/esm/trading-data.js +26 -0
  19. package/dist/esm/trading-data.js.map +1 -0
  20. package/dist/esm/trading-host.js +79 -0
  21. package/dist/esm/trading-host.js.map +1 -0
  22. package/dist/esm/watchlist-widget.js +481 -0
  23. package/dist/esm/watchlist-widget.js.map +1 -0
  24. package/dist/sstradingcontrols.js +1740 -0
  25. package/dist/sstradingcontrols.js.map +7 -0
  26. package/dist/types/active-orders-widget.d.ts +52 -0
  27. package/dist/types/active-orders-widget.d.ts.map +1 -0
  28. package/dist/types/control-types.d.ts +8 -0
  29. package/dist/types/control-types.d.ts.map +1 -0
  30. package/dist/types/dom.d.ts +6 -0
  31. package/dist/types/dom.d.ts.map +1 -0
  32. package/dist/types/formatters.d.ts +8 -0
  33. package/dist/types/formatters.d.ts.map +1 -0
  34. package/dist/types/index.d.ts +16 -0
  35. package/dist/types/index.d.ts.map +1 -0
  36. package/dist/types/positions-widget.d.ts +37 -0
  37. package/dist/types/positions-widget.d.ts.map +1 -0
  38. package/dist/types/trade-history-widget.d.ts +25 -0
  39. package/dist/types/trade-history-widget.d.ts.map +1 -0
  40. package/dist/types/trading-data.d.ts +55 -0
  41. package/dist/types/trading-data.d.ts.map +1 -0
  42. package/dist/types/trading-host.d.ts +62 -0
  43. package/dist/types/trading-host.d.ts.map +1 -0
  44. package/dist/types/watchlist-widget.d.ts +60 -0
  45. package/dist/types/watchlist-widget.d.ts.map +1 -0
  46. package/package.json +129 -0
  47. package/src/active-orders-widget.ts +411 -0
  48. package/src/control-types.ts +24 -0
  49. package/src/dom.ts +68 -0
  50. package/src/formatters.ts +72 -0
  51. package/src/index.ts +52 -0
  52. package/src/positions-widget.ts +305 -0
  53. package/src/trade-history-widget.ts +221 -0
  54. package/src/trading-data.ts +121 -0
  55. package/src/trading-host.ts +300 -0
  56. package/src/watchlist-widget.ts +513 -0
  57. package/styles/theme.css +86 -0
  58. package/styles/trading-controls.css +469 -0
  59. package/translation-keys.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ StockSharp Custom License Notice
2
+
3
+ Copyright (c) 2010-present StockSharp Platform LLC and/or its affiliates.
4
+ All rights reserved.
5
+
6
+ All source code, binaries, documentation, examples, media assets,
7
+ configuration files, and other materials contained in this repository are
8
+ the proprietary property of StockSharp, unless a file or third-party notice
9
+ explicitly states otherwise.
10
+
11
+ This repository is not licensed under the Apache License, the MIT License,
12
+ or any other general-purpose open source license.
13
+ Viewing, downloading, copying, building, modifying, using, distributing, or
14
+ otherwise accessing any part of this repository is permitted only under the
15
+ StockSharp End User License Agreement and other applicable StockSharp terms
16
+ published on the official StockSharp website:
17
+
18
+ https://stocksharp.com/en/products/eula/
19
+
20
+ StockSharp may update its license terms on the official website. Users are
21
+ responsible for monitoring the official StockSharp website and complying
22
+ with the then-current terms. If this notice conflicts with the EULA or other
23
+ terms published on the official StockSharp website, the website terms control.
24
+
25
+ Nothing in this repository grants any rights to StockSharp trademarks,
26
+ service marks, product names, logos, or other brand assets except as
27
+ expressly permitted by StockSharp in writing.
28
+
29
+ Third-party components, if any, remain subject to their respective license
30
+ terms and notices.
package/NOTICE ADDED
@@ -0,0 +1,21 @@
1
+ StockSharp Notice
2
+
3
+ StockSharp Platform LLC
4
+ https://stocksharp.com/
5
+
6
+ Copyright (c) 2010-present StockSharp Platform LLC and/or its affiliates.
7
+ All rights reserved.
8
+
9
+ The materials in this repository are proprietary to StockSharp unless a file
10
+ or third-party notice explicitly states otherwise. Use of these materials is
11
+ governed by the StockSharp End User License Agreement and other applicable
12
+ terms published on the official StockSharp website:
13
+
14
+ https://stocksharp.com/en/products/eula/
15
+
16
+ StockSharp may update its license terms on the official website. Users are
17
+ responsible for monitoring the official StockSharp website and complying
18
+ with the then-current terms.
19
+
20
+ This repository may include third-party components. Those components remain
21
+ subject to their respective license terms and notices.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # StockSharp JS Trading Controls
2
+
3
+ [![Build and test](https://github.com/StockSharp/JS-TradingControls/actions/workflows/ci.yml/badge.svg)](https://github.com/StockSharp/JS-TradingControls/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/%40stocksharp%2Ftrading-controls.svg)](https://www.npmjs.com/package/@stocksharp/trading-controls)
5
+ [![License](https://img.shields.io/badge/license-StockSharp%20EULA-c8202f.svg)](LICENSE)
6
+
7
+ **StockSharp JS Trading Controls** are the browser panels a trading screen is
8
+ made of: an **active orders** blotter with inline edit, a **positions** blotter
9
+ with a pinned cash balance, a **trade history** blotter, and a **watchlist**
10
+ with live quotes, favourites and category tabs.
11
+
12
+ Each control builds its own DOM, renders its own table through
13
+ [`@stocksharp/grids`](https://www.npmjs.com/package/@stocksharp/grids), and reaches
14
+ the outside world through exactly one object — a `TradingHost`.
15
+
16
+ [StockSharp website](https://stocksharp.com/) ·
17
+ [GitHub repository](https://github.com/StockSharp/JS-TradingControls) ·
18
+ [Issue tracker](https://github.com/StockSharp/JS-TradingControls/issues)
19
+
20
+ ## Quick start
21
+
22
+ ```sh
23
+ npm install @stocksharp/trading-controls
24
+ ```
25
+
26
+ ```ts
27
+ import { PositionsWidget } from '@stocksharp/trading-controls';
28
+ import '@stocksharp/trading-controls/styles.css';
29
+ // Optional: a working dark/light palette, if your page has none of its own.
30
+ import '@stocksharp/trading-controls/theme.css';
31
+
32
+ const panel = PositionsWidget.create(document.querySelector('#positions')!, {}, {
33
+ host, // see "The host port" below
34
+ closePosition: (pf, instrument, symbol) => api.close(pf, instrument),
35
+ reversePosition: (pf, instrument, symbol) => api.reverse(pf, instrument),
36
+ refreshPositions: () => reload(),
37
+ });
38
+
39
+ panel.update(positions);
40
+ panel.updateBalance({ available: 1000, locked: 250, total: 1250 });
41
+ ```
42
+
43
+ The package also ships a ready-to-use browser bundle exposed as
44
+ `window.SSTradingControls`:
45
+
46
+ ```html
47
+ <script src="https://cdn.jsdelivr.net/npm/@stocksharp/trading-controls@0.1.0/dist/sstradingcontrols.js"></script>
48
+ <script>
49
+ const { PositionsWidget } = window.SSTradingControls;
50
+ </script>
51
+ ```
52
+
53
+ ## The host port is the whole API surface
54
+
55
+ A control imports no translator, no settings singleton, no panel registry and no
56
+ docking manager, and it never reads a `window` global. Everything it needs
57
+ arrives as one `TradingHost`:
58
+
59
+ | member | what the host answers |
60
+ |---|---|
61
+ | `isPrimary` | is this the instance that speaks for the page? |
62
+ | `t(key, …args)` | translate; see [Text](#everything-a-control-shows-is-the-hosts) for what a key looks like |
63
+ | `presentation` | word and colour a side, an order type, a status, a P&L sign |
64
+ | `preferences` / `cache` | two stores — settings that must survive, and scratch |
65
+ | `trading` | the API, the market-data client, the active portfolio, an instrument picker |
66
+ | `ticker` | where a control reports the symbols it is showing |
67
+ | `allow(action)` | may the user do this? |
68
+ | `log(message)` | where diagnostics go |
69
+ | `close()` `spawn(state)` `persistState(patch)` `saveLayout()` | lifecycle calls back |
70
+ | `register` `unregister` `broadcast<T>` | the host's handle on live instances |
71
+
72
+ **Every member is required, and `assertHost` proves it before the control
73
+ renders anything.** A half-supplied host is how a control half-works: it draws,
74
+ it looks alive, and the one capability nobody wired is discovered by a user
75
+ clicking something that does nothing. So a missing member throws at construction
76
+ naming the path — `WatchlistWidget: host.trading.api.getExecutions is required` —
77
+ nested members included.
78
+
79
+ **Seven of them no shipped control calls**: `spawn`, `persistState`,
80
+ `saveLayout`, `log`, `trading.pickInstrument`, `marketData.resubscribe` and
81
+ `marketData.getOrders`. They are required anyway, and not on speculation — the
82
+ terminal this was extracted from has seven controls, and the three still on its
83
+ side of the boundary (order book, order entry, trade feed) are already written
84
+ against this same interface and call every one of those members. Narrowing the
85
+ port now and widening it again as each moves in would break every host twice.
86
+ If you are adopting only the four controls here, stubs are a correct answer: a
87
+ no-op `spawn`, a `log` that forwards to the console.
88
+
89
+ Two pairs are deliberately separate rather than merged:
90
+
91
+ - **`preferences` and `cache` are two stores.** The watchlist's per-symbol,
92
+ per-day price baseline is scratch. Routing it through a server-synced settings
93
+ blob is how a settings row becomes a cache.
94
+ - **`persistState` and `saveLayout` are two calls.** One records into the panel's
95
+ bag, the other flushes. Hiding a disk write inside "remember this" is exactly
96
+ the invisible coupling the port exists to remove.
97
+
98
+ ## Everything a control shows is the host's
99
+
100
+ - **Text** arrives through `t()`. There is no dictionary here and no string the
101
+ package decides the wording of — see below for the keys you have to answer.
102
+ - **Colour** is class names the control emits; `styles/trading-controls.css`
103
+ gives them meaning and reads its values from `--t-*` custom properties. Four
104
+ names go the other way: `presentation.sideClass` and `pnlClass` are the
105
+ *host's* answer, and a control forwards the string to the cell without
106
+ looking at it. The shipped stylesheet paints `side-buy` / `side-sell` and
107
+ `pnl-positive` / `pnl-negative`; answer with those, or answer with your own
108
+ names and style those yourself. The port exports the list as
109
+ `PRESENTATION_CLASSES`, and `npm test` asserts the stylesheet styles all four.
110
+ - **Data** is handed in. A control never fetches on its own — except the two
111
+ reads the port names (`getExecutions`, `searchInstruments`), which the host
112
+ implements.
113
+
114
+ ### The 52 keys a host has to answer
115
+
116
+ `t()` cannot fail. A key the host does not know is rendered to the user as
117
+ itself, so `NoActiveOrders` appears in the empty blotter and `ClosePanel`
118
+ becomes a tooltip — a missing translation looks like a typo, never like an
119
+ error. The complete list ships with the package:
120
+
121
+ ```ts
122
+ import keys from '@stocksharp/trading-controls/translation-keys.json';
123
+ // { count: 52, keys: ['Actions', 'ActiveOrders', …] }
124
+ ```
125
+
126
+ It is **generated from the sources** (`npm run i18n:update`) and re-checked by
127
+ `npm test`, so it cannot drift from what the controls actually ask for.
128
+
129
+ The keys are not derivable, which is why the list is shipped rather than
130
+ described. Most are resource identifiers (`ClosePanel`, `ExportToExcel`,
131
+ `NoActiveOrders`, `Change24hPct`); some are English phrases, because that is the
132
+ form the terminal's dictionary already held them in (`Close position on {0}`,
133
+ `No trade history`, `Locked: ${0}`). `{0}`, `{1}` … are replaced positionally
134
+ from the `args` of the same `t()` call, so a translation may reorder them.
135
+
136
+ Controls render elements, never HTML strings, so nothing here can be an
137
+ injection site. A cell that holds a button holds a real element with its own
138
+ listener rather than an `onclick` attribute reaching a global.
139
+
140
+ ## The stylesheet contract
141
+
142
+ The package **ships its stylesheet** — `@stocksharp/trading-controls/styles.css`.
143
+ Documenting the class names instead would have left an adopting page to
144
+ re-author about six hundred lines of CSS before it could see a table, which is
145
+ not "renders outside its original host" in any useful sense.
146
+
147
+ It is split in two so a host does not have to take a palette it disagrees with:
148
+
149
+ | file | what it is | when to import |
150
+ |---|---|---|
151
+ | `@stocksharp/trading-controls/styles.css` | every rule, reading `var(--t-*)` and declaring none | always |
152
+ | `@stocksharp/trading-controls/theme.css` | a working dark + light palette (`:root`, and `:root[data-bs-theme="light"]`) | only if your page has no `--t-*` tokens of its own |
153
+
154
+ **The 22 properties a host must supply** if it skips `theme.css`:
155
+
156
+ | group | properties |
157
+ |---|---|
158
+ | surfaces | `--t-bg` `--t-panel` `--t-header` `--t-hover` `--t-border` |
159
+ | text | `--t-text` `--t-text-dim` `--t-text-bright` |
160
+ | accent | `--t-accent` `--t-accent-hover` `--t-accent-text` `--t-accent-glow-soft` |
161
+ | direction | `--t-green` `--t-red` `--t-green-flash` `--t-red-flash` |
162
+ | warning | `--t-orange` (destructive but not a cancel) `--t-warning` (read this) |
163
+ | type and shape | `--t-font` `--t-mono` `--t-radius` `--t-transition` |
164
+
165
+ All 22 are required — none of them has a fallback baked into the rule that
166
+ reads it. A `var(--t-orange, #f0b90b)` would keep the rule working on a host
167
+ that never declared the token, which means the host never finds out, and the
168
+ control quietly paints a shade from a palette nobody chose. `--t-orange` and
169
+ `--t-warning` were the two that did, and no longer do.
170
+
171
+ `npm test` runs `tools/check-style-contract.mjs`, which fails if a control emits
172
+ a class the stylesheet never styles, if the stylesheet misses one of the
173
+ `PRESENTATION_CLASSES` the host may return, if a rule reads a property
174
+ `theme.css` does not declare, if `theme.css` declares one no rule reads, or if
175
+ any rule reads a property with a fallback. The contract above is therefore
176
+ checked, not just written down.
177
+
178
+ Two things the page still owns:
179
+
180
+ - **Bootstrap Icons.** A control says which glyph a button wears (`bi bi-x`,
181
+ `bi bi-arrow-clockwise`) but does not draw it, exactly as it names a colour
182
+ token without defining it.
183
+ - **`grid-empty`, `visually-hidden`, `sort-asc` / `sort-desc`** come out of
184
+ `@stocksharp/grids`. This stylesheet carries them so an adopting page does not
185
+ have to know that; the second is spelled the way Bootstrap spells it.
186
+
187
+ ## What each control is
188
+
189
+ ### `ActiveOrdersWidget`
190
+
191
+ The session's whole order list — nothing drops out on its own, so a user can
192
+ watch an order's transitions instead of having a row vanish. A terminal row is
193
+ greyed; a rejected one carries its reason on a hover icon (unwrapped out of the
194
+ venue's JSON blob when it arrives that way) and its × dismisses locally rather
195
+ than sending a cancel with nothing to cancel. Quantity, price and stop are
196
+ editable in place while the venue still holds the order; committing an edit
197
+ restates the whole triple, because a replace is not a field patch.
198
+
199
+ ### `PositionsWidget`
200
+
201
+ Alphabetical at rest — positions have no natural "newest". Cash sits above them
202
+ as a pinned row: a different shape from a position, so it supplies its own cells,
203
+ stays out of the sort and out of the exported sheet, and being content it
204
+ suppresses the "no positions" row. Close and reverse are per-row buttons wired to
205
+ deps.
206
+
207
+ ### `TradeHistoryWidget`
208
+
209
+ Read-only, newest fill on top, loaded against whichever portfolio the host names
210
+ at refresh time rather than one captured at construction.
211
+
212
+ ### `WatchlistWidget`
213
+
214
+ Live quotes with a search box, favourites and one tab per instrument category
215
+ found in the data. A quote patches the two affected cells through the grid's
216
+ `(rowKey, columnKey)` lookup instead of repainting — a repaint would cancel the
217
+ flash animation it just started. It paints a screenful (`RENDER_CAP`) while the
218
+ export and the subscription sync work over the whole filtered set, and only the
219
+ primary instance reports to the host's ticker.
220
+
221
+ ## Exported sheets say what the screen says
222
+
223
+ Every blotter exports to `.xlsx` through the grid, and a column states both
224
+ forms: the localized text the user reads (`Sell`, `Filled`) and the raw figure
225
+ under it (`2000` rather than the formatted `2,000.00`), so a sheet is sortable as
226
+ numbers and cannot drift from the table it came from.
227
+
228
+ ## Development
229
+
230
+ ```sh
231
+ npm install
232
+ npm test # typecheck + public-API snapshot + style contract + unit tests
233
+ npm run build
234
+ ```
235
+
236
+ `dist/` and `tests/_dist/` are build outputs and are gitignored.
237
+
238
+ There is no browser in the test process: `tests/fake-dom.ts` implements the slice
239
+ of DOM the controls touch, and its size is the statement of how narrow that
240
+ slice is.
@@ -0,0 +1,382 @@
1
+ // Active orders — multi-instance.
2
+ //
3
+ // Builds its own DOM (see `_buildRoot`) rather than cloning a <template> out of
4
+ // the page, so it can be constructed by any host that supplies a `TradingHost`.
5
+ // Every instance shares the same data source (a host refresh / an order update
6
+ // off the socket); each update fans through the host's broadcast so duplicates
7
+ // stay in sync.
8
+ //
9
+ // The table itself is `DataGrid` from `@stocksharp/grids`: the columns below are
10
+ // the single declaration of this blotter — caption, value, rendering, colour
11
+ // class and export all live together, and the <thead> is left empty for the
12
+ // grid to fill. Cells that hold a control return a real Node with its own
13
+ // listener, so nothing here reaches a global through an inline onclick
14
+ // attribute; the callbacks arrive as deps instead.
15
+ import { cleanRejectReason, formatPrice, formatQty } from './formatters.js';
16
+ import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
17
+ import { ControlTypes } from './control-types.js';
18
+ import { assertHost } from './trading-host.js';
19
+ import { DataGrid } from '@stocksharp/grids/data-grid';
20
+ /// StockSharp order states as they arrive on the wire. The blotter branches on
21
+ /// them for row colour, for whether a cell can be edited in place and for what
22
+ /// the × button does, so they are named once here instead of appearing as bare
23
+ /// numbers at each branch.
24
+ export const OrderStates = {
25
+ PendingRisk: 1,
26
+ Sent: 2,
27
+ Active: 3,
28
+ PartiallyFilled: 4,
29
+ Filled: 5,
30
+ Rejected: 6,
31
+ Cancelled: 7,
32
+ };
33
+ export class ActiveOrdersWidget {
34
+ static create(hostEl, state, deps) {
35
+ // Assert before building: the markup below is localized through the
36
+ // host, so a missing host has to fail here rather than render a panel
37
+ // captioned with raw English keys.
38
+ const host = assertHost(deps?.host, 'ActiveOrdersWidget');
39
+ const root = ActiveOrdersWidget._buildRoot(host);
40
+ root.id = makePanelId(ActiveOrdersWidget.TYPE);
41
+ hostEl.appendChild(root);
42
+ return new ActiveOrdersWidget(root, state || {}, deps);
43
+ }
44
+ // The panel's markup. The host stylesheet reads this structure, and a
45
+ // docking host lifts `.panel-header`'s children into its tab strip.
46
+ //
47
+ // The rail's first two buttons are the ones that could not come across
48
+ // unchanged from the terminal's Razor template: they were inline
49
+ // `onclick="terminalApp.cancelAll()"` / `refreshOrders()`, references to a
50
+ // global this control must not have. They carry `.panel-cancel-all-btn` and
51
+ // `.panel-refresh-btn` now — the latter the name the other blotters' rails
52
+ // already give the identical button — and are wired to deps. Cancel-all
53
+ // keeps a distinct accessible name from its tooltip, which is why it states
54
+ // `aria-label` rather than taking the tooltip for both.
55
+ static _buildRoot(host) {
56
+ return makePanelRoot('active-orders-panel', host.t('ActiveOrders'), [
57
+ makeElement('div', 'panel-header', {}, [
58
+ makeElement('span', '', {}, [host.t('OpenOrders')]),
59
+ makeIconButton('bt-icon-btn bt-icon-cancel panel-close-btn', host.t('ClosePanel'), 'bi-x', { type: 'button' }),
60
+ ]),
61
+ makeElement('div', 'panel-body panel-body-with-rail', {}, [
62
+ makeElement('div', 'panel-body-content', {}, [
63
+ makeElement('table', 'terminal-table active-orders-table', { role: 'table', 'aria-label': host.t('ActiveOrdersList') }, [
64
+ makeElement('thead', '', {}, []),
65
+ makeElement('tbody', 'active-orders-body', {}, []),
66
+ ]),
67
+ ]),
68
+ makeElement('div', 'panel-rail', { role: 'toolbar', 'aria-label': host.t('ActiveOrdersActions') }, [
69
+ makeIconButton('bt-icon-btn bt-icon-cancel-all panel-cancel-all-btn', host.t('CancelAll'), 'bi-x-circle', { 'aria-label': host.t('CancelAllOrders') }),
70
+ makeIconButton('bt-icon-btn panel-refresh-btn', host.t('Refresh'), 'bi-arrow-clockwise', {}),
71
+ makeIconButton('bt-icon-btn panel-export-btn', host.t('ExportToExcel'), 'bi-file-earmark-spreadsheet', {}),
72
+ ]),
73
+ ]),
74
+ ]);
75
+ }
76
+ constructor(rootEl, _state, deps) {
77
+ this._host = assertHost(deps?.host, 'ActiveOrdersWidget');
78
+ for (const name of ['cancelOrder', 'dismissOrder', 'editOrderField', 'replaceOrder', 'cancelAllOrders', 'refreshOrders']) {
79
+ if (typeof deps?.[name] !== 'function')
80
+ throw new Error(`ActiveOrdersWidget: dep "${name}" is required`);
81
+ }
82
+ this.rootEl = rootEl;
83
+ this._deps = deps;
84
+ this.el = this.rootEl.querySelector('.active-orders-body');
85
+ this._closeBtn = this.rootEl.querySelector('.panel-close-btn');
86
+ this._cancelAllBtn = this.rootEl.querySelector('.panel-cancel-all-btn');
87
+ this._refreshBtn = this.rootEl.querySelector('.panel-refresh-btn');
88
+ this._exportBtn = this.rootEl.querySelector('.panel-export-btn');
89
+ this._orders = [];
90
+ this._closeBtn?.addEventListener('click', (e) => {
91
+ e.preventDefault();
92
+ this._host.close();
93
+ });
94
+ this._cancelAllBtn?.addEventListener('click', (e) => {
95
+ e.preventDefault();
96
+ this._deps.cancelAllOrders();
97
+ });
98
+ this._refreshBtn?.addEventListener('click', (e) => {
99
+ e.preventDefault();
100
+ this._deps.refreshOrders();
101
+ });
102
+ this._exportBtn?.addEventListener('click', (e) => {
103
+ e.preventDefault();
104
+ this._export();
105
+ });
106
+ const head = this.rootEl.querySelector('.active-orders-table thead');
107
+ this._grid = head && this.el
108
+ ? new DataGrid({
109
+ head: head,
110
+ body: this.el,
111
+ columns: this._columns(),
112
+ // Newest order on top by default (localId is monotonic) — the resting view puts the
113
+ // user's most recent actions where they look, instead of raw cache order. Natural
114
+ // array order is "newest first" too (deltas unshift), but only by accident; any
115
+ // header click re-renders the same array through the sorter.
116
+ defaultSort: { col: 'id', dir: 'desc' },
117
+ // The register tx, not the friendly localId shown in the ID column: it is what
118
+ // cancel / replace / inline edit address an order by.
119
+ rowKey: (o) => String(o.id),
120
+ emptyText: this._host.t('NoActiveOrders'),
121
+ // Terminal rows are greyed out so the live ones stay visually prominent.
122
+ rowClass: (o) => ActiveOrdersWidget._rowClass(o),
123
+ })
124
+ : null;
125
+ this._host.register(this);
126
+ }
127
+ dispose() {
128
+ this._host.unregister(this);
129
+ try {
130
+ this.rootEl.remove();
131
+ }
132
+ catch { /* already detached */ }
133
+ }
134
+ update(orders) {
135
+ this._orders = orders || [];
136
+ this._grid?.setRows(this._orders);
137
+ }
138
+ // Export the table to .xlsx in the currently rendered (sorted) order, with the
139
+ // same localized side/type/status texts the user sees on screen — the columns
140
+ // carry both forms, so the sheet cannot drift from the table.
141
+ _export() {
142
+ this._grid?.download('orders', this._host.t('OpenOrders'));
143
+ }
144
+ /// Apply a single order delta from the feed. Insert if new, merge if known.
145
+ /// Nothing drops out of the list: Filled and Cancelled rows stay so the user
146
+ /// can watch an order's transitions instead of having a row vanish, and a
147
+ /// Rejected row stays so its reason can be read on hover and dismissed
148
+ /// explicitly through the × button.
149
+ applyDelta(order) {
150
+ if (!order || order.id == null)
151
+ return;
152
+ const idx = this._orders.findIndex(o => o.id === order.id);
153
+ if (idx >= 0)
154
+ this._orders[idx] = { ...this._orders[idx], ...order };
155
+ else
156
+ this._orders.unshift(order);
157
+ this.update(this._orders);
158
+ }
159
+ /// Remove a row from the local view without touching the server. Used by the
160
+ /// × button on a terminal order (nothing left to cancel), and in any other
161
+ /// client-side dismiss path.
162
+ removeOrder(orderId) {
163
+ const idx = this._orders.findIndex(o => o.id === orderId);
164
+ if (idx >= 0) {
165
+ this._orders.splice(idx, 1);
166
+ this.update(this._orders);
167
+ }
168
+ }
169
+ getOrder(orderId) {
170
+ return this._orders.find(o => o.id === orderId);
171
+ }
172
+ startInlineEdit(orderId, field) {
173
+ const order = this.getOrder(orderId);
174
+ if (!order)
175
+ return;
176
+ // Addressed by the grid's row key (the register tx) and the column key, so
177
+ // the edit lands on the right cell whatever the sort is. A lookup that
178
+ // scanned the rendered ID column for the order id would never match, because
179
+ // that column shows the friendly counter.
180
+ const cell = this._grid?.cellElement(String(orderId), field);
181
+ if (!cell || cell.querySelector('input'))
182
+ return;
183
+ const currentValue = order[field];
184
+ const input = document.createElement('input');
185
+ input.type = 'number';
186
+ input.className = 'inline-edit-input';
187
+ input.value = String(currentValue ?? '');
188
+ input.step = field === 'quantity' ? '1' : '0.01';
189
+ input.min = field === 'quantity' ? '1' : '0.01';
190
+ cell.textContent = '';
191
+ cell.appendChild(input);
192
+ input.focus();
193
+ input.select();
194
+ const commit = () => {
195
+ const newVal = parseFloat(input.value);
196
+ if (isNaN(newVal) || newVal <= 0) {
197
+ this.update(this._orders);
198
+ return;
199
+ }
200
+ if (newVal === currentValue) {
201
+ this.update(this._orders);
202
+ return;
203
+ }
204
+ const updated = { quantity: order.quantity, limitPrice: order.limitPrice, stopPrice: order.stopPrice };
205
+ updated[field] = newVal;
206
+ this._deps.replaceOrder(orderId, updated.quantity, updated.limitPrice, updated.stopPrice);
207
+ };
208
+ input.addEventListener('blur', commit);
209
+ input.addEventListener('keydown', (e) => {
210
+ if (e.key === 'Enter') {
211
+ e.preventDefault();
212
+ input.blur();
213
+ }
214
+ if (e.key === 'Escape') {
215
+ this.update(this._orders);
216
+ }
217
+ });
218
+ }
219
+ // The blotter's single column declaration: what the header says, what the
220
+ // grid sorts on, what the cell shows, how it is coloured, and what reaches
221
+ // the exported sheet.
222
+ _columns() {
223
+ // `label`, not `t` — every row lambda below binds `o` for the order,
224
+ // but keeping the translator under a name of its own reads clearer
225
+ // next to the presentation object it sits beside.
226
+ const label = (key) => this._host.t(key);
227
+ const presentation = this._host.presentation;
228
+ return [
229
+ {
230
+ key: 'id',
231
+ header: label('ID'),
232
+ exportable: true,
233
+ // Per-user friendly counter (1, 2, 3, …) issued by the host. `o.id`
234
+ // keeps the register tx for cancel / replace routing; `o.localId` is
235
+ // just the badge the user wants to see. Rows cached from before the
236
+ // counter shipped have no localId — fall back to id so they don't go
237
+ // blank until the next snapshot replay reassigns one.
238
+ value: (o) => o.localId ?? o.id,
239
+ },
240
+ {
241
+ key: 'instrument',
242
+ header: label('Sym'),
243
+ exportable: true,
244
+ value: (o) => o.instrument,
245
+ },
246
+ {
247
+ key: 'side',
248
+ header: label('Side'),
249
+ exportable: true,
250
+ value: (o) => o.side,
251
+ render: (o) => presentation.sideText(o.side),
252
+ cellClass: (o) => presentation.sideClass(o.side),
253
+ exportValue: (o) => presentation.sideText(o.side),
254
+ },
255
+ {
256
+ key: 'type',
257
+ header: label('Type'),
258
+ exportable: true,
259
+ value: (o) => o.type,
260
+ render: (o) => presentation.typeText(o.type, o.limitPrice, o.stopPrice),
261
+ exportValue: (o) => presentation.typeText(o.type, o.limitPrice, o.stopPrice),
262
+ },
263
+ {
264
+ key: 'quantity',
265
+ header: label('Qty'),
266
+ exportable: true,
267
+ value: (o) => o.quantity,
268
+ render: (o) => formatQty(o.quantity),
269
+ cellClass: (o) => ActiveOrdersWidget._editableClass(o, 'quantity'),
270
+ bindCell: (td, o) => this._bindInlineEdit(td, o, 'quantity'),
271
+ },
272
+ {
273
+ key: 'limitPrice',
274
+ header: label('Price'),
275
+ exportable: true,
276
+ value: (o) => o.limitPrice,
277
+ render: (o) => o.limitPrice ? formatPrice(o.limitPrice) : label('MKT'),
278
+ cellClass: (o) => ActiveOrdersWidget._editableClass(o, 'limitPrice'),
279
+ bindCell: (td, o) => this._bindInlineEdit(td, o, 'limitPrice'),
280
+ exportValue: (o) => o.limitPrice ?? label('MKT'),
281
+ },
282
+ {
283
+ key: 'stopPrice',
284
+ header: label('Stop'),
285
+ exportable: true,
286
+ value: (o) => o.stopPrice,
287
+ render: (o) => o.stopPrice ? formatPrice(o.stopPrice) : '--',
288
+ cellClass: (o) => ActiveOrdersWidget._editableClass(o, 'stopPrice'),
289
+ bindCell: (td, o) => this._bindInlineEdit(td, o, 'stopPrice'),
290
+ exportValue: (o) => o.stopPrice ?? '',
291
+ },
292
+ {
293
+ key: 'status',
294
+ header: label('Status'),
295
+ exportable: true,
296
+ value: (o) => o.status,
297
+ render: (o) => this._statusCell(o),
298
+ exportValue: (o) => presentation.statusText(o.status),
299
+ },
300
+ {
301
+ key: 'actions',
302
+ header: label('Actions'),
303
+ headerHidden: true,
304
+ exportable: false,
305
+ cellClass: () => 'position-actions',
306
+ render: (o) => this._actionButton(o),
307
+ },
308
+ ];
309
+ }
310
+ static _rowClass(order) {
311
+ if (order.status === OrderStates.Filled)
312
+ return 'order-filled';
313
+ if (order.status === OrderStates.Rejected)
314
+ return 'order-rejected';
315
+ if (order.status === OrderStates.Cancelled)
316
+ return 'order-cancelled';
317
+ return '';
318
+ }
319
+ // An order can still be modified while the venue holds it. A market order has
320
+ // no limit price and an unconditional order no stop price, so those cells have
321
+ // nothing to edit even then.
322
+ static _canEdit(order, field) {
323
+ const modifiable = order.status === OrderStates.Active || order.status === OrderStates.Sent;
324
+ return modifiable && (field === 'quantity' || !!order[field]);
325
+ }
326
+ static _editableClass(order, field) {
327
+ return ActiveOrdersWidget._canEdit(order, field) ? 'cell-editable' : '';
328
+ }
329
+ // Double-click to edit belongs to the whole cell rather than to a control
330
+ // inside it, which is why it is wired here instead of returned by render().
331
+ _bindInlineEdit(td, order, field) {
332
+ if (!ActiveOrdersWidget._canEdit(order, field))
333
+ return;
334
+ td.addEventListener('dblclick', () => this._deps.editOrderField(order.id, field));
335
+ }
336
+ // Status text, plus a hoverable icon carrying the rejection reason. Some
337
+ // venues wrap the human-readable rejection inside a JSON blob;
338
+ // cleanRejectReason extracts `.message` and falls back to the raw string.
339
+ // The reason goes on the element's title property, so no quote escaping is
340
+ // involved the way it was when this row was built as markup.
341
+ _statusCell(order) {
342
+ const text = this._host.presentation.statusText(order.status);
343
+ const reason = order.status === OrderStates.Rejected ? cleanRejectReason(order.rejectReason) : '';
344
+ if (!reason)
345
+ return text;
346
+ const cell = document.createDocumentFragment();
347
+ cell.appendChild(document.createTextNode(text + ' '));
348
+ const icon = document.createElement('i');
349
+ icon.className = 'bi bi-question-circle reject-reason-icon';
350
+ icon.title = reason;
351
+ cell.appendChild(icon);
352
+ return cell;
353
+ }
354
+ // The × button, wired to the controller through deps. Status decides what it
355
+ // means: Pending / Active is a real cancel over the wire, while Filled /
356
+ // Rejected / Cancelled have nothing left to cancel — the row is history kept
357
+ // around so the user can read its final state, so the button dismisses it
358
+ // locally.
359
+ _actionButton(order) {
360
+ const isTerminal = order.status === OrderStates.Filled
361
+ || order.status === OrderStates.Rejected
362
+ || order.status === OrderStates.Cancelled;
363
+ const title = isTerminal ? this._host.t('Dismiss') : this._host.t('Cancel order');
364
+ const button = document.createElement('button');
365
+ button.type = 'button';
366
+ button.className = 'bt-icon-btn bt-icon-cancel';
367
+ button.title = title;
368
+ button.setAttribute('aria-label', `${title} #${order.id}`);
369
+ const icon = document.createElement('i');
370
+ icon.className = 'bi bi-x-circle';
371
+ button.appendChild(icon);
372
+ button.addEventListener('click', () => {
373
+ if (isTerminal)
374
+ this._deps.dismissOrder(order.id);
375
+ else
376
+ this._deps.cancelOrder(order.id);
377
+ });
378
+ return button;
379
+ }
380
+ }
381
+ ActiveOrdersWidget.TYPE = ControlTypes.ActiveOrders;
382
+ //# sourceMappingURL=active-orders-widget.js.map