PluginProbe
The Innovative Form Builder – IvyForms / 1.0
The Innovative Form Builder – IvyForms v1.0
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / views / _components / input / IvySelectInput.vue

IvySelectInput.vue in The Innovative Form Builder – IvyForms 1.0, at frontend/src/views/_components/input/IvySelectInput.vue

535 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <ElSelect
3 ref="elSelectRef"
4 v-bind="{ ...$attrs, ...$props }"
5 v-model="model"
6 :placeholder="props.placeholder"
7 :disabled="props.disabled"
8 :readonly="props.readonly"
9 :multiple="props.multiple"
10 :collapse-tags="props.multiple"
11 :clearable="props.clearable && !props.readonly"
12 :class="[
13 {
14 'is-clearable': props.clearable && !props.readonly,
15 'is-filled': props.filled,
16 'is-secondary': isSecondary,
17 'is-error': props.error,
18 'is-readonly': props.readonly,
19 },
20 'ivyforms-form-item-select',
21 ]"
22 :max-collapse-tags="props.maxTags"
23 :aria-label="computedAriaLabel"
24 :fit-input-width="props.fitInputWidth"
25 :popper-class="selectPopperClass"
26 @visible-change="onVisibleChange"
27 >
28 <template v-if="!!slots['prefix'] || iconStart" #prefix>
29 <slot name="prefix">
30 <IvyIcon
31 v-if="iconStart"
32 :name="iconStart"
33 :category="iconStartCategory"
34 :type="iconStartType"
35 class="ivyforms-input__icon-start"
36 size="l"
37 color="var(--map-base-dusk-symbol-2)"
38 />
39
40 <!-- TODO Add the avatar component-->
41 <IvyIcon v-if="props.avatar" class="ivyforms-input__avatar" name="user" size="l" />
42 <!-- <IvyAvatar-->
43 <!-- v-if="props.avatar"-->
44 <!-- class="ivyforms-input__avatar"-->
45 <!-- />-->
46 </slot>
47 </template>
48 <slot />
49 </ElSelect>
50 </template>
51
52 <script setup lang="ts">
53 import { useSlots, inject, provide, computed, nextTick, unref, ref } from 'vue'
54 import type { IconCategory } from '@/types/icons/icon-category'
55 import type { IconType } from '@/types/icons/icon-type'
56 import { useLabels } from '@/composables/useLabels'
57 const { getLabel } = useLabels()
58
59 interface Props {
60 modelValue: string | string[] | null | undefined
61 label?: string
62 placeholder?: string
63 iconStart?: string
64 iconEnd?: string
65 iconStartCategory?: IconCategory
66 iconEndCategory?: IconCategory
67 avatar?: string
68 iconStartType?: IconType
69 iconEndType?: IconType
70 filled?: boolean
71 prefix?: string
72 suffix?: string
73 clearable?: boolean
74 disabled?: boolean
75 readonly?: boolean
76 multiple?: boolean
77 maxTags?: number
78 ariaLabel?: string
79 error?: boolean
80 fieldType?: string
81 fitInputWidth?: boolean
82 secondary?: boolean
83 }
84
85 const props = withDefaults(defineProps<Props>(), {
86 placeholder: '',
87 iconStartCategory: 'global',
88 iconEndCategory: 'global',
89 iconStartType: 'outline',
90 iconEndType: 'outline',
91 label: '',
92 iconStart: '',
93 iconEnd: '',
94 avatar: '',
95 prefix: '',
96 suffix: '',
97 maxTags: 3,
98 ariaLabel: '',
99 error: false,
100 readonly: false,
101 fieldType: '',
102 fitInputWidth: true,
103 secondary: false,
104 })
105
106 const injectedIsSecondary = inject('isSecondary', false)
107 const injectedFormPopperScopeClass = inject<string>('formPopperScopeClass', '')
108 const isSecondary = computed(() => props.secondary || !!unref(injectedIsSecondary))
109 provide('isSecondary', isSecondary)
110
111 const formPopperScopeClass = computed(() => {
112 const resolvedValue = unref(injectedFormPopperScopeClass)
113 return typeof resolvedValue === 'string' ? resolvedValue.trim() : ''
114 })
115
116 const slots = useSlots()
117 const elSelectRef = ref<{ $el?: HTMLElement } | null>(null)
118
119 const emit = defineEmits(['update:modelValue'])
120
121 const model = computed({
122 get: () => props.modelValue,
123 set: (value) => {
124 if (!props.readonly) {
125 emit('update:modelValue', value)
126 }
127 },
128 })
129
130 const computedAriaLabel = computed(() => props.ariaLabel || getLabel('select'))
131 const selectPopperClass = computed(() => {
132 const classes = [formPopperScopeClass.value, 'ivyforms-select-popper']
133 if (isSecondary.value) {
134 classes.push('ivyforms-select-popper--secondary')
135 }
136 return classes.filter(Boolean).join(' ')
137 })
138
139 const POPPER_BASE_CLASS = 'ivyforms-select-popper'
140
141 const clearStaleActiveDescendant = () => {
142 const rootElement = elSelectRef.value?.$el
143 if (!rootElement) {
144 return
145 }
146
147 const activeDescendantElements =
148 rootElement.querySelectorAll<HTMLElement>('[aria-activedescendant]')
149 activeDescendantElements.forEach((element) => {
150 element.removeAttribute('aria-activedescendant')
151 })
152 }
153
154 const applyDropdownA11y = (wrapElement: HTMLElement) => {
155 if (!wrapElement.hasAttribute('tabindex')) {
156 wrapElement.setAttribute('tabindex', '0')
157 }
158
159 if (!wrapElement.hasAttribute('role')) {
160 wrapElement.setAttribute('role', 'region')
161 }
162
163 if (!wrapElement.hasAttribute('aria-label')) {
164 wrapElement.setAttribute('aria-label', getLabel('select_options'))
165 }
166 }
167
168 const findDropdownWrap = (): HTMLElement | null => {
169 const popperScopeClass = formPopperScopeClass.value ? `.${formPopperScopeClass.value}` : ''
170 const wrapElements = document.querySelectorAll<HTMLElement>(
171 `${popperScopeClass}.${POPPER_BASE_CLASS} .el-select-dropdown__wrap`,
172 )
173
174 if (wrapElements.length === 0) {
175 return null
176 }
177
178 return wrapElements[wrapElements.length - 1]
179 }
180
181 const onVisibleChange = (visible: boolean) => {
182 if (!visible) {
183 clearStaleActiveDescendant()
184 return
185 }
186
187 nextTick(() => {
188 const wrapElement = findDropdownWrap()
189
190 if (wrapElement) {
191 applyDropdownA11y(wrapElement)
192 return
193 }
194
195 const observer = new MutationObserver(() => {
196 const observedWrapElement = findDropdownWrap()
197
198 if (!observedWrapElement) {
199 return
200 }
201
202 applyDropdownA11y(observedWrapElement)
203 observer.disconnect()
204 })
205
206 observer.observe(document.body, {
207 childList: true,
208 subtree: true,
209 })
210
211 setTimeout(() => {
212 observer.disconnect()
213 }, 2000)
214 })
215 }
216 </script>
217
218 <style lang="scss">
219 .ivyforms-form-item-select {
220 .el-select__wrapper {
221 border: 1px solid var(--map-base-dusk-stroke--2);
222 background: var(--map-base-dusk-o05);
223 border-radius: var(--Radius-radius-md, 8px);
224 display: flex;
225 align-items: center;
226 gap: var(--Spacing-sm, 8px);
227 padding: var(--Spacing-sm, 8px) var(--Spacing-lg, 12px);
228 height: 40px;
229 box-shadow: none !important;
230
231 &.is-disabled {
232 background: var(--map-base-dusk-o05);
233 }
234
235 &:hover:not(.is-disabled),
236 &.is-hovering:not(.is-focused):not(.is-disabled) {
237 border: 1px solid var(--map-base-dusk-stroke-0);
238 }
239
240 &:focus,
241 &:active,
242 &.is-active,
243 &.is-focused {
244 border: 1px solid var(--map-base-brand-stroke-0) !important;
245
246 .el-tag {
247 background: var(--ivyforms-select-tag-bg, var(--map-base-brand-o10));
248 color: var(--ivyforms-select-tag-text, var(--map-base-brand-symbol-0));
249
250 .el-tag__close svg {
251 color: var(--ivyforms-select-tag-text, var(--map-base-brand-symbol-0));
252 width: 14px;
253 height: 14px;
254 }
255 }
256 }
257
258 &:hover:focus,
259 &:hover.is-focused,
260 &.is-hovering.is-focused {
261 border: 1px solid var(--map-base-brand-stroke-0) !important;
262 }
263
264 .el-select__selection {
265 padding: 0;
266 font-size: 16px;
267 font-weight: 400;
268 line-height: 22px;
269 color: var(--map-base-text-0);
270 background-color: transparent;
271 display: flex;
272 align-items: center;
273 gap: 4px;
274 flex: 1 0 0;
275 }
276
277 .el-select__suffix .el-icon.el-select__caret.el-select__icon {
278 background: url('../../../assets/icons/arrows/line/chevron-down.svg') no-repeat center;
279 background-size: 20px 20px;
280 width: 20px;
281 height: 20px;
282 cursor: pointer;
283 opacity: 1 !important;
284 transform: none !important;
285 transition: none !important;
286 padding: 2px;
287
288 svg {
289 display: none;
290 }
291
292 &.el-select__clear {
293 background: url('../../../assets/icons/global/fill-duo/close-circle.svg') no-repeat center;
294 background-size: 20px 20px;
295 }
296
297 &.is-reverse {
298 background: url('../../../assets/icons/arrows/line/chevron-up.svg') no-repeat center;
299 background-size: 20px 20px;
300 }
301 }
302
303 .el-select__suffix,
304 .el-select__suffix-inner {
305 display: flex;
306 align-items: center;
307 justify-content: center;
308 align-self: center;
309 }
310
311 .el-select__suffix {
312 margin-left: auto;
313 }
314
315 .el-select__caret {
316 margin: 0 !important;
317 top: auto !important;
318 }
319
320 .el-select__placeholder {
321 color: var(--map-base-text-0);
322 }
323 }
324
325 &.is-secondary .el-select__wrapper {
326 &:active,
327 &:focus,
328 &.is-focused,
329 &.is-active {
330 border: 1px solid var(--map-base-purple-stroke-0) !important;
331
332 .el-tag {
333 background: var(--ivyforms-select-tag-bg, var(--map-base-purple-o10));
334 color: var(--ivyforms-select-tag-text, var(--map-base-purple-symbol-0));
335
336 .el-tag__close svg {
337 color: var(--ivyforms-select-tag-text, var(--map-base-purple-symbol-0));
338 width: 14px;
339 height: 14px;
340 }
341 }
342 }
343 }
344
345 // Readonly
346 &.is-readonly {
347 pointer-events: none;
348
349 .el-select__wrapper {
350 cursor: default;
351
352 // Keep normal opacity unlike disabled state
353 opacity: 1;
354 background: var(--map-base-dusk-o05);
355
356 &:hover,
357 &.is-hovering,
358 &:focus,
359 &:active,
360 &.is-active,
361 &.is-focused {
362 border: 1px solid var(--map-base-dusk-stroke--2);
363 cursor: default;
364 }
365
366 .el-select__suffix .el-icon.el-select__caret.el-select__icon {
367 display: none;
368 }
369
370 // Maintain normal text color
371 .el-select__selection {
372 color: var(--map-base-text-0);
373 cursor: default;
374 }
375 }
376
377 // Hide dropdown options
378 .el-select-dropdown {
379 display: none !important;
380 }
381 }
382
383 // Filled
384 &.is-filled {
385 .el-select__wrapper {
386 border: 1px solid var(--map-base-dusk-stroke--2);
387 background: var(--map-base-dusk-o05);
388
389 &:hover,
390 &.is-hovering {
391 border: 1px solid var(--map-base-dusk-stroke-0);
392 }
393
394 &:active,
395 &.is-active,
396 &:focus,
397 &.is-focused {
398 border: 1px solid var(--map-base-brand-stroke-0);
399
400 .el-tag {
401 background: var(--ivyforms-select-tag-bg, var(--map-base-brand-o10));
402 color: var(--ivyforms-select-tag-text, var(--map-base-brand-symbol-0));
403
404 .el-tag__close svg {
405 color: var(--ivyforms-select-tag-text, var(--map-base-brand-symbol-0));
406 width: 14px;
407 height: 14px;
408 }
409 }
410 }
411
412 .el-tag {
413 background: var(--ivyforms-select-tag-bg, var(--map-base-dusk-o20));
414 color: var(--ivyforms-select-tag-text, var(--map-base-text-0));
415
416 .el-tag__close svg {
417 color: var(--ivyforms-select-tag-text, var(--map-base-text-0));
418 width: 14px;
419 height: 14px;
420 }
421 }
422 }
423
424 &.is-error .el-select__wrapper {
425 border: 1px solid var(--map-status-error-symbol-0);
426 }
427
428 &.is-warning .el-select__wrapper {
429 border: 1px solid var(--map-status-warning-stroke-0);
430 }
431
432 .el-tag {
433 font-size: 14px;
434 font-weight: 400;
435 line-height: 20px;
436 display: flex;
437 padding: 2px 4px 2px 8px;
438 align-items: center;
439 gap: 4px;
440 cursor: pointer;
441 background: var(--ivyforms-select-tag-bg, var(--map-base-dusk-o20));
442 color: var(--ivyforms-select-tag-text, var(--map-base-text-0));
443
444 .el-tag__close {
445 background: url('../../../assets/icons/global/outline/close.svg') no-repeat center;
446 background-size: contain;
447 width: 16px;
448 height: 16px;
449 cursor: pointer;
450 margin: 0;
451
452 svg {
453 color: var(--ivyforms-select-tag-text, var(--map-base-text-0));
454 width: 14px;
455 height: 14px;
456 }
457
458 &:hover {
459 background: url('../../../assets/icons/global/outline/close.svg') no-repeat center;
460 width: 16px;
461 height: 16px;
462 }
463 }
464 }
465 }
466
467 // Option Tag
468 .el-tag--info {
469 background: var(--ivyforms-select-tag-bg, var(--map-base-dusk-o10));
470 .el-select__tags-text {
471 color: var(--ivyforms-select-tag-text, var(--map-base-text-0));
472 }
473 }
474 }
475
476 .el-select__popper {
477 .el-scrollbar {
478 border-radius: 3px;
479 max-height: 161px;
480
481 .el-select-dropdown__wrap {
482 max-height: 161px;
483 }
484 }
485 }
486
487 .ivyforms-select-popper {
488 .el-select-dropdown__item.is-selected {
489 color: var(--ivyforms-select-option-selected-text, var(--map-base-dusk-symbol-2));
490 background-color: var(--ivyforms-select-option-selected-bg, var(--map-base-brand-o10));
491 }
492
493 &.ivyforms-select-popper--secondary {
494 .el-select-dropdown__item.is-selected {
495 color: var(
496 --ivyforms-select-option-selected-text,
497 var(--map-base-purple-symbol-0)
498 ) !important;
499 background-color: var(
500 --ivyforms-select-option-selected-bg,
501 var(--map-base-purple-o10)
502 ) !important;
503 }
504 }
505 }
506
507 .el-form-item {
508 &.is-error {
509 .ivyforms-form-item-select.is-error {
510 .el-select__wrapper {
511 border: 1px solid var(--map-status-error-symbol-0);
512 }
513 }
514 }
515 }
516
517 // Rating icon colors for IvySelectInput prefix
518 .ivyforms-form-item-select {
519 .el-select__wrapper {
520 .el-select__selection {
521 color: var(--map-base-text-0);
522 }
523 }
524
525 .ivyforms-input__icon-start {
526 // All line icons use dusk color (same as IvyRating unfilled state)
527 .ivyforms-icon__svg.type-line {
528 path {
529 stroke: var(--map-base-dusk-symbol-2);
530 }
531 }
532 }
533 }
534 </style>
535