@363045841yyt/klinechart 0.8.3 → 0.8.5
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/components/BatchStockDialog.vue.d.ts +13 -0
- package/dist/components/BatchStockDialog.vue.d.ts.map +1 -0
- package/dist/components/CompareSymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/ExportProgressDialog.vue.d.ts +15 -0
- package/dist/components/ExportProgressDialog.vue.d.ts.map +1 -0
- package/dist/components/IndicatorSelector.vue.d.ts.map +1 -1
- package/dist/components/KLineChart.vue.d.ts +5 -9
- package/dist/components/KLineChart.vue.d.ts.map +1 -1
- package/dist/components/LeftToolbar.vue.d.ts.map +1 -1
- package/dist/components/SymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/TopToolbar.vue.d.ts.map +1 -1
- package/dist/composables/chart/useChartTheme.d.ts +329 -0
- package/dist/composables/chart/useChartTheme.d.ts.map +1 -0
- package/dist/composables/chart/useDrawingManager.d.ts +86 -0
- package/dist/composables/chart/useDrawingManager.d.ts.map +1 -0
- package/dist/composables/chart/useIndicatorManager.d.ts +38 -0
- package/dist/composables/chart/useIndicatorManager.d.ts.map +1 -0
- package/dist/composables/chart/useRangeSelection.d.ts +65 -0
- package/dist/composables/chart/useRangeSelection.d.ts.map +1 -0
- package/dist/composables/useTeleportedPopup.d.ts +8 -0
- package/dist/composables/useTeleportedPopup.d.ts.map +1 -0
- package/dist/index.cjs +9 -2
- package/dist/index.css +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1769 -1090
- package/dist/tools/calcRangeOverlayPixel.d.ts +15 -0
- package/dist/tools/calcRangeOverlayPixel.d.ts.map +1 -0
- package/dist/tools/getKLineIndexByTimestamp.d.ts +4 -0
- package/dist/tools/getKLineIndexByTimestamp.d.ts.map +1 -0
- package/dist/web-component.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BatchStockDialog.vue +293 -0
- package/src/components/CompareSymbolSelector.vue +35 -8
- package/src/components/Dropdown.vue +42 -19
- package/src/components/ExportProgressDialog.vue +226 -0
- package/src/components/IndicatorSelector.vue +13 -5
- package/src/components/KLineChart.vue +329 -399
- package/src/components/LeftToolbar.vue +2 -1
- package/src/components/SymbolSelector.vue +35 -8
- package/src/components/TopToolbar.vue +55 -2
- package/src/composables/chart/useChartTheme.ts +86 -0
- package/src/composables/chart/useDrawingManager.ts +67 -0
- package/src/composables/chart/useIndicatorManager.ts +307 -0
- package/src/composables/chart/useRangeSelection.ts +417 -0
- package/src/composables/useTeleportedPopup.ts +33 -0
- package/src/index.ts +41 -14
- package/src/tools/calcRangeOverlayPixel.ts +28 -0
- package/src/tools/getKLineIndexByTimestamp.ts +40 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ChartController } from '@363045841yyt/klinechart-core/controllers';
|
|
2
|
+
export interface Bounds {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function calcRangeOverlayPixel(bounds: Bounds, controller: ChartController, container: HTMLElement, viewport: {
|
|
7
|
+
scrollLeft: number;
|
|
8
|
+
plotWidth: number;
|
|
9
|
+
plotHeight: number;
|
|
10
|
+
}): {
|
|
11
|
+
left: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=calcRangeOverlayPixel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calcRangeOverlayPixel.d.ts","sourceRoot":"","sources":["../../src/tools/calcRangeOverlayPixel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2CAA2C,CAAA;AAEhF,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,eAAe,EAC3B,SAAS,EAAE,WAAW,EACtB,QAAQ,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAejD"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { KLineData } from '@363045841yyt/klinechart-core/controllers';
|
|
2
|
+
export declare function getKLineIndexByTimestamp(data: ReadonlyArray<KLineData>, timestamp: number): number | null;
|
|
3
|
+
export declare function findNearestKLineIndex(data: ReadonlyArray<KLineData>, timestamp: number, direction: 'left' | 'right'): number | null;
|
|
4
|
+
//# sourceMappingURL=getKLineIndexByTimestamp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getKLineIndexByTimestamp.d.ts","sourceRoot":"","sources":["../../src/tools/getKLineIndexByTimestamp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2CAA2C,CAAA;AAkB1E,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,EAC9B,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAIf;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,EAC9B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAAG,OAAO,GAC1B,MAAM,GAAG,IAAI,CAQf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-component.d.ts","sourceRoot":"","sources":["../src/web-component.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAE9F,QAAA,MAAM,iBAAiB;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"web-component.d.ts","sourceRoot":"","sources":["../src/web-component.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAE9F,QAAA,MAAM,iBAAiB;;;;;;;;;;;;;;aAU2qsC,CAAC;kBAAyB,CAAC;;;;;iBAAsH,CAAC;gBAAc,CAAC;;;iBAAuD,CAAC;gBAAc,CAAC;;qBAAgC,CAAC;;EARz8sC,CAAA;AAIF,OAAO,EAAE,iBAAiB,EAAE,CAAA;AAC5B,eAAe,iBAAiB,CAAA;AAEhC,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,CAAA"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport :to="teleportTarget">
|
|
3
|
+
<Transition name="overlay">
|
|
4
|
+
<div v-if="show" class="batch-overlay" @click="emit('close')">
|
|
5
|
+
<Transition name="modal">
|
|
6
|
+
<div class="batch-modal" @click.stop>
|
|
7
|
+
<div class="batch-header">
|
|
8
|
+
<span class="batch-title">批量设置股票代码</span>
|
|
9
|
+
<button class="batch-close-btn" @click="emit('close')">
|
|
10
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
11
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
12
|
+
</svg>
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="batch-body">
|
|
17
|
+
<textarea
|
|
18
|
+
v-model="codesText"
|
|
19
|
+
class="batch-textarea"
|
|
20
|
+
placeholder="每行一个股票代码 例如: 000001 600036 002415"
|
|
21
|
+
rows="8"
|
|
22
|
+
spellcheck="false"
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="batch-footer">
|
|
27
|
+
<button class="batch-btn batch-btn--cancel" @click="emit('close')">取消</button>
|
|
28
|
+
<button class="batch-btn batch-btn--confirm" @click="onApply">应用</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</Transition>
|
|
32
|
+
</div>
|
|
33
|
+
</Transition>
|
|
34
|
+
</Teleport>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
import { ref, computed } from 'vue'
|
|
39
|
+
import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
|
|
40
|
+
|
|
41
|
+
const props = defineProps<{
|
|
42
|
+
show: boolean
|
|
43
|
+
}>()
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits<{
|
|
46
|
+
close: []
|
|
47
|
+
apply: [codes: string[]]
|
|
48
|
+
}>()
|
|
49
|
+
|
|
50
|
+
const teleportTarget = useFullscreenTeleportTarget()
|
|
51
|
+
|
|
52
|
+
const codes = ref<string[]>([])
|
|
53
|
+
|
|
54
|
+
const codesText = computed({
|
|
55
|
+
get: () => codes.value.join('\n'),
|
|
56
|
+
set: (val: string) => {
|
|
57
|
+
codes.value = val
|
|
58
|
+
.split('\n')
|
|
59
|
+
.map((s) => s.trim())
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
function onApply() {
|
|
65
|
+
if (codes.value.length === 0) return
|
|
66
|
+
emit('apply', codes.value)
|
|
67
|
+
emit('close')
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<style scoped>
|
|
72
|
+
.batch-overlay {
|
|
73
|
+
position: fixed;
|
|
74
|
+
inset: 0;
|
|
75
|
+
background: rgba(0, 0, 0, 0.3);
|
|
76
|
+
backdrop-filter: blur(4px);
|
|
77
|
+
padding: 24px;
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
z-index: 1000;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.batch-modal {
|
|
85
|
+
background: var(--klc-color-tag-bg-white);
|
|
86
|
+
border: 1px solid var(--klc-color-border-button);
|
|
87
|
+
border-radius: 10px;
|
|
88
|
+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.15);
|
|
89
|
+
min-width: 360px;
|
|
90
|
+
max-width: 400px;
|
|
91
|
+
width: min(92vw, 400px);
|
|
92
|
+
max-height: min(600px, calc(100vh - 48px));
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.batch-header {
|
|
99
|
+
display: flex;
|
|
100
|
+
justify-content: space-between;
|
|
101
|
+
align-items: center;
|
|
102
|
+
padding: 14px 18px 14px 20px;
|
|
103
|
+
background: var(--klc-color-background);
|
|
104
|
+
border-bottom: 1px solid var(--klc-color-grid-major);
|
|
105
|
+
flex-shrink: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.batch-title {
|
|
109
|
+
font-size: 15px;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
color: var(--klc-color-foreground);
|
|
112
|
+
line-height: 1.35;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.batch-close-btn {
|
|
116
|
+
background: var(--klc-color-tag-bg-white);
|
|
117
|
+
border: 1px solid var(--klc-color-border-button);
|
|
118
|
+
border-radius: 7px;
|
|
119
|
+
width: 30px;
|
|
120
|
+
height: 30px;
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
color: var(--klc-color-axis-text);
|
|
126
|
+
transition:
|
|
127
|
+
background 0.15s,
|
|
128
|
+
color 0.15s,
|
|
129
|
+
border-color 0.15s;
|
|
130
|
+
padding: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.batch-close-btn:hover {
|
|
134
|
+
background: var(--klc-color-tag-bg-hover);
|
|
135
|
+
color: var(--klc-color-foreground);
|
|
136
|
+
border-color: var(--klc-color-axis-line);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.batch-close-btn svg {
|
|
140
|
+
width: 14px;
|
|
141
|
+
height: 14px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.batch-body {
|
|
145
|
+
padding: 16px 20px;
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
gap: 12px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.batch-textarea {
|
|
152
|
+
width: 100%;
|
|
153
|
+
min-height: 160px;
|
|
154
|
+
padding: 10px 12px;
|
|
155
|
+
border: 1px solid var(--klc-color-border-button);
|
|
156
|
+
border-radius: 6px;
|
|
157
|
+
background: var(--klc-color-background);
|
|
158
|
+
color: var(--klc-color-foreground);
|
|
159
|
+
font-size: 13px;
|
|
160
|
+
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
161
|
+
line-height: 1.5;
|
|
162
|
+
resize: vertical;
|
|
163
|
+
outline: none;
|
|
164
|
+
transition: border-color 0.15s;
|
|
165
|
+
box-sizing: border-box;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.batch-textarea:focus {
|
|
169
|
+
border-color: var(--klc-color-axis-text);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.batch-textarea::placeholder {
|
|
173
|
+
color: var(--klc-color-axis-text);
|
|
174
|
+
opacity: 0.5;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.batch-footer {
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
justify-content: flex-end;
|
|
181
|
+
gap: 8px;
|
|
182
|
+
padding: 12px 20px;
|
|
183
|
+
background: var(--klc-color-background);
|
|
184
|
+
border-top: 1px solid var(--klc-color-grid-major);
|
|
185
|
+
flex-shrink: 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.batch-btn {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
justify-content: center;
|
|
192
|
+
min-width: 68px;
|
|
193
|
+
height: 32px;
|
|
194
|
+
padding: 0 14px;
|
|
195
|
+
border-radius: 7px;
|
|
196
|
+
font-size: 13px;
|
|
197
|
+
font-weight: 500;
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
border: 1px solid transparent;
|
|
200
|
+
transition:
|
|
201
|
+
background 0.15s,
|
|
202
|
+
border-color 0.15s,
|
|
203
|
+
color 0.15s,
|
|
204
|
+
box-shadow 0.15s,
|
|
205
|
+
transform 0.15s;
|
|
206
|
+
line-height: 1;
|
|
207
|
+
white-space: nowrap;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.batch-btn--cancel {
|
|
211
|
+
background: transparent;
|
|
212
|
+
border-color: var(--klc-color-axis-line);
|
|
213
|
+
color: var(--klc-color-axis-text);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.batch-btn--cancel:hover {
|
|
217
|
+
background: var(--klc-color-tag-bg-hover);
|
|
218
|
+
color: var(--klc-color-foreground);
|
|
219
|
+
border-color: var(--klc-color-axis-text);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.batch-btn--confirm {
|
|
223
|
+
background: var(--klc-color-foreground);
|
|
224
|
+
border-color: var(--klc-color-foreground);
|
|
225
|
+
color: var(--klc-color-background);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.batch-btn--confirm:hover {
|
|
229
|
+
background: var(--klc-color-foreground);
|
|
230
|
+
border-color: var(--klc-color-foreground);
|
|
231
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
|
|
232
|
+
transform: translateY(-1px);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.batch-btn--confirm:active {
|
|
236
|
+
transform: translateY(0);
|
|
237
|
+
box-shadow: none;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.overlay-enter-active,
|
|
241
|
+
.overlay-leave-active {
|
|
242
|
+
transition: opacity 0.2s ease;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.overlay-enter-from,
|
|
246
|
+
.overlay-leave-to {
|
|
247
|
+
opacity: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.modal-enter-active {
|
|
251
|
+
transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.modal-leave-active {
|
|
255
|
+
transition: all 0.16s ease-in;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.modal-enter-from {
|
|
259
|
+
opacity: 0;
|
|
260
|
+
transform: scale(0.96) translateY(-10px);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.modal-leave-to {
|
|
264
|
+
opacity: 0;
|
|
265
|
+
transform: scale(0.98) translateY(8px);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@media (max-width: 480px) {
|
|
269
|
+
.batch-overlay {
|
|
270
|
+
padding: 12px;
|
|
271
|
+
align-items: flex-end;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.batch-modal {
|
|
275
|
+
min-width: 0;
|
|
276
|
+
width: 100%;
|
|
277
|
+
max-height: calc(100vh - 24px);
|
|
278
|
+
border-radius: 10px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.batch-header,
|
|
282
|
+
.batch-body,
|
|
283
|
+
.batch-footer {
|
|
284
|
+
padding-left: 16px;
|
|
285
|
+
padding-right: 16px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.batch-footer {
|
|
289
|
+
flex-direction: column-reverse;
|
|
290
|
+
align-items: stretch;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
</style>
|
|
@@ -14,8 +14,16 @@
|
|
|
14
14
|
<span v-if="comparisonLoading" class="compare-chip__spinner" />
|
|
15
15
|
<span v-if="selected.length > 0" class="compare-chip__badge">{{ selected.length }}</span>
|
|
16
16
|
</button>
|
|
17
|
-
<
|
|
18
|
-
<
|
|
17
|
+
<Teleport :to="teleportTarget">
|
|
18
|
+
<Transition name="symbol-popover">
|
|
19
|
+
<div
|
|
20
|
+
v-if="showPopup"
|
|
21
|
+
ref="popupRef"
|
|
22
|
+
class="compare-popover"
|
|
23
|
+
:style="popupStyle"
|
|
24
|
+
role="dialog"
|
|
25
|
+
aria-label="比较商品"
|
|
26
|
+
>
|
|
19
27
|
<div class="compare-search">
|
|
20
28
|
<span class="compare-search__icon" aria-hidden="true">
|
|
21
29
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
|
@@ -134,13 +142,16 @@
|
|
|
134
142
|
</button>
|
|
135
143
|
</div>
|
|
136
144
|
</div>
|
|
137
|
-
|
|
145
|
+
</Transition>
|
|
146
|
+
</Teleport>
|
|
138
147
|
</div>
|
|
139
148
|
</template>
|
|
140
149
|
|
|
141
150
|
<script setup lang="ts">
|
|
142
|
-
import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
151
|
+
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
143
152
|
import type { SymbolItem } from './SymbolSelector.vue'
|
|
153
|
+
import { useTeleportedPopup } from '../composables/useTeleportedPopup'
|
|
154
|
+
import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
|
|
144
155
|
|
|
145
156
|
const props = withDefaults(defineProps<{
|
|
146
157
|
symbols: SymbolItem[]
|
|
@@ -160,6 +171,15 @@ const showPopup = ref(false)
|
|
|
160
171
|
const searchQuery = ref('')
|
|
161
172
|
const searchInputRef = ref<HTMLInputElement | null>(null)
|
|
162
173
|
const rootRef = ref<HTMLElement | null>(null)
|
|
174
|
+
const popupRef = ref<HTMLElement | null>(null)
|
|
175
|
+
|
|
176
|
+
const teleportTarget = useFullscreenTeleportTarget()
|
|
177
|
+
|
|
178
|
+
const { popupStyle, startPositionSync, stopPositionSync } = useTeleportedPopup(
|
|
179
|
+
rootRef,
|
|
180
|
+
popupRef,
|
|
181
|
+
8,
|
|
182
|
+
)
|
|
163
183
|
|
|
164
184
|
const selectedSet = computed(() => new Set(props.selected ?? []))
|
|
165
185
|
|
|
@@ -202,13 +222,23 @@ function togglePopup() {
|
|
|
202
222
|
}
|
|
203
223
|
}
|
|
204
224
|
|
|
225
|
+
watch(showPopup, (val) => {
|
|
226
|
+
if (val) {
|
|
227
|
+
startPositionSync()
|
|
228
|
+
} else {
|
|
229
|
+
stopPositionSync()
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
205
233
|
function clearSearch() {
|
|
206
234
|
searchQuery.value = ''
|
|
207
235
|
searchInputRef.value?.focus()
|
|
208
236
|
}
|
|
209
237
|
|
|
210
238
|
function onDocumentClick(e: MouseEvent) {
|
|
211
|
-
|
|
239
|
+
const root = rootRef.value
|
|
240
|
+
const popup = popupRef.value
|
|
241
|
+
if (root && !root.contains(e.target as Node) && !popup?.contains(e.target as Node)) {
|
|
212
242
|
showPopup.value = false
|
|
213
243
|
searchQuery.value = ''
|
|
214
244
|
}
|
|
@@ -302,9 +332,6 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onDocumentClick)
|
|
|
302
332
|
}
|
|
303
333
|
|
|
304
334
|
.compare-popover {
|
|
305
|
-
position: absolute;
|
|
306
|
-
top: calc(100% + 8px);
|
|
307
|
-
left: 0;
|
|
308
335
|
z-index: 20;
|
|
309
336
|
width: min(360px, calc(100vw - 24px));
|
|
310
337
|
padding: 14px;
|
|
@@ -19,20 +19,29 @@
|
|
|
19
19
|
<span class="dropdown__chevron" aria-hidden="true"></span>
|
|
20
20
|
</button>
|
|
21
21
|
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
v-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
:aria-selected="option.value === selectedValue"
|
|
31
|
-
@click="selectOption(option.value)"
|
|
22
|
+
<Teleport :to="teleportTarget">
|
|
23
|
+
<div
|
|
24
|
+
v-if="isOpen"
|
|
25
|
+
ref="menuRef"
|
|
26
|
+
class="dropdown__menu"
|
|
27
|
+
:style="menuStyle"
|
|
28
|
+
role="listbox"
|
|
29
|
+
tabindex="-1"
|
|
32
30
|
>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
<button
|
|
32
|
+
v-for="option in options"
|
|
33
|
+
:key="option.value"
|
|
34
|
+
type="button"
|
|
35
|
+
class="dropdown__option"
|
|
36
|
+
:class="{ 'is-selected': option.value === selectedValue }"
|
|
37
|
+
role="option"
|
|
38
|
+
:aria-selected="option.value === selectedValue"
|
|
39
|
+
@click="selectOption(option.value)"
|
|
40
|
+
>
|
|
41
|
+
{{ option.label }}
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</Teleport>
|
|
36
45
|
</div>
|
|
37
46
|
</template>
|
|
38
47
|
|
|
@@ -44,6 +53,8 @@ let dropdownIdSeed = 0
|
|
|
44
53
|
|
|
45
54
|
<script setup lang="ts">
|
|
46
55
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
|
56
|
+
import { useTeleportedPopup } from '../composables/useTeleportedPopup'
|
|
57
|
+
import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
|
|
47
58
|
|
|
48
59
|
export interface DropdownOption<T extends string = string> {
|
|
49
60
|
label: string
|
|
@@ -71,10 +82,19 @@ const emit = defineEmits<{
|
|
|
71
82
|
|
|
72
83
|
const rootRef = ref<HTMLElement | null>(null)
|
|
73
84
|
const triggerRef = ref<HTMLElement | null>(null)
|
|
85
|
+
const menuRef = ref<HTMLElement | null>(null)
|
|
74
86
|
const isOpen = ref(false)
|
|
75
87
|
const menuWidth = ref(0)
|
|
76
88
|
const dropdownId = ++dropdownIdSeed
|
|
77
89
|
|
|
90
|
+
const teleportTarget = useFullscreenTeleportTarget()
|
|
91
|
+
|
|
92
|
+
const { popupStyle, startPositionSync, stopPositionSync } = useTeleportedPopup(
|
|
93
|
+
triggerRef,
|
|
94
|
+
menuRef,
|
|
95
|
+
4,
|
|
96
|
+
)
|
|
97
|
+
|
|
78
98
|
const triggerStyle = computed(() => {
|
|
79
99
|
if (props.minWidth) return { minWidth: props.minWidth }
|
|
80
100
|
return {}
|
|
@@ -83,7 +103,11 @@ const triggerStyle = computed(() => {
|
|
|
83
103
|
const menuStyle = computed(() => {
|
|
84
104
|
if (!isOpen.value) return undefined
|
|
85
105
|
const w = menuWidth.value || (props.minWidth ? parseInt(props.minWidth) : 0)
|
|
86
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
width: w ? `${w}px` : undefined,
|
|
108
|
+
zIndex: 1010,
|
|
109
|
+
...popupStyle.value,
|
|
110
|
+
}
|
|
87
111
|
})
|
|
88
112
|
|
|
89
113
|
const selectedValue = computed(() => {
|
|
@@ -107,6 +131,7 @@ function open() {
|
|
|
107
131
|
activeDropdownClose = close
|
|
108
132
|
menuWidth.value = triggerRef.value?.offsetWidth ?? 0
|
|
109
133
|
isOpen.value = true
|
|
134
|
+
startPositionSync()
|
|
110
135
|
document.addEventListener('pointerdown', handleDocumentPointerDown)
|
|
111
136
|
}
|
|
112
137
|
|
|
@@ -117,6 +142,7 @@ function close() {
|
|
|
117
142
|
activeDropdownId = 0
|
|
118
143
|
activeDropdownClose = null
|
|
119
144
|
}
|
|
145
|
+
stopPositionSync()
|
|
120
146
|
document.removeEventListener('pointerdown', handleDocumentPointerDown)
|
|
121
147
|
}
|
|
122
148
|
|
|
@@ -135,7 +161,8 @@ function selectOption(value: string) {
|
|
|
135
161
|
|
|
136
162
|
function handleDocumentPointerDown(event: PointerEvent) {
|
|
137
163
|
const root = rootRef.value
|
|
138
|
-
|
|
164
|
+
const menu = menuRef.value
|
|
165
|
+
if (root && !root.contains(event.target as Node | null) && !menu?.contains(event.target as Node | null)) {
|
|
139
166
|
close()
|
|
140
167
|
}
|
|
141
168
|
}
|
|
@@ -219,10 +246,6 @@ onBeforeUnmount(close)
|
|
|
219
246
|
}
|
|
220
247
|
|
|
221
248
|
.dropdown__menu {
|
|
222
|
-
position: absolute;
|
|
223
|
-
z-index: 30;
|
|
224
|
-
top: calc(100% + 4px);
|
|
225
|
-
left: 0;
|
|
226
249
|
padding: 4px;
|
|
227
250
|
border: 1px solid var(--klc-color-border-button);
|
|
228
251
|
border-radius: 4px;
|