HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux spn-python 5.15.0-89-generic #99-Ubuntu SMP Mon Oct 30 20:42:41 UTC 2023 x86_64
User: arjun (1000)
PHP: 8.1.2-1ubuntu2.20
Disabled: NONE
Upload Files
File: //var/www/html/TriadGov/wp-content/plugins/wpforms/assets/pro/js/admin/builder/fields/layout.js
/* global WPFormsBuilder, wpforms_builder, WPFormsUtils */

// noinspection ES6ConvertVarToLetConst
/**
 * Form Builder Field Layout module.
 *
 * @since 1.7.7
 */

var WPForms = window.WPForms || {}; // eslint-disable-line no-var

WPForms.Admin = WPForms.Admin || {};
WPForms.Admin.Builder = WPForms.Admin.Builder || {};

WPForms.Admin.Builder.FieldLayout = WPForms.Admin.Builder.FieldLayout || ( function( document, window, $ ) {
	/**
	 * Elements holder.
	 *
	 * @since 1.7.7
	 *
	 * @type {Object}
	 */
	let el = {};

	/**
	 * Public functions and properties.
	 *
	 * @since 1.7.7
	 *
	 * @type {Object}
	 */
	const app = {
		/**
		 * Start the engine.
		 *
		 * @since 1.7.7
		 */
		init() {
			$( app.ready );
		},

		/**
		 * DOM is fully loaded.
		 *
		 * @since 1.7.7
		 */
		ready() {
			app.setup();
			app.initLabels();
			app.events();
			app.rowDisplayHeightBalance( $( '.wpforms-layout-display-rows, .wpforms-layout-display-blocks' ) );
		},

		/**
		 * Setup. Prepare some variables.
		 *
		 * @since 1.7.7
		 */
		setup() {
			// Cache DOM elements.
			el = {
				$builder:            $( '#wpforms-builder' ),
				$fieldOptions:       $( '#wpforms-field-options' ),
				$sortableFieldsWrap: $( '#wpforms-panel-fields .wpforms-field-wrap' ),
			};
		},

		/**
		 * Bind events.
		 *
		 * @since 1.7.7
		 */
		events() {
			el.$builder
				.on( 'change', '.wpforms-field-option-row-preset input', app.presetChange )
				.on( 'change', '.wpforms-field-option-row-display select', app.displayChange )
				.on( 'mouseenter', '.wpforms-field-layout-columns .wpforms-field', app.subfieldMouseEnter )
				.on( 'mouseleave', '.wpforms-field-layout-columns .wpforms-field', app.subfieldMouseLeave )
				.on( 'wpformsFieldAdd', app.fieldAdd )
				.on( 'wpformsBeforeFieldAddToDOM', app.beforeFieldAddToDOM )
				.on( 'wpformsBeforeFieldAddOnClick', app.beforeFieldAddOnClick )
				.on( 'wpformsBeforeFieldDelete', app.beforeFieldDelete )
				.on( 'wpformsBeforeFieldDeleteAlert', app.adjustDeleteFieldAlert )
				.on( 'wpformsFieldOptionTabToggle', app.fieldOptionsUpdate )
				.on( 'wpformsFieldMoveRejected', app.fieldMoveRejected )
				.on( 'wpformsBeforeFieldDuplicate', app.beforeFieldDuplicate )
				.on( 'wpformsFieldDuplicated', app.fieldDuplicated )
				.on( 'wpformsFieldDelete', app.handleFieldDelete )
				.on( 'wpformsFieldAdd wpformsFieldChoiceAdd wpformsFieldChoiceDelete wpformsFieldDynamicChoiceToggle wpformsFieldLayoutChangeDisplay', app.handleFieldOperations )
				.on( 'wpformsFieldMoveRejected', app.handleFieldMoveRejected )
				.on( 'wpformsFieldMove', app.handleFieldMove )
				.on( 'wpformsFieldDragOver wpformsFieldDragChange', app.handleFieldDrag )
				.on( 'change', '.wpforms-field-option-row-size select', app.handleFieldSizeChange );
		},

		/**
		 * Determine whether the field type is a layout-based field.
		 *
		 * @since 1.8.9
		 *
		 * @param {string} fieldType Field type.
		 *
		 * @return {boolean} True if it is the layout-based field.
		 */
		isLayoutBasedField( fieldType ) {
			return [ 'layout', 'repeater' ].includes( fieldType );
		},

		/**
		 * Field delete event handler.
		 *
		 * @since 1.8.8
		 *
		 * @param {Object} e                   Event object.
		 * @param {string} fieldID             Field ID.
		 * @param {string} fieldType           Field type.
		 * @param {jQuery} $fieldLayoutWrapper Field layout wrapper.
		 */
		handleFieldDelete( e, fieldID, fieldType, $fieldLayoutWrapper ) {
			if ( $fieldLayoutWrapper.length === 0 || ! $fieldLayoutWrapper.hasClass( 'wpforms-layout-display-rows' ) ) {
				return;
			}

			app.rowDisplayHeightBalance( $fieldLayoutWrapper );

			$fieldLayoutWrapper.closest( '.wpforms-field-layout' ).removeClass( 'wpforms-field-child-hovered' );
		},

		/**
		 * Field change size event handler.
		 *
		 * @since 1.9.0
		 */
		handleFieldSizeChange() {
			const $this = $( this );
			const fieldID = $this.parent().data( 'field-id' );
			const $field = $( `#wpforms-field-${ fieldID }` );
			const $rows = $field.parents( '.wpforms-layout-display-rows, .wpforms-layout-display-blocks' );

			if ( ! $rows.length ) {
				return;
			}

			app.rowDisplayHeightBalance( $rows );
		},

		/**
		 * Field operations event handler.
		 *
		 * @since 1.8.8
		 *
		 * @param {Object} e       Event object.
		 * @param {string} fieldID Field ID.
		 */
		handleFieldOperations( e, fieldID ) {
			const $field = $( `#wpforms-field-${ fieldID }` );
			const $rows = $field.find( '.wpforms-layout-display-rows, .wpforms-layout-display-blocks' );

			if ( ! $rows.length ) {
				return;
			}

			app.rowDisplayHeightBalance( $rows );
		},

		/**
		 * Field move rejected event handler.
		 *
		 * @since 1.8.8
		 *
		 * @param {Object} e              Event object.
		 * @param {jQuery} $rejectedField Rejected a field object.
		 */
		handleFieldMoveRejected( e, $rejectedField ) {
			const fieldID = $rejectedField.prev( '.wpforms-field, .wpforms-alert' ).data( 'field-id' );
			const $field = $( `#wpforms-field-${ fieldID }` );
			const $rows = $field.find( '.wpforms-layout-display-rows, .wpforms-layout-display-blocks' );

			if ( ! $rows.length ) {
				return;
			}

			app.rowDisplayHeightBalance( $rows );
		},

		/**
		 * Field move event handler.
		 *
		 * @since 1.8.8
		 *
		 * @param {Object} e  Event object.
		 * @param {Object} ui UI object.
		 */
		handleFieldMove( e, ui ) {
			const $field = ui.item.first();
			const $rows = $field.parents( '.wpforms-layout-display-rows, .wpforms-layout-display-blocks' );

			if ( ! $rows.length ) {
				return;
			}

			app.rowDisplayHeightBalance( $rows );
		},

		/**
		 * Field drag event handler.
		 *
		 * @since 1.8.8
		 *
		 * @param {Object} e       Event object.
		 * @param {string} fieldID Field ID.
		 * @param {jQuery} $target Target element.
		 */
		handleFieldDrag( e, fieldID, $target ) {
			if ( ! $target.hasClass( 'wpforms-layout-column' ) ) {
				return;
			}

			const $rows = $target.parents( '.wpforms-layout-display-rows, .wpforms-layout-display-blocks' );

			if ( ! $rows.length ) {
				return;
			}

			app.rowDisplayHeightBalance( $rows );
		},

		/**
		 * Display option event handler.
		 *
		 * @since 1.8.8
		 */
		displayChange() {
			const $select = $( this );
			const display = $select.val();
			const fieldId = $select.closest( '.wpforms-field-option-row-display' ).data( 'field-id' );
			const $fieldPreviewColumns = $( `#wpforms-field-${ fieldId }` ).find( '.wpforms-field-layout-columns' );

			$select.closest( '.wpforms-field-option-row-display' ).parent().find( '.wpforms-field-option-row-preset' )
				.toggleClass( 'wpforms-layout-display-rows', display === 'rows' );

			$fieldPreviewColumns
				.toggleClass( 'wpforms-layout-display-rows', display === 'rows' )
				.toggleClass( 'wpforms-layout-display-columns', display === 'columns' )
				.find( '.wpforms-field' ).css( 'margin-bottom', display === 'columns' ? 5 : '' );

			el.$builder.trigger( 'wpformsFieldLayoutChangeDisplay', [ fieldId ] );
		},

		/**
		 * Display row fields height balance.
		 *
		 * @since 1.8.8
		 *
		 * @param {Object} $rows Rows container.
		 */
		rowDisplayHeightBalance( $rows ) {
			$rows.each( function() {
				const $wrapper = $( this );
				const data = [];

				$wrapper.find( '.wpforms-field, .wpforms-field-drag-placeholder' ).each( function() {
					const $field = $( this );
					const index = $field.index();

					data[ index ] = Math.max( data[ index ] || 0, $field.outerHeight() );
				} );

				$wrapper.find( '.wpforms-field, .wpforms-field-drag-placeholder' ).each( function() {
					const $field = $( this );

					$field.css( 'margin-bottom', data[ $field.index() ] - $field.outerHeight() + 5 );
				} );
			} );

			/**
			 * The Event fired when the height balance was performed.
			 *
			 * @since 1.8.9
			 *
			 * @param {Object} data Layout field data object.
			 */
			el.$builder.trigger( 'wpformsLayoutAfterHeightBalance', { $rows } );
		},

		/**
		 * Column placeholder click event handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Object} e Event object.
		 */
		presetChange( e ) { // eslint-disable-line no-unused-vars
			const $input = $( this ),
				preset = $input.val(),
				$presetsRow = $input.closest( '.wpforms-field-option-row-preset' ),
				fieldId = $presetsRow.data( 'field-id' ),
				$fieldPreview = $( `#wpforms-field-${ fieldId }` ),
				$fieldPreviewColumns = $fieldPreview.find( '.wpforms-field-layout-columns' ),
				$columnFields = [];

			$( document ).trigger( 'wpformsLayoutPresetChanged', $presetsRow );

			// Detach and store in an array all the fields from columns.
			$fieldPreviewColumns.find( '.wpforms-layout-column' ).each( function( columnIndex ) {
				$columnFields[ columnIndex ] = $( this ).find( '.wpforms-field' ).detach();
			} );

			const oldColumnsData = app.getFieldColumnsData( fieldId ),
				newColumnsData = preset.split( '-' )
					.map( function( width, i ) {
						return {
							width_preset: width, // eslint-disable-line camelcase
							fields: oldColumnsData[ i ] ? oldColumnsData[ i ].fields : [],
						};
					} );

			// Generate new columns markup. Update field columns data.
			app.updateFieldColumnsData( fieldId, newColumnsData );
			$fieldPreviewColumns.html( app.generatePreviewColumns( newColumnsData ) );

			// Restore all the fields in new columns.
			$fieldPreviewColumns.find( '.wpforms-layout-column' ).each( function( columnIndex ) {
				const $column = $( this );

				$column.append( $columnFields[ columnIndex ] );
				WPForms.Admin.Builder.DragFields.initSortableContainer( $column );
			} );

			// When the new preset has fewer columns than the old one.
			if ( newColumnsData.length < oldColumnsData.length ) {
				// Combine all the remaining fields.
				let $allRemainingFields = $( [] );

				for ( let i = newColumnsData.length; i < oldColumnsData.length; i++ ) {
					$allRemainingFields = $allRemainingFields.add( $columnFields[ i ] );
				}

				// Remove custom margin-bottom from remaining fields.
				$allRemainingFields.css( 'margin-bottom', '' );

				// Add all the remaining fields to the base level after the layout field.
				$fieldPreview.after( $allRemainingFields );
			}

			app.rowDisplayHeightBalance( $fieldPreviewColumns );

			// Update the field options order inside the Layout field.
			app.reorderLayoutFieldsOptions( $fieldPreview );

			/**
			 * Event fired at the end of changing the layout preset.
			 *
			 * @since 1.7.8
			 *
			 * @param {Object} data Layout field data object.
			 */
			el.$builder.trigger( 'wpformsLayoutAfterPresetChange', { fieldId, preset, newColumnsData, oldColumnsData } );
		},

		/**
		 * Generate preview columns HTML.
		 *
		 * @since 1.7.7
		 *
		 * @param {Array} columnsData Columns data.
		 *
		 * @return {string} Preview columns HTML.
		 */
		generatePreviewColumns( columnsData ) {
			if ( ! columnsData?.length ) {
				return '';
			}

			const placeholder = wp.template( 'wpforms-layout-field-column-plus-placeholder-template' )();
			const columnsHTML = columnsData.map( function( column ) {
				return `<div class="wpforms-layout-column wpforms-layout-column-${ column.width_preset }">
							${ placeholder }
						</div>`;
			} );

			return columnsHTML.join( '' );
		},

		/**
		 * Get field columns data.
		 *
		 * @since 1.7.7
		 *
		 * @param {number} fieldId Field Id.
		 *
		 * @return {Array} Columns data.
		 */
		getFieldColumnsData( fieldId ) {
			const dataJson = $( `#wpforms-field-option-${ fieldId }-columns-json` ).val();
			let	data;

			try {
				data = JSON.parse( dataJson );
			} catch ( e ) {
				data = [];
			}

			return data;
		},

		/**
		 * Check if field with given ID is inside one of the columns in the layout with given ID.
		 *
		 * @since 1.7.8
		 *
		 * @param {number} layoutId The ID of the layout field.
		 * @param {number} fieldId  The ID of the field to look for.
		 *
		 * @return {boolean} If the field exists in the layout.
		 */
		columnsHasFieldID( layoutId, fieldId ) {
			/*
			 * Get field columns data, and filter it to have only those columns, which in column.fields has field ID value.
			 * Return true if the length of such reduced data is bigger than 0.
			 */
			return app.getFieldColumnsData( layoutId ).filter( function( column ) {
				return column.fields && column.fields.includes( fieldId );
			} ).length > 0;
		},

		/**
		 * Update field columns data.
		 *
		 * @since 1.7.7
		 * @since 1.7.8 Added new triggers: `wpformsLayoutColumnsDataUpdated`, `wpformsLayoutAfterUpdateColumnsData`.
		 *
		 * @param {number} fieldId Field ID.
		 * @param {Array}  data    Columns data.
		 */
		updateFieldColumnsData( fieldId, data ) {
			const $holder = $( `#wpforms-field-option-${ fieldId }-columns-json` ),
				currentColumnsData = $holder.val(),
				newColumnsData = JSON.stringify( data );

			$holder.val( newColumnsData );

			if ( currentColumnsData !== newColumnsData ) {
				el.$builder.trigger( 'wpformsLayoutColumnsDataUpdated', { fieldId, data } );
			}

			el.$builder.trigger( 'wpformsLayoutAfterUpdateColumnsData', { fieldId, data } );
		},

		/**
		 * Event `wpformsBeforeFieldAddToDOM` handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Event}  e           Event.
		 * @param {Object} options     Field add additional options.
		 * @param {jQuery} $newField   New field preview object.
		 * @param {jQuery} $newOptions New field options object.
		 * @param {jQuery} $sortable   Sortable container.
		 */
		beforeFieldAddToDOM( e, options, $newField, $newOptions, $sortable ) {
			if (
				! $sortable ||
				! $sortable.length ||
				! $sortable.hasClass( 'wpforms-layout-column' )
			) {
				return;
			}

			// This is needed to skip adding the field to the base level.
			// The corresponding check is in `admin-builder.js` before calling the `app.fieldAddToBaseLevel()`.
			e.skipAddFieldToBaseLevel = true;

			// Add the field to the column of the Layout field.
			app.fieldAddToColumn(
				$newField,
				$newOptions,
				options.position,
				$sortable
			);
		},

		/**
		 * Event `wpformsFieldAdd` handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Event}  e       Event.
		 * @param {number} fieldId Field id.
		 * @param {string} type    Field type.
		 */
		fieldAdd( e, fieldId, type ) {
			const $fieldOptions = $( `#wpforms-field-option-${ fieldId }` ),
				$prevFieldOptions = $fieldOptions.prev(),
				prevFieldType = $prevFieldOptions.find( '.wpforms-field-option-hidden-type' ).val();

			// In the case of new field was placed right after the Layout field,
			// we must reorder the options of the fields in the columns of this layout.
			if ( app.isLayoutBasedField( prevFieldType ) ) {
				const prevFieldId = $prevFieldOptions.find( '.wpforms-field-option-hidden-id' ).val();

				app.reorderLayoutFieldsOptions(
					$( `#wpforms-field-${ prevFieldId }` )
				);
			}

			if ( ! app.isLayoutBasedField( type ) ) {
				return;
			}

			el.$builder.find( `#wpforms-field-${ fieldId } .wpforms-layout-column` ).each( function() {
				WPForms.Admin.Builder.DragFields.initSortableContainer( $( this ) );
			} );
		},

		/**
		 * Event `wpformsBeforeFieldAddOnClick` handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Event}  e      Event.
		 * @param {string} type   Field type.
		 * @param {jQuery} $field Field button object
		 */
		beforeFieldAddOnClick( e, type, $field ) {
			const $defaultColumn = el.$sortableFieldsWrap.find( '.wpforms-fields-sortable-default' );

			if ( ! $defaultColumn.length ) {
				return;
			}

			if ( app.isFieldAllowedInColum( type, $defaultColumn ) ) {
				return;
			}

			e.preventDefault();
			app.fieldMoveRejected( e, $field, null, null );
		},

		/**
		 * Event `wpformsBeforeFieldDelete` handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Event}  e       Event.
		 * @param {number} fieldId Field id.
		 * @param {string} type    Field type.
		 */
		beforeFieldDelete( e, fieldId, type ) {
			if ( ! app.isLayoutBasedField( type ) ) {
				app.removeFieldFromColumns( fieldId );

				// When a user deletes the field in the column, we need to remove
				// the class `wpforms-field-child-hovered` from the Layout field
				// to bring back the Duplicate and Delete icons.
				$( `#wpforms-field-${ fieldId }` )
					.closest( '.wpforms-field' )
					.removeClass( 'wpforms-field-child-hovered' );

				return;
			}

			const columnsData = app.getFieldColumnsData( fieldId );

			// Delete all the fields inside columns.
			columnsData.forEach( function( column ) {
				if ( ! Array.isArray( column.fields ) ) {
					return;
				}

				column.fields.forEach( function( columnFieldId ) {
					WPFormsBuilder.fieldDeleteById( columnFieldId );
				} );
			} );
		},

		/**
		 * Adjust warning text for delete field alert.
		 *
		 * @since 1.7.7
		 *
		 * @param {Object} e         Triggered event.
		 * @param {Object} fieldData Field data.
		 * @param {string} type      Field type.
		 */
		adjustDeleteFieldAlert( e, fieldData, type ) {
			if ( ! app.isLayoutBasedField( type ) ) {
				return;
			}

			e.preventDefault();

			const content = wpforms_builder[ type ]?.delete_confirm
				? wpforms_builder[ type ].delete_confirm
				: wpforms_builder.layout.delete_confirm;

			$.confirm( {
				title: false,
				content,
				icon: 'fa fa-exclamation-circle',
				type: 'orange',
				buttons: {
					confirm: {
						text: wpforms_builder.ok,
						btnClass: 'btn-confirm',
						keys: [ 'enter' ],
						action() {
							WPFormsBuilder.fieldDeleteById( fieldData.id );
						},
					},
					cancel: {
						text: wpforms_builder.cancel,
					},
				},
			} );
		},

		/**
		 * Update field options according to the position of the field.
		 * Event `wpformsFieldOptionTabToggle` handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Event}  e       Event.
		 * @param {number} fieldId Field id.
		 */
		fieldOptionsUpdate( e, fieldId ) {
			app.fieldLegacyLayoutSelectorUpdate( fieldId );
			app.fieldSizeOptionUpdate( fieldId );
		},

		/**
		 * Update the legacy layout option.
		 *
		 * @since 1.7.7
		 *
		 * @param {number} fieldId Field ID.
		 */
		fieldLegacyLayoutSelectorUpdate( fieldId ) {
			const $fieldLegacyLayoutSelector = $( `#wpforms-field-option-row-${ fieldId }-css .layout-selector-display` );

			let	$fieldLegacyLayoutNotice = $fieldLegacyLayoutSelector.find( '.wpforms-alert-layout' );

			if ( $fieldLegacyLayoutNotice.length ) {
				return;
			}

			// Add "Layouts Have Moved!" notice.
			$fieldLegacyLayoutNotice = $( `
				<div class="wpforms-alert-warning wpforms-alert-layout wpforms-alert wpforms-alert-nomargin">
					<h4>${ wpforms_builder.layout.legacy_layout_notice_title }</h4>
					<p>${ wpforms_builder.layout.legacy_layout_notice_text }</p>
				</div>
			` );

			$fieldLegacyLayoutSelector.append( $fieldLegacyLayoutNotice );

			// Hide the legacy layout selector.
			$fieldLegacyLayoutSelector.find( '.heading, .layouts' ).addClass( 'wpforms-hidden-strict' );
		},

		/**
		 * Update the Field Size option according to the position of the field.
		 *
		 * @since 1.7.7
		 *
		 * @param {number} fieldId Field ID.
		 */
		fieldSizeOptionUpdate( fieldId ) {
			const $field = $( `#wpforms-field-${ fieldId }` ),
				type = $field.data( 'field-type' ),
				$column = $field.closest( '.wpforms-layout-column' ),
				isFieldInColumn = $column.length > 0,
				parentType = $column.closest( '.wpforms-field' ).data( 'field-type' ) ?? 'layout';

			// Do not touch the Field Size option for certain fields.
			if ( [ 'textarea', 'richtext' ].indexOf( type ) > -1 ) {
				return;
			}

			const $fieldSizeOptionRow = $( `#wpforms-field-option-row-${ fieldId }-size` );
			let	$fieldSizeOptionNotice = $fieldSizeOptionRow.find( '.wpforms-notice-field-size' );
			const isColumnFullWidth = $column.hasClass( 'wpforms-layout-column-100' ) && $field.closest( '.wpforms-field-layout' ).length;

			// Add "Field size cannot be changed" notice.
			$fieldSizeOptionNotice.remove();
			$fieldSizeOptionNotice = $( `
				<label class="sub-label wpforms-notice-field-size" title="${ wpforms_builder[ parentType ].size_notice_tooltip }">
					${ wpforms_builder[ parentType ].size_notice_text }
				</label>
			` );

			$fieldSizeOptionRow.append( $fieldSizeOptionNotice );

			const $fieldSizeOptionSelect = $fieldSizeOptionRow.find( 'select' );

			// Toggle field size selector title attribute.
			if ( isFieldInColumn ) {
				$fieldSizeOptionSelect.attr( 'title', wpforms_builder[ parentType ].size_notice_tooltip );
			} else {
				$fieldSizeOptionSelect.attr( 'title', '' );
			}

			// Toggle field size selector `disabled` attribute and notice visibility.
			const isDisabled = isFieldInColumn && ! isColumnFullWidth;

			$fieldSizeOptionSelect.toggleClass( 'wpforms-disabled', isDisabled );
			$fieldSizeOptionNotice.toggleClass( 'wpforms-hidden', ! isDisabled );
		},

		/**
		 * Receive field to column inside the Layout Field.
		 *
		 * @since 1.7.7
		 *
		 * @param {number} fieldId   Field ID.
		 * @param {number} position  Field position inside the column.
		 * @param {jQuery} $sortable Sortable column container.
		 */
		receiveFieldToColumn( fieldId, position, $sortable ) {
			// Remove the field from all the columns.
			app.removeFieldFromColumns( fieldId );

			// Do not need to continue if the field dropped to the main fields wrap.
			if ( ! $sortable || ! $sortable.hasClass( 'wpforms-layout-column' ) ) {
				return;
			}

			app.positionFieldInColumn( fieldId, position, $sortable );
			app.fieldOptionsUpdate( null, fieldId );

			/**
			 * Trigger on the end of the process of receiving field in layout's column.
			 *
			 * @since 1.7.8
			 *
			 * @param {Object} data Field data object.
			 */
			el.$builder.trigger( 'wpformsLayoutAfterReceiveFieldToColumn', { fieldId, position, column: $sortable } );
		},

		/**
		 * Remove field from all the columns in all Layout fields.
		 *
		 * @since 1.7.7
		 *
		 * @param {number} fieldId Field ID.
		 */
		removeFieldFromColumns( fieldId ) {
			fieldId = Number( fieldId );

			el.$builder.find( '.wpforms-field' ).each( function() {
				const $field = $( this );

				if ( ! app.isLayoutBasedField( $field.data( 'field-type' ) ) ) {
					return;
				}

				const layoutFieldId = Number( $field.data( 'field-id' ) );
				const columnsData = app.getFieldColumnsData( layoutFieldId );

				for ( let i = 0; i < columnsData.length; i++ ) {
					if ( ! Array.isArray( columnsData[ i ].fields ) ) {
						continue;
					}

					// Remove field from the column's list of fields.
					columnsData[ i ].fields = columnsData[ i ].fields.filter( function( id ) {
						return Number( id ) !== fieldId;
					} );
				}

				app.updateFieldColumnsData( layoutFieldId, columnsData );
			} );
		},

		/**
		 * Position field in the column inside the Layout Field.
		 *
		 * @since 1.7.7
		 *
		 * @param {number} fieldId   Field ID.
		 * @param {number} position  The new position of the field inside the column.
		 * @param {jQuery} $sortable Sortable column container.
		 */
		positionFieldInColumn( fieldId, position, $sortable ) {
			// Proceed only in the column.
			if ( ! $sortable || ! $sortable.hasClass( 'wpforms-layout-column' ) ) {
				return;
			}

			const $layoutField = $sortable.closest( '.wpforms-field' ),
				layoutFieldId = $layoutField.data( 'field-id' ),
				columnIndex = $sortable.index();

			const columnsData = app.getFieldColumnsData( layoutFieldId );

			// Skip if there is no data of the column.
			if ( ! columnsData || ! columnsData[ columnIndex ] ) {
				return;
			}

			const column = columnsData[ columnIndex ];

			column.fields = Array.isArray( column.fields ) ? column.fields : [];

			fieldId = Number( fieldId );

			// Remove field from the column.
			column.fields = column.fields.filter( function( id ) {
				return Number( id ) !== fieldId;
			} );

			// Add field to the new position in the column.
			column.fields.splice( position, 0, fieldId );

			columnsData[ columnIndex ] = column;

			app.updateFieldColumnsData( layoutFieldId, columnsData );
		},

		/**
		 * Duplicate Layout field.
		 *
		 * @since 1.7.7
		 *
		 * @param {number} layoutFieldId Layout field ID.
		 */
		duplicateLayoutField( layoutFieldId ) {
			const $layoutField = $( `#wpforms-field-${ layoutFieldId }` );

			if ( ! app.isLayoutBasedField( $layoutField.data( 'field-type' ) ) ) {
				return;
			}

			const columnsData = app.getFieldColumnsData( layoutFieldId ),
				newLayoutFieldID = WPFormsBuilder.fieldDuplicateRoutine( layoutFieldId, true ),
				$newLayoutField = $( `#wpforms-field-${ newLayoutFieldID }` ),
				$newLayoutFieldOptions = $( `#wpforms-field-option-${ newLayoutFieldID }` ),
				$newLayoutFieldColumn = $newLayoutField.find( '.wpforms-layout-column' ),
				newColumnsData = JSON.parse( JSON.stringify( columnsData ) ),
				preset = $( `#wpforms-field-option-${ layoutFieldId } .wpforms-field-option-row-preset input:checked` ).val();

			// Duplicate preset option value.
			$newLayoutFieldOptions.find( `#wpforms-field-option-${ newLayoutFieldID }-preset-${ preset }` ).prop( 'checked', true );

			// Delete the fields from the columns.
			$newLayoutField.find( '.wpforms-layout-column .wpforms-field' ).remove();

			// Reset "active column" state.
			$newLayoutField.find( '.wpforms-fields-sortable-default' ).removeClass( 'wpforms-fields-sortable-default' );

			columnsData.forEach( function( column, index ) {
				newColumnsData[ index ].fields = [];

				if ( ! Array.isArray( column.fields ) ) {
					return;
				}

				const $newColumn = $newLayoutFieldColumn.eq( index );

				column.fields.forEach( function( fieldId ) {
					const $field = $( `#wpforms-field-${ fieldId }` );

					// Skip if there is no field OR duplicate field button.
					if ( ! $field.length || ! $field.find( '> .wpforms-field-duplicate' ).length ) {
						return;
					}

					const newFieldID = WPFormsBuilder.fieldDuplicateRoutine( fieldId, false ),
						$newField = $( `#wpforms-field-${ newFieldID }` ).detach().removeClass( 'active' ),
						$newFieldOptions = $( `#wpforms-field-option-${ newFieldID }` );

					$newColumn.append( $newField );
					$newFieldOptions.hide();
					newColumnsData[ index ].fields.push( newFieldID );
				} );
			} );

			app.updateFieldColumnsData( newLayoutFieldID, newColumnsData );
			app.reorderLayoutFieldsOptions( $newLayoutField );

			// Activate duplicate field to keep consistent behavior with other fields.
			$newLayoutField.trigger( 'click' );

			WPFormsUtils.triggerEvent( el.$builder, 'wpformsFieldDuplicated', [ layoutFieldId, $layoutField, newLayoutFieldID, $newLayoutField ] );
		},

		/**
		 * Subfield mouseenter event handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Object} e Event object.
		 */
		subfieldMouseEnter( e ) { // eslint-disable-line no-unused-vars
			$( this )
				.closest( '.wpforms-field-layout-columns' ).closest( '.wpforms-field' )
				.addClass( 'wpforms-field-child-hovered' );
		},

		/**
		 * Subfield mouseleave event handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Object} e Event object.
		 */
		subfieldMouseLeave( e ) { // eslint-disable-line no-unused-vars
			$( this )
				.closest( '.wpforms-field-layout-columns' ).closest( '.wpforms-field' )
				.removeClass( 'wpforms-field-child-hovered' );
		},

		/**
		 * Init labels in all the Layout fields.
		 *
		 * @since 1.7.7
		 */
		initLabels() {
			$( '.wpforms-field-option-layout .wpforms-field-option-row-label input' ).trigger( 'input' );
		},

		/**
		 * Add field to the column of the Layout field.
		 *
		 * @since 1.7.7
		 *
		 * @param {jQuery}        $newField   New field preview.
		 * @param {jQuery}        $newOptions New field options.
		 * @param {number|string} position    New field position.
		 * @param {jQuery}        $column     Sortable column container.
		 */
		fieldAddToColumn( $newField, $newOptions, position, $column ) {
			const $fields = $column.find( '.wpforms-field' );

			if ( position === 'bottom' ) {
				position = $fields.length;
			}

			const $fieldInPosition = $fields.eq( position ),
				fieldInPositionId = $fieldInPosition.data( 'field-id' );

			if ( $fieldInPosition.length ) {
				$fieldInPosition.before( $newField );
			} else {
				$column.append( $newField );
			}

			const $fieldOptionsInPosition = $( `#wpforms-field-option-${ fieldInPositionId }` );

			if ( $fieldOptionsInPosition.length ) {
				$fieldOptionsInPosition.before( $newOptions );
			} else {
				el.$fieldOptions.append( $newOptions );
			}

			app.receiveFieldToColumn(
				$newField.data( 'field-id' ),
				position,
				$column
			);

			app.reorderLayoutFieldsOptions(
				$column.closest( '.wpforms-field-layout, .wpforms-field-repeater' )
			);
		},

		/**
		 * Display "This field is not allowed" alert modal.
		 * Event `wpformsFieldMoveRejected` handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Event}  e             Event.
		 * @param {jQuery} $field        Field object.
		 * @param {Object} ui            Sortable ui object.
		 * @param {jQuery} $targetColumn Target column element.
		 */
		fieldMoveRejected( e, $field, ui, $targetColumn ) { // eslint-disable-line no-unused-vars
			const type = $field.data( 'field-type' );
			const name = type ? $( `#wpforms-add-fields-${ type }` ).text() : $field.text();

			let modalOptions = {
				title: wpforms_builder.heads_up,
				content: wpforms_builder.layout.not_allowed_alert_text.replace( /%s/g, `<strong>${ name }</strong>` ),
				type: 'red',
			};

			/**
			 * Filter the field move rejected alert modal options.
			 *
			 * @since 1.8.9
			 *
			 * @param {Object} modalOptions  Field move rejected alert modal options.
			 * @param {jQuery} $field        Field element object.
			 * @param {Object} ui            Sortable ui object.
			 * @param {jQuery} $targetColumn Target column element.
			 *
			 * @return {string} Filtered the field rejected message.
			 */
			modalOptions = wp.hooks.applyFilters( 'wpforms.LayoutField.fieldMoveRejectedModalOptions', modalOptions, $field, ui, $targetColumn );

			$.confirm( {
				title: modalOptions.title,
				content: modalOptions.content,
				icon: 'fa fa-exclamation-circle',
				type: modalOptions.type,
				buttons: {
					confirm: {
						text: wpforms_builder.ok,
						btnClass: 'btn-confirm',
						keys: [ 'enter' ],
					},
				},
			} );
		},

		/**
		 * Reorder fields options of the fields in columns.
		 * It is critical for proper functioning EP and PB fields.
		 *
		 * @since 1.7.7
		 *
		 * @param {jQuery} $layoutField Layout field object.
		 */
		reorderLayoutFieldsOptions( $layoutField ) {
			if ( ! $layoutField?.length || ! app.isLayoutBasedField( $layoutField.data( 'field-type' ) ) ) {
				return;
			}

			const layoutFieldId = $layoutField.data( 'field-id' ),
				columnsData = app.getFieldColumnsData( layoutFieldId );

			let $lastFieldOptions = $( `#wpforms-field-option-${ layoutFieldId }` );

			columnsData.forEach( function( column, c ) {
				if ( ! Array.isArray( column.fields ) ) {
					return;
				}

				const fields = column.fields.slice();

				column.fields.forEach( function( fieldId ) {
					let $fieldOptions = $( `#wpforms-field-option-${ fieldId }` );

					if ( ! $fieldOptions.length ) {
						// Remove not existing field.
						const fieldIndex = fields.indexOf( fieldId );

						if ( fieldIndex !== -1 ) {
							fields.splice( fieldIndex, 1 );
						}

						return;
					}

					$fieldOptions = $fieldOptions.detach();
					$lastFieldOptions.after( $fieldOptions );
					$lastFieldOptions = $fieldOptions;
				} );

				column.fields = fields;

				columnsData[ c ] = column;
			} );

			app.updateFieldColumnsData( layoutFieldId, columnsData );
		},

		/**
		 * Whether the field type is allowed to be in column.
		 *
		 * @since 1.7.7
		 *
		 * @param {string} fieldType Field type to check.
		 * @param {jQuery} $target   Target element.
		 *
		 * @return {boolean} True if allowed.
		 */
		isFieldAllowedInColum( fieldType, $target ) {
			const isAllowed = wpforms_builder.layout.not_allowed_fields.indexOf( fieldType ) < 0;

			/**
			 * Allows developers to determine whether the field is allowed to be in column.
			 *
			 * @since 1.8.9
			 *
			 * @param {boolean} isAllowed Whether the field is allowed to be placed in the column.
			 * @param {string}  fieldType Field type.
			 * @param {jQuery}  $target   Target element.
			 *
			 * @return {boolean} True if allowed.
			 */
			return wp.hooks.applyFilters( 'wpforms.LayoutField.isFieldAllowedInColumn', isAllowed, fieldType, $target );
		},

		/**
		 * Event `wpformsBeforeFieldDuplicate` handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Event}  e       Event.
		 * @param {number} fieldId Field Id.
		 * @param {jQuery} $field  Field object.
		 */
		beforeFieldDuplicate( e, fieldId, $field ) {
			// Run for the Layout-based fields only.
			if ( ! app.isLayoutBasedField( $field.data( 'field-type' ) ) ) {
				return;
			}

			e.preventDefault();
			app.duplicateLayoutField( fieldId );
			WPFormsBuilder.increaseNextFieldIdAjaxRequest();
		},

		/**
		 * Event `wpformsFieldDuplicated` handler.
		 *
		 * @since 1.7.7
		 *
		 * @param {Event}  e          Event.
		 * @param {number} fieldId    Field ID.
		 * @param {jQuery} $field     Field object.
		 * @param {number} newFieldId New field ID.
		 * @param {jQuery} $newField  New field object.
		 */
		fieldDuplicated( e, fieldId, $field, newFieldId, $newField ) {
			// Skip the Layout-based fields.
			if ( app.isLayoutBasedField( $field.data( 'field-type' ) ) ) {
				return;
			}

			app.positionFieldInColumn(
				newFieldId,
				$newField.index() - 1,
				$newField.parent()
			);
		},
	};

	// Provide access to public functions/properties.
	return app;
}( document, window, jQuery ) );

// Initialize.
WPForms.Admin.Builder.FieldLayout.init();