PluginProbe
The Innovative Form Builder – IvyForms / 1.0
The Innovative Form Builder – IvyForms v1.0
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 1.0, at frontend/src/views/public/FormRender.vue

2,056 lines 74.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div ref="formWrapperRef" :class="formWrapperClasses">
3 <div v-if="showSuccessMessage" class="ivyforms-success-message">
4 <SafeHTML :html="successMessage || getLabel('thank_you')" field-type="confirmation" />
5 </div>
6 <!-- Form title-->
7 <template v-else>
8 <div v-if="showSuccessMessageReset" class="ivyforms-success-message ivyforms-mb-24">
9 <SafeHTML :html="successMessage || getLabel('thank_you')" field-type="confirmation" />
10 </div>
11
12 <h3
13 v-if="formData.showTitle"
14 :class="['ivyforms-form-title', 'ivyforms-form-title-' + formData.id]"
15 >
16 {{ formData.name }}
17 </h3>
18 <!-- Form description-->
19 <p
20 v-if="formData.showDescription"
21 :class="['ivyforms-form-description', 'ivyforms-form-description-' + formData.id]"
22 >
23 {{ formData.description }}
24 </p>
25
26 <IvyForm
27 :id="'ivyforms-form-' + formData.id"
28 ref="formRef"
29 :class="'ivyforms-form ivyforms-form-' + formData.id"
30 :data-form-id="formData.id"
31 :rules="formRules"
32 :model="formValues"
33 :label-position="getFormLabelPosition()"
34 @submit.prevent="validateAndSubmit"
35 >
36 <template v-for="(row, rowIdx) in fieldsByRow" :key="'row-' + rowIdx">
37 <div
38 class="ivyforms-form-row ivyforms-flex ivyforms-width-100 ivyforms-flex-wrap-wrap"
39 :style="{ gap: horizontalGapCss }"
40 >
41 <template v-for="field in row" :key="field.type + '_' + field.fieldIndex">
42 <div
43 class="ivyforms-form-field"
44 :style="{
45 width:
46 row.length > 1
47 ? `calc(${field.width || 100}% - (${horizontalGapCss} * ${(row.length - 1) / row.length}))`
48 : `${field.width || 100}%`,
49 flex: `0 0 auto`,
50 }"
51 >
52 <component
53 :is="formFieldsTemplates[field.type]"
54 ref="formFieldsCollectorRefs"
55 v-model="formValues[field.type + '_' + field.fieldIndex]"
56 :field="field"
57 :error="fieldErrors[field.type + '_' + field.fieldIndex]"
58 :field-errors="fieldErrors"
59 :label-position="getFormLabelPosition()"
60 :horizontal-gap="horizontalGapCss"
61 ></component>
62 </div>
63 </template>
64 </div>
65 </template>
66 <!-- Add hidden inputs fields for referer, post id ... -->
67 <div
68 :class="[
69 'ivyforms-form-submit',
70 `ivyforms-form-submit--${formData.formActionButtons?.submitButtonSettings?.position || 'default'}`,
71 ]"
72 >
73 <IvyButtonAction
74 :id="'ivyforms-form-submit-button-' + formData.id"
75 priority="tertiary"
76 type="border"
77 :disabled="isSubmitting"
78 @click.prevent.stop="validateAndSubmit"
79 >
80 {{ formData.formActionButtons?.submitButtonSettings?.label || getLabel('submit') }}
81 <IvyCircleLoader
82 v-if="isSubmitting"
83 :completed="isSubmitCompleted"
84 class="ivyforms-ml-8"
85 />
86 </IvyButtonAction>
87 </div>
88 </IvyForm>
89 </template>
90 </div>
91 </template>
92
93 <script setup lang="ts">
94 import {
95 type Component,
96 type ComponentPublicInstance,
97 computed,
98 markRaw,
99 nextTick,
100 onMounted,
101 provide,
102 reactive,
103 ref,
104 watch,
105 } from 'vue'
106 import IvyForm from '@/views/_components/form/IvyForm.vue'
107 import IvyButtonAction from '@/views/_components/button/IvyButtonAction.vue'
108 import { useApiClient } from '@/composables/useApiClient'
109 import { useWcagColors } from '@/composables/useWcagColors'
110 // * Form Field templates
111 import IvyTextFormField from '../public/fields/TextFormField.vue'
112 import IvyParagraphFormField from '../public/fields/ParagraphFormField.vue'
113 import IvyEmailFormField from '../public/fields/EmailFormField.vue'
114 import IvyNumberFormField from '../public/fields/NumberFormField.vue'
115 import IvyPhoneFormField from '../public/fields/PhoneFormField.vue'
116 import IvyWebsiteFormField from '../public/fields/WebsiteFormField.vue'
117 import IvyRadioFormField from '../public/fields/RadioFormField.vue'
118 import NameFormField from '../public/fields/NameFormField.vue'
119 import RecaptchaFormField from '../public/fields/RecaptchaFormField.vue'
120 import TurnstileFormField from '../public/fields/TurnstileFormField.vue'
121 import HCaptchaFormField from '../public/fields/HCaptchaFormField.vue'
122 import IvyAddressFormField from '../public/fields/AddressFormField.vue'
123 import CheckboxFormField from '../public/fields/CheckboxFormField.vue'
124 import TimeFormField from '../public/fields/TimeFormField.vue'
125 import DateFormField from '../public/fields/DateFormField.vue'
126 import RatingFormField from '../public/fields/RatingFormField.vue'
127 import HtmlFormField from '../public/fields/HtmlFormField.vue'
128 import type { Field } from '@/types/field'
129 import type { Confirmation } from '@/types/confirmations'
130 import type { FormStyleSettings } from '@/types/form-style'
131 import { useLabels } from '@/composables/useLabels'
132 import { useTimeField } from '@/composables/useTimeField'
133 import { useDateField } from '@/composables/useDateField'
134 import IvyMessage from '@/views/_components/message/ivyMessage.ts'
135 import SelectFormField from '@/views/public/fields/SelectFormField.vue'
136 import { applyFormStyles, parseCssClassList } from '@/utils/formStyleGenerator'
137 import { buildFormPopperScopeClass } from '@/utils/form-styles/selectors'
138 import IvyCircleLoader from '@/views/_components/loader/IvyCircleLoader.vue'
139 import { resolveHorizontalSpacing } from '@/composables/useHorizontalSpacing'
140
141 // Initialize WCAG color system for public forms
142 const { startWatching } = useWcagColors()
143 startWatching()
144
145 // Global templates storage - stored on window to persist across script re-evaluations
146 // This is critical for Elementor/Gutenberg editor refreshes where scripts reload
147 // but Pro components were already registered in a previous load
148 // On regular frontend pages, we use a local store since persistence isn't needed
149 type GlobalTemplateStore = Record<string, Component>
150
151 // Check if we're in an editor preview context (Gutenberg or Elementor)
152 const isEditorContext =
153 typeof window !== 'undefined' &&
154 (window.IvyForms?.isGutenbergEditor || window.IvyForms?.isElementorEditor)
155
156 const getGlobalTemplatesStore = (): GlobalTemplateStore => {
157 // Only use window storage in editor context where script re-evaluations occur
158 if (isEditorContext && typeof window !== 'undefined') {
159 const w = window as Window & { _ivyFormsGlobalTemplates?: GlobalTemplateStore }
160 if (!w._ivyFormsGlobalTemplates) {
161 w._ivyFormsGlobalTemplates = {}
162 }
163 return w._ivyFormsGlobalTemplates
164 }
165 // On regular frontend pages, use local store
166 return {}
167 }
168 const globalTemplatesStore = getGlobalTemplatesStore()
169
170 // Hook to allow Pro plugin to register additional field components
171 const registerFieldTemplateGlobal = (fieldType: string, component: Component) => {
172 if (component && fieldType) {
173 const wrappedComponent = markRaw(component)
174 globalTemplatesStore[fieldType] = wrappedComponent
175
176 // Also update the reactive templates if available
177 const w = window as Window & {
178 _updateFormTemplates?: (fieldType: string, component: Component) => void
179 }
180 if (w._updateFormTemplates) {
181 w._updateFormTemplates(fieldType, wrappedComponent)
182 }
183
184 // Trigger a global event to notify any listening components
185 if (typeof window !== 'undefined') {
186 window.dispatchEvent(
187 new CustomEvent('ivyforms:template_registered', {
188 detail: { fieldType, component: wrappedComponent },
189 }),
190 )
191 }
192 }
193 }
194
195 // Set up global hook immediately
196 if (typeof window !== 'undefined') {
197 const w = window as Window & {
198 IvyForms?: {
199 api?: {
200 registerFieldTemplate?: (fieldType: string, component: Component) => void
201 fields?: {
202 registerPublic?: (fieldType: string, component: Component) => void
203 }
204 hooks?: {
205 registerFieldInit?: (
206 callback: (
207 field: Field,
208 context: {
209 formValues: Record<string, unknown>
210 formRules: Record<string, unknown>
211 getLabel: (key: string) => string
212 fieldErrors: Record<string, unknown>
213 },
214 ) => void,
215 ) => void
216 registerProFieldType?: (fieldType: string) => void
217 }
218 }
219 hooks?: {
220 fieldInit?: Array<
221 (
222 field: Field,
223 context: {
224 formValues: Record<string, unknown>
225 formRules: Record<string, unknown>
226 getLabel: (key: string) => string
227 fieldErrors: Record<string, unknown>
228 },
229 ) => void
230 >
231 }
232 _publicFieldQueue?: Array<{ fieldType: string; component: Component }>
233 _proFieldTypes?: Set<string>
234 }
235 }
236 // Initialize IvyForms globals with proper structure (types defined in global.d.ts)
237 // Note: Only minimal public API is initialized here, full API is in admin context
238 // These objects are populated dynamically at runtime, so we cast through 'unknown'
239 // which is type-safe since they will be fully initialized before use
240 if (!w.IvyForms) {
241 w.IvyForms = {
242 api: {
243 fields: { admin: {}, public: {} },
244 hooks: { actions: {}, filters: {} },
245 fieldOptions: { registry: {} },
246 },
247 hooks: {
248 actions: {},
249 filters: {},
250 fieldInit: [],
251 },
252 fields: {},
253 } as unknown as typeof w.IvyForms
254 }
255 if (!w.IvyForms.api) {
256 w.IvyForms.api = {
257 fields: { admin: {}, public: {} },
258 hooks: { actions: {}, filters: {} },
259 fieldOptions: { registry: {} },
260 } as unknown as typeof w.IvyForms.api
261 }
262 if (!w.IvyForms.api.fields) {
263 w.IvyForms.api.fields = {
264 admin: {},
265 public: {},
266 } as unknown as typeof w.IvyForms.api.fields
267 }
268
269 // Make it available globally
270 w.IvyForms.api.fields.registerPublic = registerFieldTemplateGlobal
271
272 // Process any queued registrations
273 const queue = w.IvyForms._publicFieldQueue || []
274 queue.forEach(({ fieldType, component }) => registerFieldTemplateGlobal(fieldType, component))
275
276 // Set up hooks system
277 if (!w.IvyForms.hooks) {
278 w.IvyForms.hooks = {
279 actions: {},
280 filters: {},
281 fieldInit: [],
282 } as unknown as typeof w.IvyForms.hooks
283 }
284 if (!w.IvyForms.hooks.fieldInit) {
285 w.IvyForms.hooks.fieldInit = []
286 }
287
288 // Initialize Pro field types registry
289 if (!w.IvyForms._proFieldTypes) {
290 w.IvyForms._proFieldTypes = new Set<string>()
291 }
292
293 // Add API to register field initialization hooks
294 if (!w.IvyForms.api.hooks) {
295 w.IvyForms.api.hooks = {
296 actions: {},
297 filters: {},
298 } as unknown as typeof w.IvyForms.api.hooks
299 }
300 w.IvyForms.api.hooks.registerFieldInit = (
301 callback: (
302 field: Field,
303 context: {
304 formValues: Record<string, unknown>
305 formRules: Record<string, unknown>
306 getLabel: (key: string) => string
307 fieldErrors: Record<string, unknown>
308 },
309 ) => void,
310 ) => {
311 if (typeof callback === 'function') {
312 w.IvyForms!.hooks!.fieldInit!.push(callback)
313 }
314 }
315
316 // Add API to register Pro field types (fields that skip Lite validation)
317 w.IvyForms.api.hooks.registerProFieldType = (fieldType: string) => {
318 if (fieldType && typeof fieldType === 'string') {
319 w.IvyForms!._proFieldTypes!.add(fieldType)
320 }
321 }
322 }
323
324 interface RecaptchaFieldComponent extends ComponentPublicInstance {
325 $props: {
326 field: {
327 type: string
328 fieldIndex: number
329 }
330 }
331 executeRecaptcha?: () => Promise<string>
332 }
333
334 interface TurnstileFieldComponent extends ComponentPublicInstance {
335 $props: {
336 field: {
337 type: string
338 fieldIndex: number
339 }
340 modelValue?: string
341 }
342 }
343
344 interface HCaptchaFieldComponent extends ComponentPublicInstance {
345 $props: {
346 field: {
347 type: string
348 fieldIndex: number
349 }
350 modelValue?: string
351 }
352 }
353
354 const { getLabel } = useLabels()
355 const { parseTimeToMinutes, isValidTimeFormat } = useTimeField()
356 const { isValidDateFormat, isDateInValidRange } = useDateField()
357 const { request } = useApiClient()
358 const skipValidation = ref(false)
359 const isSubmitting = ref(false)
360 const isSubmitCompleted = ref(false)
361
362 // Helper: normalize backend fields for preview (merge Name children into parent)
363 function normalizePreviewFields(rawFields: Field[]): Field[] {
364 if (!Array.isArray(rawFields) || rawFields.length === 0) return []
365
366 // Preserve original order index to stably order children without subFieldIndex
367 const apiOrder = new Map<Field, number>()
368 rawFields.forEach((f, i) => apiOrder.set(f, i))
369
370 const isChild = (f: Field) => f.parentId !== undefined && f.parentId !== null
371 const parents = rawFields.filter((f) => !isChild(f))
372
373 const result: Field[] = []
374
375 for (const parent of parents) {
376 // Collect children by matching parentId to parent.id or parent.fieldIndex
377 const children = rawFields.filter(
378 (c) =>
379 isChild(c) &&
380 (c.parentId === (parent as Field).id || c.parentId === (parent as Field).fieldIndex),
381 )
382
383 // Propagate common settings to top-level field if provided
384 const pSettings = (parent.settings || {}) as Record<string, unknown>
385 if (pSettings) {
386 if (parent.hideLabel === undefined && typeof pSettings.hideLabel !== 'undefined') {
387 ;(parent as Record<string, unknown>).hideLabel = !!pSettings.hideLabel
388 }
389 if (parent.readOnly === undefined && typeof pSettings.readOnly !== 'undefined') {
390 ;(parent as Record<string, unknown>).readOnly = !!(pSettings.readOnly as boolean)
391 }
392 if (
393 (parent as Record<string, unknown>).description === undefined &&
394 typeof pSettings.description !== 'undefined'
395 ) {
396 ;(parent as Record<string, unknown>).description = String(pSettings.description || '')
397 }
398 if (
399 (parent as Record<string, unknown>).cssClasses === undefined &&
400 typeof pSettings.cssClasses !== 'undefined'
401 ) {
402 ;(parent as Record<string, unknown>).cssClasses = String(pSettings.cssClasses || '')
403 }
404 }
405
406 // Apply Pro filters to parse field (e.g., conditional logic parser)
407 let processedParent = parent
408 if (window.IvyForms?.hooks?.applyFilters) {
409 processedParent = window.IvyForms.hooks.applyFilters(
410 'ivyforms/form-render/filter_parse_field',
411 parent,
412 ) as Field
413 }
414
415 // Use processedParent for rest of function
416 const parentField = processedParent
417
418 // Merge children for name and address fields
419 if (children.length >= 2 || parentField.type === 'name' || parentField.type === 'address') {
420 // Sort children: first those with explicit subFieldIndex, then the rest by API order
421 const ordered = [...children].sort((a, b) => {
422 const as = ((a.settings || {}) as Record<string, unknown>).subFieldIndex
423 const bs = ((b.settings || {}) as Record<string, unknown>).subFieldIndex
424 const aHas = typeof as === 'number'
425 const bHas = typeof bs === 'number'
426 if (aHas && bHas) return (as as number) - (bs as number)
427 if (aHas) return -1
428 if (bHas) return 1
429 return (apiOrder.get(a) || 0) - (apiOrder.get(b) || 0)
430 })
431
432 if (parentField.type === 'name') {
433 // Merge as name field
434 const nameFieldTypes: string[] = []
435 ordered.forEach((child, idx) => {
436 const key = `nameField${idx + 1}`
437 nameFieldTypes.push(key)
438 const cs = (child.settings || {}) as Record<string, unknown>
439 const prec = parentField as unknown as Record<string, unknown>
440 prec[`${key}Label`] = (child.label as unknown) || ''
441 prec[`${key}Placeholder`] = (child.placeholder as unknown) || ''
442 prec[`${key}Required`] = !!child.required
443 // Required message: prefer settings.requiredMessage, fallback to legacy top-level
444 if (typeof cs.requiredMessage !== 'undefined') {
445 prec[`${key}RequiredMessage`] = cs.requiredMessage
446 } else if (typeof (child as Record<string, unknown>).requiredMessage !== 'undefined') {
447 prec[`${key}RequiredMessage`] = (child as Record<string, unknown>).requiredMessage
448 }
449 // Default value
450 if (typeof child.defaultValue !== 'undefined') {
451 prec[`${key}Value`] = child.defaultValue as unknown
452 }
453 // Hide sublabel (support both settings.hideLabel and legacy child.hideLabel)
454 const childHasSettingsHide = typeof cs.hideLabel !== 'undefined'
455 const childHasTopLevelHide =
456 typeof (child as Record<string, unknown>).hideLabel !== 'undefined'
457 if (childHasSettingsHide || childHasTopLevelHide) {
458 const childHideLabel = childHasSettingsHide
459 ? !!(cs.hideLabel as boolean)
460 : !!((child as Record<string, unknown>).hideLabel as boolean)
461 prec[`${key}HideLabel`] = childHideLabel
462 }
463 // Subfield description
464 if (typeof (child as Record<string, unknown>).description !== 'undefined') {
465 prec[`${key}Description`] = String(
466 ((child as Record<string, unknown>).description as string) || '',
467 )
468 }
469 // Visibility
470 if (typeof (child as Record<string, unknown>).description !== 'undefined') {
471 prec[`${key}Visible`] = String(
472 ((child as Record<string, unknown>).visible as boolean) || true,
473 )
474 }
475 })
476 ;(parentField as Record<string, unknown>).nameFieldTypes = nameFieldTypes
477 parentField.type = 'name'
478 } else if (parentField.type === 'address') {
479 // Merge as address field
480 // Use canonical order for address fields (must match IvyAddressFormField.vue and builder)
481 const canonicalOrder = ['streetAddress', 'addressLine2', 'city', 'state', 'zip', 'country']
482 const addressFieldTypes = []
483 // If there are NO children, still populate all 6 subfields on the parent
484 if (ordered.length === 0) {
485 canonicalOrder.forEach((fieldType) => {
486 addressFieldTypes.push(fieldType)
487 const prec = parentField as unknown as Record<string, unknown>
488 prec[`${fieldType}Label`] =
489 (parentField as Record<string, unknown>)[`${fieldType}Label`] || ''
490 prec[`${fieldType}Placeholder`] =
491 (parentField as Record<string, unknown>)[`${fieldType}Placeholder`] || ''
492 prec[`${fieldType}Required`] =
493 (parentField as Record<string, unknown>)[`${fieldType}Required`] || false
494 prec[`${fieldType}RequiredMessage`] =
495 (parentField as Record<string, unknown>)[`${fieldType}RequiredMessage`] || ''
496 prec[`${fieldType}Value`] =
497 (parentField as Record<string, unknown>)[`${fieldType}Value`] || ''
498 prec[`${fieldType}HideLabel`] =
499 (parentField as Record<string, unknown>)[`${fieldType}HideLabel`] || false
500 prec[`${fieldType}Description`] =
501 (parentField as Record<string, unknown>)[`${fieldType}Description`] || ''
502 prec[`${fieldType}Visible`] =
503 (parentField as Record<string, unknown>)[`${fieldType}Visible`] || true
504 })
505 ;(parentField as Record<string, unknown>).addressFieldTypes = canonicalOrder
506 parentField.type = 'address'
507 } else {
508 canonicalOrder.forEach((fieldType, idx) => {
509 // Find child by settings.fieldType, or by label (case-insensitive, spaces removed), or fallback to ordered[idx] if exists
510 let child =
511 ordered.find((c) => {
512 const settings = parseSettings((c as unknown as Record<string, unknown>).settings)
513 return (
514 (settings && settings.fieldType === fieldType) ||
515 (c.label && c.label.toLowerCase().replace(/\s/g, '') === fieldType.toLowerCase())
516 )
517 }) || ordered.find((c) => c.type === fieldType)
518 // Only fallback to ordered[idx] if it exists and is not undefined
519 if (!child && Array.isArray(ordered) && ordered.length > idx && ordered[idx]) {
520 child = ordered[idx]
521 }
522 addressFieldTypes.push(fieldType)
523 const cs = child && child.settings ? parseSettings(child.settings) : {}
524 const prec = parentField as unknown as Record<string, unknown>
525 prec[`${fieldType}Label`] = child ? (child.label as unknown) || '' : ''
526 prec[`${fieldType}Placeholder`] = child ? (child.placeholder as unknown) || '' : ''
527 prec[`${fieldType}Required`] = child ? !!child.required : false
528 if (child && typeof cs.requiredMessage !== 'undefined') {
529 prec[`${fieldType}RequiredMessage`] = cs.requiredMessage
530 } else if (
531 child &&
532 typeof (child as Record<string, unknown>).requiredMessage !== 'undefined'
533 ) {
534 prec[`${fieldType}RequiredMessage`] = (
535 child as Record<string, unknown>
536 ).requiredMessage
537 } else {
538 prec[`${fieldType}RequiredMessage`] = ''
539 }
540 if (child && typeof child.defaultValue !== 'undefined') {
541 prec[`${fieldType}Value`] = child.defaultValue as unknown
542 } else {
543 prec[`${fieldType}Value`] = ''
544 }
545 const childHasSettingsHide = child && typeof cs.hideLabel !== 'undefined'
546 const childHasTopLevelHide =
547 child && typeof (child as Record<string, unknown>).hideLabel !== 'undefined'
548 if (childHasSettingsHide || childHasTopLevelHide) {
549 const childHideLabel = childHasSettingsHide
550 ? !!(cs.hideLabel as boolean)
551 : !!((child as Record<string, unknown>).hideLabel as boolean)
552 prec[`${fieldType}HideLabel`] = childHideLabel
553 } else {
554 prec[`${fieldType}HideLabel`] = false
555 }
556 if (child && typeof (child as Record<string, unknown>).description !== 'undefined') {
557 prec[`${fieldType}Description`] = String(
558 ((child as Record<string, unknown>).description as string) || '',
559 )
560 } else {
561 prec[`${fieldType}Description`] = ''
562 }
563 if (child && typeof (child as Record<string, unknown>).visible !== 'undefined') {
564 prec[`${fieldType}Visible`] = !!((child as Record<string, unknown>)
565 .visible as boolean)
566 } else {
567 prec[`${fieldType}Visible`] = true
568 }
569 })
570 ;(parentField as Record<string, unknown>).addressFieldTypes = canonicalOrder
571 parentField.type = 'address'
572 }
573 }
574 }
575
576 result.push(parentField)
577 }
578
579 return result
580 }
581
582 // Safely parse settings which may be stored as JSON string or as an object
583 const parseSettings = (s: unknown): Record<string, unknown> => {
584 if (!s) return {}
585 if (typeof s === 'string') {
586 try {
587 return JSON.parse(s) as Record<string, unknown>
588 } catch {
589 return {}
590 }
591 }
592 if (typeof s === 'object') {
593 return s as Record<string, unknown>
594 }
595 return {}
596 }
597
598 // Form data
599 const formFields = ref([])
600 const formValues = ref({})
601 const formRules = ref({})
602 const fieldErrors = ref({})
603 const isLoading = ref(true)
604 const formRef = ref(null)
605 const formWrapperRef = ref(null)
606 const formFieldsCollectorRefs = ref([])
607 const formNonce = ref(null)
608 const formConfirmation = ref([])
609 const showSuccessMessage = ref(false)
610 const showSuccessMessageReset = ref(false)
611 const successMessage = ref('')
612 const postId = ref('')
613 const refererUrl = ref('')
614
615 // Computed property to filter visible fields
616 // Hook: ivyforms_fields_visible can be used by Pro to add conditional logic filtering
617 const visibleFields = computed(() => {
618 if (!formFields.value || formFields.value.length === 0) {
619 return []
620 }
621
622 // Allow Pro plugin to filter fields (e.g., conditional logic)
623 let fields = [...formFields.value]
624
625 if (window.IvyForms?.hooks?.applyFilters) {
626 fields = window.IvyForms.hooks.applyFilters(
627 'ivyforms/form-render/filter_fields_visible',
628 fields,
629 formValues.value,
630 )
631 }
632
633 return fields
634 })
635
636 // Organize fields into rows based on rowIndex and columnIndex
637 const fieldsByRow = computed(() => {
638 const fields = visibleFields.value
639 if (!fields || fields.length === 0) {
640 return []
641 }
642
643 // Group fields by rowIndex
644 const rowsMap: Map<number, Field[]> = new Map()
645
646 fields.forEach((field) => {
647 const rowIdx = field.rowIndex ?? 0
648 if (!rowsMap.has(rowIdx)) {
649 rowsMap.set(rowIdx, [])
650 }
651 rowsMap.get(rowIdx)!.push(field)
652 })
653
654 // Sort rows by rowIndex and sort fields within each row by columnIndex
655 return Array.from(rowsMap.entries())
656 .sort(([aIdx], [bIdx]) => aIdx - bIdx)
657 .map(([, fields]) => {
658 return fields.sort((a, b) => (a.columnIndex ?? 0) - (b.columnIndex ?? 0))
659 })
660 })
661
662 interface FormData {
663 id: string | number
664 name?: string
665 description?: string
666 showTitle?: boolean
667 showDescription?: boolean
668 storeEntries?: boolean
669 defaultValue?: string
670 counter?: number
671 fields?: Field[]
672 confirmations?: Confirmation[]
673 formActionButtons?: {
674 submitButtonSettings?: {
675 label: string
676 position: 'default' | 'left' | 'center' | 'right'
677 }
678 }
679 styleSettings?: unknown
680 }
681
682 // Define the shape of entries provided by the global window variable
683 interface GlobalFormEntry {
684 id: string | number
685 form: FormData
686 nonce: string
687 fields: Field[]
688 confirmations: Confirmation[]
689 }
690
691 // Update your refs with proper typing
692 const formData = ref<FormData>({
693 id: '',
694 name: '',
695 description: '',
696 showTitle: true,
697 showDescription: false,
698 storeEntries: true,
699 defaultValue: '',
700 counter: 0,
701 fields: [],
702 confirmations: [],
703 formActionButtons: {
704 submitButtonSettings: {
705 label: getLabel('submit'),
706 position: 'default',
707 },
708 },
709 })
710
711 const horizontalGapCss = computed(() => {
712 return resolveHorizontalSpacing(formData.value.styleSettings)
713 })
714 const formPopperScopeClass = computed(() => {
715 return buildFormPopperScopeClass(formData.value.id || 'public-form')
716 })
717
718 const styleClassList = computed(() => {
719 const styleSettings = (formData.value.styleSettings || {}) as FormStyleSettings
720 const parsedCustomClasses = parseCssClassList(styleSettings.customCssClass || '')
721 const parsedAdditionalClasses = parseCssClassList(styleSettings.additionalCssClasses || '')
722
723 return [...parsedCustomClasses, ...parsedAdditionalClasses]
724 })
725
726 const formWrapperClasses = computed(() => {
727 const formId = formData.value.id
728
729 return [
730 'ivyforms-form-wrapper',
731 `ivyforms-form-wrapper-${formId}`,
732 `ivyforms-form-${formId}`,
733 ...styleClassList.value,
734 ]
735 })
736 // * Form Fields Object - start with base templates then add global ones
737 const baseTemplates = {
738 text: markRaw(IvyTextFormField),
739 email: markRaw(IvyEmailFormField),
740 textarea: markRaw(IvyParagraphFormField),
741 number: markRaw(IvyNumberFormField),
742 phone: markRaw(IvyPhoneFormField),
743 website: markRaw(IvyWebsiteFormField),
744 time: markRaw(TimeFormField),
745 date: markRaw(DateFormField),
746 recaptcha: markRaw(RecaptchaFormField),
747 turnstile: markRaw(TurnstileFormField),
748 hcaptcha: markRaw(HCaptchaFormField),
749 html: markRaw(HtmlFormField),
750 radio: markRaw(IvyRadioFormField),
751 select: markRaw(SelectFormField),
752 name: markRaw(NameFormField),
753 address: markRaw(IvyAddressFormField),
754 checkbox: markRaw(CheckboxFormField),
755 'multi-select': markRaw(SelectFormField),
756 rating: markRaw(RatingFormField),
757 }
758
759 // Create reactive templates and manually add global store items
760 const formFieldsTemplates = reactive({ ...baseTemplates })
761 // Add any existing global templates
762 Object.entries(globalTemplatesStore).forEach(([fieldType, component]) => {
763 formFieldsTemplates[fieldType] = component
764 })
765 // Force reactivity trigger
766 const templateUpdateTrigger = ref(0)
767 // Set up update function for late registrations
768 if (typeof window !== 'undefined') {
769 ;(
770 window as Window & { _updateFormTemplates?: (fieldType: string, component: Component) => void }
771 )._updateFormTemplates = (fieldType: string, component: Component) => {
772 formFieldsTemplates[fieldType] = component
773 templateUpdateTrigger.value++ // Force Vue reactivity
774 }
775
776 // Listen for template registration events
777 window.addEventListener('ivyforms:template_registered', (event: Event) => {
778 const customEvent = event as CustomEvent<{ fieldType: string; component: Component }>
779 const { fieldType, component } = customEvent.detail
780 formFieldsTemplates[fieldType] = component
781 templateUpdateTrigger.value++ // Force Vue reactivity
782 })
783
784 // Also check if there are already registered templates in global store
785 Object.entries(globalTemplatesStore).forEach(([fieldType, component]) => {
786 formFieldsTemplates[fieldType] = component
787 })
788 }
789
790 // Initialize from global variable
791 onMounted(async () => {
792 const w = window as unknown as { wpIvyFormDataList?: Record<string, GlobalFormEntry> }
793 if (typeof window !== 'undefined' && w.wpIvyFormDataList) {
794 const counter = (formWrapperRef.value as HTMLElement)
795 .closest('.ivyforms-frontend')!
796 .getAttribute('data-counter') as string
797 const formsData = w.wpIvyFormDataList[counter]
798 // Check if formData is an object and has the required properties
799 if (typeof formsData === 'object' && formsData !== null && formsData.id) {
800 // Assign the form data to the reactive variable
801 formData.value = formsData.form
802 formNonce.value = formsData.nonce
803 // Normalize fields for preview
804 formFields.value = normalizePreviewFields(formsData.fields || [])
805 formConfirmation.value = formsData.confirmations || []
806
807 // Wait for Pro plugins to register their hooks before initializing fields
808 // This ensures all extensions can add field-specific initialization logic
809 await new Promise<void>((resolve) => {
810 if (typeof window !== 'undefined') {
811 // Check if Pro is ready
812 const wExt = window as Window & { IvyFormsProReady?: boolean }
813 if (wExt.IvyFormsProReady) {
814 resolve()
815 return
816 }
817
818 // Wait for Pro ready event
819 const handler = () => {
820 resolve()
821 window.removeEventListener('ivyforms:pro_ready', handler)
822 }
823 window.addEventListener('ivyforms:pro_ready', handler, { once: true })
824
825 // Fallback timeout - initialize after 100ms even if Pro doesn't signal ready
826 setTimeout(() => {
827 window.removeEventListener('ivyforms:pro_ready', handler)
828 resolve()
829 }, 100)
830 } else {
831 resolve()
832 }
833 })
834
835 // �
836 NOW initialize fields AFTER Pro is ready
837 // This ensures visibleFields computed will apply conditional logic hooks
838 formFields.value = normalizePreviewFields(formsData.fields || [])
839
840 // Initialize form values with defaults or empty strings
841 formFields.value.forEach((field) => {
842 const key = field.type + '_' + field.fieldIndex
843
844 // Special handling for name field type
845 if (field.type === 'name') {
846 const nameValue: Record<string, string> = {}
847 const nameFieldTypes = field.nameFieldTypes || ['nameField1', 'nameField2']
848 // Initialize each dynamic name field
849 nameFieldTypes.forEach((fieldType) => {
850 nameValue[fieldType] = (field as Record<string, string>)[`${fieldType}Value`] || ''
851 })
852 formValues.value[key] = nameValue
853 } else if (field.type === 'address') {
854 // initialize all Address field subfields
855 const addressValue: Record<string, string> = {}
856 const addressFieldTypes = field.addressFieldTypes || [
857 'streetAddress',
858 'addressLine2',
859 'city',
860 'state',
861 'zip',
862 'country',
863 ]
864 addressFieldTypes.forEach((fieldType) => {
865 addressValue[fieldType] = (field as Record<string, string>)[`${fieldType}Value`] || ''
866 })
867 formValues.value[key] = addressValue
868 } else if (field.type === 'checkbox' && Array.isArray(field.fieldOptions)) {
869 const defaultOpts = field.fieldOptions
870 .filter((opt) => opt.isDefault)
871 .map((opt) => opt.value)
872 formValues.value[key] = defaultOpts.length ? defaultOpts : undefined
873 } else if (
874 (field.type === 'radio' || field.type === 'select') &&
875 Array.isArray(field.fieldOptions)
876 ) {
877 const defaultOpt = field.fieldOptions.find((opt) => opt.isDefault)
878 formValues.value[key] = defaultOpt ? defaultOpt.value : ''
879 } else if (field.type === 'rating' && Array.isArray(field.fieldOptions)) {
880 const defaultOpt = field.fieldOptions.find((opt) => opt.isDefault)
881 formValues.value[key] = defaultOpt ? Number(defaultOpt.value) : undefined
882 } else if (field.type === 'multi-select' && Array.isArray(field.fieldOptions)) {
883 const defaultOpts = field.fieldOptions
884 .filter((opt) => opt.isDefault)
885 .map((opt) => opt.value)
886 formValues.value[key] = defaultOpts.length ? defaultOpts : undefined
887 } else if (field.type === 'website') {
888 // Handle website fields with prefix and suffix
889 let websiteValue = field.defaultValue || ''
890 const prefix = field.inputPrefix || ''
891 const suffix = field.inputSuffix || ''
892
893 // If there's a defaultValue, strip any existing prefix/suffix first
894 if (websiteValue && (prefix || suffix)) {
895 // Remove prefix if it exists
896 if (prefix && websiteValue.startsWith(prefix)) {
897 websiteValue = websiteValue.substring(prefix.length)
898 }
899 // Remove suffix if it exists
900 if (suffix && websiteValue.endsWith(suffix)) {
901 websiteValue = websiteValue.substring(0, websiteValue.length - suffix.length)
902 }
903 }
904
905 formValues.value[key] = websiteValue
906 } else if (field.type === 'date') {
907 // Sanitize date default values - filter out invalid dates with MIN_SAFE_INTEGER
908 let dateValue = field.defaultValue || ''
909 if (
910 typeof dateValue === 'string' &&
911 (dateValue.includes('-9007199254740991') || dateValue.includes('MIN_SAFE_INTEGER'))
912 ) {
913 dateValue = ''
914 }
915 formValues.value[key] = dateValue
916 } else {
917 formValues.value[key] = field.defaultValue || ''
918 }
919
920 if (field.type === 'name') {
921 const nameFieldTypes = field.nameFieldTypes || ['nameField1', 'nameField2']
922 nameFieldTypes.forEach((nfType: string) => {
923 const subKey = `${field.type}_${field.fieldIndex}.${nfType}`
924 const label =
925 (field as Record<string, string>)[`${nfType}Label`] ||
926 getLabel(`name_field_${nfType.replace('nameField', '')}`)
927 const rulesForSub: unknown[] = []
928 if ((field as Record<string, unknown>)[`${nfType}Required`]) {
929 const customMsg = (field as Record<string, unknown>)[`${nfType}RequiredMessage`]
930 const message =
931 typeof customMsg === 'string' && (customMsg as string).trim() !== ''
932 ? (customMsg as string)
933 : `${label} ${getLabel('required')}`
934 rulesForSub.push({
935 required: true,
936 message,
937 trigger: ['submit'],
938 })
939 }
940 ;(formRules.value as Record<string, unknown>)[subKey] = rulesForSub
941 })
942 } else if (field.type === 'address') {
943 const addressFieldTypes = field.addressFieldTypes || [
944 'streetAddress',
945 'addressLine2',
946 'city',
947 'state',
948 'zip',
949 'country',
950 ]
951 addressFieldTypes.forEach((afType: string) => {
952 const subKey = `${field.type}_${field.fieldIndex}.${afType}`
953 const label =
954 (field as Record<string, string>)[`${afType}Label`] ||
955 getLabel(`address_field_${afType.replace('addressField', '')}`)
956 const rulesForSub: unknown[] = []
957 if ((field as Record<string, unknown>)[`${afType}Required`]) {
958 const customMsg = (field as Record<string, unknown>)[`${afType}RequiredMessage`]
959 const message =
960 typeof customMsg === 'string' && (customMsg as string).trim() !== ''
961 ? (customMsg as string)
962 : `${label} ${getLabel('required')}`
963 rulesForSub.push({
964 required: true,
965 message,
966 trigger: ['submit'],
967 })
968 }
969 ;(formRules.value as Record<string, unknown>)[subKey] = rulesForSub
970 })
971 } else {
972 const rules = []
973
974 // Check if this field type is registered as a Pro field
975 // Pro fields handle their own validation via fieldInit hooks
976 const w = window as Window & { IvyForms?: { _proFieldTypes?: Set<string> } }
977 const isProField = w.IvyForms?._proFieldTypes?.has(field.type) || false
978
979 // Required rule (skip Pro fields - Pro handles them via fieldInit hooks)
980 if (field.required && !isProField) {
981 rules.push({
982 required: true,
983 message: getRequiredMessage(field),
984 trigger: ['submit'],
985 })
986 }
987
988 // Format validations
989 if (field.type === 'email') {
990 rules.push({
991 validator: (_rule: unknown, value: string, callback: (err?: Error) => void) => {
992 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
993 if (skipValidation.value) {
994 callback()
995 return
996 }
997 if (!value) {
998 if (field.required) {
999 callback(new Error(getLabel('enter_email_address')))
1000 } else {
1001 callback()
1002 }
1003 return
1004 }
1005 if (!emailRegex.test(value)) {
1006 callback(new Error(getLabel('enter_email_address')))
1007 return
1008 }
1009 callback()
1010 },
1011 trigger: ['submit'],
1012 })
1013 }
1014
1015 if (field.type === 'phone') {
1016 rules.push({
1017 pattern: /^[+\d]?(?:[\d\s.-]{7,15})$/,
1018 message: getLabel('enter_phone_number'),
1019 trigger: ['submit'],
1020 })
1021 }
1022
1023 if (field.type === 'website') {
1024 rules.push({
1025 validator: (_rule: unknown, value: unknown, callback: (err?: Error) => void) => {
1026 // Check if field is required and empty
1027 if (field.required && (value === '' || value === null || value === undefined)) {
1028 return callback(new Error(getRequiredMessage(field)))
1029 }
1030 if (value === '' || value === null || value === undefined) {
1031 return callback()
1032 }
1033
1034 let urlToValidate = String(value)
1035
1036 // If field has prefix and/or suffix, construct the full URL for validation
1037 const prefix = field.inputPrefix || ''
1038 const suffix = field.inputSuffix || ''
1039
1040 if (prefix || suffix) {
1041 // User entered only the middle part, construct full URL
1042 urlToValidate = prefix + urlToValidate + suffix
1043 }
1044
1045 // Validate the complete URL
1046 const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/i
1047 if (!urlPattern.test(urlToValidate)) {
1048 return callback(new Error(getLabel('enter_valid_website')))
1049 }
1050
1051 callback()
1052 },
1053 trigger: ['submit'],
1054 })
1055 }
1056
1057 if (field.type === 'time') {
1058 rules.push({
1059 validator: (_: unknown, value: string, callback: (e?: Error) => void) => {
1060 const trimmedValue = typeof value === 'string' ? value.trim() : value
1061 const isEmpty = trimmedValue == null || trimmedValue === ''
1062 const isPartial = trimmedValue === '__PARTIAL__'
1063
1064 // Dropdown mode - check if all parts are selected
1065 if (field.timeFieldType === 'dropdown') {
1066 if (field.required && (isEmpty || isPartial)) {
1067 return callback(new Error(getRequiredMessage(field)))
1068 }
1069 if (isPartial) {
1070 return callback(new Error(getLabel('enter_valid_time')))
1071 }
1072 if (!isEmpty && !isValidTimeFormat(trimmedValue, field.timeFormat, 'dropdown')) {
1073 return callback(new Error(getLabel('enter_valid_time')))
1074 }
1075 return callback()
1076 }
1077
1078 // Required validation
1079 if (field.required && isEmpty) {
1080 return callback(new Error(getRequiredMessage(field)))
1081 }
1082
1083 // Format validation using centralized helper
1084 const hasValidFormat =
1085 isEmpty || isValidTimeFormat(trimmedValue, field.timeFormat, field.timeFieldType)
1086 if (!isEmpty && !hasValidFormat) {
1087 return callback(new Error(getLabel('enter_valid_time')))
1088 }
1089
1090 // Min/max range validation
1091 const hasMin =
1092 field.minTimeValue !== null &&
1093 field.minTimeValue !== undefined &&
1094 field.minTimeValue !== ''
1095 const hasMax =
1096 field.maxTimeValue !== null &&
1097 field.maxTimeValue !== undefined &&
1098 field.maxTimeValue !== ''
1099
1100 if ((hasMin || hasMax) && !isEmpty && hasValidFormat) {
1101 const currentTimeMinutes = parseTimeToMinutes(trimmedValue)
1102 if (currentTimeMinutes === null) {
1103 return callback(new Error(getLabel('enter_valid_time')))
1104 }
1105
1106 const minTimeMinutes = hasMin
1107 ? parseTimeToMinutes(String(field.minTimeValue))
1108 : null
1109 if (hasMin && minTimeMinutes === null) {
1110 return callback(
1111 new Error(`Malformed minimum time value: ${field.minTimeValue}`),
1112 )
1113 }
1114
1115 const maxTimeMinutes = hasMax
1116 ? parseTimeToMinutes(String(field.maxTimeValue))
1117 : null
1118 if (hasMax && maxTimeMinutes === null) {
1119 return callback(
1120 new Error(`Malformed maximum time value: ${field.maxTimeValue}`),
1121 )
1122 }
1123
1124 if (minTimeMinutes !== null && currentTimeMinutes < minTimeMinutes) {
1125 return callback(
1126 new Error(
1127 getLabel('time_must_be_after').replace('{min}', String(field.minTimeValue)),
1128 ),
1129 )
1130 }
1131
1132 if (maxTimeMinutes !== null && currentTimeMinutes > maxTimeMinutes) {
1133 return callback(
1134 new Error(
1135 getLabel('time_must_be_before').replace(
1136 '{max}',
1137 String(field.maxTimeValue),
1138 ),
1139 ),
1140 )
1141 }
1142 }
1143
1144 return callback()
1145 },
1146 trigger: ['submit'],
1147 })
1148 }
1149
1150 if (field.type === 'date') {
1151 rules.push({
1152 validator: (_: unknown, value: string, callback: (e?: Error) => void) => {
1153 const trimmedValue = typeof value === 'string' ? value.trim() : value
1154 const isEmpty = trimmedValue == null || trimmedValue === ''
1155 const isPartial = trimmedValue === '__PARTIAL__'
1156
1157 // Dropdown mode - check if all parts are selected
1158 if (field.dateFieldType === 'dropdown') {
1159 if (field.required && (isEmpty || isPartial)) {
1160 return callback(new Error(getRequiredMessage(field)))
1161 }
1162 if (isPartial) {
1163 return callback(new Error(getLabel('enter_valid_date')))
1164 }
1165 return callback()
1166 }
1167
1168 // Required validation
1169 if (field.required && isEmpty) {
1170 return callback(new Error(getRequiredMessage(field)))
1171 }
1172
1173 // Empty but not required - allow
1174 if (isEmpty) {
1175 return callback()
1176 }
1177
1178 // Format validation using centralized helper
1179 const dateFormat = (field.dateFormat as string) || 'MM/DD/YYYY'
1180 if (!isValidDateFormat(trimmedValue, dateFormat)) {
1181 return callback(new Error(getLabel('enter_date_valid_format')))
1182 }
1183
1184 // Min/max range validation using centralized helper
1185 const rangeValidation = isDateInValidRange(
1186 trimmedValue,
1187 dateFormat,
1188 field.minDateValue,
1189 field.maxDateValue,
1190 )
1191 if (!rangeValidation.isInRange) {
1192 // Map error messages to user-friendly labels
1193 if (rangeValidation.error?.includes('Malformed minimum')) {
1194 return callback(
1195 new Error(`Malformed minimum date value: ${field.minDateValue}`),
1196 )
1197 }
1198 if (rangeValidation.error?.includes('Malformed maximum')) {
1199 return callback(
1200 new Error(`Malformed maximum date value: ${field.maxDateValue}`),
1201 )
1202 }
1203 if (rangeValidation.error?.includes('at or after')) {
1204 return callback(
1205 new Error(
1206 getLabel('date_must_be_after').replace('{min}', String(field.minDateValue)),
1207 ),
1208 )
1209 }
1210 if (rangeValidation.error?.includes('at or before')) {
1211 return callback(
1212 new Error(
1213 getLabel('date_must_be_before').replace(
1214 '{max}',
1215 String(field.maxDateValue),
1216 ),
1217 ),
1218 )
1219 }
1220 return callback(new Error(rangeValidation.error || getLabel('enter_valid_date')))
1221 }
1222
1223 return callback()
1224 },
1225 trigger: ['submit'],
1226 })
1227 }
1228
1229 if (field.type === 'number' && (field.minValue !== null || field.maxValue !== null)) {
1230 rules.push({
1231 validator: (_rule: unknown, value: unknown, callback: (err?: Error) => void) => {
1232 if (value === '' || value === null || value === undefined) {
1233 return callback()
1234 }
1235 const num = Number(value)
1236 if (isNaN(num)) {
1237 return callback(new Error(getLabel('enter_valid_number')))
1238 }
1239 const hasMin = field.minValue !== null && field.minValue !== undefined
1240 const hasMax = field.maxValue !== null && field.maxValue !== undefined
1241 if (hasMin && num < field.minValue) {
1242 if (hasMax) {
1243 const tpl = getLabel('number_out_of_range')
1244 return callback(
1245 new Error(
1246 tpl
1247 .replace('{min}', String(field.minValue))
1248 .replace('{max}', String(field.maxValue)),
1249 ),
1250 )
1251 }
1252 const tpl = getLabel('number_cannot_be_smaller_than')
1253 return callback(new Error(tpl.replace('{min}', String(field.minValue))))
1254 }
1255 if (hasMax && num > field.maxValue && field.minValue !== field.maxValue) {
1256 if (hasMin) {
1257 const tpl = getLabel('number_out_of_range')
1258 return callback(
1259 new Error(
1260 tpl
1261 .replace('{min}', String(field.minValue))
1262 .replace('{max}', String(field.maxValue)),
1263 ),
1264 )
1265 }
1266 const tpl = getLabel('number_cannot_be_greater_than')
1267 return callback(new Error(tpl.replace('{max}', String(field.maxValue))))
1268 }
1269 callback()
1270 },
1271 trigger: ['change', 'blur', 'submit'],
1272 })
1273
1274 // Real-time watcher for immediate feedback when exceeding limits
1275 watch(
1276 () => formValues.value[key],
1277 (val) => {
1278 const hasMin = field.minValue !== null && field.minValue !== undefined
1279 const hasMax = field.maxValue !== null && field.maxValue !== undefined
1280 if (val === '' || val === null || val === undefined) {
1281 if (fieldErrors.value[key]) delete fieldErrors.value[key]
1282 return
1283 }
1284 const num = Number(val)
1285 if (isNaN(num)) {
1286 fieldErrors.value[key] = getLabel('enter_valid_number')
1287 return
1288 }
1289 if (
1290 ((hasMin && num < field.minValue) || (hasMax && num > field.maxValue)) &&
1291 field.minValue !== field.maxValue
1292 ) {
1293 if (hasMin && hasMax) {
1294 const tpl = getLabel('number_out_of_range')
1295 fieldErrors.value[key] = tpl
1296 .replace('{min}', String(field.minValue))
1297 .replace('{max}', String(field.maxValue))
1298 } else if (hasMin && num < field.minValue) {
1299 const tpl = getLabel('number_cannot_be_smaller_than')
1300 fieldErrors.value[key] = tpl.replace('{min}', String(field.minValue))
1301 } else if (hasMax && num > field.maxValue) {
1302 const tpl = getLabel('number_cannot_be_greater_than')
1303 fieldErrors.value[key] = tpl.replace('{max}', String(field.maxValue))
1304 }
1305 return
1306 }
1307 if (fieldErrors.value[key]) delete fieldErrors.value[key]
1308 },
1309 )
1310 }
1311
1312 formRules.value[key] = rules
1313 }
1314
1315 // Real-time watcher for date min/max validation (only for input mode)
1316 if (
1317 field.type === 'date' &&
1318 field.dateFieldType === 'input' &&
1319 (field.minDateValue !== null || field.maxDateValue !== null)
1320 ) {
1321 const hasMin =
1322 field.minDateValue !== null &&
1323 field.minDateValue !== undefined &&
1324 field.minDateValue !== ''
1325 const hasMax =
1326 field.maxDateValue !== null &&
1327 field.maxDateValue !== undefined &&
1328 field.maxDateValue !== ''
1329
1330 if (hasMin || hasMax) {
1331 watch(
1332 () => formValues.value[key],
1333 (val) => {
1334 // Skip validation if empty or partial
1335 if (!val || val === '' || val === '__PARTIAL__') {
1336 if (fieldErrors.value[key]) delete fieldErrors.value[key]
1337 return
1338 }
1339
1340 const dateFormat = (field.dateFormat as string) || 'MM/DD/YYYY'
1341
1342 const isDateComplete = (dateStr: string, format: string): boolean => {
1343 let expectedLength = format.length
1344
1345 if (format.includes('M/D') || format.includes('D/M')) {
1346 expectedLength = 8
1347 } else if (format.includes('MM/DD') || format.includes('DD/MM')) {
1348 expectedLength = 10
1349 } else if (format.includes('YYYY-MM-DD') || format.includes('YYYY-M-D')) {
1350 expectedLength = 10
1351 } else if (format.includes('DD.MM.YYYY') || format.includes('D.M.YYYY')) {
1352 expectedLength = 10
1353 }
1354
1355 if (dateStr.length < expectedLength) {
1356 return false
1357 }
1358
1359 let parts: string[] = []
1360 if (dateStr.indexOf('/') !== -1) {
1361 parts = dateStr.split('/')
1362 } else if (dateStr.indexOf('-') !== -1) {
1363 parts = dateStr.split('-')
1364 } else if (dateStr.indexOf('.') !== -1) {
1365 parts = dateStr.split('.')
1366 }
1367
1368 if (parts.length === 3) {
1369 let yearPart = ''
1370 if (format.startsWith('YYYY')) {
1371 yearPart = parts[0]
1372 } else {
1373 yearPart = parts[2]
1374 }
1375
1376 if (yearPart.length !== 4 || isNaN(Number(yearPart))) {
1377 return false
1378 }
1379 } else {
1380 return false
1381 }
1382
1383 return true
1384 }
1385
1386 if (!isDateComplete(String(val), dateFormat)) {
1387 if (fieldErrors.value[key]) delete fieldErrors.value[key]
1388 return
1389 }
1390
1391 const currentDate = parseDateToObject(String(val), dateFormat)
1392 if (!currentDate) {
1393 if (fieldErrors.value[key]) delete fieldErrors.value[key]
1394 return
1395 }
1396
1397 const minDate = hasMin
1398 ? parseDateToObject(String(field.minDateValue), dateFormat)
1399 : null
1400 const maxDate = hasMax
1401 ? parseDateToObject(String(field.maxDateValue), dateFormat)
1402 : null
1403
1404 // Use nextTick to ensure error is set AFTER clearFieldError is called
1405 nextTick(() => {
1406 if (minDate && currentDate < minDate) {
1407 const message = getLabel('date_must_be_after')
1408 fieldErrors.value[key] = message.replace('{min}', String(field.minDateValue))
1409 return
1410 }
1411
1412 if (maxDate && currentDate > maxDate) {
1413 const message = getLabel('date_must_be_before')
1414 fieldErrors.value[key] = message.replace('{max}', String(field.maxDateValue))
1415 return
1416 }
1417
1418 if (fieldErrors.value[key]) delete fieldErrors.value[key]
1419 })
1420 },
1421 )
1422 }
1423 }
1424
1425 // Email confirmation extension
1426 if (field.type === 'email' && field.confirmFieldEnabled) {
1427 const confirmKey = 'emailConfirm_' + field.fieldIndex
1428 formValues.value[confirmKey] = ''
1429 const confirmRules = []
1430 confirmRules.push({
1431 validator: (_rule: unknown, value: string, callback: (err?: Error) => void) => {
1432 const mainVal = formValues.value[key]
1433 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
1434 if (!mainVal) {
1435 callback()
1436 return
1437 }
1438 if (!value) {
1439 callback(new Error(getLabel('enter_email_address')))
1440 return
1441 }
1442 if (!emailRegex.test(value)) {
1443 callback(new Error(getLabel('enter_email_address')))
1444 return
1445 }
1446 if (mainVal && value !== mainVal) {
1447 callback(new Error(getLabel('emails_do_not_match')))
1448 return
1449 }
1450 callback()
1451 },
1452 trigger: ['submit'],
1453 })
1454 formRules.value[confirmKey] = confirmRules
1455 }
1456
1457 // Allow Pro or other extensions to add field-specific initialization
1458 // Hook: ivyforms:field_init
1459 if (typeof window !== 'undefined') {
1460 const w = window as Window & {
1461 IvyForms?: {
1462 hooks?: {
1463 fieldInit?: Array<
1464 (
1465 field: Field,
1466 context: {
1467 formValues: typeof formValues.value
1468 formRules: typeof formRules.value
1469 getLabel: typeof getLabel
1470 fieldErrors: typeof fieldErrors.value
1471 },
1472 ) => void
1473 >
1474 }
1475 }
1476 }
1477
1478 const hooks = w.IvyForms?.hooks?.fieldInit || []
1479 hooks.forEach((hook) => {
1480 try {
1481 hook(field, {
1482 formValues: formValues.value,
1483 formRules: formRules.value,
1484 getLabel,
1485 fieldErrors: fieldErrors.value,
1486 })
1487 } catch (error) {
1488 console.error(getLabel('error_in_field_init_hook'), error)
1489 }
1490 })
1491 }
1492 })
1493
1494 // Form is loaded, remove loader
1495 isLoading.value = false
1496
1497 // Clear any validation errors that may have been triggered during initialization
1498 // This prevents fields from showing errors on page load
1499 // Use setTimeout to ensure this runs after all DOM updates and isLoading changes
1500 setTimeout(() => {
1501 if (formRef.value) {
1502 formRef.value.clearValidate()
1503 }
1504 }, 50)
1505 // add vanila js remove class from loader
1506 const counter = (formWrapperRef.value as HTMLElement)
1507 .closest('.ivyforms-frontend')
1508 ?.getAttribute('data-counter')
1509 const loader = document.querySelector(`.ivyforms-skeleton-${counter}`)
1510 if (loader) {
1511 loader.remove()
1512 }
1513
1514 // Apply form styles if styleSettings exist
1515 if (formData.value.styleSettings) {
1516 applyFormStyles(formData.value.id as number, formData.value.styleSettings)
1517 }
1518 } else {
1519 console.error(getLabel('invalid_form_data'), formData)
1520 }
1521 } else {
1522 console.error(getLabel('form_data_not_found'))
1523 // Even with error, don't show loader anymore
1524 isLoading.value = false
1525 }
1526
1527 // After loading form data, get the outer wrapper by ID and read data attribute
1528 let counter = formData.value.counter || ''
1529 if (!counter && formWrapperRef.value) {
1530 const closest = formWrapperRef.value.closest('.ivyforms-frontend')
1531 if (closest && closest.dataset.counter) {
1532 counter = closest.dataset.counter
1533 }
1534 }
1535 if (counter) {
1536 const outerWrapper = document.getElementById('ivyforms-app-' + counter)
1537 if (outerWrapper && outerWrapper.dataset) {
1538 postId.value = outerWrapper.dataset.postId || ''
1539 refererUrl.value = outerWrapper.dataset.referer || document.referrer || ''
1540 }
1541 }
1542 })
1543
1544 function getRequiredMessage(field: Field): string {
1545 const rm = field.requiredMessage
1546 if (typeof rm === 'string' && rm.trim() !== '') {
1547 return rm
1548 }
1549 let fieldMessage: string
1550 fieldMessage = ucFirst(field.label || field.type)
1551 fieldMessage += ` ${getLabel('required')}`
1552 return fieldMessage
1553 }
1554
1555 const ucFirst = (str: string) => (str ? str[0]?.toLocaleUpperCase() + str.slice(1) : '')
1556
1557 // Helper function to parse date string to Date object with validation
1558 const parseDateToObject = (dateStr: string, format: string): Date | null => {
1559 let day = 0,
1560 month = 0,
1561 year = 0
1562 let parts: string[] = []
1563 if (dateStr.indexOf('/') !== -1) {
1564 parts = dateStr.split('/')
1565 } else if (dateStr.indexOf('-') !== -1) {
1566 parts = dateStr.split('-')
1567 } else if (dateStr.indexOf('.') !== -1) {
1568 parts = dateStr.split('.')
1569 } else {
1570 parts = [dateStr]
1571 }
1572
1573 if (format.startsWith('MM') || format.startsWith('M')) {
1574 // MM/DD/YYYY or M/D/YYYY
1575 month = parseInt(parts[0], 10)
1576 day = parseInt(parts[1], 10)
1577 year = parseInt(parts[2], 10)
1578 } else if (format.startsWith('DD') || format.startsWith('D')) {
1579 // DD/MM/YYYY or D/M/YYYY or DD.MM.YYYY
1580 day = parseInt(parts[0], 10)
1581 month = parseInt(parts[1], 10)
1582 year = parseInt(parts[2], 10)
1583 } else if (format.startsWith('YYYY')) {
1584 // YYYY-MM-DD or YYYY-M-D
1585 year = parseInt(parts[0], 10)
1586 month = parseInt(parts[1], 10)
1587 day = parseInt(parts[2], 10)
1588 }
1589
1590 if (day && month && year) {
1591 const date = new Date(year, month - 1, day)
1592 // Validate that the date is actually valid (e.g., not Feb 31)
1593 if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
1594 return date
1595 }
1596 }
1597 return null
1598 }
1599
1600 // Get the most common label position for the form
1601 const getFormLabelPosition = (): 'left' | 'right' | 'top' => {
1602 if (!formFields.value || formFields.value.length === 0) {
1603 return 'top'
1604 }
1605
1606 // Count label positions
1607 const positionCounts = formFields.value.reduce((counts, field) => {
1608 const position = field.labelPosition || 'top'
1609 counts[position] = (counts[position] || 0) + 1
1610 return counts
1611 }, {})
1612
1613 // Find the most common position
1614 const mostCommonPosition = Object.keys(positionCounts).reduce((a, b) =>
1615 positionCounts[a] > positionCounts[b] ? a : b,
1616 )
1617
1618 // Convert to Element Plus format
1619 switch (mostCommonPosition) {
1620 case 'left':
1621 return 'left'
1622 case 'right':
1623 return 'right'
1624 case 'top':
1625 case 'inside':
1626 default:
1627 return 'top'
1628 }
1629 }
1630
1631 // Validate form and submit
1632 const validateAndSubmit = async () => {
1633 if (!formRef.value || isSubmitting.value) return
1634 isSubmitting.value = true
1635
1636 await formRef.value.validate(
1637 async (valid: boolean, errors: Record<string, { message: string }[]>) => {
1638 if (valid) {
1639 // Handle reCAPTCHA v3 execution before form submission
1640 const hasRecaptchaField = formFields.value.some((field) => field.type === 'recaptcha')
1641
1642 if (hasRecaptchaField) {
1643 // Find reCAPTCHA field component ref
1644 const recaptchaFieldRef = formFieldsCollectorRefs.value?.find(
1645 (ref: ComponentPublicInstance): ref is RecaptchaFieldComponent =>
1646 ref &&
1647 (ref as RecaptchaFieldComponent).$props &&
1648 (ref as RecaptchaFieldComponent).$props.field &&
1649 (ref as RecaptchaFieldComponent).$props.field.type === 'recaptcha',
1650 )
1651 if (recaptchaFieldRef && recaptchaFieldRef.executeRecaptcha) {
1652 try {
1653 // For v3, this will execute the reCAPTCHA and get a token
1654 // For v2, this will return the current token
1655 const token = await recaptchaFieldRef.executeRecaptcha()
1656
1657 // Update the form value with the token
1658 const fieldKey = `recaptcha_${recaptchaFieldRef.$props.field.fieldIndex}`
1659 formValues.value[fieldKey] = token
1660 } catch (error) {
1661 console.error('reCAPTCHA execution failed:', error)
1662 isSubmitting.value = false
1663 return
1664 }
1665 }
1666 }
1667
1668 // Handle Turnstile validation before form submission
1669 const hasTurnstileField = formFields.value.some((field) => field.type === 'turnstile')
1670
1671 if (hasTurnstileField) {
1672 // Find Turnstile field component ref
1673 const turnstileFieldRef = formFieldsCollectorRefs.value?.find(
1674 (ref: ComponentPublicInstance): ref is TurnstileFieldComponent =>
1675 ref &&
1676 (ref as TurnstileFieldComponent).$props &&
1677 (ref as TurnstileFieldComponent).$props.field &&
1678 (ref as TurnstileFieldComponent).$props.field.type === 'turnstile',
1679 )
1680
1681 if (turnstileFieldRef) {
1682 const fieldKey = `turnstile_${turnstileFieldRef.$props.field.fieldIndex}`
1683 const turnstileToken = formValues.value[fieldKey]
1684
1685 // Check if Turnstile token exists
1686 if (!turnstileToken || turnstileToken.toString().trim() === '') {
1687 // Set error for the Turnstile field
1688 fieldErrors.value = {
1689 ...fieldErrors.value,
1690 [fieldKey]: getLabel('turnstile_complete_verification'),
1691 }
1692 scrollToFirstInvalidField()
1693 isSubmitting.value = false
1694 return
1695 }
1696 }
1697 }
1698
1699 // Handle hCaptcha validation before form submission
1700 const hasHCaptchaField = formFields.value.some((field) => field.type === 'hcaptcha')
1701
1702 if (hasHCaptchaField) {
1703 // Find hCaptcha field component ref
1704 const hcaptchaFieldRef = formFieldsCollectorRefs.value?.find(
1705 (ref: ComponentPublicInstance): ref is HCaptchaFieldComponent =>
1706 ref &&
1707 (ref as HCaptchaFieldComponent).$props &&
1708 (ref as HCaptchaFieldComponent).$props.field &&
1709 (ref as HCaptchaFieldComponent).$props.field.type === 'hcaptcha',
1710 )
1711
1712 if (hcaptchaFieldRef) {
1713 const fieldKey = `hcaptcha_${hcaptchaFieldRef.$props.field.fieldIndex}`
1714 const hcaptchaToken = formValues.value[fieldKey]
1715
1716 // Check if hCaptcha token exists
1717 if (!hcaptchaToken || hcaptchaToken.toString().trim() === '') {
1718 // Set error for the hCaptcha field
1719 fieldErrors.value = {
1720 ...fieldErrors.value,
1721 [fieldKey]: getLabel('hcaptcha_complete_verification'),
1722 }
1723 scrollToFirstInvalidField()
1724 isSubmitting.value = false
1725 return
1726 }
1727 }
1728 }
1729
1730 await submitForm()
1731 } else {
1732 fieldErrors.value = {} // Clear previous
1733 Object.keys(errors).forEach((fieldKey) => {
1734 const rawMsg = errors[fieldKey][0]?.message || getLabel('invalid_input')
1735 const displayMsg = rawMsg
1736 ;(fieldErrors.value as Record<string, string>)[fieldKey] = displayMsg
1737 })
1738 scrollToFirstInvalidField()
1739 isSubmitting.value = false
1740 }
1741 },
1742 )
1743 }
1744
1745 const scrollToFirstInvalidField = () => {
1746 // Try to find the first field with error
1747 // Look for error classes or aria-invalid, or use the error keys
1748 nextTick(() => {
1749 // Try by error class (Element Plus/ivyforms)
1750 const invalidField = document.querySelector(
1751 '.ivyforms-form-field .is-error input, .ivyforms-form-field .is-error textarea, .ivyforms-form-field .is-error select',
1752 )
1753 if (invalidField && typeof invalidField.scrollIntoView === 'function') {
1754 invalidField.scrollIntoView({ behavior: 'smooth', block: 'center' })
1755 if (invalidField instanceof HTMLElement && typeof invalidField.focus === 'function') {
1756 invalidField.focus({ preventScroll: true })
1757 }
1758 }
1759 })
1760 }
1761
1762 // Clear error when user starts typing
1763 const clearFieldError = (fieldKey: string) => {
1764 if ((fieldErrors.value as Record<string, unknown>)[fieldKey]) {
1765 delete (fieldErrors.value as Record<string, unknown>)[fieldKey]
1766 }
1767 // Also clear parent key for name subfields like name_<index>.nameFieldX
1768 if (fieldKey.includes('.')) {
1769 const parentKey = fieldKey.split('.')[0]
1770 if ((fieldErrors.value as Record<string, unknown>)[parentKey]) {
1771 delete (fieldErrors.value as Record<string, unknown>)[parentKey]
1772 }
1773 }
1774 }
1775
1776 // Provide the clearFieldError function to child components
1777 provide('clearFieldError', clearFieldError)
1778 // Provide reactive form values so nested fields (e.g., email confirmation) can mutate the same model
1779 provide('formValues', formValues)
1780 // Provide horizontal gap for child components to use in their internal layouts
1781 provide('horizontalGap', horizontalGapCss)
1782 provide('formPopperScopeClass', formPopperScopeClass)
1783
1784 const resetForm = () => {
1785 skipValidation.value = true
1786 formValues.value = {}
1787 nextTick(() => {
1788 skipValidation.value = false
1789 })
1790 }
1791 // Submit form
1792 const submitForm = async () => {
1793 // Clear previous errors before each submit
1794 fieldErrors.value = {}
1795
1796 // Process form values to handle website fields with prefix/suffix
1797 const processedValues = { ...formValues.value }
1798
1799 formFields.value.forEach((field) => {
1800 if (field.type === 'website') {
1801 const key = field.type + '_' + field.fieldIndex
1802 const fullValue = processedValues[key] || ''
1803 const prefix = field.inputPrefix || ''
1804 const suffix = field.inputSuffix || ''
1805
1806 let userInput = fullValue
1807
1808 // Only add prefix/suffix if the value doesn't already have them
1809 if (fullValue && (prefix || suffix)) {
1810 // Check if prefix is already present before adding
1811 if (prefix && !userInput.startsWith(prefix)) {
1812 userInput = prefix + userInput
1813 }
1814
1815 // Check if suffix is already present before adding
1816 if (suffix && !userInput.endsWith(suffix)) {
1817 userInput = userInput + suffix
1818 }
1819 }
1820
1821 processedValues[key] = userInput
1822 }
1823 })
1824
1825 try {
1826 const { data, error, status } = await request('form/submission/', {
1827 method: 'POST',
1828 headers: {
1829 'Content-Type': 'application/json',
1830 },
1831 data: {
1832 formId: formData.value.id,
1833 values: processedValues,
1834 nonce: formNonce.value,
1835 postId: postId.value,
1836 referer: refererUrl.value,
1837 },
1838 })
1839
1840 // Check for duplicate value error from backend
1841 if (data.data.data.is_duplicate) {
1842 const duplicateFields = data.data.data.duplicate_errors
1843
1844 for (const field of formFields.value) {
1845 const fieldKey = field.type + '_' + field.fieldIndex
1846
1847 if (duplicateFields[field.id]) {
1848 const fieldValue = formValues.value[fieldKey]
1849 if (fieldValue && fieldValue.toString().trim() !== '') {
1850 ;(fieldErrors.value as Record<string, string>)[fieldKey] = getLabel(
1851 'duplicate_value_error_label',
1852 ).replace('{label}', field.label?.toLowerCase() || 'value')
1853 }
1854 }
1855 }
1856 isSubmitting.value = false
1857 return
1858 }
1859
1860 // Handle response
1861 if (status !== 200 || error) {
1862 IvyMessage({
1863 type: 'error',
1864 message: getLabel('error_submitting_form') + (error ? error.message : data),
1865 })
1866 isSubmitting.value = false
1867 return
1868 }
1869
1870 // Show completed state briefly before hiding loader
1871 isSubmitCompleted.value = true
1872 await new Promise((resolve) => setTimeout(resolve, 600))
1873
1874 // Reset form or show success message
1875 resetForm()
1876
1877 isSubmitting.value = false
1878 isSubmitCompleted.value = false
1879
1880 const type = formConfirmation.value[0].type
1881 const enabled = formConfirmation.value[0].enabled
1882 const showForm = formConfirmation.value[0].showForm
1883
1884 if (!enabled) return
1885
1886 if (type === 'successMessage') {
1887 if (!showForm) {
1888 showSuccessMessage.value = true
1889 successMessage.value =
1890 data.data.data.confirmation || formConfirmation.value[0].message || getLabel('thank_you')
1891 return
1892 } else {
1893 showSuccessMessageReset.value = true
1894 successMessage.value =
1895 data.data.data.confirmation || formConfirmation.value[0].message || getLabel('thank_you')
1896
1897 // Hide the success message after 5 seconds
1898 setTimeout(() => {
1899 const messageElement = document.querySelector('.ivyforms-success-message')
1900 if (messageElement) {
1901 messageElement.classList.add('ivyforms-fade-out')
1902 }
1903 setTimeout(() => {
1904 showSuccessMessageReset.value = false
1905 }, 500) // Match the CSS transition duration
1906 }, 5000)
1907 }
1908 } else if (type === 'redirectToPage' && formConfirmation.value[0].pageUrl) {
1909 // Use pageUrl that was generated on the backend
1910 window.location.href = formConfirmation.value[0].pageUrl
1911 } else if (type === 'redirectToCustomUrl' && formConfirmation.value[0].url) {
1912 window.location.href = formConfirmation.value[0].url
1913 }
1914 } catch (error) {
1915 console.error(getLabel('error_submitting_form'), error)
1916 isSubmitting.value = false
1917 // Show error message
1918 }
1919 }
1920 </script>
1921
1922 <style lang="scss" scoped>
1923 .ivyforms-form-wrapper {
1924 width: 100%;
1925 max-width: 720px;
1926 margin: 0 auto;
1927 padding: 0 16px;
1928 box-sizing: border-box;
1929
1930 @media (max-width: 768px) {
1931 padding: 0 12px;
1932 max-width: 100%;
1933 }
1934
1935 .ivyforms-form-title {
1936 font-size: 24px;
1937 font-weight: 600;
1938 margin-bottom: 12px;
1939 color: var(--map-base-text-0);
1940 }
1941
1942 .ivyforms-form-description {
1943 font-size: 16px;
1944 margin-bottom: 24px;
1945 color: var(--map-base-text-1);
1946 }
1947
1948 .ivyforms-form-row {
1949 max-width: 100%;
1950 box-sizing: border-box;
1951
1952 // Mobile responsive - stack fields vertically on small screens
1953 @media (max-width: 768px) {
1954 flex-direction: column;
1955 gap: 8px;
1956
1957 .ivyforms-form-field {
1958 width: 100% !important;
1959 max-width: 100% !important;
1960 flex: 1 1 100% !important;
1961 }
1962 }
1963
1964 // Tablet - allow 2 columns
1965 @media (min-width: 769px) and (max-width: 1024px) {
1966 flex-wrap: wrap;
1967
1968 .ivyforms-form-field {
1969 min-width: calc(50% - 8px);
1970
1971 // If field has width > 50%, make it full width
1972 &[style*='flex: 100']*='flex: 100'],
1973 &[style*='flex: 75']*='flex: 75'],
1974 &[style*='flex: 66']*='flex: 66'] {
1975 flex: 1 1 100% !important;
1976 }
1977 }
1978 }
1979 }
1980
1981 .ivyforms-form-field {
1982 min-width: 0;
1983 max-width: 100%;
1984 box-sizing: border-box;
1985
1986 :deep(.el-input),
1987 :deep(.el-select),
1988 :deep(.el-textarea),
1989 :deep(.el-date-picker),
1990 :deep(.ivyforms-input) {
1991 max-width: 100%;
1992 box-sizing: border-box;
1993 }
1994
1995 :deep(.el-input__wrapper) {
1996 max-width: 100%;
1997 box-sizing: border-box;
1998 }
1999 }
2000
2001 .ivyforms-field {
2002 padding: 12px 0;
2003 }
2004
2005 .ivyforms-form-submit {
2006 margin-top: 12px;
2007 display: flex;
2008
2009 // Position modifiers
2010 &--default,
2011 &--left {
2012 justify-content: flex-start;
2013 }
2014
2015 &--center {
2016 justify-content: center;
2017 }
2018
2019 &--right {
2020 justify-content: flex-end;
2021 }
2022 }
2023
2024 ::v-deep(.ivyforms-success-message) {
2025 display: flex;
2026 flex-direction: column;
2027 align-items: center;
2028 justify-content: center;
2029 font-size: 20px;
2030 padding: 20px;
2031 border: 1px solid var(--map-base-brand-symbol-0);
2032 border-radius: 8px;
2033 text-align: center;
2034 transition: opacity 0.5s ease-in-out;
2035 opacity: 1;
2036
2037 &.ivyforms-fade-out {
2038 opacity: 0;
2039 }
2040
2041 .ivyforms-safe-html {
2042 display: flex;
2043 flex-direction: column;
2044 align-items: center;
2045 justify-content: center;
2046
2047 img {
2048 max-width: 100%;
2049 height: auto;
2050 margin: 10px 0;
2051 }
2052 }
2053 }
2054 }
2055 </style>
2056