@coderyo/ui-shell 1.0.3 → 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 +395 -2
- package/dist/index.js +2765 -181
- package/dist/index.js.map +1 -1
- package/package.json +20 -18
package/dist/index.js
CHANGED
|
@@ -73,7 +73,11 @@ function saveIndicatorConfig(symbol, interval, config) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// src/settings-panel.ts
|
|
76
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
DEFAULT_INDICATOR_CONFIG,
|
|
78
|
+
disableIndicatorLayer,
|
|
79
|
+
listActiveIndicatorLayers
|
|
80
|
+
} from "@coderyo/indicators";
|
|
77
81
|
import { t } from "@coderyo/i18n";
|
|
78
82
|
function mountSettingsPanel(parent, opts = {}) {
|
|
79
83
|
let open = false;
|
|
@@ -88,12 +92,27 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
88
92
|
btn.title = t("settings.title", "\u8A2D\u5B9A");
|
|
89
93
|
btn.textContent = "\u2699";
|
|
90
94
|
btn.style.cssText = "background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:14px;";
|
|
91
|
-
const
|
|
92
|
-
|
|
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);
|
|
93
112
|
const tabs = document.createElement("div");
|
|
94
|
-
tabs.style.cssText = "display:flex;border-bottom:1px solid #30363d;";
|
|
113
|
+
tabs.style.cssText = "display:flex;border-bottom:1px solid #30363d;flex-shrink:0;";
|
|
95
114
|
const content = document.createElement("div");
|
|
96
|
-
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;";
|
|
97
116
|
const tabIds = [
|
|
98
117
|
["chart", t("settings.tab.chart", "\u5716\u8868")],
|
|
99
118
|
["drawing", t("settings.tab.drawing", "\u7E6A\u5716")],
|
|
@@ -125,16 +144,16 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
125
144
|
row.append(input, document.createTextNode(label));
|
|
126
145
|
return row;
|
|
127
146
|
};
|
|
128
|
-
const actionButton = (label, onClick) => {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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) => {
|
|
134
153
|
e.stopPropagation();
|
|
135
154
|
onClick();
|
|
136
155
|
};
|
|
137
|
-
return
|
|
156
|
+
return b;
|
|
138
157
|
};
|
|
139
158
|
const numberField = (label, value, onChange) => {
|
|
140
159
|
const row = document.createElement("label");
|
|
@@ -148,6 +167,49 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
148
167
|
row.appendChild(input);
|
|
149
168
|
return row;
|
|
150
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
|
+
};
|
|
151
213
|
const renderContent = () => {
|
|
152
214
|
content.replaceChildren();
|
|
153
215
|
if (tab === "chart") {
|
|
@@ -166,31 +228,43 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
166
228
|
opts.onReturnToCursorChange?.(v);
|
|
167
229
|
})
|
|
168
230
|
);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
);
|
|
176
|
-
}
|
|
231
|
+
content.appendChild(
|
|
232
|
+
actionButton(t("settings.drawing.clearAll", "\u6E05\u9664\u6240\u6709\u756B\u7DDA"), () => {
|
|
233
|
+
opts.onClearAllDrawings?.();
|
|
234
|
+
renderContent();
|
|
235
|
+
})
|
|
236
|
+
);
|
|
177
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);
|
|
178
249
|
content.appendChild(
|
|
179
250
|
checkbox(t("settings.ind.macd", "MACD \u7A97\u683C"), indicatorConfig.showMacd, (v) => {
|
|
180
251
|
indicatorConfig = { ...indicatorConfig, showMacd: v };
|
|
181
252
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
253
|
+
renderContent();
|
|
182
254
|
})
|
|
183
255
|
);
|
|
184
256
|
content.appendChild(
|
|
185
257
|
checkbox(t("settings.ind.rsi", "RSI \u7A97\u683C"), indicatorConfig.showRsi, (v) => {
|
|
186
258
|
indicatorConfig = { ...indicatorConfig, showRsi: v };
|
|
187
259
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
260
|
+
renderContent();
|
|
188
261
|
})
|
|
189
262
|
);
|
|
190
263
|
content.appendChild(
|
|
191
264
|
checkbox(t("settings.ind.kdj", "KDJ \u7A97\u683C"), indicatorConfig.showKdj, (v) => {
|
|
192
265
|
indicatorConfig = { ...indicatorConfig, showKdj: v };
|
|
193
266
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
267
|
+
renderContent();
|
|
194
268
|
})
|
|
195
269
|
);
|
|
196
270
|
const src = document.createElement("label");
|
|
@@ -215,6 +289,7 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
215
289
|
checkbox(t("settings.ind.ema", "EMA \u758A\u52A0"), indicatorConfig.showEma, (v) => {
|
|
216
290
|
indicatorConfig = { ...indicatorConfig, showEma: v };
|
|
217
291
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
292
|
+
renderContent();
|
|
218
293
|
})
|
|
219
294
|
);
|
|
220
295
|
content.appendChild(
|
|
@@ -227,6 +302,7 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
227
302
|
checkbox(t("settings.ind.boll", "BOLL \u901A\u9053"), indicatorConfig.showBoll, (v) => {
|
|
228
303
|
indicatorConfig = { ...indicatorConfig, showBoll: v };
|
|
229
304
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
305
|
+
renderContent();
|
|
230
306
|
})
|
|
231
307
|
);
|
|
232
308
|
content.appendChild(
|
|
@@ -265,37 +341,50 @@ function mountSettingsPanel(parent, opts = {}) {
|
|
|
265
341
|
opts.onIndicatorConfigChange?.(indicatorConfig);
|
|
266
342
|
})
|
|
267
343
|
);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
);
|
|
276
|
-
}
|
|
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
|
+
);
|
|
277
351
|
}
|
|
278
352
|
};
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
btn.onclick = (e) => {
|
|
283
|
-
e.stopPropagation();
|
|
284
|
-
open = !open;
|
|
353
|
+
const setOpen = (next) => {
|
|
354
|
+
open = next;
|
|
355
|
+
overlay.style.display = open ? "flex" : "none";
|
|
285
356
|
if (open) {
|
|
286
357
|
if (opts.indicatorConfig) indicatorConfig = { ...opts.indicatorConfig };
|
|
287
358
|
renderContent();
|
|
288
359
|
}
|
|
289
|
-
panel.style.display = open ? "block" : "none";
|
|
290
360
|
};
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
361
|
+
renderTabs();
|
|
362
|
+
renderContent();
|
|
363
|
+
dialog.append(header, tabs, content);
|
|
364
|
+
overlay.appendChild(dialog);
|
|
365
|
+
document.body.appendChild(overlay);
|
|
366
|
+
btn.onclick = (e) => {
|
|
367
|
+
e.stopPropagation();
|
|
368
|
+
setOpen(!open);
|
|
294
369
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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);
|
|
298
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
|
+
};
|
|
299
388
|
return wrap;
|
|
300
389
|
}
|
|
301
390
|
|
|
@@ -713,6 +802,9 @@ function createI18nProvider(defaultLocale = "zh-TW") {
|
|
|
713
802
|
};
|
|
714
803
|
}
|
|
715
804
|
|
|
805
|
+
// src/chart-layout.ts
|
|
806
|
+
import { t as t7 } from "@coderyo/i18n";
|
|
807
|
+
|
|
716
808
|
// src/context-menu.ts
|
|
717
809
|
import { t as t3 } from "@coderyo/i18n";
|
|
718
810
|
function attachChartContextMenu(chartHost, opts = {}) {
|
|
@@ -778,7 +870,7 @@ function mountCrosshairLegend(chartHost, opts = {}) {
|
|
|
778
870
|
title.style.cssText = "color:#8b949e;margin-bottom:2px;";
|
|
779
871
|
const body = document.createElement("div");
|
|
780
872
|
box.append(title, body);
|
|
781
|
-
chartHost.appendChild(box);
|
|
873
|
+
if (chartHost) chartHost.appendChild(box);
|
|
782
874
|
const fmt2 = (n) => n == null ? "\u2014" : n.toLocaleString(void 0, { maximumFractionDigits: 2 });
|
|
783
875
|
const render = (payload) => {
|
|
784
876
|
const parts = [opts.symbol, opts.interval].filter(Boolean);
|
|
@@ -811,7 +903,7 @@ import { t as t4 } from "@coderyo/i18n";
|
|
|
811
903
|
function mountDrawingPropertiesPanel(parent, opts = {}) {
|
|
812
904
|
const panel = document.createElement("aside");
|
|
813
905
|
panel.className = "tv-drawing-props";
|
|
814
|
-
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;";
|
|
815
907
|
const title = document.createElement("div");
|
|
816
908
|
title.textContent = t4("drawing.props.title", "\u7E6A\u5716\u5C6C\u6027");
|
|
817
909
|
title.style.cssText = "font-weight:600;margin-bottom:10px;";
|
|
@@ -889,6 +981,414 @@ function mountIndicatorPaneHost(parent) {
|
|
|
889
981
|
return host;
|
|
890
982
|
}
|
|
891
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
|
+
|
|
892
1392
|
// src/layout-features.ts
|
|
893
1393
|
var DEFAULT_LAYOUT_FEATURES = {
|
|
894
1394
|
showTopBar: false,
|
|
@@ -1072,7 +1572,7 @@ var DRAWING_TOOLS = [
|
|
|
1072
1572
|
];
|
|
1073
1573
|
var MOBILE_MQ = "(max-width: 768px)";
|
|
1074
1574
|
function mountToolButtons(parent, activeTool, onSelect, layout) {
|
|
1075
|
-
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;";
|
|
1076
1576
|
const activeStyle = btnStyle.replace("#21262d", "#388bfd").replace("#e6edf3", "#fff");
|
|
1077
1577
|
const buttons = /* @__PURE__ */ new Map();
|
|
1078
1578
|
for (const tool of DRAWING_TOOLS) {
|
|
@@ -1095,21 +1595,26 @@ function mountToolButtons(parent, activeTool, onSelect, layout) {
|
|
|
1095
1595
|
}
|
|
1096
1596
|
function mountChartLayout(root, opts = {}) {
|
|
1097
1597
|
let layoutFeatures = resolveLayoutFeatures(opts);
|
|
1598
|
+
const layoutId = opts.layoutId ?? "default";
|
|
1599
|
+
root.replaceChildren();
|
|
1098
1600
|
root.style.display = "flex";
|
|
1099
1601
|
root.style.flexDirection = "column";
|
|
1100
|
-
root.style.height = "100%";
|
|
1101
|
-
root.style.width = "100%";
|
|
1602
|
+
root.style.height = root.style.height || "100%";
|
|
1603
|
+
root.style.width = root.style.width || "100%";
|
|
1102
1604
|
root.style.minWidth = "0";
|
|
1103
1605
|
root.style.boxSizing = "border-box";
|
|
1104
|
-
root.style.overflow = "
|
|
1105
|
-
const
|
|
1106
|
-
const
|
|
1107
|
-
themeProvider.
|
|
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) => {
|
|
1108
1612
|
root.style.background = theme === "dark" ? "#0d1117" : "#f6f8fa";
|
|
1109
1613
|
});
|
|
1110
1614
|
let activeTool = opts.activeDrawingTool ?? "cursor";
|
|
1111
1615
|
let setActiveDesktop = null;
|
|
1112
1616
|
let setActiveMobile = null;
|
|
1617
|
+
let lastSelectedDrawing = null;
|
|
1113
1618
|
const setActiveDrawingTool = (tool) => {
|
|
1114
1619
|
activeTool = tool;
|
|
1115
1620
|
setActiveDesktop?.(tool);
|
|
@@ -1119,69 +1624,173 @@ function mountChartLayout(root, opts = {}) {
|
|
|
1119
1624
|
setActiveDrawingTool(tool);
|
|
1120
1625
|
opts.onDrawingToolSelect?.(tool);
|
|
1121
1626
|
};
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
|
|
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;";
|
|
1134
1640
|
const chartHost = document.createElement("div");
|
|
1135
|
-
chartHost.
|
|
1136
|
-
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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, {
|
|
1141
1653
|
onStyleChange: opts.onDrawingStyleChange
|
|
1142
1654
|
});
|
|
1143
|
-
opts.
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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;
|
|
1148
1701
|
let setActiveInterval = () => {
|
|
1149
1702
|
};
|
|
1150
|
-
const crosshairLegend = mountCrosshairLegend(
|
|
1703
|
+
const crosshairLegend = mountCrosshairLegend(
|
|
1704
|
+
layerCompositorManaged ? void 0 : chartMain,
|
|
1705
|
+
{ symbol: opts.initialSymbol }
|
|
1706
|
+
);
|
|
1151
1707
|
let detachContextMenu = () => {
|
|
1152
1708
|
};
|
|
1153
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
|
+
};
|
|
1154
1734
|
const mountLeftToolbar = () => {
|
|
1155
|
-
|
|
1156
|
-
const desktopTools = mountToolButtons(
|
|
1735
|
+
leftToolbar.replaceChildren();
|
|
1736
|
+
const desktopTools = mountToolButtons(leftToolbar, activeTool, onToolSelect, "column");
|
|
1157
1737
|
setActiveDesktop = desktopTools.setActive;
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const mobileTools = mountToolButtons(
|
|
1738
|
+
mountToolbarActions(leftToolbar, true);
|
|
1739
|
+
bottomToolbar.replaceChildren();
|
|
1740
|
+
const mobileTools = mountToolButtons(bottomToolbar, activeTool, onToolSelect, "row");
|
|
1161
1741
|
setActiveMobile = mobileTools.setActive;
|
|
1162
|
-
|
|
1163
|
-
const applyLayout = () => {
|
|
1164
|
-
const mobile = mq.matches;
|
|
1165
|
-
leftAside.style.display = mobile ? "none" : "flex";
|
|
1166
|
-
const showBottom = layoutFeatures.showBottomToolbar !== false;
|
|
1167
|
-
bottomBar.style.display = mobile && showBottom ? "flex" : "none";
|
|
1168
|
-
};
|
|
1169
|
-
mq.addEventListener("change", applyLayout);
|
|
1170
|
-
applyLayout();
|
|
1742
|
+
mountToolbarActions(bottomToolbar, false);
|
|
1171
1743
|
};
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
+
}
|
|
1177
1787
|
};
|
|
1178
1788
|
const applyLayoutFeatures = () => {
|
|
1179
1789
|
const f = layoutFeatures;
|
|
1180
1790
|
if (f.showTopBar) {
|
|
1181
|
-
|
|
1182
|
-
headerSlot.style.display = "";
|
|
1791
|
+
topBarHost.replaceChildren();
|
|
1183
1792
|
const mounted = mountTopBar(
|
|
1184
|
-
|
|
1793
|
+
topBarHost,
|
|
1185
1794
|
Object.assign(opts, {
|
|
1186
1795
|
symbolInput: f.symbolInput,
|
|
1187
1796
|
showSettings: f.showSettings,
|
|
@@ -1196,18 +1805,44 @@ function mountChartLayout(root, opts = {}) {
|
|
|
1196
1805
|
topBar = mounted.el;
|
|
1197
1806
|
setActiveInterval = mounted.setActiveInterval;
|
|
1198
1807
|
} else {
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
+
}
|
|
1208
1843
|
detachContextMenu();
|
|
1209
1844
|
if (f.showContextMenu) {
|
|
1210
|
-
detachContextMenu = attachChartContextMenu(
|
|
1845
|
+
detachContextMenu = attachChartContextMenu(chartMain, {
|
|
1211
1846
|
actions: opts.contextMenuActions
|
|
1212
1847
|
});
|
|
1213
1848
|
}
|
|
@@ -1215,10 +1850,36 @@ function mountChartLayout(root, opts = {}) {
|
|
|
1215
1850
|
bindShortcutsModal();
|
|
1216
1851
|
shortcutsBound = true;
|
|
1217
1852
|
}
|
|
1853
|
+
applyPropertiesPanelLayout(lastSelectedDrawing);
|
|
1218
1854
|
};
|
|
1855
|
+
const handleDrawingSelection = (drawing) => {
|
|
1856
|
+
lastSelectedDrawing = drawing;
|
|
1857
|
+
propertiesPanel.bind(drawing);
|
|
1858
|
+
applyPropertiesPanelLayout(drawing);
|
|
1859
|
+
};
|
|
1860
|
+
opts.onDrawingSelectionBind?.(propertiesPanel.bind);
|
|
1219
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;
|
|
1220
1876
|
return {
|
|
1221
|
-
|
|
1877
|
+
layoutRoot: layoutShell.root,
|
|
1878
|
+
layoutGrid: layoutShell.grid,
|
|
1879
|
+
chartHost: chartMain,
|
|
1880
|
+
chartMain,
|
|
1881
|
+
chartVolume,
|
|
1882
|
+
drawingOverlay,
|
|
1222
1883
|
indicatorHost,
|
|
1223
1884
|
topBar,
|
|
1224
1885
|
setActiveInterval,
|
|
@@ -1227,86 +1888,1950 @@ function mountChartLayout(root, opts = {}) {
|
|
|
1227
1888
|
detachContextMenu,
|
|
1228
1889
|
setActiveDrawingTool,
|
|
1229
1890
|
propertiesPanel,
|
|
1891
|
+
handleDrawingSelection,
|
|
1892
|
+
syncCompositorShellVisibility,
|
|
1893
|
+
bindLayerCompositorController,
|
|
1230
1894
|
setLayoutFeatures: (patch) => {
|
|
1231
1895
|
layoutFeatures = mergeLayoutFeatures(layoutFeatures, patch);
|
|
1232
1896
|
applyLayoutFeatures();
|
|
1897
|
+
syncCompositorShellVisibilityIfBound();
|
|
1233
1898
|
},
|
|
1234
|
-
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
|
+
}
|
|
1235
1917
|
};
|
|
1236
1918
|
}
|
|
1237
1919
|
|
|
1238
|
-
// src/
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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)
|
|
1256
1943
|
};
|
|
1257
|
-
|
|
1258
|
-
|
|
1944
|
+
}
|
|
1945
|
+
const drawing = layers.find((l) => l.type === "overlay.drawing" && l.pageId === pageId);
|
|
1259
1946
|
if (drawing) {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
)
|
|
1267
|
-
|
|
1268
|
-
add(t7("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
|
|
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;
|
|
1269
1955
|
}
|
|
1270
|
-
add(t7("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
|
|
1271
1956
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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;
|
|
1279
1998
|
}
|
|
1280
1999
|
|
|
1281
|
-
// src/
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
|
1298
2062
|
};
|
|
1299
|
-
|
|
1300
|
-
|
|
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
|
|
1301
2076
|
};
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
return
|
|
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
|
+
});
|
|
1305
2094
|
}
|
|
1306
2095
|
|
|
1307
|
-
// src/
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
|
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";
|
|
3765
|
+
function openDrawingContextMenu(clientX, clientY, drawing, handlers) {
|
|
3766
|
+
const menu = document.createElement("div");
|
|
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;";
|
|
3768
|
+
menu.style.left = `${clientX}px`;
|
|
3769
|
+
menu.style.top = `${clientY}px`;
|
|
3770
|
+
const add = (label, fn, disabled = false) => {
|
|
3771
|
+
if (!fn) return;
|
|
3772
|
+
const btn = document.createElement("button");
|
|
3773
|
+
btn.type = "button";
|
|
3774
|
+
btn.textContent = label;
|
|
3775
|
+
btn.disabled = disabled;
|
|
3776
|
+
btn.style.cssText = "display:block;width:100%;text-align:left;padding:8px 12px;border:none;background:transparent;color:#e6edf3;font-size:12px;cursor:pointer;";
|
|
3777
|
+
if (disabled) btn.style.opacity = "0.45";
|
|
3778
|
+
btn.onclick = () => {
|
|
3779
|
+
fn();
|
|
3780
|
+
close();
|
|
3781
|
+
};
|
|
3782
|
+
menu.appendChild(btn);
|
|
3783
|
+
};
|
|
3784
|
+
if (drawing) {
|
|
3785
|
+
const locked = Boolean(drawing.meta?.locked);
|
|
3786
|
+
add(t10("drawing.ctx.delete", "\u522A\u9664"), handlers.onDelete);
|
|
3787
|
+
add(t10("drawing.ctx.copy", "\u8907\u88FD"), handlers.onCopy);
|
|
3788
|
+
add(
|
|
3789
|
+
locked ? t10("drawing.ctx.unlock", "\u89E3\u9396") : t10("drawing.ctx.lock", "\u9396\u5B9A"),
|
|
3790
|
+
handlers.onToggleLock
|
|
3791
|
+
);
|
|
3792
|
+
if (drawing.type === "text") {
|
|
3793
|
+
add(t10("drawing.ctx.editText", "\u7DE8\u8F2F\u6587\u5B57"), handlers.onEditText);
|
|
3794
|
+
}
|
|
3795
|
+
add(t10("drawing.ctx.deselect", "\u53D6\u6D88\u9078\u53D6"), handlers.onDeselect);
|
|
3796
|
+
}
|
|
3797
|
+
document.body.appendChild(menu);
|
|
3798
|
+
const close = () => {
|
|
3799
|
+
menu.remove();
|
|
3800
|
+
document.removeEventListener("click", close);
|
|
3801
|
+
};
|
|
3802
|
+
setTimeout(() => document.addEventListener("click", close), 0);
|
|
3803
|
+
return close;
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
// src/code-snippet-panel.ts
|
|
3807
|
+
function mountCodeSnippetPanel(parent, getCode) {
|
|
3808
|
+
const wrap = document.createElement("details");
|
|
3809
|
+
wrap.style.cssText = "flex-shrink:0;border-top:1px solid #30363d;background:#161b22;padding:6px 12px;font-size:11px;color:#8b949e;";
|
|
3810
|
+
const summary = document.createElement("summary");
|
|
3811
|
+
summary.textContent = "\u5D4C\u5165\u7A0B\u5F0F\u78BC\uFF08\u6574\u5408\u65B9\uFF09";
|
|
3812
|
+
summary.style.cssText = "cursor:pointer;color:#58a6ff;user-select:none;";
|
|
3813
|
+
const pre = document.createElement("pre");
|
|
3814
|
+
pre.style.cssText = "margin:8px 0 0;padding:10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:11px;overflow:auto;max-height:160px;white-space:pre-wrap;";
|
|
3815
|
+
const copyBtn = document.createElement("button");
|
|
3816
|
+
copyBtn.type = "button";
|
|
3817
|
+
copyBtn.textContent = "\u8907\u88FD";
|
|
3818
|
+
copyBtn.style.cssText = "margin-top:6px;padding:4px 10px;background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:4px;cursor:pointer;font-size:11px;";
|
|
3819
|
+
copyBtn.onclick = () => {
|
|
3820
|
+
const code = getCode();
|
|
3821
|
+
pre.textContent = code;
|
|
3822
|
+
void navigator.clipboard.writeText(code);
|
|
3823
|
+
};
|
|
3824
|
+
wrap.ontoggle = () => {
|
|
3825
|
+
if (wrap.open) pre.textContent = getCode();
|
|
3826
|
+
};
|
|
3827
|
+
wrap.append(summary, pre, copyBtn);
|
|
3828
|
+
parent.appendChild(wrap);
|
|
3829
|
+
return wrap;
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
// src/pine-editor-panel.ts
|
|
3833
|
+
import { compilePineLite } from "@coderyo/pine-lite";
|
|
3834
|
+
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
1310
3835
|
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching } from "@codemirror/language";
|
|
1311
3836
|
import { linter } from "@codemirror/lint";
|
|
1312
3837
|
import { EditorState } from "@codemirror/state";
|
|
@@ -1319,7 +3844,7 @@ import {
|
|
|
1319
3844
|
lineNumbers,
|
|
1320
3845
|
placeholder
|
|
1321
3846
|
} from "@codemirror/view";
|
|
1322
|
-
import { t as
|
|
3847
|
+
import { t as t11 } from "@coderyo/i18n";
|
|
1323
3848
|
|
|
1324
3849
|
// src/pine-language.ts
|
|
1325
3850
|
import { StreamLanguage } from "@codemirror/language";
|
|
@@ -1379,10 +3904,10 @@ function createPineLinter(onStatus) {
|
|
|
1379
3904
|
const src = view.state.doc.toString();
|
|
1380
3905
|
const result = compilePineLite(src);
|
|
1381
3906
|
if (result.ok) {
|
|
1382
|
-
onStatus?.(
|
|
3907
|
+
onStatus?.(t11("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA"), true);
|
|
1383
3908
|
return [];
|
|
1384
3909
|
}
|
|
1385
|
-
onStatus?.(result.errors[0] ??
|
|
3910
|
+
onStatus?.(result.errors[0] ?? t11("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4"), false);
|
|
1386
3911
|
return pineDiagnosticsToCm(src, result.diagnostics ?? []);
|
|
1387
3912
|
});
|
|
1388
3913
|
}
|
|
@@ -1411,16 +3936,16 @@ function mountPineEditorPanel(parent, opts = {}) {
|
|
|
1411
3936
|
wrap.open = true;
|
|
1412
3937
|
wrap.style.cssText = "flex-shrink:0;border-top:1px solid #30363d;background:#161b22;max-height:220px;display:flex;flex-direction:column;";
|
|
1413
3938
|
const summary = document.createElement("summary");
|
|
1414
|
-
summary.textContent =
|
|
3939
|
+
summary.textContent = t11("pine.editor.title", "Pine \u8173\u672C\u7DE8\u8F2F\u5668");
|
|
1415
3940
|
summary.style.cssText = "cursor:pointer;color:#58a6ff;user-select:none;padding:6px 12px;font-size:12px;flex-shrink:0;";
|
|
1416
3941
|
const toolbar = document.createElement("div");
|
|
1417
3942
|
toolbar.style.cssText = "display:flex;align-items:center;gap:8px;padding:0 12px 6px;flex-shrink:0;";
|
|
1418
3943
|
const status = document.createElement("span");
|
|
1419
3944
|
status.style.cssText = "font-size:11px;color:#8b949e;flex:1;";
|
|
1420
|
-
status.textContent =
|
|
3945
|
+
status.textContent = t11("pine.status.idle", "\u5C31\u7DD2");
|
|
1421
3946
|
const applyBtn = document.createElement("button");
|
|
1422
3947
|
applyBtn.type = "button";
|
|
1423
|
-
applyBtn.textContent =
|
|
3948
|
+
applyBtn.textContent = t11("pine.apply", "\u5957\u7528\u81F3\u5716\u8868");
|
|
1424
3949
|
applyBtn.style.cssText = "padding:4px 10px;background:#238636;color:#fff;border:1px solid #2ea043;border-radius:4px;cursor:pointer;font-size:11px;";
|
|
1425
3950
|
const host = document.createElement("div");
|
|
1426
3951
|
host.style.cssText = "flex:1;min-height:120px;max-height:160px;overflow:hidden;border-top:1px solid #30363d;";
|
|
@@ -1435,10 +3960,10 @@ function mountPineEditorPanel(parent, opts = {}) {
|
|
|
1435
3960
|
savePineScriptPreference(src);
|
|
1436
3961
|
if (r.ok) {
|
|
1437
3962
|
status.style.color = "#3fb950";
|
|
1438
|
-
status.textContent =
|
|
3963
|
+
status.textContent = t11("pine.status.ok", "\u8A9E\u6CD5\u6B63\u78BA");
|
|
1439
3964
|
} else {
|
|
1440
3965
|
status.style.color = "#f85149";
|
|
1441
|
-
status.textContent = r.errors[0] ??
|
|
3966
|
+
status.textContent = r.errors[0] ?? t11("pine.status.error", "\u8A9E\u6CD5\u932F\u8AA4");
|
|
1442
3967
|
}
|
|
1443
3968
|
};
|
|
1444
3969
|
const extensions = [
|
|
@@ -1456,7 +3981,7 @@ function mountPineEditorPanel(parent, opts = {}) {
|
|
|
1456
3981
|
status.textContent = msg;
|
|
1457
3982
|
}),
|
|
1458
3983
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
1459
|
-
placeholder(
|
|
3984
|
+
placeholder(t11("pine.placeholder", "// plot(sma(close, 20))")),
|
|
1460
3985
|
EditorView.updateListener.of((u) => {
|
|
1461
3986
|
if (!u.docChanged) return;
|
|
1462
3987
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
@@ -1485,29 +4010,70 @@ function mountPineEditorPanel(parent, opts = {}) {
|
|
|
1485
4010
|
};
|
|
1486
4011
|
}
|
|
1487
4012
|
export {
|
|
4013
|
+
BUILTIN_PRESETS,
|
|
4014
|
+
CHART_PANE_LAYER_TYPES,
|
|
1488
4015
|
DEFAULT_LAYOUT_FEATURES,
|
|
4016
|
+
DEFAULT_LAYOUT_SCHEMA,
|
|
4017
|
+
DEFAULT_SYNC_TIME_SCALE_GROUP,
|
|
1489
4018
|
GRID_SETTING_KEY,
|
|
4019
|
+
LAYER_PRESET_VERSION,
|
|
4020
|
+
LAYOUT_SCHEMA_VERSION,
|
|
4021
|
+
LayerController,
|
|
4022
|
+
OVERLAY_LAYER_TYPES,
|
|
1490
4023
|
PINE_SCRIPT_STORAGE_KEY,
|
|
1491
4024
|
RETURN_CURSOR_KEY,
|
|
1492
4025
|
THEME_STORAGE_KEY,
|
|
4026
|
+
VENDOR_COMPACT_PRESET,
|
|
4027
|
+
VENDOR_DEFAULT_PRESET,
|
|
4028
|
+
VENDOR_DUAL_SYNC_PRESET,
|
|
4029
|
+
applyCompositorShellFeatures,
|
|
1493
4030
|
applyThemeToDocument,
|
|
1494
4031
|
attachChartContextMenu,
|
|
4032
|
+
attachLayerEditor,
|
|
4033
|
+
bindLayerTimeScaleSync,
|
|
1495
4034
|
bindShortcutsModal,
|
|
4035
|
+
clampBBox,
|
|
4036
|
+
clampFrame,
|
|
4037
|
+
cloneLayoutPreset,
|
|
4038
|
+
cloneLayoutSchema,
|
|
4039
|
+
createCompositorShell,
|
|
1496
4040
|
createDemoLayoutOptions,
|
|
1497
4041
|
createI18nProvider,
|
|
4042
|
+
createLayoutGrid,
|
|
1498
4043
|
createSymbolSearchDialog,
|
|
1499
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,
|
|
1500
4060
|
loadIndicatorConfig,
|
|
4061
|
+
loadLayoutSchema,
|
|
1501
4062
|
loadPineScriptPreference,
|
|
4063
|
+
loadPreset,
|
|
1502
4064
|
loadReturnToCursorPreference,
|
|
1503
4065
|
loadShowGridPreference,
|
|
1504
4066
|
loadTheme,
|
|
1505
4067
|
mergeLayoutFeatures,
|
|
4068
|
+
mergeLayoutPreset,
|
|
1506
4069
|
mountChartLayout,
|
|
1507
4070
|
mountCodeSnippetPanel,
|
|
1508
4071
|
mountCrosshairLegend,
|
|
1509
4072
|
mountDrawingPropertiesPanel,
|
|
4073
|
+
mountLayerCompositor,
|
|
4074
|
+
mountLayerPanel,
|
|
1510
4075
|
mountLogoSlot,
|
|
4076
|
+
mountPageNavigator,
|
|
1511
4077
|
mountPineEditorPanel,
|
|
1512
4078
|
mountSettingsPanel as mountSettingsMenu,
|
|
1513
4079
|
mountSettingsPanel,
|
|
@@ -1516,14 +4082,32 @@ export {
|
|
|
1516
4082
|
mountSymbolSearchDialogTrigger,
|
|
1517
4083
|
mountThemeToggle,
|
|
1518
4084
|
mountTopBar,
|
|
4085
|
+
moveLayerFrames,
|
|
4086
|
+
normalizeLayoutPreset,
|
|
4087
|
+
normalizeLayoutSchema,
|
|
1519
4088
|
openDrawingContextMenu,
|
|
1520
4089
|
openShortcutsModal,
|
|
4090
|
+
paneIdFromWidgetKey,
|
|
1521
4091
|
pineLanguage,
|
|
4092
|
+
presetStorageKey,
|
|
4093
|
+
resizeGroupFrames,
|
|
1522
4094
|
resolveLayoutFeatures,
|
|
4095
|
+
resolveLayoutSchema,
|
|
4096
|
+
resolvePaneLayerIds,
|
|
4097
|
+
resolvePreset,
|
|
1523
4098
|
saveIndicatorConfig,
|
|
4099
|
+
saveLayoutSchema,
|
|
1524
4100
|
savePineScriptPreference,
|
|
4101
|
+
savePreset,
|
|
1525
4102
|
saveReturnToCursorPreference,
|
|
1526
4103
|
saveShowGridPreference,
|
|
1527
|
-
saveTheme
|
|
4104
|
+
saveTheme,
|
|
4105
|
+
splitLegacyChartHost,
|
|
4106
|
+
syncAllOverlayLayersToMain,
|
|
4107
|
+
syncCompositorShellVisibilityFromFeatures,
|
|
4108
|
+
syncCrosshairLegendToMain,
|
|
4109
|
+
syncOverlayLayersToMain,
|
|
4110
|
+
upgradeIndicatorHostType,
|
|
4111
|
+
widgetKeyForChartPaneType
|
|
1528
4112
|
};
|
|
1529
4113
|
//# sourceMappingURL=index.js.map
|