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

397 lines 9.3 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);
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 .el-select__selection {
181 padding: 0;
182 font-size: 16px;
183 font-weight: 400;
184 line-height: 22px;
185 color: var(--map-base-text-0);
186 background-color: transparent;
187 display: flex;
188 align-items: center;
189 gap: 4px;
190 flex: 1 0 0;
191 }
192
193 .el-select__suffix .el-icon.el-select__caret.el-select__icon {
194 background: url('../../../assets/icons/arrows/line/chevron-down.svg') no-repeat center;
195 background-size: contain;
196 width: 20px;
197 height: 20px;
198 cursor: pointer;
199 opacity: 1 !important;
200
201 &.el-select__clear {
202 background: url('../../../assets/icons/global/fill-duo/close-circle.svg') no-repeat center;
203 }
204 }
205 }
206
207 &.is-secondary .el-select__wrapper {
208 &:active,
209 &:focus,
210 &.is-focused,
211 &.is-active {
212 border: 1px solid var(--map-base-purple-stroke-0);
213
214 .el-tag {
215 background: var(--map-base-purple-o10);
216 color: var(--map-base-purple-symbol-0);
217
218 .el-tag__close svg {
219 color: var(--map-base-purple-symbol-0);
220 width: 14px;
221 height: 14px;
222 }
223 }
224 }
225
226 .el-select__wrapper {
227 // Active & Focus
228 &:active,
229 &:focus,
230 &.is-focused,
231 &.is-active {
232 border: 1px solid var(--map-base-purple-stroke-0);
233 }
234 }
235 }
236
237 // Readonly
238 &.is-readonly {
239 pointer-events: none;
240
241 .el-select__wrapper {
242 cursor: default;
243
244 // Keep normal opacity unlike disabled state
245 opacity: 1;
246 background: var(--map-base-dusk-o05);
247
248 &:hover,
249 &.is-hovering,
250 &:focus,
251 &:active,
252 &.is-active,
253 &.is-focused {
254 border: 1px solid var(--map-base-dusk-stroke--2);
255 cursor: default;
256 }
257
258 .el-select__suffix .el-icon.el-select__caret.el-select__icon {
259 display: none;
260 }
261
262 // Maintain normal text color
263 .el-select__selection {
264 color: var(--map-base-text-0);
265 cursor: default;
266 }
267 }
268
269 // Hide dropdown options
270 .el-select-dropdown {
271 display: none !important;
272 }
273 }
274
275 // Filled
276 &.is-filled {
277 .el-select__wrapper {
278 border: 1px solid var(--map-base-dusk-stroke--2);
279 background: var(--map-base-dusk-o05);
280
281 &:hover,
282 &.is-hovering {
283 border: 1px solid var(--map-base-dusk-stroke-0);
284 }
285
286 &:active,
287 &.is-active,
288 &:focus,
289 &.is-focused {
290 border: 1px solid var(--map-base-brand-stroke-0);
291
292 .el-tag {
293 background: var(--map-base-brand-o10);
294 color: var(--map-base-brand-symbol-0);
295
296 .el-tag__close svg {
297 color: var(--map-base-brand-symbol-0);
298 width: 14px;
299 height: 14px;
300 }
301 }
302 }
303
304 .el-tag {
305 background: var(--map-base-dusk-o20);
306 color: var(--map-base-text-0);
307
308 .el-tag__close svg {
309 color: var(--map-base-text-0);
310 width: 14px;
311 height: 14px;
312 }
313 }
314 }
315
316 &.is-error .el-select__wrapper {
317 border: 1px solid var(--map-status-error-symbol-0);
318 }
319
320 &.is-warning .el-select__wrapper {
321 border: 1px solid var(--map-status-warning-stroke-0);
322 }
323
324 .el-tag {
325 font-size: 14px;
326 font-weight: 400;
327 line-height: 20px;
328 display: flex;
329 padding: 2px 4px 2px 8px;
330 align-items: center;
331 gap: 4px;
332 cursor: pointer;
333 background: var(--map-base-dusk-o20);
334 color: var(--map-base-text-0);
335
336 .el-tag__close {
337 background: url('../../../assets/icons/global/outline/close.svg') no-repeat center;
338 background-size: contain;
339 width: 16px;
340 height: 16px;
341 cursor: pointer;
342 margin: 0;
343
344 svg {
345 color: var(--map-base-text-0);
346 width: 14px;
347 height: 14px;
348 }
349
350 &:hover {
351 background: url('../../../assets/icons/global/outline/close.svg') no-repeat center;
352 width: 16px;
353 height: 16px;
354 }
355 }
356 }
357 }
358
359 // Option Tag
360 .el-tag--info {
361 background: var(--map-base-dusk-o10);
362 .el-select__tags-text {
363 color: var(--map-base-text-0);
364 }
365 }
366
367 // Short variant for time field type
368 &__dropdown {
369 &--short {
370 .el-scrollbar {
371 max-height: 144px;
372
373 .el-select-dropdown__wrap {
374 max-height: 144px;
375 }
376 }
377 }
378 }
379 }
380
381 .el-select__popper {
382 .el-scrollbar {
383 border-radius: 3px;
384 }
385 }
386
387 .el-form-item {
388 &.is-error {
389 .ivyforms-form-item-select.is-error {
390 .el-select__wrapper {
391 border: 1px solid var(--map-status-error-symbol-0);
392 }
393 }
394 }
395 }
396 </style>
397