@coderyo/ui-shell 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +464 -6
- package/dist/index.js +3048 -134
- package/dist/index.js.map +1 -1
- package/package.json +20 -17
package/dist/index.js
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
// src/top-bar.ts
|
|
2
2
|
import { DEFAULT_INTERVALS } from "@coderyo/data";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
// src/logo-slot.ts
|
|
5
|
+
function mountLogoSlot(parent, opts = {}) {
|
|
6
|
+
const slot = document.createElement("div");
|
|
7
|
+
slot.className = "tv-logo-slot";
|
|
8
|
+
slot.style.cssText = "display:flex;align-items:center;gap:8px;margin-right:12px;flex-shrink:0;font-weight:600;font-size:13px;color:var(--tv-fg,#e6edf3);";
|
|
9
|
+
const label = opts.label ?? "TradView";
|
|
10
|
+
const text = document.createElement("span");
|
|
11
|
+
text.textContent = label;
|
|
12
|
+
const children = [];
|
|
13
|
+
if (opts.imageSrc) {
|
|
14
|
+
const img = document.createElement("img");
|
|
15
|
+
img.src = opts.imageSrc;
|
|
16
|
+
img.alt = label;
|
|
17
|
+
img.style.cssText = "height:22px;width:auto;display:block;";
|
|
18
|
+
children.push(img);
|
|
19
|
+
}
|
|
20
|
+
children.push(text);
|
|
21
|
+
if (opts.href) {
|
|
22
|
+
const link = document.createElement("a");
|
|
23
|
+
link.href = opts.href;
|
|
24
|
+
link.style.cssText = "color:inherit;text-decoration:none;display:flex;align-items:center;gap:8px;";
|
|
25
|
+
link.append(...children);
|
|
26
|
+
slot.appendChild(link);
|
|
27
|
+
} else {
|
|
28
|
+
slot.append(...children);
|
|
29
|
+
}
|
|
30
|
+
parent.appendChild(slot);
|
|
31
|
+
return slot;
|
|
32
|
+
}
|
|
4
33
|
|
|
5
34
|
// src/user-preferences.ts
|
|
6
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
defaultChartStorage,
|
|
37
|
+
loadIndicatorConfig as loadIndicatorConfigFromCore,
|
|
38
|
+
saveIndicatorConfig as saveIndicatorConfigToCore
|
|
39
|
+
} from "@coderyo/core";
|
|
7
40
|
var GRID_SETTING_KEY = "tradview:settings:showGrid";
|
|
8
41
|
var RETURN_CURSOR_KEY = "tradview:settings:returnToCursorAfterDraw";
|
|
9
42
|
function loadShowGridPreference() {
|
|
@@ -33,30 +66,25 @@ function saveReturnToCursorPreference(v) {
|
|
|
33
66
|
}
|
|
34
67
|
}
|
|
35
68
|
function loadIndicatorConfig(symbol, interval) {
|
|
36
|
-
|
|
37
|
-
const raw = localStorage.getItem(indicatorConfigStorageKey(symbol, interval));
|
|
38
|
-
if (!raw) return { ...DEFAULT_INDICATOR_CONFIG };
|
|
39
|
-
return { ...DEFAULT_INDICATOR_CONFIG, ...JSON.parse(raw) };
|
|
40
|
-
} catch {
|
|
41
|
-
return { ...DEFAULT_INDICATOR_CONFIG };
|
|
42
|
-
}
|
|
69
|
+
return loadIndicatorConfigFromCore(defaultChartStorage, symbol, interval);
|
|
43
70
|
}
|
|
44
71
|
function saveIndicatorConfig(symbol, interval, config) {
|
|
45
|
-
|
|
46
|
-
localStorage.setItem(indicatorConfigStorageKey(symbol, interval), JSON.stringify(config));
|
|
47
|
-
} catch {
|
|
48
|
-
}
|
|
72
|
+
saveIndicatorConfigToCore(defaultChartStorage, symbol, interval, config);
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
// src/settings-panel.ts
|
|
52
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
DEFAULT_INDICATOR_CONFIG,
|
|
78
|
+
disableIndicatorLayer,
|
|
79
|
+
listActiveIndicatorLayers
|
|
80
|
+
} from "@coderyo/indicators";
|
|
53
81
|
import { t } from "@coderyo/i18n";
|
|
54
82
|
function mountSettingsPanel(parent, opts = {}) {
|
|
55
83
|
let open = false;
|
|
56
84
|
let tab = "chart";
|
|
57
85
|
let showGrid = opts.showGrid ?? loadShowGridPreference();
|
|
58
86
|
let returnToCursor = opts.returnToCursorAfterDraw ?? loadReturnToCursorPreference();
|
|
59
|
-
let indicatorConfig = { ...opts.indicatorConfig ??
|
|
87
|
+
let indicatorConfig = { ...opts.indicatorConfig ?? DEFAULT_INDICATOR_CONFIG };
|
|
60
88
|
const wrap = document.createElement("div");
|
|
61
89
|
wrap.style.cssText = "position:relative;";
|
|
62
90
|
const btn = document.createElement("button");
|
|
@@ -64,12 +92,27 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
64
92
|
btn.title = t("settings.title", "\u8A2D\u5B9A");
|
|
65
93
|
btn.textContent = "\u2699";
|
|
66
94
|
btn.style.cssText = "background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:14px;";
|
|
67
|
-
const
|
|
68
|
-
|
|
95
|
+
const overlay = document.createElement("div");
|
|
96
|
+
overlay.style.cssText = "display:none;position:fixed;inset:0;z-index:2000;background:#01040999;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;";
|
|
97
|
+
const dialog = document.createElement("div");
|
|
98
|
+
dialog.setAttribute("role", "dialog");
|
|
99
|
+
dialog.setAttribute("aria-modal", "true");
|
|
100
|
+
dialog.style.cssText = "width:min(420px,100%);max-height:min(85vh,640px);display:flex;flex-direction:column;background:#161b22;border:1px solid #30363d;border-radius:8px;box-shadow:0 16px 48px #010409cc;overflow:hidden;";
|
|
101
|
+
const header = document.createElement("div");
|
|
102
|
+
header.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid #30363d;";
|
|
103
|
+
const title = document.createElement("span");
|
|
104
|
+
title.textContent = t("settings.title", "\u8A2D\u5B9A");
|
|
105
|
+
title.style.cssText = "font-size:14px;font-weight:600;color:#e6edf3;";
|
|
106
|
+
const closeBtn = document.createElement("button");
|
|
107
|
+
closeBtn.type = "button";
|
|
108
|
+
closeBtn.textContent = "\xD7";
|
|
109
|
+
closeBtn.title = t("settings.close", "\u95DC\u9589");
|
|
110
|
+
closeBtn.style.cssText = "background:transparent;border:none;color:#8b949e;font-size:20px;line-height:1;cursor:pointer;padding:0 4px;";
|
|
111
|
+
header.append(title, closeBtn);
|
|
69
112
|
const tabs = document.createElement("div");
|
|
70
|
-
tabs.style.cssText = "display:flex;border-bottom:1px solid #30363d;";
|
|
113
|
+
tabs.style.cssText = "display:flex;border-bottom:1px solid #30363d;flex-shrink:0;";
|
|
71
114
|
const content = document.createElement("div");
|
|
72
|
-
content.style.cssText = "padding:10px 12px;font-size:12px;";
|
|
115
|
+
content.style.cssText = "padding:10px 12px;font-size:12px;overflow:auto;flex:1;min-height:0;";
|
|
73
116
|
const tabIds = [
|
|
74
117
|
["chart", t("settings.tab.chart", "\u5716\u8868")],
|
|
75
118
|
["drawing", t("settings.tab.drawing", "\u7E6A\u5716")],
|
|
@@ -101,6 +144,17 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
101
144
|
row.append(input, document.createTextNode(label));
|
|
102
145
|
return row;
|
|
103
146
|
};
|
|
147
|
+
const actionButton = (label, onClick, danger = true) => {
|
|
148
|
+
const b = document.createElement("button");
|
|
149
|
+
b.type = "button";
|
|
150
|
+
b.textContent = label;
|
|
151
|
+
b.style.cssText = `display:block;width:100%;margin-top:10px;padding:6px 10px;border-radius:4px;border:1px solid ${danger ? "#f85149" : "#30363d"};background:#21262d;color:${danger ? "#f85149" : "#e6edf3"};cursor:pointer;font-size:12px;`;
|
|
152
|
+
b.onclick = (e) => {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
onClick();
|
|
155
|
+
};
|
|
156
|
+
return b;
|
|
157
|
+
};
|
|
104
158
|
const numberField = (label, value, onChange) => {
|
|
105
159
|
const row = document.createElement("label");
|
|
106
160
|
row.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;";
|
|
@@ -113,6 +167,49 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
113
167
|
row.appendChild(input);
|
|
114
168
|
return row;
|
|
115
169
|
};
|
|
170
|
+
const renderLayerList = () => {
|
|
171
|
+
const layers = listActiveIndicatorLayers(indicatorConfig);
|
|
172
|
+
const section = document.createElement("div");
|
|
173
|
+
section.style.marginBottom = "12px";
|
|
174
|
+
const heading = document.createElement("div");
|
|
175
|
+
heading.textContent = t("settings.ind.layers", "\u6307\u6A19\u5716\u5C64");
|
|
176
|
+
heading.style.cssText = "font-weight:600;color:#e6edf3;margin-bottom:6px;font-size:12px;";
|
|
177
|
+
section.appendChild(heading);
|
|
178
|
+
if (layers.length === 0) {
|
|
179
|
+
const empty = document.createElement("div");
|
|
180
|
+
empty.textContent = t("settings.ind.layersEmpty", "\u76EE\u524D\u6C92\u6709\u6307\u6A19");
|
|
181
|
+
empty.style.cssText = "color:#8b949e;font-size:11px;";
|
|
182
|
+
section.appendChild(empty);
|
|
183
|
+
return section;
|
|
184
|
+
}
|
|
185
|
+
for (const layer of layers) {
|
|
186
|
+
const row = document.createElement("div");
|
|
187
|
+
row.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 8px;margin-bottom:4px;border-radius:4px;background:#0d1117;border:1px solid #30363d;";
|
|
188
|
+
const meta = document.createElement("div");
|
|
189
|
+
meta.style.cssText = "min-width:0;";
|
|
190
|
+
const name = document.createElement("div");
|
|
191
|
+
name.textContent = layer.label;
|
|
192
|
+
name.style.cssText = "color:#e6edf3;font-size:12px;";
|
|
193
|
+
const target = document.createElement("div");
|
|
194
|
+
target.textContent = layer.target === "main" ? t("settings.ind.layerMain", "\u4E3B\u5716") : t("settings.ind.layerPane", "\u526F\u5716");
|
|
195
|
+
target.style.cssText = "color:#8b949e;font-size:10px;margin-top:2px;";
|
|
196
|
+
meta.append(name, target);
|
|
197
|
+
const remove = document.createElement("button");
|
|
198
|
+
remove.type = "button";
|
|
199
|
+
remove.textContent = t("settings.ind.layerRemove", "\u79FB\u9664");
|
|
200
|
+
remove.title = t("settings.ind.layerRemove", "\u79FB\u9664");
|
|
201
|
+
remove.style.cssText = "flex-shrink:0;padding:2px 8px;border-radius:4px;border:1px solid #f85149;background:transparent;color:#f85149;cursor:pointer;font-size:11px;";
|
|
202
|
+
remove.onclick = (e) => {
|
|
203
|
+
e.stopPropagation();
|
|
204
|
+
indicatorConfig = disableIndicatorLayer(indicatorConfig, layer.id);
|
|
205
|
+
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
206
|
+
renderContent();
|
|
207
|
+
};
|
|
208
|
+
row.append(meta, remove);
|
|
209
|
+
section.appendChild(row);
|
|
210
|
+
}
|
|
211
|
+
return section;
|
|
212
|
+
};
|
|
116
213
|
const renderContent = () => {
|
|
117
214
|
content.replaceChildren();
|
|
118
215
|
if (tab === "chart") {
|
|
@@ -131,23 +228,43 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
131
228
|
opts.onReturnToCursorChange?.(v);
|
|
132
229
|
})
|
|
133
230
|
);
|
|
231
|
+
content.appendChild(
|
|
232
|
+
actionButton(t("settings.drawing.clearAll", "\u6E05\u9664\u6240\u6709\u756B\u7DDA"), () => {
|
|
233
|
+
opts.onClearAllDrawings?.();
|
|
234
|
+
renderContent();
|
|
235
|
+
})
|
|
236
|
+
);
|
|
134
237
|
} else {
|
|
238
|
+
content.appendChild(renderLayerList());
|
|
239
|
+
content.appendChild(
|
|
240
|
+
checkbox(t("settings.ind.volume", "\u6210\u4EA4\u91CF\u526F\u5716"), indicatorConfig.showVolume, (v) => {
|
|
241
|
+
indicatorConfig = { ...indicatorConfig, showVolume: v };
|
|
242
|
+
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
243
|
+
renderContent();
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
const divider = document.createElement("div");
|
|
247
|
+
divider.style.cssText = "height:1px;background:#30363d;margin:10px 0;";
|
|
248
|
+
content.appendChild(divider);
|
|
135
249
|
content.appendChild(
|
|
136
250
|
checkbox(t("settings.ind.macd", "MACD \u7A97\u683C"), indicatorConfig.showMacd, (v) => {
|
|
137
251
|
indicatorConfig = { ...indicatorConfig, showMacd: v };
|
|
138
252
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
253
|
+
renderContent();
|
|
139
254
|
})
|
|
140
255
|
);
|
|
141
256
|
content.appendChild(
|
|
142
257
|
checkbox(t("settings.ind.rsi", "RSI \u7A97\u683C"), indicatorConfig.showRsi, (v) => {
|
|
143
258
|
indicatorConfig = { ...indicatorConfig, showRsi: v };
|
|
144
259
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
260
|
+
renderContent();
|
|
145
261
|
})
|
|
146
262
|
);
|
|
147
263
|
content.appendChild(
|
|
148
264
|
checkbox(t("settings.ind.kdj", "KDJ \u7A97\u683C"), indicatorConfig.showKdj, (v) => {
|
|
149
265
|
indicatorConfig = { ...indicatorConfig, showKdj: v };
|
|
150
266
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
267
|
+
renderContent();
|
|
151
268
|
})
|
|
152
269
|
);
|
|
153
270
|
const src = document.createElement("label");
|
|
@@ -172,6 +289,7 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
172
289
|
checkbox(t("settings.ind.ema", "EMA \u758A\u52A0"), indicatorConfig.showEma, (v) => {
|
|
173
290
|
indicatorConfig = { ...indicatorConfig, showEma: v };
|
|
174
291
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
292
|
+
renderContent();
|
|
175
293
|
})
|
|
176
294
|
);
|
|
177
295
|
content.appendChild(
|
|
@@ -184,6 +302,7 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
184
302
|
checkbox(t("settings.ind.boll", "BOLL \u901A\u9053"), indicatorConfig.showBoll, (v) => {
|
|
185
303
|
indicatorConfig = { ...indicatorConfig, showBoll: v };
|
|
186
304
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
305
|
+
renderContent();
|
|
187
306
|
})
|
|
188
307
|
);
|
|
189
308
|
content.appendChild(
|
|
@@ -222,23 +341,151 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
222
341
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
223
342
|
})
|
|
224
343
|
);
|
|
344
|
+
content.appendChild(
|
|
345
|
+
actionButton(t("settings.ind.clearAll", "\u6E05\u7A7A\u6240\u6709\u6307\u6A19"), () => {
|
|
346
|
+
opts.onClearAllIndicators?.();
|
|
347
|
+
if (opts.indicatorConfig) indicatorConfig = { ...opts.indicatorConfig };
|
|
348
|
+
renderContent();
|
|
349
|
+
})
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const setOpen = (next) => {
|
|
354
|
+
open = next;
|
|
355
|
+
overlay.style.display = open ? "flex" : "none";
|
|
356
|
+
if (open) {
|
|
357
|
+
if (opts.indicatorConfig) indicatorConfig = { ...opts.indicatorConfig };
|
|
358
|
+
renderContent();
|
|
225
359
|
}
|
|
226
360
|
};
|
|
227
361
|
renderTabs();
|
|
228
362
|
renderContent();
|
|
229
|
-
|
|
363
|
+
dialog.append(header, tabs, content);
|
|
364
|
+
overlay.appendChild(dialog);
|
|
365
|
+
document.body.appendChild(overlay);
|
|
230
366
|
btn.onclick = (e) => {
|
|
231
367
|
e.stopPropagation();
|
|
232
|
-
|
|
233
|
-
|
|
368
|
+
setOpen(!open);
|
|
369
|
+
};
|
|
370
|
+
closeBtn.onclick = (e) => {
|
|
371
|
+
e.stopPropagation();
|
|
372
|
+
setOpen(false);
|
|
373
|
+
};
|
|
374
|
+
overlay.onclick = () => setOpen(false);
|
|
375
|
+
dialog.onclick = (e) => e.stopPropagation();
|
|
376
|
+
const onKey = (e) => {
|
|
377
|
+
if (e.key === "Escape" && open) setOpen(false);
|
|
378
|
+
};
|
|
379
|
+
document.addEventListener("keydown", onKey);
|
|
380
|
+
wrap.append(btn);
|
|
381
|
+
parent.appendChild(wrap);
|
|
382
|
+
const origRemove = wrap.remove.bind(wrap);
|
|
383
|
+
wrap.remove = () => {
|
|
384
|
+
document.removeEventListener("keydown", onKey);
|
|
385
|
+
overlay.remove();
|
|
386
|
+
origRemove();
|
|
387
|
+
};
|
|
388
|
+
return wrap;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/symbol-search-dialog.ts
|
|
392
|
+
function createSymbolSearchDialog(opts) {
|
|
393
|
+
const i18n = opts.i18n;
|
|
394
|
+
const backdrop = document.createElement("div");
|
|
395
|
+
backdrop.className = "tv-symbol-dialog-backdrop";
|
|
396
|
+
backdrop.style.cssText = "display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,0.55);align-items:flex-start;justify-content:center;padding:12vh 16px 16px;";
|
|
397
|
+
const panel = document.createElement("div");
|
|
398
|
+
panel.className = "tv-symbol-dialog";
|
|
399
|
+
panel.style.cssText = "width:min(420px,100%);background:var(--tv-surface,#161b22);border:1px solid var(--tv-border,#30363d);border-radius:8px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,0.4);";
|
|
400
|
+
const title = document.createElement("div");
|
|
401
|
+
title.textContent = i18n?.t("symbol.search", "\u641C\u5C0B\u5546\u54C1") ?? "\u641C\u5C0B\u5546\u54C1";
|
|
402
|
+
title.style.cssText = "font-size:13px;font-weight:600;color:var(--tv-fg,#e6edf3);margin-bottom:8px;";
|
|
403
|
+
const input = document.createElement("input");
|
|
404
|
+
input.type = "search";
|
|
405
|
+
input.placeholder = i18n?.t("symbol.search", "\u641C\u5C0B\u5546\u54C1") ?? "\u641C\u5C0B\u5546\u54C1";
|
|
406
|
+
input.value = opts.initialSymbol ?? "";
|
|
407
|
+
input.style.cssText = "width:100%;box-sizing:border-box;padding:8px 10px;border-radius:4px;border:1px solid var(--tv-border,#30363d);background:var(--tv-bg,#0d1117);color:var(--tv-fg,#e6edf3);font-size:13px;";
|
|
408
|
+
const list = document.createElement("div");
|
|
409
|
+
list.style.cssText = "margin-top:8px;max-height:240px;overflow:auto;";
|
|
410
|
+
panel.append(title, input, list);
|
|
411
|
+
backdrop.appendChild(panel);
|
|
412
|
+
let timer;
|
|
413
|
+
let mounted = false;
|
|
414
|
+
const renderHits = (hits) => {
|
|
415
|
+
list.replaceChildren();
|
|
416
|
+
if (hits.length === 0) {
|
|
417
|
+
const empty = document.createElement("div");
|
|
418
|
+
empty.textContent = "\u2014";
|
|
419
|
+
empty.style.cssText = "padding:8px;color:var(--tv-muted,#8b949e);font-size:12px;";
|
|
420
|
+
list.appendChild(empty);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
for (const hit of hits) {
|
|
424
|
+
const row = document.createElement("button");
|
|
425
|
+
row.type = "button";
|
|
426
|
+
row.textContent = hit.exchange ? `${hit.symbol} \xB7 ${hit.exchange}` : hit.symbol;
|
|
427
|
+
row.style.cssText = "display:block;width:100%;text-align:left;padding:8px 10px;border:none;background:transparent;color:var(--tv-fg,#e6edf3);cursor:pointer;font-size:12px;border-radius:4px;";
|
|
428
|
+
row.onmouseenter = () => {
|
|
429
|
+
row.style.background = "var(--tv-btn-bg,#21262d)";
|
|
430
|
+
};
|
|
431
|
+
row.onmouseleave = () => {
|
|
432
|
+
row.style.background = "transparent";
|
|
433
|
+
};
|
|
434
|
+
row.onclick = () => {
|
|
435
|
+
opts.onSelect(hit.symbol);
|
|
436
|
+
close();
|
|
437
|
+
};
|
|
438
|
+
list.appendChild(row);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
const runSearch = () => {
|
|
442
|
+
const q = input.value.trim();
|
|
443
|
+
if (q.length < 1) {
|
|
444
|
+
list.replaceChildren();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
void opts.onSearch(q).then(renderHits).catch(() => list.replaceChildren());
|
|
448
|
+
};
|
|
449
|
+
input.addEventListener("input", () => {
|
|
450
|
+
clearTimeout(timer);
|
|
451
|
+
timer = setTimeout(runSearch, 200);
|
|
452
|
+
});
|
|
453
|
+
input.addEventListener("keydown", (e) => {
|
|
454
|
+
if (e.key === "Escape") close();
|
|
455
|
+
if (e.key === "Enter") runSearch();
|
|
456
|
+
});
|
|
457
|
+
backdrop.addEventListener("click", (e) => {
|
|
458
|
+
if (e.target === backdrop) close();
|
|
459
|
+
});
|
|
460
|
+
const open = () => {
|
|
461
|
+
if (!mounted) {
|
|
462
|
+
document.body.appendChild(backdrop);
|
|
463
|
+
mounted = true;
|
|
464
|
+
}
|
|
465
|
+
backdrop.style.display = "flex";
|
|
466
|
+
input.focus();
|
|
467
|
+
input.select();
|
|
468
|
+
runSearch();
|
|
234
469
|
};
|
|
235
470
|
const close = () => {
|
|
236
|
-
|
|
237
|
-
panel.style.display = "none";
|
|
471
|
+
backdrop.style.display = "none";
|
|
238
472
|
};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
473
|
+
const destroy = () => {
|
|
474
|
+
clearTimeout(timer);
|
|
475
|
+
backdrop.remove();
|
|
476
|
+
mounted = false;
|
|
477
|
+
};
|
|
478
|
+
return { open, close, destroy };
|
|
479
|
+
}
|
|
480
|
+
function mountSymbolSearchDialogTrigger(parent, dialog, opts) {
|
|
481
|
+
const wrap = document.createElement("div");
|
|
482
|
+
wrap.style.cssText = "display:flex;align-items:center;margin-right:8px;flex-shrink:0;";
|
|
483
|
+
const btn = document.createElement("button");
|
|
484
|
+
btn.type = "button";
|
|
485
|
+
btn.textContent = opts.initialSymbol ?? (opts.i18n?.t("symbol.search", "\u641C\u5C0B\u5546\u54C1") ?? "\u641C\u5C0B\u5546\u54C1");
|
|
486
|
+
btn.style.cssText = "min-width:140px;text-align:left;background:var(--tv-bg,#0d1117);color:var(--tv-fg,#e6edf3);border:1px solid var(--tv-border,#30363d);border-radius:4px;padding:4px 10px;cursor:pointer;font-size:12px;";
|
|
487
|
+
btn.onclick = () => dialog.open();
|
|
488
|
+
wrap.appendChild(btn);
|
|
242
489
|
parent.appendChild(wrap);
|
|
243
490
|
return wrap;
|
|
244
491
|
}
|
|
@@ -306,13 +553,32 @@ function mountSymbolSearch(parent, opts) {
|
|
|
306
553
|
return wrap;
|
|
307
554
|
}
|
|
308
555
|
|
|
556
|
+
// src/theme-toggle.ts
|
|
557
|
+
function mountThemeToggle(parent, opts) {
|
|
558
|
+
const i18n = opts.i18n;
|
|
559
|
+
const btn = document.createElement("button");
|
|
560
|
+
btn.type = "button";
|
|
561
|
+
btn.className = "tv-theme-toggle";
|
|
562
|
+
btn.style.cssText = "background:var(--tv-btn-bg,#21262d);color:var(--tv-fg,#e6edf3);border:1px solid var(--tv-border,#30363d);border-radius:4px;padding:4px 10px;cursor:pointer;font-size:12px;flex-shrink:0;";
|
|
563
|
+
const paint = (theme) => {
|
|
564
|
+
btn.textContent = theme === "dark" ? i18n?.t("theme.dark", "\u6DF1\u8272") ?? "\u6DF1\u8272" : i18n?.t("theme.light", "\u6DFA\u8272") ?? "\u6DFA\u8272";
|
|
565
|
+
};
|
|
566
|
+
btn.onclick = () => {
|
|
567
|
+
const next = opts.themeProvider.toggle();
|
|
568
|
+
opts.onThemeChange?.(next);
|
|
569
|
+
};
|
|
570
|
+
opts.themeProvider.subscribe((theme) => paint(theme));
|
|
571
|
+
parent.appendChild(btn);
|
|
572
|
+
return btn;
|
|
573
|
+
}
|
|
574
|
+
|
|
309
575
|
// src/top-bar.ts
|
|
310
576
|
function mountManualSymbolInput(parent, opts) {
|
|
311
577
|
const wrap = document.createElement("div");
|
|
312
578
|
wrap.style.cssText = "display:flex;align-items:center;gap:4px;margin-right:8px;";
|
|
313
579
|
const input = document.createElement("input");
|
|
314
580
|
input.type = "text";
|
|
315
|
-
input.placeholder =
|
|
581
|
+
input.placeholder = opts.i18n?.t("symbol.search", "Symbol") ?? "Symbol";
|
|
316
582
|
input.value = opts.initialSymbol ?? "";
|
|
317
583
|
input.style.cssText = "width:140px;background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:4px;padding:4px 8px;font-size:12px;";
|
|
318
584
|
const apply = () => {
|
|
@@ -332,9 +598,14 @@ function mountManualSymbolInput(parent, opts) {
|
|
|
332
598
|
parent.appendChild(wrap);
|
|
333
599
|
}
|
|
334
600
|
function mountTopBar(parent, opts = {}) {
|
|
601
|
+
const i18n = opts.i18n;
|
|
602
|
+
const tKey = (key, fallback) => i18n?.t(key, fallback) ?? fallback;
|
|
335
603
|
const bar = document.createElement("div");
|
|
336
604
|
bar.className = "tv-topbar";
|
|
337
|
-
bar.style.cssText = "display:flex;gap:8px;padding:8px 12px;align-items:center;border-bottom:1px solid
|
|
605
|
+
bar.style.cssText = "display:flex;gap:8px;padding:8px 12px;align-items:center;border-bottom:1px solid var(--tv-border,#30363d);background:var(--tv-surface,#161b22);flex-shrink:0;box-sizing:border-box;width:100%;min-width:0;overflow-x:auto;overflow-y:visible;position:relative;";
|
|
606
|
+
if (opts.logo !== false) {
|
|
607
|
+
mountLogoSlot(bar, opts.logo ?? { label: "TradView" });
|
|
608
|
+
}
|
|
338
609
|
const symbolMode = opts.symbolInput ?? "manual";
|
|
339
610
|
if (symbolMode === "search" && opts.onSymbolSearch && opts.onSymbolSelect) {
|
|
340
611
|
mountSymbolSearch(bar, {
|
|
@@ -342,10 +613,22 @@ function mountTopBar(parent, opts = {}) {
|
|
|
342
613
|
onSearch: opts.onSymbolSearch,
|
|
343
614
|
onSelect: opts.onSymbolSelect
|
|
344
615
|
});
|
|
616
|
+
} else if (symbolMode === "dialog" && opts.onSymbolSearch && opts.onSymbolSelect) {
|
|
617
|
+
const dialog = createSymbolSearchDialog({
|
|
618
|
+
initialSymbol: opts.initialSymbol,
|
|
619
|
+
onSearch: opts.onSymbolSearch,
|
|
620
|
+
onSelect: opts.onSymbolSelect,
|
|
621
|
+
i18n
|
|
622
|
+
});
|
|
623
|
+
mountSymbolSearchDialogTrigger(bar, dialog, {
|
|
624
|
+
initialSymbol: opts.initialSymbol,
|
|
625
|
+
i18n
|
|
626
|
+
});
|
|
345
627
|
} else if (symbolMode === "manual" && opts.onSymbolSelect) {
|
|
346
628
|
mountManualSymbolInput(bar, {
|
|
347
629
|
initialSymbol: opts.initialSymbol,
|
|
348
|
-
onSymbolSelect: opts.onSymbolSelect
|
|
630
|
+
onSymbolSelect: opts.onSymbolSelect,
|
|
631
|
+
i18n
|
|
349
632
|
});
|
|
350
633
|
}
|
|
351
634
|
const intervals = opts.intervals ?? DEFAULT_INTERVALS;
|
|
@@ -364,7 +647,7 @@ function mountTopBar(parent, opts = {}) {
|
|
|
364
647
|
for (const iv of intervals) {
|
|
365
648
|
const btn = document.createElement("button");
|
|
366
649
|
btn.type = "button";
|
|
367
|
-
btn.textContent =
|
|
650
|
+
btn.textContent = tKey(`interval.${iv}`, iv);
|
|
368
651
|
btn.style.cssText = btnStyle;
|
|
369
652
|
btn.onclick = () => {
|
|
370
653
|
if (activeInterval === iv) return;
|
|
@@ -388,7 +671,15 @@ function mountTopBar(parent, opts = {}) {
|
|
|
388
671
|
b.onclick = () => fn?.();
|
|
389
672
|
return b;
|
|
390
673
|
};
|
|
391
|
-
|
|
674
|
+
if (opts.themeProvider) {
|
|
675
|
+
mountThemeToggle(bar, {
|
|
676
|
+
themeProvider: opts.themeProvider,
|
|
677
|
+
i18n,
|
|
678
|
+
onThemeChange: opts.onThemeChange
|
|
679
|
+
});
|
|
680
|
+
} else {
|
|
681
|
+
bar.appendChild(mkBtn(tKey("theme.dark", "\u4E3B\u984C"), opts.onThemeToggle));
|
|
682
|
+
}
|
|
392
683
|
if (opts.showSettings && opts.settings) mountSettingsPanel(bar, opts.settings);
|
|
393
684
|
bar.appendChild(mkBtn("\u26F6", opts.onFullscreen));
|
|
394
685
|
bar.appendChild(mkBtn("\u{1F4F7}", opts.onScreenshot));
|
|
@@ -403,8 +694,119 @@ function mountTopBar(parent, opts = {}) {
|
|
|
403
694
|
};
|
|
404
695
|
}
|
|
405
696
|
|
|
697
|
+
// src/theme-provider.ts
|
|
698
|
+
var THEME_STORAGE_KEY = "tradview:theme";
|
|
699
|
+
var THEME_VARS = {
|
|
700
|
+
dark: {
|
|
701
|
+
"--tv-bg": "#0d1117",
|
|
702
|
+
"--tv-surface": "#161b22",
|
|
703
|
+
"--tv-border": "#30363d",
|
|
704
|
+
"--tv-fg": "#e6edf3",
|
|
705
|
+
"--tv-muted": "#8b949e",
|
|
706
|
+
"--tv-accent": "#388bfd",
|
|
707
|
+
"--tv-btn-bg": "#21262d"
|
|
708
|
+
},
|
|
709
|
+
light: {
|
|
710
|
+
"--tv-bg": "#ffffff",
|
|
711
|
+
"--tv-surface": "#f6f8fa",
|
|
712
|
+
"--tv-border": "#d0d7de",
|
|
713
|
+
"--tv-fg": "#24292f",
|
|
714
|
+
"--tv-muted": "#656d76",
|
|
715
|
+
"--tv-accent": "#0969da",
|
|
716
|
+
"--tv-btn-bg": "#f6f8fa"
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
function loadTheme() {
|
|
720
|
+
try {
|
|
721
|
+
const v = localStorage.getItem(THEME_STORAGE_KEY);
|
|
722
|
+
return v === "light" ? "light" : "dark";
|
|
723
|
+
} catch {
|
|
724
|
+
return "dark";
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function saveTheme(theme) {
|
|
728
|
+
try {
|
|
729
|
+
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
|
730
|
+
} catch {
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function applyThemeToDocument(theme, root = document.documentElement) {
|
|
734
|
+
const vars = THEME_VARS[theme];
|
|
735
|
+
for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
|
|
736
|
+
root.dataset.tradviewTheme = theme;
|
|
737
|
+
}
|
|
738
|
+
function createThemeProvider(initial) {
|
|
739
|
+
let theme = initial ?? loadTheme();
|
|
740
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
741
|
+
const notify = () => {
|
|
742
|
+
applyThemeToDocument(theme);
|
|
743
|
+
for (const l of listeners) l(theme);
|
|
744
|
+
};
|
|
745
|
+
applyThemeToDocument(theme);
|
|
746
|
+
return {
|
|
747
|
+
getTheme: () => theme,
|
|
748
|
+
setTheme: (next) => {
|
|
749
|
+
if (next === theme) return;
|
|
750
|
+
theme = next;
|
|
751
|
+
saveTheme(theme);
|
|
752
|
+
notify();
|
|
753
|
+
},
|
|
754
|
+
toggle: () => {
|
|
755
|
+
theme = theme === "dark" ? "light" : "dark";
|
|
756
|
+
saveTheme(theme);
|
|
757
|
+
notify();
|
|
758
|
+
return theme;
|
|
759
|
+
},
|
|
760
|
+
subscribe: (listener) => {
|
|
761
|
+
listeners.add(listener);
|
|
762
|
+
listener(theme);
|
|
763
|
+
return () => listeners.delete(listener);
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/i18n-provider.ts
|
|
769
|
+
import {
|
|
770
|
+
getLocale,
|
|
771
|
+
registerLocale,
|
|
772
|
+
setLocale,
|
|
773
|
+
t as i18nT
|
|
774
|
+
} from "@coderyo/i18n";
|
|
775
|
+
function interpolate(text, params) {
|
|
776
|
+
if (!params) return text;
|
|
777
|
+
return text.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
|
|
778
|
+
}
|
|
779
|
+
function createI18nProvider(defaultLocale = "zh-TW") {
|
|
780
|
+
setLocale(defaultLocale);
|
|
781
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
782
|
+
return {
|
|
783
|
+
t(key, fallback, params) {
|
|
784
|
+
return interpolate(i18nT(key, fallback), params);
|
|
785
|
+
},
|
|
786
|
+
getLocale,
|
|
787
|
+
setLocale(locale) {
|
|
788
|
+
setLocale(locale);
|
|
789
|
+
for (const l of listeners) l(locale);
|
|
790
|
+
},
|
|
791
|
+
registerLocale(loc, dictionary) {
|
|
792
|
+
registerLocale(loc, dictionary);
|
|
793
|
+
if (getLocale() === loc) {
|
|
794
|
+
for (const l of listeners) l(loc);
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
subscribe(listener) {
|
|
798
|
+
listeners.add(listener);
|
|
799
|
+
listener(getLocale());
|
|
800
|
+
return () => listeners.delete(listener);
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/chart-layout.ts
|
|
806
|
+
import { t as t7 } from "@coderyo/i18n";
|
|
807
|
+
|
|
406
808
|
// src/context-menu.ts
|
|
407
|
-
import { t as
|
|
809
|
+
import { t as t3 } from "@coderyo/i18n";
|
|
408
810
|
function attachChartContextMenu(chartHost, opts = {}) {
|
|
409
811
|
let menu = null;
|
|
410
812
|
const close = () => {
|
|
@@ -418,7 +820,7 @@ function attachChartContextMenu(chartHost, opts = {}) {
|
|
|
418
820
|
const actions = opts.actions ?? [
|
|
419
821
|
{
|
|
420
822
|
id: "fit",
|
|
421
|
-
label:
|
|
823
|
+
label: t3("context.fitContent", "\u9069\u914D\u756B\u9762"),
|
|
422
824
|
onClick: () => {
|
|
423
825
|
}
|
|
424
826
|
}
|
|
@@ -468,7 +870,7 @@ function mountCrosshairLegend(chartHost, opts = {}) {
|
|
|
468
870
|
title.style.cssText = "color:#8b949e;margin-bottom:2px;";
|
|
469
871
|
const body = document.createElement("div");
|
|
470
872
|
box.append(title, body);
|
|
471
|
-
chartHost.appendChild(box);
|
|
873
|
+
if (chartHost) chartHost.appendChild(box);
|
|
472
874
|
const fmt2 = (n) => n == null ? "\u2014" : n.toLocaleString(void 0, { maximumFractionDigits: 2 });
|
|
473
875
|
const render = (payload) => {
|
|
474
876
|
const parts = [opts.symbol, opts.interval].filter(Boolean);
|
|
@@ -497,13 +899,13 @@ O ${fmt2(o?.o)} H ${fmt2(o?.h)} L ${fmt2(o?.l)} C ${fmt2(o?.c)}`;
|
|
|
497
899
|
}
|
|
498
900
|
|
|
499
901
|
// src/drawing-properties-panel.ts
|
|
500
|
-
import { t as
|
|
902
|
+
import { t as t4 } from "@coderyo/i18n";
|
|
501
903
|
function mountDrawingPropertiesPanel(parent, opts = {}) {
|
|
502
904
|
const panel = document.createElement("aside");
|
|
503
905
|
panel.className = "tv-drawing-props";
|
|
504
|
-
panel.style.cssText = "display:none;width:
|
|
906
|
+
panel.style.cssText = "display:none;width:100%;max-width:200px;flex-shrink:0;border-left:1px solid #30363d;background:#161b22;padding:10px 12px;font-size:12px;color:#e6edf3;overflow:auto;box-sizing:border-box;";
|
|
505
907
|
const title = document.createElement("div");
|
|
506
|
-
title.textContent =
|
|
908
|
+
title.textContent = t4("drawing.props.title", "\u7E6A\u5716\u5C6C\u6027");
|
|
507
909
|
title.style.cssText = "font-weight:600;margin-bottom:10px;";
|
|
508
910
|
const typeEl = document.createElement("div");
|
|
509
911
|
typeEl.style.color = "#8b949e";
|
|
@@ -517,26 +919,26 @@ function mountDrawingPropertiesPanel(parent, opts = {}) {
|
|
|
517
919
|
row.appendChild(span);
|
|
518
920
|
return row;
|
|
519
921
|
};
|
|
520
|
-
const colorRow = mkRow(
|
|
922
|
+
const colorRow = mkRow(t4("drawing.props.color", "\u984F\u8272"));
|
|
521
923
|
const colorInput = document.createElement("input");
|
|
522
924
|
colorInput.type = "color";
|
|
523
925
|
colorInput.style.cssText = "width:100%;height:28px;border:none;background:transparent;cursor:pointer;";
|
|
524
926
|
colorRow.appendChild(colorInput);
|
|
525
|
-
const widthRow = mkRow(
|
|
927
|
+
const widthRow = mkRow(t4("drawing.props.lineWidth", "\u7DDA\u5BEC"));
|
|
526
928
|
const widthInput = document.createElement("input");
|
|
527
929
|
widthInput.type = "range";
|
|
528
930
|
widthInput.min = "1";
|
|
529
931
|
widthInput.max = "6";
|
|
530
932
|
widthInput.style.width = "100%";
|
|
531
933
|
widthRow.appendChild(widthInput);
|
|
532
|
-
const textRow = mkRow(
|
|
934
|
+
const textRow = mkRow(t4("drawing.props.text", "\u6587\u5B57"));
|
|
533
935
|
const textInput = document.createElement("input");
|
|
534
936
|
textInput.type = "text";
|
|
535
937
|
textInput.style.cssText = "padding:4px 8px;border-radius:4px;border:1px solid #30363d;background:#0d1117;color:#e6edf3;";
|
|
536
938
|
textRow.appendChild(textInput);
|
|
537
939
|
const lockedHint = document.createElement("div");
|
|
538
940
|
lockedHint.style.cssText = "color:#f78166;font-size:11px;margin-top:6px;display:none;";
|
|
539
|
-
lockedHint.textContent =
|
|
941
|
+
lockedHint.textContent = t4("drawing.props.locked", "\u5DF2\u9396\u5B9A \u2014 \u89E3\u9396\u5F8C\u53EF\u7DE8\u8F2F");
|
|
540
942
|
panel.append(title, typeEl, colorRow, widthRow, textRow, lockedHint);
|
|
541
943
|
parent.appendChild(panel);
|
|
542
944
|
const emit = () => {
|
|
@@ -555,7 +957,7 @@ function mountDrawingPropertiesPanel(parent, opts = {}) {
|
|
|
555
957
|
return;
|
|
556
958
|
}
|
|
557
959
|
panel.style.display = "block";
|
|
558
|
-
typeEl.textContent = `${
|
|
960
|
+
typeEl.textContent = `${t4("drawing.props.type", "\u985E\u578B")}: ${drawing.type}`;
|
|
559
961
|
const meta = drawing.meta ?? {};
|
|
560
962
|
colorInput.value = String(meta.color ?? "#58a6ff");
|
|
561
963
|
widthInput.value = String(meta.lineWidth ?? 2);
|
|
@@ -579,6 +981,414 @@ function mountIndicatorPaneHost(parent) {
|
|
|
579
981
|
return host;
|
|
580
982
|
}
|
|
581
983
|
|
|
984
|
+
// src/layout-schema.ts
|
|
985
|
+
var LAYOUT_SCHEMA_VERSION = 1;
|
|
986
|
+
var DEFAULT_LAYOUT_SCHEMA = {
|
|
987
|
+
version: 1,
|
|
988
|
+
columns: 12,
|
|
989
|
+
rows: 12,
|
|
990
|
+
widgets: [
|
|
991
|
+
{ id: "topBar", col: 0, row: 0, colSpan: 12, rowSpan: 1 },
|
|
992
|
+
{ id: "leftToolbar", col: 0, row: 1, colSpan: 1, rowSpan: 10 },
|
|
993
|
+
{ id: "chartHost", col: 1, row: 1, colSpan: 9, rowSpan: 6 },
|
|
994
|
+
{ id: "indicatorHost", col: 1, row: 7, colSpan: 9, rowSpan: 2 },
|
|
995
|
+
{ id: "statusBar", col: 1, row: 9, colSpan: 9, rowSpan: 1 },
|
|
996
|
+
{ id: "propertiesPanel", col: 10, row: 1, colSpan: 2, rowSpan: 9 },
|
|
997
|
+
{ id: "bottomToolbar", col: 0, row: 11, colSpan: 12, rowSpan: 1 }
|
|
998
|
+
]
|
|
999
|
+
};
|
|
1000
|
+
function layoutStorageKey(layoutId) {
|
|
1001
|
+
return `tradview:layout:v${LAYOUT_SCHEMA_VERSION}:${layoutId}`;
|
|
1002
|
+
}
|
|
1003
|
+
function resolveLayoutSchema(partial) {
|
|
1004
|
+
if (!partial) return cloneLayoutSchema(DEFAULT_LAYOUT_SCHEMA);
|
|
1005
|
+
return normalizeLayoutSchema(partial);
|
|
1006
|
+
}
|
|
1007
|
+
function cloneLayoutSchema(schema) {
|
|
1008
|
+
return {
|
|
1009
|
+
version: 1,
|
|
1010
|
+
columns: schema.columns,
|
|
1011
|
+
rows: schema.rows,
|
|
1012
|
+
widgets: schema.widgets.map((w) => ({ ...w }))
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function normalizeLayoutSchema(input) {
|
|
1016
|
+
const columns = Math.max(4, Math.min(24, input.columns || 12));
|
|
1017
|
+
const rows = Math.max(4, Math.min(24, input.rows || 12));
|
|
1018
|
+
const widgets = [];
|
|
1019
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1020
|
+
for (const w of input.widgets ?? []) {
|
|
1021
|
+
if (!isLayoutWidgetId(w.id) || seen.has(w.id)) continue;
|
|
1022
|
+
seen.add(w.id);
|
|
1023
|
+
widgets.push(clampPlacement(w, columns, rows));
|
|
1024
|
+
}
|
|
1025
|
+
for (const d of DEFAULT_LAYOUT_SCHEMA.widgets) {
|
|
1026
|
+
if (!seen.has(d.id)) widgets.push({ ...d });
|
|
1027
|
+
}
|
|
1028
|
+
return { version: 1, columns, rows, widgets };
|
|
1029
|
+
}
|
|
1030
|
+
function clampPlacement(w, columns, rows) {
|
|
1031
|
+
const colSpan = Math.max(1, Math.min(columns, w.colSpan || 1));
|
|
1032
|
+
const rowSpan = Math.max(1, Math.min(rows, w.rowSpan || 1));
|
|
1033
|
+
const col = Math.max(0, Math.min(columns - colSpan, w.col || 0));
|
|
1034
|
+
const row = Math.max(0, Math.min(rows - rowSpan, w.row || 0));
|
|
1035
|
+
return { id: w.id, col, row, colSpan, rowSpan };
|
|
1036
|
+
}
|
|
1037
|
+
function isLayoutWidgetId(id) {
|
|
1038
|
+
return id === "topBar" || id === "leftToolbar" || id === "bottomToolbar" || id === "chartHost" || id === "indicatorHost" || id === "statusBar" || id === "propertiesPanel";
|
|
1039
|
+
}
|
|
1040
|
+
function loadLayoutSchema(layoutId) {
|
|
1041
|
+
try {
|
|
1042
|
+
const raw = localStorage.getItem(layoutStorageKey(layoutId));
|
|
1043
|
+
if (!raw) return null;
|
|
1044
|
+
return normalizeLayoutSchema(JSON.parse(raw));
|
|
1045
|
+
} catch {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function saveLayoutSchema(layoutId, schema) {
|
|
1050
|
+
try {
|
|
1051
|
+
localStorage.setItem(layoutStorageKey(layoutId), JSON.stringify(normalizeLayoutSchema(schema)));
|
|
1052
|
+
} catch {
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function getWidgetPlacement(schema, id) {
|
|
1056
|
+
return schema.widgets.find((w) => w.id === id);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/layout-engine.ts
|
|
1060
|
+
function createLayoutGrid(opts) {
|
|
1061
|
+
let schema = cloneLayoutSchema(opts.schema);
|
|
1062
|
+
const cells = /* @__PURE__ */ new Map();
|
|
1063
|
+
const root = document.createElement("div");
|
|
1064
|
+
root.className = "tv-layout-root";
|
|
1065
|
+
root.style.cssText = "display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;width:100%;height:100%;box-sizing:border-box;";
|
|
1066
|
+
const grid = document.createElement("div");
|
|
1067
|
+
grid.className = "tv-layout-grid";
|
|
1068
|
+
grid.style.cssText = "display:grid;flex:1;min-height:0;min-width:0;width:100%;position:relative;gap:0;";
|
|
1069
|
+
root.appendChild(grid);
|
|
1070
|
+
for (const w of schema.widgets) {
|
|
1071
|
+
const cell = document.createElement("div");
|
|
1072
|
+
cell.className = `tv-layout-cell tv-layout-cell--${w.id}`;
|
|
1073
|
+
cell.dataset.widgetId = w.id;
|
|
1074
|
+
cell.style.cssText = "min-width:0;min-height:0;overflow:hidden;position:relative;box-sizing:border-box;";
|
|
1075
|
+
const el = opts.widgets[w.id];
|
|
1076
|
+
if (el) {
|
|
1077
|
+
el.style.width = "100%";
|
|
1078
|
+
el.style.height = "100%";
|
|
1079
|
+
el.style.minWidth = "0";
|
|
1080
|
+
el.style.minHeight = "0";
|
|
1081
|
+
if (w.id === "chartHost") el.style.position = "relative";
|
|
1082
|
+
cell.appendChild(el);
|
|
1083
|
+
}
|
|
1084
|
+
cells.set(w.id, cell);
|
|
1085
|
+
grid.appendChild(cell);
|
|
1086
|
+
}
|
|
1087
|
+
let editorCtl = null;
|
|
1088
|
+
const applySchema = (next, features) => {
|
|
1089
|
+
schema = cloneLayoutSchema(next);
|
|
1090
|
+
applyGridTemplate(grid, schema, features);
|
|
1091
|
+
for (const w of schema.widgets) {
|
|
1092
|
+
const cell = cells.get(w.id);
|
|
1093
|
+
if (!cell) continue;
|
|
1094
|
+
applyPlacementStyle(cell, w);
|
|
1095
|
+
const visible = widgetVisible(w.id, features);
|
|
1096
|
+
cell.style.display = visible ? "" : "none";
|
|
1097
|
+
cell.style.pointerEvents = visible ? "" : "none";
|
|
1098
|
+
}
|
|
1099
|
+
editorCtl?.refresh(schema);
|
|
1100
|
+
};
|
|
1101
|
+
const enableEditor = (enabled) => {
|
|
1102
|
+
if (enabled) {
|
|
1103
|
+
if (!editorCtl) {
|
|
1104
|
+
editorCtl = new LayoutEditorController(grid, cells, schema, (s) => {
|
|
1105
|
+
schema = s;
|
|
1106
|
+
opts.onSchemaChange?.(s);
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
editorCtl.enable(true);
|
|
1110
|
+
root.classList.add("tv-layout--edit");
|
|
1111
|
+
} else {
|
|
1112
|
+
editorCtl?.enable(false);
|
|
1113
|
+
root.classList.remove("tv-layout--edit");
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
applySchema(schema, opts.features);
|
|
1117
|
+
if (opts.editor) enableEditor(true);
|
|
1118
|
+
return {
|
|
1119
|
+
root,
|
|
1120
|
+
grid,
|
|
1121
|
+
cells,
|
|
1122
|
+
applySchema,
|
|
1123
|
+
enableEditor,
|
|
1124
|
+
getSchema: () => cloneLayoutSchema(schema),
|
|
1125
|
+
setSchema: (s) => {
|
|
1126
|
+
schema = cloneLayoutSchema(s);
|
|
1127
|
+
applySchema(schema, opts.features);
|
|
1128
|
+
},
|
|
1129
|
+
destroyEditor: () => {
|
|
1130
|
+
editorCtl?.destroy();
|
|
1131
|
+
editorCtl = null;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function widgetVisible(id, f) {
|
|
1136
|
+
switch (id) {
|
|
1137
|
+
case "topBar":
|
|
1138
|
+
return f.showTopBar;
|
|
1139
|
+
case "leftToolbar":
|
|
1140
|
+
return f.showLeftToolbar;
|
|
1141
|
+
case "bottomToolbar":
|
|
1142
|
+
return f.showBottomToolbar;
|
|
1143
|
+
case "statusBar":
|
|
1144
|
+
return f.showStatusBar;
|
|
1145
|
+
case "propertiesPanel":
|
|
1146
|
+
return f.showPropertiesPanel;
|
|
1147
|
+
case "chartHost":
|
|
1148
|
+
case "indicatorHost":
|
|
1149
|
+
return true;
|
|
1150
|
+
default:
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
var LEFT_TOOLBAR_COL_PX = 44;
|
|
1155
|
+
function applyGridTemplate(grid, schema, features) {
|
|
1156
|
+
const narrowToolbar = features?.showLeftToolbar ?? false;
|
|
1157
|
+
if (narrowToolbar && schema.columns >= 2) {
|
|
1158
|
+
grid.style.gridTemplateColumns = `${LEFT_TOOLBAR_COL_PX}px repeat(${schema.columns - 1}, minmax(0, 1fr))`;
|
|
1159
|
+
} else {
|
|
1160
|
+
grid.style.gridTemplateColumns = `repeat(${schema.columns}, minmax(0, 1fr))`;
|
|
1161
|
+
}
|
|
1162
|
+
grid.style.gridTemplateRows = `repeat(${schema.rows}, minmax(0, 1fr))`;
|
|
1163
|
+
}
|
|
1164
|
+
function applyPlacementStyle(cell, w) {
|
|
1165
|
+
cell.style.gridColumn = `${w.col + 1} / span ${w.colSpan}`;
|
|
1166
|
+
cell.style.gridRow = `${w.row + 1} / span ${w.rowSpan}`;
|
|
1167
|
+
}
|
|
1168
|
+
var LayoutEditorController = class {
|
|
1169
|
+
constructor(grid, cells, schema, onChange) {
|
|
1170
|
+
this.grid = grid;
|
|
1171
|
+
this.cells = cells;
|
|
1172
|
+
this.schema = schema;
|
|
1173
|
+
this.onChange = onChange;
|
|
1174
|
+
}
|
|
1175
|
+
grid;
|
|
1176
|
+
cells;
|
|
1177
|
+
schema;
|
|
1178
|
+
onChange;
|
|
1179
|
+
enabled = false;
|
|
1180
|
+
drag = null;
|
|
1181
|
+
resize = null;
|
|
1182
|
+
cleanups = [];
|
|
1183
|
+
refresh(schema) {
|
|
1184
|
+
this.schema = cloneLayoutSchema(schema);
|
|
1185
|
+
for (const w of this.schema.widgets) {
|
|
1186
|
+
const cell = this.cells.get(w.id);
|
|
1187
|
+
if (cell) applyPlacementStyle(cell, w);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
enable(on) {
|
|
1191
|
+
if (this.enabled === on) return;
|
|
1192
|
+
this.enabled = on;
|
|
1193
|
+
if (on) this.bind();
|
|
1194
|
+
else this.unbind();
|
|
1195
|
+
}
|
|
1196
|
+
destroy() {
|
|
1197
|
+
this.enable(false);
|
|
1198
|
+
}
|
|
1199
|
+
bind() {
|
|
1200
|
+
for (const [id, cell] of this.cells) {
|
|
1201
|
+
const handle = document.createElement("div");
|
|
1202
|
+
handle.className = "tv-layout-drag-handle";
|
|
1203
|
+
handle.title = `Drag ${id}`;
|
|
1204
|
+
handle.style.cssText = "position:absolute;top:2px;left:2px;z-index:20;padding:2px 6px;font-size:10px;background:#388bfd;color:#fff;border-radius:3px;cursor:grab;user-select:none;opacity:0.85;";
|
|
1205
|
+
handle.textContent = id;
|
|
1206
|
+
const resize = document.createElement("div");
|
|
1207
|
+
resize.className = "tv-layout-resize-handle";
|
|
1208
|
+
resize.title = "Resize";
|
|
1209
|
+
resize.style.cssText = "position:absolute;right:2px;bottom:2px;width:12px;height:12px;z-index:20;background:#388bfd;border-radius:2px;cursor:nwse-resize;opacity:0.85;";
|
|
1210
|
+
cell.style.outline = "1px dashed #388bfd";
|
|
1211
|
+
cell.appendChild(handle);
|
|
1212
|
+
cell.appendChild(resize);
|
|
1213
|
+
const onDragDown = (ev) => {
|
|
1214
|
+
ev.preventDefault();
|
|
1215
|
+
const p = getPlacement(this.schema, id);
|
|
1216
|
+
if (!p) return;
|
|
1217
|
+
this.drag = { id, startX: ev.clientX, startY: ev.clientY, origin: { ...p } };
|
|
1218
|
+
handle.setPointerCapture(ev.pointerId);
|
|
1219
|
+
};
|
|
1220
|
+
const onDragMove = (ev) => {
|
|
1221
|
+
if (!this.drag || this.drag.id !== id) return;
|
|
1222
|
+
const delta = pixelDeltaToGrid(this.grid, this.schema, ev.clientX - this.drag.startX, ev.clientY - this.drag.startY);
|
|
1223
|
+
const next = clampPlacement2(
|
|
1224
|
+
{
|
|
1225
|
+
...this.drag.origin,
|
|
1226
|
+
col: this.drag.origin.col + delta.dCol,
|
|
1227
|
+
row: this.drag.origin.row + delta.dRow
|
|
1228
|
+
},
|
|
1229
|
+
this.schema
|
|
1230
|
+
);
|
|
1231
|
+
updatePlacement(this.schema, next);
|
|
1232
|
+
applyPlacementStyle(cell, next);
|
|
1233
|
+
};
|
|
1234
|
+
const onDragUp = (ev) => {
|
|
1235
|
+
if (!this.drag || this.drag.id !== id) return;
|
|
1236
|
+
this.drag = null;
|
|
1237
|
+
handle.releasePointerCapture(ev.pointerId);
|
|
1238
|
+
this.onChange(cloneLayoutSchema(this.schema));
|
|
1239
|
+
};
|
|
1240
|
+
const onResizeDown = (ev) => {
|
|
1241
|
+
ev.preventDefault();
|
|
1242
|
+
ev.stopPropagation();
|
|
1243
|
+
const p = getPlacement(this.schema, id);
|
|
1244
|
+
if (!p) return;
|
|
1245
|
+
this.resize = { id, startX: ev.clientX, startY: ev.clientY, origin: { ...p } };
|
|
1246
|
+
resize.setPointerCapture(ev.pointerId);
|
|
1247
|
+
};
|
|
1248
|
+
const onResizeMove = (ev) => {
|
|
1249
|
+
if (!this.resize || this.resize.id !== id) return;
|
|
1250
|
+
const delta = pixelDeltaToGrid(this.grid, this.schema, ev.clientX - this.resize.startX, ev.clientY - this.resize.startY);
|
|
1251
|
+
const next = clampPlacement2(
|
|
1252
|
+
{
|
|
1253
|
+
...this.resize.origin,
|
|
1254
|
+
colSpan: this.resize.origin.colSpan + delta.dCol,
|
|
1255
|
+
rowSpan: this.resize.origin.rowSpan + delta.dRow
|
|
1256
|
+
},
|
|
1257
|
+
this.schema
|
|
1258
|
+
);
|
|
1259
|
+
updatePlacement(this.schema, next);
|
|
1260
|
+
applyPlacementStyle(cell, next);
|
|
1261
|
+
};
|
|
1262
|
+
const onResizeUp = (ev) => {
|
|
1263
|
+
if (!this.resize || this.resize.id !== id) return;
|
|
1264
|
+
this.resize = null;
|
|
1265
|
+
resize.releasePointerCapture(ev.pointerId);
|
|
1266
|
+
this.onChange(cloneLayoutSchema(this.schema));
|
|
1267
|
+
};
|
|
1268
|
+
handle.addEventListener("pointerdown", onDragDown);
|
|
1269
|
+
handle.addEventListener("pointermove", onDragMove);
|
|
1270
|
+
handle.addEventListener("pointerup", onDragUp);
|
|
1271
|
+
resize.addEventListener("pointerdown", onResizeDown);
|
|
1272
|
+
resize.addEventListener("pointermove", onResizeMove);
|
|
1273
|
+
resize.addEventListener("pointerup", onResizeUp);
|
|
1274
|
+
this.cleanups.push(() => {
|
|
1275
|
+
handle.remove();
|
|
1276
|
+
resize.remove();
|
|
1277
|
+
cell.style.outline = "";
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
unbind() {
|
|
1282
|
+
for (const fn of this.cleanups) fn();
|
|
1283
|
+
this.cleanups.length = 0;
|
|
1284
|
+
this.drag = null;
|
|
1285
|
+
this.resize = null;
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
function getPlacement(schema, id) {
|
|
1289
|
+
return schema.widgets.find((w) => w.id === id);
|
|
1290
|
+
}
|
|
1291
|
+
function updatePlacement(schema, p) {
|
|
1292
|
+
const i = schema.widgets.findIndex((w) => w.id === p.id);
|
|
1293
|
+
if (i >= 0) schema.widgets[i] = p;
|
|
1294
|
+
}
|
|
1295
|
+
function clampPlacement2(p, schema) {
|
|
1296
|
+
const colSpan = Math.max(1, Math.min(schema.columns - p.col, p.colSpan));
|
|
1297
|
+
const rowSpan = Math.max(1, Math.min(schema.rows - p.row, p.rowSpan));
|
|
1298
|
+
const col = Math.max(0, Math.min(schema.columns - colSpan, p.col));
|
|
1299
|
+
const row = Math.max(0, Math.min(schema.rows - rowSpan, p.row));
|
|
1300
|
+
return { ...p, col, row, colSpan, rowSpan };
|
|
1301
|
+
}
|
|
1302
|
+
function pixelDeltaToGrid(grid, schema, dx, dy) {
|
|
1303
|
+
const rect = grid.getBoundingClientRect();
|
|
1304
|
+
const cellW = rect.width / schema.columns;
|
|
1305
|
+
const cellH = rect.height / schema.rows;
|
|
1306
|
+
return {
|
|
1307
|
+
dCol: cellW > 0 ? Math.round(dx / cellW) : 0,
|
|
1308
|
+
dRow: cellH > 0 ? Math.round(dy / cellH) : 0
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/layer/compositor-shell.ts
|
|
1313
|
+
function createCompositorShell(opts) {
|
|
1314
|
+
const cells = /* @__PURE__ */ new Map();
|
|
1315
|
+
const root = document.createElement("div");
|
|
1316
|
+
root.className = "tv-layout-root tv-layout-root--compositor";
|
|
1317
|
+
root.style.cssText = "display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;width:100%;height:100%;box-sizing:border-box;position:relative;";
|
|
1318
|
+
const grid = document.createElement("div");
|
|
1319
|
+
grid.className = "tv-compositor-shell-grid";
|
|
1320
|
+
grid.style.cssText = "position:absolute;inset:0;overflow:hidden;visibility:hidden;pointer-events:none;z-index:0;";
|
|
1321
|
+
root.appendChild(grid);
|
|
1322
|
+
const widgetIds = [
|
|
1323
|
+
"topBar",
|
|
1324
|
+
"leftToolbar",
|
|
1325
|
+
"bottomToolbar",
|
|
1326
|
+
"chartHost",
|
|
1327
|
+
"indicatorHost",
|
|
1328
|
+
"statusBar",
|
|
1329
|
+
"propertiesPanel"
|
|
1330
|
+
];
|
|
1331
|
+
for (const id of widgetIds) {
|
|
1332
|
+
const cell = document.createElement("div");
|
|
1333
|
+
cell.className = `tv-layout-cell tv-layout-cell--${id}`;
|
|
1334
|
+
cell.dataset.widgetId = id;
|
|
1335
|
+
cell.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;box-sizing:border-box;";
|
|
1336
|
+
const el = opts.widgets[id];
|
|
1337
|
+
if (el) {
|
|
1338
|
+
el.style.width = "100%";
|
|
1339
|
+
el.style.height = "100%";
|
|
1340
|
+
el.style.minWidth = "0";
|
|
1341
|
+
el.style.minHeight = "0";
|
|
1342
|
+
if (id === "chartHost") el.style.position = "relative";
|
|
1343
|
+
cell.appendChild(el);
|
|
1344
|
+
}
|
|
1345
|
+
cells.set(id, cell);
|
|
1346
|
+
grid.appendChild(cell);
|
|
1347
|
+
}
|
|
1348
|
+
return { root, grid, cells };
|
|
1349
|
+
}
|
|
1350
|
+
function applyCompositorShellFeatures(_cells, _features) {
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/layer/compositor-shell-sync.ts
|
|
1354
|
+
var SHELL_FEATURE_KEYS = [
|
|
1355
|
+
"showTopBar",
|
|
1356
|
+
"showLeftToolbar",
|
|
1357
|
+
"showBottomToolbar",
|
|
1358
|
+
"showStatusBar",
|
|
1359
|
+
"showPropertiesPanel",
|
|
1360
|
+
"showCrosshairLegend"
|
|
1361
|
+
];
|
|
1362
|
+
function shellLayerMatchesFeature(layer, feature) {
|
|
1363
|
+
switch (feature) {
|
|
1364
|
+
case "showTopBar":
|
|
1365
|
+
return layer.type === "shell.topBar" || layer.widgetKey === "topBar";
|
|
1366
|
+
case "showLeftToolbar":
|
|
1367
|
+
return layer.type === "shell.leftToolbar" || layer.widgetKey === "leftToolbar";
|
|
1368
|
+
case "showBottomToolbar":
|
|
1369
|
+
return layer.type === "shell.bottomToolbar" || layer.widgetKey === "bottomToolbar";
|
|
1370
|
+
case "showStatusBar":
|
|
1371
|
+
return layer.type === "shell.statusBar" || layer.widgetKey === "statusBar";
|
|
1372
|
+
case "showPropertiesPanel":
|
|
1373
|
+
return layer.type === "shell.propertiesPanel" || layer.widgetKey === "propertiesPanel";
|
|
1374
|
+
case "showCrosshairLegend":
|
|
1375
|
+
return layer.type === "overlay.crosshairLegend" || layer.widgetKey === "crosshairLegend";
|
|
1376
|
+
default:
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function syncCompositorShellVisibilityFromFeatures(controller, features) {
|
|
1381
|
+
const pageLayers = controller.getLayersForActivePage();
|
|
1382
|
+
for (const feature of SHELL_FEATURE_KEYS) {
|
|
1383
|
+
const visible = features[feature];
|
|
1384
|
+
for (const layer of pageLayers) {
|
|
1385
|
+
if (shellLayerMatchesFeature(layer, feature)) {
|
|
1386
|
+
controller.setLayerVisible(layer.id, visible);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
582
1392
|
// src/layout-features.ts
|
|
583
1393
|
var DEFAULT_LAYOUT_FEATURES = {
|
|
584
1394
|
showTopBar: false,
|
|
@@ -639,7 +1449,7 @@ function createDemoLayoutOptions(partial = {}) {
|
|
|
639
1449
|
}
|
|
640
1450
|
|
|
641
1451
|
// src/status-bar.ts
|
|
642
|
-
import { getLocale, setLocale, t as
|
|
1452
|
+
import { getLocale as getLocale2, setLocale as setLocale2, t as t5 } from "@coderyo/i18n";
|
|
643
1453
|
function fmt(n, digits = 2) {
|
|
644
1454
|
if (n == null || Number.isNaN(n)) return "\u2014";
|
|
645
1455
|
return n.toLocaleString(void 0, { maximumFractionDigits: digits });
|
|
@@ -656,7 +1466,7 @@ function mountStatusBar(parent, opts = {}) {
|
|
|
656
1466
|
const localeWrap = document.createElement("label");
|
|
657
1467
|
localeWrap.style.cssText = "display:flex;align-items:center;gap:4px;margin-left:auto;";
|
|
658
1468
|
const localeLabel = document.createElement("span");
|
|
659
|
-
localeLabel.textContent =
|
|
1469
|
+
localeLabel.textContent = t5("status.locale", "\u8A9E\u7CFB");
|
|
660
1470
|
const localeSelect = document.createElement("select");
|
|
661
1471
|
localeSelect.style.cssText = "background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:4px;font-size:11px;padding:2px 4px;";
|
|
662
1472
|
for (const loc of ["zh-TW", "en"]) {
|
|
@@ -665,11 +1475,11 @@ function mountStatusBar(parent, opts = {}) {
|
|
|
665
1475
|
opt.textContent = loc;
|
|
666
1476
|
localeSelect.appendChild(opt);
|
|
667
1477
|
}
|
|
668
|
-
localeSelect.value = opts.locale ??
|
|
1478
|
+
localeSelect.value = opts.locale ?? getLocale2();
|
|
669
1479
|
localeSelect.onchange = () => {
|
|
670
|
-
|
|
1480
|
+
setLocale2(localeSelect.value);
|
|
671
1481
|
opts.onLocaleChange?.(localeSelect.value);
|
|
672
|
-
localeLabel.textContent =
|
|
1482
|
+
localeLabel.textContent = t5("status.locale", "\u8A9E\u7CFB");
|
|
673
1483
|
render(opts);
|
|
674
1484
|
};
|
|
675
1485
|
localeWrap.append(localeLabel, localeSelect);
|
|
@@ -677,14 +1487,14 @@ function mountStatusBar(parent, opts = {}) {
|
|
|
677
1487
|
parent.appendChild(bar);
|
|
678
1488
|
const render = (state) => {
|
|
679
1489
|
const merged = { ...opts, ...state };
|
|
680
|
-
conn.textContent = `${
|
|
1490
|
+
conn.textContent = `${t5("status.connection", "\u9023\u7DDA")}\uFF1A${merged.connection ?? "\u2014"}`;
|
|
681
1491
|
const parts = [merged.symbol, merged.interval].filter(Boolean);
|
|
682
1492
|
sym.textContent = parts.length ? parts.join(" \xB7 ") : "";
|
|
683
1493
|
const o = merged.ohlcv;
|
|
684
1494
|
if (o && (o.o != null || o.c != null)) {
|
|
685
1495
|
ohlcv.textContent = `O ${fmt(o.o)} H ${fmt(o.h)} L ${fmt(o.l)} C ${fmt(o.c)} V ${fmt(o.v, 0)}`;
|
|
686
1496
|
} else {
|
|
687
|
-
ohlcv.textContent =
|
|
1497
|
+
ohlcv.textContent = t5("status.ohlcvHint", "\u79FB\u52D5\u5341\u5B57\u7DDA\u67E5\u770B OHLCV");
|
|
688
1498
|
}
|
|
689
1499
|
};
|
|
690
1500
|
render(opts);
|
|
@@ -698,7 +1508,7 @@ function mountStatusBar(parent, opts = {}) {
|
|
|
698
1508
|
}
|
|
699
1509
|
|
|
700
1510
|
// src/shortcuts-modal.ts
|
|
701
|
-
import { t as
|
|
1511
|
+
import { t as t6 } from "@coderyo/i18n";
|
|
702
1512
|
var SHORTCUTS = [
|
|
703
1513
|
{ key: "\u2196 / Esc", desc: "\u6E38\u6A19\uFF08\u9078\u53D6/\u7DE8\u8F2F\u7E6A\u5716\uFF09" },
|
|
704
1514
|
{ key: "Delete", desc: "\u522A\u9664\u9078\u4E2D\u7E6A\u5716" },
|
|
@@ -727,7 +1537,7 @@ function openShortcutsModal() {
|
|
|
727
1537
|
const box = document.createElement("div");
|
|
728
1538
|
box.style.cssText = "min-width:320px;max-width:90vw;padding:16px 20px;background:#161b22;border:1px solid #30363d;border-radius:8px;color:#e6edf3;";
|
|
729
1539
|
const h = document.createElement("h2");
|
|
730
|
-
h.textContent =
|
|
1540
|
+
h.textContent = t6("shortcuts.title", "\u5FEB\u6377\u9375");
|
|
731
1541
|
h.style.cssText = "font-size:16px;margin:0 0 12px;";
|
|
732
1542
|
const list = document.createElement("div");
|
|
733
1543
|
list.style.cssText = "font-size:13px;line-height:1.8;";
|
|
@@ -738,7 +1548,7 @@ function openShortcutsModal() {
|
|
|
738
1548
|
}
|
|
739
1549
|
const closeBtn = document.createElement("button");
|
|
740
1550
|
closeBtn.type = "button";
|
|
741
|
-
closeBtn.textContent =
|
|
1551
|
+
closeBtn.textContent = t6("shortcuts.close", "\u95DC\u9589");
|
|
742
1552
|
closeBtn.style.cssText = "margin-top:14px;padding:6px 14px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;";
|
|
743
1553
|
const close = () => backdrop.remove();
|
|
744
1554
|
closeBtn.onclick = close;
|
|
@@ -762,7 +1572,7 @@ var DRAWING_TOOLS = [
|
|
|
762
1572
|
];
|
|
763
1573
|
var MOBILE_MQ = "(max-width: 768px)";
|
|
764
1574
|
function mountToolButtons(parent, activeTool, onSelect, layout) {
|
|
765
|
-
const btnStyle = layout === "column" ? "width:
|
|
1575
|
+
const btnStyle = layout === "column" ? "width:32px;height:28px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:13px;padding:0;" : "min-width:36px;height:32px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:13px;padding:0 6px;";
|
|
766
1576
|
const activeStyle = btnStyle.replace("#21262d", "#388bfd").replace("#e6edf3", "#fff");
|
|
767
1577
|
const buttons = /* @__PURE__ */ new Map();
|
|
768
1578
|
for (const tool of DRAWING_TOOLS) {
|
|
@@ -785,17 +1595,26 @@ function mountToolButtons(parent, activeTool, onSelect, layout) {
|
|
|
785
1595
|
}
|
|
786
1596
|
function mountChartLayout(root, opts = {}) {
|
|
787
1597
|
let layoutFeatures = resolveLayoutFeatures(opts);
|
|
1598
|
+
const layoutId = opts.layoutId ?? "default";
|
|
1599
|
+
root.replaceChildren();
|
|
788
1600
|
root.style.display = "flex";
|
|
789
1601
|
root.style.flexDirection = "column";
|
|
790
|
-
root.style.height = "100%";
|
|
791
|
-
root.style.width = "100%";
|
|
1602
|
+
root.style.height = root.style.height || "100%";
|
|
1603
|
+
root.style.width = root.style.width || "100%";
|
|
792
1604
|
root.style.minWidth = "0";
|
|
793
1605
|
root.style.boxSizing = "border-box";
|
|
794
|
-
root.style.overflow = "
|
|
795
|
-
|
|
1606
|
+
root.style.overflow = "hidden";
|
|
1607
|
+
const useAutoTheme = opts.autoThemeProvider !== false;
|
|
1608
|
+
const useAutoI18n = opts.autoI18n !== false;
|
|
1609
|
+
const themeProvider = opts.themeProvider ?? (useAutoTheme ? createThemeProvider() : void 0);
|
|
1610
|
+
const i18nProvider = opts.i18n ?? (useAutoI18n ? createI18nProvider() : void 0);
|
|
1611
|
+
themeProvider?.subscribe((theme) => {
|
|
1612
|
+
root.style.background = theme === "dark" ? "#0d1117" : "#f6f8fa";
|
|
1613
|
+
});
|
|
796
1614
|
let activeTool = opts.activeDrawingTool ?? "cursor";
|
|
797
1615
|
let setActiveDesktop = null;
|
|
798
1616
|
let setActiveMobile = null;
|
|
1617
|
+
let lastSelectedDrawing = null;
|
|
799
1618
|
const setActiveDrawingTool = (tool) => {
|
|
800
1619
|
activeTool = tool;
|
|
801
1620
|
setActiveDesktop?.(tool);
|
|
@@ -805,89 +1624,225 @@ function mountChartLayout(root, opts = {}) {
|
|
|
805
1624
|
setActiveDrawingTool(tool);
|
|
806
1625
|
opts.onDrawingToolSelect?.(tool);
|
|
807
1626
|
};
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
|
|
1627
|
+
const topBarHost = document.createElement("div");
|
|
1628
|
+
topBarHost.className = "tv-layout-header";
|
|
1629
|
+
topBarHost.style.cssText = "width:100%;min-width:0;overflow:visible;position:relative;z-index:30;";
|
|
1630
|
+
const leftToolbar = document.createElement("aside");
|
|
1631
|
+
leftToolbar.style.cssText = "width:100%;height:100%;max-width:44px;border-right:1px solid #30363d;background:#161b22;display:flex;flex-direction:column;align-items:center;padding:4px 2px;gap:4px;box-sizing:border-box;overflow:hidden;";
|
|
1632
|
+
const bottomToolbar = document.createElement("div");
|
|
1633
|
+
bottomToolbar.style.cssText = "width:100%;height:100%;gap:6px;padding:6px 8px;border-top:1px solid #30363d;background:#161b22;overflow-x:auto;box-sizing:border-box;display:flex;flex-direction:row;";
|
|
1634
|
+
const chartMain = document.createElement("div");
|
|
1635
|
+
chartMain.className = "tv-chart-main-mount";
|
|
1636
|
+
chartMain.style.cssText = "width:100%;height:100%;min-height:120px;position:relative;overflow:hidden;box-sizing:border-box;";
|
|
1637
|
+
const chartVolume = document.createElement("div");
|
|
1638
|
+
chartVolume.className = "tv-chart-volume-mount";
|
|
1639
|
+
chartVolume.style.cssText = "width:100%;height:100%;min-height:48px;position:relative;overflow:hidden;box-sizing:border-box;";
|
|
820
1640
|
const chartHost = document.createElement("div");
|
|
821
|
-
chartHost.
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1641
|
+
chartHost.className = "tv-chart-grid-anchor";
|
|
1642
|
+
chartHost.style.cssText = "width:100%;height:100%;min-height:0;position:relative;overflow:hidden;box-sizing:border-box;";
|
|
1643
|
+
const indicatorMount = document.createElement("div");
|
|
1644
|
+
indicatorMount.style.cssText = "width:100%;height:100%;min-height:0;display:flex;flex-direction:column;";
|
|
1645
|
+
const indicatorHost = mountIndicatorPaneHost(indicatorMount);
|
|
1646
|
+
indicatorHost.style.flex = "1";
|
|
1647
|
+
const statusMount = document.createElement("div");
|
|
1648
|
+
statusMount.style.cssText = "width:100%;height:100%;";
|
|
1649
|
+
const statusBar = mountStatusBar(statusMount, opts.statusBar ?? {});
|
|
1650
|
+
const propertiesMount = document.createElement("div");
|
|
1651
|
+
propertiesMount.style.cssText = "width:100%;height:100%;min-height:0;";
|
|
1652
|
+
const propertiesPanel = mountDrawingPropertiesPanel(propertiesMount, {
|
|
827
1653
|
onStyleChange: opts.onDrawingStyleChange
|
|
828
1654
|
});
|
|
829
|
-
opts.
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1655
|
+
const layerCompositorManaged = opts.layerCompositorManaged === true;
|
|
1656
|
+
const drawingOverlay = chartMain;
|
|
1657
|
+
let loaded = null;
|
|
1658
|
+
let layoutSchema = resolveLayoutSchema(null);
|
|
1659
|
+
if (!layerCompositorManaged) {
|
|
1660
|
+
loaded = opts.layout !== void 0 && opts.layout !== null ? resolveLayoutSchema(opts.layout) : opts.layoutPersist ? loadLayoutSchema(layoutId) : null;
|
|
1661
|
+
layoutSchema = resolveLayoutSchema(loaded);
|
|
1662
|
+
}
|
|
1663
|
+
const persistLayout = (schema) => {
|
|
1664
|
+
if (layerCompositorManaged) return;
|
|
1665
|
+
layoutSchema = resolveLayoutSchema(schema);
|
|
1666
|
+
if (opts.layoutPersist) saveLayoutSchema(layoutId, layoutSchema);
|
|
1667
|
+
opts.onLayoutChange?.(layoutSchema);
|
|
1668
|
+
};
|
|
1669
|
+
let layoutShell;
|
|
1670
|
+
if (layerCompositorManaged) {
|
|
1671
|
+
layoutShell = createCompositorShell({
|
|
1672
|
+
widgets: {
|
|
1673
|
+
topBar: topBarHost,
|
|
1674
|
+
leftToolbar,
|
|
1675
|
+
bottomToolbar,
|
|
1676
|
+
chartHost,
|
|
1677
|
+
indicatorHost: indicatorMount,
|
|
1678
|
+
statusBar: statusMount,
|
|
1679
|
+
propertiesPanel: propertiesMount
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
} else {
|
|
1683
|
+
layoutShell = createLayoutGrid({
|
|
1684
|
+
schema: layoutSchema,
|
|
1685
|
+
features: layoutFeatures,
|
|
1686
|
+
editor: opts.layoutEditor ?? false,
|
|
1687
|
+
onSchemaChange: persistLayout,
|
|
1688
|
+
widgets: {
|
|
1689
|
+
topBar: topBarHost,
|
|
1690
|
+
leftToolbar,
|
|
1691
|
+
bottomToolbar,
|
|
1692
|
+
chartHost,
|
|
1693
|
+
indicatorHost: indicatorMount,
|
|
1694
|
+
statusBar: statusMount,
|
|
1695
|
+
propertiesPanel: propertiesMount
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
root.appendChild(layoutShell.root);
|
|
1700
|
+
let topBar = topBarHost;
|
|
834
1701
|
let setActiveInterval = () => {
|
|
835
1702
|
};
|
|
836
|
-
const crosshairLegend = mountCrosshairLegend(
|
|
1703
|
+
const crosshairLegend = mountCrosshairLegend(
|
|
1704
|
+
layerCompositorManaged ? void 0 : chartMain,
|
|
1705
|
+
{ symbol: opts.initialSymbol }
|
|
1706
|
+
);
|
|
837
1707
|
let detachContextMenu = () => {
|
|
838
1708
|
};
|
|
839
1709
|
let shortcutsBound = false;
|
|
1710
|
+
let disposeMobileMq = null;
|
|
1711
|
+
const mountToolbarActions = (parent, compact) => {
|
|
1712
|
+
const settings = opts.settings;
|
|
1713
|
+
if (!settings?.onClearAllDrawings && !settings?.onClearAllIndicators) return;
|
|
1714
|
+
const spacer = document.createElement("div");
|
|
1715
|
+
spacer.style.flex = compact ? "1" : "0";
|
|
1716
|
+
spacer.style.minHeight = compact ? "4px" : "0";
|
|
1717
|
+
parent.appendChild(spacer);
|
|
1718
|
+
const mk = (label, fn) => {
|
|
1719
|
+
const b = document.createElement("button");
|
|
1720
|
+
b.type = "button";
|
|
1721
|
+
b.title = label;
|
|
1722
|
+
b.textContent = compact ? label === t7("toolbar.clearDrawings", "\u6E05\u9664\u7E6A\u5716") ? "\u2327" : "\u2205" : label;
|
|
1723
|
+
b.style.cssText = compact ? "width:32px;height:24px;font-size:10px;background:#21262d;color:#f85149;border:1px solid #30363d;border-radius:4px;cursor:pointer;" : "min-width:36px;height:28px;font-size:10px;background:#21262d;color:#f85149;border:1px solid #30363d;border-radius:4px;cursor:pointer;padding:0 4px;";
|
|
1724
|
+
b.onclick = () => fn?.();
|
|
1725
|
+
return b;
|
|
1726
|
+
};
|
|
1727
|
+
if (settings.onClearAllDrawings) {
|
|
1728
|
+
parent.appendChild(mk(t7("toolbar.clearDrawings", "\u6E05\u9664\u7E6A\u5716"), settings.onClearAllDrawings));
|
|
1729
|
+
}
|
|
1730
|
+
if (settings.onClearAllIndicators) {
|
|
1731
|
+
parent.appendChild(mk(t7("toolbar.clearIndicators", "\u6E05\u9664\u6307\u6A19"), settings.onClearAllIndicators));
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
840
1734
|
const mountLeftToolbar = () => {
|
|
841
|
-
|
|
842
|
-
const desktopTools = mountToolButtons(
|
|
1735
|
+
leftToolbar.replaceChildren();
|
|
1736
|
+
const desktopTools = mountToolButtons(leftToolbar, activeTool, onToolSelect, "column");
|
|
843
1737
|
setActiveDesktop = desktopTools.setActive;
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const mobileTools = mountToolButtons(
|
|
1738
|
+
mountToolbarActions(leftToolbar, true);
|
|
1739
|
+
bottomToolbar.replaceChildren();
|
|
1740
|
+
const mobileTools = mountToolButtons(bottomToolbar, activeTool, onToolSelect, "row");
|
|
847
1741
|
setActiveMobile = mobileTools.setActive;
|
|
848
|
-
|
|
849
|
-
const applyLayout = () => {
|
|
850
|
-
const mobile = mq.matches;
|
|
851
|
-
leftAside.style.display = mobile ? "none" : "flex";
|
|
852
|
-
const showBottom = layoutFeatures.showBottomToolbar !== false;
|
|
853
|
-
bottomBar.style.display = mobile && showBottom ? "flex" : "none";
|
|
854
|
-
};
|
|
855
|
-
mq.addEventListener("change", applyLayout);
|
|
856
|
-
applyLayout();
|
|
1742
|
+
mountToolbarActions(bottomToolbar, false);
|
|
857
1743
|
};
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1744
|
+
const chartAreaColStart = () => layoutFeatures.showLeftToolbar ? 2 : 1;
|
|
1745
|
+
const applyPropertiesPanelLayout = (drawing) => {
|
|
1746
|
+
const showPanel = layoutFeatures.showPropertiesPanel && drawing != null;
|
|
1747
|
+
propertiesPanel.el.style.display = showPanel ? "block" : "none";
|
|
1748
|
+
if (layerCompositorManaged) return;
|
|
1749
|
+
const propsCell = layoutShell.cells.get("propertiesPanel");
|
|
1750
|
+
const chartCell = layoutShell.cells.get("chartHost");
|
|
1751
|
+
const indCell = layoutShell.cells.get("indicatorHost");
|
|
1752
|
+
const statusCell = layoutShell.cells.get("statusBar");
|
|
1753
|
+
if (!propsCell || !chartCell) return;
|
|
1754
|
+
const cw = getWidgetPlacement(layoutSchema, "chartHost");
|
|
1755
|
+
const iw = getWidgetPlacement(layoutSchema, "indicatorHost");
|
|
1756
|
+
const sw = getWidgetPlacement(layoutSchema, "statusBar");
|
|
1757
|
+
const pw = getWidgetPlacement(layoutSchema, "propertiesPanel");
|
|
1758
|
+
const applyPlacement = (cell, p) => {
|
|
1759
|
+
cell.style.gridColumn = `${p.col + 1} / span ${p.colSpan}`;
|
|
1760
|
+
cell.style.gridRow = `${p.row + 1} / span ${p.rowSpan}`;
|
|
1761
|
+
};
|
|
1762
|
+
if (showPanel) {
|
|
1763
|
+
propsCell.style.display = "";
|
|
1764
|
+
if (pw) applyPlacement(propsCell, pw);
|
|
1765
|
+
if (cw) applyPlacement(chartCell, cw);
|
|
1766
|
+
if (indCell && iw) {
|
|
1767
|
+
indCell.style.display = "";
|
|
1768
|
+
applyPlacement(indCell, iw);
|
|
1769
|
+
}
|
|
1770
|
+
if (statusCell && sw) applyPlacement(statusCell, sw);
|
|
1771
|
+
} else {
|
|
1772
|
+
propsCell.style.display = "none";
|
|
1773
|
+
const end = layoutSchema.columns + 1;
|
|
1774
|
+
const start = chartAreaColStart();
|
|
1775
|
+
chartCell.style.gridColumn = `${start} / ${end}`;
|
|
1776
|
+
if (cw) chartCell.style.gridRow = `${cw.row + 1} / span ${cw.rowSpan}`;
|
|
1777
|
+
if (indCell && iw) {
|
|
1778
|
+
indCell.style.display = "";
|
|
1779
|
+
indCell.style.gridColumn = `${start} / ${end}`;
|
|
1780
|
+
indCell.style.gridRow = `${iw.row + 1} / span ${iw.rowSpan}`;
|
|
1781
|
+
}
|
|
1782
|
+
if (statusCell && sw) {
|
|
1783
|
+
statusCell.style.gridColumn = `${start} / ${end}`;
|
|
1784
|
+
statusCell.style.gridRow = `${sw.row + 1} / span ${sw.rowSpan}`;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
863
1787
|
};
|
|
864
1788
|
const applyLayoutFeatures = () => {
|
|
865
1789
|
const f = layoutFeatures;
|
|
866
1790
|
if (f.showTopBar) {
|
|
867
|
-
|
|
868
|
-
headerSlot.style.display = "";
|
|
1791
|
+
topBarHost.replaceChildren();
|
|
869
1792
|
const mounted = mountTopBar(
|
|
870
|
-
|
|
1793
|
+
topBarHost,
|
|
871
1794
|
Object.assign(opts, {
|
|
872
1795
|
symbolInput: f.symbolInput,
|
|
873
|
-
showSettings: f.showSettings
|
|
1796
|
+
showSettings: f.showSettings,
|
|
1797
|
+
themeProvider: opts.themeProvider ?? themeProvider,
|
|
1798
|
+
i18n: opts.i18n ?? i18nProvider,
|
|
1799
|
+
onThemeChange: (theme) => {
|
|
1800
|
+
opts.onThemeChange?.(theme);
|
|
1801
|
+
if (!opts.onThemeChange) opts.onThemeToggle?.();
|
|
1802
|
+
}
|
|
874
1803
|
})
|
|
875
1804
|
);
|
|
876
1805
|
topBar = mounted.el;
|
|
877
1806
|
setActiveInterval = mounted.setActiveInterval;
|
|
878
1807
|
} else {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1808
|
+
topBarHost.replaceChildren();
|
|
1809
|
+
topBar = topBarHost;
|
|
1810
|
+
}
|
|
1811
|
+
if (f.showLeftToolbar || f.showBottomToolbar) mountLeftToolbar();
|
|
1812
|
+
else {
|
|
1813
|
+
leftToolbar.replaceChildren();
|
|
1814
|
+
bottomToolbar.replaceChildren();
|
|
1815
|
+
setActiveDesktop = null;
|
|
1816
|
+
setActiveMobile = null;
|
|
1817
|
+
}
|
|
1818
|
+
if (!layerCompositorManaged) {
|
|
1819
|
+
layoutShell.applySchema(layoutSchema, f);
|
|
1820
|
+
}
|
|
1821
|
+
disposeMobileMq?.();
|
|
1822
|
+
const mq = window.matchMedia(MOBILE_MQ);
|
|
1823
|
+
const applyMobileTools = () => {
|
|
1824
|
+
const features = layoutFeatures;
|
|
1825
|
+
const mobile = mq.matches;
|
|
1826
|
+
if (features.showLeftToolbar) {
|
|
1827
|
+
leftToolbar.style.display = mobile ? "none" : "flex";
|
|
1828
|
+
}
|
|
1829
|
+
if (features.showBottomToolbar) {
|
|
1830
|
+
bottomToolbar.style.display = mobile ? "flex" : "none";
|
|
1831
|
+
const bottomCell = layoutShell.cells.get("bottomToolbar");
|
|
1832
|
+
if (bottomCell) {
|
|
1833
|
+
bottomCell.style.display = mobile && features.showBottomToolbar ? "" : "none";
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
mq.addEventListener("change", applyMobileTools);
|
|
1838
|
+
disposeMobileMq = () => mq.removeEventListener("change", applyMobileTools);
|
|
1839
|
+
applyMobileTools();
|
|
1840
|
+
if (!layerCompositorManaged) {
|
|
1841
|
+
crosshairLegend.el.style.display = f.showCrosshairLegend ? "" : "none";
|
|
1842
|
+
}
|
|
1843
|
+
detachContextMenu();
|
|
889
1844
|
if (f.showContextMenu) {
|
|
890
|
-
detachContextMenu = attachChartContextMenu(
|
|
1845
|
+
detachContextMenu = attachChartContextMenu(chartMain, {
|
|
891
1846
|
actions: opts.contextMenuActions
|
|
892
1847
|
});
|
|
893
1848
|
}
|
|
@@ -895,10 +1850,36 @@ function mountChartLayout(root, opts = {}) {
|
|
|
895
1850
|
bindShortcutsModal();
|
|
896
1851
|
shortcutsBound = true;
|
|
897
1852
|
}
|
|
1853
|
+
applyPropertiesPanelLayout(lastSelectedDrawing);
|
|
1854
|
+
};
|
|
1855
|
+
const handleDrawingSelection = (drawing) => {
|
|
1856
|
+
lastSelectedDrawing = drawing;
|
|
1857
|
+
propertiesPanel.bind(drawing);
|
|
1858
|
+
applyPropertiesPanelLayout(drawing);
|
|
898
1859
|
};
|
|
1860
|
+
opts.onDrawingSelectionBind?.(propertiesPanel.bind);
|
|
899
1861
|
applyLayoutFeatures();
|
|
1862
|
+
applyPropertiesPanelLayout(null);
|
|
1863
|
+
let boundCompositorController = null;
|
|
1864
|
+
const syncCompositorShellVisibility = layerCompositorManaged ? (controller) => {
|
|
1865
|
+
syncCompositorShellVisibilityFromFeatures(controller, layoutFeatures);
|
|
1866
|
+
} : void 0;
|
|
1867
|
+
const syncCompositorShellVisibilityIfBound = () => {
|
|
1868
|
+
if (boundCompositorController) {
|
|
1869
|
+
syncCompositorShellVisibilityFromFeatures(boundCompositorController, layoutFeatures);
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
const bindLayerCompositorController = layerCompositorManaged ? (controller) => {
|
|
1873
|
+
boundCompositorController = controller;
|
|
1874
|
+
syncCompositorShellVisibilityFromFeatures(controller, layoutFeatures);
|
|
1875
|
+
} : void 0;
|
|
900
1876
|
return {
|
|
901
|
-
|
|
1877
|
+
layoutRoot: layoutShell.root,
|
|
1878
|
+
layoutGrid: layoutShell.grid,
|
|
1879
|
+
chartHost: chartMain,
|
|
1880
|
+
chartMain,
|
|
1881
|
+
chartVolume,
|
|
1882
|
+
drawingOverlay,
|
|
902
1883
|
indicatorHost,
|
|
903
1884
|
topBar,
|
|
904
1885
|
setActiveInterval,
|
|
@@ -907,16 +1888,1880 @@ function mountChartLayout(root, opts = {}) {
|
|
|
907
1888
|
detachContextMenu,
|
|
908
1889
|
setActiveDrawingTool,
|
|
909
1890
|
propertiesPanel,
|
|
1891
|
+
handleDrawingSelection,
|
|
1892
|
+
syncCompositorShellVisibility,
|
|
1893
|
+
bindLayerCompositorController,
|
|
910
1894
|
setLayoutFeatures: (patch) => {
|
|
911
1895
|
layoutFeatures = mergeLayoutFeatures(layoutFeatures, patch);
|
|
912
1896
|
applyLayoutFeatures();
|
|
1897
|
+
syncCompositorShellVisibilityIfBound();
|
|
913
1898
|
},
|
|
914
|
-
getLayoutFeatures: () => ({ ...layoutFeatures })
|
|
1899
|
+
getLayoutFeatures: () => ({ ...layoutFeatures }),
|
|
1900
|
+
getLayoutSchema: () => layerCompositorManaged ? layoutSchema : layoutShell.getSchema(),
|
|
1901
|
+
setLayoutSchema: (schema) => {
|
|
1902
|
+
if (layerCompositorManaged) return;
|
|
1903
|
+
layoutSchema = resolveLayoutSchema(schema);
|
|
1904
|
+
const grid = layoutShell;
|
|
1905
|
+
grid.setSchema(layoutSchema);
|
|
1906
|
+
grid.applySchema(layoutSchema, layoutFeatures);
|
|
1907
|
+
persistLayout(layoutSchema);
|
|
1908
|
+
},
|
|
1909
|
+
enableLayoutEditor: (enabled) => {
|
|
1910
|
+
if (layerCompositorManaged) return;
|
|
1911
|
+
layoutShell.enableEditor(enabled);
|
|
1912
|
+
},
|
|
1913
|
+
saveLayout: () => {
|
|
1914
|
+
if (layerCompositorManaged) return;
|
|
1915
|
+
persistLayout(layoutShell.getSchema());
|
|
1916
|
+
}
|
|
915
1917
|
};
|
|
916
1918
|
}
|
|
917
1919
|
|
|
918
|
-
// src/
|
|
1920
|
+
// src/layer/types.ts
|
|
1921
|
+
var LAYER_PRESET_VERSION = 2;
|
|
1922
|
+
|
|
1923
|
+
// src/layer/overlay-mount.ts
|
|
1924
|
+
var OVERLAY_LAYER_TYPES = [
|
|
1925
|
+
"overlay.crosshairLegend",
|
|
1926
|
+
"overlay.drawing"
|
|
1927
|
+
];
|
|
1928
|
+
function isOverlayLayerType(type) {
|
|
1929
|
+
return OVERLAY_LAYER_TYPES.includes(type);
|
|
1930
|
+
}
|
|
1931
|
+
function syncOverlayLayersToMain(layers, pageId) {
|
|
1932
|
+
const main = layers.find((l) => l.type === "chart.main" && l.pageId === pageId);
|
|
1933
|
+
if (!main) return;
|
|
1934
|
+
const legend = layers.find(
|
|
1935
|
+
(l) => l.type === "overlay.crosshairLegend" && l.pageId === pageId
|
|
1936
|
+
);
|
|
1937
|
+
if (legend) {
|
|
1938
|
+
legend.frame = {
|
|
1939
|
+
x: main.frame.x,
|
|
1940
|
+
y: main.frame.y,
|
|
1941
|
+
w: Math.min(0.35, main.frame.w),
|
|
1942
|
+
h: Math.min(0.1, main.frame.h)
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
const drawing = layers.find((l) => l.type === "overlay.drawing" && l.pageId === pageId);
|
|
1946
|
+
if (drawing) {
|
|
1947
|
+
drawing.frame = {
|
|
1948
|
+
x: main.frame.x,
|
|
1949
|
+
y: main.frame.y,
|
|
1950
|
+
w: main.frame.w,
|
|
1951
|
+
h: main.frame.h
|
|
1952
|
+
};
|
|
1953
|
+
if (drawing.zIndex <= main.zIndex) {
|
|
1954
|
+
drawing.zIndex = main.zIndex + 2;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
function syncAllOverlayLayersToMain(layers) {
|
|
1959
|
+
const pageIds = [...new Set(layers.map((l) => l.pageId))];
|
|
1960
|
+
for (const pageId of pageIds) {
|
|
1961
|
+
syncOverlayLayersToMain(layers, pageId);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function getDrawingOverlayVisible(controller) {
|
|
1965
|
+
const drawing = controller.getLayersForActivePage().find((l) => l.type === "overlay.drawing");
|
|
1966
|
+
return drawing?.visible ?? true;
|
|
1967
|
+
}
|
|
1968
|
+
function ensureOverlayLayers(layers, pageId) {
|
|
1969
|
+
const main = layers.find((l) => l.type === "chart.main" && l.pageId === pageId);
|
|
1970
|
+
if (!main) return layers;
|
|
1971
|
+
const out = [...layers];
|
|
1972
|
+
if (!out.some((l) => l.type === "overlay.crosshairLegend" && l.pageId === pageId)) {
|
|
1973
|
+
out.push({
|
|
1974
|
+
id: `layer-crosshairLegend-${pageId}`,
|
|
1975
|
+
pageId,
|
|
1976
|
+
type: "overlay.crosshairLegend",
|
|
1977
|
+
widgetKey: "crosshairLegend",
|
|
1978
|
+
frame: { x: main.frame.x, y: main.frame.y, w: 0.2, h: 0.08 },
|
|
1979
|
+
zIndex: main.zIndex + 5,
|
|
1980
|
+
visible: true,
|
|
1981
|
+
locked: false
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
if (!out.some((l) => l.type === "overlay.drawing" && l.pageId === pageId)) {
|
|
1985
|
+
out.push({
|
|
1986
|
+
id: `layer-drawingOverlay-${pageId}`,
|
|
1987
|
+
pageId,
|
|
1988
|
+
type: "overlay.drawing",
|
|
1989
|
+
widgetKey: "drawingOverlay",
|
|
1990
|
+
frame: { ...main.frame },
|
|
1991
|
+
zIndex: main.zIndex + 3,
|
|
1992
|
+
visible: true,
|
|
1993
|
+
locked: false
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
syncOverlayLayersToMain(out, pageId);
|
|
1997
|
+
return out;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// src/layer/chart-pane-mount.ts
|
|
2001
|
+
var CHART_PANE_LAYER_TYPES = [
|
|
2002
|
+
"chart.main",
|
|
2003
|
+
"chart.volume",
|
|
2004
|
+
"chart.indicator",
|
|
2005
|
+
"chart.host",
|
|
2006
|
+
"chart.indicatorHost"
|
|
2007
|
+
];
|
|
2008
|
+
var DEFAULT_SYNC_TIME_SCALE_GROUP = "chart-timescale";
|
|
2009
|
+
function isChartPaneLayerType(type) {
|
|
2010
|
+
return CHART_PANE_LAYER_TYPES.includes(type);
|
|
2011
|
+
}
|
|
2012
|
+
function widgetKeyForChartPaneType(type) {
|
|
2013
|
+
switch (type) {
|
|
2014
|
+
case "chart.main":
|
|
2015
|
+
return "chartMain";
|
|
2016
|
+
case "chart.volume":
|
|
2017
|
+
return "chartVolume";
|
|
2018
|
+
case "chart.indicator":
|
|
2019
|
+
case "chart.indicatorHost":
|
|
2020
|
+
return "chartIndicator";
|
|
2021
|
+
case "chart.host":
|
|
2022
|
+
return "chartMain";
|
|
2023
|
+
default:
|
|
2024
|
+
return null;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
function paneIdFromWidgetKey(widgetKey) {
|
|
2028
|
+
switch (widgetKey) {
|
|
2029
|
+
case "chartMain":
|
|
2030
|
+
case "chartHost":
|
|
2031
|
+
return "main";
|
|
2032
|
+
case "chartVolume":
|
|
2033
|
+
return "volume";
|
|
2034
|
+
case "chartIndicator":
|
|
2035
|
+
case "indicatorHost":
|
|
2036
|
+
return "indicator";
|
|
2037
|
+
default:
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
function expandLegacyChartHostLayers(layers) {
|
|
2042
|
+
return splitLegacyChartHost(layers).layers;
|
|
2043
|
+
}
|
|
2044
|
+
function splitLegacyChartHost(layers) {
|
|
2045
|
+
const host = layers.find((l) => l.type === "chart.host");
|
|
2046
|
+
if (!host || layers.some((l) => l.type === "chart.main")) {
|
|
2047
|
+
return { layers, idRemap: /* @__PURE__ */ new Map() };
|
|
2048
|
+
}
|
|
2049
|
+
const mainRatio = 0.65;
|
|
2050
|
+
const main = {
|
|
2051
|
+
...host,
|
|
2052
|
+
id: `${host.id}-main`,
|
|
2053
|
+
type: "chart.main",
|
|
2054
|
+
widgetKey: "chartMain",
|
|
2055
|
+
frame: {
|
|
2056
|
+
x: host.frame.x,
|
|
2057
|
+
y: host.frame.y,
|
|
2058
|
+
w: host.frame.w,
|
|
2059
|
+
h: host.frame.h * mainRatio
|
|
2060
|
+
},
|
|
2061
|
+
syncTimeScaleGroupId: host.syncTimeScaleGroupId
|
|
2062
|
+
};
|
|
2063
|
+
const volume = {
|
|
2064
|
+
...host,
|
|
2065
|
+
id: `${host.id}-volume`,
|
|
2066
|
+
type: "chart.volume",
|
|
2067
|
+
widgetKey: "chartVolume",
|
|
2068
|
+
frame: {
|
|
2069
|
+
x: host.frame.x,
|
|
2070
|
+
y: host.frame.y + host.frame.h * mainRatio,
|
|
2071
|
+
w: host.frame.w,
|
|
2072
|
+
h: host.frame.h * (1 - mainRatio)
|
|
2073
|
+
},
|
|
2074
|
+
zIndex: host.zIndex + 1,
|
|
2075
|
+
syncTimeScaleGroupId: host.syncTimeScaleGroupId
|
|
2076
|
+
};
|
|
2077
|
+
const rest = layers.filter((l) => l.id !== host.id);
|
|
2078
|
+
const idRemap = /* @__PURE__ */ new Map([[host.id, [main.id, volume.id]]]);
|
|
2079
|
+
return { layers: [...rest, main, volume], idRemap };
|
|
2080
|
+
}
|
|
2081
|
+
function syncCrosshairLegendToMain(layers) {
|
|
2082
|
+
syncAllOverlayLayersToMain(layers);
|
|
2083
|
+
}
|
|
2084
|
+
function upgradeIndicatorHostType(layers) {
|
|
2085
|
+
return layers.map((l) => {
|
|
2086
|
+
if (l.type !== "chart.indicatorHost") return l;
|
|
2087
|
+
return {
|
|
2088
|
+
...l,
|
|
2089
|
+
type: "chart.indicator",
|
|
2090
|
+
widgetKey: l.widgetKey === "indicatorHost" ? "chartIndicator" : l.widgetKey,
|
|
2091
|
+
syncTimeScaleGroupId: l.syncTimeScaleGroupId
|
|
2092
|
+
};
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/layer/normalize.ts
|
|
2097
|
+
var SHELL_TYPES = [
|
|
2098
|
+
"shell.topBar",
|
|
2099
|
+
"shell.leftToolbar",
|
|
2100
|
+
"shell.bottomToolbar",
|
|
2101
|
+
"shell.statusBar",
|
|
2102
|
+
"shell.propertiesPanel"
|
|
2103
|
+
];
|
|
2104
|
+
var CHART_TYPES = [
|
|
2105
|
+
"chart.host",
|
|
2106
|
+
"chart.main",
|
|
2107
|
+
"chart.volume",
|
|
2108
|
+
"chart.indicator",
|
|
2109
|
+
"chart.indicatorHost"
|
|
2110
|
+
];
|
|
2111
|
+
var OVERLAY_TYPES = ["overlay.crosshairLegend", "overlay.drawing"];
|
|
2112
|
+
function isLayerType(t12) {
|
|
2113
|
+
return SHELL_TYPES.includes(t12) || CHART_TYPES.includes(t12) || OVERLAY_TYPES.includes(t12) || t12 === "group";
|
|
2114
|
+
}
|
|
2115
|
+
function clamp01(n) {
|
|
2116
|
+
if (!Number.isFinite(n)) return 0;
|
|
2117
|
+
return Math.min(1, Math.max(0, n));
|
|
2118
|
+
}
|
|
2119
|
+
function clampFrame(frame) {
|
|
2120
|
+
const w = Math.min(1, Math.max(0.02, clamp01(frame.w)));
|
|
2121
|
+
const h = Math.min(1, Math.max(0.02, clamp01(frame.h)));
|
|
2122
|
+
const x = clamp01(frame.x);
|
|
2123
|
+
const y = clamp01(frame.y);
|
|
2124
|
+
return {
|
|
2125
|
+
x: Math.min(x, 1 - w),
|
|
2126
|
+
y: Math.min(y, 1 - h),
|
|
2127
|
+
w,
|
|
2128
|
+
h
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
function normalizeLayoutPreset(input) {
|
|
2132
|
+
const pages = input.pages?.length > 0 ? input.pages.map((p) => ({
|
|
2133
|
+
id: String(p.id || "page-1"),
|
|
2134
|
+
title: String(p.title || "Page 1")
|
|
2135
|
+
})) : [{ id: "page-1", title: "\u5716\u8868" }];
|
|
2136
|
+
const pageIds = new Set(pages.map((p) => p.id));
|
|
2137
|
+
const defaultPageId = pages[0].id;
|
|
2138
|
+
const layers = [];
|
|
2139
|
+
const seenLayer = /* @__PURE__ */ new Set();
|
|
2140
|
+
for (const raw of input.layers ?? []) {
|
|
2141
|
+
if (!raw?.id || seenLayer.has(raw.id)) continue;
|
|
2142
|
+
if (!isLayerType(raw.type)) continue;
|
|
2143
|
+
seenLayer.add(raw.id);
|
|
2144
|
+
const pageId = pageIds.has(raw.pageId) ? raw.pageId : defaultPageId;
|
|
2145
|
+
layers.push({
|
|
2146
|
+
id: raw.id,
|
|
2147
|
+
pageId,
|
|
2148
|
+
type: raw.type,
|
|
2149
|
+
widgetKey: String(raw.widgetKey || raw.id),
|
|
2150
|
+
frame: clampFrame(raw.frame ?? { x: 0, y: 0, w: 1, h: 1 }),
|
|
2151
|
+
zIndex: Math.round(Number.isFinite(raw.zIndex) ? raw.zIndex : 0),
|
|
2152
|
+
visible: raw.visible !== false,
|
|
2153
|
+
locked: raw.locked === true,
|
|
2154
|
+
groupId: raw.groupId,
|
|
2155
|
+
syncTimeScaleGroupId: raw.syncTimeScaleGroupId
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
const { layers: splitLayers, idRemap } = splitLegacyChartHost(layers);
|
|
2159
|
+
let migrated = upgradeIndicatorHostType(splitLayers);
|
|
2160
|
+
for (const pageId of pageIds) {
|
|
2161
|
+
migrated = ensureOverlayLayers(migrated, pageId);
|
|
2162
|
+
}
|
|
2163
|
+
syncAllOverlayLayersToMain(migrated);
|
|
2164
|
+
migrated.sort((a, b) => a.zIndex - b.zIndex);
|
|
2165
|
+
migrated.forEach((l, i) => {
|
|
2166
|
+
l.zIndex = i;
|
|
2167
|
+
});
|
|
2168
|
+
const migratedIds = new Set(migrated.map((l) => l.id));
|
|
2169
|
+
const groups = [];
|
|
2170
|
+
const seenGroup = /* @__PURE__ */ new Set();
|
|
2171
|
+
for (const g of input.groups ?? []) {
|
|
2172
|
+
if (!g?.id || seenGroup.has(g.id)) continue;
|
|
2173
|
+
seenGroup.add(g.id);
|
|
2174
|
+
const layerIds = (g.layerIds ?? []).flatMap((id) => idRemap.get(id) ?? [id]).filter((id) => migratedIds.has(id));
|
|
2175
|
+
if (layerIds.length === 0) continue;
|
|
2176
|
+
groups.push({
|
|
2177
|
+
id: g.id,
|
|
2178
|
+
name: g.name,
|
|
2179
|
+
layerIds
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
const revisionRaw = Number(input.revision);
|
|
2183
|
+
const revision = Number.isFinite(revisionRaw) && revisionRaw >= 1 ? Math.floor(revisionRaw) : 1;
|
|
2184
|
+
return {
|
|
2185
|
+
version: LAYER_PRESET_VERSION,
|
|
2186
|
+
revision,
|
|
2187
|
+
id: String(input.id || "unnamed"),
|
|
2188
|
+
name: String(input.name || input.id || "Untitled"),
|
|
2189
|
+
author: input.author === "user" ? "user" : "integrator",
|
|
2190
|
+
readonly: input.readonly === true,
|
|
2191
|
+
forkedFrom: input.forkedFrom,
|
|
2192
|
+
pages,
|
|
2193
|
+
layers: migrated,
|
|
2194
|
+
groups
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
function cloneLayoutPreset(preset) {
|
|
2198
|
+
return normalizeLayoutPreset({
|
|
2199
|
+
...preset,
|
|
2200
|
+
pages: preset.pages.map((p) => ({ ...p })),
|
|
2201
|
+
layers: preset.layers.map((l) => ({ ...l, frame: { ...l.frame } })),
|
|
2202
|
+
groups: preset.groups.map((g) => ({ ...g, layerIds: [...g.layerIds] }))
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// src/layer/grid-to-preset.ts
|
|
2207
|
+
var WIDGET_META = {
|
|
2208
|
+
topBar: { layerId: "layer-topBar", type: "shell.topBar", widgetKey: "topBar", z: 10 },
|
|
2209
|
+
leftToolbar: { layerId: "layer-leftToolbar", type: "shell.leftToolbar", widgetKey: "leftToolbar", z: 20 },
|
|
2210
|
+
indicatorHost: {
|
|
2211
|
+
layerId: "layer-chartIndicator",
|
|
2212
|
+
type: "chart.indicator",
|
|
2213
|
+
widgetKey: "chartIndicator",
|
|
2214
|
+
z: 45
|
|
2215
|
+
},
|
|
2216
|
+
statusBar: { layerId: "layer-statusBar", type: "shell.statusBar", widgetKey: "statusBar", z: 50 },
|
|
2217
|
+
propertiesPanel: {
|
|
2218
|
+
layerId: "layer-propertiesPanel",
|
|
2219
|
+
type: "shell.propertiesPanel",
|
|
2220
|
+
widgetKey: "propertiesPanel",
|
|
2221
|
+
z: 60
|
|
2222
|
+
},
|
|
2223
|
+
bottomToolbar: { layerId: "layer-bottomToolbar", type: "shell.bottomToolbar", widgetKey: "bottomToolbar", z: 70 }
|
|
2224
|
+
};
|
|
2225
|
+
var MAIN_VOLUME_RATIO = 0.65;
|
|
2226
|
+
function chartHostLayers(w, columns, rows, pageId) {
|
|
2227
|
+
const frame = {
|
|
2228
|
+
x: w.col / columns,
|
|
2229
|
+
y: w.row / rows,
|
|
2230
|
+
w: w.colSpan / columns,
|
|
2231
|
+
h: w.rowSpan / rows
|
|
2232
|
+
};
|
|
2233
|
+
const mainH = frame.h * MAIN_VOLUME_RATIO;
|
|
2234
|
+
return [
|
|
2235
|
+
{
|
|
2236
|
+
id: "layer-chartMain",
|
|
2237
|
+
pageId,
|
|
2238
|
+
type: "chart.main",
|
|
2239
|
+
widgetKey: "chartMain",
|
|
2240
|
+
frame: { x: frame.x, y: frame.y, w: frame.w, h: mainH },
|
|
2241
|
+
zIndex: 30,
|
|
2242
|
+
visible: true,
|
|
2243
|
+
locked: false
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
id: "layer-chartVolume",
|
|
2247
|
+
pageId,
|
|
2248
|
+
type: "chart.volume",
|
|
2249
|
+
widgetKey: "chartVolume",
|
|
2250
|
+
frame: { x: frame.x, y: frame.y + mainH, w: frame.w, h: frame.h - mainH },
|
|
2251
|
+
zIndex: 35,
|
|
2252
|
+
visible: true,
|
|
2253
|
+
locked: false
|
|
2254
|
+
}
|
|
2255
|
+
];
|
|
2256
|
+
}
|
|
2257
|
+
function layoutSchemaToPreset(schema = DEFAULT_LAYOUT_SCHEMA, opts = {}) {
|
|
2258
|
+
const columns = schema.columns || 12;
|
|
2259
|
+
const rows = schema.rows || 12;
|
|
2260
|
+
const pageId = "page-1";
|
|
2261
|
+
const layers = [];
|
|
2262
|
+
for (const w of schema.widgets) {
|
|
2263
|
+
if (w.id === "chartHost") {
|
|
2264
|
+
layers.push(...chartHostLayers(w, columns, rows, pageId));
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
const meta = WIDGET_META[w.id];
|
|
2268
|
+
if (!meta) continue;
|
|
2269
|
+
layers.push({
|
|
2270
|
+
id: meta.layerId,
|
|
2271
|
+
pageId,
|
|
2272
|
+
type: meta.type,
|
|
2273
|
+
widgetKey: meta.widgetKey,
|
|
2274
|
+
frame: {
|
|
2275
|
+
x: w.col / columns,
|
|
2276
|
+
y: w.row / rows,
|
|
2277
|
+
w: w.colSpan / columns,
|
|
2278
|
+
h: w.rowSpan / rows
|
|
2279
|
+
},
|
|
2280
|
+
zIndex: meta.z,
|
|
2281
|
+
visible: true,
|
|
2282
|
+
locked: false
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
const chartMain = layers.find((l) => l.widgetKey === "chartMain");
|
|
2286
|
+
if (chartMain) {
|
|
2287
|
+
layers.push({
|
|
2288
|
+
id: "layer-crosshairLegend",
|
|
2289
|
+
pageId,
|
|
2290
|
+
type: "overlay.crosshairLegend",
|
|
2291
|
+
widgetKey: "crosshairLegend",
|
|
2292
|
+
frame: {
|
|
2293
|
+
x: chartMain.frame.x,
|
|
2294
|
+
y: chartMain.frame.y,
|
|
2295
|
+
w: Math.min(0.35, chartMain.frame.w),
|
|
2296
|
+
h: Math.min(0.1, chartMain.frame.h)
|
|
2297
|
+
},
|
|
2298
|
+
zIndex: chartMain.zIndex + 5,
|
|
2299
|
+
visible: true,
|
|
2300
|
+
locked: false
|
|
2301
|
+
});
|
|
2302
|
+
layers.push({
|
|
2303
|
+
id: "layer-drawingOverlay",
|
|
2304
|
+
pageId,
|
|
2305
|
+
type: "overlay.drawing",
|
|
2306
|
+
widgetKey: "drawingOverlay",
|
|
2307
|
+
frame: { ...chartMain.frame },
|
|
2308
|
+
zIndex: chartMain.zIndex + 3,
|
|
2309
|
+
visible: true,
|
|
2310
|
+
locked: false
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
return normalizeLayoutPreset({
|
|
2314
|
+
version: LAYER_PRESET_VERSION,
|
|
2315
|
+
id: opts.id ?? "vendor-default",
|
|
2316
|
+
name: opts.name ?? "TradView \u9810\u8A2D",
|
|
2317
|
+
author: "integrator",
|
|
2318
|
+
readonly: false,
|
|
2319
|
+
pages: [{ id: pageId, title: "\u5716\u8868" }],
|
|
2320
|
+
layers,
|
|
2321
|
+
groups: []
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// src/layer/default-presets.ts
|
|
2326
|
+
var VENDOR_DEFAULT_PRESET = layoutSchemaToPreset(void 0, {
|
|
2327
|
+
id: "vendor-default",
|
|
2328
|
+
name: "TradView \u9810\u8A2D"
|
|
2329
|
+
});
|
|
2330
|
+
var VENDOR_COMPACT_PRESET = normalizeLayoutPreset({
|
|
2331
|
+
...layoutSchemaToPreset(void 0, { id: "vendor-compact", name: "\u7CBE\u7C21\u5716\u8868" }),
|
|
2332
|
+
layers: layoutSchemaToPreset(void 0).layers.map((l) => {
|
|
2333
|
+
if (l.widgetKey === "propertiesPanel") {
|
|
2334
|
+
return { ...l, visible: false, frame: { ...l.frame, w: 1e-3, h: 1e-3 } };
|
|
2335
|
+
}
|
|
2336
|
+
if (l.widgetKey === "leftToolbar") {
|
|
2337
|
+
return { ...l, frame: { x: 0, y: 0.06, w: 0.04, h: 0.88 } };
|
|
2338
|
+
}
|
|
2339
|
+
if (l.widgetKey === "chartMain") {
|
|
2340
|
+
return { ...l, frame: { x: 0.04, y: 0.06, w: 0.96, h: 0.4 } };
|
|
2341
|
+
}
|
|
2342
|
+
if (l.widgetKey === "chartVolume") {
|
|
2343
|
+
return { ...l, frame: { x: 0.04, y: 0.46, w: 0.96, h: 0.16 } };
|
|
2344
|
+
}
|
|
2345
|
+
if (l.widgetKey === "chartIndicator") {
|
|
2346
|
+
return { ...l, frame: { x: 0.04, y: 0.68, w: 0.96, h: 0.22 } };
|
|
2347
|
+
}
|
|
2348
|
+
return l;
|
|
2349
|
+
})
|
|
2350
|
+
});
|
|
2351
|
+
var VENDOR_DUAL_SYNC_PRESET = normalizeLayoutPreset({
|
|
2352
|
+
...layoutSchemaToPreset(void 0, { id: "vendor-dual-sync", name: "\u96D9\u6642\u9593\u8EF8\u5206\u7D44" }),
|
|
2353
|
+
layers: layoutSchemaToPreset(void 0).layers.map((l) => {
|
|
2354
|
+
if (l.type === "chart.main" || l.type === "chart.volume") {
|
|
2355
|
+
return { ...l, syncTimeScaleGroupId: "prices" };
|
|
2356
|
+
}
|
|
2357
|
+
if (l.type === "chart.indicator") {
|
|
2358
|
+
return { ...l, syncTimeScaleGroupId: "osc" };
|
|
2359
|
+
}
|
|
2360
|
+
return l;
|
|
2361
|
+
})
|
|
2362
|
+
});
|
|
2363
|
+
var BUILTIN_PRESETS = [
|
|
2364
|
+
VENDOR_DEFAULT_PRESET,
|
|
2365
|
+
VENDOR_COMPACT_PRESET,
|
|
2366
|
+
VENDOR_DUAL_SYNC_PRESET
|
|
2367
|
+
];
|
|
2368
|
+
function getBuiltinPreset(id) {
|
|
2369
|
+
return BUILTIN_PRESETS.find((p) => p.id === id);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// src/layer/preset-store.ts
|
|
2373
|
+
var INDEX_KEY = "tradview:preset:v2:index";
|
|
2374
|
+
function presetStorageKey(id) {
|
|
2375
|
+
return `tradview:preset:v2:${id}`;
|
|
2376
|
+
}
|
|
2377
|
+
function readIndex() {
|
|
2378
|
+
try {
|
|
2379
|
+
const raw = localStorage.getItem(INDEX_KEY);
|
|
2380
|
+
if (!raw) return [];
|
|
2381
|
+
const arr = JSON.parse(raw);
|
|
2382
|
+
return Array.isArray(arr) ? arr.filter((id) => typeof id === "string") : [];
|
|
2383
|
+
} catch {
|
|
2384
|
+
return [];
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
function writeIndex(ids) {
|
|
2388
|
+
try {
|
|
2389
|
+
localStorage.setItem(INDEX_KEY, JSON.stringify([...new Set(ids)]));
|
|
2390
|
+
} catch {
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
function listPresets() {
|
|
2394
|
+
const out = BUILTIN_PRESETS.map((p) => ({
|
|
2395
|
+
id: p.id,
|
|
2396
|
+
name: p.name,
|
|
2397
|
+
author: p.author,
|
|
2398
|
+
readonly: p.readonly,
|
|
2399
|
+
builtin: true
|
|
2400
|
+
}));
|
|
2401
|
+
for (const id of readIndex()) {
|
|
2402
|
+
if (BUILTIN_PRESETS.some((p) => p.id === id)) continue;
|
|
2403
|
+
const loaded = loadPreset(id);
|
|
2404
|
+
if (loaded) {
|
|
2405
|
+
out.push({
|
|
2406
|
+
id: loaded.id,
|
|
2407
|
+
name: loaded.name,
|
|
2408
|
+
author: loaded.author,
|
|
2409
|
+
readonly: loaded.readonly,
|
|
2410
|
+
builtin: false
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
return out;
|
|
2415
|
+
}
|
|
2416
|
+
function loadPreset(id) {
|
|
2417
|
+
const builtin = getBuiltinPreset(id);
|
|
2418
|
+
if (builtin) return cloneLayoutPreset(builtin);
|
|
2419
|
+
try {
|
|
2420
|
+
const raw = localStorage.getItem(presetStorageKey(id));
|
|
2421
|
+
if (!raw) return null;
|
|
2422
|
+
return normalizeLayoutPreset(JSON.parse(raw));
|
|
2423
|
+
} catch {
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
function savePreset(preset) {
|
|
2428
|
+
const normalized = normalizeLayoutPreset(preset);
|
|
2429
|
+
if (getBuiltinPreset(normalized.id)) {
|
|
2430
|
+
throw new Error(`Cannot overwrite builtin preset: ${normalized.id}`);
|
|
2431
|
+
}
|
|
2432
|
+
try {
|
|
2433
|
+
localStorage.setItem(presetStorageKey(normalized.id), JSON.stringify(normalized));
|
|
2434
|
+
const ids = readIndex();
|
|
2435
|
+
if (!ids.includes(normalized.id)) {
|
|
2436
|
+
writeIndex([...ids, normalized.id]);
|
|
2437
|
+
}
|
|
2438
|
+
} catch {
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
function deleteUserPreset(id) {
|
|
2442
|
+
if (getBuiltinPreset(id)) return false;
|
|
2443
|
+
try {
|
|
2444
|
+
localStorage.removeItem(presetStorageKey(id));
|
|
2445
|
+
writeIndex(readIndex().filter((x) => x !== id));
|
|
2446
|
+
return true;
|
|
2447
|
+
} catch {
|
|
2448
|
+
return false;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function forkPreset(sourceId, newId, newName) {
|
|
2452
|
+
const src = loadPreset(sourceId);
|
|
2453
|
+
if (!src) return null;
|
|
2454
|
+
const forked = normalizeLayoutPreset({
|
|
2455
|
+
...cloneLayoutPreset(src),
|
|
2456
|
+
id: newId,
|
|
2457
|
+
name: newName,
|
|
2458
|
+
author: "user",
|
|
2459
|
+
readonly: false,
|
|
2460
|
+
forkedFrom: sourceId
|
|
2461
|
+
});
|
|
2462
|
+
savePreset(forked);
|
|
2463
|
+
return forked;
|
|
2464
|
+
}
|
|
2465
|
+
function resolvePreset(id) {
|
|
2466
|
+
if (id && typeof id === "object") return normalizeLayoutPreset(id);
|
|
2467
|
+
if (typeof id === "string") {
|
|
2468
|
+
const loaded = loadPreset(id);
|
|
2469
|
+
if (loaded) return loaded;
|
|
2470
|
+
}
|
|
2471
|
+
return cloneLayoutPreset(getBuiltinPreset("vendor-default"));
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// src/layer/group-utils.ts
|
|
2475
|
+
function getLayersBoundingBox(layers) {
|
|
2476
|
+
if (layers.length === 0) return null;
|
|
2477
|
+
let minX = 1;
|
|
2478
|
+
let minY = 1;
|
|
2479
|
+
let maxX = 0;
|
|
2480
|
+
let maxY = 0;
|
|
2481
|
+
for (const l of layers) {
|
|
2482
|
+
const f = l.frame;
|
|
2483
|
+
minX = Math.min(minX, f.x);
|
|
2484
|
+
minY = Math.min(minY, f.y);
|
|
2485
|
+
maxX = Math.max(maxX, f.x + f.w);
|
|
2486
|
+
maxY = Math.max(maxY, f.y + f.h);
|
|
2487
|
+
}
|
|
2488
|
+
const w = maxX - minX;
|
|
2489
|
+
const h = maxY - minY;
|
|
2490
|
+
if (w <= 0 || h <= 0) return null;
|
|
2491
|
+
return { x: minX, y: minY, w, h };
|
|
2492
|
+
}
|
|
2493
|
+
function moveLayerFrames(frames, dx, dy) {
|
|
2494
|
+
return frames.map(
|
|
2495
|
+
(f) => clampFrame({
|
|
2496
|
+
...f,
|
|
2497
|
+
x: f.x + dx,
|
|
2498
|
+
y: f.y + dy
|
|
2499
|
+
})
|
|
2500
|
+
);
|
|
2501
|
+
}
|
|
2502
|
+
function resizeGroupFrames(members, oldBbox, newBbox) {
|
|
2503
|
+
const ow = Math.max(oldBbox.w, 0.02);
|
|
2504
|
+
const oh = Math.max(oldBbox.h, 0.02);
|
|
2505
|
+
const nw = Math.max(newBbox.w, 0.02);
|
|
2506
|
+
const nh = Math.max(newBbox.h, 0.02);
|
|
2507
|
+
return members.map((m) => {
|
|
2508
|
+
const f = m.frame;
|
|
2509
|
+
const relX = (f.x - oldBbox.x) / ow;
|
|
2510
|
+
const relY = (f.y - oldBbox.y) / oh;
|
|
2511
|
+
const relW = f.w / ow;
|
|
2512
|
+
const relH = f.h / oh;
|
|
2513
|
+
return clampFrame({
|
|
2514
|
+
x: newBbox.x + relX * nw,
|
|
2515
|
+
y: newBbox.y + relY * nh,
|
|
2516
|
+
w: relW * nw,
|
|
2517
|
+
h: relH * nh
|
|
2518
|
+
});
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
function clampBBox(frame) {
|
|
2522
|
+
return clampFrame(frame);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// src/layer/layer-controller.ts
|
|
2526
|
+
var LayerController = class {
|
|
2527
|
+
preset;
|
|
2528
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2529
|
+
focusListeners = /* @__PURE__ */ new Set();
|
|
2530
|
+
interactionDepth = 0;
|
|
2531
|
+
pageIdSeq = 0;
|
|
2532
|
+
activePageId;
|
|
2533
|
+
/** P2: active chart pane layer (click-to-focus). */
|
|
2534
|
+
focusedPaneLayerId = null;
|
|
2535
|
+
/** Monotonic preset revision (Bridge `host.layer.setPreset`). */
|
|
2536
|
+
presetRevision;
|
|
2537
|
+
constructor(initial) {
|
|
2538
|
+
this.preset = cloneLayoutPreset(initial);
|
|
2539
|
+
this.presetRevision = this.preset.revision ?? 1;
|
|
2540
|
+
this.activePageId = this.preset.pages[0]?.id ?? "page-1";
|
|
2541
|
+
}
|
|
2542
|
+
getPreset() {
|
|
2543
|
+
return cloneLayoutPreset(this.preset);
|
|
2544
|
+
}
|
|
2545
|
+
/** True while pointer drag/resize/marquee is active — blocks setPreset. */
|
|
2546
|
+
isInteracting() {
|
|
2547
|
+
return this.interactionDepth > 0;
|
|
2548
|
+
}
|
|
2549
|
+
beginInteraction() {
|
|
2550
|
+
this.interactionDepth++;
|
|
2551
|
+
}
|
|
2552
|
+
endInteraction() {
|
|
2553
|
+
this.interactionDepth = Math.max(0, this.interactionDepth - 1);
|
|
2554
|
+
}
|
|
2555
|
+
setPreset(next) {
|
|
2556
|
+
if (this.isInteracting()) return false;
|
|
2557
|
+
this.preset = cloneLayoutPreset(next);
|
|
2558
|
+
this.presetRevision = this.preset.revision ?? 1;
|
|
2559
|
+
if (!this.preset.pages.some((p) => p.id === this.activePageId)) {
|
|
2560
|
+
this.activePageId = this.preset.pages[0]?.id ?? "page-1";
|
|
2561
|
+
}
|
|
2562
|
+
this.emit();
|
|
2563
|
+
return true;
|
|
2564
|
+
}
|
|
2565
|
+
getLayersForActivePage() {
|
|
2566
|
+
return this.preset.layers.filter((l) => l.pageId === this.activePageId).sort((a, b) => a.zIndex - b.zIndex);
|
|
2567
|
+
}
|
|
2568
|
+
getFocusedPaneLayerId() {
|
|
2569
|
+
return this.focusedPaneLayerId;
|
|
2570
|
+
}
|
|
2571
|
+
getFocusedPaneId() {
|
|
2572
|
+
const layer = this.focusedPaneLayerId ? this.getLayer(this.focusedPaneLayerId) : void 0;
|
|
2573
|
+
return layer ? paneIdFromWidgetKey(layer.widgetKey) : null;
|
|
2574
|
+
}
|
|
2575
|
+
/** Raise z-index among chart panes only; does not rebuild compositor DOM. */
|
|
2576
|
+
focusChartPane(layerId) {
|
|
2577
|
+
const layer = this.getLayer(layerId);
|
|
2578
|
+
if (!layer || layer.pageId !== this.activePageId || !isChartPaneLayerType(layer.type)) {
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
if (this.focusedPaneLayerId === layerId) return;
|
|
2582
|
+
this.focusedPaneLayerId = layerId;
|
|
2583
|
+
const chartLayers = this.getLayersForActivePage().filter(
|
|
2584
|
+
(l) => isChartPaneLayerType(l.type)
|
|
2585
|
+
);
|
|
2586
|
+
const maxZ = Math.max(...chartLayers.map((l) => l.zIndex), layer.zIndex);
|
|
2587
|
+
const FOCUS_Z_CAP = 500;
|
|
2588
|
+
if (layer.zIndex < maxZ) layer.zIndex = Math.min(maxZ + 1, FOCUS_Z_CAP);
|
|
2589
|
+
this.notifyFocus();
|
|
2590
|
+
}
|
|
2591
|
+
subscribeFocus(listener) {
|
|
2592
|
+
this.focusListeners.add(listener);
|
|
2593
|
+
return () => this.focusListeners.delete(listener);
|
|
2594
|
+
}
|
|
2595
|
+
notifyFocus() {
|
|
2596
|
+
for (const fn of this.focusListeners) fn();
|
|
2597
|
+
}
|
|
2598
|
+
afterChartMainFrameChange(pageIds) {
|
|
2599
|
+
const ids = pageIds ? [...new Set(pageIds)] : [...new Set(this.preset.layers.map((l) => l.pageId))];
|
|
2600
|
+
for (const pageId of ids) {
|
|
2601
|
+
syncOverlayLayersToMain(this.preset.layers, pageId);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
setActivePage(pageId) {
|
|
2605
|
+
if (!this.preset.pages.some((p) => p.id === pageId)) return false;
|
|
2606
|
+
if (this.activePageId === pageId) return false;
|
|
2607
|
+
this.activePageId = pageId;
|
|
2608
|
+
this.focusedPaneLayerId = null;
|
|
2609
|
+
this.emit();
|
|
2610
|
+
this.notifyFocus();
|
|
2611
|
+
return true;
|
|
2612
|
+
}
|
|
2613
|
+
addPage(title) {
|
|
2614
|
+
const suffix = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${++this.pageIdSeq}`;
|
|
2615
|
+
const id = `page-${suffix}`;
|
|
2616
|
+
const pageTitle = title?.trim() || `Page ${this.preset.pages.length + 1}`;
|
|
2617
|
+
this.preset.pages.push({ id, title: pageTitle });
|
|
2618
|
+
const templatePageId = this.activePageId;
|
|
2619
|
+
const templateLayers = this.preset.layers.filter((l) => l.pageId === templatePageId);
|
|
2620
|
+
const maxZ = Math.max(-1, ...this.preset.layers.map((l) => l.zIndex));
|
|
2621
|
+
let z = maxZ + 1;
|
|
2622
|
+
for (const tpl of templateLayers) {
|
|
2623
|
+
this.preset.layers.push({
|
|
2624
|
+
...tpl,
|
|
2625
|
+
id: `${tpl.id}-${id}`,
|
|
2626
|
+
pageId: id,
|
|
2627
|
+
frame: { ...tpl.frame },
|
|
2628
|
+
zIndex: z++,
|
|
2629
|
+
groupId: void 0
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
this.activePageId = id;
|
|
2633
|
+
this.focusedPaneLayerId = null;
|
|
2634
|
+
this.emit();
|
|
2635
|
+
return id;
|
|
2636
|
+
}
|
|
2637
|
+
renamePage(pageId, title) {
|
|
2638
|
+
const page = this.preset.pages.find((p) => p.id === pageId);
|
|
2639
|
+
if (!page) return false;
|
|
2640
|
+
const trimmed = title.trim();
|
|
2641
|
+
if (!trimmed) return false;
|
|
2642
|
+
page.title = trimmed;
|
|
2643
|
+
this.emit();
|
|
2644
|
+
return true;
|
|
2645
|
+
}
|
|
2646
|
+
removePage(pageId) {
|
|
2647
|
+
if (this.preset.pages.length <= 1) return false;
|
|
2648
|
+
const idx = this.preset.pages.findIndex((p) => p.id === pageId);
|
|
2649
|
+
if (idx < 0) return false;
|
|
2650
|
+
this.preset.pages.splice(idx, 1);
|
|
2651
|
+
this.preset.layers = this.preset.layers.filter((l) => l.pageId !== pageId);
|
|
2652
|
+
for (const g of [...this.preset.groups]) {
|
|
2653
|
+
g.layerIds = g.layerIds.filter((lid) => this.getLayer(lid)?.pageId !== pageId);
|
|
2654
|
+
if (g.layerIds.length === 0) {
|
|
2655
|
+
this.preset.groups = this.preset.groups.filter((x) => x.id !== g.id);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
if (this.activePageId === pageId) {
|
|
2659
|
+
const nextPage = this.preset.pages[idx] ?? this.preset.pages[idx - 1];
|
|
2660
|
+
this.activePageId = nextPage?.id ?? this.preset.pages[0].id;
|
|
2661
|
+
this.focusedPaneLayerId = null;
|
|
2662
|
+
}
|
|
2663
|
+
this.emit();
|
|
2664
|
+
return true;
|
|
2665
|
+
}
|
|
2666
|
+
getLayer(layerId) {
|
|
2667
|
+
return this.preset.layers.find((l) => l.id === layerId);
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* @public Set time-scale sync group for a chart pane layer (`''` clears → independent).
|
|
2671
|
+
*/
|
|
2672
|
+
setLayerSyncGroup(layerId, groupId) {
|
|
2673
|
+
const layer = this.getLayer(layerId);
|
|
2674
|
+
if (!layer || !isChartPaneLayerType(layer.type)) return false;
|
|
2675
|
+
const trimmed = groupId == null ? "" : String(groupId).trim();
|
|
2676
|
+
layer.syncTimeScaleGroupId = trimmed.length > 0 ? trimmed : void 0;
|
|
2677
|
+
this.emit();
|
|
2678
|
+
return true;
|
|
2679
|
+
}
|
|
2680
|
+
getGroup(groupId) {
|
|
2681
|
+
return this.preset.groups.find((g) => g.id === groupId);
|
|
2682
|
+
}
|
|
2683
|
+
/** Layer ids that move/resize together (group members or singleton). */
|
|
2684
|
+
getTransformLayerIds(layerId) {
|
|
2685
|
+
const layer = this.getLayer(layerId);
|
|
2686
|
+
if (!layer?.groupId) return [layerId];
|
|
2687
|
+
const group = this.getGroup(layer.groupId);
|
|
2688
|
+
if (!group || group.layerIds.length === 0) return [layerId];
|
|
2689
|
+
return group.layerIds.filter((id) => {
|
|
2690
|
+
const l = this.getLayer(id);
|
|
2691
|
+
return l && l.pageId === this.activePageId && !l.locked;
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
getGroupMemberLayers(groupId) {
|
|
2695
|
+
const group = this.getGroup(groupId);
|
|
2696
|
+
if (!group) return [];
|
|
2697
|
+
return group.layerIds.map((id) => this.getLayer(id)).filter((l) => !!l && l.pageId === this.activePageId);
|
|
2698
|
+
}
|
|
2699
|
+
setLayerFrame(layerId, frame) {
|
|
2700
|
+
const ids = this.getTransformLayerIds(layerId);
|
|
2701
|
+
if (ids.length === 1) {
|
|
2702
|
+
const layer = this.getLayer(layerId);
|
|
2703
|
+
if (!layer) return;
|
|
2704
|
+
layer.frame = clampFrame(frame);
|
|
2705
|
+
if (layer.type === "overlay.drawing") {
|
|
2706
|
+
const main = this.preset.layers.find(
|
|
2707
|
+
(l) => l.type === "chart.main" && l.pageId === layer.pageId
|
|
2708
|
+
);
|
|
2709
|
+
if (main) main.frame = clampFrame(frame);
|
|
2710
|
+
}
|
|
2711
|
+
if (layer.type === "chart.main" || layer.type === "overlay.drawing") {
|
|
2712
|
+
this.afterChartMainFrameChange([layer.pageId]);
|
|
2713
|
+
}
|
|
2714
|
+
this.emit();
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
const members = ids.map((id) => this.getLayer(id)).filter(Boolean);
|
|
2718
|
+
const oldBbox = getLayersBoundingBox(members);
|
|
2719
|
+
if (!oldBbox) return;
|
|
2720
|
+
const target = frame;
|
|
2721
|
+
const newFrames = resizeGroupFrames(members, oldBbox, target);
|
|
2722
|
+
members.forEach((m, i) => {
|
|
2723
|
+
m.frame = newFrames[i];
|
|
2724
|
+
});
|
|
2725
|
+
if (members.some((m) => m.type === "chart.main" || m.type === "overlay.drawing")) {
|
|
2726
|
+
this.afterChartMainFrameChange(members.map((m) => m.pageId));
|
|
2727
|
+
}
|
|
2728
|
+
this.emit();
|
|
2729
|
+
}
|
|
2730
|
+
expandTransformIds(layerIds) {
|
|
2731
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
2732
|
+
for (const id of layerIds) {
|
|
2733
|
+
for (const tid of this.getTransformLayerIds(id)) expanded.add(tid);
|
|
2734
|
+
}
|
|
2735
|
+
return [...expanded];
|
|
2736
|
+
}
|
|
2737
|
+
moveLayers(layerIds, dx, dy) {
|
|
2738
|
+
const unique = this.expandTransformIds(layerIds);
|
|
2739
|
+
const layers = unique.map((id) => this.getLayer(id)).filter(Boolean);
|
|
2740
|
+
if (layers.length === 0) return;
|
|
2741
|
+
const frames = moveLayerFrames(
|
|
2742
|
+
layers.map((l) => l.frame),
|
|
2743
|
+
dx,
|
|
2744
|
+
dy
|
|
2745
|
+
);
|
|
2746
|
+
layers.forEach((l, i) => {
|
|
2747
|
+
l.frame = frames[i];
|
|
2748
|
+
});
|
|
2749
|
+
if (layers.some((l) => l.type === "chart.main" || l.type === "overlay.drawing")) {
|
|
2750
|
+
this.afterChartMainFrameChange(layers.map((l) => l.pageId));
|
|
2751
|
+
}
|
|
2752
|
+
this.emit();
|
|
2753
|
+
}
|
|
2754
|
+
resizeLayersFromBbox(layerIds, newBbox) {
|
|
2755
|
+
const unique = this.expandTransformIds(layerIds);
|
|
2756
|
+
const members = unique.map((id) => this.getLayer(id)).filter((l) => !!l);
|
|
2757
|
+
if (members.length === 0) return;
|
|
2758
|
+
if (members.length === 1) {
|
|
2759
|
+
members[0].frame = clampFrame(newBbox);
|
|
2760
|
+
if (members[0].type === "chart.main" || members[0].type === "overlay.drawing") {
|
|
2761
|
+
this.afterChartMainFrameChange([members[0].pageId]);
|
|
2762
|
+
}
|
|
2763
|
+
this.emit();
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
const oldBbox = getLayersBoundingBox(members);
|
|
2767
|
+
if (!oldBbox) return;
|
|
2768
|
+
const newFrames = resizeGroupFrames(members, oldBbox, newBbox);
|
|
2769
|
+
members.forEach((m, i) => {
|
|
2770
|
+
m.frame = newFrames[i];
|
|
2771
|
+
});
|
|
2772
|
+
if (members.some((m) => m.type === "chart.main" || m.type === "overlay.drawing")) {
|
|
2773
|
+
this.afterChartMainFrameChange(members.map((m) => m.pageId));
|
|
2774
|
+
}
|
|
2775
|
+
this.emit();
|
|
2776
|
+
}
|
|
2777
|
+
setLayerVisible(layerId, visible) {
|
|
2778
|
+
const ids = this.syncGroupLayerIds(layerId);
|
|
2779
|
+
for (const id of ids) {
|
|
2780
|
+
const layer = this.getLayer(id);
|
|
2781
|
+
if (layer) layer.visible = visible;
|
|
2782
|
+
}
|
|
2783
|
+
this.emit();
|
|
2784
|
+
}
|
|
2785
|
+
setLayerLocked(layerId, locked) {
|
|
2786
|
+
const ids = this.syncGroupLayerIds(layerId);
|
|
2787
|
+
for (const id of ids) {
|
|
2788
|
+
const layer = this.getLayer(id);
|
|
2789
|
+
if (layer) layer.locked = locked;
|
|
2790
|
+
}
|
|
2791
|
+
this.emit();
|
|
2792
|
+
}
|
|
2793
|
+
syncGroupLayerIds(layerId) {
|
|
2794
|
+
const layer = this.getLayer(layerId);
|
|
2795
|
+
if (!layer?.groupId) return [layerId];
|
|
2796
|
+
const group = this.getGroup(layer.groupId);
|
|
2797
|
+
if (!group?.layerIds.length) return [layerId];
|
|
2798
|
+
const onPage = group.layerIds.filter((id) => {
|
|
2799
|
+
const l = this.getLayer(id);
|
|
2800
|
+
return l && l.pageId === this.activePageId;
|
|
2801
|
+
});
|
|
2802
|
+
return onPage.length > 0 ? onPage : [layerId];
|
|
2803
|
+
}
|
|
2804
|
+
bumpZIndex(layerId, delta) {
|
|
2805
|
+
const ordered = this.getLayersForActivePage();
|
|
2806
|
+
const idx = ordered.findIndex((l) => l.id === layerId);
|
|
2807
|
+
if (idx < 0) return;
|
|
2808
|
+
const swap = idx + delta;
|
|
2809
|
+
if (swap < 0 || swap >= ordered.length) return;
|
|
2810
|
+
const a = ordered[idx];
|
|
2811
|
+
const b = ordered[swap];
|
|
2812
|
+
const tz = a.zIndex;
|
|
2813
|
+
a.zIndex = b.zIndex;
|
|
2814
|
+
b.zIndex = tz;
|
|
2815
|
+
this.preset.layers.sort((x, y) => x.zIndex - y.zIndex);
|
|
2816
|
+
this.preset.layers.forEach((l, i) => {
|
|
2817
|
+
l.zIndex = i;
|
|
2818
|
+
});
|
|
2819
|
+
this.emit();
|
|
2820
|
+
}
|
|
2821
|
+
reorderLayers(orderedIds) {
|
|
2822
|
+
const pageLayers = this.preset.layers.filter((l) => l.pageId === this.activePageId);
|
|
2823
|
+
const byId = new Map(pageLayers.map((l) => [l.id, l]));
|
|
2824
|
+
let z = 0;
|
|
2825
|
+
for (const id of orderedIds) {
|
|
2826
|
+
const layer = byId.get(id);
|
|
2827
|
+
if (layer) layer.zIndex = z++;
|
|
2828
|
+
}
|
|
2829
|
+
for (const layer of pageLayers) {
|
|
2830
|
+
if (!orderedIds.includes(layer.id)) layer.zIndex = z++;
|
|
2831
|
+
}
|
|
2832
|
+
this.preset.layers.sort((a, b) => a.zIndex - b.zIndex);
|
|
2833
|
+
this.emit();
|
|
2834
|
+
}
|
|
2835
|
+
createBindGroup(layerIds, name) {
|
|
2836
|
+
const unique = [...new Set(layerIds)].filter((id) => {
|
|
2837
|
+
const l = this.getLayer(id);
|
|
2838
|
+
return l && l.pageId === this.activePageId;
|
|
2839
|
+
});
|
|
2840
|
+
if (unique.length < 2) return null;
|
|
2841
|
+
for (const g of this.preset.groups) {
|
|
2842
|
+
for (const id of unique) {
|
|
2843
|
+
const idx = g.layerIds.indexOf(id);
|
|
2844
|
+
if (idx >= 0) g.layerIds.splice(idx, 1);
|
|
2845
|
+
}
|
|
2846
|
+
if (g.layerIds.length === 0) {
|
|
2847
|
+
this.preset.groups = this.preset.groups.filter((x) => x.id !== g.id);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
for (const id of unique) {
|
|
2851
|
+
const layer = this.getLayer(id);
|
|
2852
|
+
if (layer) delete layer.groupId;
|
|
2853
|
+
}
|
|
2854
|
+
const groupId = `group-${Date.now()}`;
|
|
2855
|
+
const group = { id: groupId, name, layerIds: unique };
|
|
2856
|
+
this.preset.groups.push(group);
|
|
2857
|
+
for (const id of unique) {
|
|
2858
|
+
const layer = this.getLayer(id);
|
|
2859
|
+
if (layer) layer.groupId = groupId;
|
|
2860
|
+
}
|
|
2861
|
+
this.emit();
|
|
2862
|
+
return groupId;
|
|
2863
|
+
}
|
|
2864
|
+
dissolveGroup(groupId) {
|
|
2865
|
+
const group = this.getGroup(groupId);
|
|
2866
|
+
if (!group) return;
|
|
2867
|
+
for (const id of group.layerIds) {
|
|
2868
|
+
const layer = this.getLayer(id);
|
|
2869
|
+
if (layer?.groupId === groupId) delete layer.groupId;
|
|
2870
|
+
}
|
|
2871
|
+
this.preset.groups = this.preset.groups.filter((g) => g.id !== groupId);
|
|
2872
|
+
this.emit();
|
|
2873
|
+
}
|
|
2874
|
+
removeLayerFromGroup(layerId) {
|
|
2875
|
+
const layer = this.getLayer(layerId);
|
|
2876
|
+
if (!layer?.groupId) return;
|
|
2877
|
+
const groupId = layer.groupId;
|
|
2878
|
+
delete layer.groupId;
|
|
2879
|
+
const group = this.getGroup(groupId);
|
|
2880
|
+
if (!group) return;
|
|
2881
|
+
group.layerIds = group.layerIds.filter((id) => id !== layerId);
|
|
2882
|
+
if (group.layerIds.length === 0) {
|
|
2883
|
+
this.preset.groups = this.preset.groups.filter((g) => g.id !== groupId);
|
|
2884
|
+
}
|
|
2885
|
+
this.emit();
|
|
2886
|
+
}
|
|
2887
|
+
subscribe(listener) {
|
|
2888
|
+
this.listeners.add(listener);
|
|
2889
|
+
return () => this.listeners.delete(listener);
|
|
2890
|
+
}
|
|
2891
|
+
emit() {
|
|
2892
|
+
for (const fn of this.listeners) fn();
|
|
2893
|
+
}
|
|
2894
|
+
};
|
|
2895
|
+
|
|
2896
|
+
// src/layer/layer-editor.ts
|
|
2897
|
+
var HANDLE_CURSORS = {
|
|
2898
|
+
nw: "nwse-resize",
|
|
2899
|
+
ne: "nesw-resize",
|
|
2900
|
+
sw: "nesw-resize",
|
|
2901
|
+
se: "nwse-resize"
|
|
2902
|
+
};
|
|
2903
|
+
var EPS = 1e-6;
|
|
2904
|
+
function framesEqual(a, b, eps = EPS) {
|
|
2905
|
+
return Math.abs(a.x - b.x) < eps && Math.abs(a.y - b.y) < eps && Math.abs(a.w - b.w) < eps && Math.abs(a.h - b.h) < eps;
|
|
2906
|
+
}
|
|
2907
|
+
function frameIntersects(a, b) {
|
|
2908
|
+
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
2909
|
+
}
|
|
2910
|
+
function setWrapContentPassthrough(wrap, passthrough) {
|
|
2911
|
+
for (const child of wrap.children) {
|
|
2912
|
+
if (child.classList.contains("tv-layer-handle")) continue;
|
|
2913
|
+
child.style.pointerEvents = passthrough ? "none" : "";
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
function clearWrapContentPassthrough(wrap) {
|
|
2917
|
+
for (const child of wrap.children) {
|
|
2918
|
+
if (child.classList.contains("tv-layer-handle")) continue;
|
|
2919
|
+
child.style.pointerEvents = "";
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
function attachLayerEditor(compositorRoot, opts) {
|
|
2923
|
+
const { controller, onPresetChange, onMarqueeSelect } = opts;
|
|
2924
|
+
let enabled = false;
|
|
2925
|
+
let abort = null;
|
|
2926
|
+
let captureEl = null;
|
|
2927
|
+
let capturePointerId = null;
|
|
2928
|
+
const releaseCapture = () => {
|
|
2929
|
+
if (captureEl && capturePointerId !== null) {
|
|
2930
|
+
try {
|
|
2931
|
+
captureEl.releasePointerCapture(capturePointerId);
|
|
2932
|
+
} catch {
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
if (capturePointerId !== null && controller.isInteracting()) {
|
|
2936
|
+
controller.endInteraction();
|
|
2937
|
+
}
|
|
2938
|
+
captureEl = null;
|
|
2939
|
+
capturePointerId = null;
|
|
2940
|
+
};
|
|
2941
|
+
const setCapture = (el, pointerId) => {
|
|
2942
|
+
releaseCapture();
|
|
2943
|
+
captureEl = el;
|
|
2944
|
+
capturePointerId = pointerId;
|
|
2945
|
+
controller.beginInteraction();
|
|
2946
|
+
try {
|
|
2947
|
+
el.setPointerCapture(pointerId);
|
|
2948
|
+
} catch {
|
|
2949
|
+
controller.endInteraction();
|
|
2950
|
+
captureEl = null;
|
|
2951
|
+
capturePointerId = null;
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
const normDelta = (dxPx, dyPx) => {
|
|
2955
|
+
const w = compositorRoot.clientWidth || 1;
|
|
2956
|
+
const h = compositorRoot.clientHeight || 1;
|
|
2957
|
+
return { dx: dxPx / w, dy: dyPx / h };
|
|
2958
|
+
};
|
|
2959
|
+
const applyWrapStyle = (wrap, frame) => {
|
|
2960
|
+
wrap.style.left = `${frame.x * 100}%`;
|
|
2961
|
+
wrap.style.top = `${frame.y * 100}%`;
|
|
2962
|
+
wrap.style.width = `${frame.w * 100}%`;
|
|
2963
|
+
wrap.style.height = `${frame.h * 100}%`;
|
|
2964
|
+
};
|
|
2965
|
+
const decorateWrap = (wrap, layerId) => {
|
|
2966
|
+
wrap.querySelectorAll(".tv-layer-handle").forEach((el) => el.remove());
|
|
2967
|
+
const layer = controller.getLayer(layerId);
|
|
2968
|
+
if (!layer || layer.locked) {
|
|
2969
|
+
wrap.classList.remove("tv-layer-editable");
|
|
2970
|
+
wrap.style.cursor = "";
|
|
2971
|
+
wrap.style.outline = "";
|
|
2972
|
+
clearWrapContentPassthrough(wrap);
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
wrap.classList.add("tv-layer-editable");
|
|
2976
|
+
wrap.style.cursor = "move";
|
|
2977
|
+
setWrapContentPassthrough(wrap, true);
|
|
2978
|
+
if (wrap.dataset.focused === "1") {
|
|
2979
|
+
wrap.style.outline = "2px solid #58a6ff";
|
|
2980
|
+
wrap.style.outlineOffset = "-2px";
|
|
2981
|
+
} else {
|
|
2982
|
+
wrap.style.outline = "1px dashed #58a6ff88";
|
|
2983
|
+
wrap.style.outlineOffset = "";
|
|
2984
|
+
}
|
|
2985
|
+
const handles = ["nw", "ne", "sw", "se"];
|
|
2986
|
+
for (const h of handles) {
|
|
2987
|
+
const handle = document.createElement("div");
|
|
2988
|
+
handle.className = `tv-layer-handle tv-layer-handle-${h}`;
|
|
2989
|
+
handle.dataset.handle = h;
|
|
2990
|
+
handle.style.cssText = [
|
|
2991
|
+
"position:absolute",
|
|
2992
|
+
"width:8px",
|
|
2993
|
+
"height:8px",
|
|
2994
|
+
"background:#58a6ff",
|
|
2995
|
+
"border:1px solid #0d1117",
|
|
2996
|
+
"border-radius:2px",
|
|
2997
|
+
"z-index:2",
|
|
2998
|
+
`cursor:${HANDLE_CURSORS[h]}`,
|
|
2999
|
+
h.includes("n") ? "top:-4px" : "bottom:-4px",
|
|
3000
|
+
h.includes("w") ? "left:-4px" : "right:-4px"
|
|
3001
|
+
].join(";");
|
|
3002
|
+
wrap.appendChild(handle);
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
3005
|
+
const commitChange = () => {
|
|
3006
|
+
onPresetChange?.();
|
|
3007
|
+
};
|
|
3008
|
+
const computeResizeBbox = (startBbox, corner, dx, dy) => {
|
|
3009
|
+
const newBbox = { ...startBbox };
|
|
3010
|
+
if (corner.includes("e")) newBbox.w = startBbox.w + dx;
|
|
3011
|
+
if (corner.includes("w")) {
|
|
3012
|
+
newBbox.x = startBbox.x + dx;
|
|
3013
|
+
newBbox.w = startBbox.w - dx;
|
|
3014
|
+
}
|
|
3015
|
+
if (corner.includes("s")) newBbox.h = startBbox.h + dy;
|
|
3016
|
+
if (corner.includes("n")) {
|
|
3017
|
+
newBbox.y = startBbox.y + dy;
|
|
3018
|
+
newBbox.h = startBbox.h - dy;
|
|
3019
|
+
}
|
|
3020
|
+
return clampFrame(newBbox);
|
|
3021
|
+
};
|
|
3022
|
+
const bindMarquee = (signal) => {
|
|
3023
|
+
compositorRoot.addEventListener(
|
|
3024
|
+
"pointerdown",
|
|
3025
|
+
(e) => {
|
|
3026
|
+
if (!e.shiftKey) return;
|
|
3027
|
+
if (e.target.closest(".tv-layer-wrap")) return;
|
|
3028
|
+
e.preventDefault();
|
|
3029
|
+
const rootRect = compositorRoot.getBoundingClientRect();
|
|
3030
|
+
const startX = e.clientX;
|
|
3031
|
+
const startY = e.clientY;
|
|
3032
|
+
const marquee = document.createElement("div");
|
|
3033
|
+
marquee.className = "tv-layer-marquee";
|
|
3034
|
+
marquee.style.cssText = "position:absolute;border:1px dashed #58a6ff;background:#58a6ff22;pointer-events:none;z-index:9999;";
|
|
3035
|
+
compositorRoot.appendChild(marquee);
|
|
3036
|
+
setCapture(compositorRoot, e.pointerId);
|
|
3037
|
+
const updateMarquee = (cx, cy) => {
|
|
3038
|
+
const x1 = Math.min(startX, cx) - rootRect.left;
|
|
3039
|
+
const y1 = Math.min(startY, cy) - rootRect.top;
|
|
3040
|
+
const x2 = Math.max(startX, cx) - rootRect.left;
|
|
3041
|
+
const y2 = Math.max(startY, cy) - rootRect.top;
|
|
3042
|
+
marquee.style.left = `${x1}px`;
|
|
3043
|
+
marquee.style.top = `${y1}px`;
|
|
3044
|
+
marquee.style.width = `${x2 - x1}px`;
|
|
3045
|
+
marquee.style.height = `${y2 - y1}px`;
|
|
3046
|
+
};
|
|
3047
|
+
const onMove = (ev) => updateMarquee(ev.clientX, ev.clientY);
|
|
3048
|
+
const onUp = (ev) => {
|
|
3049
|
+
releaseCapture();
|
|
3050
|
+
compositorRoot.removeEventListener("pointermove", onMove);
|
|
3051
|
+
compositorRoot.removeEventListener("pointerup", onUp);
|
|
3052
|
+
compositorRoot.removeEventListener("pointercancel", onUp);
|
|
3053
|
+
const w = compositorRoot.clientWidth || 1;
|
|
3054
|
+
const h = compositorRoot.clientHeight || 1;
|
|
3055
|
+
const sel = clampFrame({
|
|
3056
|
+
x: (Math.min(startX, ev.clientX) - rootRect.left) / w,
|
|
3057
|
+
y: (Math.min(startY, ev.clientY) - rootRect.top) / h,
|
|
3058
|
+
w: Math.abs(ev.clientX - startX) / w,
|
|
3059
|
+
h: Math.abs(ev.clientY - startY) / h
|
|
3060
|
+
});
|
|
3061
|
+
marquee.remove();
|
|
3062
|
+
if (sel.w < EPS && sel.h < EPS) return;
|
|
3063
|
+
const hits = controller.getLayersForActivePage().filter((l) => frameIntersects(l.frame, sel)).map((l) => l.id);
|
|
3064
|
+
if (hits.length > 0) onMarqueeSelect?.(hits);
|
|
3065
|
+
};
|
|
3066
|
+
updateMarquee(startX, startY);
|
|
3067
|
+
compositorRoot.addEventListener("pointermove", onMove, { signal });
|
|
3068
|
+
compositorRoot.addEventListener("pointerup", onUp, { signal });
|
|
3069
|
+
compositorRoot.addEventListener("pointercancel", onUp, { signal });
|
|
3070
|
+
},
|
|
3071
|
+
{ signal }
|
|
3072
|
+
);
|
|
3073
|
+
};
|
|
3074
|
+
const bindWrap = (wrap, signal) => {
|
|
3075
|
+
const layerId = wrap.dataset.layerId;
|
|
3076
|
+
if (!layerId) return;
|
|
3077
|
+
const layer = controller.getLayer(layerId);
|
|
3078
|
+
if (!layer || layer.locked) return;
|
|
3079
|
+
const transformIds = () => controller.getTransformLayerIds(layerId);
|
|
3080
|
+
const onPointerDownMove = (e) => {
|
|
3081
|
+
if (e.target.classList.contains("tv-layer-handle")) return;
|
|
3082
|
+
e.preventDefault();
|
|
3083
|
+
e.stopPropagation();
|
|
3084
|
+
const ids = transformIds();
|
|
3085
|
+
const members = ids.map((id) => controller.getLayer(id)).filter(Boolean);
|
|
3086
|
+
const startFrames = members.map((m) => ({ ...m.frame }));
|
|
3087
|
+
const startX = e.clientX;
|
|
3088
|
+
const startY = e.clientY;
|
|
3089
|
+
setCapture(wrap, e.pointerId);
|
|
3090
|
+
wrap.classList.add("tv-layer-dragging");
|
|
3091
|
+
const onMove = (ev) => {
|
|
3092
|
+
const { dx, dy } = normDelta(ev.clientX - startX, ev.clientY - startY);
|
|
3093
|
+
const moved = startFrames.map(
|
|
3094
|
+
(f) => clampFrame({ ...f, x: f.x + dx, y: f.y + dy })
|
|
3095
|
+
);
|
|
3096
|
+
compositorRoot.querySelectorAll(".tv-layer-wrap").forEach((el) => {
|
|
3097
|
+
const w = el;
|
|
3098
|
+
const lid = w.dataset.layerId;
|
|
3099
|
+
if (!lid || !ids.includes(lid)) return;
|
|
3100
|
+
const idx = ids.indexOf(lid);
|
|
3101
|
+
if (idx >= 0) applyWrapStyle(w, moved[idx]);
|
|
3102
|
+
});
|
|
3103
|
+
};
|
|
3104
|
+
const onUp = (ev) => {
|
|
3105
|
+
releaseCapture();
|
|
3106
|
+
wrap.classList.remove("tv-layer-dragging");
|
|
3107
|
+
wrap.removeEventListener("pointermove", onMove);
|
|
3108
|
+
wrap.removeEventListener("pointerup", onUp);
|
|
3109
|
+
wrap.removeEventListener("pointercancel", onUp);
|
|
3110
|
+
const { dx, dy } = normDelta(ev.clientX - startX, ev.clientY - startY);
|
|
3111
|
+
if (Math.abs(dx) > EPS || Math.abs(dy) > EPS) {
|
|
3112
|
+
controller.moveLayers(ids, dx, dy);
|
|
3113
|
+
commitChange();
|
|
3114
|
+
}
|
|
3115
|
+
};
|
|
3116
|
+
wrap.addEventListener("pointermove", onMove, { signal });
|
|
3117
|
+
wrap.addEventListener("pointerup", onUp, { signal });
|
|
3118
|
+
wrap.addEventListener("pointercancel", onUp, { signal });
|
|
3119
|
+
};
|
|
3120
|
+
wrap.addEventListener("pointerdown", onPointerDownMove, { signal });
|
|
3121
|
+
wrap.querySelectorAll(".tv-layer-handle").forEach((handleEl) => {
|
|
3122
|
+
const handle = handleEl;
|
|
3123
|
+
const corner = handle.dataset.handle;
|
|
3124
|
+
handle.addEventListener(
|
|
3125
|
+
"pointerdown",
|
|
3126
|
+
(e) => {
|
|
3127
|
+
e.preventDefault();
|
|
3128
|
+
e.stopPropagation();
|
|
3129
|
+
const ids = transformIds();
|
|
3130
|
+
const members = ids.map((id) => controller.getLayer(id)).filter(Boolean);
|
|
3131
|
+
let startBbox;
|
|
3132
|
+
if (members.length === 1) {
|
|
3133
|
+
startBbox = { ...members[0].frame };
|
|
3134
|
+
} else {
|
|
3135
|
+
startBbox = getLayersBoundingBox(members);
|
|
3136
|
+
}
|
|
3137
|
+
if (!startBbox) return;
|
|
3138
|
+
const startX = e.clientX;
|
|
3139
|
+
const startY = e.clientY;
|
|
3140
|
+
setCapture(handle, e.pointerId);
|
|
3141
|
+
const onMove = (ev) => {
|
|
3142
|
+
const { dx, dy } = normDelta(ev.clientX - startX, ev.clientY - startY);
|
|
3143
|
+
const newBbox = computeResizeBbox(startBbox, corner, dx, dy);
|
|
3144
|
+
if (ids.length === 1) {
|
|
3145
|
+
applyWrapStyle(wrap, newBbox);
|
|
3146
|
+
} else {
|
|
3147
|
+
const resized = resizeGroupFrames(members, startBbox, newBbox);
|
|
3148
|
+
compositorRoot.querySelectorAll(".tv-layer-wrap").forEach((el) => {
|
|
3149
|
+
const w = el;
|
|
3150
|
+
const lid = w.dataset.layerId;
|
|
3151
|
+
if (!lid || !ids.includes(lid)) return;
|
|
3152
|
+
const idx = ids.indexOf(lid);
|
|
3153
|
+
if (idx >= 0) applyWrapStyle(w, resized[idx]);
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
};
|
|
3157
|
+
const onUp = (ev) => {
|
|
3158
|
+
releaseCapture();
|
|
3159
|
+
handle.removeEventListener("pointermove", onMove);
|
|
3160
|
+
handle.removeEventListener("pointerup", onUp);
|
|
3161
|
+
handle.removeEventListener("pointercancel", onUp);
|
|
3162
|
+
const { dx, dy } = normDelta(ev.clientX - startX, ev.clientY - startY);
|
|
3163
|
+
const newBbox = computeResizeBbox(startBbox, corner, dx, dy);
|
|
3164
|
+
if (!framesEqual(newBbox, startBbox) && (Math.abs(dx) > EPS || Math.abs(dy) > EPS)) {
|
|
3165
|
+
controller.resizeLayersFromBbox(ids, newBbox);
|
|
3166
|
+
commitChange();
|
|
3167
|
+
}
|
|
3168
|
+
};
|
|
3169
|
+
handle.addEventListener("pointermove", onMove, { signal });
|
|
3170
|
+
handle.addEventListener("pointerup", onUp, { signal });
|
|
3171
|
+
handle.addEventListener("pointercancel", onUp, { signal });
|
|
3172
|
+
},
|
|
3173
|
+
{ signal }
|
|
3174
|
+
);
|
|
3175
|
+
});
|
|
3176
|
+
};
|
|
3177
|
+
const cleanupEditChrome = () => {
|
|
3178
|
+
compositorRoot.classList.remove("tv-layer-edit-mode");
|
|
3179
|
+
compositorRoot.querySelectorAll(".tv-layer-marquee").forEach((el) => el.remove());
|
|
3180
|
+
compositorRoot.querySelectorAll(".tv-layer-wrap").forEach((w) => {
|
|
3181
|
+
const el = w;
|
|
3182
|
+
el.classList.remove("tv-layer-editable", "tv-layer-dragging");
|
|
3183
|
+
el.style.outline = "";
|
|
3184
|
+
el.style.cursor = "";
|
|
3185
|
+
el.querySelectorAll(".tv-layer-handle").forEach((h) => h.remove());
|
|
3186
|
+
clearWrapContentPassthrough(el);
|
|
3187
|
+
});
|
|
3188
|
+
compositorRoot.style.zIndex = "";
|
|
3189
|
+
};
|
|
3190
|
+
const rebind = () => {
|
|
3191
|
+
releaseCapture();
|
|
3192
|
+
abort?.abort();
|
|
3193
|
+
if (!enabled) {
|
|
3194
|
+
cleanupEditChrome();
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
compositorRoot.classList.add("tv-layer-edit-mode");
|
|
3198
|
+
compositorRoot.style.zIndex = "80";
|
|
3199
|
+
abort = new AbortController();
|
|
3200
|
+
const { signal } = abort;
|
|
3201
|
+
bindMarquee(signal);
|
|
3202
|
+
compositorRoot.querySelectorAll(".tv-layer-wrap").forEach((w) => {
|
|
3203
|
+
const wrap = w;
|
|
3204
|
+
const layerId = wrap.dataset.layerId;
|
|
3205
|
+
if (!layerId) return;
|
|
3206
|
+
decorateWrap(wrap, layerId);
|
|
3207
|
+
bindWrap(wrap, signal);
|
|
3208
|
+
});
|
|
3209
|
+
};
|
|
3210
|
+
return {
|
|
3211
|
+
setEnabled(next) {
|
|
3212
|
+
enabled = next;
|
|
3213
|
+
rebind();
|
|
3214
|
+
},
|
|
3215
|
+
isEnabled: () => enabled,
|
|
3216
|
+
rebind,
|
|
3217
|
+
destroy: () => {
|
|
3218
|
+
enabled = false;
|
|
3219
|
+
releaseCapture();
|
|
3220
|
+
abort?.abort();
|
|
3221
|
+
cleanupEditChrome();
|
|
3222
|
+
}
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
// src/layer/compositor.ts
|
|
3227
|
+
function resolveWidgetEl(widgets, layer) {
|
|
3228
|
+
const key = layer.widgetKey;
|
|
3229
|
+
if (layer.type === "overlay.drawing") {
|
|
3230
|
+
return widgets.drawingOverlay ?? widgets.chartMain ?? widgets.chartHost;
|
|
3231
|
+
}
|
|
3232
|
+
return widgets[key] ?? (layer.widgetKey === "chartMain" ? widgets.chartHost : void 0) ?? (layer.widgetKey === "chartIndicator" ? widgets.indicatorHost : void 0);
|
|
3233
|
+
}
|
|
3234
|
+
function applyWrapFrame(wrap, layer) {
|
|
3235
|
+
wrap.style.left = `${layer.frame.x * 100}%`;
|
|
3236
|
+
wrap.style.top = `${layer.frame.y * 100}%`;
|
|
3237
|
+
wrap.style.width = `${layer.frame.w * 100}%`;
|
|
3238
|
+
wrap.style.height = `${layer.frame.h * 100}%`;
|
|
3239
|
+
wrap.style.zIndex = String(layer.zIndex + 1);
|
|
3240
|
+
}
|
|
3241
|
+
function layerStructureSignature(layers, activePageId) {
|
|
3242
|
+
return `${activePageId}|${layers.map((l) => `${l.id}:${l.type}:${l.widgetKey}:${l.visible}:${l.locked}`).join("|")}`;
|
|
3243
|
+
}
|
|
3244
|
+
function wrapPointerEventsForLayer(layer, editorEnabled) {
|
|
3245
|
+
if (layer.locked) return "none";
|
|
3246
|
+
if (isOverlayLayerType(layer.type)) return "none";
|
|
3247
|
+
if (isChartPaneLayerType(layer.type) && editorEnabled) return "none";
|
|
3248
|
+
return "auto";
|
|
3249
|
+
}
|
|
3250
|
+
function mountLayerCompositor(parent, opts) {
|
|
3251
|
+
const controller = new LayerController(opts.preset);
|
|
3252
|
+
const wrappers = /* @__PURE__ */ new Map();
|
|
3253
|
+
let lastStructureSig = "";
|
|
3254
|
+
const root = document.createElement("div");
|
|
3255
|
+
root.className = "tv-layer-compositor";
|
|
3256
|
+
root.style.cssText = "position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:5;";
|
|
3257
|
+
parent.style.position = parent.style.position || "relative";
|
|
3258
|
+
parent.appendChild(root);
|
|
3259
|
+
const editor = attachLayerEditor(root, {
|
|
3260
|
+
controller,
|
|
3261
|
+
onPresetChange: () => opts.onPresetChange?.(controller.getPreset()),
|
|
3262
|
+
onMarqueeSelect: opts.onMarqueeSelect
|
|
3263
|
+
});
|
|
3264
|
+
const applyFocusStyles = () => {
|
|
3265
|
+
const focused = controller.focusedPaneLayerId;
|
|
3266
|
+
for (const [id, wrap] of wrappers) {
|
|
3267
|
+
const layer = controller.getLayer(id);
|
|
3268
|
+
if (!layer || !isChartPaneLayerType(layer.type)) continue;
|
|
3269
|
+
const active = id === focused;
|
|
3270
|
+
wrap.dataset.focused = active ? "1" : "0";
|
|
3271
|
+
wrap.style.outline = active ? "2px solid #58a6ff" : "none";
|
|
3272
|
+
wrap.style.outlineOffset = active ? "-2px" : "";
|
|
3273
|
+
}
|
|
3274
|
+
};
|
|
3275
|
+
const patchFocusVisuals = (layers) => {
|
|
3276
|
+
for (const layer of layers) {
|
|
3277
|
+
const wrap = wrappers.get(layer.id);
|
|
3278
|
+
if (wrap) applyWrapFrame(wrap, layer);
|
|
3279
|
+
}
|
|
3280
|
+
applyFocusStyles();
|
|
3281
|
+
};
|
|
3282
|
+
const patchLayoutFrames = (layers, patchOpts = {}) => {
|
|
3283
|
+
patchFocusVisuals(layers);
|
|
3284
|
+
if (patchOpts.rebind !== false) editor.rebind();
|
|
3285
|
+
};
|
|
3286
|
+
const ensureDefaultChartFocus = (chartPaneIds) => {
|
|
3287
|
+
if (controller.focusedPaneLayerId && !chartPaneIds.includes(controller.focusedPaneLayerId)) {
|
|
3288
|
+
controller.focusedPaneLayerId = chartPaneIds[chartPaneIds.length - 1] ?? null;
|
|
3289
|
+
}
|
|
3290
|
+
if (!controller.focusedPaneLayerId && chartPaneIds.length > 0) {
|
|
3291
|
+
controller.focusedPaneLayerId = chartPaneIds[chartPaneIds.length - 1];
|
|
3292
|
+
}
|
|
3293
|
+
};
|
|
3294
|
+
const bindChartPanePointer = (wrap, layer) => {
|
|
3295
|
+
wrap.addEventListener("pointerdown", (e) => {
|
|
3296
|
+
if (e.button !== 0 || controller.isInteracting()) return;
|
|
3297
|
+
const prev = controller.focusedPaneLayerId;
|
|
3298
|
+
if (prev === layer.id) return;
|
|
3299
|
+
queueMicrotask(() => {
|
|
3300
|
+
if (controller.isInteracting()) return;
|
|
3301
|
+
controller.focusChartPane(layer.id);
|
|
3302
|
+
opts.onChartPaneFocus?.(controller.getFocusedPaneId());
|
|
3303
|
+
});
|
|
3304
|
+
});
|
|
3305
|
+
};
|
|
3306
|
+
const syncRootPointerEvents = () => {
|
|
3307
|
+
root.style.pointerEvents = editor.isEnabled() ? "auto" : "none";
|
|
3308
|
+
};
|
|
3309
|
+
const apply = () => {
|
|
3310
|
+
syncRootPointerEvents();
|
|
3311
|
+
const preset = controller.getPreset();
|
|
3312
|
+
const layers = preset.layers.filter((l) => l.pageId === controller.activePageId && l.visible).sort((a, b) => a.zIndex - b.zIndex);
|
|
3313
|
+
if (opts.hideLegacyGrid) opts.hideLegacyGrid.style.visibility = "hidden";
|
|
3314
|
+
const chartPaneIds = layers.filter((l) => isChartPaneLayerType(l.type)).map((l) => l.id);
|
|
3315
|
+
ensureDefaultChartFocus(chartPaneIds);
|
|
3316
|
+
const structureSig = layerStructureSignature(layers, controller.activePageId);
|
|
3317
|
+
if (structureSig === lastStructureSig && wrappers.size > 0) {
|
|
3318
|
+
patchLayoutFrames(layers);
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
lastStructureSig = structureSig;
|
|
3322
|
+
root.replaceChildren();
|
|
3323
|
+
wrappers.clear();
|
|
3324
|
+
const editorOn = editor.isEnabled();
|
|
3325
|
+
for (const layer of layers) {
|
|
3326
|
+
if (layer.type === "overlay.drawing") continue;
|
|
3327
|
+
const el = resolveWidgetEl(opts.widgets, layer);
|
|
3328
|
+
if (!el) continue;
|
|
3329
|
+
const wrap = document.createElement("div");
|
|
3330
|
+
wrap.className = "tv-layer-wrap";
|
|
3331
|
+
if (isChartPaneLayerType(layer.type)) wrap.classList.add("tv-chart-pane-wrap");
|
|
3332
|
+
if (isOverlayLayerType(layer.type)) wrap.classList.add("tv-overlay-wrap");
|
|
3333
|
+
wrap.dataset.layerId = layer.id;
|
|
3334
|
+
wrap.dataset.widgetKey = layer.widgetKey;
|
|
3335
|
+
wrap.dataset.layerType = layer.type;
|
|
3336
|
+
if (layer.groupId) wrap.dataset.groupId = layer.groupId;
|
|
3337
|
+
const pe = wrapPointerEventsForLayer(layer, editorOn);
|
|
3338
|
+
wrap.style.cssText = [
|
|
3339
|
+
"position:absolute",
|
|
3340
|
+
"overflow:visible",
|
|
3341
|
+
"box-sizing:border-box",
|
|
3342
|
+
`pointer-events:${pe}`
|
|
3343
|
+
].join(";");
|
|
3344
|
+
applyWrapFrame(wrap, layer);
|
|
3345
|
+
if (isChartPaneLayerType(layer.type) && !layer.locked && !editorOn) {
|
|
3346
|
+
bindChartPanePointer(wrap, layer);
|
|
3347
|
+
}
|
|
3348
|
+
if (layer.type === "overlay.crosshairLegend") {
|
|
3349
|
+
el.style.pointerEvents = "none";
|
|
3350
|
+
}
|
|
3351
|
+
el.style.width = "100%";
|
|
3352
|
+
el.style.height = "100%";
|
|
3353
|
+
el.style.minWidth = "0";
|
|
3354
|
+
el.style.minHeight = "0";
|
|
3355
|
+
el.style.overflow = layer.type === "overlay.crosshairLegend" ? "visible" : "hidden";
|
|
3356
|
+
wrap.appendChild(el);
|
|
3357
|
+
root.appendChild(wrap);
|
|
3358
|
+
wrappers.set(layer.id, wrap);
|
|
3359
|
+
}
|
|
3360
|
+
applyFocusStyles();
|
|
3361
|
+
editor.rebind();
|
|
3362
|
+
};
|
|
3363
|
+
const unsubLayout = controller.subscribe(apply);
|
|
3364
|
+
const unsubFocus = controller.subscribeFocus(() => {
|
|
3365
|
+
const visible = controller.getLayersForActivePage().filter((l) => l.visible);
|
|
3366
|
+
patchFocusVisuals(visible);
|
|
3367
|
+
});
|
|
3368
|
+
apply();
|
|
3369
|
+
return {
|
|
3370
|
+
root,
|
|
3371
|
+
controller,
|
|
3372
|
+
apply,
|
|
3373
|
+
enableLayerEditor: (enabled) => {
|
|
3374
|
+
editor.setEnabled(enabled);
|
|
3375
|
+
syncRootPointerEvents();
|
|
3376
|
+
apply();
|
|
3377
|
+
},
|
|
3378
|
+
isLayerEditorEnabled: () => editor.isEnabled(),
|
|
3379
|
+
destroy: () => {
|
|
3380
|
+
unsubLayout();
|
|
3381
|
+
unsubFocus();
|
|
3382
|
+
editor.destroy();
|
|
3383
|
+
root.remove();
|
|
3384
|
+
if (opts.hideLegacyGrid) opts.hideLegacyGrid.style.visibility = "";
|
|
3385
|
+
}
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
// src/layer/layer-panel.ts
|
|
919
3390
|
import { t as t8 } from "@coderyo/i18n";
|
|
3391
|
+
function mountLayerPanel(parent, controller, opts = {}) {
|
|
3392
|
+
const panel = document.createElement("aside");
|
|
3393
|
+
panel.className = "tv-layer-panel";
|
|
3394
|
+
panel.style.cssText = "display:none;position:fixed;right:12px;top:72px;width:240px;max-height:min(70vh,480px);z-index:2100;background:#161b22;border:1px solid #30363d;border-radius:8px;box-shadow:0 8px 24px #01040999;flex-direction:column;overflow:hidden;font-size:11px;color:#e6edf3;";
|
|
3395
|
+
const header = document.createElement("div");
|
|
3396
|
+
header.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid #30363d;flex-shrink:0;";
|
|
3397
|
+
const title = document.createElement("span");
|
|
3398
|
+
title.textContent = t8("layer.panel.title", "\u5716\u5C64");
|
|
3399
|
+
title.style.fontWeight = "600";
|
|
3400
|
+
const closeBtn = document.createElement("button");
|
|
3401
|
+
closeBtn.type = "button";
|
|
3402
|
+
closeBtn.textContent = "\xD7";
|
|
3403
|
+
closeBtn.style.cssText = "background:transparent;border:none;color:#8b949e;font-size:18px;cursor:pointer;line-height:1;";
|
|
3404
|
+
header.append(title, closeBtn);
|
|
3405
|
+
const groupBar = document.createElement("div");
|
|
3406
|
+
groupBar.style.cssText = "display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid #30363d;flex-shrink:0;flex-wrap:wrap;";
|
|
3407
|
+
const createGroupBtn = document.createElement("button");
|
|
3408
|
+
createGroupBtn.type = "button";
|
|
3409
|
+
createGroupBtn.textContent = t8("layer.panel.createGroup", "\u5EFA\u7ACB\u7FA4\u7D44");
|
|
3410
|
+
createGroupBtn.style.cssText = "flex:1;min-width:0;padding:4px 6px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:10px;";
|
|
3411
|
+
const removeFromGroupBtn = document.createElement("button");
|
|
3412
|
+
removeFromGroupBtn.type = "button";
|
|
3413
|
+
removeFromGroupBtn.textContent = t8("layer.panel.removeFromGroup", "\u79FB\u51FA\u7FA4\u7D44");
|
|
3414
|
+
removeFromGroupBtn.style.cssText = createGroupBtn.style.cssText;
|
|
3415
|
+
const ungroupBtn = document.createElement("button");
|
|
3416
|
+
ungroupBtn.type = "button";
|
|
3417
|
+
ungroupBtn.textContent = t8("layer.panel.ungroup", "\u89E3\u6563\u7FA4\u7D44");
|
|
3418
|
+
ungroupBtn.style.cssText = createGroupBtn.style.cssText;
|
|
3419
|
+
groupBar.append(createGroupBtn, removeFromGroupBtn, ungroupBtn);
|
|
3420
|
+
const list = document.createElement("div");
|
|
3421
|
+
list.style.cssText = "overflow:auto;flex:1;min-height:0;padding:6px 4px;";
|
|
3422
|
+
const footer = document.createElement("div");
|
|
3423
|
+
footer.style.cssText = "padding:8px;border-top:1px solid #30363d;flex-shrink:0;";
|
|
3424
|
+
const saveBtn = document.createElement("button");
|
|
3425
|
+
saveBtn.type = "button";
|
|
3426
|
+
saveBtn.textContent = t8("layer.panel.saveAs", "\u53E6\u5B58\u70BA\u6211\u7684\u7BC4\u672C");
|
|
3427
|
+
saveBtn.style.cssText = "width:100%;padding:6px 8px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:11px;";
|
|
3428
|
+
footer.appendChild(saveBtn);
|
|
3429
|
+
panel.append(header, groupBar, list, footer);
|
|
3430
|
+
parent.appendChild(panel);
|
|
3431
|
+
let open = false;
|
|
3432
|
+
const selected = /* @__PURE__ */ new Set();
|
|
3433
|
+
let dragLayerId = null;
|
|
3434
|
+
const btnStyle = "width:24px;height:22px;padding:0;border:1px solid #30363d;border-radius:3px;background:#21262d;cursor:pointer;font-size:10px;";
|
|
3435
|
+
const render = () => {
|
|
3436
|
+
list.replaceChildren();
|
|
3437
|
+
const layers = [...controller.getLayersForActivePage()].sort((a, b) => b.zIndex - a.zIndex);
|
|
3438
|
+
for (const id of [...selected]) {
|
|
3439
|
+
if (!layers.some((l) => l.id === id)) selected.delete(id);
|
|
3440
|
+
}
|
|
3441
|
+
createGroupBtn.disabled = selected.size < 2;
|
|
3442
|
+
const selectedWithGroup = layers.filter((l) => selected.has(l.id) && l.groupId);
|
|
3443
|
+
ungroupBtn.disabled = selectedWithGroup.length === 0;
|
|
3444
|
+
const singleInGroup = selected.size === 1 && [...selected].some((id) => controller.getLayer(id)?.groupId);
|
|
3445
|
+
removeFromGroupBtn.disabled = !singleInGroup;
|
|
3446
|
+
for (const layer of layers) {
|
|
3447
|
+
const row = document.createElement("div");
|
|
3448
|
+
row.draggable = !layer.locked;
|
|
3449
|
+
row.dataset.layerId = layer.id;
|
|
3450
|
+
row.style.cssText = "display:flex;align-items:center;gap:4px;padding:4px 6px;margin-bottom:2px;border-radius:4px;background:#0d1117;border:1px solid #21262d;cursor:grab;";
|
|
3451
|
+
row.ondragstart = (e) => {
|
|
3452
|
+
if (layer.locked) return;
|
|
3453
|
+
dragLayerId = layer.id;
|
|
3454
|
+
e.dataTransfer?.setData("text/plain", layer.id);
|
|
3455
|
+
if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
|
|
3456
|
+
row.style.opacity = "0.55";
|
|
3457
|
+
};
|
|
3458
|
+
row.ondragend = () => {
|
|
3459
|
+
row.style.opacity = "";
|
|
3460
|
+
dragLayerId = null;
|
|
3461
|
+
};
|
|
3462
|
+
row.ondragover = (e) => {
|
|
3463
|
+
e.preventDefault();
|
|
3464
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
3465
|
+
};
|
|
3466
|
+
row.ondrop = (e) => {
|
|
3467
|
+
e.preventDefault();
|
|
3468
|
+
if (!dragLayerId || dragLayerId === layer.id) return;
|
|
3469
|
+
const visual = [...controller.getLayersForActivePage()].sort(
|
|
3470
|
+
(a, b) => b.zIndex - a.zIndex
|
|
3471
|
+
);
|
|
3472
|
+
const ids = visual.map((l) => l.id);
|
|
3473
|
+
const from = ids.indexOf(dragLayerId);
|
|
3474
|
+
const to = ids.indexOf(layer.id);
|
|
3475
|
+
if (from < 0 || to < 0) return;
|
|
3476
|
+
ids.splice(from, 1);
|
|
3477
|
+
ids.splice(to, 0, dragLayerId);
|
|
3478
|
+
controller.reorderLayers([...ids].reverse());
|
|
3479
|
+
dragLayerId = null;
|
|
3480
|
+
};
|
|
3481
|
+
const pick = document.createElement("input");
|
|
3482
|
+
pick.type = "checkbox";
|
|
3483
|
+
pick.checked = selected.has(layer.id);
|
|
3484
|
+
pick.title = t8("layer.panel.select", "\u9078\u53D6");
|
|
3485
|
+
pick.style.cssText = "width:14px;height:14px;cursor:pointer;flex-shrink:0;";
|
|
3486
|
+
pick.onchange = () => {
|
|
3487
|
+
if (pick.checked) selected.add(layer.id);
|
|
3488
|
+
else selected.delete(layer.id);
|
|
3489
|
+
render();
|
|
3490
|
+
};
|
|
3491
|
+
const eye = document.createElement("button");
|
|
3492
|
+
eye.type = "button";
|
|
3493
|
+
eye.textContent = layer.visible ? "\u{1F441}" : "\u2014";
|
|
3494
|
+
eye.title = t8("layer.panel.visibility", "\u986F\u793A");
|
|
3495
|
+
eye.style.cssText = btnStyle;
|
|
3496
|
+
const lock = document.createElement("button");
|
|
3497
|
+
lock.type = "button";
|
|
3498
|
+
lock.textContent = layer.locked ? "\u{1F512}" : "\u{1F513}";
|
|
3499
|
+
lock.title = t8("layer.panel.lock", "\u9396\u5B9A");
|
|
3500
|
+
lock.style.cssText = btnStyle;
|
|
3501
|
+
const label = document.createElement("span");
|
|
3502
|
+
const groupTag = layer.groupId ? ` [${layer.groupId.slice(-4)}]` : "";
|
|
3503
|
+
label.textContent = `${layer.widgetKey}${groupTag}`;
|
|
3504
|
+
label.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
|
3505
|
+
let syncIn = null;
|
|
3506
|
+
if (isChartPaneLayerType(layer.type)) {
|
|
3507
|
+
syncIn = document.createElement("input");
|
|
3508
|
+
syncIn.type = "text";
|
|
3509
|
+
syncIn.placeholder = t8("layer.panel.syncGroup", "\u6642\u9593\u8EF8\u7D44");
|
|
3510
|
+
syncIn.value = layer.syncTimeScaleGroupId ?? "";
|
|
3511
|
+
syncIn.title = t8(
|
|
3512
|
+
"layer.panel.syncGroupHint",
|
|
3513
|
+
"\u76F8\u540C\u7D44\u540D\u540C\u6B65\u7E2E\u653E\uFF1B\u7559\u7A7A\u5247\u7368\u7ACB"
|
|
3514
|
+
);
|
|
3515
|
+
syncIn.style.cssText = "width:52px;flex-shrink:0;padding:2px 4px;font-size:10px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:3px;";
|
|
3516
|
+
syncIn.onchange = () => {
|
|
3517
|
+
controller.setLayerSyncGroup(layer.id, syncIn.value);
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
const zUp = document.createElement("button");
|
|
3521
|
+
zUp.type = "button";
|
|
3522
|
+
zUp.textContent = "\u25B2";
|
|
3523
|
+
zUp.title = "z+";
|
|
3524
|
+
zUp.style.cssText = btnStyle;
|
|
3525
|
+
const zDown = document.createElement("button");
|
|
3526
|
+
zDown.type = "button";
|
|
3527
|
+
zDown.textContent = "\u25BC";
|
|
3528
|
+
zDown.title = "z-";
|
|
3529
|
+
zDown.style.cssText = btnStyle;
|
|
3530
|
+
eye.onclick = () => controller.setLayerVisible(layer.id, !layer.visible);
|
|
3531
|
+
lock.onclick = () => controller.setLayerLocked(layer.id, !layer.locked);
|
|
3532
|
+
zUp.onclick = () => controller.bumpZIndex(layer.id, 1);
|
|
3533
|
+
zDown.onclick = () => controller.bumpZIndex(layer.id, -1);
|
|
3534
|
+
if (syncIn) row.append(pick, eye, lock, label, syncIn, zUp, zDown);
|
|
3535
|
+
else row.append(pick, eye, lock, label, zUp, zDown);
|
|
3536
|
+
list.appendChild(row);
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
createGroupBtn.onclick = () => {
|
|
3540
|
+
const ids = [...selected];
|
|
3541
|
+
if (ids.length < 2) return;
|
|
3542
|
+
controller.createBindGroup(ids);
|
|
3543
|
+
selected.clear();
|
|
3544
|
+
render();
|
|
3545
|
+
};
|
|
3546
|
+
removeFromGroupBtn.onclick = () => {
|
|
3547
|
+
const id = [...selected][0];
|
|
3548
|
+
if (!id) return;
|
|
3549
|
+
controller.removeLayerFromGroup(id);
|
|
3550
|
+
render();
|
|
3551
|
+
};
|
|
3552
|
+
ungroupBtn.onclick = () => {
|
|
3553
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
3554
|
+
for (const id of selected) {
|
|
3555
|
+
const layer = controller.getLayer(id);
|
|
3556
|
+
if (layer?.groupId) groupIds.add(layer.groupId);
|
|
3557
|
+
}
|
|
3558
|
+
for (const gid of groupIds) controller.dissolveGroup(gid);
|
|
3559
|
+
render();
|
|
3560
|
+
};
|
|
3561
|
+
const unsub = controller.subscribe(render);
|
|
3562
|
+
render();
|
|
3563
|
+
closeBtn.onclick = () => {
|
|
3564
|
+
open = false;
|
|
3565
|
+
panel.style.display = "none";
|
|
3566
|
+
};
|
|
3567
|
+
saveBtn.onclick = () => opts.onSaveAsPreset?.(controller.getPreset());
|
|
3568
|
+
return {
|
|
3569
|
+
el: panel,
|
|
3570
|
+
destroy: () => {
|
|
3571
|
+
unsub();
|
|
3572
|
+
panel.remove();
|
|
3573
|
+
},
|
|
3574
|
+
toggle: () => {
|
|
3575
|
+
open = !open;
|
|
3576
|
+
panel.style.display = open ? "flex" : "none";
|
|
3577
|
+
return open;
|
|
3578
|
+
},
|
|
3579
|
+
selectLayers(layerIds) {
|
|
3580
|
+
selected.clear();
|
|
3581
|
+
for (const id of layerIds) selected.add(id);
|
|
3582
|
+
render();
|
|
3583
|
+
}
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
// src/layer/page-navigator.ts
|
|
3588
|
+
import { t as t9 } from "@coderyo/i18n";
|
|
3589
|
+
function mountPageNavigator(parent, controller, opts = {}) {
|
|
3590
|
+
const mq = opts.narrowMq ?? "(max-width: 768px)";
|
|
3591
|
+
const nav = document.createElement("nav");
|
|
3592
|
+
nav.className = "tv-page-navigator";
|
|
3593
|
+
nav.style.cssText = "display:none;align-items:center;gap:4px;padding:4px 8px;border-top:1px solid #30363d;background:#161b22;flex-shrink:0;flex-wrap:wrap;font-size:11px;color:#e6edf3;";
|
|
3594
|
+
const tabs = document.createElement("div");
|
|
3595
|
+
tabs.style.cssText = "display:flex;gap:4px;flex-wrap:wrap;flex:1;min-width:0;";
|
|
3596
|
+
const actions = document.createElement("div");
|
|
3597
|
+
actions.style.cssText = "display:flex;gap:4px;flex-shrink:0;";
|
|
3598
|
+
const addBtn = document.createElement("button");
|
|
3599
|
+
addBtn.type = "button";
|
|
3600
|
+
addBtn.title = t9("page.nav.add", "\u65B0\u589E\u9801\u9762");
|
|
3601
|
+
addBtn.textContent = "+";
|
|
3602
|
+
addBtn.style.cssText = "width:28px;height:24px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:14px;line-height:1;";
|
|
3603
|
+
const renameBtn = document.createElement("button");
|
|
3604
|
+
renameBtn.type = "button";
|
|
3605
|
+
renameBtn.title = t9("page.nav.rename", "\u91CD\u65B0\u547D\u540D");
|
|
3606
|
+
renameBtn.textContent = "\u270E";
|
|
3607
|
+
renameBtn.style.cssText = "width:28px;height:24px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:12px;";
|
|
3608
|
+
const delBtn = document.createElement("button");
|
|
3609
|
+
delBtn.type = "button";
|
|
3610
|
+
delBtn.title = t9("page.nav.delete", "\u522A\u9664\u9801\u9762");
|
|
3611
|
+
delBtn.textContent = "\xD7";
|
|
3612
|
+
delBtn.style.cssText = renameBtn.style.cssText;
|
|
3613
|
+
actions.append(addBtn, renameBtn, delBtn);
|
|
3614
|
+
nav.append(tabs, actions);
|
|
3615
|
+
parent.appendChild(nav);
|
|
3616
|
+
const tabBtnStyle = "padding:4px 10px;background:#21262d;color:#8b949e;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:11px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
|
3617
|
+
const tabBtnActiveStyle = tabBtnStyle.replace("#21262d", "#388bfd").replace("#8b949e", "#fff");
|
|
3618
|
+
const refresh = () => {
|
|
3619
|
+
tabs.replaceChildren();
|
|
3620
|
+
const preset = controller.getPreset();
|
|
3621
|
+
for (const page of preset.pages) {
|
|
3622
|
+
const btn = document.createElement("button");
|
|
3623
|
+
btn.type = "button";
|
|
3624
|
+
btn.textContent = page.title;
|
|
3625
|
+
btn.title = page.title;
|
|
3626
|
+
btn.style.cssText = page.id === controller.activePageId ? tabBtnActiveStyle : tabBtnStyle;
|
|
3627
|
+
btn.onclick = () => {
|
|
3628
|
+
if (controller.setActivePage(page.id)) {
|
|
3629
|
+
opts.onPageChange?.(page.id);
|
|
3630
|
+
}
|
|
3631
|
+
};
|
|
3632
|
+
tabs.appendChild(btn);
|
|
3633
|
+
}
|
|
3634
|
+
delBtn.disabled = preset.pages.length <= 1;
|
|
3635
|
+
};
|
|
3636
|
+
addBtn.onclick = () => {
|
|
3637
|
+
const title = window.prompt(t9("page.nav.addPrompt", "\u65B0\u9801\u9762\u540D\u7A31"), "");
|
|
3638
|
+
const id = controller.addPage(title ?? void 0);
|
|
3639
|
+
opts.onPageChange?.(id);
|
|
3640
|
+
};
|
|
3641
|
+
renameBtn.onclick = () => {
|
|
3642
|
+
const page = controller.getPreset().pages.find((p) => p.id === controller.activePageId);
|
|
3643
|
+
if (!page) return;
|
|
3644
|
+
let title = window.prompt(t9("page.nav.renamePrompt", "\u9801\u9762\u540D\u7A31"), page.title);
|
|
3645
|
+
if (title == null) return;
|
|
3646
|
+
while (title !== null && !controller.renamePage(page.id, title)) {
|
|
3647
|
+
title = window.prompt(
|
|
3648
|
+
t9("page.nav.renameEmpty", "\u540D\u7A31\u4E0D\u53EF\u70BA\u7A7A\uFF0C\u8ACB\u91CD\u65B0\u8F38\u5165"),
|
|
3649
|
+
title
|
|
3650
|
+
);
|
|
3651
|
+
if (title == null) return;
|
|
3652
|
+
}
|
|
3653
|
+
};
|
|
3654
|
+
delBtn.onclick = () => {
|
|
3655
|
+
const page = controller.getPreset().pages.find((p) => p.id === controller.activePageId);
|
|
3656
|
+
if (!page || controller.getPreset().pages.length <= 1) return;
|
|
3657
|
+
if (!window.confirm(t9("page.nav.deleteConfirm", `\u522A\u9664\u300C${page.title}\u300D\uFF1F`))) return;
|
|
3658
|
+
controller.removePage(page.id);
|
|
3659
|
+
opts.onPageChange?.(controller.activePageId);
|
|
3660
|
+
};
|
|
3661
|
+
const media = window.matchMedia(mq);
|
|
3662
|
+
const syncVisible = () => {
|
|
3663
|
+
nav.style.display = opts.alwaysVisible || media.matches ? "flex" : "none";
|
|
3664
|
+
};
|
|
3665
|
+
media.addEventListener("change", syncVisible);
|
|
3666
|
+
syncVisible();
|
|
3667
|
+
const unsub = controller.subscribe(refresh);
|
|
3668
|
+
refresh();
|
|
3669
|
+
return {
|
|
3670
|
+
el: nav,
|
|
3671
|
+
destroy: () => {
|
|
3672
|
+
unsub();
|
|
3673
|
+
media.removeEventListener("change", syncVisible);
|
|
3674
|
+
nav.remove();
|
|
3675
|
+
},
|
|
3676
|
+
refresh
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
// src/layer/bind-layer-time-scale-sync.ts
|
|
3681
|
+
function bindLayerTimeScaleSync(chart, controller, options) {
|
|
3682
|
+
const apply = () => {
|
|
3683
|
+
chart.applyTimeScaleSyncFromLayers(
|
|
3684
|
+
controller.getPreset().layers,
|
|
3685
|
+
controller.activePageId
|
|
3686
|
+
);
|
|
3687
|
+
options?.onSync?.();
|
|
3688
|
+
};
|
|
3689
|
+
apply();
|
|
3690
|
+
return controller.subscribe(apply);
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// src/layer/merge-preset.ts
|
|
3694
|
+
function upsertById(current, incoming) {
|
|
3695
|
+
if (!incoming?.length) return current.map((x) => ({ ...x }));
|
|
3696
|
+
const map = new Map(current.map((x) => [x.id, { ...x }]));
|
|
3697
|
+
for (const item of incoming) {
|
|
3698
|
+
if (!item?.id) continue;
|
|
3699
|
+
map.set(item.id, { ...item });
|
|
3700
|
+
}
|
|
3701
|
+
return [...map.values()];
|
|
3702
|
+
}
|
|
3703
|
+
function mergePages(current, incoming) {
|
|
3704
|
+
return upsertById(current, incoming);
|
|
3705
|
+
}
|
|
3706
|
+
function mergeLayers(current, incoming) {
|
|
3707
|
+
if (!incoming?.length) return current.map((l) => ({ ...l, frame: { ...l.frame } }));
|
|
3708
|
+
const map = new Map(current.map((l) => [l.id, { ...l, frame: { ...l.frame } }]));
|
|
3709
|
+
for (const raw of incoming) {
|
|
3710
|
+
if (!raw?.id) continue;
|
|
3711
|
+
const prev = map.get(raw.id);
|
|
3712
|
+
map.set(raw.id, {
|
|
3713
|
+
...prev ?? raw,
|
|
3714
|
+
...raw,
|
|
3715
|
+
frame: { ...prev?.frame ?? raw.frame ?? { x: 0, y: 0, w: 1, h: 1 } }
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
return [...map.values()];
|
|
3719
|
+
}
|
|
3720
|
+
function mergeGroups(current, incoming) {
|
|
3721
|
+
if (!incoming?.length) {
|
|
3722
|
+
return current.map((g) => ({ ...g, layerIds: [...g.layerIds] }));
|
|
3723
|
+
}
|
|
3724
|
+
const map = new Map(current.map((g) => [g.id, { ...g, layerIds: [...g.layerIds] }]));
|
|
3725
|
+
for (const raw of incoming) {
|
|
3726
|
+
if (!raw?.id) continue;
|
|
3727
|
+
map.set(raw.id, {
|
|
3728
|
+
id: raw.id,
|
|
3729
|
+
name: raw.name ?? map.get(raw.id)?.name,
|
|
3730
|
+
layerIds: [...raw.layerIds ?? map.get(raw.id)?.layerIds ?? []]
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
return [...map.values()];
|
|
3734
|
+
}
|
|
3735
|
+
function mergeLayoutPreset(current, partial) {
|
|
3736
|
+
return normalizeLayoutPreset({
|
|
3737
|
+
...current,
|
|
3738
|
+
...partial,
|
|
3739
|
+
version: LAYER_PRESET_VERSION,
|
|
3740
|
+
pages: mergePages(current.pages, partial.pages),
|
|
3741
|
+
layers: mergeLayers(current.layers, partial.layers),
|
|
3742
|
+
groups: mergeGroups(current.groups, partial.groups)
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
// src/layer/resolve-pane-layers.ts
|
|
3747
|
+
import {
|
|
3748
|
+
isValidLayerBridgePane,
|
|
3749
|
+
resolvePaneLayerIds as resolvePaneLayerIdsCore
|
|
3750
|
+
} from "@coderyo/core";
|
|
3751
|
+
var PANE_TO_LAYER_TYPE = {
|
|
3752
|
+
main: "chart.main",
|
|
3753
|
+
volume: "chart.volume",
|
|
3754
|
+
indicator: "chart.indicator"
|
|
3755
|
+
};
|
|
3756
|
+
function layerTypeForBridgePane(pane) {
|
|
3757
|
+
return PANE_TO_LAYER_TYPE[pane];
|
|
3758
|
+
}
|
|
3759
|
+
function resolvePaneLayerIds(preset, pane, opts) {
|
|
3760
|
+
return resolvePaneLayerIdsCore(preset, pane, opts);
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
// src/drawing-context-menu.ts
|
|
3764
|
+
import { t as t10 } from "@coderyo/i18n";
|
|
920
3765
|
function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
|
|
921
3766
|
const menu = document.createElement("div");
|
|
922
3767
|
menu.style.cssText = "position:fixed;z-index:60;min-width:168px;padding:4px 0;background:#161b22;border:1px solid #30363d;border-radius:6px;box-shadow:0 8px 24px #01040988;";
|
|
@@ -938,16 +3783,16 @@ function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
|
|
|
938
3783
|
};
|
|
939
3784
|
if (drawing) {
|
|
940
3785
|
const locked = Boolean(drawing.meta?.locked);
|
|
941
|
-
add(
|
|
942
|
-
add(
|
|
3786
|
+
add(t10("drawing.ctx.delete", "\u522A\u9664"), handlers.onDelete);
|
|
3787
|
+
add(t10("drawing.ctx.copy", "\u8907\u88FD"), handlers.onCopy);
|
|
943
3788
|
add(
|
|
944
|
-
locked ?
|
|
3789
|
+
locked ? t10("drawing.ctx.unlock", "\u89E3\u9396") : t10("drawing.ctx.lock", "\u9396\u5B9A"),
|
|
945
3790
|
handlers.onToggleLock
|
|
946
3791
|
);
|
|
947
3792
|
if (drawing.type === "text") {
|
|
948
|
-
add(
|
|
3793
|
+
add(t10("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
|
|
949
3794
|
}
|
|
950
|
-
add(
|
|
3795
|
+
add(t10("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
|
|
951
3796
|
}
|
|
952
3797
|
document.body.appendChild(menu);
|
|
953
3798
|
const close = () => {
|
|
@@ -999,7 +3844,7 @@ import {
|
|
|
999
3844
|
lineNumbers,
|
|
1000
3845
|
placeholder
|
|
1001
3846
|
} from "@codemirror/view";
|
|
1002
|
-
import { t as
|
|
3847
|
+
import { t as t11 } from "@coderyo/i18n";
|
|
1003
3848
|
|
|
1004
3849
|
// src/pine-language.ts
|
|
1005
3850
|
import { StreamLanguage } from "@codemirror/language";
|
|
@@ -1059,10 +3904,10 @@ function createPineLinter(onStatus) {
|
|
|
1059
3904
|
const src = view.state.doc.toString();
|
|
1060
3905
|
const result = compilePineLite(src);
|
|
1061
3906
|
if (result.ok) {
|
|
1062
|
-
onStatus?.(
|
|
3907
|
+
onStatus?.(t11("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA"), true);
|
|
1063
3908
|
return [];
|
|
1064
3909
|
}
|
|
1065
|
-
onStatus?.(result.errors[0] ??
|
|
3910
|
+
onStatus?.(result.errors[0] ?? t11("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4"), false);
|
|
1066
3911
|
return pineDiagnosticsToCm(src, result.diagnostics ?? []);
|
|
1067
3912
|
});
|
|
1068
3913
|
}
|
|
@@ -1091,16 +3936,16 @@ function mountPineEditorPanel(parent, opts = {}) {
|
|
|
1091
3936
|
wrap.open = true;
|
|
1092
3937
|
wrap.style.cssText = "flex-shrink:0;border-top:1px solid #30363d;background:#161b22;max-height:220px;display:flex;flex-direction:column;";
|
|
1093
3938
|
const summary = document.createElement("summary");
|
|
1094
|
-
summary.textContent =
|
|
3939
|
+
summary.textContent = t11("pine.editor.title", "Pine \u8173\u672C\u7DE8\u8F2F\u5668");
|
|
1095
3940
|
summary.style.cssText = "cursor:pointer;color:#58a6ff;user-select:none;padding:6px 12px;font-size:12px;flex-shrink:0;";
|
|
1096
3941
|
const toolbar = document.createElement("div");
|
|
1097
3942
|
toolbar.style.cssText = "display:flex;align-items:center;gap:8px;padding:0 12px 6px;flex-shrink:0;";
|
|
1098
3943
|
const status = document.createElement("span");
|
|
1099
3944
|
status.style.cssText = "font-size:11px;color:#8b949e;flex:1;";
|
|
1100
|
-
status.textContent =
|
|
3945
|
+
status.textContent = t11("pine.status.idle", "\u5C31\u7DD2");
|
|
1101
3946
|
const applyBtn = document.createElement("button");
|
|
1102
3947
|
applyBtn.type = "button";
|
|
1103
|
-
applyBtn.textContent =
|
|
3948
|
+
applyBtn.textContent = t11("pine.apply", "\u5957\u7528\u81F3\u5716\u8868");
|
|
1104
3949
|
applyBtn.style.cssText = "padding:4px 10px;background:#238636;color:#fff;border:1px solid #2ea043;border-radius:4px;cursor:pointer;font-size:11px;";
|
|
1105
3950
|
const host = document.createElement("div");
|
|
1106
3951
|
host.style.cssText = "flex:1;min-height:120px;max-height:160px;overflow:hidden;border-top:1px solid #30363d;";
|
|
@@ -1115,10 +3960,10 @@ function mountPineEditorPanel(parent, opts = {}) {
|
|
|
1115
3960
|
savePineScriptPreference(src);
|
|
1116
3961
|
if (r.ok) {
|
|
1117
3962
|
status.style.color = "#3fb950";
|
|
1118
|
-
status.textContent =
|
|
3963
|
+
status.textContent = t11("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA");
|
|
1119
3964
|
} else {
|
|
1120
3965
|
status.style.color = "#f85149";
|
|
1121
|
-
status.textContent = r.errors[0] ??
|
|
3966
|
+
status.textContent = r.errors[0] ?? t11("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4");
|
|
1122
3967
|
}
|
|
1123
3968
|
};
|
|
1124
3969
|
const extensions = [
|
|
@@ -1136,7 +3981,7 @@ function mountPineEditorPanel(parent, opts = {}) {
|
|
|
1136
3981
|
status.textContent = msg;
|
|
1137
3982
|
}),
|
|
1138
3983
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
1139
|
-
placeholder(
|
|
3984
|
+
placeholder(t11("pine.placeholder", "// plot(sma(close, 20))")),
|
|
1140
3985
|
EditorView.updateListener.of((u) => {
|
|
1141
3986
|
if (!u.docChanged) return;
|
|
1142
3987
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
@@ -1165,35 +4010,104 @@ function mountPineEditorPanel(parent, opts = {}) {
|
|
|
1165
4010
|
};
|
|
1166
4011
|
}
|
|
1167
4012
|
export {
|
|
4013
|
+
BUILTIN_PRESETS,
|
|
4014
|
+
CHART_PANE_LAYER_TYPES,
|
|
1168
4015
|
DEFAULT_LAYOUT_FEATURES,
|
|
4016
|
+
DEFAULT_LAYOUT_SCHEMA,
|
|
4017
|
+
DEFAULT_SYNC_TIME_SCALE_GROUP,
|
|
1169
4018
|
GRID_SETTING_KEY,
|
|
4019
|
+
LAYER_PRESET_VERSION,
|
|
4020
|
+
LAYOUT_SCHEMA_VERSION,
|
|
4021
|
+
LayerController,
|
|
4022
|
+
OVERLAY_LAYER_TYPES,
|
|
1170
4023
|
PINE_SCRIPT_STORAGE_KEY,
|
|
1171
4024
|
RETURN_CURSOR_KEY,
|
|
4025
|
+
THEME_STORAGE_KEY,
|
|
4026
|
+
VENDOR_COMPACT_PRESET,
|
|
4027
|
+
VENDOR_DEFAULT_PRESET,
|
|
4028
|
+
VENDOR_DUAL_SYNC_PRESET,
|
|
4029
|
+
applyCompositorShellFeatures,
|
|
4030
|
+
applyThemeToDocument,
|
|
1172
4031
|
attachChartContextMenu,
|
|
4032
|
+
attachLayerEditor,
|
|
4033
|
+
bindLayerTimeScaleSync,
|
|
1173
4034
|
bindShortcutsModal,
|
|
4035
|
+
clampBBox,
|
|
4036
|
+
clampFrame,
|
|
4037
|
+
cloneLayoutPreset,
|
|
4038
|
+
cloneLayoutSchema,
|
|
4039
|
+
createCompositorShell,
|
|
1174
4040
|
createDemoLayoutOptions,
|
|
4041
|
+
createI18nProvider,
|
|
4042
|
+
createLayoutGrid,
|
|
4043
|
+
createSymbolSearchDialog,
|
|
4044
|
+
createThemeProvider,
|
|
4045
|
+
deleteUserPreset,
|
|
4046
|
+
ensureOverlayLayers,
|
|
4047
|
+
expandLegacyChartHostLayers,
|
|
4048
|
+
forkPreset,
|
|
4049
|
+
getBuiltinPreset,
|
|
4050
|
+
getDrawingOverlayVisible,
|
|
4051
|
+
getLayersBoundingBox,
|
|
4052
|
+
getWidgetPlacement,
|
|
4053
|
+
isChartPaneLayerType,
|
|
4054
|
+
isOverlayLayerType,
|
|
4055
|
+
isValidLayerBridgePane as isValidBridgeLayerPane,
|
|
4056
|
+
layerTypeForBridgePane,
|
|
4057
|
+
layoutSchemaToPreset,
|
|
4058
|
+
layoutStorageKey,
|
|
4059
|
+
listPresets,
|
|
1175
4060
|
loadIndicatorConfig,
|
|
4061
|
+
loadLayoutSchema,
|
|
1176
4062
|
loadPineScriptPreference,
|
|
4063
|
+
loadPreset,
|
|
1177
4064
|
loadReturnToCursorPreference,
|
|
1178
4065
|
loadShowGridPreference,
|
|
4066
|
+
loadTheme,
|
|
1179
4067
|
mergeLayoutFeatures,
|
|
4068
|
+
mergeLayoutPreset,
|
|
1180
4069
|
mountChartLayout,
|
|
1181
4070
|
mountCodeSnippetPanel,
|
|
1182
4071
|
mountCrosshairLegend,
|
|
1183
4072
|
mountDrawingPropertiesPanel,
|
|
4073
|
+
mountLayerCompositor,
|
|
4074
|
+
mountLayerPanel,
|
|
4075
|
+
mountLogoSlot,
|
|
4076
|
+
mountPageNavigator,
|
|
1184
4077
|
mountPineEditorPanel,
|
|
1185
4078
|
mountSettingsPanel as mountSettingsMenu,
|
|
1186
4079
|
mountSettingsPanel,
|
|
1187
4080
|
mountStatusBar,
|
|
1188
4081
|
mountSymbolSearch,
|
|
4082
|
+
mountSymbolSearchDialogTrigger,
|
|
4083
|
+
mountThemeToggle,
|
|
1189
4084
|
mountTopBar,
|
|
4085
|
+
moveLayerFrames,
|
|
4086
|
+
normalizeLayoutPreset,
|
|
4087
|
+
normalizeLayoutSchema,
|
|
1190
4088
|
openDrawingContextMenu,
|
|
1191
4089
|
openShortcutsModal,
|
|
4090
|
+
paneIdFromWidgetKey,
|
|
1192
4091
|
pineLanguage,
|
|
4092
|
+
presetStorageKey,
|
|
4093
|
+
resizeGroupFrames,
|
|
1193
4094
|
resolveLayoutFeatures,
|
|
4095
|
+
resolveLayoutSchema,
|
|
4096
|
+
resolvePaneLayerIds,
|
|
4097
|
+
resolvePreset,
|
|
1194
4098
|
saveIndicatorConfig,
|
|
4099
|
+
saveLayoutSchema,
|
|
1195
4100
|
savePineScriptPreference,
|
|
4101
|
+
savePreset,
|
|
1196
4102
|
saveReturnToCursorPreference,
|
|
1197
|
-
saveShowGridPreference
|
|
4103
|
+
saveShowGridPreference,
|
|
4104
|
+
saveTheme,
|
|
4105
|
+
splitLegacyChartHost,
|
|
4106
|
+
syncAllOverlayLayersToMain,
|
|
4107
|
+
syncCompositorShellVisibilityFromFeatures,
|
|
4108
|
+
syncCrosshairLegendToMain,
|
|
4109
|
+
syncOverlayLayersToMain,
|
|
4110
|
+
upgradeIndicatorHostType,
|
|
4111
|
+
widgetKeyForChartPaneType
|
|
1198
4112
|
};
|
|
1199
4113
|
//# sourceMappingURL=index.js.map
|