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 / public / fields / NameFormField.vue

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

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