@teammodlr/ui-kit 3.0.0 → 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/clearbutton/ClearButton.vue.d.ts +4 -2
- package/lib/components/colorpicker/ColorPicker.vue.d.ts +6 -3
- package/lib/components/dialog/Dialog.vue.d.ts +14 -6
- package/lib/components/drawer/Drawer.vue.d.ts +8 -4
- 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 +4 -2
- 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 +14 -6
- package/lib/components/popover/Popover.vue.d.ts +4 -2
- package/lib/components/radiobuttongroup/RadioButtonGroup.vue.d.ts +4 -2
- package/lib/components/select/Select.vue.d.ts +22 -0
- package/lib/components/slider/Slider.vue.d.ts +6 -3
- 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/tree/Tree.vue.d.ts +6 -3
- package/lib/index.cjs.js +3 -3
- package/lib/index.es.js +4416 -4343
- package/lib/tsconfig.vitest.tsbuildinfo +1 -0
- package/package.json +1 -1
- package/src/components/cardcontextmenu/CardContextMenu.vue +3 -1
- package/src/components/clearbutton/ClearButton.vue +3 -1
- package/src/components/colorpicker/ColorPicker.vue +4 -1
- package/src/components/dialog/Dialog.vue +10 -1
- package/src/components/drawer/Drawer.vue +5 -1
- package/src/components/dropdown/Dropdown.vue +3 -1
- package/src/components/form/Form.vue +3 -3
- package/src/components/hovercard/HoverCard.vue +3 -1
- package/src/components/modal/Modal.vue +28 -11
- package/src/components/pagination/Pagination.vue +4 -1
- package/src/components/paneldialog/PanelDialog.vue +10 -1
- package/src/components/popover/Popover.vue +3 -1
- package/src/components/radiobuttongroup/RadioButtonGroup.vue +3 -1
- package/src/components/select/Select.vue +93 -73
- package/src/components/select/examples/index.stories.ts +2 -1
- package/src/components/select/examples/index.ts +1 -0
- package/src/components/select/examples/remote.vue +54 -0
- package/src/components/slider/Slider.vue +4 -1
- package/src/components/tab/Tab.vue +3 -1
- package/src/components/tab/TabGroup.vue +4 -4
- package/src/components/table/Table.vue +3 -3
- package/src/components/tree/Tree.vue +4 -1
|
@@ -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"
|
|
@@ -72,7 +72,8 @@
|
|
|
72
72
|
<input v-if="searchable" type="text" ref="searchRef" v-model="search" placeholder="Search"
|
|
73
73
|
:class="style.searchInput" :data-input="`select-menu-search-${id}`" />
|
|
74
74
|
</div>
|
|
75
|
-
<div v-if="
|
|
75
|
+
<div v-if="showRemoteLoading" :class="style.noResults">Loading...</div>
|
|
76
|
+
<div v-else-if="computedOptions.length === 0" :class="style.noResults">No results</div>
|
|
76
77
|
<VList
|
|
77
78
|
v-else
|
|
78
79
|
ref="virtualListRef"
|
|
@@ -116,6 +117,7 @@ import {
|
|
|
116
117
|
computed,
|
|
117
118
|
defineComponent,
|
|
118
119
|
nextTick,
|
|
120
|
+
onBeforeUnmount,
|
|
119
121
|
ref,
|
|
120
122
|
watch,
|
|
121
123
|
watchEffect,
|
|
@@ -143,7 +145,9 @@ export default defineComponent({
|
|
|
143
145
|
...(useInput.emits() as unknown as InputEmitsMap),
|
|
144
146
|
'on-open': () => true,
|
|
145
147
|
'on-close': () => true,
|
|
146
|
-
clear: () => true
|
|
148
|
+
clear: () => true,
|
|
149
|
+
// Emitted when the search text changes (debounced when `remote`). Emits '' when the menu closes.
|
|
150
|
+
search: (_query: string) => true
|
|
147
151
|
},
|
|
148
152
|
props: {
|
|
149
153
|
...useInput.props(),
|
|
@@ -167,6 +171,17 @@ export default defineComponent({
|
|
|
167
171
|
type: Boolean,
|
|
168
172
|
default: false
|
|
169
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
|
+
},
|
|
170
185
|
size: {
|
|
171
186
|
type: String as PropType<Size>,
|
|
172
187
|
default: 'md'
|
|
@@ -302,6 +317,23 @@ export default defineComponent({
|
|
|
302
317
|
|
|
303
318
|
const disabled = computed(() => formDisabled || props.disabled);
|
|
304
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
|
+
);
|
|
305
337
|
|
|
306
338
|
const selected = computed<any | any[]>({
|
|
307
339
|
get() {
|
|
@@ -366,80 +398,44 @@ export default defineComponent({
|
|
|
366
398
|
};
|
|
367
399
|
});
|
|
368
400
|
|
|
401
|
+
const showRemoteLoading = computed(() => props.remote && loading.value);
|
|
402
|
+
|
|
369
403
|
const computedOptions = computed(() => {
|
|
370
404
|
if (!props.options || props.options.length == 0) {
|
|
371
405
|
return [];
|
|
372
406
|
}
|
|
373
407
|
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return option[props.optionLabel]
|
|
379
|
-
.toLowerCase()
|
|
380
|
-
.includes(search.value.toLowerCase());
|
|
381
|
-
})
|
|
382
|
-
.map(option => {
|
|
383
|
-
if (option[props.optionValue] === undefined) {
|
|
384
|
-
throw new Error('Option value is undefined');
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (option[props.optionLabel] === undefined) {
|
|
388
|
-
throw new Error('Option label is undefined');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
let value = option[props.optionValue];
|
|
392
|
-
let active: boolean;
|
|
393
|
-
if (props.multiple && Array.isArray(selected.value)) {
|
|
394
|
-
active = selected.value.includes(value);
|
|
395
|
-
} else {
|
|
396
|
-
active = selected.value === value;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return {
|
|
400
|
-
value: value,
|
|
401
|
-
label: option[props.optionLabel],
|
|
402
|
-
active: active,
|
|
403
|
-
onClick: () => handleOptionClick(value)
|
|
404
|
-
};
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
if (props.allowBlank) {
|
|
408
|
-
options.unshift({
|
|
409
|
-
value: '',
|
|
410
|
-
label: '',
|
|
411
|
-
active: false,
|
|
412
|
-
onClick: () => handleOptionClick('')
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return options;
|
|
417
|
-
}
|
|
408
|
+
// Remote options are already filtered by the server
|
|
409
|
+
const query = props.searchable && !props.remote
|
|
410
|
+
? search.value.toLowerCase()
|
|
411
|
+
: '';
|
|
418
412
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
}
|
|
423
419
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
420
|
+
if (option[props.optionLabel] === undefined) {
|
|
421
|
+
throw new Error('Option label is undefined');
|
|
422
|
+
}
|
|
427
423
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
}
|
|
435
431
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
432
|
+
return {
|
|
433
|
+
value: value,
|
|
434
|
+
label: option[props.optionLabel],
|
|
435
|
+
active: active,
|
|
436
|
+
onClick: () => handleOptionClick(value)
|
|
437
|
+
};
|
|
438
|
+
});
|
|
443
439
|
|
|
444
440
|
if (props.allowBlank) {
|
|
445
441
|
options.unshift({
|
|
@@ -476,7 +472,7 @@ export default defineComponent({
|
|
|
476
472
|
}
|
|
477
473
|
|
|
478
474
|
function openSelect(forceOpen = false) {
|
|
479
|
-
if (
|
|
475
|
+
if (blocked.value) {
|
|
480
476
|
return;
|
|
481
477
|
}
|
|
482
478
|
|
|
@@ -643,8 +639,7 @@ export default defineComponent({
|
|
|
643
639
|
const showClear = computed(() => {
|
|
644
640
|
return props.clearable
|
|
645
641
|
&& hasValue.value
|
|
646
|
-
&& !
|
|
647
|
-
&& !loading.value;
|
|
642
|
+
&& !blocked.value;
|
|
648
643
|
});
|
|
649
644
|
|
|
650
645
|
function clearSelected() {
|
|
@@ -831,7 +826,7 @@ export default defineComponent({
|
|
|
831
826
|
[style.focused]: !props.noBorder && (open.value || focused.value),
|
|
832
827
|
[style.fillBase]: props.fill,
|
|
833
828
|
[style.noBorder]: props.noBorder,
|
|
834
|
-
'm-select--disabled':
|
|
829
|
+
'm-select--disabled': blocked.value,
|
|
835
830
|
'm-select--focused': open.value || focused.value,
|
|
836
831
|
'm-select--multiple': props.multiple,
|
|
837
832
|
'm-select--error': formError.value
|
|
@@ -842,7 +837,7 @@ export default defineComponent({
|
|
|
842
837
|
function getSelectedLabel(value: string | number | []) {
|
|
843
838
|
const option = props.options.find(
|
|
844
839
|
i => i[props.optionValue] === value
|
|
845
|
-
);
|
|
840
|
+
) ?? (props.remote ? knownOptions.get(value) : undefined);
|
|
846
841
|
if (!option) {
|
|
847
842
|
return null;
|
|
848
843
|
}
|
|
@@ -865,6 +860,28 @@ export default defineComponent({
|
|
|
865
860
|
}
|
|
866
861
|
});
|
|
867
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;
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
|
|
868
885
|
watch(open, (value: boolean) => {
|
|
869
886
|
nextTick(() => {
|
|
870
887
|
if (!value) {
|
|
@@ -925,6 +942,7 @@ export default defineComponent({
|
|
|
925
942
|
const isOpen = computed(() => {
|
|
926
943
|
if (
|
|
927
944
|
props.multiple &&
|
|
945
|
+
!props.remote &&
|
|
928
946
|
computedOptions.value.length == 0 &&
|
|
929
947
|
!search.value
|
|
930
948
|
) {
|
|
@@ -1002,6 +1020,8 @@ export default defineComponent({
|
|
|
1002
1020
|
inputListeners,
|
|
1003
1021
|
style,
|
|
1004
1022
|
focused,
|
|
1023
|
+
blocked,
|
|
1024
|
+
showRemoteLoading,
|
|
1005
1025
|
fade,
|
|
1006
1026
|
toggleAllItems,
|
|
1007
1027
|
optionItemHeight,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import MSelect from '../Select.vue';
|
|
2
|
-
import { MSelectBasic, MSelectSearchable, MSelectMultiple, MSelectFill, MSelectClearable } from './index';
|
|
2
|
+
import { MSelectBasic, MSelectSearchable, MSelectMultiple, MSelectFill, MSelectClearable, MSelectRemote } from './index';
|
|
3
3
|
|
|
4
4
|
import { createStory, sizeArgType } from '../../../__storybook__';
|
|
5
5
|
|
|
@@ -16,3 +16,4 @@ export const Searchable = createStory(MSelectSearchable);
|
|
|
16
16
|
export const Multiple = createStory(MSelectMultiple);
|
|
17
17
|
export const Fill = createStory(MSelectFill);
|
|
18
18
|
export const Clearable = createStory(MSelectClearable);
|
|
19
|
+
export const Remote = createStory(MSelectRemote);
|
|
@@ -3,3 +3,4 @@ export { default as MSelectSearchable } from './searchable.vue';
|
|
|
3
3
|
export { default as MSelectMultiple } from './multiple.vue';
|
|
4
4
|
export { default as MSelectFill } from './fill.vue';
|
|
5
5
|
export { default as MSelectClearable } from './clearable.vue';
|
|
6
|
+
export { default as MSelectRemote } from './remote.vue';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MSelect
|
|
3
|
+
v-model="value"
|
|
4
|
+
:options="options"
|
|
5
|
+
:option-value="'id'"
|
|
6
|
+
:option-label="'name'"
|
|
7
|
+
:loading="loading"
|
|
8
|
+
searchable
|
|
9
|
+
remote
|
|
10
|
+
@search="onSearch"
|
|
11
|
+
@on-open="onSearch('')"
|
|
12
|
+
/>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts" setup>
|
|
16
|
+
import { ref } from "vue";
|
|
17
|
+
|
|
18
|
+
const everything = Array.from({ length: 500 }, (_, i) => ({
|
|
19
|
+
id: i,
|
|
20
|
+
name: `Option ${i + 1}`
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Stand-in for a server call
|
|
24
|
+
function fetchOptions(query: string): Promise<typeof everything> {
|
|
25
|
+
return new Promise(resolve => {
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
resolve(
|
|
28
|
+
everything
|
|
29
|
+
.filter(o => o.name.toLowerCase().includes(query.toLowerCase()))
|
|
30
|
+
.slice(0, 20)
|
|
31
|
+
);
|
|
32
|
+
}, 500);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const value = ref();
|
|
37
|
+
const options = ref<typeof everything>([]);
|
|
38
|
+
const loading = ref(false);
|
|
39
|
+
let latest = 0;
|
|
40
|
+
|
|
41
|
+
async function onSearch(query: string) {
|
|
42
|
+
const request = ++latest;
|
|
43
|
+
loading.value = true;
|
|
44
|
+
const result = await fetchOptions(query);
|
|
45
|
+
|
|
46
|
+
// Ignore stale responses
|
|
47
|
+
if (request !== latest) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
options.value = result;
|
|
52
|
+
loading.value = false;
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
@@ -29,7 +29,10 @@ export default defineComponent({
|
|
|
29
29
|
default: '',
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
|
-
emits:
|
|
32
|
+
emits: {
|
|
33
|
+
'update:modelValue': (_value: number) => true,
|
|
34
|
+
change: (_value: number) => true
|
|
35
|
+
},
|
|
33
36
|
setup(props, { emit }) {
|
|
34
37
|
const slider = ref<HTMLElement | null>(null);
|
|
35
38
|
const dragging = ref(false);
|
|
@@ -48,7 +48,9 @@ export default defineComponent({
|
|
|
48
48
|
default: false
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
|
-
emits:
|
|
51
|
+
emits: {
|
|
52
|
+
onClose: (_tab: { value: any; label: any }) => true
|
|
53
|
+
},
|
|
52
54
|
setup(props, { attrs, slots }) {
|
|
53
55
|
const computedValue = computed(() => props.value);
|
|
54
56
|
const computedLabel = computed(() => props.label || props.value);
|
|
@@ -53,10 +53,10 @@ export default defineComponent({
|
|
|
53
53
|
validator: (value: string) => ['base', 'card'].includes(value)
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
|
-
emits:
|
|
57
|
-
'update:modelValue',
|
|
58
|
-
'update:selected'
|
|
59
|
-
|
|
56
|
+
emits: {
|
|
57
|
+
'update:modelValue': (_tab: any) => true,
|
|
58
|
+
'update:selected': (_tab: any) => true
|
|
59
|
+
},
|
|
60
60
|
setup(props, { emit }) {
|
|
61
61
|
const wrapperRef = ref<HTMLElement>();
|
|
62
62
|
const tabsRef = ref<HTMLElement>();
|
|
@@ -22,7 +22,10 @@ export default defineComponent({
|
|
|
22
22
|
components: { TreeNode },
|
|
23
23
|
name: 'Tree',
|
|
24
24
|
props: treeProps,
|
|
25
|
-
emits:
|
|
25
|
+
emits: {
|
|
26
|
+
'update:expandedKeys': (_keys: any) => true,
|
|
27
|
+
dblclick: (_node: any) => true
|
|
28
|
+
},
|
|
26
29
|
setup(props, { emit }) {
|
|
27
30
|
const selfElRef = ref<HTMLDivElement | null>(null);
|
|
28
31
|
const controlledExpandedKeysRef = toRef(props, 'expandedKeys');
|