PluginProbe
The Innovative Form Builder – IvyForms / 0.6.1.1
The Innovative Form Builder – IvyForms v0.6.1.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 / IvyTextInput.vue

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

460 lines 10.9 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 // Restart autofill detection after a brief delay
176 setTimeout(() => {
177 setupAutofillDetection()
178 }, 200)
179 }
180 },
181 )
182
183 onMounted(() => {
184 // Set up autofill detection after component is mounted and Element Plus is ready
185 setTimeout(() => {
186 cleanupAutofill = setupAutofillDetection()
187 }, 100)
188 })
189
190 onUnmounted(() => {
191 if (cleanupAutofill) {
192 cleanupAutofill()
193 }
194 })
195
196 // Sync localModelValue with props.modelValue
197 watch(
198 () => props.modelValue,
199 (newValue) => {
200 localModelValue.value = newValue
201 },
202 )
203
204 // Emit update:modelValue when localModelValue changes
205 watch(localModelValue, (newValue) => {
206 emit('update:modelValue', newValue)
207 })
208
209 const clearInput = () => {
210 localModelValue.value = ''
211 }
212
213 const focus = () => {
214 elInputRef.value?.focus()
215 }
216
217 defineExpose({
218 focus,
219 })
220 </script>
221
222 <style lang="scss">
223 // Input
224 .ivyforms-input {
225 &.el-input {
226 height: 40px;
227
228 .el-input__wrapper {
229 display: flex;
230 align-items: center;
231 gap: var(--Spacing-sm, 8px);
232 padding: var(--Spacing-sm, 4px) var(--Spacing-lg, 12px);
233 align-self: stretch;
234 border-radius: var(--Radius-radius-md, 8px);
235 border: 1px solid var(--map-base-dusk-stroke--2);
236 background: var(--map-base-dusk-o05);
237 box-shadow: none !important;
238 outline: none !important;
239 transition: none !important;
240 box-sizing: border-box;
241
242 .el-input__prefix-inner > :last-child {
243 margin-right: 0;
244 }
245
246 // Hover
247 &:hover,
248 &.is-hover {
249 border: 1px solid var(--map-base-dusk-stroke-0);
250 background: var(--map-base-dusk-o05);
251 }
252
253 // Inner input
254 input {
255 padding: 0 !important;
256 margin: 0 !important;
257 font-size: 16px;
258 font-style: normal;
259 font-weight: 400;
260 line-height: 22px;
261 color: var(--map-base-text-0);
262 border: none !important;
263 box-shadow: none !important;
264 background-color: transparent;
265 display: flex;
266 align-items: center;
267 gap: 4px;
268 flex: 1 0 0;
269 outline: none !important;
270 box-sizing: border-box;
271 }
272
273 // Active & Focus
274 &:active,
275 &:focus,
276 &.is-focus,
277 &.is-active {
278 border: 1px solid var(--map-base-brand-stroke-0);
279 background: var(--map-base-dusk-o05);
280 box-shadow: none !important;
281 outline: none !important;
282 padding: var(--Spacing-sm, 4px) var(--Spacing-lg, 12px) !important;
283 }
284
285 // Clear Icon
286 .ivyforms-input__clear {
287 cursor: pointer;
288 width: 20px;
289 height: 20px;
290 transform: scale(1);
291 margin: 0;
292 }
293
294 .el-input__clear {
295 display: none !important;
296
297 &::before {
298 display: none !important;
299 }
300 }
301 }
302
303 // Disabled state
304 &.is-disabled {
305 .el-input__wrapper {
306 background: var(--map-base-dusk-o05);
307 border-color: var(--map-base-dusk-stroke--2);
308 box-shadow: none;
309
310 input {
311 color: var(--map-base-text-0);
312 opacity: 0.5;
313 }
314 }
315 }
316
317 // Secondary
318 &.is-secondary {
319 .el-input__wrapper {
320 // Active & Focus
321 &:active,
322 &:focus,
323 &.is-focus,
324 &.is-active {
325 border: 1px solid var(--map-base-purple-stroke-0);
326 }
327 }
328 }
329 }
330 }
331
332 // Form Item
333 .el-form-item {
334 &.ivyforms-form-item-input {
335 // Secondary
336 &.is-secondary {
337 .ivyforms-input {
338 &.el-input {
339 .el-input__wrapper {
340 // Active & Focus
341 &:active,
342 &:focus,
343 &.is-focus,
344 &.is-active {
345 border: 1px solid var(--map-base-purple-stroke-0);
346 }
347 }
348 }
349 }
350 }
351 }
352
353 // Error and warning states
354 &.is-error {
355 .ivyforms-input {
356 &.el-input {
357 .el-input__wrapper {
358 border: 1px solid var(--map-status-error-symbol-0) !important;
359 box-shadow: none !important;
360 }
361
362 .el-input__wrapper:hover,
363 .el-input__wrapper:focus,
364 .el-input__wrapper:active,
365 .el-input__wrapper.is-focus,
366 .el-input__wrapper.is-active {
367 border: 1px solid var(--map-status-error-symbol-0) !important;
368 box-shadow: none !important;
369 }
370 }
371 }
372 }
373
374 &.is-warning {
375 .ivyforms-input {
376 &.el-input {
377 .el-input__wrapper {
378 border: 1px solid var(--map-status-warning-stroke-0);
379 }
380 }
381 }
382 }
383
384 // Textarea
385 &.ivyforms-form-item-textarea {
386 .ivyforms-textarea {
387 display: flex;
388 flex-direction: column;
389 align-items: flex-start;
390 font-size: 16px;
391 font-style: normal;
392 font-weight: 400;
393 line-height: 22px;
394
395 // Element Textarea
396 .el-textarea__inner {
397 border-radius: var(--Radius-radius-md, 8px);
398 border: 1px solid var(--map-base-dusk-stroke--2);
399 background-color: var(--map-base-dusk-o05);
400 display: flex;
401 align-items: center;
402 gap: var(--Spacing-sm, 8px);
403 align-self: stretch;
404 box-shadow: none;
405 color: var(--map-base-text-0);
406
407 // Hover
408 &:hover {
409 border: 1px solid var(--map-base-dusk-stroke-0);
410 background: var(--map-base-dusk-o05);
411 }
412
413 // Active & Focus
414 &:active,
415 &:focus,
416 &.is-focus,
417 &.is-active {
418 border: 1px solid var(--map-base-brand-stroke-0);
419 background: var(--map-base-dusk-o05);
420 box-shadow: none;
421 }
422 }
423 }
424
425 // Secondary
426 &.is-secondary {
427 .ivyforms-textarea {
428 .el-textarea__inner {
429 // Active & Focus
430 &:active,
431 &:focus,
432 &.is-focus,
433 &.is-active {
434 border: 1px solid var(--map-base-purple-stroke-0);
435 }
436 }
437 }
438 }
439 }
440
441 // Error and warning states
442 &.is-error {
443 .ivyforms-textarea {
444 .el-textarea__inner {
445 border: 1px solid var(--map-status-error-symbol-0) !important;
446 box-shadow: 0 0 0 1px var(--el-color-danger) inset !important;
447 }
448 }
449 }
450
451 &.is-warning {
452 .ivyforms-textarea {
453 .el-textarea__inner {
454 border: 1px solid var(--map-status-warning-stroke-0);
455 }
456 }
457 }
458 }
459 </style>
460