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

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

1,474 lines 54.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div
3 ref="formWrapperRef"
4 :class="['ivyforms-form-wrapper', 'ivyforms-form-wrapper-' + formData.id]"
5 >
6 <div v-if="showSuccessMessage" class="ivyforms-success-message">
7 {{ successMessage || getLabel('thank_you') }}
8 </div>
9 <!-- Form title-->
10 <template v-else>
11 <div v-if="showSuccessMessageReset" class="ivyforms-success-message ivyforms-mb-24">
12 {{ successMessage || getLabel('thank_you') }}
13 </div>
14
15 <h2
16 v-if="formData.showTitle"
17 :class="['ivyforms-form-title', 'ivyforms-form-title-' + formData.id]"
18 >
19 {{ formData.name }}
20 </h2>
21 <!-- Form description-->
22 <p
23 v-if="formData.showDescription"
24 :class="['ivyforms-form-description', 'ivyforms-form-description-' + formData.id]"
25 >
26 {{ formData.description }}
27 </p>
28
29 <IvyForm
30 :id="'ivyforms-form-' + formData.id"
31 ref="formRef"
32 :class="'ivyforms-form ivyforms-form-' + formData.id"
33 :data-form-id="formData.id"
34 :rules="formRules"
35 :model="formValues"
36 :label-position="getFormLabelPosition()"
37 @submit.prevent="submitForm"
38 >
39 <template v-for="field in formFields" :key="field.type + '_' + field.fieldIndex">
40 <component
41 :is="formFieldsTemplates[field.type]"
42 ref="formFieldsCollectorRefs"
43 v-model="formValues[field.type + '_' + field.fieldIndex]"
44 :field="field"
45 :error="fieldErrors[field.type + '_' + field.fieldIndex]"
46 :field-errors="fieldErrors"
47 :label-position="getFormLabelPosition()"
48 ></component>
49 </template>
50 <!-- Add hidden inputs fields for referer, post id ... -->
51 <div class="ivyforms-form-submit">
52 <IvyButtonAction
53 :id="'ivyforms-form-submit-button-' + formData.id"
54 priority="tertiary"
55 type="border"
56 @click.prevent.stop="validateAndSubmit"
57 >
58 {{ getLabel('submit') }}
59 </IvyButtonAction>
60 </div>
61 </IvyForm>
62 </template>
63 </div>
64 </template>
65
66 <script setup lang="ts">
67 import {
68 type Component,
69 type ComponentPublicInstance,
70 markRaw,
71 nextTick,
72 onMounted,
73 provide,
74 reactive,
75 ref,
76 watch,
77 } from 'vue'
78 import IvyForm from '@/views/_components/form/IvyForm.vue'
79 import IvyButtonAction from '@/views/_components/button/IvyButtonAction.vue'
80 import { useApiClient } from '@/composables/useApiClient'
81 import { useWcagColors } from '@/composables/useWcagColors'
82 // * Form Field templates
83 import IvyTextFormField from '../public/fields/TextFormField.vue'
84 import IvyParagraphFormField from '../public/fields/ParagraphFormField.vue'
85 import IvyEmailFormField from '../public/fields/EmailFormField.vue'
86 import IvyNumberFormField from '../public/fields/NumberFormField.vue'
87 import IvyPhoneFormField from '../public/fields/PhoneFormField.vue'
88 import IvyWebsiteFormField from '../public/fields/WebsiteFormField.vue'
89 import IvyRadioFormField from '../public/fields/RadioFormField.vue'
90 import NameFormField from '../public/fields/NameFormField.vue'
91 import RecaptchaFormField from '../public/fields/RecaptchaFormField.vue'
92 import IvyAddressFormField from '../public/fields/AddressFormField.vue'
93 import CheckboxFormField from '../public/fields/CheckboxFormField.vue'
94 import TimeFormField from '../public/fields/TimeFormField.vue'
95 import type { Field } from '@/types/field'
96 import type { Confirmation } from '@/types/confirmations'
97 import { useLabels } from '@/composables/useLabels'
98 import IvyMessage from '@/views/_components/message/ivyMessage.ts'
99 import utilRedirect from '@/utils/utilRedirects'
100 import { useRouter } from 'vue-router'
101 import SelectFormField from '@/views/public/fields/SelectFormField.vue'
102
103 // Initialize WCAG color system for public forms
104 const { startWatching } = useWcagColors()
105 startWatching()
106
107 // Global templates storage and hook setup
108 const globalTemplatesStore: Record<string, Component> = {}
109
110 // Hook to allow Pro plugin to register additional field components
111 const registerFieldTemplateGlobal = (fieldType: string, component: Component) => {
112 if (component && fieldType) {
113 const wrappedComponent = markRaw(component)
114 globalTemplatesStore[fieldType] = wrappedComponent
115
116 // Also update the reactive templates if available
117 const w = window as Window & {
118 _updateFormTemplates?: (fieldType: string, component: Component) => void
119 }
120 if (w._updateFormTemplates) {
121 w._updateFormTemplates(fieldType, wrappedComponent)
122 }
123
124 // Trigger a global event to notify any listening components
125 if (typeof window !== 'undefined') {
126 window.dispatchEvent(
127 new CustomEvent('ivyforms:template_registered', {
128 detail: { fieldType, component: wrappedComponent },
129 }),
130 )
131 }
132 }
133 }
134
135 // Set up global hook immediately
136 if (typeof window !== 'undefined') {
137 const w = window as Window & {
138 IvyForms?: {
139 api?: {
140 registerFieldTemplate?: (fieldType: string, component: Component) => void
141 fields?: {
142 registerPublic?: (fieldType: string, component: Component) => void
143 }
144 hooks?: {
145 registerFieldInit?: (
146 callback: (
147 field: Field,
148 context: {
149 formValues: Record<string, unknown>
150 formRules: Record<string, unknown>
151 getLabel: (key: string) => string
152 fieldErrors: Record<string, unknown>
153 },
154 ) => void,
155 ) => void
156 }
157 }
158 hooks?: {
159 fieldInit?: Array<
160 (
161 field: Field,
162 context: {
163 formValues: Record<string, unknown>
164 formRules: Record<string, unknown>
165 getLabel: (key: string) => string
166 fieldErrors: Record<string, unknown>
167 },
168 ) => void
169 >
170 }
171 _publicFieldQueue?: Array<{ fieldType: string; component: Component }>
172 }
173 }
174 // Initialize IvyForms globals with proper structure (types defined in global.d.ts)
175 // Note: Only minimal public API is initialized here, full API is in admin context
176 // These objects are populated dynamically at runtime, so we cast through 'unknown'
177 // which is type-safe since they will be fully initialized before use
178 if (!w.IvyForms) {
179 w.IvyForms = {
180 api: {
181 fields: { admin: {}, public: {} },
182 hooks: { actions: {}, filters: {} },
183 fieldOptions: { registry: {} },
184 },
185 hooks: {
186 actions: {},
187 filters: {},
188 fieldInit: [],
189 },
190 fields: {},
191 } as unknown as typeof w.IvyForms
192 }
193 if (!w.IvyForms.api) {
194 w.IvyForms.api = {
195 fields: { admin: {}, public: {} },
196 hooks: { actions: {}, filters: {} },
197 fieldOptions: { registry: {} },
198 } as unknown as typeof w.IvyForms.api
199 }
200 if (!w.IvyForms.api.fields) {
201 w.IvyForms.api.fields = {
202 admin: {},
203 public: {},
204 } as unknown as typeof w.IvyForms.api.fields
205 }
206
207 // Make it available globally
208 w.IvyForms.api.fields.registerPublic = registerFieldTemplateGlobal
209
210 // Process any queued registrations
211 const queue = w.IvyForms._publicFieldQueue || []
212 queue.forEach(({ fieldType, component }) => registerFieldTemplateGlobal(fieldType, component))
213
214 // Set up hooks system
215 if (!w.IvyForms.hooks) {
216 w.IvyForms.hooks = {
217 actions: {},
218 filters: {},
219 fieldInit: [],
220 } as unknown as typeof w.IvyForms.hooks
221 }
222 if (!w.IvyForms.hooks.fieldInit) {
223 w.IvyForms.hooks.fieldInit = []
224 }
225
226 // Add API to register field initialization hooks
227 if (!w.IvyForms.api.hooks) {
228 w.IvyForms.api.hooks = {
229 actions: {},
230 filters: {},
231 } as unknown as typeof w.IvyForms.api.hooks
232 }
233 w.IvyForms.api.hooks.registerFieldInit = (
234 callback: (
235 field: Field,
236 context: {
237 formValues: Record<string, unknown>
238 formRules: Record<string, unknown>
239 getLabel: (key: string) => string
240 fieldErrors: Record<string, unknown>
241 },
242 ) => void,
243 ) => {
244 if (typeof callback === 'function') {
245 w.IvyForms!.hooks!.fieldInit!.push(callback)
246 }
247 }
248 }
249
250 interface RecaptchaFieldComponent extends ComponentPublicInstance {
251 $props: {
252 field: {
253 type: string
254 fieldIndex: number
255 }
256 }
257 executeRecaptcha?: () => Promise<string>
258 }
259
260 const { getLabel } = useLabels()
261 const { request } = useApiClient()
262 const { redirectToUrl } = utilRedirect()
263 const router = useRouter()
264 const skipValidation = ref(false)
265
266 // Helper: normalize backend fields for preview (merge Name children into parent)
267 function normalizePreviewFields(rawFields: Field[]): Field[] {
268 if (!Array.isArray(rawFields) || rawFields.length === 0) return []
269
270 // Preserve original order index to stably order children without subFieldIndex
271 const apiOrder = new Map<Field, number>()
272 rawFields.forEach((f, i) => apiOrder.set(f, i))
273
274 const isChild = (f: Field) => f.parentId !== undefined && f.parentId !== null
275 const parents = rawFields.filter((f) => !isChild(f))
276
277 const result: Field[] = []
278
279 for (const parent of parents) {
280 // Collect children by matching parentId to parent.id or parent.fieldIndex
281 const children = rawFields.filter(
282 (c) =>
283 isChild(c) &&
284 (c.parentId === (parent as Field).id || c.parentId === (parent as Field).fieldIndex),
285 )
286
287 // Propagate common settings to top-level field if provided
288 const pSettings = (parent.settings || {}) as Record<string, unknown>
289 if (pSettings) {
290 if (parent.hideLabel === undefined && typeof pSettings.hideLabel !== 'undefined') {
291 ;(parent as Record<string, unknown>).hideLabel = !!pSettings.hideLabel
292 }
293 if (parent.readOnly === undefined && typeof pSettings.readOnly !== 'undefined') {
294 ;(parent as Record<string, unknown>).readOnly = !!(pSettings.readOnly as boolean)
295 }
296 if (
297 (parent as Record<string, unknown>).description === undefined &&
298 typeof pSettings.description !== 'undefined'
299 ) {
300 ;(parent as Record<string, unknown>).description = String(pSettings.description || '')
301 }
302 if (
303 (parent as Record<string, unknown>).cssClasses === undefined &&
304 typeof pSettings.cssClasses !== 'undefined'
305 ) {
306 ;(parent as Record<string, unknown>).cssClasses = String(pSettings.cssClasses || '')
307 }
308 }
309
310 // Merge children for name and address fields
311 if (children.length >= 2 || parent.type === 'name' || parent.type === 'address') {
312 // Sort children: first those with explicit subFieldIndex, then the rest by API order
313 const ordered = [...children].sort((a, b) => {
314 const as = ((a.settings || {}) as Record<string, unknown>).subFieldIndex
315 const bs = ((b.settings || {}) as Record<string, unknown>).subFieldIndex
316 const aHas = typeof as === 'number'
317 const bHas = typeof bs === 'number'
318 if (aHas && bHas) return (as as number) - (bs as number)
319 if (aHas) return -1
320 if (bHas) return 1
321 return (apiOrder.get(a) || 0) - (apiOrder.get(b) || 0)
322 })
323
324 if (parent.type === 'name') {
325 // Merge as name field
326 const nameFieldTypes: string[] = []
327 ordered.forEach((child, idx) => {
328 const key = `nameField${idx + 1}`
329 nameFieldTypes.push(key)
330 const cs = (child.settings || {}) as Record<string, unknown>
331 const prec = parent as unknown as Record<string, unknown>
332 prec[`${key}Label`] = (child.label as unknown) || ''
333 prec[`${key}Placeholder`] = (child.placeholder as unknown) || ''
334 prec[`${key}Required`] = !!child.required
335 // Required message: prefer settings.requiredMessage, fallback to legacy top-level
336 if (typeof cs.requiredMessage !== 'undefined') {
337 prec[`${key}RequiredMessage`] = cs.requiredMessage
338 } else if (typeof (child as Record<string, unknown>).requiredMessage !== 'undefined') {
339 prec[`${key}RequiredMessage`] = (child as Record<string, unknown>).requiredMessage
340 }
341 // Default value
342 if (typeof child.defaultValue !== 'undefined') {
343 prec[`${key}Value`] = child.defaultValue as unknown
344 }
345 // Hide sublabel (support both settings.hideLabel and legacy child.hideLabel)
346 const childHasSettingsHide = typeof cs.hideLabel !== 'undefined'
347 const childHasTopLevelHide =
348 typeof (child as Record<string, unknown>).hideLabel !== 'undefined'
349 if (childHasSettingsHide || childHasTopLevelHide) {
350 const childHideLabel = childHasSettingsHide
351 ? !!(cs.hideLabel as boolean)
352 : !!((child as Record<string, unknown>).hideLabel as boolean)
353 prec[`${key}HideLabel`] = childHideLabel
354 }
355 // Subfield description
356 if (typeof (child as Record<string, unknown>).description !== 'undefined') {
357 prec[`${key}Description`] = String(
358 ((child as Record<string, unknown>).description as string) || '',
359 )
360 }
361 // Visibility
362 if (typeof (child as Record<string, unknown>).description !== 'undefined') {
363 prec[`${key}Visible`] = String(
364 ((child as Record<string, unknown>).visible as boolean) || true,
365 )
366 }
367 })
368 ;(parent as Record<string, unknown>).nameFieldTypes = nameFieldTypes
369 parent.type = 'name'
370 } else if (parent.type === 'address') {
371 // Merge as address field
372 // Use canonical order for address fields (must match IvyAddressFormField.vue and builder)
373 const canonicalOrder = ['streetAddress', 'addressLine2', 'city', 'state', 'zip', 'country']
374 const addressFieldTypes = []
375 // If there are NO children, still populate all 6 subfields on the parent
376 if (ordered.length === 0) {
377 canonicalOrder.forEach((fieldType) => {
378 addressFieldTypes.push(fieldType)
379 const prec = parent as unknown as Record<string, unknown>
380 prec[`${fieldType}Label`] =
381 (parent as Record<string, unknown>)[`${fieldType}Label`] || ''
382 prec[`${fieldType}Placeholder`] =
383 (parent as Record<string, unknown>)[`${fieldType}Placeholder`] || ''
384 prec[`${fieldType}Required`] =
385 (parent as Record<string, unknown>)[`${fieldType}Required`] || false
386 prec[`${fieldType}RequiredMessage`] =
387 (parent as Record<string, unknown>)[`${fieldType}RequiredMessage`] || ''
388 prec[`${fieldType}Value`] =
389 (parent as Record<string, unknown>)[`${fieldType}Value`] || ''
390 prec[`${fieldType}HideLabel`] =
391 (parent as Record<string, unknown>)[`${fieldType}HideLabel`] || false
392 prec[`${fieldType}Description`] =
393 (parent as Record<string, unknown>)[`${fieldType}Description`] || ''
394 prec[`${fieldType}Visible`] =
395 (parent as Record<string, unknown>)[`${fieldType}Visible`] || true
396 })
397 ;(parent as Record<string, unknown>).addressFieldTypes = canonicalOrder
398 parent.type = 'address'
399 } else {
400 canonicalOrder.forEach((fieldType, idx) => {
401 // Find child by settings.fieldType, or by label (case-insensitive, spaces removed), or fallback to ordered[idx] if exists
402 let child =
403 ordered.find((c) => {
404 const settings = parseSettings((c as unknown as Record<string, unknown>).settings)
405 return (
406 (settings && settings.fieldType === fieldType) ||
407 (c.label && c.label.toLowerCase().replace(/\s/g, '') === fieldType.toLowerCase())
408 )
409 }) || ordered.find((c) => c.type === fieldType)
410 // Only fallback to ordered[idx] if it exists and is not undefined
411 if (!child && Array.isArray(ordered) && ordered.length > idx && ordered[idx]) {
412 child = ordered[idx]
413 }
414 addressFieldTypes.push(fieldType)
415 const cs = child && child.settings ? parseSettings(child.settings) : {}
416 const prec = parent as unknown as Record<string, unknown>
417 prec[`${fieldType}Label`] = child ? (child.label as unknown) || '' : ''
418 prec[`${fieldType}Placeholder`] = child ? (child.placeholder as unknown) || '' : ''
419 prec[`${fieldType}Required`] = child ? !!child.required : false
420 if (child && typeof cs.requiredMessage !== 'undefined') {
421 prec[`${fieldType}RequiredMessage`] = cs.requiredMessage
422 } else if (
423 child &&
424 typeof (child as Record<string, unknown>).requiredMessage !== 'undefined'
425 ) {
426 prec[`${fieldType}RequiredMessage`] = (
427 child as Record<string, unknown>
428 ).requiredMessage
429 } else {
430 prec[`${fieldType}RequiredMessage`] = ''
431 }
432 if (child && typeof child.defaultValue !== 'undefined') {
433 prec[`${fieldType}Value`] = child.defaultValue as unknown
434 } else {
435 prec[`${fieldType}Value`] = ''
436 }
437 const childHasSettingsHide = child && typeof cs.hideLabel !== 'undefined'
438 const childHasTopLevelHide =
439 child && typeof (child as Record<string, unknown>).hideLabel !== 'undefined'
440 if (childHasSettingsHide || childHasTopLevelHide) {
441 const childHideLabel = childHasSettingsHide
442 ? !!(cs.hideLabel as boolean)
443 : !!((child as Record<string, unknown>).hideLabel as boolean)
444 prec[`${fieldType}HideLabel`] = childHideLabel
445 } else {
446 prec[`${fieldType}HideLabel`] = false
447 }
448 if (child && typeof (child as Record<string, unknown>).description !== 'undefined') {
449 prec[`${fieldType}Description`] = String(
450 ((child as Record<string, unknown>).description as string) || '',
451 )
452 } else {
453 prec[`${fieldType}Description`] = ''
454 }
455 if (child && typeof (child as Record<string, unknown>).visible !== 'undefined') {
456 prec[`${fieldType}Visible`] = !!((child as Record<string, unknown>)
457 .visible as boolean)
458 } else {
459 prec[`${fieldType}Visible`] = true
460 }
461 })
462 ;(parent as Record<string, unknown>).addressFieldTypes = canonicalOrder
463 parent.type = 'address'
464 }
465 }
466 }
467
468 result.push(parent)
469 }
470
471 return result
472 }
473
474 // Safely parse settings which may be stored as JSON string or as an object
475 const parseSettings = (s: unknown): Record<string, unknown> => {
476 if (!s) return {}
477 if (typeof s === 'string') {
478 try {
479 return JSON.parse(s) as Record<string, unknown>
480 } catch {
481 return {}
482 }
483 }
484 if (typeof s === 'object') {
485 return s as Record<string, unknown>
486 }
487 return {}
488 }
489
490 // Form data
491 const formFields = ref([])
492 const formValues = ref({})
493 const formRules = ref({})
494 const fieldErrors = ref({})
495 const isLoading = ref(true)
496 const formRef = ref(null)
497 const formWrapperRef = ref(null)
498 const formFieldsCollectorRefs = ref([])
499 const formNonce = ref(null)
500 const formConfirmation = ref([])
501 const showSuccessMessage = ref(false)
502 const showSuccessMessageReset = ref(false)
503 const successMessage = ref('')
504 const postId = ref('')
505 const refererUrl = ref('')
506
507 interface FormData {
508 id: string | number
509 name?: string
510 description?: string
511 showTitle?: boolean
512 showDescription?: boolean
513 storeEntries?: boolean
514 defaultValue?: string
515 counter?: number
516 fields?: Field[]
517 confirmations?: Confirmation[]
518 }
519
520 // Define the shape of entries provided by the global window variable
521 interface GlobalFormEntry {
522 id: string | number
523 form: FormData
524 nonce: string
525 fields: Field[]
526 confirmations: Confirmation[]
527 }
528
529 // Update your refs with proper typing
530 const formData = ref<FormData>({
531 id: '',
532 name: '',
533 description: '',
534 showTitle: true,
535 showDescription: false,
536 storeEntries: true,
537 defaultValue: '',
538 counter: 0,
539 fields: [],
540 confirmations: [],
541 })
542 // * Form Fields Object - start with base templates then add global ones
543 const baseTemplates = {
544 text: markRaw(IvyTextFormField),
545 email: markRaw(IvyEmailFormField),
546 textarea: markRaw(IvyParagraphFormField),
547 number: markRaw(IvyNumberFormField),
548 phone: markRaw(IvyPhoneFormField),
549 website: markRaw(IvyWebsiteFormField),
550 time: markRaw(TimeFormField),
551 recaptcha: markRaw(RecaptchaFormField),
552 radio: markRaw(IvyRadioFormField),
553 select: markRaw(SelectFormField),
554 name: markRaw(NameFormField),
555 address: markRaw(IvyAddressFormField),
556 checkbox: markRaw(CheckboxFormField),
557 'multi-select': markRaw(SelectFormField),
558 }
559
560 // Create reactive templates and manually add global store items
561 const formFieldsTemplates = reactive({ ...baseTemplates })
562 // Add any existing global templates
563 Object.entries(globalTemplatesStore).forEach(([fieldType, component]) => {
564 formFieldsTemplates[fieldType] = component
565 })
566 // Force reactivity trigger
567 const templateUpdateTrigger = ref(0)
568 // Set up update function for late registrations
569 if (typeof window !== 'undefined') {
570 ;(
571 window as Window & { _updateFormTemplates?: (fieldType: string, component: Component) => void }
572 )._updateFormTemplates = (fieldType: string, component: Component) => {
573 formFieldsTemplates[fieldType] = component
574 templateUpdateTrigger.value++ // Force Vue reactivity
575 }
576
577 // Listen for template registration events
578 window.addEventListener('ivyforms:template_registered', (event: Event) => {
579 const customEvent = event as CustomEvent<{ fieldType: string; component: Component }>
580 const { fieldType, component } = customEvent.detail
581 formFieldsTemplates[fieldType] = component
582 templateUpdateTrigger.value++ // Force Vue reactivity
583 })
584
585 // Also check if there are already registered templates in global store
586 Object.entries(globalTemplatesStore).forEach(([fieldType, component]) => {
587 formFieldsTemplates[fieldType] = component
588 })
589 }
590
591 // Initialize from global variable
592 onMounted(async () => {
593 const w = window as unknown as { wpIvyFormDataList?: Record<string, GlobalFormEntry> }
594 if (typeof window !== 'undefined' && w.wpIvyFormDataList) {
595 const counter = (formWrapperRef.value as HTMLElement)
596 .closest('.ivyforms-frontend')!
597 .getAttribute('data-counter') as string
598 const formsData = w.wpIvyFormDataList[counter]
599 // Check if formData is an object and has the required properties
600 if (typeof formsData === 'object' && formsData !== null && formsData.id) {
601 // Assign the form data to the reactive variable
602 formData.value = formsData.form
603 formNonce.value = formsData.nonce
604 // Normalize fields for preview
605 formFields.value = normalizePreviewFields(formsData.fields || [])
606 formConfirmation.value = formsData.confirmations || []
607
608 // Wait for Pro plugins to register their hooks before initializing fields
609 // This ensures all extensions can add field-specific initialization logic
610 await new Promise<void>((resolve) => {
611 if (typeof window !== 'undefined') {
612 // Check if Pro is ready
613 const wExt = window as Window & { IvyFormsProReady?: boolean }
614 if (wExt.IvyFormsProReady) {
615 resolve()
616 return
617 }
618
619 // Wait for Pro ready event
620 const handler = () => {
621 resolve()
622 window.removeEventListener('ivyforms:pro_ready', handler)
623 }
624 window.addEventListener('ivyforms:pro_ready', handler, { once: true })
625
626 // Fallback timeout - initialize after 100ms even if Pro doesn't signal ready
627 setTimeout(() => {
628 window.removeEventListener('ivyforms:pro_ready', handler)
629 resolve()
630 }, 100)
631 } else {
632 resolve()
633 }
634 })
635
636 // Initialize form values with defaults or empty strings
637 formFields.value.forEach((field) => {
638 const key = field.type + '_' + field.fieldIndex
639
640 // Special handling for name field type
641 if (field.type === 'name') {
642 const nameValue: Record<string, string> = {}
643 const nameFieldTypes = field.nameFieldTypes || ['nameField1', 'nameField2']
644 // Initialize each dynamic name field
645 nameFieldTypes.forEach((fieldType) => {
646 nameValue[fieldType] = (field as Record<string, string>)[`${fieldType}Value`] || ''
647 })
648 formValues.value[key] = nameValue
649 } else if (field.type === 'address') {
650 // initialize all Address field subfields
651 const addressValue: Record<string, string> = {}
652 const addressFieldTypes = field.addressFieldTypes || [
653 'streetAddress',
654 'addressLine2',
655 'city',
656 'state',
657 'zip',
658 'country',
659 ]
660 addressFieldTypes.forEach((fieldType) => {
661 addressValue[fieldType] = (field as Record<string, string>)[`${fieldType}Value`] || ''
662 })
663 formValues.value[key] = addressValue
664 } else if (field.type === 'checkbox' && Array.isArray(field.fieldOptions)) {
665 const defaultOpts = field.fieldOptions
666 .filter((opt) => opt.isDefault)
667 .map((opt) => opt.value)
668 formValues.value[key] = defaultOpts.length ? defaultOpts : undefined
669 } else if (
670 (field.type === 'radio' || field.type === 'select') &&
671 Array.isArray(field.fieldOptions)
672 ) {
673 const defaultOpt = field.fieldOptions.find((opt) => opt.isDefault)
674 formValues.value[key] = defaultOpt ? defaultOpt.value : ''
675 } else if (field.type === 'multi-select' && Array.isArray(field.fieldOptions)) {
676 const defaultOpts = field.fieldOptions
677 .filter((opt) => opt.isDefault)
678 .map((opt) => opt.value)
679 formValues.value[key] = defaultOpts.length ? defaultOpts : undefined
680 } else if (field.type === 'website') {
681 // Handle website fields with prefix and suffix
682 let websiteValue = field.defaultValue || ''
683 const prefix = field.inputPrefix || ''
684 const suffix = field.inputSuffix || ''
685
686 // If there's a defaultValue, strip any existing prefix/suffix first
687 if (websiteValue && (prefix || suffix)) {
688 // Remove prefix if it exists
689 if (prefix && websiteValue.startsWith(prefix)) {
690 websiteValue = websiteValue.substring(prefix.length)
691 }
692 // Remove suffix if it exists
693 if (suffix && websiteValue.endsWith(suffix)) {
694 websiteValue = websiteValue.substring(0, websiteValue.length - suffix.length)
695 }
696 }
697
698 formValues.value[key] = websiteValue
699 } else {
700 formValues.value[key] = field.defaultValue || ''
701 }
702
703 if (field.type === 'name') {
704 const nameFieldTypes = field.nameFieldTypes || ['nameField1', 'nameField2']
705 nameFieldTypes.forEach((nfType: string) => {
706 const subKey = `${field.type}_${field.fieldIndex}.${nfType}`
707 const label =
708 (field as Record<string, string>)[`${nfType}Label`] ||
709 getLabel(`name_field_${nfType.replace('nameField', '')}`)
710 const rulesForSub: unknown[] = []
711 if ((field as Record<string, unknown>)[`${nfType}Required`]) {
712 const customMsg = (field as Record<string, unknown>)[`${nfType}RequiredMessage`]
713 const message =
714 typeof customMsg === 'string' && (customMsg as string).trim() !== ''
715 ? (customMsg as string)
716 : `${label} ${getLabel('required')}`
717 rulesForSub.push({
718 required: true,
719 message,
720 trigger: ['submit'],
721 })
722 }
723 ;(formRules.value as Record<string, unknown>)[subKey] = rulesForSub
724 })
725 } else if (field.type === 'address') {
726 const addressFieldTypes = field.addressFieldTypes || [
727 'streetAddress',
728 'addressLine2',
729 'city',
730 'state',
731 'zip',
732 'country',
733 ]
734 addressFieldTypes.forEach((afType: string) => {
735 const subKey = `${field.type}_${field.fieldIndex}.${afType}`
736 const label =
737 (field as Record<string, string>)[`${afType}Label`] ||
738 getLabel(`address_field_${afType.replace('addressField', '')}`)
739 const rulesForSub: unknown[] = []
740 if ((field as Record<string, unknown>)[`${afType}Required`]) {
741 const customMsg = (field as Record<string, unknown>)[`${afType}RequiredMessage`]
742 const message =
743 typeof customMsg === 'string' && (customMsg as string).trim() !== ''
744 ? (customMsg as string)
745 : `${label} ${getLabel('required')}`
746 rulesForSub.push({
747 required: true,
748 message,
749 trigger: ['submit'],
750 })
751 }
752 ;(formRules.value as Record<string, unknown>)[subKey] = rulesForSub
753 })
754 } else {
755 const rules = []
756
757 // Required rule (skip password fields - Pro handles them to prevent immediate validation)
758 if (field.required && field.type !== 'password') {
759 rules.push({
760 required: true,
761 message: getRequiredMessage(field),
762 trigger: ['submit'],
763 })
764 }
765
766 // Format validations
767 if (field.type === 'email') {
768 rules.push({
769 validator: (_rule: unknown, value: string, callback: (err?: Error) => void) => {
770 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
771 if (skipValidation.value) {
772 callback()
773 return
774 }
775 if (!value) {
776 if (field.required) {
777 callback(new Error(getLabel('enter_email_address')))
778 } else {
779 callback()
780 }
781 return
782 }
783 if (!emailRegex.test(value)) {
784 callback(new Error(getLabel('enter_email_address')))
785 return
786 }
787 callback()
788 },
789 trigger: ['submit'],
790 })
791 }
792
793 if (field.type === 'phone') {
794 rules.push({
795 pattern: /^[+\d]?(?:[\d\s.-]{7,15})$/,
796 message: getLabel('enter_phone_number'),
797 trigger: ['submit'],
798 })
799 }
800
801 if (field.type === 'website') {
802 rules.push({
803 validator: (_rule: unknown, value: unknown, callback: (err?: Error) => void) => {
804 // Check if field is required and empty
805 if (field.required && (value === '' || value === null || value === undefined)) {
806 return callback(new Error(getRequiredMessage(field)))
807 }
808 if (value === '' || value === null || value === undefined) {
809 return callback()
810 }
811
812 let urlToValidate = String(value)
813
814 // If field has prefix and/or suffix, construct the full URL for validation
815 const prefix = field.inputPrefix || ''
816 const suffix = field.inputSuffix || ''
817
818 if (prefix || suffix) {
819 // User entered only the middle part, construct full URL
820 urlToValidate = prefix + urlToValidate + suffix
821 }
822
823 // Validate the complete URL
824 const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/i
825 if (!urlPattern.test(urlToValidate)) {
826 return callback(new Error(getLabel('enter_valid_website')))
827 }
828
829 callback()
830 },
831 trigger: ['submit'],
832 })
833 }
834
835 if (field.type === 'time') {
836 rules.push({
837 validator: (_: unknown, value: string, callback: (e?: Error) => void) => {
838 // Trim spaces before validation
839 const trimmedValue = typeof value === 'string' ? value.trim() : value
840 const isEmpty = trimmedValue == null || trimmedValue === ''
841 const isPartial = trimmedValue === '__PARTIAL__'
842 let validPattern = false
843 let errorMsg = ''
844
845 // Special handling for dropdown mode - check if all parts are selected
846 if (field.timeFieldType === 'dropdown') {
847 if (field.required && (isEmpty || isPartial)) {
848 return callback(new Error(getRequiredMessage(field)))
849 }
850 if (isPartial) {
851 return callback(new Error(getLabel('enter_valid_time')))
852 }
853 if (!isEmpty) {
854 let hasValidFormat = false
855 if (field.timeFormat === 'ampm') {
856 hasValidFormat = /^(0?[1-9]|1[0-2]):[0-5]\d ?([AP]M|[ap]m)$/.test(
857 trimmedValue,
858 )
859 } else {
860 hasValidFormat = /^(?:[01]?\d|2[0-3]):[0-5]\d$/.test(trimmedValue)
861 }
862 if (!hasValidFormat) {
863 return callback(new Error(getLabel('enter_valid_time')))
864 }
865 }
866 return callback()
867 }
868
869 // For time-picker and input modes, use pattern validation
870 if (field.timeFieldType === 'time-picker') {
871 validPattern =
872 isEmpty ||
873 /^(?:[01]\d|2[0-3]):[0-5]\d(?:\s?(?:AM|PM|am|pm))?$/.test(trimmedValue)
874 errorMsg = getLabel('enter_valid_time')
875 } else if (field.timeFormat === 'ampm') {
876 validPattern = /^(0?[1-9]|1[0-2]):[0-5]\d (AM|PM|am|pm)$/.test(trimmedValue)
877 errorMsg = getLabel('enter_valid_time')
878 } else {
879 validPattern = /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(trimmedValue)
880 errorMsg = getLabel('enter_valid_time')
881 }
882 if (field.required && isEmpty) {
883 return callback(new Error(getRequiredMessage(field)))
884 }
885 if (!isEmpty && !validPattern) {
886 return callback(new Error(errorMsg))
887 }
888 return callback()
889 },
890 trigger: ['submit'],
891 })
892 }
893
894 if (field.type === 'number' && (field.minValue !== null || field.maxValue !== null)) {
895 rules.push({
896 validator: (_rule: unknown, value: unknown, callback: (err?: Error) => void) => {
897 if (value === '' || value === null || value === undefined) {
898 return callback()
899 }
900 const num = Number(value)
901 if (isNaN(num)) {
902 return callback(new Error(getLabel('enter_valid_number')))
903 }
904 const hasMin = field.minValue !== null && field.minValue !== undefined
905 const hasMax = field.maxValue !== null && field.maxValue !== undefined
906 if (hasMin && num < field.minValue) {
907 if (hasMax) {
908 const tpl = getLabel('number_out_of_range')
909 return callback(
910 new Error(
911 tpl
912 .replace('{min}', String(field.minValue))
913 .replace('{max}', String(field.maxValue)),
914 ),
915 )
916 }
917 const tpl = getLabel('number_cannot_be_smaller_than')
918 return callback(new Error(tpl.replace('{min}', String(field.minValue))))
919 }
920 if (hasMax && num > field.maxValue && field.minValue !== field.maxValue) {
921 if (hasMin) {
922 const tpl = getLabel('number_out_of_range')
923 return callback(
924 new Error(
925 tpl
926 .replace('{min}', String(field.minValue))
927 .replace('{max}', String(field.maxValue)),
928 ),
929 )
930 }
931 const tpl = getLabel('number_cannot_be_greater_than')
932 return callback(new Error(tpl.replace('{max}', String(field.maxValue))))
933 }
934 callback()
935 },
936 trigger: ['change', 'blur', 'submit'],
937 })
938
939 // Real-time watcher for immediate feedback when exceeding limits
940 watch(
941 () => formValues.value[key],
942 (val) => {
943 const hasMin = field.minValue !== null && field.minValue !== undefined
944 const hasMax = field.maxValue !== null && field.maxValue !== undefined
945 if (val === '' || val === null || val === undefined) {
946 if (fieldErrors.value[key]) delete fieldErrors.value[key]
947 return
948 }
949 const num = Number(val)
950 if (isNaN(num)) {
951 fieldErrors.value[key] = getLabel('enter_valid_number')
952 return
953 }
954 if (
955 ((hasMin && num < field.minValue) || (hasMax && num > field.maxValue)) &&
956 field.minValue !== field.maxValue
957 ) {
958 if (hasMin && hasMax) {
959 const tpl = getLabel('number_out_of_range')
960 fieldErrors.value[key] = tpl
961 .replace('{min}', String(field.minValue))
962 .replace('{max}', String(field.maxValue))
963 } else if (hasMin && num < field.minValue) {
964 const tpl = getLabel('number_cannot_be_smaller_than')
965 fieldErrors.value[key] = tpl.replace('{min}', String(field.minValue))
966 } else if (hasMax && num > field.maxValue) {
967 const tpl = getLabel('number_cannot_be_greater_than')
968 fieldErrors.value[key] = tpl.replace('{max}', String(field.maxValue))
969 }
970 return
971 }
972 if (fieldErrors.value[key]) delete fieldErrors.value[key]
973 },
974 )
975 }
976
977 formRules.value[key] = rules
978 }
979 // Email confirmation extension
980 if (field.type === 'email' && field.confirmFieldEnabled) {
981 const confirmKey = 'emailConfirm_' + field.fieldIndex
982 formValues.value[confirmKey] = ''
983 const confirmRules = []
984 confirmRules.push({
985 validator: (_rule: unknown, value: string, callback: (err?: Error) => void) => {
986 const mainVal = formValues.value[key]
987 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
988 if (!mainVal) {
989 callback()
990 return
991 }
992 if (!value) {
993 callback(new Error(getLabel('enter_email_address')))
994 return
995 }
996 if (!emailRegex.test(value)) {
997 callback(new Error(getLabel('enter_email_address')))
998 return
999 }
1000 if (mainVal && value !== mainVal) {
1001 callback(new Error(getLabel('emails_do_not_match')))
1002 return
1003 }
1004 callback()
1005 },
1006 trigger: ['submit'],
1007 })
1008 formRules.value[confirmKey] = confirmRules
1009 }
1010
1011 // Allow Pro or other extensions to add field-specific initialization
1012 // Hook: ivyforms:field_init
1013 if (typeof window !== 'undefined') {
1014 const w = window as Window & {
1015 IvyForms?: {
1016 hooks?: {
1017 fieldInit?: Array<
1018 (
1019 field: Field,
1020 context: {
1021 formValues: typeof formValues.value
1022 formRules: typeof formRules.value
1023 getLabel: typeof getLabel
1024 fieldErrors: typeof fieldErrors.value
1025 },
1026 ) => void
1027 >
1028 }
1029 }
1030 }
1031
1032 const hooks = w.IvyForms?.hooks?.fieldInit || []
1033 hooks.forEach((hook) => {
1034 try {
1035 hook(field, {
1036 formValues: formValues.value,
1037 formRules: formRules.value,
1038 getLabel,
1039 fieldErrors: fieldErrors.value,
1040 })
1041 } catch (error) {
1042 console.error(getLabel('error_in_field_init_hook'), error)
1043 }
1044 })
1045 }
1046 })
1047
1048 // Form is loaded, remove loader
1049 isLoading.value = false
1050
1051 // Clear any validation errors that may have been triggered during initialization
1052 // This prevents fields from showing errors on page load
1053 // Use setTimeout to ensure this runs after all DOM updates and isLoading changes
1054 setTimeout(() => {
1055 if (formRef.value) {
1056 formRef.value.clearValidate()
1057 }
1058 }, 50)
1059 // add vanila js remove class from loader
1060 const counter = (formWrapperRef.value as HTMLElement)
1061 .closest('.ivyforms-frontend')
1062 ?.getAttribute('data-counter')
1063 const loader = document.querySelector(`.ivyforms-skeleton-${counter}`)
1064 if (loader) {
1065 loader.remove()
1066 }
1067 } else {
1068 console.error(getLabel('invalid_form_data'), formData)
1069 }
1070 } else {
1071 console.error(getLabel('form_data_not_found'))
1072 // Even with error, don't show loader anymore
1073 isLoading.value = false
1074 }
1075
1076 // After loading form data, get the outer wrapper by ID and read data attribute
1077 let counter = formData.value.counter || ''
1078 if (!counter && formWrapperRef.value) {
1079 const closest = formWrapperRef.value.closest('.ivyforms-frontend')
1080 if (closest && closest.dataset.counter) {
1081 counter = closest.dataset.counter
1082 }
1083 }
1084 if (counter) {
1085 const outerWrapper = document.getElementById('ivyforms-app-' + counter)
1086 if (outerWrapper && outerWrapper.dataset) {
1087 postId.value = outerWrapper.dataset.postId || ''
1088 refererUrl.value = outerWrapper.dataset.referer || document.referrer || ''
1089 }
1090 }
1091 })
1092
1093 function getRequiredMessage(field: Field): string {
1094 const rm = field.requiredMessage
1095 if (typeof rm === 'string' && rm.trim() !== '') {
1096 return rm
1097 }
1098 let fieldMessage: string
1099 fieldMessage = ucFirst(field.label || field.type)
1100 fieldMessage += ` ${getLabel('required')}`
1101 return fieldMessage
1102 }
1103
1104 const ucFirst = (str: string) => (str ? str[0]?.toLocaleUpperCase() + str.slice(1) : '')
1105
1106 // Get the most common label position for the form
1107 const getFormLabelPosition = (): 'left' | 'right' | 'top' => {
1108 if (!formFields.value || formFields.value.length === 0) {
1109 return 'top'
1110 }
1111
1112 // Count label positions
1113 const positionCounts = formFields.value.reduce((counts, field) => {
1114 const position = field.labelPosition || 'top'
1115 counts[position] = (counts[position] || 0) + 1
1116 return counts
1117 }, {})
1118
1119 // Find the most common position
1120 const mostCommonPosition = Object.keys(positionCounts).reduce((a, b) =>
1121 positionCounts[a] > positionCounts[b] ? a : b,
1122 )
1123
1124 // Convert to Element Plus format
1125 switch (mostCommonPosition) {
1126 case 'left':
1127 return 'left'
1128 case 'right':
1129 return 'right'
1130 case 'top':
1131 case 'inside':
1132 default:
1133 return 'top'
1134 }
1135 }
1136
1137 // Validate form and submit
1138 const validateAndSubmit = async () => {
1139 if (!formRef.value) return
1140
1141 await formRef.value.validate(
1142 async (valid: boolean, errors: Record<string, { message: string }[]>) => {
1143 if (valid) {
1144 // Handle reCAPTCHA v3 execution before form submission
1145 const hasRecaptchaField = formFields.value.some((field) => field.type === 'recaptcha')
1146
1147 if (hasRecaptchaField) {
1148 // Find reCAPTCHA field component ref
1149 const recaptchaFieldRef = formFieldsCollectorRefs.value?.find(
1150 (ref: ComponentPublicInstance): ref is RecaptchaFieldComponent =>
1151 ref &&
1152 (ref as RecaptchaFieldComponent).$props &&
1153 (ref as RecaptchaFieldComponent).$props.field &&
1154 (ref as RecaptchaFieldComponent).$props.field.type === 'recaptcha',
1155 )
1156 if (recaptchaFieldRef && recaptchaFieldRef.executeRecaptcha) {
1157 try {
1158 // For v3, this will execute the reCAPTCHA and get a token
1159 // For v2, this will return the current token
1160 const token = await recaptchaFieldRef.executeRecaptcha()
1161
1162 // Update the form value with the token
1163 const fieldKey = `recaptcha_${recaptchaFieldRef.$props.field.fieldIndex}`
1164 formValues.value[fieldKey] = token
1165 } catch (error) {
1166 console.error('reCAPTCHA execution failed:', error)
1167 // You might want to show an error message to the user here
1168 return
1169 }
1170 }
1171 }
1172
1173 submitForm()
1174 } else {
1175 fieldErrors.value = {} // Clear previous
1176 Object.keys(errors).forEach((fieldKey) => {
1177 const rawMsg = errors[fieldKey][0]?.message || getLabel('invalid_input')
1178 let displayMsg = rawMsg
1179
1180 // If this is a Name field, store error on the specific subfield key
1181 if (fieldKey.startsWith('name_')) {
1182 const [parentKey, subfield] = fieldKey.split('.')
1183
1184 if (subfield) {
1185 // This is a specific subfield error (e.g., name_1.nameField1)
1186 const parts = parentKey.split('_')
1187 const fieldIndex = Number(parts[1])
1188 const field = formFields.value.find(
1189 (f) => f.type === 'name' && Number(f.fieldIndex) === fieldIndex,
1190 )
1191
1192 if (field) {
1193 // Try to get the specific required message for better error message
1194 const nfType = subfield
1195 if (nfType && (field as Record<string, unknown>)[`${nfType}RequiredMessage`]) {
1196 displayMsg =
1197 (field as Record<string, string>)[`${nfType}RequiredMessage`] || rawMsg
1198 } else if (nfType && (field as Record<string, unknown>)[`${nfType}Label`]) {
1199 const sublabel = (field as Record<string, string>)[`${nfType}Label`]
1200 displayMsg = sublabel ? `${sublabel} ${getLabel('required')}` : rawMsg
1201 }
1202 }
1203
1204 // Store error on the specific subfield key so NameFormField can target it
1205 ;(fieldErrors.value as Record<string, string>)[fieldKey] = displayMsg
1206 } else {
1207 // Fallback for parent-level name errors
1208 ;(fieldErrors.value as Record<string, string>)[fieldKey] = displayMsg
1209 }
1210 } else {
1211 ;(fieldErrors.value as Record<string, string>)[fieldKey] = displayMsg
1212 }
1213 })
1214 }
1215 },
1216 )
1217 }
1218
1219 // Clear error when user starts typing
1220 const clearFieldError = (fieldKey: string) => {
1221 if ((fieldErrors.value as Record<string, unknown>)[fieldKey]) {
1222 delete (fieldErrors.value as Record<string, unknown>)[fieldKey]
1223 }
1224 // Also clear parent key for name subfields like name_<index>.nameFieldX
1225 if (fieldKey.includes('.')) {
1226 const parentKey = fieldKey.split('.')[0]
1227 if ((fieldErrors.value as Record<string, unknown>)[parentKey]) {
1228 delete (fieldErrors.value as Record<string, unknown>)[parentKey]
1229 }
1230 }
1231 }
1232
1233 // Provide the clearFieldError function to child components
1234 provide('clearFieldError', clearFieldError)
1235 // Provide reactive form values so nested fields (e.g., email confirmation) can mutate the same model
1236 provide('formValues', formValues)
1237
1238 const resetForm = () => {
1239 skipValidation.value = true
1240 formValues.value = {}
1241 nextTick(() => {
1242 skipValidation.value = false
1243 })
1244 }
1245 // Submit form
1246 const submitForm = async () => {
1247 // Clear previous errors before each submit
1248 fieldErrors.value = {}
1249
1250 // Process form values to handle website fields with prefix/suffix
1251 const processedValues = { ...formValues.value }
1252
1253 formFields.value.forEach((field) => {
1254 if (field.type === 'website') {
1255 const key = field.type + '_' + field.fieldIndex
1256 const fullValue = processedValues[key] || ''
1257 const prefix = field.inputPrefix || ''
1258 const suffix = field.inputSuffix || ''
1259
1260 let userInput = fullValue
1261
1262 // Only add prefix/suffix if the value doesn't already have them
1263 if (fullValue && (prefix || suffix)) {
1264 // Check if prefix is already present before adding
1265 if (prefix && !userInput.startsWith(prefix)) {
1266 userInput = prefix + userInput
1267 }
1268
1269 // Check if suffix is already present before adding
1270 if (suffix && !userInput.endsWith(suffix)) {
1271 userInput = userInput + suffix
1272 }
1273 }
1274
1275 processedValues[key] = userInput
1276 }
1277 })
1278
1279 try {
1280 const { data, error, status } = await request('form/submission/', {
1281 method: 'POST',
1282 headers: {
1283 'Content-Type': 'application/json',
1284 },
1285 data: {
1286 formId: formData.value.id,
1287 values: processedValues,
1288 nonce: formNonce.value,
1289 postId: postId.value,
1290 referer: refererUrl.value,
1291 },
1292 })
1293
1294 // Check for duplicate value error from backend
1295 if (data.data.data.is_duplicate) {
1296 const duplicateFields = data.data.data.duplicate_errors
1297
1298 for (const field of formFields.value) {
1299 const fieldKey = field.type + '_' + field.fieldIndex
1300
1301 if (duplicateFields[field.id]) {
1302 const fieldValue = formValues.value[fieldKey]
1303 if (fieldValue && fieldValue.toString().trim() !== '') {
1304 ;(fieldErrors.value as Record<string, string>)[fieldKey] = getLabel(
1305 'duplicate_value_error_label',
1306 ).replace('{label}', field.label?.toLowerCase() || 'value')
1307 }
1308 }
1309 }
1310 return
1311 }
1312
1313 // Handle response
1314 if (status !== 200 || error) {
1315 IvyMessage({
1316 type: 'error',
1317 message: getLabel('error_submitting_form') + (error ? error.message : data),
1318 })
1319 return
1320 }
1321
1322 // Reset form or show success message
1323 resetForm()
1324 const type = formConfirmation.value[0].type
1325 const enabled = formConfirmation.value[0].enabled
1326 const showForm = formConfirmation.value[0].showForm
1327
1328 if (!enabled) return
1329
1330 if (type === 'successMessage') {
1331 if (!showForm) {
1332 showSuccessMessage.value = true
1333 successMessage.value =
1334 data.data.data.confirmation || formConfirmation.value[0].message || getLabel('thank_you')
1335 return
1336 } else {
1337 showSuccessMessageReset.value = true
1338 successMessage.value =
1339 data.data.data.confirmation || formConfirmation.value[0].message || getLabel('thank_you')
1340
1341 // Hide the success message after 5 seconds
1342 setTimeout(() => {
1343 const messageElement = document.querySelector('.ivyforms-success-message')
1344 if (messageElement) {
1345 messageElement.classList.add('ivyforms-fade-out')
1346 }
1347 setTimeout(() => {
1348 showSuccessMessageReset.value = false
1349 }, 500) // Match the CSS transition duration
1350 }, 5000)
1351 }
1352 } else if (type === 'redirectToPage' && formConfirmation.value[0].page) {
1353 const pageId = formConfirmation.value[0].page
1354 if (pageId) {
1355 if (router) {
1356 await router.push({ path: '/', query: { p: pageId } })
1357 } else {
1358 redirectToUrl(`/?p=${pageId}`)
1359 }
1360 }
1361 } else if (type === 'redirectToCustomUrl' && formConfirmation.value[0].url) {
1362 redirectToUrl(formConfirmation.value[0].url)
1363 }
1364 } catch (error) {
1365 console.error(getLabel('error_submitting_form'), error)
1366 // Show error message
1367 }
1368 }
1369 </script>
1370
1371 <style lang="scss" scoped>
1372 .ivyforms-form-wrapper {
1373 width: 100%;
1374 max-width: 720px;
1375 margin: 0 auto;
1376
1377 .ivyforms-form-title {
1378 font-size: 24px;
1379 font-weight: 600;
1380 margin-bottom: 12px;
1381 color: var(--map-base-text-0);
1382 }
1383
1384 .ivyforms-form-description {
1385 font-size: 16px;
1386 margin-bottom: 24px;
1387 color: var(--map-base-text-1);
1388 }
1389
1390 .ivyforms-field {
1391 padding: 12px 0;
1392 }
1393
1394 .ivyforms-form-submit {
1395 margin-top: 12px;
1396 }
1397
1398 .ivyforms-form-item {
1399 .el-form-item {
1400 // When labelPosition is 'left' - use left label class for positioning
1401 &[class*='el-form-item--label-left']*='el-form-item--label-left'] {
1402 flex-direction: row;
1403 align-items: flex-start;
1404
1405 .el-form-item__label {
1406 flex-shrink: 0;
1407 margin-right: 16px; // Add spacing between label and input
1408 margin-bottom: 0; // Remove bottom margin for side positioning
1409 min-width: 50px; // Ensure label has minimum width
1410
1411 // Style the left label when positioned on the left
1412 .ivyforms-form-item__left-label {
1413 align-items: center;
1414 display: flex;
1415 gap: 4px;
1416 }
1417 }
1418
1419 .el-form-item__content {
1420 flex: 1;
1421 }
1422 }
1423
1424 // When labelPosition is 'right' - use right label class for positioning
1425 &[class*='el-form-item--label-right']*='el-form-item--label-right'] {
1426 flex-direction: row;
1427 align-items: flex-start;
1428
1429 .el-form-item__label {
1430 flex-shrink: 0;
1431 margin-left: 16px; // Add spacing for right-positioned labels
1432 margin-right: 0;
1433 margin-bottom: 0;
1434 min-width: 50px;
1435 order: 2; // Move label to the right side
1436
1437 // Style the right label when positioned on the right
1438 .ivyforms-form-item__right-label {
1439 align-items: center;
1440 display: flex;
1441 gap: 4px;
1442 // Override the underline styling for positional right labels
1443 color: var(--map-base-text-0);
1444 text-decoration: none;
1445 margin-left: 0;
1446 }
1447 }
1448
1449 .el-form-item__content {
1450 flex: 1;
1451 order: 1; // Move content to the left side
1452 }
1453 }
1454 }
1455 }
1456
1457 ::v-deep(.ivyforms-success-message) {
1458 font-size: 20px;
1459 color: var(--map-base-brand-symbol-0);
1460 padding: 20px;
1461 background-color: var(--map-base-brand-o05);
1462 border: 1px solid var(--map-base-brand-symbol-0);
1463 border-radius: 8px;
1464 text-align: center;
1465 transition: opacity 0.5s ease-in-out;
1466 opacity: 1;
1467
1468 &.ivyforms-fade-out {
1469 opacity: 0;
1470 }
1471 }
1472 }
1473 </style>
1474