/* global wpforms_builder, WPFormsUtils, tinyMCE */
// noinspection JSUnusedGlobalSymbols
* @param window.DOMPurify
* @param wpforms_builder.currency_decimal
* @param wpforms_builder.currency_decimals
* @param wpforms_builder.currency_symbol
* @param wpforms_builder.currency_symbol_pos
* @param wpforms_builder.currency_thousands
* The savedState property is deprecated.
* Save the current form state to determine if the form was changed.
// This file contains a collection of utility functions.
// Init Radio Group for Checkboxes.
wpf.initRadioGroupForCheckboxes();
// Save field and choice order for sorting later.
// The following items should all trigger the fieldUpdate trigger.
.on( 'wpformsFieldAdd', wpf.setFieldOrders )
.on( 'wpformsFieldDuplicated', wpf.setFieldOrders )
.on( 'wpformsFieldDelete', wpf.setFieldOrders )
.on( 'wpformsFieldMove', wpf.setFieldOrders )
.on( 'wpformsFieldAdd', wpf.setChoicesOrders )
.on( 'wpformsFieldChoiceAdd', wpf.setChoicesOrders )
.on( 'wpformsFieldChoiceDelete', wpf.setChoicesOrders )
.on( 'wpformsFieldChoiceMove', wpf.setChoicesOrders )
.on( 'wpformsFieldAdd', wpf.fieldUpdate )
.on( 'wpformsFieldDelete', wpf.fieldUpdate )
.on( 'wpformsFieldMove', wpf.fieldUpdate )
.on( 'wpformsFieldChoiceAdd', wpf.fieldUpdate )
.on( 'wpformsFieldChoiceDelete', wpf.fieldUpdate )
.on( 'wpformsFieldChoiceMove', wpf.fieldUpdate )
.on( 'wpformsFieldDynamicChoiceToggle', wpf.fieldUpdate )
.on( 'focusout', '.wpforms-field-option-row-label input', wpf.fieldUpdate )
.on( 'focusout', '.wpforms-field-option-row-choices input.label', wpf.fieldUpdate );
* Store the order of the fields.
jQuery( '.wpforms-field-option' ).each( function() {
wpf.orders.fields.push( jQuery( this ).data( 'field-id' ) );
* Store the order of the choices for each field.
jQuery( '.choices-list' ).each( function() {
const fieldID = jQuery( this ).data( 'field-id' );
wpf.orders.choices[ 'field_' + fieldID ] = [];
jQuery( this ).find( 'li' ).each( function() {
wpf.orders.choices[ 'field_' + fieldID ].push( jQuery( this ).data( 'key' ) );
* Return the order of choices for a specific field.
* @param {number|string} id Field ID.
* @return {Array} Choices.
jQuery( '#wpforms-field-option-' + id ).find( '.choices-list li' ).each( function() {
choices.push( jQuery( this ).data( 'key' ) );
* Maintain multiselect dropdown with search.
* If a 'multiple select' has selected choices - hide a placeholder text.
* In case if select is empty - we return placeholder text.
* @param {Object} self Current object.
initMultipleSelectWithSearch( self ) {
const $element = jQuery( self.passedElement.element ),
$input = jQuery( self.input.element );
if ( $element.prop( 'multiple' ) ) {
$input.data( 'placeholder', $input.attr( 'placeholder' ) );
// We need to save the style attribute to restore it later and make placeholder visible.
$input.data( 'style', $input.attr( 'style' ) );
if ( self.getValue( true ).length ) {
$input.removeAttr( 'placeholder' );
$element.on( 'change', function() {
// eslint-disable-next-line no-unused-expressions
self.getValue( true ).length
? $input.removeAttr( 'placeholder' )
: $input.attr( 'placeholder', $input.data( 'placeholder' ) ).attr( 'style', $input.data( 'style' ) );
* Display/hide show more icon inside multiselect dropdown.
* @param {string} container Container element.
showMoreButtonForChoices( container ) {
if ( jQuery( container ).data( 'type' ) === 'select-one' ) {
const first = jQuery( container ).find( '.choices__list--multiple .choices__item' ).first(),
last = jQuery( container ).find( '.choices__list--multiple .choices__item' ).last();
jQuery( container ).removeClass( 'choices__show-more' );
if ( first.length > 0 && last.length > 0 && first.position().top !== last.position().top ) {
jQuery( container ).addClass( 'choices__show-more' );
* Initialize event handlers for choices.
initializeChoicesEventHandlers() {
// Show more button for choices.
jQuery( document ).on( 'addItem removeItem', '.choices:not(.is-disabled)', function() {
wpf.showMoreButtonForChoices( this );
// Remove focus from input when the dropdown is hidden.
jQuery( document ).on( 'hideDropdown', '.choices:not(.is-disabled)', function() {
jQuery( this ).find( '.choices__inner input.choices__input' ).trigger( 'blur' );
* Reinitialize show more choices.
* @param {Object} container Container element.
reInitShowMoreChoices( container ) {
container.find( '.choices select' ).each( function() {
const $choiceInstance = jQuery( this ).data( 'choicesjs' );
wpf.showMoreButtonForChoices( $choiceInstance?.containerOuter.element );
* Trigger fired for all field-update-related actions.
const fields = wpf.getFields();
jQuery( document ).trigger( 'wpformsFieldUpdate', [ fields ] );
wpf.debug( 'fieldUpdate triggered' );
* Dynamically get the fields from the current form state.
* @since 1.8.9 Added `allowedFields` parameter.
* @param {Array|boolean|undefined} allowedFields Allowed fields.
* @param {boolean|undefined} useCache Use cache.
* @param {boolean|undefined} isAllowedRepeaterFields Is repeater fields allowed?
* @param {Object|undefined} fieldsToExclude Fields to exclude.
* @return {Object} Fields.
getFields( allowedFields = undefined, useCache = undefined, isAllowedRepeaterFields = undefined, fieldsToExclude = undefined ) { // eslint-disable-line complexity, max-lines-per-function
useCache = useCache || false;
if ( useCache && ! jQuery.isEmptyObject( wpf.cachedFields ) ) {
// Use cache if told and cache is primed.
fields = jQuery.extend( {}, wpf.cachedFields );
wpf.debug( 'getFields triggered (cached)' );
// Normal processing, get fields from builder and prime cache.
const formData = wpf.formObject( '#wpforms-field-options' );
fields = formData.fields;
for ( const key in fields ) {
if ( ! fields[ key ].type || jQuery.inArray( fields[ key ].type, fieldBlockList ) > -1 ) {
if ( fields[ key ]?.type === 'repeater' ) {
Object.values( fields[ key ][ 'columns-json' ] ?? {} ).forEach( ( column ) => {
Object.values( column?.fields ?? [] ).forEach( ( field ) => {
if ( ! fields[ field ] ) {
fields[ field ].label += ' (' + fields[ key ].label + ')';
fields[ field ].isRepeater = true;
// Add additional fields to the field object.
wpf.addAdditionalFields( fields );
// Cache all the fields now that they have been ordered and initially processed.
wpf.cachedFields = jQuery.extend( {}, fields );
wpf.debug( 'getFields triggered' );
if ( ! isAllowedRepeaterFields ) {
for ( const key in fields ) {
if ( fields[ key ]?.isRepeater ) {
for ( const key in fieldsToExclude ) {
// If we should only return specific field types, remove the others.
if ( allowedFields && allowedFields.constructor === Array ) {
for ( const key in fields ) {
if ( jQuery.inArray( fields[ key ].type, allowedFields ) === -1 ) {
if ( Object.keys( fields ).length === 0 ) {
const orderedFields = [];
for ( const fieldKey in wpf.orders.fields ) {
const fieldId = wpf.orders.fields[ fieldKey ];
if ( ! fields[ fieldId ] ) {
orderedFields.push( fields[ fieldId ] );
return Object.assign( {}, orderedFields );
* Add additional fields to the field object.
* @param {Object} fields Fields object.
* @return {Object} Fields object with additional fields.
addAdditionalFields( fields ) {
for ( const key in fields ) {
if ( [ 'name', 'date-time' ].includes( fields[ key ]?.type ) ) {
// Get the name format and split it into an array.
const nameFormat = fields[ key ].format;
// Add the name fields to the field object
fields[ key ].additional = nameFormat.split( '-' );
if ( fields[ key ]?.type === 'address' ) {
// Get all keys with "_placeholder" in the name (address1_placeholder, address2_placeholder, etc.)
const addressFields = Object.keys( fields[ key ] ).filter( ( fieldKey ) => fieldKey.includes( '_placeholder' ) );
// Remove "_placeholder" from the keys
addressFields.forEach( ( fieldKey, index ) => {
addressFields[ index ] = fieldKey.replace( '_placeholder', '' );
// Add the address fields to the field object
fields[ key ].additional = addressFields;
* Get a field settings object.
* @param {number|string} id Field ID.
* @return {Object} Field settings.
const field = wpf.formObject( '#wpforms-field-option-' + id );
if ( ! Object.keys( field ).length ) {
return field.fields[ Object.keys( field.fields )[ 0 ] ];
* Toggle the loading state/indicator of a field option.
* @param {string|Element} option jQuery object, or DOM element selector.
* @param {boolean} unload True if you need to unload spinner, and vice versa.
fieldOptionLoading( option, unload = undefined ) {
const $option = jQuery( option ),
$label = $option.find( 'label' ),
spinner = '<i class="wpforms-loading-spinner wpforms-loading-inline"></i>';
unload = typeof unload !== 'undefined';
$label.find( '.wpforms-loading-spinner' ).remove();
$label.find( '.wpforms-help-tooltip' ).show();
$option.find( 'input,select,textarea' ).prop( 'disabled', false );
$label.append( spinner );
$label.find( '.wpforms-help-tooltip' ).hide();
$option.find( 'input,select,textarea' ).prop( 'disabled', true );
* @param {Object} el Element.
* @return {string} Form state.
// eslint-disable-next-line
console.warn( 'WARNING! Function "wpf.getFormState( el )" has been deprecated.' );
// Serialize tested the most performant string we can use for comparisons.
return jQuery( el ).serialize();
* Remove items from an array.
* @param {Array} array An array.
* @param {any} item Array item.
* @return {number} Count of removed items.
removeArrayItem( array, item ) {
for ( let index = 0; index < array.length; index++ ) {
if ( array[ index ] === item ) {
array.splice( index, 1 );
* @param {string} str String to sanitize.
* @return {string} String after sanitization.
if ( typeof str === 'string' || str instanceof String ) {