@baron1996/klinecharts-runtime 0.7.2 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/drawing/capabilities.d.ts +10 -2
- package/dist/drawing/capabilities.d.ts.map +1 -1
- package/dist/drawing/scene-runtime-factory.d.ts +6 -1
- package/dist/drawing/scene-runtime-factory.d.ts.map +1 -1
- package/dist/drawing/scene-runtime-factory.js +1 -1
- package/dist/drawing/session-controller.d.ts +1 -0
- package/dist/drawing/session-controller.d.ts.map +1 -1
- package/dist/drawing/session-controller.js +40 -2
- package/dist/drawing/workspace-events.d.ts +13 -0
- package/dist/drawing/workspace-events.d.ts.map +1 -1
- package/dist/drawing/workspace-runtime.d.ts +12 -4
- package/dist/drawing/workspace-runtime.d.ts.map +1 -1
- package/dist/drawing/workspace-runtime.js +87 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +26 -0
- package/dist/toolbar/drawing-floating-toolbar-styles.d.ts +2 -0
- package/dist/toolbar/drawing-floating-toolbar-styles.d.ts.map +1 -0
- package/dist/toolbar/drawing-floating-toolbar-styles.js +250 -0
- package/dist/toolbar/drawing-floating-toolbar.d.ts +8 -0
- package/dist/toolbar/drawing-floating-toolbar.d.ts.map +1 -0
- package/dist/toolbar/drawing-floating-toolbar.js +373 -0
- package/dist/toolbar/standard-toolbar-styles.d.ts.map +1 -1
- package/dist/toolbar/standard-toolbar-styles.js +0 -18
- package/dist/toolbar/standard-toolbar.d.ts.map +1 -1
- package/dist/toolbar/standard-toolbar.js +15 -172
- package/dist/toolbar/toolbar-icons.d.ts +22 -0
- package/dist/toolbar/toolbar-icons.d.ts.map +1 -1
- package/dist/toolbar/toolbar-icons.js +8 -0
- package/dist/toolbar/toolbar-tools.d.ts +1 -6
- package/dist/toolbar/toolbar-tools.d.ts.map +1 -1
- package/dist/toolbar/toolbar-tools.js +0 -6
- package/dist/types.d.ts +9 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/runtime.js
CHANGED
|
@@ -72,6 +72,8 @@ export class KLineSceneRuntime {
|
|
|
72
72
|
#unsubscribeCrosshair = null;
|
|
73
73
|
/** 当前选中标注的稳定 ID。 */
|
|
74
74
|
#selectedOverlayId = null;
|
|
75
|
+
/** 供对象级编辑器订阅 Drawing 变化,不暴露 Adapter 事件。 */
|
|
76
|
+
#drawingChangeListeners = new Set();
|
|
75
77
|
/** 确定性标注 ID 递增序号。 */
|
|
76
78
|
#overlaySequence = 0;
|
|
77
79
|
/** 确定性指标 ID 递增序号。 */
|
|
@@ -104,6 +106,11 @@ export class KLineSceneRuntime {
|
|
|
104
106
|
this.#selectedOverlayId = null;
|
|
105
107
|
}
|
|
106
108
|
this.#events.emit(toRuntimeEvent(event));
|
|
109
|
+
if (event.type.startsWith('overlay-')) {
|
|
110
|
+
for (const listener of this.#drawingChangeListeners) {
|
|
111
|
+
listener();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
107
114
|
});
|
|
108
115
|
this.#unsubscribeCrosshair = adapter.subscribeCrosshair((snapshot) => {
|
|
109
116
|
this.#events.emit({
|
|
@@ -281,6 +288,13 @@ export class KLineSceneRuntime {
|
|
|
281
288
|
}
|
|
282
289
|
return overlayToDrawingSnapshot(this.updateOverlay({ ...overlay, text }), this.getScene().period);
|
|
283
290
|
}
|
|
291
|
+
updateDrawingLocked(id, locked) {
|
|
292
|
+
const overlay = this.getOverlay(id);
|
|
293
|
+
if (overlay === undefined) {
|
|
294
|
+
throw new SceneError('INVALID_REFERENCE', `/overlays/${id}`, `Overlay ${id} does not exist.`);
|
|
295
|
+
}
|
|
296
|
+
return overlayToDrawingSnapshot(this.updateOverlay({ ...overlay, locked }), this.getScene().period);
|
|
297
|
+
}
|
|
284
298
|
removeDrawing(id) {
|
|
285
299
|
return this.removeOverlay(id);
|
|
286
300
|
}
|
|
@@ -296,6 +310,17 @@ export class KLineSceneRuntime {
|
|
|
296
310
|
hitTestDrawing(point) {
|
|
297
311
|
return this.#adapter.hitTestOverlay(point)?.overlayId ?? null;
|
|
298
312
|
}
|
|
313
|
+
getDrawingMutationState() {
|
|
314
|
+
this.#assertActive();
|
|
315
|
+
return 'ready';
|
|
316
|
+
}
|
|
317
|
+
subscribeDrawingChanges(listener) {
|
|
318
|
+
this.#assertActive();
|
|
319
|
+
this.#drawingChangeListeners.add(listener);
|
|
320
|
+
return () => {
|
|
321
|
+
this.#drawingChangeListeners.delete(listener);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
299
324
|
exportArtifact(fileName = this.#defaultFileName) {
|
|
300
325
|
return {
|
|
301
326
|
bytes: serializeCanonicalScene(this.exportScene()),
|
|
@@ -436,6 +461,7 @@ export class KLineSceneRuntime {
|
|
|
436
461
|
this.#adapter.dispose();
|
|
437
462
|
this.#unsubscribeAdapter();
|
|
438
463
|
this.#events.clear();
|
|
464
|
+
this.#drawingChangeListeners.clear();
|
|
439
465
|
}
|
|
440
466
|
}
|
|
441
467
|
export async function createKLineSceneRuntime(container, scene, options = {}) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawing-floating-toolbar-styles.d.ts","sourceRoot":"","sources":["../../src/toolbar/drawing-floating-toolbar-styles.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,QAyP3C,CAAC"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
export const DRAWING_FLOATING_TOOLBAR_STYLES = String.raw `
|
|
2
|
+
.baron-drawing-toolbar,
|
|
3
|
+
.baron-drawing-toolbar * {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.baron-drawing-toolbar {
|
|
8
|
+
position: fixed;
|
|
9
|
+
z-index: 9998;
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
min-height: 48px;
|
|
13
|
+
padding: 5px 7px 5px 4px;
|
|
14
|
+
color: rgba(32, 35, 42, 1);
|
|
15
|
+
background: rgba(255, 255, 255, 0.98);
|
|
16
|
+
border: 1px solid rgba(218, 220, 226, 1);
|
|
17
|
+
border-radius: 12px;
|
|
18
|
+
box-shadow:
|
|
19
|
+
0 2px 5px rgba(20, 29, 48, 0.08),
|
|
20
|
+
0 10px 28px rgba(20, 29, 48, 0.13);
|
|
21
|
+
font-family: "Baron Sans", "Noto Sans SC", sans-serif;
|
|
22
|
+
font-size: 12px;
|
|
23
|
+
line-height: 1;
|
|
24
|
+
user-select: none;
|
|
25
|
+
transition:
|
|
26
|
+
opacity 120ms ease,
|
|
27
|
+
box-shadow 120ms ease;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.baron-drawing-toolbar[hidden] {
|
|
31
|
+
display: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.baron-drawing-toolbar[data-dragging="true"] {
|
|
35
|
+
box-shadow:
|
|
36
|
+
0 3px 8px rgba(20, 29, 48, 0.12),
|
|
37
|
+
0 16px 40px rgba(20, 29, 48, 0.18);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.baron-drawing-toolbar__grip {
|
|
41
|
+
display: grid;
|
|
42
|
+
flex: 0 0 auto;
|
|
43
|
+
grid-template-columns: repeat(2, 3px);
|
|
44
|
+
gap: 3px;
|
|
45
|
+
width: 24px;
|
|
46
|
+
height: 36px;
|
|
47
|
+
padding: 8px 6px;
|
|
48
|
+
place-content: center;
|
|
49
|
+
color: rgba(153, 158, 170, 1);
|
|
50
|
+
background: transparent;
|
|
51
|
+
border: 0;
|
|
52
|
+
border-radius: 7px;
|
|
53
|
+
cursor: grab;
|
|
54
|
+
touch-action: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.baron-drawing-toolbar__grip:active {
|
|
58
|
+
cursor: grabbing;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.baron-drawing-toolbar__grip-dot {
|
|
62
|
+
width: 3px;
|
|
63
|
+
height: 3px;
|
|
64
|
+
background: currentcolor;
|
|
65
|
+
border-radius: 50%;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.baron-drawing-toolbar__type,
|
|
69
|
+
.baron-drawing-toolbar__button,
|
|
70
|
+
.baron-drawing-toolbar__control {
|
|
71
|
+
position: relative;
|
|
72
|
+
display: grid;
|
|
73
|
+
flex: 0 0 auto;
|
|
74
|
+
height: 36px;
|
|
75
|
+
place-items: center;
|
|
76
|
+
color: rgba(32, 35, 42, 1);
|
|
77
|
+
background: transparent;
|
|
78
|
+
border: 0;
|
|
79
|
+
border-radius: 8px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.baron-drawing-toolbar__type,
|
|
83
|
+
.baron-drawing-toolbar__button {
|
|
84
|
+
width: 38px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.baron-drawing-toolbar__type {
|
|
88
|
+
color: rgba(62, 68, 80, 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.baron-drawing-toolbar__type svg,
|
|
92
|
+
.baron-drawing-toolbar__button svg {
|
|
93
|
+
width: 21px;
|
|
94
|
+
height: 21px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.baron-drawing-toolbar__button {
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
transition:
|
|
100
|
+
color 120ms ease,
|
|
101
|
+
background-color 120ms ease;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.baron-drawing-toolbar__button:hover:not(:disabled),
|
|
105
|
+
.baron-drawing-toolbar__grip:hover {
|
|
106
|
+
color: rgba(41, 98, 255, 1);
|
|
107
|
+
background: rgba(41, 98, 255, 0.08);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.baron-drawing-toolbar__button:focus-visible,
|
|
111
|
+
.baron-drawing-toolbar__grip:focus-visible,
|
|
112
|
+
.baron-drawing-toolbar__control:focus-within {
|
|
113
|
+
outline: 2px solid rgba(41, 98, 255, 1);
|
|
114
|
+
outline-offset: 1px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.baron-drawing-toolbar__button:disabled,
|
|
118
|
+
.baron-drawing-toolbar__control:has(:disabled) {
|
|
119
|
+
cursor: not-allowed;
|
|
120
|
+
opacity: 0.42;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.baron-drawing-toolbar__separator {
|
|
124
|
+
flex: 0 0 auto;
|
|
125
|
+
width: 1px;
|
|
126
|
+
height: 26px;
|
|
127
|
+
margin: 0 4px;
|
|
128
|
+
background: rgba(218, 220, 226, 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.baron-drawing-toolbar__control {
|
|
132
|
+
min-width: 48px;
|
|
133
|
+
padding: 0 5px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.baron-drawing-toolbar__control select {
|
|
137
|
+
height: 30px;
|
|
138
|
+
padding: 0 20px 0 7px;
|
|
139
|
+
color: inherit;
|
|
140
|
+
background: transparent;
|
|
141
|
+
border: 0;
|
|
142
|
+
font: inherit;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
outline: 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.baron-drawing-toolbar__control--style select {
|
|
148
|
+
min-width: 66px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.baron-drawing-toolbar__control--width select {
|
|
152
|
+
min-width: 58px;
|
|
153
|
+
font-size: 15px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.baron-drawing-toolbar__color {
|
|
157
|
+
position: relative;
|
|
158
|
+
display: grid;
|
|
159
|
+
flex: 0 0 auto;
|
|
160
|
+
width: 42px;
|
|
161
|
+
height: 36px;
|
|
162
|
+
place-items: center;
|
|
163
|
+
border-radius: 8px;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.baron-drawing-toolbar__color:hover {
|
|
168
|
+
background: rgba(41, 98, 255, 0.08);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.baron-drawing-toolbar__color input {
|
|
172
|
+
position: absolute;
|
|
173
|
+
width: 1px;
|
|
174
|
+
height: 1px;
|
|
175
|
+
opacity: 0;
|
|
176
|
+
pointer-events: none;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.baron-drawing-toolbar__color-icon {
|
|
180
|
+
position: relative;
|
|
181
|
+
display: block;
|
|
182
|
+
width: 22px;
|
|
183
|
+
height: 22px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.baron-drawing-toolbar__color-icon::before {
|
|
187
|
+
position: absolute;
|
|
188
|
+
top: 2px;
|
|
189
|
+
left: 7px;
|
|
190
|
+
width: 8px;
|
|
191
|
+
height: 14px;
|
|
192
|
+
content: "";
|
|
193
|
+
border: 2px solid currentcolor;
|
|
194
|
+
border-radius: 2px 2px 5px 2px;
|
|
195
|
+
transform: rotate(42deg);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.baron-drawing-toolbar__color-icon::after {
|
|
199
|
+
position: absolute;
|
|
200
|
+
right: 1px;
|
|
201
|
+
bottom: 0;
|
|
202
|
+
left: 1px;
|
|
203
|
+
height: 4px;
|
|
204
|
+
content: "";
|
|
205
|
+
background: var(--baron-drawing-line-color, rgba(41, 98, 255, 1));
|
|
206
|
+
border-radius: 2px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.baron-drawing-toolbar__text {
|
|
210
|
+
width: 128px;
|
|
211
|
+
height: 30px;
|
|
212
|
+
padding: 0 9px;
|
|
213
|
+
color: rgba(32, 35, 42, 1);
|
|
214
|
+
background: rgba(248, 249, 251, 1);
|
|
215
|
+
border: 1px solid rgba(218, 220, 226, 1);
|
|
216
|
+
border-radius: 7px;
|
|
217
|
+
font: inherit;
|
|
218
|
+
outline: 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.baron-drawing-toolbar__text:focus {
|
|
222
|
+
border-color: rgba(41, 98, 255, 1);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.baron-drawing-toolbar__status {
|
|
226
|
+
position: absolute;
|
|
227
|
+
top: calc(100% + 6px);
|
|
228
|
+
left: 8px;
|
|
229
|
+
max-width: 320px;
|
|
230
|
+
padding: 6px 8px;
|
|
231
|
+
color: rgba(179, 44, 38, 1);
|
|
232
|
+
background: rgba(255, 247, 246, 0.98);
|
|
233
|
+
border: 1px solid rgba(226, 181, 177, 1);
|
|
234
|
+
border-radius: 6px;
|
|
235
|
+
font-size: 11px;
|
|
236
|
+
line-height: 1.35;
|
|
237
|
+
box-shadow: 0 6px 18px rgba(20, 29, 48, 0.1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.baron-drawing-toolbar__status[hidden] {
|
|
241
|
+
display: none;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@media (prefers-reduced-motion: reduce) {
|
|
245
|
+
.baron-drawing-toolbar,
|
|
246
|
+
.baron-drawing-toolbar__button {
|
|
247
|
+
transition: none;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DrawingRuntimeCapability } from '../drawing/capabilities.js';
|
|
2
|
+
import type { DrawingFloatingToolbar, DrawingFloatingToolbarOptions } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* 创建选中 Drawing 的对象级浮动工具栏。工具栏使用 body portal,避免修改图表引擎 DOM;
|
|
5
|
+
* 拖动位置只保存在当前实例内,刷新后回到图表顶部中央。
|
|
6
|
+
*/
|
|
7
|
+
export declare function createDrawingFloatingToolbar(target: HTMLElement, runtime: DrawingRuntimeCapability, options?: DrawingFloatingToolbarOptions): DrawingFloatingToolbar;
|
|
8
|
+
//# sourceMappingURL=drawing-floating-toolbar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawing-floating-toolbar.d.ts","sourceRoot":"","sources":["../../src/toolbar/drawing-floating-toolbar.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAC3E,OAAO,KAAK,EACX,sBAAsB,EACtB,6BAA6B,EAC7B,MAAM,aAAa,CAAC;AAgFrB;;;GAGG;AACH,wBAAgB,4BAA4B,CAC3C,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,wBAAwB,EACjC,OAAO,GAAE,6BAAkC,GACzC,sBAAsB,CA6WxB"}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { registerRuntimeTeardown } from '../lifecycle.js';
|
|
2
|
+
import { createToolbarIcon } from './toolbar-icons.js';
|
|
3
|
+
import { OVERLAY_TOOL_PRESENTATIONS } from './toolbar-tools.js';
|
|
4
|
+
import { DRAWING_FLOATING_TOOLBAR_STYLES } from './drawing-floating-toolbar-styles.js';
|
|
5
|
+
const VIEWPORT_PADDING = 8;
|
|
6
|
+
const TARGET_PADDING = 10;
|
|
7
|
+
const TARGET_TOP_GAP = 12;
|
|
8
|
+
const TEXT_DRAWING_TYPES = new Set(['simpleAnnotation', 'simpleTag', 'callout', 'text']);
|
|
9
|
+
function htmlHexColorToSceneRgba(value) {
|
|
10
|
+
const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(value);
|
|
11
|
+
if (match === null) {
|
|
12
|
+
throw new TypeError(`Invalid HTML color value: ${value}`);
|
|
13
|
+
}
|
|
14
|
+
return `rgba(${Number.parseInt(match[1], 16)}, ${Number.parseInt(match[2], 16)}, ${Number.parseInt(match[3], 16)}, 1)`;
|
|
15
|
+
}
|
|
16
|
+
function sceneColorToHtmlHex(value) {
|
|
17
|
+
const hex = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
18
|
+
if (hex !== null) {
|
|
19
|
+
return `#${hex[1].toLowerCase()}`;
|
|
20
|
+
}
|
|
21
|
+
const rgba = /^rgba?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)/i.exec(value);
|
|
22
|
+
if (rgba === null) {
|
|
23
|
+
return '#2962ff';
|
|
24
|
+
}
|
|
25
|
+
return `#${[rgba[1], rgba[2], rgba[3]]
|
|
26
|
+
.map((component) => Math.max(0, Math.min(255, Math.round(Number(component)))).toString(16).padStart(2, '0'))
|
|
27
|
+
.join('')}`;
|
|
28
|
+
}
|
|
29
|
+
function createSeparator() {
|
|
30
|
+
const separator = document.createElement('span');
|
|
31
|
+
separator.className = 'baron-drawing-toolbar__separator';
|
|
32
|
+
separator.setAttribute('aria-hidden', 'true');
|
|
33
|
+
return separator;
|
|
34
|
+
}
|
|
35
|
+
function createSelect(className, action, label, values) {
|
|
36
|
+
const root = document.createElement('label');
|
|
37
|
+
root.className = `baron-drawing-toolbar__control ${className}`;
|
|
38
|
+
const select = document.createElement('select');
|
|
39
|
+
select.dataset.action = action;
|
|
40
|
+
select.setAttribute('aria-label', label);
|
|
41
|
+
for (const item of values) {
|
|
42
|
+
const option = document.createElement('option');
|
|
43
|
+
option.value = item.value;
|
|
44
|
+
option.textContent = item.label;
|
|
45
|
+
select.append(option);
|
|
46
|
+
}
|
|
47
|
+
root.append(select);
|
|
48
|
+
return { label: root, select };
|
|
49
|
+
}
|
|
50
|
+
function createButton(action, label, icon) {
|
|
51
|
+
const button = document.createElement('button');
|
|
52
|
+
button.type = 'button';
|
|
53
|
+
button.className = 'baron-drawing-toolbar__button';
|
|
54
|
+
button.dataset.action = action;
|
|
55
|
+
button.setAttribute('aria-label', label);
|
|
56
|
+
button.title = label;
|
|
57
|
+
button.append(createToolbarIcon(icon));
|
|
58
|
+
return button;
|
|
59
|
+
}
|
|
60
|
+
function selectedDrawing(runtime) {
|
|
61
|
+
const id = runtime.getSelectedDrawingId();
|
|
62
|
+
return id === undefined ? undefined : runtime.getDrawing(id);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 创建选中 Drawing 的对象级浮动工具栏。工具栏使用 body portal,避免修改图表引擎 DOM;
|
|
66
|
+
* 拖动位置只保存在当前实例内,刷新后回到图表顶部中央。
|
|
67
|
+
*/
|
|
68
|
+
export function createDrawingFloatingToolbar(target, runtime, options = {}) {
|
|
69
|
+
const style = document.createElement('style');
|
|
70
|
+
style.dataset.baronDrawingToolbarStyles = '';
|
|
71
|
+
style.textContent = DRAWING_FLOATING_TOOLBAR_STYLES;
|
|
72
|
+
const root = document.createElement('div');
|
|
73
|
+
root.className = 'baron-drawing-toolbar';
|
|
74
|
+
root.setAttribute('role', 'toolbar');
|
|
75
|
+
root.setAttribute('aria-label', '选中 Drawing 设置');
|
|
76
|
+
root.hidden = true;
|
|
77
|
+
const grip = document.createElement('button');
|
|
78
|
+
grip.type = 'button';
|
|
79
|
+
grip.className = 'baron-drawing-toolbar__grip';
|
|
80
|
+
grip.dataset.action = 'drag';
|
|
81
|
+
grip.setAttribute('aria-label', '拖动 Drawing 工具栏');
|
|
82
|
+
grip.title = '拖动工具栏';
|
|
83
|
+
for (let index = 0; index < 6; index++) {
|
|
84
|
+
const dot = document.createElement('span');
|
|
85
|
+
dot.className = 'baron-drawing-toolbar__grip-dot';
|
|
86
|
+
grip.append(dot);
|
|
87
|
+
}
|
|
88
|
+
grip.hidden = options.draggable === false;
|
|
89
|
+
const type = document.createElement('span');
|
|
90
|
+
type.className = 'baron-drawing-toolbar__type';
|
|
91
|
+
type.dataset.action = 'drawing-type';
|
|
92
|
+
const color = document.createElement('label');
|
|
93
|
+
color.className = 'baron-drawing-toolbar__color';
|
|
94
|
+
color.setAttribute('aria-label', '线色');
|
|
95
|
+
color.title = '线色';
|
|
96
|
+
const colorIcon = document.createElement('span');
|
|
97
|
+
colorIcon.className = 'baron-drawing-toolbar__color-icon';
|
|
98
|
+
const colorInput = document.createElement('input');
|
|
99
|
+
colorInput.type = 'color';
|
|
100
|
+
colorInput.dataset.action = 'line-color';
|
|
101
|
+
colorInput.setAttribute('aria-label', '线色');
|
|
102
|
+
color.append(colorIcon, colorInput);
|
|
103
|
+
const lineStyle = createSelect('baron-drawing-toolbar__control--style', 'line-style', '线型', [
|
|
104
|
+
{ value: 'solid', label: '实线' },
|
|
105
|
+
{ value: 'dashed', label: '虚线' },
|
|
106
|
+
{ value: 'dotted', label: '点线' },
|
|
107
|
+
]);
|
|
108
|
+
const lineWidth = createSelect('baron-drawing-toolbar__control--width', 'line-width', '线宽', [0.5, 1, 1.5, 2, 3, 4, 6, 8, 10].map((value) => ({
|
|
109
|
+
value: String(value),
|
|
110
|
+
label: `${value}px`,
|
|
111
|
+
})));
|
|
112
|
+
const textInput = document.createElement('input');
|
|
113
|
+
textInput.type = 'text';
|
|
114
|
+
textInput.className = 'baron-drawing-toolbar__text';
|
|
115
|
+
textInput.dataset.action = 'drawing-text';
|
|
116
|
+
textInput.setAttribute('aria-label', 'Drawing 文本');
|
|
117
|
+
textInput.placeholder = '输入文本';
|
|
118
|
+
textInput.hidden = true;
|
|
119
|
+
const lock = createButton('toggle-lock', '锁定 Drawing', 'lock');
|
|
120
|
+
const remove = createButton('delete', '删除 Drawing', 'trash');
|
|
121
|
+
const status = document.createElement('span');
|
|
122
|
+
status.className = 'baron-drawing-toolbar__status';
|
|
123
|
+
status.setAttribute('role', 'status');
|
|
124
|
+
status.setAttribute('aria-live', 'polite');
|
|
125
|
+
status.hidden = true;
|
|
126
|
+
root.append(grip, type, createSeparator(), color, lineStyle.label, lineWidth.label, textInput, createSeparator(), lock, remove, status);
|
|
127
|
+
document.head.append(style);
|
|
128
|
+
document.body.append(root);
|
|
129
|
+
let destroyed = false;
|
|
130
|
+
let userPositioned = false;
|
|
131
|
+
let dragging = null;
|
|
132
|
+
let currentId;
|
|
133
|
+
const setStatus = (message) => {
|
|
134
|
+
status.textContent = message;
|
|
135
|
+
status.hidden = message.length === 0;
|
|
136
|
+
};
|
|
137
|
+
const bounds = () => {
|
|
138
|
+
const targetRect = target.getBoundingClientRect();
|
|
139
|
+
const width = root.offsetWidth;
|
|
140
|
+
const height = root.offsetHeight;
|
|
141
|
+
const viewportMinLeft = VIEWPORT_PADDING;
|
|
142
|
+
const viewportMaxLeft = Math.max(viewportMinLeft, window.innerWidth - width - VIEWPORT_PADDING);
|
|
143
|
+
const targetMinLeft = targetRect.left + TARGET_PADDING;
|
|
144
|
+
const targetMaxLeft = targetRect.right - width - TARGET_PADDING;
|
|
145
|
+
const minLeft = Math.max(viewportMinLeft, Math.min(targetMinLeft, viewportMaxLeft));
|
|
146
|
+
const maxLeft = Math.max(minLeft, Math.min(viewportMaxLeft, targetMaxLeft));
|
|
147
|
+
const viewportMinTop = VIEWPORT_PADDING;
|
|
148
|
+
const viewportMaxTop = Math.max(viewportMinTop, window.innerHeight - height - VIEWPORT_PADDING);
|
|
149
|
+
const targetMinTop = targetRect.top + TARGET_PADDING;
|
|
150
|
+
const targetMaxTop = targetRect.bottom - height - TARGET_PADDING;
|
|
151
|
+
const minTop = Math.max(viewportMinTop, Math.min(targetMinTop, viewportMaxTop));
|
|
152
|
+
const maxTop = Math.max(minTop, Math.min(viewportMaxTop, targetMaxTop));
|
|
153
|
+
return { minLeft, maxLeft, minTop, maxTop };
|
|
154
|
+
};
|
|
155
|
+
const setPosition = (left, top) => {
|
|
156
|
+
const value = bounds();
|
|
157
|
+
root.style.left = `${Math.round(Math.max(value.minLeft, Math.min(value.maxLeft, left)))}px`;
|
|
158
|
+
root.style.top = `${Math.round(Math.max(value.minTop, Math.min(value.maxTop, top)))}px`;
|
|
159
|
+
};
|
|
160
|
+
const positionAtDefault = () => {
|
|
161
|
+
if (root.hidden) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const targetRect = target.getBoundingClientRect();
|
|
165
|
+
setPosition(targetRect.left + (targetRect.width - root.offsetWidth) / 2, targetRect.top + TARGET_TOP_GAP);
|
|
166
|
+
};
|
|
167
|
+
const clampCurrentPosition = () => {
|
|
168
|
+
if (root.hidden) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!userPositioned) {
|
|
172
|
+
positionAtDefault();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const left = Number.parseFloat(root.style.left);
|
|
176
|
+
const top = Number.parseFloat(root.style.top);
|
|
177
|
+
setPosition(Number.isFinite(left) ? left : 0, Number.isFinite(top) ? top : 0);
|
|
178
|
+
};
|
|
179
|
+
const updateStyles = (drawing, change) => {
|
|
180
|
+
runtime.updateDrawingStyles(drawing.id, {
|
|
181
|
+
...structuredClone(drawing.styles),
|
|
182
|
+
line: {
|
|
183
|
+
...structuredClone(drawing.styles.line),
|
|
184
|
+
...change,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
const performMutation = (action) => {
|
|
189
|
+
const drawing = selectedDrawing(runtime);
|
|
190
|
+
if (drawing === undefined || runtime.getDrawingMutationState() !== 'ready') {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
setStatus('');
|
|
195
|
+
action(drawing);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
setStatus(error instanceof Error ? error.message : String(error));
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const render = () => {
|
|
202
|
+
if (destroyed) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const drawing = selectedDrawing(runtime);
|
|
206
|
+
if (drawing === undefined) {
|
|
207
|
+
currentId = undefined;
|
|
208
|
+
root.hidden = true;
|
|
209
|
+
setStatus('');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const wasHidden = root.hidden;
|
|
213
|
+
root.hidden = false;
|
|
214
|
+
root.dataset.drawingId = drawing.id;
|
|
215
|
+
root.dataset.drawingType = drawing.type;
|
|
216
|
+
if (currentId !== drawing.id) {
|
|
217
|
+
currentId = drawing.id;
|
|
218
|
+
setStatus('');
|
|
219
|
+
}
|
|
220
|
+
type.replaceChildren();
|
|
221
|
+
const presentation = OVERLAY_TOOL_PRESENTATIONS[drawing.type];
|
|
222
|
+
type.append(createToolbarIcon(presentation.icon));
|
|
223
|
+
type.title = presentation.label;
|
|
224
|
+
type.setAttribute('aria-label', presentation.label);
|
|
225
|
+
const colorValue = sceneColorToHtmlHex(drawing.styles.line.color);
|
|
226
|
+
colorInput.value = colorValue;
|
|
227
|
+
colorIcon.style.setProperty('--baron-drawing-line-color', drawing.styles.line.color);
|
|
228
|
+
lineStyle.select.value = drawing.styles.line.style;
|
|
229
|
+
const widthValue = String(drawing.styles.line.size);
|
|
230
|
+
if (!Array.from(lineWidth.select.options).some((option) => option.value === widthValue)) {
|
|
231
|
+
const option = document.createElement('option');
|
|
232
|
+
option.value = widthValue;
|
|
233
|
+
option.textContent = `${widthValue}px`;
|
|
234
|
+
lineWidth.select.append(option);
|
|
235
|
+
}
|
|
236
|
+
lineWidth.select.value = widthValue;
|
|
237
|
+
const isText = TEXT_DRAWING_TYPES.has(drawing.type);
|
|
238
|
+
textInput.hidden = !isText;
|
|
239
|
+
textInput.value = isText ? drawing.text ?? '' : '';
|
|
240
|
+
lock.replaceChildren(createToolbarIcon(drawing.locked ? 'lock' : 'lockOpen'));
|
|
241
|
+
lock.setAttribute('aria-label', drawing.locked ? '解锁 Drawing' : '锁定 Drawing');
|
|
242
|
+
lock.title = drawing.locked ? '解锁' : '锁定';
|
|
243
|
+
lock.setAttribute('aria-pressed', String(drawing.locked));
|
|
244
|
+
const busy = runtime.getDrawingMutationState() !== 'ready';
|
|
245
|
+
colorInput.disabled = busy || drawing.locked;
|
|
246
|
+
lineStyle.select.disabled = busy || drawing.locked;
|
|
247
|
+
lineWidth.select.disabled = busy || drawing.locked;
|
|
248
|
+
textInput.disabled = busy || drawing.locked;
|
|
249
|
+
lock.disabled = busy;
|
|
250
|
+
remove.disabled = busy || drawing.locked;
|
|
251
|
+
root.setAttribute('aria-busy', String(busy));
|
|
252
|
+
if (wasHidden || !userPositioned) {
|
|
253
|
+
positionAtDefault();
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
clampCurrentPosition();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const handleColorChange = () => performMutation((drawing) => {
|
|
260
|
+
updateStyles(drawing, { color: htmlHexColorToSceneRgba(colorInput.value) });
|
|
261
|
+
});
|
|
262
|
+
const handleLineStyleChange = () => performMutation((drawing) => {
|
|
263
|
+
updateStyles(drawing, {
|
|
264
|
+
style: lineStyle.select.value,
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
const handleLineWidthChange = () => performMutation((drawing) => {
|
|
268
|
+
updateStyles(drawing, { size: Number(lineWidth.select.value) });
|
|
269
|
+
});
|
|
270
|
+
const handleTextChange = () => performMutation((drawing) => {
|
|
271
|
+
runtime.updateDrawingText(drawing.id, textInput.value);
|
|
272
|
+
});
|
|
273
|
+
const handleLock = () => performMutation((drawing) => {
|
|
274
|
+
runtime.updateDrawingLocked(drawing.id, !drawing.locked);
|
|
275
|
+
});
|
|
276
|
+
const handleDelete = () => performMutation((drawing) => {
|
|
277
|
+
if (drawing.locked) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if ((options.deleteBehavior ?? 'direct') === 'request') {
|
|
281
|
+
runtime.requestDrawingDelete(drawing.id);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
runtime.removeDrawing(drawing.id);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
const handleDragStart = (event) => {
|
|
288
|
+
if (options.draggable === false || event.button !== 0) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const rect = root.getBoundingClientRect();
|
|
292
|
+
dragging = {
|
|
293
|
+
pointerId: event.pointerId,
|
|
294
|
+
offsetX: event.clientX - rect.left,
|
|
295
|
+
offsetY: event.clientY - rect.top,
|
|
296
|
+
};
|
|
297
|
+
userPositioned = true;
|
|
298
|
+
root.dataset.dragging = 'true';
|
|
299
|
+
grip.setPointerCapture(event.pointerId);
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
};
|
|
302
|
+
const handleDragMove = (event) => {
|
|
303
|
+
if (dragging === null || dragging.pointerId !== event.pointerId) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
setPosition(event.clientX - dragging.offsetX, event.clientY - dragging.offsetY);
|
|
307
|
+
};
|
|
308
|
+
const finishDrag = (event) => {
|
|
309
|
+
if (dragging === null || dragging.pointerId !== event.pointerId) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (grip.hasPointerCapture(event.pointerId)) {
|
|
313
|
+
grip.releasePointerCapture(event.pointerId);
|
|
314
|
+
}
|
|
315
|
+
dragging = null;
|
|
316
|
+
root.dataset.dragging = 'false';
|
|
317
|
+
};
|
|
318
|
+
colorInput.addEventListener('change', handleColorChange);
|
|
319
|
+
lineStyle.select.addEventListener('change', handleLineStyleChange);
|
|
320
|
+
lineWidth.select.addEventListener('change', handleLineWidthChange);
|
|
321
|
+
textInput.addEventListener('change', handleTextChange);
|
|
322
|
+
lock.addEventListener('click', handleLock);
|
|
323
|
+
remove.addEventListener('click', handleDelete);
|
|
324
|
+
grip.addEventListener('pointerdown', handleDragStart);
|
|
325
|
+
grip.addEventListener('pointermove', handleDragMove);
|
|
326
|
+
grip.addEventListener('pointerup', finishDrag);
|
|
327
|
+
grip.addEventListener('pointercancel', finishDrag);
|
|
328
|
+
const unsubscribe = runtime.subscribeDrawingChanges(render);
|
|
329
|
+
const handleViewportChange = () => clampCurrentPosition();
|
|
330
|
+
window.addEventListener('resize', handleViewportChange);
|
|
331
|
+
window.addEventListener('scroll', handleViewportChange, true);
|
|
332
|
+
const resizeObserver = typeof ResizeObserver === 'undefined'
|
|
333
|
+
? null
|
|
334
|
+
: new ResizeObserver(handleViewportChange);
|
|
335
|
+
resizeObserver?.observe(target);
|
|
336
|
+
render();
|
|
337
|
+
let unregisterRuntime = () => { };
|
|
338
|
+
const toolbar = {
|
|
339
|
+
element: root,
|
|
340
|
+
resetPosition() {
|
|
341
|
+
if (destroyed) {
|
|
342
|
+
throw new Error('DRAWING_FLOATING_TOOLBAR_DESTROYED');
|
|
343
|
+
}
|
|
344
|
+
userPositioned = false;
|
|
345
|
+
positionAtDefault();
|
|
346
|
+
},
|
|
347
|
+
destroy() {
|
|
348
|
+
if (destroyed) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
destroyed = true;
|
|
352
|
+
unregisterRuntime();
|
|
353
|
+
unsubscribe();
|
|
354
|
+
resizeObserver?.disconnect();
|
|
355
|
+
window.removeEventListener('resize', handleViewportChange);
|
|
356
|
+
window.removeEventListener('scroll', handleViewportChange, true);
|
|
357
|
+
colorInput.removeEventListener('change', handleColorChange);
|
|
358
|
+
lineStyle.select.removeEventListener('change', handleLineStyleChange);
|
|
359
|
+
lineWidth.select.removeEventListener('change', handleLineWidthChange);
|
|
360
|
+
textInput.removeEventListener('change', handleTextChange);
|
|
361
|
+
lock.removeEventListener('click', handleLock);
|
|
362
|
+
remove.removeEventListener('click', handleDelete);
|
|
363
|
+
grip.removeEventListener('pointerdown', handleDragStart);
|
|
364
|
+
grip.removeEventListener('pointermove', handleDragMove);
|
|
365
|
+
grip.removeEventListener('pointerup', finishDrag);
|
|
366
|
+
grip.removeEventListener('pointercancel', finishDrag);
|
|
367
|
+
root.remove();
|
|
368
|
+
style.remove();
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
unregisterRuntime = registerRuntimeTeardown(runtime, () => toolbar.destroy());
|
|
372
|
+
return toolbar;
|
|
373
|
+
}
|