/* 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 `
${ placeholder }
`;
} );
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 = $( `