@teammodlr/ui-kit 2.1.47 → 2.1.51

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.
Files changed (82) hide show
  1. package/lib/components/avatar/Avatar.vue.d.ts +1 -1
  2. package/lib/components/badge/style/Badge.css.d.ts +2 -2
  3. package/lib/components/button/Button.vue.d.ts +4 -4
  4. package/lib/components/button/style/Button.css.d.ts +3 -3
  5. package/lib/components/checkbox/Checkbox.vue.d.ts +4 -4
  6. package/lib/components/configprovider/ConfigProvider.vue.d.ts +18 -11
  7. package/lib/components/confirm/ConfirmProvider.vue.d.ts +10 -10
  8. package/lib/components/dropdown/DropdownItem.vue.d.ts +2 -2
  9. package/lib/components/dropdown/style/Dropdown.css.d.ts +1 -1
  10. package/lib/components/input/Input.vue.d.ts +6 -6
  11. package/lib/components/link/Link.vue.d.ts +2 -2
  12. package/lib/components/messageprovider/Message.vue.d.ts +2 -2
  13. package/lib/components/messageprovider/MessageProvider.vue.d.ts +5 -5
  14. package/lib/components/messageprovider/style/Message.css.d.ts +1 -1
  15. package/lib/components/modal/Modal.vue.d.ts +1 -1
  16. package/lib/components/radio/Radio.vue.d.ts +6 -6
  17. package/lib/components/radiobutton/RadioButton.vue.d.ts +4 -4
  18. package/lib/components/renderer/renderer.d.ts +1 -1
  19. package/lib/components/select/Select.vue.d.ts +21 -20
  20. package/lib/components/switch/Switch.vue.d.ts +4 -4
  21. package/lib/components/tab/Tab.vue.d.ts +4 -6
  22. package/lib/components/tab/TabGroup.vue.d.ts +17 -3
  23. package/lib/components/tab/style/Tab.css.d.ts +7 -0
  24. package/lib/components/tab/style/index.d.ts +1 -0
  25. package/lib/components/table/Table.vue.d.ts +2 -2
  26. package/lib/components/textarea/Textarea.vue.d.ts +5 -5
  27. package/lib/composables/useInput.d.ts +1 -1
  28. package/lib/index.cjs.js +9 -12
  29. package/lib/index.es.js +2059 -2102
  30. package/lib/install.d.ts +1 -1
  31. package/lib/style.css +1 -1
  32. package/package.json +76 -90
  33. package/src/components/avatar/examples/index.stories.ts +25 -25
  34. package/src/components/avatargroup/examples/index.stories.ts +19 -19
  35. package/src/components/badge/examples/index.stories.ts +19 -19
  36. package/src/components/breadcrumb/examples/index.stories.ts +16 -16
  37. package/src/components/button/examples/index.stories.ts +37 -37
  38. package/src/components/buttongroup/examples/index.stories.ts +17 -17
  39. package/src/components/checkbox/examples/index.stories.ts +16 -16
  40. package/src/components/colorpicker/examples/index.stories.ts +12 -12
  41. package/src/components/configprovider/ConfigProvider.vue +38 -31
  42. package/src/components/confirm/ConfirmProvider.vue +1 -2
  43. package/src/components/confirm/examples/index.stories.ts +26 -26
  44. package/src/components/dialog/examples/index.stories.ts +14 -14
  45. package/src/components/dropdown/examples/index.stories.ts +26 -26
  46. package/src/components/form/examples/index.stories.ts +23 -23
  47. package/src/components/image/examples/index.stories.ts +10 -10
  48. package/src/components/input/examples/index.stories.ts +26 -26
  49. package/src/components/link/examples/index.stories.ts +15 -15
  50. package/src/components/messageprovider/examples/index.stories.ts +26 -26
  51. package/src/components/modal/examples/index.stories.ts +16 -16
  52. package/src/components/pagination/examples/index.stories.ts +11 -11
  53. package/src/components/radio/examples/index.stories.ts +16 -16
  54. package/src/components/radiobuttongroup/examples/index.stories.ts +18 -18
  55. package/src/components/renderer/examlpes/index.stories.ts +14 -14
  56. package/src/components/select/Select.vue +755 -755
  57. package/src/components/select/examples/basic.vue +26 -26
  58. package/src/components/select/examples/index.stories.ts +16 -16
  59. package/src/components/select/types.ts +27 -27
  60. package/src/components/skeleton/examples/index.stories.ts +13 -13
  61. package/src/components/switch/examples/index.stories.ts +15 -15
  62. package/src/components/switch/style/Switch.css.ts +135 -135
  63. package/src/components/tab/Tab.vue +85 -98
  64. package/src/components/tab/TabGroup.vue +96 -90
  65. package/src/components/tab/examples/index.stories.ts +21 -10
  66. package/src/components/tab/style/Tab.css.ts +137 -0
  67. package/src/components/tab/style/index.ts +1 -0
  68. package/src/components/table/examples/index.stories.ts +17 -17
  69. package/src/components/tag/examples/index.stories.ts +16 -16
  70. package/src/components/textarea/examples/index.stories.ts +14 -14
  71. package/src/components/tooltip/examples/index.stories.ts +17 -17
  72. package/src/components/transfer/examples/index.stories.ts +12 -12
  73. package/src/components/tree/examples/index.stories.ts +12 -12
  74. package/src/components/virtualscroll/examples/index.stories.ts +11 -11
  75. package/src/composables/useCachedUid.ts +15 -16
  76. package/src/composables/useInput.ts +151 -151
  77. package/src/examples/cards/cards.stories.ts +9 -9
  78. package/src/examples/modal/modal.stories.ts +9 -9
  79. package/src/types/props.ts +21 -21
  80. package/src/utils/call.ts +34 -34
  81. package/src/utils/injection.ts +17 -21
  82. package/src/components/tab/Tab.css +0 -113
@@ -1,755 +1,755 @@
1
- <template>
2
- <div ref="selectRef" tabindex="-1">
3
- <label v-if="label" :for="id" :class="style.label" :id="`${id}-label`">
4
- {{ label }}
5
- </label>
6
- <div v-if="description" :class="style.description">
7
- {{ description }}
8
- </div>
9
-
10
- <div :class="computedWrapperClasses" v-on="inputListeners" :id="id" ref="container"
11
- :aria-disabled="disabled ? 'true' : 'false'" :aria-labelledby="`${id}-label`" tabindex="0"
12
- @mousedown.prevent="onSelectClick" @focusin.stop="focusSelect">
13
- <div :class="selectComputedClasses">
14
- <template v-if="
15
- multiple &&
16
- Array.isArray(selected) &&
17
- selected.length > 0
18
- ">
19
- <MTag v-for="value in selectedMax" :key="value" :closeable="true"
20
- @close="event => removeSelected(event, value)" :size="size">
21
- {{ getSelectedLabel(value) }}
22
- </MTag>
23
- <template v-if="selectedCountNotShown > 0">
24
- <MTag :size="size">
25
- +{{ selectedCountNotShown }} more
26
- </MTag>
27
- </template>
28
- </template>
29
- <template v-else-if="!multiple && selected">
30
- {{ getSelectedLabel(selected) }}
31
- </template>
32
- <template v-else>
33
- <div :class="style.placeholder">{{ placeholder }}</div>
34
- </template>
35
- </div>
36
-
37
- <Teleport :to="'body'">
38
- <Transition :enter-active-class="scale.enterActive" :leave-active-class="scale.leaveActive"
39
- :enter-from-class="scale.enterFrom" :leave-to-class="scale.leaveTo">
40
- <div :class="style.menu" v-if="isOpen" ref="popper" :style="computedConsistentWidth"
41
- :aria-hidden="isOpen ? 'false' : 'true'">
42
- <div v-if="searchable || (multiple && selectAll)" :class="style.search">
43
- <div v-if="multiple && selectAll" :class="style.checkAll">
44
- <button role="button" type="button" :class="{
45
- [style.checkAllButton]: true,
46
- [style.checkAllButtonActive]:
47
- computedOptions.length > 0 &&
48
- computedOptions.length ===
49
- selected.length
50
- }" @click.prevent="toggleAllItems" aria-description="Select all items">
51
- <svg v-if="
52
- computedOptions.length > 0 &&
53
- computedOptions.length ===
54
- selected.length
55
- " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
56
- aria-hidden="true">
57
- <path fill-rule="evenodd"
58
- 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"
59
- clip-rule="evenodd"></path>
60
- </svg>
61
- </button>
62
- </div>
63
- <input v-if="searchable" type="text" ref="searchRef" v-model="search" placeholder="Search"
64
- :class="style.searchInput" />
65
- </div>
66
- <div :class="style.options">
67
- <div v-for="(option, index) in computedOptions" :key="index" ref="optionsRef" :class="{
68
- [style.option]: true,
69
- [style.optionSelected]: option.active,
70
- [style.optionFocused]:
71
- selectedIndex === index
72
- }" @click.prevent="option.onClick" @hover="selectedIndex = index">
73
- <span :class="style.optionIcon">
74
- <svg v-if="option.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
75
- fill="currentColor" aria-hidden="true">
76
- <path fill-rule="evenodd"
77
- 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"
78
- clip-rule="evenodd"></path>
79
- </svg>
80
- </span>
81
- <span :class="style.optionLabel">{{
82
- option.label
83
- }}</span>
84
- </div>
85
- <template v-if="computedOptions.length === 0">
86
- <div :class="style.noResults">No results</div>
87
- </template>
88
- </div>
89
- </div>
90
- </Transition>
91
- </Teleport>
92
- </div>
93
- <div v-if="inputError" :class="style.errorMessage">
94
- {{ inputError }}
95
- </div>
96
- </div>
97
- </template>
98
-
99
- <script lang="ts">
100
- import { useInput } from '../../composables/useInput';
101
- import { usePopper, type Placement } from '../../composables/usePopper';
102
- import type { MaybeArray, Size } from '../../types/props';
103
- import { onClickOutside, useEventListener } from '@vueuse/core';
104
- import {
105
- computed,
106
- defineComponent,
107
- nextTick,
108
- ref,
109
- watch,
110
- watchEffect,
111
- type PropType
112
- } from 'vue';
113
- import MTag from '../tag/Tag.vue';
114
- import * as style from './style';
115
- import { scale } from '../../styles/transitions.css';
116
- import { useInteractive } from '../../composables/useInteractive';
117
- import type { OnUpdateValue, OnUpdateValueImpl } from './types';
118
- import { call } from "../../utils/call";
119
-
120
- export default defineComponent({
121
- name: 'MSelect',
122
- emits: {
123
- ...useInput.emits()
124
- },
125
- props: {
126
- ...useInput.props(),
127
- ...useInteractive.props(),
128
- label: {
129
- type: String
130
- },
131
- placeholder: {
132
- type: String,
133
- default: 'Select an option'
134
- },
135
- options: {
136
- type: Array as PropType<Array<any>>,
137
- required: true
138
- },
139
- searchable: {
140
- type: Boolean,
141
- default: false
142
- },
143
- multiple: {
144
- type: Boolean,
145
- default: false
146
- },
147
- size: {
148
- type: String as PropType<Size>,
149
- default: 'md'
150
- },
151
- description: {
152
- type: String,
153
- default: ''
154
- },
155
- optionValue: {
156
- type: String,
157
- default: 'value'
158
- },
159
- optionLabel: {
160
- type: String,
161
- default: 'label'
162
- },
163
- transitionName: {
164
- type: String,
165
- default: 'fade'
166
- },
167
- consistentWidth: {
168
- type: Boolean,
169
- default: true
170
- },
171
- disabled: {
172
- type: Boolean,
173
- default: false
174
- },
175
- loading: {
176
- type: Boolean,
177
- default: false
178
- },
179
- error: {
180
- type: String
181
- },
182
- allowBlank: {
183
- type: Boolean,
184
- default: false
185
- },
186
- selectAll: {
187
- type: Boolean,
188
- default: false
189
- },
190
- max: {
191
- type: Number,
192
- default: 5
193
- },
194
- 'onUpdate:value': [Function, Array] as PropType<MaybeArray<OnUpdateValue> | undefined>,
195
- onUpdateValue: [Function, Array] as PropType<MaybeArray<OnUpdateValue> | undefined>,
196
- },
197
- components: {
198
- MTag
199
- },
200
- setup(props, { emit, expose }) {
201
- const container = ref<HTMLElement | null>(null);
202
- const wrapper = ref<HTMLElement>();
203
- const optionsRef = ref<HTMLElement[]>();
204
- const searchRef = ref<HTMLElement>();
205
- const selectRef = ref<HTMLElement>();
206
- const selectedIndex = ref<number | undefined>(0);
207
- const open = ref<boolean>(false);
208
- const focused = ref<boolean>(false);
209
- const search = ref<string>('');
210
- const placement = ref<Placement>('bottom-start');
211
- const transfer = ref(false);
212
- const width = ref<number>(0);
213
- const keyboardNavigation = ref<boolean>(false);
214
-
215
- function doUpdateValue(
216
- value: string | number | Array<string | number> | null,
217
- option: object | null
218
- ): void {
219
- const {
220
- 'onUpdate:value': _onUpdateValue,
221
- onUpdateValue
222
- } = props;
223
-
224
- if (onUpdateValue) call(onUpdateValue as OnUpdateValueImpl, value, option)
225
- if (_onUpdateValue) {
226
- call(_onUpdateValue as OnUpdateValueImpl, value, option)
227
- }
228
- }
229
-
230
-
231
- const { focus, blur } = useInteractive(container);
232
- // TODO: Add debounce on keyboard movement (hover flicks it back up)
233
- const {
234
- id,
235
- insideForm,
236
- inputListeners,
237
- formError,
238
- formDisabled,
239
- formLoading
240
- } = useInput(props, { focus, emit, withListeners: false });
241
-
242
- const { popper, updatePopper } = usePopper({
243
- placement,
244
- transfer,
245
- reference: container,
246
- wrapper,
247
- offset: [0, 5]
248
- });
249
-
250
- const disabled = computed(() => formDisabled || props.disabled);
251
- const loading = computed(() => formLoading || props.loading);
252
-
253
- const selected = computed<any | any[]>({
254
- get() {
255
- if (props.multiple) {
256
- if (!props.modelValue) {
257
- return [];
258
- }
259
-
260
- if (Array.isArray(props.modelValue)) {
261
- return props.modelValue;
262
- }
263
-
264
- return [props.modelValue];
265
- }
266
-
267
- return props.modelValue || null;
268
- },
269
- set(value: string | number | []) {
270
- emit('update:modelValue', value);
271
- doUpdateValue(value, null);
272
- }
273
- });
274
-
275
- const selectedMax = computed(() => {
276
- if (props.max <= 0) {
277
- return selected.value;
278
- }
279
-
280
- if (
281
- props.multiple &&
282
- Array.isArray(selected.value) &&
283
- selected.value.length > props.max
284
- ) {
285
- return selected.value.slice(0, props.max);
286
- }
287
-
288
- return selected.value;
289
- });
290
-
291
- const selectedCountNotShown = computed(() => {
292
- if (
293
- props.multiple &&
294
- Array.isArray(selected.value) &&
295
- props.max > 0
296
- ) {
297
- return selected.value.length - props.max;
298
- }
299
-
300
- return 0;
301
- });
302
-
303
- const computedConsistentWidth = computed(() => {
304
- if (props.consistentWidth) {
305
- return {
306
- width: width.value + 'px'
307
- };
308
- }
309
-
310
- return {};
311
- });
312
-
313
- const computedOptions = computed(() => {
314
- if (!props.options || props.options.length == 0) {
315
- return [];
316
- }
317
-
318
- // Filter options by search
319
- if (props.searchable && search.value) {
320
- let options = props.options
321
- .filter(option => {
322
- return option[props.optionLabel]
323
- .toLowerCase()
324
- .includes(search.value.toLowerCase());
325
- })
326
- .map(option => {
327
- if (option[props.optionValue] === undefined) {
328
- throw new Error('Option value is undefined');
329
- }
330
-
331
- if (option[props.optionLabel] === undefined) {
332
- throw new Error('Option label is undefined');
333
- }
334
-
335
- let value = option[props.optionValue];
336
- let active: boolean;
337
- if (props.multiple && Array.isArray(selected.value)) {
338
- active = selected.value.includes(value);
339
- } else {
340
- active = selected.value === value;
341
- }
342
-
343
- return {
344
- value: value,
345
- label: option[props.optionLabel],
346
- active: active,
347
- onClick: () => handleOptionClick(value)
348
- };
349
- });
350
-
351
- if (props.allowBlank) {
352
- options.unshift({
353
- value: '',
354
- label: '',
355
- active: false,
356
- onClick: () => handleOptionClick('')
357
- });
358
- }
359
-
360
- return options;
361
- }
362
-
363
- let options = props.options.map(option => {
364
- if (option[props.optionValue] === undefined) {
365
- throw new Error('Option value is undefined');
366
- }
367
-
368
- if (option[props.optionLabel] === undefined) {
369
- throw new Error('Option label is undefined');
370
- }
371
-
372
- let value = option[props.optionValue];
373
- let active: boolean;
374
- if (props.multiple && Array.isArray(selected.value)) {
375
- active = selected.value.includes(value);
376
- } else {
377
- active = selected.value === value;
378
- }
379
-
380
- return {
381
- value: value,
382
- label: option[props.optionLabel],
383
- active: active,
384
- onClick: () => handleOptionClick(value)
385
- };
386
- });
387
-
388
- if (props.allowBlank) {
389
- options.unshift({
390
- value: '',
391
- label: '',
392
- active: false,
393
- onClick: () => handleOptionClick('')
394
- });
395
- }
396
-
397
- return options;
398
- });
399
-
400
- function toggleAllItems() {
401
- if (!props.multiple) {
402
- return;
403
- }
404
-
405
- if (computedOptions.value.length === 0) {
406
- return;
407
- }
408
-
409
- if (Array.isArray(selected.value)) {
410
- if (selected.value.length === computedOptions.value.length) {
411
- selected.value = [];
412
- } else {
413
- selected.value = computedOptions.value.map(
414
- option => option.value
415
- );
416
- }
417
- } else {
418
- selected.value = [];
419
- }
420
- }
421
-
422
- function openSelect() {
423
- if (disabled.value || loading.value) {
424
- return;
425
- }
426
-
427
- if (open.value) {
428
- open.value = false;
429
- return;
430
- }
431
- open.value = true;
432
- }
433
-
434
- function getSelectedIndex(
435
- start: number | undefined,
436
- direction = 'down'
437
- ) {
438
- if (typeof start === 'undefined') {
439
- start = direction === 'down' ? -1 : 1;
440
- }
441
-
442
- if (direction === 'down') {
443
- let next = start + 1;
444
-
445
- if (next > computedOptions.value.length - 1) {
446
- next = 0;
447
- }
448
-
449
- selectedIndex.value = next;
450
- } else {
451
- let next = start - 1;
452
-
453
- if (next < 0) {
454
- next = computedOptions.value.length - 1;
455
- }
456
-
457
- selectedIndex.value = next;
458
- }
459
- }
460
-
461
- function handleOptionClick(value: string | number) {
462
- const option = computedOptions.value.find(
463
- item => item.value === value
464
- );
465
- if (!option) {
466
- return;
467
- }
468
-
469
- if (props.multiple) {
470
- if (Array.isArray(selected.value)) {
471
- const index = selected.value.indexOf(value);
472
-
473
- if (index !== -1) {
474
- selected.value.splice(index, 1);
475
- } else {
476
- selected.value.push(value);
477
- emit('update:modelValue', selected.value);
478
- }
479
- } else {
480
- selected.value = [value];
481
- }
482
-
483
- updatePopper();
484
- } else {
485
- selected.value = value;
486
- open.value = false;
487
- }
488
- }
489
-
490
- function removeSelected(event: MouseEvent, value: string | number) {
491
- event.preventDefault();
492
- event.stopPropagation();
493
- if (!props.multiple) {
494
- return;
495
- }
496
-
497
- if (Array.isArray(selected.value)) {
498
- const index = selected.value.indexOf(value);
499
-
500
- if (index !== -1) {
501
- selected.value.splice(index, 1);
502
- }
503
-
504
- updatePopper();
505
- }
506
- }
507
-
508
- useEventListener('resize', () => {
509
- open.value = false;
510
- });
511
-
512
- function handleKeyDown(e: Event) {
513
- if (computedOptions.value.length === 0) {
514
- return;
515
- }
516
-
517
- if (props.disabled) {
518
- return;
519
- }
520
-
521
- keyboardNavigation.value = true;
522
-
523
- const event: KeyboardEvent = e as KeyboardEvent;
524
-
525
- switch (event.code) {
526
- case 'Tab':
527
- if (open.value) {
528
- open.value = false;
529
- }
530
- break;
531
- case 'ArrowDown':
532
- e.preventDefault();
533
- if (!open.value) {
534
- open.value = true;
535
- return;
536
- }
537
-
538
- getSelectedIndex(selectedIndex.value, 'down');
539
- break;
540
- case 'ArrowUp':
541
- e.preventDefault();
542
- if (!open.value) {
543
- return;
544
- }
545
-
546
- getSelectedIndex(selectedIndex.value, 'up');
547
- break;
548
- case 'Enter':
549
- e.preventDefault();
550
- e.stopPropagation();
551
-
552
- if (!open.value) {
553
- open.value = true;
554
- return;
555
- }
556
-
557
- if (typeof selectedIndex.value !== 'undefined') {
558
- handleOptionClick(
559
- computedOptions.value[selectedIndex.value].value
560
- );
561
- if (!props.multiple) {
562
- open.value = false;
563
- }
564
- }
565
- break;
566
- case 'Escape':
567
- e.preventDefault();
568
- e.stopPropagation();
569
-
570
- if (!open.value) {
571
- return;
572
- }
573
-
574
- open.value = false;
575
- break;
576
- }
577
- }
578
-
579
- const transitionName = computed(() => {
580
- return `m-transition__${props.transitionName}`;
581
- });
582
-
583
- const containerComputedClasses = computed(() => {
584
- return [
585
- 'm-select__container',
586
- {
587
- 'm-form__input': insideForm
588
- }
589
- ];
590
- });
591
-
592
- const computedWrapperClasses = computed(() => {
593
- return [
594
- style.wrapper,
595
- {
596
- [style.focused]: focused.value
597
- }
598
- ];
599
- });
600
-
601
- const selectComputedClasses = computed(() => {
602
- return [
603
- style.base,
604
- {
605
- [style.selectedTags]:
606
- props.multiple &&
607
- Array.isArray(selected.value) &&
608
- selected.value.length > 0,
609
- [style.focused]: open.value,
610
- // [`m-select--${props.size}`]: props.size,
611
- 'm-select--disabled': disabled.value || loading.value,
612
- 'm-select--focused': open.value,
613
- 'm-select--multiple': props.multiple,
614
- 'm-select--error': formError.value
615
- }
616
- ];
617
- });
618
-
619
- function getSelectedLabel(value: string | number | []) {
620
- const option = props.options.find(
621
- i => i[props.optionValue] === value
622
- );
623
- if (!option) {
624
- return null;
625
- }
626
-
627
- return option[props.optionLabel];
628
- }
629
-
630
- useEventListener(selectRef, 'keydown', handleKeyDown);
631
- useEventListener(searchRef, 'keydown', handleKeyDown);
632
-
633
- onClickOutside(
634
- container,
635
- () => {
636
- open.value = false;
637
- },
638
- { ignore: [searchRef, popper] }
639
- );
640
-
641
- watchEffect(() => {
642
- if (container.value && open.value) {
643
- width.value = container.value.offsetWidth;
644
- }
645
- });
646
-
647
- watch(selectedIndex, (index: number | undefined) => {
648
- if (!keyboardNavigation.value) {
649
- return;
650
- }
651
- if (typeof index !== 'undefined' && optionsRef.value) {
652
- optionsRef.value[index].scrollIntoView({
653
- block: 'nearest',
654
- inline: 'nearest'
655
- });
656
- }
657
- });
658
-
659
- watch(open, (value: boolean) => {
660
- nextTick(() => {
661
- if (!value) {
662
- search.value = '';
663
- }
664
-
665
- if (props.searchable && searchRef.value) {
666
- searchRef.value.focus();
667
- }
668
- if (value && optionsRef.value) {
669
- const index = computedOptions.value.findIndex(
670
- i => i.value === selected.value
671
- );
672
- if (index !== -1) {
673
- optionsRef.value[index].scrollIntoView({
674
- block: 'nearest'
675
- });
676
- selectedIndex.value = index;
677
- }
678
- }
679
-
680
- keyboardNavigation.value = false;
681
- });
682
- });
683
-
684
- const inputError = computed(() => {
685
- return formError.value || props.error;
686
- });
687
-
688
- const isOpen = computed(() => {
689
- if (
690
- props.multiple &&
691
- computedOptions.value.length == 0 &&
692
- !search.value
693
- ) {
694
- return false;
695
- }
696
- return open.value;
697
- });
698
-
699
- const focusSelect = (event: FocusEvent) => {
700
- setTimeout(() => {
701
- if (selectRef.value && !open.value) {
702
- openSelect();
703
- }
704
- }, 10);
705
- };
706
-
707
- const onSelectClick = (event: MouseEvent) => {
708
- event.preventDefault();
709
- event.stopPropagation();
710
- openSelect();
711
- };
712
-
713
- expose({
714
- focus,
715
- blur
716
- });
717
-
718
- return {
719
- container,
720
- optionsRef,
721
- searchRef,
722
- selectRef,
723
- popper,
724
- focusSelect,
725
- onSelectClick,
726
-
727
- id,
728
- isOpen,
729
- open,
730
- selected,
731
- selectedMax,
732
- selectedCountNotShown,
733
- computedOptions,
734
- selectedIndex,
735
- search,
736
-
737
- containerComputedClasses,
738
- selectComputedClasses,
739
- computedWrapperClasses,
740
- computedConsistentWidth,
741
-
742
- getSelectedLabel,
743
- openSelect,
744
- removeSelected,
745
- inputError,
746
- transitionName,
747
- inputListeners,
748
- style,
749
- focused,
750
- scale,
751
- toggleAllItems
752
- };
753
- }
754
- });
755
- </script>
1
+ <template>
2
+ <div ref="selectRef" tabindex="-1">
3
+ <label v-if="label" :for="id" :class="style.label" :id="`${id}-label`">
4
+ {{ label }}
5
+ </label>
6
+ <div v-if="description" :class="style.description">
7
+ {{ description }}
8
+ </div>
9
+
10
+ <div :class="computedWrapperClasses" v-on="inputListeners" :id="id" ref="container"
11
+ :aria-disabled="disabled ? 'true' : 'false'" :aria-labelledby="`${id}-label`" tabindex="0"
12
+ @mousedown.prevent="onSelectClick" @focusin.stop="focusSelect">
13
+ <div :class="selectComputedClasses">
14
+ <template v-if="
15
+ multiple &&
16
+ Array.isArray(selected) &&
17
+ selected.length > 0
18
+ ">
19
+ <MTag v-for="value in selectedMax" :key="value" :closeable="true"
20
+ @close="event => removeSelected(event, value)" :size="size">
21
+ {{ getSelectedLabel(value) }}
22
+ </MTag>
23
+ <template v-if="selectedCountNotShown > 0">
24
+ <MTag :size="size">
25
+ +{{ selectedCountNotShown }} more
26
+ </MTag>
27
+ </template>
28
+ </template>
29
+ <template v-else-if="!multiple && selected">
30
+ {{ getSelectedLabel(selected) }}
31
+ </template>
32
+ <template v-else>
33
+ <div :class="style.placeholder">{{ placeholder }}</div>
34
+ </template>
35
+ </div>
36
+
37
+ <Teleport :to="'body'">
38
+ <Transition :enter-active-class="scale.enterActive" :leave-active-class="scale.leaveActive"
39
+ :enter-from-class="scale.enterFrom" :leave-to-class="scale.leaveTo">
40
+ <div :class="style.menu" v-if="isOpen" ref="popper" :style="computedConsistentWidth"
41
+ :aria-hidden="isOpen ? 'false' : 'true'">
42
+ <div v-if="searchable || (multiple && selectAll)" :class="style.search">
43
+ <div v-if="multiple && selectAll" :class="style.checkAll">
44
+ <button role="button" type="button" :class="{
45
+ [style.checkAllButton]: true,
46
+ [style.checkAllButtonActive]:
47
+ computedOptions.length > 0 &&
48
+ computedOptions.length ===
49
+ selected.length
50
+ }" @click.prevent="toggleAllItems" aria-description="Select all items">
51
+ <svg v-if="
52
+ computedOptions.length > 0 &&
53
+ computedOptions.length ===
54
+ selected.length
55
+ " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
56
+ aria-hidden="true">
57
+ <path fill-rule="evenodd"
58
+ 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"
59
+ clip-rule="evenodd"></path>
60
+ </svg>
61
+ </button>
62
+ </div>
63
+ <input v-if="searchable" type="text" ref="searchRef" v-model="search" placeholder="Search"
64
+ :class="style.searchInput" />
65
+ </div>
66
+ <div :class="style.options">
67
+ <div v-for="(option, index) in computedOptions" :key="index" ref="optionsRef" :class="{
68
+ [style.option]: true,
69
+ [style.optionSelected]: option.active,
70
+ [style.optionFocused]:
71
+ selectedIndex === index
72
+ }" @click.prevent="option.onClick" @hover="selectedIndex = index">
73
+ <span :class="style.optionIcon">
74
+ <svg v-if="option.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
75
+ fill="currentColor" aria-hidden="true">
76
+ <path fill-rule="evenodd"
77
+ 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"
78
+ clip-rule="evenodd"></path>
79
+ </svg>
80
+ </span>
81
+ <span :class="style.optionLabel">{{
82
+ option.label
83
+ }}</span>
84
+ </div>
85
+ <template v-if="computedOptions.length === 0">
86
+ <div :class="style.noResults">No results</div>
87
+ </template>
88
+ </div>
89
+ </div>
90
+ </Transition>
91
+ </Teleport>
92
+ </div>
93
+ <div v-if="inputError" :class="style.errorMessage">
94
+ {{ inputError }}
95
+ </div>
96
+ </div>
97
+ </template>
98
+
99
+ <script lang="ts">
100
+ import { useInput } from '../../composables/useInput';
101
+ import { usePopper, type Placement } from '../../composables/usePopper';
102
+ import type { MaybeArray, Size } from '../../types/props';
103
+ import { onClickOutside, useEventListener } from '@vueuse/core';
104
+ import {
105
+ computed,
106
+ defineComponent,
107
+ nextTick,
108
+ ref,
109
+ watch,
110
+ watchEffect,
111
+ type PropType
112
+ } from 'vue';
113
+ import MTag from '../tag/Tag.vue';
114
+ import * as style from './style';
115
+ import { scale } from '../../styles/transitions.css';
116
+ import { useInteractive } from '../../composables/useInteractive';
117
+ import type { OnUpdateValue, OnUpdateValueImpl } from './types';
118
+ import { call } from "../../utils/call";
119
+
120
+ export default defineComponent({
121
+ name: 'MSelect',
122
+ emits: {
123
+ ...useInput.emits()
124
+ },
125
+ props: {
126
+ ...useInput.props(),
127
+ ...useInteractive.props(),
128
+ label: {
129
+ type: String
130
+ },
131
+ placeholder: {
132
+ type: String,
133
+ default: 'Select an option'
134
+ },
135
+ options: {
136
+ type: Array as PropType<Array<any>>,
137
+ required: true
138
+ },
139
+ searchable: {
140
+ type: Boolean,
141
+ default: false
142
+ },
143
+ multiple: {
144
+ type: Boolean,
145
+ default: false
146
+ },
147
+ size: {
148
+ type: String as PropType<Size>,
149
+ default: 'md'
150
+ },
151
+ description: {
152
+ type: String,
153
+ default: ''
154
+ },
155
+ optionValue: {
156
+ type: String,
157
+ default: 'value'
158
+ },
159
+ optionLabel: {
160
+ type: String,
161
+ default: 'label'
162
+ },
163
+ transitionName: {
164
+ type: String,
165
+ default: 'fade'
166
+ },
167
+ consistentWidth: {
168
+ type: Boolean,
169
+ default: true
170
+ },
171
+ disabled: {
172
+ type: Boolean,
173
+ default: false
174
+ },
175
+ loading: {
176
+ type: Boolean,
177
+ default: false
178
+ },
179
+ error: {
180
+ type: String
181
+ },
182
+ allowBlank: {
183
+ type: Boolean,
184
+ default: false
185
+ },
186
+ selectAll: {
187
+ type: Boolean,
188
+ default: false
189
+ },
190
+ max: {
191
+ type: Number,
192
+ default: 5
193
+ },
194
+ 'onUpdate:value': [Function, Array] as PropType<MaybeArray<OnUpdateValue> | undefined>,
195
+ onUpdateValue: [Function, Array] as PropType<MaybeArray<OnUpdateValue> | undefined>,
196
+ },
197
+ components: {
198
+ MTag
199
+ },
200
+ setup(props, { emit, expose }) {
201
+ const container = ref<HTMLElement | null>(null);
202
+ const wrapper = ref<HTMLElement>();
203
+ const optionsRef = ref<HTMLElement[]>();
204
+ const searchRef = ref<HTMLElement>();
205
+ const selectRef = ref<HTMLElement>();
206
+ const selectedIndex = ref<number | undefined>(0);
207
+ const open = ref<boolean>(false);
208
+ const focused = ref<boolean>(false);
209
+ const search = ref<string>('');
210
+ const placement = ref<Placement>('bottom-start');
211
+ const transfer = ref(false);
212
+ const width = ref<number>(0);
213
+ const keyboardNavigation = ref<boolean>(false);
214
+
215
+ function doUpdateValue(
216
+ value: string | number | Array<string | number> | null,
217
+ option: object | null
218
+ ): void {
219
+ const {
220
+ 'onUpdate:value': _onUpdateValue,
221
+ onUpdateValue
222
+ } = props;
223
+
224
+ if (onUpdateValue) call(onUpdateValue as OnUpdateValueImpl, value, option)
225
+ if (_onUpdateValue) {
226
+ call(_onUpdateValue as OnUpdateValueImpl, value, option)
227
+ }
228
+ }
229
+
230
+
231
+ const { focus, blur } = useInteractive(container);
232
+ // TODO: Add debounce on keyboard movement (hover flicks it back up)
233
+ const {
234
+ id,
235
+ insideForm,
236
+ inputListeners,
237
+ formError,
238
+ formDisabled,
239
+ formLoading
240
+ } = useInput(props, { focus, emit, withListeners: false });
241
+
242
+ const { popper, updatePopper } = usePopper({
243
+ placement,
244
+ transfer,
245
+ reference: container,
246
+ wrapper,
247
+ offset: [0, 5]
248
+ });
249
+
250
+ const disabled = computed(() => formDisabled || props.disabled);
251
+ const loading = computed(() => formLoading || props.loading);
252
+
253
+ const selected = computed<any | any[]>({
254
+ get() {
255
+ if (props.multiple) {
256
+ if (!props.modelValue) {
257
+ return [];
258
+ }
259
+
260
+ if (Array.isArray(props.modelValue)) {
261
+ return props.modelValue;
262
+ }
263
+
264
+ return [props.modelValue];
265
+ }
266
+
267
+ return props.modelValue || null;
268
+ },
269
+ set(value: string | number | []) {
270
+ emit('update:modelValue', value);
271
+ doUpdateValue(value, null);
272
+ }
273
+ });
274
+
275
+ const selectedMax = computed(() => {
276
+ if (props.max <= 0) {
277
+ return selected.value;
278
+ }
279
+
280
+ if (
281
+ props.multiple &&
282
+ Array.isArray(selected.value) &&
283
+ selected.value.length > props.max
284
+ ) {
285
+ return selected.value.slice(0, props.max);
286
+ }
287
+
288
+ return selected.value;
289
+ });
290
+
291
+ const selectedCountNotShown = computed(() => {
292
+ if (
293
+ props.multiple &&
294
+ Array.isArray(selected.value) &&
295
+ props.max > 0
296
+ ) {
297
+ return selected.value.length - props.max;
298
+ }
299
+
300
+ return 0;
301
+ });
302
+
303
+ const computedConsistentWidth = computed(() => {
304
+ if (props.consistentWidth) {
305
+ return {
306
+ width: width.value + 'px'
307
+ };
308
+ }
309
+
310
+ return {};
311
+ });
312
+
313
+ const computedOptions = computed(() => {
314
+ if (!props.options || props.options.length == 0) {
315
+ return [];
316
+ }
317
+
318
+ // Filter options by search
319
+ if (props.searchable && search.value) {
320
+ let options = props.options
321
+ .filter(option => {
322
+ return option[props.optionLabel]
323
+ .toLowerCase()
324
+ .includes(search.value.toLowerCase());
325
+ })
326
+ .map(option => {
327
+ if (option[props.optionValue] === undefined) {
328
+ throw new Error('Option value is undefined');
329
+ }
330
+
331
+ if (option[props.optionLabel] === undefined) {
332
+ throw new Error('Option label is undefined');
333
+ }
334
+
335
+ let value = option[props.optionValue];
336
+ let active: boolean;
337
+ if (props.multiple && Array.isArray(selected.value)) {
338
+ active = selected.value.includes(value);
339
+ } else {
340
+ active = selected.value === value;
341
+ }
342
+
343
+ return {
344
+ value: value,
345
+ label: option[props.optionLabel],
346
+ active: active,
347
+ onClick: () => handleOptionClick(value)
348
+ };
349
+ });
350
+
351
+ if (props.allowBlank) {
352
+ options.unshift({
353
+ value: '',
354
+ label: '',
355
+ active: false,
356
+ onClick: () => handleOptionClick('')
357
+ });
358
+ }
359
+
360
+ return options;
361
+ }
362
+
363
+ let options = props.options.map(option => {
364
+ if (option[props.optionValue] === undefined) {
365
+ throw new Error('Option value is undefined');
366
+ }
367
+
368
+ if (option[props.optionLabel] === undefined) {
369
+ throw new Error('Option label is undefined');
370
+ }
371
+
372
+ let value = option[props.optionValue];
373
+ let active: boolean;
374
+ if (props.multiple && Array.isArray(selected.value)) {
375
+ active = selected.value.includes(value);
376
+ } else {
377
+ active = selected.value === value;
378
+ }
379
+
380
+ return {
381
+ value: value,
382
+ label: option[props.optionLabel],
383
+ active: active,
384
+ onClick: () => handleOptionClick(value)
385
+ };
386
+ });
387
+
388
+ if (props.allowBlank) {
389
+ options.unshift({
390
+ value: '',
391
+ label: '',
392
+ active: false,
393
+ onClick: () => handleOptionClick('')
394
+ });
395
+ }
396
+
397
+ return options;
398
+ });
399
+
400
+ function toggleAllItems() {
401
+ if (!props.multiple) {
402
+ return;
403
+ }
404
+
405
+ if (computedOptions.value.length === 0) {
406
+ return;
407
+ }
408
+
409
+ if (Array.isArray(selected.value)) {
410
+ if (selected.value.length === computedOptions.value.length) {
411
+ selected.value = [];
412
+ } else {
413
+ selected.value = computedOptions.value.map(
414
+ option => option.value
415
+ );
416
+ }
417
+ } else {
418
+ selected.value = [];
419
+ }
420
+ }
421
+
422
+ function openSelect() {
423
+ if (disabled.value || loading.value) {
424
+ return;
425
+ }
426
+
427
+ if (open.value) {
428
+ open.value = false;
429
+ return;
430
+ }
431
+ open.value = true;
432
+ }
433
+
434
+ function getSelectedIndex(
435
+ start: number | undefined,
436
+ direction = 'down'
437
+ ) {
438
+ if (typeof start === 'undefined') {
439
+ start = direction === 'down' ? -1 : 1;
440
+ }
441
+
442
+ if (direction === 'down') {
443
+ let next = start + 1;
444
+
445
+ if (next > computedOptions.value.length - 1) {
446
+ next = 0;
447
+ }
448
+
449
+ selectedIndex.value = next;
450
+ } else {
451
+ let next = start - 1;
452
+
453
+ if (next < 0) {
454
+ next = computedOptions.value.length - 1;
455
+ }
456
+
457
+ selectedIndex.value = next;
458
+ }
459
+ }
460
+
461
+ function handleOptionClick(value: string | number) {
462
+ const option = computedOptions.value.find(
463
+ item => item.value === value
464
+ );
465
+ if (!option) {
466
+ return;
467
+ }
468
+
469
+ if (props.multiple) {
470
+ if (Array.isArray(selected.value)) {
471
+ const index = selected.value.indexOf(value);
472
+
473
+ if (index !== -1) {
474
+ selected.value.splice(index, 1);
475
+ } else {
476
+ selected.value.push(value);
477
+ emit('update:modelValue', selected.value);
478
+ }
479
+ } else {
480
+ selected.value = [value];
481
+ }
482
+
483
+ updatePopper();
484
+ } else {
485
+ selected.value = value;
486
+ open.value = false;
487
+ }
488
+ }
489
+
490
+ function removeSelected(event: MouseEvent, value: string | number) {
491
+ event.preventDefault();
492
+ event.stopPropagation();
493
+ if (!props.multiple) {
494
+ return;
495
+ }
496
+
497
+ if (Array.isArray(selected.value)) {
498
+ const index = selected.value.indexOf(value);
499
+
500
+ if (index !== -1) {
501
+ selected.value.splice(index, 1);
502
+ }
503
+
504
+ updatePopper();
505
+ }
506
+ }
507
+
508
+ useEventListener('resize', () => {
509
+ open.value = false;
510
+ });
511
+
512
+ function handleKeyDown(e: Event) {
513
+ if (computedOptions.value.length === 0) {
514
+ return;
515
+ }
516
+
517
+ if (props.disabled) {
518
+ return;
519
+ }
520
+
521
+ keyboardNavigation.value = true;
522
+
523
+ const event: KeyboardEvent = e as KeyboardEvent;
524
+
525
+ switch (event.code) {
526
+ case 'Tab':
527
+ if (open.value) {
528
+ open.value = false;
529
+ }
530
+ break;
531
+ case 'ArrowDown':
532
+ e.preventDefault();
533
+ if (!open.value) {
534
+ open.value = true;
535
+ return;
536
+ }
537
+
538
+ getSelectedIndex(selectedIndex.value, 'down');
539
+ break;
540
+ case 'ArrowUp':
541
+ e.preventDefault();
542
+ if (!open.value) {
543
+ return;
544
+ }
545
+
546
+ getSelectedIndex(selectedIndex.value, 'up');
547
+ break;
548
+ case 'Enter':
549
+ e.preventDefault();
550
+ e.stopPropagation();
551
+
552
+ if (!open.value) {
553
+ open.value = true;
554
+ return;
555
+ }
556
+
557
+ if (typeof selectedIndex.value !== 'undefined') {
558
+ handleOptionClick(
559
+ computedOptions.value[selectedIndex.value].value
560
+ );
561
+ if (!props.multiple) {
562
+ open.value = false;
563
+ }
564
+ }
565
+ break;
566
+ case 'Escape':
567
+ e.preventDefault();
568
+ e.stopPropagation();
569
+
570
+ if (!open.value) {
571
+ return;
572
+ }
573
+
574
+ open.value = false;
575
+ break;
576
+ }
577
+ }
578
+
579
+ const transitionName = computed(() => {
580
+ return `m-transition__${props.transitionName}`;
581
+ });
582
+
583
+ const containerComputedClasses = computed(() => {
584
+ return [
585
+ 'm-select__container',
586
+ {
587
+ 'm-form__input': insideForm
588
+ }
589
+ ];
590
+ });
591
+
592
+ const computedWrapperClasses = computed(() => {
593
+ return [
594
+ style.wrapper,
595
+ {
596
+ [style.focused]: focused.value
597
+ }
598
+ ];
599
+ });
600
+
601
+ const selectComputedClasses = computed(() => {
602
+ return [
603
+ style.base,
604
+ {
605
+ [style.selectedTags]:
606
+ props.multiple &&
607
+ Array.isArray(selected.value) &&
608
+ selected.value.length > 0,
609
+ [style.focused]: open.value,
610
+ // [`m-select--${props.size}`]: props.size,
611
+ 'm-select--disabled': disabled.value || loading.value,
612
+ 'm-select--focused': open.value,
613
+ 'm-select--multiple': props.multiple,
614
+ 'm-select--error': formError.value
615
+ }
616
+ ];
617
+ });
618
+
619
+ function getSelectedLabel(value: string | number | []) {
620
+ const option = props.options.find(
621
+ i => i[props.optionValue] === value
622
+ );
623
+ if (!option) {
624
+ return null;
625
+ }
626
+
627
+ return option[props.optionLabel];
628
+ }
629
+
630
+ useEventListener(selectRef, 'keydown', handleKeyDown);
631
+ useEventListener(searchRef, 'keydown', handleKeyDown);
632
+
633
+ onClickOutside(
634
+ container,
635
+ () => {
636
+ open.value = false;
637
+ },
638
+ { ignore: [searchRef, popper] }
639
+ );
640
+
641
+ watchEffect(() => {
642
+ if (container.value && open.value) {
643
+ width.value = container.value.offsetWidth;
644
+ }
645
+ });
646
+
647
+ watch(selectedIndex, (index: number | undefined) => {
648
+ if (!keyboardNavigation.value) {
649
+ return;
650
+ }
651
+ if (typeof index !== 'undefined' && optionsRef.value) {
652
+ optionsRef.value[index].scrollIntoView({
653
+ block: 'nearest',
654
+ inline: 'nearest'
655
+ });
656
+ }
657
+ });
658
+
659
+ watch(open, (value: boolean) => {
660
+ nextTick(() => {
661
+ if (!value) {
662
+ search.value = '';
663
+ }
664
+
665
+ if (props.searchable && searchRef.value) {
666
+ searchRef.value.focus();
667
+ }
668
+ if (value && optionsRef.value) {
669
+ const index = computedOptions.value.findIndex(
670
+ i => i.value === selected.value
671
+ );
672
+ if (index !== -1) {
673
+ optionsRef.value[index].scrollIntoView({
674
+ block: 'nearest'
675
+ });
676
+ selectedIndex.value = index;
677
+ }
678
+ }
679
+
680
+ keyboardNavigation.value = false;
681
+ });
682
+ });
683
+
684
+ const inputError = computed(() => {
685
+ return formError.value || props.error;
686
+ });
687
+
688
+ const isOpen = computed(() => {
689
+ if (
690
+ props.multiple &&
691
+ computedOptions.value.length == 0 &&
692
+ !search.value
693
+ ) {
694
+ return false;
695
+ }
696
+ return open.value;
697
+ });
698
+
699
+ const focusSelect = (event: FocusEvent) => {
700
+ setTimeout(() => {
701
+ if (selectRef.value && !open.value) {
702
+ openSelect();
703
+ }
704
+ }, 10);
705
+ };
706
+
707
+ const onSelectClick = (event: MouseEvent) => {
708
+ event.preventDefault();
709
+ event.stopPropagation();
710
+ openSelect();
711
+ };
712
+
713
+ expose({
714
+ focus,
715
+ blur
716
+ });
717
+
718
+ return {
719
+ container,
720
+ optionsRef,
721
+ searchRef,
722
+ selectRef,
723
+ popper,
724
+ focusSelect,
725
+ onSelectClick,
726
+
727
+ id,
728
+ isOpen,
729
+ open,
730
+ selected,
731
+ selectedMax,
732
+ selectedCountNotShown,
733
+ computedOptions,
734
+ selectedIndex,
735
+ search,
736
+
737
+ containerComputedClasses,
738
+ selectComputedClasses,
739
+ computedWrapperClasses,
740
+ computedConsistentWidth,
741
+
742
+ getSelectedLabel,
743
+ openSelect,
744
+ removeSelected,
745
+ inputError,
746
+ transitionName,
747
+ inputListeners,
748
+ style,
749
+ focused,
750
+ scale,
751
+ toggleAllItems
752
+ };
753
+ }
754
+ });
755
+ </script>