@milaboratories/uikit 2.2.21 → 2.2.23
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/CHANGELOG.md +12 -0
- package/dist/pl-uikit.js +3127 -3113
- package/dist/pl-uikit.umd.cjs +7 -7
- package/dist/src/index.d.ts +1 -0
- package/dist/src/utils/DropdownOverlay/DropdownOverlay.vue.d.ts +28 -0
- package/dist/src/utils/DropdownOverlay/index.d.ts +1 -0
- package/dist/style.css +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/components/PlDropdown/PlDropdown.vue +24 -67
- package/src/components/PlDropdownMulti/PlDropdownMulti.vue +36 -69
- package/src/components/PlFileDialog/Remote.vue +12 -4
- package/src/components/PlTextField/PlTextField.vue +3 -3
- package/src/components/PlTextField/pl-text-field.scss +5 -1
- package/src/index.ts +1 -0
- package/src/utils/DropdownOverlay/DropdownOverlay.vue +81 -0
- package/src/utils/DropdownOverlay/index.ts +1 -0
package/package.json
CHANGED
|
@@ -9,20 +9,19 @@ export default {
|
|
|
9
9
|
|
|
10
10
|
<script lang="ts" setup generic="M = unknown">
|
|
11
11
|
import './pl-dropdown.scss';
|
|
12
|
-
import { computed, reactive, ref, unref, useSlots, watch, watchPostEffect } from 'vue';
|
|
13
|
-
import { tap
|
|
12
|
+
import { computed, reactive, ref, unref, useSlots, useTemplateRef, watch, watchPostEffect } from 'vue';
|
|
13
|
+
import { tap } from '@/helpers/functions';
|
|
14
14
|
import { PlTooltip } from '@/components/PlTooltip';
|
|
15
15
|
import DoubleContour from '@/utils/DoubleContour.vue';
|
|
16
16
|
import { useLabelNotch } from '@/utils/useLabelNotch';
|
|
17
17
|
import type { ListOption, ListOptionNormalized } from '@/types';
|
|
18
|
-
import { scrollIntoView } from '@/helpers/dom';
|
|
19
18
|
import { deepEqual } from '@/helpers/objects';
|
|
20
19
|
import DropdownListItem from '@/components/DropdownListItem.vue';
|
|
21
20
|
import LongText from '@/components/LongText.vue';
|
|
22
21
|
import { normalizeListOptions } from '@/helpers/utils';
|
|
23
|
-
import { useElementPosition } from '@/composition/usePosition';
|
|
24
22
|
import { PlIcon16 } from '../PlIcon16';
|
|
25
23
|
import { PlMaskIcon24 } from '../PlMaskIcon24';
|
|
24
|
+
import { DropdownOverlay } from '@/utils/DropdownOverlay';
|
|
26
25
|
|
|
27
26
|
const emit = defineEmits<{
|
|
28
27
|
/**
|
|
@@ -104,10 +103,11 @@ const props = withDefaults(
|
|
|
104
103
|
|
|
105
104
|
const slots = useSlots();
|
|
106
105
|
|
|
107
|
-
const
|
|
108
|
-
const list = ref<HTMLElement | undefined>();
|
|
106
|
+
const rootRef = ref<HTMLElement | undefined>();
|
|
109
107
|
const input = ref<HTMLInputElement | undefined>();
|
|
110
108
|
|
|
109
|
+
const overlay = useTemplateRef('overlay');
|
|
110
|
+
|
|
111
111
|
const data = reactive({
|
|
112
112
|
search: '',
|
|
113
113
|
activeIndex: -1,
|
|
@@ -216,7 +216,7 @@ const selectOption = (v: M | undefined) => {
|
|
|
216
216
|
emit('update:modelValue', v);
|
|
217
217
|
data.search = '';
|
|
218
218
|
data.open = false;
|
|
219
|
-
|
|
219
|
+
rootRef?.value?.focus();
|
|
220
220
|
};
|
|
221
221
|
|
|
222
222
|
const clear = () => emit('update:modelValue', undefined);
|
|
@@ -230,24 +230,12 @@ const onInputFocus = () => (data.open = true);
|
|
|
230
230
|
const onFocusOut = (event: FocusEvent) => {
|
|
231
231
|
const relatedTarget = event.relatedTarget as Node | null;
|
|
232
232
|
|
|
233
|
-
if (!
|
|
233
|
+
if (!rootRef.value?.contains(relatedTarget) && !overlay.value?.listRef?.contains(relatedTarget)) {
|
|
234
234
|
data.search = '';
|
|
235
235
|
data.open = false;
|
|
236
236
|
}
|
|
237
237
|
};
|
|
238
238
|
|
|
239
|
-
const scrollIntoActive = () => {
|
|
240
|
-
const $list = list.value;
|
|
241
|
-
|
|
242
|
-
if (!$list) {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
tapIf($list.querySelector('.hovered-item') as HTMLElement, (opt) => {
|
|
247
|
-
scrollIntoView($list, opt);
|
|
248
|
-
});
|
|
249
|
-
};
|
|
250
|
-
|
|
251
239
|
const handleKeydown = (e: { code: string; preventDefault(): void }) => {
|
|
252
240
|
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.code)) {
|
|
253
241
|
return;
|
|
@@ -266,7 +254,7 @@ const handleKeydown = (e: { code: string; preventDefault(): void }) => {
|
|
|
266
254
|
|
|
267
255
|
if (e.code === 'Escape') {
|
|
268
256
|
data.open = false;
|
|
269
|
-
|
|
257
|
+
rootRef.value?.focus();
|
|
270
258
|
}
|
|
271
259
|
|
|
272
260
|
const filtered = unref(filteredRef);
|
|
@@ -290,7 +278,7 @@ const handleKeydown = (e: { code: string; preventDefault(): void }) => {
|
|
|
290
278
|
data.activeIndex = filteredRef.value[newIndex].index ?? -1;
|
|
291
279
|
};
|
|
292
280
|
|
|
293
|
-
useLabelNotch(
|
|
281
|
+
useLabelNotch(rootRef);
|
|
294
282
|
|
|
295
283
|
watch(() => props.modelValue, updateActive, { immediate: true });
|
|
296
284
|
|
|
@@ -303,44 +291,15 @@ watchPostEffect(() => {
|
|
|
303
291
|
data.search; // to watch
|
|
304
292
|
|
|
305
293
|
if (data.activeIndex >= 0 && data.open) {
|
|
306
|
-
scrollIntoActive();
|
|
294
|
+
overlay.value?.scrollIntoActive();
|
|
307
295
|
}
|
|
308
296
|
});
|
|
309
|
-
|
|
310
|
-
const optionsStyle = reactive({
|
|
311
|
-
top: '0px',
|
|
312
|
-
left: '0px',
|
|
313
|
-
width: '0px',
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
watch(list, (el) => {
|
|
317
|
-
if (el) {
|
|
318
|
-
const rect = el.getBoundingClientRect();
|
|
319
|
-
data.optionsHeight = rect.height;
|
|
320
|
-
window.dispatchEvent(new CustomEvent('adjust'));
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
useElementPosition(root, (pos) => {
|
|
325
|
-
const focusWidth = 3; // see css
|
|
326
|
-
|
|
327
|
-
const downTopOffset = pos.top + pos.height + focusWidth;
|
|
328
|
-
|
|
329
|
-
if (downTopOffset + data.optionsHeight > pos.clientHeight) {
|
|
330
|
-
optionsStyle.top = pos.top - data.optionsHeight - focusWidth + 'px';
|
|
331
|
-
} else {
|
|
332
|
-
optionsStyle.top = downTopOffset + 'px';
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
optionsStyle.left = pos.left + 'px';
|
|
336
|
-
optionsStyle.width = pos.width + 'px';
|
|
337
|
-
});
|
|
338
297
|
</script>
|
|
339
298
|
|
|
340
299
|
<template>
|
|
341
300
|
<div class="pl-dropdown__envelope">
|
|
342
301
|
<div
|
|
343
|
-
ref="
|
|
302
|
+
ref="rootRef"
|
|
344
303
|
:tabindex="tabindex"
|
|
345
304
|
class="pl-dropdown"
|
|
346
305
|
:class="{ open: data.open, error, disabled: isDisabled }"
|
|
@@ -383,20 +342,18 @@ useElementPosition(root, (pos) => {
|
|
|
383
342
|
</template>
|
|
384
343
|
</PlTooltip>
|
|
385
344
|
</label>
|
|
386
|
-
<
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
</div>
|
|
399
|
-
</Teleport>
|
|
345
|
+
<DropdownOverlay v-if="data.open" ref="overlay" :root="rootRef" class="pl-dropdown__options" tabindex="-1" :gap="3">
|
|
346
|
+
<DropdownListItem
|
|
347
|
+
v-for="(item, index) in filteredRef"
|
|
348
|
+
:key="index"
|
|
349
|
+
:option="item"
|
|
350
|
+
:is-selected="item.isSelected"
|
|
351
|
+
:is-hovered="item.isActive"
|
|
352
|
+
:size="optionSize"
|
|
353
|
+
@click.stop="selectOption(item.value)"
|
|
354
|
+
/>
|
|
355
|
+
<div v-if="!filteredRef.length" class="nothing-found">Nothing found</div>
|
|
356
|
+
</DropdownOverlay>
|
|
400
357
|
<DoubleContour class="pl-dropdown__contour" />
|
|
401
358
|
</div>
|
|
402
359
|
</div>
|
|
@@ -9,18 +9,17 @@ export default {
|
|
|
9
9
|
|
|
10
10
|
<script lang="ts" setup generic="M = unknown">
|
|
11
11
|
import './pl-dropdown-multi.scss';
|
|
12
|
-
import { computed, reactive, ref, unref, useSlots, watch, watchPostEffect } from 'vue';
|
|
13
|
-
import { tap
|
|
12
|
+
import { computed, reactive, ref, unref, useSlots, useTemplateRef, watch, watchPostEffect } from 'vue';
|
|
13
|
+
import { tap } from '@/helpers/functions';
|
|
14
14
|
import { PlTooltip } from '@/components/PlTooltip';
|
|
15
15
|
import { PlChip } from '@/components/PlChip';
|
|
16
16
|
import DoubleContour from '@/utils/DoubleContour.vue';
|
|
17
17
|
import { useLabelNotch } from '@/utils/useLabelNotch';
|
|
18
18
|
import type { ListOption } from '@/types';
|
|
19
|
-
import { scrollIntoView } from '@/helpers/dom';
|
|
20
19
|
import DropdownListItem from '@/components/DropdownListItem.vue';
|
|
21
20
|
import { deepEqual, deepIncludes } from '@/helpers/objects';
|
|
22
21
|
import { normalizeListOptions } from '@/helpers/utils';
|
|
23
|
-
import
|
|
22
|
+
import DropdownOverlay from '@/utils/DropdownOverlay/DropdownOverlay.vue';
|
|
24
23
|
|
|
25
24
|
const emit = defineEmits<{
|
|
26
25
|
(e: 'update:modelValue', v: M[]): void;
|
|
@@ -77,9 +76,10 @@ const props = withDefaults(
|
|
|
77
76
|
);
|
|
78
77
|
|
|
79
78
|
const rootRef = ref<HTMLElement | undefined>();
|
|
80
|
-
const list = ref<HTMLElement | undefined>();
|
|
81
79
|
const input = ref<HTMLInputElement | undefined>();
|
|
82
80
|
|
|
81
|
+
const overlay = useTemplateRef('overlay');
|
|
82
|
+
|
|
83
83
|
const data = reactive({
|
|
84
84
|
search: '',
|
|
85
85
|
activeOption: -1,
|
|
@@ -153,24 +153,14 @@ const toggleModel = () => (data.open = !data.open);
|
|
|
153
153
|
const onFocusOut = (event: FocusEvent) => {
|
|
154
154
|
const relatedTarget = event.relatedTarget as Node | null;
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
console.log('>>>> overlay.value?.$el', overlay.value?.$el);
|
|
157
|
+
|
|
158
|
+
if (!rootRef.value?.contains(relatedTarget) && !overlay.value?.listRef?.contains(relatedTarget)) {
|
|
157
159
|
data.search = '';
|
|
158
160
|
data.open = false;
|
|
159
161
|
}
|
|
160
162
|
};
|
|
161
163
|
|
|
162
|
-
const scrollIntoActive = () => {
|
|
163
|
-
const $list = list.value;
|
|
164
|
-
|
|
165
|
-
if (!$list) {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
tapIf($list.querySelector('.hovered-item') as HTMLElement, (opt) => {
|
|
170
|
-
scrollIntoView($list, opt);
|
|
171
|
-
});
|
|
172
|
-
};
|
|
173
|
-
|
|
174
164
|
const handleKeydown = (e: { code: string; preventDefault(): void }) => {
|
|
175
165
|
const { open, activeOption } = data;
|
|
176
166
|
|
|
@@ -206,7 +196,7 @@ const handleKeydown = (e: { code: string; preventDefault(): void }) => {
|
|
|
206
196
|
|
|
207
197
|
data.activeOption = Math.abs(activeOption + d + length) % length;
|
|
208
198
|
|
|
209
|
-
requestAnimationFrame(scrollIntoActive);
|
|
199
|
+
requestAnimationFrame(() => overlay.value?.scrollIntoActive());
|
|
210
200
|
};
|
|
211
201
|
|
|
212
202
|
useLabelNotch(rootRef);
|
|
@@ -221,38 +211,9 @@ watchPostEffect(() => {
|
|
|
221
211
|
data.search;
|
|
222
212
|
|
|
223
213
|
if (data.open) {
|
|
224
|
-
scrollIntoActive();
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
const optionsStyle = reactive({
|
|
229
|
-
top: '0px',
|
|
230
|
-
left: '0px',
|
|
231
|
-
width: '0px',
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
watch(list, (el) => {
|
|
235
|
-
if (el) {
|
|
236
|
-
const rect = el.getBoundingClientRect();
|
|
237
|
-
data.optionsHeight = rect.height;
|
|
238
|
-
window.dispatchEvent(new CustomEvent('adjust'));
|
|
214
|
+
overlay.value?.scrollIntoActive();
|
|
239
215
|
}
|
|
240
216
|
});
|
|
241
|
-
|
|
242
|
-
useElementPosition(rootRef, (pos) => {
|
|
243
|
-
const focusWidth = 5; // see css
|
|
244
|
-
|
|
245
|
-
const downTopOffset = pos.top + pos.height + focusWidth;
|
|
246
|
-
|
|
247
|
-
if (downTopOffset + data.optionsHeight > pos.clientHeight) {
|
|
248
|
-
optionsStyle.top = pos.top - data.optionsHeight - focusWidth + 'px';
|
|
249
|
-
} else {
|
|
250
|
-
optionsStyle.top = downTopOffset + 'px';
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
optionsStyle.left = pos.left + 'px';
|
|
254
|
-
optionsStyle.width = pos.width + 'px';
|
|
255
|
-
});
|
|
256
217
|
</script>
|
|
257
218
|
|
|
258
219
|
<template>
|
|
@@ -297,27 +258,33 @@ useElementPosition(rootRef, (pos) => {
|
|
|
297
258
|
</template>
|
|
298
259
|
</PlTooltip>
|
|
299
260
|
</label>
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
:is-hovered="data.activeOption == index"
|
|
314
|
-
size="medium"
|
|
315
|
-
use-checkbox
|
|
316
|
-
@click.stop="selectOption(item.value)"
|
|
317
|
-
/>
|
|
318
|
-
<div v-if="!filteredOptionsRef.length" class="nothing-found">Nothing found</div>
|
|
261
|
+
<DropdownOverlay
|
|
262
|
+
v-if="data.open"
|
|
263
|
+
ref="overlay"
|
|
264
|
+
:root="rootRef"
|
|
265
|
+
class="pl-multi-dropdown__options"
|
|
266
|
+
:gap="3"
|
|
267
|
+
tabindex="-1"
|
|
268
|
+
@focusout="onFocusOut"
|
|
269
|
+
>
|
|
270
|
+
<div class="pl-multi-dropdown__open-chips-container">
|
|
271
|
+
<PlChip v-for="(opt, i) in selectedOptionsRef" :key="i" closeable small @close="unselectOption(opt.value)">
|
|
272
|
+
{{ opt.label || opt.value }}
|
|
273
|
+
</PlChip>
|
|
319
274
|
</div>
|
|
320
|
-
|
|
275
|
+
<DropdownListItem
|
|
276
|
+
v-for="(item, index) in filteredOptionsRef"
|
|
277
|
+
:key="index"
|
|
278
|
+
:option="item"
|
|
279
|
+
:text-item="'text'"
|
|
280
|
+
:is-selected="item.selected"
|
|
281
|
+
:is-hovered="data.activeOption == index"
|
|
282
|
+
size="medium"
|
|
283
|
+
use-checkbox
|
|
284
|
+
@click.stop="selectOption(item.value)"
|
|
285
|
+
/>
|
|
286
|
+
<div v-if="!filteredOptionsRef.length" class="nothing-found">Nothing found</div>
|
|
287
|
+
</DropdownOverlay>
|
|
321
288
|
<DoubleContour class="pl-multi-dropdown__contour" />
|
|
322
289
|
</div>
|
|
323
290
|
</div>
|
|
@@ -11,6 +11,9 @@ import { defaultData, useVisibleItems, vTextOverflown } from './remote';
|
|
|
11
11
|
import { PlSearchField } from '../PlSearchField';
|
|
12
12
|
import { PlIcon16 } from '../PlIcon16';
|
|
13
13
|
|
|
14
|
+
// note that on a Mac, a click combined with the control key is intercepted by the operating system and used to open a context menu, so ctrlKey is not detectable on click events.
|
|
15
|
+
const isCtrlOrMeta = (ev: KeyboardEvent | MouseEvent) => ev.ctrlKey || ev.metaKey;
|
|
16
|
+
|
|
14
17
|
defineEmits<{
|
|
15
18
|
(e: 'update:modelValue', value: boolean): void;
|
|
16
19
|
(e: 'import:files', value: ImportedFiles): void;
|
|
@@ -119,7 +122,10 @@ const setDirPath = (dirPath: string) => {
|
|
|
119
122
|
};
|
|
120
123
|
|
|
121
124
|
const selectFile = (ev: MouseEvent, file: FileDialogItem) => {
|
|
122
|
-
const { shiftKey
|
|
125
|
+
const { shiftKey } = ev;
|
|
126
|
+
|
|
127
|
+
const ctrlOrMetaKey = isCtrlOrMeta(ev);
|
|
128
|
+
|
|
123
129
|
const { lastSelected } = data;
|
|
124
130
|
|
|
125
131
|
ev.preventDefault();
|
|
@@ -135,7 +141,7 @@ const selectFile = (ev: MouseEvent, file: FileDialogItem) => {
|
|
|
135
141
|
return;
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
if (!
|
|
144
|
+
if (!ctrlOrMetaKey && !shiftKey) {
|
|
139
145
|
data.items.forEach((f) => {
|
|
140
146
|
if (f.id !== file.id) {
|
|
141
147
|
f.selected = false;
|
|
@@ -238,12 +244,14 @@ useEventListener(document, 'keydown', (ev: KeyboardEvent) => {
|
|
|
238
244
|
return;
|
|
239
245
|
}
|
|
240
246
|
|
|
241
|
-
|
|
247
|
+
const ctrlOrMetaKey = isCtrlOrMeta(ev);
|
|
248
|
+
|
|
249
|
+
if (ctrlOrMetaKey && ev.code === 'KeyA') {
|
|
242
250
|
ev.preventDefault();
|
|
243
251
|
selectAll();
|
|
244
252
|
}
|
|
245
253
|
|
|
246
|
-
if (
|
|
254
|
+
if (ctrlOrMetaKey && ev.shiftKey && ev.code === 'Period') {
|
|
247
255
|
ev.preventDefault();
|
|
248
256
|
data.showHiddenItems = !data.showHiddenItems;
|
|
249
257
|
}
|
|
@@ -13,7 +13,7 @@ import DoubleContour from '@/utils/DoubleContour.vue';
|
|
|
13
13
|
import { useLabelNotch } from '@/utils/useLabelNotch';
|
|
14
14
|
import { useValidation } from '@/utils/useValidation';
|
|
15
15
|
import { PlIcon16 } from '../PlIcon16';
|
|
16
|
-
import {
|
|
16
|
+
import { PlMaskIcon24 } from '../PlMaskIcon24';
|
|
17
17
|
import type { Equal } from '@milaboratories/helpers';
|
|
18
18
|
|
|
19
19
|
const slots = useSlots();
|
|
@@ -160,7 +160,7 @@ const displayErrors = computed(() => {
|
|
|
160
160
|
|
|
161
161
|
const hasErrors = computed(() => displayErrors.value.length > 0);
|
|
162
162
|
|
|
163
|
-
const canShowClearable = computed(() => props.clearable && nonEmpty.value && props.type !== 'password');
|
|
163
|
+
const canShowClearable = computed(() => props.clearable && nonEmpty.value && props.type !== 'password' && !props.disabled);
|
|
164
164
|
|
|
165
165
|
const togglePasswordVisibility = () => (showPassword.value = !showPassword.value);
|
|
166
166
|
|
|
@@ -206,7 +206,7 @@ useLabelNotch(rootRef);
|
|
|
206
206
|
/>
|
|
207
207
|
<div class="pl-text-field__append">
|
|
208
208
|
<PlIcon16 v-if="canShowClearable" class="pl-text-field__clearable" name="delete-clear" @click="clear" />
|
|
209
|
-
<
|
|
209
|
+
<PlMaskIcon24 v-if="type === 'password'" :name="passwordIcon" style="cursor: pointer" @click="togglePasswordVisibility" />
|
|
210
210
|
<slot name="append" />
|
|
211
211
|
</div>
|
|
212
212
|
<DoubleContour class="pl-text-field__contour" />
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
.pl-text-field {
|
|
4
4
|
$root: &;
|
|
5
5
|
|
|
6
|
+
--pl-text-field-text-color: var(--txt-01);
|
|
6
7
|
--contour-color: var(--txt-01);
|
|
7
8
|
--label-color: var(--txt-01);
|
|
8
9
|
--contour-border-width: 1px;
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
border: none;
|
|
37
38
|
font-size: inherit;
|
|
38
39
|
background-color: transparent;
|
|
39
|
-
color: var(--
|
|
40
|
+
color: var(--pl-text-field-text-color);
|
|
40
41
|
caret-color: var(--border-color-focus);
|
|
41
42
|
cursor: inherit;
|
|
42
43
|
|
|
@@ -140,6 +141,9 @@
|
|
|
140
141
|
|
|
141
142
|
&.disabled {
|
|
142
143
|
--contour-color: var(--color-dis-01);
|
|
144
|
+
--label-color: var(--dis-01);
|
|
145
|
+
--pl-text-field-text-color: var(--dis-01);
|
|
146
|
+
--mask-icon-bg-color: var(--dis-01);
|
|
143
147
|
cursor: not-allowed;
|
|
144
148
|
}
|
|
145
149
|
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { useElementPosition } from '@/composition/usePosition';
|
|
3
|
+
import { scrollIntoView } from '@/helpers/dom';
|
|
4
|
+
import { tapIf } from '@milaboratories/helpers';
|
|
5
|
+
import { reactive, ref, toRef, watch } from 'vue';
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
root: HTMLElement | undefined; // element to "track"
|
|
9
|
+
gap?: number; // additional gap between overlay and "root" component
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const data = reactive({
|
|
13
|
+
optionsHeight: 0,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const optionsStyle = reactive({
|
|
17
|
+
top: '0px',
|
|
18
|
+
left: '0px',
|
|
19
|
+
width: '0px',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const rootRef = toRef(props, 'root');
|
|
23
|
+
|
|
24
|
+
const listRef = ref<HTMLElement>();
|
|
25
|
+
|
|
26
|
+
const scrollIntoActive = () => {
|
|
27
|
+
const $list = listRef.value;
|
|
28
|
+
|
|
29
|
+
if (!$list) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
tapIf($list.querySelector('.hovered-item') as HTMLElement, (opt) => {
|
|
34
|
+
scrollIntoView($list, opt);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
defineExpose({
|
|
39
|
+
scrollIntoActive,
|
|
40
|
+
listRef,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
watch(listRef, (el) => {
|
|
44
|
+
if (el) {
|
|
45
|
+
requestAnimationFrame(() => {
|
|
46
|
+
const rect = el.getBoundingClientRect();
|
|
47
|
+
data.optionsHeight = rect.height;
|
|
48
|
+
window.dispatchEvent(new CustomEvent('adjust'));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
useElementPosition(rootRef, (pos) => {
|
|
54
|
+
const bodyRect = document.body.getBoundingClientRect();
|
|
55
|
+
|
|
56
|
+
const top = pos.top - bodyRect.top;
|
|
57
|
+
|
|
58
|
+
const left = pos.left - bodyRect.left;
|
|
59
|
+
|
|
60
|
+
const gap = props.gap ?? 0;
|
|
61
|
+
|
|
62
|
+
const downTopOffset = top + pos.height + gap;
|
|
63
|
+
|
|
64
|
+
if (downTopOffset + data.optionsHeight > pos.clientHeight) {
|
|
65
|
+
optionsStyle.top = top - data.optionsHeight - gap + 'px';
|
|
66
|
+
} else {
|
|
67
|
+
optionsStyle.top = downTopOffset + 'px';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
optionsStyle.left = left + 'px';
|
|
71
|
+
optionsStyle.width = pos.width + 'px';
|
|
72
|
+
});
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<Teleport to="body">
|
|
77
|
+
<div ref="listRef" v-bind="$attrs" :style="optionsStyle" tabindex="-1">
|
|
78
|
+
<slot ref="list" />
|
|
79
|
+
</div>
|
|
80
|
+
</Teleport>
|
|
81
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as DropdownOverlay } from './DropdownOverlay.vue';
|