@luxalgo/vela 0.6.4 → 0.6.6
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 +16 -16
- package/dist/{DataProvider-DrN7ZIou.d.cts → DataProvider-DDUYw2qP.d.cts} +1 -1
- package/dist/{DataProvider-BmHkcwvJ.d.ts → DataProvider-JJq7M2it.d.ts} +1 -1
- package/dist/{history-CvOtsjT5.d.ts → bottombar-COnL2Sk4.d.ts} +110 -127
- package/dist/{history-BmyjaVir.d.cts → bottombar-DeDXbF_K.d.cts} +110 -127
- package/dist/{chunk-6FA4TDUF.js → chunk-JQFO2WMU.js} +2666 -2333
- package/dist/{chunk-BEJ57HP4.js → chunk-OEUGUXL7.js} +14 -1
- package/dist/{chunk-3QX6QOCX.js → chunk-TMXK2SJR.js} +5852 -3340
- package/dist/{contributions-2bEmCF9S.d.cts → contributions-BqCIsbaP.d.ts} +108 -4
- package/dist/{contributions-DatoAzMx.d.ts → contributions-DNMqrAuw.d.cts} +108 -4
- package/dist/index.cjs +2681 -2332
- package/dist/index.d.cts +65 -14
- package/dist/index.d.ts +65 -14
- package/dist/index.js +2 -2
- package/dist/{options-YdiaaVjt.d.cts → options-BaVTMXaO.d.cts} +48 -2
- package/dist/{options-YdiaaVjt.d.ts → options-BaVTMXaO.d.ts} +48 -2
- package/dist/{plugin-CkiStRaI.d.cts → plugin-CN8U2__Q.d.cts} +11 -5
- package/dist/{plugin-BeCMF1VQ.d.ts → plugin-WnvNNJOp.d.ts} +11 -5
- package/dist/plugin.cjs +16 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +1 -1
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +2661 -2312
- package/dist/vela.global.min.js +48 -48
- package/dist/widget.cjs +35658 -34074
- package/dist/widget.d.cts +39 -271
- package/dist/widget.d.ts +39 -271
- package/dist/widget.js +60 -1313
- package/dist/workspace.cjs +1584 -960
- package/dist/workspace.d.cts +111 -15
- package/dist/workspace.d.ts +111 -15
- package/dist/workspace.js +6 -2281
- package/package.json +1 -1
package/dist/widget.js
CHANGED
|
@@ -1,1351 +1,98 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { Bottombar, ChartContextMenu, DataWindow, IndicatorPicker, ObjectTree, PanelDock, RANGE_PRESETS, ShortcutsHelp, Statusline, SymbolPicker, TimeframeQuick, Topbar, Watermark, dataWindowSections, decimalsFor, decodeState, encodeState, filterSymbols, fmtChange, fmtPrice,
|
|
3
|
-
|
|
4
|
-
export {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import { ensureUIHost, Menu, applyPlotOverlayTokens } from './chunk-OICPLNU7.js';
|
|
9
|
-
import { injectStyles } from './chunk-PN5KWFZ4.js';
|
|
10
|
-
|
|
11
|
-
// src/widget/url-state.ts
|
|
12
|
-
var PARAMS = [
|
|
13
|
-
["symbol", "symbol"],
|
|
14
|
-
["timeframe", "interval"],
|
|
15
|
-
["priceStyle", "style"],
|
|
16
|
-
["timezone", "tz"],
|
|
17
|
-
["bars", "bars"],
|
|
18
|
-
["session", "session"]
|
|
19
|
-
];
|
|
20
|
-
function readUrlState(search) {
|
|
21
|
-
const out = {};
|
|
22
|
-
try {
|
|
23
|
-
const q = new URLSearchParams(search);
|
|
24
|
-
for (const [key, param] of PARAMS) {
|
|
25
|
-
const v = q.get(param);
|
|
26
|
-
if (v) out[key] = v;
|
|
27
|
-
}
|
|
28
|
-
} catch {
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
function writeUrlState(state) {
|
|
33
|
-
if (typeof history === "undefined" || typeof location === "undefined") return;
|
|
34
|
-
try {
|
|
35
|
-
const q = new URLSearchParams(location.search);
|
|
36
|
-
for (const [key, param] of PARAMS) {
|
|
37
|
-
const v = state[key];
|
|
38
|
-
if (v) q.set(param, v);
|
|
39
|
-
else q.delete(param);
|
|
40
|
-
}
|
|
41
|
-
const qs = q.toString();
|
|
42
|
-
history.replaceState(null, "", `${location.pathname}${qs ? `?${qs}` : ""}${location.hash}`);
|
|
43
|
-
} catch {
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
import { VelaWorkspace } from './chunk-TMXK2SJR.js';
|
|
2
|
+
export { Bottombar, ChartContextMenu, DataWindow, IndicatorPicker, ObjectTree, PanelDock, RANGE_PRESETS, ShortcutsHelp, Statusline, SymbolPicker, TimeframeQuick, Topbar, Watermark, dataWindowSections, decimalsFor, decodeState, encodeState, filterSymbols, fmtChange, fmtPrice, localStorageAdapter, parseTimeframe, priceStyleLabel, resolveIndicators, sanitizeState, timeframeLabel, timeframeMs } from './chunk-TMXK2SJR.js';
|
|
3
|
+
export { TIMEZONES, normalizeTimezone, tzButtonLabel, tzMenuLabel, tzOffset } from './chunk-JQFO2WMU.js';
|
|
4
|
+
export { DEFAULT_PANEL_MAX_WIDTH, DEFAULT_PANEL_MIN_WIDTH, DEFAULT_PANEL_ORDER, DEFAULT_PANEL_WIDTH, SidePanel, clampPanelWidth, registerSidePanel, registerStatePersistence, registerWidgetAction, registerWidgetAttachment, sidePanels, statePersistenceHandlers, unregisterSidePanel, unregisterStatePersistence, unregisterWidgetAction, unregisterWidgetAttachment, widgetActions, widgetAttachments } from './chunk-OEUGUXL7.js';
|
|
5
|
+
import './chunk-2R7BANEM.js';
|
|
6
|
+
import './chunk-OICPLNU7.js';
|
|
7
|
+
import './chunk-PN5KWFZ4.js';
|
|
46
8
|
|
|
47
9
|
// src/widget/VelaWidget.ts
|
|
48
|
-
var
|
|
49
|
-
var WIDGET_STYLE_ID = "vela-widget";
|
|
50
|
-
var WIDGET_CSS = `
|
|
51
|
-
.vela-widget { position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; background: var(--vela-bg); }
|
|
52
|
-
.vela-widget-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
53
|
-
.vela-widget-chart { position: relative; flex: 1 1 auto; min-width: 0; }
|
|
54
|
-
`;
|
|
10
|
+
var deprecationWarned = false;
|
|
55
11
|
var VelaWidget = class {
|
|
56
12
|
constructor(container, opts) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this.shortcutsHelp = null;
|
|
63
|
-
this.inner = null;
|
|
64
|
-
/** The manifest library (loaded once). */
|
|
65
|
-
this.manifest = [];
|
|
66
|
-
/** Live instances — the SAME entry may be added several times. */
|
|
67
|
-
this.instances = [];
|
|
68
|
-
/** Native-indicator catalog of the CURRENT chart (refreshed per rebuild/change). */
|
|
69
|
-
this.nativeCatalog = [];
|
|
70
|
-
/** Present native types, mirrored SYNC from the chart (see syncPresentNatives). */
|
|
71
|
-
this.presentNatives = [];
|
|
72
|
-
this.openDialogs = 0;
|
|
73
|
-
this.onRootKeydown = (ev) => this.routeTyping(ev);
|
|
74
|
-
/** Indicator titles (the in-chart legend rows) shown — reapplied across rebuilds. */
|
|
75
|
-
this.indicatorTitlesOn = true;
|
|
76
|
-
/** Plot values beside the legend titles shown — reapplied across rebuilds. */
|
|
77
|
-
this.indicatorValuesOn = true;
|
|
78
|
-
this.pendingRange = null;
|
|
79
|
-
/** Symbol already reported as unservable (the core re-reports on every index settle). */
|
|
80
|
-
this.unresolvedToasted = null;
|
|
81
|
-
/** Extra fetch depth the ACTIVE range chip needs — a view concern, kept apart from the
|
|
82
|
-
* user's own `bars` setting so a preset never overwrites their preference. */
|
|
83
|
-
this.rangeBars = 0;
|
|
84
|
-
this.toast = null;
|
|
85
|
-
this.alerts = [];
|
|
86
|
-
this.alertsMenu = null;
|
|
87
|
-
this.glider = new Glider(() => this.inner);
|
|
88
|
-
/** Unified app+drawings undo timeline (Ctrl+Z / Ctrl+Y). Late-resolves the CURRENT
|
|
89
|
-
* inner chart so steps recorded before a rebuild never act on a destroyed instance. */
|
|
90
|
-
this.history = new WidgetHistory(() => this.inner);
|
|
91
|
-
this.lastCrossPrice = null;
|
|
92
|
-
this.lastCrossTime = null;
|
|
93
|
-
/** Renderer cosmetic template carried across rebuilds (and persisted when enabled). */
|
|
94
|
-
this.savedConfig = null;
|
|
95
|
-
/** User-drawings document carried across rebuilds (and persisted when enabled). */
|
|
96
|
-
this.savedDrawings = null;
|
|
97
|
-
/** A restored indicator ledger waiting for the manifest to resolve. */
|
|
98
|
-
this.pendingIndicators = null;
|
|
99
|
-
/** Volume presence decided by a RESTORED ledger (null = follow the option). */
|
|
100
|
-
this.ledgerVolume = null;
|
|
101
|
-
/** The manifest can no longer change the instance set: it resolved, or no `indicators`
|
|
102
|
-
* option exists so nothing will ever resolve. Gates the ledger's pending fallback —
|
|
103
|
-
* once settled, a live empty set means "the user removed everything" and persists so. */
|
|
104
|
-
this.manifestSettled = false;
|
|
105
|
-
/** The volume auto-add rides the first candles (`load:end`); until then the registry
|
|
106
|
-
* can't show it and the persisted ledger reports the INTENT instead. */
|
|
107
|
-
this.volumeMayBePending = true;
|
|
108
|
-
/** True when the boot state came from the LEGACY three-key layout — the first
|
|
109
|
-
* unified save then drops the old `:config`/`:drawings` sub-keys. */
|
|
110
|
-
this.legacyKeys = false;
|
|
111
|
-
this.stateTimer = null;
|
|
112
|
-
this.stateListeners = /* @__PURE__ */ new Set();
|
|
113
|
-
/** Favorite drawing-tool types — mirrored from chart events, reapplied on rebuilds. */
|
|
114
|
-
this.favs = [];
|
|
115
|
-
/** Favorite timeframes — the topbar's quick-switch chips, persisted with the shell. */
|
|
116
|
-
this.tfFavs = [];
|
|
117
|
-
/** Mounted attachment disposers (by id), torn down at destroy. */
|
|
118
|
-
this.attachmentDisposers = /* @__PURE__ */ new Map();
|
|
119
|
-
this.onUnload = () => this.persistNow();
|
|
120
|
-
this.indicatorsPromise = null;
|
|
121
|
-
this.destroyed = false;
|
|
122
|
-
this.buildSeq = 0;
|
|
123
|
-
this.tfDrawer = null;
|
|
124
|
-
this.drawingsDrawer = null;
|
|
125
|
-
this.moreDrawer = null;
|
|
126
|
-
this.timezoneDrawer = null;
|
|
127
|
-
this.priceScaleDrawer = null;
|
|
128
|
-
/** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
|
|
129
|
-
this.priceScalePressY = 0;
|
|
130
|
-
/** The active range chip id (both bottom bars + the timeframe drawer mirror it). */
|
|
131
|
-
this.activeRangeId = null;
|
|
132
|
-
const hostEl = typeof container === "string" ? document.querySelector(container) : container;
|
|
133
|
-
if (!hostEl) throw new Error(`VelaWidget: container not found: ${String(container)}`);
|
|
134
|
-
registerBuiltinChartTypes();
|
|
135
|
-
this.opts = opts;
|
|
136
|
-
this.storageKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-widget" : opts.persist;
|
|
137
|
-
this.storage = opts.storage ?? localStorageAdapter();
|
|
138
|
-
let boot = null;
|
|
139
|
-
if (this.storageKey !== null) {
|
|
140
|
-
const raw = this.storage.get(this.storageKey);
|
|
141
|
-
if (raw instanceof Promise) void this.bootAsync(raw);
|
|
142
|
-
else {
|
|
143
|
-
boot = raw != null ? decodeState(raw) : null;
|
|
144
|
-
if (!boot) boot = this.bootLegacySync(raw);
|
|
145
|
-
}
|
|
146
|
-
window.addEventListener("beforeunload", this.onUnload);
|
|
13
|
+
if (!deprecationWarned) {
|
|
14
|
+
deprecationWarned = true;
|
|
15
|
+
console.warn(
|
|
16
|
+
"[vela] VelaWidget is deprecated and will be removed in a future release. Use `new VelaWorkspace(container, { ...options, layout: false })` \u2014 pass `persist: 'vela-widget'` to keep the same stored state. " + (opts.urlState ? "The `urlState` option is no longer supported and was ignored." : "")
|
|
17
|
+
);
|
|
147
18
|
}
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.bars = Number(fromUrl.bars ?? bootCell?.bars ?? opts.bars ?? 1e3);
|
|
155
|
-
this.session = normalizeSession(fromUrl.session ?? bootCell?.session ?? opts.session);
|
|
156
|
-
this.watermarkOn = bootCell?.watermark !== void 0 ? bootCell.watermark : opts.watermark !== false;
|
|
157
|
-
this.indicatorTitlesOn = bootCell?.indicatorTitles ?? true;
|
|
158
|
-
this.indicatorValuesOn = bootCell?.indicatorValues ?? true;
|
|
159
|
-
this.favs = boot?.favorites ? [...boot.favorites] : [];
|
|
160
|
-
this.tfFavs = boot?.timeframeFavorites ? [...boot.timeframeFavorites] : [];
|
|
161
|
-
this.savedConfig = bootCell?.rendererConfig ?? null;
|
|
162
|
-
this.savedDrawings = bootCell?.drawings ?? null;
|
|
163
|
-
this.pendingIndicators = bootCell?.indicators ?? null;
|
|
164
|
-
this.ledgerVolume = bootCell?.indicators ? bootCell.indicators.natives.includes("volume") : null;
|
|
165
|
-
this.manifestSettled = opts.indicators === void 0;
|
|
166
|
-
const doc = hostEl.ownerDocument;
|
|
167
|
-
injectStyles(WIDGET_STYLE_ID, WIDGET_CSS, doc);
|
|
168
|
-
this.root = doc.createElement("div");
|
|
169
|
-
this.root.className = "vela-widget";
|
|
170
|
-
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
171
|
-
this.symbolPicker = new SymbolPicker({
|
|
172
|
-
host: this.root,
|
|
173
|
-
onSelect: (symbol) => this.setSymbol(symbol),
|
|
174
|
-
onOpenChange: (open) => {
|
|
175
|
-
if (open) this.inner?.renderer.closeDialogs();
|
|
176
|
-
this.trackDialog(open);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
|
|
180
|
-
host: this.root,
|
|
181
|
-
library: () => [
|
|
182
|
-
...this.nativeCatalog.filter((n) => n.supported).map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
|
|
183
|
-
...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
|
|
184
|
-
],
|
|
185
|
-
onChart: () => [
|
|
186
|
-
...this.nativeCatalog.filter((n) => n.present).map((n) => ({ name: n.title, native: true, nativeType: n.type })),
|
|
187
|
-
...this.instances.map((it) => ({ name: it.entry.name, language: it.entry.language }))
|
|
188
|
-
],
|
|
189
|
-
onAdd: (i) => {
|
|
190
|
-
const natives = this.nativeCatalog.filter((n) => n.supported);
|
|
191
|
-
if (i < natives.length) this.addNative(natives[i].type);
|
|
192
|
-
else this.addInstance(i - natives.length);
|
|
193
|
-
},
|
|
194
|
-
onRemove: (i) => {
|
|
195
|
-
const present = this.nativeCatalog.filter((n) => n.present);
|
|
196
|
-
if (i < present.length) this.removeNative(present[i].type);
|
|
197
|
-
else this.removeInstance(i - present.length);
|
|
198
|
-
},
|
|
199
|
-
onOpenChange: (open) => {
|
|
200
|
-
if (open) this.inner?.renderer.closeDialogs();
|
|
201
|
-
this.trackDialog(open);
|
|
202
|
-
}
|
|
203
|
-
}) : null;
|
|
204
|
-
this.tfQuick = new TimeframeQuick({
|
|
205
|
-
host: this.root,
|
|
206
|
-
onApply: (tf) => this.setTimeframe(tf),
|
|
207
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
208
|
-
});
|
|
209
|
-
const picker = this.indicatorPicker;
|
|
210
|
-
this.topbar = new Topbar(this.root, {
|
|
211
|
-
symbol: this.symbol,
|
|
212
|
-
onSymbolClick: () => this.symbolPicker.open(),
|
|
213
|
-
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
214
|
-
onUndoClick: () => this.history.undo(),
|
|
215
|
-
onRedoClick: () => this.history.redo(),
|
|
216
|
-
onScreenshotClick: () => this.downloadScreenshot(),
|
|
217
|
-
onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
|
|
218
|
-
timeframe: this.timeframe,
|
|
219
|
-
timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
|
|
220
|
-
timeframeFavorites: this.tfFavs,
|
|
221
|
-
priceStyle: this.priceStyle,
|
|
222
|
-
onTimeframe: (tf) => this.setTimeframe(tf),
|
|
223
|
-
onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
|
|
224
|
-
onPriceStyle: (style) => this.setPriceStyle(style),
|
|
225
|
-
getContext: () => this.context()
|
|
226
|
-
});
|
|
227
|
-
this.history.onChange(() => this.topbar.setHistoryState(this.history.canUndo, this.history.canRedo));
|
|
228
|
-
const main = doc.createElement("div");
|
|
229
|
-
main.className = "vela-widget-main";
|
|
230
|
-
this.chartHost = doc.createElement("div");
|
|
231
|
-
this.chartHost.className = "vela-widget-chart";
|
|
232
|
-
main.appendChild(this.chartHost);
|
|
233
|
-
this.dock = new PanelDock(main, {
|
|
234
|
-
chrome: this.topbar,
|
|
235
|
-
context: () => this.context(),
|
|
236
|
-
changed: () => this.markStateDirty()
|
|
237
|
-
});
|
|
238
|
-
this.objectTree = new ObjectTree(main);
|
|
239
|
-
this.dataWindow = new DataWindow(main);
|
|
240
|
-
this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
|
|
241
|
-
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
242
|
-
this.dock.refresh();
|
|
243
|
-
this.root.appendChild(main);
|
|
244
|
-
this.contextMenu = new ChartContextMenu(this.chartHost, {
|
|
245
|
-
resetView: () => {
|
|
246
|
-
this.inner?.renderer.set("autoScale", true);
|
|
247
|
-
this.inner?.setVisibleRangePreset("ALL");
|
|
248
|
-
},
|
|
249
|
-
timezone: () => this.timezone,
|
|
250
|
-
setTimezone: (zone) => this.setTimezone(zone),
|
|
251
|
-
getContext: () => this.context()
|
|
252
|
-
});
|
|
253
|
-
this.toast = new Toast(this.chartHost);
|
|
254
|
-
this.watermark = opts.watermark !== false ? new Watermark(this.chartHost, this.symbol, this.timeframe) : null;
|
|
255
|
-
this.watermark?.setVisible(this.watermarkOn);
|
|
256
|
-
this.statusline = opts.statusline !== false ? new Statusline(this.chartHost, this.symbol) : null;
|
|
257
|
-
this.statusline?.setMeta(this.timeframe, parseSymbol(this.symbol).provider ?? "");
|
|
258
|
-
this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
|
|
259
|
-
this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
|
|
260
|
-
timezone: this.timezone,
|
|
261
|
-
onRange: (preset) => this.applyRange(preset),
|
|
262
|
-
onTimezone: (zone) => this.setTimezone(zone),
|
|
263
|
-
onSession: (session) => this.setSession(session),
|
|
264
|
-
onSettingsClick: () => this.inner?.renderer.openSettings()
|
|
265
|
-
}) : null;
|
|
266
|
-
this.mobileBar = opts.bottombar !== false ? new MobileBar(this.root, {
|
|
267
|
-
symbol: this.symbol,
|
|
268
|
-
timeframe: this.timeframe,
|
|
269
|
-
onSymbolClick: () => this.symbolPicker.open(),
|
|
270
|
-
onTimeframeClick: () => this.openTimeframeDrawer(),
|
|
271
|
-
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
272
|
-
getContext: () => this.context(),
|
|
273
|
-
onDrawingsClick: () => this.openDrawingsDrawer(),
|
|
274
|
-
onMoreClick: () => this.openMoreDrawer(),
|
|
275
|
-
onSettingsClick: () => this.inner?.renderer.openSettings()
|
|
276
|
-
}) : null;
|
|
277
|
-
this.drawingPill = new DrawingPill(this.chartHost);
|
|
278
|
-
hostEl.appendChild(this.root);
|
|
279
|
-
this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
|
|
280
|
-
this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
|
|
281
|
-
this.keymap = new KeymapManager();
|
|
282
|
-
this.keymap.attach(this.root);
|
|
283
|
-
this.keymap.register({
|
|
284
|
-
id: "chart.screenshot",
|
|
285
|
-
keys: "mod+alt+s",
|
|
286
|
-
label: "Download a chart screenshot",
|
|
287
|
-
category: "Chart",
|
|
288
|
-
run: () => this.downloadScreenshot()
|
|
289
|
-
});
|
|
290
|
-
this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.inner?.setVisibleRangePreset("ALL") });
|
|
291
|
-
this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: "Toggle logarithmic scale", category: "Chart", run: () => this.inner?.renderer.set("logScale", !this.inner.renderer.get("logScale")) });
|
|
292
|
-
this.keymap.register({
|
|
293
|
-
id: "chart.toggle-percent",
|
|
294
|
-
keys: "alt+p",
|
|
295
|
-
label: "Toggle percent scale",
|
|
296
|
-
category: "Chart",
|
|
297
|
-
run: () => {
|
|
298
|
-
const mode = this.inner?.renderer.get("scaleMode");
|
|
299
|
-
this.inner?.renderer.set("scaleMode", mode === "percent" ? "price" : "percent");
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
this.keymap.register({ id: "drawings.trendline", keys: "alt+t", label: "Arm the trend line tool", category: "Drawings", run: () => this.inner?.drawings.setTool("trendline") });
|
|
303
|
-
this.keymap.register({
|
|
304
|
-
id: "drawings.hline-cursor",
|
|
305
|
-
keys: "alt+h",
|
|
306
|
-
label: "Horizontal line at the cursor price",
|
|
307
|
-
category: "Drawings",
|
|
308
|
-
run: () => {
|
|
309
|
-
if (this.lastCrossPrice != null && this.lastCrossTime != null)
|
|
310
|
-
this.inner?.drawings.add("hline", { anchors: [{ time: this.lastCrossTime, price: this.lastCrossPrice }] });
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
this.keymap.register({
|
|
314
|
-
id: "drawings.vline-cursor",
|
|
315
|
-
keys: "alt+v",
|
|
316
|
-
label: "Vertical line at the cursor time",
|
|
317
|
-
category: "Drawings",
|
|
318
|
-
run: () => {
|
|
319
|
-
if (this.lastCrossTime != null && this.lastCrossPrice != null)
|
|
320
|
-
this.inner?.drawings.add("vline", { anchors: [{ time: this.lastCrossTime, price: this.lastCrossPrice }] });
|
|
321
|
-
}
|
|
19
|
+
const { urlState: _urlState, height: _height, persist, ...rest } = opts;
|
|
20
|
+
this.ws = new VelaWorkspace(container, {
|
|
21
|
+
...rest,
|
|
22
|
+
layout: false,
|
|
23
|
+
// The widget's historical storage key — existing persisted state restores.
|
|
24
|
+
persist: persist === true ? "vela-widget" : persist
|
|
322
25
|
});
|
|
323
|
-
this.keymap.register({ id: "history.undo", keys: ["mod+z"], label: "Undo", category: "Edit", run: () => this.history.undo() });
|
|
324
|
-
this.keymap.register({ id: "history.redo", keys: ["mod+y", "mod+shift+z"], label: "Redo", category: "Edit", run: () => this.history.redo() });
|
|
325
|
-
this.keymap.register({ id: "view.zoom-in", keys: "mod+arrowup", label: "Zoom in", category: "Chart", run: () => this.glider.zoom(ZOOM_IN) });
|
|
326
|
-
this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
|
|
327
|
-
this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.inner?.panBy(-PAN_FAST) });
|
|
328
|
-
this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.inner?.panBy(PAN_FAST) });
|
|
329
|
-
if (picker) this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => picker.open() });
|
|
330
|
-
this.keymap.register({
|
|
331
|
-
id: "help.shortcuts",
|
|
332
|
-
keys: "?",
|
|
333
|
-
label: "Show this shortcuts panel",
|
|
334
|
-
category: "Help",
|
|
335
|
-
run: () => {
|
|
336
|
-
this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open) => this.trackDialog(open)));
|
|
337
|
-
this.shortcutsHelp.open();
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
this.root.addEventListener("keydown", this.onRootKeydown);
|
|
341
|
-
this.root.tabIndex = -1;
|
|
342
|
-
this.rebuild();
|
|
343
|
-
this.mountAttachments();
|
|
344
|
-
if (opts.autofocus) this.inner?.renderer.focus();
|
|
345
|
-
}
|
|
346
|
-
/** The mode flipped (container resized across the breakpoint, or a coarse-pointer
|
|
347
|
-
* change). Close the open surfaces — a desktop card must not linger over the
|
|
348
|
-
* mobile chrome (and vice versa) — and re-present the renderer's own chrome. */
|
|
349
|
-
onLayoutModeChange(mode) {
|
|
350
|
-
this.symbolPicker.close();
|
|
351
|
-
this.indicatorPicker?.close();
|
|
352
|
-
this.tfDrawer?.close();
|
|
353
|
-
this.drawingsDrawer?.close();
|
|
354
|
-
this.moreDrawer?.close();
|
|
355
|
-
this.timezoneDrawer?.close();
|
|
356
|
-
this.priceScaleDrawer?.close();
|
|
357
|
-
this.inner?.renderer.closeDialogs();
|
|
358
|
-
this.inner?.renderer.setLayoutMode(mode);
|
|
359
|
-
}
|
|
360
|
-
// ── mobile drawers (built on first open; every list is read live at open time) ──
|
|
361
|
-
openTimeframeDrawer() {
|
|
362
|
-
this.tfDrawer ?? (this.tfDrawer = new TimeframeDrawer({
|
|
363
|
-
host: this.root,
|
|
364
|
-
timeframes: this.opts.timeframes ?? DEFAULT_TIMEFRAMES,
|
|
365
|
-
ranges: RANGE_PRESETS,
|
|
366
|
-
currentTimeframe: () => this.timeframe,
|
|
367
|
-
activeRange: () => this.activeRangeId,
|
|
368
|
-
onTimeframe: (tf) => this.setTimeframe(tf),
|
|
369
|
-
onRange: (preset) => this.applyRange(preset),
|
|
370
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
371
|
-
}));
|
|
372
|
-
this.tfDrawer.open();
|
|
373
|
-
}
|
|
374
|
-
openDrawingsDrawer() {
|
|
375
|
-
this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
|
|
376
|
-
host: this.root,
|
|
377
|
-
toolbar: () => buildToolbar(this.opts.drawings).definition,
|
|
378
|
-
currentTool: () => this.inner?.drawings.getTool() ?? null,
|
|
379
|
-
isFavorite: (type) => this.inner?.drawings.isFavorite(type) ?? false,
|
|
380
|
-
onFavorite: (type, on) => this.inner?.drawings.setFavorite(type, on),
|
|
381
|
-
onSelect: (type) => this.inner?.drawings.setTool(type),
|
|
382
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
383
|
-
}));
|
|
384
|
-
this.drawingsDrawer.open();
|
|
385
26
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
onRedo: () => this.history.redo(),
|
|
391
|
-
onScreenshot: () => this.downloadScreenshot(),
|
|
392
|
-
canUndo: () => this.history.canUndo,
|
|
393
|
-
canRedo: () => this.history.canRedo,
|
|
394
|
-
priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
|
|
395
|
-
priceStyle: () => this.priceStyle,
|
|
396
|
-
onPriceStyle: (id) => this.setPriceStyle(id),
|
|
397
|
-
panels: () => [...this.dock.list()],
|
|
398
|
-
onTogglePanel: (id) => this.dock.toggle(id),
|
|
399
|
-
alerts: () => this.alerts,
|
|
400
|
-
// Left-aligned actions have their own bottom-bar stop — only the rest
|
|
401
|
-
// lands in the drawer, or every left action would appear twice.
|
|
402
|
-
actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
|
|
403
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
404
|
-
}));
|
|
405
|
-
this.moreDrawer.open();
|
|
27
|
+
/** The shell root element (the workspace root; carries `vela-widget` as an alias). */
|
|
28
|
+
get root() {
|
|
29
|
+
this.ws.root.classList.add("vela-widget");
|
|
30
|
+
return this.ws.root;
|
|
406
31
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
host: this.root,
|
|
410
|
-
timezone: () => this.timezone,
|
|
411
|
-
onTimezone: (zone) => this.setTimezone(zone),
|
|
412
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
413
|
-
}));
|
|
414
|
-
this.timezoneDrawer.open();
|
|
32
|
+
get keymap() {
|
|
33
|
+
return this.ws.keymap;
|
|
415
34
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
host: this.root,
|
|
420
|
-
chart: () => this.inner,
|
|
421
|
-
pressY: () => this.priceScalePressY,
|
|
422
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
423
|
-
}));
|
|
424
|
-
this.priceScaleDrawer.open();
|
|
35
|
+
/** The chart's unified undo timeline — resolve it fresh, never cache it. */
|
|
36
|
+
get history() {
|
|
37
|
+
return this.ws.active.history;
|
|
425
38
|
}
|
|
426
|
-
/**
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (this.attachmentDisposers.has(att.id)) continue;
|
|
430
|
-
try {
|
|
431
|
-
this.attachmentDisposers.set(att.id, att.mount(this.context()));
|
|
432
|
-
} catch (err) {
|
|
433
|
-
console.warn(`[vela] widget attachment "${att.id}" failed to mount:`, err);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
39
|
+
/** The inner headless chart. */
|
|
40
|
+
get chart() {
|
|
41
|
+
return this.ws.chart;
|
|
436
42
|
}
|
|
437
|
-
/**
|
|
43
|
+
/** A fresh contribution context (the workspace's is a superset of the widget's). */
|
|
438
44
|
context() {
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
get chart() {
|
|
442
|
-
return self.chart;
|
|
443
|
-
},
|
|
444
|
-
symbol: this.symbol,
|
|
445
|
-
timeframe: this.timeframe,
|
|
446
|
-
priceStyle: this.priceStyle,
|
|
447
|
-
setSymbol: (symbol) => this.setSymbol(symbol),
|
|
448
|
-
setTimeframe: (tf) => this.setTimeframe(tf),
|
|
449
|
-
setPriceStyle: (style) => this.setPriceStyle(style),
|
|
450
|
-
openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
|
|
451
|
-
togglePanel: (id, open) => this.dock.toggle(id, open),
|
|
452
|
-
host: this.root,
|
|
453
|
-
toast: (message, kind) => this.toast?.show(message, kind)
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
/** The recent engine alerts (topbar bell). */
|
|
457
|
-
openAlertsMenu(anchor) {
|
|
458
|
-
this.alertsMenu?.destroy();
|
|
459
|
-
const items = this.alerts.length ? this.alerts.map((a, i) => ({
|
|
460
|
-
id: String(i),
|
|
461
|
-
label: `${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 70)
|
|
462
|
-
})) : [{ id: "none", label: "No alerts yet", disabled: true }];
|
|
463
|
-
this.alertsMenu = new Menu({ host: this.root, items, onSelect: () => {
|
|
464
|
-
} });
|
|
465
|
-
const r = anchor.getBoundingClientRect();
|
|
466
|
-
this.alertsMenu.openAt(r.left, r.bottom + 4);
|
|
45
|
+
return this.ws.context();
|
|
467
46
|
}
|
|
468
|
-
/** Re-project contributed topbar actions and side panels (after late registrations). */
|
|
469
47
|
refreshActions() {
|
|
470
|
-
this.
|
|
471
|
-
this.topbar.renderActions();
|
|
472
|
-
this.mobileBar?.renderActions();
|
|
473
|
-
this.dock.refresh();
|
|
474
|
-
if (this.inner) this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => this.context()));
|
|
475
|
-
}
|
|
476
|
-
/** The inner headless chart of the CURRENT build — becomes a new instance after a
|
|
477
|
-
* symbol/timeframe change; don't cache it across awaits. */
|
|
478
|
-
get chart() {
|
|
479
|
-
if (!this.inner) throw new Error("VelaWidget is destroyed");
|
|
480
|
-
return this.inner;
|
|
481
|
-
}
|
|
482
|
-
/** Chrome that must follow the timeframe wherever it changes from (menu, quick entry,
|
|
483
|
-
* range chip, API). Keeping it in one place is what stopped the status line from
|
|
484
|
-
* drifting out of sync with the topbar. */
|
|
485
|
-
syncTimeframeChrome(tf) {
|
|
486
|
-
this.timeframe = tf;
|
|
487
|
-
this.topbar.setTimeframe(tf);
|
|
488
|
-
this.mobileBar?.setTimeframe(tf);
|
|
489
|
-
this.watermark?.update(this.symbol, tf);
|
|
490
|
-
this.statusline?.setMeta(tf, this.providerLabel());
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* The venue label to show: the LISTING prefix the resolved symbol's data declares
|
|
494
|
-
* (`NASDAQ` for AAPL), else the provider that RESOLVED the symbol — never the one
|
|
495
|
-
* configured or typed. They differ whenever the `provider` option is just a default,
|
|
496
|
-
* the prefix was a listing venue, or the spelling isn't registered — the status line
|
|
497
|
-
* must not claim a venue that served nothing.
|
|
498
|
-
*/
|
|
499
|
-
providerLabel() {
|
|
500
|
-
const display = this.inner?.data.displayPrefix(this.symbol);
|
|
501
|
-
return display ?? parseSymbol(this.symbol).provider ?? "";
|
|
502
|
-
}
|
|
503
|
-
setTimeframe(tf) {
|
|
504
|
-
if (tf === this.timeframe || this.destroyed) return;
|
|
505
|
-
this.activeRangeId = null;
|
|
506
|
-
this.bottombar?.setActiveRange(null);
|
|
507
|
-
this.rangeBars = 0;
|
|
508
|
-
this.syncTimeframeChrome(tf);
|
|
509
|
-
this.markStateDirty();
|
|
510
|
-
void this.inner?.setMarket({ timeframe: tf, bars: this.bars });
|
|
511
|
-
}
|
|
512
|
-
/**
|
|
513
|
-
* Switch the shown trading session (RTH/ETH) in place — a full reload, like a
|
|
514
|
-
* timeframe change (the two sessions are different bar series). No-op on a chart
|
|
515
|
-
* already showing that session; meaningless on continuous markets (the toggle is
|
|
516
|
-
* disabled there, but the API tolerates the call — the provider ignores the flag).
|
|
517
|
-
*/
|
|
518
|
-
setSession(session) {
|
|
519
|
-
if (session === this.session || session === "regular" && this.session === void 0 || this.destroyed) return;
|
|
520
|
-
this.session = session;
|
|
521
|
-
this.refreshSessionToggle();
|
|
522
|
-
this.markStateDirty();
|
|
523
|
-
void this.inner?.setMarket({ session });
|
|
524
|
-
}
|
|
525
|
-
/** Re-derive the session chrome from the ACTIVE symbol's metadata: the RTH/ETH
|
|
526
|
-
* toggle shows (and the settings dialog's Trading session group appears) iff the
|
|
527
|
-
* resolved symbol declares a real session vocabulary (not `24x7`); the pre/post
|
|
528
|
-
* shading re-derives for the new market either way. */
|
|
529
|
-
refreshSessionToggle() {
|
|
530
|
-
const chart = this.inner;
|
|
531
|
-
if (!chart) return;
|
|
532
|
-
void chart.data.symbolInfo(this.symbol).then((si) => {
|
|
533
|
-
if (this.destroyed || this.inner !== chart) return;
|
|
534
|
-
const enabled = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
535
|
-
this.bottombar?.setSession({ session: this.session ?? "regular", enabled });
|
|
536
|
-
if (enabled !== this.sessionsAvailable) {
|
|
537
|
-
this.sessionsAvailable = enabled;
|
|
538
|
-
this.pushSettingsSections();
|
|
539
|
-
}
|
|
540
|
-
this.refreshSessionShading();
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
/** (Re)derive the pre/post-market shading bands for the current market. The loaded
|
|
544
|
-
* depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
|
|
545
|
-
refreshSessionShading() {
|
|
546
|
-
const chart = this.inner;
|
|
547
|
-
if (!chart) return;
|
|
548
|
-
const lookbackMs = Math.max(this.bars, this.rangeBars) * timeframeMs(this.timeframe);
|
|
549
|
-
this.sessionShading.track(chart.data, this.symbol, { session: this.session ?? "regular", lookbackMs });
|
|
48
|
+
this.ws.refreshActions();
|
|
550
49
|
}
|
|
551
|
-
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
552
|
-
* live by the dialog swatch) — the widget only proxies them into its settings rows. */
|
|
553
|
-
sessionShadeColor(key) {
|
|
554
|
-
const cfg = this.inner?.renderer.getConfig();
|
|
555
|
-
const v = cfg?.sessions?.[key];
|
|
556
|
-
return typeof v === "string" ? v : "";
|
|
557
|
-
}
|
|
558
|
-
setSessionShadeColor(key, color) {
|
|
559
|
-
this.inner?.renderer.applyConfig({ sessions: { [key]: color } });
|
|
560
|
-
this.markStateDirty();
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* (Re)contribute the widget's settings-dialog sections: status line parts, the fetch
|
|
564
|
-
* depth, the watermark toggle, and — only while the active symbol HAS sessions — the
|
|
565
|
-
* Trading session group (RTH/ETH switch + the pre/post-market shading colors) inside
|
|
566
|
-
* the Symbol tab. Re-run whenever a gate changes (the dialog reads them on open).
|
|
567
|
-
*/
|
|
568
|
-
pushSettingsSections() {
|
|
569
|
-
const chart = this.inner;
|
|
570
|
-
if (!chart) return;
|
|
571
|
-
const rth = "Regular hours (RTH)";
|
|
572
|
-
const eth = "Extended hours (ETH)";
|
|
573
|
-
const sessionSection = {
|
|
574
|
-
title: "Trading session",
|
|
575
|
-
placement: "symbol",
|
|
576
|
-
rows: [
|
|
577
|
-
{
|
|
578
|
-
kind: "select",
|
|
579
|
-
label: "Session",
|
|
580
|
-
options: [rth, eth],
|
|
581
|
-
get: () => (this.session ?? "regular") === "extended" ? eth : rth,
|
|
582
|
-
set: (v) => this.setSession(v === eth ? "extended" : "regular")
|
|
583
|
-
},
|
|
584
|
-
{
|
|
585
|
-
kind: "color",
|
|
586
|
-
label: "Pre-market",
|
|
587
|
-
get: () => this.sessionShadeColor("premarketColor"),
|
|
588
|
-
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
589
|
-
},
|
|
590
|
-
{
|
|
591
|
-
kind: "color",
|
|
592
|
-
label: "Post-market",
|
|
593
|
-
get: () => this.sessionShadeColor("postmarketColor"),
|
|
594
|
-
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
595
|
-
}
|
|
596
|
-
]
|
|
597
|
-
};
|
|
598
|
-
const advanced = {
|
|
599
|
-
title: "Advanced",
|
|
600
|
-
placement: "end",
|
|
601
|
-
rows: [
|
|
602
|
-
{
|
|
603
|
-
kind: "select",
|
|
604
|
-
label: "Bars to fetch",
|
|
605
|
-
options: ["500", "1000", "2000", "5000", "10000", "20000"],
|
|
606
|
-
get: () => String(this.bars),
|
|
607
|
-
set: (v) => {
|
|
608
|
-
this.bars = Number(v);
|
|
609
|
-
this.markStateDirty();
|
|
610
|
-
void this.inner?.setMarket({ bars: Math.max(this.bars, this.rangeBars) });
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
]
|
|
614
|
-
};
|
|
615
|
-
const watermarkSection = {
|
|
616
|
-
title: "Watermark",
|
|
617
|
-
placement: "symbol",
|
|
618
|
-
rows: [
|
|
619
|
-
{
|
|
620
|
-
kind: "toggle",
|
|
621
|
-
label: "Symbol watermark",
|
|
622
|
-
get: () => this.watermarkOn,
|
|
623
|
-
set: (v) => this.setWatermarkVisible(v)
|
|
624
|
-
}
|
|
625
|
-
]
|
|
626
|
-
};
|
|
627
|
-
const sections = [];
|
|
628
|
-
if (this.statusline) {
|
|
629
|
-
const sl = this.statusline;
|
|
630
|
-
sections.push({
|
|
631
|
-
title: "Status line",
|
|
632
|
-
rows: [
|
|
633
|
-
{ kind: "heading", label: "Status line" },
|
|
634
|
-
{ kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
|
|
635
|
-
{ kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
|
|
636
|
-
{ kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
|
|
637
|
-
{ kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
|
|
638
|
-
{ kind: "heading", label: "Indicators" },
|
|
639
|
-
{
|
|
640
|
-
kind: "toggle",
|
|
641
|
-
label: "Titles",
|
|
642
|
-
get: () => this.indicatorTitlesOn,
|
|
643
|
-
set: (v) => this.setIndicatorTitlesVisible(v)
|
|
644
|
-
},
|
|
645
|
-
{
|
|
646
|
-
kind: "toggle",
|
|
647
|
-
label: "Values",
|
|
648
|
-
get: () => this.indicatorValuesOn,
|
|
649
|
-
set: (v) => this.setIndicatorValuesVisible(v)
|
|
650
|
-
}
|
|
651
|
-
]
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
sections.push(advanced);
|
|
655
|
-
if (this.sessionsAvailable) sections.push(sessionSection);
|
|
656
|
-
sections.push(watermarkSection);
|
|
657
|
-
chart.renderer.setSettingsSections(sections);
|
|
658
|
-
}
|
|
659
|
-
/** Star/unstar a timeframe — the topbar chips and dropdown stars follow, and the
|
|
660
|
-
* set persists with the rest of the shell state. */
|
|
661
|
-
setTimeframeFavorite(tf, on) {
|
|
662
|
-
if (on === this.tfFavs.includes(tf)) return;
|
|
663
|
-
this.tfFavs = on ? [tf, ...this.tfFavs] : this.tfFavs.filter((f) => f !== tf);
|
|
664
|
-
this.topbar.setTimeframeFavorites(this.tfFavs);
|
|
665
|
-
this.markStateDirty();
|
|
666
|
-
}
|
|
667
|
-
/**
|
|
668
|
-
* Switch the chart symbol in place. An `EXCHANGE:` prefix pins the venue (the
|
|
669
|
-
* symbol picker composes one from the row the user pointed at) — a bare ticker
|
|
670
|
-
* resolves against the registered providers in declaration order.
|
|
671
|
-
*/
|
|
672
50
|
setSymbol(symbol) {
|
|
673
|
-
|
|
674
|
-
if (symbol === this.symbol) return;
|
|
675
|
-
this.unresolvedToasted = null;
|
|
676
|
-
this.symbol = symbol;
|
|
677
|
-
this.topbar.setSymbol(symbol);
|
|
678
|
-
this.mobileBar?.setSymbol(symbol);
|
|
679
|
-
this.statusline?.setSymbol(symbol);
|
|
680
|
-
this.objectTree.setSymbol(symbol);
|
|
681
|
-
this.watermark?.update(symbol, this.timeframe);
|
|
682
|
-
this.markStateDirty();
|
|
683
|
-
void this.inner?.setMarket({ symbol });
|
|
684
|
-
this.statusline?.setMeta(this.timeframe, this.providerLabel());
|
|
685
|
-
}
|
|
686
|
-
// ── state surface (same triplet as the workspace: getState / applyState / state:changed) ──
|
|
687
|
-
/**
|
|
688
|
-
* Snapshot the COMPLETE widget state as the unified shell document — the SAME
|
|
689
|
-
* format `VelaWorkspace.getState()` returns, with a single `c1` cell: market,
|
|
690
|
-
* display prefs, the renderer config document, the user-drawings document, and
|
|
691
|
-
* the indicator ledger. This is what `persist` writes; hosts build custom flows
|
|
692
|
-
* on it (server snapshots, share links, templates) — and a saved widget state
|
|
693
|
-
* drops into a workspace slot as-is.
|
|
694
|
-
*/
|
|
695
|
-
getState() {
|
|
696
|
-
const cell = {};
|
|
697
|
-
const live = this.inner?.market;
|
|
698
|
-
const symbol = live?.symbol ?? this.symbol;
|
|
699
|
-
if (symbol) cell.symbol = symbol;
|
|
700
|
-
const provider = parseSymbol(symbol ?? "").provider ?? void 0;
|
|
701
|
-
if (provider) cell.provider = provider;
|
|
702
|
-
cell.timeframe = live?.timeframe ?? this.timeframe;
|
|
703
|
-
cell.priceStyle = this.priceStyle;
|
|
704
|
-
if (this.bars > 0) cell.bars = this.bars;
|
|
705
|
-
const session = normalizeSession(live?.session ?? this.session);
|
|
706
|
-
if (session === "extended") cell.session = session;
|
|
707
|
-
cell.watermark = this.watermarkOn;
|
|
708
|
-
cell.indicatorTitles = this.indicatorTitlesOn;
|
|
709
|
-
cell.indicatorValues = this.indicatorValuesOn;
|
|
710
|
-
if (this.inner) {
|
|
711
|
-
cell.rendererConfig = this.inner.renderer.getConfig();
|
|
712
|
-
cell.drawings = this.inner.drawings.toJSON();
|
|
713
|
-
} else {
|
|
714
|
-
if (this.savedConfig != null) cell.rendererConfig = this.savedConfig;
|
|
715
|
-
if (this.savedDrawings != null) cell.drawings = this.savedDrawings;
|
|
716
|
-
}
|
|
717
|
-
cell.indicators = indicatorLedger({
|
|
718
|
-
present: this.inner ? this.inner.presentNativeIndicators() : this.pendingIndicators?.natives ?? [],
|
|
719
|
-
instanceNames: this.instances.map((it) => it.entry.name),
|
|
720
|
-
pendingManifest: this.pendingIndicators?.manifest ?? null,
|
|
721
|
-
manifestSettled: this.manifestSettled,
|
|
722
|
-
volumePending: this.volumeMayBePending && (this.ledgerVolume ?? this.opts.volume !== false)
|
|
723
|
-
});
|
|
724
|
-
const state = { version: 1, layout: "1", activeCellId: "c1", timezone: this.timezone, charts: [{ id: "c1", ...cell }] };
|
|
725
|
-
if (this.favs.length > 0) state.favorites = [...this.favs];
|
|
726
|
-
if (this.tfFavs.length > 0) state.timeframeFavorites = [...this.tfFavs];
|
|
727
|
-
const panels = this.dock.getState();
|
|
728
|
-
if (panels) state.panels = panels;
|
|
729
|
-
return state;
|
|
51
|
+
this.ws.active.setSymbol(symbol);
|
|
730
52
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
* first cell of a multi-cell document applies). Untrusted-safe: malformed fields
|
|
734
|
-
* are dropped. Applied IN PLACE: the chart instance survives (market switches via
|
|
735
|
-
* `setMarket`), config/drawings/indicators are replaced. With `urlState` enabled,
|
|
736
|
-
* URL params still win over the document's market/prefs fields.
|
|
737
|
-
*/
|
|
738
|
-
applyState(state) {
|
|
739
|
-
if (this.destroyed) return;
|
|
740
|
-
const st = sanitizeState(state);
|
|
741
|
-
if (!st) return;
|
|
742
|
-
const cell = st.charts.find((c) => c.id === "c1") ?? st.charts[0];
|
|
743
|
-
const fromUrl = this.opts.urlState ? readUrlState(typeof location !== "undefined" ? location.search : "") : {};
|
|
744
|
-
const tz = fromUrl.timezone ?? st.timezone;
|
|
745
|
-
if (tz && tz !== this.timezone) this.setTimezone(tz);
|
|
746
|
-
if (st.favorites) {
|
|
747
|
-
this.favs = [...st.favorites];
|
|
748
|
-
this.inner?.drawings.setFavorites(this.favs);
|
|
749
|
-
}
|
|
750
|
-
if (st.timeframeFavorites) {
|
|
751
|
-
this.tfFavs = [...st.timeframeFavorites];
|
|
752
|
-
this.topbar.setTimeframeFavorites(this.tfFavs);
|
|
753
|
-
}
|
|
754
|
-
this.dock.applyState(st.panels);
|
|
755
|
-
if (cell) {
|
|
756
|
-
const style = fromUrl.priceStyle ?? cell.priceStyle;
|
|
757
|
-
if (style && style !== this.priceStyle) this.setPriceStyle(style);
|
|
758
|
-
if (cell.watermark !== void 0 && cell.watermark !== this.watermarkOn) this.setWatermarkVisible(cell.watermark);
|
|
759
|
-
if (cell.indicatorTitles !== void 0 && cell.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cell.indicatorTitles);
|
|
760
|
-
if (cell.indicatorValues !== void 0 && cell.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cell.indicatorValues);
|
|
761
|
-
if (cell.rendererConfig != null) {
|
|
762
|
-
this.savedConfig = cell.rendererConfig;
|
|
763
|
-
this.inner?.renderer.applyConfig(cell.rendererConfig);
|
|
764
|
-
}
|
|
765
|
-
if (cell.drawings != null) {
|
|
766
|
-
this.savedDrawings = cell.drawings;
|
|
767
|
-
this.inner?.drawings.fromJSON(cell.drawings);
|
|
768
|
-
}
|
|
769
|
-
if (cell.indicators) this.applyIndicatorLedger(cell.indicators);
|
|
770
|
-
const symbol = fromUrl.symbol ?? prefixedSymbol(cell);
|
|
771
|
-
const timeframe = fromUrl.timeframe ?? cell.timeframe;
|
|
772
|
-
const bars = Number(fromUrl.bars ?? cell.bars ?? 0);
|
|
773
|
-
const session = normalizeSession(fromUrl.session ?? cell.session);
|
|
774
|
-
const next = {};
|
|
775
|
-
if (symbol && symbol !== this.symbol) next.symbol = symbol;
|
|
776
|
-
if (timeframe && timeframe !== this.timeframe) next.timeframe = timeframe;
|
|
777
|
-
if (session && session !== (this.session ?? "regular")) {
|
|
778
|
-
this.session = session;
|
|
779
|
-
next.session = session;
|
|
780
|
-
this.refreshSessionToggle();
|
|
781
|
-
}
|
|
782
|
-
if (bars > 0 && bars !== this.bars) {
|
|
783
|
-
this.bars = bars;
|
|
784
|
-
next.bars = Math.max(bars, this.rangeBars);
|
|
785
|
-
}
|
|
786
|
-
if (Object.keys(next).length > 0) void this.inner?.setMarket(next);
|
|
787
|
-
}
|
|
788
|
-
this.markStateDirty();
|
|
789
|
-
}
|
|
790
|
-
/** Subscribe to `state:changed` — the persistable state changed (debounced ~500ms);
|
|
791
|
-
* re-pull {@link getState}. Returns an unsubscribe function. */
|
|
792
|
-
on(event, handler) {
|
|
793
|
-
if (event !== "state:changed") return () => void 0;
|
|
794
|
-
this.stateListeners.add(handler);
|
|
795
|
-
return () => this.stateListeners.delete(handler);
|
|
53
|
+
setTimeframe(timeframe) {
|
|
54
|
+
this.ws.active.setTimeframe(timeframe);
|
|
796
55
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
* manifest instances are re-created by name — held until the manifest resolves. */
|
|
800
|
-
applyIndicatorLedger(led) {
|
|
801
|
-
const chart = this.inner;
|
|
802
|
-
if (!chart) return;
|
|
803
|
-
this.ledgerVolume = led.natives.includes("volume");
|
|
804
|
-
this.history.silently(() => {
|
|
805
|
-
for (const type of led.natives) {
|
|
806
|
-
if (!this.nativeCatalog.some((n) => n.type === type && n.present)) chart.addNativeIndicator(type);
|
|
807
|
-
}
|
|
808
|
-
for (const n of this.nativeCatalog) {
|
|
809
|
-
if (n.present && !led.natives.includes(n.type)) chart.addNativeIndicator(n.type).remove();
|
|
810
|
-
}
|
|
811
|
-
for (const it of [...this.instances]) this.dropInstance(it);
|
|
812
|
-
});
|
|
813
|
-
if (this.manifest.length > 0) {
|
|
814
|
-
for (const name of led.manifest) {
|
|
815
|
-
const entry = this.manifest.find((e) => e.name === name);
|
|
816
|
-
if (entry) this.instances.push({ entry, handle: this.addToChart(chart, entry) });
|
|
817
|
-
}
|
|
818
|
-
this.pendingIndicators = null;
|
|
819
|
-
} else if (!this.manifestSettled) {
|
|
820
|
-
this.pendingIndicators = led;
|
|
821
|
-
} else {
|
|
822
|
-
this.pendingIndicators = null;
|
|
823
|
-
}
|
|
824
|
-
this.refreshNativeCatalog();
|
|
825
|
-
this.syncIndicatorCount();
|
|
826
|
-
}
|
|
827
|
-
/** SYNC boot fallback: a LEGACY three-key payload read at construction. */
|
|
828
|
-
bootLegacySync(rawMain) {
|
|
829
|
-
if (this.storageKey === null) return null;
|
|
830
|
-
const cfg = this.storage.get(`${this.storageKey}:config`);
|
|
831
|
-
const drw = this.storage.get(`${this.storageKey}:drawings`);
|
|
832
|
-
if (cfg instanceof Promise || drw instanceof Promise) return null;
|
|
833
|
-
const doc = sanitizeState(legacyWidgetState(parsePersisted(rawMain), cfg, drw));
|
|
834
|
-
if (doc) this.legacyKeys = true;
|
|
835
|
-
return doc;
|
|
56
|
+
setSession(session) {
|
|
57
|
+
this.ws.active.setSession(session);
|
|
836
58
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
async bootAsync(rawMain) {
|
|
840
|
-
let doc = null;
|
|
841
|
-
try {
|
|
842
|
-
const raw = await rawMain;
|
|
843
|
-
doc = raw != null ? decodeState(raw) : null;
|
|
844
|
-
if (!doc && this.storageKey !== null) {
|
|
845
|
-
const [cfg, drw] = await Promise.all([
|
|
846
|
-
Promise.resolve(this.storage.get(`${this.storageKey}:config`)),
|
|
847
|
-
Promise.resolve(this.storage.get(`${this.storageKey}:drawings`))
|
|
848
|
-
]);
|
|
849
|
-
doc = sanitizeState(legacyWidgetState(parsePersisted(raw), cfg, drw));
|
|
850
|
-
if (doc) this.legacyKeys = true;
|
|
851
|
-
}
|
|
852
|
-
} catch {
|
|
853
|
-
doc = null;
|
|
854
|
-
}
|
|
855
|
-
if (doc && !this.destroyed) this.applyState(doc);
|
|
59
|
+
setPriceStyle(style) {
|
|
60
|
+
this.ws.active.setPriceStyle(style);
|
|
856
61
|
}
|
|
857
|
-
/** Show/hide the symbol watermark behind the chart (persisted). */
|
|
858
62
|
setWatermarkVisible(visible) {
|
|
859
|
-
this.
|
|
860
|
-
this.watermark?.setVisible(visible);
|
|
861
|
-
this.markStateDirty();
|
|
63
|
+
this.ws.active.setWatermarkVisible(visible);
|
|
862
64
|
}
|
|
863
|
-
/** Show/hide the indicator titles — the in-chart legend rows (persisted). */
|
|
864
65
|
setIndicatorTitlesVisible(visible) {
|
|
865
|
-
this.
|
|
866
|
-
this.inner?.renderer.set("indicatorTitles", visible);
|
|
867
|
-
this.markStateDirty();
|
|
66
|
+
this.ws.active.setIndicatorTitlesVisible(visible);
|
|
868
67
|
}
|
|
869
|
-
/** Show/hide the plot values beside every indicator's legend title (persisted). */
|
|
870
68
|
setIndicatorValuesVisible(visible) {
|
|
871
|
-
this.
|
|
872
|
-
this.inner?.renderer.set("indicatorValues", visible);
|
|
873
|
-
this.markStateDirty();
|
|
69
|
+
this.ws.active.setIndicatorValuesVisible(visible);
|
|
874
70
|
}
|
|
875
|
-
/** Applied LIVE (renderer feature) — no rebuild; persists across rebuilds. */
|
|
876
|
-
setPriceStyle(style) {
|
|
877
|
-
this.priceStyle = style;
|
|
878
|
-
this.topbar.setPriceStyle(style);
|
|
879
|
-
this.inner?.renderer.set("priceStyle", style);
|
|
880
|
-
this.syncStatuslineColors();
|
|
881
|
-
this.markStateDirty();
|
|
882
|
-
}
|
|
883
|
-
/** OHLC/change ink in the status line follows the ACTIVE price style's configured
|
|
884
|
-
* colors and direction rule (candle bodies by close-vs-open, baseline by position
|
|
885
|
-
* against the live baseline price, …) instead of the fixed theme tokens. */
|
|
886
|
-
syncStatuslineColors() {
|
|
887
|
-
if (!this.statusline || !this.inner) return;
|
|
888
|
-
this.statusline.setDirectionColors(...statuslineInkOf(this.inner.renderer, this.priceStyle));
|
|
889
|
-
}
|
|
890
|
-
/** The widget shell (topbar, panels) keeps the app theme; the chart host's tokens
|
|
891
|
-
* re-derive from the LIVE plot surface (see {@link applyPlotOverlayTokens}). */
|
|
892
|
-
syncPlotOverlayTokens() {
|
|
893
|
-
applyPlotOverlayTokens(this.chartHost, resolveTheme(this.opts.theme), this.inner?.renderer.getConfig() ?? null);
|
|
894
|
-
}
|
|
895
|
-
/**
|
|
896
|
-
* Swap the widget theme at runtime — `'dark'`, `'light'`, or a full custom theme.
|
|
897
|
-
* Applied LIVE: the inner chart re-skins through `Vela.setTheme` and its
|
|
898
|
-
* `theme:changed` event re-tokens the widget chrome (topbar, menus, panels) in
|
|
899
|
-
* place; the choice also persists into rebuilds. Same switch the user reaches from
|
|
900
|
-
* chart settings → Canvas → Theme.
|
|
901
|
-
*/
|
|
902
71
|
setTheme(theme) {
|
|
903
|
-
|
|
904
|
-
this.opts.theme = theme;
|
|
905
|
-
this.inner?.setTheme(theme);
|
|
72
|
+
this.ws.setTheme(theme);
|
|
906
73
|
}
|
|
907
|
-
/** Applied LIVE (renderer feature) — no rebuild; persists across rebuilds. */
|
|
908
74
|
setTimezone(zone) {
|
|
909
|
-
this.
|
|
910
|
-
this.bottombar?.setTimezone(zone);
|
|
911
|
-
this.inner?.renderer.set("timezone", zone);
|
|
912
|
-
this.markStateDirty();
|
|
75
|
+
this.ws.setTimezone(zone);
|
|
913
76
|
}
|
|
914
|
-
/**
|
|
915
|
-
* Range chip: switch to the preset's timeframe, fetch enough history for its window,
|
|
916
|
-
* and frame it once ready. The in-place `setMarket` loads the depth, so it also runs
|
|
917
|
-
* when only the DEPTH grows (same timeframe, deeper window).
|
|
918
|
-
*/
|
|
919
77
|
applyRange(preset) {
|
|
920
|
-
|
|
921
|
-
this.activeRangeId = preset.id;
|
|
922
|
-
this.bottombar?.setActiveRange(preset.id);
|
|
923
|
-
this.pendingRange = preset;
|
|
924
|
-
const tfChanged = preset.tf !== this.timeframe;
|
|
925
|
-
const deeper = preset.bars > Math.max(this.bars, this.rangeBars);
|
|
926
|
-
this.rangeBars = preset.bars;
|
|
927
|
-
if (tfChanged) this.syncTimeframeChrome(preset.tf);
|
|
928
|
-
if (tfChanged || deeper) {
|
|
929
|
-
void this.inner?.setMarket({ timeframe: preset.tf, bars: Math.max(this.bars, this.rangeBars), visibleRange: preset.preset }).then(() => {
|
|
930
|
-
if (!this.destroyed && this.pendingRange === preset) {
|
|
931
|
-
this.inner?.setVisibleRangePreset(preset.preset);
|
|
932
|
-
this.pendingRange = null;
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
} else {
|
|
936
|
-
this.inner?.setVisibleRangePreset(preset.preset);
|
|
937
|
-
this.pendingRange = null;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
destroy() {
|
|
941
|
-
this.destroyed = true;
|
|
942
|
-
this.persistNow();
|
|
943
|
-
this.inner?.destroy();
|
|
944
|
-
this.inner = null;
|
|
945
|
-
for (const dispose of this.attachmentDisposers.values()) {
|
|
946
|
-
try {
|
|
947
|
-
dispose();
|
|
948
|
-
} catch {
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
this.attachmentDisposers.clear();
|
|
952
|
-
window.removeEventListener("beforeunload", this.onUnload);
|
|
953
|
-
this.root.removeEventListener("keydown", this.onRootKeydown);
|
|
954
|
-
this.topbar.destroy();
|
|
955
|
-
this.dock.destroy();
|
|
956
|
-
this.objectTree.destroy();
|
|
957
|
-
this.dataWindow.destroy();
|
|
958
|
-
this.shortcutsHelp?.destroy();
|
|
959
|
-
this.contextMenu.destroy();
|
|
960
|
-
this.symbolPicker.destroy();
|
|
961
|
-
this.indicatorPicker?.destroy();
|
|
962
|
-
this.tfQuick.destroy();
|
|
963
|
-
this.marketStatus?.stop();
|
|
964
|
-
this.sessionShading.stop();
|
|
965
|
-
this.statusline?.destroy();
|
|
966
|
-
this.watermark?.destroy();
|
|
967
|
-
this.bottombar?.destroy();
|
|
968
|
-
this.mobileBar?.destroy();
|
|
969
|
-
this.drawingPill.destroy();
|
|
970
|
-
this.tfDrawer?.destroy();
|
|
971
|
-
this.drawingsDrawer?.destroy();
|
|
972
|
-
this.moreDrawer?.destroy();
|
|
973
|
-
this.timezoneDrawer?.destroy();
|
|
974
|
-
this.priceScaleDrawer?.destroy();
|
|
975
|
-
this.toast?.destroy();
|
|
976
|
-
this.alertsMenu?.destroy();
|
|
977
|
-
this.glider.stop();
|
|
978
|
-
this.history.destroy();
|
|
979
|
-
this.keymap.destroy();
|
|
980
|
-
this.layoutCtl.destroy();
|
|
981
|
-
this.root.remove();
|
|
982
|
-
}
|
|
983
|
-
/** Destroy + recreate the inner chart with the current symbol/timeframe, then
|
|
984
|
-
* re-register providers/engines, rebind chrome, and re-add manifest indicators. */
|
|
985
|
-
rebuild() {
|
|
986
|
-
const seq = ++this.buildSeq;
|
|
987
|
-
if (this.inner) {
|
|
988
|
-
this.savedConfig = this.inner.renderer.getConfig();
|
|
989
|
-
this.savedDrawings = this.inner.drawings.toJSON();
|
|
990
|
-
}
|
|
991
|
-
this.inner?.destroy();
|
|
992
|
-
const {
|
|
993
|
-
providers,
|
|
994
|
-
engines,
|
|
995
|
-
indicators,
|
|
996
|
-
timeframes: _timeframes,
|
|
997
|
-
priceStyle: _priceStyle,
|
|
998
|
-
timezone: _timezone,
|
|
999
|
-
statusline: _statusline,
|
|
1000
|
-
watermark: _watermark,
|
|
1001
|
-
bottombar: _bottombar,
|
|
1002
|
-
layoutMode: _layoutMode,
|
|
1003
|
-
...chartOpts
|
|
1004
|
-
} = this.opts;
|
|
1005
|
-
const chart = new Vela(this.chartHost, {
|
|
1006
|
-
...chartOpts,
|
|
1007
|
-
symbol: this.symbol,
|
|
1008
|
-
timeframe: this.timeframe,
|
|
1009
|
-
bars: Math.max(this.bars, this.rangeBars),
|
|
1010
|
-
session: this.session,
|
|
1011
|
-
volume: this.ledgerVolume ?? chartOpts.volume,
|
|
1012
|
-
// A pending range chip frames the FIRST paint (no preview flash, no re-frame).
|
|
1013
|
-
...this.pendingRange ? { visibleRange: this.pendingRange.preset } : {}
|
|
1014
|
-
});
|
|
1015
|
-
for (const [name, make] of Object.entries(providers ?? {})) chart.data.registerProvider(name, make());
|
|
1016
|
-
for (const [language, make] of Object.entries(resolveEngines(engines))) chart.registerEngine(language, make());
|
|
1017
|
-
this.inner = chart;
|
|
1018
|
-
this.symbolPicker.setSource(() => chart.data.symbols());
|
|
1019
|
-
void chart.data.ready().then(() => {
|
|
1020
|
-
if (this.inner !== chart) return;
|
|
1021
|
-
this.statusline?.setMeta(this.timeframe, this.providerLabel());
|
|
1022
|
-
this.refreshSessionToggle();
|
|
1023
|
-
this.marketStatus?.track(chart.data, this.symbol);
|
|
1024
|
-
});
|
|
1025
|
-
if (this.layoutCtl !== void 0) chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
1026
|
-
this.objectTree.setSymbol(this.symbol);
|
|
1027
|
-
chart.renderer.setLegendActions(legendActionsProviderFor(chart, () => this.context()));
|
|
1028
|
-
if (chart.renderer.supports("historyChords")) chart.renderer.set("historyChords", false);
|
|
1029
|
-
this.dock.onChart(chart);
|
|
1030
|
-
this.contextMenu.onChart(chart);
|
|
1031
|
-
chart.renderer.onAxisLongPress((e) => {
|
|
1032
|
-
if (this.layoutCtl.current !== "mobile") return;
|
|
1033
|
-
if (e.axis === "time") this.openTimezoneDrawer();
|
|
1034
|
-
else this.openPriceScaleDrawer(e.y);
|
|
1035
|
-
});
|
|
1036
|
-
this.refreshNativeCatalog();
|
|
1037
|
-
chart.on("indicator:added", () => {
|
|
1038
|
-
this.syncPresentNatives();
|
|
1039
|
-
this.refreshNativeCatalog();
|
|
1040
|
-
this.markStateDirty();
|
|
1041
|
-
});
|
|
1042
|
-
chart.on("indicator:removed", ({ id }) => {
|
|
1043
|
-
const idx = this.instances.findIndex((it) => it.handle?.id === id);
|
|
1044
|
-
if (idx >= 0) {
|
|
1045
|
-
const snapshot = this.instances[idx];
|
|
1046
|
-
this.instances.splice(idx, 1);
|
|
1047
|
-
this.syncIndicatorCount();
|
|
1048
|
-
this.history.push({
|
|
1049
|
-
undo: () => {
|
|
1050
|
-
snapshot.handle = this.inner ? this.addToChart(this.inner, snapshot.entry) : null;
|
|
1051
|
-
this.instances.push(snapshot);
|
|
1052
|
-
this.syncIndicatorCount();
|
|
1053
|
-
},
|
|
1054
|
-
redo: () => this.dropInstance(snapshot)
|
|
1055
|
-
});
|
|
1056
|
-
} else {
|
|
1057
|
-
const now = this.inner?.presentNativeIndicators() ?? [];
|
|
1058
|
-
for (const type of this.presentNatives.filter((t) => !now.includes(t))) {
|
|
1059
|
-
this.history.push({
|
|
1060
|
-
undo: () => {
|
|
1061
|
-
this.inner?.addNativeIndicator(type);
|
|
1062
|
-
this.refreshNativeCatalog();
|
|
1063
|
-
},
|
|
1064
|
-
redo: () => {
|
|
1065
|
-
this.inner?.addNativeIndicator(type).remove();
|
|
1066
|
-
this.refreshNativeCatalog();
|
|
1067
|
-
}
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
this.syncPresentNatives();
|
|
1072
|
-
this.refreshNativeCatalog();
|
|
1073
|
-
this.markStateDirty();
|
|
1074
|
-
});
|
|
1075
|
-
if (this.pendingIndicators) {
|
|
1076
|
-
for (const type of this.pendingIndicators.natives) chart.addNativeIndicator(type);
|
|
1077
|
-
if (this.manifestSettled) this.pendingIndicators = null;
|
|
1078
|
-
}
|
|
1079
|
-
this.syncPresentNatives();
|
|
1080
|
-
chart.on("load:end", () => {
|
|
1081
|
-
this.volumeMayBePending = false;
|
|
1082
|
-
});
|
|
1083
|
-
chart.on("load:start", () => this.watermark?.setLoading(true));
|
|
1084
|
-
chart.on("load:end", () => this.watermark?.setLoading(false));
|
|
1085
|
-
chart.on("theme:changed", (t) => {
|
|
1086
|
-
this.opts.theme = t;
|
|
1087
|
-
ensureUIHost(this.root, t);
|
|
1088
|
-
this.syncPlotOverlayTokens();
|
|
1089
|
-
});
|
|
1090
|
-
chart.on("market:changed", ({ symbol, timeframe }) => {
|
|
1091
|
-
this.refreshNativeCatalog();
|
|
1092
|
-
if (this.statusline) this.statusline.onChart(chart);
|
|
1093
|
-
if (symbol !== this.symbol) {
|
|
1094
|
-
this.symbol = symbol;
|
|
1095
|
-
this.topbar.setSymbol(symbol);
|
|
1096
|
-
this.mobileBar?.setSymbol(symbol);
|
|
1097
|
-
this.statusline?.setSymbol(symbol);
|
|
1098
|
-
this.objectTree.setSymbol(symbol);
|
|
1099
|
-
this.watermark?.update(symbol, this.timeframe);
|
|
1100
|
-
this.refreshSessionToggle();
|
|
1101
|
-
this.marketStatus?.track(chart.data, symbol);
|
|
1102
|
-
}
|
|
1103
|
-
if (timeframe !== this.timeframe) {
|
|
1104
|
-
this.syncTimeframeChrome(timeframe);
|
|
1105
|
-
this.refreshSessionShading();
|
|
1106
|
-
}
|
|
1107
|
-
});
|
|
1108
|
-
this.history.onChart(chart);
|
|
1109
|
-
chart.on("alert", (alert) => {
|
|
1110
|
-
this.alerts.unshift({ title: alert.title ?? "Alert", message: alert.message, time: alert.time });
|
|
1111
|
-
if (this.alerts.length > 20) this.alerts.pop();
|
|
1112
|
-
this.toast?.show(`${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
|
|
1113
|
-
this.topbar.setAlertCount(this.alerts.length);
|
|
1114
|
-
});
|
|
1115
|
-
chart.on("indicator:error", ({ error }) => this.toast?.show(error.message, "error", 5e3));
|
|
1116
|
-
chart.on("data:unresolved", ({ symbol, providers: providers2 }) => {
|
|
1117
|
-
if (this.unresolvedToasted === symbol) return;
|
|
1118
|
-
this.unresolvedToasted = symbol;
|
|
1119
|
-
const list = providers2.length > 0 ? providers2.join(", ") : "none";
|
|
1120
|
-
this.toast?.show(`No registered provider serves "${symbol}" (registered: ${list})`, "error", 6e3);
|
|
1121
|
-
});
|
|
1122
|
-
if (this.favs.length > 0) chart.drawings.setFavorites(this.favs);
|
|
1123
|
-
chart.on("drawing:favorites", ({ favorites }) => {
|
|
1124
|
-
this.favs = favorites;
|
|
1125
|
-
this.markStateDirty();
|
|
1126
|
-
});
|
|
1127
|
-
if (chart.drawings.supported) chart.drawings.setToolShortcuts(toolShortcutHints(this.keymap));
|
|
1128
|
-
chart.on("drawing:created", () => this.markStateDirty());
|
|
1129
|
-
chart.on("drawing:edited", () => this.markStateDirty());
|
|
1130
|
-
chart.on("drawing:removed", () => this.markStateDirty());
|
|
1131
|
-
chart.renderer.onCrosshairMove((e) => {
|
|
1132
|
-
this.lastCrossPrice = e.price;
|
|
1133
|
-
this.lastCrossTime = e.time;
|
|
1134
|
-
});
|
|
1135
|
-
this.topbar.renderActions();
|
|
1136
|
-
this.mobileBar?.renderActions();
|
|
1137
|
-
if (this.savedConfig != null) chart.renderer.applyConfig(this.savedConfig);
|
|
1138
|
-
if (this.savedDrawings != null) chart.drawings.fromJSON(this.savedDrawings);
|
|
1139
|
-
if (this.priceStyle !== "candles") chart.renderer.set("priceStyle", this.priceStyle);
|
|
1140
|
-
if (this.timezone !== "Etc/UTC") chart.renderer.set("timezone", this.timezone);
|
|
1141
|
-
if (!this.indicatorTitlesOn) chart.renderer.set("indicatorTitles", false);
|
|
1142
|
-
if (!this.indicatorValuesOn) chart.renderer.set("indicatorValues", false);
|
|
1143
|
-
chart.renderer.onConfigChanged(() => {
|
|
1144
|
-
const zone = chart.renderer.get("timezone");
|
|
1145
|
-
if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.timezone)) {
|
|
1146
|
-
this.setTimezone(normalizeTimezone(zone));
|
|
1147
|
-
}
|
|
1148
|
-
this.syncStatuslineColors();
|
|
1149
|
-
this.syncPlotOverlayTokens();
|
|
1150
|
-
});
|
|
1151
|
-
this.syncPlotOverlayTokens();
|
|
1152
|
-
this.statusline?.onChart(chart);
|
|
1153
|
-
this.drawingPill.onChart(chart);
|
|
1154
|
-
this.syncStatuslineColors();
|
|
1155
|
-
this.pushSettingsSections();
|
|
1156
|
-
void chart.ready().then(() => {
|
|
1157
|
-
if (this.buildSeq !== seq || this.destroyed) return;
|
|
1158
|
-
if (this.pendingRange) {
|
|
1159
|
-
chart.setVisibleRangePreset(this.pendingRange.preset);
|
|
1160
|
-
this.pendingRange = null;
|
|
1161
|
-
}
|
|
1162
|
-
this.statusline?.setMeta(this.timeframe, this.providerLabel());
|
|
1163
|
-
});
|
|
1164
|
-
if (this.manifest.length || this.instances.length) {
|
|
1165
|
-
for (const it of this.instances) it.handle = this.addToChart(chart, it.entry);
|
|
1166
|
-
this.syncIndicatorCount();
|
|
1167
|
-
} else if (indicators !== void 0) {
|
|
1168
|
-
this.indicatorsPromise ?? (this.indicatorsPromise = resolveIndicators(indicators).then((list) => {
|
|
1169
|
-
this.manifest = list;
|
|
1170
|
-
const pending = this.pendingIndicators;
|
|
1171
|
-
this.instances = pending ? pending.manifest.map((name) => list.find((e) => e.name === name)).filter((e) => e != null).map((entry) => ({ entry, handle: null })) : list.filter((e) => e.enabled).map((entry) => ({ entry, handle: null }));
|
|
1172
|
-
if (pending) this.pendingIndicators = null;
|
|
1173
|
-
this.manifestSettled = true;
|
|
1174
|
-
return list;
|
|
1175
|
-
}));
|
|
1176
|
-
void this.indicatorsPromise.then(() => {
|
|
1177
|
-
if (this.buildSeq !== seq || this.destroyed) return;
|
|
1178
|
-
for (const it of this.instances) {
|
|
1179
|
-
it.handle = this.addToChart(chart, it.entry);
|
|
1180
|
-
}
|
|
1181
|
-
this.syncIndicatorCount();
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
78
|
+
this.ws.active.applyRange(preset);
|
|
1184
79
|
}
|
|
1185
|
-
/**
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
if (!chart) return;
|
|
1189
|
-
void chart.availableNativeIndicators().then((list) => {
|
|
1190
|
-
if (this.inner !== chart || this.destroyed) return;
|
|
1191
|
-
this.nativeCatalog = list.map((n) => ({ type: n.type, title: n.title, supported: n.supported, present: n.present, beta: n.beta }));
|
|
1192
|
-
this.syncIndicatorCount();
|
|
1193
|
-
this.indicatorPicker?.sync();
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
/** Add a native indicator (single-instance per type — the core dedupes). */
|
|
1197
|
-
addNative(type) {
|
|
1198
|
-
this.inner?.addNativeIndicator(type);
|
|
1199
|
-
this.syncPresentNatives();
|
|
1200
|
-
this.refreshNativeCatalog();
|
|
1201
|
-
this.history.push({
|
|
1202
|
-
undo: () => {
|
|
1203
|
-
this.inner?.addNativeIndicator(type).remove();
|
|
1204
|
-
this.refreshNativeCatalog();
|
|
1205
|
-
},
|
|
1206
|
-
redo: () => {
|
|
1207
|
-
this.inner?.addNativeIndicator(type);
|
|
1208
|
-
this.refreshNativeCatalog();
|
|
1209
|
-
}
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
removeNative(type) {
|
|
1213
|
-
this.inner?.addNativeIndicator(type).remove();
|
|
1214
|
-
this.refreshNativeCatalog();
|
|
1215
|
-
}
|
|
1216
|
-
/** Sync mirror of the chart's present native types — the removal handler diffs
|
|
1217
|
-
* against it to identify (and record) whichever type was just removed. */
|
|
1218
|
-
syncPresentNatives() {
|
|
1219
|
-
this.presentNatives = this.inner?.presentNativeIndicators() ?? [];
|
|
1220
|
-
}
|
|
1221
|
-
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
1222
|
-
addInstance(libraryIndex) {
|
|
1223
|
-
const entry = this.manifest[libraryIndex];
|
|
1224
|
-
if (!entry || this.destroyed) return;
|
|
1225
|
-
const it = { entry, handle: this.inner ? this.addToChart(this.inner, entry) : null };
|
|
1226
|
-
this.instances.push(it);
|
|
1227
|
-
this.syncIndicatorCount();
|
|
1228
|
-
const snapshot = it;
|
|
1229
|
-
this.history.push({
|
|
1230
|
-
undo: () => this.dropInstance(snapshot),
|
|
1231
|
-
redo: () => {
|
|
1232
|
-
snapshot.handle = this.inner ? this.addToChart(this.inner, snapshot.entry) : null;
|
|
1233
|
-
this.instances.push(snapshot);
|
|
1234
|
-
this.syncIndicatorCount();
|
|
1235
|
-
}
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
/** Remove one live instance (picker trash). */
|
|
1239
|
-
removeInstance(instanceIndex) {
|
|
1240
|
-
const it = this.instances[instanceIndex];
|
|
1241
|
-
if (!it || this.destroyed) return;
|
|
1242
|
-
this.dropInstance(it);
|
|
1243
|
-
const snapshot = it;
|
|
1244
|
-
this.history.push({
|
|
1245
|
-
undo: () => {
|
|
1246
|
-
snapshot.handle = this.inner ? this.addToChart(this.inner, snapshot.entry) : null;
|
|
1247
|
-
this.instances.push(snapshot);
|
|
1248
|
-
this.syncIndicatorCount();
|
|
1249
|
-
},
|
|
1250
|
-
redo: () => this.dropInstance(snapshot)
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
|
-
dropInstance(it) {
|
|
1254
|
-
const idx = this.instances.indexOf(it);
|
|
1255
|
-
if (idx >= 0) this.instances.splice(idx, 1);
|
|
1256
|
-
try {
|
|
1257
|
-
it.handle?.remove();
|
|
1258
|
-
} catch {
|
|
1259
|
-
}
|
|
1260
|
-
it.handle = null;
|
|
1261
|
-
this.syncIndicatorCount();
|
|
1262
|
-
}
|
|
1263
|
-
syncIndicatorCount() {
|
|
1264
|
-
this.topbar.setIndicatorCount(this.instances.length + this.nativeCatalog.filter((n) => n.present).length);
|
|
1265
|
-
}
|
|
1266
|
-
addToChart(chart, ind) {
|
|
1267
|
-
try {
|
|
1268
|
-
return chart.addIndicator(ind.script, ind.language !== void 0 ? { language: ind.language } : void 0);
|
|
1269
|
-
} catch (err) {
|
|
1270
|
-
console.warn(`[vela] indicator "${ind.name}" failed to add:`, err);
|
|
1271
|
-
return null;
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
/** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
|
|
1275
|
-
routeTyping(ev) {
|
|
1276
|
-
if (this.destroyed || this.openDialogs > 0) return;
|
|
1277
|
-
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
1278
|
-
if (isEditableTarget(ev)) return;
|
|
1279
|
-
const key = ev.key;
|
|
1280
|
-
if (/^[a-zA-Z]$/.test(key)) {
|
|
1281
|
-
ev.preventDefault();
|
|
1282
|
-
this.symbolPicker.open(key.toUpperCase());
|
|
1283
|
-
} else if (/^[0-9]$/.test(key)) {
|
|
1284
|
-
ev.preventDefault();
|
|
1285
|
-
this.tfQuick.open(key);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
trackDialog(open) {
|
|
1289
|
-
this.openDialogs = Math.max(0, this.openDialogs + (open ? 1 : -1));
|
|
1290
|
-
if (open) this.keymap.pushScope("dialog");
|
|
1291
|
-
else this.keymap.popScope("dialog");
|
|
80
|
+
/** The unified state document (`layout: '1'`, one cell). */
|
|
81
|
+
getState() {
|
|
82
|
+
return this.ws.getState();
|
|
1292
83
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
const a = this.root.ownerDocument.createElement("a");
|
|
1297
|
-
a.href = url;
|
|
1298
|
-
a.download = `${this.symbol || "chart"}-${this.timeframe}.png`;
|
|
1299
|
-
a.click();
|
|
84
|
+
/** Restore a state document IN PLACE — the chart instance survives. */
|
|
85
|
+
applyState(state) {
|
|
86
|
+
this.ws.applyState(state);
|
|
1300
87
|
}
|
|
1301
|
-
/**
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
if (this.stateTimer != null) clearTimeout(this.stateTimer);
|
|
1306
|
-
this.stateTimer = setTimeout(() => {
|
|
1307
|
-
this.stateTimer = null;
|
|
1308
|
-
for (const listener of [...this.stateListeners]) listener();
|
|
1309
|
-
this.persistNow();
|
|
1310
|
-
}, 500);
|
|
88
|
+
/** Subscribe to `state:changed` (the only widget event — unknown names no-op). */
|
|
89
|
+
on(event, handler) {
|
|
90
|
+
if (event !== "state:changed") return () => void 0;
|
|
91
|
+
return this.ws.on("state:changed", handler);
|
|
1311
92
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
* key in the unified format and drops the old `:config`/`:drawings` sub-keys. */
|
|
1315
|
-
persistNow() {
|
|
1316
|
-
if (this.stateTimer != null) {
|
|
1317
|
-
clearTimeout(this.stateTimer);
|
|
1318
|
-
this.stateTimer = null;
|
|
1319
|
-
}
|
|
1320
|
-
const state = this.getState();
|
|
1321
|
-
if (this.storageKey !== null) {
|
|
1322
|
-
try {
|
|
1323
|
-
void this.storage.set(this.storageKey, encodeState(state));
|
|
1324
|
-
} catch {
|
|
1325
|
-
}
|
|
1326
|
-
if (this.legacyKeys) {
|
|
1327
|
-
this.legacyKeys = false;
|
|
1328
|
-
try {
|
|
1329
|
-
void this.storage.remove?.(`${this.storageKey}:config`);
|
|
1330
|
-
void this.storage.remove?.(`${this.storageKey}:drawings`);
|
|
1331
|
-
} catch {
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
if (this.opts.urlState) {
|
|
1336
|
-
writeUrlState({
|
|
1337
|
-
symbol: this.symbol,
|
|
1338
|
-
timeframe: this.timeframe,
|
|
1339
|
-
priceStyle: this.priceStyle,
|
|
1340
|
-
timezone: this.timezone,
|
|
1341
|
-
bars: String(this.bars),
|
|
1342
|
-
// Empty clears the param — the default (regular) keeps links lean.
|
|
1343
|
-
session: this.session === "extended" ? "extended" : "",
|
|
1344
|
-
watermark: this.watermarkOn ? "1" : "0",
|
|
1345
|
-
favorites: this.favs.join(",")
|
|
1346
|
-
});
|
|
1347
|
-
}
|
|
93
|
+
destroy() {
|
|
94
|
+
this.ws.destroy();
|
|
1348
95
|
}
|
|
1349
96
|
};
|
|
1350
97
|
|
|
1351
|
-
export { VelaWidget
|
|
98
|
+
export { VelaWidget };
|