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

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

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