/* global WPFormsBuilder, wpforms_builder, WPFormsUtils */
/**
* Form Builder Field Layout module.
*
* @since 1.7.7
*/
'use strict';
var WPForms = window.WPForms || {};
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: function() {
$( app.ready );
},
/**
* DOM is fully loaded.
*
* @since 1.7.7
*/
ready: function() {
app.setup();
app.initLabels();
app.events();
},
/**
* Setup. Prepare some variables.
*
* @since 1.7.7
*/
setup: function() {
// 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: function() {
el.$builder
.on( 'click', '.wpforms-layout-column-placeholder', app.columnPlaceholderClick )
.on( 'change', '.wpforms-field-option-row-preset input', app.presetChange )
.on( 'mouseenter', '.wpforms-field-layout .wpforms-field', app.subfieldMouseEnter )
.on( 'mouseleave', '.wpforms-field-layout .wpforms-field', app.subfieldMouseLeave )
.on( 'wpformsFieldAddDragStart', app.fieldCantAddModal )
.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 );
},
/**
* Column placeholder click event handler.
*
* @since 1.7.7
*
* @param {object} e Event object.
*/
columnPlaceholderClick: function( e ) {
e.stopPropagation();
const $placeholder = $( this ),
$column = $placeholder.closest( '.wpforms-layout-column' ),
isActive = $column.hasClass( 'wpforms-fields-sortable-default' ),
$allColumns = el.$sortableFieldsWrap.find( '.wpforms-layout-column' );
$allColumns.removeClass( 'wpforms-fields-sortable-default' );
$column.toggleClass( 'wpforms-fields-sortable-default', ! isActive );
WPFormsBuilder.fieldTabToggle( 'add-fields' );
},
/**
* Column placeholder click event handler.
*
* @since 1.7.7
*
* @param {object} e Event object.
*/
presetChange: function( e ) {
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' );
let $columnFields = [];
// Detach and store in array all the fields from columns.
$fieldPreviewColumns.find( '.wpforms-layout-column' ).each( function( columnIndex ) {
$columnFields[ columnIndex ] = $( this ).find( '.wpforms-field' ).detach();
} );
let oldColumnsData = app.getFieldColumnsData( fieldId ),
newColumnsData = preset.split( '-' )
.map( function( width, i ) {
return {
'width_preset': width,
'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 );
} );
// Continue only if 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 ] );
}
// Add all the remaining fields to the base level after the layout field.
$fieldPreview.after( $allRemainingFields );
}
// Update the order of the options of the fields inside the Layout field.
app.reorderLayoutFieldsOptions( $fieldPreview );
/**
* Event fired at the end of the change of layout preset.
*
* @since 1.7.8
*
* @param {object} data Layout field data object.
*/
el.$builder.trigger( 'wpformsLayoutAfterPresetChange', { fieldId: fieldId, preset: preset, newColumnsData: newColumnsData, oldColumnsData: oldColumnsData } );
},
/**
* Generate preview columns HTML.
*
* @since 1.7.7
*
* @param {Array} columnsData Columns data.
*
* @returns {string} Preview columns HTML.
*/
generatePreviewColumns: function( columnsData ) {
if ( ! columnsData || ! 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 {integer|number} fieldId Field Id.
*
* @returns {Array} Columns data.
*/
getFieldColumnsData: function( fieldId ) {
let dataJson = $( `#wpforms-field-option-${fieldId}-columns-json` ).val(),
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 {int} layoutId The ID of the layout field.
* @param {int} fieldId The ID of the field to look for.
*
* @returns {boolean} If the field exists in the layout.
*/
columnsHasFieldID: function( 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 length of such reduced data is bigger than 0.
*/
return app.getFieldColumnsData( layoutId ).filter( function( column ) {
return column.fields.includes( fieldId );
} ).length > 0;
},
/**
* Update field columns data.
*
* @since 1.7.7
* @since 1.7.8 Added new triggers: `wpformsLayoutColumnsDataUpdated`, `wpformsLayoutAfterUpdateColumnsData`.
*
* @param {integer|number} fieldId Field Id.
* @param {Array} data Columns data.
*/
updateFieldColumnsData: function( 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: fieldId, data: data } );
}
el.$builder.trigger( 'wpformsLayoutAfterUpdateColumnsData', { fieldId: fieldId, data: 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: function( 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.
// 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 {integer} fieldId Field id.
* @param {string} type Field type.
*/
fieldAdd: function( 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 ( prevFieldType === 'layout' ) {
const prevFieldId = $prevFieldOptions.find( '.wpforms-field-option-hidden-id' ).val();
app.reorderLayoutFieldsOptions(
$( `#wpforms-field-${prevFieldId}` )
);
}
if ( type !== 'layout' ) {
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: function( e, type, $field ) {
app.fieldCantAddModal( e, type, {} );
if ( e.isDefaultPrevented() ) {
return;
}
if ( app.isFieldAllowedInColum( type ) ) {
return;
}
const $defaultColumn = el.$sortableFieldsWrap.find( '.wpforms-fields-sortable-default' );
if ( ! $defaultColumn.length ) {
return;
}
e.preventDefault();
app.fieldMoveRejected( e, $field, null );
},
/**
* Event `wpformsBeforeFieldDelete` handler.
*
* @since 1.7.7
*
* @param {Event} e Event.
* @param {integer} fieldId Field id.
* @param {string} type Field type.
*/
beforeFieldDelete: function( e, fieldId, type ) {
if ( type !== 'layout' ) {
app.removeFieldFromColumns( fieldId );
// When user delete the field in the column, we need to remove
// the class `wpforms-field-child-hovered` from the Layout field
// in order to bring back the Duplicate and Delete icons.
$( `#wpforms-field-${fieldId}` )
.closest( '.wpforms-field-layout' )
.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( fieldId ) {
WPFormsBuilder.fieldDeleteById( fieldId );
} );
} );
},
/**
* 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: function( e, fieldData, type ) {
if ( type !== 'layout' ) {
return;
}
e.preventDefault();
$.confirm( {
title: false,
content: wpforms_builder.layout.delete_confirm,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action: function() {
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 {integer} fieldId Field id.
*/
fieldOptionsUpdate: function( e, fieldId ) {
app.fieldLegacyLayoutSelectorUpdate( fieldId );
app.fieldSizeOptionUpdate( fieldId );
app.fieldOptionGroupsToggle( fieldId );
},
/**
* Update legacy layout option.
*
* @since 1.7.7
*
* @param {integer} fieldId Field Id.
*/
fieldLegacyLayoutSelectorUpdate: function( 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 = $( `