@teammodlr/ui-kit 3.0.0-beta.9 → 3.0.1
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/lib/components/cardcontextmenu/CardContextMenu.vue.d.ts +4 -2
- package/lib/components/checkbox/Checkbox.vue.d.ts +4 -4
- package/lib/components/clearbutton/ClearButton.vue.d.ts +9 -0
- package/lib/components/clearbutton/style/ClearButton.css.d.ts +2 -0
- package/lib/components/clearbutton/style/index.d.ts +1 -0
- package/lib/components/colorpicker/ColorPicker.vue.d.ts +6 -4
- package/lib/components/colorpicker/style/ColorPicker.css.d.ts +1 -0
- package/lib/components/dialog/Dialog.vue.d.ts +14 -6
- package/lib/components/dialog/style/Dialog.css.d.ts +2 -0
- package/lib/components/drawer/Drawer.vue.d.ts +146 -0
- package/lib/components/drawer/style/Drawer.css.d.ts +33 -0
- package/lib/components/drawer/style/index.d.ts +1 -0
- package/lib/components/dropdown/Dropdown.vue.d.ts +4 -2
- package/lib/components/form/Form.vue.d.ts +4 -2
- package/lib/components/hovercard/HoverCard.vue.d.ts +124 -0
- package/lib/components/hovercard/style/HoverCard.css.d.ts +3 -0
- package/lib/components/hovercard/style/index.d.ts +1 -0
- package/lib/components/index.d.ts +3 -0
- package/lib/components/input/Input.vue.d.ts +20 -5
- package/lib/components/input/style/Input.css.d.ts +4 -0
- package/lib/components/modal/Modal.vue.d.ts +11 -4
- package/lib/components/pagination/Pagination.vue.d.ts +6 -3
- package/lib/components/paneldialog/PanelDialog.vue.d.ts +175 -0
- package/lib/components/paneldialog/style/PanelDialog.css.d.ts +8 -0
- package/lib/components/paneldialog/style/index.d.ts +1 -0
- package/lib/components/popover/Popover.vue.d.ts +28 -2
- package/lib/components/radio/Radio.vue.d.ts +5 -5
- package/lib/components/radiobutton/RadioButton.vue.d.ts +4 -4
- package/lib/components/radiobuttongroup/RadioButtonGroup.vue.d.ts +4 -2
- package/lib/components/select/Select.vue.d.ts +94 -91
- package/lib/components/select/style/Select.css.d.ts +9 -1
- package/lib/components/slider/Slider.vue.d.ts +6 -3
- package/lib/components/switch/Switch.vue.d.ts +4 -4
- package/lib/components/tab/Tab.vue.d.ts +10 -2
- package/lib/components/tab/TabGroup.vue.d.ts +6 -3
- package/lib/components/table/Table.vue.d.ts +4 -2
- package/lib/components/textarea/Textarea.vue.d.ts +16 -5
- package/lib/components/textarea/style/Textarea.css.d.ts +2 -0
- package/lib/components/tree/Tree.vue.d.ts +6 -3
- package/lib/composables/useInput.d.ts +9 -2
- package/lib/index.cjs.js +3 -3
- package/lib/index.es.js +8526 -6393
- package/lib/style.css +1 -1
- package/lib/tsconfig.app.tsbuildinfo +1 -0
- package/lib/tsconfig.vitest.tsbuildinfo +1 -0
- package/package.json +7 -4
- package/src/components/cardcontextmenu/CardContextMenu.vue +3 -1
- package/src/components/cardcontextmenu/style/Dropdown.css.ts +7 -0
- package/src/components/clearbutton/ClearButton.vue +41 -0
- package/src/components/clearbutton/style/ClearButton.css.ts +37 -0
- package/src/components/clearbutton/style/index.ts +1 -0
- package/src/components/colorpicker/ColorPicker.vue +12 -12
- package/src/components/colorpicker/examples/basic.vue +8 -17
- package/src/components/colorpicker/style/ColorPicker.css.ts +6 -1
- package/src/components/dialog/Dialog.vue +12 -6
- package/src/components/dialog/style/Dialog.css.ts +30 -0
- package/src/components/drawer/Drawer.vue +280 -0
- package/src/components/drawer/examples/basic.vue +15 -0
- package/src/components/drawer/examples/custom-title.vue +21 -0
- package/src/components/drawer/examples/index.stories.ts +24 -0
- package/src/components/drawer/examples/index.ts +7 -0
- package/src/components/drawer/examples/no-mask.vue +15 -0
- package/src/components/drawer/examples/no-outside-close.vue +15 -0
- package/src/components/drawer/examples/placements.vue +28 -0
- package/src/components/drawer/examples/with-footer.vue +21 -0
- package/src/components/drawer/examples/with-trigger.vue +13 -0
- package/src/components/drawer/style/Drawer.css.ts +208 -0
- package/src/components/drawer/style/index.ts +1 -0
- package/src/components/dropdown/Dropdown.vue +4 -2
- package/src/components/form/Form.vue +3 -3
- package/src/components/hovercard/HoverCard.vue +165 -0
- package/src/components/hovercard/style/HoverCard.css.ts +175 -0
- package/src/components/hovercard/style/index.ts +1 -0
- package/src/components/index.ts +3 -0
- package/src/components/input/Input.vue +52 -4
- package/src/components/input/examples/clearable.vue +18 -0
- package/src/components/input/examples/index.stories.ts +3 -1
- package/src/components/input/examples/index.ts +1 -0
- package/src/components/input/style/Input.css.ts +19 -0
- package/src/components/messageprovider/MessageProvider.vue +33 -7
- package/src/components/modal/Modal.vue +28 -11
- package/src/components/pagination/Pagination.vue +4 -1
- package/src/components/paneldialog/PanelDialog.vue +509 -0
- package/src/components/paneldialog/examples/basic.vue +36 -0
- package/src/components/paneldialog/examples/index.stories.ts +22 -0
- package/src/components/paneldialog/examples/index.ts +2 -0
- package/src/components/paneldialog/examples/modal.vue +31 -0
- package/src/components/paneldialog/style/PanelDialog.css.ts +90 -0
- package/src/components/paneldialog/style/index.ts +1 -0
- package/src/components/popover/Popover.vue +9 -2
- package/src/components/radiobuttongroup/RadioButtonGroup.vue +3 -1
- package/src/components/select/Select.vue +211 -111
- package/src/components/select/examples/clearable.vue +18 -0
- package/src/components/select/examples/fill.vue +52 -0
- package/src/components/select/examples/index.stories.ts +4 -1
- package/src/components/select/examples/index.ts +3 -0
- package/src/components/select/examples/remote.vue +54 -0
- package/src/components/select/style/Select.css.ts +140 -24
- package/src/components/slider/Slider.vue +4 -1
- package/src/components/tab/Tab.vue +4 -2
- package/src/components/tab/TabGroup.vue +4 -4
- package/src/components/tab/style/Tab.css.ts +6 -0
- package/src/components/table/Table.vue +3 -3
- package/src/components/textarea/Textarea.vue +40 -4
- package/src/components/textarea/examples/clearable.vue +16 -0
- package/src/components/textarea/examples/index.stories.ts +2 -1
- package/src/components/textarea/examples/index.ts +1 -0
- package/src/components/textarea/style/Textarea.css.ts +10 -0
- package/src/components/tree/Tree.vue +4 -1
- package/src/composables/useInput.ts +10 -2
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
:side="resolvedSide"
|
|
10
10
|
:align="resolvedAlign"
|
|
11
11
|
:side-offset="sideOffset"
|
|
12
|
+
:collision-padding="collisionPadding"
|
|
12
13
|
:style="{ zIndex: zIndex }"
|
|
13
14
|
>
|
|
14
15
|
<slot name="content" />
|
|
@@ -68,12 +69,16 @@ export default defineComponent({
|
|
|
68
69
|
},
|
|
69
70
|
transfer: {
|
|
70
71
|
type: Boolean,
|
|
71
|
-
default:
|
|
72
|
+
default: true
|
|
72
73
|
},
|
|
73
74
|
sideOffset: {
|
|
74
75
|
type: Number,
|
|
75
76
|
default: 5
|
|
76
77
|
},
|
|
78
|
+
collisionPadding: {
|
|
79
|
+
type: [Number, Object] as PropType<number | { top?: number; right?: number; bottom?: number; left?: number }>,
|
|
80
|
+
default: 10
|
|
81
|
+
},
|
|
77
82
|
arrow: {
|
|
78
83
|
type: Boolean,
|
|
79
84
|
default: false
|
|
@@ -86,7 +91,9 @@ export default defineComponent({
|
|
|
86
91
|
type: Number
|
|
87
92
|
}
|
|
88
93
|
},
|
|
89
|
-
emits:
|
|
94
|
+
emits: {
|
|
95
|
+
'update:open': (_value: boolean) => true
|
|
96
|
+
},
|
|
90
97
|
setup(props, { emit }) {
|
|
91
98
|
const attrs = useAttrs();
|
|
92
99
|
const { nextZIndex } = useZIndex();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<PopoverRoot :open="isOpen" @update:open="handleOpenChange">
|
|
3
|
-
<div ref="selectRef" tabindex="-1">
|
|
3
|
+
<div ref="selectRef" tabindex="-1" :class="fillRootClass">
|
|
4
4
|
<label v-if="label" :for="id" :class="style.label" :id="`${id}-label`">
|
|
5
5
|
{{ label }}
|
|
6
6
|
</label>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{{ description }}
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
<PopoverTrigger as-child :disabled="
|
|
11
|
+
<PopoverTrigger as-child :disabled="blocked">
|
|
12
12
|
<div :class="computedWrapperClasses" v-on="inputListeners" :id="id" ref="container"
|
|
13
13
|
:aria-disabled="disabled ? 'true' : 'false'" :aria-labelledby="`${id}-label`" tabindex="0"
|
|
14
14
|
data-m-select-trigger="true"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<div :class="selectComputedClasses">
|
|
17
17
|
<template v-if="multiple && Array.isArray(selected) && selected.length > 0">
|
|
18
18
|
<MTag v-for="value in selectedMax" :key="value" :closeable="true"
|
|
19
|
-
@close="
|
|
19
|
+
@close="() => removeSelected(value)" :size="size">
|
|
20
20
|
{{ getSelectedLabel(value) }}
|
|
21
21
|
</MTag>
|
|
22
22
|
<template v-if="selectedCountNotShown > 0">
|
|
@@ -26,15 +26,16 @@
|
|
|
26
26
|
</template>
|
|
27
27
|
</template>
|
|
28
28
|
<template v-else-if="!multiple && selected">
|
|
29
|
-
{{ getSelectedLabel(selected) }}
|
|
29
|
+
<span :class="fillLabelClass">{{ getSelectedLabel(selected) }}</span>
|
|
30
30
|
</template>
|
|
31
31
|
<template v-else>
|
|
32
|
-
<div :class="style.placeholder">{{ placeholder }}</div>
|
|
32
|
+
<div :class="[style.placeholder, fillLabelClass]">{{ placeholder }}</div>
|
|
33
33
|
</template>
|
|
34
34
|
<span :class="selectComputedArrowClasses">
|
|
35
35
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>
|
|
36
36
|
</span>
|
|
37
37
|
</div>
|
|
38
|
+
<MClearButton v-if="showClear" :class="style.clear" @clear="clearSelected" />
|
|
38
39
|
</div>
|
|
39
40
|
</PopoverTrigger>
|
|
40
41
|
|
|
@@ -71,24 +72,31 @@
|
|
|
71
72
|
<input v-if="searchable" type="text" ref="searchRef" v-model="search" placeholder="Search"
|
|
72
73
|
:class="style.searchInput" :data-input="`select-menu-search-${id}`" />
|
|
73
74
|
</div>
|
|
74
|
-
<div :class="style.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
75
|
+
<div v-if="showRemoteLoading" :class="style.noResults">Loading...</div>
|
|
76
|
+
<div v-else-if="computedOptions.length === 0" :class="style.noResults">No results</div>
|
|
77
|
+
<VList
|
|
78
|
+
v-else
|
|
79
|
+
ref="virtualListRef"
|
|
80
|
+
:data="computedOptions"
|
|
81
|
+
:item-size="optionItemHeight"
|
|
82
|
+
:style="{ height: optionsHeight + 'px', padding: '4px' }"
|
|
83
|
+
>
|
|
84
|
+
<template #default="{ item, index }">
|
|
85
|
+
<div :class="[optionSizeClass, {
|
|
86
|
+
[style.option]: true,
|
|
87
|
+
[style.optionSelected]: item.active,
|
|
88
|
+
[style.optionFocused]: selectedIndex === index
|
|
89
|
+
}]" @click.prevent="item.onClick" @mouseenter="selectedIndex = index"
|
|
90
|
+
:data-element="`select-menu-option-${id}-${item.value}`">
|
|
91
|
+
<span :class="style.optionIcon">
|
|
92
|
+
<svg v-if="item.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
93
|
+
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd"></path>
|
|
94
|
+
</svg>
|
|
95
|
+
</span>
|
|
96
|
+
<span :class="style.optionLabel">{{ item.label }}</span>
|
|
97
|
+
</div>
|
|
90
98
|
</template>
|
|
91
|
-
</
|
|
99
|
+
</VList>
|
|
92
100
|
</PopoverContent>
|
|
93
101
|
</Transition>
|
|
94
102
|
</PopoverPortal>
|
|
@@ -101,7 +109,7 @@
|
|
|
101
109
|
</template>
|
|
102
110
|
|
|
103
111
|
<script lang="ts">
|
|
104
|
-
import { useInput } from '../../composables/useInput';
|
|
112
|
+
import { useInput, type InputEmitsMap } from '../../composables/useInput';
|
|
105
113
|
import type { MaybeArray, Size } from '../../types/props';
|
|
106
114
|
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui';
|
|
107
115
|
import { useEventListener } from '@vueuse/core';
|
|
@@ -109,13 +117,20 @@ import {
|
|
|
109
117
|
computed,
|
|
110
118
|
defineComponent,
|
|
111
119
|
nextTick,
|
|
120
|
+
onBeforeUnmount,
|
|
112
121
|
ref,
|
|
113
122
|
watch,
|
|
114
123
|
watchEffect,
|
|
115
124
|
type PropType
|
|
116
125
|
} from 'vue';
|
|
117
126
|
import MTag from '../tag/Tag.vue';
|
|
127
|
+
import MClearButton from '../clearbutton/ClearButton.vue';
|
|
128
|
+
import { VList } from 'virtua/vue';
|
|
118
129
|
import * as style from './style';
|
|
130
|
+
|
|
131
|
+
interface VListHandle {
|
|
132
|
+
scrollToIndex(index: number, opts?: { align?: 'start' | 'center' | 'end' | 'nearest' }): void;
|
|
133
|
+
}
|
|
119
134
|
import { fade } from '../../styles/transitions.css';
|
|
120
135
|
import { useInteractive } from '../../composables/useInteractive';
|
|
121
136
|
import type { OnUpdateValue, OnUpdateValueImpl } from './types';
|
|
@@ -127,7 +142,12 @@ let shouldOpenNextFocusedSelect = false;
|
|
|
127
142
|
export default defineComponent({
|
|
128
143
|
name: 'MSelect',
|
|
129
144
|
emits: {
|
|
130
|
-
...useInput.emits()
|
|
145
|
+
...(useInput.emits() as unknown as InputEmitsMap),
|
|
146
|
+
'on-open': () => true,
|
|
147
|
+
'on-close': () => true,
|
|
148
|
+
clear: () => true,
|
|
149
|
+
// Emitted when the search text changes (debounced when `remote`). Emits '' when the menu closes.
|
|
150
|
+
search: (_query: string) => true
|
|
131
151
|
},
|
|
132
152
|
props: {
|
|
133
153
|
...useInput.props(),
|
|
@@ -151,6 +171,17 @@ export default defineComponent({
|
|
|
151
171
|
type: Boolean,
|
|
152
172
|
default: false
|
|
153
173
|
},
|
|
174
|
+
// Options are loaded from a server: search no longer filters locally,
|
|
175
|
+
// the consumer listens to `search` and updates `options`.
|
|
176
|
+
remote: {
|
|
177
|
+
type: Boolean,
|
|
178
|
+
default: false
|
|
179
|
+
},
|
|
180
|
+
// Debounce (ms) applied to the `search` event when `remote`
|
|
181
|
+
searchDebounce: {
|
|
182
|
+
type: Number,
|
|
183
|
+
default: 300
|
|
184
|
+
},
|
|
154
185
|
size: {
|
|
155
186
|
type: String as PropType<Size>,
|
|
156
187
|
default: 'md'
|
|
@@ -184,7 +215,7 @@ export default defineComponent({
|
|
|
184
215
|
default: false
|
|
185
216
|
},
|
|
186
217
|
error: {
|
|
187
|
-
type: String
|
|
218
|
+
type: String as PropType<string | null>
|
|
188
219
|
},
|
|
189
220
|
allowBlank: {
|
|
190
221
|
type: Boolean,
|
|
@@ -202,9 +233,26 @@ export default defineComponent({
|
|
|
202
233
|
type: Boolean,
|
|
203
234
|
default: false
|
|
204
235
|
},
|
|
236
|
+
fill: {
|
|
237
|
+
type: Boolean,
|
|
238
|
+
default: false
|
|
239
|
+
},
|
|
240
|
+
noBorder: {
|
|
241
|
+
type: Boolean,
|
|
242
|
+
default: false
|
|
243
|
+
},
|
|
205
244
|
zIndex: {
|
|
206
245
|
type: Number
|
|
207
246
|
},
|
|
247
|
+
clearable: {
|
|
248
|
+
type: Boolean,
|
|
249
|
+
default: false
|
|
250
|
+
},
|
|
251
|
+
// Value a single select resets to when cleared (e.g. a "Don't restrict" option)
|
|
252
|
+
clearValue: {
|
|
253
|
+
type: [String, Number, Boolean, Object] as PropType<string | number | boolean | object | null>,
|
|
254
|
+
default: undefined
|
|
255
|
+
},
|
|
208
256
|
showDataOptions: {
|
|
209
257
|
type: Boolean,
|
|
210
258
|
default: true
|
|
@@ -214,6 +262,8 @@ export default defineComponent({
|
|
|
214
262
|
},
|
|
215
263
|
components: {
|
|
216
264
|
MTag,
|
|
265
|
+
MClearButton,
|
|
266
|
+
VList,
|
|
217
267
|
PopoverContent,
|
|
218
268
|
PopoverPortal,
|
|
219
269
|
PopoverRoot,
|
|
@@ -221,7 +271,7 @@ export default defineComponent({
|
|
|
221
271
|
},
|
|
222
272
|
setup(props, { emit, expose }) {
|
|
223
273
|
const container = ref<HTMLElement | null>(null);
|
|
224
|
-
const
|
|
274
|
+
const virtualListRef = ref<VListHandle | null>(null);
|
|
225
275
|
const searchRef = ref<HTMLInputElement>();
|
|
226
276
|
const selectRef = ref<HTMLElement>();
|
|
227
277
|
const popper = ref<HTMLElement>();
|
|
@@ -267,6 +317,23 @@ export default defineComponent({
|
|
|
267
317
|
|
|
268
318
|
const disabled = computed(() => formDisabled || props.disabled);
|
|
269
319
|
const loading = computed(() => formLoading || props.loading);
|
|
320
|
+
// In remote mode `loading` is shown inside the menu (so the user can keep
|
|
321
|
+
// typing while a search is in flight) rather than locking the trigger.
|
|
322
|
+
const blocked = computed(() => disabled.value || (loading.value && !props.remote));
|
|
323
|
+
|
|
324
|
+
// Remote options come and go between searches, so remember every option we've
|
|
325
|
+
// seen to keep labels for selected values that are no longer in `options`.
|
|
326
|
+
const knownOptions = new Map<any, any>();
|
|
327
|
+
watch(
|
|
328
|
+
() => props.options,
|
|
329
|
+
(options) => {
|
|
330
|
+
if (!props.remote || !options) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
options.forEach(option => knownOptions.set(option[props.optionValue], option));
|
|
334
|
+
},
|
|
335
|
+
{ immediate: true }
|
|
336
|
+
);
|
|
270
337
|
|
|
271
338
|
const selected = computed<any | any[]>({
|
|
272
339
|
get() {
|
|
@@ -331,80 +398,44 @@ export default defineComponent({
|
|
|
331
398
|
};
|
|
332
399
|
});
|
|
333
400
|
|
|
401
|
+
const showRemoteLoading = computed(() => props.remote && loading.value);
|
|
402
|
+
|
|
334
403
|
const computedOptions = computed(() => {
|
|
335
404
|
if (!props.options || props.options.length == 0) {
|
|
336
405
|
return [];
|
|
337
406
|
}
|
|
338
407
|
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return option[props.optionLabel]
|
|
344
|
-
.toLowerCase()
|
|
345
|
-
.includes(search.value.toLowerCase());
|
|
346
|
-
})
|
|
347
|
-
.map(option => {
|
|
348
|
-
if (option[props.optionValue] === undefined) {
|
|
349
|
-
throw new Error('Option value is undefined');
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (option[props.optionLabel] === undefined) {
|
|
353
|
-
throw new Error('Option label is undefined');
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
let value = option[props.optionValue];
|
|
357
|
-
let active: boolean;
|
|
358
|
-
if (props.multiple && Array.isArray(selected.value)) {
|
|
359
|
-
active = selected.value.includes(value);
|
|
360
|
-
} else {
|
|
361
|
-
active = selected.value === value;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return {
|
|
365
|
-
value: value,
|
|
366
|
-
label: option[props.optionLabel],
|
|
367
|
-
active: active,
|
|
368
|
-
onClick: () => handleOptionClick(value)
|
|
369
|
-
};
|
|
370
|
-
});
|
|
408
|
+
// Remote options are already filtered by the server
|
|
409
|
+
const query = props.searchable && !props.remote
|
|
410
|
+
? search.value.toLowerCase()
|
|
411
|
+
: '';
|
|
371
412
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return options;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
let options = props.options.map(option => {
|
|
385
|
-
if (option[props.optionValue] === undefined) {
|
|
386
|
-
throw new Error('Option value is undefined');
|
|
387
|
-
}
|
|
413
|
+
const options = props.options
|
|
414
|
+
.filter(option => !query || option[props.optionLabel].toLowerCase().includes(query))
|
|
415
|
+
.map(option => {
|
|
416
|
+
if (option[props.optionValue] === undefined) {
|
|
417
|
+
throw new Error('Option value is undefined');
|
|
418
|
+
}
|
|
388
419
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
420
|
+
if (option[props.optionLabel] === undefined) {
|
|
421
|
+
throw new Error('Option label is undefined');
|
|
422
|
+
}
|
|
392
423
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
424
|
+
const value = option[props.optionValue];
|
|
425
|
+
let active: boolean;
|
|
426
|
+
if (props.multiple && Array.isArray(selected.value)) {
|
|
427
|
+
active = selected.value.includes(value);
|
|
428
|
+
} else {
|
|
429
|
+
active = selected.value === value;
|
|
430
|
+
}
|
|
400
431
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
432
|
+
return {
|
|
433
|
+
value: value,
|
|
434
|
+
label: option[props.optionLabel],
|
|
435
|
+
active: active,
|
|
436
|
+
onClick: () => handleOptionClick(value)
|
|
437
|
+
};
|
|
438
|
+
});
|
|
408
439
|
|
|
409
440
|
if (props.allowBlank) {
|
|
410
441
|
options.unshift({
|
|
@@ -441,7 +472,7 @@ export default defineComponent({
|
|
|
441
472
|
}
|
|
442
473
|
|
|
443
474
|
function openSelect(forceOpen = false) {
|
|
444
|
-
if (
|
|
475
|
+
if (blocked.value) {
|
|
445
476
|
return;
|
|
446
477
|
}
|
|
447
478
|
|
|
@@ -593,9 +624,33 @@ export default defineComponent({
|
|
|
593
624
|
}
|
|
594
625
|
}
|
|
595
626
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
627
|
+
const hasValue = computed(() => {
|
|
628
|
+
if (props.multiple) {
|
|
629
|
+
return Array.isArray(selected.value) && selected.value.length > 0;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (props.clearValue !== undefined && selected.value === props.clearValue) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return !!selected.value;
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const showClear = computed(() => {
|
|
640
|
+
return props.clearable
|
|
641
|
+
&& hasValue.value
|
|
642
|
+
&& !blocked.value;
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
function clearSelected() {
|
|
646
|
+
selected.value = props.multiple
|
|
647
|
+
? []
|
|
648
|
+
: (props.clearValue !== undefined ? props.clearValue : '');
|
|
649
|
+
emit('clear');
|
|
650
|
+
container.value?.focus();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function removeSelected(value: string | number) {
|
|
599
654
|
if (!props.multiple) {
|
|
600
655
|
return;
|
|
601
656
|
}
|
|
@@ -741,21 +796,37 @@ export default defineComponent({
|
|
|
741
796
|
]
|
|
742
797
|
})
|
|
743
798
|
|
|
799
|
+
const optionItemHeightMap: Record<string, number> = { xs: 30, sm: 32, md: 38, lg: 42, xl: 46 };
|
|
800
|
+
const optionItemHeight = computed(() => optionItemHeightMap[props.size] ?? 38);
|
|
801
|
+
const optionsHeight = computed(() => {
|
|
802
|
+
const total = computedOptions.value.length * optionItemHeight.value + 8; // 8px = paddingTop+paddingBottom
|
|
803
|
+
return Math.min(total, 222);
|
|
804
|
+
});
|
|
805
|
+
const optionSizeClass = computed(() => style.optionSizes[props.size]);
|
|
806
|
+
const fillRootClass = computed(() => props.fill ? style.fillRoot : undefined);
|
|
807
|
+
const fillLabelClass = computed(() => props.fill ? style.fillLabel : undefined);
|
|
808
|
+
|
|
744
809
|
const computedWrapperClasses = computed(() => {
|
|
745
|
-
return [
|
|
810
|
+
return [
|
|
811
|
+
style.wrapper,
|
|
812
|
+
{ [style.fill]: props.fill }
|
|
813
|
+
];
|
|
746
814
|
});
|
|
747
815
|
|
|
748
816
|
const selectComputedClasses = computed(() => {
|
|
749
817
|
return [
|
|
750
818
|
style.base,
|
|
819
|
+
style.sizes[props.size],
|
|
751
820
|
{
|
|
821
|
+
[style.hasClear]: showClear.value,
|
|
752
822
|
[style.selectedTags]:
|
|
753
823
|
props.multiple &&
|
|
754
824
|
Array.isArray(selected.value) &&
|
|
755
825
|
selected.value.length > 0,
|
|
756
|
-
[style.focused]: open.value || focused.value,
|
|
757
|
-
|
|
758
|
-
|
|
826
|
+
[style.focused]: !props.noBorder && (open.value || focused.value),
|
|
827
|
+
[style.fillBase]: props.fill,
|
|
828
|
+
[style.noBorder]: props.noBorder,
|
|
829
|
+
'm-select--disabled': blocked.value,
|
|
759
830
|
'm-select--focused': open.value || focused.value,
|
|
760
831
|
'm-select--multiple': props.multiple,
|
|
761
832
|
'm-select--error': formError.value
|
|
@@ -766,7 +837,7 @@ export default defineComponent({
|
|
|
766
837
|
function getSelectedLabel(value: string | number | []) {
|
|
767
838
|
const option = props.options.find(
|
|
768
839
|
i => i[props.optionValue] === value
|
|
769
|
-
);
|
|
840
|
+
) ?? (props.remote ? knownOptions.get(value) : undefined);
|
|
770
841
|
if (!option) {
|
|
771
842
|
return null;
|
|
772
843
|
}
|
|
@@ -784,21 +855,42 @@ export default defineComponent({
|
|
|
784
855
|
if (!keyboardNavigation.value) {
|
|
785
856
|
return;
|
|
786
857
|
}
|
|
787
|
-
if (typeof index !== 'undefined'
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
858
|
+
if (typeof index !== 'undefined') {
|
|
859
|
+
virtualListRef.value?.scrollToIndex(index, { align: 'nearest' });
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// Emit search changes. Remote searches are debounced so we don't hit the
|
|
864
|
+
// server on every keystroke; clearing the query (e.g. on close) is immediate.
|
|
865
|
+
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
866
|
+
watch(search, (query: string) => {
|
|
867
|
+
clearTimeout(searchTimer);
|
|
868
|
+
|
|
869
|
+
if (props.remote && props.searchDebounce > 0 && query !== '') {
|
|
870
|
+
searchTimer = setTimeout(() => emit('search', query), props.searchDebounce);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
emit('search', query);
|
|
875
|
+
});
|
|
876
|
+
onBeforeUnmount(() => clearTimeout(searchTimer));
|
|
877
|
+
|
|
878
|
+
// Remote options replace the list under the cursor, keep the highlight in range
|
|
879
|
+
watch(computedOptions, (options) => {
|
|
880
|
+
if (selectedIndex.value !== undefined && selectedIndex.value >= options.length) {
|
|
881
|
+
selectedIndex.value = 0;
|
|
792
882
|
}
|
|
793
883
|
});
|
|
794
884
|
|
|
795
885
|
watch(open, (value: boolean) => {
|
|
796
886
|
nextTick(() => {
|
|
797
887
|
if (!value) {
|
|
888
|
+
emit('on-close');
|
|
798
889
|
search.value = '';
|
|
799
890
|
}
|
|
800
891
|
|
|
801
892
|
if (value) {
|
|
893
|
+
emit('on-open');
|
|
802
894
|
zIndex.value = nextZIndex();
|
|
803
895
|
}
|
|
804
896
|
|
|
@@ -816,7 +908,7 @@ export default defineComponent({
|
|
|
816
908
|
});
|
|
817
909
|
}
|
|
818
910
|
|
|
819
|
-
if (value
|
|
911
|
+
if (value) {
|
|
820
912
|
const selectedValue = Array.isArray(selected.value)
|
|
821
913
|
? selected.value[0]
|
|
822
914
|
: selected.value;
|
|
@@ -824,9 +916,7 @@ export default defineComponent({
|
|
|
824
916
|
i => i.value === selectedValue
|
|
825
917
|
);
|
|
826
918
|
if (index !== -1) {
|
|
827
|
-
|
|
828
|
-
block: 'nearest'
|
|
829
|
-
});
|
|
919
|
+
nextTick(() => virtualListRef.value?.scrollToIndex(index, { align: 'nearest' }));
|
|
830
920
|
selectedIndex.value = index;
|
|
831
921
|
} else if (computedOptions.value.length > 0) {
|
|
832
922
|
selectedIndex.value = 0;
|
|
@@ -852,6 +942,7 @@ export default defineComponent({
|
|
|
852
942
|
const isOpen = computed(() => {
|
|
853
943
|
if (
|
|
854
944
|
props.multiple &&
|
|
945
|
+
!props.remote &&
|
|
855
946
|
computedOptions.value.length == 0 &&
|
|
856
947
|
!search.value
|
|
857
948
|
) {
|
|
@@ -894,7 +985,7 @@ export default defineComponent({
|
|
|
894
985
|
|
|
895
986
|
return {
|
|
896
987
|
container,
|
|
897
|
-
|
|
988
|
+
virtualListRef,
|
|
898
989
|
searchRef,
|
|
899
990
|
selectRef,
|
|
900
991
|
popper,
|
|
@@ -922,13 +1013,22 @@ export default defineComponent({
|
|
|
922
1013
|
getSelectedLabel,
|
|
923
1014
|
openSelect,
|
|
924
1015
|
removeSelected,
|
|
1016
|
+
showClear,
|
|
1017
|
+
clearSelected,
|
|
925
1018
|
inputError,
|
|
926
1019
|
transitionName,
|
|
927
1020
|
inputListeners,
|
|
928
1021
|
style,
|
|
929
1022
|
focused,
|
|
1023
|
+
blocked,
|
|
1024
|
+
showRemoteLoading,
|
|
930
1025
|
fade,
|
|
931
|
-
toggleAllItems
|
|
1026
|
+
toggleAllItems,
|
|
1027
|
+
optionItemHeight,
|
|
1028
|
+
optionsHeight,
|
|
1029
|
+
optionSizeClass,
|
|
1030
|
+
fillRootClass,
|
|
1031
|
+
fillLabelClass
|
|
932
1032
|
};
|
|
933
1033
|
}
|
|
934
1034
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 320px;">
|
|
3
|
+
<MSelect v-model="single" :options="options" clearable />
|
|
4
|
+
<MSelect v-model="multiple" :options="options" multiple clearable />
|
|
5
|
+
</div>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script lang="ts" setup>
|
|
9
|
+
import { ref } from 'vue';
|
|
10
|
+
|
|
11
|
+
const options = Array.from({ length: 10 }, (_, i) => ({
|
|
12
|
+
value: i + 1,
|
|
13
|
+
label: `Option ${i + 1}`
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const single = ref(2);
|
|
17
|
+
const multiple = ref([1, 2]);
|
|
18
|
+
</script>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div style="display: grid; grid-template-columns: 120px 1fr 1fr; border: 1px solid #e5e7eb;">
|
|
3
|
+
<div style="padding: 6px 8px; font-size: 13px; border-right: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb; background: #f9fafb; font-weight: 500;">Row 1</div>
|
|
4
|
+
<div style="height: 36px; border-right: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;">
|
|
5
|
+
<MSelect size="xs" v-model="row1Status" :options="statusOptions" fill no-border />
|
|
6
|
+
</div>
|
|
7
|
+
<div style="height: 36px; border-bottom: 1px solid #e5e7eb;">
|
|
8
|
+
<MSelect size="xs" v-model="row1Priority" :options="priorityOptions" fill no-border />
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div style="padding: 6px 8px; font-size: 13px; border-right: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb; background: #f9fafb; font-weight: 500;">Row 2</div>
|
|
12
|
+
<div style="height: 36px; border-right: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;">
|
|
13
|
+
<MSelect size="xs" v-model="row2Status" :options="statusOptions" fill no-border />
|
|
14
|
+
</div>
|
|
15
|
+
<div style="height: 36px; border-bottom: 1px solid #e5e7eb;">
|
|
16
|
+
<MSelect size="xs" v-model="row2Priority" :options="priorityOptions" fill no-border />
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div style="padding: 6px 8px; font-size: 13px; border-right: 1px solid #e5e7eb; background: #f9fafb; font-weight: 500;">Row 3</div>
|
|
20
|
+
<div style="height: 36px; border-right: 1px solid #e5e7eb;">
|
|
21
|
+
<MSelect size="xs" v-model="row3Status" :options="statusOptions" fill no-border />
|
|
22
|
+
</div>
|
|
23
|
+
<div style="height: 36px;">
|
|
24
|
+
<MSelect size="xs" v-model="row3Priority" :options="priorityOptions" fill no-border />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script lang="ts" setup>
|
|
30
|
+
import { ref } from 'vue';
|
|
31
|
+
|
|
32
|
+
const statusOptions = [
|
|
33
|
+
{ value: 'todo', label: 'To Do' },
|
|
34
|
+
{ value: 'in_progress', label: 'In Progress' },
|
|
35
|
+
{ value: 'in_review', label: 'In Review' },
|
|
36
|
+
{ value: 'done', label: 'Done' }
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const priorityOptions = [
|
|
40
|
+
{ value: 'low', label: 'Low' },
|
|
41
|
+
{ value: 'medium', label: 'Medium' },
|
|
42
|
+
{ value: 'high', label: 'High' },
|
|
43
|
+
{ value: 'critical', label: 'Critical' }
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const row1Status = ref('in_progress');
|
|
47
|
+
const row1Priority = ref('high');
|
|
48
|
+
const row2Status = ref('todo');
|
|
49
|
+
const row2Priority = ref('medium');
|
|
50
|
+
const row3Status = ref('done');
|
|
51
|
+
const row3Priority = ref('low');
|
|
52
|
+
</script>
|