PageSourceSearch

https://www.thrustflight.com/wp-content/plugins/formidable-pro/js/formidablepro.js?ver=6.35

js thrustflight.com collected 2026-09-26 04:41:15 UTC 315,796 bytes, 10,504 lines download raw bytes

1function frmProFormJS() {
2	/* globals frm_js, frmFrontForm, google, jQuery */
3	/* globals __frmDatepicker, __frmDropzone, __frmUniqueTimes */
4	/* globals __FRMLOOKUP, __FRMCALC, __FRMRULES, __FRMCURR */
5	/* globals __frmChosen, __frmHideOrShowFields, __frmDepDynamicFields */
6	/* globals __frmDepLookupFields, __frmMasks, __FRMTABLES */
7	/* globals frmCheckboxI18n */
8
9	'use strict';
10	let currentlyAddingRow = false;
11	let action = '';
12	let processesRunning = 0;
13	const lookupQueues = {};
14	let hiddenSubmitButtons = [];
15	let pendingDynamicFieldAjax = [];
16	const frmMaskInstances = new Map();
17	const pendingLookupFieldAjax = [];
18	let listWrappersOriginal = {};
19	const intlPhoneInputs = {};
20	let autoId = 0;
21
22	function setNextPage( e ) {
23		var closestButton;
24		if ( this.className.includes('frm_rootline_title') ) {
25			closestButton = this.previousElementSibling;
26			closestButton.click();
27			return;
28		}
29
30		if ( this.className.includes('frm_rootline_single') ) {
31			this.querySelector( 'input' ).click();
32			return;
33		}
34
35		/*jshint validthis:true */
36		var $thisObj = jQuery( this );
37		var thisType = $thisObj.attr( 'type' );
38		if ( thisType !== 'submit' ) {
39			e.preventDefault();
40		}
41
42		var f = $thisObj.parents( 'form' ).first(),
43			v = '',
44			d = '',
45			thisName = this.name;
46
47		if ( thisName === 'frm_prev_page' || this.className.includes('frm_prev_page') ) {
48			v = jQuery( f ).find( '.frm_next_page' ).attr( 'id' ).replace( 'frm_next_p_', '' );
49			if ( f.length ) {
50				maybeAddEmptyHiddenInputsForCheckboxes( f.get( 0 ) );
51			}
52		} else if ( thisName === 'frm_save_draft' || this.className.includes('frm_save_draft') ) {
53			d = 1;
54		} else if ( this.className.includes('frm_page_skip') ) {
55			var goingTo = $thisObj.data( 'page' );
56			var formId = jQuery( f ).find( 'input[name="form_id"]' ).val();
57			var orderField = jQuery( f ).find( 'input[name="frm_page_order_' + formId + '"]' );
58			jQuery( f ).append( '<input name="frm_last_page" type="hidden" value="' + orderField.val() + '" />' );
59
60			if ( goingTo === '' ) {
61				orderField.remove();
62			} else {
63				orderField.val( goingTo );
64			}
65		} else if ( this.className.includes('frm_page_back') ) {
66			v = $thisObj.data( 'page' );
67		}
68
69		if ( 1 === d ) {
70			resetTinyMceOnDraftSave();
71		} else {
72			resetTinyMceOnPageTurn();
73		}
74
75		jQuery( '.frm_next_page' ).val( v );
76		jQuery( '.frm_saving_draft' ).val( d );
77
78		if ( thisType !== 'submit' ) {
79			f.trigger( 'submit' );
80		}
81	}
82
83	/**
84	 * When no checkboxes are checked, no $_POST data is sent.
85	 * This causes issues when going back when drafts are enabled.
86	 * The draft gets saved when the next button is pressed, and then
87	 * gets reloaded after being unchecked because there is no $_POST data.
88	 * This adds a hidden input with no value in its place so $_POST data does get sent.
89	 *
90	 * @since 6.8.3
91	 *
92	 * @param {HTMLElement} form
93	 * @returns {void}
94	 */
95	function maybeAddEmptyHiddenInputsForCheckboxes( form ) {
96		form.querySelectorAll( '.frm_opt_container' ).forEach(
97			( optContainer ) => {
98				var checkboxes, hiddenInput;
99				checkboxes = optContainer.querySelectorAll( 'input[type="checkbox"]' );
100				if ( checkboxes.length && ! jQuery( checkboxes ).filter( ':checked' ).length ) {
101					hiddenInput = document.createElement( 'input' );
102					hiddenInput.setAttribute( 'type', 'hidden' );
103					hiddenInput.setAttribute( 'name', checkboxes[ 0 ].getAttribute( 'name' ) );
104					optContainer.append( hiddenInput );
105				}
106			}
107		);
108	}
109
110	function resetTinyMceOnDraftSave() {
111		jQuery( document ).one( 'frmFormComplete', () => {
112			jQuery( '.wp-editor-area' ).each( function() {
113				reInitializeRichText( this.id );
114			});
115		});
116	}
117
118	function resetTinyMceOnPageTurn() {
119		var removeIds = [];
120		jQuery( '.frm_form_field .wp-editor-area' ).each(
121			function() {
122				removeIds.push( this.id );
123			}
124		);
125		jQuery( document ).one( 'frmPageChanged', () => {
126			var removeIndex, removeId;
127			// Remove tinymce from RTE fields on page update so they can initialize properly when the page becomes active again
128			for ( removeIndex = 0; removeIndex < removeIds.length; ++removeIndex ) {
129				removeId = removeIds[ removeIndex ];
130				removeRichText( removeId );
131			}
132			checkConditionalLogic();
133		});
134	}
135
136	function toggleSection( e ) {
137		var $toggleContainer, togglingOn;
138
139		if ( e.key !== undefined ) {
140			if ( e.key !== ' ' && e.key !== 'Enter' ) {
141				return;
142			}
143		} else if ( e.keyCode !== undefined && e.keyCode !== 32 && e.keyCode !== 13 ) {
144			return;
145		}
146
147		e.preventDefault();
148
149		/*jshint validthis:true */
150		$toggleContainer = jQuery( this ).parent().children( '.frm_toggle_container' );
151		togglingOn       = 'none' === $toggleContainer.get( 0 ).style.display;
152
153		if ( togglingOn ) {
154			$toggleContainer.show();
155		}
156
157		triggerEvent( document, 'frmBeforeToggleSection', {
158			toggleButton: this
159		});
160
161		if ( togglingOn ) {
162			$toggleContainer.hide();
163		}
164
165		$toggleContainer.slideToggle( 'fast' );
166
167		if ( togglingOn ) {
168			this.className += ' active';
169			this.setAttribute( 'aria-expanded', 'true' );
170		} else {
171			this.className = this.className.replace( ' active', '' );
172			this.setAttribute( 'aria-expanded', 'false' );
173		}
174	}
175
176	function loadDateFields() {
177		jQuery( document ).on( 'focusin', '.frm_date', triggerDateField );
178		document.addEventListener( 'focusout', keepFloatingLabelOnDateBlur, true );
179
180		loadUniqueTimeFields();
181	}
182
183	function frmDatepickerPro( input, settings ) {
184
185		if ( input._flatpickr ) {
186			return input._flatpickr;
187		}
188
189		const _this = this;
190
191		// Init the static methods
192		this.initAccessiblity    = frmDatepickerPro.initAccessiblity;
193		this.initMonthSelector   = frmDatepickerPro.initMonthSelector;
194		this.initYearSelector    = frmDatepickerPro.initYearSelector;
195		this.parseFunctionConfig = frmDatepickerPro.parseFunctionConfig;
196		this.getDateRange        = frmDatepickerPro.getDateRange;
197		this.callbacks           = frmDatepickerPro.callbacks;
198		this.getDefaultConfigs   = frmDatepickerPro.getDefaultConfigs;
199		this.isUsingACustomTheme = frmDatepickerPro.isUsingACustomTheme;
200
201		this.getConfigs = function() {
202			if ( 'undefined' === typeof settings.datepickerJsOptions ) {
203				return _this.getDefaultConfigs( settings, input );
204			}
205			return { ..._this.getDefaultConfigs( settings, input ), ..._this.parseFunctionConfig( settings.datepickerJsOptions ) };
206		};
207		return flatpickr( input, this.getConfigs() );
208	}
209
210	frmDatepickerPro.getThemeType = function() {
211		const themeFile      = document.querySelector( 'link[href*="flatpickr/dist/themes/"]' );
212		const themeSelectors = [
213			{ name: 'frm-dark', selector: 'dark.css' },
214			{ name: 'frm-material-blue', selector: 'material_blue.css' },
215			{ name: 'frm-material-green', selector: 'material_green.css' },
216			{ name: 'frm-material-orange', selector: 'material_orange.css' },
217			{ name: 'frm-material-red', selector: 'material_red.css' },
218			{ name: 'frm-airbnb', selector: 'airbnb.css' },
219			{ name: 'frm-confetti', selector: 'confetti.css' }
220		];
221
222		if ( themeFile && themeFile.href ) {
223			for ( const theme of themeSelectors ) {
224				if ( themeFile.href.includes( theme.selector ) ) {
225					return theme.name;
226				}
227			}
228		}
229
230		return '';
231	};
232	frmDatepickerPro.themeType = frmDatepickerPro.getThemeType();
233	frmDatepickerPro.isUsingACustomTheme = frmDatepickerPro.themeType !== '';
234	frmDatepickerPro.callbacks = {};
235
236	/**
237	 * Get the style classes to copy from the form onto the calendar.
238	 *
239	 * A popup calendar is appended to the body, so it sits outside of the form and
240	 * cannot inherit either the style class or the CSS variables scoped to it. The
241	 * jQuery UI datepicker copies the form classes for the same reason, in
242	 * addFormidableClassToDatepicker.
243	 *
244	 * @since 6.35
245	 *
246	 * @param {HTMLElement} input The date input the calendar belongs to.
247	 * @returns {Array} Class names to add to the calendar container.
248	 */
249	frmDatepickerPro.getStyleClasses = function( input ) {
250		const formContainer = input.closest( '.with_frm_style' );
251		if ( ! formContainer ) {
252			return [ 'with_frm_style' ];
253		}
254
255		return [ 'with_frm_style' ].concat(
256			Array.prototype.filter.call(
257				formContainer.classList,
258				( className ) => className.startsWith( 'frm_style_' )
259			)
260		);
261	};
262
263	/**
264	 * Gets the repeater row an element sits in, so a lookup cannot escape into another row.
265	 *
266	 * Every row renders the same ids and field ids, so a document wide lookup always answers with
267	 * the first row and leaves every later row pointing at it.
268	 *
269	 * @since 6.24
270	 *
271	 * @param {HTMLElement} element An element inside the row.
272	 * @return {Element|Document} The row, or the document when the element is not in a repeater.
273	 */
274	frmDatepickerPro.getRepeaterRow = function( element ) {
275		return element.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ) || document;
276	};
277
278	/**
279	 * Finds the hidden input an inline picker writes its value into.
280	 *
281	 * The alt field selector is built from the field key alone, so inside a repeater it matches
282	 * nothing: every row appends its own index to the id. Falling back to the element's own id
283	 * covers that, scoped to the row so one row cannot answer for another.
284	 *
285	 * @since 6.24
286	 *
287	 * @param {HTMLElement} input       The element the picker is built on.
288	 * @param {string}      altSelector The alt field selector from the field settings.
289	 * @return {HTMLElement|null} The alt input, or null when there is none.
290	 */
291	frmDatepickerPro.getAltInputElement = function( input, altSelector ) {
292		const scope = frmDatepickerPro.getRepeaterRow( input );
293
294		if ( input.id ) {
295			const rowAltInput = scope.querySelector( '#' + CSS.escape( input.id + '_alt' ) );
296			if ( rowAltInput ) {
297				return rowAltInput;
298			}
299		}
300
301		return altSelector ? scope.querySelector( altSelector ) : null;
302	};
303
304	frmDatepickerPro.callbacks.onOpen = function( selectedDates, dateStr, instance ) {
305		instance.frmStyleClasses = frmDatepickerPro.getStyleClasses( instance.input );
306		instance.calendarContainer.classList.add( 'frm-datepicker', ...instance.frmStyleClasses );
307		if ( frmDatepickerPro.isUsingACustomTheme ) {
308			instance.calendarContainer.classList.add( 'frm-datepicker-custom-theme', frmDatepickerPro.themeType );
309		}
310	};
311	frmDatepickerPro.callbacks.onClose = function( selectedDates, dateStr, instance ) {
312		if ( ! instance.config.inline ) {
313			instance.calendarContainer.classList.remove( 'frm-datepicker', ...( instance.frmStyleClasses || [ 'with_frm_style' ] ) );
314			if ( frmDatepickerPro.isUsingACustomTheme ) {
315				instance.calendarContainer.classList.remove( 'frm-datepicker-custom-theme', frmDatepickerPro.themeType );
316			}
317		}
318
319		triggerEvent( instance.input, 'frmFlatpickrClosed', {
320			instance,
321		});
322	};
323
324	/**
325	 * Get the default configs for the datepicker.
326	 *
327	 * @since 6.19
328	 *
329	 * @returns {object}
330	 */
331	frmDatepickerPro.getDefaultConfigs = function( config, input ) {
332
333		this.getSettings = function() {
334			const result = {};
335			if ( config.formidable_dates || config.options ) {
336				return config;
337			}
338
339			result.formidable_dates = config;
340			result.options = config.datepickerOptions;
341			return result;
342		};
343
344		this.getDatesDisabled = function( settings ) {
345			const disabledDates = settings?.formidable_dates?.datesDisabled || settings?.options?.datesDisabled;
346
347			if ( ! Array.isArray( disabledDates ) ) {
348				return [];
349			}
350
351			return disabledDates.map( date => new Date( date + 'T00:00:00' ) );
352		};
353
354		/**
355		 * Gets the input a picker's value belongs in.
356		 *
357		 * An inline picker is built on a div, so the value it produces has to be written to the
358		 * hidden input the field actually submits. That input is the picker's alt input, which is
359		 * resolved per repeater row, so prefer it over looking one up by name.
360		 *
361		 * @since 6.24
362		 *
363		 * @param {Object} instance The Flatpickr instance.
364		 * @return {HTMLElement} The input to write to, or the element the picker was built on.
365		 */
366		this.getInstanceElement = ( instance ) => {
367			if ( ! instance.config.inline || 'INPUT' === instance.element.nodeName ) {
368				return instance.element;
369			}
370
371			if ( instance.config.altInputElement ) {
372				return instance.config.altInputElement;
373			}
374
375			const fieldId = instance.element.dataset.fieldId;
376
377			// A repeater row names its inputs item_meta[section][row][field], so an exact
378			// item_meta[field] match only ever finds a field that is not in a repeater, and a
379			// document wide lookup would answer with the first row for every row.
380			const scope = frmDatepickerPro.getRepeaterRow( instance.element );
381
382			return scope.querySelector( `input[name="item_meta[${ fieldId }]"], input[name$="][${ fieldId }]"]` ) || instance.element;
383		};
384
385
386		/**
387		 * COMPATIBILITY PATH. Do not fix bugs here.
388		 *
389		 * Date ranges belong to the Dates add-on, which now owns this as
390		 * frmDatepickerInstance.syncRangeFields. This copy only runs for a site that has updated
391		 * Pro but not yet the add-on, so that its ranges keep working rather than breaking on
392		 * upgrade. Treat it as frozen: fix range behaviour in the add-on, and delete this once the
393		 * add-on version that owns it has been out long enough.
394		 *
395		 * @since 6.24
396		 *
397		 * @param {string} mode The mode of the datepicker.
398		 * @param {object} instance The instance of the datepicker.
399		 * @param {string} dateStr The date string.
400		 * @param {array} selectedDates The selected dates.
401		 * @returns {void}
402		 */
403		this.updateRangeFieldsOnChange = ( mode, instance, dateStr, selectedDates ) => {
404			if ( 'range' !== mode ) {
405				return;
406			}
407
408			// The pair is keyed by the start field's id. On the end field that is
409			// data-range-start-field-id, so it must win over the field's own id.
410			const fieldId                = input.dataset.rangeStartFieldId || input.dataset.fieldId;
411			const [ startDate, endDate ] = selectedDates;
412			const instanceElement        = this.getInstanceElement( instance );
413
414			// Every row of a repeater renders the same data-field-id, so a document wide lookup would
415			// always resolve to the first row and leave every later row unsplit. Search inside the row
416			// when there is one.
417			const scope = instanceElement.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ) || document;
418
419			// The name suffix match is the fallback for a half of the pair that is carried over as a
420			// hidden input, which is how an earlier page of a multi page form is preserved. It covers
421			// both item_meta[123] and the item_meta[section][row][123] form a repeater uses.
422			const startDateField = scope.querySelector( `input[data-field-id="${ fieldId }"]` )
423				|| scope.querySelector( `input[type="hidden"][name$="[${ fieldId }]"]` );
424
425			// Resolve the end field whatever the selection state is. Skipping it while only one date is
426			// picked used to leave the two fields holding different ranges. The name suffix match is
427			// the same multi page fallback the start field gets, keyed off the end field id the start
428			// field carries, since a half preserved from another page has no data attributes.
429			const endFieldId = instanceElement.dataset.rangeEndFieldId;
430			const endDateField = scope.querySelector( `input[data-range-start-field-id="${ fieldId }"]` )
431				|| ( endFieldId ? scope.querySelector( `input[type="hidden"][name$="[${ endFieldId }]"]` ) : null );
432
433			// A range field is not always paired, and the partner is not always rendered yet. Split
434			// this field's own value anyway, so a visible input never keeps the joined range string.
435			if ( ! startDateField || ! endDateField ) {
436				if ( 'undefined' !== typeof endDate ) {
437					instanceElement.value = flatpickr.formatDate( input.dataset.rangeStartFieldId ? endDate : startDate, settings.options.fpDateFormat );
438				}
439				return;
440			}
441
442			/**
443			 * Points one half of the pair at its own date.
444			 *
445			 * The partner does not always have a picker. On a multi page form the other half arrives as
446			 * a plain hidden input, and reading .setDate off it used to throw before the value was
447			 * written, which left a stale date behind and saved a range nobody picked.
448			 *
449			 * @param {HTMLElement} field The field to write to.
450			 * @param {Date}        date  The date that half represents.
451			 * @returns {void}
452			 */
453			const setHalf = ( field, date ) => {
454				if ( field._flatpickr ) {
455					field._flatpickr.setDate( dateStr );
456				}
457
458				field.value = flatpickr.formatDate( date, settings.options.fpDateFormat );
459			};
460
461			instanceElement.dataset.rangeValue = dateStr;
462
463			// The range is only half picked. Point both halves of the pair at that one date, so
464			// neither field is left holding a range from an earlier selection. Blanking the end field
465			// instead would not survive, since the add-on refreshes every picker after this runs and
466			// Flatpickr rewrites each input from its own selected dates.
467			if ( 'undefined' === typeof endDate ) {
468				const partialValue = flatpickr.formatDate( startDate, settings.options.fpDateFormat );
469
470				startDateField.dataset.rangeValue = dateStr;
471				endDateField.dataset.rangeValue   = dateStr;
472
473				if ( instanceElement !== startDateField && startDateField._flatpickr ) {
474					startDateField._flatpickr.setDate( startDate );
475				}
476				if ( instanceElement !== endDateField && endDateField._flatpickr ) {
477					endDateField._flatpickr.setDate( startDate );
478				}
479
480				startDateField.value = partialValue;
481				endDateField.value   = partialValue;
482				return;
483			}
484
485			if ( instanceElement === startDateField ) {
486				instanceElement.value           = flatpickr.formatDate( startDate, settings.options.fpDateFormat );
487				endDateField.dataset.rangeValue = dateStr;
488
489				setHalf( endDateField, endDate );
490
491				// Trigger the change event to end range field to update the calculation
492				endDateField.dispatchEvent( new Event( 'change', { bubbles: true } ) );
493			}
494
495			if ( instanceElement === endDateField ) {
496				instanceElement.value             = flatpickr.formatDate( endDate, settings.options.fpDateFormat );
497				startDateField.dataset.rangeValue = dateStr;
498
499				setHalf( startDateField, startDate );
500			}
501		};
502
503		const settings      = this.getSettings();
504		const mode          = input.classList.contains( 'frm_date_range' ) || ( settings.formidable_dates && settings.formidable_dates.isRangeEnabled ) ? 'range' : 'single';
505		const inline        = settings.formidable_dates && settings.formidable_dates.inline;
506		const showMonths    = inline && 'range' === mode ? 2 : 1;
507
508		const { min, max }  = frmDatepickerPro.getDateRange( settings );
509		const disabledDates = this.getDatesDisabled( settings );
510		const firstDay      = settings.options.firstDay || 1;
511
512		const flatpickrOptions = {
513			dateFormat: settings.options.fpDateFormat,
514			inline: settings.formidable_dates && settings.formidable_dates.inline,
515			minDate: min,
516			maxDate: max,
517			monthSelectorType: 'true' === settings.options.changeMonth ? 'dropdown' : 'static',
518			changeYear: settings.options.changeYear,
519			closeOnSelect: true,
520			showMonths,
521			locale: {
522				...flatpickr.l10ns[ frmDatepickerPro.getFlatpickrLocaleKey( settings.locale || settings.options.locale ) ],
523				firstDayOfWeek: firstDay,
524			},
525			mode,
526			disable: [
527				function( date ) {
528					if ( ! settings.formidable_dates ) {
529						return false;
530					}
531
532					// The Dates add-on owns these settings and ranks its three rules in one place, so
533					// defer to it when it is active. Listing blackout dates as separate disable entries
534					// could not work with it, since Flatpickr treats each entry as its own veto and an
535					// excepted date would never be able to re-enable a blacked out day.
536					if ( 'function' === typeof window.frmDatepickerInstance?.isDateAllowed ) {
537						return ! window.frmDatepickerInstance.isDateAllowed( date, settings.formidable_dates );
538					}
539
540					return settings.formidable_dates.daysEnabled && -1 === settings.formidable_dates.daysEnabled.indexOf( date.getDay() );
541				},
542				function( date ) {
543					// The frm_selectable_dates and frm_dates_selectable_response filters supply a
544					// JavaScript expression that only jQuery UI's beforeShowDay used to evaluate, so a
545					// site relying on either filter lost it the moment Flatpickr was switched on.
546					if ( ! settings.formidable_dates || 'function' !== typeof window.frmDatepickerInstance?.passesSelectableResponse ) {
547						return false;
548					}
549
550					return ! window.frmDatepickerInstance.passesSelectableResponse( date, settings.formidable_dates );
551				},
552				...( 'function' === typeof window.frmDatepickerInstance?.isDateAllowed ? [] : ( disabledDates ? disabledDates : [] ) )
553			],
554			onReady( selectedDates, dateStr, instance ) {
555				// The Dates add-on owns this. Its own copy is only missing on a site that has not
556				// updated it yet, which still has to get its saved ranges back.
557				const setDefaultRange = 'function' === typeof window.frmDatepickerInstance?.setDefaultRangeValue
558					? window.frmDatepickerInstance.setDefaultRangeValue
559					: frmDatepickerPro.setDefaultRangeValue;
560
561				setTimeout( () => setDefaultRange( instance.element ), 0 );
562				if ( instance.config.inline ) {
563					instance.calendarContainer.classList.add( 'frm-datepicker', 'with_frm_style', 'frm_date_inline' );
564					if ( frmDatepickerPro.isUsingACustomTheme ) {
565						instance.calendarContainer.classList.add( 'frm-datepicker-custom-theme', frmDatepickerPro.themeType );
566					}
567				}
568				frmDatepickerPro.initMonthSelector( instance );
569				frmDatepickerPro.initYearSelector( instance );
570				frmDatepickerPro.initAccessiblity( instance );
571			},
572			onChange: ( selectedDates, dateStr, instance ) => {
573				if ( instance.config.inline ) {
574					const altInput = instance.config.altInputElement || instance.altInput;
575					if ( altInput ) {
576						altInput.value = dateStr;
577						altInput.dispatchEvent( new Event( 'change', { bubbles: true } ) );
578					}
579				}
580
581				// Flatpickr only keeps the hooks built in this object, so an onChange handler passed in
582				// through the field settings (the Dates add-on sets one) never reaches it. Call it
583				// explicitly. It runs before the range sync because refreshing a picker makes Flatpickr
584				// rewrite the input from its own selected dates, which for a range is the whole range
585				// string. Syncing afterwards puts the single date back on each half of the pair.
586				//
587				// The add-on's handler was unreachable until this call existed, so the version that
588				// predates it reads a variable that is out of scope there and throws every time. That
589				// version never ran this code at all, so skip it rather than start raising errors on a
590				// site whose only change was updating Pro. getFieldIdFromField is what it reaches for,
591				// and the add-on that owns it is the first one whose handler survives being called.
592				const addOnHandlesOnChange = 'function' === typeof window.frmDatepickerInstance?.getFieldIdFromField;
593
594				if ( addOnHandlesOnChange && settings.options && 'function' === typeof settings.options.onChange ) {
595					try {
596						settings.options.onChange( selectedDates, dateStr, instance );
597					} catch ( error ) {
598						// Nothing below depends on it, so a handler that throws must not take the range
599						// syncing down with it.
600						if ( window.console && window.console.error ) {
601							window.console.error( error );
602						}
603					}
604				}
605
606				if ( 'range' === mode ) {
607					// Same story as onReady: the add-on owns the split, this falls back to Pro's own
608					// copy only while the add-on is behind.
609					if ( 'function' === typeof window.frmDatepickerInstance?.syncRangeFields ) {
610						window.frmDatepickerInstance.syncRangeFields( {
611							input,
612							instanceElement: this.getInstanceElement( instance ),
613							dateStr,
614							selectedDates,
615							dateFormat: settings.options.fpDateFormat
616						} );
617					} else {
618						this.updateRangeFieldsOnChange( mode, instance, dateStr, selectedDates );
619					}
620				}
621			},
622			onOpen( selectedDates, dateStr, instance ) {
623				frmDatepickerPro.callbacks.onOpen( selectedDates, dateStr, instance );
624			},
625			onClose: ( selectedDates, dateStr, instance ) => {
626				frmDatepickerPro.callbacks.onClose( selectedDates, dateStr, instance );
627
628				// Closing on an unfinished range is Flatpickr discarding the selection: it empties
629				// its own input and forgets the date. The other half of the pair is not its input
630				// though, so without this it keeps a date the range never ended up using. The add-on
631				// owns both halves, so hand it the close.
632				if ( 'range' === mode && 'function' === typeof window.frmDatepickerInstance?.clearIncompleteRange ) {
633					window.frmDatepickerInstance.clearIncompleteRange( {
634						input,
635						instanceElement: this.getInstanceElement( instance ),
636						selectedDates
637					} );
638				}
639			},
640			// jQuery UI drew the same calendar on every device. Flatpickr hands a phone the browser's
641			// native date input instead, and that input carries over only minDate and maxDate, so the
642			// disable predicate above and range mode would both be silently dropped there.
643			disableMobile: true,
644			shorthandCurrentMonth: true,
645			altInputClass: ''
646		};
647
648		if ( settings.formidable_dates && settings.formidable_dates.inline ) {
649			flatpickrOptions.altInput = true;
650			flatpickrOptions.altInputElement = frmDatepickerPro.getAltInputElement( input, settings.options.altField );
651		}
652
653		return flatpickrOptions;
654	};
655
656
657	/**
658	 * Locale codes whose Flatpickr l10n key is not the code the date field stores.
659	 *
660	 * The field's locale setting uses jQuery UI's codes, and Flatpickr names a number of the same
661	 * languages differently, so a straight lookup silently falls back to English for these. The
662	 * value is the key the matching l10n file registers on flatpickr.l10ns, which is not always the
663	 * file's own name either. Entries with no Flatpickr equivalent at all are left out on purpose,
664	 * since English is all that is available for them.
665	 *
666	 * @since 6.35
667	 *
668	 * @type {object}
669	 */
670	frmDatepickerPro.localeKeyMap = {
671		'ar-DZ': 'ar',
672		'ca': 'ca',
673		'cy-GB': 'cy',
674		'de-AT': 'at',
675		'el': 'gr',
676		'fr-CA': 'fr',
677		'fr-CH': 'fr',
678		'kk': 'kz',
679		'nb': 'no',
680		'pt-BR': 'pt',
681		'sr': 'sr',
682		'sr-SR': 'sr',
683		'vi': 'vn',
684		'zh-CN': 'zh',
685		'zh-HK': 'zh_tw',
686		'zh-TW': 'zh_tw'
687	};
688
689	/**
690	 * Gets the key a locale's translations are registered under on flatpickr.l10ns.
691	 *
692	 * @since 6.35
693	 *
694	 * @param {string} locale The locale stored on the date field.
695	 * @returns {string}
696	 */
697	frmDatepickerPro.getFlatpickrLocaleKey = function( locale ) {
698		if ( ! locale ) {
699			return 'default';
700		}
701
702		return frmDatepickerPro.localeKeyMap[ locale ] || locale;
703	};
704
705	/**
706	 * COMPATIBILITY PATH. Do not fix bugs here.
707	 *
708	 * Superseded by frmDatepickerInstance.setDefaultRangeValue in the Dates add-on. Kept so a site
709	 * running an older add-on against this version of Pro still gets its saved ranges back onto the
710	 * calendar. Frozen, see updateRangeFieldsOnChange above.
711	 *
712	 * @since 6.24
713	 *
714	 * @param {object} input
715	 * @returns {void}
716	 */
717	frmDatepickerPro.setDefaultRangeValue = function( instanceElement ) {
718		let input = instanceElement;
719
720		if ( instanceElement._flatpickr.config.inline ) {
721			const fieldId = instanceElement.dataset.fieldId;
722			input = document.querySelector( `input[name="item_meta[${ fieldId }]"]` );
723			if ( ! input ) {
724				const container = instanceElement.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' );
725				if ( container ) {
726					input = container.querySelector( `input[name^="item_meta[${ fieldId }]["]` );
727				}
728			}
729			if ( ! input ) {
730				return;
731			}
732			input.dataset.fieldId = fieldId;
733		}
734
735		if ( ! input.classList.contains( 'frm_date_range' ) ) {
736			return null;
737		}
738
739		// Scope to the repeater row for the same reason updateRangeFieldsOnChange does: every row
740		// carries the same data-field-id, so a document wide lookup resolves to the first row.
741		const scope = input.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ) || document;
742
743		// On a multi page form the half that lives on another page is preserved as a bare hidden
744		// input carrying neither data attribute, so fall back to matching its name. Without this the
745		// pair cannot be resolved and the date already picked on the other page is not shown here.
746		const findHalf = ( selector, fieldId ) => scope.querySelector( selector ) || ( fieldId ? scope.querySelector( `input[type="hidden"][name$="[${ fieldId }]"]` ) : null );
747
748		const startDate = input.dataset.rangeStartFieldId ? findHalf( `input[data-field-id="${ input.dataset.rangeStartFieldId }"]`, input.dataset.rangeStartFieldId ) : input;
749		const endDate = input.dataset.rangeStartFieldId ? input : findHalf( `input[data-range-start-field-id="${ input.dataset.fieldId }"]`, input.dataset.rangeEndFieldId );
750
751		// A range field can be left unpaired, for instance when its partner field was deleted. Reading
752		// a value off either half then throws and the datepicker never finishes setting up.
753		if ( ! startDate || ! endDate ) {
754			return;
755		}
756
757		const startDateValue = startDate.value;
758		const endDateValue = endDate.value;
759
760		// Flatpickr parses a range string by splitting it on the separator of the active locale, so a
761		// hardcoded ' to ' is unreadable in every other language.
762		const rangeValue = startDateValue + frmDatepickerPro.getRangeSeparator( instanceElement._flatpickr ) + endDateValue;
763
764		if ( instanceElement._flatpickr.config.inline ) {
765			instanceElement._flatpickr.setDate( rangeValue );
766			startDate.value = startDateValue;
767			endDate.value = endDateValue;
768			return;
769		}
770
771		// When the inline datepicker is enabled, the range’s default value is controlled by the “Start Date” field.
772		// If the Flatpickr instance for “Start Date” isn’t available, setDefaultRangeValue is executed for the “End Date” field when the "Display Inline" is activated - the default dates are already updated by the "Start Date" field
773		if ( 'undefined' === typeof startDate._flatpickr ) {
774			return;
775		}
776
777		startDate._flatpickr.setDate( rangeValue );
778		startDate.value = startDateValue;
779		if ( endDate._flatpickr ) {
780			endDate._flatpickr.setDate( rangeValue );
781		}
782		endDate.value = endDateValue;
783	};
784
785	/**
786	 * Gets the range separator Flatpickr uses to join and split a range string.
787	 *
788	 * @since 6.35
789	 *
790	 * @param {object} instance The Flatpickr instance.
791	 * @returns {string}
792	 */
793	frmDatepickerPro.getRangeSeparator = function( instance ) {
794		return instance?.l10n?.rangeSeparator || ' to ';
795	};
796
797	/**
798	 * Initialize the month selector for the datepicker.
799	 *
800	 * @since 6.19
801	 *
802	 * @param {object} instance
803	 * @returns {void}
804	 */
805	frmDatepickerPro.initMonthSelector = function( instance ) {
806		if ( 'static' !== instance.config.monthSelectorType ) {
807			return;
808		}
809		instance.calendarContainer.classList.add( 'frm-date-no-month-select' );
810	};
811
812	/**
813	 * Initialize the year selector for the datepicker.
814	 *
815	 * @since 6.19
816	 *
817	 * @param {object} instance
818	 * @returns {void}
819	 */
820	frmDatepickerPro.initYearSelector = function( instance ) {
821		if ( 'false' !== instance.config.changeYear ) {
822			return;
823		}
824		instance.calendarContainer.classList.add( 'frm-date-no-year-select' );
825	};
826
827	/**
828	 * Initialize the accessibility for the datepicker.
829	 *
830	 * @since 6.19
831	 *
832	 * @param {object} instance
833	 * @returns {void}
834	 */
835	frmDatepickerPro.initAccessiblity = function( instance ) {
836
837		/**
838		 * Initialize the accessibility for the datepicker.
839		 *
840		 * @since 6.19
841		 *
842		 * @returns {void}
843		 */
844		this.init = function() {
845			instance.calendarContainer.setAttribute( 'role', 'dialog' );
846			instance.calendarContainer.setAttribute( 'tabindex', '0' );
847			this.prevButton();
848			this.nextButton();
849		};
850
851		/**
852		 * Initialize the previous button for the datepicker.
853		 *
854		 * @since 6.19
855		 *
856		 * @returns {void}
857		 */
858		this.prevButton = function(  ) {
859			const prevArrow = instance.calendarContainer.querySelector( '.flatpickr-prev-month' );
860			if ( null === prevArrow ) {
861				return;
862			}
863			prevArrow.setAttribute( 'tabindex', '0' );
864			prevArrow.setAttribute( 'role', 'button' );
865			prevArrow.addEventListener( 'keydown', ( event ) => {
866				if ( event.key === 'Enter' ) {
867					prevArrow.click();
868				}
869			});
870		};
871
872		/**
873		 * Initialize the next button for the datepicker.
874		 *
875		 * @since 6.19
876		 *
877		 * @returns {void}
878		 */
879		this.nextButton = function(  ) {
880			const nextArrow = instance.calendarContainer.querySelector( '.flatpickr-next-month' );
881			if ( null === nextArrow ) {
882				return;
883			}
884			nextArrow.setAttribute( 'tabindex', '0' );
885			nextArrow.setAttribute( 'role', 'button' );
886			nextArrow.addEventListener( 'keydown', ( event ) => {
887				if ( event.key === 'Enter' ) {
888					nextArrow.click();
889				}
890			});
891		};
892
893		this.init();
894	};
895
896	/**
897	 * Get the date range for the datepicker.
898	 *
899	 * @since 6.19
900	 *
901	 * @param {object} optionsData
902	 * @returns {object}
903	 */
904	frmDatepickerPro.getDateRange = function( optionsData ) {
905		const dates = {
906			min: null,
907			max: null,
908		};
909		const [ minYear, maxYear ] = optionsData.options.yearRange.split( ':' );
910
911		dates.min = new Date( minYear + '-01-01T00:00:00' );
912		dates.max = new Date( maxYear + '-12-31T23:59:59' );
913
914		if ( ! optionsData.formidable_dates ) {
915			return dates;
916		}
917
918		if ( optionsData.formidable_dates.maximum_date_cond ) {
919			if ( optionsData.options.maxDate ) {
920				dates.max = optionsData.options.maxDate;
921			} else {
922				dates.max = frmDatepickerPro.resolveDateLimit( optionsData.formidable_dates.maximum_date_cond, optionsData.formidable_dates.maximum_date_val, dates.max );
923			}
924		}
925		if ( optionsData.formidable_dates.minimum_date_cond ) {
926			if ( optionsData.options.minDate ) {
927				dates.min = optionsData.options.minDate;
928			} else {
929				dates.min = frmDatepickerPro.resolveDateLimit( optionsData.formidable_dates.minimum_date_cond, optionsData.formidable_dates.minimum_date_val, dates.min );
930			}
931		}
932
933		return dates;
934	};
935
936	/**
937	 * Resolves a minimum or maximum limit from the Dates add-on settings.
938	 *
939	 * Only reached when the add-on has not already worked the limit out, which happens when the
940	 * datepicker is built before the add-on has run over the field.
941	 *
942	 * @since 6.35
943	 *
944	 * @param {string} condition   How the limit is set: a specific date, today, or field_<key>.
945	 * @param {string} offsetValue The specific date, or the offset to apply to today.
946	 * @param {object} fallback    Limit to keep when the condition cannot be resolved here.
947	 * @returns {object|string}
948	 */
949	frmDatepickerPro.resolveDateLimit = function( condition, offsetValue, fallback ) {
950		// A limit measured from another date field depends on what that field currently holds, which
951		// only the Dates add-on tracks. Guessing an offset from today would invent a wrong limit.
952		if ( 0 === condition.indexOf( 'field_' ) ) {
953			return fallback;
954		}
955
956		// A specific date is already a date, there is no offset to apply.
957		if ( 'today' !== condition ) {
958			return offsetValue;
959		}
960
961		// Offsets are the add-on's own setting, so prefer its parser and keep one definition of what
962		// "1 week" means. Older versions of the add-on do not expose it, so fall back to the local
963		// copy below rather than dropping the limit for anyone who updated only this plugin.
964		if ( 'function' === typeof window.frmDatepickerInstance?.applyDateOffset ) {
965			// An offset carrying no unit, an empty one included, means today itself.
966			return window.frmDatepickerInstance.applyDateOffset( new Date(), offsetValue );
967		}
968
969		const offsetDate = frmDatepickerPro.parseOffsetDate( offsetValue );
970
971		return offsetDate instanceof Date ? offsetDate : new Date();
972	};
973
974	/**
975	 * Parse the offset date from the datepickerJsOptions.
976	 *
977	 * Kept only for the case where the Dates add-on is older than the version that shares its own
978	 * parser. When both plugins are current, resolveDateLimit uses the add-on's instead, so the two
979	 * cannot disagree about what an offset means.
980	 *
981	 * @since 6.19
982	 *
983	 * @param {string} date
984	 * @returns {object|string}
985	 */
986	frmDatepickerPro.parseOffsetDate = function( date ) {
987		if ( ! date ) {
988			return date;
989		}
990
991		// Sign optional, and weeks and single letter units accepted, matching what the add-on allows.
992		const match = String( date ).toLowerCase().replace( /\s+/g, '' )
993			.match( /^([+-]?\d+)(d|day|days|w|week|weeks|m|month|months|y|year|years)$/ );
994
995		if ( ! match ) {
996			return date;
997		}
998
999		const amount      = parseInt( match[ 1 ], 10 );
1000		const unit        = match[ 2 ];
1001		const currentDate = new Date();
1002
1003		if ( unit.startsWith( 'd' ) ) {
1004			currentDate.setDate( currentDate.getDate() + amount );
1005		} else if ( unit.startsWith( 'w' ) ) {
1006			currentDate.setDate( currentDate.getDate() + ( 7 * amount ) );
1007		} else if ( unit.startsWith( 'm' ) ) {
1008			currentDate.setMonth( currentDate.getMonth() + amount );
1009		} else if ( unit.startsWith( 'y' ) ) {
1010			currentDate.setFullYear( currentDate.getFullYear() + amount );
1011		}
1012
1013		return currentDate;
1014	};
1015
1016	/**
1017	 * Parse the function config from the datepickerJsOptions.
1018	 *
1019	 * @since 6.19
1020	 *
1021	 * @param {string} config
1022	 * @returns {object}
1023	 */
1024	frmDatepickerPro.parseFunctionConfig = function( config ) {
1025		const parsed = JSON.parse( config );
1026
1027		const stringToFunction = ( str ) => {
1028			if ( typeof str === 'string' && str.startsWith( 'function' ) ) {
1029				return new Function( 'return ' + str )();
1030			}
1031			return str;
1032		};
1033
1034		Object.keys( parsed ).forEach( key => {
1035			if ( Array.isArray( parsed[ key ] ) ) {
1036				parsed[ key ] = parsed[ key ].map( item => stringToFunction( item ) );
1037			} else {
1038				parsed[ key ] = stringToFunction( parsed[ key ] );
1039			}
1040		});
1041
1042		return parsed;
1043	};
1044
1045	/**
1046	 * Check if the flatpickr library is available.
1047	 *
1048	 * @since 6.19
1049	 */
1050	frmDatepickerPro.useFlatpickr = () => window.frm_js && window.frm_js.datepickerLibrary === 'flatpickr';
1051
1052	function triggerDateField() {
1053		/*jshint validthis:true */
1054		if ( this.className.includes('frm_custom_date') || typeof __frmDatepicker === 'undefined' ) {
1055			return;
1056		}
1057
1058		var dateFields = __frmDatepicker,
1059			id = this.id,
1060			idParts = id.split( '-' ),
1061			altID = '';
1062
1063		if ( isRepeatingFieldByName( this.name ) ) {
1064			altID = 'input[id^="' + idParts[ 0 ] + '"]';
1065		} else {
1066			altID = 'input[id^="' + idParts.join( '-' ) + '"]';
1067		}
1068
1069		var optKey = 0;
1070		for ( var i = 0; i < dateFields.length; i++ ) {
1071			if ( dateFields[ i ].triggerID === '#' + id || dateFields[ i ].triggerID == altID ) {
1072				optKey = i;
1073				break;
1074			}
1075		}
1076
1077		if ( dateFields[ optKey ].options.defaultDate !== '' ) {
1078			dateFields[ optKey ].options.defaultDate = new Date( dateFields[ optKey ].options.defaultDate );
1079		}
1080
1081		if ( frmDatepickerPro.useFlatpickr() ) {
1082			new frmDatepickerPro( this, dateFields[ optKey ] );
1083		} else {
1084			// Legacy datepicker.
1085			// TO DO: Remove this once the legacy datepicker is removed.
1086			dateFields[ optKey ].options.beforeShow = frmProForm.addFormidableClassToDatepicker;
1087			dateFields[ optKey ].options.onClose = frmProForm.removeFormidableClassFromDatepicker;
1088
1089			jQuery( this ).datepicker( jQuery.extend(
1090				{},
1091				jQuery.datepicker.regional[ dateFields[ optKey ].locale ],
1092				dateFields[ optKey ].options
1093			));
1094		}
1095	}
1096
1097	function loadDropzones( repeatRow ) {
1098		if ( typeof __frmDropzone === 'undefined' ) {
1099			return;
1100		}
1101
1102		var uploadFields = __frmDropzone;
1103		for ( var i = 0; i < uploadFields.length; i++ ) {
1104			loadDropzone( i, repeatRow );
1105		}
1106	}
1107
1108	function loadDropzone( i, repeatRow ) {
1109		var field, max, uploadedCount, form,
1110			uploadFields = __frmDropzone,
1111			uploadField = uploadFields[ i ],
1112			selector = '#' + uploadField.htmlID + '_dropzone',
1113			fieldName = uploadField.fieldName;
1114
1115		let label = uploadField.label;
1116
1117		if ( repeatRow !== undefined && selector.includes('-0_dropzone') ) {
1118			selector = selector.replace( '-0_dropzone', '-' + repeatRow + '_dropzone' );
1119			fieldName = fieldName.replace( '[0]', '[' + repeatRow + ']' );
1120			label = label.replace( /-0$/, '-' + repeatRow );
1121			delete uploadField.mockFiles;
1122		}
1123
1124		field = jQuery( selector );
1125		if ( field.length < 1 || field.hasClass( 'dz-clickable' ) || field.hasClass( 'dz-started' ) ) {
1126			return;
1127		}
1128
1129		max = uploadField.maxFiles;
1130		if ( uploadField.mockFiles !== undefined ) {
1131			uploadedCount = uploadField.mockFiles.length;
1132			if ( max > 0 ) {
1133				max = max - uploadedCount;
1134			}
1135		}
1136
1137		form = field.closest( 'form' );
1138		uploadField = uploadFields[ i ];
1139
1140		const dropzoneConfig = {
1141			url: getAjaxUrl( form.get( 0 ) ),
1142			headers: {
1143				'Frm-Dropzone': 1
1144			},
1145			addRemoveLinks: false,
1146			paramName: field.attr( 'id' ).replace( '_dropzone', '' ),
1147			maxFilesize: uploadField.maxFilesize,
1148			minFilesize: uploadField.minFilesize,
1149			maxFiles: max,
1150			uploadMultiple: uploadField.uploadMultiple,
1151			hiddenInputContainer: field.parent()[0],
1152			dictDefaultMessage: uploadField.defaultMessage,
1153			dictFallbackMessage: uploadField.fallbackMessage,
1154			dictFallbackText: uploadField.fallbackText,
1155			dictFileTooBig: uploadField.fileTooBig,
1156			dictFileTooSmall: uploadField.fileTooSmall,
1157			dictInvalidFileType: uploadField.invalidFileType,
1158			dictResponseError: uploadField.responseError,
1159			dictCancelUpload: uploadField.cancel,
1160			dictCancelUploadConfirmation: uploadField.cancelConfirm,
1161			dictRemoveFile: uploadField.remove,
1162			dictMaxFilesExceeded: uploadField.maxFilesExceeded,
1163			resizeMethod: 'contain',
1164			resizeWidth: uploadField.resizeWidth,
1165			resizeHeight: uploadField.resizeHeight,
1166			thumbnailWidth: 60,
1167			thumbnailHeight: 60,
1168			timeout: uploadField.timeout,
1169			previewTemplate: filePreviewHTML( uploadField ),
1170			acceptedFiles: uploadField.acceptedFiles,
1171			fallback() {
1172				// Force ajax submit to turn off
1173				jQuery( this.element ).closest( 'form' ).removeClass( 'frm_ajax_submit' );
1174			},
1175			init() {
1176				var hidden, mockFileIndex, mockFileData, mockFile;
1177
1178				hidden = field.parent().find( '.dz-hidden-input' );
1179
1180				if ( hidden.attr( 'id' ) === undefined ) {
1181					hidden.attr( 'id', label );
1182				}
1183
1184				this.on( 'thumbnail', function( file ) {
1185					if ( file.size < 1024 * 1024 * ( this.options.minFilesize ) ) {
1186						if ( 'function' === typeof file.rejectSize ) {
1187							file.rejectSize();
1188						}
1189					}
1190				});
1191
1192				this.on( 'sending', function( file, xhr, formData ) {
1193
1194					if ( isSpam( uploadFields[ i ].parentFormID, uploadField.checkHoneypot ) ) {
1195						this.removeFile( file );
1196						alert( frm_js.file_spam );
1197						return false;
1198					}
1199
1200					formData.append( 'action', 'frm_submit_dropzone' );
1201					formData.append( 'field_id', uploadFields[ i ].fieldID );
1202					formData.append( 'form_id', uploadFields[ i ].formID );
1203					formData.append( 'nonce', frm_js.nonce );
1204
1205					if ( form.get( 0 ).hasAttribute( 'data-token' ) ) {
1206						formData.append( 'antispam_token', form.get( 0 ).getAttribute( 'data-token' ) );
1207					}
1208				});
1209
1210				this.on( 'processing', function() {
1211					if ( ! this.options.uploadMultiple ) {
1212						this.removeEventListeners();
1213					}
1214				});
1215
1216				this.on( 'success', function( file, response ) {
1217					var mediaIDs, m, mediaID;
1218
1219					mediaIDs = JSON.parse( response );
1220					for ( m = 0; m < mediaIDs.length; m++ ) {
1221						if ( uploadFields[ i ].uploadMultiple !== true ) {
1222							mediaID = mediaIDs[ m ];
1223							jQuery( 'input[name="' + fieldName + '"]' ).val( mediaID );
1224						}
1225					}
1226
1227					if ( this.options.uploadMultiple === false ) {
1228						this.disable();
1229					}
1230
1231					// Clear the errors here so it only happens on upload.
1232					// complete and addedfile both get triggered when reloading a form with a file ID.
1233					clearErrorsOnUpload( file.previewElement );
1234				});
1235
1236				this.on( 'successmultiple', ( files, response ) => {
1237					var mediaIDs = JSON.parse( response );
1238					for ( var m = 0; m < files.length; m++ ) {
1239						jQuery( files[ m ].previewElement ).append( getHiddenUploadHTML( uploadFields[ i ], mediaIDs[ m ], fieldName ) );
1240					}
1241				});
1242
1243				this.on( 'complete', ( file ) => {
1244					var fileName, node, img, thumbnail;
1245
1246					processesRunning--;
1247
1248					if ( form.length ) {
1249						removeSubmitLoading( form.get( 0 ), uploadFields[ i ].formID, processesRunning );
1250					}
1251
1252					if ( file.mediaID === undefined ) {
1253						return;
1254					}
1255
1256					if ( uploadFields[ i ].uploadMultiple ) {
1257						jQuery( file.previewElement ).append( getHiddenUploadHTML( uploadFields[ i ], file.mediaID, fieldName ) );
1258					}
1259
1260					// Add download link to the file
1261					fileName = file.previewElement.querySelectorAll( '[data-dz-name]' );
1262					for ( var _i = 0, _len = fileName.length; _i < _len; _i++ ) {
1263						node = fileName[ _i ];
1264						if ( file.accessible ) {
1265							node.innerHTML = '<a href="' + file.url + '" target="_blank" rel="noopener">' + file.name + '</a>';
1266						} else {
1267							node.innerHTML = file.name;
1268						}
1269
1270						if ( file.ext ) {
1271							img = file.previewElement.querySelector( '.dz-image img' );
1272							if ( null !== img ) {
1273								thumbnail = maybeGetExtensionThumbnail( file.ext );
1274								if ( false !== thumbnail ) {
1275									img.setAttribute( 'src', thumbnail );
1276								}
1277							}
1278						}
1279					}
1280				});
1281
1282				this.on( 'addedfile', ( file ) => {
1283					var ext, thumbnail;
1284					ext = file.name.split( '.' ).pop();
1285					thumbnail = maybeGetExtensionThumbnail( ext );
1286					processesRunning++;
1287					frmFrontForm.showSubmitLoading( form );
1288					if ( false !== thumbnail ) {
1289						jQuery( file.previewElement ).find( '.dz-image img' ).attr( 'src', thumbnail );
1290					}
1291				});
1292
1293				function clearErrorsOnUpload( fileElement ) {
1294					var container = fileElement.closest( '.frm_form_field' );
1295					if ( ! container ) {
1296						return;
1297					}
1298
1299					container.classList.remove( 'frm_blank_field', 'has-error' );
1300					container.querySelectorAll( '.form-field .frm_error, .frm_error_style' ).forEach(
1301						( error ) => {
1302							if ( error.parentNode ) {
1303								error.parentNode.removeChild( error );
1304							}
1305						}
1306					);
1307				}
1308
1309				this.on( 'removedfile', function( file ) {
1310					var fileCount = this.files.length;
1311
1312					if ( this.options.uploadMultiple === false && fileCount < 1 ) {
1313						this.enable();
1314					}
1315
1316					if ( file.accepted !== false && uploadFields[ i ].uploadMultiple !== true ) {
1317						jQuery( 'input[name="' + fieldName + '"]' ).val( '' );
1318					}
1319
1320					if ( file.accepted !== false && file.mediaID !== undefined ) {
1321						jQuery( file.previewElement ).remove();
1322						fileCount = this.files.length;
1323						this.options.maxFiles = uploadFields[ i ].maxFiles - fileCount;
1324					}
1325				});
1326
1327				if ( uploadFields[ i ].mockFiles !== undefined ) {
1328					for ( mockFileIndex = 0; mockFileIndex < uploadFields[ i ].mockFiles.length; mockFileIndex++ ) {
1329						mockFileData = uploadFields[ i ].mockFiles[ mockFileIndex ];
1330						mockFile = {
1331							name: mockFileData.name,
1332							size: mockFileData.size,
1333							url: mockFileData.file_url,
1334							mediaID: mockFileData.id,
1335							accessible: mockFileData.accessible,
1336							ext: mockFileData.ext,
1337							type: mockFileData.type
1338						};
1339
1340						this.emit( 'addedfile', mockFile );
1341						if ( mockFile.accessible && 'string' === typeof mockFile.type && 0 === mockFile.type.indexOf( 'image/' ) ) {
1342							this.emit( 'thumbnail', mockFile, mockFileData.url );
1343						}
1344
1345						this.emit( 'complete', mockFile );
1346						this.files.push( mockFile );
1347					}
1348				}
1349			},
1350			accept( file, done ) {
1351				var message = this.options.dictFileTooSmall.replace( '{{minFilesize}}', this.options.minFilesize );
1352				file.rejectSize = function() {
1353					done( message );
1354				};
1355
1356				return done();
1357			}
1358		};
1359
1360		/**
1361		 * Fixes Chrome Android bug where cloud storage files (Google Drive, etc.) fail to upload.
1362		 *
1363		 * @see https://bugs.chromium.org/p/chromium/issues/detail?id=1063576
1364		 */
1365		if ( /Android/i.test( navigator.userAgent ) && /Chrome/i.test( navigator.userAgent ) ) {
1366			/**
1367			 * Transforms a file before Dropzone uploads it on Android Chrome.
1368			 *
1369			 * @param {File}     file The file selected by the user.
1370			 * @param {Function} done Callback invoked with the file or Blob to upload.
1371			 * @returns {void}
1372			 */
1373			dropzoneConfig.transformFile = function( file, done ) {
1374				/**
1375				 * Forwards the file through Dropzone's resize when configured for images.
1376				 *
1377				 * @param {File|Blob} fileToUpload The file or in-memory Blob ready to upload.
1378				 * @returns {void}
1379				 */
1380				const maybeResize = ( fileToUpload ) => {
1381					if ( ( this.options.resizeWidth || this.options.resizeHeight ) && fileToUpload.type && fileToUpload.type.match( /image.*/ ) ) {
1382						this.resizeImage( fileToUpload, this.options.resizeWidth, this.options.resizeHeight, this.options.resizeMethod, done );
1383						return;
1384					}
1385					done( fileToUpload );
1386				};
1387
1388				const reader = new FileReader();
1389				reader.onload = () => {
1390					const blob = new Blob( [ reader.result ], { type: file.type } );
1391					blob.name = file.name;
1392					blob.lastModified = file.lastModified;
1393					maybeResize( blob );
1394				};
1395				reader.onerror = () => maybeResize( file );
1396				reader.readAsArrayBuffer( file );
1397			};
1398		}
1399
1400		field.dropzone( dropzoneConfig );
1401	}
1402
1403	/**
1404	 * Load imask for the given container.
1405	 *
1406	 * @since 6.23
1407	 *
1408	 * @param {string|undefined} containerId
1409	 * @returns {void}
1410	 */
1411	function loadImask( containerId ) {
1412		if ( ! window.IMask ) {
1413			return;
1414		}
1415
1416		let inputs;
1417		if ( containerId ) {
1418			inputs = document.querySelectorAll( '#' + containerId + ' input[data-frmmask]' );
1419		} else {
1420			inputs = document.querySelectorAll( 'input[data-frmmask]' );
1421		}
1422
1423		inputs.forEach(
1424			( input ) => {
1425				if ( input.getAttribute( 'data-frm-imask-initialized' ) ) {
1426					return;
1427				}
1428
1429				input.setAttribute( 'data-frm-imask-initialized', 1 );
1430
1431				const mask = IMask(
1432					input,
1433					{
1434						mask: input.dataset.frmmask,
1435						lazy: true,
1436						definitions: {
1437							'*': /[a-zA-Z0-9]/
1438						}
1439					}
1440				);
1441
1442				// Store the mask instance for later access
1443				frmMaskInstances.set( input, mask );
1444
1445				input.addEventListener( 'change', () => mask.updateValue() );
1446
1447				// Show placeholder on focus
1448				input.addEventListener( 'focus', () => {
1449					mask.updateOptions({ lazy: false });
1450
1451					// Extend the maxlength by 1 to fix an issue where the placeholder characters would prevent
1452					// characters from being added.
1453					if ( input.hasAttribute( 'maxlength' ) && input.value.length >= input.maxLength ) {
1454						if ( ! input.hasAttribute( 'original-maxlength' ) ) {
1455							input.setAttribute( 'original-maxlength', input.maxLength );
1456						}
1457						input.maxLength = parseInt( input.getAttribute( 'original-maxlength' ) ) + 1;
1458					}
1459
1460					if ( '' === mask.unmaskedValue ) {
1461						// This fixes an issue in Chrome, where it
1462						// automatically would move the cursor to the end of the input.
1463						mask.alignCursor( 0 );
1464					}
1465				} );
1466
1467				input.addEventListener( 'blur', () => {
1468					input.value = input.value.trim();
1469
1470					if ( '' === mask.unmaskedValue ) {
1471						mask.updateOptions({ lazy: true });
1472					}
1473
1474					if ( input.hasAttribute( 'original-maxlength' ) ) {
1475						input.maxLength = parseInt( input.getAttribute( 'original-maxlength' ) );
1476					}
1477				} );
1478			}
1479		);
1480	}
1481
1482	/**
1483	 * Remove the submit loading class from a form and
1484	 * only enable the submit button if it is not conditionally disabled.
1485	 *
1486	 * @param {HTMLElement} form
1487	 * @param {string} formId
1488	 * @param {number} processesRunning
1489	 * @returns {void}
1490	 */
1491	function removeSubmitLoading( form, formId, processesRunning ) {
1492		var isFinalSubmitButton, enable;
1493
1494		isFinalSubmitButton = form.querySelector( '.frm_submit .frm_button_submit.frm_final_submit' );
1495		enable              = ! isFinalSubmitButton || ! submitButtonIsConditionallyDisabled( formId ) ? 'enable' : '';
1496
1497		if ( '' === enable ) {
1498			// If "enable" is not passed, the save draft button stays disabled.
1499			// This code enables the save draft button.
1500			jQuery( '.frm_loading_form' ).find( 'a.frm_save_draft' ).css( 'pointer-events', '' );
1501		}
1502
1503		frmFrontForm.removeSubmitLoading( jQuery( form ), enable, processesRunning );
1504	}
1505
1506	/**
1507	 * Check if the submit button is conditionally disabled.
1508	 * This is required for Stripe link so the button does not get enabled at the wrong time after completing the Stripe elements.
1509	 *
1510	 * @since 6.7.1
1511	 *
1512	 * @param {String} formId
1513	 * @returns {bool}
1514	 */
1515	function submitButtonIsConditionallyDisabled( formId ) {
1516		return submitButtonIsConditionallyNotAvailable( formId ) && 'disable' === __FRMRULES[ 'submit_' + formId ].hideDisable;
1517	}
1518
1519	/**
1520	 * Check submit button is conditionally "hidden". This is also used for the enabled check and is used in submitButtonIsConditionallyDisabled.
1521	 *
1522	 * @since 6.7.1
1523	 *
1524	 * @param {String} formId
1525	 * @returns bool
1526	 */
1527	function submitButtonIsConditionallyNotAvailable( formId ) {
1528		var hideFields = document.getElementById( 'frm_hide_fields_' + formId );
1529		return hideFields && -1 !== hideFields.value.indexOf( '"frm_form_' + formId + '_container .frm_final_submit"' );
1530	}
1531
1532	function maybeGetExtensionThumbnail( ext ) {
1533		if ( -1 !== [ 'jpg', 'jpeg', 'png' ].indexOf( ext ) ) {
1534			return false;
1535		}
1536
1537		if ( 'pdf' === ext ) {
1538			return getProPluginUrl() + '/images/pdf.svg';
1539		}
1540
1541		if ( -1 !== ext.indexOf( 'xls' ) ) {
1542			return getProPluginUrl() + '/images/xls.svg';
1543		}
1544
1545		return getProPluginUrl() + '/images/doc.svg';
1546	}
1547
1548	function getProPluginUrl() {
1549		var freePluginUrlSplitBySlashes = frm_js.images_url.split( '/' );
1550		freePluginUrlSplitBySlashes.pop();
1551		freePluginUrlSplitBySlashes.pop();
1552		freePluginUrlSplitBySlashes.push( 'formidable-pro' );
1553		return freePluginUrlSplitBySlashes.join( '/' );
1554	}
1555
1556	function filePreviewHTML( field ) {
1557		return '<div class="dz-preview dz-file-preview">\n' +
1558		'<div class="dz-image"><img data-dz-thumbnail /></div>\n' +
1559		'<div class="dz-column">\n' +
1560		'<div class="dz-details">\n' +
1561		'<div class="dz-filename"><span data-dz-name></span></div>\n' +
1562		' ' + // add white space between file name and file size.
1563		'<div class="dz-size"><span data-dz-size></span></div>\n' +
1564		'<a class="dz-remove frm_remove_link" href="javascript:undefined;" data-dz-remove title="' + field.remove + '">' +
1565		'<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m11.5 4.5-7 7M4.5 4.5l7 7" stroke="#667085" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
1566		'</a>' +
1567		'</div>\n' +
1568		'<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>\n' +
1569		'<div class="dz-error-message"><span data-dz-errormessage></span></div>\n' +
1570		'</div>\n' +
1571		'</div>';
1572	}
1573
1574	function getHiddenUploadHTML( field, mediaID, fieldName ) {
1575		return '<input name="' + fieldName + '[]" type="hidden" value="' + mediaID + '" data-frmfile="' + field.fieldID + '" />';
1576	}
1577
1578	function removeFile() {
1579		/*jshint validthis:true */
1580		var fieldName = jQuery( this ).data( 'frm-remove' );
1581		fadeOut( jQuery( this ).closest( '.dz-preview' ) );
1582		var singleField = jQuery( 'input[name="' + fieldName + '"]' );
1583		if ( singleField.length ) {
1584			singleField.val( '' );
1585		}
1586	}
1587
1588	function postToAjaxUrl( form, data, success, error, extraParams ) {
1589		var ajaxParams = 'object' === typeof extraParams ? extraParams : {};
1590
1591		ajaxParams.type = 'POST';
1592		ajaxParams.url = getAjaxUrl( form );
1593		ajaxParams.data = data;
1594		ajaxParams.success = success;
1595
1596		if ( 'function' === typeof error ) {
1597			ajaxParams.error = error;
1598		}
1599
1600		jQuery.ajax( ajaxParams );
1601	}
1602
1603	function getAjaxUrl( form ) {
1604		var ajaxUrl, action;
1605
1606		ajaxUrl = frm_js.ajax_url;
1607		action = form.getAttribute( 'action' );
1608
1609		if ( 'string' === typeof action && -1 !== action.indexOf( '?action=frm_forms_preview' ) ) {
1610			ajaxUrl = action.split( '?action=frm_forms_preview' )[0];
1611		}
1612
1613		return ajaxUrl;
1614	}
1615
1616	function isSpam( formID, checkHoneypot ) {
1617		if ( isHeadless() ) {
1618			return true;
1619		}
1620		return checkHoneypot && isHoneypotSpam( formID );
1621	}
1622
1623	/**
1624	 * Check if submission is Honeypot spam
1625	 *
1626	 * @since 2.03.08
1627	 *
1628	 * @returns {boolean}
1629	 */
1630	function isHoneypotSpam( formID ) {
1631		var honeypotField = document.getElementById( 'frm_email_' + formID );
1632		if ( honeypotField === null ) {
1633			honeypotField = document.getElementById( 'frm_form_' + formID + '_container' )?.querySelector( '.frm_verify[id^="field_"]' );
1634		}
1635		return honeypotField !== null && honeypotField.value !== '';
1636	}
1637
1638	function isHeadless() {
1639		return (
1640			window._phantom || window.callPhantom || //phantomjs
1641			window.__phantomas || //PhantomJS-based web perf metrics
1642			window.Buffer || //nodejs
1643			window.emit || //couchjs
1644			window.spawn  //rhino
1645		);
1646	}
1647
1648	/**
1649	 * Show "Other" text box when Other item is checked/selected
1650	 * Hide and clear text box when item is unchecked/unselected
1651	 */
1652	function showOtherText() {
1653        /*jshint validthis:true */
1654        var type = this.type,
1655			other = false,
1656			select = false;
1657
1658        // Dropdowns
1659        if ( type === 'select-one' ) {
1660            select = true;
1661            var curOpt = this.options[ this.selectedIndex ];
1662            if ( curOpt !== undefined && curOpt.className === 'frm_other_trigger' ) {
1663                other = true;
1664            }
1665        } else if ( type === 'select-multiple' ) {
1666            select = true;
1667            var allOpts = this.options;
1668            other = false;
1669            for ( var i = 0; i < allOpts.length; i++ ) {
1670                if ( allOpts[ i ].className === 'frm_other_trigger' ) {
1671                    if ( allOpts[ i ].selected ) {
1672                        other = true;
1673                        break;
1674                    }
1675                }
1676            }
1677        }
1678        if ( select ) {
1679			var otherField = jQuery( this ).parent().children( '.frm_other_input' );
1680
1681			if ( otherField.length ) {
1682				if ( other ) {
1683					// Remove frm_pos_none
1684					otherField[ 0 ].className = otherField[ 0 ].className.replace( 'frm_pos_none', '' );
1685				} else {
1686					// Add frm_pos_none
1687					if ( otherField[ 0 ].className.indexOf( 'frm_pos_none' ) < 1 ) {
1688						otherField[ 0 ].className = otherField[ 0 ].className + ' frm_pos_none';
1689					}
1690					otherField[ 0 ].value = '';
1691				}
1692			}
1693
1694        // Radio
1695        } else if ( type === 'radio' ) {
1696			if ( jQuery( this ).is( ':checked' ) ) {
1697				jQuery( this ).closest( '.frm_radio' ).children( '.frm_other_input' ).removeClass( 'frm_pos_none' );
1698				jQuery( this ).closest( '.frm_radio' ).siblings().children( '.frm_other_input' ).addClass( 'frm_pos_none' ).val( '' );
1699			}
1700        // Checkboxes
1701        } else if ( type === 'checkbox' ) {
1702            if ( this.checked ) {
1703                jQuery( this ).closest( '.frm_checkbox' ).children( '.frm_other_input' ).removeClass( 'frm_pos_none' );
1704            } else {
1705                jQuery( this ).closest( '.frm_checkbox' ).children( '.frm_other_input' ).addClass( 'frm_pos_none' ).val( '' );
1706            }
1707        }
1708	}
1709
1710	function setToggleAriaChecked() {
1711		this.nextElementSibling.setAttribute( 'aria-checked', this.checked ? 'true' : 'false' );
1712	}
1713
1714	function maybeCheckDependent( _, field, fieldId, e ) {
1715		var $field, originalEvent;
1716
1717		$field = jQuery( field );
1718		checkFieldsWithConditionalLogicDependentOnThis( fieldId, $field );
1719
1720		originalEvent = getOriginalEvent( e );
1721		checkFieldsWatchingLookup( fieldId, $field, originalEvent );
1722		doCalculation( fieldId, $field );
1723	}
1724
1725	function getOriginalEvent( e ) {
1726		var originalEvent;
1727		if ( e.originalEvent !== undefined || e.currentTarget.className.includes('frm_chzn') || e.currentTarget.className.includes('frm_slimselect') ) {
1728			originalEvent = 'value changed';
1729		} else {
1730			originalEvent = 'other';
1731		}
1732		return originalEvent;
1733	}
1734
1735	/*****************************************************
1736	 Conditional Logic Functions
1737	 ******************************************************/
1738
1739	/**
1740	 * Check if a changed field has other fields depending on it.
1741	 */
1742	function checkFieldsWithConditionalLogicDependentOnThis( fieldId, changedInput ) {
1743		if ( typeof __FRMRULES  === 'undefined' ||
1744			__FRMRULES[ fieldId ] === undefined ||
1745			__FRMRULES[ fieldId ].dependents.length < 1 ||
1746			changedInput === null ||
1747			changedInput === undefined
1748		) {
1749			return;
1750		}
1751
1752		var triggerFieldArgs = __FRMRULES[ fieldId ];
1753		var repeatArgs       = getRepeatArgsFromFieldName( changedInput[ 0 ].name );
1754
1755		pendingDynamicFieldAjax = [];
1756
1757		for ( var i = 0, l = triggerFieldArgs.dependents.length; i < l; i++ ) {
1758			hideOrShowFieldById( triggerFieldArgs.dependents[ i ], repeatArgs );
1759		}
1760
1761		processPendingAjax();
1762	}
1763
1764	function processPendingAjax() {
1765		var fieldsToProcess, postData, data, formId, form;
1766
1767		if ( ! pendingDynamicFieldAjax.length ) {
1768			return;
1769		}
1770
1771		// Copy pendingDynamicFieldAjax as it gets reset to [] every time checkFieldsWithConditionalLogicDependentOnThis is called.
1772		fieldsToProcess = pendingDynamicFieldAjax.slice();
1773		postData        = [];
1774
1775		for ( data in fieldsToProcess ) {
1776			postData.push( fieldsToProcess[ data ].data );
1777		}
1778
1779		formId = fieldsToProcess[0].args.depFieldArgs.formId;
1780
1781		function processDynamicField( html, depFieldArgs, onCurrentPage ) {
1782			var $fieldDiv, $optContainer, $listInputs, listVal;
1783
1784			if ( onCurrentPage ) {
1785				$fieldDiv = jQuery( '#' + depFieldArgs.containerId );
1786				addLoadingIcon( $fieldDiv );
1787
1788				$optContainer = $fieldDiv.find( '.frm_opt_container, .frm_data_container' );
1789				$optContainer.html( html );
1790				$listInputs = $optContainer.children( 'input' );
1791				listVal = $listInputs.val();
1792				removeLoadingIcon( $optContainer );
1793				if ( '' === html || '' === listVal ) {
1794					hideDynamicField( depFieldArgs );
1795				} else {
1796					showDynamicField( depFieldArgs, $fieldDiv, $listInputs, true );
1797				}
1798			} else {
1799				updateHiddenDynamicListField( depFieldArgs, html );
1800			}
1801		}
1802
1803		function ajaxHandler( response ) {
1804			var i;
1805			for ( i = 0; i < fieldsToProcess.length; i++ ) {
1806				processDynamicField(
1807					'undefined' === typeof response[ i ] ? '' : response[ i ],
1808					fieldsToProcess[ i ].args.depFieldArgs,
1809					fieldsToProcess[ i ].args.onCurrentPage
1810				);
1811			}
1812		}
1813
1814		form = getFormById( formId );
1815		if ( form ) {
1816			postToAjaxUrl(
1817				form,
1818				{
1819					action: 'frm_fields_ajax_get_data_arr',
1820					postData
1821				},
1822				ajaxHandler,
1823				( response ) => {
1824					console.error( response );
1825				},
1826				{
1827					dataType: 'json'
1828				}
1829			);
1830		}
1831	}
1832
1833	/**
1834	 * Hide or show all instances of a field using the field ID
1835	 *
1836	 * @param fieldId
1837	 * @param triggerFieldRepeatArgs
1838     */
1839	function hideOrShowFieldById( fieldId, triggerFieldRepeatArgs ) {
1840		var depFieldArgs = getRulesForSingleField( fieldId );
1841
1842		if ( depFieldArgs === false || depFieldArgs.conditions.length < 1 ) {
1843			// If field has no logic on it, stop now
1844			return;
1845		}
1846
1847		var childFieldDivIds = getAllFieldDivIds( depFieldArgs, triggerFieldRepeatArgs );
1848
1849		var childFieldNum = childFieldDivIds.length;
1850		for ( var i = 0; i < childFieldNum; i++ ) {
1851			depFieldArgs.containerId = childFieldDivIds[ i ];
1852			addRepeatRow( depFieldArgs, childFieldDivIds[ i ]);
1853			hideOrShowSingleField( depFieldArgs );
1854		}
1855	}
1856
1857	/**
1858	 * Get all the field divs that should be hidden or shown, regardless of whether they're on the current page
1859	 *
1860	 * @param {Object} depFieldArgs
1861	 * @param {bool} depFieldArgs.isRepeating
1862	 * @param {string} depFieldArgs.fieldId
1863	 * @param {object} triggerFieldArgs
1864	 * @param {string} triggerFieldArgs.repeatingSection
1865	 * @param {string} triggerFieldArgs.repeatRow
1866	 * @param {string} depFieldArgs.inEmbedForm
1867	 * @returns {Array}
1868     */
1869	function getAllFieldDivIds( depFieldArgs, triggerFieldArgs ) {
1870		var childFieldDivs = [];
1871
1872		if ( depFieldArgs.isRepeating ) {
1873			if ( triggerFieldArgs.repeatingSection !== '' ) {
1874				// If trigger field is repeating/embedded, use its section row in selector
1875				var container = 'frm_field_' + depFieldArgs.fieldId + '-';
1876				container += triggerFieldArgs.repeatingSection + '-' + triggerFieldArgs.repeatRow + '_container';
1877				childFieldDivs.push( container );
1878			} else {
1879				// If trigger field is not repeating/embedded, get all repeating/embedded field divs
1880				childFieldDivs = getAllRepeatingFieldDivIds( depFieldArgs );
1881			}
1882		} else if ( depFieldArgs.fieldType === 'submit' ) {
1883			childFieldDivs.push( getSubmitButtonContainerID( depFieldArgs ) );
1884		} else {
1885			childFieldDivs.push( 'frm_field_' + depFieldArgs.fieldId + '_container' );
1886		}
1887
1888		return childFieldDivs;
1889	}
1890
1891	/**
1892	 * Return selector for submit button for use in Conditional Logic.
1893	 *
1894	 * @param depFieldArgs
1895	 * @returns {string}
1896	 */
1897	function getSubmitButtonContainerID( depFieldArgs ) {
1898		return 'frm_form_' + depFieldArgs.formId + '_container .frm_final_submit';
1899	}
1900
1901	/**
1902	 * Get all instances of a repeating field
1903	 *
1904	 * @since 2.01.0
1905	 * @param {Object} depFieldArgs
1906	 * @param {string} depFieldArgs.fieldId
1907     */
1908	function getAllRepeatingFieldDivIds( depFieldArgs ) {
1909		var childFieldDivs = [],
1910			containerFieldId = getContainerFieldId( depFieldArgs );
1911
1912		// TODO: what if section is inside embedded form?
1913
1914		if ( isFieldDivOnPage( 'frm_field_' + containerFieldId + '_container' ) ) {
1915			childFieldDivs = getRepeatingFieldDivIdsOnCurrentPage( depFieldArgs.fieldId );
1916		} else {
1917			childFieldDivs = getRepeatingFieldDivIdsAcrossPage( depFieldArgs );
1918		}
1919
1920		return childFieldDivs;
1921	}
1922
1923	/**
1924	 * Get all repeating field divs on the current page
1925	 *
1926	 * @since 2.02.06
1927	 * @param string fieldId
1928	 * @returns {Array}
1929     */
1930	function getRepeatingFieldDivIdsOnCurrentPage( fieldId ) {
1931		var childFieldDivs = [],
1932			childFields = document.querySelectorAll( '.frm_field_' + fieldId + '_container' );
1933
1934		for ( var i = 0, l = childFields.length; i < l; i++ ) {
1935			childFieldDivs.push( childFields[ i ].id );
1936		}
1937
1938		return childFieldDivs;
1939	}
1940
1941	/**
1942	 * Get the field divs for repeating fields across a page
1943	 *
1944	 * @param {Object} depFieldArgs
1945	 * @param {string} depFieldArgs.fieldId
1946	 * @returns {Array}
1947	 */
1948	function getRepeatingFieldDivIdsAcrossPage( depFieldArgs ) {
1949		var childFieldDivs = [],
1950			containerFieldId = getContainerFieldId( depFieldArgs ),
1951			fieldDiv = 'frm_field_' + depFieldArgs.fieldId + '-' + containerFieldId + '-',
1952			allRows = document.querySelectorAll( '[name="item_meta[' + containerFieldId + '][row_ids][]"]' );
1953
1954        for ( var i = 0, l = allRows.length; i < l; i++ ) {
1955            if ( allRows[ i ].value !== '' ) {
1956                childFieldDivs.push( fieldDiv + allRows[ i ].value + '_container' );
1957            }
1958        }
1959
1960        if ( childFieldDivs.length < 1 ) {
1961			childFieldDivs.push( fieldDiv + '0_container' );
1962		}
1963
1964		return childFieldDivs;
1965	}
1966
1967	/**
1968	 *
1969	 * @param {Object} depFieldArgs
1970	 * @param {string} depFieldArgs.inSection
1971	 * @param {string} depFieldArgs.inEmbedForm
1972	 * @returns {string}
1973     */
1974	function getContainerFieldId( depFieldArgs ) {
1975		var containerFieldId = '';
1976		if ( depFieldArgs.inEmbedForm !== '0' ) {
1977			containerFieldId = depFieldArgs.inEmbedForm;
1978		} else if ( depFieldArgs.inSection !== '0' ) {
1979			containerFieldId = depFieldArgs.inSection;
1980		}
1981
1982		return containerFieldId;
1983	}
1984
1985	/**
1986	 * @param depFieldArgs
1987	 * @param {bool} depFieldArgs.isRepeating
1988	 * @param childFieldDivId
1989     */
1990	function addRepeatRow( depFieldArgs, childFieldDivId ) {
1991		if ( depFieldArgs.isRepeating ) {
1992			var divParts = childFieldDivId.replace( '_container', '' ).split( '-' );
1993			depFieldArgs.repeatRow = divParts[ 2 ];
1994		} else {
1995			depFieldArgs.repeatRow = '';
1996		}
1997	}
1998
1999	/**
2000	 * @param {object} depFieldArgs
2001	 * @return {void}
2002	 */
2003	function hideOrShowSingleField( depFieldArgs ) {
2004		var i, add,
2005			logicOutcomes = [],
2006			len = depFieldArgs.conditions.length;
2007
2008		for ( i = 0; i < len; i++ ) {
2009			add = checkLogicCondition( depFieldArgs.conditions[ i ], depFieldArgs );
2010			if ( add !== null ) {
2011				// Prevent fields not on the page from being hidden without a chance.
2012				logicOutcomes.push( add );
2013			}
2014		}
2015
2016		if ( logicOutcomes.length ) {
2017			// Only continue if the field was found on the page.
2018			routeToHideOrShowField( depFieldArgs, logicOutcomes );
2019		}
2020	}
2021
2022	function getRulesForSingleField( fieldId ) {
2023		if ( typeof __FRMRULES  === 'undefined' || __FRMRULES[ fieldId ] === undefined ) {
2024			return false;
2025		}
2026
2027		return __FRMRULES[ fieldId ];
2028	}
2029
2030	/**
2031	 * @param {Array} logicCondition
2032	 * @param {string} logicCondition.fieldId
2033	 * @param depFieldArgs
2034	 * @returns {*}
2035     */
2036	function checkLogicCondition( logicCondition, depFieldArgs ) {
2037		var fieldId = logicCondition.fieldId,
2038			logicFieldArgs = getRulesForSingleField( fieldId ),
2039			fieldValue = getFieldValue( logicFieldArgs, depFieldArgs );
2040
2041		if ( fieldValue === null ) {
2042			// The field wasn't found.
2043			return null;
2044		}
2045
2046		return getLogicConditionOutcome( logicCondition, fieldValue, depFieldArgs, logicFieldArgs );
2047	}
2048
2049	/**
2050	 * Get the value from any field.
2051	 *
2052	 * @param {object} logicFieldArgs
2053	 * @param {string} logicFieldArgs.inputType
2054	 * @param {object} depFieldArgs
2055	 * @returns {string}
2056     */
2057	function getFieldValue( logicFieldArgs, depFieldArgs ) {
2058		var fieldValue = '';
2059
2060		if ( 'name' === logicFieldArgs.fieldType ) {
2061			fieldValue = getValueFromNameField( logicFieldArgs, depFieldArgs );
2062		} else if ( logicFieldArgs.inputType === 'radio' || logicFieldArgs.inputType === 'checkbox' || logicFieldArgs.inputType === 'toggle' ) {
2063			fieldValue = getValueFromRadioOrCheckbox( logicFieldArgs, depFieldArgs );
2064		} else if ( logicFieldArgs.inputType === 'date' ) {
2065			fieldValue = getValueForDateField( logicFieldArgs, depFieldArgs );
2066		} else {
2067			fieldValue = getValueFromTextOrDropdown( logicFieldArgs, depFieldArgs );
2068		}
2069
2070		fieldValue = cleanFinalFieldValue( fieldValue );
2071
2072		return fieldValue;
2073	}
2074
2075	/**
2076	 * @since 6.21
2077	 *
2078	 * @param {object} logicFieldArgs
2079	 * @param {object} depFieldArgs
2080	 * @return {string}
2081	 */
2082	function getValueForDateField( logicFieldArgs, depFieldArgs ) {
2083		const fieldId        = logicFieldArgs.fieldId;
2084		const fieldContainer = document.getElementById( 'frm_field_' + fieldId + '_container' );
2085
2086		if ( fieldContainer && fieldContainer.querySelector( '.frm_date_inline' ) ) {
2087			const altField = fieldContainer.querySelector( '#field_' + logicFieldArgs.fieldKey + '_alt' );
2088			if ( altField ) {
2089				return altField.value;
2090			}
2091		}
2092
2093		return getValueFromTextOrDropdown( logicFieldArgs, depFieldArgs );
2094	}
2095
2096	function getValueFromNameField( logicFieldArgs, depFieldArgs ) {
2097		var inputName, inputs, nameValues;
2098
2099		inputName  = buildLogicFieldInputName( logicFieldArgs, depFieldArgs );
2100		inputs     = document.querySelectorAll( '[name^="' + inputName + '"]' );
2101		nameValues = [];
2102
2103		inputs.forEach(
2104			( input ) => {
2105				nameValues.push( input.value );
2106			}
2107		);
2108
2109		return nameValues.join( ' ' );
2110	}
2111
2112	/**
2113	 * Get the value from a Text or Dropdown field
2114	 *
2115	 * @param logicFieldArgs
2116	 * @param {string} logicFieldArgs.fieldKey
2117	 * @param {bool} logicFieldArgs.isRepeating
2118	 * @param {bool} logicFieldArgs.isMultiSelect
2119	 * @param depFieldArgs
2120	 * @param {string} depFieldArgs.repeatRow
2121 	 */
2122	function getValueFromTextOrDropdown( logicFieldArgs, depFieldArgs ) {
2123		var logicFieldValue = '';
2124
2125		if ( logicFieldArgs.isMultiSelect === true ) {
2126			return getValueFromMultiSelectDropdown( logicFieldArgs, depFieldArgs );
2127		}
2128
2129		var fieldCall = 'field_' + logicFieldArgs.fieldKey;
2130		if ( logicFieldArgs.isRepeating ) {
2131			// If trigger field is repeating, dependent field is repeating too
2132			fieldCall += '-' + depFieldArgs.repeatRow;
2133		}
2134
2135		var logicFieldInput = document.getElementById( fieldCall );
2136
2137		if ( logicFieldInput === null ) {
2138			logicFieldValue = parseTimeValue( logicFieldArgs, fieldCall );
2139			if ( logicFieldValue === '' ) {
2140				// Check for hidden fields.
2141				logicFieldValue = getValueFromMultiSelectDropdown( logicFieldArgs, depFieldArgs );
2142			}
2143		} else {
2144			logicFieldValue = logicFieldInput.value;
2145		}
2146
2147		return logicFieldValue;
2148	}
2149
2150	function parseTimeValue( logicFieldArgs, fieldCall ) {
2151		var logicFieldValue = '';
2152		if ( logicFieldArgs.fieldType === 'time' ) {
2153			var hour = document.getElementById( fieldCall + '_H' );
2154			if ( hour !== null ) {
2155				var minute = document.getElementById( fieldCall + '_m' );
2156				logicFieldValue = hour.value + ':' + minute.value;
2157
2158				var pm = document.getElementById( fieldCall + '_A' );
2159				if ( logicFieldValue == ':' ) {
2160					logicFieldValue = '';
2161				} else if ( pm !== null ) {
2162					logicFieldValue += ' ' + pm.value;
2163				}
2164			}
2165		}
2166		return logicFieldValue;
2167	}
2168
2169	function getValueFromMultiSelectDropdown( logicFieldArgs, depFieldArgs ) {
2170		var inputName = buildLogicFieldInputName( logicFieldArgs, depFieldArgs ),
2171			logicFieldInputs = document.querySelectorAll( '[name^="' + inputName + '"]' ),
2172			selectedVals = [];
2173
2174		// TODO: What about if it's read-only?
2175
2176		if ( logicFieldInputs.length == 1 && logicFieldInputs[ 0 ].type !== 'hidden' ) {
2177			selectedVals = jQuery( '[name^="' + inputName + '"]' ).val();
2178			if ( selectedVals === null ) {
2179				selectedVals = '';
2180			}
2181		} else {
2182			selectedVals = getValuesFromCheckboxInputs( logicFieldInputs );
2183		}
2184
2185		return selectedVals;
2186	}
2187
2188	/**
2189	 * Get the value from a Radio or Checkbox field trigger field
2190	 *
2191	 * @param {Object} logicFieldArgs
2192	 * @param {string} logicFieldArgs.inputType
2193	 * @param {Object} depFieldArgs
2194	 * @returns {String|Array}
2195     */
2196	function getValueFromRadioOrCheckbox( logicFieldArgs, depFieldArgs ) {
2197		var logicFieldValue,
2198			inputName = buildLogicFieldInputName( logicFieldArgs, depFieldArgs ),
2199			logicFieldInputs = document.querySelectorAll( 'input[name^="' + inputName + '"]' );
2200
2201		if ( logicFieldInputs.length === 0 ) {
2202			// Don't continue if the field doesn't exist.
2203			return null;
2204		}
2205
2206		if ( logicFieldArgs.inputType === 'checkbox' || logicFieldArgs.inputType === 'toggle' ) {
2207			logicFieldValue = getValuesFromCheckboxInputs( logicFieldInputs );
2208		} else {
2209			logicFieldValue = getValueFromRadioInputs( logicFieldInputs );
2210		}
2211
2212		return logicFieldValue;
2213	}
2214
2215	/**
2216	 * Build a logic field's input name
2217	 * Does not include full name for checkbox, address, or multi-select fields
2218	 *
2219	 * @param {object} logicFieldArgs
2220	 * @param {boolean} logicFieldArgs.isRepeating
2221	 * @param {string} logicFieldArgs.fieldId
2222	 * @param {object} depFieldArgs
2223	 * @param {string} depFieldArgs.inEmbedForm
2224	 * @param {string} depFieldArgs.inSection
2225	 * @param {string} depFieldArgs.repeatRow
2226	 * @returns {string}
2227     */
2228	function buildLogicFieldInputName( logicFieldArgs, depFieldArgs ) {
2229		var inputName = '';
2230
2231		if ( logicFieldArgs.isRepeating ) {
2232			// If the trigger field is repeating, the child must be repeating as well
2233			var sectionId = '';
2234			if ( depFieldArgs.inEmbedForm !== '0' ) {
2235				sectionId = depFieldArgs.inEmbedForm;
2236			} else {
2237				sectionId = depFieldArgs.inSection;
2238			}
2239			var rowId = depFieldArgs.repeatRow;
2240			inputName = 'item_meta[' + sectionId + '][' + rowId + '][' + logicFieldArgs.fieldId + ']';
2241		} else {
2242			inputName = 'item_meta[' + logicFieldArgs.fieldId + ']';
2243		}
2244
2245		return inputName;
2246	}
2247
2248	function getValuesFromCheckboxInputs( inputs ) {
2249		var checkedVals = [];
2250
2251		for ( var i = 0, l = inputs.length; i < l; i++ ) {
2252			if ( inputs[ i ].type === 'hidden' || inputs[ i ].checked ) {
2253				checkedVals.push( inputs[ i ].value );
2254			} else if ( inputs[ i ].dataset.off !== undefined ) {
2255				checkedVals.push( inputs[ i ].dataset.off );
2256			}
2257		}
2258
2259		if ( checkedVals.length === 0 ) {
2260			checkedVals = false;
2261		}
2262
2263		return checkedVals;
2264	}
2265
2266	function cleanFinalFieldValue( fieldValue ) {
2267		if ( fieldValue === undefined ) {
2268			fieldValue = '';
2269		} else if ( typeof fieldValue === 'string' ) {
2270			fieldValue = fieldValue.trim();
2271		}
2272
2273		return fieldValue;
2274	}
2275
2276	/**
2277	 * Check whether a particular conditional logic condition is true or false
2278	 *
2279	 * @param {Array} logicCondition
2280	 * @param {operator:string} logicCondition.operator
2281	 * @param {value:string} logicCondition.value
2282	 * @param {String|Array} fieldValue
2283	 * @param {Object} depFieldArgs
2284	 * @param {string} depFieldArgs.fieldType
2285	 * @param {Object} logicFieldArgs
2286	 * @param {fieldType:string} logicFieldArgs.fieldType
2287     * @returns {Boolean}
2288     */
2289	function getLogicConditionOutcome( logicCondition, fieldValue, depFieldArgs, logicFieldArgs ) {
2290		var outcome;
2291
2292		if ( depFieldArgs.fieldType === 'data' && logicFieldArgs.fieldType === 'data' ) {
2293			// If dep field is Dynamic and logic field is Dynamic
2294			outcome = getDynamicFieldLogicOutcome( logicCondition, fieldValue, depFieldArgs );
2295		} else {
2296			const values = maybePrepareValuesCompared( logicCondition, fieldValue, depFieldArgs.formId );
2297			outcome = operators( logicCondition.operator, values.logicValue, values.fieldValue );
2298		}
2299
2300		return outcome;
2301	}
2302
2303	/**
2304	 * @param {Array} logicCondition
2305	 * @param {string} logicCondition.operator
2306	 * @param {string} logicCondition.value
2307	 * @param {string|Array} fieldValue
2308	 * @param {object} depFieldArgs
2309	 * @param {Array} depFieldArgs.dataLogic
2310	 * @returns {boolean}
2311     */
2312	function getDynamicFieldLogicOutcome( logicCondition, fieldValue, depFieldArgs ) {
2313		var outcome = false;
2314		if ( logicCondition.value === '' ) {
2315			// Logic: "Dynamic field is equal to/not equal to anything"
2316
2317			if ( fieldValue === '' || ( fieldValue.length == 1 && fieldValue[ 0 ] === '' ) ) {
2318				outcome = false;
2319			} else {
2320				outcome = true;
2321			}
2322		} else {
2323			const values = maybePrepareValuesCompared( logicCondition, fieldValue, depFieldArgs.formId );
2324			// Logic: "Dynamic field is equal to/not equal to specific option"
2325			outcome = operators( logicCondition.operator, values.logicValue, values.fieldValue );
2326		}
2327		depFieldArgs.dataLogic = logicCondition;
2328		depFieldArgs.dataLogic.actualValue = fieldValue;
2329
2330		return outcome;
2331	}
2332
2333	/**
2334	 * @since 6.19
2335	 *
2336	 * @param {object}        logicCondition
2337	 * @param {number|string} fieldValue
2338	 * @param {string}        formId
2339	 * @returns {object}
2340	 */
2341	function maybePrepareValuesCompared( logicCondition, fieldValue, formId ) {
2342		let logicValue = logicCondition.value;
2343		if ( 'undefined' === typeof __FRMRULES || 'undefined' === typeof __FRMRULES[ logicCondition.fieldId ] ) {
2344			return { logicValue, fieldValue };
2345		}
2346		const logicConditionFieldType = __FRMRULES[ logicCondition.fieldId ].fieldType;
2347		if ( 'total' === logicConditionFieldType ) {
2348			const currency = getCurrency( formId );
2349			logicValue     = preparePrice( logicCondition.value, currency );
2350			fieldValue     = preparePrice( fieldValue, currency );
2351		} else if ( 'date' === logicConditionFieldType ) {
2352			const dateFormat = getDateFormatForField( logicCondition.fieldId );
2353			if ( false !== dateFormat ) {
2354				logicValue = prepareDate( logicValue, dateFormat );
2355				fieldValue = prepareDate( fieldValue, dateFormat );
2356			}
2357		}
2358		return { logicValue, fieldValue };
2359	}
2360
2361	/**
2362	 * Change date to Y-m-d format if the current format is not sortable.
2363	 *
2364	 * @since 6.21
2365	 *
2366	 * @param {string} date
2367	 * @param {string} format
2368	 * @return {string}
2369	 */
2370	function prepareDate( date, format ) {
2371		if ( 'Y-m-d' === format ) {
2372			return date;
2373		}
2374
2375		const possibleSeparators = [ '/', '.', '-' ];
2376		let separator            = false;
2377		possibleSeparators.forEach( ( sep ) => {
2378			if ( format.includes( sep ) ) {
2379				separator = sep;
2380			}
2381		} );
2382		if ( separator === false ) {
2383			return date;
2384		}
2385
2386		const split = date.split( separator );
2387		if ( 3 !== split.length ) {
2388			return date;
2389		}
2390
2391		let yearPart, monthPart, dayPart;
2392
2393		switch ( format ) {
2394			case 'Y/m/d':
2395				yearPart  = split[ 0 ];
2396				monthPart = split[ 1 ];
2397				dayPart   = split[ 2 ];
2398				break;
2399			case 'n/j/Y':
2400			case 'm/d/Y':
2401				monthPart = split[ 0 ];
2402				dayPart   = split[ 1 ];
2403				yearPart  = split[ 2 ];
2404				break;
2405			case 'd/m/Y':
2406			case 'd.m.Y':
2407			case 'j/m/Y':
2408			case 'j/n/Y':
2409			case 'j-m-Y':
2410			default:
2411				dayPart   = split[ 0 ];
2412				monthPart = split[ 1 ];
2413				yearPart  = split[ 2 ];
2414				break;
2415		}
2416
2417		if ( yearPart.length !== 4 ) {
2418			return date;
2419		}
2420
2421		if ( monthPart.length < 2 ) {
2422			monthPart = '0' + monthPart;
2423		}
2424
2425		if ( dayPart.length < 2 ) {
2426			dayPart = '0' + dayPart;
2427		}
2428
2429		return yearPart + '-' + monthPart + '-' + dayPart;
2430	}
2431
2432	/**
2433	 * @since 6.21
2434	 *
2435	 * @param {string} fieldId
2436	 * @return {string|false}
2437	 */
2438	function getDateFormatForField( fieldId ) {
2439		if ( ! window.__frmDatepicker ) {
2440			return false;
2441		}
2442
2443		let fieldRule = false;
2444		__frmDatepicker.forEach( ( rule ) => {
2445			if ( ! rule.fieldId || rule.fieldId === fieldId ) {
2446				fieldRule = rule;
2447			}
2448		} );
2449
2450		if ( false === fieldRule ) {
2451			return false;
2452		}
2453
2454		return fieldRule.options.fpDateFormat;
2455	}
2456
2457	function operators( op, a, b ) {
2458		var theOperators;
2459
2460		a = prepareLogicValueForComparison( a );
2461		b = prepareEnteredValueForComparison( a, b );
2462
2463		if ( typeof a === 'string' && a.indexOf( '&quot;' ) != '-1' && operators( op, a.replace( '&quot;', '"' ), b ) ) {
2464			return true;
2465		}
2466
2467		theOperators = {
2468			'=='( c, d ) {
2469				return c === d;
2470			},
2471			'!='( c, d ) {
2472				return c !== d;
2473			},
2474			'<'( c, d ) {
2475				return c > d;
2476			},
2477			'<='( c, d ) {
2478				return c >= d;
2479			},
2480			'>'( c, d ) {
2481				return c < d;
2482			},
2483			'>='( c, d ) {
2484				return c <= d;
2485			},
2486			'LIKE'( c, d ) {
2487				if ( ! d ) {
2488					/* If no value, then assume no match */
2489					return false;
2490				}
2491
2492				c = prepareLogicValueForLikeComparison( c );
2493				d = prepareEnteredValueForLikeComparison( c, d );
2494
2495				return d.includes(c);
2496			},
2497			'not LIKE'( c, d ) {
2498				if ( ! d ) {
2499					/* If no value, then assume no match */
2500					return true;
2501				}
2502
2503				c = prepareLogicValueForLikeComparison( c );
2504				d = prepareEnteredValueForLikeComparison( c, d );
2505
2506				return !d.includes(c);
2507			},
2508			// Starts with
2509			'LIKE%'( c, d ) {
2510				if ( ! d ) {
2511					/* If no value, then assume no match */
2512					return false;
2513				}
2514
2515				c = prepareLogicValueForLikeComparison( c );
2516				d = prepareEnteredValueForLikeComparison( c, d );
2517
2518				if ( Array.isArray( d ) ) {
2519					// If d is still an array after calling prepareEnteredValueForLikeComparison, there is no match.
2520					return false;
2521				}
2522
2523				return d.substr( 0, c.length ) === c;
2524			},
2525			// Ends with
2526			'%LIKE'( c, d ) {
2527				if ( ! d ) {
2528					/* If no value, then assume no match */
2529					return false;
2530				}
2531
2532				c = prepareLogicValueForLikeComparison( c );
2533				d = prepareEnteredValueForLikeComparison( c, d );
2534
2535				if ( Array.isArray( d ) ) {
2536					// If d is still an array after calling prepareEnteredValueForLikeComparison, there is no match.
2537					return false;
2538				}
2539
2540				return d.substr( -c.length ) === c;
2541			}
2542		};
2543
2544		if ( 'function' !== typeof theOperators[ op ]) {
2545			op = '=='; // Fallback to Equals to.
2546		}
2547
2548		return theOperators[ op ]( a, b );
2549	}
2550
2551	function prepareLogicValueForComparison( a ) {
2552		if ( shouldParseFloat( a ) ) {
2553			a = parseFloat( a );
2554		} else if ( typeof a === 'string' ) {
2555			a = a.trim();
2556		}
2557
2558		return a;
2559	}
2560
2561	/**
2562	 * @param {string} value
2563	 * @returns {bool}
2564	 */
2565	function shouldParseFloat( value ) {
2566		return String( value ).search( /^\s*(\+|-)?((\d+(\.\d+)?)|(\.\d+))\s*$/ ) !== -1;
2567	}
2568
2569	function prepareEnteredValueForComparison( a, b ) {
2570		if ( b === undefined || b === null || b === false ) {
2571			b = '';
2572		}
2573
2574		if ( Array.isArray( b ) && jQuery.inArray( String( a ), b ) > -1 ) {
2575			b = a;
2576		}
2577
2578		if ( typeof a === 'number' && typeof b === 'string' && shouldParseFloat( b ) ) {
2579			b = parseFloat( b );
2580		}
2581
2582		if ( typeof b === 'string' ) {
2583			b = b.trim();
2584		}
2585
2586		return b;
2587	}
2588
2589	function prepareLogicValueForLikeComparison( val ) {
2590		return prepareValueForLikeComparison( val );
2591	}
2592
2593	function prepareEnteredValueForLikeComparison( logicValue, enteredValue ) {
2594		enteredValue = prepareValueForLikeComparison( enteredValue );
2595
2596		var currentValue = '';
2597		if ( Array.isArray( enteredValue ) ) {
2598			for ( var i = 0, l = enteredValue.length; i < l; i++ ) {
2599				currentValue = enteredValue[ i ].toLowerCase();
2600				if ( currentValue.includes(logicValue) ) {
2601					enteredValue = logicValue;
2602					break;
2603				}
2604			}
2605		}
2606
2607		return enteredValue;
2608	}
2609
2610	function prepareValueForLikeComparison( val ) {
2611		if ( typeof val === 'string' ) {
2612			val = val.toLowerCase();
2613		} else if ( typeof val === 'number' ) {
2614			val = val.toString();
2615		}
2616		return val;
2617	}
2618
2619	/**
2620	 *
2621	 * @param {Object} depFieldArgs
2622	 * @param {string} depFieldArgs.containerId
2623	 * @param {string} depFieldArgs.fieldType
2624	 * @param logicOutcomes
2625     */
2626	function routeToHideOrShowField( depFieldArgs, logicOutcomes ) {
2627		var onCurrentPage,
2628			action = getHideOrShowAction( depFieldArgs, logicOutcomes );
2629
2630		if ( depFieldArgs.fieldType === 'submit' ) {
2631			onCurrentPage = isSubmitButtonOnPage( depFieldArgs.containerId );
2632		} else {
2633			onCurrentPage = isFieldDivOnPage( depFieldArgs.containerId );
2634		}
2635
2636		if ( action === 'show' ) {
2637			if ( depFieldArgs.fieldType === 'data' && depFieldArgs.hasOwnProperty( 'dataLogic' ) ) {
2638				// Only update dynamic field options/value if it is dependent on another Dynamic field
2639				updateDynamicField( depFieldArgs, onCurrentPage );
2640			} else {
2641				showFieldAndSetValue( depFieldArgs, onCurrentPage );
2642			}
2643		} else {
2644			hideFieldAndClearValue( depFieldArgs, onCurrentPage );
2645		}
2646	}
2647
2648	function isFieldDivOnPage( containerId ) {
2649		var fieldDiv = document.getElementById( containerId );
2650
2651		return fieldDiv !== null;
2652	}
2653
2654	/**
2655	 * Checks if specified submit button is on the current page.
2656	 *
2657	 * @param depFieldArgs
2658	 * @returns {boolean}
2659	 */
2660	function isSubmitButtonOnPage( container ) {
2661		var submitButton = document.querySelector( '#' + container );
2662
2663		return submitButton != null;
2664	}
2665
2666	/**
2667	 * @param {Object} depFieldArgs
2668	 * @param {string} depFieldArgs.anyAll
2669	 * @param {string} depFieldArgs.showHide
2670	 * @param {Array} logicOutcomes
2671	 * @returns {string}
2672     */
2673	function getHideOrShowAction( depFieldArgs, logicOutcomes ) {
2674		if ( depFieldArgs.anyAll === 'any' ) {
2675			// If any of the following match logic
2676			if ( logicOutcomes.includes(true) ) {
2677				action = depFieldArgs.showHide;
2678			} else {
2679				action = reverseAction( depFieldArgs.showHide );
2680			}
2681		} else {
2682			// If all of the following match logic
2683			if ( logicOutcomes.includes(false) ) {
2684				action = reverseAction( depFieldArgs.showHide );
2685			} else {
2686				action = depFieldArgs.showHide;
2687			}
2688		}
2689
2690		return action;
2691	}
2692
2693	function reverseAction( action ) {
2694		if ( action === 'show' ) {
2695			action = 'hide';
2696		} else {
2697			action = 'show';
2698		}
2699		return action;
2700	}
2701
2702	/**
2703	 * @param {Object} depFieldArgs
2704	 * @param {string} depFieldArgs.containerId
2705	 * @param {string} depFieldArgs.formId
2706	 * @param {bool} onCurrentPage
2707     */
2708	function showFieldAndSetValue( depFieldArgs, onCurrentPage ) {
2709		if ( isFieldCurrentlyShown( depFieldArgs.containerId, depFieldArgs.formId ) ) {
2710			return;
2711		}
2712
2713		removeFromHideFields( depFieldArgs.containerId, depFieldArgs.formId );
2714
2715		if ( depFieldArgs.fieldType === 'submit' ) {
2716			if ( onCurrentPage ) {
2717				showOrEnableSubmitButton( depFieldArgs );
2718			}
2719			return;
2720		}
2721		if ( onCurrentPage ) {
2722			// Set value, then show field
2723			setValuesInsideFieldOnPage( depFieldArgs.containerId, depFieldArgs );
2724			showFieldContainer( depFieldArgs.containerId );
2725			triggerEvent( document, 'frmShowField' );
2726			if ( depFieldArgs.inputType === 'rte' ) {
2727				reInitializeRichText( 'field_' + depFieldArgs.fieldKey );
2728			}
2729		} else {
2730			setValuesInsideFieldAcrossPage( depFieldArgs );
2731		}
2732	}
2733
2734	/**
2735	 * @param {string} fieldId
2736	 */
2737	function reInitializeRichText( fieldId ) {
2738		var isVisible = 'undefined' !== typeof tinyMCE.editors[ fieldId ] && ! tinyMCE.editors[ fieldId ].isHidden();
2739		if ( ! isVisible ) {
2740			return;
2741		}
2742		removeRichText( fieldId );
2743		initRichText( fieldId );
2744	}
2745
2746	/**
2747	 * Show or enable submit button, as appropriate based on submit Conditional Logic.
2748	 *
2749	 * @param depFieldArgs
2750	 */
2751	function showOrEnableSubmitButton( depFieldArgs ) {
2752		if ( depFieldArgs.hideDisable && depFieldArgs.hideDisable === 'disable' ) {
2753			enableButton( '#' + depFieldArgs.containerId );
2754		} else {
2755			showFieldContainer( depFieldArgs.containerId );
2756		}
2757
2758		removeSubmitButtonFromHiddenList( depFieldArgs );
2759	}
2760
2761	/**
2762	 * Remove submit button from list of hidden submit buttons on page
2763	 *
2764	 * @param depFieldArgs
2765	 */
2766	function removeSubmitButtonFromHiddenList( depFieldArgs ) {
2767		hiddenSubmitButtons = hiddenSubmitButtons.filter( ( button ) => {
2768			return button !== depFieldArgs.formKey;
2769		});
2770	}
2771
2772	/**
2773	 * Enable button with given selector.
2774	 *
2775	 * @param {string} buttonSelector
2776	 *
2777	 * @returns {void}
2778	 */
2779	function enableButton( buttonSelector ) {
2780		var button = document.querySelector( buttonSelector );
2781		if ( button && ! button.closest( '.frm_loading_form' ) ) {
2782			button.disabled = false;
2783		}
2784	}
2785
2786	/**
2787	 * Set the value for all inputs inside of a field div on the current page
2788	 *
2789	 * @param {string} container
2790	 * @param {Object} depFieldArgs
2791	 * @param {string} depFieldArgs.fieldType
2792	 * @param {string} depFieldArgs.formId
2793 	 */
2794	function setValuesInsideFieldOnPage( container, depFieldArgs ) {
2795		var inputs = getInputsInFieldOnPage( container ),
2796			inContainer = ( depFieldArgs.fieldType === 'divider' || depFieldArgs.fieldType === 'form' );
2797
2798		setValueForInputs( inputs, inContainer, depFieldArgs.formId, 'required' );
2799	}
2800
2801	/**
2802	 * Set the value for all inputs inside of a field across a page
2803	 *
2804	 * @param {Object} depFieldArgs
2805	 * @param {string} depFieldArgs.fieldType
2806	 * @param {string} depFieldArgs.formId
2807	 */
2808	function setValuesInsideFieldAcrossPage( depFieldArgs ) {
2809		var inputs = getInputsInFieldAcrossPage( depFieldArgs ),
2810			inContainer = depFieldArgs.fieldType === 'divider' || depFieldArgs.fieldType === 'form' || depFieldArgs.isRepeating;
2811
2812		setValueForInputs( inputs, inContainer, depFieldArgs.formId );
2813	}
2814
2815	/**
2816	 * Gets inputs in field on page.
2817	 *
2818	 * @since 5.4 Accepted HTML element as param.
2819	 *
2820	 * @param {String|HTMLElement} containerId ID of container or container element.
2821	 * @return {Array}
2822	 */
2823	function getInputsInFieldOnPage( containerId ) {
2824		var container = 'string' === typeof containerId ? document.getElementById( containerId ) : containerId;
2825		return container.querySelectorAll( 'select[name^="item_meta"], textarea[name^="item_meta"], input[name^="item_meta"]' );
2826	}
2827
2828	/**
2829	 * @param {Object} depFieldArgs
2830	 * @param {string} depFieldArgs.fieldType
2831	 * @returns {Array}
2832     */
2833	function getInputsInFieldAcrossPage( depFieldArgs ) {
2834		var inputs = [];
2835
2836		if ( depFieldArgs.fieldType === 'divider' ) {
2837			inputs = getInputsInHiddenSection( depFieldArgs );
2838		} else if ( depFieldArgs.fieldType === 'form' ) {
2839			inputs = getInputsInHiddenEmbeddedForm( depFieldArgs );
2840		} else {
2841			inputs = getHiddenInputs( depFieldArgs );
2842		}
2843
2844		return inputs;
2845	}
2846
2847	/**
2848	 * Get the inputs for a non-repeating field that is type=hidden
2849	 * @param {object} depFieldArgs
2850	 * @param {bool} depFieldArgs.isRepeating
2851	 * @param {string} depFieldArgs.inSection
2852	 * @param {string} depFieldArgs.repeatRow
2853	 * @param {string} depFieldArgs.fieldId
2854	 * @returns {NodeList}
2855     */
2856	function getHiddenInputs( depFieldArgs ) {
2857		var name = '';
2858		if ( depFieldArgs.isRepeating ) {
2859			//item_meta[section-id][row-id][field-id]
2860			var containerFieldId = getContainerFieldId( depFieldArgs );
2861			name = 'item_meta[' + containerFieldId + '][' + depFieldArgs.repeatRow + '][' + depFieldArgs.fieldId + ']';
2862		} else {
2863			// item_meta[field-id]
2864			name = 'item_meta[' + depFieldArgs.fieldId + ']';
2865		}
2866		return document.querySelectorAll( '[name^="' + name + '"]' );
2867	}
2868
2869	function setValueForInputs( inputs, inContainer, formId, setRequired ) {
2870		var input, prevInput, i;
2871
2872		if ( ! inputs.length ) {
2873			return;
2874		}
2875
2876		for ( i = 0; i < inputs.length; i++ ) {
2877			input = inputs[ i ];
2878
2879			// Don't set the value if the field is in a section and it's conditionally hidden
2880			if ( inContainer && isChildInputConditionallyHidden( input, formId ) ) {
2881				continue;
2882			}
2883
2884			if ( setRequired === 'required' ) {
2885				maybeAddRequiredTag( input );
2886			}
2887
2888			if ( skipSetValue( i, prevInput, inputs ) ) {
2889				continue;
2890			}
2891
2892			setDefaultValue( input, inContainer );
2893			maybeSetWatchingFieldValue( input );
2894			setShownProduct( input );
2895			maybeDoCalcForSingleField( input );
2896
2897			prevInput = input;
2898		}
2899	}
2900
2901	/**
2902	 * When a field is shown with logic, add the html and aria
2903	 * required attributes if the field is required.
2904	 */
2905	function maybeAddRequiredTag( input ) {
2906		var isRequired, isOptional;
2907
2908		if ( input.type === 'checkbox' || input.type === 'radio' || input.type === 'file' ) {
2909			return;
2910		}
2911
2912		isRequired = input.parentElement.className.indexOf( 'frm_required_field' );
2913		isOptional = input.className.indexOf( 'frm_optional' );
2914
2915		if ( isRequired > -1 && isOptional === -1 ) {
2916			input.setAttribute( 'aria-required', true );
2917		}
2918	}
2919
2920	/**
2921	 * Don't loop through every input in a radio/checkbox/other field
2922	 * TODO: Improve this for checkboxes and address fields
2923	 */
2924	function skipSetValue( i, prevInput, inputs ) {
2925		var typeArray = [ 'checkbox', 'radio' ];
2926
2927		if ( i < 1 || prevInput === undefined ) {
2928			return false;
2929		}
2930
2931		if ( null !== inputs[ i ].getAttribute( 'data-frmprice' ) ) {
2932			// never skip calculating a product price.
2933			return false;
2934		}
2935
2936		var isOther = inputs[ i ].className.includes('frm_other_input');
2937		return isOther || ( prevInput.name == inputs[ i ].name && typeArray.includes(prevInput.type) );
2938	}
2939
2940	// Check if a field input inside of a section or embedded form is conditionally hidden
2941	function isChildInputConditionallyHidden( input, formId ) {
2942		var fieldDivPart = frmFrontForm.getFieldId( input, true ),
2943			fieldDivId = 'frm_field_' + fieldDivPart + '_container';
2944
2945		return isFieldConditionallyHidden( fieldDivId, formId );
2946	}
2947
2948	function showFieldContainer( containerId ) {
2949		var $container = jQuery( '#' + containerId ).show();
2950		if ( $container.hasClass( 'frm_inside_container' ) && null === $container.find( 'select' ).val() ) {
2951			$container.find( 'select' ).val( '' ).trigger( 'change' );
2952		}
2953	}
2954
2955	/**
2956	 * @param {Object} depFieldArgs
2957	 * @param {string} depFieldArgs.containerId
2958	 * @param {string} depFieldArgs.formId
2959	 * @param onCurrentPage
2960     */
2961	function hideFieldAndClearValue( depFieldArgs, onCurrentPage ) {
2962		if ( isFieldConditionallyHidden( depFieldArgs.containerId, depFieldArgs.formId ) ) {
2963			return;
2964		}
2965
2966		addToHideFields( depFieldArgs.containerId, depFieldArgs.formId );
2967
2968		if ( depFieldArgs.fieldType === 'submit' ) {
2969			if ( onCurrentPage ) {
2970				hideOrDisableSubmitButton( depFieldArgs );
2971			}
2972			return;
2973		}
2974
2975		if ( onCurrentPage ) {
2976			hideFieldContainer( depFieldArgs.containerId );
2977			clearInputsInFieldOnPage( depFieldArgs.containerId );
2978		} else {
2979			clearInputsInFieldAcrossPage( depFieldArgs );
2980		}
2981	}
2982
2983	/**
2984	 * Hide or disable given submit button as appropriate, based on submit Conditional Logic
2985	 *
2986	 * @param depFieldArgs
2987	 */
2988	function hideOrDisableSubmitButton( depFieldArgs ) {
2989		if ( depFieldArgs.containerId == undefined ) {
2990			depFieldArgs.containerId = getSubmitButtonContainerID( depFieldArgs );
2991		}
2992
2993		addSubmitButtonToHiddenList( depFieldArgs );
2994
2995		if ( depFieldArgs.hideDisable && depFieldArgs.hideDisable === 'disable' ) {
2996			disableButton( '#' + depFieldArgs.containerId );
2997		} else {
2998			hideFieldContainer( depFieldArgs.containerId );
2999		}
3000	}
3001
3002	/**
3003	 * Add submit button to list of hidden submit buttons on page
3004	 *
3005	 * @param depFieldArgs
3006	 */
3007	function addSubmitButtonToHiddenList( depFieldArgs ) {
3008		hiddenSubmitButtons.push( depFieldArgs.formKey );
3009	}
3010
3011	/**
3012	 * Checks if a particular submit button on the current page is hidden
3013	 *
3014	 * @param {string} formKey
3015	 * @returns {boolean}
3016	 */
3017	function isOnPageSubmitButtonHidden( formKey ) {
3018		return hiddenSubmitButtons.includes(formKey);
3019	}
3020
3021	/**
3022	 * Hide submit button with specified container ID that was previously hidden.
3023	 *
3024	 * @param submitContainerID
3025	 */
3026	function hidePreviouslyHiddenSubmitButton( submitContainerID ) {
3027		var formId = submitContainerID.replace( 'frm_form_', '' );
3028		formId = formId.replace( '_container .frm_final_submit', '' );
3029
3030		var depFieldArgs = getRulesForSingleField( 'submit_' + formId );
3031		if ( depFieldArgs ) {
3032			hideOrDisableSubmitButton( depFieldArgs );
3033		}
3034	}
3035
3036	/**
3037	 * Get the form key (e.g. t3cq5) from the form element id (e.g. form_t3cq5)
3038	 *
3039	 * @param elementId
3040	 * @returns {string} form key
3041	 */
3042	function getFormKeyFromFormElementID( elementId ) {
3043		return elementId.replace( 'form_', '' );
3044	}
3045
3046	function hideFieldContainer( containerId ) {
3047		jQuery( '#' + containerId ).hide();
3048	}
3049
3050	/**
3051	 * Disable button with specified selector
3052	 *
3053	 * @param buttonSelector
3054	 */
3055	function disableButton( buttonSelector ) {
3056		jQuery( buttonSelector ).prop( 'disabled', true );
3057	}
3058
3059	function jsonParse( str ) {
3060		try {
3061			var obj = JSON.parse( str );
3062			return obj;
3063		} catch ( e ) {
3064			return false;
3065		}
3066	}
3067
3068	function clearInputsInFieldOnPage( containerId ) {
3069		var inputs = getInputsInFieldOnPage( containerId );
3070		clearValueForInputs( inputs, 'required' );
3071	}
3072
3073	function clearInputsInFieldAcrossPage( depFieldArgs ) {
3074		var inputs = getInputsInFieldAcrossPage( depFieldArgs );
3075		clearValueForInputs( inputs );
3076	}
3077
3078	/**
3079	 * Get all the child inputs in a hidden section (across a page)
3080	 *
3081	 * @param {Object} depFieldArgs
3082	 * @param {string} depFieldArgs.fieldType
3083	 * @param {string} depFieldArgs.fieldId
3084 	 */
3085	function getInputsInHiddenSection( depFieldArgs ) {
3086		// If a section, get all inputs with data attribute
3087		var inputs = [];
3088		if ( depFieldArgs.fieldType === 'divider' ) {
3089			inputs = document.querySelectorAll( '[data-sectionid="' + depFieldArgs.fieldId + '"]' );
3090		}
3091
3092		return inputs;
3093	}
3094
3095	// Get all the child inputs in a hidden embedded form (across a page)
3096	function getInputsInHiddenEmbeddedForm( depFieldArgs ) {
3097		// TODO: maybe remove form and [0]?
3098		// TODO: what if someone is using embeddedformfieldkey-moretext as their field key? Add data attribute for this
3099		return document.querySelectorAll( '[id^="field_' + depFieldArgs.fieldKey + '-"]' );
3100	}
3101
3102	/**
3103	 * Changes dropdown placeholder option color.
3104	 *
3105	 * @since 6.16.1
3106	 *
3107	 * @param {HTMLElement} select
3108	 *
3109	 * @returns {Void}
3110	 */
3111	function changeSelectColor( select ) {
3112		if ( select.options[select.selectedIndex]?.classList.contains( 'frm-select-placeholder' ) ) {
3113			const styleElement = select.closest( '.with_frm_style' );
3114			const textColorDisabled = styleElement ? getComputedStyle( styleElement ).getPropertyValue( '--text-color-disabled' ).trim() : '';
3115			select.style.setProperty( 'color', textColorDisabled, 'important' );
3116		}
3117	}
3118
3119	/**
3120	 * Clears value of field inputs.
3121	 *
3122	 * @since 5.4 Added the third param.
3123	 *
3124	 * @param {Array}   inputs         Array of inputs.
3125	 * @param {String}  required       Required string.
3126	 * @param {Boolean} resetToDefault Is `true` if reset to default value. Otherwise, reset to empty value.
3127	 */
3128	function clearValueForInputs( inputs, required, resetToDefault ) {
3129		var prevInput, blankSelect, valueChanged, l, i, input, defaultVal, reset, linkedRadioInput;
3130
3131		if ( inputs.length < 1 ) {
3132			return;
3133		}
3134
3135		valueChanged = true;
3136
3137		l = inputs.length;
3138		for ( i = 0; i < l; i++ ) {
3139			input = inputs[ i ];
3140
3141			defaultVal = input.getAttribute( 'data-frmval' );
3142			reset      = resetToDefault && defaultVal;
3143
3144			// Don't remove values from some fields.
3145			if ( input.className.includes('frm_dnc') || input.name.includes('[row_ids]') ) {
3146				prevInput = input;
3147				continue;
3148			}
3149
3150			if ( i > 0 && prevInput.name != input.name && valueChanged === true ) {
3151				// Only trigger a change after all inputs in a field are cleared
3152				triggerChange( jQuery( prevInput ) );
3153			}
3154
3155			valueChanged = true;
3156
3157			if ( input.type === 'radio' || input.type === 'checkbox' ) {
3158				if ( ! reset ) {
3159					input.checked = false;
3160				} else if ( 'radio' === input.type ) {
3161					input.checked = defaultVal === input.value;
3162				} else {
3163					resetCheckboxInputToValue( input, defaultVal );
3164				}
3165
3166				maybeClearStarRatingInput( input );
3167			} else if ( input.tagName === 'SELECT' ) {
3168				if ( isSlimSelect( input ) ) {
3169					setSlimValue( input, reset ? defaultVal : '' );
3170				} else {
3171					if ( ! reset && resetToDefault && input.getAttribute( 'data-placeholder' ) ) {
3172						// When a dropdown has no default value, a placeholder option is added.
3173						// We want to reset to that, as it works the same as an empty default value.
3174						reset      = true;
3175						defaultVal = '';
3176					}
3177
3178					if ( ! reset ) {
3179						blankSelect = input.selectedIndex === 0 && input.options[ 0 ].text.trim() === '';
3180						if ( blankSelect || ( input.selectedIndex === -1 ) ) {
3181							valueChanged = false;
3182						} else {
3183							input.selectedIndex = -1;
3184						}
3185					} else {
3186						valueChanged = resetSelectInputToValue( input, defaultVal );
3187					}
3188
3189					const chosenId     = input.id.replace( /[^\w]/g, '_' ); // Match what the script is doing.
3190					const autocomplete = document.getElementById( chosenId + '_chosen' );
3191					if ( autocomplete !== null ) {
3192						jQuery( input ).trigger( 'chosen:updated' );
3193					}
3194				}
3195			} else if ( input.type === 'range' ) {
3196				let sliderDefault = defaultVal;
3197				if ( ! reset ) {
3198					// Reset to mid value
3199					const min = parseFloat( input.getAttribute( 'min' ) );
3200					const max = parseFloat( input.getAttribute( 'max' ) );
3201					const mid = ( ( max - min ) / 2 ) + min;
3202					const step  = parseFloat( input.getAttribute( 'step' ), 10 );
3203					const midSteps = Math.round( mid / step );
3204					sliderDefault = midSteps * step;
3205					input.parentElement.querySelector( '.frm_range_value' ).textContent = sliderDefault;
3206				}
3207				input.value = sliderDefault;
3208				initRangeInput( [ input ] );
3209			} else if ( input.getAttribute( 'data-frmprice' ) !== null ) {
3210				setHiddenProduct( input );
3211			} else {
3212				if ( ! reset ) {
3213					input.value = '';
3214				} else {
3215					input.value = defaultVal;
3216				}
3217
3218				linkedRadioInput = input.id.includes('-otext') ? document.getElementById( input.id.replace( '-otext', '' ) ) : null;
3219				if ( linkedRadioInput && linkedRadioInput.checked === false ) {
3220					input.classList.add( 'frm_pos_none' );
3221				}
3222
3223				if ( null !== input.getAttribute( 'data-frmfile' ) ) {
3224					clearDropzoneFiles( input );
3225				}
3226				if ( input.type === 'hidden' && input.closest( '.frm_range_container ' ) ) {
3227					resetRangeInput( input );
3228				}
3229			}
3230
3231			if ( required === 'required' ) {
3232				input.required = false;
3233				input.setAttribute( 'aria-required', false );
3234			}
3235
3236			prevInput = inputs[ i ];
3237		}
3238
3239		// trigger a change for the final input in the loop
3240		if ( valueChanged === true ) {
3241			triggerChange( jQuery( prevInput ) );
3242		}
3243	}
3244
3245	function resetRangeInput( input ) {
3246		const rangeContainer          = input.parentElement;
3247		const rangeSliderWrapperClone = rangeContainer.querySelector( '.frm-slider-wrapper' ).cloneNode( true );
3248		// Replace old range slider elements with cloned ones to prevent jerky behavior after reset when initializeRangeSlider is called.
3249		rangeContainer.querySelector( 'div:last-of-type' ).replaceChild( rangeSliderWrapperClone, rangeContainer.querySelector( '.frm-slider-wrapper' ) );
3250		initializeRangeSlider( input, rangeContainer );
3251	}
3252
3253	function setSlimValue( input, value ) {
3254		if ( value.length && '[' === value[0] && -1 !== value.indexOf( ',' ) && ']' === value[ value.length - 1 ] && 'multiple' === input.getAttribute( 'multiple' ) ) {
3255			// Array values are stored as a JSON string.
3256			value = JSON.parse( value );
3257		}
3258		input.slim.setSelected( value );
3259	}
3260
3261	/**
3262	 * Maybe clear star rating.
3263	 *
3264	 * @since 5.4
3265	 *
3266	 * @param {HTMLElement} input This should be the last input in star rating.
3267	 */
3268	function maybeClearStarRatingInput( input ) {
3269		var starGroup, checkedInput;
3270		if ( 'radio' !== input.type || ! input.matches( '.frm-star-group input:last-of-type' ) ) {
3271			return;
3272		}
3273
3274		starGroup    = input.closest( '.frm-star-group' );
3275		checkedInput = starGroup.querySelector( 'input:checked' );
3276		if ( checkedInput ) {
3277			updateStars( checkedInput );
3278		} else {
3279			clearStars( starGroup, true );
3280		}
3281	}
3282
3283	/**
3284	 * Resets checkbox input to a value.
3285	 *
3286	 * @since 5.4
3287	 *
3288	 * @param {HTMLElement} input Checkbox element.
3289	 * @param {Object}      val   The value.
3290	 */
3291	function resetCheckboxInputToValue( input, val ) {
3292		var i;
3293
3294		val = jsonParse( val );
3295
3296		if ( ! val ) {
3297			return;
3298		}
3299
3300		for ( i in val ) {
3301			if ( val[ i ] === input.value ) {
3302				input.checked = true;
3303				return;
3304			}
3305		}
3306
3307		input.checked = false;
3308	}
3309
3310	/**
3311	 * Resets select input to a value.
3312	 *
3313	 * @since 5.4
3314	 *
3315	 * @param {HTMLElement}   input Select element.
3316	 * @param {String|Object} val   The value. Is object if multiselect.
3317	 * @return {Boolean}            Return `true` if value is changed.
3318	 */
3319	function resetSelectInputToValue( input, val ) {
3320		if ( input.multiple ) {
3321			return resetMultiSelectInputToValue( input, val );
3322		}
3323
3324		var i,
3325			valueChanged = false,
3326			options = input.querySelectorAll( 'option' );
3327
3328		for ( i = 0; i < options.length; i++ ) {
3329			if ( val === options[ i ].value && ! options[ i ].selected ) {
3330				options[ i ].selected = true;
3331				valueChanged = true;
3332				continue;
3333			}
3334
3335			if ( val !== options[ i ].value && options[ i ].selected ) {
3336				options[ i ].selected = false;
3337				valueChanged = true;
3338			}
3339		}
3340
3341		return valueChanged;
3342	}
3343
3344	/**
3345	 * Resets multiselect input to a value.
3346	 *
3347	 * @since 5.4
3348	 *
3349	 * @param {HTMLElement} input Select element.
3350	 * @param {Object}      val   The value.
3351	 * @return {Boolean}          Return `true` if value is changed.
3352	 */
3353	function resetMultiSelectInputToValue( input, val ) {
3354		val = jsonParse( val );
3355
3356		if ( ! val ) {
3357			return false;
3358		}
3359
3360		var i, contained,
3361			valueChanged = false,
3362			options = input.querySelectorAll( 'option' );
3363
3364		for ( i = 0; i < options.length; i++ ) {
3365			contained = objectContainValue( val, options[ i ].value );
3366			if ( contained && ! options[ i ].selected ) {
3367				options[ i ].selected = true;
3368				valueChanged = true;
3369				continue;
3370			}
3371
3372			if ( ! contained && options[ i ].selected ) {
3373				options[ i ].selected = false;
3374				valueChanged = true;
3375			}
3376		}
3377
3378		return valueChanged;
3379	}
3380
3381	/**
3382	 * Checks if object contains a value.
3383	 *
3384	 * @since 5.4
3385	 *
3386	 * @param {Object} obj Object.
3387	 * @param {Mixed}  val Value.
3388	 * @return {Boolean}
3389	 */
3390	function objectContainValue( obj, val ) {
3391		var x;
3392
3393		for ( x in obj ) {
3394			if ( obj[ x ] === val ) {
3395				return true;
3396			}
3397		}
3398
3399		return false;
3400	}
3401
3402	function clearDropzoneFiles( hiddenFileIdField ) {
3403		var dropzoneElement = hiddenFileIdField.nextElementSibling;
3404		if ( dropzoneElement && -1 !== dropzoneElement.className.indexOf( 'frm_dropzone' ) && 'object' === typeof dropzoneElement.dropzone && 'function' === typeof dropzoneElement.dropzone.removeAllFiles ) {
3405			dropzoneElement.dropzone.removeAllFiles( true );
3406		}
3407	}
3408
3409	function isFieldCurrentlyShown( containerId, formId ) {
3410		return isFieldConditionallyHidden( containerId, formId ) === false;
3411	}
3412
3413	function isFieldConditionallyHidden( containerId, formId ) {
3414		var hidden = false,
3415			hiddenFields = getHiddenFields( formId );
3416
3417		if ( hiddenFields.includes(containerId) ) {
3418			hidden = true;
3419		}
3420
3421		return hidden;
3422	}
3423
3424	function clearHideFields() {
3425		var hideFieldInputs = document.querySelectorAll( '[id^="frm_hide_fields_"]' );
3426		clearValueForInputs( hideFieldInputs );
3427	}
3428
3429	function addToHideFields( htmlFieldId, formId ) { //TODO: why is this run on submit?
3430		// Get all currently hidden fields
3431		var hiddenFields = getHiddenFields( formId );
3432
3433		if ( hiddenFields.includes(htmlFieldId) ) {
3434			// If field id is already in the array, move on
3435		} else {
3436			// Add new conditionally hidden field to array
3437			hiddenFields.push( htmlFieldId );
3438
3439			// Set the hiddenFields value in the frm_hide_field_formID input
3440			hiddenFields = JSON.stringify( hiddenFields );
3441			var frmHideFieldsInput = document.getElementById( 'frm_hide_fields_' + formId );
3442			if ( frmHideFieldsInput !== null ) {
3443				frmHideFieldsInput.value = hiddenFields;
3444			}
3445		}
3446	}
3447
3448	function getAllHiddenFields() {
3449		var formId, i,
3450			hiddenFields = [],
3451			hideFieldInputs = document.querySelectorAll( '*[id^="frm_hide_fields_"]' ),
3452			formTotal = hideFieldInputs.length;
3453
3454		for ( i = 0; i < formTotal; i++ ) {
3455			formId = hideFieldInputs[ i ].id.replace( 'frm_hide_fields_', '' );
3456			hiddenFields = hiddenFields.concat( getHiddenFields( formId ) );
3457		}
3458
3459		return hiddenFields;
3460	}
3461
3462	function getHiddenFields( formId ) {
3463		var hiddenFields = [];
3464
3465		// Fetch the hidden fields from the frm_hide_fields_formId input
3466		var frmHideFieldsInput = document.getElementById( 'frm_hide_fields_' + formId );
3467		if ( frmHideFieldsInput === null ) {
3468			return hiddenFields;
3469		}
3470
3471		hiddenFields = frmHideFieldsInput.value;
3472		if ( hiddenFields ) {
3473			hiddenFields = JSON.parse( hiddenFields );
3474		} else {
3475			hiddenFields = [];
3476		}
3477
3478		return hiddenFields;
3479	}
3480
3481	function setDefaultValue( input, inContainer ) {
3482		var placeholder, isMultipleSelect,
3483			$input = jQuery( input ),
3484			defaultValue = $input.data( 'frmval' );
3485
3486		if ( defaultValue === undefined && input.classList.contains( 'wp-editor-area' ) ) {
3487			// set value in rich text
3488			var defaultField = document.getElementById( input.id + '-frmval' );
3489			if ( defaultField !== null ) {
3490				defaultValue = defaultField.value;
3491				var targetTinyMceEditor = tinymce.get( input.id );
3492				if ( null !== targetTinyMceEditor ) {
3493					targetTinyMceEditor.setContent( defaultValue );
3494				}
3495			}
3496		} else if ( defaultValue === undefined && input.type === 'hidden' ) {
3497			//with read only select input, value is in sibling select
3498			var $select = $input.next( 'select[disabled]' );
3499			if ( $select.length > 0 ) {
3500				defaultValue = $select.data( 'frmval' );
3501			}
3502		}
3503
3504		placeholder = defaultValue;
3505		defaultValue = setDropdownPlaceholder( defaultValue, input );
3506		if ( placeholder !== defaultValue ) {
3507			placeholder = true;
3508		}
3509
3510		if ( defaultValue !== undefined ) {
3511			var numericKey = new RegExp( /\[\d*\]$/i );
3512
3513			if ( input.type === 'checkbox' || input.type === 'radio' ) {
3514				setCheckboxOrRadioDefaultValue( input.name, defaultValue );
3515
3516			} else if ( input.type === 'hidden' && input.name.includes('[]') ) {
3517				setHiddenCheckboxDefaultValue( input.name, defaultValue );
3518
3519			// Set for hidden checkbox fields that aren't on the current page. Skip hidden fields in a repeater.
3520			} else if ( ! inContainer && input.type === 'hidden' && input.name.includes('][') && numericKey.test( input.name ) ) {
3521				setHiddenCheckboxDefaultValue( input.name.replace( numericKey, '' ), defaultValue );
3522
3523			} else {
3524				isMultipleSelect = false;
3525
3526				if ( placeholder && 'boolean' !== typeof placeholder && input.tagName === 'SELECT' && -1 !== input.className.indexOf( 'frm_chzn' ) ) {
3527					placeholder = false;
3528				}
3529
3530				if ( 'SELECT' === input.tagName && 'multiple' === input.getAttribute( 'multiple' ) ) {
3531					isMultipleSelect = true;
3532				}
3533
3534				if ( defaultValue.constructor === Object ) {
3535					if ( ! isMultipleSelect ) {
3536						var addressType = input.getAttribute( 'name' ).split( '[' ).slice( -1 )[ 0 ];
3537						if ( addressType !== null ) {
3538							addressType = addressType.replace( ']', '' );
3539							defaultValue = defaultValue[ addressType ];
3540							if ( defaultValue === undefined ) {
3541								defaultValue = '';
3542							}
3543						}
3544					}
3545				}
3546
3547				if ( isMultipleSelect ) {
3548					selectMultiselectOptions( input, defaultValue );
3549				} else {
3550					if ( typeof defaultValue === 'object' ) {
3551						// Don't replace a field shortcode with the id.
3552						defaultValue = '[' + defaultValue + ']';
3553					}
3554
3555					if ( isSlimSelect( input ) ) {
3556						// This is true even when no placeholder or default is set when autocomplete is enabled.
3557						// So we need to check if the default value is an empty string and remove the hidden placeholder.
3558						if ( '' === defaultValue ) {
3559							maybeRemoveHiddenPlaceholder( input );
3560						}
3561
3562						// Convert numbers to strings so SlimSelect matches properly.
3563						input.slim.setSelected( `${ defaultValue }` );
3564					} else if ( input.classList.contains( 'frm_chzn' ) ) {
3565						if ( '' === defaultValue ) {
3566							maybeRemoveHiddenPlaceholder( input );
3567						}
3568
3569						jQuery( input ).val( defaultValue ).trigger( 'chosen:updated' );
3570					} else {
3571						input.value = defaultValue;
3572
3573						// Trigger change event for range slider input so it knows to sync the handles.
3574						if ( 'hidden' === input.type && input.dataset.isRangeSliderInitialized ) {
3575							input.dispatchEvent( new Event( 'change' ) );
3576						}
3577
3578						if ( 'range' === input.type ) {
3579							// Trigger input event
3580							input.dispatchEvent( new Event( 'input' ) );
3581						}
3582					}
3583				}
3584				if ( 'SELECT' === input.tagName ) {
3585					changeSelectColor( input );
3586				}
3587			}
3588
3589			if ( ! placeholder && input.tagName === 'SELECT' ) {
3590				maybeUpdateChosenOptions( input );
3591				if ( input.value === '' ) {
3592					setOtherSelectValue( input, defaultValue );
3593				}
3594			}
3595
3596			triggerChange( $input );
3597		} else if ( 'SELECT' === input.tagName ) {
3598			maybeRemoveHiddenPlaceholder( input );
3599		}
3600	}
3601
3602	/**
3603	 * @since 6.16.2
3604	 *
3605	 * @param {HTMLElement} input A select element.
3606	 * @return {boolean} True if the hidden placeholder is detected and removed.
3607	 */
3608	function maybeRemoveHiddenPlaceholder( input ) {
3609		const hiddenPlaceholder = input.querySelector( 'option[value=""].frm_hidden.frm_hidden_placeholder' );
3610		if ( ! hiddenPlaceholder ) {
3611			return false;
3612		}
3613
3614		hiddenPlaceholder.remove();
3615		return true;
3616	}
3617
3618	function isSlimSelect( input ) {
3619		return input.classList.contains( 'frm_slimselect' ) && 'object' === typeof input.slim;
3620	}
3621
3622	function selectMultiselectOptions( select, values ) {
3623		if ( isSlimSelect( select ) && 'function' === typeof Object.values ) {
3624			select.slim.setSelected( Object.values( values ) );
3625			return;
3626		}
3627
3628		var valueKey, option;
3629		for ( valueKey in values ) {
3630			option = select.querySelector( 'option[value="' + values[ valueKey ] + '"]' );
3631			if ( option ) {
3632				option.selected = true;
3633			}
3634		}
3635	}
3636
3637	/**
3638	 * Select the option with placeholder if applicable.
3639	 */
3640	function setDropdownPlaceholder( defaultValue, input ) {
3641		var placeholder;
3642		if ( defaultValue === undefined && input.tagName === 'SELECT' ) {
3643			placeholder = input.getAttribute( 'data-placeholder' );
3644			if ( placeholder !== null ) {
3645				defaultValue = '';
3646			}
3647		}
3648		return defaultValue;
3649	}
3650
3651	function setCheckboxOrRadioDefaultValue( inputName, defaultValue ) {
3652		// Get all checkbox/radio inputs for this field
3653		var radioInputs = document.getElementsByName( inputName ),
3654			isSet = false,
3655			firstInput = false;
3656
3657		if ( typeof defaultValue === 'object' ) {
3658			// Convert the object to an array.
3659			defaultValue = Object.keys( defaultValue ).map(
3660				( key ) => {
3661					return defaultValue[ key ];
3662				}
3663			);
3664		}
3665
3666		let hiddenFieldIndex = 0;
3667
3668		// Loop through options and set the default value
3669		for ( var i = 0, l = radioInputs.length; i < l; i++ ) {
3670			if ( firstInput === false ) {
3671				firstInput = radioInputs[ i ];
3672			}
3673
3674			if ( radioInputs[ i ].type === 'hidden' ) {
3675				// If field is read-only and there is a hidden input
3676				if ( Array.isArray( defaultValue ) && defaultValue[ hiddenFieldIndex ] !== null && 'undefined' !== typeof defaultValue[ hiddenFieldIndex ] ) {
3677					radioInputs[ i ].value = defaultValue[ hiddenFieldIndex ];
3678				} else {
3679					radioInputs[ i ].value = defaultValue;
3680				}
3681				hiddenFieldIndex++;
3682				isSet = true;
3683			} else if ( radioInputs[ i ].value == defaultValue ||
3684				( Array.isArray( defaultValue ) && defaultValue.includes(radioInputs[ i ].value) ) ) {
3685				// If input's value matches the default value, set checked to true
3686
3687				radioInputs[ i ].checked = true;
3688				isSet = true;
3689				if ( radioInputs[ i ].type === 'radio' ) {
3690					break;
3691				}
3692			}
3693		}
3694
3695		if ( ! isSet && firstInput !== false ) {
3696			setOtherValueLimited( firstInput, defaultValue );
3697		}
3698	}
3699
3700	// Set the default value for hidden checkbox or multi-select dropdown fields
3701	function setHiddenCheckboxDefaultValue( inputName, defaultValue ) {
3702		// Get all the hidden inputs with the same name
3703		var hiddenInputs = jQuery( 'input[name^="' + inputName + '"]' ).get();
3704
3705		if ( typeof defaultValue === 'object' ) {
3706			// Convert the object to an array.
3707			defaultValue = Object.keys( defaultValue ).map(
3708				( key ) => {
3709					return defaultValue[ key ];
3710				}
3711			);
3712		}
3713
3714		if ( Array.isArray( defaultValue ) ) {
3715			for ( var i = 0, l = defaultValue.length; i < l; i++ ) {
3716				if ( i in hiddenInputs ) {
3717					hiddenInputs[ i ].value = defaultValue[ i ];
3718				} else {
3719					// TODO: accommodate for when there are multiple default values but the user has removed some
3720				}
3721			}
3722		} else if ( hiddenInputs[ 0 ] !== null && hiddenInputs[ 0 ] !== undefined ) {
3723			hiddenInputs[ 0 ].value = defaultValue;
3724		}
3725	}
3726
3727	function removeFromHideFields( htmlFieldId, formId ) {
3728		// Get all currently hidden fields
3729		var hiddenFields = getHiddenFields( formId );
3730
3731		// If field id is in the array, delete it
3732		var itemIndex = hiddenFields.indexOf( htmlFieldId );
3733		if ( itemIndex > -1 ) {
3734			// Remove field from the hiddenFields array
3735			hiddenFields.splice( itemIndex, 1 );
3736
3737			// Update frm_hide_fields_formId input
3738			hiddenFields = JSON.stringify( hiddenFields );
3739			var frmHideFieldsInput = document.getElementById( 'frm_hide_fields_' + formId );
3740			frmHideFieldsInput.value = hiddenFields;
3741		}
3742	}
3743
3744	/*****************************************************
3745	 * Lookup Field Functions
3746	 ******************************************************/
3747
3748	/**
3749	 * Check all fields that are "watching" a lookup field that changed
3750 	 */
3751	function checkFieldsWatchingLookup( fieldId, changedInput, originalEvent ) {
3752		if ( typeof __FRMLOOKUP  === 'undefined' ||
3753			__FRMLOOKUP[ fieldId ] === undefined ||
3754			__FRMLOOKUP[ fieldId ].dependents.length < 1 ||
3755			changedInput === null ||
3756			changedInput === undefined
3757		) {
3758			return;
3759		}
3760
3761		var triggerFieldArgs = __FRMLOOKUP[ fieldId ];
3762
3763		var parentRepeatArgs = getRepeatArgsFromFieldName( changedInput[ 0 ].name );
3764
3765		for ( var i = 0, l = triggerFieldArgs.dependents.length; i < l; i++ ) {
3766			updateWatchingFieldById( triggerFieldArgs.dependents[ i ], parentRepeatArgs, originalEvent );
3767		}
3768	}
3769
3770	/**
3771	 * Update all instances of a "watching" field
3772	 *
3773	 * @since 2.01.0
3774	 *
3775	 * @param {string} fieldId
3776	 * @param {Object} parentRepeatArgs
3777	 * @param {string} originalEvent
3778	 * @return {void}
3779     */
3780	function updateWatchingFieldById( fieldId, parentRepeatArgs, originalEvent ) {
3781		var childFieldArgs = getLookupArgsForSingleField( fieldId );
3782
3783		// If lookup field has no parents, no need to update this field
3784		if ( childFieldArgs === false || childFieldArgs.parents.length < 1 ) {
3785			return;
3786		}
3787
3788		if ( childFieldArgs.fieldType === 'lookup' ) {
3789			updateLookupFieldOptions( childFieldArgs, parentRepeatArgs );
3790		} else if ( originalEvent === 'value changed' ) {
3791			// If the original event was NOT triggered from a direct value change to the Lookup field,
3792			// do not update the text field value.
3793			updateWatchingFieldValue( childFieldArgs, parentRepeatArgs );
3794		}
3795	}
3796
3797	/**
3798	 * Update a Lookup field's options
3799	 *
3800	 * @param {Object} childFieldArgs
3801	 * @param {Object} parentRepeatArgs
3802	 * @param {String} parentRepeatArgs.repeatRow
3803     */
3804	function updateLookupFieldOptions( childFieldArgs, parentRepeatArgs ) {
3805		var childFieldElements = [];
3806		if ( parentRepeatArgs.repeatRow !== '' ) {
3807			childFieldElements = getRepeatingFieldDivOnCurrentPage( childFieldArgs, parentRepeatArgs );
3808		} else {
3809			childFieldElements = getAllFieldDivsOnCurrentPage( childFieldArgs );
3810		}
3811
3812		for ( var i = 0, l = childFieldElements.length; i < l; i++ ) {
3813			addRepeatRow( childFieldArgs, childFieldElements[ i ].id );
3814			updateSingleLookupField( childFieldArgs, childFieldElements[ i ]);
3815		}
3816
3817		processPendingLookups();
3818	}
3819
3820	/**
3821	 * Go through the list of pending look ups and process them all.
3822	 * These are done in batches of 20 at a time.
3823	 *
3824	 * @since 6.15
3825	 *
3826	 * @return {void}
3827	 */
3828	function processPendingLookups() {
3829		const unprocessedPendingLookups = getUnprocessedPendingLookups();
3830
3831		if ( ! unprocessedPendingLookups.length ) {
3832			return;
3833		}
3834
3835		const formId = unprocessedPendingLookups[0].childFieldArgs.formId;
3836
3837		// We want to allow duplicate form ids (inside repeaters) here because the number of calls
3838		// to enableFormAfterLookup matters for handling the processesRunning variable.
3839		const allFormIds = [];
3840
3841		const batchSize = 20;
3842		const batches   = Math.ceil( unprocessedPendingLookups.length / batchSize );
3843		for ( let batchNumber = 0; batchNumber < batches; ++batchNumber ) {
3844			const start = batchNumber * batchSize;
3845			const end   = start + batchSize;
3846
3847			const currentBatch = unprocessedPendingLookups.slice( start, end );
3848
3849			currentBatch.forEach(
3850				( pendingLookup ) => {
3851					pendingLookup.pending = false;
3852					allFormIds.push( pendingLookup.childFieldArgs.formId );
3853				}
3854			);
3855
3856			postToAjaxUrl(
3857				getFormById( formId ),
3858				{
3859					action: 'frm_replace_lookup_field_options_arr',
3860					postData: currentBatch.map( pendingLookup => pendingLookup.childFieldArgs ),
3861					nonce: frm_js.nonce
3862				},
3863				( newOptionsByFieldId ) => {
3864					allFormIds.forEach( enableFormAfterLookup );
3865
3866					Object.entries( newOptionsByFieldId ).forEach( entry => {
3867						const key          = entry[0];
3868						const newOptions   = entry[1];
3869						const optionLabels = 'undefined' !== typeof newOptionsByFieldId[ key + '_label' ] ? newOptionsByFieldId[ key + '_label' ] : [];
3870
3871						currentBatch.forEach(
3872							function( pendingLookup ) {
3873								if ( ! isNaN( key ) && pendingLookup.childFieldArgs.unique === parseInt( key ) ) {
3874									pendingLookup.callback( newOptions, optionLabels );
3875								}
3876							}
3877						);
3878					});
3879				},
3880				false,
3881				{
3882					dataType: 'json'
3883				}
3884			);
3885		}
3886	}
3887
3888	/**
3889	 * @return {Array}
3890	 */
3891	function getUnprocessedPendingLookups() {
3892		return pendingLookupFieldAjax.filter( pendingLookup => pendingLookup.pending );
3893	}
3894
3895	/**
3896	 * Get the div for a repeating field on the current page
3897	 * @param {Object} childFieldArgs
3898	 * @param {string} childFieldArgs.fieldId
3899	 * @param {Object} parentRepeatArgs
3900	 * @param {string} parentRepeatArgs.repeatingSection
3901	 * @param {string} parentRepeatArgs.repeatRow
3902	 * @returns {Array}
3903     */
3904	function getRepeatingFieldDivOnCurrentPage( childFieldArgs, parentRepeatArgs ) {
3905		var childFieldDivs = [],
3906			selector = 'frm_field_' + childFieldArgs.fieldId + '-';
3907
3908		selector += parentRepeatArgs.repeatingSection + '-' + parentRepeatArgs.repeatRow + '_container';
3909		var container = document.getElementById( selector );
3910		if ( container !== null ) {
3911			childFieldDivs.push( container );
3912		}
3913
3914		return childFieldDivs;
3915	}
3916
3917	function updateWatchingFieldValue( childFieldArgs, parentRepeatArgs ) {
3918		var childFieldElements = getAllTextFieldInputs( childFieldArgs, parentRepeatArgs );
3919
3920		for ( var i = 0, l = childFieldElements.length; i < l; i++ ) {
3921			addRepeatRowForInput( childFieldElements[ i ].name, childFieldArgs );
3922			updateSingleWatchingField( childFieldArgs, childFieldElements[ i ]);
3923		}
3924	}
3925
3926	/**
3927	 * Get the Lookup Args for a field ID
3928	 *
3929	 * @param {string} fieldId
3930	 * @return {boolean|Object}
3931     */
3932	function getLookupArgsForSingleField( fieldId ) {
3933		if ( typeof __FRMLOOKUP  === 'undefined' || __FRMLOOKUP[ fieldId ] === undefined ) {
3934			return false;
3935		}
3936
3937		return __FRMLOOKUP[ fieldId ];
3938	}
3939
3940	/**
3941	 * Update a single Lookup field
3942	 *
3943	 * @since 2.01.0
3944	 * @param {Object} childFieldArgs
3945	 * @param {string} childFieldArgs.inputType
3946	 * @param {object} childElement
3947     */
3948	function updateSingleLookupField( childFieldArgs, childElement ) {
3949		childFieldArgs.parentVals = getParentLookupFieldVals( childFieldArgs );
3950
3951		if ( childFieldArgs.inputType === 'select' ) {
3952			maybeReplaceSelectLookupFieldOptions( childFieldArgs, childElement );
3953		} else if ( childFieldArgs.inputType === 'radio' || childFieldArgs.inputType === 'checkbox' ) {
3954			maybeReplaceCbRadioLookupOptions( childFieldArgs, childElement );
3955		} else if ( childFieldArgs.inputType === 'data' ) {
3956			maybeReplaceLookupList( childFieldArgs, childElement );
3957		}
3958	}
3959
3960	/**
3961	 * Update a standard field that is "watching" a Lookup
3962	 *
3963	 * @since 2.01.0
3964	 *
3965	 * @param {Object} childFieldArgs
3966	 * @param {object} childElement
3967	 * @return {void}
3968	 */
3969	function updateSingleWatchingField( childFieldArgs, childElement ) {
3970		childFieldArgs.parentVals = getParentLookupFieldVals( childFieldArgs );
3971
3972		if ( currentLookupHasQueue( childElement.id ) ) {
3973			addLookupToQueueOfTwo( childFieldArgs, childElement );
3974			return;
3975		}
3976
3977		addLookupToQueueOfTwo( childFieldArgs, childElement );
3978
3979		maybeInsertValueInFieldWatchingLookup( childFieldArgs, childElement );
3980	}
3981
3982	/**
3983	 * Get all the occurrences of a specific Text field
3984	 *
3985	 * @since 2.01.0
3986	 * @param {Object} childFieldArgs
3987	 * @param {boolean} childFieldArgs.isRepeating
3988	 * @param {string} childFieldArgs.fieldKey
3989	 * @param {Object} parentRepeatArgs
3990	 * @param {string} parentRepeatArgs.repeatingSection
3991	 * @param {string} parentRepeatArgs.repeatRow
3992	 * @return {NodeList}
3993 	 */
3994	function getAllTextFieldInputs( childFieldArgs, parentRepeatArgs ) {
3995		var selector = 'field_' + childFieldArgs.fieldKey;
3996		if ( childFieldArgs.isRepeating ) {
3997			if ( parentRepeatArgs.repeatingSection !== '' ) {
3998				// If trigger field is repeating/embedded, use its section row in selector
3999				selector = '[id="' + selector + '-' + parentRepeatArgs.repeatRow + '"]';
4000			} else {
4001				// If trigger field is not repeating/embedded, get all repeating field inputs
4002				selector = '[id^="' + selector + '-"]';
4003			}
4004		} else {
4005			selector = '[id="' + selector + '"]';
4006		}
4007
4008		return document.querySelectorAll( selector );
4009	}
4010
4011	// Set the value in a regular field that is watching a lookup field when it is conditionally shown
4012	function maybeSetWatchingFieldValue( input ) {
4013		var fieldId = frmFrontForm.getFieldId( input, false ),
4014			childFieldArgs = getLookupArgsForSingleField( fieldId );
4015
4016		// If lookup field has no parents, no need to update this field
4017		if ( childFieldArgs === false || childFieldArgs.fieldType === 'lookup' ) {
4018			return;
4019		}
4020
4021		updateSingleWatchingField( childFieldArgs, input );
4022	}
4023
4024	/**
4025	 * Get all divs on the current page for a given field
4026	 *
4027	 * @since 2.01.0
4028	 * @param {Object} childFieldArgs
4029	 * @param {boolean} childFieldArgs.isRepeating
4030	 * @param {string} childFieldArgs.fieldId
4031	 * @returns {Array}
4032     */
4033	function getAllFieldDivsOnCurrentPage( childFieldArgs ) {
4034		var childFieldDivs = [];
4035
4036		if ( childFieldArgs.isRepeating ) {
4037			childFieldDivs = document.querySelectorAll( '.frm_field_' + childFieldArgs.fieldId + '_container' );
4038		} else {
4039			var container = document.getElementById( 'frm_field_' + childFieldArgs.fieldId + '_container' );
4040			if ( container !== null ) {
4041				childFieldDivs.push( container );
4042			}
4043		}
4044
4045		return childFieldDivs;
4046	}
4047
4048	// Get the field values from all parents
4049	function getParentLookupFieldVals( childFieldArgs ) {
4050		var parentFieldArgs,
4051			parentVals = [],
4052			parentIds = childFieldArgs.parents,
4053			parentValue = false;
4054
4055		for ( var i = 0, l = parentIds.length; i < l; i++ ) {
4056			parentFieldArgs = getLookupArgsForSingleField( parentIds[ i ]);
4057			parentValue = getFieldValue( parentFieldArgs, childFieldArgs );
4058
4059			// If any parents have blank values, don't waste time looking for values
4060			if ( parentValue === '' || parentValue === false ) {
4061				parentVals = false;
4062				break;
4063			}
4064
4065			parentVals[ i ] = parentValue;
4066		}
4067
4068		return parentVals;
4069	}
4070
4071	// Get the value from array of radio inputs (could be type="hidden" or type="radio")
4072	function getValueFromRadioInputs( radioInputs ) {
4073		var radioValue = false,
4074			l = radioInputs.length;
4075
4076		for ( var i = 0; i < l; i++ ) {
4077			if ( radioInputs[ i ].type === 'hidden' || radioInputs[ i ].checked ) {
4078				radioValue = radioInputs[ i ].value;
4079				break;
4080			}
4081		}
4082
4083		return radioValue;
4084	}
4085
4086	/**
4087	 * Maybe replace the options in a Select Lookup field
4088	 *
4089	 * @since 2.01.0
4090	 * @param {Object} childFieldArgs
4091	 * @param {Array} childFieldArgs.parents
4092	 * @param {Array} childFieldArgs.parentVals
4093	 * @param {string} childFieldArgs.fieldId
4094	 * @param {string} childFieldArgs.fieldKey
4095	 * @param {string} childFieldArgs.formId
4096	 * @param {object} childDiv
4097	 */
4098	function maybeReplaceSelectLookupFieldOptions( childFieldArgs, childDiv ) {
4099		// Get select within childDiv
4100		var childSelect = childDiv.getElementsByTagName( 'SELECT' )[ 0 ];
4101		if ( childSelect === null ) {
4102			return;
4103		}
4104
4105		var currentValue = childSelect.value;
4106
4107		if ( childFieldArgs.parentVals === false  ) {
4108			// If any parents have blank values, don't waste time looking for values
4109			childSelect.options.length = 1;
4110			childSelect.value = '';
4111			maybeUpdateChosenOptions( childSelect );
4112
4113			if ( currentValue !== '' ) {
4114				triggerChange( jQuery( childSelect ), childFieldArgs.fieldKey );
4115			}
4116
4117		} else {
4118			disableLookup( childSelect );
4119			disableFormPreLookup( childFieldArgs.formId );
4120
4121			getLookupValues(
4122				childFieldArgs,
4123				( newOptions, optionLabels ) => {
4124					replaceSelectLookupFieldOptions( childFieldArgs, childSelect, newOptions, optionLabels );
4125					triggerLookupOptionsLoaded( jQuery( childDiv ) );
4126					enableFormAfterLookup( childFieldArgs.formId );
4127				}
4128			);
4129		}
4130	}
4131
4132	// Update chosen options if autocomplete is enabled
4133	function maybeUpdateChosenOptions( childSelect ) {
4134		if ( childSelect.className.includes('frm_chzn') && jQuery().chosen ) {
4135			jQuery( childSelect ).trigger( 'chosen:updated' );
4136		}
4137	}
4138
4139	/**
4140	 * Disable a Select Lookup field and add loading image
4141	 *
4142	 * @since 2.02.11
4143	 * @param {object} childSelect
4144	 */
4145	function disableLookup( childSelect ) {
4146		childSelect.className = childSelect.className + ' frm_loading_lookup';
4147		childSelect.disabled = true;
4148		maybeUpdateChosenOptions( childSelect );
4149	}
4150
4151	/**
4152	 * Disable a form prior to a Lookup field's Ajax request
4153	 *
4154	 * @since 2.03.02
4155	 * @param {String} formId
4156     */
4157	function disableFormPreLookup( formId ) {
4158		processesRunning++;
4159
4160		if ( processesRunning === 1 ) {
4161
4162			var form = getFormById( formId );
4163			if ( form !== null ) {
4164				frmFrontForm.showSubmitLoading( jQuery( form ) );
4165			}
4166		}
4167	}
4168
4169	/**
4170	 * Enable a form if all Lookup field requests are completed
4171	 *
4172	 * @since 2.03.02
4173	 * @param {String} formId
4174	 */
4175	function enableFormAfterLookup( formId ) {
4176		var form;
4177
4178		processesRunning--;
4179		if ( processesRunning <= 0 ) {
4180			form = getFormById( formId );
4181			if ( form !== null ) {
4182				removeSubmitLoading( form, formId, processesRunning );
4183			}
4184		}
4185	}
4186
4187	/**
4188	 * Get a form element by the ID number
4189	 *
4190	 * @since 2.03.02
4191	 * @param {string} formId
4192	 * @returns {Element}
4193     */
4194	function getFormById( formId ) {
4195		var form = document.querySelector( '#frm_form_' + formId + '_container form' );
4196		if ( form === null ) {
4197			// The order is switched on the back end.
4198			form = document.getElementById( 'frm_form_' + formId + '_container' );
4199			if ( form !== null ) {
4200				form = form.closest( 'form' );
4201			}
4202		}
4203		return form;
4204	}
4205
4206	/**
4207	 * Enable a Select Lookup field and remove loading image
4208	 *
4209	 * @since 2.02.11
4210	 * @param {object} childSelect
4211	 * @param {boolean} isReadOnly
4212	 */
4213	function enableLookup( childSelect, isReadOnly ) {
4214		if ( isReadOnly === false ) {
4215			childSelect.disabled = false;
4216		}
4217		childSelect.className = childSelect.className.replace( ' frm_loading_lookup', '' );
4218	}
4219
4220	/**
4221	 * @param {HTMLElement} element
4222	 * @returns {bool}
4223	 */
4224	function isMultiSelect( element ) {
4225		return element.tagName.toLowerCase() === 'select' && element.multiple;
4226	}
4227
4228	/**
4229	 * Gets selected options for a multiselect element.
4230	 *
4231	 * @param {HTMLElement} element
4232	 * @returns {Array}
4233	 */
4234	function getSelectedOptions( element ) {
4235		if ( isSlimSelect( element ) ) {
4236			return element.slim.getSelected();
4237		}
4238
4239		if ( ! isMultiSelect( element ) ) {
4240			return element.value;
4241		}
4242
4243		var i, option,
4244			selectedOptions = [];
4245		for ( i = 0; i < element.options.length; i++ ) {
4246			option = element.options[i];
4247			if ( option.selected ) {
4248				selectedOptions.push( option.value );
4249			}
4250		}
4251
4252		return selectedOptions;
4253	}
4254
4255	/**
4256	 * Sets selected options for a dropdown element.
4257	 *
4258	 * @param {HTMLElement} element
4259	 * @param {Array} values
4260	 */
4261	function setSelectedOptions( element, values ) {
4262		var option;
4263
4264		if ( ! isMultiSelect( element ) ) {
4265			element.value = values;
4266			return;
4267		}
4268
4269		// Reset all options to unselected
4270		Array.from( element.options ).forEach( ( option ) => {
4271			option.selected = false;
4272		});
4273
4274		// Set selected attribute for the specified values
4275		values.forEach( ( value ) => {
4276			option = element.querySelector( 'option[value="' + value + '"]' );
4277			if ( option ) {
4278				option.selected = true;
4279			}
4280		});
4281	}
4282
4283	/**
4284	 * Replace the options in a Select Lookup field
4285	 *
4286	 * @since 2.01.0
4287	 * @param {Object} fieldArgs
4288	 * @param {string} fieldArgs.fieldKey
4289	 * @param {boolean} fieldArgs.isReadOnly
4290	 * @param {object} childSelect
4291	 * @param {Array} newOptions
4292	 */
4293	function replaceSelectLookupFieldOptions( fieldArgs, childSelect, newOptions, optionLabels ) {
4294		var origVal, i, optsLength, newOption, optionData;
4295
4296		// Get selected options
4297		origVal = getSelectedOptions( childSelect );
4298
4299		// Remove old options
4300		for ( i = childSelect.options.length; i > 0; i-- ) {
4301			childSelect.remove( i );
4302		}
4303
4304		// Add new options
4305		optsLength = newOptions.length;
4306		optionData = [];
4307
4308		if ( childSelect.options.length > 0 && '' === childSelect.options[0].value ) {
4309			optionData.push({
4310				text: childSelect.options[0].textContent,
4311				value: childSelect.options[0].value,
4312				placeholder: true
4313			});
4314		}
4315
4316		for ( i = 0; i < optsLength; i++ ) {
4317			const optionLabel            = optionLabels[ i ] || newOptions[ i ];
4318			newOption                    = new Option( optionLabel, newOptions[ i ], false, false );
4319			childSelect.options[ i + 1 ] = newOption;
4320			optionData.push( newOption );
4321		}
4322
4323		if ( childSelect.slim ) {
4324			childSelect.slim.setData( optionData );
4325		}
4326
4327		setSelectLookupVal( childSelect, origVal );
4328
4329		enableLookup( childSelect, fieldArgs.isReadOnly );
4330
4331		maybeUpdateChosenOptions( childSelect );
4332
4333		// Trigger a change if the new value is different from the old value
4334		if ( getSelectedOptions( childSelect ).toString() !== origVal.toString() ) {
4335			triggerChange( jQuery( childSelect ), fieldArgs.fieldKey );
4336		}
4337	}
4338
4339	/**
4340	 * Set the value in a refreshed Lookup Field.
4341	 *
4342	 * @param {HTMLElement} childSelect
4343	 * @param {Array|string} origVal
4344	 * @returns {void}
4345	 */
4346	function setSelectLookupVal( childSelect, origVal ) {
4347		if ( isSlimSelect( childSelect ) ) {
4348			if ( ! valueIsSingleItemArrayWithEmptyString( origVal ) ) {
4349				setSlimValue( childSelect, origVal );
4350			}
4351			return;
4352		}
4353
4354		// Try setting the dropdown to the original value
4355		setSelectedOptions( childSelect, origVal );
4356
4357		if ( childSelect.value === '' ) {
4358			// If the original value is no longer present, try setting to default value
4359			var defaultValue = childSelect.getAttribute( 'data-frmval' );
4360			if ( defaultValue !== null ) {
4361				childSelect.value = defaultValue;
4362			}
4363		}
4364
4365		if ( childSelect.value === '' ) {
4366			// Set the value to empty string again to fix the issue with blank option show.
4367			childSelect.value = '';
4368		}
4369	}
4370
4371	/**
4372	 * @since 6.8.3
4373	 *
4374	 * @param {Array|string} value
4375	 * @returns {boolean}
4376	 */
4377	function valueIsSingleItemArrayWithEmptyString( value ) {
4378		return Array.isArray( value ) && 1 === value.length && '' === value[0];
4379	}
4380
4381	/**
4382	 * Either hide checkbox/radio Lookup field or update its options
4383	 *
4384	 * @since 2.01.01
4385	 * @param {Object} childFieldArgs
4386	 * @param {Array} childFieldArgs.parentVals
4387	 * @param {object} childDiv
4388     */
4389	function maybeReplaceCbRadioLookupOptions( childFieldArgs, childDiv ) {
4390		if ( childFieldArgs.parentVals === false  ) {
4391			// If any parents have blank values, don't waste time looking for values
4392
4393			var inputs = childDiv.getElementsByTagName( 'input' );
4394			maybeHideRadioLookup( childFieldArgs, childDiv );
4395			clearValueForInputs( inputs );
4396
4397		} else {
4398			replaceCbRadioLookupOptions( childFieldArgs, childDiv );
4399		}
4400	}
4401
4402	/**
4403	 * Update the options in a checkbox/radio lookup field
4404	 *
4405	 * @since 2.01.01
4406	 * @param {Object} childFieldArgs
4407	 * @param {string} childFieldArgs.inputType
4408	 * @param {Array} childFieldArgs.parents
4409	 * @param {Array} childFieldArgs.parentVals
4410	 * @param {string} childFieldArgs.fieldId
4411	 * @param {string} childFieldArgs.repeatRow
4412	 * @param {string} childFieldArgs.fieldKey
4413	 * @param {string} childFieldArgs.formId
4414	 * @param {object} childDiv
4415     */
4416	function replaceCbRadioLookupOptions( childFieldArgs, childDiv ) {
4417		var optContainer, inputs, currentValue, defaultValue, form, data, success;
4418
4419		optContainer = childDiv.getElementsByClassName( 'frm_opt_container' )[ 0 ];
4420		inputs = optContainer.getElementsByTagName( 'input' );
4421		currentValue = '';
4422
4423		addLoadingIconJS( childDiv, optContainer );
4424
4425		if ( childFieldArgs.inputType == 'radio' ) {
4426			currentValue = getValueFromRadioInputs( inputs );
4427		} else {
4428			currentValue = getValuesFromCheckboxInputs( inputs );
4429		}
4430
4431		defaultValue = jQuery( inputs[ 0 ]).data( 'frmval' );
4432		disableFormPreLookup( childFieldArgs.formId );
4433
4434		form = getFormById( childFieldArgs.formId );
4435
4436		data = {
4437			action: 'frm_replace_cb_radio_lookup_options',
4438			parent_fields: childFieldArgs.parents,
4439			parent_vals: childFieldArgs.parentVals,
4440			field_id: childFieldArgs.fieldId,
4441			container_field_id: getContainerFieldId( childFieldArgs ),
4442			row_index: childFieldArgs.repeatRow,
4443			current_value: currentValue,
4444			default_value: defaultValue,
4445			nonce: frm_js.nonce
4446		};
4447
4448		success = function( newHtml ) {
4449			var input;
4450			optContainer.innerHTML = newHtml;
4451
4452			removeLoadingIconJS( childDiv, optContainer );
4453
4454			if ( inputs.length == 1 && inputs[ 0 ].value === '' ) {
4455				maybeHideRadioLookup( childFieldArgs, childDiv );
4456			} else {
4457				maybeShowRadioLookup( childFieldArgs, childDiv );
4458				maybeSetDefaultCbRadioValue( childFieldArgs, inputs, defaultValue );
4459			}
4460
4461			input = inputs[0];
4462			triggerChange( jQuery( input ), childFieldArgs.fieldKey );
4463			triggerLookupOptionsLoaded( jQuery( childDiv ) );
4464
4465			enableFormAfterLookup( childFieldArgs.formId );
4466		};
4467
4468		postToAjaxUrl( form, data, success );
4469	}
4470
4471	/**
4472	 * @since 4.0.03
4473	 */
4474	function maybeReplaceLookupList( childFieldArgs, childDiv ) {
4475		var inputs = childDiv.getElementsByTagName( 'input' ),
4476			content = inputs[0].previousElementSibling;
4477
4478		if ( childFieldArgs.parentVals === false  ) {
4479			// If any parents have blank values, don't waste time looking for values
4480			maybeHideRadioLookup( childFieldArgs, childDiv );
4481			if ( content !== undefined ) {
4482				content.innerHTML = '';
4483			}
4484		} else {
4485			getLookupValues( childFieldArgs, ( response, optionLabels ) => {
4486				content.innerHTML = optionLabels.length ? optionLabels.join( ', ' ) : response.join( ', ' );
4487				inputs[0].value = response;
4488
4489				maybeShowRadioLookup( childFieldArgs, childDiv );
4490				triggerLookupOptionsLoaded( jQuery( childDiv ) );
4491			});
4492		}
4493	}
4494
4495	/**
4496	 * Get the lookup options in an array.
4497	 * @since 4.0.03
4498	 */
4499	function getLookupValues( childFieldArgs, callback ) {
4500		disableFormPreLookup( childFieldArgs.formId );
4501
4502		const pendingLookupArgs = {
4503			formId: childFieldArgs.formId,
4504			fieldId: childFieldArgs.fieldId,
4505			parents: childFieldArgs.parents,
4506			parentVals: childFieldArgs.parentVals,
4507			unique: getAutoId()
4508		};
4509
4510		pendingLookupFieldAjax.push({ childFieldArgs: pendingLookupArgs, callback, pending: true });
4511	}
4512
4513	function getAutoId() {
4514		return autoId++;
4515	}
4516
4517	/**
4518	 * Trigger the frm_lookup_options_loaded event on the field div
4519	 *
4520	 * @since 2.03.05
4521	 *
4522	 * @param {Object} $fieldDiv
4523	 */
4524	function triggerLookupOptionsLoaded( $fieldDiv ) {
4525		$fieldDiv.trigger( 'frmLookupOptionsLoaded' );
4526	}
4527
4528	/**
4529	 * Select the defatul value in a radio/checkbox field if no value is selected
4530	 *
4531	 * @since 2.02.11
4532	 *
4533	 * @param {Object} inputs
4534	 * @param {Object} childFieldArgs
4535	 * @param {string} childFieldArgs.inputType
4536	 * @param {(string|Array)} defaultValue
4537     */
4538	function maybeSetDefaultCbRadioValue( childFieldArgs, inputs, defaultValue ) {
4539		if ( defaultValue === undefined ) {
4540			return;
4541		}
4542
4543		var currentValue = false;
4544		if ( childFieldArgs.inputType === 'radio' ) {
4545			currentValue = getValueFromRadioInputs( inputs );
4546		} else {
4547			currentValue = getValuesFromCheckboxInputs( inputs );
4548		}
4549
4550		if ( currentValue !== false || inputs.length < 1 ) {
4551			return;
4552		}
4553
4554		var inputName = inputs[ 0 ].name;
4555		setCheckboxOrRadioDefaultValue( inputName, defaultValue );
4556	}
4557
4558	/**
4559	 * Hide a Radio Lookup field if it doesn't have any options
4560	 *
4561	 * @since 2.01.01
4562	 * @param {Object} childFieldArgs
4563	 * @param {string} childFieldArgs.formId
4564	 * @param {object} childDiv
4565	 */
4566	function maybeHideRadioLookup( childFieldArgs, childDiv ) {
4567		if ( isFieldConditionallyHidden( childDiv.id, childFieldArgs.formId ) ) {
4568			return;
4569		}
4570
4571		hideFieldContainer( childDiv.id );
4572		addToHideFields( childDiv.id, childFieldArgs.formId );
4573	}
4574
4575	/**
4576	 * Show a radio Lookup field if it has options and conditional logic says it should be shown
4577	 *
4578	 * @since 2.01.01
4579	 * @param {Object} childFieldArgs
4580	 * @param {string} childFieldArgs.formId
4581	 * @param {string} childFieldArgs.fieldId
4582	 * @param {string} childFieldArgs.repeatRow
4583	 * @param {object} childDiv
4584	 * @return {void}
4585	 */
4586	function maybeShowRadioLookup( childFieldArgs, childDiv ) {
4587		if ( isFieldCurrentlyShown( childDiv.id, childFieldArgs.formId ) ) {
4588			return;
4589		}
4590
4591		var logicArgs = getRulesForSingleField( childFieldArgs.fieldId );
4592		if ( logicArgs === false || logicArgs.conditions.length < 1 ) {
4593			removeFromHideFields( childDiv.id, childFieldArgs.formId );
4594			showFieldContainer( childDiv.id );
4595		} else {
4596			logicArgs.containerId = childDiv.id;
4597			logicArgs.repeatRow   = childFieldArgs.repeatRow;
4598			hideOrShowSingleField( logicArgs );
4599		}
4600	}
4601
4602	/**
4603	 * Get new value for a text field if all Lookup Field parents have a value
4604	 *
4605	 * @since 2.01.0
4606	 * @param {Object} childFieldArgs
4607	 * @param {string} childFieldArgs.formId
4608	 * @param {Array} childFieldArgs.parents
4609	 * @param {Array} childFieldArgs.parentVals
4610	 * @param {string} childFieldArgs.fieldKey
4611	 * @param {string} childFieldArgs.fieldId
4612	 * @param {object} childInput
4613     */
4614	function maybeInsertValueInFieldWatchingLookup( childFieldArgs, childInput ) {
4615		if ( isChildInputConditionallyHidden( childInput, childFieldArgs.formId ) ) {
4616			// TODO: What if field is in conditionally hidden section?
4617			checkQueueAfterLookupCompleted( childInput.id );
4618			return;
4619		}
4620
4621		if ( childFieldArgs.parentVals === false  ) {
4622			// If any parents have blank values, set the field value to the default value
4623			var newValue = childInput.getAttribute( 'data-frmval' );
4624			if ( newValue === null ) {
4625				newValue = '';
4626			}
4627			insertValueInFieldWatchingLookup( childFieldArgs, childInput, newValue );
4628			checkQueueAfterLookupCompleted( childInput.id );
4629		} else {
4630			// If all parents have values, check for a new value
4631
4632			disableFormPreLookup( childFieldArgs.formId );
4633
4634			postToAjaxUrl(
4635				getFormById( childFieldArgs.formId ),
4636				{
4637					action: 'frm_get_lookup_text_value',
4638					parent_fields: childFieldArgs.parents,
4639					parent_vals: childFieldArgs.parentVals,
4640					field_id: childFieldArgs.fieldId,
4641					nonce: frm_js.nonce
4642				},
4643				( newValue ) => {
4644					if ( ! isChildInputConditionallyHidden( childInput, childFieldArgs.formId ) && childInput.value != newValue ) {
4645						insertValueInFieldWatchingLookup( childFieldArgs.fieldKey, childInput, newValue );
4646					}
4647
4648					enableFormAfterLookup( childFieldArgs.formId );
4649					checkQueueAfterLookupCompleted( childInput.id );
4650				}
4651			);
4652		}
4653	}
4654
4655	/**
4656	 * Check if the current Lookup watcher field has a queue
4657	 *
4658	 * @since 2.03.05
4659	 *
4660	 * @param {string} elementId
4661	 * @returns {boolean}
4662     */
4663	function currentLookupHasQueue( elementId ) {
4664		return ( elementId in lookupQueues && lookupQueues[ elementId ].length > 0 );
4665	}
4666
4667	/**
4668	 * Add the current Lookup watcher to a queue of size two
4669	 *
4670	 * @since 2.03.05
4671	 *
4672	 * @param {Object} childFieldArgs
4673	 * @param {Object} childInput
4674     */
4675	function addLookupToQueueOfTwo( childFieldArgs, childInput ) {
4676		var elementId = childInput.id;
4677
4678		if ( elementId in lookupQueues ) {
4679			if ( lookupQueues[ elementId ].length >= 2 ) {
4680				lookupQueues[ elementId ] = lookupQueues[ elementId ].slice( 0, 1 );
4681			}
4682		} else {
4683			lookupQueues[ elementId ] = [];
4684		}
4685
4686		lookupQueues[ elementId ].push({ childFieldArgs, childInput });
4687	}
4688
4689	/**
4690	 * Check the lookupQueue after a value lookup is completed
4691	 *
4692	 * @since 2.03.05
4693	 *
4694	 * @param {string} elementId
4695     */
4696	function checkQueueAfterLookupCompleted( elementId ) {
4697		removeLookupFromQueue( elementId );
4698		doNextItemInLookupQueue( elementId );
4699	}
4700
4701	/**
4702	 * Remove a Lookup from the queue
4703	 *
4704	 * @since 2.03.05
4705	 *
4706	 * @param {string} elementId
4707	 */
4708	function removeLookupFromQueue( elementId ) {
4709		lookupQueues[ elementId ].shift();
4710	}
4711
4712	/**
4713	 * Check the current Lookup queue
4714	 *
4715	 * @since 2.03.05
4716	 *
4717	 * @param {string} elementId
4718	 * @return {void}
4719	 */
4720	function doNextItemInLookupQueue( elementId ) {
4721		if ( ! currentLookupHasQueue( elementId ) ) {
4722			return;
4723		}
4724
4725		const childFieldArgs = lookupQueues[ elementId ][ 0 ].childFieldArgs;
4726		const childInput     = lookupQueues[ elementId ][ 0 ].childInput;
4727
4728		updateParentValsForLookup( childInput.name, childFieldArgs );
4729		maybeInsertValueInFieldWatchingLookup( childFieldArgs, childInput );
4730	}
4731
4732	/**
4733	 * Ensures Lookup field parentVals are updated for the current field.
4734	 * Fixes issue #5611
4735	 *
4736	 * @since 6.19
4737	 *
4738	 * @param {string} childInputName
4739	 * @param {object} childFieldArgs
4740	 *
4741	 * @returns {void}
4742	 */
4743	function updateParentValsForLookup( childInputName, childFieldArgs ) {
4744		if ( ! childInputName ) {
4745			return;
4746		}
4747		childFieldArgs.repeatRow  = getRepeatArgsFromFieldName( childInputName ).repeatRow;
4748		childFieldArgs.parentVals = getParentLookupFieldVals( childFieldArgs );
4749	}
4750
4751	/**
4752	 * Convert html entities back into their character values
4753	 *
4754	 * @param {string} string
4755	 * @returns {string}
4756	 */
4757	function decodeEntities( string ) {
4758		var decoded = string.replace( /&amp;/g, '&' )
4759			.replace( /&lt;/g, '<' )
4760			.replace( /&gt;/g, '>' )
4761			.replace( /&quot;/g, '"' )
4762			.replace( /&#039;/g, '\'' );
4763		return decoded;
4764	}
4765
4766	/**
4767	 * Insert a new text field Lookup value
4768	 *
4769	 * @since 2.01.0
4770	 * @param {string} fieldKey
4771	 * @param {object} childInput
4772	 * @param {string} newValue
4773 	 */
4774	function insertValueInFieldWatchingLookup( fieldKey, childInput, newValue ) {
4775		childInput.value = decodeEntities( newValue );
4776		triggerChange( jQuery( childInput ), fieldKey );
4777	}
4778
4779	/**
4780	 * Add the repeat Row to the child field args
4781	 *
4782	 * @since 2.01.0
4783	 * @param {string} fieldName
4784	 * @param {Object} childFieldArgs
4785     */
4786	function addRepeatRowForInput( fieldName, childFieldArgs ) {
4787		var repeatArgs = getRepeatArgsFromFieldName( fieldName );
4788
4789		if ( repeatArgs.repeatRow !== '' ) {
4790			childFieldArgs.repeatRow = repeatArgs.repeatRow;
4791		} else {
4792			childFieldArgs.repeatRow = '';
4793		}
4794	}
4795
4796	/*******************************************************
4797	 Dynamic Field Functions
4798	 *******************************************************/
4799
4800	// Update a Dynamic field's data or options
4801	function updateDynamicField( depFieldArgs, onCurrentPage ) {
4802		var depFieldArgsCopy = cloneObjectForDynamicFields( depFieldArgs );
4803
4804		if ( depFieldArgsCopy.inputType === 'data' ) {
4805			updateDynamicListData( depFieldArgsCopy, onCurrentPage );
4806		} else if ( onCurrentPage ) {
4807			// Only update the options if field is on the current page
4808			updateDynamicFieldOptions( depFieldArgsCopy );
4809		}
4810	}
4811
4812	/**
4813	 * Clone the depFieldArgs object for use in ajax requests
4814	 *
4815	 * @since 2.01.0
4816	 * @param {Object} depFieldArgs
4817	 * @param {string|Array} depFieldArgs.dataLogic.actualValue
4818	 * @param {string} depFieldArgs.fieldId
4819	 * @param {string} depFieldArgs.fieldKey
4820	 * @param {string} depFieldArgs.formId
4821	 * @param {string} depFieldArgs.containerId
4822	 * @param {string} depFieldArgs.repeatRow
4823	 * @param {string} depFieldArgs.inputType
4824	 * @return {Object} dynamicFieldArgs
4825	 */
4826	function cloneObjectForDynamicFields( depFieldArgs ) {
4827		var dataLogic = {
4828			actualValue: depFieldArgs.dataLogic.actualValue,
4829			fieldId: depFieldArgs.dataLogic.fieldId
4830		};
4831
4832		var dynamicFieldArgs = {
4833			fieldId: depFieldArgs.fieldId,
4834			fieldKey: depFieldArgs.fieldKey,
4835			formId: depFieldArgs.formId,
4836			containerId: depFieldArgs.containerId,
4837			repeatRow: depFieldArgs.repeatRow,
4838			dataLogic,
4839			children: '',
4840			inputType: depFieldArgs.inputType
4841		};
4842
4843		return dynamicFieldArgs;
4844	}
4845
4846	pendingDynamicFieldAjax = [];
4847
4848	/**
4849	 * Update a Dynamic List field
4850	 *
4851	 * @since 2.01
4852	 * @param {Object} depFieldArgs
4853	 * @param {string} depFieldArgs.containerId
4854	 * @param {string|Array} depFieldArgs.dataLogic.actualValue
4855	 * @param {string} depFieldArgs.fieldId
4856 	 */
4857	function updateDynamicListData( depFieldArgs, onCurrentPage ) {
4858		var $fieldDiv;
4859
4860		if ( onCurrentPage ) {
4861			$fieldDiv = jQuery( '#' + depFieldArgs.containerId );
4862			addLoadingIcon( $fieldDiv );
4863		}
4864
4865		pendingDynamicFieldAjax.push({
4866			args: {
4867				depFieldArgs,
4868				onCurrentPage
4869			},
4870			data: {
4871				entry_id: depFieldArgs.dataLogic.actualValue,
4872				current_field: depFieldArgs.fieldId,
4873				hide_id: depFieldArgs.containerId,
4874				on_current_page: onCurrentPage,
4875				nonce: frm_js.nonce
4876			}
4877		});
4878	}
4879
4880	/**
4881	 * Update a Dynamic dropdown, radio, or checkbox options
4882	 *
4883	 * @since 2.01
4884	 * @param {Object} depFieldArgs
4885	 * @param {string} depFieldArgs.containerId
4886	 * @param {string} depFieldArgs.dataLogic.fieldId
4887	 * @param {string|Array} depFieldArgs.dataLogic.actualValue
4888	 * @param {string} depFieldArgs.fieldId
4889	 */
4890	function updateDynamicFieldOptions( depFieldArgs ) {
4891		var $fieldDiv = jQuery( '#' + depFieldArgs.containerId ),
4892			$fieldInputs = $fieldDiv.find( 'select[name^="item_meta"], input[name^="item_meta"]' ),
4893			prevValue = getFieldValueFromInputs( $fieldInputs ),
4894			defaultVal = $fieldInputs.data( 'frmval' ),
4895			editingEntry = $fieldDiv.closest( 'form' ).find( 'input[name="id"]' ).val();
4896
4897		addLoadingIcon( $fieldDiv );
4898
4899		postToAjaxUrl(
4900			getFormById( depFieldArgs.formId ),
4901			{
4902				action: 'frm_fields_ajax_data_options',
4903				trigger_field_id: depFieldArgs.dataLogic.fieldId,
4904				entry_id: depFieldArgs.dataLogic.actualValue,
4905				field_id: depFieldArgs.fieldId,
4906				default_value: defaultVal,
4907				container_id: depFieldArgs.containerId,
4908				editing_entry: editingEntry,
4909				prev_val: prevValue,
4910				nonce: frm_js.nonce
4911			},
4912			( html ) => {
4913				var $optContainer = $fieldDiv.find( '.frm_opt_container, .frm_data_container' );
4914				$optContainer.html( html );
4915				var $dynamicFieldInputs = $optContainer.find( 'select, input[type="checkbox"], input[type="radio"]' );
4916
4917				removeLoadingIcon( $optContainer );
4918
4919				if ( html === '' || $dynamicFieldInputs.length < 1 ) {
4920					hideDynamicField( depFieldArgs );
4921				} else {
4922					var valueChanged = dynamicFieldValueChanged( depFieldArgs, $dynamicFieldInputs, prevValue );
4923					showDynamicField( depFieldArgs, $fieldDiv, $dynamicFieldInputs, valueChanged );
4924				}
4925			}
4926		);
4927	}
4928
4929	function dynamicFieldValueChanged( depFieldArgs, $dynamicFieldInputs, prevValue ) {
4930		var newValue = getFieldValueFromInputs( $dynamicFieldInputs );
4931		return ( prevValue !== newValue );
4932	}
4933
4934	/**
4935	 * Update the value in a hidden Dynamic List field
4936	 *
4937	 * @since 2.01.01
4938	 * @param {Object} depFieldArgs
4939	 * @param {string} depFieldArgs.fieldKey
4940	 * @param {string} depFieldArgs.repeatRow
4941	 * @param {string} depFieldArgs.containerId
4942	 * @param {string} depFieldArgs.formId
4943     */
4944	function updateHiddenDynamicListField( depFieldArgs, newValue ) {
4945		// Get the Dynamic List input
4946		var inputId = 'field_' + depFieldArgs.fieldKey;
4947		if ( depFieldArgs.repeatRow !== '' ) {
4948			inputId += '-' + depFieldArgs.repeatRow;
4949		}
4950		var listInput = document.getElementById( inputId );
4951
4952		if ( listInput === null ) {
4953			return;
4954		}
4955
4956		// Set the new value
4957		listInput.value = newValue;
4958
4959		// Remove field from hidden field list
4960		if ( isFieldConditionallyHidden( depFieldArgs.containerId, depFieldArgs.formId ) ) {
4961			removeFromHideFields( depFieldArgs.containerId, depFieldArgs.formId );
4962		}
4963
4964		triggerChange( jQuery( listInput ) );
4965	}
4966
4967	// Add the loading icon with jQuery
4968	function addLoadingIcon( $fieldDiv ) {
4969		var currentHTML = $fieldDiv.html();
4970
4971		if ( currentHTML.includes('frm-loading-img') ) {
4972			// Loading image already present
4973		} else {
4974			var loadingIcon = '<span class="frm-loading-img"></span>';
4975			$fieldDiv.html( currentHTML + loadingIcon );
4976
4977			var $optContainer = $fieldDiv.find( '.frm_opt_container, .frm_data_container' );
4978			$optContainer.hide();
4979		}
4980	}
4981
4982	// Add the loading icon with JavaScript
4983	function addLoadingIconJS( fieldDiv, optContainer ) {
4984		var currentHTML = fieldDiv.innerHTML;
4985
4986		if ( currentHTML.includes('frm-loading-img') ) {
4987			// Loading image already present
4988		} else {
4989			optContainer.classList.add( 'frm_hidden' );
4990
4991			var loadingIcon = document.createElement( 'span' );
4992			loadingIcon.setAttribute( 'class', 'frm-loading-img' );
4993			fieldDiv.insertBefore( loadingIcon, optContainer.nextSibling );
4994		}
4995	}
4996
4997	// Remove the loading icon with jQuery
4998	function removeLoadingIcon( $optContainer ) {
4999		$optContainer.parent().children( '.frm-loading-img' ).remove();
5000		$optContainer.show();
5001	}
5002
5003	// Remove the loading icon with JavaScript
5004	function removeLoadingIconJS( fieldDiv, optContainer ) {
5005		var loadingIcon = fieldDiv.getElementsByClassName( 'frm-loading-img' )[ 0 ];
5006		if ( loadingIcon !== null && loadingIcon !== undefined ) {
5007			loadingIcon.parentNode.removeChild( loadingIcon );
5008		}
5009
5010		optContainer.classList.remove( 'frm_hidden' );
5011	}
5012
5013	// Get the field value from all the inputs
5014	function getFieldValueFromInputs( $inputs ) {
5015		var fieldValue = [],
5016			currentValue = '';
5017
5018		$inputs.each(
5019			function() {
5020				currentValue = this.value;
5021				if ( this.type === 'radio' || this.type === 'checkbox' ) {
5022					if ( this.checked === true ) {
5023						fieldValue.push( currentValue );
5024					}
5025				} else if ( currentValue !== '' ) {
5026					fieldValue.push( currentValue );
5027				}
5028			}
5029		);
5030
5031		if ( fieldValue.length === 0 ) {
5032			fieldValue = '';
5033		}
5034
5035		return fieldValue;
5036	}
5037
5038	// Hide and clear a Dynamic Field
5039	function hideDynamicField( depFieldArgs ) {
5040		hideFieldAndClearValue( depFieldArgs, true );
5041	}
5042
5043	// Show Dynamic field
5044	function showDynamicField( depFieldArgs, $fieldDiv, $fieldInputs, valueChanged ) {
5045		if ( isFieldConditionallyHidden( depFieldArgs.containerId, depFieldArgs.formId ) ) {
5046			removeFromHideFields( depFieldArgs.containerId, depFieldArgs.formId );
5047			$fieldDiv.show();
5048		}
5049
5050		if ( $fieldInputs.hasClass( 'frm_chzn' ) || $fieldInputs.hasClass( 'frm_slimselect' ) ) {
5051			loadAutocomplete();
5052		}
5053
5054		if ( valueChanged === true ) {
5055			triggerChange( $fieldInputs );
5056		}
5057	}
5058
5059	/*************************************************
5060	 Calculations
5061	 ************************************************/
5062
5063	function triggerCalc() {
5064		if ( typeof __FRMCALC === 'undefined' ) {
5065			// there are no calculations on this page
5066			return;
5067		}
5068
5069		var triggers = __FRMCALC.triggers;
5070		if ( triggers ) {
5071			jQuery( triggers.join() ).trigger({ type: 'change', selfTriggered: true });
5072		}
5073
5074		triggerCalcWithoutFields();
5075	}
5076
5077	function triggerCalcWithoutFields() {
5078		var calcs = __FRMCALC.calc,
5079			vals = [];
5080
5081		for ( var fieldKey in calcs ) {
5082			if ( calcs[ fieldKey ].fields.length < 1 && calcs[ fieldKey ].calc && '0' !== calcs[ fieldKey ].calc ) {
5083				var totalField = document.getElementById( 'field_' + fieldKey );
5084				if ( totalField !== null && ! isChildInputConditionallyHidden( totalField, calcs[ fieldKey ].form_id ) ) {
5085					// if field is not hidden, do calculation
5086					doSingleCalculation( __FRMCALC, fieldKey, vals );
5087				}
5088			}
5089		}
5090	}
5091
5092	function doCalculation( fieldId, triggerField ) {
5093		if ( typeof __FRMCALC === 'undefined' ) {
5094			// there are no calculations on this page
5095			return;
5096		}
5097
5098		var allCalcs = __FRMCALC,
5099			calc = allCalcs.fields[ fieldId ],
5100			vals = [];
5101
5102		if ( calc === undefined ) {
5103			// this field is not used in a calculation
5104			return;
5105		}
5106
5107		var keys = calc.total;
5108		var len = keys.length;
5109		var pages = getStartEndPage( allCalcs.calc[ keys[ 0 ] ]);
5110
5111		// loop through each calculation this field is used in
5112		for ( var i = 0, l = len; i < l; i++ ) {
5113			var totalOnPage = isTotalFieldOnPage( allCalcs.calc[ keys[ i ] ], pages );
5114
5115			// Proceed with calculation if total field is not conditionally hidden
5116			if ( totalOnPage && isTotalFieldConditionallyHidden( allCalcs.calc[ keys[ i ] ], triggerField.attr( 'name' ) ) === false ) {
5117				doSingleCalculation( allCalcs, keys[ i ], vals, triggerField );
5118			}
5119		}
5120	}
5121
5122	/**
5123	 * @param thisField
5124	 * @since 2.05.06
5125	 */
5126	function getStartEndPage( thisField ) {
5127		var formId = thisField.form_id,
5128			formContainer = document.getElementById( 'frm_form_' + formId + '_container' );
5129
5130		if ( formContainer === null && thisField.in_section ) {
5131			var fieldContainer = document.getElementById( 'frm_field_' + thisField.in_section + '_container' );
5132
5133			if ( fieldContainer === null ) {
5134				// bail now and run the calculation if we can't determine if it's on the page
5135				return [];
5136			}
5137
5138			formContainer = closest( fieldContainer, ( el ) => {
5139				return el.tagName === 'FORM';
5140			});
5141
5142			formId = formContainer.elements.namedItem( 'form_id' ).value;
5143		}
5144
5145		var hasPreviousPage = formContainer.getElementsByClassName( 'frm_next_page' );
5146		var hasAnotherPage = document.getElementById( 'frm_page_order_' + formId );
5147
5148		var pages = [];
5149		if ( hasPreviousPage.length > 0 ) {
5150			pages.start = hasPreviousPage[ 0 ];
5151		}
5152		if ( hasAnotherPage !== null ) {
5153			pages.end = hasAnotherPage;
5154		}
5155
5156		return pages;
5157	}
5158
5159	function closest( el, fn ) {
5160		return el && ( fn( el ) ? el : closest( el.parentNode, fn ) );
5161	}
5162
5163	/**
5164	 * If the total field is not on the current page, don't trigger the calculation
5165	 *
5166	 * @param calcDetails
5167	 * @param pages
5168	 * @since 2.05.06
5169	 */
5170	function isTotalFieldOnPage( calcDetails, pages ) {
5171		if ( pages.start !== undefined || pages.end !== undefined ) {
5172			// the form has pages
5173			var hiddenTotalField = jQuery( 'input[type=hidden][name*="[' + calcDetails.field_id + ']"]' );
5174			if ( hiddenTotalField.length ) {
5175				// the total field is a hidden field
5176				return isHiddenTotalOnPage( hiddenTotalField, pages );
5177			}
5178		}
5179
5180		return true;
5181	}
5182
5183	function isHiddenTotalOnPage( hiddenTotalField, pages ) {
5184		var onPage,
5185			hiddenParent = hiddenTotalField.closest( '.frm_form_field' );
5186
5187		if ( hiddenParent.length ) {
5188			// the field is in a section
5189			return true;
5190		}
5191
5192		var totalPos = hiddenTotalField.index();
5193		var isAfterStart = true;
5194		var isBeforeEnd = true;
5195		if ( pages.start !== undefined ) {
5196			isAfterStart = jQuery( pages.start ).index() < totalPos;
5197		}
5198		if ( pages.end !== undefined ) {
5199			isBeforeEnd = jQuery( pages.end ).index() > totalPos;
5200		}
5201
5202		onPage = ( isAfterStart && isBeforeEnd );
5203		if ( ! onPage ) {
5204			onPage = hiddenTotalField.closest( '.do-calculation' ).length > 0;
5205		}
5206
5207		return onPage;
5208	}
5209
5210	/**
5211	 * Check if a total field is conditionally hidden
5212	 * @param {Object} calcDetails
5213	 * @param {string} calcDetails.field_id
5214	 * @param {string} calcDetails.form_id
5215	 * @param {string} calcDetails.inSection
5216	 * @param {string} calcDetails.inEmbedForm
5217	 * @param {string} triggerFieldName
5218	 * @returns {boolean}
5219     */
5220	function isTotalFieldConditionallyHidden( calcDetails, triggerFieldName ) {
5221		var hidden = false,
5222			fieldId = calcDetails.field_id,
5223			formId = calcDetails.form_id,
5224			hiddenFields = getHiddenFields( formId );
5225
5226		// Check if there are any conditionally hidden fields
5227		if ( hiddenFields.length < 1 ) {
5228			return hidden;
5229		}
5230
5231		if ( calcDetails.inSection === '0' && calcDetails.inEmbedForm === '0' ) {
5232			// Field is not in a section or embedded form
5233			hidden = isNonRepeatingFieldConditionallyHidden( fieldId, hiddenFields );
5234		} else {
5235			// Field is in a section or embedded form
5236			var repeatArgs = getRepeatArgsFromFieldName( triggerFieldName );
5237
5238			if ( isNonRepeatingFieldConditionallyHidden( fieldId, hiddenFields ) ) {
5239				// Check standard field
5240				hidden = true;
5241			} else if ( isRepeatingFieldConditionallyHidden( fieldId, repeatArgs, hiddenFields ) ) {
5242				// Check repeating field
5243				hidden = true;
5244			} else if ( calcDetails.inSection !== '0' && calcDetails.inEmbedForm !== '0' ) {
5245				// Check section in embedded form
5246				hidden = isRepeatingFieldConditionallyHidden( calcDetails.inSection, repeatArgs, hiddenFields );
5247			} else if ( calcDetails.inSection !== '0' ) {
5248				// Check section
5249				hidden = isNonRepeatingFieldConditionallyHidden( calcDetails.inSection, hiddenFields );
5250			} else if ( calcDetails.inEmbedForm !== '0' ) {
5251				// Check embedded form
5252				hidden = isNonRepeatingFieldConditionallyHidden( calcDetails.inEmbedForm, hiddenFields );
5253			}
5254		}
5255
5256		return hidden;
5257	}
5258
5259	// Check if a non-repeating field is conditionally hidden
5260	function isNonRepeatingFieldConditionallyHidden( fieldId, hiddenFields ) {
5261		var htmlID = 'frm_field_' + fieldId + '_container';
5262		return ( hiddenFields.includes(htmlID) );
5263	}
5264
5265	// Check if a repeating field is conditionally hidden
5266	function isRepeatingFieldConditionallyHidden( fieldId, repeatArgs, hiddenFields ) {
5267		var hidden = false;
5268
5269		if ( repeatArgs.repeatingSection ) {
5270			var fieldRepeatId = 'frm_field_' + fieldId + '-' + repeatArgs.repeatingSection;
5271			fieldRepeatId += '-' + repeatArgs.repeatRow + '_container';
5272			hidden = ( hiddenFields.includes(fieldRepeatId) );
5273		}
5274
5275		return hidden;
5276	}
5277
5278	function maybeShowCalculationsErrorAlert( err, fieldKey, thisFullCalc ) {
5279		var alertMessage = '';
5280
5281		if ( ( ! jQuery( 'form' ).hasClass( 'frm-admin-viewing' ) ) ) {
5282			return;
5283		}
5284
5285		alertMessage += frm_js.calc_error + ' ' + fieldKey + ':\n\n';
5286		alertMessage += thisFullCalc + '\n\n';
5287
5288		if ( err.message ) {
5289			alertMessage += err.message + '\n\n';
5290		}
5291		alert( alertMessage );
5292	}
5293
5294	/**
5295	 * Adjust a Date for timezone offset
5296	 *
5297	 * @param {Date} date
5298	 * @returns {Date}
5299	 */
5300	function treatAsUTC( date ) {
5301		var copy = new Date( date.valueOf() );
5302		copy.setMinutes( copy.getMinutes() - copy.getTimezoneOffset() );
5303		return copy;
5304	}
5305
5306	/**
5307	 * Try to change date strings and ints to Date objects before calculations
5308	 *
5309	 * @param {mixed} date
5310	 * @returns {mixed}
5311	 */
5312	function normalizeDate( date ) {
5313		switch ( typeof date ) {
5314			case 'number': return new Date( date * 86400000 ); // 1000 * 60 * 60 * 24 (milliseconds per day)
5315			case 'string': return new Date( date );
5316			default:       return date;
5317		}
5318	}
5319
5320	/**
5321	 * Calculate the difference between two dates.
5322	 * This is triggered by the [age] and [date_calc] shortcodes.
5323	 *
5324	 * @param {mixed} a                    Field date.
5325	 * @param {mixed} b                    Compare date.
5326	 * @param {string} format              Either 'days' or 'years'.
5327	 * @param {number|undefined} fieldId   This is defined by the datepicker add-on.
5328	 * @param {number|undefined} compareId This is defined by the datepicker add-on.
5329	 * @returns {int}
5330	 */
5331	function calculateDateDifference( a, b, format, fieldId, compareId ) {
5332		a = normalizeDate( a );
5333		b = normalizeDate( b );
5334
5335		switch ( format ) {
5336			case 'days': {
5337				if ( 'function' === typeof window.frmCalcDateDifferenceDays && fieldId && compareId ) {
5338					return window.frmCalcDateDifferenceDays( treatAsUTC( a ), treatAsUTC( b ), fieldId, compareId );
5339				}
5340
5341				return Math.floor( ( treatAsUTC( b ) - treatAsUTC( a ) ) / 86400000 ); // 1000 * 60 * 60 * 24 (milliseconds per day)
5342			}
5343
5344			case 'years': default: {
5345				var years = b.getFullYear() - a.getFullYear();
5346				if ( b.getMonth() < a.getMonth() || b.getMonth() === a.getMonth() && b.getDate() < a.getDate() ) {
5347					years--;
5348				}
5349
5350				return years;
5351			}
5352		}
5353	}
5354
5355	/**
5356	 * @param {object} triggerField
5357	 * @param {string} fieldKey
5358	 *
5359	 * @returns {object}
5360	 */
5361	function getTotalOrCalcField( triggerField, fieldKey ) {
5362		const form = document.contains( triggerField[0] ) ? triggerField.closest( 'form' ) : jQuery( `#frm_field_${triggerField[0].dataset.sectionid}_container` ).closest( 'form' );
5363		return form.find( fieldKey );
5364	}
5365
5366	/**
5367	 * Set value on a field, properly handling IMask instances.
5368	 * IMask handles formatting (leading zeros, fixed literals, etc.) natively
5369	 * when value is set through the mask instance.
5370	 *
5371	 * @since 6.23
5372	 *
5373	 * @param {jQuery} field The field element
5374	 * @param {string|number} value The value to set
5375	 * @returns {void}
5376	 */
5377	function setFieldValueWithMask( field, value ) {
5378		const fieldElement = field.get( 0 );
5379
5380		if ( fieldElement && frmMaskInstances.has( fieldElement ) ) {
5381			const padded = padValueForMask( String( value ), fieldElement.dataset.frmmask || '' );
5382			frmMaskInstances.get( fieldElement ).value = padded;
5383		} else {
5384			field.val( value );
5385		}
5386	}
5387
5388	/**
5389	 * Pad a numeric value so it fills all digit slots in a mask pattern.
5390	 * IMask processes characters left-to-right and cannot pad on its own,
5391	 * so e.g. value "5" with mask "00.00" needs to become "05.00".
5392	 *
5393	 * Non-numeric values or masks without digit slots are returned as-is.
5394	 *
5395	 * @since 6.23
5396	 *
5397	 * @param {string} value The string value to pad.
5398	 * @param {string} mask  The mask pattern (e.g. "00", "00.00", "P0").
5399	 * @returns {string}
5400	 */
5401	function padValueForMask( value, mask ) {
5402		if ( ! mask ) {
5403			return value;
5404		}
5405
5406		// Strip non-digit/non-decimal/non-minus characters from the value for counting.
5407		const numericValue = value.replace( /[^0-9.\-]/g, '' );
5408		if ( '' === numericValue || isNaN( Number( numericValue ) ) ) {
5409			return value;
5410		}
5411
5412		// Split the mask into parts around the literal "." if present.
5413		const maskParts = mask.split( '.' );
5414		const intMask = maskParts[0];
5415		const decMask = maskParts.length > 1 ? maskParts.slice( 1 ).join( '.' ) : '';
5416
5417		// Count only digit-slot characters (0 and 9) in each part.
5418		const intSlots = ( intMask.match( /[09]/g ) || [] ).length;
5419		const decSlots = ( decMask.match( /[09]/g ) || [] ).length;
5420
5421		if ( 0 === intSlots && 0 === decSlots ) {
5422			return value;
5423		}
5424
5425		// Split the numeric value around the decimal point.
5426		const valueParts = numericValue.split( '.' );
5427		let intValue = ( valueParts[0] || '0' ).replace( /^-/, '' );
5428		const isNegative = numericValue.startsWith( '-' );
5429
5430		// Pad integer part with leading zeros.
5431		if ( intSlots > intValue.length ) {
5432			intValue = intValue.padStart( intSlots, '0' );
5433		}
5434
5435		let result = ( isNegative ? '-' : '' ) + intValue;
5436
5437		// Pad decimal part with trailing zeros.
5438		if ( decSlots > 0 ) {
5439			let decValue = valueParts[1] || '';
5440			decValue = decValue.padEnd( decSlots, '0' ).substring( 0, decSlots );
5441			result += '.' + decValue;
5442		}
5443
5444		return result;
5445	}
5446
5447	function doSingleCalculation( allCalcs, fieldKey, vals, triggerField ) {
5448		var currency, total, dec, updatedTotal, totalField,
5449			thisCalc = allCalcs.calc[ fieldKey ],
5450			thisFullCalc = thisCalc.calc,
5451			fieldInfo = {
5452				triggerField,
5453				inSection: false,
5454				thisFieldCall: 'input[id^="field_' + fieldKey + '-"]'
5455			};
5456
5457		if ( triggerField !== undefined && triggerField && triggerField.length ) {
5458			totalField = getTotalOrCalcField( triggerField, '#field_' + fieldKey );
5459		} else {
5460			totalField = jQuery( document.getElementById( 'field_' + fieldKey ) );
5461		}
5462
5463		if ( totalField.get( 0 )?.dataset.minGap ) { // Avoid running calculation for Range sliders.
5464			return;
5465		}
5466
5467		// exit early for sliders as there is no calculation for such types and avoid errors that might rise due to formatting the value
5468		if ( totalField.attr( 'type' ) === 'range' ) {
5469			return;
5470		}
5471
5472		// TODO: update this to work more like conditional logic
5473		if ( totalField.length < 1 && triggerField !== undefined ) {
5474			// check if the total field is inside of a repeating/embedded form
5475			fieldInfo.inSection = true;
5476			fieldInfo.thisFieldId = objectSearch( allCalcs.fieldsWithCalc, fieldKey );
5477			totalField = getSiblingField( fieldInfo );
5478		}
5479
5480		if ( totalField === null || totalField.length < 1 ) {
5481			return;
5482		}
5483
5484		// loop through the fields in this calculation
5485		thisFullCalc = getValsForSingleCalc( thisCalc, thisFullCalc, allCalcs, vals, fieldInfo );
5486
5487		total = '';
5488		dec = '';
5489
5490		if ( 'function' === typeof window[ 'frmProGetCalcTotal' + thisCalc.calc_type ]) {
5491			total = window[ 'frmProGetCalcTotal' + thisCalc.calc_type ].call( thisCalc, thisFullCalc, { totalField });
5492		} else if ( thisCalc.calc_type === 'text' ) {
5493			total = thisFullCalc;
5494		} else {
5495			if ( 'date' === thisCalc.calc_type ) {
5496				// If the datepicker JS did not load, skip date calculations.
5497				return;
5498			}
5499
5500			// Set the number of decimal places
5501			dec = thisCalc.calc_dec;
5502
5503			// allow .toFixed for reverse compatibility
5504			if ( thisFullCalc.includes(').toFixed(') ) {
5505				var calcParts = thisFullCalc.split( ').toFixed(' );
5506				if ( isNumeric( calcParts[ 1 ]) ) {
5507					dec = calcParts[ 1 ];
5508					thisFullCalc = thisFullCalc.replace( ').toFixed(' + dec, '' );
5509				}
5510			}
5511
5512			thisFullCalc = trimNumericCalculation( thisFullCalc );
5513
5514			if ( thisFullCalc !== '' ) {
5515				try {
5516					total = parseFloat( eval( thisFullCalc ) );
5517				} catch ( err ) {
5518					maybeShowCalculationsErrorAlert( err, fieldKey, thisFullCalc );
5519				}
5520			}
5521
5522			if ( total === undefined || isNaN( total ) ) {
5523				total = 0;
5524			}
5525
5526			// Set decimal points
5527			if ( isNumeric( dec ) && total !== '' ) {
5528				total = total.toFixed( dec );
5529			}
5530		}
5531
5532		if ( thisCalc.is_currency === true && isNumeric( total ) ) {
5533			currency = getCurrencyFromCalcRule( thisCalc );
5534			if ( currency.decimals > 0 ) {
5535				total = Math.round10( total, currency.decimals );
5536				total = maybeAddTrailingZeroToPrice( total, currency );
5537				dec = currency.decimals;
5538			}
5539		}
5540
5541		if ( totalField.val() === total ) {
5542			setDisplayedTotal( totalField, total, currency );
5543			return;
5544		}
5545
5546		updatedTotal = false;
5547		if ( ( isNumeric( dec ) || thisCalc.is_currency ) && [ 'number', 'text' ].includes(totalField.attr( 'type' )) ) {
5548			if ( total.toString().slice( -1 ) == '0' && navigator.userAgent.toLowerCase().includes('firefox') ) {
5549				// Change the input to text in Firefox. Otherwise, trailing decimals will fail.
5550				totalField[0].setAttribute( 'type', 'text' );
5551			}
5552
5553			// The wrapper is frm_with_box when only one of prepend/append is set, and frm_with_boxes when both are.
5554			// A number input cannot hold a comma decimal separator, so switch it back to a period before setting it.
5555			if ( totalField.parent().is( '.frm_with_box.frm_hidden, .frm_with_boxes.frm_hidden' ) && 'string' === typeof total ) {
5556				updatedTotal = true;
5557				setFieldValueWithMask( totalField, total.replace( ',', '.' ) );
5558			}
5559		}
5560
5561		if ( ! updatedTotal ) {
5562			// Set value using proper mask handling
5563			setFieldValueWithMask( totalField, total );
5564		}
5565
5566		triggerEvent( document, 'frmCalcUpdatedTotal', {
5567			totalField,
5568			total
5569		});
5570
5571		if ( triggerField === null || triggerField === undefined || totalField.attr( 'name' ) != triggerField.attr( 'name' ) ) {
5572			totalField.trigger({ type: 'change', selfTriggered: true, frmTriggered: fieldKey });
5573		}
5574
5575		setDisplayedTotal( totalField, total, currency );
5576	}
5577
5578	/**
5579	 * Show the total for frm_total fields.
5580	 */
5581	function setDisplayedTotal( totalField, total, currency ) {
5582		var prepend, append,
5583			showTotal = totalField.parent().prev();
5584
5585		if ( ! showTotal.hasClass( 'frm_total_formatted' ) ) {
5586			return;
5587		}
5588
5589		prepend = showTotal.data( 'prepend' );
5590		append = showTotal.data( 'append' );
5591		if ( prepend === undefined ) {
5592			prepend = '';
5593		}
5594		if ( append === undefined ) {
5595			append = '';
5596		}
5597
5598		if ( typeof currency === 'object' ) {
5599			total = formatCurrency( total, currency );
5600			if ( currency.symbol_left === prepend ) {
5601				prepend = '';
5602			}
5603			if ( currency.symbol_right === append ) {
5604				append = '';
5605			}
5606		}
5607
5608		if ( prepend !== '' ) {
5609			prepend = '<span class="frm_inline_pre">' + prepend + '</span>';
5610		}
5611
5612		if ( append !== '' ) {
5613			append = '<span class="frm_inline_pre">' + append + '</span>';
5614		}
5615
5616		showTotal.html( prepend + '<span class="frm_inline_total">' + total + '</span>' + append );
5617	}
5618
5619	function getValsForSingleCalc( thisCalc, thisFullCalc, allCalcs, vals, fieldInfo ) {
5620		var fCount, f, field, date, findVar;
5621
5622		fCount = thisCalc.fields.length;
5623
5624		for ( f = 0; f < fCount; f++ ) {
5625			field = {
5626				triggerField: fieldInfo.triggerField,
5627				thisFieldId: thisCalc.fields[ f ],
5628				inSection: fieldInfo.inSection,
5629				valKey: fieldInfo.inSection + '' + thisCalc.fields[ f ],
5630				thisField: allCalcs.fields[ thisCalc.fields[ f ] ],
5631				thisFieldCall: 'input' + allCalcs.fieldKeys[ thisCalc.fields[ f ] ],
5632				formID: thisCalc.form_id
5633			};
5634
5635			field = getCallForField( field, allCalcs );
5636
5637			if ( ! thisCalc.calc_type ) {
5638				// Math calc type.
5639				field.valKey = 'num' + field.valKey;
5640				vals = getCalcFieldId( field, allCalcs, vals );
5641
5642				if ( vals[ field.valKey ] === undefined || isNaN( vals[ field.valKey ]) ) {
5643					vals[ field.valKey ] = 0;
5644
5645					if ( field.thisField.type === 'date' ) {
5646						date = tryToGetDateValue( field );
5647						if ( null !== date ) {
5648							vals[ field.valKey ] = Math.floor( date.getTime() / 86400000 );
5649						} else {
5650							thisFullCalc = '';
5651						}
5652					}
5653				} else if ( 0 === vals[ field.valKey ] && field.thisField.type === 'date' && dateValueShouldBeClearedForDateCalculation( field, fieldInfo ) ) {
5654					thisFullCalc = '';
5655				}
5656			} else {
5657				// Text calc type.
5658				field.valKey = 'text' + field.valKey;
5659				vals = getTextCalcFieldId( field, vals );
5660				if ( vals[ field.valKey ] === undefined ) {
5661					vals[ field.valKey ] = '';
5662				}
5663			}
5664
5665			if ( thisCalc.calc_type === 'text' ) {
5666				thisFullCalc = replaceShortcodesWithShowOptions( thisFullCalc, vals, field );
5667			}
5668
5669			findVar = '[' + field.thisFieldId + ']';
5670			findVar = findVar.replace( /([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1' );
5671			thisFullCalc = thisFullCalc.replace( new RegExp( findVar, 'g' ), vals[ field.valKey ]);
5672		}
5673
5674		return thisFullCalc;
5675	}
5676
5677	/**
5678	 * Support show=label (for field with options) and a show= option for each part of a
5679	 * multi-part field, like show=first on a name field or show=city on an address field,
5680	 * in text calculations.
5681	 *
5682	 * @param {String} fullCalc
5683	 * @param {Array} vals
5684	 * @param {Object} field
5685	 * @returns {String}
5686	 */
5687	function replaceShortcodesWithShowOptions( fullCalc, vals, field ) {
5688		fullCalc = replaceShowShortcode(
5689			fullCalc, vals, field, 'label',
5690			() => {
5691				return getOptionLabelsFromValues( vals[ field.valKey ], field );
5692			}
5693		);
5694
5695		Array.prototype.forEach.call(
5696			getComboSubFields(),
5697			( subField ) => {
5698				fullCalc = replaceSubFieldShortcode( fullCalc, vals, field, subField );
5699			}
5700		);
5701
5702		return fullCalc;
5703	}
5704
5705	/**
5706	 * The parts of a multi-part field that a text calculation can show on their own.
5707	 * Kept in step with FrmProFormsHelper::get_combo_sub_fields, which decides
5708	 * which show= options survive into the formula.
5709	 *
5710	 * @returns {Array}
5711	 */
5712	function getComboSubFields() {
5713		return [
5714			// Name field parts.
5715			'first',
5716			'middle',
5717			'last',
5718			// Address field parts.
5719			'line1',
5720			'line2',
5721			'city',
5722			'state',
5723			'zip',
5724			'country'
5725		];
5726	}
5727
5728	function replaceSubFieldShortcode( fullCalc, vals, field, show ) {
5729		var valueCallback = function() {
5730			var match = false;
5731			document.querySelectorAll( field.thisFieldCall ).forEach(
5732				( input ) => {
5733					if ( show === input.id.substr( -show.length ) ) {
5734						match = input;
5735					}
5736				}
5737			);
5738			return match ? match.value : '';
5739		};
5740		return replaceShowShortcode( fullCalc, vals, field, show, valueCallback );
5741	}
5742
5743	function replaceShowShortcode( fullCalc, vals, field, show, valueCallback ) {
5744		var findVar;
5745
5746		findVar = '[' + field.thisFieldId + ' show=' + show + ']';
5747		if ( -1 === fullCalc.indexOf( findVar ) ) {
5748			return fullCalc;
5749		}
5750
5751		vals[ field.valKey + show ] = valueCallback();
5752		findVar                     = findVar.replace( /([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1' );
5753		fullCalc                    = fullCalc.replace( new RegExp( findVar, 'g' ), vals[ field.valKey + show ]);
5754
5755		return fullCalc;
5756	}
5757
5758	function tryToGetDateValue( field ) {
5759		var $element = jQuery( field.thisField.key );
5760		return $element.hasClass( 'hasDatepicker' ) ? $element.datepicker( 'getDate' ) : null;
5761	}
5762
5763	/**
5764	 * Check if a date value is ready for use in a calculation.
5765	 */
5766	function dateValueShouldBeClearedForDateCalculation( field, fieldInfo ) {
5767		if ( fieldInfo.triggerField !== null ) {
5768			if ( fieldInfo.triggerField.is( 'input' ) ) {
5769				if ( datepickerFieldShouldBeClearedForDateCalculation( fieldInfo.triggerField ) ) {
5770					return fieldShouldBeClearedForDateCalculation( field.thisFieldCall, field.thisField.key );
5771				}
5772				return '' === fieldInfo.triggerField.val();
5773			}
5774			return fieldInfo.triggerField.is( 'div' ) && fieldInfo.triggerField.end().is( 'input' ) && '' === fieldInfo.triggerField.end().val();
5775		} else if ( fieldShouldBeClearedForDateCalculation( field.thisFieldCall, field.thisField.key ) ) {
5776			return true;
5777		}
5778		return false;
5779	}
5780
5781	function datepickerFieldShouldBeClearedForDateCalculation( field ) {
5782		var dateValue = field.hasClass( 'hasDatepicker' ) ? field.datepicker( 'getDate' ) : null;
5783		return null !== dateValue && -72000000 !== dateValue.getTime();
5784	}
5785
5786	function fieldShouldBeClearedForDateCalculation( fieldCall, fieldKey ) {
5787		return 0 === fieldCall.indexOf( 'input' ) && 0 === fieldKey.indexOf( '[id=' ) && '' === jQuery( fieldKey ).val();
5788	}
5789
5790	function getOptionLabelsFromValues( value, field ) {
5791		var fieldId, options, split, labels, length, index;
5792
5793		fieldId = field.thisFieldId;
5794		if ( 'undefined' === typeof __FRMCALC.options || 'undefined' === typeof __FRMCALC.options[ fieldId ]) {
5795			return value;
5796		}
5797
5798		options = __FRMCALC.options[ fieldId ];
5799
5800		if ( 'checkbox' === field.thisField.type ) {
5801			split = value.split( ', ' );
5802			labels = [];
5803			length = split.length;
5804			for ( index = 0; index < length; ++index ) {
5805				if ( 'undefined' !== typeof options[ split[ index ] ]) {
5806					labels.push( options[ split[ index ] ]);
5807				}
5808			}
5809			return labels.join( ', ' );
5810		}
5811
5812		return 'undefined' !== typeof options[ value ] ? options[ value ] : '';
5813	}
5814
5815	/**
5816	 * Trim non-numeric characters from the end of a numeric calculation
5817	 *
5818	 * @since 2.03.02
5819	 * @param {String} numericCalc
5820	 * @returns {String}
5821	 */
5822	function trimNumericCalculation( numericCalc ) {
5823		var lastChar = numericCalc.charAt( numericCalc.length - 1 );
5824		if ( lastChar === '+' || lastChar === '-' ) {
5825			numericCalc = numericCalc.substr( 0, numericCalc.length - 1 );
5826		}
5827
5828		return numericCalc;
5829	}
5830
5831	function getCallForField( field, allCalcs ) {
5832		if ( field.thisField.type === 'checkbox' || field.thisField.type === 'radio' || field.thisField.type === 'scale' || field.thisField.type === 'star' ) {
5833			field.thisFieldCall = field.thisFieldCall + ':checked,' + field.thisFieldCall + '[type=hidden]';
5834		} else if ( field.thisField.type === 'select' || field.thisField.type === 'time' ) {
5835			field.thisFieldCall = 'select' + allCalcs.fieldKeys[ field.thisFieldId ] + ' option:selected,' + field.thisFieldCall + '[type=hidden]';
5836		} else if ( field.thisField.type === 'textarea' ) {
5837			field.thisFieldCall = field.thisFieldCall + ',textarea' + allCalcs.fieldKeys[ field.thisFieldId ];
5838		}
5839		return field;
5840	}
5841
5842	function maybeDoCalcForSingleField( fieldInput ) {
5843		if ( typeof __FRMCALC === 'undefined' ) {
5844			// there are no calculations on this page
5845			return;
5846		}
5847
5848		// Exit now if field is a type that can't do calculations
5849		if ( ! fieldCanDoCalc( fieldInput.type ) ) {
5850			return;
5851		}
5852
5853		var allCalcs = __FRMCALC,
5854			fieldKey = getFieldKey( fieldInput.id, fieldInput.name ),
5855			triggerField = maybeGetTriggerField( fieldInput ),
5856			vals = [];
5857
5858		if ( allCalcs.calc[ fieldKey ] === undefined ) {
5859			// This field doesn't have any calculations
5860			return;
5861		}
5862
5863		doSingleCalculation( allCalcs, fieldKey, vals, triggerField );
5864	}
5865
5866	function fieldCanDoCalc( fieldType ) {
5867		return -1 !== [ 'text', 'hidden', 'number', 'textarea' ].indexOf( fieldType );
5868	}
5869
5870	function getFieldKey( fieldHtmlId, fieldName ) {
5871		var fieldKey = fieldHtmlId.replace( 'field_', '' ),
5872			newFieldKey = '';
5873
5874		if ( isRepeatingFieldByName( fieldName ) ) {
5875			var fieldKeyParts = fieldKey.split( '-' );
5876			for ( var i = 0; i < fieldKeyParts.length - 1; i++ ) {
5877				if ( newFieldKey === '' ) {
5878					newFieldKey = fieldKeyParts[ i ];
5879				} else {
5880					newFieldKey = newFieldKey + '-' + fieldKeyParts[ i ];
5881				}
5882			}
5883			fieldKey = newFieldKey;
5884		}
5885
5886		return fieldKey;
5887	}
5888
5889	function maybeGetTriggerField( fieldInput ) {
5890		var triggerField = null;
5891		if ( isRepeatingFieldByName( fieldInput.name ) ) {
5892			if ( fieldInput.type !== 'hidden' ) {
5893				triggerField = jQuery( fieldInput ).closest( '.frm_form_field' );
5894			} else {
5895				triggerField = jQuery( fieldInput );
5896			}
5897		}
5898
5899		return triggerField;
5900	}
5901
5902	function isRepeatingFieldByName( fieldName ) {
5903		var fieldNameParts = fieldName.split( '][' );
5904		return fieldNameParts.length >= 3;
5905	}
5906
5907	function getCalcFieldId( field, allCalcs, vals ) {
5908		if ( vals[ field.valKey ] !== undefined && vals[ field.valKey ] !== 0 ) {
5909			return vals;
5910		}
5911
5912		vals[ field.valKey ] = 0;
5913
5914		var currency,
5915			calcField = getCalcField( field );
5916
5917		if ( calcField === false ) {
5918			return vals;
5919		}
5920
5921		calcField.each( function() {
5922			const thisVal = getOptionValue( field.thisField, this );
5923			const hasCustomFormat = this.classList.contains( 'frm-has-number-format' );
5924
5925			if ( field.thisField.type === 'date' ) {
5926				var d = getDateFieldValue( allCalcs.date, thisVal );
5927                if ( d !== null ) {
5928					vals[ field.valKey ] = Math.ceil( d / ( 1000 * 60 * 60 * 24 ) );
5929                }
5930			} else if ( 'data' === field.thisField.type ) {
5931				vals[ field.valKey ] = 0;
5932				if ( '' !== thisVal ) {
5933					if ( 'SELECT' === this.tagName ) {
5934						vals[ field.valKey ] = parseFloat( this.querySelector( 'option[value="' + thisVal + '"]' ).textContent );
5935					} else if ( null !== this.closest( '.frm_checkbox' ) ) {
5936						vals[ field.valKey ] = 0;
5937						jQuery( this.closest( '.frm_opt_container' ).querySelectorAll( 'input:checked' ) ).each(
5938							function() {
5939								vals[ field.valKey ] += parseFloat( this.parentNode.textContent );
5940							}
5941						);
5942					} else {
5943						vals[ field.valKey ] = parseFloat( this.parentNode.textContent );
5944					}
5945				}
5946			} else if ( this.hasAttribute( 'data-frmprice' ) || field.thisField.type === 'total' || hasCustomFormat ) {
5947				// data-frmprice means product field.
5948				currency = getCurrency( field.formID );
5949				vals[ field.valKey ] += parseFloat(
5950					! currency
5951						? thisVal
5952						: ( hasCustomFormat && String( thisVal ).trim().startsWith( '-' ) ? '-' : '' ) + preparePrice( thisVal, currency )
5953					);
5954			} else {
5955				var n = thisVal;
5956
5957				if ( n !== '' && n !== 0 ) {
5958					n = n.trim();
5959					// Include e to support scientific notation like 10e2.
5960					n = parseFloat( n.replace( /,/g, '' ).match( /-?[\d\.e]+$/ ) );
5961				}
5962
5963				if ( n === undefined || isNaN( n ) || n === '' ) {
5964					n = 0;
5965				}
5966				vals[ field.valKey ] += n;
5967			}
5968		});
5969
5970		return vals;
5971	}
5972
5973	function getTextCalcFieldId( field, vals ) {
5974		if ( vals[ field.valKey ] !== undefined && vals[ field.valKey ] !== '' ) {
5975			return vals;
5976		}
5977
5978		vals[ field.valKey ] = '';
5979
5980		var calcField = getCalcField( field );
5981		if ( calcField === false ) {
5982			return vals;
5983		}
5984
5985		var count = 0;
5986		var sep = '';
5987
5988		calcField.each( function() {
5989			var thisVal = getOptionValue( field.thisField, this );
5990			thisVal = thisVal.trim();
5991
5992			sep = getCalcSep( field, count );
5993
5994			if ( thisVal !== '' ) {
5995				vals[ field.valKey ] += sep + thisVal;
5996				count++;
5997			}
5998		});
5999
6000		return vals;
6001	}
6002
6003	function getCalcSep( field, count ) {
6004		var sep = '';
6005		if ( count > 0 ) {
6006			if ( field.thisField.type === 'time' ) {
6007				if ( count == 1 ) {
6008					sep = ':';
6009				} else if ( count == 2 ) {
6010					sep = ' ';
6011				}
6012			} else {
6013				sep = ', ';
6014			}
6015
6016			var customSep = jQuery( document ).triggerHandler( 'frmCalSeparation', [ field.thisField, count ]);
6017			if ( customSep !== undefined ) {
6018				sep = customSep;
6019			}
6020		}
6021		return sep;
6022	}
6023
6024	function getCalcField( field ) {
6025		var calcField;
6026		if ( field.inSection === false ) {
6027			if ( 'name' === field.thisField.type ) {
6028				return getOffScreenFieldForName( field );
6029			}
6030
6031			if ( 'undefined' !== typeof field.triggerField && field.triggerField && field.triggerField.length ) {
6032				calcField = getTotalOrCalcField( field.triggerField, field.thisFieldCall );
6033			} else {
6034				calcField = jQuery( field.thisFieldCall );
6035			}
6036			if ( ! calcField.length && -1 !== [ 'date', 'data' ].indexOf( field.thisField.type ) ) {
6037				calcField = jQuery( field.thisField.key );
6038				if ( ! calcField.length && 'data' === field.thisField.type ) {
6039					// radio and checkbox fields have additional hyphenated values in their ids.
6040					calcField = jQuery( field.thisField.key.replace( '="', '^=' ).replace( '"]', '-' ) + ']:checked' );
6041				}
6042			}
6043		} else {
6044			calcField = getSiblingField( field );
6045		}
6046
6047		if ( calcField === null || calcField === undefined || calcField.length < 1 ) {
6048			calcField = false;
6049		}
6050
6051		if ( calcField.length > 1 ) {
6052			calcField = filterCalcField( calcField, field.thisFieldId );
6053		}
6054
6055		return calcField;
6056	}
6057
6058	/**
6059	 * Filters calculation fields (elements) to be involved in the current calculation. This is so that elements that are in calcField just
6060	 * because their id starts with a similar string while they don't relate to the calculation are removed from the list.
6061	 *
6062	 * Fixes #1337.
6063	 *
6064	 * @param {Object} $calcField    A jQuery object that contains a set of `elements to be involved in the current calculation.
6065	 * @param {String} thisFieldCall The query selector used to select calculation elements.
6066	 */
6067	function filterCalcField( $calcField, thisFieldId ) {
6068		return $calcField.filter( function() {
6069			var target = 'OPTION' === this.nodeName ? this.closest( 'select' ) : this;
6070			return target && target.name && target.name.includes(thisFieldId);
6071		});
6072	}
6073
6074	/**
6075	 * Create an input with both first and last name.
6076	 * This isn't actually added to the page, but is used for Name calculations.
6077	 *
6078	 * @param {Object} field
6079	 * @returns {Object}
6080	 */
6081	function getOffScreenFieldForName( field ) {
6082		var nameParts, input;
6083
6084		nameParts = [];
6085		document.querySelectorAll( field.thisFieldCall ).forEach(
6086			( input ) => {
6087				nameParts.push( input.value );
6088			}
6089		);
6090
6091		input       = document.createElement( 'input' );
6092		input.value = nameParts.join( ' ' );
6093		return jQuery( input );
6094	}
6095
6096	/**
6097	* Get the value from a date field regardless of whether datepicker is defined for it
6098	* Limitations: If using a format with a 2-digit date, '20' will be added to the front if the year is prior to 70
6099	*/
6100	function getDateFieldValue( dateFormat, thisVal ) {
6101		var d = 0;
6102
6103		if ( ! thisVal ) {
6104			// If no value was selected in date field, use 0
6105		} else if ( jQuery.datepicker === undefined ) {
6106			// If date field is not on the current page
6107
6108			const splitAt = [ '/', '.' ].find( char =>
6108 dateFormat.includes( char ) ) || '-';
6109			var year = '',
6110				month = '',
6111				day = '',
6112				formatPieces = dateFormat.split( splitAt ),
6113				datePieces = thisVal.split( splitAt );
6114
6115			for ( var i = 0; i < formatPieces.length; i++ ) {
6116				if ( formatPieces[ i ] === 'y' ) {
6117					var currentYear = new Date().getFullYear() + 15;
6118					var currentYearPlusFifteen = currentYear.toString().substr( 2, 2 );
6119
6120					if ( datePieces[ i ] > currentYearPlusFifteen ) {
6121						year = '19' + datePieces[ i ];
6122					} else {
6123						year = '20' + datePieces[ i ];
6124					}
6125				} else if ( formatPieces[ i ] === 'yy' ) {
6126					year = datePieces[ i ];
6127				} else if ( formatPieces[ i ] === 'm' || formatPieces[ i ] === 'mm' ) {
6128					month = datePieces[ i ];
6129					if ( month.length < 2 ) {
6130						month = '0' + month;
6131					}
6132				} else if ( formatPieces[ i ] === 'd' || formatPieces[ i ] === 'dd' ) {
6133					day = datePieces[ i ];
6134					if ( day.length < 2 ) {
6135						day = '0' + day;
6136					}
6137				}
6138			}
6139
6140			d = Date.parse( year + '-' + month + '-' + day );
6141		} else {
6142			d = jQuery.datepicker.parseDate( dateFormat, thisVal );
6143		}
6144		return d;
6145	}
6146
6147	function getSiblingField( field ) {
6148		if ( field.triggerField === undefined ) {
6149			return null;
6150		}
6151
6152		var fields = null,
6153			container = field.triggerField.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ),
6154			repeatArgs = getRepeatArgsFromFieldName( field.triggerField.attr( 'name' ) ),
6155			siblingFieldCall = field.thisFieldCall.replace( '[id=', '[id^=' ).replace( /-"]/g, '-' + repeatArgs.repeatRow + '"]' );
6156
6157		if ( container.length || repeatArgs.repeatRow !== '' ) {
6158			if ( container.length ) {
6159				fields = container.find( siblingFieldCall + ',' + siblingFieldCall.replace( 'input[', 'select[' ) );
6160			} else {
6161				fields = jQuery( siblingFieldCall );
6162			}
6163
6164			if ( fields === null || fields === undefined || fields.length < 1 ) {
6165				fields = uncheckedSiblingOrOutsideSection( field, container, siblingFieldCall );
6166			}
6167		} else {
6168			// the trigger is not in the repeating section
6169			fields = getNonSiblingField( field );
6170		}
6171
6172		return fields;
6173	}
6174
6175	function uncheckedSiblingOrOutsideSection( field, container, siblingFieldCall ) {
6176		var fields = null;
6177		if ( siblingFieldCall.indexOf( ':checked' ) ) {
6178			// check if the field has nothing selected, or is not inside the section
6179			var inSection = container.find( siblingFieldCall.replace( ':checked', '' ) );
6180			if ( inSection.length < 1 ) {
6181				fields = getNonSiblingField( field );
6182			}
6183		} else {
6184			// the field holding the value is outside of the section
6185			fields = getNonSiblingField( field );
6186		}
6187		return fields;
6188	}
6189
6190	function getNonSiblingField( field ) {
6191		var nonSiblingField = jQuery( field.thisFieldCall + ',' + field.thisFieldCall.replace( 'input[', 'select[' ) );
6192		if ( ! nonSiblingField.length && 'input[' === field.thisFieldCall.substr( 0, 6 ) ) {
6193			if ( 'undefined' !== typeof field.triggerField && field.triggerField.is( 'div' ) && field.triggerField.hasClass( 'frm_form_field' ) ) {
6194				nonSiblingField = field.triggerField.find( field.thisFieldCall.replace( 'input[', 'textarea[' ) );
6195			} else {
6196				nonSiblingField = jQuery( field.thisFieldCall.replace( 'input[', 'textarea[' ) );
6197			}
6198		}
6199		return nonSiblingField;
6200	}
6201
6202	function getOptionValue( thisField, currentOpt ) {
6203		var thisVal;
6204
6205		// If current option is an other option, get other value
6206		if ( isOtherOption( thisField, currentOpt ) ) {
6207			thisVal = getOtherValueAnyField( thisField, currentOpt );
6208		} else if ( currentOpt.type === 'checkbox' || currentOpt.type === 'radio' ) {
6209
6210			if ( currentOpt.checked ) {
6211				// hasAttribute( 'data-frmprice' ) is a product field.
6212				thisVal = currentOpt.hasAttribute( 'data-frmprice' ) ? currentOpt.dataset.frmprice : currentOpt.value;
6213			} else {
6214				thisVal = currentOpt.dataset.off;
6215			}
6216		} else if ( currentOpt.hasAttribute( 'data-frmprice' ) ) {
6217			thisVal = currentOpt.dataset.frmprice;
6218		} else if ( isCurrencyFormatField( thisField ) ) {
6219			const fieldKey = thisField.key.replace( '[id="field_', '' ).replace( '"]', '' );
6220			const calcRule = window.__FRMCALC?.calc?.[ fieldKey ];
6221
6222			thisVal = jQuery( currentOpt ).val();
6223
6224			if ( ! calcRule ) {
6225				return thisVal;
6226			}
6227
6228			const resultFieldKey = thisField.total[0];
6229			const totalField     = resultFieldKey ? window.__FRMCALC?.calc?.[ resultFieldKey ] : false;
6230
6231			if ( ! totalField || totalField.calc_type !== 'text' ) {
6232				const isNegative = '-' === thisVal.charAt( 0 );
6233				const currency   = getCurrencyFromCalcRule( calcRule );
6234
6235				thisVal = '' + preparePrice( thisVal, currency );
6236
6237				// Calling preparePrice() will remove the negative sign, so add it back if needed
6238				if ( isNegative ) {
6239					thisVal = '-' + thisVal;
6240				}
6241			}
6242		} else {
6243			thisVal = jQuery( currentOpt ).val();
6244		}
6245
6246		if ( thisVal === undefined ) {
6247			thisVal = '';
6248		}
6249		return thisVal;
6250	}
6251
6252	function isCurrencyFormatField( field ) {
6253		const fieldKey = field.key.replace( '[id="field_', '' ).replace( '"]', '' );
6254		const isCalcField = window.__FRMCALC?.calc?.[ fieldKey ]?.is_currency;
6255		return isCalcField ? true : false;
6256	}
6257
6258	/* Check if current option is an "Other" option (not an Other text field) */
6259	function isOtherOption( thisField, currentOpt ) {
6260		var isOtherOpt = false;
6261
6262		// If hidden, check for a value
6263		if ( currentOpt.type === 'hidden' ) {
6264			if ( getOtherValueLimited( currentOpt ) !== '' ) {
6265				isOtherOpt = true;
6266			}
6267		} else if ( thisField.type === 'select' ) {
6268			// If a visible dropdown field
6269			var optClass = currentOpt.className;
6270			if ( optClass && optClass.includes('frm_other_trigger') ) {
6271				isOtherOpt = true;
6272			}
6273		} else if ( thisField.type === 'checkbox' || thisField.type === 'radio' ) {
6274			// If visible checkbox/radio field
6275			if ( currentOpt.id.includes('-other_') && !currentOpt.id.includes('-otext') ) {
6276				isOtherOpt = true;
6277			}
6278		}
6279
6280		return isOtherOpt;
6281	}
6282
6283	/* Get the value from an "Other" text field */
6284	/* Does NOT work for visible select fields */
6285	function getOtherValueLimited( currentOpt ) {
6286		var otherVal = '',
6287			otherText = document.getElementById( currentOpt.id + '-otext' );
6288
6289		if ( otherText !== null && otherText.value !== '' ) {
6290			otherVal = otherText.value;
6291		}
6292		return otherVal;
6293	}
6294
6295	/* Get value from Other text field */
6296	function getOtherValueAnyField( thisField, currentOpt ) {
6297		var otherVal = 0;
6298
6299		if ( thisField.type === 'select' ) {
6300			if ( currentOpt.type === 'hidden' ) {
6301				if ( isCurrentOptRepeating( currentOpt ) ) {
6302					// Do nothing because regular doCalculation code takes care of it
6303				} else {
6304					otherVal = getOtherValueLimited( currentOpt );
6305				}
6306			} else {
6307				otherVal = getOtherSelectValue( currentOpt );
6308			}
6309		} else if ( thisField.type === 'checkbox' || thisField.type === 'radio' ) {
6310			if ( currentOpt.type === 'hidden' ) {
6311				// Do nothing because regular doCalculation code takes care of it
6312			} else {
6313				otherVal = getOtherValueLimited( currentOpt );
6314			}
6315		}
6316
6317		return otherVal;
6318	}
6319
6320	/* Check if current option is in a repeating section */
6321	function isCurrentOptRepeating( currentOpt ) {
6322		var isRepeating = false,
6323			parts = currentOpt.name.split( '[' );
6324
6325		if ( parts.length > 2 ) {
6326			isRepeating = true;
6327		}
6328		return isRepeating;
6329	}
6330
6331	/* Get value from Other text field in a visible dropdown field */
6332	function getOtherSelectValue( currentOpt ) {
6333		var fields = getOtherSelects( currentOpt );
6334		return fields.val();
6335	}
6336
6337	/**
6338	 * Fill in the other text, and select the other option from a dropdown
6339	 */
6340	function setOtherSelectValue( thisField, value ) {
6341		var i,
6342			fields = getOtherSelects( thisField );
6343
6344		if ( fields.length < 1 ) {
6345			return;
6346		}
6347
6348		fields.val( value );
6349
6350		for ( i = 0; i < thisField.options.length; i++ ) {
6351			if ( thisField.options[ i ].className.includes('frm_other_trigger') ) {
6352				thisField.options[ i ].selected = true;
6353			}
6354		}
6355	}
6356
6357	function getOtherSelects( currentOpt ) {
6358		return jQuery( currentOpt ).closest( '.frm_other_container' ).find( '.frm_other_input' );
6359	}
6360
6361	function setOtherValueLimited( thisField, value ) {
6362		var otherText, baseId, parentInput,
6363			i = 0,
6364			idParts = thisField.id.split( '-' );
6365		idParts.pop(); //remove the last id
6366		baseId = idParts.join( '-' );
6367		otherText = document.querySelectorAll( '[id^=' + baseId + '-other][id$=otext]' );
6368
6369		if ( otherText.length > 0 ) {
6370			for ( i = 0; i < otherText.length; i++ ) {
6371				if ( otherText[ i ].value === '' ) {
6372					otherText[ i ].value = value;
6373					parentInput = document.getElementById( otherText[ i ].id.replace( '-otext', '' ) );
6374					if ( parentInput !== null ) {
6375						parentInput.checked = true;
6376					}
6377				}
6378			}
6379		}
6380	}
6381
6382	function savingDraftEntry( object ) {
6383		var isDraft = false,
6384			savingDraft = jQuery( object ).find( '.frm_saving_draft' );
6385
6386		if ( savingDraft.length ) {
6387			isDraft = savingDraft.val();
6388		}
6389		return isDraft;
6390	}
6391
6392	function goingToPrevPage( object ) {
6393		var goingBack = false,
6394			nextPage = jQuery( object ).find( '.frm_next_page' );
6395
6396		if ( nextPage.length && nextPage.val() ) {
6397			var formID = jQuery( object ).find( 'input[name="form_id"]' ).val();
6398			var prevPage = jQuery( object ).find( 'input[name="frm_page_order_' + formID + '"]' );
6399			if ( prevPage.length ) {
6400				prevPage = parseInt( prevPage.val() );
6401			} else {
6402				prevPage = 0;
6403			}
6404
6405			if ( ! prevPage || ( parseInt( nextPage.val() ) < prevPage ) ) {
6406				goingBack = true;
6407			}
6408		}
6409
6410		return goingBack;
6411	}
6412
6413	function afterFormSubmitted( event, form ) {
6414		// if the success message is showing, run the logic
6415		checkConditionalLogic( 'pageLoad' );
6416		doEditInPlaceCleanUp( form );
6417		checkFieldsOnPage();
6418		maybeShowMoreStepsButton();
6419	}
6420
6421	function afterPageChanged() {
6422		checkFieldsOnPage();
6423		addTopAddRowBtnForRepeater();
6424		maybeDisableCheckboxesWithLimit();
6425		// make sure this comes last so that the total is calculated only after knowing the visible fields
6426		calcProductsTotal();
6427		maybeShowMoreStepsButton();
6428		// calculations in a repeater need to be done again after page is changed to fix issue #3414
6429		triggerChangeOnCalcTriggers();
6430		maybeAddIntlTelInput( document.querySelectorAll( '.frm-intl-tel-input' ) );
6431		initRangeInput( document.querySelectorAll( '.with_frm_style input[type=range]' ) );
6432		initFormatFieldValueNumbers();
6433	}
6434
6435	function triggerChangeOnCalcTriggers() {
6436		if ( 'undefined' === typeof __FRMCALC || 'undefined' === typeof __FRMCALC.fieldKeys ) {
6437			return;
6438		}
6439
6440		Object.values( __FRMCALC.fieldKeys ).forEach( ( key ) => {
6441			jQuery( key + ':not(label):not([type=hidden])' ).each( function() {
6442				jQuery( this ).trigger({ type: 'change', selfTriggered: true });
6443			});
6444		});
6445	}
6446
6447	/** Google Tables **/
6448
6449	function generateGoogleTables( graphs, graphType ) {
6450		for ( var num = 0; num < graphs.length; num++ ) {
6451			generateSingleGoogleTable( graphs[ num ], graphType );
6452		}
6453	}
6454
6455	function generateSingleGoogleTable( opts, type ) {
6456		google.load( 'visualization', '1.0', { packages: [ type ], callback() {
6457			compileGoogleTable( opts );
6458		} });
6459	}
6460
6461	function compileGoogleTable( opts ) {
6462		var data = new google.visualization.DataTable(),
6463			showID = false;
6464
6465		const ownerDocument = opts.document || document;
6466
6467		if ( jQuery.inArray( 'id', opts.options.fields ) !== -1 ) {
6468			showID = true;
6469			data.addColumn( 'number', frm_js.id );
6470		}
6471
6472		var colCount = opts.fields.length;
6473		var type = 'string';
6474		for ( var i = 0, l = colCount; i < l; i++ ) {
6475			var thisCol = opts.fields[ i ];
6476			type = getGraphType( thisCol );
6477
6478			data.addColumn( type, thisCol.name );
6479		}
6480
6481		var showEdit = false;
6482		if ( opts.options.edit_link ) {
6483			showEdit = true;
6484			data.addColumn( 'string', opts.options.edit_link );
6485		}
6486
6487		var showDelete = false;
6488		if ( opts.options.delete_link ) {
6489			showDelete = true;
6490			data.addColumn( 'string', opts.options.delete_link );
6491		}
6492
6493		var col = 0;
6494		if ( opts.entries !== null ) {
6495			var entryCount = opts.entries.length;
6496			data.addRows( entryCount );
6497
6498			var row = 0;
6499
6500			for ( var e = 0, len = entryCount; e < len; e++ ) {
6501				col = 0;
6502				var entry = opts.entries[ e ];
6503				if ( showID ) {
6504					data.setCell( row, col, entry.id );
6505					col++;
6506				}
6507
6508				for ( var field = 0, fieldCount = colCount; field < fieldCount; field++ ) {
6509					var thisEntryCol = opts.fields[ field ];
6510					type = getGraphType( thisEntryCol );
6511
6512					var fieldVal = entry.metas[ thisEntryCol.id ];
6513					if ( type === 'number' && ( fieldVal === null || fieldVal === '' ) ) {
6514						fieldVal = 0;
6515					} else if ( type === 'boolean' ) {
6516						if ( fieldVal === null || fieldVal == 'false' || fieldVal === false ) {
6517							fieldVal = false;
6518						} else {
6519							fieldVal = true;
6520						}
6521					}
6522
6523					data.setCell( row, col, fieldVal );
6524
6525					col++;
6526				}
6527
6528				if ( showEdit ) {
6529					if ( entry.editLink !== undefined ) {
6530						data.setCell( row, col, '<a href="' + entry.editLink + '">' + opts.options.edit_link + '</a>' );
6531					} else {
6532						data.setCell( row, col, '' );
6533					}
6534					col++;
6535				}
6536
6537				if ( showDelete ) {
6538					if ( entry.deleteLink !== undefined ) {
6539						data.setCell( row, col, '<a href="' + entry.deleteLink + '" class="frm_delete_link" data-frmconfirm="' + opts.options.confirm + '">' + opts.options.delete_link + '</a>' );
6540					} else {
6541						data.setCell( row, col, '' );
6542					}
6543				}
6544
6545				row++;
6546			}
6547		} else {
6548			data.addRows( 1 );
6549			col = 0;
6550
6551			for ( i = 0, l = colCount; i < l; i++ ) {
6552				if ( col > 0 ) {
6553					data.setCell( 0, col, '' );
6554				} else {
6555					data.setCell( 0, col, opts.options.no_entries );
6556				}
6557				col++;
6558			}
6559		}
6560
6561		var chart = new google.visualization.Table( ownerDocument.getElementById( 'frm_google_table_' + opts.options.form_id ) );
6562		chart.draw( data, opts.graphOpts );
6563	}
6564
6565	/** Google Graphs **/
6566
6567	function generateGoogleGraphs( graphs ) {
6568		var l, i;
6569		l = graphs.length;
6570		for ( i = 0; i < l; i++ ) {
6571			generateSingleGoogleGraph( graphs[ i ]);
6572
6573			if ( 'string' === typeof graphs[ i ].options.width && '%' === graphs[ i ].options.width.substr( -1 ) ) {
6574				addResponsiveGraphListener( graphs[ i ]);
6575			}
6576		}
6577	}
6578
6579	function addResponsiveGraphListener( graphData ) {
6580		window.addEventListener(
6581			'resize',
6582			() => {
6583				generateSingleGoogleGraph( graphData );
6584			}
6585		);
6586	}
6587
6588	function generateSingleGoogleGraph( graphData ) {
6589		google.charts.load( 'current', { packages: [ graphData.package ] });
6590		google.charts.setOnLoadCallback( () => {
6591			compileGoogleGraph( graphData );
6592		});
6593	}
6594
6595	function compileGoogleGraph( graphData ) {
6596		var data = new google.visualization.DataTable();
6597		data = google.visualization.arrayToDataTable( graphData.data );
6598
6599		const ownerDocument = graphData.document || document;
6600
6601		var chartDiv = ownerDocument.getElementById( 'chart_' + graphData.graph_id );
6602		if ( chartDiv === null ) {
6603			return;
6604		}
6605
6606		var type = ( graphData.type.charAt( 0 ).toUpperCase() + graphData.type.slice( 1 ) );
6607		if ( type !== 'Histogram' && type !== 'Table' ) {
6608			type += 'Chart';
6609		}
6610
6611		var chart = new google.visualization[ type ]( chartDiv );
6612
6613		chart.draw( data, graphData.options );
6614		jQuery( ownerDocument ).trigger( 'frmDrawChart', [ chart, 'chart_' + graphData.graph_id, data ]);
6615	}
6616
6617	function getGraphType( field ) {
6618		var type = 'string';
6619		if ( field.type === 'number' ) {
6620			type = 'number';
6621		} else if ( field.type === 'checkbox' || field.type === 'select' ) {
6622			var optCount = field.options.length;
6623			if ( field.type === 'select' && field.options[ 0 ] === '' ) {
6624				if ( field.field_options.post_field === 'post_status' ) {
6625					optCount = 3;
6626				} else {
6627					optCount = optCount - 1;
6628				}
6629			}
6630			if ( optCount == 1 ) {
6631				type = 'boolean';
6632			}
6633		}
6634		return type;
6635	}
6636
6637	/** Repeating Fields **/
6638
6639	function removeRow() {
6640		if ( ! confirmRowRemoval() ) {
6641			return;
6642		}
6643
6644		/*jshint validthis:true */
6645		var rowNum = jQuery( this ).data( 'key' ),
6646			sectionID = jQuery( this ).data( 'parent' ),
6647			id = 'frm_section_' + sectionID + '-' + rowNum,
6648			thisRow = jQuery( this ).parents( 'div[id^="frm_section_"]' ),
6649			fields = thisRow.find( 'input, select, textarea, .frm_html_container' ),
6650			formId = jQuery( this ).closest( 'form' ).find( 'input[name="form_id"]' ).val();
6651
6652		thisRow.fadeOut( 'slow', () => {
6653			const repeaterRow = thisRow[0].closest( '.frm_section_heading');
6654			if ( repeaterRow.querySelectorAll( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' )?.length === 1 ) {
6655				repeaterRow.querySelector( '.frm_hidden_container.frm_repeat_buttons.frm_hidden' ).style.display = 'inline-block';
6656			}
6657			thisRow.remove();
6658
6659			fields.each( function() {
6660				/* update calculations when a row is removed */
6661				var fieldID;
6662				if ( this.matches( '.frm_html_container' ) ) {
6663					fieldID = getHtmlFieldID( this );
6664				} else {
6665					fieldID = frmFrontForm.getFieldId( this, false );
6666				}
6667
6668				if ( this.type != 'file' ) {
6669					doCalculation( fieldID, jQuery( this ) );
6670				}
6671
6672				var container = 'frm_field_' + fieldID + '-' + sectionID + '-' + rowNum + '_container';
6673				removeFromHideFields( container, formId );
6674
6675				// If the field is an RTE, remove the editor associated with it
6676				//  this is done so that if/when the field is Added again it get initialized
6677				if ( this.classList.contains( 'wp-editor-area' ) ) {
6678					removeRichText( this.id );
6679				}
6680			});
6681
6682			showAddButton( sectionID );
6683			maybeHideRemoveButtons( sectionID );
6684
6685			/* eslint-disable camelcase */
6686			if ( typeof frmThemeOverride_frmRemoveRow === 'function' ) {
6687				frmThemeOverride_frmRemoveRow( id, thisRow );
6688			}
6689			/* eslint-enable camelcase */
6690
6691			jQuery( document ).trigger( 'frmAfterRemoveRow' );
6692		});
6693
6694		return false;
6695	}
6696
6697	/**
6698	 * When a repeater row is removed, check for the minimum number of rows and maybe hide remove buttons.
6699	 *
6700	 * @param {string|number} sectionID
6701	 * @returns {void}
6702	 */
6703	function maybeHideRemoveButtons( sectionID ) {
6704		var sectionContainer, repeatButtons, minRows, currentRows;
6705
6706		sectionContainer = document.querySelector( '#frm_field_' + sectionID + '_container' );
6707		if ( ! sectionContainer ) {
6708			return;
6709		}
6710
6711		repeatButtons = sectionContainer.querySelector( '.frm_repeat_buttons[data-repeat-min]' );
6712		if ( ! repeatButtons || ! repeatButtons.dataset.repeatMin ) {
6713			return;
6714		}
6715
6716		minRows = repeatButtons.dataset.repeatMin;
6717		currentRows = document.querySelectorAll( '.frm_repeat_' + sectionID ).length;
6718		if ( currentRows <= minRows ) {
6719			hideRemoveButtons( sectionID );
6720		}
6721	}
6722
6723	/**
6724	 * Gets the html field ID from the container in a repeater. Ex. frm_field_151-152-0_container (151 is the field ID in this case).
6725	 *
6726	 * @param {Array} field
6727	 * @returns {Number}
6728	 */
6729	function getHtmlFieldID( field ) {
6730		var parentIDParts;
6731
6732		parentIDParts = field.id.split( '_' );
6733
6734		if ( parentIDParts.length < 3 ) {
6735			return 0;
6736		}
6737
6738		parentIDParts = parentIDParts[2];
6739		parentIDParts = parentIDParts.split( '-' );
6740
6741		if ( ! parentIDParts.length ) {
6742			return 0;
6743		}
6744
6745		return parentIDParts[0];
6746	}
6747
6748	function confirmRowRemoval() {
6749		if ( ! frm_js.repeaterRowDeleteConfirmation ) {
6750			return true;
6751		}
6752		return confirm( frm_js.repeaterRowDeleteConfirmation );
6753	}
6754
6755	function hideAddButton( sectionID ) {
6756		getRepeaterAddButtons( sectionID ).forEach(
6757			( button ) => {
6758				button.classList.add( 'frm_hide_add_button' );
6759			}
6760		);
6761	}
6762
6763	function showAddButton( sectionID ) {
6764		getRepeaterAddButtons( sectionID ).forEach(
6765			( button ) => {
6766				button.classList.remove( 'frm_hide_add_button' );
6767			}
6768		);
6769	}
6770
6771	function getRepeaterAddButtons( sectionID ) {
6772		return getFieldContainerChildren( sectionID, '.frm_add_form_row' );
6773	}
6774
6775	function hideRemoveButtons( sectionID ) {
6776		getRepeaterRemoveButtons( sectionID ).forEach(
6777			( button ) => {
6778				button.classList.add( 'frm_hide_remove_button' );
6779			}
6780		);
6781	}
6782
6783	function showRemoveButtons( sectionID ) {
6784		getRepeaterRemoveButtons( sectionID ).forEach(
6785			( button ) => {
6786				button.classList.remove( 'frm_hide_remove_button' );
6787			}
6788		);
6789	}
6790
6791	function getRepeaterRemoveButtons( sectionID ) {
6792		return getFieldContainerChildren( sectionID, '.frm_remove_form_row' );
6793	}
6794
6795	/**
6796	 * @param {string|number} fieldD
6797	 * @returns {HTMLElement|null}
6798	 */
6799	function getFieldContainer( fieldD ) {
6800		return document.getElementById( 'frm_field_' + fieldD + '_container' );
6801	}
6802
6803	function getFieldContainerChildren( fieldID, childSelector ) {
6804		var container = getFieldContainer( fieldID );
6805		if ( ! container ) {
6806			return [];
6807		}
6808		return container.querySelectorAll( childSelector );
6809	}
6810
6811	function addRow() {
6812		/*jshint validthis:true */
6813		var thisBtn, id, i, numberOfSections, lastRowIndex, stateField, state, form, data, success, error, extraParams;
6814
6815		// If row is currently being added, leave now
6816		if ( currentlyAddingRow === true ) {
6817			return false;
6818		}
6819
6820		// Indicate that a row is being added (so double clicking Add button doesn't cause problems)
6821		currentlyAddingRow = true;
6822
6823		thisBtn = jQuery( this );
6824		id = thisBtn.data( 'parent' );
6825		i = 0;
6826		numberOfSections = jQuery( '.frm_repeat_' + id ).length;
6827
6828		if ( numberOfSections > 0 ) {
6829			lastRowIndex = false;
6830			document.querySelectorAll( '.frm_repeat_' + id ).forEach(
6831				( element ) => {
6832					var strippedId = element.id.replace( 'frm_section_' + id + '-', '' ),
6833						parsedId;
6834
6835					if ( ! strippedId.length || 'i' === strippedId[0]) {
6836						return;
6837					}
6838
6839					parsedId = parseInt( strippedId );
6840					// strippedId should be a unique number index.
6841					// Since there is no "i" prefix, this is not an entry ID.
6842					if ( parsedId && ( false === lastRowIndex || parsedId > lastRowIndex ) ) {
6843						lastRowIndex = parsedId;
6844					}
6845				}
6846			);
6847
6848			if ( false === lastRowIndex ) {
6849				i = 1;
6850			} else {
6851				i = lastRowIndex + 1;
6852			}
6853		}
6854
6855		stateField = document.querySelector( 'input[name="frm_state"]' );
6856		state = null !== stateField ? stateField.value : '';
6857
6858		form = jQuery( this ).closest( 'form' ).get( 0 );
6859		data = {
6860			action: 'frm_add_form_row',
6861			field_id: id,
6862			i,
6863			numberOfSections,
6864			nonce: frm_js.nonce,
6865			frm_state: state
6866		};
6867
6868		/**
6869		 * Allow other add-ons to add additional data when a new repeater row is added.
6870		 *
6871		 * @since 6.25
6872		 *
6873		 * @param {Object} data
6874		 */
6875		triggerEvent( form, 'frmBeforeNewRepeaterRow', data );
6876
6877		success = function( r ) {
6878			var html, item, checked, fieldID, fieldObject, repeatArgs, j, inputRanges;
6879
6880			//only process row if row actually added
6881			if ( r.html ) {
6882				html = r.html;
6883				item = jQuery( html ).addClass( 'frm-fade-in' );
6884				const repeaterSection = thisBtn[0].closest( '.frm_section_heading' );
6885				const toggleContainer = repeaterSection.querySelector( '.frm_toggle_container.frm_grid_container' );
6886				if ( toggleContainer ) {
6887					toggleContainer.append( item[0] );
6888				} else {
6889					repeaterSection.append( item[0] );
6890				}
6891				const repeatButtons = repeaterSection.querySelector( '.frm_hidden_container.frm_repeat_buttons.frm_hidden' );
6892				if ( repeatButtons ) {
6893					repeatButtons.style.display = 'none';
6894				}
6895
6896				inputRanges = item[0].querySelectorAll( 'input[type=range]' );
6897				for ( j = 0; j < inputRanges.length; j++ ) {
6898					handleSliderEvent.call( inputRanges[j]);
6899				}
6900
6901				if ( r.is_repeat_limit_reached ) {
6902					hideAddButton( id );
6903				}
6904
6905				if ( ! r.passes_repeat_min_check ) {
6906					hideRemoveButtons( id );
6907				} else {
6908					showRemoveButtons( id );
6909				}
6910
6911				checked = [ 'other' ];
6912				repeatArgs = {
6913					repeatingSection: id.toString(),
6914					repeatRow: i.toString()
6915				};
6916
6917				// hide fields with conditional logic
6918				jQuery( html ).find( 'input, select, textarea' ).each( function() {
6919					// Readonly dropdown fields won't have a name attribute
6920					if ( this.name === '' ) {
6921						return true;
6922					}
6923					if ( this.type === 'file' ) {
6924						fieldID = this.name.replace( 'file', '' ).split( '-' )[0];
6925					} else {
6926						fieldID = this.name.replace( 'item_meta[', '' ).split( ']' )[ 2 ].replace( '[', '' );
6927					}
6928					if ( jQuery.inArray( fieldID, checked ) == -1 ) {
6929						if ( this.id === false || this.id === '' ) {
6930							return;
6931						}
6932
6933						fieldObject = jQuery( '#' + this.id );
6934						checked.push( fieldID );
6935						hideOrShowFieldById( fieldID, repeatArgs );
6936						updateWatchingFieldById( fieldID, repeatArgs, 'value changed' );
6937						// TODO: maybe trigger a change instead of running these three functions
6938						checkFieldsWithConditionalLogicDependentOnThis( fieldID, fieldObject );
6939						checkFieldsWatchingLookup( fieldID, fieldObject, 'value changed' );
6940						doCalculation( fieldID, fieldObject );
6941						maybeDoCalcForSingleField( fieldObject.get( 0 ) );
6942					}
6943				});
6944
6945				jQuery( html ).find( '.frm_html_container' ).each(
6946					function() {
6947						// check heading logic
6948						var fieldID = this.id.replace( 'frm_field_', '' ).split( '-' )[ 0 ];
6949						checked.push( fieldID );
6950						hideOrShowFieldById( fieldID, repeatArgs );
6951					}
6952				);
6953
6954				maybeAddIntlTelInput( item.find( '.frm-intl-tel-input' ).get() );
6955				if ( item.closest( '.with_frm_style' ).length > 0 ) {
6956					initRangeInput( item.find( 'input[type=range]' ).get() );
6957				}
6958
6959				initFormatFieldValueNumbers();
6960				loadDropzones( repeatArgs.repeatRow );
6961				loadSliders();
6962				loadAutocomplete();
6963				loadImask();
6964
6965				// Load rich textboxes.
6966				jQuery( html ).find( '.frm_html_container' ).each( function() {
6967					// check heading logic
6968					var fieldID = this.id.replace( 'frm_field_', '' ).split( '-' )[ 0 ];
6969					checked.push( fieldID );
6970					hideOrShowFieldById( fieldID, repeatArgs );
6971				});
6972
6973				// Find any RTEs in the new row/repeat and initialize the editor for them
6974				//  there is an assumption here that tinyMCEPreInit.mceInit[0] refers to
6975				//  an RTE that was initially loaded on the page. It's a safe assumption as
6976				//  adding a row/repeat wouldn't have any RTEs if there wasn't one on the
6977				//  page in the first place.
6978				jQuery( html ).find( '.wp-editor-area' ).each( function() {
6979					initRichText( this.id );
6980				});
6981			}
6982
6983			/* eslint-disable camelcase */
6984			if ( typeof frmThemeOverride_frmAddRow === 'function' ) {
6985				frmThemeOverride_frmAddRow( id, r );
6986			}
6987			/* eslint-enable camelcase */
6988
6989			jQuery( document ).trigger( 'frmAfterAddRow' );
6990
6991			handleRangeSliders();
6992			triggerEvent( document, 'frmAfterAddRepeaterRow', { repeater: item.get(0) });
6993
6994			jQuery( '.frm_repeat_' + id ).each( function( i ) {
6995				this.style.zIndex = 999 - i;
6996			});
6997
6998			currentlyAddingRow = false;
6999		};
7000
7001		error = function() {
7002			currentlyAddingRow = false;
7003		};
7004
7005		extraParams = { dataType: 'json' };
7006
7007		postToAjaxUrl( form, data, success, error, extraParams );
7008
7009		return false;
7010	}
7011
7012	function triggerToggleClickOnSpace( e ) {
7013		if ( 32 === e.which ) {
7014			this.click();
7015		}
7016	}
7017
7018	function removeRichText( id ) {
7019		tinymce.EditorManager.execCommand( 'mceRemoveEditor', true, id );
7020	}
7021
7022	function initRichText( id ) {
7023		var key = Object.keys( tinyMCEPreInit.mceInit )[0],
7024			orgSettings = tinyMCEPreInit.mceInit[ key ],
7025			newValues = {
7026				selector: '#' + id,
7027				body_class: orgSettings.body_class.replace( key, id )
7028			},
7029			newSettings = Object.assign({}, orgSettings, newValues );
7030		tinymce.init( newSettings );
7031	}
7032
7033	/*****************************************************
7034	 * In-place edit
7035	 ******************************************************/
7036
7037	function editEntry() {
7038		/*jshint validthis:true */
7039		var $edit         = jQuery( this ),
7040			entryId       = $edit.data( 'entryid' ),
7041			prefix        = $edit.data( 'prefix' ),
7042			postId        = $edit.data( 'pageid' ),
7043			formId        = $edit.data( 'formid' ),
7044			cancel        = $edit.data( 'cancel' ),
7045			fields        = $edit.data( 'fields' ),
7046			excludeFields = $edit.data( 'excludefields' ),
7047			startPage     = $edit.data( 'startpage' ),
7048			$cont         = jQuery( document.getElementById( prefix + entryId ) ),
7049			orig          = $cont.html();
7050
7051		$cont.html( '<span class="frm-loading-img" id="' + prefix + entryId + '"></span><div class="frm_orig_content" style="display:none">' + orig + '</div>' );
7052		jQuery.ajax({
7053			type: 'POST',
7054			url: getUrlForInPlaceEdit(),
7055			dataType: 'html',
7056			data: {
7057				action: 'frm_entries_edit_entry_ajax',
7058				post_id: postId,
7059				entry_id: entryId,
7060				id: formId,
7061				nonce: frm_js.nonce,
7062				fields,
7063				exclude_fields: excludeFields,
7064				start_page: startPage
7065			},
7066			success( html ) {
7067				$cont.children( '.frm-loading-img' ).replaceWith( html );
7068				$edit.removeClass( 'frm_inplace_edit' ).addClass( 'frm_cancel_edit' );
7069				$edit.html( cancel );
7070				checkConditionalLogic( 'editInPlace' );
7071
7072				// Make sure fields just loaded are properly bound
7073				if ( typeof frmFrontForm.fieldValueChanged === 'function' ) {
7074					jQuery( document ).on( 'change', '.frm-show-form input[name^="item_meta"], .frm-show-form select[name^="item_meta"], .frm-show-form textarea[name^="item_meta"]', frmFrontForm.fieldValueChanged );
7075				}
7076				checkFieldsOnPage( prefix + entryId );
7077				triggerEvent( document, 'frmInPlaceEdit' );
7078			}
7079		});
7080		return false;
7081	}
7082
7083	function getUrlForInPlaceEdit() {
7084		var url         = frm_js.ajax_url,
7085			queryParams = getQueryParamsForInPlaceEdit();
7086
7087		Array.prototype.forEach.call(
7088			Object.keys( queryParams ),
7089			( queryParamKey ) => {
7090				url += -1 === url.indexOf( '?' ) ? '?' : '&';
7091				url += queryParamKey + '=' + queryParams[ queryParamKey ];
7092			}
7093		);
7094
7095		return url;
7096	}
7097
7098	function getQueryParamsForInPlaceEdit() {
7099		var queryParams, queryString, pairs, keysToSkip, length, i, pair, key, value;
7100
7101		queryParams = {};
7102		queryString = window.location.search.substring( 1 );
7103		pairs       = queryString.split( '&' );
7104		keysToSkip  = [
7105			'action',
7106			'start_page',
7107			'nonce',
7108			'post_id',
7109			'entry_id',
7110			'id',
7111			'fields',
7112			'doing_wp_cron'
7113		];
7114
7115		length = pairs.length;
7116		for ( i = 0; i < length; i++ ) {
7117			pair = pairs[ i ].split( '=' );
7118			key  = decodeURIComponent( pair[0]);
7119
7120			if ( -1 !== keysToSkip.indexOf( key ) ) {
7121				continue;
7122			}
7123
7124			value = decodeURIComponent( pair[1]);
7125
7126			// Check if the key already exists in the object.
7127			if ( queryParams.hasOwnProperty( key ) ) {
7128				// If it's an array, push the new value.
7129				if ( Array.isArray( queryParams[ key ]) ) {
7130					queryParams[ key ].push( value );
7131				} else {
7132					// If it's not an array, convert it to an array and push the new value.
7133					queryParams[ key ] = [ queryParams[ key ], value ];
7134				}
7135			} else {
7136				// If the key doesn't exist, create a new entry.
7137				queryParams[ key ] = value;
7138			}
7139		}
7140
7141		return queryParams;
7142	}
7143
7144	function cancelEdit( event ) {
7145		/*jshint validthis:true */
7146
7147		event.preventDefault();
7148
7149		const $cancelLink = jQuery( this );
7150		const prefix      = $cancelLink.data( 'prefix' );
7151		const entryId     = $cancelLink.data( 'entryid' );
7152		const $cont       = jQuery( document.getElementById( prefix + entryId ) );
7153
7154		$cont.children( '.frm_forms' ).replaceWith( '' );
7155		$cont.children( '.frm_orig_content' ).fadeIn( 'slow' ).removeClass( 'frm_orig_content' );
7156
7157		switchCancelToEdit( $cancelLink );
7158	}
7159
7160	/**
7161	 * Switch a Cancel Link back to Edit
7162	 *
7163	 * @since 2.03.08
7164	 *
7165	 * @param {object} $link
7166     */
7167	function switchCancelToEdit( $link ) {
7168		var label = $link.data( 'edit' );
7169		$link.removeClass( 'frm_cancel_edit' ).addClass( 'frm_inplace_edit' );
7170		$link.html( label );
7171	}
7172
7173	function deleteEntry() {
7174		/*jshint validthis:true */
7175		var entryId, prefix,
7176			$link = jQuery( this ),
7177			confirmText = $link.data( 'deleteconfirm' );
7178
7179		if ( confirm( confirmText ) ) {
7180			entryId = $link.data( 'entryid' );
7181			prefix = $link.data( 'prefix' );
7182
7183			$link.replaceWith( '<span class="frm-loading-img" id="frm_delete_' + entryId + '"></span>' );
7184			jQuery.ajax({
7185				type: 'POST',
7186				url: frm_js.ajax_url,
7187				data: { action: 'frm_entries_destroy', entry: entryId, nonce: frm_js.nonce },
7188				success( html ) {
7189					if ( html.replace( /^\s+|\s+$/g, '' ) === 'success' ) {
7190						var container = jQuery( document.getElementById( prefix + entryId ) );
7191						container.fadeOut( 'slow', () => {
7192							container.remove();
7193						});
7194						jQuery( document.getElementById( 'frm_delete_' + entryId ) ).fadeOut( 'slow' );
7195						jQuery( document ).trigger( 'frmEntryDeleted', [ entryId ]);
7196					} else {
7197						jQuery( document.getElementById( 'frm_delete_' + entryId ) ).replaceWith( html );
7198					}
7199				}
7200			});
7201		}
7202		return false;
7203	}
7204
7205	/**
7206	 * Switch Cancel link back to Edit link after entry is updated in-place
7207	 *
7208	 * @since 2.03.08
7209	 *
7210	 * @param {object} form
7211     */
7212	function doEditInPlaceCleanUp( form ) {
7213		var entryIdField = jQuery( form ).find( 'input[name="id"]' );
7214
7215		if ( entryIdField.length ) {
7216			var link = document.getElementById( 'frm_edit_' + entryIdField.val() );
7217
7218			if ( isCancelLink( link ) ) {
7219				switchCancelToEdit( jQuery( link ) );
7220			}
7221		}
7222	}
7223
7224	/**
7225	 * Check if a link is a cancel link
7226	 *
7227	 * @since 2.03.08
7228	 *
7229	 * @param {object} link
7230	 * @returns {boolean}
7231     */
7232	function isCancelLink( link ) {
7233		return ( link !== null && link.className.includes('frm_cancel_edit') );
7234	}
7235
7236	/**********************************************
7237	 * Ajax time field unique check
7238	 *********************************************/
7239
7240	function loadUniqueTimeFields() {
7241		var timeFields, i, dateField;
7242
7243		if ( typeof __frmUniqueTimes === 'undefined' ) {
7244			return;
7245		}
7246
7247		timeFields = __frmUniqueTimes;
7248		for ( i = 0; i < timeFields.length; i++ ) {
7249			dateField = document.getElementById( timeFields[ i ].dateID );
7250			jQuery( dateField ).on( 'change', maybeTriggerUniqueTime );
7251			if ( '' !== dateField.value ) {
7252				jQuery( dateField ).trigger( 'change' );
7253			}
7254		}
7255	}
7256
7257	function maybeTriggerUniqueTime() {
7258		/*jshint validthis:true */
7259		var timeFields = __frmUniqueTimes;
7260		for ( var i = 0; i < timeFields.length; i++ ) {
7261			if ( timeFields[ i ].dateID == this.id ) {
7262				frmProForm.removeUsedTimes( this, timeFields[ i ].timeID );
7263			}
7264		}
7265	}
7266
7267	/**********************************************
7268	 * General Helpers
7269	 *********************************************/
7270
7271	function checkFieldsOnPage( containerId, event ) {
7272		if ( 'undefined' === typeof event ) {
7273			event = '';
7274		}
7275		checkPreviouslyHiddenFields();
7276		loadDateFields();
7277		loadCustomInputMasks();
7278		loadSliders();
7279		loadAutocomplete( containerId );
7280		checkDynamicFields( event );
7281		checkLookupFields();
7282		// Use a timeout to avoid conflicts with Datepicker Calculationns.
7283		// If this runs right away, the Datepicker Calculation function may not be available yet.
7284		setTimeout( triggerCalc, 0 );
7285		loadDropzones();
7286		loadImask( containerId );
7287		checkPasswordFields();
7288		triggerLookupWatchUpdates();
7289	}
7290
7291	function triggerLookupWatchUpdates() {
7292		var i, fieldId, keys, value, $changedInput;
7293
7294		if ( typeof __FRMLOOKUP  === 'undefined' || document.querySelector( 'form input[name="id"]' ) ) {
7295			return;
7296		}
7297
7298		keys = Object.keys( __FRMLOOKUP );
7299		for ( i = 0; i < keys.length; i++ ) {
7300			fieldId = keys[ i ];
7301			value   = __FRMLOOKUP[ fieldId ];
7302			if ( value.dependents.length <= 0 ) {
7303				continue;
7304			}
7305
7306			$changedInput = jQuery( '#field_' + value.fieldKey );
7307			if ( $changedInput.length ) {
7308				checkFieldsWatchingLookup( fieldId, $changedInput, 'value changed' );
7309			}
7310		}
7311	}
7312
7313	function checkPasswordFields() {
7314		var passwordFields = document.querySelectorAll( '.frm_strength_meter' ),
7315			event = document.createEvent( 'HTMLEvents' );
7316
7317		event.initEvent( 'keyup', true, true );
7318
7319		for ( var i = 0; i < passwordFields.length; i++ ) {
7320			passwordFields[ i ].dispatchEvent( event );
7321		}
7322	}
7323
7324	function checkPreviouslyHiddenFields() {
7325		if ( typeof __frmHideFields !== 'undefined' ) {
7326			frmProForm.hidePreviouslyHiddenFields();
7327		}
7328	}
7329
7330	/**
7331	 * @param {string|undefined} containerId
7332	 * @returns {void}
7333	 */
7334	function loadAutocomplete( containerId ) {
7335		loadChosen( containerId );
7336		loadSlimSelect( containerId );
7337	}
7338
7339	/**
7340	 * @param {string|undefined} chosenContainer
7341	 * @returns {void}
7342	 */
7343	function loadChosen( chosenContainer ) {
7344		var opts;
7345
7346		if ( ! jQuery().chosen ) {
7347			return;
7348		}
7349
7350		opts = {
7351			allow_single_deselect: true,
7352			no_results_text: frm_js.no_results,
7353			search_contains: true
7354		};
7355		if ( typeof __frmChosen !== 'undefined' ) {
7356			opts = '{' + __frmChosen + '}';
7357		}
7358
7359		if ( chosenContainer !== undefined ) {
7360			jQuery( '#' + chosenContainer ).find( '.frm_chzn' ).chosen( opts );
7361		} else {
7362			jQuery( '.frm_chzn' ).chosen( opts );
7363		}
7364	}
7365
7366	function loadSlimSelect( containerId ) {
7367		var container, dropdowns;
7368
7369		if ( 'undefined' === typeof SlimSelect ) {
7370			return;
7371		}
7372
7373		if ( 'undefined' !== typeof containerId ) {
7374			container = document.getElementById( containerId );
7375			if ( ! container ) {
7376				return;
7377			}
7378
7379			dropdowns = container.querySelectorAll( 'select.frm_slimselect' );
7380		} else {
7381			dropdowns = document.querySelectorAll( 'select.frm_slimselect' );
7382		}
7383
7384		dropdowns.forEach(
7385			( autocompleteInput ) => {
7386				var emptyOption, allowDeselect, isMultiSelect, tabindex;
7387
7388				if ( 'none' === autocompleteInput.style.display || autocompleteInput.classList.contains( 'ss-main' ) || autocompleteInput.classList.contains( 'ss-content' ) ) {
7389					// Skip anything that is already initialized.
7390					return;
7391				}
7392
7393				emptyOption   = autocompleteInput.querySelector( 'option[value=""]' );
7394				allowDeselect = false;
7395				isMultiSelect = 'multiple' === autocompleteInput.getAttribute( 'multiple' );
7396
7397				if ( emptyOption && '' === emptyOption.textContent.trim() ) {
7398					allowDeselect = true;
7399					if ( isMultiSelect ) {
7400						emptyOption.parentElement.removeChild( emptyOption );
7401					} else {
7402						emptyOption.setAttribute( 'data-placeholder', 'true' );
7403					}
7404				} else if ( autocompleteInput.querySelector( 'option[data-placeholder="true"]' ) ) {
7405					allowDeselect = true;
7406				}
7407
7408				tabindex = autocompleteInput.getAttribute( 'tabindex' );
7409
7410				autocompleteInput.style.color = '';
7411				new SlimSelect({
7412					select: autocompleteInput,
7413					settings: {
7414						placeholderText: '',
7415						searchText: frm_js.no_results,
7416						searchPlaceholder: ' ', // Avoid default "Search" placeholder.
7417						allowDeselect,
7418						closeOnSelect: ! isMultiSelect,
7419						keepOrder: true
7420					},
7421					events: {
7422						afterOpen() {
7423							// Make some accessibility adjustments to the .ss-content pop up.
7424							var ssContent, ssContentSearchInput, ssList, label;
7425
7426							ssContent = document.querySelector( '.ss-content[data-id="' + autocompleteInput.dataset.id + '"]' );
7427							if ( ! ssContent ) {
7428								return;
7429							}
7430
7431							// The listbox role should be on the .ss-list element.
7432							ssContent.removeAttribute( 'role' );
7433
7434							ssContentSearchInput = ssContent.querySelector( '.ss-search input' );
7435							if ( ! ssContentSearchInput ) {
7436								return;
7437							}
7438
7439							ssContentSearchInput.removeAttribute( 'aria-label' );
7440
7441							ssList = ssContent.querySelector( '.ss-list' );
7442							if ( ssList ) {
7443								// Set a height so the list is scrollable.
7444								ssList.style.height = getHeightForSlimSelectList( ssContent ) + 'px';
7445								ssList.setAttribute( 'role', 'listbox' );
7446							}
7447
7448							// The search input and list elements should have labels.
7449							label = document.querySelector( 'label[for="' + autocompleteInput.id + '"]' );
7450							if ( label ) {
7451								ssContentSearchInput.setAttribute( 'aria-labelledby', label.id );
7452								if ( ssList ) {
7453									ssList.setAttribute( 'aria-labelledby', label.id );
7454								}
7455							}
7456						}
7457					}
7458				});
7459
7460				if ( tabindex ) {
7461					copyTabIndexAttributeToSlimSelectElement( autocompleteInput, tabindex );
7462				}
7463
7464				makeSlimSelectAccessibilityChanges( autocompleteInput );
7465				maybeApplySlimSelectAutoWidth( autocompleteInput, allowDeselect );
7466
7467				autocompleteInput.addEventListener(
7468					'change',
7469					function() {
7470						// This listener is required to toggle the Other input, and for conditional logic based on this field.
7471						jQuery( autocompleteInput ).trigger( 'change' );
7472					}
7473				);
7474			}
7475		);
7476	}
7477
7478	function copyTabIndexAttributeToSlimSelectElement( autocompleteInput, tabindex ) {
7479		var slimselectElement = getSlimSelectMainElement( autocompleteInput );
7480		if ( slimselectElement ) {
7481			slimselectElement.setAttribute( 'tabindex', tabindex );
7482		}
7483	}
7484
7485	function getSlimSelectMainElement( autocompleteInput ) {
7486		return document.querySelector( '.ss-main[data-id="' + autocompleteInput.getAttribute( 'data-id' ) + '"]' );
7487	}
7488
7489	/**
7490	 * @param {HTMLElement} ssContent
7491	 * @returns {number}
7492	 */
7493	function getHeightForSlimSelectList( ssContent ) {
7494		var ssSearch, listHeight;
7495
7496		listHeight = jQuery( ssContent ).outerHeight();
7497		ssSearch   = ssContent.querySelector( '.ss-search' );
7498
7499		if ( ssSearch ) {
7500			listHeight -= jQuery( ssSearch ).outerHeight();
7501		}
7502
7503		return listHeight;
7504	}
7505
7506	/**
7507	 * @since 6.8
7508	 *
7509	 * @param {HTMLElement} autocompleteInput
7510	 * @returns {void}
7511	 */
7512	function makeSlimSelectAccessibilityChanges( autocompleteInput ) {
7513		addAriaLabelledByToSlimSelectMainElement( autocompleteInput );
7514		openSlimSelectOnLabelClick( autocompleteInput );
7515	}
7516
7517	/**
7518	 * If the autocomplete input is set to "Automatic width", use the target dropdown width as reference.
7519	 *
7520	 * @since 6.8.4
7521	 *
7522	 * @param {HTMLElement} autocompleteInput
7523	 * @param {boolean} allowDeselect
7524	 * @returns {void}
7525	 */
7526	function maybeApplySlimSelectAutoWidth( autocompleteInput, allowDeselect ) {
7527		var slimselectElement, extraRequiredWidth;
7528
7529		if ( ! autocompleteInput.classList.contains( 'auto_width' ) ) {
7530			return;
7531		}
7532
7533		slimselectElement = getSlimSelectMainElement( autocompleteInput );
7534		if ( slimselectElement ) {
7535			// The Slim Select dropdown is a bit larger than a regular dropdown because of the "X" icon for deselecting.
7536			// If we are not showing the "X", we don't need as much extra space.
7537			extraRequiredWidth = allowDeselect ? 36 : 14;
7538			slimselectElement.style.minWidth = ( jQuery( autocompleteInput ).outerWidth() + extraRequiredWidth ) + 'px';
7539		}
7540	}
7541
7542	/**
7543	 * @since 6.8
7544	 *
7545	 * @param {HTMLElement} autocompleteInput
7546	 * @returns {void}
7547	 */
7548	function addAriaLabelledByToSlimSelectMainElement( autocompleteInput ) {
7549		var label, slimselectElement;
7550
7551		label = document.querySelector( 'label[for="' + autocompleteInput.id + '"]' );
7552		if ( label ) {
7553			slimselectElement = getSlimSelectMainElement( autocompleteInput );
7554			if ( slimselectElement ) {
7555				slimselectElement.setAttribute( 'aria-labelledby', label.id );
7556			}
7557		}
7558	}
7559
7560	/**
7561	 * @since 6.8
7562	 *
7563	 * @param {HTMLElement} autocompleteInput
7564	 * @returns {void}
7565	 */
7566	function openSlimSelectOnLabelClick( autocompleteInput ) {
7567		var label = document.querySelector( 'label[for="' + autocompleteInput.id + '"]' );
7568		if ( label ) {
7569			label.addEventListener( 'click', labelListener );
7570			function labelListener() {
7571				setTimeout(
7572					() => {
7573						autocompleteInput.slim.open();
7574					},
7575					0
7576				);
7577			}
7578		}
7579	}
7580
7581	function isPreviousSelectedStar( input ) {
7582		return null !== input.getAttribute( 'data-frm-star-selected' );
7583	}
7584
7585	function loadStars() {
7586		if ( isPreviousSelectedStar( this ) ) {
7587			clearStars( this.parentElement, false );
7588			return;
7589		}
7590		/*jshint validthis:true */
7591		updateStars( this, true );
7592	}
7593
7594	function hoverStars() {
7595		/*jshint validthis:true */
7596		var input = this.previousSibling;
7597		if ( input.checked || isPreviousSelectedStar( this ) ) {
7598			// Do not need to update stars when hovering on the current checked star.
7599			return;
7600		}
7601		updateStars( input, false );
7602	}
7603
7604	/**
7605	 * Updates star rating.
7606	 *
7607	 * @param {HTMLElement} hovered Hovered or clicked input.
7608	 * @param {Boolean}     onClick Is true if this update is on click, false if is on hover.
7609	 */
7610	function updateStars( hovered, onClick ) {
7611		var starGroup = hovered.parentElement,
7612			stars = starGroup.children,
7613			current = parseInt( hovered.value ),
7614			starClass = 'star-rating',
7615			selectLabel = false;
7616
7617		starGroup.className += ' frm-star-hovered';
7618
7619		for ( var i = 0; i < stars.length; i++ ) {
7620			if ( stars[ i ].matches( '.' + starClass ) ) {
7621				if ( selectLabel ) {
7622					stars[ i ].className += ' star-rating-hover';
7623				} else {
7624					stars[ i ].classList.remove( 'star-rating-hover', 'star-rating-on' );
7625				}
7626			} else {
7627				selectLabel = ( parseInt( stars[ i ].value ) <= current );
7628
7629				if ( onClick ) {
7630					// Remove selected attribute from all star inputs.
7631					stars[ i ].removeAttribute( 'data-frm-star-selected' );
7632				}
7633			}
7634		}
7635
7636		if ( onClick ) {
7637			// Add selected class to clicked star input.
7638			hovered.setAttribute( 'data-frm-star-selected', '' );
7639		}
7640	}
7641
7642	function unhoverStars() {
7643		/*jshint validthis:true */
7644		var input = this.previousSibling,
7645			starGroup = input.parentElement;
7646		starGroup.classList.remove( 'frm-star-hovered' );
7647		var stars = starGroup.children;
7648		var selected = jQuery( starGroup ).find( 'input:checked' ).attr( 'id' );
7649		var isSelected = '';
7650		var starClass = 'star-rating';
7651
7652		for ( var i = stars.length - 1; i > 0; i-- ) {
7653			if ( stars[ i ].matches( '.' + starClass ) ) {
7654				stars[ i ].classList.remove( 'star-rating-hover' );
7655				if ( isSelected === '' && selected !== undefined && stars[ i ].getAttribute( 'for' ) == selected ) {
7656					isSelected = ' star-rating-on';
7657				}
7658				if ( isSelected !== '' ) {
7659					stars[ i ].className += isSelected;
7660				}
7661			}
7662		}
7663	}
7664
7665	/**
7666	 * Clears star rating.
7667	 *
7668	 * @since 5.4
7669	 *
7670	 * @param {HTMLElement} starGroup    Star group element.
7671	 * @param {Boolean}     noClearInput Whether to clear input value or not. Default is `false`.
7672	 */
7673	function clearStars( starGroup, noClearInput ) {
7674		var labels, input;
7675
7676		labels = starGroup.querySelectorAll( '.star-rating-on' );
7677		if ( labels && labels.length ) {
7678			labels.forEach( ( el ) => {
7679				el.classList.remove( 'star-rating-on' );
7680			});
7681		}
7682
7683		if ( ! noClearInput ) {
7684			input = starGroup.querySelector( 'input[type="radio"]:checked' );
7685			if ( input ) {
7686				input.checked = false;
7687				input.removeAttribute( 'data-frm-star-selected' );
7688			}
7689		}
7690	}
7691
7692	/**
7693	 * Formats a value as currency.
7694	 */
7695	function formatValueAsCurrency( value, fieldKey ) {
7696		const currency = getCurrencyFromCalcRule( __FRMCALC.calc[ fieldKey ]);
7697		return new DOMParser().parseFromString(
7698			formatCurrency( normalizeTotal( value, currency ), currency ),
7699			'text/html'
7700		).documentElement.textContent;
7701	}
7702
7703	function handleSliderEvent() {
7704		if ( this.type !== 'range' ) {
7705			return;
7706		}
7707		const sliderValueEl = this.parentNode.querySelector( '.frm_range_value' );
7708		const fieldKey = getFieldKey( this.id, this.name );
7709		if ( window.__FRMCALC?.calc[ fieldKey ]) {
7710			sliderValueEl.textContent = formatValueAsCurrency( this.value, fieldKey );
7711		} else {
7712			sliderValueEl.textContent = this.value;
7713		}
7714	}
7715
7716	function loadSliders() {
7717		handleRangeSliders();
7718	}
7719
7720	function getCurrencyFromCalcRule( calcRule ) {
7721		return 'undefined' !== typeof calcRule.custom_currency ? calcRule.custom_currency : getCurrency( calcRule.form_id );
7722	}
7723
7724	function setInlineFormWidth() {
7725		var children, f, inlineForm,
7726			inlineForms = jQuery( '.frm_inline_form .frm_fields_container' );
7727
7728		if ( inlineForms.length ) {
7729			for ( f = 0; f < inlineForms.length; f++ ) {
7730				inlineForm = jQuery( inlineForms[ f ]);
7731				children = inlineForm.children( '.frm_form_field' );
7732
7733				if ( children.length <= 12 && ! fieldHasLayoutClass( children.last() ) ) {
7734					addAutoInlineLayout( inlineForm, children );
7735				}
7736			}
7737		}
7738	}
7739
7740	function fieldHasLayoutClass( field ) {
7741		var i,
7742			classList = field.attr( 'class' ),
7743			layoutClasses = [ 'frm_full', 'half', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth' ];
7744
7745		if ( classList === undefined ) {
7746			return false;
7747		}
7748		for ( i = 1; i <= 12; i++ ) {
7749			if ( field.hasClass( 'frm' + i ) ) {
7750				return true;
7751			}
7752
7753			if ( i === 12 ) {
7754				for ( var c = 0; c < layoutClasses.length; c++ ) {
7755					if ( classList.includes(layoutClasses[ c ]) ) {
7756						return true;
7757					}
7758
7759					if ( c === layoutClasses.length - 1 ) {
7760						return false;
7761					}
7762				}
7763			}
7764		}
7765	}
7766
7767	function addAutoInlineLayout( inlineForm, children ) {
7768		var fieldCount, colCount, i;
7769
7770		fieldCount = children.length + 1; // include the submit button
7771		colCount = Math.max( 2, Math.ceil( 12 / fieldCount ) );
7772		for ( i = 0; i < children.length; i++ ) {
7773			if ( ! fieldHasLayoutClass( jQuery( children[ i ]) ) ) {
7774				jQuery( children[ i ]).addClass( 'frm' + colCount );
7775			}
7776		}
7777
7778		inlineForm.children( '.frm_submit' ).addClass( 'frm' + colCount );
7779	}
7780
7781	function checkConditionalLogic( event ) {
7782		if ( typeof __frmHideOrShowFields !== 'undefined' ) {
7783			frmProForm.hideOrShowFields( __frmHideOrShowFields, event );
7784		} else {
7785			showForm();
7786		}
7787	}
7788
7789	function showForm() {
7790		document.querySelectorAll( '.frm_pro_form' ).forEach(
7791			form => {
7792				jQuery( form ).fadeIn( 'slow' );
7793				triggerEvent( form, 'frmProAfterFormFadeIn' );
7794			}
7795		);
7796	}
7797
7798	function checkDynamicFields( event ) {
7799		if ( typeof __frmDepDynamicFields !== 'undefined' ) {
7800			if ( 'pageLoad' === event && typeof __frmHideOrShowFields === 'undefined' ) {
7801				clearHideFields();
7802			}
7803
7804			frmProForm.checkDependentDynamicFields( __frmDepDynamicFields );
7805		}
7806	}
7807
7808	function checkLookupFields() {
7809		if ( typeof __frmDepLookupFields !== 'undefined' ) {
7810			frmProForm.checkDependentLookupFields( __frmDepLookupFields );
7811		}
7812	}
7813
7814	function triggerChange( input, fieldKey ) {
7815		if ( fieldKey === undefined ) {
7816			fieldKey = 'dependent';
7817		}
7818
7819		if ( input.length > 1 ) {
7820			input = input.eq( 0 );
7821		}
7822
7823		input.trigger({ type: 'change', selfTriggered: true, frmTriggered: fieldKey });
7824	}
7825
7826	function loadCustomInputMasks() {
7827		if ( typeof __frmMasks === 'undefined' ) {
7828			return;
7829		}
7830
7831		var maskFields = __frmMasks;
7832		for ( var i = 0; i < maskFields.length; i++ ) {
7833			jQuery( maskFields[ i ].trigger ).attr( 'data-frmmask', maskFields[ i ].mask );
7834		}
7835	}
7836
7837	// Get the section ID and repeat row from a field name
7838	function getRepeatArgsFromFieldName( fieldName ) {
7839		var repeatArgs = { repeatingSection: '', repeatRow: '' };
7840
7841		if ( fieldName !== undefined && isRepeatingFieldByName( fieldName ) ) {
7842			var inputNameParts = fieldName.split( '][' );
7843			repeatArgs.repeatingSection = inputNameParts[ 0 ].replace( 'item_meta[', '' );
7844			repeatArgs.repeatRow = inputNameParts[ 1 ];
7845		}
7846
7847		return repeatArgs;
7848	}
7849
7850	function fadeOut( $remove ) {
7851		$remove.fadeOut( 'slow', () => {
7852			$remove.remove();
7853		});
7854	}
7855
7856	function objectSearch( array, value ) {
7857		for ( var prop in array ) {
7858			if ( array.hasOwnProperty( prop ) ) {
7859				if ( array[ prop ] === value ) {
7860					return prop;
7861				}
7862			}
7863		}
7864		return null;
7865	}
7866
7867	function isNumeric( obj ) {
7868		return ! Array.isArray( obj ) && ( obj - parseFloat( obj ) + 1 ) >= 0;
7869	}
7870
7871	function checkPasswordField() {
7872		var fieldId,
7873			fieldIdSplit,
7874			checks,
7875			split,
7876			suffix,
7877			check,
7878			span;
7879
7880		/*jshint validthis:true */
7881		if ( this.className.includes('frm_strength_meter') ) {
7882			fieldId = this.name.substr( this.name.indexOf( '[' ) + 1 ).replace( /\]\[\d\]\[/, '-' );
7883			if ( fieldId[ fieldId.length - 1 ] === ']' ) {
7884				fieldId = fieldId.substr( 0, fieldId.length - 1 );
7885			}
7886			fieldIdSplit = fieldId.split( '-' );
7887
7888			if ( fieldIdSplit.length === 2 ) {
7889				fieldId = fieldIdSplit[1] + '-' + fieldIdSplit[0];
7890			}
7891
7892			checks = passwordChecks();
7893			split = this.id.split( '-' );
7894			suffix = split.length > 1 && ! isNaN( split[ split.length - 1 ]) ? '-' + split[ split.length - 1 ] : '';
7895
7896			for ( check in checks ) {
7897				span = document.getElementById( 'frm-pass-' + check + '-' + fieldId + suffix );
7898				addOrRemoveVerifyPass( checks[ check ], this.value, span );
7899			}
7900		}
7901	}
7902
7903	function passwordChecks() {
7904		return {
7905			'eight-char': /^.{8,}$/,
7906			number: /\d/,
7907			uppercase: /[A-Z]/,
7908			lowercase: /[a-z]/,
7909			'special-char': /(?=.*[^a-zA-Z0-9])/
7910		};
7911	}
7912
7913	function addOrRemoveVerifyPass( regEx, password, span ) {
7914		if ( span !== null ) {
7915			var remove = regEx.test( password );
7916			if ( remove ) {
7917				maybeRemovePassReq( span );
7918			} else {
7919				maybeRemovePassVerified( span );
7920			}
7921		}
7922	}
7923
7924	function maybeRemovePassReq( span ) {
7925		if ( span.classList.contains( 'frm-pass-req' ) ) {
7926			span.classList.remove( 'frm-pass-req' );
7927			span.classList.add( 'frm-pass-verified' );
7928		}
7929	}
7930
7931	function maybeRemovePassVerified( span ) {
7932		if ( span.classList.contains( 'frm-pass-verified' ) ) {
7933			span.classList.remove( 'frm-pass-verified' );
7934			span.classList.add( 'frm-pass-req' );
7935		}
7936	}
7937
7938	function checkCheckboxSelectionLimit() {
7939		/*jshint validthis:true */
7940		var limit = parseInt( this.getAttribute( 'data-frmlimit' ) ),
7941			checked = this.checked;
7942
7943		if ( ! limit ) {
7944			return;
7945		}
7946
7947		var allBoxes = jQuery( this ).parents( '.frm_opt_container' ).find( 'input[type="checkbox"]' );
7948		if ( limit >= allBoxes.length ) {
7949			return;
7950		}
7951
7952		var checkedBoxes = allBoxes.filter( function() {
7953			return this.checked;
7954		});
7955
7956		if ( checked ) {
7957			if ( checkedBoxes.length >= limit ) {
7958				allBoxes.filter( function() {
7959					return ! this.checked;
7960				}).attr( 'disabled', 'disabled' );
7961			}
7962		} else {
7963			const checkboxesLimitNotReached = allBoxes.filter( function() {
7964				return ! this.getAttribute( 'data-max-reached' );
7965			});
7966
7967			checkboxesLimitNotReached.prop( 'disabled', false );
7968		}
7969	}
7970
7971	/**
7972	 * Set up 'Add Row' buttons that will show up when all repeated fields have been removed.
7973	 * Do it this way while we await CSS4 selectors that can do sth like .frm_repeat_sec:first-of-class.
7974	 *
7975	 * @since 4.02.01
7976	 * @return {void}
7977	 */
7978	function addTopAddRowBtnForRepeater() {
7979		jQuery( '.frm_section_heading:has(div[class*="frm_repeat_"])' ).each( function() {
7980			const firstRepeatedSection = jQuery( this ).find( 'div[class*="frm_repeat_"]' ).first()[0];
7981			const addButtonWrapper     = document.createElement( 'div' );
7982
7983			addButtonWrapper.classList.add( 'frm_form_field', 'frm_hidden_container', 'frm_repeat_buttons', 'frm_hidden' );
7984
7985			const addButton = firstRepeatedSection.querySelector( '.frm_add_form_row' );
7986			if ( addButton ) {
7987				addButtonWrapper.append( addButton.cloneNode( true ) );
7988			}
7989
7990			const buttonContainer = firstRepeatedSection.closest( '.frm_toggle_container.frm_grid_container' );
7991			if ( buttonContainer ) {
7992				buttonContainer.prepend( addButtonWrapper );
7993			} else {
7994				firstRepeatedSection.parentNode.insertBefore( addButtonWrapper, firstRepeatedSection );
7995			}
7996		});
7997	}
7998
7999	function maybeDisableCheckboxesWithLimit() {
8000		// Disable unchecked ones after form submit error if limit is already reached.
8001		jQuery( 'input[type="checkbox"][data-frmlimit]:not(:checked)' ).each( function() {
8002			var limit = parseInt( this.getAttribute( 'data-frmlimit' ) );
8003
8004			if ( ! limit ) {
8005				return;
8006			}
8007
8008			var allBoxes = jQuery( this ).parents( '.frm_opt_container' ).find( 'input[type="checkbox"]' );
8009			if ( limit >= allBoxes.length ) {
8010				// Unreasonable limit
8011				return;
8012			}
8013
8014			var checkedBoxes = allBoxes.filter( function() {
8015				return this.checked;
8016			});
8017
8018			if ( limit > checkedBoxes.length ) {
8019				return;
8020			}
8021
8022			this.setAttribute( 'disabled', 'disabled' );
8023		});
8024	}
8025
8026	/**
8027	 * Check to make sure sure the quantity field value is within the min and max values.
8028	 *
8029	 * @param {HTMLElement} input
8030	 * @return {number}
8031	 */
8032	function checkQuantityFieldMinMax( input ) {
8033		if ( '' === input.value ) {
8034			// Leave the value if it is empty.
8035			return 0;
8036		}
8037
8038		const val = parseFloat( input.value ? input.value.trim() : 0 );
8039		if ( isNaN( val ) ) {
8040			return 0;
8041		}
8042
8043		let max = input.hasAttribute( 'max' ) ? parseFloat( input.getAttribute( 'max' ) ) : 0;
8044		let min = input.hasAttribute( 'min' ) ? parseFloat( input.getAttribute( 'min' ) ) : 0;
8045
8046		max = isNaN( max ) ? 0 : max;
8047		min = isNaN( min ) ? 0 : ( min < 0 ? 0 : min );
8048
8049		if ( val < min ) {
8050			input.value = min;
8051			return min;
8052		}
8053
8054		if ( 0 !== max && val > max ) {
8055			input.value = max;
8056			return max;
8057		}
8058
8059		return val;
8060	}
8061
8062	/**
8063	 * @since 4.04.01
8064	 */
8065	function setHiddenProduct( input ) {
8066		input.setAttribute( 'data-frmhidden', '1' );
8067		triggerChange( jQuery( input ) );
8068	}
8069
8070	/**
8071	 * @since 4.04.01
8072	 */
8073	function setHiddenProductContainer( container ) {
8074		if ( container.innerHTML.includes('data-frmprice') ) {
8075			jQuery( container ).find( 'input[data-frmprice], select:has([data-frmprice])' ).attr( 'data-frmhidden', '1' );
8076		}
8077	}
8078
8079	/**
8080	 * @since 4.04.01
8081	 */
8082	function setShownProduct( input ) {
8083		var wasHidden = input.getAttribute( 'data-frmhidden' );
8084
8085		if ( wasHidden !== null ) {
8086			input.removeAttribute( 'data-frmhidden' );
8087			triggerChange( jQuery( input ) );
8088		}
8089	}
8090
8091	/**
8092	 * @since 4.04
8093	 */
8094	function calcProductsTotal( e ) {
8095		var formTotals = [],
8096			totalFields;
8097
8098		if ( typeof __FRMCURR  === 'undefined' ) {
8099			return;
8100		}
8101
8102		if ( undefined !== e && 'undefined' !== typeof e.target && ( 'keyup' === e.type || 'change' === e.type ) ) {
8103			// an event has been fired
8104			var el = e.target;
8105			if ( el.hasAttribute( 'data-frmprice' ) && el instanceof HTMLInputElement && 'text' === el.type ) {
8106				// user-defined product
8107				el.setAttribute( 'data-frmprice', el.value.trim() );
8108			}
8109		}
8110
8111		totalFields = jQuery( '[data-frmtotal]' );
8112		if ( ! totalFields.length ) {
8113			return;
8114		}
8115
8116		totalFields.each( function() {
8117			var currency, formId, formatted,
8118				total = 0,
8119				totalField = jQuery( this ),
8120				$form = totalField.closest( 'form' ),
8121				isRepeatingTotal = isRepeatingFieldByName( this.name );
8122
8123			if ( ! $form.length ) {
8124				return;
8125			}
8126
8127			formId = $form.find( 'input[name="form_id"]' ).val();
8128			currency = getCurrency( formId );
8129
8130			if ( formTotals[ formId ] !== undefined && ! isRepeatingTotal ) {
8131				total = formTotals[ formId ];
8132			} else {
8133
8134				$form.find( 'input[data-frmprice],select:has([data-frmprice])' ).each( function() {
8135					var quantity, $this,
8136						price = 0,
8137						isUserDef = false,
8138						isSingle = false;
8139
8140					// total fields inside repeaters are for their corresponding rows only.
8141					if ( isRepeatingTotal && ! isRepeatingWithTotal( this, totalField[0]) ) {
8142						return;
8143					}
8144
8145					if ( this.hasAttribute( 'data-frmhigherpg' ) || isProductFieldHidden( this ) ) {
8146						return;
8147					}
8148
8149					if ( this.tagName === 'SELECT' ) {
8150						if ( this.selectedIndex !== -1 ) {
8151							price = this.options[ this.selectedIndex ].getAttribute( 'data-frmprice' );
8152						}
8153					} else {
8154						isUserDef = 'text' === this.type;
8155						isSingle = 'hidden' === this.type;
8156						$this = jQuery( this );
8157						if ( ( ! isUserDef && ! isSingle ) && ! $this.is( ':checked' ) ) {
8158							return;
8159						}
8160						price = this.getAttribute( 'data-frmprice' );
8161					}
8162
8163					if ( ! price ) {
8164						price = 0;
8165					} else {
8166						price = preparePrice( price, currency );
8167						quantity = getQuantity( isUserDef, this );
8168						price = parseFloat( quantity ) * parseFloat( price );
8169					}
8170
8171					if ( 'true' === this.getAttribute( 'data-frmdiscount' ) ) {
8172						price = price * -1;
8173					}
8174
8175					total += price;
8176				});
8177
8178				if ( ! isRepeatingTotal ) {
8179					formTotals[ formId ] = total;
8180				}
8181			}
8182
8183			total = isNaN( total ) ? 0 : total;
8184
8185			// Set a decimal separator for currency if no default for it
8186			currency.decimal_separator = currency.decimal_separator.trim(); // first remove unnecessary space(s)
8187			if ( ! currency.decimal_separator.length ) {
8188				currency.decimal_separator = '.';
8189			}
8190
8191			// Round to the currency's decimal precision so floating-point artifacts
8192			// like 189.89999999999998 aren't posted as the submitted total value.
8193			// Avoid the normalized total as it can use commas as decimal separators.
8194			totalField.val( roundTotal( total, currency ) );
8195
8196			// Make sure '0' is always set as '0.00' to avoid issues where
8197			// the value is interpreted as empty instead of zero.
8198			if ( '0' === totalField.val() && 2 === currency.decimals ) {
8199				totalField.val( '0.00' );
8200			}
8201
8202			total = normalizeTotal( total, currency );
8203
8204			// because of e.g. fields that might be using this field for calculations
8205			triggerChange( totalField );
8206
8207			total = formatCurrency( total, currency );
8208			formatted = totalField.prev( '.frm_total_formatted' );
8209			if ( formatted.length < 1 ) {
8210				// In case paragraphs have been added to the form.
8211				formatted = totalField.closest( '.frm_form_field' ).find( '.frm_total_formatted' );
8212			}
8213			if ( formatted.length ) {
8214				formatted.html( total );
8215			}
8216		});
8217	}
8218
8219	/**
8220	 * Round total and maybe add trailing zeros so formatCurrency has a proper format to work with.
8221	 *
8222	 * @param {float|string} total The total amount to normalize.
8223	 * @param {Object} currency The currency object containing decimal information.
8224	 * @returns {string}
8225	 */
8226	function normalizeTotal( total, currency ) {
8227		const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
8228
8229		total = roundTotal( total, currency );
8230
8231		return maybeAddTrailingZeroToPrice( total, currency, isLargeTotal );
8232	}
8233
8234	function roundTotal( total, currency ) {
8235		const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
8236
8237		if ( ! isLargeTotal ) {
8238			const { decimals } = currency;
8239			total = decimals > 0 ? Math.round10( total, decimals ) : Math.ceil( total );
8240		}
8241
8242		return total;
8243	}
8244
8245	/**
8246	 * Format a numeric value according to the specified currency format settings.
8247	 *
8248	 * @since 4.05.01
8249	 *
8250	 * @param {string} total The numeric value to format.
8251	 * @param {Object} currency The currency formatting configuration.
8252	 * @returns {string} The formatted currency string.
8253	 */
8254	function formatCurrency( total, currency ) {
8255		total = maybeAddTrailingZeroToPrice( total, currency );
8256		if ( total.length && ( total[ total.length - 1 ] === '.' || total[ total.length - 1 ] === currency.decimal_separator ) ) {
8257			total = total.substr( 0, total.length - 1 );
8258		}
8259
8260		total = maybeRemoveTrailingZerosFromPrice( total, currency );
8261		total = addThousands( total, currency );
8262
8263		const leftSymbol = currency.symbol_left ? ( currency.symbol_left + currency.symbol_padding ) : '';
8264		const rightSymbol = currency.symbol_right ? ( currency.symbol_padding + currency.symbol_right ) : '';
8265
8266		return leftSymbol + total + rightSymbol;
8267	}
8268
8269	/**
8270	 * @since 5.0.15
8271	 */
8272	function maybeRemoveTrailingZerosFromPrice( total, currency ) {
8273		var split = total.split( currency.decimal_separator );
8274		if ( 2 !== split.length || split[1].length <= currency.decimals ) {
8275			return total;
8276		}
8277		if ( 0 === currency.decimals ) {
8278			return split[0];
8279		}
8280		return split[0] + currency.decimal_separator + split[1].substr( 0, currency.decimals );
8281	}
8282
8283	/**
8284	 * @since 5.0.10
8285	 */
8286	function addRteRequiredMessages() {
8287		var keys, length, index, key, field;
8288		if ( 'undefined' === typeof __FRMRTEREQMESSAGES ) {
8289			return;
8290		}
8291		keys = Object.keys( __FRMRTEREQMESSAGES );
8292		length = keys.length;
8293		for ( index = 0; index < length; ++index ) {
8294			key = keys[ index ];
8295			field = document.getElementById( key );
8296			if ( field ) {
8297				field.setAttribute( 'data-reqmsg', __FRMRTEREQMESSAGES[ key ]);
8298			}
8299		}
8300	}
8301
8302	/**
8303	 * @since 4.04.01
8304	 *
8305	 * This function is most suited for (product) fields that are
8306	 * on their own page i.e. not hidden (in a multi-paged form).
8307	 *
8308	 * As for fields that are conditionally hidden - themselves or
8309	 * their parent - and are not on their own page, the PHP side
8310	 * handles them well; either their HTML is not included on the
8311	 * page at all (e.g. fields in a conditionally hidden section)
8312	 * or their value is empty & price is thus 0, so no worries here.
8313	 */
8314	function isProductFieldHidden( input ) {
8315		return input.getAttribute( 'data-frmhidden' ) !== null;
8316	}
8317
8318	/**
8319	 * @since 4.04
8320	 */
8321	function isRepeatingWithTotal( input, total ) {
8322		// .+ is safer than d+ because field keys might be used at times.
8323		var regex = /item_meta\[.+?\]\[.+?\]/;
8324
8325		return isRepeatingFieldByName( input.name ) && ( total.name.match( regex )[0] === input.name.match( regex )[0]);
8326	}
8327
8328	/**
8329	 * @since 4.04
8330	 */
8331	function getCurrency( formId ) {
8332		if ( typeof __FRMCURR  !== 'undefined' && __FRMCURR[ formId ] !== undefined ) {
8333			return __FRMCURR[ formId ];
8334		}
8335	}
8336
8337	/**
8338	 * @since 4.04
8339	 */
8340	function getQuantity( isUserDef, field ) {
8341		var quantity, quantityFields, isRepeating, fieldID,
8342			$this = jQuery( field );
8343
8344		fieldID = frmFrontForm.getFieldId( field, false );
8345		if ( ! fieldID ) {
8346			return 0;
8347		}
8348
8349		isRepeating = isRepeatingFieldByName( field.name );
8350
8351		if ( isRepeating ) {
8352			// .+ is safer than d+ because field keys might be used at times.
8353			var match = field.name.match( /item_meta\[.+?\]\[.+?\]/ );
8354			if ( null === match ) {
8355				// very unlikely though
8356				return 0;
8357			}
8358			// we may need this in getQuantityFields
8359			$this.nameMatch = match[0];
8360		}
8361
8362		quantity = getQuantityField( $this, fieldID, isRepeating );
8363
8364		if ( quantity ) {
8365			quantity = checkQuantityFieldMinMax( quantity );
8366		} else {
8367			quantityFields = getQuantityFields( $this, isRepeating );
8368			if ( 1 === quantityFields.length && '' === quantityFields[0].getAttribute( 'data-frmproduct' ).trim() ) {
8369				quantity = checkQuantityFieldMinMax( quantityFields[0]);
8370			} else {
8371				// If there is no quantity field, assume 1.
8372				quantity = 1;
8373			}
8374		}
8375
8376		if ( 0 === quantity && isUserDef ) {
8377			// only user-defined fields may not have attached quantity fields
8378			quantity = 1;
8379		}
8380
8381		return quantity;
8382	}
8383
8384	function getQuantityField( elementObj, fieldID, isRepeating ) {
8385		var quantity,
8386			quantityFields = elementObj.closest( 'form' ).find( '[data-frmproduct]' );
8387
8388		fieldID = fieldID.toString();
8389
8390		quantityFields.each( function() {
8391			var ids;
8392
8393			if ( isRepeating && -1 === this.name.indexOf( elementObj.nameMatch ) ) {
8394				return true;
8395			}
8396
8397			ids = JSON.parse( this.getAttribute( 'data-frmproduct' ).trim() );
8398			if ( '' === ids ) {
8399				return true;
8400			}
8401
8402			// convert to array if necessary cos of existing fields that are already using single product fields
8403			ids = 'string' === typeof ids ? [ ids.toString() ] : ids;
8404			if ( ids.includes(fieldID) ) {
8405				quantity = this;
8406				return false;
8407			}
8408		});
8409
8410		return quantity;
8411	}
8412
8413	/**
8414	 * @since 4.04
8415	 */
8416	function getQuantityFields( elementObj, isRepeating ) {
8417		var quantityFields;
8418		if ( isRepeating ) {
8419			quantityFields = elementObj.closest( 'form' ).find( '[name^="' + elementObj.nameMatch + '"]' + '[data-frmproduct]' );
8420		} else {
8421			// make sure the search is form-based (i.e. per form) cos there could be more than 1 form on the page
8422			// not([id*="-"]) means : not inside a repeater
8423			quantityFields = elementObj.closest( 'form' ).find( '[data-frmproduct]:not([id*="-"])' );
8424		}
8425
8426		return quantityFields;
8427	}
8428
8429	/**
8430	 * @since 4.04
8431	 */
8432	function preparePrice( price, currency ) {
8433		var matches;
8434
8435		if ( ! price ) {
8436			return 0;
8437		}
8438		price = price + ''; // convert to string just to be sure
8439
8440		const regex = getRegexForPrice( currency );
8441
8442		matches = price.match( regex );
8443		if ( null === matches ) {
8444			return 0;
8445		}
8446
8447		price = matches.length ? matches[ matches.length - 1 ] : 0;
8448		price = price.trim();
8449
8450		// Fix issues with parsing Fr.15.00. The regex catches .15.00.
8451		// This checks for the leading decimal and removes it.
8452		if ( currency.decimal_separator === '.' && 3 === price.split( '.' ).length && price[0] === '.' ) {
8453			price = price.substr( 1 );
8454		}
8455
8456		if ( price ) {
8457			price = maybeUseDecimal( price, currency );
8458			price = price.split( currency.thousand_separator ).join( '' ).replace( currency.decimal_separator, '.' );
8459		}
8460
8461		return price;
8462	}
8463
8464	/**
8465	 * @param {Object} currency The currency object.
8466	 * @return {RegExp}
8467	 */
8468	function getRegexForPrice( currency ) {
8469		let regexString = '[0-9,.';
8470
8471		if ( currency.thousand_separator !== '.' && currency.thousand_separator !== ',' ) {
8472			regexString += currency.thousand_separator;
8473		}
8474		if ( currency.decimal_separator !== '.' && currency.decimal_separator !== ',' ) {
8475			regexString += currency.decimal_separator;
8476		}
8477
8478		regexString += ']*\\.?\\,?[0-9]+';
8479
8480		return new RegExp( regexString, 'g' );
8481	}
8482
8483	/**
8484	 * @since 4.04
8485	 */
8486	function maybeUseDecimal( amount, currency ) {
8487		var usedForDecimal, amountParts;
8488		if ( currency.thousand_separator == '.' ) {
8489			amountParts = amount.split( '.' );
8490			usedForDecimal = ( 2 == amountParts.length && 2 == amountParts[1].length );
8491			if ( usedForDecimal ) {
8492				amount = amount.replace( '.', currency.decimal_separator );
8493			}
8494		}
8495		return amount;
8496	}
8497
8498	/**
8499	 * Add trailing zeros to a price if necessary and replace the decimal separator.
8500	 *
8501	 * @since 4.04
8502	 *
8503	 * @param {number|string} price The price to format.
8504	 * @param {Object}
8504 currency The currency object containing the decimal separator.
8505	 * @param {boolean} [force=false] Whether to force processing even if the price is not a number.
8506	 * @returns {string}
8507	 */
8508	function maybeAddTrailingZeroToPrice( price, currency, force = false ) {
8509		if ( 'number' !== typeof price && ! force ) {
8510			return price;
8511		}
8512
8513		price += ''; // first convert to string
8514		var pos = price.indexOf( '.' );
8515
8516		if ( pos === -1 ) {
8517			price = price + '.';
8518
8519			for ( let n = 0; n < currency.decimals; ++n ) {
8520				price += '0';
8521			}
8522		} else {
8523			const decimalsString = price.substring( pos + 1 );
8524			if ( decimalsString.length < currency.decimals ) {
8525				if ( decimalsString.length < 2 ) {
8526					price += '0';
8527				}
8528
8529				for ( let n = 2; n < currency.decimals; ++n ) {
8530					price += '0';
8531				}
8532			}
8533		}
8534
8535		return price.replace( '.', currency.decimal_separator );
8536	}
8537
8538	/**
8539	 * Format a numeric string by adding thousand separators.
8540	 *
8541	 * @since 4.04.04
8542	 *
8543	 * @param {string|number} total The numeric value to format.
8544	 * @param {Object} options Formatting options.
8545	 * @param {string} options.decimal_separator Character used as decimal separator.
8546	 * @param {string} options.thousand_separator Character used as thousand separator.
8547	 *
8548	 * @returns {string}
8549	 */
8550	function addThousands( total, { decimal_separator, thousand_separator } ) {
8551		const split = decimal_separator === ''
8552			? [ total.toString() ]
8553			: total.split( decimal_separator );
8554
8555		if ( thousand_separator ) {
8556			split[0] = split[0].replace( /\B(?=(\d{3})+(?!\d))/g, thousand_separator );
8557		}
8558
8559		return split.join( decimal_separator );
8560	}
8561
8562	/**
8563	 * @since 5.1
8564	 */
8565	function setAutoHeightForTextArea() {
8566		document.querySelectorAll( '.frm-show-form textarea' ).forEach(
8567			( element ) => {
8568				var minHeight, callback;
8569
8570				if ( element.dataset.autoGrow === undefined || element.getAttribute( 'frm-autogrow' ) ) {
8571					return;
8572				}
8573
8574				minHeight = getElementHeight( element );
8575				element.style.overflowY = 'hidden';
8576				element.style.transition = 'none';
8577
8578				callback = function() {
8579					adjustHeight( element, minHeight );
8580				};
8581
8582				callback();
8583				element.addEventListener( 'input', callback );
8584
8585				if ( shouldCallResizeCallback() ) {
8586					window.addEventListener( 'resize', callback );
8587				}
8588
8589				document.addEventListener( 'frmShowField', callback );
8590				element.setAttribute( 'frm-autogrow', 1 );
8591			}
8592		);
8593	}
8594
8595	/**
8596	 * Returns true if window resize callback should be called.
8597	 *
8598	 * Avoid this resize callback on Android and iPhone devices because it causes shaking issues. See issue #5349 for more information.
8599	 *
8600	 * @since 6.16.1
8601	 *
8602	 * @return {Boolean}
8603	 */
8604	function shouldCallResizeCallback() {
8605		if ( ! ( 'userAgent' in navigator ) ) {
8606			return true;
8607		}
8608		const userAgent = navigator.userAgent.toLowerCase();
8609		return !userAgent.includes('android') && !userAgent.includes('iphone');
8610	}
8611
8612	function getElementHeight( element ) {
8613		var clone, container, height;
8614
8615		clone = element.cloneNode( true );
8616		clone.style.position = 'absolute';
8617		clone.style.left = '-10000px';
8618		clone.style.top = '-10000px';
8619
8620		container = jQuery( element ).closest( '.frm_forms' ).get( 0 );
8621		container.append( clone );
8622
8623		height = clone.clientHeight;
8624
8625		container.removeChild( clone );
8626
8627		return height;
8628	}
8629
8630	/**
8631	 * @since 5.1
8632	 */
8633	function adjustHeight( el, minHeight ) {
8634		if ( minHeight >=  el.scrollHeight ) {
8635			return;
8636		}
8637
8638		el.style.height = 0;
8639		el.style.height = Math.max( minHeight, el.scrollHeight ) + 'px';
8640	}
8641
8642	function updateContentLength() {
8643		function onChange( e ) {
8644			var length, max, type,
8645				messageEl = e.target.nextElementSibling,
8646				countEl = messageEl.querySelector( 'span' );
8647
8648			if ( ! countEl ) {
8649				return;
8650			}
8651
8652			type = messageEl.getAttribute( 'data-max-type' );
8653			max  = parseInt( messageEl.getAttribute( 'data-max' ) );
8654			if ( 'word' === type ) {
8655				length = e.target.value.split( /\s+/ ).filter(
8656					( word ) => {
8657						return word;
8658					}
8659				).length;
8660			} else {
8661				length = e.target.value.length;
8662			}
8663
8664			countEl.innerText = length;
8665
8666			messageEl.classList.toggle( 'frm_limit_error', length > max );
8667		}
8668
8669		document.addEventListener( 'input', function( e ) {
8670			var target;
8671			// loop parent nodes from the target to the delegation node
8672			for ( target = e.target; target && target != this; target = target.parentNode ) {
8673				if ( target.matches( 'textarea' ) && target.nextElementSibling && target.nextElementSibling.matches( '.frm_pro_max_limit_desc' ) ) {
8674					onChange( e );
8675					break;
8676				}
8677			}
8678		}, false );
8679	}
8680
8681	/**
8682	 * Triggers custom event.
8683	 *
8684	 * @since 5.1
8685	 * @since 5.3.3 Added the third parameter.
8686	 *
8687	 * @param {HTMLElement} element   Element.
8688	 * @param {String}      eventType Event name.
8689	 * @param {Object}      data      Data to add to the event.
8690	 */
8691	function triggerEvent( element, eventType, data ) {
8692		if ( 'function' === typeof frmFrontForm.triggerCustomEvent ) {
8693			frmFrontForm.triggerCustomEvent( element, eventType, data );
8694		}
8695	}
8696
8697	/**
8698	 * Handles start over button.
8699	 *
8700	 * @since 5.4
8701	 */
8702	function startOverButton() {
8703		/**
8704		 * Gets all field inputs.
8705		 *
8706		 * @param {HTMLElement} formEl Form element.
8707		 * @return {Array}
8708		 */
8709		function getInputs( formEl ) {
8710			return getInputsInFieldOnPage( formEl );
8711		}
8712
8713		/**
8714		 * Resets field inputs.
8715		 *
8716		 * @param {HTMLElement} formEl Form element.
8717		 */
8718		function resetInputs( formEl ) {
8719			var inputs = getInputs( formEl );
8720
8721			/**
8722			 * Resets a single repeater.
8723			 *
8724			 * @param {HTMLElement} repeatBtns Repeater buttons.
8725			 */
8726			function resetRepeater( repeatBtns ) {
8727				var repeater, items;
8728
8729				repeater = repeatBtns.parentElement;
8730				items    = repeater.querySelectorAll( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' );
8731				if ( ! items.length ) {
8732					// Add the first row if all rows are deleted.
8733					currentlyAddingRow = false;
8734					repeatBtns.querySelector( '.frm_add_form_row' ).click();
8735				} else if ( items.length > 1 ) {
8736					// Remove all rows except the first one.
8737					items.forEach( ( item, index ) => {
8738						if ( index ) {
8739							item.parentElement.removeChild( item );
8740						} else {
8741							item.querySelector( '.frm_add_form_row' ).classList.remove( 'frm_hide_add_button' );
8742						}
8743					});
8744				}
8745			}
8746
8747			// Remove repeater sections.
8748			formEl.querySelectorAll( '.frm_section_heading > .frm_repeat_buttons' ).forEach( resetRepeater );
8749
8750			// Clear field value.
8751			clearValueForInputs( inputs, '', true );
8752
8753			// Remove "disabled" attribute added by limit selections option.
8754			inputs.forEach( ( input ) => {
8755				if ( input.disabled && input.getAttribute( 'data-frmlimit' ) ) {
8756					input.removeAttribute( 'disabled' );
8757				}
8758			});
8759		}
8760
8761		/**
8762		 * Checks if given form is a multiple pages form.
8763		 *
8764		 * @param {Number} formId Form Id.
8765		 * @return {Boolean}
8766		 */
8767		function isMultiPagesForm( formId ) {
8768			return document.getElementById( 'frm_page_order_' + formId ) || document.querySelector( '#frm_form_' + formId + '_container input[name="frm_next_page"]' );
8769		}
8770
8771		/**
8772		 * Reloads form.
8773		 *
8774		 * @param {Number}      formId Form ID.
8775		 * @param {HTMLElement} formEl Form element.
8776		 */
8777		function reloadForm( formId, formEl ) {
8778			const stateField = document.querySelector( 'input[name="frm_state"]' );
8779			const state      = null !== stateField ? stateField.value : '';
8780
8781			formEl.classList.add( 'frm_loading_form' );
8782			postToAjaxUrl(
8783				formEl,
8784				{
8785					action: 'frm_load_form',
8786					form: formId,
8787					_ajax_nonce: frm_js.nonce,
8788					frm_state: state
8789				},
8790				( response ) => {
8791					var idValueMapping;
8792					if ( ! response.success ) {
8793						console.warn( response );
8794						return;
8795					}
8796
8797					idValueMapping = getDefaultValuesFromForm( formEl );
8798
8799					jQuery( formEl.closest( '.frm_forms' ) ).replaceWith( response.data );
8800
8801					setDefaultValues( idValueMapping );
8802					maybeShowMoreStepsButton();
8803
8804					if ( 'undefined' !== typeof __frmAjaxDropzone ) {
8805						window.__frmDropzone = __frmAjaxDropzone;
8806					}
8807
8808					checkConditionalLogic( 'pageLoad' );
8809					checkFieldsOnPage( 'frm_form_' + formId + '_container' );
8810
8811					triggerCompletedEvent( formId );
8812				},
8813				( response ) => {
8814					console.warn( response );
8815				}
8816			);
8817		}
8818
8819		/**
8820		 * Gets the default values of given form.
8821		 *
8822		 * @param {HTMLElement} formEl Form element.
8823		 * @return {Object}            Object with keys are input IDs, values are the default values.
8824		 */
8825		function getDefaultValuesFromForm( formEl ) {
8826			var inputs,
8827				values = {};
8828
8829			inputs = formEl.querySelectorAll( '[data-frmval]' );
8830			inputs.forEach( ( input ) => {
8831				values[ input.id ] = input.getAttribute( 'data-frmval' );
8832			});
8833
8834			return values;
8835		}
8836
8837		/**
8838		 * Sets default values of fields in given form from the id value mapping built before.
8839		 *
8840		 * @param {Object} idValueMapping Object with keys are input IDs, values are the default values.
8841		 */
8842		function setDefaultValues( idValueMapping ) {
8843			Object.keys( idValueMapping ).forEach( ( id ) => {
8844				var input = document.getElementById( id );
8845				if ( ! input ) {
8846					return;
8847				}
8848
8849				input.setAttribute( 'data-frmval', idValueMapping[ id ]);
8850
8851				if ( 'checkbox' === input.type || 'radio' === input.type ) {
8852					// Every checkbox and radio input has data-frmval, the one has value === data-frmval is the default.
8853					if ( input.value === idValueMapping[ id ]) {
8854						input.checked = true;
8855					}
8856					return;
8857				}
8858
8859				input.value = idValueMapping[ id ];
8860			});
8861		}
8862
8863		/**
8864		 * Checks if form has Save draft enabled.
8865		 *
8866		 * @param {HTMLElement} formEl Form element.
8867		 * @return {Boolean}
8868		 */
8869		function hasSaveDraft( formEl ) {
8870			return ! ! formEl.querySelector( '.frm_save_draft' );
8871		}
8872
8873		/**
8874		 * Deletes draft version.
8875		 *
8876		 * @param {Number}      formId Form ID.
8877		 * @param {HTMLElement} formEl Form element.
8878		 */
8879		function deleteDraft( formId, formEl ) {
8880			postToAjaxUrl(
8881				formEl,
8882				{
8883					action: 'frm_delete_draft_entry',
8884					form: formId,
8885					_ajax_nonce: frm_js.nonce
8886				}
8887			);
8888		}
8889
8890		function onClickStartOver( e ) {
8891			e.preventDefault();
8892
8893			var formEl, formId, draftIdInput;
8894			formEl = e.target.closest( 'form' );
8895			if ( ! formEl ) {
8896				return;
8897			}
8898
8899			formId = formEl.querySelector( 'input[name="form_id"]' ).value;
8900
8901			if ( hasSaveDraft( formEl ) ) {
8902				deleteDraft( formId, formEl );
8903
8904				// Clear draft inputs.
8905				draftIdInput = formEl.querySelector( 'input[name="id"]' );
8906				if ( draftIdInput ) {
8907					draftIdInput.remove();
8908				}
8909
8910				formEl.querySelector( 'input[name="frm_action"]' ).value = 'create';
8911			}
8912
8913			if ( isMultiPagesForm( formId ) ) {
8914				reloadForm( formId, formEl );
8915			} else {
8916				resetInputs( formEl );
8917				triggerCompletedEvent( formId );
8918			}
8919		}
8920
8921		function triggerCompletedEvent( formId ) {
8922			triggerEvent( document, 'frm_after_start_over', { formId });
8923		}
8924
8925		document.addEventListener( 'click', function( e ) {
8926			var target;
8927
8928			// loop parent nodes from the target to the delegation node.
8929			for ( target = e.target; target && target != this; target = target.parentNode ) {
8930				if ( target.matches( '.frm_start_over' ) ) {
8931					onClickStartOver.call( target, e );
8932					break;
8933				}
8934			}
8935		}, false );
8936	}
8937
8938	function maybeShowMoreStepsButton() {
8939		var i, listWrappers, listWrapper, rootlineSteps,
8940			wrappingElementsCount, startIndex, hiddenSteps,
8941			showMoreButtonLi, showMoreButton, hiddenStepsWrapper,
8942			oldIE;
8943
8944		listWrappers = document.getElementsByClassName( 'frm_rootline' );
8945		copyRootlines( listWrappers );
8946		oldIE = isOldIEVersion( 10 );
8947
8948		for ( i = 0; i < listWrappers.length; i++ ) {
8949			listWrapper = listWrappers[ i ];
8950			if ( oldIE ) {
8951				// Hide the rootlines in < IE 11.
8952				listWrapper.className += ' frm_hidden';
8953				continue;
8954			}
8955
8956			rootlineSteps = listWrapper.children;
8957
8958			wrappingElementsCount = countOverflowPages( rootlineSteps );
8959
8960			if ( ! wrappingElementsCount ) {
8961				continue;
8962			}
8963
8964			showMoreButton = listWrapper.querySelector( '.frm_rootline_show_more_btn' );
8965			if ( ! showMoreButton ) {
8966				continue;
8967			}
8968
8969			showMoreButton.addEventListener( 'click', showMoreSteps );
8970
8971			// Show the existing "..." button.
8972			showMoreButtonLi = showMoreButton.parentNode;
8973			showMoreButtonLi.className = showMoreButtonLi.className.replace( ' frm_hidden', '' );
8974
8975			// Don't count the overflow step in the HTML.
8976			startIndex  = rootlineSteps.length - wrappingElementsCount > 1 ? rootlineSteps.length - wrappingElementsCount - 3 : 0;
8977			hiddenSteps = [].slice.call( rootlineSteps, Math.max( startIndex, 1 ), rootlineSteps.length - 2 );
8978
8979			hiddenStepsWrapper = showMoreButtonLi.querySelector( '.frm_rootline_hidden_steps' );
8980
8981			hiddenSteps.forEach( ( hiddenStep ) => {
8982				hiddenStepsWrapper.append( hiddenStep );
8983			});
8984
8985			moveRootlineTitles( hiddenStepsWrapper, listWrapper, showMoreButton );
8986
8987			listWrapper.insertBefore( showMoreButtonLi, listWrapper.children[ listWrapper.children.length - 1 ]);
8988
8989			if ( listWrapper.children[ listWrapper.children.length - 1 ].className.includes('frm_current_page') ) {
8990				updateRootlineStyle( hiddenStepsWrapper );
8991			}
8992		}
8993	}
8994
8995	/**
8996	 * Check if the browser is IE. If it matches the max or is lower, return true.
8997	 *
8998	 * @param int max
8999	 * @returns bool
9000	 */
9001	function isOldIEVersion( max ) {
9002		var version,
9003			myNav = navigator.userAgent.toLowerCase();
9004		version = myNav.includes('msie') ? parseInt( myNav.split( 'msie' )[1]) : false;
9005		return version !== false && max >= version;
9006	}
9007
9008	function countOverflowPages( rootlineSteps ) {
9009		var j,
9010			wrappingElementsCount = 0;
9011
9012		for ( j = 0; j < rootlineSteps.length; j++ ) {
9013			if ( rootlineSteps[ j ].offsetTop !== rootlineSteps[0].offsetTop && !rootlineSteps[ j ].className.includes('frm_rootline_show_hidden_steps_btn') ) {
9014				wrappingElementsCount++;
9015			}
9016		}
9017		return wrappingElementsCount;
9018	}
9019
9020	function moveRootlineTitles( hiddenStepsWrapper, listWrapper, showMoreButton ) {
9021		var currentPageTitle, currentStepTitle, rootlineGroup,
9022			activeHiddenStepLink = hiddenStepsWrapper.querySelector( 'input:not(.frm_page_back):not(.frm_page_skip)' );
9023
9024		if ( activeHiddenStepLink ) {
9025			currentPageTitle = activeHiddenStepLink.parentElement.querySelector( '.frm_rootline_title' );
9026
9027			maybeUpdateRootlineTitles( activeHiddenStepLink.parentElement.previousElementSibling, hiddenStepsWrapper );
9028
9029			showMoreButton.parentElement.className += ' active';
9030			showMoreButton.className += ' active';
9031		} else {
9032			currentPageTitle = listWrapper.querySelector( '.frm_current_page' ).querySelector( '.frm_rootline_title' );
9033		}
9034
9035		if ( ! currentPageTitle ) {
9036			return;
9037		}
9038
9039		currentStepTitle = currentPageTitle.textContent;
9040		if ( ! currentStepTitle ) {
9041			return;
9042		}
9043
9044		// Add current step on top of rootline bar.
9045		rootlineGroup = listWrapper.closest( '.frm_rootline_group' );
9046
9047		showCurrentHiddenStepText( currentStepTitle );
9048	}
9049
9050	function showCurrentHiddenStepText( currentStepTitle ) {
9051		var rootlineCurrentStep = document.createElement( 'span' );
9052		rootlineCurrentStep.className = 'frm_rootline_title';
9053		rootlineCurrentStep.textContent = currentStepTitle;
9054		document.querySelector( '.frm_rootline_show_hidden_steps_btn' ).append( rootlineCurrentStep );
9055	}
9056
9057	function copyRootlines( listWrappers ) {
9058		var i, listWrapper, form, clone, rootlinesBackup;
9059		rootlinesBackup = {};
9060		for ( i = 0; i < listWrappers.length; i++ ) {
9061			listWrapper = listWrappers[ i ];
9062			form = listWrapper.closest( 'form' );
9063			clone = listWrapper.cloneNode( true );
9064
9065			// Don't bake a transient disabled state (eg. mid dependent-field load) into the
9066			// snapshot used to rebuild the rootline on resize, or the "Show More" button gets
9067			// stuck disabled forever after a resize lands during that window.
9068			if ( ! form.classList.contains( 'frm_loading_form' ) ) {
9069				clone.querySelectorAll( 'input[type="button"]' ).forEach( button => button.disabled = false );
9070			}
9071
9072			rootlinesBackup[ form.getAttribute( 'id' ) ] = clone;
9073		}
9074		listWrappersOriginal = rootlinesBackup;
9075	}
9076
9077	function maybeUpdateRootlineTitles( previousPageLink, hiddenStepsWrapper ) {
9078		var i;
9079		if ( previousPageLink ) {
9080			i = 0;
9081			while ( previousPageLink ) {
9082				i++;
9083				previousPageLink = previousPageLink.previousElementSibling;
9084			}
9085			updateRootlineStyle( hiddenStepsWrapper, i );
9086		}
9087	}
9088
9089	function updateRootlineStyle( hiddenStepsWrapper, uptoIndex ) {
9090		var rootlineTitles, rootlineTitle;
9091
9092		if ( ! uptoIndex ) {
9093			uptoIndex = hiddenStepsWrapper.children.length;
9094		}
9095		rootlineTitles = [].slice.call( hiddenStepsWrapper.children, 0, uptoIndex );
9096		rootlineTitles.forEach( ( el ) => {
9097			rootlineTitle = el.querySelector( '.frm_rootline_title' );
9098			if ( rootlineTitle ) {
9099				rootlineTitle.className += ' frm_prev_page_title';
9100			}
9101		});
9102	}
9103
9104	function showMoreSteps( e ) {
9105		var hiddenStepsWrapper = e.target.parentElement.querySelector( 'ul' );
9106		if ( hiddenStepsWrapper.className.includes('frm_hidden') ) {
9107			hiddenStepsWrapper.className = hiddenStepsWrapper.className.replace( ' frm_hidden', '' );
9108		} else {
9109			hiddenStepsWrapper.className += ' frm_hidden';
9110		}
9111	}
9112
9113	/**
9114	 * Validates form values when submitting form.
9115	 */
9116	function validateForm() {
9117		document.addEventListener( 'frm_get_ajax_form_errors', ( event ) => {
9118			if ( ! event.frmData.formEl ) {
9119				return;
9120			}
9121
9122			validateCheckboxMinSelections( event.frmData.formEl, event.frmData.errors );
9123		});
9124	}
9125
9126	/**
9127	 * @since 6.14
9128	 *
9129	 * @param {HTMLElement|Object} formEl Form object. This might be a jQuery object.
9130	 * @param {Array} errors
9131	 * @return {void}
9132	 */
9133	function validateCheckboxMinSelections( formEl, errors ) {
9134		var checkboxes,
9135			checkedField = {},
9136			errorMsg = frmCheckboxI18n.errorMsg.min_selections;
9137
9138		if ( 'function' === typeof formEl.get ) {
9139			// Get the HTMLElement from a jQuery object.
9140			formEl = formEl.get( 0 );
9141		}
9142
9143		checkboxes = formEl.querySelectorAll( 'input[type="checkbox"][data-frmmin]:checked' );
9144		checkboxes.forEach( ( checkbox ) => {
9145			var min, fieldEl, checkedCheckboxes, key;
9146
9147			min = parseInt( checkbox.dataset.frmmin, 10 );
9148			if ( ! min ) {
9149				return;
9150			}
9151			fieldEl = checkbox.closest( '.frm_form_field' );
9152			key     = fieldEl.id.replace( 'frm_field_', '' ).replace( '_container', '' );
9153
9154			if ( checkedField[ key ]) {
9155				// This field is processed before.
9156				return;
9157			}
9158
9159			checkedCheckboxes = fieldEl.querySelectorAll( 'input[type="checkbox"]:checked' );
9160			if ( ! checkedCheckboxes.length ) {
9161				// If no checkbox is checked, let required field validation do its job.
9162				return;
9163			}
9164
9165			if ( checkedCheckboxes.length < min ) {
9166				errors[ key ] = errorMsg.replace( '%1$d', min ).replace( '%2$d', checkedCheckboxes.length );
9167			}
9168
9169			checkedField[ key ] = true;
9170		});
9171	}
9172
9173	function validateFieldValue() {
9174		document.addEventListener( 'frm_validate_field_value', ( event ) => {
9175			if ( 'object' !== typeof event.frmData.field || 'object' !== typeof event.frmData.errors ) {
9176				return;
9177			}
9178
9179			if ( 'password' === event.frmData.field.type ) {
9180				validatePasswordStrength( event.frmData.field, event.frmData.errors );
9181			}
9182		});
9183	}
9184
9185	function validatePasswordStrength( field, errors ) {
9186		var check, regex, checks;
9187
9188		if ( 'object' !== typeof window.frm_password_checks ) {
9189			return;
9190		}
9191
9192		if ( -1 === field.className.indexOf( 'frm_strong_pass' ) || 0 === field.id.indexOf( 'field_conf_' ) ) {
9193			return;
9194		}
9195
9196		checks = window.frm_password_checks;
9197
9198		for ( check in checks ) {
9199			regex = checks[ check ].regex.slice( 1, checks[ check ].regex.length - 1 );
9200			regex = new RegExp( regex );
9201			if ( ! regex.test( field.value ) ) {
9202				errors[ frmFrontForm.getFieldId( field ) ] = checks[ check ].message;
9203				return;
9204			}
9205		}
9206	}
9207
9208	function maybeTriggerCalc( event ) {
9209		if ( event.persisted || ( window.performance && window.performance.getEntriesByType( 'navigation' )[0].type === 'back_forward' ) ) {
9210			triggerCalc();
9211		}
9212	}
9213
9214	function showMoreStepsButtonEvents() {
9215		var timeout;
9216
9217		/**
9218		 * @since 6.34
9219		 */
9220		function onViewportResize() {
9221			var i, listWrappers, listWrapper, form;
9222			listWrappers = document.getElementsByClassName( 'frm_rootline' );
9223			for ( i = 0; i < listWrappers.length; i++ ) {
9224				listWrapper = listWrappers[ i ];
9225				form = listWrapper.closest( 'form' );
9226				form.querySelector( '.frm_rootline_group' ).replaceChild( listWrappersOriginal[ form.getAttribute( 'id' ) ], listWrapper );
9227			}
9228
9229			clearTimeout( timeout );
9230			timeout = setTimeout( maybeShowMoreStepsButton(), 100 );
9231		}
9232
9233		window.addEventListener( 'resize', onViewportResize );
9234
9235		// Some mobile browsers (eg. iOS Safari) only fire visualViewport's resize event, not
9236		// window's, when the address bar collapses/expands without changing the layout viewport.
9237		if ( window.visualViewport ) {
9238			window.visualViewport.addEventListener( 'resize', onViewportResize );
9239		}
9240	}
9241
9242	function handleShowPasswordBtn() {
9243		documentOn( 'click', '.frm_show_password_btn', ( event ) => {
9244			var input = event.target.closest( '.frm_show_password_wrapper' ).querySelector( 'input' ),
9245				button = input.nextElementSibling;
9246
9247			if ( 'password' === input.type ) {
9248				input.type = 'text';
9249				button.setAttribute( 'data-show-password-label', button.title );
9250				button.title = button.getAttribute( 'data-hide-password-label' );
9251			} else {
9252				input.type = 'password';
9253				button.title = button.getAttribute( 'data-show-password-label' );
9254			}
9255
9256			button.setAttribute( 'aria-label', button.title );
9257		});
9258	}
9259
9260	function handleRangeSliders() {
9261		document.querySelectorAll( '.frm_range_container' ).forEach( ( rangeContainer ) => {
9262			const rangeInput = rangeContainer.querySelector( 'input[type="hidden"]' );
9263			if ( ! rangeInput || rangeInput.dataset.isRangeSliderInitialized ) {
9264				return;
9265			}
9266
9267			initializeRangeSlider( rangeInput, rangeContainer );
9268		});
9269	}
9270
9271	/**
9272	 * Does the same as jQuery( document ).on( 'event', 'selector', handler ).
9273	 *
9274	 * @since 6.3.1
9275	 *
9276	 * @param {String}         event    Event name.
9277	 * @param {String}         selector Selector.
9278	 * @param {Function}       handler  Handler.
9279	 * @param {Boolean|Object} options  Options to be added to `addEventListener()` method. Default is `false`.
9280	 */
9281	function documentOn( event, selector, handler, options ) {
9282		if ( 'undefined' === typeof options ) {
9283			options = false;
9284		}
9285
9286		document.addEventListener( event, function( e ) {
9287			var target;
9288
9289			// loop parent nodes from the target to the delegation node.
9290			for ( target = e.target; target && target != this; target = target.parentNode ) {
9291				if ( target && target.matches && target.matches( selector ) ) {
9292					handler.call( target, e );
9293					break;
9294				}
9295			}
9296		}, options );
9297	}
9298
9299	/**
9300	 * @since 6.5.1
9301	 */
9302	function handleElementorPopupConflicts() {
9303		var elementorPopupWrapper = document.querySelector( '.elementor-popup-modal' );
9304
9305		if ( null !== elementorPopupWrapper ) {
9306			// Make dropzone items clickable.
9307			elementorPopupWrapper.querySelectorAll( '.frm_dropzone' ).forEach( ( item ) => {
9308				item.classList.remove( 'dz-clickable' );
9309			});
9310
9311			// Remove field chosen containers.
9312			elementorPopupWrapper.querySelectorAll( '.frm_form_field .chosen-container' ).forEach( ( chosenContainer ) => {
9313				chosenContainer.remove();
9314			});
9315		}
9316
9317		loadDropzones();
9318		loadAutocomplete();
9319	}
9320
9321	function getAllFormClasses( input ) {
9322		var formContainer, formClasses;
9323
9324		formContainer = input.closest( '.with_frm_style' );
9325		if ( ! formContainer ) {
9326			return [];
9327		}
9328
9329		formClasses = [];
9330		Array.prototype.forEach.call(
9331			formContainer.className.split( ' ' ),
9332			( className ) => {
9333				var trimmedClassName = className.trim();
9334				if ( '' !== trimmedClassName && 'frm_forms' !== trimmedClassName ) {
9335					formClasses.push( trimmedClassName );
9336				}
9337			}
9338		);
9339
9340		return formClasses;
9341	}
9342
9343	function maybeAddIntlTelInput( phoneInputs ) {
9344		if ( document.body.classList.contains( 'wp-admin' ) || typeof window.intlTelInput !== 'function' ) {
9345			return;
9346		}
9347
9348		phoneInputs.forEach( ( phoneInput ) => {
9349			const parentContainer = document.createElement( 'div' );
9350			parentContainer.classList.add( 'frm_forms', 'with_frm_style' );
9351
9352			document.body.append( parentContainer );
9353
9354			const intlPhoneConfig = {
9355				initialCountry: 'auto',
9356				formatOnDisplay: false,
9357				dropdownContainer: parentContainer,
9358				geoIpLookup( callback ) {
9359					if ( 'function' === typeof window.fetch ) {
9360						fetch( 'https://ipapi.co/json' )
9361							.then( function( res ) { return res.json(); } )
9362							.then( function( data ) { callback( data.country_code ); } )
9363							.catch( function() { callback( 'us' ); } );
9364					}
9365				},
9366				loadUtils: () => import( getProPluginUrl() + '/js/intl-tel-input-utils.min.js' ),
9367			};
9368
9369			// Allow someone to specify which countries to show in the dropdown
9370			// by adding data-only-countries in a shortcode.
9371			if ( phoneInput.dataset.onlyCountries ) {
9372				intlPhoneConfig.onlyCountries = phoneInput.dataset.onlyCountries.split( ',' );
9373
9374				if ( 1 === intlPhoneConfig.onlyCountries.length ) {
9375					intlPhoneConfig.initialCountry = intlPhoneConfig.onlyCountries[0];
9376					intlPhoneConfig.allowDropdown  = false;
9377				}
9378			}
9379
9380			if ( phoneInput.dataset.countryOrder ) {
9381				intlPhoneConfig.countryOrder = phoneInput.dataset.countryOrder.split( ',' );
9382			}
9383
9384			const intlTelInputInstance = window.intlTelInput( phoneInput, intlPhoneConfig );
9385
9386			phoneInput.addEventListener( 'countrychange', function() {
9387				if ( ! this.value ) {
9388					return;
9389				}
9390				this.dispatchEvent( new Event( 'change', { bubbles: true } ) );
9391			});
9392
9393			// Update the input value with the full number including the country code
9394			phoneInput.addEventListener( 'blur', function() {
9395				if ( phoneInput.dataset.numberFormat ) {
9396					phoneInput.value = intlTelInputInstance.getNumber( determineNumberFormat( phoneInput.dataset.numberFormat ) );
9397				} else {
9398					phoneInput.value = intlTelInputInstance.getNumber();
9399				}
9400			});
9401
9402			intlPhoneInputs[ phoneInput.id ] = intlTelInputInstance;
9403		});
9404	}
9405
9406	/**
9407	 * Determines the number format based on the provided format string.
9408	 *
9409	 * @param {String} numberFormat The number format string used in the shortcode.
9410	 * @returns {Number} The number format number.
9411	 */
9412	function determineNumberFormat( numberFormat ) {
9413		numberFormat = numberFormat.toLowerCase();
9414		const map = {
9415			e164: 0,
9416			international: 1,
9417			national: 2,
9418			rfc3966: 3,
9419		};
9420		return map[ numberFormat ] ?? 0;
9421	}
9422
9423	/**
9424	 * We have set 'overflow-x = clip;' initially to fix horizontal animation issue but it should be reverted
9425	 * after that to avoid another style break.
9426	 *
9427	 * See issue #4895.
9428	 */
9429	function maybeUpdateFormsOverflowX() {
9430		document.querySelectorAll('.frm-show-form').forEach(
9431			( slideinForm ) => {
9432				slideinForm.style['overflow-x'] = 'visible';
9433			}
9434		);
9435	}
9436
9437	/**
9438	 * Updates the background gradient of a range input field based on its current value.
9439	 *
9440	 * @since 6.12
9441	 */
9442	function initRangeInput( rangeInputs ) {
9443		rangeInputs.forEach( ( rangeInput ) => {
9444			updateRangeInputBackground( rangeInput );
9445			rangeInput.addEventListener( 'input', function() {
9446				updateRangeInputBackground( rangeInput );
9447			});
9448			rangeInput.addEventListener( 'onchange', function() {
9449				updateRangeInputBackground( rangeInput );
9450			});
9451		});
9452
9453		function updateRangeInputBackground( rangeInput ) {
9454			// Calculate the percentage of the current slider value
9455			const sliderValuePercent = ( rangeInput.value - rangeInput.min ) / ( rangeInput.max - rangeInput.min ) * 100;
9456			// Update the background gradient of the slider
9457			rangeInput.style.background = `linear-gradient(to right, var(--slider-color) 0%, var(--slider-color) ${sliderValuePercent}%, var(--slider-bar-color) ${sliderValuePercent}% 100%)`;
9458		}
9459	}
9460
9461	/**
9462	 * Initialize number formatting and input restrictions for `.frm-has-number-format` fields.
9463	 */
9464	function initFormatFieldValueNumbers() {
9465		document.querySelectorAll( '.frm-has-number-format:not([data-has-number-format-events])' ).forEach( field => {
9466			field.dataset.hasNumberFormatEvents = true;
9467
9468			document.addEventListener( 'frmShowField', () => onShowField( field ) );
9469			field.addEventListener( 'focus', handleFocus );
9470			field.addEventListener( 'blur', handleBlur );
9471
9472			if ( field.classList.contains( 'frm_field_number' ) ) {
9473				field.addEventListener( 'keydown', restrictToNumericInput );
9474			}
9475
9476			triggerFormatting( field );
9477		});
9478
9479		/**
9480		 * Trigger formatting manually.
9481		 *
9482		 * @param {HTMLElement} field Field element.
9483		 */
9484		function triggerFormatting( field ) {
9485			field.dispatchEvent( new Event( 'blur', { bubbles: true } ) );
9486		}
9487
9488		/**
9489		 * Handle the 'frmShowField' event.
9490		 *
9491		 * @param {HTMLElement} field Field element.
9492		 */
9493		function onShowField( field ) {
9494			if ( ! field.offsetHeight || field.value !== '' ) {
9495				return;
9496			}
9497
9498			// Restore raw value and apply formatting when field becomes visible.
9499			const { rawValue, frmval } = field.dataset;
9500			const valueToUse           = rawValue || frmval;
9501			if ( valueToUse ?? false ) {
9502				field.value = valueToUse;
9503				triggerFormatting( field );
9504			}
9505		}
9506
9507		/**
9508		 * Restore the unformatted value when the field gains focus.
9509		 *
9510		 * @param {FocusEvent} event Focus event for the field.
9511		 */
9512		function handleFocus( { target: field } ) {
9513			const { rawValue } = field.dataset;
9514
9515			if ( rawValue !== null ) {
9516				field.value = rawValue;
9517			}
9518		}
9519
9520		/**
9521		 * Save the raw value and apply formatting when the field loses focus.
9522		 *
9523		 * @param {FocusEvent} event Blur event for the field.
9524		 */
9525		function handleBlur( event ) {
9526			const field            = event.target;
9527			const previousValue    = field.value;
9528			const previousRawValue = field.getAttribute( 'data-raw-value' ) ?? '';
9529			const fieldKey         = getFieldKey( field.id, field.name );
9530			const userTyped        = field.value;
9531
9532			field.setAttribute( 'data-raw-value', userTyped );
9533			field.value = applyNumberFormatting( userTyped, fieldKey );
9534
9535			if ( previousValue !== field.value || previousRawValue !== userTyped ) {
9536				if ( event.isTrusted ) {
9537					field.dispatchEvent( new Event( 'change', { bubbles: true } ) );
9538				} else {
9539					jQuery( field ).trigger({ type: 'change', selfTriggered: true });
9540				}
9541			}
9542		}
9543
9544		/**
9545		 * Restrict input to valid numeric characters based on custom separators.
9546		 *
9547		 * @param {KeyboardEvent} event Keydown event for the field.
9548		 */
9549		function restrictToNumericInput( event ) {
9550			const { target: field, key, ctrlKey, metaKey } = event;
9551			const fieldKey = getFieldKey( field.id, field.name );
9552			const formatConfig = getCurrencyFromCalcRule( __FRMCALC.calc?.[ fieldKey ] ) || {};
9553			const decimalSeparator = formatConfig.decimal_separator ?? '.';
9554			const thousandSeparator = formatConfig.thousand_separator ?? '';
9555			const decimals = parseInt(formatConfig.decimals ?? 2, 10);
9556
9557			const allowedKeys = [
9558				'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
9559				'Backspace', 'Delete', 'Tab', 'Home', 'End', 'Enter',
9560			];
9561
9562			if ( allowedKeys.includes( key ) || ctrlKey || metaKey ) {
9563				return;
9564			}
9565
9566			// Build regex based on allowed chars
9567			let regexPattern = '^[0-9\\+\\-';
9568
9569			// Only add decimal separator if decimals > 0
9570			if ( decimals > 0 ) {
9571				regexPattern += escapeForRegex( decimalSeparator );
9572			}
9573
9574			if ( thousandSeparator ) {
9575				regexPattern += escapeForRegex( thousandSeparator );
9576			}
9577
9578			regexPattern += ']$';
9579
9580			const oneCharRegex = new RegExp( regexPattern );
9581
9582			if ( ! oneCharRegex.test( key ) ) {
9583				event.preventDefault();
9584				return;
9585			}
9586
9587			// Prevent decimal input if decimals = 0
9588			if ( decimals === 0 && key === ( decimalSeparator !== '' ? decimalSeparator : '.' ) ) {
9589				event.preventDefault();
9590				return;
9591			}
9592
9593			// Only allow one decimal point
9594			if ( key === decimalSeparator && field.value.includes( decimalSeparator ) ) {
9595				event.preventDefault();
9596				return;
9597			}
9598
9599			// If user typed '+' or '-', allow only if it's the first character
9600			if ( ( key === '+' || key === '-' ) && field.selectionStart !== 0 ) {
9601				event.preventDefault();
9602			}
9603		}
9604
9605		/**
9606		 * Format numeric substrings in the provided text based on the custom format configuration.
9607		 *
9608		 * @param {string} text The text containing numbers to format.
9609		 * @param {string} fieldKey The field key used to retrieve the format configuration.
9610		 * @returns {string} The text with formatted numbers.
9611		 */
9612		function applyNumberFormatting( text, fieldKey ) {
9613			if ( ! __FRMCALC?.calc?.[ fieldKey ] ) {
9614				return text;
9615			}
9616
9617			const formatConfig = getCurrencyFromCalcRule( __FRMCALC.calc[fieldKey] );
9618			const {
9619				decimal_separator: decimalSeparator = '.',
9620				thousand_separator: thousandSeparator = ',',
9621				symbol_left: symbolLeft = '',
9622				symbol_right: symbolRight = '',
9623				symbol_padding: symbolPadding = ''
9624			} = formatConfig;
9625
9626			const escapedDecimalSeparator = escapeForRegex( decimalSeparator );
9627			const escapedThousandSeparator = escapeForRegex( thousandSeparator );
9628			const escapedSymbolLeft = escapeForRegex( symbolLeft );
9629			const escapedSymbolRight = escapeForRegex( symbolRight );
9630			const escapedSymbolPadding = escapeForRegex( symbolPadding );
9631
9632			// Build the regex pattern
9633			let pattern = '(?<!\\S)';
9634
9635			// Add left symbol with padding if there is a left symbol
9636			if ( symbolLeft ) {
9637				pattern += `(?:${escapedSymbolLeft}${symbolPadding ? escapedSymbolPadding : ''})?`;
9638			}
9639
9640			// Add the core numeric pattern
9641			pattern += `([-+]?\\d+(?:${escapedThousandSeparator}\\d+)*(?:${escapedDecimalSeparator}\\d+)?)`;
9642
9643			// Add right symbol with padding if there is a right symbol
9644			if ( symbolRight ) {
9645				pattern += `(?:${symbolPadding ? escapedSymbolPadding : ''}${escapedSymbolRight})?`;
9646			}
9647
9648			pattern += '(?!\\S)';
9649
9650			const numericRegex = new RegExp( pattern, 'g' );
9651
9652			return text.replace( numericRegex, ( match ) => {
9653				const sign = match.startsWith( '-' ) ? '-' : ( match.startsWith( '+' ) ? '+' : '' );
9654				const signRemoved = match.replace( /^[-+]/, '' );
9655
9656				const number = stripSymbolsAndFormatNumber( signRemoved, formatConfig );
9657				const formatted = formatCurrency( normalizeTotal( number, formatConfig ), formatConfig );
9658
9659				return `${sign}${formatted}`;
9660			});
9661		}
9662
9663		/**
9664		 * Strip currency symbols and convert a numeric string to a standard decimal format by removing custom separators.
9665		 *
9666		 * @param {string} value The numeric string.
9667		 * @param {Object} config The format configuration.
9668		 * @param {string} [config.decimal_separator] The custom decimal separator.
9669		 * @param {string} [config.thousand_separator] The custom thousand separator.
9670		 * @param {string} [config.symbol_left] The left currency symbol.
9671		 * @param {string} [config.symbol_right] The right currency symbol.
9672		 * @returns {string} The numeric string in standard decimal format.
9673		 */
9674		function stripSymbolsAndFormatNumber( value, config = {} ) {
9675			const {
9676				symbol_left: symbolLeft = '',
9677				symbol_right: symbolRight = '',
9678				decimal_separator: decimalSeparator = '.',
9679				thousand_separator: thousandSeparator = ','
9680			} = config;
9681
9682			let cleanedValue = value.trim();
9683
9684			// Remove left and right symbols if present
9685			if ( symbolLeft ) {
9686				cleanedValue = cleanedValue.replace( symbolLeft, '' );
9687			}
9688
9689			if ( symbolRight ) {
9690				cleanedValue = cleanedValue.replace( symbolRight, '' );
9691			}
9692
9693			cleanedValue = cleanedValue.trim();
9694
9695			// Remove thousand separators and replace custom decimal separator
9696			if ( thousandSeparator ) {
9697				const split = cleanedValue.split( thousandSeparator );
9698				// Make sure that it isn't actually a decimal separator first.
9699				if ( split.length !== 2 || split[1].length > 2 ) {
9700					cleanedValue = split.join( '' );
9701				} else {
9702					cleanedValue = split[0] + '.' + split[1];
9703				}
9704			}
9705
9706			if ( decimalSeparator.trim() && decimalSeparator !== '.' ) {
9707				cleanedValue = cleanedValue.split( decimalSeparator ).join( '.' );
9708			}
9709
9710			return cleanedValue.trim();
9711		}
9712
9713		/**
9714		 * Escape special characters in a string for use in regular expressions.
9715		 *
9716		 * @param {string} str The string to escape.
9717		 * @returns {string} The escaped string.
9718		 */
9719		function escapeForRegex( str ) {
9720			return str.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
9721		}
9722	}
9723
9724	/**
9725	 * Initializes a range slider.
9726	 *
9727	 * @param {HTMLInputElement} rangeInput     The input element of the range slider.
9728	 * @param {HTMLElement}      rangeContainer The container element of the range slider.
9729	 * @return {void}
9730	 */
9731	function initializeRangeSlider( rangeInput, rangeContainer ) {
9732		const parseDataset = dataset => {
9733			const parsed = {};
9734			for ( const [ key, value ] of Object.entries( dataset ) ) {
9735			  parsed[ key ] = parseFloat( value, 10 );
9736			}
9737			return parsed;
9738		};
9739		const { minGap, maxGap } = parseDataset( rangeInput.dataset );
9740
9741		const min      = parseFloat( rangeInput.min, 10 );
9742		const max      = parseFloat( rangeInput.max, 10 );
9743		const minRange = minGap || 0;
9744		const maxRange = maxGap || max - min;
9745
9746		// Validate inputs
9747		if ( min >= max ) {
9748			throw new Error( 'Min value must be less than max value' );
9749		}
9750
9751		if ( minRange <= 0 || minRange > ( max - min ) ) {
9752			throw new Error( 'Invalid minimum range value' );
9753		}
9754
9755		if ( maxRange < minRange || maxRange > ( max - min ) ) {
9756			throw new Error( 'Invalid maximum range value' );
9757		}
9758
9759		const values          = rangeInput.value.split( ',' );
9760		values[0]             ||= min;
9761		values[1]             ||= max;
9762		const step            = parseFloat( rangeInput.step, 10 );
9763		const sliderRange     = max - min;
9764		const stepInPercent   = step / sliderRange * 100;
9765		const minRangePercent = ( minRange / sliderRange ) * 100;
9766		const maxRangePercent = ( maxRange / sliderRange ) * 100;
9767		const minHandle       = rangeContainer.querySelector( '.min-handle' );
9768		const maxHandle       = rangeContainer.querySelector( '.max-handle' );
9769		const rangeElement    = rangeContainer.querySelector( '.frm-slider-range' );
9770		const minValueDisplay = rangeContainer.querySelector( '.min-value' );
9771		const maxValueDisplay = rangeContainer.querySelector( '.max-value' );
9772		const track           = rangeContainer.querySelector( '.frm-slider-track' );
9773
9774		let decimalsToUse = 0;
9775
9776		[ step, min, max, minRange, maxRange ].forEach( value => {
9777			const decimal = countDecimals( value );
9778			if ( decimal > decimalsToUse ) {
9779				decimalsToUse = decimal;
9780			}
9781		});
9782
9783		const initialMinValue = Math.max( values[0], min );
9784		// An end value below the start value is not a supported range. Collapse the handles rather
9785		// than letting the filled section render with a negative width. setHandlesBasedOnValue
9786		// already guards this the same way.
9787		const initialMaxValue = Math.max( Math.min( values[1], initialMinValue + maxRange, max ), initialMinValue );
9788
9789		// Measured at drag start, when the slider is guaranteed to be visible.
9790		let trackWidth = 0;
9791
9792		// Initialize positions
9793		let minPos = ( initialMinValue - min ) / sliderRange * 100;
9794		let maxPos = ( initialMaxValue - min ) / sliderRange * 100;
9795
9796		updateHandlesPositionAndValue();
9797
9798		// State variables for dragging
9799		let isDragging = false;
9800		let activeHandle = null;
9801		let startX = 0;
9802		let startMinPos = 0;
9803		let startMaxPos = 0;
9804
9805		// Setup both handles
9806		setupHandle( minHandle );
9807		setupHandle( maxHandle );
9808
9809		rangeInput.addEventListener( 'change', setHandlesBasedOnValue );
9810
9811		function setupHandle( handle ) {
9812			handle.addEventListener( 'touchstart', handleTouchStart, { passive: false });
9813			handle.addEventListener( 'mousedown', handleMouseDown );
9814		}
9815
9816		function handleTouchStart( e ) {
9817			isDragging = true;
9818			trackWidth = track.getBoundingClientRect().width;
9819			activeHandle = e.target;
9820			const touch = e.touches[0];
9821			startX = touch.clientX;
9822			startMinPos = minPos;
9823			startMaxPos = maxPos;
9824			document.addEventListener( 'touchmove', handleTouchMove, {passive: false});
9825			document.addEventListener( 'touchend', handleTouchEnd );
9826			e.preventDefault();
9827		}
9828
9829		function handleMouseDown( e ) {
9830			isDragging = true;
9831			trackWidth = track.getBoundingClientRect().width;
9832			activeHandle = e.target;
9833			startX = e.clientX;
9834			startMinPos = minPos;
9835			startMaxPos = maxPos;
9836			document.addEventListener( 'mousemove', onMouseMove );
9837			document.addEventListener( 'mouseup', onMouseUp );
9838			e.preventDefault();
9839		}
9840
9841		function handleTouchMove( e ) {
9842			if ( ! isDragging ) {
9843				return;
9844			}
9845			const touch = e.touches[0];
9846			const mouseEvent = new MouseEvent( 'mousemove', {
9847				clientX: touch.clientX,
9848				clientY: touch.clientY
9849			});
9850			onMouseMove( mouseEvent, e );
9851			e.preventDefault();
9852		}
9853
9854		function handleTouchEnd() {
9855			document.removeEventListener( 'touchmove', handleTouchMove );
9856			document.removeEventListener( 'touchend', handleTouchEnd );
9857		}
9858
9859		function onMouseMove( e ) {
9860			if ( ! isDragging ) {
9861				return;
9862			}
9863
9864			const deltaX       = e.clientX - startX;
9865			const deltaPercent = ( deltaX / trackWidth ) * 100;
9866			const minHandleIsMoved = activeHandle === minHandle;
9867			let newMinPos, newMaxPos, hasMovedMinimumSteps;
9868
9869			if ( minHandleIsMoved ) {
9870				newMinPos = startMinPos + deltaPercent; // The new left handle's position in % -- at the current mouse position.
9871				hasMovedMinimumSteps = Math.abs( newMinPos - minPos ) >= stepInPercent;
9872			} else {
9873				newMaxPos = startMaxPos + deltaPercent;
9874				hasMovedMinimumSteps = Math.abs( newMaxPos - maxPos ) >= stepInPercent;
9875			}
9876
9877			if ( ! hasMovedMinimumSteps ) {
9878				return;
9879			}
9880
9881			const movingRight      = minHandleIsMoved ? newMinPos > minPos : newMaxPos > maxPos;
9882			const numberOfNewSteps = movingRight ? Math.floor( deltaPercent / stepInPercent ) : Math.ceil( deltaPercent / stepInPercent );
9883			const newRange         = activeHandle === minHandle ? maxPos - newMinPos : newMaxPos - minPos; // The distance between the right and left handles in %.
9884			const stepsDelta       = numberOfNewSteps * stepInPercent;
9885
9886			if ( activeHandle === minHandle ) {
9887				if ( newRange < minRangePercent ) { // Move the whole range to right.
9888					const tempMinPos = Math.max( startMinPos + stepsDelta, 0 );
9889					if ( tempMinPos + minRangePercent <= 100 ) {
9890						minPos = tempMinPos;
9891						maxPos = Math.max( 0, minPos + minRangePercent );
9892					}
9893				} else if ( newRange > maxRangePercent ) { // Move the whole range to left.
9894					minPos = Math.max( 0, startMinPos + stepsDelta );
9895					maxPos = minPos + maxRangePercent;
9896				} else {
9897					// Normal movement within constraints
9898					minPos = Math.max( 0, startMinPos + stepsDelta );
9899				}
9900				updateHandlesPositionAndValue();
9901				return;
9902			}
9903
9904			// Max handle is moved
9905			if ( newRange < minRangePercent ) { // Move the whole range to left.
9906				const tempMaxPos = Math.min( startMaxPos + stepsDelta, 100 );
9907				if ( tempMaxPos - minRangePercent >= 0 ) {
9908					maxPos = tempMaxPos;
9909					minPos = Math.max( 0, maxPos - minRangePercent );
9910				}
9911			} else if ( newRange > maxRangePercent ) { // Move the whole range to right.
9912				maxPos = Math.min( 100, startMaxPos + stepsDelta );
9913				minPos = maxPos - maxRangePercent;
9914			} else {
9915				// Normal movement within constraints
9916				maxPos = Math.min( 100, startMaxPos + stepsDelta );
9917			}
9918			updateHandlesPositionAndValue();
9919		}
9920
9921		function updateHandlesPositionAndValue() {
9922			minPos = parseFloat( minPos );
9923			maxPos = parseFloat( maxPos );
9924
9925			// Update min and max handles positions.
9926			minHandle.style.left  = `${minPos}%`;
9927			maxHandle.style.right = `${100-maxPos}%`;
9928
9929			// Update range element.
9930			updateRangeElement();
9931
9932			// Update displayed values.
9933			const { currentMin, currentMax } = prepareCurrentValues();
9934			updateDisplayedValues( currentMin, currentMax );
9935
9936			// Update input value.
9937			rangeContainer.querySelector( 'input' ).value = `${currentMin},${currentMax}`;
9938		}
9939
9940		function countDecimals( num ) {
9941			const str          = num.toString();
9942			const decimalIndex = str.indexOf( '.' );
9943			return decimalIndex === -1 ? 0 : str.length - decimalIndex - 1;
9944		}
9945
9946		function formatSliderValue ( value ) {
9947			const fieldKey = getFieldKey( rangeInput.id, rangeInput.name );
9948			if ( window.__FRMCALC?.calc[ fieldKey ]) {
9949				return formatValueAsCurrency( value, fieldKey );
9950			}
9951			return value;
9952		}
9953
9954		function updateDisplayedValues( currentMin, currentMax ) {
9955			minValueDisplay.textContent = formatSliderValue( currentMin );
9956			maxValueDisplay.textContent = formatSliderValue( currentMax );
9957		}
9958
9959		function prepareCurrentValues() {
9960			let currentMin = ( min + ( minPos / stepInPercent ) * step ).toFixed( decimalsToUse );
9961			let currentMax = ( min + ( maxPos / stepInPercent ) * step ).toFixed( decimalsToUse );
9962			currentMin     = Number( currentMin ).toPrecision().toString();
9963			currentMax     = Number( currentMax ).toPrecision().toString();
9964
9965			return { currentMin, currentMax };
9966		}
9967
9968		function onMouseUp() {
9969			isDragging = false;
9970			document.removeEventListener( 'mousemove', onMouseMove );
9971			document.removeEventListener( 'mouseup', onMouseUp );
9972		}
9973
9974		function updateRangeElement() {
9975			rangeElement.style.left  = `${minPos}%`;
9976			rangeElement.style.width = `${maxPos - minPos}%`;
9977		}
9978
9979		/**
9980		 * Change events are only triggered for the range slider when the default value is set
9981		 * The default value is set when the field is conditionally shown.
9982		 */
9983		function setHandlesBasedOnValue() {
9984			const { minGap }      = parseDataset( rangeInput.dataset );
9985			const min             = parseFloat( rangeInput.min, 10 );
9986			const max             = parseFloat( rangeInput.max, 10 );
9987			const minRange        = minGap || 0;
9988			const sliderRange     = max - min;
9989			const values          = rangeInput.value.split( ',' );
9990			values[0]             ||= min;
9991			values[1]             ||= max;
9992			const initialMinValue = Math.max( values[0], min );
9993			const initialMaxValue = Math.min( Math.max( values[1], initialMinValue + minRange ), max );
9994
9995			minPos = ( initialMinValue - min ) / sliderRange * 100;
9996			maxPos = ( initialMaxValue - min ) / sliderRange * 100;
9997			updateHandlesPositionAndValue();
9998		}
9999	}
10000
10001	return {
10002		init() {
10003			maybeUpdateFormsOverflowX();
10004
10005			maybeAddIntlTelInput( document.querySelectorAll( '.frm-intl-tel-input' ) );
10006			initRangeInput( document.querySelectorAll( '.with_frm_style input[type=range]' ) );
10007			initFormatFieldValueNumbers();
10008
10009			addEventListener( 'pageshow', maybeTriggerCalc );
10010
10011			jQuery( document ).on( 'frmFormComplete', afterFormSubmitted );
10012			jQuery( document ).on( 'frmPageChanged', afterPageChanged );
10013			jQuery( document ).on( 'frmAfterAddRow frmAfterRemoveRow', calcProductsTotal );
10014
10015			jQuery( document ).on( 'click', '.frm_trigger', toggleSection );
10016			jQuery( document ).on( 'keydown', '.frm_trigger', toggleSection );
10017
10018			var $blankField = jQuery( '.frm_blank_field' );
10019			if ( $blankField.length ) {
10020				$blankField.closest( '.frm_toggle_container' ).prev( '.frm_trigger' ).trigger( 'click' );
10021			}
10022
10023			jQuery( document ).on( 'click', '.frm_remove_link', removeFile );
10024
10025			jQuery( document ).on( 'frmFieldChanged', maybeCheckDependent );
10026
10027			jQuery( document ).on( 'keyup', 'input.frm_strength_meter', checkPasswordField );
10028			jQuery( document ).on( 'keydown', '.frm_switch', triggerToggleClickOnSpace );
10029
10030			jQuery( document ).on( 'mouseenter click', '.frm-star-group input', loadStars );
10031			jQuery( document ).on( 'mouseenter', '.frm-star-group .star-rating:not(.star-rating-readonly)', hoverStars );
10032			jQuery( document ).on( 'mouseleave', '.frm-star-group .star-rating:not(.star-rating-readonly)', unhoverStars );
10033
10034			jQuery( document ).on(
10035				'click',
10036				'.frm-show-form input[type="submit"], .frm-show-form input[name="frm_prev_page"], .frm_page_back, .frm_page_skip, .frm-show-form .frm_save_draft, .frm_prev_page, .frm_button_submit, .frm_rootline_show_hidden_steps_btn .frm_rootline_single',
10037				setNextPage
10038			);
10039
10040			jQuery( document ).on( 'change', '.frm_other_container input[type="checkbox"], .frm_other_container input[type="radio"], .frm_other_container select', showOtherText );
10041			jQuery( document ).on( 'change', '.frm_switch_block input[type="checkbox"]', setToggleAriaChecked );
10042
10043			jQuery( document ).on( 'click', '.frm_remove_form_row', removeRow );
10044			jQuery( document ).on( 'click', '.frm_add_form_row', addRow );
10045
10046			// In place edit.
10047			jQuery( document ).on( 'click', '.frm_edit_link_container a.frm_inplace_edit', editEntry );
10048			jQuery( document ).on( 'click', '.frm_edit_link_container a.frm_cancel_edit', cancelEdit );
10049			jQuery( document ).on( 'click', '.frm_ajax_delete', deleteEntry );
10050
10051			// toggle collapsible entries shortcode
10052			jQuery( '.frm_month_heading, .frm_year_heading' ).on( 'click', function() {
10053				var content = jQuery( this ).children( '.ui-icon-triangle-1-e, .ui-icon-triangle-1-s' );
10054				if ( content.hasClass( 'ui-icon-triangle-1-e' ) ) {
10055					content.addClass( 'ui-icon-triangle-1-s' ).removeClass( 'ui-icon-triangle-1-e' );
10056					jQuery( this ).next( '.frm_toggle_container' ).fadeIn( 'slow' );
10057				} else {
10058					content.addClass( 'ui-icon-triangle-1-e' ).removeClass( 'ui-icon-triangle-1-s' );
10059					jQuery( this ).next( '.frm_toggle_container' ).hide();
10060				}
10061			});
10062
10063			// Elementor popup show event. Fix Elementor Popup && FF PRO fields( file upload | dynamic field ) conflicts
10064			jQuery( document ).on( 'elementor/popup/show', handleElementorPopupConflicts );
10065
10066			addTopAddRowBtnForRepeater();
10067
10068			// Checkbox Selection Limit
10069			jQuery( document ).on( 'click', 'input[type="checkbox"][data-frmlimit]', checkCheckboxSelectionLimit );
10070
10071			// Total field calc
10072			jQuery( document ).on( 'change', '[type="checkbox"][data-frmprice],[type="radio"][data-frmprice],[type="hidden"][data-frmprice],select:has([data-frmprice])', calcProductsTotal );
10073			jQuery( document ).on( 'keyup change', '[data-frmproduct],[type="text"][data-frmprice]', calcProductsTotal );
10074
10075			jQuery( document ).on( 'frmFormComplete frmPageChanged frmInPlaceEdit frmAfterAddRow', setAutoHeightForTextArea );
10076
10077			maybeDisableCheckboxesWithLimit();
10078
10079			setInlineFormWidth();
10080			checkConditionalLogic( 'pageLoad' );
10081			checkFieldsOnPage( undefined, 'pageLoad' );
10082			processPendingAjax();
10083
10084			addRteRequiredMessages();
10085			setAutoHeightForTextArea();
10086			updateContentLength();
10087
10088			// make sure this comes last, particularly after checkConditionalLogic & checkFieldsOnPage
10089			calcProductsTotal();
10090
10091			startOverButton();
10092
10093			maybeShowMoreStepsButton();
10094
10095			showMoreStepsButtonEvents();
10096
10097			validateFieldValue();
10098			validateForm();
10099
10100			handleShowPasswordBtn();
10101
10102			jQuery( document ).on( 'input change', 'input[data-frmrange]', handleSliderEvent );
10103		},
10104
10105		savingDraft( object ) {
10106			return savingDraftEntry( object );
10107		},
10108
10109		goingToPreviousPage( object ) {
10110			return goingToPrevPage( object );
10111		},
10112
10113		hideOrShowFields( ids, event ) {
10114			if ( 'pageLoad' === event ) {
10115				clearHideFields();
10116			}
10117			var len = ids.length,
10118				repeatArgs = { repeatingSection: '', repeatRow: '' };
10119			for ( var i = 0, l = len; i < l; i++ ) {
10120				hideOrShowFieldById( ids[ i ], repeatArgs );
10121				if ( i == ( l - 1 ) ) {
10122					showForm();
10123				}
10124			}
10125		},
10126
10127		hidePreviouslyHiddenFields() {
10128			var hiddenFields = getAllHiddenFields(),
10129				len = hiddenFields.length;
10130			for ( var i = 0, l = len; i < l; i++ ) {
10131				var container = document.getElementById( hiddenFields[ i ]);
10132
10133				//check for submit button
10134				if ( container == null ) {
10135					container = document.querySelector( '#' + hiddenFields[ i ]);
10136					if ( container != null && hiddenFields[ i ].includes('frm_final_submit') ) {
10137						hidePreviouslyHiddenSubmitButton( hiddenFields[ i ]);
10138						continue;
10139					}
10140				}
10141
10142				if ( container !== null ) {
10143					container.style.display = 'none';
10144					setHiddenProductContainer( container );
10145				}
10146			}
10147		},
10148
10149		submitAllowed( object ) {
10150			var formElementId = object.getAttribute( 'id' );
10151
10152			if ( ! isSubmitButtonOnPage( formElementId + ' .frm_final_submit' ) || goingToPrevPage( object ) || savingDraftEntry( object ) ) {
10153				return true;
10154			}
10155
10156			var formKey = getFormKeyFromFormElementID( formElementId );
10157
10158			return ! isOnPageSubmitButtonHidden( formKey );
10159		},
10160
10161		checkDependentDynamicFields( ids ) {
10162			var len = ids.length,
10163				repeatArgs = { repeatingSection: '', repeatRow: '' };
10164			for ( var i = 0, l = len; i < l; i++ ) {
10165				hideOrShowFieldById( ids[ i ], repeatArgs );
10166			}
10167		},
10168
10169		checkDependentLookupFields( ids ) {
10170			var fieldId,
10171				repeatArgs = { repeatingSection: '', repeatRow: '' };
10172			for ( var i = 0, l = ids.length; i < l; i++ ) {
10173				fieldId = ids[ i ];
10174				updateWatchingFieldById( fieldId, repeatArgs, 'value changed' );
10175			}
10176		},
10177
10178		loadGoogle() {
10179			var graphs, packages, i;
10180
10181			if ( typeof google === 'undefined' || ! google || ! google.load ) {
10182				setTimeout( frmProForm.loadGoogle, 30 );
10183				return;
10184			}
10185
10186			graphs = __FRMTABLES;
10187			packages = Object.keys( graphs );
10188
10189			for ( i = 0; i < packages.length; i++ ) {
10190				if ( packages[ i ] === 'graphs' ) {
10191					generateGoogleGraphs( graphs[ packages[ i ] ]);
10192				} else {
10193					generateGoogleTables( graphs[ packages[ i ] ], packages[ i ]);
10194				}
10195			}
10196		},
10197
10198		removeUsedTimes( obj, timeField ) {
10199			var $form, form, e, data, success, extraParams;
10200
10201			$form = jQuery( obj ).parents( 'form' ).first();
10202			form = $form.get( 0 );
10203			e = $form.find( 'input[name="id"]' );
10204
10205			data = {
10206				action: 'frm_fields_ajax_time_options',
10207				time_field: timeField,
10208				date_field: obj.id,
10209				entry_id: ( e ? e.val() : '' ), date: jQuery( obj ).val(),
10210				nonce: frm_js.nonce
10211			};
10212
10213			success = function( opts ) {
10214				if ( null !== document.getElementById( timeField + '_H' ) ) {
10215					frmProForm.removeUsedTimesForMultipleDropdown( opts, timeField );
10216					return;
10217				}
10218
10219				var $timeField = jQuery( document.getElementById( timeField ) );
10220				$timeField.find( 'option' ).prop( 'disabled', false );
10221				if ( opts.length > 0 ) {
10222					for ( var i = 0, l = opts.length; i < l; i++ ) {
10223						$timeField.get( 0 ).querySelectorAll( 'option[value="' + opts[ i ] + '"]' ).forEach(
10224							( option ) => {
10225								option.disabled = true;
10226								if ( option.selected ) {
10227									option.selected = false;
10228								}
10229							}
10230						);
10231					}
10232				}
10233			};
10234
10235			extraParams = { dataType: 'json' };
10236
10237			postToAjaxUrl( form, data, success, false, extraParams );
10238		},
10239
10240		removeUsedTimesForMultipleDropdown( times, timeField ) {
10241			const self = this;
10242
10243			self.hDropdown = null;
10244			self.mDropdown = null;
10245			self.ampmDropdown = null;
10246
10247			function getTimeDropdownsElements( timeField ) {
10248				return {
10249					h: getTimeDropdownsElements.prototype.h[ timeField ],
10250					m: getTimeDropdownsElements.prototype.m[ timeField ],
10251					ampm: getTimeDropdownsElements.prototype.ampm[ timeField ]
10252				};
10253			}
10254
10255			this.init = function() {
10256				if ( 'undefined' === typeof getTimeDropdownsElements.prototype.h ) {
10257					getTimeDropdownsElements.prototype.h = [];
10258					getTimeDropdownsElements.prototype.m = [];
10259					getTimeDropdownsElements.prototype.ampm = [];
10260				}
10261
10262				if ( 'undefined' !== typeof getTimeDropdownsElements.prototype.h[ timeField ] ) {
10263					return;
10264				}
10265
10266				getTimeDropdownsElements.prototype.h[ timeField ] = document.getElementById( timeField + '_H' );
10267				getTimeDropdownsElements.prototype.m[ timeField ] = document.getElementById( timeField + '_m' );
10268				getTimeDropdownsElements.prototype.ampm[ timeField ] = document.getElementById( timeField + '_A' );
10269
10270				if ( null === getTimeDropdownsElements( timeField ).h ) {
10271					return;
10272				}
10273
10274				getTimeDropdownsElements( timeField ).h.addEventListener( 'change', ( event ) =>{
10275					self.disableMinutes( event.target.value );
10276				});
10277				getTimeDropdownsElements( timeField ).m.addEventListener( 'change', ( event ) =>{
10278				});
10279				getTimeDropdownsElements( timeField ).ampm.addEventListener( 'change', () =>{
10280					self.disableHours();
10281				});
10282
10283				self.disableHours();
10284				self.disableAmpm();
10285
10286			};
10287
10288			this.checkIfTimeIsDisabled =  time => times.includes( time );
10289
10290			this.getActiveTime = function() {
10291				var h = getTimeDropdownsElements( timeField ).h.value || '12',
10292					m = getTimeDropdownsElements( timeField ).m.value || '00',
10293					ampm = getTimeDropdownsElements( timeField ).ampm.value || 'AM';
10294
10295				return h + ':' + m + ' ' + ampm;
10296			};
10297
10298			this.getMeridiem = function() {
10299				return getTimeDropdownsElements( timeField ).ampm.value || 'AM';
10300			};
10301
10302			this.listPossibleHourTimes = function( hour ) {
10303				const minutes = [];
10304				const ampm = [];
10305				const minutesElement = getTimeDropdownsElements( timeField ).m;
10306				const ampmElement = getTimeDropdownsElements( timeField ).ampm;
10307
10308				for ( let i = 0; i < ampmElement.options.length; i++ ) {
10309					ampm.push( ampmElement.options[ i ].value );
10310				}
10311
10312				for ( let i = 0; i < minutesElement.options.length; i++ ) {
10313					if ( '' !== minutesElement.options[ i ].value ) {
10314						minutes.push( minutesElement.options[ i ].value );
10315					}
10316				}
10317
10318				return ampm.reduce( ( array, ampm ) => {
10319					array[ ampm ] = minutes.map( ( minute ) => {
10320						return hour + ':' + minute + ' ' + ampm;
10321					});
10322					return array;
10323				}, {} );
10324			};
10325
10326			this.hourHasEmptySlots = function( hour ) {
10327				const possibleTimes = this.listPossibleHourTimes( hour );
10328				const ampm = this.getMeridiem();
10329				const allPossibleTimes = possibleTimes[ ampm ];
10330				return ! allPossibleTimes.every( ( time ) => {
10331					return times.includes( time );
10332				});
10333			};
10334
10335			this.disableHours = function() {
10336				if ( null === getTimeDropdownsElements( timeField ).h ) {
10337					return;
10338				}
10339
10340				getTimeDropdownsElements( timeField ).h.querySelectorAll( 'option' ).forEach( ( option ) => {
10341					option.disabled = ! self.hourHasEmptySlots( option.value );
10342				});
10343			};
10344
10345			this.disableMinutes = function( hour ) {
10346				if ( null === getTimeDropdownsElements( timeField ).m ) {
10347					return;
10348				}
10349
10350				getTimeDropdownsElements( timeField ).m.querySelectorAll( 'option' ).forEach( ( option ) => {
10351					const timeString = hour + ':' + option.value + ' AM';
10352					option.disabled = times.includes( timeString );
10353				});
10354			};
10355
10356			this.disableAmpm = function() {
10357				if ( null === getTimeDropdownsElements( timeField ).ampm ) {
10358					return;
10359				}
10360				getTimeDropdownsElements( timeField ).ampm.querySelectorAll( 'option' ).forEach( ( option ) => {
10361					if ( times.includes( option.value ) ) {
10362						option.disabled = true;
10363					}
10364				});
10365			};
10366
10367			this.init();
10368		},
10369
10370		changeRte( editor ) {
10371			editor.on( 'change', () => {
10372				var content = editor.getBody().innerHTML;
10373				jQuery( '#' + editor.id ).val( content ).trigger( 'change' );
10374			});
10375		},
10376
10377		addFormidableClassToDatepicker( _, options ) {
10378			if ( options.dpDiv ) {
10379				Array.prototype.forEach.call(
10380					getAllFormClasses( options.input.get( 0 ) ),
10381					( formClass ) => {
10382						options.dpDiv.get( 0 ).classList.add( formClass );
10383					}
10384				);
10385
10386				options.dpDiv.addClass( 'frm-datepicker' );
10387				options.dpDiv.get( 0 ).setAttribute( 'is-formidable-datepicker', 1 );
10388				options.input.get( 0 )?.classList.add( 'active' );
10389			}
10390			return options;
10391		},
10392
10393		removeFormidableClassFromDatepicker( _, options ) {
10394			var dpDiv;
10395			if ( options.dpDiv ) {
10396				dpDiv = options.dpDiv.get( 0 );
10397				dpDiv.removeAttribute( 'is-formidable-datepicker' );
10398				options.input.get( 0 )?.classList.remove( 'active' );
10399
10400				// Delay it slightly so it doesn't use styling while it's fading out.
10401				setTimeout(
10402					() => {
10403						if ( dpDiv.hasAttribute( 'is-formidable-datepicker' ) ) {
10404							// Avoid removing it the class if the datepicker was immediately opened again.
10405							return;
10406						}
10407
10408						Array.prototype.forEach.call(
10409							getAllFormClasses( options.input.get( 0 ) ),
10410							function( formClass ) {
10411								options.dpDiv.get( 0 ).classList.remove( formClass );
10412							}
10413						);
10414
10415						jQuery( dpDiv ).removeClass( 'frm-datepicker' );
10416					},
10417					400
10418				);
10419			}
10420		},
10421
10422		isIntlPhoneInput( field ) {
10423			var pattern = field.getAttribute( 'pattern' );
10424			if ( '\\+?\\d{1,4}[\\s\\-]?(?:\\(\\d{1,3}\\)[\\s\\-]?)?\\d{1,4}[\\s\\-]?\\d{1,4}[\\s\\-]?\\d{1,4}$' !== pattern ) {
10425				return false;
10426			}
10427			return 'undefined' !== typeof intlPhoneInputs && 'undefined' !== typeof intlPhoneInputs[ field.id ];
10428		},
10429
10430		validateIntlPhoneInput( field ) {
10431			return intlPhoneInputs[ field.id ].isValidNumber();
10432		},
10433
10434		frmDatepicker: frmDatepickerPro,
10435	};
10436
10437	/**
10438	 * Keep label floating if datepicker is open (prevents jumping when clicking date)
10439	 *
10440	 * @param {Event} event Focusout event
10441	 * @return {void}
10442	 */
10443	function keepFloatingLabelOnDateBlur( event ) {
10444		const input = event.target.closest( '.frm_date' );
10445		if ( input?.classList.contains( 'active' ) ) {
10446			input.closest( '.frm_inside_container' )?.classList.add( 'frm_label_float_top' );
10447		}
10448	}
10449}
10450var frmProForm = frmProFormJS();
10451
10452document.addEventListener(
10453	'frmMaybeDelayFocus',
10454	( event ) => {
10455		if ( 'object' !== typeof event.frmData || 'undefined' === typeof event.frmData.input ) {
10456			return;
10457		}
10458
10459		const input = event.frmData.input;
10460
10461		// Possibly delay focus.
10462		const form = input.closest( 'form' );
10463		if ( form ) {
10464			const focusHandler = () => {
10465				input.focus();
10466				form.removeEventListener( 'frmProAfterFormFadeIn', focusHandler );
10467			};
10468			form.addEventListener( 'frmProAfterFormFadeIn', focusHandler );
10469		}
10470	}
10471);
10472
10473jQuery( document ).ready( () => {
10474	frmProForm.init();
10475});
10476
10477if ( ! Math.round10 ) {
10478	// https://www.jacklmoore.com/notes/rounding-in-javascript/
10479	Math.round10 = function( value, decimals ) {
10480		return Number( Math.round( value + 'e' + decimals ) + 'e-' + decimals );
10481	};
10482}
10483
10484window.frmUpdateField = function( entryId, fieldId, value, message, num ) {
10485	jQuery( document.getElementById( 'frm_update_field_' + entryId + '_' + fieldId + '_' + num ) ).html( '<span class="frm-loading-img"></span>' );
10486	jQuery.ajax({
10487		type: 'POST',
10488		url: frm_js.ajax_url, // eslint-disable-line camelcase
10489		data: {
10490			action: 'frm_entries_update_field_ajax',
10491			entry_id: entryId,
10492			field_id: fieldId,
10493			value,
10494			nonce: frm_js.nonce // eslint-disable-line camelcase
10495		},
10496		success() {
10497			if ( message.replace( /^\s+|\s+$/g, '' ) === '' ) {
10498				jQuery( document.getElementById( 'frm_update_field_' + entryId + '_' + fieldId + '_' + num ) ).fadeOut( 'slow' );
10499			} else {
10500				jQuery( document.getElementById( 'frm_update_field_' + entryId + '_' + fieldId + '_' + num ) ).replaceWith( message );
10501			}
10502		}
10503	});
10504};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.