' ).parent().html();
newSettingsBlock = newSettingsBlock.replace( /\[conditionals]\[(\d+)]\[(\d+)]/g, '[conditionals][0][0]' );
// If there are no existing blocks, we need to add the new block to the section
if ( $firstSettingsBlock.length === 0 ) {
const $section = $el.closest( '.wpforms-panel-content-section' );
$section.append( newSettingsBlock );
} else {
// Otherwise, add it before the first block
$firstSettingsBlock.before( newSettingsBlock );
}
// Get the newly added block - it's either the first or the last block in the section.
const $addedSettingBlock = $firstSettingsBlock.length === 0 ? $el.closest( '.wpforms-panel-content-section' ).find( '.wpforms-builder-settings-block' ).first() : $firstSettingsBlock.prev();
// Reset the confirmation type to the 1st one.
if ( blockType === 'confirmation' ) {
app.prepareChoicesJSField( $addedSettingBlock, nextID );
app.confirmationFieldsToggle( $( '.wpforms-panel-field-confirmations-type' ).first() );
}
// Init the WP Editor.
if ( typeof tinymce !== 'undefined' && typeof wp.editor !== 'undefined' && blockType === 'confirmation' ) {
wp.editor.initialize( 'wpforms-panel-field-confirmations-message-' + nextID, s.tinymceDefaults );
}
// Init tooltips for a new section.
wpf.initTooltips();
$builder.trigger( 'wpformsSettingsBlockAdded', [ $addedSettingBlock ] );
$el.attr( 'data-next-id', nextID + 1 );
},
},
cancel: {
text: wpforms_builder.cancel,
},
},
} );
// We need to process this event here because we need a confirmation modal object defined,
// so we can intrude into it.
// Pressing Enter will click the Ok button.
$builder.on( 'keypress', '#settings-block-name', function( e ) {
if ( e.keyCode === 13 ) {
$( modal.buttons.confirm.el ).trigger( 'click' );
}
} );
},
/**
* Reset the 'Select Page' field to it's initial state then
* re-initialize ChoicesJS on it.
*
* @since 1.7.9
*
* @param {jQuery} $addedSettingBlock Newly added Settings Block jQuery object.
* @param {number} addedSettingBlockID Number ID used when `$addedSettingBlock` was created.
*/
prepareChoicesJSField( $addedSettingBlock, addedSettingBlockID ) {
const $addedConfirmationWrap = $addedSettingBlock.find( `#wpforms-panel-field-confirmations-${ addedSettingBlockID }-page-wrap` );
if ( $addedConfirmationWrap.length <= 0 ) {
return;
}
const $confirmationSelectPageField = $addedConfirmationWrap.find( `#wpforms-panel-field-confirmations-${ addedSettingBlockID }-page` );
if ( $confirmationSelectPageField.length <= 0 && ! $confirmationSelectPageField.hasClass( 'choicesjs-select' ) ) {
return;
}
const $choicesWrapper = $addedConfirmationWrap.find( '.choices' );
if ( $choicesWrapper.length <= 0 ) {
return;
}
// Remove ChoicesJS-related attr.
const $selectPageField = $confirmationSelectPageField.first();
$selectPageField.removeAttr( 'data-choice' );
$selectPageField.removeAttr( 'hidden' );
$selectPageField.removeClass( 'choices__input' );
// Move the select page field to it's initial location in the DOM.
$( $selectPageField ).appendTo( $addedConfirmationWrap.first() );
// Remove the `.choices` wrapper.
$choicesWrapper.first().remove();
// Re-init ChoicesJS.
app.dropdownField.events.choicesInit( $selectPageField );
},
/**
* Show settings block editing interface.
*
* @since 1.4.8
*
* @param {jQuery} $el Element.
*/
settingsBlockNameEditingShow( $el ) {
const headerHolder = $el.parents( '.wpforms-builder-settings-block-name-holder' ),
nameHolder = headerHolder.find( '.wpforms-builder-settings-block-name' );
nameHolder
.addClass( 'editing' )
.hide();
// Make the editing interface active and in focus
headerHolder.find( '.wpforms-builder-settings-block-name-edit' ).addClass( 'active' );
wpf.focusCaretToEnd( headerHolder.find( 'input' ) );
},
/**
* Update settings block name and hide editing interface.
*
* @since 1.4.8
*
* @param {jQuery} $el Element.
*/
settingsBlockNameEditingHide( $el ) {
const headerHolder = $el.parents( '.wpforms-builder-settings-block-header' ),
nameHolder = headerHolder.find( '.wpforms-builder-settings-block-name' ),
editHolder = headerHolder.find( '.wpforms-builder-settings-block-name-edit' );
let currentName = editHolder.find( 'input' ).val().trim();
const blockType = $el.data( 'block-type' ) || $el.closest( '.wpforms-builder-settings-block' ).data( 'block-type' );
// Provide a default value for the empty settings block name.
if ( ! currentName.length ) {
currentName = wpforms_builder[ blockType + '_def_name' ];
}
// This is done for sanitizing.
editHolder.find( 'input' ).val( currentName );
nameHolder.text( currentName );
// Editing should be hidden, displaying - active.
nameHolder
.removeClass( 'editing' )
.show();
editHolder.removeClass( 'active' );
},
/**
* Clone the Notification block with all of its content and events.
* Put the newly created clone above the target.
*
* @since 1.6.5
* @since 1.7.7 Registered `wpformsSettingsBlockCloned` trigger.
*
* @param {Object} $el Clone icon DOM element.
*/
settingsBlockPanelClone( $el ) { // eslint-disable-line max-lines-per-function
const $panel = $el.closest( '.wpforms-panel-content-section' ),
$addNewSettingButton = $panel.find( '.wpforms-builder-settings-block-add' ),
$settingsBlock = $el.closest( '.wpforms-builder-settings-block' ),
$settingBlockContent = $settingsBlock.find( '.wpforms-builder-settings-block-content' ),
settingsBlockId = parseInt( $addNewSettingButton.attr( 'data-next-id' ), 10 ),
settingsBlockType = $settingsBlock.data( 'block-type' ),
settingsBlockName = $settingsBlock.find( '.wpforms-builder-settings-block-name' ).text().trim() + wpforms_builder[ settingsBlockType + '_clone' ],
isVisibleContent = $settingBlockContent.is( ':hidden' );
// Restore tooltips before cloning.
wpf.restoreTooltips( $settingsBlock );
const $clone = $settingsBlock.clone( false, true );
// Save the open / close state while cloning.
app.settingsBlockUpdateState( isVisibleContent, settingsBlockId, settingsBlockType );
// Change the cloned setting block ID and name.
$clone.data( 'block-id', settingsBlockId ).attr( 'data-block-id', settingsBlockId );
$clone.find( '.wpforms-builder-settings-block-name-holder span' ).text( settingsBlockName );
$clone.find( '.wpforms-builder-settings-block-name-holder input' ).val( settingsBlockName );
$clone.removeClass( 'wpforms-builder-settings-block-default' );
// Change the Next Settings block ID for the "Add new" button.
$addNewSettingButton.attr( 'data-next-id', settingsBlockId + 1 );
// Change the name attribute.
$clone.find( 'input, textarea, select' ).each( function() {
const $this = $( this );
if ( $this.attr( 'name' ) ) {
$this.attr( 'name', $this.attr( 'name' ).replace( /\[(\d+)]/, '[' + settingsBlockId + ']' ) );
}
if ( $this.data( 'name' ) ) {
$this.data( 'name', $this.data( 'name' ).replace( /\[(\d+)]/, '[' + settingsBlockId + ']' ) );
}
if ( $this.attr( 'class' ) ) {
$this.attr( 'class', $this.attr( 'class' ).replace( /-(\d+)/, '-' + settingsBlockId ) );
}
if ( $this.attr( 'data-radio-group' ) ) {
$this.attr( 'data-radio-group', $this.attr( 'data-radio-group' ).replace( /(\d+)-/, settingsBlockId + '-' ) );
}
} );
// Change IDs/data-attributes in DOM elements.
$clone.find( '*' ).each( function() {
const $this = $( this );
if ( $this.attr( 'id' ) ) {
$this.attr( 'id', $this.attr( 'id' ).replace( /-(\d+)/, '-' + settingsBlockId ) );
}
if ( $this.attr( 'for' ) ) {
$this.attr( 'for', $this.attr( 'for' ).replace( /-(\d+)-/, '-' + settingsBlockId + '-' ) );
}
if ( $this.data( 'input-name' ) ) {
$this.data( 'input-name', $this.data( 'input-name' ).replace( /\[(\d+)]/, '[' + settingsBlockId + ']' ) );
}
} );
// Transfer selected values to copy elements since jQuery doesn't clone the current selected state.
$settingsBlock.find( 'select' ).each( function() {
const baseSelectName = $( this ).attr( 'name' ),
clonedSelectName = $( this ).attr( 'name' ).replace( /\[(\d+)]/, '[' + settingsBlockId + ']' );
$clone.find( 'select[name="' + clonedSelectName + '"]' ).val( $( this ).attr( 'name', baseSelectName ).val() );
} );
// Insert before the target settings block.
$clone
.css( 'display', 'none' )
.insertBefore( $settingsBlock )
.show( 'fast', function() {
// Init tooltips for a new section.
wpf.initTooltips();
} );
$builder.trigger( 'wpformsSettingsBlockCloned', [ $clone, $settingsBlock.data( 'block-id' ) ] );
},
/**
* Show or hide settings block panel content.
*
* @since 1.4.8
* @since 1.9.6.1 Added `isShow` parameter.
*
* @param {Object} $el Toggle an icon DOM element.
* @param {boolean|null} isShow Force showing or hiding. If null - toggle (default), if true - show, if false - hide.
*/
settingsBlockPanelToggle( $el, isShow = null ) {
const $settingsBlock = $el.closest( '.wpforms-builder-settings-block' ),
settingsBlockId = $settingsBlock.data( 'block-id' ),
settingsBlockType = $settingsBlock.data( 'block-type' ),
$content = $settingsBlock.find( '.wpforms-builder-settings-block-content' ),
isVisible = $content.is( ':visible' ),
slideSettings = {
duration: 400,
start() {
// Send it early to save fast.
// It's an animation start, so we should save the state for the animation end (reversed).
app.settingsBlockUpdateState( isVisible, settingsBlockId, settingsBlockType );
},
always() {
if ( $content.is( ':visible' ) ) {
$el.html( '
' );
} else {
$el.html( '
' );
}
},
};
$content.stop();
// Determine the action based on the force parameter.
if ( isShow === true ) {
$content.slideDown( slideSettings );
return;
} else if ( isShow === false ) {
$content.slideUp( slideSettings );
return;
}
$content.slideToggle( slideSettings );
},
/**
* Delete settings block.
*
* @since 1.4.8
* @since 1.6.1.2 Registered `wpformsSettingsBlockDeleted` trigger.
*
* @param {jQuery} $el Delete button element.
*/
settingsBlockDelete( $el ) {
const $contentSection = $el.closest( '.wpforms-panel-content-section' );
// Skip if only one block persist.
// This condition should not be executed in normal circumstances.
if ( $contentSection.find( '.wpforms-builder-settings-block' ).length < 2 && $el.parents( '.wpforms-builder-settings-block' ).data( 'block-type' ) !== 'pdf' ) {
return;
}
const $currentBlock = $el.closest( '.wpforms-builder-settings-block' ),
blockType = $currentBlock.data( 'block-type' );
$.confirm( {
title: false,
content: wpforms_builder[ blockType + '_delete' ],
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
const settingsBlockId = $currentBlock.data( 'block-id' ),
settingsBlockType = $currentBlock.data( 'block-type' );
/* eslint-disable camelcase */
$.post( wpforms_builder.ajax_url, {
action: 'wpforms_builder_settings_block_state_remove',
nonce: wpforms_builder.nonce,
block_id: settingsBlockId,
block_type: settingsBlockType,
form_id: s.formID,
} );
/* eslint-enable */
$currentBlock.remove();
$builder.trigger( 'wpformsSettingsBlockDeleted', [ blockType, settingsBlockId ] );
},
},
cancel: {
text: wpforms_builder.cancel,
},
},
} );
},
/**
* Change the open / close state for setting block.
*
* @since 1.6.5
*
* @param {boolean} isVisible State status.
* @param {number} settingsBlockId Block ID.
* @param {string} settingsBlockType Block type.
*/
settingsBlockUpdateState( isVisible, settingsBlockId, settingsBlockType ) {
/* eslint-disable camelcase */
$.post( wpforms_builder.ajax_url, {
action: 'wpforms_builder_settings_block_state_save',
state: isVisible ? 'closed' : 'opened',
form_id: s.formID,
block_id: settingsBlockId,
block_type: settingsBlockType,
nonce: wpforms_builder.nonce,
} );
},
/**
* Change visibility for notification elements, e.g.,
* the Enable This Notification toggle and notification status.
* The elements are invisible when the form has only one notification,
* and customers can turn off all notifications instead.
*
* @since 1.9.2
* @deprecated 1.9.5 Always visible.
*/
notificationsUpdateElementsVisibility() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.notificationsUpdateElementsVisibility()" has been deprecated.' );
},
/**
* Update the notification status to display if the notification is active or inactive.
*
* @since 1.9.2
*
* @since 1.9.2
*
* @param {jQuery} $notification Notification element.
*/
notificationUpdateStatus( $notification ) {
const notificationId = $notification.data( 'block-id' ),
$notificationEnable = $( `#wpforms-panel-field-notifications-${ notificationId }-enable` );
const $status = $notification.find( '.wpforms-builder-settings-block-status' );
app.changeStatusButton( $status, $notificationEnable.val() !== '0' );
if ( ! $notificationEnable.val() ) {
$notificationEnable.val( '1' );
}
},
/**
* Change the status of a notification.
*
* @since 1.9.5
*
* @param {jQuery} $statusButton The status button element.
*/
notificationChangeStatus( $statusButton ) {
const $notification = $statusButton.closest( '.wpforms-notification' ),
notificationId = $notification.data( 'block-id' ),
$notificationEnable = $( `#wpforms-panel-field-notifications-${ notificationId }-enable` ),
isActive = $statusButton.data( 'active' );
app.changeStatusButton( $statusButton, ! isActive );
$notificationEnable.val( ! isActive ? '1' : '0' );
},
/**
* Handles the toggle functionality of the status button, updating its state
* and reflecting the change in a corresponding hidden input field.
*
* @since 1.9.6.1
*
* @param {jQuery} $statusButton The jQuery object for the status button being toggled.
*/
handleStatusButton( $statusButton ) {
const connectionId = $statusButton.data( 'connection-id' ),
isActive = $statusButton.data( 'active' );
app.changeStatusButton( $statusButton, ! isActive );
$( `#wpforms-connection-status-${ connectionId }` ).val( ! isActive ? '1' : '0' );
},
/**
* Change the status of a button.
*
* @since 1.9.5
*
* @param {jQuery} $button The button element.
* @param {boolean} isActive Whether the button is active.
*/
changeStatusButton( $button, isActive ) {
$button.removeClass( 'wpforms-badge-green wpforms-badge-silver' );
const $icon = $button.find( '.fa' ),
$label = $button.find( '.wpforms-status-label' );
$icon.removeClass( 'fa-check fa-times' );
if ( isActive ) {
$button.addClass( 'wpforms-badge-green' );
$icon.addClass( 'fa-check' );
$label.text( wpforms_builder.active );
$button.attr( 'title', wpforms_builder.deactivate );
} else {
$button.addClass( 'wpforms-badge-silver' );
$icon.addClass( 'fa-times' );
$label.text( wpforms_builder.inactive );
$button.attr( 'title', wpforms_builder.activate );
}
$button.data( 'active', isActive );
},
//--------------------------------------------------------------------//
// Revisions Panel
//--------------------------------------------------------------------//
/**
* Element bindings for Revisions panel.
*
* @since 1.7.3
*/
bindUIActionsRevisions() {
// Update a revisions panel when it becomes active.
$builder.on( 'wpformsPanelSwitched', function( event, panel ) {
if ( panel !== 'revisions' ) {
return;
}
app.updateRevisionsList();
app.updateRevisionPreview();
} );
// Update the revision list when the form was saved with a revisions panel being active.
$builder.on( 'wpformsSaved', function() {
if ( wpf.getQueryString( 'view' ) !== 'revisions' ) {
return;
}
app.updateRevisionsList();
} );
// Switch to the Revisions panel when the link is clicked.
$builder.on( 'click', '.wpforms-panel-content-revisions-link', function( e ) {
e.preventDefault();
app.panelSwitch( 'revisions' );
} );
},
/**
* Fetch and update a list of form revisions.
*
* @since 1.7.3
*/
updateRevisionsList() {
const $revisionsButtonBadge = $( '.wpforms-panel-revisions-button .badge-exclamation' );
// Revisions' badge exists, send a request and remove the badge on successful response.
if ( $revisionsButtonBadge.length ) {
$.post( wpforms_builder.ajax_url, {
action: 'wpforms_mark_panel_viewed',
form_id: s.formID, // eslint-disable-line camelcase
nonce: wpforms_builder.nonce,
} )
.done( function( response ) {
// eslint-disable-next-line no-unused-expressions
response.success ? $revisionsButtonBadge.remove() : wpf.debug( response );
} )
.fail( function( xhr, textStatus ) {
wpf.debug( xhr.responseText || textStatus || '' );
} );
}
// Revisions are disabled, no need to fetch a list of revisions.
if ( ! $builder.hasClass( 'wpforms-revisions-enabled' ) ) {
return;
}
const $revisionsList = $( '#wpforms-panel-revisions .wpforms-revisions-content' );
// Dim the list, send a request and replace the list on successful response.
$revisionsList.fadeTo( 250, 0.25, function() {
$.post( wpforms_builder.ajax_url, {
action: 'wpforms_get_form_revisions',
form_id: s.formID, // eslint-disable-line camelcase
revision_id: wpf.getQueryString( 'revision_id' ), // eslint-disable-line camelcase
nonce: wpforms_builder.nonce,
} )
.done( function( response ) {
// eslint-disable-next-line no-unused-expressions
response.success ? $revisionsList.replaceWith( response.data.html ) : wpf.debug( response );
} )
.fail( function( xhr, textStatus ) {
wpf.debug( xhr.responseText || textStatus || '' );
// Undim the list to reset the UI.
$revisionsList.fadeTo( 250, 1 );
} );
} );
},
/**
* Clone form preview from Fields panel.
*
* @since 1.7.3
*/
updateRevisionPreview() {
// Clone preview DOM from a Fields panel.
const $preview = elements.$formPreview.clone();
// Clean up the cloned preview, remove unnecessary elements, set states, etc.
$preview
.find( '.wpforms-field-duplicate, .wpforms-field-delete, .wpforms-field-helper, .wpforms-debug' )
.remove()
.end();
$preview
.find( '.wpforms-field-wrap' )
.removeClass( 'ui-sortable' )
.addClass( 'ui-sortable-disabled' );
$preview
.find( '.wpforms-field' )
.removeClass( 'ui-sortable-handle ui-draggable ui-draggable-handle active' )
.removeAttr( 'id data-field-id data-field-type' )
.removeData();
$preview
.find( '.wpforms-field-submit-button' )
.prop( 'disabled', true );
// Put the cleaned-up clone into a Preview panel.
if ( elements.$revisionPreview.hasClass( 'has-preview' ) ) {
elements
.$revisionPreview
.find( '.wpforms-preview-wrap' )
.replaceWith( $preview );
} else {
elements
.$revisionPreview
.append( $preview )
.addClass( 'has-preview' );
}
},
/**
* Inform the user about making this version the default if the revision is currently loaded, and it was modified.
*
* @since 1.7.3
*/
confirmSaveRevision() {
$.confirm( {
title: wpforms_builder.heads_up,
content: wpforms_builder.revision_update_confirm,
icon: 'fa fa-exclamation-circle',
type: 'orange',
closeIcon: false,
buttons: {
confirm: {
text: wpforms_builder.save,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
// Put the Form Builder into "saving state".
$builder.addClass( 'wpforms-revision-is-saving' );
// Save the revision as the current version and reload the Form Builder.
WPFormsBuilder.formSave( false ).done( app.revisionSavedReload );
},
},
cancel: {
text: wpforms_builder.cancel,
action() {
WPFormsBuilder.setCloseConfirmation( true );
},
},
},
} );
},
/**
* When a modified revision was saved as a current version, reload the Form Builder with the current tab active.
*
* @since 1.7.3
*/
revisionSavedReload() {
wpf.updateQueryString( 'view', wpf.getQueryString( 'view' ) );
wpf.removeQueryParam( 'revision_id' );
window.location.reload();
},
//--------------------------------------------------------------------//
// Save and Exit
//--------------------------------------------------------------------//
/**
* Element bindings for Embed and Save/Exit items.
*
* @since 1.0.0
* @since 1.5.8 Added trigger on `wpformsSaved` event to remove a `newform` URL-parameter.
*/
bindUIActionsSaveExit() {
// Embed form.
$builder.on( 'click', '#wpforms-embed', function( e ) {
e.preventDefault();
if ( $( this ).hasClass( 'wpforms-disabled' ) || $( this ).hasClass( 'wpforms-btn-light-grey-disabled' ) ) {
return;
}
WPFormsFormEmbedWizard.openPopup();
} );
// Save form.
$builder.on( 'click', '#wpforms-save', function( e ) {
e.preventDefault();
app.formSave( false );
} );
// Exit builder.
$builder.on( 'click', '#wpforms-exit', function( e ) {
e.preventDefault();
app.formExit();
} );
// After form save.
$builder.on( 'wpformsSaved', function() {
/**
* Remove the `newform` parameter if it's in URL, otherwise we can get a "race condition".
* E.g., form settings will be updated before some provider connection is loaded.
*/
wpf.removeQueryParam( 'newform' );
} );
},
// eslint-disable-next-line jsdoc/require-returns
/**
* Save form.
*
* @since 1.0.0
* @since 1.7.5 Added `wpformsBeforeSave` trigger.
*
* @param {boolean} redirect Whether to redirect after save.
*/
formSave( redirect ) { // eslint-disable-line max-lines-per-function
// Saving a revision directly is not allowed. We need to notify the user that it will overwrite the current version.
if ( $builder.hasClass( 'wpforms-is-revision' ) && ! $builder.hasClass( 'wpforms-revision-is-saving' ) ) {
app.confirmSaveRevision();
return;
}
if ( typeof tinyMCE !== 'undefined' ) {
tinyMCE.triggerSave();
}
const event = WPFormsUtils.triggerEvent( $builder, 'wpformsBeforeSave' );
// Allow callbacks on `wpformsBeforeSave` to cancel form submission by triggering `event.preventDefault()`.
if ( event.isDefaultPrevented() ) {
return;
}
const $saveBtn = elements.$saveButton,
$icon = $saveBtn.find( 'i.fa-check' ),
$spinner = $saveBtn.find( 'i.wpforms-loading-spinner' ),
$label = $saveBtn.find( 'span' ),
text = $label.text();
$label.text( wpforms_builder.saving );
$saveBtn.prop( 'disabled', true );
$icon.addClass( 'wpforms-hidden' );
$spinner.removeClass( 'wpforms-hidden' );
const data = {
action: 'wpforms_save_form',
data: JSON.stringify( app.serializeAllData( $( '#wpforms-builder-form' ) ) ),
id: s.formID,
nonce: wpforms_builder.nonce,
};
const onSaveFormFail = function( xhr ) {
wpf.debug( xhr );
let errorMessage = '';
if ( xhr.status === 403 ) {
errorMessage = wpforms_builder.error_save_form_forbidden;
}
app.formSaveError( errorMessage );
};
return $.post( wpforms_builder.ajax_url, data, function( response ) {
if ( response.success ) {
wpf.initialSave = false;
// We need to save the form next tick to ensure that JS fields are already initialized.
setTimeout( () => {
wpf._updateFormState();
$builder.trigger( 'wpformsSaved', response.data );
if ( redirect !== true ) {
return;
}
if ( app.isBuilderInPopup() ) {
app.builderInPopupClose( 'saved' );
return;
}
window.location.href = wpforms_builder.exit_url;
}, 0 );
} else {
wpf.debug( response );
app.formSaveError( response.data );
}
} ).fail( onSaveFormFail ).always( function() {
$label.text( text );
$saveBtn.prop( 'disabled', false );
$spinner.addClass( 'wpforms-hidden' );
$icon.removeClass( 'wpforms-hidden' );
} );
},
/**
* Serialize all form data including checkboxes that are not checked.
*
* @since 1.9.0
*
* @param {Object} $form Form jQuery object.
*
* @return {Array} Form data.
*/
serializeAllData( $form ) {
const formData = $form.serializeArray();
$form.find( '.wpforms-field-option-layout .wpforms-field-option-row-label_hide input[type=checkbox]' ).each( function() {
const $checkbox = $( this );
const name = $checkbox.attr( 'name' );
const value = $checkbox.is( ':checked' ) ? '1' : '';
if ( ! value ) {
formData.push( { name, value } );
}
} );
return formData;
},
/**
* Form save error.
*
* @since 1.6.3
*
* @param {string} error Error message.
*/
formSaveError( error = '' ) {
// Default error message.
if ( wpf.empty( error ) ) {
error = wpforms_builder.error_save_form;
}
// Display error in a modal window.
$.confirm( {
title: wpforms_builder.heads_up,
content: '
' + error + '
' + wpforms_builder.error_contact_support + '
',
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Exit form builder.
*
* @since 1.0.0
*/
formExit() {
if ( app.isBuilderInPopup() && app.formIsSaved() ) {
app.builderInPopupClose( 'saved' );
return;
}
if ( app.formIsSaved() ) {
window.location.href = wpforms_builder.exit_url;
} else {
$.confirm( {
title: false,
content: wpforms_builder.exit_confirm,
icon: 'fa fa-exclamation-circle',
type: 'orange',
closeIcon: true,
buttons: {
confirm: {
text: wpforms_builder.save_exit,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
app.formSave( true );
},
},
cancel: {
text: wpforms_builder.exit,
action() {
closeConfirmation = false;
if ( app.isBuilderInPopup() ) {
app.builderInPopupClose( 'canceled' );
return;
}
window.location.href = wpforms_builder.exit_url;
},
},
},
} );
}
},
/**
* Close confirmation setter.
*
* @since 1.6.2
*
* @param {boolean} confirm Close confirmation flag value.
*/
setCloseConfirmation( confirm ) {
closeConfirmation = !! confirm;
},
/**
* Check the current form state.
*
* @since 1.0.0
*
* @return {boolean} True if the form is saved.
*/
formIsSaved() { // eslint-disable-line complexity
if ( typeof wpf.savedFormState !== 'object' || Object.keys( wpf.savedFormState ).length === 0 ) {
return false;
}
const isDebugEnabled = wpf.isDebug();
const differences = {};
const currentState = wpf._getCurrentFormState();
// Compare current state with saved state
for ( const key in currentState ) {
if ( currentState[ key ] !== wpf.savedFormState[ key ] ) {
if ( ! isDebugEnabled ) {
return false;
}
differences[ key ] = {
old: wpf.savedFormState[ key ],
new: currentState[ key ],
};
}
}
// Check for deleted fields
for ( const key in wpf.savedFormState ) {
if ( ! ( key in currentState ) ) {
if ( ! isDebugEnabled ) {
return false;
}
differences[ key ] = {
old: wpf.savedFormState[ key ],
new: undefined,
};
}
}
if ( ! Object.keys( differences ).length ) {
return true;
}
wpf.debug( 'Form state differences:', differences );
return false;
},
/**
* Check if the builder opened in the popup (iframe).
*
* @since 1.6.2
*
* @return {boolean} True if builder opened in the popup.
*/
isBuilderInPopup() {
return window.self !== window.parent && window.self.frameElement.id === 'wpforms-builder-iframe';
},
/**
* Close popup with the form builder.
*
* @since 1.6.2
*
* @param {string} action Performed action: saved or canceled.
*/
builderInPopupClose( action ) {
const $popup = window.parent.jQuery( '.wpforms-builder-popup' );
const $title = $( '.wpforms-center-form-name' ).text();
$popup.find( '#wpforms-builder-iframe' ).attr( 'src', 'about:blank' );
$popup.fadeOut();
$popup.trigger( 'wpformsBuilderInPopupClose', [ action, s.formID, $title ] );
},
//--------------------------------------------------------------------//
// General / global
//--------------------------------------------------------------------//
/**
* Element bindings for general and global items.
*
* @since 1.2.0
*/
bindUIActionsGeneral() { // eslint-disable-line max-lines-per-function
// Toggle Smart Tags
$builder.on( 'click', '.toggle-smart-tag-display', app.smartTagToggle );
$builder.on( 'click', '.smart-tags-list-display a', app.smartTagInsert );
// Toggle unfoldable group of fields
$builder.on( 'click', '.wpforms-panel-fields-group.unfoldable .wpforms-panel-fields-group-title', app.toggleUnfoldableGroup );
// Hide the field preview helper box.
$builder.on( 'click', '.wpforms-field-helper-hide ', app.hideFieldHelper );
// Restrict user money input fields
$builder.on( 'input', '.wpforms-money-input', function() {
const $this = $( this ),
amount = $this.val(),
start = $this[ 0 ].selectionStart,
end = $this[ 0 ].selectionEnd;
$this.val( amount.replace( /[^0-9.,]/g, '' ) );
$this[ 0 ].setSelectionRange( start, end );
} );
// Format user money input fields
$builder.on( 'focusout', '.wpforms-money-input', function() {
const $this = $( this ),
amount = $this.val();
if ( ! amount ) {
return amount;
}
const sanitized = wpf.amountSanitize( amount ),
formatted = wpf.amountFormat( sanitized );
$this.val( formatted );
} );
// Show/hide a group of options.
$builder.on( 'change', '.wpforms-panel-field-toggle', function() {
const $input = $( this );
if ( $input.prop( 'disabled' ) ) {
return;
}
$input.prop( 'disabled', true );
app.toggleOptionsGroup( $input );
} );
// Upload or add an image.
$builder.on( 'click', '.wpforms-image-upload-add', function( event ) {
event.preventDefault();
const $this = $( this );
const $container = $this.parent();
const mediaFrame = wpf.initMediaLibrary( {
title: wpforms_builder.upload_image_title,
extensions: wpforms_builder.upload_image_extensions,
extensionsError: wpforms_builder.upload_image_extensions_error,
buttonText: wpforms_builder.upload_image_button,
} );
mediaFrame.on( 'select', function() {
const mediaAttachment = mediaFrame.state().get( 'selection' ).first().toJSON();
const $preview = $container.find( '.preview' );
$container.find( '.source' ).val( mediaAttachment.url );
$preview.empty();
$preview.prepend( '

' );
if ( $this.data( 'after-upload' ) === 'hide' ) {
$this.hide();
}
$builder.trigger( 'wpformsImageUploadAdd', [ $this, $container ] );
} ).on( 'close', function() {
mediaFrame.off( 'library:selection:add' );
} );
// Now that everything has been set, let's open up the frame.
mediaFrame.open();
} );
// Remove and uploaded image.
$builder.on( 'click', '.wpforms-image-upload-remove', function( event ) {
event.preventDefault();
const $container = $( this ).parent().parent();
$container.find( '.preview' ).empty();
$container.find( '.wpforms-image-upload-add' ).show();
$container.find( '.source' ).val( '' );
$builder.trigger( 'wpformsImageUploadRemove', [ $( this ), $container ] );
} );
// Validate email smart tags in Notifications fields.
$builder.on( 'blur', '.wpforms-notification .wpforms-panel-field-text input:not([type="search"])', function() {
app.validateEmailSmartTags( $( this ) );
} );
$builder.on( 'blur', '.wpforms-notification .wpforms-panel-field-textarea textarea', function() {
app.validateEmailSmartTags( $( this ) );
} );
// Validate From Email in Notification settings.
$builder.on( 'focusout', '.wpforms-notification .wpforms-panel-field.js-wpforms-from-email-validation input:not([type="search"])', app.validateFromEmail );
$builder.on( 'wpformsPanelSectionSwitch', app.notificationsPanelSectionSwitch );
// Mobile notice primary button / close icon click.
$builder.on( 'click', '#wpforms-builder-mobile-notice .wpforms-fullscreen-notice-button-primary, #wpforms-builder-mobile-notice .close', function() {
window.location.href = wpforms_builder.exit_url;
} );
// Mobile notice secondary button click.
$builder.on( 'click', '#wpforms-builder-mobile-notice .wpforms-fullscreen-notice-button-secondary', function() {
window.location.href = wpf.updateQueryString( 'force_desktop_view', 1, window.location.href );
} );
// License Alert close button click.
$( '#wpforms-builder-license-alert .close' ).on( 'click', function() {
window.location.href = wpforms_builder.exit_url;
} );
// License Alert dismiss button click.
$( '#wpforms-builder-license-alert .dismiss' ).on( 'click', function( event ) {
event.preventDefault();
$( '#wpforms-builder-license-alert' ).remove();
wpCookies.set( 'wpforms-builder-license-alert', 'true', 3600 );
} );
// Don't allow the Akismet setting to be enabled if the Akismet plugin isn't available.
$builder.on( 'change', '#wpforms-panel-field-settings-akismet.wpforms-akismet-disabled', function() {
const $this = $( this ),
akismetStatus = $this.data( 'akismet-status' );
if ( $this.prop( 'checked' ) ) {
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder[ akismetStatus ],
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
onClose() {
$this.prop( 'checked', false );
},
} );
}
} );
// Re-init the Show More button for multiselect instances when it's visible.
$builder.on( 'wpformsPanelSectionSwitch wpformsPanelSwitched', function() {
wpf.reInitShowMoreChoices( $( '#wpforms-panel-providers, #wpforms-panel-settings' ) );
} );
},
/**
* Notification section switch event handler.
*
* @since 1.8.2.3
*
* @param {Object} e Event object.
* @param {string} panel Panel name.
*/
notificationsPanelSectionSwitch( e, panel ) {
if ( panel !== 'notifications' ) {
return;
}
$( '.wpforms-notification .wpforms-panel-field.js-wpforms-from-email-validation input' ).trigger( 'focusout' );
},
/**
* Check if one of the payment addons payments enabled.
*
* @since 1.7.5
*
* @return {boolean} True if one of the payment addons payment enabled.
*/
isPaymentsEnabled() {
let paymentEnabled = false;
$( app.getPaymentsTogglesSelector() ).each( function() {
if ( $( this ).prop( 'checked' ) ) {
paymentEnabled = true;
return false;
}
} );
return paymentEnabled;
},
/**
* Get Payments toggles selector.
*
* @since 1.7.5
*
* @return {string} List of selectors.
*/
getPaymentsTogglesSelector() {
return `.wpforms-panel-content-section-payment-toggle-one-time input,
.wpforms-panel-content-section-payment-toggle-recurring input,
#wpforms-panel-field-stripe-enable,
#wpforms-panel-field-paypal_standard-enable,
#wpforms-panel-field-authorize_net-enable,
#wpforms-panel-field-square-enable`;
},
/**
* Toggle an options group.
*
* @since 1.6.3
*
* @param {Object} $input Toggled field.
*/
toggleOptionsGroup( $input ) {
const name = $input.attr( 'name' );
let value = '';
const $body = $( '.wpforms-panel-field-toggle-body[data-toggle="' + name + '"]' ),
enableInput = function() {
$input.prop( 'disabled', false );
};
app.toggleProviderActiveIcon( $input );
if ( $body.length === 0 ) {
enableInput();
return;
}
const type = $input.attr( 'type' );
if ( 'checkbox' === type || 'radio' === type ) {
value = $input.prop( 'checked' ) ? $input.val() : '0';
} else {
value = $input.val();
}
$body.each( function() {
/** @type {JQ|jQuery} */
const $this = $( this );
// eslint-disable-next-line no-unused-expressions
$this.attr( 'data-toggle-value' ).toString() === value.toString()
? $this.slideDown( 150, enableInput )
: $this.slideUp( 150, enableInput );
} );
},
/**
* Toggle Provider Active icon.
*
* @since 1.9.3
*
* @param {Object} $input Toggled field.
*/
toggleProviderActiveIcon( $input ) {
const provider = $input.closest( '.wpforms-panel-content-section' ).data( 'provider' );
const wrappers = [
'wpforms-panel-field-' + provider + '-enable-wrap',
'wpforms-panel-field-' + provider + '-enable_one_time-wrap',
'wpforms-panel-field-' + provider + '-enable_recurring-wrap',
];
if ( ! provider || ! wrappers.includes( $input.attr( 'id' ) ) ) {
return;
}
let isActive = false;
wrappers.forEach( ( wrapper ) => {
const $wrapper = $( '#' + wrapper );
if ( $wrapper.length && $wrapper.find( 'input' ).is( ':checked' ) ) {
isActive = true;
}
} );
const $sidebar = $( `.wpforms-panel-sidebar-section[data-section=${ provider }]` ),
$check_icon = $sidebar.find( '.fa-check-circle-o' );
$check_icon.toggleClass( 'wpforms-hidden', ! isActive );
},
/**
* Toggle all option groups.
*
* @since 1.6.3
*
* @param {jQuery} $context Context container jQuery object.
*/
toggleAllOptionGroups( $context ) {
$context = $context || $builder || $( '#wpforms-builder' ) || $( 'body' );
if ( ! $context ) {
return;
}
// Show a toggled body.
$context.find( '.wpforms-panel-field-toggle' ).each( function() {
const $input = $( this );
$input.prop( 'disabled', true );
app.toggleOptionsGroup( $input );
} );
},
/**
* Toggle unfoldable group of fields.
*
* @since 1.6.8
*
* @param {Object} e Event object.
*/
toggleUnfoldableGroup( e ) {
e.preventDefault();
const /** @type {JQ|jQuery} */ $title = $( e.target ),
$group = $title.closest( '.wpforms-panel-fields-group' ),
$inner = $group.find( '.wpforms-panel-fields-group-inner' ),
cookieName = 'wpforms_fields_group_' + $group.data( 'group' );
if ( $group.hasClass( 'opened' ) ) {
wpCookies.remove( cookieName );
$inner.stop().slideUp( 150, function() {
$group.removeClass( 'opened' );
} );
} else {
wpCookies.set( cookieName, 'true', 2592000 ); // 1 month.
$group.addClass( 'opened' );
$inner.stop().slideDown( 150 );
}
},
/**
* Hide the field preview helper box.
*
* @since 1.7.1
*
* @param {Object} e Event object.
*/
hideFieldHelper( e ) {
e.preventDefault();
e.stopPropagation();
const $helpers = $( '.wpforms-field-helper' ),
cookieName = 'wpforms_field_helper_hide';
wpCookies.set( cookieName, 'true', 30 * 24 * 60 * 60 ); // 1 month.
$helpers.hide();
},
/**
* Smart Tag toggling.
*
* @since 1.0.1
* @since 1.6.9 Simplify method.
* @since 1.9.5 Deprecated.
*
* @param {Event} e Event.
*/
smartTagToggle( e ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.smartTagToggle()" has been deprecated.' );
e.preventDefault();
// Prevent ajax to validate the default email queued on focusout event.
elements.$focusOutTarget = null;
const $this = $( this ),
$wrapper = $this.closest( '.wpforms-panel-field,.wpforms-field-option-row' );
if ( $wrapper.hasClass( 'smart-tags-toggling' ) ) {
return;
}
$wrapper.addClass( 'smart-tags-toggling' );
if ( $this.hasClass( 'smart-tag-showing' ) ) {
app.removeSmartTagsList( $this );
return;
}
app.insertSmartTagsList( $this );
},
/**
* Remove a Smart Tag list.
*
* @since 1.6.9
* @since 1.9.5 Deprecated.
*
* @param {JQ|jQuery} $el Toggle element.
*/
removeSmartTagsList( $el ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.removeSmartTagsList()" has been deprecated.' );
const $wrapper = $el.closest( '.wpforms-panel-field,.wpforms-field-option-row' ),
$list = $wrapper.find( '.smart-tags-list-display' );
$el.find( 'span' ).text( wpforms_builder.smart_tags_show );
$list.slideUp( '', function() {
$list.remove();
$el.removeClass( 'smart-tag-showing' );
$wrapper.removeClass( 'smart-tags-toggling' );
} );
},
/**
* Insert Smart Tag list.
*
* @since 1.6.9
* @since 1.9.5 Deprecated.
*
* @param {JQ|jQuery} $el Toggle element.
*/
insertSmartTagsList( $el ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.insertSmartTagsList()" has been deprecated.' );
const $wrapper = $el.closest( '.wpforms-panel-field,.wpforms-field-option-row' );
let $label = $el.closest( 'label' ),
insideLabel = true;
if ( ! $label.length ) {
$label = $wrapper.find( 'label' );
insideLabel = false;
}
const smartTagList = app.getSmartTagsList( $el, $label.attr( 'for' ).indexOf( 'wpforms-field-option-' ) !== -1 );
// eslint-disable-next-line no-unused-expressions
insideLabel
? $label.after( smartTagList )
: $el.after( smartTagList );
$el.find( 'span' ).text( wpforms_builder.smart_tags_hide );
$wrapper.find( '.smart-tags-list-display' ).slideDown( '', function() {
$el.addClass( 'smart-tag-showing' );
$wrapper.removeClass( 'smart-tags-toggling' );
} );
},
/**
* Get Smart Tag list markup.
*
* @since 1.6.9
* @since 1.9.5 Deprecated.
*
* @param {jQuery} $el Toggle element.
* @param {boolean} isFieldOption Is a field option.
*
* @return {string} Smart Tags list markup.
*/
getSmartTagsList( $el, isFieldOption ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.getSmartTagsList()" has been deprecated.' );
let smartTagList;
smartTagList = '
';
return smartTagList;
},
/**
* Get Smart Tag fields elements markup.
*
* @since 1.6.9
* @since 1.9.5 Deprecated.
*
* @param {jQuery} $el Toggle element.
*
* @return {string} Smart Tags list elements markup.
*/
getSmartTagsListFieldsElements( $el ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.getSmartTagsListFieldsElements()" has been deprecated.' );
const type = $el.data( 'type' );
if ( ! [ 'fields', 'all' ].includes( type ) ) {
return '';
}
const fields = app.getSmartTagsFields( $el );
if ( ! fields ) {
return '
' + wpforms_builder.fields_unavailable + '';
}
let smartTagListElements = '';
smartTagListElements += '
' + wpforms_builder.fields_available + '';
for ( const fieldKey in fields ) {
smartTagListElements += app.getSmartTagsListFieldsElement( fields[ fieldKey ] );
}
return smartTagListElements;
},
/**
* Get fields that possible to create smart tag.
*
* @since 1.6.9
* @since 1.9.5 Deprecated.
*
* @param {jQuery} $el Toggle element.
*
* @return {Array} Fields for smart tags.
*/
getSmartTagsFields( $el ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.getSmartTagsFields()" has been deprecated.' );
const allowed = $el.data( 'fields' );
const isAllowedRepeater = $el.data( 'allow-repeated-fields' );
const allowedFields = allowed ? allowed.split( ',' ) : undefined;
return wpf.getFields( allowedFields, true, isAllowedRepeater );
},
/**
* Get field markup for the Smart Tags list.
*
* @since 1.6.9
* @since 1.9.5 Deprecated.
*
* @param {Object} field A field.
*
* @return {string} Smart Tags field markup.
*/
getSmartTagsListFieldsElement( field ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.getSmartTagsListFieldsElement()" has been deprecated.' );
const label = field.label
? wpf.encodeHTMLEntities( wpf.sanitizeHTML( field.label ) )
: wpforms_builder.field + ' #' + field.id;
let html = `
${ label }`;
const additionalTags = field.additional || [];
// Add additional tags for `name`, `date/time` and `address` fields.
if ( additionalTags.length > 1 ) {
additionalTags.forEach( ( additionalTag ) => {
// Capitalize the first letter and add space before numbers.
const additionalTagLabel = additionalTag.charAt( 0 ).toUpperCase() + additionalTag.slice( 1 ).replace( /(\D)(\d)/g, '$1 $2' );
html += `
${ label } – ${ additionalTagLabel }`;
} );
}
return html;
},
/**
* Get Smart Tag other elements' markup.
*
* @since 1.6.9
* @since 1.9.5 Deprecated.
*
* @param {jQuery} $el Toggle element.
* @param {boolean} isFieldOption Is a field option.
*
* @return {string} Smart Tags list element markup.
*/
getSmartTagsListOtherElements( $el, isFieldOption ) {// eslint-disable-line complexity
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.getSmartTagsListOtherElements()" has been deprecated.' );
const type = $el.data( 'type' );
let smartTagListElements;
if ( type !== 'other' && type !== 'all' ) {
return '';
}
smartTagListElements = '
' + wpforms_builder.other + '';
for ( const smartTagKey in wpforms_builder.smart_tags ) {
if (
( isFieldOption && wpforms_builder.smart_tags_disabled_for_fields.includes( smartTagKey ) ) ||
(
$el.data( 'location' ) === 'confirmations' &&
wpforms_builder.smart_tags_disabled_for_confirmations.includes( smartTagKey )
)
) {
continue;
}
smartTagListElements += '
' + wpforms_builder.smart_tags[ smartTagKey ] + '';
}
return smartTagListElements;
},
/**
* Smart Tag insert.
*
* @since 1.0.1
* @since 1.6.9 TinyMCE compatibility.
* @since 1.9.5 Deprecated.
*
* @param {Event} e Event.
*/
smartTagInsert( e ) { // eslint-disable-line complexity
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.smartTagInsert()" has been deprecated.' );
e.preventDefault();
const /** @type {JQ|jQuery} */ $this = $( this ),
$list = $this.closest( '.smart-tags-list-display' ),
$wrapper = $list.closest( '.wpforms-panel-field,.wpforms-field-option-row' ),
$toggle = $wrapper.find( '.toggle-smart-tag-display' ),
$input = $wrapper.find( 'input[type=text], textarea' ),
meta = $this.data( 'meta' ),
additional = $this.data( 'additional' ) ? '|' + $this.data( 'additional' ) : '',
type = $this.data( 'type' );
let smartTag = type === 'field' ? '{field_id="' + meta + additional + '"}' : '{' + meta + '}',
editor;
if ( typeof tinyMCE !== 'undefined' ) {
editor = tinyMCE.get( $input.prop( 'id' ) );
if ( editor && ! editor.hasFocus() ) {
editor.focus( true );
}
}
if ( editor && ! editor.isHidden() ) {
editor.insertContent( smartTag );
} else {
smartTag = ' ' + smartTag + ' ';
$input.insertAtCaret( smartTag );
// Remove redundant spaces after wrapping smartTag into spaces.
$input.val( $input.val().trim().replace( ' ', ' ' ) );
$input.trigger( 'focus' ).trigger( 'input' );
}
// Remove the list, all done!
$list.slideUp( '', function() {
$list.remove();
} );
$toggle.find( 'span' ).text( wpforms_builder.smart_tags_show );
$wrapper.find( '.toggle-smart-tag-display' ).removeClass( 'smart-tag-showing' );
},
/**
* Validate email smart tags in Notifications fields.
*
* @since 1.4.9
* @since 1.9.5 Deprecated.
*
* @param {Object} $el Input field to check the value for.
*/
validateEmailSmartTags( $el ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.validateEmailSmartTags()" has been deprecated.' );
let val = $el.val();
if ( ! val ) {
return;
}
// Turns '{email@domain.com}' into 'email@domain.com'.
// Email RegEx inspired by http://emailregex.com
val = val.replace( /{(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))}/g, function( x ) {
return x.slice( 1, -1 );
} );
$el.val( val );
},
/**
* Validate the Email field smart tag `{field_id="N"}` and return the error.
*
* @since 1.9.5
*
* @param {string} value Input field value.
*
* @return {string|null} Error message or null in case the regexp pattern doesn't match.
*/
getEmailFieldSmartTagError( value ) {
// Detects `{field_id="N"}` smart tags.
const fieldSmartTagRegex = /\{field_id="(\d+)"}/g;
if ( ! fieldSmartTagRegex.test( value ) ) {
return null;
}
// Reset regex lastIndex to ensure we start from the beginning.
fieldSmartTagRegex.lastIndex = 0;
// Extract the field ID from the smart tag.
const match = fieldSmartTagRegex.exec( value );
const fieldId = match ? match[ 1 ] : null;
const fieldSettings = wpf.getField( fieldId );
if ( fieldSettings && fieldSettings.type === 'email' ) {
return '';
}
return wpforms_builder.allow_only_email_fields;
},
/**
* Validate From Email in the Notification block.
*
* @since 1.8.1
*/
validateFromEmail() {
// Detect repeated execution.
if ( wpf.isRepeatedCall( 'validateFromEmail' ) ) {
return;
}
const $field = $( this );
const value = $field.val();
if ( $field.data( 'value' ) === value ) {
return;
}
$field.data( 'value', value );
const $fieldWrapper = $field.parent();
const warningClass = 'wpforms-panel-field-warning';
const blockedSymbolsRegex = /[\s,;]/g;
if ( blockedSymbolsRegex.test( value.trim() ) ) {
$fieldWrapper.addClass( warningClass );
app.printNotice( wpforms_builder.allow_only_one_email, $fieldWrapper );
return;
}
if ( ! app.shouldCallAjaxValidation( value, $fieldWrapper, warningClass ) ) {
return;
}
app.ajaxValidation( value, $fieldWrapper, warningClass );
},
/**
* Whether we should call Ajax validation.
*
* @since 1.9.5
*
* @param {*} value Field value.
* @param {jQuery} $fieldWrapper Field wrapper.
* @param {string} warningClass Warning class.
*
* @return {boolean} True if Ajax validation should be performed, otherwise false.
*/
shouldCallAjaxValidation( value, $fieldWrapper, warningClass ) {
let error = '';
let callAjaxValidation = true;
// If the field is empty.
error = value === '' ? wpforms_builder.empty_email_address : '';
// If the field is not empty, check for the `{field_id}` smart tag.
if ( error === '' ) {
error = app.getEmailFieldSmartTagError( value );
callAjaxValidation = error === null;
}
// If there is an error, we don't need to make an AJAX request.
if ( error ) {
$fieldWrapper.addClass( warningClass );
app.printNotice( error, $fieldWrapper, value === '' );
return false;
}
if ( ! callAjaxValidation ) {
$fieldWrapper.removeClass( warningClass );
app.removeNotice( $fieldWrapper );
return false;
}
return true;
},
/**
* Whether we should call Ajax validation.
*
* @since 1.9.5
*
* @param {*} value Field value.
* @param {jQuery} $fieldWrapper Field wrapper.
* @param {string} warningClass Warning class.
*/
ajaxValidation( value, $fieldWrapper, warningClass ) {
const data = {
form_id: s.formID, // eslint-disable-line camelcase
email: value,
nonce: wpforms_builder.nonce,
action: 'wpforms_builder_notification_from_email_validate',
};
// noinspection JSUnusedLocalSymbols
$.post(
wpforms_builder.ajax_url, data, function( res ) {
app.removeNotice( $fieldWrapper );
if ( res.success ) {
$fieldWrapper.removeClass( warningClass );
return;
}
$fieldWrapper.addClass( warningClass );
$fieldWrapper.append( res.data );
}
).fail( function( xhr ) {
// eslint-disable-next-line no-console
console.log( xhr.responseText );
} );
},
/**
* Disabled fields.
* Addon fields in Lite initialization.
*
* @since 1.9.4
*/
disabledFields: {
init() {
app.disabledFields.initCouponsChoicesJS();
app.disabledFields.initFileUploadChoicesJS();
},
/**
* Initialize Choices.js for the Coupon field.
*
* @since 1.9.4
*/
initCouponsChoicesJS() {
if ( typeof window.Choices !== 'function' || WPForms.Admin.Builder.Coupons ) {
return;
}
$( '.wpforms-field-option-row-allowed_coupons select:not(.choices__input)' ).each( function() {
const $select = $( this );
const choicesInstance = new Choices(
$select.get( 0 ),
{
shouldSort: false,
removeItemButton: true,
renderChoicesLimit: 5,
callbackOnInit() {
wpf.showMoreButtonForChoices( this.containerOuter.element );
},
} );
// Save the Choices.js instance for future access.
$select.data( 'choicesjs', choicesInstance );
} );
},
/**
* Initialize Choices.js for the File Upload field.
*
* @since 1.9.4
*/
initFileUploadChoicesJS() {
if ( typeof window.Choices !== 'function' || WPForms.Admin.Builder.FieldFileUpload ) {
return;
}
const $selects = $( '.wpforms-file-upload-user-roles-select, .wpforms-file-upload-user-names-select' );
$selects.each( function() {
new Choices( $( this )[ 0 ], { removeItemButton: true } );
} );
},
},
//--------------------------------------------------------------------//
// Icon Choices
//--------------------------------------------------------------------//
/**
* Icon Choices component.
*
* @since 1.7.9
*/
iconChoices: {
/**
* Runtime component cache.
*
* Field "toggle": "Use Icon Choices" toggle that initiated the installation.
* Field "previousModal": Last open modal that may need to be closed.
*
* @since 1.7.9
*/
cache: {},
/**
* Component configuration settings.
*
* @since 1.7.9
*/
config: {
colorPropertyName: '--wpforms-icon-choices-color',
},
/**
* Initialize the component.
*
* @since 1.7.9
*/
init() {
// Extend jquery-confirm plugin with max-height support for the content area.
app.iconChoices.extendJqueryConfirm();
$builder.on( 'wpformsBuilderReady', function( event ) {
// If there are Icon Choices fields but the library is not installed - force install prompt.
if ( wpforms_builder.icon_choices.is_active && ! wpforms_builder.icon_choices.is_installed ) {
app.iconChoices.openInstallPromptModal( true );
// Prevent the Form Builder from getting ready (hold the loading state).
event.preventDefault();
}
} );
// Toggle Icon Choices on or off.
$builder.on( 'change', '.wpforms-field-option-row-choices_icons input', app.iconChoices.toggleIconChoices );
// Change accent color.
$builder.on( 'change', '.wpforms-field-option-row-choices_icons_color .wpforms-color-picker', app.iconChoices.changeIconsColor );
// Update field preview when option value is changed (style, size).
$builder.on( 'change', '.wpforms-field-option-row-choices_icons_style select, .wpforms-field-option-row-choices_icons_size select', function() {
const fieldID = $( this ).parent().data( 'field-id' ),
fieldType = $( '#wpforms-field-option-' + fieldID ).find( '.wpforms-field-option-hidden-type' ).val();
app.fieldChoiceUpdate( fieldType, fieldID );
} );
// Open Icon Picker modal.
$builder.on( 'click', '.wpforms-field-option-row-choices .choices-list .wpforms-icon-select', app.iconChoices.openIconPickerModal );
},
/**
* Turn the feature on or off.
*
* @since 1.7.9
*/
toggleIconChoices() { // eslint-disable-line complexity
const $this = $( this ),
checked = $this.is( ':checked' );
// Check if a required icon library is installed.
if ( checked && ! wpforms_builder.icon_choices.is_installed ) {
app.iconChoices.cache.toggle = $this;
app.iconChoices.openInstallPromptModal();
return;
}
const fieldID = $this.closest( '.wpforms-field-option-row' ).data( 'field-id' );
const $fieldOptions = $( `#wpforms-field-option-${ fieldID }` ),
$imageChoices = $fieldOptions.find( `#wpforms-field-option-${ fieldID }-choices_images` ),
$choicesList = $fieldOptions.find( `#wpforms-field-option-row-${ fieldID }-choices ul` );
// Turn Image Choice off.
if ( checked && $imageChoices.is( ':checked' ) ) {
$imageChoices.prop( 'checked', false ).trigger( 'change' );
}
// Toggle Advanced > Dynamic Choices on or off.
$fieldOptions.find( `#wpforms-field-option-row-${ fieldID }-dynamic_choices` ).toggleClass( 'wpforms-hidden', checked );
// Toggle subfields.
$fieldOptions.find( `#wpforms-field-option-row-${ fieldID }-choices_icons_color` ).toggleClass( 'wpforms-hidden' );
$fieldOptions.find( `#wpforms-field-option-row-${ fieldID }-choices_icons_size` ).toggleClass( 'wpforms-hidden' );
$fieldOptions.find( `#wpforms-field-option-row-${ fieldID }-choices_icons_style` ).toggleClass( 'wpforms-hidden' );
const $colorOption = $fieldOptions.find( `#wpforms-field-option-${ fieldID }-choices_icons_color` ),
colorValue = _.isEmpty( $colorOption.val() ) ? wpforms_builder.icon_choices.default_color : $colorOption.val();
// Set accent color for all choices.
$choicesList.prop( 'style', `${ app.iconChoices.config.colorPropertyName }: ${ colorValue };` );
// Toggle icon selectors with previews for all choices.
$choicesList.toggleClass( 'show-icons', checked );
// Set the layout to inline on activation, revert to one column on deactivation.
$fieldOptions.find( `#wpforms-field-option-${ fieldID }-input_columns` ).val( checked ? 'inline' : '' ).trigger( 'change' );
$( `#wpforms-field-option-row-${ fieldID }-choices_icons_hide` ).toggleClass( 'wpforms-hidden', ! checked );
// Finally, update the preview.
app.fieldChoiceUpdate( $fieldOptions.find( '.wpforms-field-option-hidden-type' ).val(), fieldID );
},
/**
* Change accent color and update previews.
*
* @since 1.7.9
*/
changeIconsColor() {
const $this = $( this ),
fieldID = $this.parents( '.wpforms-field-option-row' ).data( 'field-id' ),
$field = $( '#wpforms-field-option-' + fieldID ),
type = $field.find( '.wpforms-field-option-hidden-type' ).val(),
$choicesList = $field.find( '.wpforms-field-option-row-choices .choices-list' ),
colorValue = app.getValidColorPickerValue( $this );
// Update icons color in options panel.
$choicesList.prop( 'style', `${ app.iconChoices.config.colorPropertyName }: ${ colorValue };` );
// Update preview.
app.fieldChoiceUpdate( type, fieldID );
},
/**
* Open a modal prompting to install the icon library for Icon Choices.
*
* @since 1.7.9
*
* @param {boolean} force Whether it's a normal installation procedure or forced if the library is necessary but is missing.
*/
openInstallPromptModal( force = false ) {
const content = force
? wpforms_builder.icon_choices.strings.reinstall_prompt_content
: wpforms_builder.icon_choices.strings.install_prompt_content;
const modal = $.confirm( {
title: wpforms_builder.heads_up,
content,
icon: 'fa fa-info-circle',
type: 'orange',
buttons: {
continue: {
text: wpforms_builder.continue,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
this.setIcon( 'fa fa-cloud-download' );
this.setTitle( wpforms_builder.icon_choices.strings.install_title );
this.setContent( wpforms_builder.icon_choices.strings.install_content );
$.each( this.buttons, function( _index, button ) {
button.hide();
} );
app.iconChoices.installIconLibrary();
// Do not close the modal.
return false;
},
},
},
onOpen() {
// Turn the toggle off during normal installation.
if ( ! force && app.iconChoices.cache.toggle ) {
app.iconChoices.cache.toggle.prop( 'checked', false );
}
app.iconChoices.cache.previousModal = this;
},
} );
// Add a Cancel button for normal installation routine only.
if ( ! force ) {
modal.buttons.cancel = {
text: wpforms_builder.cancel,
keys: [ 'esc' ],
action() {
app.iconChoices.cache.toggle.prop( 'checked', false );
},
};
}
},
/**
* Silently download and install the icon library on the server.
*
* @since 1.7.9
*/
installIconLibrary() {
const data = {
// eslint-disable-next-line camelcase
_wp_http_referer: wpf.updateQueryString( '_wp_http_referer', null ),
nonce: wpforms_builder.nonce,
action: 'wpforms_icon_choices_install',
};
$.ajaxSetup( {
type: 'POST',
timeout: 120000, // 2 minutes.
} );
$.post( wpforms_builder.ajax_url, data, function( response ) {
// eslint-disable-next-line no-unused-expressions
response.success
? app.iconChoices.openInstallSuccessModal()
: app.iconChoices.openInstallErrorModal( response );
} ).fail( function( jqXHR ) {
app.iconChoices.openInstallErrorModal( jqXHR );
} );
},
/**
* Open a modal on icon library installation success.
*
* @since 1.7.9
*/
openInstallSuccessModal() {
$.confirm( {
title: wpforms_builder.done,
content: wpforms_builder.icon_choices.strings.install_success_content,
icon: 'fa fa-check-circle',
type: 'green',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
if ( app.iconChoices.cache.toggle ) {
app.iconChoices.cache.toggle.prop( 'checked', true );
const fieldId = app.iconChoices.cache.toggle.parents( '.wpforms-field-option-row' ).data( 'field-id' );
const $imageChoices = $builder.find( `#wpforms-field-option-${ fieldId }-choices_images` );
// Turn Image Choice off, if needed, without triggering change event.
if ( $imageChoices.is( ':checked' ) ) {
$imageChoices.prop( 'checked', false );
}
}
wpforms_builder.exit_url = window.location.href;
app.formSave( true );
},
},
},
onOpen() {
if ( app.iconChoices.cache.toggle ) {
const fieldId = app.iconChoices.cache.toggle.parents( '.wpforms-field-option-row-choices_icons' ).data( 'field-id' );
$builder.find( `#wpforms-field-option-${ fieldId }-input_columns` ).val( 'inline' );
}
app.iconChoices.cache.previousModal.close();
},
} );
},
/**
* Open a modal on icon library installation failure.
*
* @since 1.7.9
*
* @param {Object} errorData Unsuccessful ajax JSON response or jqXHR object.
*/
openInstallErrorModal( errorData ) {
$.confirm( {
title: wpforms_builder.uh_oh,
content: wpforms_builder.icon_choices.strings.install_error_content,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
if ( app.iconChoices.cache.toggle ) {
app.iconChoices.cache.toggle.prop( 'checked', false );
} else {
app.formSaveError();
}
},
},
},
onOpen() {
wpf.debug( errorData );
app.iconChoices.cache.previousModal.close();
},
onDestroy() {
// Clean up the cache, we're done.
delete app.iconChoices.cache.previousModal;
delete app.iconChoices.cache.toggle;
},
} );
},
/**
* Extend jquery-confirm plugin with support of max-height for the content area.
*
* @since 1.7.9
*/
extendJqueryConfirm() {
// Extend a method of global instance.
// noinspection JSUnusedGlobalSymbols
window.Jconfirm.prototype._updateContentMaxHeight = function() {
// noinspection JSUnresolvedReference
const height =
$( window ).height() -
( this.$jconfirmBox.outerHeight() - this.$contentPane.outerHeight() ) -
( this.offsetTop + this.offsetBottom );
// Custom property, if set via jquery-confirm options.
// noinspection JSUnresolvedReference
const maxHeight = this.contentMaxHeight || height;
// noinspection JSUnresolvedReference
this.$contentPane.css( {
'max-height': Math.min( maxHeight, height ) + 'px',
} );
};
},
/**
* Open Icon Picker modal.
*
* @since 1.7.9
*/
openIconPickerModal() {
const $this = $( this );
const data = {
fieldId: $this.parents( '.wpforms-field-option-row' ).data( 'field-id' ),
choiceId: $this.parent().data( 'key' ),
selectedIcon: $this.find( '.source-icon' ).val(),
selectedIconStyle: $this.find( '.source-icon-style' ).val(),
};
const title = `
${ wpforms_builder.icon_choices.strings.icon_picker_title }
${ wpforms_builder.icon_choices.strings.icon_picker_description }
`;
const content = `
`;
$.confirm( {
title,
titleClass: 'wpforms-icon-picker-title',
content,
icon: false,
closeIcon: true,
type: 'orange',
backgroundDismiss: true,
boxWidth: 800,
contentMaxHeight: 368, // Custom property, see app.iconChoices.extendJqueryConfirm().
smoothContent: false,
buttons: false,
onOpenBefore() {
// Add custom classes to target various elements.
this.$body.addClass( 'wpforms-icon-picker-jconfirm-box' );
this.$contentPane.addClass( 'wpforms-icon-picker-jconfirm-content-pane' );
},
onContentReady() {
/** @type {Modal} */
const modal = this;
// Initialize the list of icons with List.js and display the 1st page.
app.iconChoices.initIconsList( data );
// Focus the search input.
modal.$title.find( '.search' ).focus();
// Listen for clicks on icons to select them.
modal.$content.find( '.wpforms-icon-picker-icons' ).on( 'click', 'li', function() {
app.iconChoices.selectIcon( modal, $( this ) );
} );
},
} );
},
/**
* Initialize List.js in the Icon Selector modal on demand and cache it.
*
* @since 1.7.9
*
* @param {Object} data Source option data - field and choice IDs, selected icon name and style.
*/
initIconsList( data ) {
const options = {
valueNames: [ 'name' ],
listClass: 'wpforms-icon-picker-icons',
page: wpforms_builder.icon_choices.icons_per_page,
pagination: {
paginationClass: 'wpforms-icon-picker-pagination',
},
item( values ) {
const maybeSelectedClass = ( values.icon === data.selectedIcon && values.style === data.selectedIconStyle ) ? 'class="selected"' : '';
return `
${ values.icon }
`;
},
indexAsync: true,
};
// Initialize List.js instance.
const iconsList = new List( 'wpforms-icon-picker-icons', options, wpforms_builder.icon_choices.icons );
// Initialize infinite scroll pagination on the list instance.
app.iconChoices.infiniteScrollPagination( iconsList );
// Bind search to custom input.
$( '#wpforms-icon-picker-search' ).on( 'keyup', function() {
// Custom partial match search.
iconsList.search( $( this ).val(), [ 'name' ], function( searchString ) {
for ( let index = 0, length = iconsList.items.length; index < length; index++ ) {
iconsList.items[ index ].found = ( new RegExp( searchString ) ).test( iconsList.items[ index ].values().icon );
}
} );
} );
// Show a "nothing found" message if the search returned no results.
iconsList.on( 'searchComplete', function() {
const $element = $( '.wpforms-icon-picker-not-found' );
$element.html( $element.data( 'message' ).replace( '{keyword}', $( '#wpforms-icon-picker-search' ).val() ) );
$element.toggleClass( 'wpforms-hidden', ! _.isEmpty( iconsList.matchingItems ) );
} );
},
/**
* Handle infinite scroll on the list of icons.
*
* @since 1.7.9
*
* @param {Object} list List.js instance.
*/
infiniteScrollPagination( list ) {
let page = 1;
const options = {
root: document.querySelector( '.wpforms-icon-picker-jconfirm-content-pane' ),
rootMargin: '600px', // 5 rows of icons. Formula: 20 + ( (96 + 20) * rows ).
};
const observer = new IntersectionObserver( function( entries ) {
if ( ! entries[ 0 ].isIntersecting ) {
return;
}
page++;
list.show( 0, page * wpforms_builder.icon_choices.icons_per_page );
}, options );
observer.observe( document.querySelector( '.wpforms-icon-picker-pagination' ) );
},
/**
* When an icon is selected, update the choice and the field preview.
*
* @since 1.7.9
*
* @param {Object} modal Current jQuery Confirm modal instance.
* @param {jQuery} $this The list item (icon) that was clicked.
*/
selectIcon( modal, $this ) {
const fieldId = $this.parent().data( 'field-id' );
const choiceId = $this.parent().data( 'choice-id' );
const icon = $this.data( 'icon' );
const iconStyle = $this.data( 'icon-style' );
const $choice = $( `#wpforms-field-option-row-${ fieldId }-choices ul li[data-key=${ choiceId }]` );
const fieldType = $( `#wpforms-field-option-row-${ fieldId }-choices ul` ).data( 'field-type' );
$this.addClass( 'selected' );
$this.siblings( '.selected' ).removeClass( 'selected' );
$choice.find( '.wpforms-icon-select span' ).text( icon );
$choice.find( '.wpforms-icon-select .ic-fa-preview' ).removeClass().addClass( `ic-fa-preview ic-fa-${ iconStyle } ic-fa-${ icon }` );
$choice.find( '.wpforms-icon-select .source-icon' ).val( icon );
$choice.find( '.wpforms-icon-select .source-icon-style' ).val( iconStyle );
app.fieldChoiceUpdate( fieldType, fieldId );
modal.close();
},
},
//--------------------------------------------------------------------//
// Alerts (notices).
//--------------------------------------------------------------------//
/**
* Click on the Dismiss notice button.
*
* @since 1.6.7
*/
dismissNotice() {
$builder.on( 'click', '.wpforms-alert-field-not-available .wpforms-dismiss-button', function( e ) {
e.preventDefault();
const $button = $( this ),
$alert = $button.closest( '.wpforms-alert' ),
fieldId = $button.data( 'field-id' );
$alert.addClass( 'out' );
setTimeout( function() {
$alert.remove();
}, 250 );
if ( fieldId ) {
$( '#wpforms-field-option-' + fieldId ).remove();
}
} );
},
//--------------------------------------------------------------------//
// Other functions.
//--------------------------------------------------------------------//
/**
* Trim long form titles.
*
* @since 1.0.0
*/
trimFormTitle() {
const $title = $( '.wpforms-center-form-name' );
if ( $title.text().length > 38 ) {
const shortTitle = $title.text().trim().substring( 0, 38 ).split( ' ' ).slice( 0, -1 ).join( ' ' ) + '...';
$title.text( shortTitle );
}
},
/**
* Load or refresh the color picker.
*
* @since 1.2.1
* @since 1.7.9 Added default value support.
* @since 1.9.7 Added `$context` and `options` parameters.
*
* @param {jQuery|null} $context Container to search for color picker elements.
* @param {Object|null} options Color picker options.
*/
loadColorPickers( $context = null, options = null ) {
$context = $context || $builder;
options = options || {};
$context.find( '.wpforms-color-picker' ).each( function() {
const $input = $( this );
const $minicolors = $input.closest( '.minicolors' );
// Skip the focused (active) color picker.
if ( $minicolors.hasClass( 'minicolors-focus' ) && options.skipFocused ) {
return;
}
// If it appears to be already initialized, reset. This is necessary when duplicating fields with color pickers.
if ( $input.hasClass( 'minicolors-input' ) ) {
$input.minicolors( 'destroy' );
}
const pickerOptions = {
defaultValue: $input.data( 'fallback-color' ) || '',
...options,
};
$input.minicolors( pickerOptions );
} );
},
/**
* Get a valid color value from the color picker or a default one.
*
* @since 1.7.9
*
* @param {Object} $colorPicker Current field.
*
* @return {string} Always valid color value.
*/
getValidColorPickerValue( $colorPicker ) {
const color = $colorPicker.minicolors( 'value' );
// jQuery MiniColors returns a "black" RGB object if the color value is invalid.
const isInvalid = _.isEqual( $colorPicker.minicolors( 'rgbObject' ), { r: 0, g: 0, b: 0 } );
const isBlack = _.includes( [ '#000', '#000000' ], color );
// If default value isn't provided via the data attribute, use black.
const defaultValue = $colorPicker.data( 'fallback-color' ) || '#000000';
return isInvalid && ! isBlack ? defaultValue : color;
},
/**
* Hotkeys:
* Ctrl+H - Help.
* Ctrl+P - Preview.
* Ctrl+B - Embed.
* Ctrl+E - Entries.
* Ctrl+S - Save.
* Ctrl+Q - Exit.
* Ctrl+/ - Keyboard Shortcuts modal.
* Ctrl+F - Focus search fields input.
* Ctrl+T - Toggle sidebar (Alt+S on Windows and Linux).
*
* @since 1.2.4
*/
builderHotkeys() {
$( document ).on( 'keydown', function( e ) { // eslint-disable-line complexity
// Toggle sidebar on Alt+S (on Windows and Linux).
if ( ( browser.isLinux || browser.isWindows ) && e.altKey && e.keyCode === 83 ) {
$( elements.$sidebarToggle, $builder ).trigger( 'click' );
return;
}
if ( ! e.ctrlKey ) {
return;
}
switch ( e.keyCode ) {
case 72: // Open the Help screen on Ctrl+H.
$( elements.$helpButton, $builder ).trigger( 'click' );
break;
case 80: // Open the Form Preview tab on Ctrl+P.
window.open( wpforms_builder.preview_url );
break;
case 66: // Trigger the Embed modal on Ctrl+B.
$( elements.$embedButton, $builder ).trigger( 'click' );
break;
case 69: // Open the Entries tab on Ctrl+E.
window.open( wpforms_builder.entries_url );
break;
case 83: // Trigger the Builder save on Ctrl+S.
$( elements.$saveButton, $builder ).trigger( 'click' );
break;
case 81: // Trigger the Exit on Ctrl+Q.
$( elements.$exitButton, $builder ).trigger( 'click' );
break;
case 191: // Keyboard shortcuts modal on Ctrl+/.
app.openKeyboardShortcutsModal();
break;
case 84: // Toggle sidebar on Ctrl+T.
$( elements.$sidebarToggle, $builder ).trigger( 'click' );
break;
case 70: // Focus search fields input on Ctrl+F.
elements.$addFieldsTab.trigger( 'click' );
elements.$fieldsSidebar.scrollTop( 0 );
elements.$searchInput.focus();
break;
default:
return;
}
return false;
} );
},
/**
* Open Keyboard Shortcuts modal.
*
* @since 1.6.9
*/
openKeyboardShortcutsModal() {
// Close the already opened instance.
if ( $( '.wpforms-builder-keyboard-shortcuts' ).length ) {
jconfirm.instances[ jconfirm.instances.length - 1 ].close();
return;
}
$.alert( {
title: wpforms_builder.shortcuts_modal_title,
content: wpforms_builder.shortcuts_modal_msg + wp.template( 'wpforms-builder-keyboard-shortcuts' )(),
icon: 'fa fa-keyboard-o',
type: 'blue',
boxWidth: '550px',
smoothContent: false,
buttons: {
confirm: {
text: wpforms_builder.close,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
onOpenBefore() {
this.$body.addClass( 'wpforms-builder-keyboard-shortcuts' );
// Dynamically change the shortcut key documentation on Windows/Linux.
if ( browser.isLinux || browser.isWindows ) {
this.$body.find( '.shortcut-key.shortcut-key-ctrl-t' ).html( '
alts' );
}
},
} );
},
/**
* Register JS templates for various elements.
*
* @since 1.4.8
*/
registerTemplates() {
if ( typeof WPForms === 'undefined' ) {
return;
}
WPForms.Admin.Builder.Templates.add( [
'wpforms-builder-confirmations-message-field',
'wpforms-builder-conditional-logic-toggle-field',
] );
},
/**
* Exit builder.
*
* @since 1.5.7
* @since 1.7.8 Deprecated.
*/
exitBack() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.exitBack()" has been deprecated.' );
},
/**
* Update select field placeholder.
*
* Updates the select field placeholder to be "--- Select Choice".
* First checks if the field has required to be toggled on and if this is not a multiple selection field.
* Does not update the placeholder if it is already set.
*
* @since 1.9.6
*
* @param {number} id Field id.
* @param {jQuery} $preview Field preview.
*/
onUpdateSelectPlaceholder( id, $preview ) { // eslint-disable-line complexity
if (
! [ 'select', 'payment-select' ].includes( $preview.data( 'field-type' ) ) ||
! $preview.hasClass( 'required' ) ||
$( `#wpforms-field-option-${ id }-multiple` ).prop( 'checked' )
) {
return;
}
// Check if this field has a preselected default value.
if ( app.dropdownField.helpers.hasDefaults( id ) ) {
return;
}
app.updateSelectPlaceholder( id );
},
/**
* Update the selected placeholder if it does not have value already.
*
* @since 1.9.6
*
* @param {number} fieldId Field id.
*/
updateSelectPlaceholder( fieldId ) {
const $placeholder = $( `#wpforms-field-option-${ fieldId }-placeholder` );
if ( ! $placeholder.val() ) {
$placeholder.val( wpforms_builder.select_choice ).trigger( 'input' );
}
},
/**
* Acts when user deselects default choice on dropdown.
*
* @since 1.9.6
*
* @param {number} fieldId Field id.
*/
maybeUpdateRequiredPlaceholder( fieldId ) {
const isRequired = $( `#wpforms-field-option-${ fieldId }-required` ).is( ':checked' );
if ( ! isRequired ) {
return;
}
app.updateSelectPlaceholder( fieldId );
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
WPFormsBuilder.init();