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 / IvyTextInput.vue

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

461 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <ElInput
3 ref="elInputRef"
4 v-bind="{ ...$attrs, ...$props }"
5 v-model="localModelValue"
6 :class="[
7 type === 'textarea' ? 'ivyforms-textarea' : 'ivyforms-input',
8 { 'is-filled': props.filled, 'is-secondary': props.secondary },
9 ]"
10 :show-password="showPassword !== undefined ? showPassword : type === 'password'"
11 :type="localInputType"
12 @focus="isFocused = true"
13 @blur="isFocused = false"
14 >
15 <template v-if="!!slots['prefix'] || iconStart" #prefix>
16 <slot name="prefix">
17 <IvyIcon
18 v-if="iconStart"
19 :name="iconStart"
20 :category="iconStartCategory"
21 :type="iconStartType"
22 class="ivyforms-input__icon-start"
23 size="l"
24 color="var(--map-base-dusk-symbol-2)"
25 />
26 </slot>
27 </template>
28 <template v-if="!!slots['suffix'] || iconEnd || props.clearable" #suffix>
29 <slot name="suffix">
30 <IvyIcon
31 v-if="props.iconEnd"
32 :name="props.iconEnd"
33 :category="props.iconEndCategory"
34 :type="props.iconEndType"
35 class="ivyforms-input__icon-end"
36 size="l"
37 color="var(--map-base-dusk-symbol-2)"
38 />
39 <IvyIcon
40 v-if="props.clearable && localModelValue"
41 name="close-circle"
42 class="ivyforms-input__clear"
43 size="l"
44 color="var(--map-base-dusk-symbol-2)"
45 type="fill-duo"
46 @click.stop="clearInput"
47 />
48 </slot>
49 </template>
50 </ElInput>
51 </template>
52
53 <script setup lang="ts">
54 import { ref, watch, useSlots, onMounted, onUnmounted } from 'vue'
55 import type { IconCategory } from '@/types/icons/icon-category'
56 import type { IconType } from '@/types/icons/icon-type'
57
58 interface Props {
59 type?:
60 | 'text'
61 | 'textarea'
62 | 'number'
63 | 'select'
64 | 'multiple-select'
65 | 'phone'
66 | 'email'
67 | 'website'
68 | 'password'
69 placeholder?: string
70 iconStart?: string
71 iconEnd?: string
72 iconStartCategory?: IconCategory
73 iconEndCategory?: IconCategory
74 avatar?: string
75 iconStartType?: IconType
76 iconEndType?: IconType
77 filled?: boolean
78 secondary?: boolean
79 prefix?: string
80 suffix?: string
81 clearable?: boolean
82 showPassword?: boolean
83 modelValue: string | number | null | undefined
84 }
85
86 const props = withDefaults(defineProps<Props>(), {
87 type: 'text',
88 placeholder: '',
89 iconStart: '',
90 iconEnd: '',
91 iconStartCategory: 'global',
92 iconEndCategory: 'global',
93 iconStartType: 'outline',
94 iconEndType: 'outline',
95 avatar: '',
96 prefix: '',
97 suffix: '',
98 showPassword: undefined,
99 })
100
101 const emit = defineEmits(['update:modelValue'])
102
103 const localModelValue = ref(props.modelValue)
104 const localInputType = ref(props.type)
105
106 const slots = useSlots()
107
108 const elInputRef = ref()
109 const isFocused = ref(false)
110
111 let cleanupAutofill: (() => void) | null = null
112
113 // Autofill detection - simplified without problematic animation events
114 const setupAutofillDetection = () => {
115 if (!elInputRef.value) return
116
117 // Skip autofill detection for textarea elements (paragraph fields)
118 if (props.type === 'textarea') {
119 return
120 }
121
122 const inputElement =
123 elInputRef.value.input || elInputRef.value.$el?.querySelector('input') || elInputRef.value.$el
124 if (!inputElement) return
125
126 // Only apply autofill detection to actual input elements, not textareas
127 if (inputElement.tagName?.toLowerCase() === 'textarea') {
128 return
129 }
130
131 // Clean up previous detection if exists
132 if (cleanupAutofill) {
133 cleanupAutofill()
134 cleanupAutofill = null
135 }
136
137 // Single check function that handles all autofill detection
138 const syncInputValue = () => {
139 if (inputElement.value !== localModelValue.value) {
140 localModelValue.value = inputElement.value
141 }
142 }
143
144 // Strategy 1: Polling for first 3 seconds with reasonable intervals
145 const intervalId = setInterval(syncInputValue, 100)
146 const timeoutId = setTimeout(() => clearInterval(intervalId), 3000)
147
148 // Strategy 2: Basic event listeners (removed problematic animation events)
149 const eventTypes = ['input', 'change', 'blur']
150 eventTypes.forEach((eventType) => {
151 inputElement.addEventListener(eventType, syncInputValue, { passive: true })
152 })
153
154 // Cleanup function
155 cleanupAutofill = () => {
156 clearInterval(intervalId)
157 clearTimeout(timeoutId)
158 eventTypes.forEach((eventType) => {
159 inputElement.removeEventListener(eventType, syncInputValue)
160 })
161 }
162
163 return cleanupAutofill
164 }
165
166 // Watch for modelValue changes (especially form resets)
167 watch(
168 () => props.modelValue,
169 (newValue, oldValue) => {
170 // If input was reset (empty/null value) after being populated, restart autofill detection
171 const isReset = !newValue || newValue === ''
172 const wasPopulated = oldValue && oldValue !== ''
173
174 if (isReset && wasPopulated) {
175 console.log('Input reset detected, restarting autofill detection')
176 // Restart autofill detection after a brief delay
177 setTimeout(() => {
178 setupAutofillDetection()
179 }, 200)
180 }
181 },
182 )
183
184 onMounted(() => {
185 // Set up autofill detection after component is mounted and Element Plus is ready
186 setTimeout(() => {
187 cleanupAutofill = setupAutofillDetection()
188 }, 100)
189 })
190
191 onUnmounted(() => {
192 if (cleanupAutofill) {
193 cleanupAutofill()
194 }
195 })
196
197 // Sync localModelValue with props.modelValue
198 watch(
199 () => props.modelValue,
200 (newValue) => {
201 localModelValue.value = newValue
202 },
203 )
204
205 // Emit update:modelValue when localModelValue changes
206 watch(localModelValue, (newValue) => {
207 emit('update:modelValue', newValue)
208 })
209
210 const clearInput = () => {
211 localModelValue.value = ''
212 }
213
214 const focus = () => {
215 elInputRef.value?.focus()
216 }
217
218 defineExpose({
219 focus,
220 })
221 </script>
222
223 <style lang="scss">
224 // Input
225 .ivyforms-input {
226 &.el-input {
227 height: 40px;
228
229 .el-input__wrapper {
230 display: flex;
231 align-items: center;
232 gap: var(--Spacing-sm, 8px);
233 padding: var(--Spacing-sm, 4px) var(--Spacing-lg, 12px);
234 align-self: stretch;
235 border-radius: var(--Radius-radius-md, 8px);
236 border: 1px solid var(--map-base-dusk-stroke--2);
237 background: var(--map-base-dusk-o05);
238 box-shadow: none !important;
239 outline: none !important;
240 transition: none !important;
241 box-sizing: border-box;
242
243 .el-input__prefix-inner > :last-child {
244 margin-right: 0;
245 }
246
247 // Hover
248 &:hover,
249 &.is-hover {
250 border: 1px solid var(--map-base-dusk-stroke-0);
251 background: var(--map-base-dusk-o05);
252 }
253
254 // Inner input
255 input {
256 padding: 0 !important;
257 margin: 0 !important;
258 font-size: 16px;
259 font-style: normal;
260 font-weight: 400;
261 line-height: 22px;
262 color: var(--map-base-text-0);
263 border: none !important;
264 box-shadow: none !important;
265 background-color: transparent;
266 display: flex;
267 align-items: center;
268 gap: 4px;
269 flex: 1 0 0;
270 outline: none !important;
271 box-sizing: border-box;
272 }
273
274 // Active & Focus
275 &:active,
276 &:focus,
277 &.is-focus,
278 &.is-active {
279 border: 1px solid var(--map-base-brand-stroke-0);
280 background: var(--map-base-dusk-o05);
281 box-shadow: none !important;
282 outline: none !important;
283 padding: var(--Spacing-sm, 4px) var(--Spacing-lg, 12px) !important;
284 }
285
286 // Clear Icon
287 .ivyforms-input__clear {
288 cursor: pointer;
289 width: 20px;
290 height: 20px;
291 transform: scale(1);
292 margin: 0;
293 }
294
295 .el-input__clear {
296 display: none !important;
297
298 &::before {
299 display: none !important;
300 }
301 }
302 }
303
304 // Disabled state
305 &.is-disabled {
306 .el-input__wrapper {
307 background: var(--map-base-dusk-o05);
308 border-color: var(--map-base-dusk-stroke--2);
309 box-shadow: none;
310
311 input {
312 color: var(--map-base-text-0);
313 opacity: 0.5;
314 }
315 }
316 }
317
318 // Secondary
319 &.is-secondary {
320 .el-input__wrapper {
321 // Active & Focus
322 &:active,
323 &:focus,
324 &.is-focus,
325 &.is-active {
326 border: 1px solid var(--map-base-purple-stroke-0);
327 }
328 }
329 }
330 }
331 }
332
333 // Form Item
334 .el-form-item {
335 &.ivyforms-form-item-input {
336 // Secondary
337 &.is-secondary {
338 .ivyforms-input {
339 &.el-input {
340 .el-input__wrapper {
341 // Active & Focus
342 &:active,
343 &:focus,
344 &.is-focus,
345 &.is-active {
346 border: 1px solid var(--map-base-purple-stroke-0);
347 }
348 }
349 }
350 }
351 }
352 }
353
354 // Error and warning states
355 &.is-error {
356 .ivyforms-input {
357 &.el-input {
358 .el-input__wrapper {
359 border: 1px solid var(--map-status-error-symbol-0) !important;
360 box-shadow: none !important;
361 }
362
363 .el-input__wrapper:hover,
364 .el-input__wrapper:focus,
365 .el-input__wrapper:active,
366 .el-input__wrapper.is-focus,
367 .el-input__wrapper.is-active {
368 border: 1px solid var(--map-status-error-symbol-0) !important;
369 box-shadow: none !important;
370 }
371 }
372 }
373 }
374
375 &.is-warning {
376 .ivyforms-input {
377 &.el-input {
378 .el-input__wrapper {
379 border: 1px solid var(--map-status-warning-stroke-0);
380 }
381 }
382 }
383 }
384
385 // Textarea
386 &.ivyforms-form-item-textarea {
387 .ivyforms-textarea {
388 display: flex;
389 flex-direction: column;
390 align-items: flex-start;
391 font-size: 16px;
392 font-style: normal;
393 font-weight: 400;
394 line-height: 22px;
395
396 // Element Textarea
397 .el-textarea__inner {
398 border-radius: var(--Radius-radius-md, 8px);
399 border: 1px solid var(--map-base-dusk-stroke--2);
400 background-color: var(--map-base-dusk-o05);
401 display: flex;
402 align-items: center;
403 gap: var(--Spacing-sm, 8px);
404 align-self: stretch;
405 box-shadow: none;
406 color: var(--map-base-text-0);
407
408 // Hover
409 &:hover {
410 border: 1px solid var(--map-base-dusk-stroke-0);
411 background: var(--map-base-dusk-o05);
412 }
413
414 // Active & Focus
415 &:active,
416 &:focus,
417 &.is-focus,
418 &.is-active {
419 border: 1px solid var(--map-base-brand-stroke-0);
420 background: var(--map-base-dusk-o05);
421 box-shadow: none;
422 }
423 }
424 }
425
426 // Secondary
427 &.is-secondary {
428 .ivyforms-textarea {
429 .el-textarea__inner {
430 // Active & Focus
431 &:active,
432 &:focus,
433 &.is-focus,
434 &.is-active {
435 border: 1px solid var(--map-base-purple-stroke-0);
436 }
437 }
438 }
439 }
440 }
441
442 // Error and warning states
443 &.is-error {
444 .ivyforms-textarea {
445 .el-textarea__inner {
446 border: 1px solid var(--map-status-error-symbol-0) !important;
447 box-shadow: 0 0 0 1px var(--el-color-danger) inset !important;
448 }
449 }
450 }
451
452 &.is-warning {
453 .ivyforms-textarea {
454 .el-textarea__inner {
455 border: 1px solid var(--map-status-warning-stroke-0);
456 }
457 }
458 }
459 }
460 </style>
461