@carbon/vue 3.0.5-alpha.0 → 3.0.7-alpha.0
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/README.md +16 -0
- package/dist/carbon-vue-3.common.js +22859 -11008
- package/dist/carbon-vue-3.common.js.map +1 -1
- package/dist/carbon-vue-3.css +1 -0
- package/dist/carbon-vue-3.umd.js +22859 -11008
- package/dist/carbon-vue-3.umd.js.map +1 -1
- package/dist/carbon-vue-3.umd.min.js +30 -2
- package/dist/carbon-vue-3.umd.min.js.map +1 -1
- package/package.json +5 -4
- package/src/components/CvAccordion/CvAccordion.stories.js +1 -0
- package/src/components/CvButton/CvButton.stories.js +3 -3
- package/src/components/CvButton/CvButtonSkeleton.stories.js +79 -0
- package/src/components/CvButton/CvButtonSkeleton.vue +19 -0
- package/src/components/CvButton/index.js +8 -1
- package/src/components/CvDatePicker/CvDatePicker-notes.md +86 -0
- package/src/components/CvDatePicker/CvDatePicker.stories.js +241 -0
- package/src/components/CvDatePicker/CvDatePicker.vue +402 -0
- package/src/components/CvDatePicker/CvDatePickerSkeleton.vue +106 -0
- package/src/components/CvDatePicker/index.js +4 -0
- package/src/components/CvDatePicker/plugins/appendToPlugin.js +64 -0
- package/src/components/CvDatePicker/plugins/fixEventsPlugin.js +79 -0
- package/src/components/CvDatePicker/plugins/monthSelectPlugin.js +86 -0
- package/src/components/CvDatePicker/plugins/rangePlugin.js +47 -0
- package/src/components/CvDropdown/CvDropdown.stories.mdx +407 -0
- package/src/components/CvDropdown/CvDropdown.vue +418 -0
- package/src/components/CvDropdown/CvDropdownItem.vue +86 -0
- package/src/components/CvDropdown/CvDropdownSkeleton.vue +17 -0
- package/src/components/CvDropdown/index.js +5 -0
- package/src/components/CvForm/CvForm.stories.mdx +193 -0
- package/src/components/CvForm/CvForm.vue +9 -0
- package/src/components/CvForm/CvFormGroup.vue +27 -0
- package/src/components/CvForm/CvFormItem.vue +9 -0
- package/src/components/CvForm/index.js +5 -0
- package/src/components/CvInlineLoading/CvInlineLoading.stories.mdx +87 -0
- package/src/components/CvInlineLoading/CvInlineLoading.vue +144 -0
- package/src/components/CvInlineLoading/consts.js +9 -0
- package/src/components/CvInlineLoading/index.js +4 -0
- package/src/components/CvModal/CvModal.stories.mdx +236 -0
- package/src/components/CvModal/CvModal.vue +401 -0
- package/src/components/CvModal/index.js +3 -0
- package/src/components/CvMultiSelect/CvMultiSelect.stories.mdx +333 -0
- package/src/components/CvMultiSelect/CvMultiSelect.vue +705 -0
- package/src/components/CvMultiSelect/consts.js +16 -0
- package/src/components/CvMultiSelect/index.js +3 -0
- package/src/components/CvNumberInput/CvNumberInput.stories.js +265 -0
- package/src/components/CvNumberInput/CvNumberInput.vue +227 -0
- package/src/components/CvNumberInput/CvNumberInputSkeleton.stories.js +30 -0
- package/src/components/CvNumberInput/CvNumberInputSkeleton.vue +10 -0
- package/src/components/CvNumberInput/StorybookGroupConst.js +1 -0
- package/src/components/CvNumberInput/__tests__/__snapshots__/CvNumberInput.spec.js.snap +21 -0
- package/src/components/CvNumberInput/index.js +5 -0
- package/src/components/CvTabs/CvTab.vue +77 -0
- package/src/components/CvTabs/CvTabs.stories.mdx +175 -0
- package/src/components/CvTabs/CvTabs.vue +406 -0
- package/src/components/CvTabs/CvTabsSkeleton.vue +24 -0
- package/src/components/CvTabs/_SbContainer.vue +22 -0
- package/src/components/CvTabs/index.js +5 -0
- package/src/components/CvToggle/CvToggle-Skeleton.vue +29 -0
- package/src/components/CvToggle/CvToggle.stories.js +232 -0
- package/src/components/CvToggle/CvToggle.vue +70 -0
- package/src/components/CvToggle/__tests__/__snapshots__/CvToggle.spec.js.snap +7 -0
- package/src/components/CvToggle/index.js +4 -0
- package/src/global/storybook-utils/story-source-code.js +1 -0
- package/src/use/cvInput.js +34 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="el"
|
|
4
|
+
:class="[
|
|
5
|
+
`cv-multi-select ${carbonPrefix}--multi-select__wrapper ${carbonPrefix}--list-box__wrapper`,
|
|
6
|
+
{
|
|
7
|
+
[`${carbonPrefix}--multi-select__wrapper--inline`]: inline,
|
|
8
|
+
[`${carbonPrefix}--list-box__wrapper--inline`]: inline,
|
|
9
|
+
[`${carbonPrefix}--multi-select__wrapper--inline--invalid]`]:
|
|
10
|
+
inlineInvalid,
|
|
11
|
+
[`${carbonPrefix}--list-box__wrapper--inline--invalid`]: inlineInvalid,
|
|
12
|
+
[`${carbonPrefix}--multi-select--filterable`]: isFilterable,
|
|
13
|
+
},
|
|
14
|
+
]"
|
|
15
|
+
@focusout="onFocusOut"
|
|
16
|
+
>
|
|
17
|
+
<label
|
|
18
|
+
v-if="title"
|
|
19
|
+
:id="`${uid}-label`"
|
|
20
|
+
:class="[
|
|
21
|
+
`${carbonPrefix}--label`,
|
|
22
|
+
{ [`${carbonPrefix}--label--disabled`]: disabled },
|
|
23
|
+
]"
|
|
24
|
+
>{{ title }}</label
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
:aria-labelledby="`${uid}-label`"
|
|
28
|
+
role="listbox"
|
|
29
|
+
tabindex="-1"
|
|
30
|
+
:class="[
|
|
31
|
+
`${carbonPrefix}--multi-select ${carbonPrefix}--list-box`,
|
|
32
|
+
{
|
|
33
|
+
[`${carbonPrefix}--list-box--up`]: isDirectionUp,
|
|
34
|
+
[`${carbonPrefix}--list-box--light`]: isLight,
|
|
35
|
+
[`${carbonPrefix}--list-box--expanded`]: data.open,
|
|
36
|
+
[`${carbonPrefix}--multi-select--warning`]: isWarning,
|
|
37
|
+
[`${carbonPrefix}--list-box--warning`]: isWarning,
|
|
38
|
+
[`${carbonPrefix}--multi-select--invalid`]: data.isInvalid,
|
|
39
|
+
[`${carbonPrefix}--multi-select--disabled`]: disabled,
|
|
40
|
+
[`${carbonPrefix}--list-box--disabled`]: disabled,
|
|
41
|
+
[`${carbonPrefix}--multi-select--inline`]: inline,
|
|
42
|
+
[`${carbonPrefix}--list-box--inline`]: inline,
|
|
43
|
+
[`${carbonPrefix}--multi-select--selected`]:
|
|
44
|
+
data.selectedItems.length > 0,
|
|
45
|
+
[`${carbonPrefix}--combo-box`]: isFilterable,
|
|
46
|
+
},
|
|
47
|
+
]"
|
|
48
|
+
:data-invalid="data.isInvalid ? true : undefined"
|
|
49
|
+
v-bind="$attrs"
|
|
50
|
+
@keydown.down.prevent="onDown"
|
|
51
|
+
@keydown.up.prevent="onUp"
|
|
52
|
+
@keydown.enter.prevent="onEnter"
|
|
53
|
+
@keydown.esc.prevent="onEsc"
|
|
54
|
+
@click="onClick"
|
|
55
|
+
>
|
|
56
|
+
<warning-alt-filled
|
|
57
|
+
v-if="isWarning"
|
|
58
|
+
:class="`${carbonPrefix}--list-box__invalid-icon ${carbonPrefix}--list-box__invalid-icon--warning`"
|
|
59
|
+
/>
|
|
60
|
+
<warning-filled
|
|
61
|
+
v-if="data.isInvalid"
|
|
62
|
+
:class="`${carbonPrefix}--list-box__invalid-icon`"
|
|
63
|
+
/>
|
|
64
|
+
<button
|
|
65
|
+
ref="elButton"
|
|
66
|
+
type="button"
|
|
67
|
+
:class="`${carbonPrefix}--list-box__field`"
|
|
68
|
+
:aria-disabled="disabled"
|
|
69
|
+
aria-haspopup="listbox"
|
|
70
|
+
:aria-expanded="data.open ? 'true' : 'false'"
|
|
71
|
+
:aria-owns="uid"
|
|
72
|
+
:aria-controls="uid"
|
|
73
|
+
tabindex="0"
|
|
74
|
+
:aria-label="data.open ? 'close menu' : 'open menu'"
|
|
75
|
+
data-toggle="true"
|
|
76
|
+
>
|
|
77
|
+
<span ref="elTag">
|
|
78
|
+
<cv-tag
|
|
79
|
+
v-if="data.selectedItems.length > 0"
|
|
80
|
+
:class="{
|
|
81
|
+
[`${carbonPrefix}--list-box__selection--multi`]:
|
|
82
|
+
isFilterable && data.selectedItems.length > 0,
|
|
83
|
+
}"
|
|
84
|
+
:disabled="disabled"
|
|
85
|
+
:kind="filterTagKind"
|
|
86
|
+
:filter="true"
|
|
87
|
+
:label="`${data.selectedItems.length}`"
|
|
88
|
+
:style="filterableTagOverride"
|
|
89
|
+
@remove="clearValues"
|
|
90
|
+
/>
|
|
91
|
+
</span>
|
|
92
|
+
<span
|
|
93
|
+
v-if="!isFilterable"
|
|
94
|
+
:class="`${carbonPrefix}--list-box__label`"
|
|
95
|
+
>{{ label }}</span
|
|
96
|
+
>
|
|
97
|
+
<template v-else>
|
|
98
|
+
<input
|
|
99
|
+
ref="elInput"
|
|
100
|
+
v-model="internalFilter"
|
|
101
|
+
:class="[
|
|
102
|
+
`${carbonPrefix}--text-input`,
|
|
103
|
+
{
|
|
104
|
+
[`${carbonPrefix}--text-input--empty`]:
|
|
105
|
+
!internalFilter || internalFilter.length === 0,
|
|
106
|
+
},
|
|
107
|
+
]"
|
|
108
|
+
:aria-controls="uid"
|
|
109
|
+
aria-autocomplete="list"
|
|
110
|
+
role="combobox"
|
|
111
|
+
:aria-expanded="data.open ? 'true' : 'false'"
|
|
112
|
+
autocomplete="off"
|
|
113
|
+
:placeholder="label"
|
|
114
|
+
@input="onInput"
|
|
115
|
+
@focus="inputFocus"
|
|
116
|
+
@click.stop.prevent="inputClick"
|
|
117
|
+
/>
|
|
118
|
+
<div
|
|
119
|
+
v-if="internalFilter.length > 0"
|
|
120
|
+
role="button"
|
|
121
|
+
:class="`${carbonPrefix}--list-box__selection`"
|
|
122
|
+
tabindex="0"
|
|
123
|
+
title="Clear filter"
|
|
124
|
+
@click.stop="clearFilter"
|
|
125
|
+
@keydown.enter.stop.prevent="clearFilter"
|
|
126
|
+
@keydown.space.stop.prevent
|
|
127
|
+
@keyup.space.stop.prevent="clearFilter"
|
|
128
|
+
>
|
|
129
|
+
<close-icon :style="moveCloseIcon" />
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
|
+
<div
|
|
133
|
+
:class="[
|
|
134
|
+
`${carbonPrefix}--list-box__menu-icon`,
|
|
135
|
+
{ [`${carbonPrefix}--list-box__menu-icon--open`]: data.open },
|
|
136
|
+
]"
|
|
137
|
+
role="button"
|
|
138
|
+
>
|
|
139
|
+
<chevron-down :aria-label="data.open ? 'Close menu' : 'Open menu'" />
|
|
140
|
+
</div>
|
|
141
|
+
</button>
|
|
142
|
+
|
|
143
|
+
<div
|
|
144
|
+
:id="uid"
|
|
145
|
+
ref="elList"
|
|
146
|
+
:aria-labelledby="`${uid}-label`"
|
|
147
|
+
:class="`${carbonPrefix}--list-box__menu`"
|
|
148
|
+
role="listbox"
|
|
149
|
+
>
|
|
150
|
+
<div
|
|
151
|
+
v-for="(item, index) in data.options"
|
|
152
|
+
ref="elOption"
|
|
153
|
+
role="menuitem"
|
|
154
|
+
:key="`multi-select-${index}`"
|
|
155
|
+
:class="[
|
|
156
|
+
`${carbonPrefix}--list-box__menu-item`,
|
|
157
|
+
{
|
|
158
|
+
[`${carbonPrefix}--list-box__menu-item--highlighted`]:
|
|
159
|
+
highlighted === item.value,
|
|
160
|
+
},
|
|
161
|
+
]"
|
|
162
|
+
@click.stop.prevent="onItemClick(item.value)"
|
|
163
|
+
@mousemove="onMousemove(item.value)"
|
|
164
|
+
@mousedown.prevent
|
|
165
|
+
>
|
|
166
|
+
<div :class="`${carbonPrefix}--list-box__menu-item__option`">
|
|
167
|
+
<cv-checkbox
|
|
168
|
+
v-model="data.selectedItems"
|
|
169
|
+
tabindex="-1"
|
|
170
|
+
:form-item="false"
|
|
171
|
+
:value="item.value"
|
|
172
|
+
:name="item.name"
|
|
173
|
+
:label="item.label"
|
|
174
|
+
:disabled="item.disabled"
|
|
175
|
+
style="pointer-events: none"
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div v-if="data.isInvalid" :class="`${carbonPrefix}--form-requirement`">
|
|
182
|
+
<slot name="invalid-message">{{ invalidMessage }}</slot>
|
|
183
|
+
</div>
|
|
184
|
+
<div v-else-if="isWarning" :class="`${carbonPrefix}--form-requirement`">
|
|
185
|
+
<slot name="warning-message">{{ warningMessage }}</slot>
|
|
186
|
+
</div>
|
|
187
|
+
<div
|
|
188
|
+
v-if="isHelper"
|
|
189
|
+
:class="[
|
|
190
|
+
`${carbonPrefix}--form__helper-text`,
|
|
191
|
+
{ [`${carbonPrefix}--form__helper-text--disabled`]: disabled },
|
|
192
|
+
]"
|
|
193
|
+
>
|
|
194
|
+
<slot name="helper-text">{{ helperText }}</slot>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
198
|
+
|
|
199
|
+
<script setup>
|
|
200
|
+
import { carbonPrefix } from '../../global/settings';
|
|
201
|
+
import { props as propsCvId, useCvId } from '../../use/cvId';
|
|
202
|
+
import { props as propsTheme, useIsLight } from '../../use/cvTheme';
|
|
203
|
+
import WarningFilled from '@carbon/icons-vue/es/warning--filled/16';
|
|
204
|
+
import WarningAltFilled from '@carbon/icons-vue/es/warning--alt--filled/16';
|
|
205
|
+
import ChevronDown from '@carbon/icons-vue/es/chevron--down/16';
|
|
206
|
+
import CloseIcon from '@carbon/icons-vue/es/close/16';
|
|
207
|
+
import CvCheckbox from '../CvCheckbox/CvCheckbox.vue';
|
|
208
|
+
import CvTag from '../CvTag/CvTag.vue';
|
|
209
|
+
import {
|
|
210
|
+
computed,
|
|
211
|
+
nextTick,
|
|
212
|
+
onMounted,
|
|
213
|
+
onUpdated,
|
|
214
|
+
reactive,
|
|
215
|
+
ref,
|
|
216
|
+
useSlots,
|
|
217
|
+
watch,
|
|
218
|
+
} from 'vue';
|
|
219
|
+
import {
|
|
220
|
+
DIRECTION_BOTTOM,
|
|
221
|
+
DIRECTION_TOP,
|
|
222
|
+
DIRECTIONS,
|
|
223
|
+
FIXED,
|
|
224
|
+
selectionFeedbackOptions,
|
|
225
|
+
TOP,
|
|
226
|
+
TOP_AFTER_REOPEN,
|
|
227
|
+
} from './consts';
|
|
228
|
+
import { tagKinds } from '../CvTag/consts';
|
|
229
|
+
|
|
230
|
+
defineOptions({
|
|
231
|
+
inheritAttrs: false,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Select options shape
|
|
236
|
+
* @typedef {Object} MultiSelectOption
|
|
237
|
+
* @property {string} name
|
|
238
|
+
* @property {string} label
|
|
239
|
+
* @property {string} value
|
|
240
|
+
* @property {boolean|undefined} disabled
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
const props = defineProps({
|
|
244
|
+
/**
|
|
245
|
+
* Automatically filter items based on label
|
|
246
|
+
*/
|
|
247
|
+
autoFilter: { type: Boolean, default: false },
|
|
248
|
+
/**
|
|
249
|
+
* Highlight the first filtered item match. Requires either `autoFilter`
|
|
250
|
+
* or `filterable` to be `true`.
|
|
251
|
+
*/
|
|
252
|
+
autoHighlight: { type: Boolean, default: false },
|
|
253
|
+
/**
|
|
254
|
+
* Specify the direction of the multiselect dropdown. Can be either `top` or `bottom`.
|
|
255
|
+
*/
|
|
256
|
+
direction: {
|
|
257
|
+
type: String,
|
|
258
|
+
default: DIRECTION_BOTTOM,
|
|
259
|
+
validator: val => DIRECTIONS.includes(val),
|
|
260
|
+
},
|
|
261
|
+
/**
|
|
262
|
+
* Disable the control
|
|
263
|
+
*/
|
|
264
|
+
disabled: { type: Boolean, default: false },
|
|
265
|
+
/**
|
|
266
|
+
* enable filterable multi-select. User must manually update the options in response to the `filter` event.
|
|
267
|
+
*/
|
|
268
|
+
filterable: { type: Boolean, default: false },
|
|
269
|
+
/**
|
|
270
|
+
* A CvTag is displayed for sections and this property controls the color/kind on tag shown.
|
|
271
|
+
*/
|
|
272
|
+
filterTagKind: {
|
|
273
|
+
type: String,
|
|
274
|
+
default: 'high-contrast',
|
|
275
|
+
validator: val => tagKinds.includes(val),
|
|
276
|
+
},
|
|
277
|
+
/**
|
|
278
|
+
* Provide helper text that is used alongside the control label for additional help
|
|
279
|
+
*/
|
|
280
|
+
helperText: { type: String, default: undefined },
|
|
281
|
+
/**
|
|
282
|
+
* The value of an option to initially highlight
|
|
283
|
+
*/
|
|
284
|
+
highlight: String,
|
|
285
|
+
/**
|
|
286
|
+
* Specify 'true' to create an inline multi-select.
|
|
287
|
+
*/
|
|
288
|
+
inline: { type: Boolean, default: false },
|
|
289
|
+
/**
|
|
290
|
+
* Message which is displayed if the value is invalid.
|
|
291
|
+
*/
|
|
292
|
+
invalidMessage: { type: String, default: undefined },
|
|
293
|
+
/**
|
|
294
|
+
* Generic label that will be used as the textual representation of what this field is for
|
|
295
|
+
*/
|
|
296
|
+
label: {
|
|
297
|
+
type: String,
|
|
298
|
+
default: 'Choose options',
|
|
299
|
+
},
|
|
300
|
+
/***
|
|
301
|
+
* options in the form [{ label: '', value: '', name: '', disabled: false}]
|
|
302
|
+
* Array<MultiSelectOption>
|
|
303
|
+
*/
|
|
304
|
+
options: {
|
|
305
|
+
type: Array,
|
|
306
|
+
required: true,
|
|
307
|
+
validator(list) {
|
|
308
|
+
const result = list.every(
|
|
309
|
+
item =>
|
|
310
|
+
typeof item.name === 'string' &&
|
|
311
|
+
typeof item.label === 'string' &&
|
|
312
|
+
typeof item.value === 'string'
|
|
313
|
+
);
|
|
314
|
+
if (!result) {
|
|
315
|
+
console.warn(
|
|
316
|
+
'CvMultiSelect - all options must have name, label and value'
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
/**
|
|
323
|
+
* Specify feedback (mode) of the selection.
|
|
324
|
+
* - top: selected item jumps to top
|
|
325
|
+
* - fixed: selected item stays at its position
|
|
326
|
+
* - top-after-reopen: selected items jump to top after reopening dropdown
|
|
327
|
+
*/
|
|
328
|
+
selectionFeedback: {
|
|
329
|
+
type: String,
|
|
330
|
+
default: selectionFeedbackOptions[TOP_AFTER_REOPEN],
|
|
331
|
+
validator(val) {
|
|
332
|
+
if (!selectionFeedbackOptions.includes(val)) {
|
|
333
|
+
console.warn(
|
|
334
|
+
`CvMultiSelect: invalid selectionFeedback "${val}", use one of ${selectionFeedbackOptions}`
|
|
335
|
+
);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
/**
|
|
342
|
+
* Provide text to be used in a <label> element that is tied to the multiselect via ARIA attributes.
|
|
343
|
+
*/
|
|
344
|
+
title: String,
|
|
345
|
+
/***
|
|
346
|
+
* Allow users to pass in arbitrary items from their collection that are pre-selected
|
|
347
|
+
*/
|
|
348
|
+
value: { type: Array, default: () => [] },
|
|
349
|
+
/**
|
|
350
|
+
* Provide the text that is displayed and put the control in warning state
|
|
351
|
+
*/
|
|
352
|
+
warningMessage: { type: String, default: undefined },
|
|
353
|
+
modelValue: {
|
|
354
|
+
type: Array,
|
|
355
|
+
validator: val => {
|
|
356
|
+
console.error(
|
|
357
|
+
`v-model for cv-multi-select is deprecated. Specify "v-model:value" instead [${val}]`
|
|
358
|
+
);
|
|
359
|
+
return true;
|
|
360
|
+
},
|
|
361
|
+
default: undefined,
|
|
362
|
+
},
|
|
363
|
+
...propsCvId,
|
|
364
|
+
...propsTheme,
|
|
365
|
+
});
|
|
366
|
+
const uid = useCvId(props, true);
|
|
367
|
+
const isLight = useIsLight(props);
|
|
368
|
+
|
|
369
|
+
const data = reactive({
|
|
370
|
+
open: false,
|
|
371
|
+
/***
|
|
372
|
+
* Array<MultiSelectOption>
|
|
373
|
+
*/
|
|
374
|
+
options: [],
|
|
375
|
+
selectedItems: [],
|
|
376
|
+
highlighted: null,
|
|
377
|
+
filter: '',
|
|
378
|
+
isHelper: false,
|
|
379
|
+
isWarning: false,
|
|
380
|
+
isInvalid: false,
|
|
381
|
+
});
|
|
382
|
+
const emit = defineEmits(['update:value', 'change', 'filter']);
|
|
383
|
+
watch(
|
|
384
|
+
() => data.selectedItems,
|
|
385
|
+
() => {
|
|
386
|
+
if (JSON.stringify(data.selectedItems) !== JSON.stringify(props.value)) {
|
|
387
|
+
emit('change', data.selectedItems);
|
|
388
|
+
emit('update:value', data.selectedItems);
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
deep: true,
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const isDirectionUp = computed(() => {
|
|
397
|
+
return props.direction === DIRECTION_TOP;
|
|
398
|
+
});
|
|
399
|
+
const moveCloseIcon = computed(() => {
|
|
400
|
+
if (isWarning.value || data.isInvalid)
|
|
401
|
+
return { left: '-1rem', position: 'relative' };
|
|
402
|
+
else return undefined;
|
|
403
|
+
});
|
|
404
|
+
const inlineInvalid = computed(() => {
|
|
405
|
+
return props.inline && data.isInvalid;
|
|
406
|
+
});
|
|
407
|
+
const isFilterable = computed(() => {
|
|
408
|
+
return props.filterable || props.autoFilter;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
function updateSelectedItems() {
|
|
412
|
+
data.selectedItems = props.value.filter(
|
|
413
|
+
/***
|
|
414
|
+
* @param {string} item
|
|
415
|
+
* @returns {boolean}
|
|
416
|
+
*/
|
|
417
|
+
item =>
|
|
418
|
+
data.options.some(
|
|
419
|
+
/***
|
|
420
|
+
* @param {MultiSelectOption} opt
|
|
421
|
+
* @returns {boolean}
|
|
422
|
+
*/
|
|
423
|
+
opt => opt.value === item.trim()
|
|
424
|
+
)
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const slots = useSlots();
|
|
429
|
+
onMounted(checkSlots);
|
|
430
|
+
onUpdated(checkSlots);
|
|
431
|
+
onMounted(updateOptions);
|
|
432
|
+
onMounted(updateSelectedItems);
|
|
433
|
+
|
|
434
|
+
watch(() => props.value, updateSelectedItems);
|
|
435
|
+
watch(() => props.options, updateOptions);
|
|
436
|
+
watch(() => props.selectionFeedback, updateOptions);
|
|
437
|
+
|
|
438
|
+
const highlighted = computed({
|
|
439
|
+
get() {
|
|
440
|
+
return data.highlighted;
|
|
441
|
+
},
|
|
442
|
+
set(val) {
|
|
443
|
+
let firstMatchIndex = data.options.findIndex(
|
|
444
|
+
/***
|
|
445
|
+
* @param {MultiSelectOption} item
|
|
446
|
+
* @returns {boolean}
|
|
447
|
+
*/
|
|
448
|
+
item => item.value === val
|
|
449
|
+
);
|
|
450
|
+
if (firstMatchIndex < 0) {
|
|
451
|
+
firstMatchIndex = data.options.length ? 0 : -1;
|
|
452
|
+
data.highlighted = firstMatchIndex >= 0 ? data.options[0].value : '';
|
|
453
|
+
} else {
|
|
454
|
+
data.highlighted = val;
|
|
455
|
+
}
|
|
456
|
+
if (firstMatchIndex >= 0) {
|
|
457
|
+
nextTick(() => {
|
|
458
|
+
// $nextTick to prevent highlight check ahead of list update on filter
|
|
459
|
+
checkHighlightPosition(firstMatchIndex);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
onMounted(() => {
|
|
465
|
+
highlighted.value = props.value ? props.value : props.highlight; // override highlight with value if provided
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const internalFilter = computed({
|
|
469
|
+
get() {
|
|
470
|
+
return data.filter;
|
|
471
|
+
},
|
|
472
|
+
set(val) {
|
|
473
|
+
data.filter = val ? val : '';
|
|
474
|
+
emit('filter', val);
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
const filterableTagOverride = computed(() => {
|
|
478
|
+
// <style carbon tweaks - DO NOT USE STYLE TAG as it causes SSR issues
|
|
479
|
+
return props.filterable ? { marginTop: 0, marginBottom: 0 } : {};
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const elInput = ref(null);
|
|
483
|
+
const elList = ref(null);
|
|
484
|
+
const elOption = ref(null);
|
|
485
|
+
const elButton = ref(null);
|
|
486
|
+
const el = ref(null);
|
|
487
|
+
const elTag = ref(null);
|
|
488
|
+
|
|
489
|
+
function checkSlots() {
|
|
490
|
+
// NOTE: slots is not reactive so needs to be managed on updated
|
|
491
|
+
data.isInvalid = !!(slots['invalid-message'] || props.invalidMessage?.length);
|
|
492
|
+
data.isWarning = !!(slots['warning-message'] || props.warningMessage?.length);
|
|
493
|
+
data.isHelper = !!(slots['helper-text'] || props.helperText?.length);
|
|
494
|
+
}
|
|
495
|
+
const isWarning = computed(() => {
|
|
496
|
+
if (data.isInvalid) return false;
|
|
497
|
+
else return !!data.isWarning;
|
|
498
|
+
});
|
|
499
|
+
const isHelper = computed(() => {
|
|
500
|
+
if (data.isInvalid || props.inline || isWarning.value) return false;
|
|
501
|
+
else return !!data.isHelper;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
function clearFilter() {
|
|
505
|
+
internalFilter.value = '';
|
|
506
|
+
elInput.value?.focus();
|
|
507
|
+
doOpen(true);
|
|
508
|
+
updateOptions();
|
|
509
|
+
}
|
|
510
|
+
function checkHighlightPosition(newHighlight) {
|
|
511
|
+
if (elList.value && elOption.value && elOption.value[newHighlight]) {
|
|
512
|
+
if (elList.value?.scrollTop >= elOption.value[newHighlight].offsetTop) {
|
|
513
|
+
elList.value.scrollTop = elOption.value[newHighlight].offsetTop;
|
|
514
|
+
} else if (
|
|
515
|
+
elList.value.scrollTop + elList.value.clientHeight <
|
|
516
|
+
elOption.value[newHighlight].offsetTop +
|
|
517
|
+
elOption.value[newHighlight].offsetHeight
|
|
518
|
+
) {
|
|
519
|
+
elList.value.scrollTop =
|
|
520
|
+
elOption.value[newHighlight].offsetTop +
|
|
521
|
+
elOption.value[newHighlight].offsetHeight -
|
|
522
|
+
elList.value.clientHeight;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function doMove(up) {
|
|
527
|
+
if (data.options.length > 0) {
|
|
528
|
+
// re-query could have changed
|
|
529
|
+
const currentHighlight = data.options.findIndex(
|
|
530
|
+
/***
|
|
531
|
+
* @param {MultiSelectOption} item
|
|
532
|
+
* @returns {boolean}
|
|
533
|
+
*/
|
|
534
|
+
item => item.value === highlighted.value
|
|
535
|
+
);
|
|
536
|
+
let newHighlight;
|
|
537
|
+
|
|
538
|
+
if (up) {
|
|
539
|
+
if (currentHighlight <= 0) {
|
|
540
|
+
newHighlight = data.options.length - 1;
|
|
541
|
+
} else {
|
|
542
|
+
newHighlight = currentHighlight - 1;
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
if (currentHighlight >= data.options.length - 1) {
|
|
546
|
+
newHighlight = 0;
|
|
547
|
+
} else {
|
|
548
|
+
newHighlight = currentHighlight + 1;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
highlighted.value = data.options[newHighlight].value;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function updateOptions() {
|
|
555
|
+
if (props.autoFilter) {
|
|
556
|
+
const escFilter = internalFilter.value.replace(
|
|
557
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
558
|
+
'\\$&'
|
|
559
|
+
);
|
|
560
|
+
const pat = new RegExp(escFilter, 'iu');
|
|
561
|
+
data.options = props.options.filter(opt => pat.test(opt.label)).slice(0);
|
|
562
|
+
} else {
|
|
563
|
+
data.options = props.options.slice(0);
|
|
564
|
+
}
|
|
565
|
+
highlighted.value = props.highlight;
|
|
566
|
+
|
|
567
|
+
// multi select unique part
|
|
568
|
+
if (props.selectionFeedback !== selectionFeedbackOptions[FIXED]) {
|
|
569
|
+
// if included in data value move to top
|
|
570
|
+
data.options.sort(
|
|
571
|
+
/***
|
|
572
|
+
* @param {MultiSelectOption} a
|
|
573
|
+
* @param {MultiSelectOption} b
|
|
574
|
+
* @returns {number}
|
|
575
|
+
*/
|
|
576
|
+
(a, b) =>
|
|
577
|
+
(data.selectedItems.includes(a.value) ? -1 : 1) -
|
|
578
|
+
(data.selectedItems.includes(b.value) ? -1 : 1)
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function updateHighlight() {
|
|
583
|
+
let firstMatchIndex;
|
|
584
|
+
if (props.autoHighlight && data.options.length > 0) {
|
|
585
|
+
// then highlight first match
|
|
586
|
+
const filterRegex = new RegExp(internalFilter.value, 'iu');
|
|
587
|
+
firstMatchIndex = data.options.findIndex(
|
|
588
|
+
/***
|
|
589
|
+
* @param {MultiSelectOption} item
|
|
590
|
+
* @returns {boolean}
|
|
591
|
+
*/
|
|
592
|
+
item => filterRegex.test(item.label)
|
|
593
|
+
);
|
|
594
|
+
if (firstMatchIndex < 0) {
|
|
595
|
+
firstMatchIndex = 0;
|
|
596
|
+
}
|
|
597
|
+
highlighted.value = data.options[firstMatchIndex].value;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function onInput() {
|
|
601
|
+
doOpen(true);
|
|
602
|
+
|
|
603
|
+
updateOptions();
|
|
604
|
+
updateHighlight();
|
|
605
|
+
}
|
|
606
|
+
function doOpen(newVal) {
|
|
607
|
+
if (
|
|
608
|
+
newVal &&
|
|
609
|
+
!data.open &&
|
|
610
|
+
props.selectionFeedback === selectionFeedbackOptions[TOP_AFTER_REOPEN]
|
|
611
|
+
) {
|
|
612
|
+
updateOptions();
|
|
613
|
+
}
|
|
614
|
+
data.open = newVal;
|
|
615
|
+
}
|
|
616
|
+
function onDown() {
|
|
617
|
+
if (!data.open) {
|
|
618
|
+
doOpen(true);
|
|
619
|
+
} else {
|
|
620
|
+
doMove(false);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function onUp() {
|
|
624
|
+
if (data.open) {
|
|
625
|
+
doMove(true);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function inputOrButtonFocus() {
|
|
629
|
+
if (props.filterable) {
|
|
630
|
+
elInput.value?.focus();
|
|
631
|
+
} else {
|
|
632
|
+
elButton.value?.focus();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function onEsc() {
|
|
636
|
+
doOpen(false);
|
|
637
|
+
inputOrButtonFocus();
|
|
638
|
+
}
|
|
639
|
+
function onEnter() {
|
|
640
|
+
if (data.open) {
|
|
641
|
+
onItemClick(highlighted.value);
|
|
642
|
+
elInput.value?.focus();
|
|
643
|
+
internalFilter.value = '';
|
|
644
|
+
|
|
645
|
+
doOpen(false);
|
|
646
|
+
updateOptions();
|
|
647
|
+
} else {
|
|
648
|
+
doOpen(true);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function onClick(ev) {
|
|
652
|
+
if (props.disabled) {
|
|
653
|
+
ev.preventDefault();
|
|
654
|
+
} else {
|
|
655
|
+
if (data.open) {
|
|
656
|
+
inputOrButtonFocus();
|
|
657
|
+
// done this way round otherwise will auto open on focus.
|
|
658
|
+
nextTick(() => {
|
|
659
|
+
doOpen(false);
|
|
660
|
+
});
|
|
661
|
+
} else {
|
|
662
|
+
doOpen(true);
|
|
663
|
+
inputOrButtonFocus();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function clearValues() {
|
|
668
|
+
data.selectedItems = [];
|
|
669
|
+
inputOrButtonFocus();
|
|
670
|
+
}
|
|
671
|
+
function onFocusOut(ev) {
|
|
672
|
+
if (
|
|
673
|
+
!el.value?.contains(ev.relatedTarget) &&
|
|
674
|
+
!elTag.value?.contains(ev.target)
|
|
675
|
+
) {
|
|
676
|
+
doOpen(false);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function onMousemove(val) {
|
|
680
|
+
highlighted.value = val;
|
|
681
|
+
}
|
|
682
|
+
function onItemClick(val) {
|
|
683
|
+
const option = props.options.find(item => item.value === val);
|
|
684
|
+
if (option && !option.disabled) {
|
|
685
|
+
const index = data.selectedItems.findIndex(item => val === item);
|
|
686
|
+
if (index > -1) {
|
|
687
|
+
data.selectedItems.splice(index, 1);
|
|
688
|
+
} else {
|
|
689
|
+
data.selectedItems.push(val);
|
|
690
|
+
}
|
|
691
|
+
if (props.selectionFeedback === selectionFeedbackOptions[TOP]) {
|
|
692
|
+
updateOptions();
|
|
693
|
+
}
|
|
694
|
+
elButton.value?.focus();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function inputClick() {
|
|
698
|
+
if (!data.open) {
|
|
699
|
+
doOpen(true);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function inputFocus() {
|
|
703
|
+
doOpen(true);
|
|
704
|
+
}
|
|
705
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const TOP_AFTER_REOPEN = 0;
|
|
2
|
+
const TOP = 1;
|
|
3
|
+
const FIXED = 2;
|
|
4
|
+
const DIRECTION_BOTTOM = 'bottom';
|
|
5
|
+
const DIRECTION_TOP = 'top';
|
|
6
|
+
const DIRECTIONS = [DIRECTION_TOP, DIRECTION_BOTTOM];
|
|
7
|
+
const selectionFeedbackOptions = ['top-after-reopen', 'top', 'fixed'];
|
|
8
|
+
export {
|
|
9
|
+
TOP_AFTER_REOPEN,
|
|
10
|
+
TOP,
|
|
11
|
+
FIXED,
|
|
12
|
+
selectionFeedbackOptions,
|
|
13
|
+
DIRECTIONS,
|
|
14
|
+
DIRECTION_TOP,
|
|
15
|
+
DIRECTION_BOTTOM,
|
|
16
|
+
};
|