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

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