PluginProbe
The Innovative Form Builder – IvyForms / 0.6
The Innovative Form Builder – IvyForms v0.6
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 0.6, at frontend/src/views/_components/input/IvySelectInput.vue

403 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <ElSelect
3 ref="elInputRef"
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 :popper-class="props.fieldType === 'time' ? 'ivyforms-form-item-select__dropdown--short' : ''"
23 :max-collapse-tags="props.maxTags"
24 :aria-label="computedAriaLabel"
25 >
26 <template v-if="!!slots['prefix'] || iconStart" #prefix>
27 <slot name="prefix">
28 <IvyIcon
29 v-if="iconStart"
30 :name="iconStart"
31 :category="iconStartCategory"
32 :type="iconStartType"
33 class="ivyforms-input__icon-start"
34 size="l"
35 color="var(--map-base-dusk-symbol-2)"
36 />
37
38 <!-- TODO Add the avatar component-->
39 <IvyIcon v-if="props.avatar" class="ivyforms-input__avatar" name="user" size="l" />
40 <!-- <IvyAvatar-->
41 <!-- v-if="props.avatar"-->
42 <!-- class="ivyforms-input__avatar"-->
43 <!-- />-->
44 </slot>
45 </template>
46 <slot />
47 </ElSelect>
48 </template>
49
50 <script setup lang="ts">
51 import { useSlots, inject, computed, ref, onMounted, nextTick } from 'vue'
52 import type { IconCategory } from '@/types/icons/icon-category'
53 import type { IconType } from '@/types/icons/icon-type'
54 import { useLabels } from '@/composables/useLabels'
55 const { getLabel } = useLabels()
56
57 interface Props {
58 modelValue: string | string[] | null | undefined
59 label?: string
60 placeholder?: string
61 iconStart?: string
62 iconEnd?: string
63 iconStartCategory?: IconCategory
64 iconEndCategory?: IconCategory
65 avatar?: string
66 iconStartType?: IconType
67 iconEndType?: IconType
68 filled?: boolean
69 prefix?: string
70 suffix?: string
71 clearable?: boolean
72 disabled?: boolean
73 readonly?: boolean
74 multiple?: boolean
75 maxTags?: number
76 ariaLabel?: string
77 error?: boolean
78 fieldType?: string
79 }
80
81 const props = withDefaults(defineProps<Props>(), {
82 placeholder: '',
83 iconStartCategory: 'global',
84 iconEndCategory: 'global',
85 iconStartType: 'outline',
86 iconEndType: 'outline',
87 label: '',
88 iconStart: '',
89 iconEnd: '',
90 avatar: '',
91 prefix: '',
92 suffix: '',
93 maxTags: 3,
94 ariaLabel: '',
95 error: false,
96 readonly: false,
97 fieldType: '',
98 })
99
100 const isSecondary = inject('isSecondary', false)
101
102 const slots = useSlots()
103
104 const emit = defineEmits(['update:modelValue'])
105
106 const elInputRef = ref(null)
107
108 const model = computed({
109 get: () => props.modelValue,
110 set: (value) => {
111 if (!props.readonly) {
112 emit('update:modelValue', value)
113 }
114 },
115 })
116
117 const computedAriaLabel = computed(() => props.ariaLabel || getLabel('select'))
118
119 // Add keyboard accessibility to select dropdown wrap element
120 onMounted(() => {
121 nextTick(() => {
122 // Find all el-select-dropdown__wrap elements and make them focusable
123 const dropdownWraps = document.querySelectorAll('.el-select-dropdown__wrap')
124 dropdownWraps.forEach((wrap) => {
125 const wrapElement = wrap as HTMLElement
126 // Make dropdown wrap focusable with keyboard
127 if (!wrapElement.hasAttribute('tabindex')) {
128 wrapElement.setAttribute('tabindex', '0')
129 }
130 // Add ARIA role for better screen reader support
131 if (!wrapElement.hasAttribute('role')) {
132 wrapElement.setAttribute('role', 'region')
133 wrapElement.setAttribute('aria-label', getLabel('select_options'))
134 }
135 })
136 })
137 })
138 </script>
139
140 <style lang="scss">
141 .ivyforms-form-item-select {
142 .el-select__wrapper {
143 border: 1px solid var(--map-base-dusk-stroke--2);
144 background: var(--map-base-dusk-o05);
145 border-radius: var(--Radius-radius-md, 8px);
146 display: flex;
147 align-items: center;
148 gap: var(--Spacing-sm, 8px);
149 padding: var(--Spacing-sm, 8px) var(--Spacing-lg, 12px);
150 height: 40px;
151 box-shadow: none !important;
152
153 &.is-disabled {
154 background: var(--map-base-dusk-o05);
155 }
156
157 &:hover:not(.is-disabled),
158 &.is-hovering:not(.is-focused):not(.is-disabled) {
159 border: 1px solid var(--map-base-dusk-stroke-0);
160 }
161
162 &:focus,
163 &:active,
164 &.is-active,
165 &.is-focused {
166 border: 1px solid var(--map-base-brand-stroke-0) !important;
167
168 .el-tag {
169 background: var(--map-base-brand-o10);
170 color: var(--map-base-brand-symbol-0);
171
172 .el-tag__close svg {
173 color: var(--map-base-brand-symbol-0);
174 width: 14px;
175 height: 14px;
176 }
177 }
178 }
179
180 &:hover:focus,
181 &:hover.is-focused,
182 &.is-hovering.is-focused {
183 border: 1px solid var(--map-base-brand-stroke-0) !important;
184 }
185
186 .el-select__selection {
187 padding: 0;
188 font-size: 16px;
189 font-weight: 400;
190 line-height: 22px;
191 color: var(--map-base-text-0);
192 background-color: transparent;
193 display: flex;
194 align-items: center;
195 gap: 4px;
196 flex: 1 0 0;
197 }
198
199 .el-select__suffix .el-icon.el-select__caret.el-select__icon {
200 background: url('../../../assets/icons/arrows/line/chevron-down.svg') no-repeat center;
201 background-size: contain;
202 width: 20px;
203 height: 20px;
204 cursor: pointer;
205 opacity: 1 !important;
206
207 &.el-select__clear {
208 background: url('../../../assets/icons/global/fill-duo/close-circle.svg') no-repeat center;
209 }
210 }
211 }
212
213 &.is-secondary .el-select__wrapper {
214 &:active,
215 &:focus,
216 &.is-focused,
217 &.is-active {
218 border: 1px solid var(--map-base-purple-stroke-0);
219
220 .el-tag {
221 background: var(--map-base-purple-o10);
222 color: var(--map-base-purple-symbol-0);
223
224 .el-tag__close svg {
225 color: var(--map-base-purple-symbol-0);
226 width: 14px;
227 height: 14px;
228 }
229 }
230 }
231
232 .el-select__wrapper {
233 // Active & Focus
234 &:active,
235 &:focus,
236 &.is-focused,
237 &.is-active {
238 border: 1px solid var(--map-base-purple-stroke-0);
239 }
240 }
241 }
242
243 // Readonly
244 &.is-readonly {
245 pointer-events: none;
246
247 .el-select__wrapper {
248 cursor: default;
249
250 // Keep normal opacity unlike disabled state
251 opacity: 1;
252 background: var(--map-base-dusk-o05);
253
254 &:hover,
255 &.is-hovering,
256 &:focus,
257 &:active,
258 &.is-active,
259 &.is-focused {
260 border: 1px solid var(--map-base-dusk-stroke--2);
261 cursor: default;
262 }
263
264 .el-select__suffix .el-icon.el-select__caret.el-select__icon {
265 display: none;
266 }
267
268 // Maintain normal text color
269 .el-select__selection {
270 color: var(--map-base-text-0);
271 cursor: default;
272 }
273 }
274
275 // Hide dropdown options
276 .el-select-dropdown {
277 display: none !important;
278 }
279 }
280
281 // Filled
282 &.is-filled {
283 .el-select__wrapper {
284 border: 1px solid var(--map-base-dusk-stroke--2);
285 background: var(--map-base-dusk-o05);
286
287 &:hover,
288 &.is-hovering {
289 border: 1px solid var(--map-base-dusk-stroke-0);
290 }
291
292 &:active,
293 &.is-active,
294 &:focus,
295 &.is-focused {
296 border: 1px solid var(--map-base-brand-stroke-0);
297
298 .el-tag {
299 background: var(--map-base-brand-o10);
300 color: var(--map-base-brand-symbol-0);
301
302 .el-tag__close svg {
303 color: var(--map-base-brand-symbol-0);
304 width: 14px;
305 height: 14px;
306 }
307 }
308 }
309
310 .el-tag {
311 background: var(--map-base-dusk-o20);
312 color: var(--map-base-text-0);
313
314 .el-tag__close svg {
315 color: var(--map-base-text-0);
316 width: 14px;
317 height: 14px;
318 }
319 }
320 }
321
322 &.is-error .el-select__wrapper {
323 border: 1px solid var(--map-status-error-symbol-0);
324 }
325
326 &.is-warning .el-select__wrapper {
327 border: 1px solid var(--map-status-warning-stroke-0);
328 }
329
330 .el-tag {
331 font-size: 14px;
332 font-weight: 400;
333 line-height: 20px;
334 display: flex;
335 padding: 2px 4px 2px 8px;
336 align-items: center;
337 gap: 4px;
338 cursor: pointer;
339 background: var(--map-base-dusk-o20);
340 color: var(--map-base-text-0);
341
342 .el-tag__close {
343 background: url('../../../assets/icons/global/outline/close.svg') no-repeat center;
344 background-size: contain;
345 width: 16px;
346 height: 16px;
347 cursor: pointer;
348 margin: 0;
349
350 svg {
351 color: var(--map-base-text-0);
352 width: 14px;
353 height: 14px;
354 }
355
356 &:hover {
357 background: url('../../../assets/icons/global/outline/close.svg') no-repeat center;
358 width: 16px;
359 height: 16px;
360 }
361 }
362 }
363 }
364
365 // Option Tag
366 .el-tag--info {
367 background: var(--map-base-dusk-o10);
368 .el-select__tags-text {
369 color: var(--map-base-text-0);
370 }
371 }
372
373 // Short variant for time field type
374 &__dropdown {
375 &--short {
376 .el-scrollbar {
377 max-height: 144px;
378
379 .el-select-dropdown__wrap {
380 max-height: 144px;
381 }
382 }
383 }
384 }
385 }
386
387 .el-select__popper {
388 .el-scrollbar {
389 border-radius: 3px;
390 }
391 }
392
393 .el-form-item {
394 &.is-error {
395 .ivyforms-form-item-select.is-error {
396 .el-select__wrapper {
397 border: 1px solid var(--map-status-error-symbol-0);
398 }
399 }
400 }
401 }
402 </style>
403