PluginProbe
The Innovative Form Builder – IvyForms / 0.8
The Innovative Form Builder – IvyForms v0.8
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 / public / fields / NameFormField.vue

NameFormField.vue in The Innovative Form Builder – IvyForms 0.8, at frontend/src/views/public/fields/NameFormField.vue

358 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div
3 :class="[
4 'ivyforms-field',
5 'ivyforms-field__name',
6 'ivyforms-field__name_' + field.id,
7 field.cssClasses,
8 {
9 'main-label-visible': !field.hideLabel,
10 'ivyforms-name-field--is-readonly': !!field.readOnly,
11 },
12 `ivyforms-name-field--label-${effectiveLabelPosition}`,
13 ]"
14 >
15 <div
16 v-if="
17 !field.hideLabel &&
18 (effectiveLabelPosition === 'top' || effectiveLabelPosition === 'default')
19 "
20 class="ivyforms-name-field__label ivyforms-gap-4 ivyforms-align-items-center ivyforms-mb-8 regular-16"
21 >
22 {{ field.label }}
23 </div>
24
25 <div
26 class="ivyforms-name-field__container"
27 :class="{
28 'ivyforms-name-field__container--flex-row':
29 effectiveLabelPosition === 'left' || effectiveLabelPosition === 'right',
30 'ivyforms-name-field__container--flex-column':
31 effectiveLabelPosition === 'top' || effectiveLabelPosition === 'default',
32 }"
33 >
34 <div
35 v-if="!field.hideLabel && effectiveLabelPosition === 'left'"
36 class="ivyforms-name-field__label ivyforms-name-field__label--left ivyforms-gap-4 ivyforms-align-items-center regular-16"
37 >
38 {{ field.label }}
39 </div>
40
41 <!-- Fields layout -->
42 <div
43 class="ivyforms-name-field__layout ivyforms-width-100 ivyforms-gap-16"
44 :class="{
45 'ivyforms-name-field__layout--with-left-label': effectiveLabelPosition === 'left',
46 'ivyforms-name-field__layout--with-right-label': effectiveLabelPosition === 'right',
47 }"
48 :style="gridStyle"
49 >
50 <div
51 v-for="(fieldData, idx) in nameFields"
52 :key="fieldData.fieldID"
53 class="ivyforms-name-field__layout__field-wrapper"
54 >
55 <IvyFormItem
56 :required="isSubRequired(idx)"
57 :prop="`${field.type}_${field.fieldIndex}.${getTypeByIndex(idx)}`"
58 :error="getSubfieldError(getTypeByIndex(idx))"
59 :show-info="!!getSubDescription(idx)"
60 :info-text="getSubDescription(idx) || ''"
61 :show-info-icon="false"
62 >
63 <template #label>
64 <div
65 class="ivyforms-name-field__layout__field-wrapper__sublabel ivyforms-align-items-center ivyforms-gap-4 medium-14"
66 >
67 <span v-if="!fieldData.hide">{{ fieldData.label }}</span>
68 </div>
69 </template>
70 <IvyTextInput
71 :id="fieldData.fieldID"
72 :model-value="getFieldValue(getTypeByIndex(idx))"
73 class="ivyforms-name-field__name-input"
74 type="text"
75 :placeholder="fieldData.placeholder"
76 :disabled="isDisabled"
77 :required="isSubRequired(idx)"
78 :aria-required="isSubRequired(idx) ? 'true' : 'false'"
79 @input="updateFieldValue(getTypeByIndex(idx), $event)"
80 />
81 </IvyFormItem>
82 </div>
83 </div>
84
85 <div
86 v-if="!field.hideLabel && effectiveLabelPosition === 'right'"
87 class="ivyforms-name-field__label ivyforms-name-field__label--right ivyforms-gap-4 ivyforms-align-items-center regular-16"
88 >
89 {{ field.label }}
90 </div>
91 </div>
92 </div>
93 </template>
94
95 <script setup lang="ts">
96 import { computed, inject, onMounted, onUnmounted, watch } from 'vue'
97 import { useAutofillDetection, type AutofillField } from '@/composables/useAutofillDetection'
98 import type { Field, NameFieldValue } from '@/types/field'
99
100 interface FieldProps {
101 modelValue: NameFieldValue
102 field: Field
103 disabled?: boolean
104 error?: string
105 fieldErrors?: Record<string, string>
106 }
107
108 const props = withDefaults(defineProps<FieldProps>(), {
109 modelValue: () => ({}),
110 disabled: false,
111 error: '',
112 fieldErrors: () => ({}),
113 })
114 const emit = defineEmits(['update:modelValue'])
115
116 // Inject error clearer from parent form
117 const clearFieldError = inject<(fieldKey: string) => void>('clearFieldError', () => {})
118
119 // Disable input if component disabled OR field.readOnly is set
120 const isDisabled = computed(() => !!props.disabled || !!props.field.readOnly)
121
122 // Computed property for effective label position
123 const effectiveLabelPosition = computed(() => {
124 const position = props.field?.labelPosition
125 // If position is 'default' or not set, use 'top' as the actual position
126 if (position === 'default' || !position) {
127 return 'top'
128 }
129 return position
130 })
131
132 // Get specific error for a subfield
133 const getSubfieldError = (fieldType: string): string => {
134 const subKey = `${props.field.type}_${props.field.fieldIndex}.${fieldType}`
135 return props.fieldErrors?.[subKey] || ''
136 }
137
138 const nameFieldTypes = computed(() => props.field.nameFieldTypes || ['nameField1', 'nameField2'])
139
140 const nameFields = computed(() => {
141 return nameFieldTypes.value.map((fieldType) => {
142 // Raw label without i18n fallback, so cleared sublabels are treated as hidden
143 const rawLabel = ((props.field as Record<string, string>)[`${fieldType}Label`] ?? '') as string
144 const isCleared = rawLabel.trim() === ''
145 const explicitHide = (props.field as Record<string, boolean>)[`${fieldType}HideLabel`] || false
146 const hide = explicitHide || isCleared
147 return {
148 label: rawLabel, // no fallback; empty means hidden via `hide`
149 placeholder: (props.field as Record<string, string>)[`${fieldType}Placeholder`] || '',
150 fieldID: `ivyforms-field__name-${fieldType}_${(props.field.formId || '').toString()}_${(props.field.fieldIndex || '').toString()}`,
151 hide,
152 }
153 })
154 })
155
156 const getTypeByIndex = (idx: number): string => nameFieldTypes.value[idx] || ''
157 const isSubRequired = (idx: number): boolean => {
158 const t = getTypeByIndex(idx)
159 return !!(props.field as Record<string, unknown>)[`${t}Required`]
160 }
161
162 // Subfield description helper (per sub)
163 const getSubDescription = (idx: number): string => {
164 const t = getTypeByIndex(idx)
165 const val = (props.field as Record<string, unknown>)[`${t}Description`]
166 return typeof val === 'string' ? val : ''
167 }
168
169 // Single row grid with equal columns for 1–5 subfields
170 const gridStyle = computed(() => {
171 const count = Math.max(1, Math.min(5, nameFields.value.length || 0))
172 return { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }
173 })
174
175 const getFieldValue = (fieldType: string) => props.modelValue?.[fieldType] || ''
176
177 const updateFieldValue = (fieldType: string, value: string) => {
178 const newModelValue: NameFieldValue = { ...props.modelValue, [fieldType]: value }
179 emit('update:modelValue', newModelValue)
180
181 // Clear specific subfield error immediately on input (e.g., after submit validation)
182 const subKey = `${props.field.type}_${props.field.fieldIndex}.${fieldType}`
183 clearFieldError(subKey)
184 }
185
186 // Setup autofill detection using the composable
187 const autofillFields = computed<AutofillField[]>(() => {
188 return nameFields.value.map((fieldData, idx) => ({
189 id: fieldData.fieldID,
190 getValue: () => getFieldValue(getTypeByIndex(idx)),
191 setValue: (value: string) => updateFieldValue(getTypeByIndex(idx), value),
192 onClearError: () => {
193 const subKey = `${props.field.type}_${props.field.fieldIndex}.${getTypeByIndex(idx)}`
194 clearFieldError(subKey)
195 },
196 }))
197 })
198
199 const { restartDetection, checkForInitialAutofill } = useAutofillDetection({
200 fields: autofillFields.value,
201 pollingDuration: 7000, // Increased for slow autofill
202 pollingInterval: 100,
203 initialCheckDelay: 100, // Faster initial check
204 })
205
206 // Manual check on mount and when fields become available
207 onMounted(() => {
208 // Initial check immediately after mount
209 setTimeout(checkForInitialAutofill, 50)
210 // Additional checks for delayed autofill
211 setTimeout(checkForInitialAutofill, 300)
212 setTimeout(checkForInitialAutofill, 1000)
213 })
214
215 // Watch for field changes and restart detection
216 watch(
217 () => nameFields.value.map((f) => f.fieldID),
218 () => {
219 setTimeout(restartDetection, 100)
220 },
221 { flush: 'post' },
222 )
223
224 // Watch for modelValue changes (especially form resets)
225 watch(
226 () => props.modelValue,
227 (newValue, oldValue) => {
228 // If form was reset (empty object) or significant change, restart autofill detection
229 const isReset = Object.keys(newValue || {}).length === 0
230 const wasPopulated = oldValue && Object.keys(oldValue).length > 0
231
232 if (isReset && wasPopulated) {
233 setTimeout(restartDetection, 200)
234 }
235 },
236 { deep: true },
237 )
238
239 onUnmounted(() => {})
240 </script>
241
242 <style lang="scss" scoped>
243 :deep(.ivyforms-form-item .el-form-item .el-form-item__label) {
244 justify-content: flex-start !important;
245 }
246
247 .ivyforms-name-field {
248 &--is-readonly {
249 opacity: 0.7;
250 // Prevent hover/focus border changes when read-only
251 :deep(.el-input__wrapper),
252 :deep(.el-input__wrapper:hover),
253 :deep(.el-input__wrapper.is-focus) {
254 border-color: var(--map-base-dusk-stroke--2) !important;
255 box-shadow: none !important;
256 }
257 :deep(input[disabled]) {
258 cursor: not-allowed;
259 }
260 }
261
262 &__label {
263 color: var(--map-base-text-0);
264 display: inline-flex;
265
266 // Left positioning
267 &--left {
268 flex-shrink: 0;
269 margin-right: 8px;
270 margin-bottom: 0;
271 min-width: 100px;
272 display: flex;
273 align-items: center;
274 }
275
276 // Right positioning
277 &--right {
278 flex-shrink: 0;
279 margin-left: 8px;
280 margin-bottom: 0;
281 min-width: 100px;
282 display: flex;
283 align-items: center;
284 }
285 }
286
287 &__container {
288 display: flex;
289 width: 100%;
290
291 &--flex-row {
292 flex-direction: row;
293 align-items: flex-start;
294 gap: 0;
295 }
296
297 &--flex-column {
298 flex-direction: column;
299 }
300 }
301
302 &__layout {
303 display: grid;
304 flex: 1;
305
306 &--with-left-label,
307 &--with-right-label {
308 margin-bottom: 0;
309 }
310
311 &__field-wrapper {
312 min-width: 0;
313
314 &__sublabel {
315 display: inline-flex;
316 color: var(--map-base-text-0);
317 height: 20px;
318 span {
319 white-space: nowrap;
320 }
321
322 &__asterisk {
323 color: var(--map-status-error-fill-0);
324 }
325 }
326 }
327 }
328
329 // Specific positioning styles
330 &--label-left {
331 .ivyforms-name-field__container {
332 align-items: flex-start;
333 }
334
335 .ivyforms-name-field__layout {
336 flex: 1;
337 }
338 }
339
340 &--label-right {
341 .ivyforms-name-field__container {
342 align-items: flex-start;
343 }
344
345 .ivyforms-name-field__layout {
346 flex: 1;
347 }
348 }
349
350 &--label-top,
351 &--label-default {
352 .ivyforms-name-field__layout {
353 width: 100%;
354 }
355 }
356 }
357 </style>
358