@baron1996/klinecharts-runtime 0.9.13 → 0.9.14

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.
@@ -0,0 +1,684 @@
1
+ import { SUPPORTED_OVERLAYS } from '@baron1996/klinecharts-adapter';
2
+ import { MAIN_PANE_INDICATOR_PRESETS } from '../indicator-presentation.js';
3
+ import { registerRuntimeTeardown } from '../lifecycle.js';
4
+ import { CHART_WORKSPACE_TOOLBAR_STYLES } from './chart-workspace-toolbar-styles.js';
5
+ import { createToolbarIcon } from './toolbar-icons.js';
6
+ import { OVERLAY_TOOL_PRESENTATIONS, TEXT_OVERLAY_TYPES, TOOLBAR_GROUPS, } from './toolbar-tools.js';
7
+ let nextWorkspaceToolbarId = 1;
8
+ function runtimeStateCapability(runtime) {
9
+ const candidate = runtime;
10
+ return typeof candidate.getRuntimeState === 'function' &&
11
+ typeof candidate.subscribeRuntimeState === 'function'
12
+ ? candidate
13
+ : undefined;
14
+ }
15
+ function createButton(options) {
16
+ const button = document.createElement('button');
17
+ button.type = 'button';
18
+ button.className = `baron-chart-workspace-toolbar__button${options.className === undefined ? '' : ` ${options.className}`}`;
19
+ button.setAttribute('aria-label', options.label);
20
+ if (options.icon !== undefined) {
21
+ button.append(createToolbarIcon(options.icon));
22
+ }
23
+ if (options.text !== undefined) {
24
+ button.append(document.createTextNode(options.text));
25
+ }
26
+ return button;
27
+ }
28
+ function createSection(label, end = false) {
29
+ const section = document.createElement('div');
30
+ section.className = `baron-chart-workspace-toolbar__section${end ? ' baron-chart-workspace-toolbar__section--end' : ''}`;
31
+ section.setAttribute('role', 'group');
32
+ section.setAttribute('aria-label', label);
33
+ return section;
34
+ }
35
+ function createPopover(button, id, cleanupCallbacks) {
36
+ const element = document.createElement('div');
37
+ element.id = id;
38
+ element.className = 'baron-chart-workspace-popover';
39
+ element.hidden = true;
40
+ button.setAttribute('aria-controls', id);
41
+ button.setAttribute('aria-expanded', 'false');
42
+ document.body.append(element);
43
+ const position = () => {
44
+ const anchor = button.getBoundingClientRect();
45
+ const bounds = element.getBoundingClientRect();
46
+ const left = Math.min(Math.max(8, anchor.left), Math.max(8, window.innerWidth - bounds.width - 8));
47
+ element.style.left = `${Math.round(left)}px`;
48
+ element.style.top = `${Math.round(anchor.bottom + 7)}px`;
49
+ };
50
+ const close = () => {
51
+ button.setAttribute('aria-expanded', 'false');
52
+ element.classList.remove('baron-chart-workspace-popover--open');
53
+ element.hidden = true;
54
+ };
55
+ const open = () => {
56
+ element.hidden = false;
57
+ position();
58
+ button.setAttribute('aria-expanded', 'true');
59
+ requestAnimationFrame(() => {
60
+ if (!element.hidden) {
61
+ element.classList.add('baron-chart-workspace-popover--open');
62
+ }
63
+ });
64
+ };
65
+ const toggle = () => {
66
+ if (element.hidden) {
67
+ open();
68
+ }
69
+ else {
70
+ close();
71
+ }
72
+ };
73
+ const handleButtonClick = () => toggle();
74
+ const handleOutsidePointer = (event) => {
75
+ const target = event.target;
76
+ if (target instanceof Node &&
77
+ !element.contains(target) &&
78
+ !button.contains(target)) {
79
+ close();
80
+ }
81
+ };
82
+ const handleKeyDown = (event) => {
83
+ if (event.key === 'Escape' && !element.hidden) {
84
+ close();
85
+ button.focus();
86
+ }
87
+ };
88
+ const handleViewportChange = () => close();
89
+ button.addEventListener('click', handleButtonClick);
90
+ document.addEventListener('pointerdown', handleOutsidePointer, true);
91
+ document.addEventListener('keydown', handleKeyDown);
92
+ window.addEventListener('resize', handleViewportChange);
93
+ window.addEventListener('scroll', handleViewportChange, true);
94
+ cleanupCallbacks.push(() => {
95
+ button.removeEventListener('click', handleButtonClick);
96
+ document.removeEventListener('pointerdown', handleOutsidePointer, true);
97
+ document.removeEventListener('keydown', handleKeyDown);
98
+ window.removeEventListener('resize', handleViewportChange);
99
+ window.removeEventListener('scroll', handleViewportChange, true);
100
+ });
101
+ return {
102
+ element,
103
+ toggle,
104
+ close,
105
+ destroy() {
106
+ close();
107
+ element.remove();
108
+ },
109
+ };
110
+ }
111
+ function createTooltip(cleanupCallbacks) {
112
+ const tooltip = document.createElement('div');
113
+ tooltip.className = 'baron-chart-workspace-tooltip';
114
+ tooltip.setAttribute('role', 'tooltip');
115
+ tooltip.hidden = true;
116
+ document.body.append(tooltip);
117
+ const hide = () => {
118
+ tooltip.hidden = true;
119
+ };
120
+ const bind = (button, label) => {
121
+ const show = () => {
122
+ const bounds = button.getBoundingClientRect();
123
+ tooltip.textContent = label;
124
+ tooltip.hidden = false;
125
+ const tooltipBounds = tooltip.getBoundingClientRect();
126
+ const left = Math.min(bounds.right + 8, window.innerWidth - tooltipBounds.width - 8);
127
+ const top = Math.min(Math.max(8, bounds.top + (bounds.height - tooltipBounds.height) / 2), window.innerHeight - tooltipBounds.height - 8);
128
+ tooltip.style.left = `${Math.round(left)}px`;
129
+ tooltip.style.top = `${Math.round(top)}px`;
130
+ };
131
+ button.addEventListener('mouseenter', show);
132
+ button.addEventListener('mouseleave', hide);
133
+ button.addEventListener('focus', show);
134
+ button.addEventListener('blur', hide);
135
+ cleanupCallbacks.push(() => {
136
+ button.removeEventListener('mouseenter', show);
137
+ button.removeEventListener('mouseleave', hide);
138
+ button.removeEventListener('focus', show);
139
+ button.removeEventListener('blur', hide);
140
+ });
141
+ };
142
+ return { bind, hide, destroy: () => tooltip.remove() };
143
+ }
144
+ function applyHostActionState(control, state) {
145
+ if (state.pressed !== undefined) {
146
+ control.button.setAttribute('aria-pressed', String(state.pressed));
147
+ }
148
+ if (state.disabled !== undefined) {
149
+ control.button.disabled = state.disabled;
150
+ }
151
+ if (state.pending !== undefined) {
152
+ control.button.setAttribute('aria-busy', String(state.pending));
153
+ }
154
+ if ('errorMessage' in state) {
155
+ const message = state.errorMessage?.trim() ?? '';
156
+ control.error.textContent = message;
157
+ control.error.hidden = message.length === 0;
158
+ if (message.length === 0) {
159
+ control.button.removeAttribute('aria-errormessage');
160
+ }
161
+ else {
162
+ control.button.setAttribute('aria-errormessage', control.error.id);
163
+ }
164
+ }
165
+ }
166
+ function defaultTimezoneChoices(runtime) {
167
+ const chartTimezone = runtime.getDisplayTimezone();
168
+ const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
169
+ return [
170
+ {
171
+ value: 'chart',
172
+ label: `图表 · ${chartTimezone}`,
173
+ timezone: chartTimezone,
174
+ },
175
+ {
176
+ value: 'local',
177
+ label: `本机 · ${localTimezone}`,
178
+ timezone: localTimezone,
179
+ },
180
+ { value: 'utc', label: 'UTC', timezone: 'UTC' },
181
+ ];
182
+ }
183
+ /**
184
+ * 创建 Pro 风格的复合工具栏。Baron 只发出周期/复权宿主意图,
185
+ * 指标、展示时区、价格轴和主序列在浏览器 Runtime 内即时生效。
186
+ */
187
+ export function createChartWorkspaceToolbar(containers, runtime, options = {}) {
188
+ if (containers.top === containers.left) {
189
+ throw new TypeError('CHART_WORKSPACE_TOOLBAR_CONTAINERS_MUST_DIFFER');
190
+ }
191
+ const descriptor = runtime.getRuntimeCapabilityDescriptor({
192
+ hostActions: [
193
+ ...(options.periodActions ?? []),
194
+ ...(options.settingsHostActions ?? []),
195
+ ],
196
+ });
197
+ const toolbarId = nextWorkspaceToolbarId++;
198
+ const cleanupCallbacks = [];
199
+ const dataControls = [];
200
+ const drawingControls = [];
201
+ const hostActionControls = new Map();
202
+ const openPopovers = [];
203
+ const style = document.createElement('style');
204
+ style.dataset.baronChartWorkspaceToolbarStyles = '';
205
+ style.textContent = CHART_WORKSPACE_TOOLBAR_STYLES;
206
+ const top = document.createElement('div');
207
+ top.className =
208
+ 'baron-chart-workspace-toolbar baron-chart-workspace-toolbar--top';
209
+ top.setAttribute('role', 'toolbar');
210
+ top.setAttribute('aria-label', 'K 线图表工具');
211
+ top.append(style);
212
+ const left = document.createElement('div');
213
+ left.className =
214
+ 'baron-chart-workspace-toolbar baron-chart-workspace-toolbar--left';
215
+ left.setAttribute('role', 'toolbar');
216
+ left.setAttribute('aria-label', 'K 线画图工具');
217
+ const tooltip = createTooltip(cleanupCallbacks);
218
+ const hostActions = [
219
+ ...(options.periodActions ?? []),
220
+ ...(options.settingsHostActions ?? []),
221
+ ];
222
+ for (const action of hostActions) {
223
+ if (hostActionControls.has(action.actionId)) {
224
+ throw new TypeError(`CHART_WORKSPACE_TOOLBAR_DUPLICATE_HOST_ACTION: ${action.actionId}`);
225
+ }
226
+ const button = createButton({ label: action.label, text: action.label });
227
+ button.dataset.hostAction = action.actionId;
228
+ button.setAttribute('aria-pressed', String(action.pressed ?? false));
229
+ button.setAttribute('aria-busy', String(action.pending ?? false));
230
+ button.disabled = action.disabled ?? false;
231
+ const error = document.createElement('div');
232
+ error.className = 'baron-chart-workspace-toolbar__error';
233
+ error.id = `baron-workspace-host-error-${toolbarId}-${hostActionControls.size}`;
234
+ error.hidden = true;
235
+ error.setAttribute('role', 'alert');
236
+ const control = { button, error };
237
+ hostActionControls.set(action.actionId, control);
238
+ applyHostActionState(control, {
239
+ errorMessage: action.errorMessage ?? null,
240
+ });
241
+ const request = () => runtime.requestHostAction(action.actionId, runtime.getSelectedDrawingId() ?? null);
242
+ button.addEventListener('click', request);
243
+ cleanupCallbacks.push(() => button.removeEventListener('click', request));
244
+ }
245
+ const periodSection = createSection('周期');
246
+ for (const action of options.periodActions ?? []) {
247
+ const control = hostActionControls.get(action.actionId);
248
+ control.button.classList.add('baron-chart-workspace-toolbar__period');
249
+ periodSection.append(control.button, control.error);
250
+ }
251
+ top.append(periodSection);
252
+ const primarySection = createSection('图表能力');
253
+ const divider = document.createElement('span');
254
+ divider.className = 'baron-chart-workspace-toolbar__divider';
255
+ divider.setAttribute('aria-hidden', 'true');
256
+ primarySection.append(divider);
257
+ const indicatorButton = createButton({
258
+ label: '主图指标',
259
+ text: '指标',
260
+ icon: 'indicator',
261
+ });
262
+ indicatorButton.dataset.action = 'main-indicators';
263
+ primarySection.append(indicatorButton);
264
+ dataControls.push(indicatorButton);
265
+ const indicatorPopover = createPopover(indicatorButton, `baron-workspace-indicators-${toolbarId}`, cleanupCallbacks);
266
+ openPopovers.push(indicatorPopover);
267
+ const indicatorTitle = document.createElement('div');
268
+ indicatorTitle.className = 'baron-chart-workspace-popover__title';
269
+ indicatorTitle.textContent = '主图指标 · 浏览器实时计算';
270
+ const indicatorGrid = document.createElement('div');
271
+ indicatorGrid.className = 'baron-chart-workspace-popover__grid';
272
+ const indicatorButtons = new Map();
273
+ const refreshIndicators = () => {
274
+ const activeNames = new Set(runtime.listMainIndicators().map((indicator) => indicator.name));
275
+ for (const preset of MAIN_PANE_INDICATOR_PRESETS) {
276
+ indicatorButtons
277
+ .get(preset.name)
278
+ ?.setAttribute('aria-pressed', String(activeNames.has(preset.name)));
279
+ }
280
+ };
281
+ for (const preset of MAIN_PANE_INDICATOR_PRESETS) {
282
+ const button = createButton({
283
+ label: `${preset.label} 主图指标`,
284
+ text: preset.label,
285
+ });
286
+ button.dataset.indicatorName = preset.name;
287
+ button.setAttribute('aria-pressed', 'false');
288
+ const toggle = () => {
289
+ const current = runtime
290
+ .listMainIndicators()
291
+ .filter((indicator) => indicator.name === preset.name);
292
+ if (current.length > 0) {
293
+ for (const indicator of current) {
294
+ runtime.removeMainIndicator(indicator.id);
295
+ }
296
+ }
297
+ else {
298
+ runtime.addMainIndicator({
299
+ name: preset.name,
300
+ calcParams: preset.calcParams,
301
+ });
302
+ }
303
+ refreshIndicators();
304
+ };
305
+ button.addEventListener('click', toggle);
306
+ cleanupCallbacks.push(() => button.removeEventListener('click', toggle));
307
+ indicatorButtons.set(preset.name, button);
308
+ indicatorGrid.append(button);
309
+ }
310
+ indicatorPopover.element.append(indicatorTitle, indicatorGrid);
311
+ const timezoneChoices = options.displayTimezoneChoices ?? defaultTimezoneChoices(runtime);
312
+ const timezoneLabel = document.createElement('label');
313
+ timezoneLabel.className = 'baron-chart-workspace-toolbar__timezone';
314
+ timezoneLabel.append(createToolbarIcon('timezone'));
315
+ const timezoneSelect = document.createElement('select');
316
+ timezoneSelect.className = 'baron-chart-workspace-toolbar__select';
317
+ timezoneSelect.dataset.action = 'display-timezone';
318
+ timezoneSelect.setAttribute('aria-label', '显示时区');
319
+ for (const choice of timezoneChoices) {
320
+ const option = document.createElement('option');
321
+ option.value = choice.value;
322
+ option.textContent = choice.label;
323
+ timezoneSelect.append(option);
324
+ }
325
+ const initialTimezoneValue = options.activeDisplayTimezoneValue ??
326
+ timezoneChoices.find((choice) => choice.timezone === runtime.getDisplayTimezone())?.value;
327
+ if (initialTimezoneValue !== undefined) {
328
+ timezoneSelect.value = initialTimezoneValue;
329
+ }
330
+ let committedTimezoneValue = timezoneSelect.value;
331
+ const changeTimezone = () => {
332
+ const choice = timezoneChoices.find((candidate) => candidate.value === timezoneSelect.value);
333
+ if (choice === undefined) {
334
+ return;
335
+ }
336
+ try {
337
+ runtime.setDisplayTimezone(choice.timezone);
338
+ committedTimezoneValue = choice.value;
339
+ options.onDisplayTimezoneChange?.(choice);
340
+ }
341
+ catch (error) {
342
+ timezoneSelect.value = committedTimezoneValue;
343
+ throw error;
344
+ }
345
+ };
346
+ timezoneSelect.addEventListener('change', changeTimezone);
347
+ cleanupCallbacks.push(() => timezoneSelect.removeEventListener('change', changeTimezone));
348
+ timezoneLabel.append(timezoneSelect);
349
+ primarySection.append(timezoneLabel);
350
+ top.append(primarySection);
351
+ const endSection = createSection('设置与全屏', true);
352
+ const settingsButton = createButton({ label: '图表设置', icon: 'settings' });
353
+ settingsButton.dataset.action = 'settings';
354
+ endSection.append(settingsButton);
355
+ const settingsPopover = createPopover(settingsButton, `baron-workspace-settings-${toolbarId}`, cleanupCallbacks);
356
+ openPopovers.push(settingsPopover);
357
+ if ((options.settingsHostActions?.length ?? 0) > 0) {
358
+ const group = document.createElement('div');
359
+ group.className = 'baron-chart-workspace-popover__group';
360
+ const title = document.createElement('div');
361
+ title.className = 'baron-chart-workspace-popover__title';
362
+ title.textContent = '宿主设置';
363
+ const grid = document.createElement('div');
364
+ grid.className = 'baron-chart-workspace-popover__grid';
365
+ for (const action of options.settingsHostActions ?? []) {
366
+ const control = hostActionControls.get(action.actionId);
367
+ grid.append(control.button);
368
+ group.append(control.error);
369
+ }
370
+ group.prepend(title, grid);
371
+ settingsPopover.element.append(group);
372
+ }
373
+ const chartSettingsGroup = document.createElement('div');
374
+ chartSettingsGroup.className = 'baron-chart-workspace-popover__group';
375
+ const chartSettingsTitle = document.createElement('div');
376
+ chartSettingsTitle.className = 'baron-chart-workspace-popover__title';
377
+ chartSettingsTitle.textContent = '图表显示';
378
+ chartSettingsGroup.append(chartSettingsTitle);
379
+ if (descriptor.valueAxis.mutable) {
380
+ const row = document.createElement('label');
381
+ row.className = 'baron-chart-workspace-popover__row';
382
+ const label = document.createElement('span');
383
+ label.className = 'baron-chart-workspace-popover__label';
384
+ label.textContent = '价格轴';
385
+ const select = document.createElement('select');
386
+ select.className = 'baron-chart-workspace-toolbar__select';
387
+ select.dataset.action = 'price-scale';
388
+ for (const scale of descriptor.valueAxis.supportedScales) {
389
+ const option = document.createElement('option');
390
+ option.value = scale;
391
+ option.textContent = scale === 'linear' ? '线性' : '对数';
392
+ select.append(option);
393
+ }
394
+ select.value = descriptor.valueAxis.activeScale;
395
+ let committedScale = descriptor.valueAxis.activeScale;
396
+ const changeScale = async () => {
397
+ try {
398
+ await runtime.setValueAxisScale(select.value);
399
+ committedScale = select.value;
400
+ }
401
+ catch (error) {
402
+ select.value = committedScale;
403
+ throw error;
404
+ }
405
+ };
406
+ select.addEventListener('change', changeScale);
407
+ cleanupCallbacks.push(() => select.removeEventListener('change', changeScale));
408
+ dataControls.push(select);
409
+ row.append(label, select);
410
+ chartSettingsGroup.append(row);
411
+ }
412
+ if (descriptor.mainSeriesPresentation !== null) {
413
+ const mainSeries = descriptor.mainSeriesPresentation;
414
+ const row = document.createElement('label');
415
+ row.className = 'baron-chart-workspace-popover__row';
416
+ const label = document.createElement('span');
417
+ label.className = 'baron-chart-workspace-popover__label';
418
+ label.textContent = '主序列';
419
+ const select = document.createElement('select');
420
+ select.className = 'baron-chart-workspace-toolbar__select';
421
+ select.dataset.action = 'main-series';
422
+ for (const presentation of mainSeries.presentations) {
423
+ const option = document.createElement('option');
424
+ option.value = presentation.type;
425
+ option.textContent = presentationLabel(presentation.type);
426
+ select.append(option);
427
+ }
428
+ select.value = mainSeries.activeType;
429
+ const changeMainSeries = () => {
430
+ const presentation = mainSeries.presentations.find((candidate) => candidate.type === select.value);
431
+ if (presentation !== undefined) {
432
+ select.value =
433
+ runtime.setMainSeriesPresentation(presentation).activeType;
434
+ }
435
+ };
436
+ select.addEventListener('change', changeMainSeries);
437
+ cleanupCallbacks.push(() => select.removeEventListener('change', changeMainSeries));
438
+ dataControls.push(select);
439
+ row.append(label, select);
440
+ chartSettingsGroup.append(row);
441
+ }
442
+ settingsPopover.element.append(chartSettingsGroup);
443
+ if (options.fullscreenControl !== 'hidden') {
444
+ const fullscreenTarget = options.fullscreenTarget ?? containers.top.parentElement;
445
+ const fullscreenButton = createButton({
446
+ label: '进入全屏',
447
+ icon: 'fullscreen',
448
+ });
449
+ fullscreenButton.dataset.action = 'fullscreen';
450
+ fullscreenButton.disabled =
451
+ fullscreenTarget === null ||
452
+ typeof fullscreenTarget.requestFullscreen !== 'function';
453
+ const refreshFullscreen = () => {
454
+ const active = fullscreenTarget !== null &&
455
+ document.fullscreenElement === fullscreenTarget;
456
+ fullscreenButton.setAttribute('aria-pressed', String(active));
457
+ fullscreenButton.setAttribute('aria-label', active ? '退出全屏' : '进入全屏');
458
+ fullscreenButton.replaceChildren(createToolbarIcon(active ? 'fullscreenExit' : 'fullscreen'));
459
+ };
460
+ const toggleFullscreen = async () => {
461
+ if (document.fullscreenElement !== null) {
462
+ await document.exitFullscreen();
463
+ }
464
+ else {
465
+ await fullscreenTarget?.requestFullscreen();
466
+ }
467
+ };
468
+ fullscreenButton.addEventListener('click', toggleFullscreen);
469
+ document.addEventListener('fullscreenchange', refreshFullscreen);
470
+ cleanupCallbacks.push(() => fullscreenButton.removeEventListener('click', toggleFullscreen), () => document.removeEventListener('fullscreenchange', refreshFullscreen));
471
+ endSection.append(fullscreenButton);
472
+ }
473
+ top.append(endSection);
474
+ const drawableTypes = (descriptor.drawingTypes.length === 0
475
+ ? SUPPORTED_OVERLAYS
476
+ : descriptor.drawingTypes);
477
+ const overlayButtons = [];
478
+ for (const group of TOOLBAR_GROUPS) {
479
+ if (group.id === 'edit' || group.id === 'action') {
480
+ continue;
481
+ }
482
+ const section = createSection(group.label);
483
+ for (const overlayType of drawableTypes) {
484
+ const presentation = OVERLAY_TOOL_PRESENTATIONS[overlayType];
485
+ if (presentation.group !== group.id) {
486
+ continue;
487
+ }
488
+ const button = createButton({
489
+ label: presentation.label,
490
+ icon: presentation.icon,
491
+ });
492
+ button.dataset.overlayType = overlayType;
493
+ button.setAttribute('aria-pressed', 'false');
494
+ const startDrawing = (text) => {
495
+ runtime.startDrawing(overlayType, text === undefined ? {} : { text });
496
+ for (const candidate of overlayButtons) {
497
+ candidate.setAttribute('aria-pressed', String(candidate === button));
498
+ }
499
+ tooltip.hide();
500
+ };
501
+ if (TEXT_OVERLAY_TYPES.has(overlayType)) {
502
+ const textPopover = createPopover(button, `baron-workspace-text-${toolbarId}-${overlayType}`, cleanupCallbacks);
503
+ openPopovers.push(textPopover);
504
+ const form = document.createElement('form');
505
+ form.className = 'baron-chart-workspace-popover__text-form';
506
+ const input = document.createElement('input');
507
+ input.type = 'text';
508
+ input.placeholder = '输入标注文本';
509
+ input.setAttribute('aria-label', `${presentation.label}文本`);
510
+ const confirm = createButton({
511
+ label: `开始绘制${presentation.label}`,
512
+ text: '开始绘制',
513
+ });
514
+ confirm.type = 'submit';
515
+ const submit = (event) => {
516
+ event.preventDefault();
517
+ startDrawing(input.value);
518
+ textPopover.close();
519
+ };
520
+ form.addEventListener('submit', submit);
521
+ cleanupCallbacks.push(() => form.removeEventListener('submit', submit));
522
+ const focusInput = () => {
523
+ requestAnimationFrame(() => {
524
+ if (!textPopover.element.hidden) {
525
+ input.focus();
526
+ }
527
+ });
528
+ };
529
+ button.addEventListener('click', focusInput);
530
+ cleanupCallbacks.push(() => button.removeEventListener('click', focusInput));
531
+ dataControls.push(input, confirm);
532
+ form.append(input, confirm);
533
+ textPopover.element.append(form);
534
+ }
535
+ else {
536
+ const start = () => startDrawing();
537
+ button.addEventListener('click', start);
538
+ cleanupCallbacks.push(() => button.removeEventListener('click', start));
539
+ }
540
+ tooltip.bind(button, presentation.label);
541
+ overlayButtons.push(button);
542
+ drawingControls.push(button);
543
+ dataControls.push(button);
544
+ section.append(button);
545
+ }
546
+ if (section.childElementCount > 0) {
547
+ left.append(section);
548
+ }
549
+ }
550
+ const drawingActions = createSection('标注操作');
551
+ const clearButton = createButton({ label: '清空全部标注', icon: 'clearAll' });
552
+ clearButton.dataset.action = 'clear-all';
553
+ const clearDrawings = () => {
554
+ for (const drawing of runtime.listDrawings()) {
555
+ if (!drawing.locked) {
556
+ runtime.removeDrawing(drawing.id);
557
+ }
558
+ }
559
+ runtime.selectDrawing(null);
560
+ };
561
+ clearButton.addEventListener('click', clearDrawings);
562
+ cleanupCallbacks.push(() => clearButton.removeEventListener('click', clearDrawings));
563
+ tooltip.bind(clearButton, '清空全部标注');
564
+ drawingControls.push(clearButton);
565
+ dataControls.push(clearButton);
566
+ drawingActions.append(clearButton);
567
+ left.append(drawingActions);
568
+ containers.top.append(top);
569
+ containers.left.append(left);
570
+ const stateCapability = runtimeStateCapability(runtime);
571
+ let runtimeReady = stateCapability === undefined;
572
+ let hostDataDisabled = false;
573
+ let hostDrawingDisabled = false;
574
+ const applyDisabledState = () => {
575
+ for (const control of dataControls) {
576
+ control.disabled = !runtimeReady || hostDataDisabled;
577
+ }
578
+ if (hostDrawingDisabled) {
579
+ for (const control of drawingControls) {
580
+ control.disabled = true;
581
+ }
582
+ }
583
+ };
584
+ let unsubscribeIndicatorChanges;
585
+ if (stateCapability !== undefined) {
586
+ const applyRuntimeState = (state) => {
587
+ runtimeReady = state === 'ready';
588
+ top.dataset.runtimeState = state;
589
+ left.dataset.runtimeState = state;
590
+ if (runtimeReady) {
591
+ refreshIndicators();
592
+ unsubscribeIndicatorChanges ??=
593
+ runtime.subscribeDrawingChanges(refreshIndicators);
594
+ }
595
+ applyDisabledState();
596
+ };
597
+ applyRuntimeState(stateCapability.getRuntimeState());
598
+ cleanupCallbacks.push(stateCapability.subscribeRuntimeState(applyRuntimeState));
599
+ }
600
+ else {
601
+ refreshIndicators();
602
+ unsubscribeIndicatorChanges =
603
+ runtime.subscribeDrawingChanges(refreshIndicators);
604
+ }
605
+ cleanupCallbacks.push(() => unsubscribeIndicatorChanges?.());
606
+ let destroyed = false;
607
+ let unregisterRuntime = () => { };
608
+ const toolbar = {
609
+ topElement: top,
610
+ leftElement: left,
611
+ setDataActionsDisabled(disabled) {
612
+ if (destroyed) {
613
+ throw new Error('CHART_WORKSPACE_TOOLBAR_DESTROYED');
614
+ }
615
+ hostDataDisabled = disabled;
616
+ applyDisabledState();
617
+ },
618
+ setDrawingActionsDisabled(disabled) {
619
+ if (destroyed) {
620
+ throw new Error('CHART_WORKSPACE_TOOLBAR_DESTROYED');
621
+ }
622
+ hostDrawingDisabled = disabled;
623
+ applyDisabledState();
624
+ },
625
+ setHostActionState(actionId, state) {
626
+ if (destroyed) {
627
+ throw new Error('CHART_WORKSPACE_TOOLBAR_DESTROYED');
628
+ }
629
+ const control = hostActionControls.get(actionId);
630
+ if (control === undefined) {
631
+ throw new TypeError(`CHART_WORKSPACE_TOOLBAR_UNKNOWN_HOST_ACTION: ${actionId}`);
632
+ }
633
+ applyHostActionState(control, state);
634
+ },
635
+ setDisplayTimezoneChoice(value) {
636
+ if (destroyed) {
637
+ throw new Error('CHART_WORKSPACE_TOOLBAR_DESTROYED');
638
+ }
639
+ const choice = timezoneChoices.find((candidate) => candidate.value === value);
640
+ if (choice === undefined) {
641
+ throw new TypeError(`CHART_WORKSPACE_TOOLBAR_UNKNOWN_TIMEZONE: ${value}`);
642
+ }
643
+ runtime.setDisplayTimezone(choice.timezone);
644
+ timezoneSelect.value = value;
645
+ committedTimezoneValue = value;
646
+ },
647
+ destroy() {
648
+ if (destroyed) {
649
+ return;
650
+ }
651
+ destroyed = true;
652
+ unregisterRuntime();
653
+ for (const cleanup of cleanupCallbacks) {
654
+ cleanup();
655
+ }
656
+ for (const popover of openPopovers) {
657
+ popover.destroy();
658
+ }
659
+ tooltip.destroy();
660
+ top.remove();
661
+ left.remove();
662
+ },
663
+ };
664
+ unregisterRuntime = registerRuntimeTeardown(runtime, () => toolbar.destroy());
665
+ return toolbar;
666
+ }
667
+ function presentationLabel(type) {
668
+ switch (type) {
669
+ case 'candle_solid':
670
+ return '实心蜡烛';
671
+ case 'candle_stroke':
672
+ return '描边蜡烛';
673
+ case 'candle_up_stroke':
674
+ return '上涨描边';
675
+ case 'candle_down_stroke':
676
+ return '下跌描边';
677
+ case 'ohlc':
678
+ return 'OHLC';
679
+ case 'area':
680
+ return '收盘价折线';
681
+ default:
682
+ return type;
683
+ }
684
+ }