Files
root b6df4dbb92 Snapshot: MLS sync fixes, image refresh, plugin/theme updates
MLS plugin fixes from this session:
- Fix silent insert failures: location column NOT NULL was rejecting wpdb->insert calls,
  causing ~18k new properties since Dec 2025 to be lost. Inserts now build raw SQL
  with ST_PointFromText so the spatial column is populated atomically.
- Auto-refresh expired media URLs in MLS_Media_Handler::fetch_and_cache(), guarded by
  a property-level GET_LOCK so concurrent fetches share one API refresh.
- Normalize WP_Error to null in mls_get_property_image() so callers can rely on the
  documented string|null contract.
- Support comma-separated property_type filters in MLS_Query and MLS_Cluster so the
  homepage "View All Commercial" link (?property_type=Commercial+Sale,Land,Farm)
  actually filters correctly.
- Incremental sync now looks back 10 minutes past the latest modification timestamp
  as a safety margin against missed records.
- Smart sync exits silently (info-level, not warning) when a full sync is in progress.

Operational:
- New cron: weekly full sync Sundays at 3 AM (/usr/local/bin/mls-full-sync).
- New cron: hourly 2GB cap on mls-thumbnails/ and cache/transformed-images/
  (/usr/local/bin/mls-image-cache-cap).
- Logrotate config for wp-content/debug.log (2-day retention, daily rotation,
  delaycompress).

Repo policy:
- CLAUDE.md updated with explicit "commit everything except build artifacts" policy.
- .gitignore: untrack runtime image caches and debug.log rotations.

Other modifications in this snapshot are pre-existing in-flight theme/plugin/db_content_updates work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:32:23 +00:00

9816 lines
306 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* global wpforms_builder, wpf, jconfirm, wpforms_panel_switch, Choices, WPForms */
/* global WPFormsFormEmbedWizard, wpCookies, tinyMCE, WPFormsUtils, List, wpforms_preset_choices */
// noinspection TypeScriptUMDGlobal
/**
* @param window.Jconfirm.prototype._updateContentMaxHeight
* @param wpforms_builder.allow_deny_lists_intersect
* @param wpforms_builder.allow_only_email_fields
* @param wpforms_builder.allow_only_one_email
* @param wpforms_builder.are_you_sure_to_close
* @param wpforms_builder.bulk_add_button
* @param wpforms_builder.bulk_add_heading
* @param wpforms_builder.bulk_add_hide
* @param wpforms_builder.bulk_add_placeholder
* @param wpforms_builder.bulk_add_presets_show
* @param wpforms_builder.bulk_add_show
* @param wpforms_builder.choices_icons
* @param wpforms_builder.choices_images
* @param wpforms_builder.date_select_day
* @param wpforms_builder.date_select_month
* @param wpforms_builder.delete_choice_confirm
* @param wpforms_builder.duplicate_confirm
* @param wpforms_builder.duplicate_copy
* @param wpforms_builder.dynamic_choices.empty_message
* @param wpforms_builder.dynamic_choices.limit_message
* @param wpforms_builder.empty_email_address
* @param wpforms_builder.empty_label
* @param wpforms_builder.entry_preview_default_notice
* @param wpforms_builder.entry_preview_require_page_break
* @param wpforms_builder.entry_preview_require_previous_button
* @param wpforms_builder.error_choice
* @param wpforms_builder.error_contact_support
* @param wpforms_builder.error_number_slider_increment
* @param wpforms_builder.error_save_form
* @param wpforms_builder.error_save_form_forbidden
* @param wpforms_builder.exit_confirm
* @param wpforms_builder.field_locked_no_delete_msg
* @param wpforms_builder.field_locked_no_duplicate_msg
* @param wpforms_builder.file_upload.preview_hint
* @param wpforms_builder.file_upload.preview_title_plural
* @param wpforms_builder.file_upload.preview_title_plural_camera
* @param wpforms_builder.file_upload.preview_title_single
* @param wpforms_builder.file_upload.preview_title_single_camera
* @param wpforms_builder.icon_choices.choice_empty_label_tpl
* @param wpforms_builder.icon_choices.default_color
* @param wpforms_builder.icon_choices.default_icon
* @param wpforms_builder.icon_choices.default_icon_style
* @param wpforms_builder.icon_choices.delete_confirm
* @param wpforms_builder.icon_choices.field_locked
* @param wpforms_builder.icon_choices.icons_per_page
* @param wpforms_builder.icon_choices.is_active
* @param wpforms_builder.icon_choices.is_installed
* @param wpforms_builder.icon_choices.strings.icon_picker_description
* @param wpforms_builder.icon_choices.strings.icon_picker_not_found
* @param wpforms_builder.icon_choices.strings.icon_picker_search_placeholder
* @param wpforms_builder.icon_choices.strings.icon_picker_title
* @param wpforms_builder.icon_choices.strings.install_content
* @param wpforms_builder.icon_choices.strings.install_error_content
* @param wpforms_builder.icon_choices.strings.install_prompt_content
* @param wpforms_builder.icon_choices.strings.install_success_content
* @param wpforms_builder.icon_choices.strings.install_title
* @param wpforms_builder.icon_choices.strings.reinstall_prompt_content
* @param wpforms_builder.layout_selector_column
* @param wpforms_builder.layout_selector_hide
* @param wpforms_builder.layout_selector_layout
* @param wpforms_builder.layout_selector_show
* @param wpforms_builder.notification_by_status_enable_alert
* @param wpforms_builder.notification_by_status_switch_alert
* @param wpforms_builder.number_slider_error_valid_default_value
* @param wpforms_builder.payment_choice_empty_label_tpl
* @param wpforms_builder.preview_url
* @param wpforms_builder.pro
* @param wpforms_builder.redirect_url_field_error
* @param wpforms_builder.restricted_default_email
* @param wpforms_builder.restricted_rules
* @param wpforms_builder.revision_update_confirm
* @param wpforms_builder.save_exit
* @param wpforms_builder.scrollbars_css_url
* @param wpforms_builder.select_choice
* @param wpforms_builder.shortcuts_modal_msg
* @param wpforms_builder.shortcuts_modal_title
* @param wpforms_builder.smart_tags_disabled_for_confirmations
* @param wpforms_builder.smart_tags_dropdown_mce_icon
* @param wpforms_builder.smart_tags_hide
* @param wpforms_builder.smart_tags_show
* @param wpforms_builder.something_went_wrong
* @param wpforms_builder.template_modal_msg
* @param wpforms_builder.template_modal_title
* @param wpforms_builder.upload_image_button
* @param wpforms_builder.upload_image_extensions
* @param wpforms_builder.upload_image_extensions_error
* @param wpforms_builder.upload_image_remove
* @param wpforms_builder.upload_image_title
* @param wpforms_builder.wpforms_builder.bulk_add_presets_hide
*/
/* noinspection JSUnusedLocalSymbols */
/* eslint-disable no-unused-expressions, no-shadow */
// noinspection ES6ConvertVarToLetConst
var WPFormsBuilder = window.WPFormsBuilder || ( function( document, window, $ ) { // eslint-disable-line no-var
let s,
$builder;
const elements = {},
browser = {};
/**
* Whether to show the close confirmation dialog or not.
*
* @since 1.6.0
*
* @type {boolean}
*/
let closeConfirmation = true;
/**
* A field is adding.
*
* @since 1.7.1
*
* @type {boolean}
*/
let adding = false;
/**
* Preview tab.
*
* @since 1.9.4
*
* @type {Window|null}
*/
let previewTab = null;
// noinspection JSUnusedGlobalSymbols
const app = {
/**
* @typedef {Object} JQ
* @property {(selector: string) => JQ} closest Closest function.
* @property {(prop: string) => string} css Css function.
* @property {(prop: string) => string} data Data function.
* @property {() => JQ} fadeIn FadeIn function.
* @property {(selector: string) => JQ} find Find function.
* @property {(text: string) => undefined} insertAtCaret InsertAtCaret function.
* @property {(selector: string) => JQ} next Next function.
* @property {() => JQ} slideDown SlideDown function.
* @property {() => JQ} slideToggle SlideToggle function.
* @property {() => JQ} slideUp SlideUp function.
* @property {() => JQ} stop Stop function.
*/
/**
* @param {(selector: string) => JQ} $ The jQuery-compatible function.
*/
/**
* @typedef {Object} jqXHR
* @property {(callback: (data: any, textStatus: string, jqXHR: jqXHR) => void) => jqXHR} done Done function.
*/
/**
* @typedef {Object} Options
* @property {string|number} position Position.
*/
/**
* @typedef {Object} Modal
* @property {jQuery} $title Title.
* @property {jQuery} $content Content.
*/
/* eslint-disable camelcase */
settings: {
spinner: '<i class="wpforms-loading-spinner"></i>',
spinnerInline: '<i class="wpforms-loading-spinner wpforms-loading-inline"></i>',
tinymceDefaults: {
tinymce: { toolbar1: 'bold,italic,underline,blockquote,strikethrough,bullist,numlist,alignleft,aligncenter,alignright,undo,redo,link' },
quicktags: true,
},
pagebreakTop: false,
pagebreakBottom: false,
upload_img_modal: false,
choicesLimit: 20, // Choices limit for fields different from Dropdown.
choicesLimitLong: 250, // Choices limit for Dropdown field.
},
/**
* Start the engine.
*
* @since 1.0.0
*/
init() {
const that = this;
wpforms_panel_switch = true;
s = this.settings;
// Document ready.
$( app.ready );
// Page load.
$( window ).on( 'load', function() {
// In the case of jQuery 3.+, we need to wait for a ready event first.
if ( typeof $.ready.then === 'function' ) {
$.ready.then( app.load );
} else {
app.load();
}
} );
$( window ).on( 'beforeunload', function() {
if ( ! that.formIsSaved() && closeConfirmation ) {
return wpforms_builder.are_you_sure_to_close;
}
} );
},
/**
* Page load.
*
* @since 1.0.0
* @since 1.7.9 Added `wpformsBuilderReady` hook.
*
* @return {false|void} False if default event is prevented.
*/
load() {
// Trigger initial save for new forms.
if ( wpf.getQueryString( 'newform' ) ) {
app.formSave( false );
}
const panel = $( '#wpforms-panels-toggle .active' ).data( 'panel' );
// Render form preview on the Revisions panel if the panel is active.
if ( panel === 'revisions' ) {
app.updateRevisionPreview();
}
// Allow callbacks to prevent making Form Builder ready...
const event = WPFormsUtils.triggerEvent( $builder, 'wpformsBuilderReady' );
// ...by triggering `event.preventDefault()`.
if ( event.isDefaultPrevented() ) {
return false;
}
// Hide the loading overlay and make the Form Builder ready to use.
app.hideLoadingOverlay();
app.determineActiveSections();
// Confirmations' initial setup.
app.confirmationsSetup();
WPFormsUtils.triggerEvent( $builder, 'wpformsBuilderConfirmationsReady' );
app.loadMsWinCSS();
// Maybe display informational modal.
// noinspection JSUnresolvedReference, EqualityComparisonWithCoercionJS
if ( wpforms_builder.template_modal_display == '1' && 'fields' === wpf.getQueryString( 'view' ) ) { // eslint-disable-line
$.alert( {
title: wpforms_builder.template_modal_title,
content: wpforms_builder.template_modal_msg,
icon: 'fa fa-info-circle',
type: 'blue',
buttons: {
confirm: {
text: wpforms_builder.close,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
}
wpf._updateFormState();
},
/**
* Init elements cache.
*
* @since 1.9.2
*/
initElementsCache() {
// Cache builder element.
$builder = $( '#wpforms-builder' );
browser.isWindows = /Win/.test( navigator.userAgent );
browser.isLinux = /Linux/.test( navigator.userAgent );
browser.isMac = /Mac/.test( navigator.userAgent );
// Action buttons.
elements.$helpButton = $( '.js-wpforms-help' );
elements.$previewButton = $( '#wpforms-preview-btn' );
elements.$embedButton = $( '#wpforms-embed' );
elements.$saveButton = $( '#wpforms-save' );
elements.$exitButton = $( '#wpforms-exit' );
// Cache other elements.
elements.$noFieldsOptions = $( '#wpforms-panel-fields .wpforms-no-fields-holder .no-fields' );
elements.$noFieldsPreview = $( '#wpforms-panel-fields .wpforms-no-fields-holder .no-fields-preview' );
elements.$formPreview = $( '#wpforms-panel-fields .wpforms-preview-wrap' );
elements.$revisionPreview = $( '#wpforms-panel-revisions .wpforms-panel-content' );
elements.defaultEmailSelector = '.wpforms-field-option-email .wpforms-field-option-row-default_value input';
elements.$defaultEmail = $( elements.defaultEmailSelector );
elements.$focusOutTarget = null;
elements.$nextFieldId = $( '#wpforms-field-id' );
elements.$addFieldsTab = $( '#add-fields a' );
elements.$fieldOptions = $( '#wpforms-field-options' );
elements.$fieldsPreviewWrap = $( '#wpforms-panel-fields .wpforms-panel-content-wrap' );
elements.$sortableFieldsWrap = $( '#wpforms-panel-fields .wpforms-field-wrap' );
elements.$addFieldsButtons = $( '.wpforms-add-fields-button' ).not( '.not-draggable' ).not( '.warning-modal' ).not( '.education-modal' );
elements.$fieldsSidebar = $( '#wpforms-panel-fields .wpforms-add-fields' );
elements.$searchInput = $( '#wpforms-search-fields-input' );
elements.$sidebarToggle = $( '.wpforms-panels .wpforms-panel-sidebar-content .wpforms-panel-sidebar-toggle' );
},
/**
* Document ready.
*
* @since 1.0.0
*/
ready() { // eslint-disable-line max-lines-per-function
if ( app.isVisitedViaBackButton() ) {
location.reload();
return;
}
app.initElementsCache();
// Add `_wp_http_referer` to the data of every AJAX request.
$.ajaxSetup( {
data: {
// eslint-disable-next-line camelcase
_wp_http_referer: wpf.updateQueryString( '_wp_http_referer', null ),
},
} );
// Remove Embed button if builder opened in the popup.
if ( app.isBuilderInPopup() ) {
elements.$embedButton.remove();
elements.$previewButton.addClass( 'wpforms-alone' );
}
// Bind all actions.
app.bindUIActions();
// Setup/cache some vars not available before
s.formID = $( '#wpforms-builder-form' ).data( 'id' );
s.pagebreakTop = $( '.wpforms-pagebreak-top' ).length;
s.pagebreakBottom = $( '.wpforms-pagebreak-bottom' ).length;
// Disable implicit submission for every form inside the builder.
// All form values are managed by JS and should not be submitted by pressing Enter.
$builder.on( 'keypress', '#wpforms-builder-form :input:not(textarea)', function( e ) {
if ( e.keyCode === 13 ) {
e.preventDefault();
}
} );
app.loadEntryPreviewFields();
// Drag and drop sortable elements.
app.fieldChoiceSortable( 'select' );
app.fieldChoiceSortable( 'radio' );
app.fieldChoiceSortable( 'checkbox' );
app.fieldChoiceSortable( 'payment-multiple' );
app.fieldChoiceSortable( 'payment-checkbox' );
app.fieldChoiceSortable( 'payment-select' );
// Set field group visibility.
$( '.wpforms-add-fields-group' ).each( function() {
app.fieldGroupToggle( $( this ), 'load' );
} );
app.registerTemplates();
// Trim long form titles.
app.trimFormTitle();
// Load Tooltips.
wpf.initTooltips();
// Load Color Pickers.
app.loadColorPickers();
// Hide/Show CAPTCHA in form.
app.captchaToggle();
// Notification settings.
app.notificationToggle();
app.notificationsByStatusAlerts();
// Secret builder hotkeys.
app.builderHotkeys();
// jquery-confirm defaults.
jconfirm.defaults = {
closeIcon: false,
backgroundDismiss: false,
escapeKey: true,
animationBounce: 1,
useBootstrap: false,
theme: 'modern',
boxWidth: '400px',
animateFromElement: false,
content: wpforms_builder.something_went_wrong,
};
app.dropdownField.init();
app.iconChoices.init();
app.disabledFields.init();
app.checkEmptyDynamicChoices();
app.initSomeFieldOptions();
app.dismissNotice();
wpf.initializeChoicesEventHandlers();
},
checkEmptyDynamicChoices() {
const choices = wpf.orders.choices || {};
if ( ! Object.keys( choices ).length ) {
return;
}
wpf.orders.fields.forEach( function( fieldId ) {
const isDynamic = app.dropdownField.helpers.isDynamicChoices( fieldId );
if ( ! isDynamic ) {
return;
}
const $fieldPreview = $( '#wpforms-field-' + fieldId );
const type = app.dropdownField.helpers.getDynamicChoicesOptionType( fieldId );
const source = app.dropdownField.helpers.getDynamicChoicesOptionSource( fieldId );
const isModern = app.dropdownField.helpers.isDynamicChoicesOptionModern( fieldId );
let isEmpty = isModern
? $fieldPreview.find( '.has-no-choices' ).length
: $fieldPreview.find( '.primary-input option:not(.placeholder), .primary-input li' ).length === 0;
if ( isModern && ! isEmpty ) {
const placeholder = $( '#wpforms-field-option-' + fieldId + '-placeholder' ).val();
const choices = app.dropdownField.helpers.getInitialChoices( fieldId );
isEmpty = choices.length === 1 && choices[ 0 ].label === placeholder && choices[ 0 ].placeholder === true;
}
if ( isEmpty ) {
app.emptyChoicesNotice( fieldId, source, type );
}
} );
},
/**
* Load Microsoft Windows specific stylesheet.
*
* @since 1.6.8
*/
loadMsWinCSS() {
// Detect OS & browsers.
if ( browser.isMac ) {
return;
}
// language=HTML
$( '<link>' )
.appendTo( 'head' )
.attr( {
type: 'text/css',
rel: 'stylesheet',
href: wpforms_builder.scrollbars_css_url,
} );
},
/**
* Builder was visited via the back button in the browser.
*
* @since 1.6.5
*
* @return {boolean} True if the builder was visited via back button in browser.
*/
isVisitedViaBackButton() {
if ( ! performance ) {
return false;
}
let isVisitedViaBackButton = false;
performance.getEntriesByType( 'navigation' ).forEach( function( nav ) {
if ( nav.type === 'back_forward' ) {
isVisitedViaBackButton = true;
}
} );
return isVisitedViaBackButton;
},
/**
* Remove loading overlay.
*
* @since 1.6.8
*/
hideLoadingOverlay() {
const $overlay = $( '#wpforms-builder-overlay' );
$overlay.addClass( 'fade-out' );
setTimeout( function() {
$overlay.hide();
}, 250 );
},
/**
* Show loading overlay.
*
* @since 1.6.8
*/
showLoadingOverlay() {
const $overlay = $( '#wpforms-builder-overlay' );
$overlay.removeClass( 'fade-out' );
$overlay.show();
},
/**
* Initialize some fields options controls.
*
* @since 1.6.3
*/
initSomeFieldOptions() {
// Show a toggled options group.
app.toggleAllOptionGroups( $builder );
// Date/Time field Date type option.
$builder.find( '.wpforms-field-option-row-date .type select' ).trigger( 'change' );
// Read-Only + Required field options init.
$builder.find( '.wpforms-field-option-row-read_only input:checked' ).trigger( 'change' );
},
/**
* Dropdown field component.
*
* @since 1.6.1
*/
dropdownField: {
/**
* Field configuration.
*
* @since 1.6.1
*/
config: {
modernClass: 'choicesjs-select',
args: {
searchEnabled: false,
searchChoices: false,
renderChoiceLimit: 1,
shouldSort: false,
callbackOnInit() {
const $element = $( this.containerOuter.element ),
$previewSelect = $element.closest( '.wpforms-field' ).find( 'select' );
// Turn off disabled styles.
if ( $element.hasClass( 'is-disabled' ) ) {
$element.removeClass( 'is-disabled' );
}
// Disable instances on the preview panel.
if ( $previewSelect.is( '[readonly]' ) ) {
this.disable();
$previewSelect.prop( 'disabled', false );
}
if ( this.passedElement.element.multiple ) {
// Hide a placeholder if the field has selected choices.
if ( this.getValue( true ).length ) {
$( this.input.element ).addClass( 'choices__input--hidden' );
}
}
// Decode allowed HTML entities for choices.
$element.find( '.choices__item--selectable' ).each( function() {
const $choice = $( this );
const text = wpf.decodeAllowedHTMLEntities( $choice.text() );
$choice.text( text );
} );
},
},
},
/**
* Initialization for a field component.
*
* @since 1.6.1
*/
init() {
// Choices.js init.
$builder.find( '.' + app.dropdownField.config.modernClass ).each( function() {
app.dropdownField.events.choicesInit( $( this ) );
} );
// Multiple option.
$builder.on(
'change',
'.wpforms-field-option-select .wpforms-field-option-row-multiple input',
app.dropdownField.events.multiple
);
// Style option.
$builder.on(
'change',
'.wpforms-field-option-select .wpforms-field-option-row-style select, .wpforms-field-option-payment-select .wpforms-field-option-row-style select',
app.dropdownField.events.applyStyle
);
// Add the ability to close the drop-down menu.
$builder.on( 'click', '.choices', function( e ) {
const $choices = $( this ),
choicesObj = $choices.find( 'select' ).data( 'choicesjs' );
if (
choicesObj &&
$choices.hasClass( 'is-open' ) &&
e.target.classList.contains( 'choices__inner' )
) {
choicesObj.hideDropdown();
}
} );
},
/**
* Field events.
*
* @since 1.6.1
*/
events: {
/**
* Load Choices.js library.
*
* @since 1.6.1
*
* @param {Object} $element jQuery element selector.
*/
choicesInit( $element ) {
const useAjax = $element.data( 'choicesjs-use-ajax' ) === 1;
let instance;
if ( $element.data( 'choicesjs-callback-fn' ) === 'select_pages' ) {
instance = WPForms.Admin.Builder.WPFormsChoicesJS.setup(
$element[ 0 ],
app.dropdownField.config.args,
{
action: 'wpforms_ajax_search_pages_for_dropdown',
nonce: useAjax ? wpforms_builder.nonce : null,
}
);
} else {
instance = new Choices( $element[ 0 ], app.dropdownField.config.args );
}
app.dropdownField.helpers.setInstance( $element, instance );
app.dropdownField.helpers.addPlaceholderChoice( $element, instance );
$element.closest( '.choices' ).toggleClass( 'wpforms-hidden', ! instance.config.choices.length );
},
/**
* Multiple option callback.
*
* @since 1.6.1
*
* @param {Object} event Event object.
*/
multiple( event ) {
const fieldId = $( this ).closest( '.wpforms-field-option-row-multiple' ).data().fieldId,
$primary = app.dropdownField.helpers.getPrimarySelector( fieldId ),
$optionChoicesItems = $( `#wpforms-field-option-row-${ fieldId }-choices input.default` ),
$placeholder = $primary.find( '.placeholder' ),
isDynamicChoices = app.dropdownField.helpers.isDynamicChoices( fieldId ),
isMultiple = event.target.checked,
choicesType = isMultiple ? 'checkbox' : 'radio';
// Add/remove a `multiple` attribute.
$primary.prop( 'multiple', isMultiple );
// Change a `Choices` fields type:
// checkbox - needed for multiple selection
// radio - needed for single selection
$optionChoicesItems.prop( 'type', choicesType );
// Dynamic Choices doesn't have default choices (selected options) - make all as unselected.
if ( isDynamicChoices ) {
$primary.find( 'option:selected' ).prop( 'selected', false );
}
// Gets default choices.
const selectedChoices = $optionChoicesItems.filter( ':checked' );
if ( ! isMultiple && selectedChoices.length ) {
// Uncheck all choices.
$optionChoicesItems.prop( 'checked', false );
// For a single selection, we can choose only one.
$( selectedChoices.get( 0 ) ).prop( 'checked', true );
}
// Toggle selection for a placeholder option based on a select type.
if ( $placeholder.length ) {
$placeholder.prop( 'selected', ! isMultiple );
}
// Update a primary field.
app.dropdownField.helpers.update( fieldId, isDynamicChoices );
},
/**
* Apply a style to <select> - modern or classic.
*
* @since 1.6.1
*/
applyStyle() {
const $field = $( this ),
fieldId = $field.closest( '.wpforms-field-option-row-style' ).data().fieldId,
fieldVal = $field.val();
if ( 'modern' === fieldVal ) {
app.dropdownField.helpers.convertClassicToModern( fieldId );
} else {
app.dropdownField.helpers.convertModernToClassic( fieldId );
}
},
},
helpers: {
/**
* Get Modern select options and prepare them for the Classic <select>.
*
* @since 1.6.1
*
* @param {string} fieldId Field ID.
*/
convertModernToClassic: ( fieldId ) => {
const $primary = app.dropdownField.helpers.getPrimarySelector( fieldId ),
isDynamicChoices = app.dropdownField.helpers.isDynamicChoices( fieldId ),
instance = app.dropdownField.helpers.getInstance( $primary ),
$sidebarChoices = $( '#wpforms-field-option-row-' + fieldId + '-choices' ),
$sidebarList = $sidebarChoices.find( '.choices-list' ),
elementsCount = $sidebarList.find( 'li' ).length;
if ( instance && typeof instance.destroy === 'function' ) {
// Destroy the instance of Choices.js.
instance.destroy();
// Update a placeholder.
app.dropdownField.helpers.updatePlaceholderChoice( instance, fieldId );
}
// Update choices.
if ( ! isDynamicChoices ) {
app.fieldChoiceUpdate( 'select', fieldId, elementsCount );
}
},
/**
* Get initial choices.
*
* @since 1.8.2
*
* @param {string} fieldId Field ID.
*
* @return {Object} Choices.
*/
getInitialChoices( fieldId ) {
const $primary = app.dropdownField.helpers.getPrimarySelector( fieldId ),
instance = app.dropdownField.helpers.getInstance( $primary );
return instance.config.choices;
},
/**
* Convert a Classic to Modern style selector.
*
* @since 1.6.1
*
* @param {string} fieldId Field ID.
*/
convertClassicToModern( fieldId ) {
const $primary = app.dropdownField.helpers.getPrimarySelector( fieldId ),
isDynamicChoices = app.dropdownField.helpers.isDynamicChoices( fieldId );
// Update choices.
if ( ! isDynamicChoices ) {
app.fieldChoiceUpdate( 'select', fieldId );
}
// Call a Choices.js initialization.
app.dropdownField.events.choicesInit( $primary );
},
/**
* Update a primary field.
*
* @since 1.6.1
*
* @param {string} fieldId Field ID.
* @param {boolean} isDynamicChoices True if `Dynamic Choices` is turned on.
*/
update( fieldId, isDynamicChoices ) {
const $primary = app.dropdownField.helpers.getPrimarySelector( fieldId );
if ( app.dropdownField.helpers.isModernSelect( $primary ) ) {
// If we had a `Modern` `select` before, then we need to make re-init - destroy() + init().
app.dropdownField.helpers.convertModernToClassic( fieldId );
if ( ! isDynamicChoices ) {
app.dropdownField.events.choicesInit( $primary );
}
} else if ( ! isDynamicChoices ) {
// Update choices.
app.fieldChoiceUpdate( 'select', fieldId );
}
},
/**
* Add a new choice to behave like a placeholder.
*
* @since 1.6.1
*
* @param {Object} $jquerySelector jQuery primary selector.
* @param {Object} instance The instance of Choices.js.
*
* @return {boolean} False if a fake placeholder wasn't added.
*/
addPlaceholderChoice( $jquerySelector, instance ) { // eslint-disable-line complexity
const wpFormsField = $jquerySelector.closest( '.wpforms-field' );
if ( wpFormsField.length <= 0 ) {
return false;
}
const fieldId = wpFormsField.data().fieldId;
let hasDefaults = app.dropdownField.helpers.hasDefaults( fieldId );
if ( app.dropdownField.helpers.isDynamicChoices( fieldId ) ) {
hasDefaults = false;
}
// Already has a placeholder.
if ( false !== app.dropdownField.helpers.searchPlaceholderChoice( instance ) ) {
return false;
}
// No choices.
if ( ! instance.config.choices.length ) {
return false;
}
const placeholder = $( '#wpforms-field-option-' + fieldId + '-placeholder' ).val(),
sanitizedPlaceholder = wpf.decodeAllowedHTMLEntities( placeholder ),
isMultiple = $( instance.passedElement.element ).prop( 'multiple' ),
selected = ! ( isMultiple || hasDefaults );
// Add a new choice as a placeholder.
instance.setChoices(
[
{ value: '', label: sanitizedPlaceholder, selected, placeholder: true },
],
'value',
'label',
false
);
// Additional case for multiple select.
if ( isMultiple ) {
$( instance.input.element ).prop( 'placeholder', placeholder );
}
return true;
},
/**
* Search a choice-placeholder item.
*
* @since 1.6.1
*
* @param {Object} instance The instance of Choices.js.
*
* @return {boolean|object} False if a field doesn't have a choice-placeholder.
* Otherwise - return choice item.
*/
searchPlaceholderChoice( instance ) {
let find = false;
instance.config.choices.forEach( function( item, i ) {
if ( 'undefined' !== typeof item.placeholder && true === item.placeholder ) {
find = {
key: i,
item,
};
return false;
}
} );
return find;
},
/**
* Add/update a placeholder.
*
* @since 1.6.1
*
* @param {Object} instance The instance of Choices.js.
* @param {string} fieldId Field ID.
*/
updatePlaceholderChoice( instance, fieldId ) {
const $primary = $( instance.passedElement.element ),
placeholderValue = wpf.sanitizeHTML( $( '#wpforms-field-option-' + fieldId + '-placeholder' ).val() ),
placeholderChoice = app.dropdownField.helpers.searchPlaceholderChoice( instance );
let $placeholderOption = {};
// Get an option with placeholder.
// Note: `.placeholder` class is skipped when calling Choices.js destroy() method.
if ( 'object' === typeof placeholderChoice ) {
$placeholderOption = $( $primary.find( 'option' ).get( placeholderChoice.key ) );
}
// We have a placeholder and need to update the UI with it.
if ( '' !== placeholderValue ) {
if ( ! $.isEmptyObject( $placeholderOption ) && $placeholderOption.length ) {
// Update a placeholder option.
$placeholderOption
.addClass( 'placeholder' )
.text( placeholderValue );
} else {
// Add a placeholder option.
$primary.prepend( '<option value="" class="placeholder">' + placeholderValue + '</option>' );
}
} else if ( $placeholderOption.length ) {
// Remove the placeholder as it's empty.
$placeholderOption.remove();
}
},
/**
* Is it a `Modern` style dropdown field?
*
* @since 1.6.1
*
* @param {Object} $jquerySelector jQuery primary selector.
*
* @return {boolean} True if it's a `Modern` style select, false otherwise.
*/
isModernSelect( $jquerySelector ) {
const instance = app.dropdownField.helpers.getInstance( $jquerySelector );
if ( 'object' !== typeof instance ) {
return false;
}
if ( $.isEmptyObject( instance ) ) {
return false;
}
return instance.initialised;
},
/**
* Save an instance of Choices.js.
*
* @since 1.6.1
*
* @param {Object} $jquerySelector jQuery primary selector.
* @param {Object} instance The instance of Choices.js.
*/
setInstance( $jquerySelector, instance ) {
$jquerySelector.data( 'choicesjs', instance );
},
/**
* Retrieve an instance of Choices.js.
*
* @since 1.6.1
*
* @param {Object} $jquerySelector jQuery primary selector.
*
* @return {Object} The instance of Choices.js.
*/
getInstance( $jquerySelector ) {
return $jquerySelector.data( 'choicesjs' );
},
/**
* Get the Dynamic Choices option field.
*
* @since 1.8.2
*
* @param {string|number} fieldId Field ID.
*
* @return {HTMLElement|boolean} False if a field doesn't have a `Dynamic Choices` option.
* Otherwise - return option field.
*/
getDynamicChoicesOption( fieldId ) {
const $fieldOption = $( '#wpforms-field-option-' + fieldId + '-dynamic_choices' );
if ( ! $fieldOption.length ) {
return false;
}
return $fieldOption;
},
/**
* Is `Dynamic Choices` used?
*
* @since 1.6.1
*
* @param {string|number} fieldId Field ID.
*
* @return {boolean} True if a `Dynamic Choices` are active, false otherwise.
*/
isDynamicChoices( fieldId ) {
const $fieldOption = app.dropdownField.helpers.getDynamicChoicesOption( fieldId );
if ( ! $fieldOption.length ) {
return false;
}
return '' !== $fieldOption.val();
},
/**
* Whether the `Dynamic Choices` option type is `Modern`?
*
* @since 1.8.2
*
* @param {string|number} fieldId Field ID.
* @return {boolean} True if a `Dynamic Choices` option type is `Modern`, false otherwise.
*/
isDynamicChoicesOptionModern( fieldId ) {
const $fieldOption = $( '#wpforms-field-option-' + fieldId + '-style' );
if ( ! $fieldOption.length ) {
return false;
}
return $fieldOption.val() === 'modern';
},
/**
* Get a Dynamic Choices option type.
*
* @since 1.8.2
*
* @param {string|number} fieldId Field ID.
*
* @return {string|boolean} False if a field doesn't have a `Dynamic Choices` option.
* Otherwise - return an option type.
*/
getDynamicChoicesOptionType( fieldId ) {
const $fieldOption = app.dropdownField.helpers.getDynamicChoicesOption( fieldId );
if ( ! $fieldOption.length ) {
return false;
}
return $fieldOption.val();
},
/**
* Get a Dynamic Choices option source.
*
* @since 1.8.2
*
* @param {string|number} fieldId Field ID.
*
* @return {string|boolean} False if a field doesn't have a `Dynamic Choices` option.
* Otherwise - return an option source.
*/
getDynamicChoicesOptionSource( fieldId ) {
const type = app.dropdownField.helpers.getDynamicChoicesOptionType( fieldId );
const $fieldOption = $( '#wpforms-field-option-' + fieldId + '-dynamic_' + type );
if ( ! $fieldOption.length ) {
return false;
}
return $fieldOption.find( 'option:selected' ).text();
},
/**
* Is a field having default choices?
*
* @since 1.6.1
*
* @param {string} fieldId Field ID.
*
* @return {boolean} True if a field has default choices.
*/
hasDefaults( fieldId ) {
const $choicesList = $( `#wpforms-field-option-row-${ fieldId }-choices .choices-list` );
return !! $choicesList.find( 'input.default:checked' ).length;
},
/**
* Retrieve a jQuery selector for the Primary field.
*
* @since 1.6.1
*
* @param {string} fieldId Field ID.
*
* @return {Object} jQuery primary selector.
*/
getPrimarySelector( fieldId ) {
return $( '#wpforms-field-' + fieldId + ' .primary-input' );
},
},
},
/**
* Add number slider events listeners.
*
* @since 1.5.7
*
* @param {Object} $builder JQuery object.
*/
numberSliderEvents( $builder ) {
// Minimum update.
$builder.on(
'focusout',
'.wpforms-field-option-row-min_max .wpforms-input-row .wpforms-number-slider-min',
app.fieldNumberSliderUpdateMin
);
// Maximum update.
$builder.on(
'focusout',
'.wpforms-field-option-row-min_max .wpforms-input-row .wpforms-number-slider-max',
app.fieldNumberSliderUpdateMax
);
// Change default input value.
$builder.on(
'input',
'.wpforms-number-slider-default-value',
_.debounce( app.changeNumberSliderDefaultValue, 500 )
);
// Change the default input value if it's empty.
$builder.on(
'focusout',
'.wpforms-number-slider-default-value',
app.changeNumberSliderEmptyDefaultValue
);
// Trigger the input event on default value input to check if it's valid.
$builder.find( '.wpforms-number-slider-default-value' ).trigger( 'input' );
// Change step value.
$builder.on(
'input',
'.wpforms-number-slider-step',
_.debounce( app.changeNumberSliderStep, 500 )
);
// Check step value.
$builder.on(
'focusout',
'.wpforms-number-slider-step',
app.checkNumberSliderStep
);
// Change value display.
$builder.on(
'input',
'.wpforms-number-slider-value-display',
_.debounce( app.changeNumberSliderValueDisplay, 500 )
);
// Change min value.
$builder.on(
'input',
'.wpforms-number-slider-min',
_.debounce( app.changeNumberSliderMin, 500 )
);
// Change max value.
$builder.on(
'input',
'.wpforms-number-slider-max',
_.debounce( app.changeNumberSliderMax, 500 )
);
},
/**
* Change number slider min option.
*
* @since 1.5.7
*
* @param {Object} event Input event.
*/
changeNumberSliderMin( event ) {
const value = parseFloat( event.target.value );
if ( isNaN( value ) ) {
return;
}
const fieldID = $( event.target ).parents( '.wpforms-field-option-row' ).data( 'fieldId' );
app.updateNumberSliderDefaultValueAttr( fieldID, event.target.value, 'min' );
},
/**
* Change number slider max option.
*
* @since 1.5.7
*
* @param {Object} event Input event.
*/
changeNumberSliderMax( event ) {
const value = parseFloat( event.target.value );
if ( isNaN( value ) ) {
return;
}
const fieldID = $( event.target ).parents( '.wpforms-field-option-row' ).data( 'fieldId' );
app.updateNumberSliderDefaultValueAttr( fieldID, event.target.value, 'max' )
.updateNumberSliderStepValueMaxAttr( fieldID, event.target.value );
},
/**
* Change number slider value display option.
*
* @since 1.5.7
*
* @param {Object} event Input event.
*/
changeNumberSliderValueDisplay( event ) {
const str = event.target.value;
const fieldID = $( event.target ).parents( '.wpforms-field-option-row' ).data( 'fieldId' );
const defaultValue = document.getElementById( 'wpforms-field-option-' + fieldID + '-default_value' );
if ( defaultValue ) {
app.updateNumberSliderHintStr( fieldID, str )
.updateNumberSliderHint( fieldID, defaultValue.value );
}
},
/**
* Change the number slider step option.
*
* @since 1.5.7
*
* @param {Object} event Input event.
*/
changeNumberSliderStep( event ) {
const $el = $( this );
const value = parseFloat( $el.val() );
if ( isNaN( value ) ) {
return;
}
if ( value <= 0 ) {
return;
}
const $options = $( $el ).closest( '.wpforms-field-option' );
const max = parseFloat( $options.find( '.wpforms-number-slider-max' ).val() );
const min = parseFloat( $options.find( '.wpforms-number-slider-min' ).val() );
const maxStep = ( max - min ).toFixed( 2 );
if ( value > maxStep ) {
event.target.value = maxStep;
$el.trigger( 'input' );
return;
}
const fieldID = $( event.target ).parents( '.wpforms-field-option-row' ).data( 'fieldId' );
const defaultValue = $( '#wpforms-field-option-' + fieldID + '-default_value' ).val();
app.checkMultiplicitySliderDefaultValue( fieldID, defaultValue, value, min, max )
.updateNumberSliderAttr( fieldID, value, 'step' )
.updateNumberSliderDefaultValueAttr( fieldID, value, 'step' );
},
/**
* Check multiplicity of a slider default value.
*
* @since 1.8.4
*
* @param {string} fieldId Field ID.
* @param {number} value Default value.
* @param {number} step Step value.
* @param {number} min Min value.
* @param {number} max Max value.
*
* @return {Object} App instance.
*/
checkMultiplicitySliderDefaultValue( fieldId, value, step, min = 0, max = 0 ) {
const $printSelector = $( `#wpforms-field-option-row-${ fieldId }-default_value` );
value = parseFloat( value );
if ( ( value - min ) % step === 0 ) {
app.removeNotice( $printSelector );
return this;
}
const {
closestSmallerMultiple,
closestLargerMultiple,
} = this.calculateClosestMultiples( value, step, min, max );
const formatNumber = ( num ) => ( num % 1 === 0 ? num.toString() : num.toFixed( 2 ) );
const normalizedValue = formatNumber( value );
const normalizedSmaller = formatNumber( closestSmallerMultiple );
const normalizedLarger = formatNumber( closestLargerMultiple );
if ( normalizedSmaller === normalizedLarger ||
normalizedSmaller === normalizedValue ||
normalizedLarger === normalizedValue
) {
app.removeNotice( $printSelector );
return this;
}
const updatedMessage = wpforms_builder.number_slider_error_valid_default_value
.replace( '{from}', normalizedSmaller )
.replace( '{to}', normalizedLarger );
app.printNotice( updatedMessage, $printSelector );
return this;
},
/**
* Calculate the closest multiples for a value.
*
* @since 1.9.5
*
* @param {number} value Default value.
* @param {number} step Step value.
* @param {number} min Min value.
* @param {number} max Max value.
*
* @return {Object} Closest smaller and larger multiples.
*/
// eslint-disable-next-line complexity
calculateClosestMultiples( value, step, min, max ) {
// Calculate the closest smaller value.
let closestSmallerMultiple = min + ( Math.floor( ( value - min ) / step ) * step );
// Calculate the closest larger value.
let closestLargerMultiple = min + ( Math.ceil( ( value - min ) / step ) * step );
// Handle edge cases where the value is exactly on a step.
if ( value === closestSmallerMultiple && value !== min ) {
closestLargerMultiple = closestSmallerMultiple + step;
}
if ( value === closestLargerMultiple && value !== max ) {
closestSmallerMultiple = closestLargerMultiple - step;
}
// Handle edge cases when the value is min or max
if ( value === min ) {
closestLargerMultiple = min + step;
}
if ( value === max ) {
closestSmallerMultiple = max - step;
}
// Ensure the closest values stay within the min and max bounds.
closestSmallerMultiple = Math.max( closestSmallerMultiple, min );
closestLargerMultiple = Math.min( closestLargerMultiple, max );
return { closestSmallerMultiple, closestLargerMultiple };
},
/**
* Print a notice.
*
* @since 1.8.4
*
* @param {string} message Message to print.
* @param {Object} $printSelector jQuery element selector.
* @param {boolean} wide Wide notice flag, optional, default is false.
*/
printNotice( message, $printSelector, wide = false ) {
if ( $printSelector.length ) {
this.removeNotice( $printSelector );
const wideClass = wide ? 'wpforms-alert-warning-wide' : '';
$printSelector.append( `<div class="wpforms-alert-warning wpforms-alert ${ wideClass }"><p>${ message }</p></div>` );
}
},
/**
* Remove a notice.
*
* @since 1.8.4
*
* @param {Object} $printSelector jQuery element selector.
*/
removeNotice( $printSelector ) {
if ( $printSelector.length && $printSelector.find( '.wpforms-alert' ).length ) {
$printSelector.find( '.wpforms-alert' ).remove();
}
},
/**
* Check the number slider step option.
*
* @since 1.6.2.3
*
* @param {Object} event Focusout event object.
*/
checkNumberSliderStep( event ) {
const value = parseFloat( event.target.value );
if ( ! isNaN( value ) && value > 0 ) {
return;
}
const $input = $( this );
$.confirm( {
title: wpforms_builder.heads_up,
content: wpforms_builder.error_number_slider_increment,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
$input.val( '' ).trigger( 'focus' );
},
},
},
} );
},
/**
* Update number slider default value if it's empty.
*
* @since 1.9.3
*
* @param {Object} event Input event.
*/
changeNumberSliderEmptyDefaultValue( event ) {
const value = parseFloat( event.target.value );
if ( isNaN( value ) ) {
const newValue = parseFloat( event.target.min );
event.target.value = newValue;
const step = parseFloat( event.target.step );
const min = parseFloat( event.target.min );
const max = parseFloat( event.target.max );
const fieldID = $( event.target ).parents( '.wpforms-field-option-row-default_value' ).data( 'fieldId' );
app.checkMultiplicitySliderDefaultValue( fieldID, newValue, step, min, max )
.updateNumberSlider( fieldID, newValue )
.updateNumberSliderHint( fieldID, newValue );
}
},
/**
* Change number slider default value option.
*
* @since 1.5.7
*
* @param {Object} event Input event.
*/
changeNumberSliderDefaultValue( event ) {
const value = parseFloat( event.target.value );
if ( ! isNaN( value ) ) {
const max = parseFloat( event.target.max );
if ( value > max ) {
event.target.value = max;
return;
}
const min = parseFloat( event.target.min );
if ( value < min ) {
event.target.value = min;
return;
}
const step = parseFloat( event.target.step );
const fieldID = $( event.target ).parents( '.wpforms-field-option-row-default_value' ).data( 'fieldId' );
app.checkMultiplicitySliderDefaultValue( fieldID, value, step, min, max )
.updateNumberSlider( fieldID, value )
.updateNumberSliderHint( fieldID, value );
}
},
/**
* Update number slider default value attribute.
*
* @since 1.5.7
*
* @param {number} fieldID Field ID.
* @param {*} newValue Default value attribute.
* @param {*} attr Attribute name.
*
* @return {Object} App instance.
*/
updateNumberSliderDefaultValueAttr( fieldID, newValue, attr ) {
const input = document.getElementById( 'wpforms-field-option-' + fieldID + '-default_value' );
if ( input ) {
const value = parseFloat( input.value );
input.setAttribute( attr, newValue );
newValue = parseFloat( newValue );
if ( 'max' === attr && value > newValue ) {
input.value = newValue;
}
if ( 'min' === attr && value < newValue ) {
input.value = newValue;
}
}
return this;
},
/**
* Update number slider value.
*
* @since 1.5.7
*
* @param {number} fieldID Field ID.
* @param {string} value Number slider value.
*
* @return {Object} App instance.
*/
updateNumberSlider( fieldID, value ) {
const numberSlider = document.getElementById( 'wpforms-number-slider-' + fieldID );
if ( numberSlider ) {
numberSlider.value = value;
}
return this;
},
/**
* Update number slider attribute.
*
* @since 1.5.7
*
* @param {number} fieldID Field ID.
* @param {any} value Attribute value.
* @param {*} attr Attribute name.
*
* @return {Object} App instance.
*/
updateNumberSliderAttr( fieldID, value, attr ) {
const numberSlider = document.getElementById( 'wpforms-number-slider-' + fieldID );
if ( numberSlider ) {
numberSlider.setAttribute( attr, value );
}
return this;
},
/**
* Update number slider hint string.
*
* @since 1.5.7
*
* @param {number} fieldID Field ID.
* @param {string} str Hint string.
*
* @return {Object} App instance.
*/
updateNumberSliderHintStr( fieldID, str ) {
const hint = document.getElementById( 'wpforms-number-slider-hint-' + fieldID );
if ( hint ) {
hint.dataset.hint = str;
}
return this;
},
/**
* Update number slider Hint value.
*
* @since 1.5.7
*
* @param {number} fieldID Field ID.
* @param {string} value Hint value.
*
* @return {Object} App instance.
*/
updateNumberSliderHint( fieldID, value ) {
const hint = document.getElementById( 'wpforms-number-slider-hint-' + fieldID );
if ( hint ) {
hint.innerHTML = wpf.sanitizeHTML( hint.dataset.hint ).replaceAll( '{value}', '<b>' + value + '</b>' );
}
return this;
},
/**
* Update min attribute.
*
* @since 1.5.7
*
* @param {Object} event Input event.
*/
fieldNumberSliderUpdateMin( event ) {
const current = parseFloat( event.target.value );
if ( isNaN( current ) ) {
return;
}
const $options = $( event.target ).parents( '.wpforms-field-option-row-min_max' );
const max = parseFloat( $options.find( '.wpforms-number-slider-max' ).val() );
if ( max <= current ) {
event.preventDefault();
this.value = max;
return;
}
const fieldId = $options.data( 'field-id' );
const numberSlider = $builder.find( '#wpforms-field-' + fieldId + ' input[type="range"]' );
numberSlider.attr( 'min', current );
},
/**
* Update max attribute.
*
* @since 1.5.7
*
* @param {Object} event Input event.
*/
fieldNumberSliderUpdateMax( event ) {
const current = parseFloat( event.target.value );
if ( isNaN( current ) ) {
return;
}
const $options = $( event.target ).parents( '.wpforms-field-option-row-min_max' );
const min = parseFloat( $options.find( '.wpforms-number-slider-min' ).val() );
if ( min >= current ) {
event.preventDefault();
this.value = min;
return;
}
const fieldId = $options.data( 'field-id' );
const numberSlider = $builder.find( '#wpforms-field-' + fieldId + ' input[type="range"]' );
numberSlider.attr( 'max', current );
},
/**
* Update max attribute for step value.
*
* @since 1.5.7
*
* @param {number} fieldID Field ID.
* @param {*} newValue Default value attribute.
*
* @return {Object} App instance.
*/
updateNumberSliderStepValueMaxAttr( fieldID, newValue ) {
const input = document.getElementById( 'wpforms-field-option-' + fieldID + '-step' );
if ( input ) {
const value = parseFloat( input.value );
input.setAttribute( 'max', newValue );
newValue = parseFloat( newValue );
if ( value > newValue ) {
input.value = newValue;
$( input ).trigger( 'input' );
}
}
return this;
},
/**
* Update upload selector.
*
* @since 1.5.6
*
* @param {Object} target Changed :input.
*/
fieldFileUploadPreviewUpdate( target ) {
/**
* @type {jQuery}
*/
const $options = $( target ).parents( '.wpforms-field-option-file-upload' );
const fieldId = $options.data( 'field-id' );
const styleOption = $options.find( '#wpforms-field-option-' + fieldId + '-style' ).val();
const $maxFileNumberRow = $options.find( '#wpforms-field-option-row-' + fieldId + '-max_file_number' );
const maxFileNumber = parseInt( $maxFileNumberRow.find( 'input' ).val(), 10 );
const isCameraEnabled = $options.find( '#wpforms-field-option-' + fieldId + '-camera_enabled' ).is( ':checked' );
const $preview = $( '#wpforms-field-' + fieldId );
const classicPreview = '.wpforms-file-upload-builder-classic';
const modernPreview = '.wpforms-file-upload-builder-modern';
if ( styleOption === 'classic' ) {
$( classicPreview, $preview ).removeClass( 'wpforms-hide' );
$( modernPreview, $preview ).addClass( 'wpforms-hide' );
$maxFileNumberRow.addClass( 'wpforms-hidden' );
$( $preview ).find( '.wpforms-file-upload-capture-camera-classic' ).toggleClass( 'wpforms-hidden', ! isCameraEnabled );
} else {
// Change hint and title.
if ( maxFileNumber > 1 ) {
const titleText = isCameraEnabled
? wpforms_builder.file_upload.preview_title_plural_camera
: wpforms_builder.file_upload.preview_title_plural;
$preview
.find( '.modern-title' )
.html( titleText );
$preview
.find( '.modern-hint' )
.text( wpforms_builder.file_upload.preview_hint.replace( '{maxFileNumber}', maxFileNumber ) )
.removeClass( 'wpforms-hide' );
} else {
const titleText = isCameraEnabled
? wpforms_builder.file_upload.preview_title_single_camera
: wpforms_builder.file_upload.preview_title_single;
$preview
.find( '.modern-title' )
.html( titleText );
$preview
.find( '.modern-hint' )
.text( wpforms_builder.file_upload.preview_hint.replace( '{maxFileNumber}', 1 ) )
.addClass( 'wpforms-hide' );
}
// Display the preview.
$( classicPreview, $preview ).addClass( 'wpforms-hide' );
$( modernPreview, $preview ).removeClass( 'wpforms-hide' );
$maxFileNumberRow.removeClass( 'wpforms-hidden' );
}
},
/**
* Update limit controls by changing the checkbox.
*
* @since 1.5.6
*
* @param {number} id Field id.
* @param {boolean} checked Whether an option is checked or not.
*/
updateTextFieldsLimitControls( id, checked ) {
if ( ! checked ) {
$( '#wpforms-field-option-row-' + id + '-limit_controls' ).addClass( 'wpforms-hide' );
} else {
$( '#wpforms-field-option-row-' + id + '-limit_controls' ).removeClass( 'wpforms-hide' );
}
},
/**
* Update disabling today's date controls by changing the checkbox.
*
* @since 1.8.9.4
*
* @param {number} id Field id.
* @param {boolean} checked Whether an option is checked or not.
*/
updateDisableTodaysDateControls( id, checked ) {
$( `#wpforms-field-option-row-${ id }-date_disable_todays_date` )
.toggleClass( 'wpforms-hide', ! checked );
},
/**
* Toggles the visibility of the password eye icon.
*
* @since 1.9.8
*/
togglePasswordEyeIcon() {
const $this = $( this );
const fieldId = $this.closest( '.wpforms-field-option-row-password-visibility' ).data( 'field-id' );
const $fieldPreview = $( `#wpforms-field-${ fieldId }` );
$fieldPreview.find( '.wpforms-confirm' ).toggleClass( 'wpforms-field-password-visibility-enabled', $this.is( ':checked' ) );
},
/**
* Update Password Strength controls by changing the checkbox.
*
* @since 1.6.7
*
* @param {number} id Field id.
* @param {boolean} checked Whether an option is checked or not.
*/
updatePasswordStrengthControls( id, checked ) {
const $strengthControls = $( '#wpforms-field-option-row-' + id + '-password-strength-level' );
if ( checked ) {
$strengthControls.removeClass( 'wpforms-hidden' );
} else {
$strengthControls.addClass( 'wpforms-hidden' );
}
},
/**
* Update Rich Text media controls by changing the checkbox.
*
* @since 1.7.0
*/
updateRichTextMediaFieldsLimitControls() {
const $this = $( this ),
fieldId = $this.closest( '.wpforms-field-option-row-media_enabled' ).data( 'field-id' ),
$mediaControls = $( '#wpforms-field-option-row-' + fieldId + '-media_controls' ),
$toolbar = $( '#wpforms-field-' + fieldId + ' .wpforms-richtext-wrap .mce-toolbar-grp' );
if ( ! $this.is( ':checked' ) ) {
$mediaControls.hide();
$toolbar.removeClass( 'wpforms-field-richtext-media-enabled' );
} else {
$mediaControls.show();
$toolbar.addClass( 'wpforms-field-richtext-media-enabled' );
}
},
/**
* Update the Rich Text style preview by changing select.
*
* @since 1.7.0
*/
updateRichTextStylePreview() {
const $this = $( this ),
fieldId = $this.closest( '.wpforms-field-option-row-style' ).data( 'field-id' ),
$toolbar = $( '#wpforms-field-' + fieldId + ' .wpforms-richtext-wrap .mce-toolbar-grp' );
$toolbar.toggleClass( 'wpforms-field-richtext-toolbar-basic', $this.val() !== 'full' );
},
/**
* Element bindings.
*
* @since 1.0.0
*/
bindUIActions() {
// General Panels.
app.bindUIActionsPanels();
// Fields Panel.
app.bindUIActionsFields();
// Settings Panel.
app.bindUIActionsSettings();
// Revisions Panel.
app.bindUIActionsRevisions();
// Save and Exit.
app.bindUIActionsSaveExit();
// General/ global.
app.bindUIActionsGeneral();
// Preview actions.
app.bindUIActionsPreview();
},
/**
* Bind UI actions for the preview tab.
*
* @since 1.9.4
*/
bindUIActionsPreview() {
// Open the preview tab or focus on it if it's already opened.
elements.$previewButton.on( 'click', function( e ) {
e.preventDefault();
const previewUrl = $( this ).attr( 'href' );
// Try to check if the preview tab is still open and from the same origin
let isSameOrigin = false;
if ( previewTab && ! previewTab.closed ) {
try {
// This will throw an error if cross-origin
isSameOrigin = previewTab.location.href.includes( 'wpforms_form_preview' );
} catch ( error ) {
// Cross-origin access error, we can't access the location
isSameOrigin = false;
}
}
if ( isSameOrigin ) {
previewTab.focus();
} else {
previewTab = window.open( previewUrl, '_blank' );
}
} );
// Reload the preview tab after saving the form.
$builder.on( 'wpformsSaved', function() {
if ( previewTab && ! previewTab.closed ) {
try {
// This will throw an error if cross-origin
if ( previewTab.location.href.includes( 'wpforms_form_preview' ) ) {
previewTab.location.reload();
}
} catch ( error ) {
// Silently handle cross-origin errors
// We can't access or reload cross-origin tabs
}
}
} );
},
//--------------------------------------------------------------------//
// General Panels
//--------------------------------------------------------------------//
/**
* Element bindings for general panel tasks.
*
* @since 1.0.0
*/
bindUIActionsPanels() {
// Panel switching.
$builder.on( 'click', '#wpforms-panels-toggle button, .wpforms-panel-switch', function( e ) {
e.preventDefault();
app.panelSwitch( $( this ).data( 'panel' ) );
} );
// Panel sections switching.
$builder.on( 'click', '.wpforms-panel .wpforms-panel-sidebar-section', function( e ) {
app.panelSectionSwitch( this, e );
} );
// Panel tab switching.
$builder.on( 'click', '.wpforms-panel .wpforms-panel-content .wpforms-panel-content-section-tabs-list-item', function( e ) {
app.panelSectionTabSwitch( this, e );
} );
// Panel sidebar toggle.
$builder.on( 'click', '.wpforms-panels .wpforms-panel-sidebar-content .wpforms-panel-sidebar-toggle', function() {
$( this ).parent().toggleClass( 'wpforms-panel-sidebar-closed' );
} );
},
/**
* Switch Panels.
*
* @since 1.0.0
* @since 1.5.9 Added `wpformsPanelSwitched` trigger.
*
* @param {string} panel Panel slug.
*
* @return {void|boolean} Void or false.
*/
panelSwitch( panel ) {
const $panel = $( '#wpforms-panel-' + panel );
if ( ! $panel.hasClass( 'active' ) ) {
const event = WPFormsUtils.triggerEvent( $builder, 'wpformsPanelSwitch', [ panel ] );
// Allow callbacks on `wpformsPanelSwitch` to cancel panel switching by triggering `event.preventDefault()`.
if ( event.isDefaultPrevented() || ! wpforms_panel_switch ) {
return false;
}
$( '#wpforms-panels-toggle' ).find( 'button' ).removeClass( 'active' );
$( '.wpforms-panel' ).removeClass( 'active' );
$( '.wpforms-panel-' + panel + '-button' ).addClass( 'active' );
$panel.addClass( 'active' );
history.replaceState( {}, null, wpf.updateQueryString( 'view', panel ) );
// Update the active section parameter in the URL.
let section;
const activeSectionElement = $panel.find( '.active' );
if ( activeSectionElement.length && activeSectionElement.data( 'section' ) !== 'default' ) {
section = activeSectionElement.data( 'section' );
}
history.replaceState( {}, null, wpf.updateQueryString( 'section', section ) );
$builder.trigger( 'wpformsPanelSwitched', [ panel ] );
}
},
/**
* Switch Panel section.
*
* @since 1.0.0
*
* @param {Element} el Element.
* @param {Event} e Event.
*
* @return {boolean|void} False when not switched.
*/
panelSectionSwitch( el, e ) { // eslint-disable-line complexity
if ( e ) {
e.preventDefault();
}
const $this = $( el );
if ( $this.hasClass( 'upgrade-modal' ) || $this.hasClass( 'education-modal' ) ) {
return;
}
const $panel = $this.parent().parent(),
section = $this.data( 'section' ),
$sectionButton = $panel.find( `.wpforms-panel-sidebar-section[data-section="${ section }"]` );
if ( ! $sectionButton.hasClass( 'active' ) ) {
const event = WPFormsUtils.triggerEvent( $builder, 'wpformsPanelSectionSwitch', section );
// Allow callbacks on `wpformsPanelSectionSwitch` to cancel panel section switching by triggering `event.preventDefault()`.
if ( event.isDefaultPrevented() || ! wpforms_panel_switch ) {
return false;
}
const $sectionButtons = $panel.find( '.wpforms-panel-sidebar-section' );
$sectionButtons.removeClass( 'active' );
$sectionButton.addClass( 'active' );
$panel.find( '.wpforms-panel-content-section' ).hide().removeClass( 'active' );
$panel.find( '.wpforms-panel-content-section-' + section ).show().addClass( 'active' );
// Update the active section parameter in the URL.
history.replaceState( {}, null, wpf.updateQueryString( 'section', section ) );
}
},
/**
* Switches panel sections.
*
* @since 1.9.8.6
*
* @param {Element} el Element.
* @param {Event} e Event.
*
* @return {void}
*/
panelSectionTabSwitch( el, e ) {
e.preventDefault();
const $tabToggle = $( el ),
disabledClass = 'wpforms-disabled',
activeToggleClass = 'wpforms-panel-content-section-tabs-list-item-active',
activeTab = $tabToggle.data( 'tab' );
if ( ! activeTab || $tabToggle.hasClass( disabledClass ) || $tabToggle.hasClass( activeToggleClass ) ) {
return;
}
const event = WPFormsUtils.triggerEvent( $builder, 'wpformsPanelSectionTabBeforeSwitch', [ activeTab, $tabToggle ] );
if ( event.isDefaultPrevented() ) {
return;
}
const activeContentClass = 'wpforms-panel-content-section-tabs-content-tab-active',
$tabs = $tabToggle.closest( '.wpforms-panel-content-section-tabs-list' ),
$tabContent = $tabs.closest( '.wpforms-panel-content' ).find( '.wpforms-panel-content-section-tabs-content-tab' );
$tabs.find( '.wpforms-panel-content-section-tabs-list-item' ).removeClass( activeToggleClass );
$tabToggle.addClass( activeToggleClass );
$tabContent.removeClass( activeContentClass );
$tabContent.filter( `[data-tab="${ activeTab }"]` ).addClass( activeContentClass );
WPFormsUtils.triggerEvent( $builder, 'wpformsPanelSectionTabSwitch', [ activeTab, $tabToggle ] );
},
//--------------------------------------------------------------------//
// Setup Panel
//--------------------------------------------------------------------//
/**
* Element bindings for a Setup panel.
*
* @since 1.0.0
* @since 1.6.8 Deprecated.
*
* @deprecated Use `WPForms.Admin.Builder.Setup.events()` instead.
*/
bindUIActionsSetup() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.bindUIActionsSetup()" has been deprecated, please use the new "WPForms.Admin.Builder.Setup.events()" function instead!' );
WPForms.Admin.Builder.Setup.events();
},
/**
* Select template.
*
* @since 1.0.0
* @since 1.6.8 Deprecated.
*
* @deprecated Use `WPForms.Admin.Builder.Setup.selectTemplate()` instead.
*
* @param {Object} el DOM element object.
* @param {Object} e Event object.
*/
templateSelect( el, e ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.templateSelect()" has been deprecated, please use the new "WPForms.Admin.Builder.Setup.selectTemplate()" function instead!' );
WPForms.Admin.Builder.Setup.selectTemplate( e );
},
//--------------------------------------------------------------------//
// Fields Panel
//--------------------------------------------------------------------//
/**
* Element bindings for Fields panel.
*
* @since 1.0.0
*/
bindUIActionsFields() { // eslint-disable-line max-lines-per-function
// Switched to the Fields panel.
$builder.on( 'wpformsPanelSwitched', function( e, panel ) {
if ( panel !== 'fields' ) {
return;
}
// Detect the case when the field Options tab is active, but there is no active field on the preview panel.
if (
$( '#field-options a' ).hasClass( 'active' ) &&
$( '.wpforms-field-wrap .wpforms-field.active' ).length === 0
) {
app.fieldTabToggle( 'field-options' );
}
} );
// Field sidebar tab toggle
$builder.on( 'click', '.wpforms-tab a', function( e ) {
e.preventDefault();
app.fieldTabToggle( $( this ).parent().attr( 'id' ) );
} );
// Field sidebar group toggle
$builder.on( 'click', '.wpforms-add-fields-heading', function( e ) {
e.preventDefault();
app.fieldGroupToggle( $( this ), 'click' );
} );
// Form field preview clicking.
$builder.on( 'click', '.wpforms-field', function( event, extraParameters ) {
if ( app.isFieldPreviewActionsDisabled( this ) ) {
return;
}
// Allow clicking on the "dismiss" button inside the field.
if ( event.target.classList.contains( 'wpforms-dismiss-button' ) ) {
return;
}
// Dismiss the main context menu when it is open.
if ( WPForms.Admin.Builder.ContextMenu && extraParameters !== 'ignore-contextmenu' ) {
WPForms.Admin.Builder.ContextMenu.hideMainContextMenu( event );
}
event.stopPropagation();
app.fieldTabToggle( $( this ).data( 'field-id' ) );
} );
// Prevent interactions with inputs on the preview panel.
$builder.on( 'mousedown click', '.wpforms-field input, .wpforms-field select, .wpforms-field textarea', function( e ) {
e.preventDefault();
this.blur();
} );
// Field delete.
$builder.on( 'click', '.wpforms-field-delete', function( e ) {
e.preventDefault();
e.stopPropagation();
if ( app.isFormPreviewActionsDisabled( this ) ) {
return;
}
if ( WPForms.Admin.Builder.ContextMenu ) {
WPForms.Admin.Builder.ContextMenu.hideMenu();
}
app.fieldDelete( $( this ).parent().data( 'field-id' ) );
} );
// Field duplicate.
$builder.on( 'click', '.wpforms-field-duplicate', function( e ) {
e.preventDefault();
e.stopPropagation();
if ( app.isFormPreviewActionsDisabled( this ) ) {
return;
}
if ( WPForms.Admin.Builder.ContextMenu ) {
WPForms.Admin.Builder.ContextMenu.hideMenu();
}
app.fieldDuplicate( $( this ).parent().data( 'field-id' ) );
} );
// Field add.
$builder.on( 'click', '.wpforms-add-fields-button', function( e ) {
e.preventDefault();
const $field = $( this );
if ( $field.hasClass( 'ui-draggable-disabled' ) ) {
return;
}
const type = $field.data( 'field-type' ),
event = WPFormsUtils.triggerEvent( $builder, 'wpformsBeforeFieldAddOnClick', [ type, $field ] );
// Allow callbacks on `wpformsBeforeFieldAddOnClick` to cancel adding field
// by triggering `event.preventDefault()`.
if ( event.isDefaultPrevented() ) {
return;
}
app.fieldAdd( type, { $sortable: 'default' } );
} );
// New field choices should be sortable
$builder.on( 'wpformsFieldAdd', function( event, id, type ) {
const fieldTypes = [
'select',
'radio',
'checkbox',
'payment-multiple',
'payment-checkbox',
'payment-select',
];
if ( $.inArray( type, fieldTypes ) !== -1 ) {
app.fieldChoiceSortable( type, `#wpforms-field-option-row-${ id }-choices ul` );
}
} );
// Field option tab toggle.
$builder.on( 'wpformsFieldOptionTabToggle', function( e, fieldId ) {
app.fieldLayoutSelectorInit( fieldId );
} );
// Field choice "Add new".
$builder.on( 'click', '.wpforms-field-option-row-choices .add', function( e ) {
app.fieldChoiceAdd( e, $( this ) );
} );
// Field choice "Delete".
$builder.on( 'click', '.wpforms-field-option-row-choices .remove', function( e ) {
app.fieldChoiceDelete( e, $( this ) );
} );
// Field choices' defaults, before change.
$builder.on( 'mousedown', '.wpforms-field-option-row-choices input[type=radio]', function() {
const $this = $( this );
if ( $this.is( ':checked' ) ) {
$this.attr( 'data-checked', '1' );
} else {
$this.attr( 'data-checked', '0' );
}
} );
// Field choices' defaults.
$builder.on( 'click', '.wpforms-field-option-row-choices input[type=radio]', function() {
const $this = $( this ),
list = $this.parent().parent();
$this.parent().parent().find( 'input[type=radio]' ).not( this ).prop( 'checked', false );
if ( $this.attr( 'data-checked' ) === '1' ) {
$this.prop( 'checked', false );
$this.attr( 'data-checked', '0' );
}
app.fieldChoiceUpdate( list.data( 'field-type' ), list.data( 'field-id' ), list.find( 'li' ).length );
} );
// Field choices update preview area.
$builder.on( 'change', '.wpforms-field-option-row-choices input[type=checkbox]', function() {
const list = $( this ).parent().parent();
app.fieldChoiceUpdate( list.data( 'field-type' ), list.data( 'field-id' ), list.find( 'li' ).length );
} );
// Field choices display value toggle.
$builder.on( 'change', '.wpforms-field-option-row-show_values input', function() { // eslint-disable-line max-lines-per-function
const $toggle = $( this );
const $options = $toggle.closest( '.wpforms-field-option' );
const $choicesList = $options.find( '.wpforms-field-option-row-choices .choices-list' );
const fieldId = $choicesList.data( 'field-id' );
const type = $choicesList.data( 'field-type' );
const showValuesOn = $toggle.is( ':checked' );
// Toggle show-values class in the options UI as before.
$choicesList.toggleClass( 'show-values', showValuesOn );
// Refresh preview.
app.fieldChoiceUpdate( type, fieldId );
// If Show Values is enabled, sync the Other input value in preview from the Other choice Value field.
WPForms.Admin.Builder.MultipleChoices.updatePreviewState( fieldId );
} );
// Field choices image toggle.
$builder.on( 'change', '.wpforms-field-option-row-choices_images input', function() {
const $this = $( this ),
$optionRow = $this.closest( '.wpforms-field-option-row' ),
fieldID = $optionRow.data( 'field-id' ),
$fieldOptions = $( '#wpforms-field-option-' + fieldID ),
checked = $this.is( ':checked' ),
type = $fieldOptions.find( '.wpforms-field-option-hidden-type' ).val(),
$iconToggle = $optionRow.siblings( '.wpforms-field-option-row-choices_icons' ).find( 'input' );
// Toggle icon choices off.
if ( checked && $iconToggle.is( ':checked' ) ) {
$iconToggle.prop( 'checked', false ).trigger( 'change' );
}
$optionRow.find( '.wpforms-alert' ).toggleClass( 'wpforms-hidden' );
$fieldOptions.find( '.wpforms-field-option-row-choices ul' ).toggleClass( 'show-images' );
$fieldOptions.find( '.wpforms-field-option-row-choices_images_style' ).toggleClass( 'wpforms-hidden' );
$fieldOptions.find( '.wpforms-field-option-row-dynamic_choices' ).toggleClass( 'wpforms-hidden', checked );
if ( checked ) {
$( '#wpforms-field-option-' + fieldID + '-input_columns' ).val( 'inline' ).trigger( 'change' );
} else {
$( '#wpforms-field-option-' + fieldID + '-input_columns' ).val( '' ).trigger( 'change' );
}
$( `#wpforms-field-option-row-${ fieldID }-choices_images_hide` ).toggleClass( 'wpforms-hidden', ! checked );
app.fieldChoiceUpdate( type, fieldID );
} );
// Field choices image upload add/remove image.
$builder.on( 'wpformsImageUploadAdd wpformsImageUploadRemove', function( event, $this, $container ) {
const $list = $container.closest( '.choices-list' ),
fieldID = $list.data( 'field-id' ),
type = $list.data( 'field-type' );
app.fieldChoiceUpdate( type, fieldID );
} );
// Field choices image style toggle.
$builder.on( 'change', '.wpforms-field-option-row-choices_images_style select', function() {
const fieldID = $( this ).parent().data( 'field-id' ),
type = $( '#wpforms-field-option-' + fieldID ).find( '.wpforms-field-option-hidden-type' ).val();
app.fieldChoiceUpdate( type, fieldID );
} );
// Updates field choices text in almost real time.
$builder.on( 'keyup', '.wpforms-field-option-row-choices input.label, .wpforms-field-option-row-choices input.value', function() {
const $list = $( this ).parent().parent();
app.fieldChoiceUpdate( $list.data( 'field-type' ), $list.data( 'field-id' ) );
} );
// Sanitize field choices text on focus out.
$builder.on( 'focusout', '.wpforms-field-option-row-choices input.label, .wpforms-field-option-row-choices input.value', function() {
const input = $( this );
input.val( wpf.sanitizeHTML( input.val(), wpforms_builder.allowed_label_html_tags ) );
} );
// Field Choices Bulk Add
$builder.on( 'click', '.toggle-bulk-add-display', function( e ) {
e.preventDefault();
app.fieldChoiceBulkAddToggle( this );
} );
$builder.on( 'click', '.toggle-bulk-add-presets', function( e ) {
e.preventDefault();
/**
* @type {JQ|jQuery}
*/
const $presetList = $( this ).closest( '.bulk-add-display' ).find( 'ul' );
if ( $presetList.css( 'display' ) === 'block' ) {
$( this ).text( wpforms_builder.bulk_add_presets_show );
} else {
$( this ).text( wpforms_builder.bulk_add_presets_hide );
}
$presetList.stop().slideToggle();
} );
$builder.on( 'click', '.bulk-add-preset-insert', function( e ) {
e.preventDefault();
/** @type {JQ|jQuery} */
const $this = $( this );
const preset = $this.data( 'preset' ),
$container = $this.closest( '.bulk-add-display' ),
$presetList = $container.find( 'ul' ),
$presetToggle = $container.find( '.toggle-bulk-add-presets' ),
$textarea = $container.find( 'textarea' );
$textarea.val( '' );
$textarea.insertAtCaret( wpforms_preset_choices[ preset ].choices.join( '\n' ) );
$presetToggle.text( wpforms_builder.bulk_add_presets_show );
$presetList.slideUp();
} );
$builder.on( 'click', '.bulk-add-insert', function( e ) {
e.preventDefault();
app.fieldChoiceBulkAddInsert( this );
} );
// Field Options group tabs.
$builder.on( 'click', '.wpforms-field-option-group-toggle:not(.education-modal)', function( e ) {
const event = WPFormsUtils.triggerEvent( $builder, 'wpformsFieldOptionGroupToggle' );
// Allow callbacks on `wpformsFieldOptionGroupToggle` to cancel the tab toggle by triggering `event.preventDefault()`.
if ( event.isDefaultPrevented() ) {
return false;
}
e.preventDefault();
const $group = $( this ).closest( '.wpforms-field-option-group' );
$group.siblings( '.wpforms-field-option-group' ).removeClass( 'active' );
$group.addClass( 'active' );
$builder.trigger( 'wpformsFieldOptionGroupToggled', [ $group ] );
} );
// Display toggle for an Address field hide address line 2 option.
$builder.on( 'change', '.wpforms-field-option-address input.wpforms-subfield-hide', function() {
const $optionRow = $( this ).closest( '.wpforms-field-option-row' ),
id = $optionRow.data( 'field-id' ),
subfield = $optionRow.data( 'subfield' );
$( '#wpforms-field-' + id ).find( '.wpforms-' + subfield ).toggleClass( 'wpforms-hide' );
} );
// Real-time updates for the "Label" field option.
$builder.on( 'input', '.wpforms-field-option-row-label input, .wpforms-field-option-row-name input', function() {
const $this = $( this ),
id = $this.parent().data( 'field-id' ),
$preview = $( '#wpforms-field-' + id ),
type = $preview.data( 'field-type' );
let value = $this.val(),
showEmptyLabel = value.length === 0;
// Do not modify the label of the HTML field.
if ( type === 'html' ) {
showEmptyLabel = false;
}
if ( showEmptyLabel ) {
value = wpforms_builder.empty_label;
}
$preview.toggleClass( 'label_empty', showEmptyLabel ).find( '> .label-title .text' ).text( value );
} );
// Real-time updates for the "Description" field option
$builder.on( 'input', '.wpforms-field-option-row-description textarea', function() {
// noinspection CssUnusedSymbol
const $this = $( this ),
value = wpf.sanitizeHTML( $this.val() ),
id = $this.parent().data( 'field-id' ),
// IIF description is not following other fields structure and needs to be selected separately.
$desc = $(
`#wpforms-field-${ id } > .description,
#wpforms-field-${ id } .format-selected-single > .description,
#wpforms-field-${ id } .wpforms-field-internal-information-row-description`
);
app.updateDescription( $desc, value );
$this.trigger( 'wpformsDescriptionFieldUpdated', { id, descField: $desc, value } );
} );
// Real-time updates the for "Required" field option
$builder.on( 'change', '.wpforms-field-option-row-required input', function() {
const id = $( this ).closest( '.wpforms-field-option-row' ).data( 'field-id' ),
$preview = $( '#wpforms-field-' + id );
$preview.toggleClass( 'required' );
app.onUpdateSelectPlaceholder( id, $preview );
} );
// Real-time updates for a selected default choice option.
$builder.on( 'click', '.choices-list input.default', function() {
const $this = $( this ),
fieldId = $this.closest( '.choices-list' ).data( 'field-id' ),
checked = $this.is( ':checked' );
if ( ! checked ) {
app.maybeUpdateRequiredPlaceholder( fieldId );
}
/**
* Default choice option changing state event.
*
* @since 1.9.8.3
*
* @type {Object} The selected default choice option.
*/
$builder.trigger( 'wpformsChoicesSetDefault', [ this ] );
} );
// Real-time updates for the "Summary" field option
$builder.on( 'change', '.wpforms-field-option-row-summary input', function() {
const $this = $( this ),
id = $this.closest( '.wpforms-field-option-row' ).data( 'field-id' );
$( `#wpforms-field-${ id }` ).toggleClass( 'wpforms-summary-enabled' );
$this.closest( '.wpforms-field-option-group-inner' ).find( '.wpforms-total-summary-alert' ).toggleClass( 'wpforms-hidden' );
} );
// Real-time updates for the "Confirmation" field option
$builder.on( 'change', '.wpforms-field-option-row-confirmation input', function() {
const id = $( this ).closest( '.wpforms-field-option-row' ).data( 'field-id' );
$( '#wpforms-field-' + id ).find( '.wpforms-confirm' ).toggleClass( 'wpforms-confirm-enabled wpforms-confirm-disabled' );
$( '#wpforms-field-option-' + id ).toggleClass( 'wpforms-confirm-enabled wpforms-confirm-disabled' );
} );
// Real-time updates for "Filter" field option
$builder.on( 'change', '.wpforms-field-option-row-filter_type select', function() {
const id = $( this ).parent().data( 'field-id' ),
$toggledField = $( '#wpforms-field-option-' + id );
if ( $( this ).val() ) {
$toggledField.removeClass( 'wpforms-filter-allowlist' );
$toggledField.removeClass( 'wpforms-filter-denylist' );
$toggledField.addClass( 'wpforms-filter-' + $( this ).val() );
} else {
$toggledField.removeClass( 'wpforms-filter-allowlist' );
$toggledField.removeClass( 'wpforms-filter-denylist' );
}
} );
$builder.on( 'focusout', '.wpforms-field-option-row-allowlist textarea,.wpforms-field-option-row-denylist textarea', function() { // eslint-disable-line max-lines-per-function
const $currentField = $( this );
let $current = 'allow';
$currentField.next( '.wpforms-alert' ).remove();
if ( $currentField.val() === '' ) {
return;
}
const $allowField = $( '.wpforms-field-option-row-allowlist textarea' ),
$denyField = $( '.wpforms-field-option-row-denylist textarea' );
if ( $currentField.is( $denyField ) ) {
$current = 'deny';
}
$.get(
wpforms_builder.ajax_url,
{
nonce: wpforms_builder.nonce,
content: JSON.stringify(
{
allow: $allowField.val(),
deny: $denyField.val(),
current: $current,
}
),
action: 'wpforms_sanitize_restricted_rules',
},
function( res ) {
if ( res.success ) {
$currentField.val( res.data.currentField );
const intersect = res.data.intersect;
if ( intersect.length !== 0 ) {
const content = '<p>' + wpforms_builder.allow_deny_lists_intersect + '</p>' +
'<p class="bold">' + intersect + '</p>';
$.alert( {
title: wpforms_builder.heads_up,
content,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
}
const restricted = res.data.restricted || 0;
if ( restricted ) {
$currentField.after( '<div class="wpforms-alert-warning wpforms-alert"><p>' + wpforms_builder.restricted_rules + '</p></div>' );
}
}
}
);
} );
// Save focusout target.
$builder.on( 'focusout', elements.defaultEmailSelector, function() {
elements.$focusOutTarget = $( this );
app.focusOutEvent();
} );
// Real-time updates for the "Size" field option
$builder.on( 'change', '.wpforms-field-option-row-size select', function() {
const $this = $( this ),
value = $this.val(),
id = $this.parent().data( 'field-id' );
$( '#wpforms-field-' + id ).removeClass( 'size-small size-medium size-large' ).addClass( 'size-' + value );
} );
// Real-time updates for the "Placeholder" field option.
$builder.on( 'input', '.wpforms-field-option-row-placeholder input', function() { // eslint-disable-line complexity
const $this = $( this ),
id = $this.parent().data( 'field-id' ),
$preview = $( '#wpforms-field-' + id ),
$primary = $preview.find( '.primary-input' );
let value = wpf.sanitizeHTML( $this.val() );
// Single Item Field - if a placeholder is cleared, set it to "price" placeholder.
if ( $preview.data( 'field-type' ) === 'payment-single' && value === '' ) {
value = $( '#wpforms-field-option-' + id + '-price' ).prop( 'placeholder' );
}
// Set the placeholder value for `input` fields.
if ( ! $primary.is( 'select' ) ) {
$primary.prop( 'placeholder', value );
return;
}
// Modern select style.
if ( app.dropdownField.helpers.isModernSelect( $primary ) ) {
const choiceInstance = app.dropdownField.helpers.getInstance( $primary );
// Additional case for multiple select.
if ( $primary.prop( 'multiple' ) ) {
$( choiceInstance.input.element ).prop( 'placeholder', value );
} else {
choiceInstance.setChoiceByValue( '' );
$primary.closest( '.choices' ).find( '.choices__inner .choices__placeholder' ).text( value );
const isDynamicChoices = $( '#wpforms-field-option-' + id + '-dynamic_choices' ).val();
// We need to re-initialize the modern dropdown to properly determine and update placeholder.
app.dropdownField.helpers.update( id, isDynamicChoices );
}
return;
}
const $placeholder = $primary.find( '.placeholder' );
// Classic select style.
if ( ! value.length && $placeholder.length ) {
$placeholder.remove();
} else {
if ( $placeholder.length ) {
$placeholder.text( value );
} else {
$primary.prepend( '<option value="" class="placeholder">' + value + '</option>' );
}
$primary.find( '.placeholder' ).prop( 'selected', ! $primary.prop( 'multiple' ) );
}
} );
// Real-time updates for the "Confirmation Placeholder" field option
$builder.on( 'input', '.wpforms-field-option-row-confirmation_placeholder input', function() {
const $this = $( this ),
value = $this.val(),
id = $this.parent().data( 'field-id' );
$( '#wpforms-field-' + id ).find( '.secondary-input' ).attr( 'placeholder', value );
} );
// Real-time updates for Date/Time, and Name "Placeholder" field options
$builder.on( 'input', '.wpforms-field-option .format-selected input.placeholder', function() {
const $this = $( this );
const value = $this.val();
const $fieldOptionRow = $this.closest( '.wpforms-field-option-row' );
const id = $fieldOptionRow.data( 'field-id' );
const subfield = $fieldOptionRow.data( 'subfield' );
$( '#wpforms-field-' + id ).find( '.wpforms-' + subfield + ' input' ).attr( 'placeholder', value );
} );
// Real-time updates for Address field "Placeholder" field options.
$builder.on( 'input', '.wpforms-field-option-address input.placeholder', function() {
const $this = $( this );
const $fieldOptionRow = $this.closest( '.wpforms-field-option-row' );
const id = $fieldOptionRow.data( 'field-id' );
const subfield = $fieldOptionRow.data( 'subfield' );
const $fieldPreviews = $( '#wpforms-field-' + id + ' .wpforms-' + subfield ).find( 'input, select' );
const $default = $fieldOptionRow.find( '#wpforms-field-option-' + id + '-' + subfield + '_default' );
const defaultValue = $default.val();
const defaultText = $default.find( 'option:selected' ).text();
const placeholderValue = $this.val();
$fieldPreviews.each( function() {
const $fieldPreview = $( this );
if ( $fieldPreview.is( 'select' ) ) {
const $option = $fieldPreview.find( '.placeholder' );
const value = defaultValue === '' && placeholderValue !== '' ? placeholderValue : defaultText;
$option.text( value );
return;
}
$fieldPreview.attr( 'placeholder', placeholderValue );
} );
} );
// Real-time updates for the "Default" field option.
$builder.on( 'input', '.wpforms-field-option-row-default_value input:not([type="search"])', function() {
const $this = $( this );
const value = wpf.sanitizeHTML( $this.val() );
const id = $this.closest( '.wpforms-field-option-row' ).data( 'field-id' );
const $preview = $( '#wpforms-field-' + id + ' .primary-input' );
$preview.val( value );
} );
// Real-time updates for "Default" field option of the Name and Address fields.
$builder.on( 'input', '.wpforms-field-options-column input.default', function() {
const $this = $( this );
const value = wpf.sanitizeHTML( $this.val() );
const $fieldOptionRow = $this.closest( '.wpforms-field-option-row' );
const id = $fieldOptionRow.data( 'field-id' );
const subfield = $fieldOptionRow.data( 'subfield' );
const $fieldPreview = $( '#wpforms-field-' + id + ' .wpforms-' + subfield + ' input' );
$fieldPreview.val( value );
} );
// Real-time updates for "Default" select field option of the Address field.
$builder.on( 'change', '.wpforms-field-option-address select.default', function() {
const $this = $( this );
const value = $this.val();
const textValue = $this.find( 'option:selected' ).text();
const $fieldOptionRow = $this.closest( '.wpforms-field-option-row' );
const id = $fieldOptionRow.data( 'field-id' );
const subfield = $fieldOptionRow.data( 'subfield' );
const scheme = $( '#wpforms-field-option-' + id + '-scheme' ).val();
const $placeholder = $fieldOptionRow.find( '#wpforms-field-option-' + id + '-' + subfield + '_placeholder' );
const placeholderValue = $placeholder.val();
const $fieldPreview = $( '#wpforms-field-' + id + ' .wpforms-address-scheme-' + scheme + ' .wpforms-' + subfield + ' .placeholder' );
value === '' && placeholderValue.trim().length > 0
? $fieldPreview.text( placeholderValue )
: $fieldPreview.text( textValue );
} );
// Real-time updates for the "Confirmation Placeholder" field option
$builder.on( 'input', '.wpforms-field-option-row-confirmation_placeholder input', function() {
const $this = $( this ),
value = $this.val(),
id = $this.parent().data( 'field-id' );
$( '#wpforms-field-' + id ).find( '.secondary-input' ).attr( 'placeholder', value );
} );
// Real-time updates for the "Hide Label" field option.
$builder.on( 'change', '.wpforms-field-option-row-label_hide input', function() {
const id = $( this ).closest( '.wpforms-field-option-row' ).data( 'field-id' );
$( '#wpforms-field-' + id ).toggleClass( 'label_hide' );
} );
// Real-time updates for a Sub Label visibility field option.
$builder.on( 'change', '.wpforms-field-option-row-sublabel_hide input', function() {
const id = $( this ).closest( '.wpforms-field-option-row' ).data( 'field-id' );
$( '#wpforms-field-' + id ).toggleClass( 'sublabel_hide' );
} );
// Real-time updates for the "Read-Only" field option.
$builder.on( 'change', '.wpforms-field-option-row-read_only input', app.fieldReadOnlyToggleChange );
// Real-time updates for a Quantity visibility field option.
$builder.on( 'change', '.wpforms-field-option-row-enable_quantity input', function() {
const id = $( this ).closest( '.wpforms-field-option-row' ).data( 'field-id' ),
$preview = $( `#wpforms-field-${ id }` );
$( `#wpforms-field-option-row-${ id }-quantity` ).toggleClass( 'wpforms-hidden' );
$preview.find( '.quantity-input' ).toggleClass( 'wpforms-hidden' );
$preview.toggleClass( 'payment-quantity-enabled' );
} );
// Real-time updates for Quantity preview minimum value.
$builder.on( 'input', '.wpforms-field-option-row-quantity input', function() {
const $this = $( this );
// Allow only a positive integer value less than 9999.
$this.val( Math.min( Math.abs( Math.round( $this.val() ) ), 9999 ) );
const $optionRow = $this.closest( '.wpforms-field-option-row' ),
id = $optionRow.data( 'field-id' ),
isMinInput = $this.hasClass( 'min-quantity-input' ),
$minInput = $optionRow.find( '.min-quantity-input' ),
$maxInput = $optionRow.find( '.max-quantity-input' );
if ( isMinInput ) {
$( '#wpforms-field-' + id ).find( '.quantity-input option' ).text( $this.val() );
}
$minInput.toggleClass( 'wpforms-error', parseInt( $minInput.val(), 10 ) > parseInt( $maxInput.val(), 10 ) );
} );
// Real-time updates for Date/Time, Name and Single Item "Format" option.
$builder.on( 'change', '.wpforms-field-option-row-format select', function() {
const $this = $( this ),
value = $this.val(),
id = $this.parent().data( 'field-id' ),
$sublabelToggle = $( '#wpforms-field-option-row-' + id + '-sublabel_hide' ),
$preview = $( '#wpforms-field-' + id );
$preview.find( '.format-selected' ).removeClass().addClass( 'format-selected format-selected-' + value );
$( '#wpforms-field-option-' + id ).find( '.format-selected' ).removeClass().addClass( 'format-selected format-selected-' + value );
// Show toggle for "Hide Sub labels" only when the field consists of more than one subfield.
if ( [ 'date-time', 'first-last', 'first-middle-last' ].includes( value ) ) {
$sublabelToggle.removeClass( 'wpforms-hidden' );
} else {
$sublabelToggle.addClass( 'wpforms-hidden' );
}
// Hide the label field if it's not a single item.
$( `#wpforms-field-option-row-${ id }-price_label` ).toggleClass( 'wpforms-hidden', value !== 'single' );
// Toggle options based on Single Item "Format".
if ( [ 'single', 'user', 'hidden' ].includes( value ) ) {
const isUserDefined = value === 'user',
isSingle = value === 'single',
isHidden = value === 'hidden',
isQuantityEnabled = $( '#wpforms-field-option-' + id + '-enable_quantity' ).is( ':checked' ),
$minPriceOption = $( '#wpforms-field-option-' + id + '-min_price' ),
minPrice = wpf.amountSanitize( $minPriceOption.val() ),
isValidMinPrice = minPrice >= $minPriceOption.data( 'minimum-price' ),
$minPriceOptionRow = $( '#wpforms-field-option-row-' + id + '-min_price' );
// Toggle Placeholder option.
$( '#wpforms-field-option-row-' + id + '-placeholder' ).toggleClass( 'wpforms-hidden', ! isUserDefined );
// Toggle Quantity options.
$( '#wpforms-field-option-row-' + id + '-enable_quantity' ).toggleClass( 'wpforms-hidden', ! isSingle );
$( '#wpforms-field-option-row-' + id + '-quantities_alert' ).toggleClass( 'wpforms-hidden', ! isSingle );
$( '#wpforms-field-option-row-' + id + '-quantity' ).toggleClass( 'wpforms-hidden', ! isSingle || ! isQuantityEnabled );
$preview.find( '.quantity-input' ).toggleClass( 'wpforms-hidden', ! isSingle || ! isQuantityEnabled );
// Toggle Minimum Price options.
$minPriceOptionRow.toggleClass( 'wpforms-hidden', ! isUserDefined );
$minPriceOptionRow.find( '.wpforms-item-minimum-price-alert' ).toggleClass( 'wpforms-hidden', isValidMinPrice );
$preview.find( '.item-min-price' ).toggleClass( 'wpforms-hidden', isUserDefined && minPrice <= 0 );
$preview.toggleClass( 'min-price-warning', ! isValidMinPrice );
$preview.find( '.fa-exclamation-triangle' ).toggleClass( 'wpforms-hidden', isValidMinPrice );
// Toggle the label
$( `#wpforms-field-${ id } .item-price-single` ).toggleClass( 'wpforms-hidden', ! isSingle );
$( `#wpforms-field-${ id } .item-price-hidden` ).toggleClass( 'wpforms-hidden', ! isHidden );
}
} );
// Real-time updates specific for Address "Scheme" option
$builder.on( 'change', '.wpforms-field-option-row-scheme select', function() {
const $this = $( this );
const value = $this.val();
const fieldId = $this.parent().data( 'field-id' );
const $fieldPreview = $( `#wpforms-field-${ fieldId }` );
const $stateOption = $( `#wpforms-field-option-row-${ fieldId }-state` );
const $countryOption = $( `#wpforms-field-option-row-${ fieldId }-country` );
// Switch the scheme in a Preview panel.
$fieldPreview.find( '.wpforms-address-scheme' ).addClass( 'wpforms-hide' );
$fieldPreview.find( `.wpforms-address-scheme-${ value }` ).removeClass( 'wpforms-hide' );
// Show an or hide country option depending on the scheme.
const $countryPreviewField = $fieldPreview.find( `.wpforms-address-scheme-${ value } .wpforms-country select, .wpforms-address-scheme-${ value } .wpforms-country input` );
$countryPreviewField.length === 0
? $countryOption.addClass( 'wpforms-hidden' )
: $countryOption.removeClass( 'wpforms-hidden' );
// Inputs/selects for a currently selected scheme and the one that we're changing to.
const $currentState = $stateOption.find( '.default .default' ).not( '.wpforms-hidden-strict' );
const $newState = $stateOption.find( `.default [data-scheme="${ value }"]` );
const $currentCountry = $countryOption.find( '.default .default' ).not( '.wpforms-hidden-strict' );
const $newCountry = $countryOption.find( `.default [data-scheme="${ value }"]` );
// Switch the state field type in options to match the scheme.
$newState.attr( {
id: $currentState.attr( 'id' ),
name: $currentState.attr( 'name' ),
} ).removeClass( 'wpforms-hidden-strict' );
$currentState.attr( { id: '', name: '' } ).addClass( 'wpforms-hidden-strict' );
$newCountry.attr( {
id: $currentCountry.attr( 'id' ),
name: $currentCountry.attr( 'name' ),
} ).removeClass( 'wpforms-hidden-strict' );
$currentCountry.attr( { id: '', name: '' } ).addClass( 'wpforms-hidden-strict' );
} );
// Real-time updates for a Date/Time date type
$builder.on( 'change', '.wpforms-field-option-row-date .type select', function() {
const $this = $( this ),
value = $this.val(),
id = $( this ).closest( '.wpforms-field-option-row' ).data( 'field-id' ),
addClass = value === 'datepicker' ? 'wpforms-date-type-datepicker' : 'wpforms-date-type-dropdown',
removeClass = value === 'datepicker' ? 'wpforms-date-type-dropdown' : 'wpforms-date-type-datepicker';
$( '#wpforms-field-' + id ).find( '.wpforms-date' ).addClass( addClass ).removeClass( removeClass );
$( '#wpforms-field-option-' + id ).addClass( addClass ).removeClass( removeClass );
const $limitDays = $this.closest( '.wpforms-field-option-group-advanced' )
.find( '.wpforms-field-option-row-date_limit_days, .wpforms-field-option-row-date_limit_days_options, .wpforms-field-option-row-date_disable_past_dates' ),
$limitDaysOptions = $( '#wpforms-field-option-row-' + id + '-date_limit_days_options' );
if ( value === 'dropdown' ) {
const $dateSelect = $( '#wpforms-field-option-' + id + '-date_format' );
if ( $dateSelect.find( 'option:selected' ).hasClass( 'datepicker-only' ) ) {
$dateSelect.prop( 'selectedIndex', 0 ).trigger( 'change' );
}
$limitDays.hide();
} else {
$limitDays.show();
$( '#wpforms-field-option-' + id + '-date_limit_days' ).is( ':checked' )
? $limitDaysOptions.show()
: $limitDaysOptions.hide();
}
} );
// Real-time updates for Date/Time date select format
$builder.on( 'change', '.wpforms-field-option-row-date .format select', function() {
const $this = $( this ),
value = $this.val(),
id = $( this ).closest( '.wpforms-field-option-row' ).data( 'field-id' ),
$field = $( '#wpforms-field-' + id );
if ( value === 'm/d/Y' || value === 'm.d.Y' ) {
$field.find( '.wpforms-date-dropdown .first option' ).text( wpforms_builder.date_select_month );
$field.find( '.wpforms-date-dropdown .second option' ).text( wpforms_builder.date_select_day );
$field.find( '.wpforms-date-dropdown .third option' ).text( wpforms_builder.date_select_year );
} else if ( value === 'd/m/Y' || value === 'd.m.Y' ) {
$field.find( '.wpforms-date-dropdown .first option' ).text( wpforms_builder.date_select_day );
$field.find( '.wpforms-date-dropdown .second option' ).text( wpforms_builder.date_select_month );
$field.find( '.wpforms-date-dropdown .third option' ).text( wpforms_builder.date_select_year );
} else if ( value === 'Y/m/d' || value === 'Y.m.d' ) {
$field.find( '.wpforms-date-dropdown .first option' ).text( wpforms_builder.date_select_year );
$field.find( '.wpforms-date-dropdown .second option' ).text( wpforms_builder.date_select_month );
$field.find( '.wpforms-date-dropdown .third option' ).text( wpforms_builder.date_select_day );
}
} );
// Real-time updates for Date/Time select format
$builder.on( 'change', '.wpforms-field-option-row-time .format select', function() {
const $this = $( this ),
id = $this.closest( '.wpforms-field-option-row' ).data( 'field-id' );
let options = '',
hh;
// Determine a time format type.
// If the format contains `g` or `h`, then this is a 12-hour format, otherwise 24 hours.
const format = $this.val().match( /[gh]/ ) ? 12 : 24,
minHour = format === 12 ? 1 : 0,
maxHour = format === 12 ? 13 : 24;
// Generate a new set of hour options.
for ( let i = minHour; i < maxHour; i++ ) {
hh = i < 10 ? '0' + i : i;
options += '<option value="{hh}">{hh}</option>'.replace( /{hh}/g, hh );
}
_.forEach( [ 'start', 'end' ], function( field ) {
const $hour = $builder.find( '#wpforms-field-option-' + id + '-time_limit_hours_' + field + '_hour' ),
$ampm = $builder.find( '#wpforms-field-option-' + id + '-time_limit_hours_' + field + '_ampm' );
let hourValue = parseInt( $hour.val(), 10 ),
ampmValue = $ampm.val();
if ( format === 24 ) {
hourValue = ampmValue === 'pm' ? hourValue + 12 : hourValue;
} else {
ampmValue = hourValue > 12 ? 'pm' : 'am';
hourValue = hourValue > 12 ? hourValue - 12 : hourValue;
}
hourValue = hourValue < 10 ? '0' + hourValue : hourValue;
$hour.html( options ).val( hourValue );
$ampm.toggleClass( 'wpforms-hidden-strict', format === 24 ).val( ampmValue );
$ampm.nextAll( 'div' ).toggleClass( 'wpforms-hidden-strict', format === 12 );
} );
} );
// Real-time updates for the "Hide Divider Line" field option.
$builder.on( 'change', '.wpforms-field-option-row-hide_divider_line input', function() {
const id = $( this ).closest( '.wpforms-field-option-row' ).data( 'field-id' );
$( `#wpforms-field-${ id }` ).toggleClass( 'hide_line' );
} );
// Consider the field active when a disabled nav button is clicked
$builder.on( 'click', '.wpforms-pagebreak-button', function( e ) {
e.preventDefault();
$( this ).closest( '.wpforms-field' ).trigger( 'click' );
} );
/*
* Pagebreak field.
*/
app.fieldPageBreakInitDisplayPrevious( $builder.find( '.wpforms-field-pagebreak.wpforms-pagebreak-normal' ).first() );
$builder
.on( 'input', '.wpforms-field-option-row-next input', function() {
// Real-time updates for the "Next" pagebreak field option.
const $this = $( this ),
value = $this.val(),
$next = $( '#wpforms-field-' + $this.parent().data( 'field-id' ) ).find( '.wpforms-pagebreak-next' );
if ( value ) {
$next.css( 'display', 'inline-block' ).text( value );
} else {
$next.css( 'display', 'none' ).empty();
}
} )
.on( 'input', '.wpforms-field-option-row-prev input', function() {
// Real-time updates for the "Prev" pagebreak field option.
const $this = $( this ),
value = $this.val().trim(),
$field = $( '#wpforms-field-' + $this.parent().data( 'field-id' ) ),
$prevBtn = $field.find( '.wpforms-pagebreak-prev' );
if ( value && $field.prevAll( '.wpforms-field-pagebreak.wpforms-pagebreak-normal' ).length > 0 ) {
$prevBtn.removeClass( 'wpforms-hidden' ).text( value );
} else {
$prevBtn.addClass( 'wpforms-hidden' ).empty();
}
} )
.on( 'change', '.wpforms-field-option-row-prev_toggle input', function() { // eslint-disable-line complexity
// Real-time updates for "Display Previous" pagebreak field option.
const $input = $( this ),
$wrapper = $input.closest( '.wpforms-field-option-row-prev_toggle' );
if ( $wrapper.hasClass( 'wpforms-entry-preview-block' ) ) {
return;
}
const $prev = $input.closest( '.wpforms-field-option-group-inner' ).find( '.wpforms-field-option-row-prev' );
const $prevLabel = $prev.find( 'input' ),
$prevBtn = $( '#wpforms-field-' + $input.closest( '.wpforms-field-option' ).data( 'field-id' ) ).find( '.wpforms-pagebreak-prev' );
$prev.toggleClass( 'wpforms-hidden', ! $input.prop( 'checked' ) );
$prevBtn.toggleClass( 'wpforms-hidden', ! $input.prop( 'checked' ) );
if ( $input.prop( 'checked' ) && ! $prevLabel.val() ) {
let message = $prevLabel.data( 'last-value' );
message = message && message.trim() ? message.trim() : wpforms_builder.previous;
$prevLabel.val( message );
}
// Backward compatibility for forms that were created before the toggle was added.
if ( ! $input.prop( 'checked' ) ) {
$prevLabel.data( 'last-value', $prevLabel.val() );
$prevLabel.val( '' );
}
$prevLabel.trigger( 'input' );
} )
.on( 'wpformsFieldAdd', app.fieldPagebreakAdd )
.on( 'wpformsFieldDelete', app.fieldPagebreakDelete )
.on( 'wpformsFieldAdd', app.toggleOrderSummaryConfirmation )
.on( 'wpformsFieldDelete', app.toggleOrderSummaryConfirmation )
.on( 'wpformsBeforeFieldDelete', app.fieldEntryPreviewDelete );
// Update Display Previous option visibility for all Pagebreak fields.
$builder.on( 'wpformsFieldMove wpformsFieldAdd wpformsFieldDelete', function() {
$builder.find( '.wpforms-field-pagebreak.wpforms-pagebreak-normal' ).each( function() {
app.fieldPageBreakInitDisplayPrevious( $( this ) );
} );
} );
// Real-time updates for "Page Title" pagebreak field option
$builder.on( 'input', '.wpforms-field-option-row-title input', function() {
const $this = $( this ),
value = $this.val(),
id = $this.parent().data( 'field-id' );
if ( value ) {
$( '#wpforms-field-' + id ).find( '.wpforms-pagebreak-title' ).text( value );
} else {
$( '#wpforms-field-' + id ).find( '.wpforms-pagebreak-title' ).empty();
}
} );
// Real-time updates for "Page Navigation Alignment" pagebreak field option
$builder.on( 'change', '.wpforms-field-option-row-nav_align select', function() {
const $this = $( this );
let value = $this.val();
if ( ! value ) {
value = 'center';
}
$( '.wpforms-pagebreak-buttons' )
.removeClass( 'wpforms-pagebreak-buttons-center wpforms-pagebreak-buttons-left wpforms-pagebreak-buttons-right wpforms-pagebreak-buttons-split' )
.addClass( 'wpforms-pagebreak-buttons-' + value );
} );
// Real-time updates for Single Item field "Item Price" option.
$builder.on( 'input', '.wpforms-field-option-row-price input', function() {
const $this = $( this ),
value = $this.val(),
formatted = wpf.amountFormat( wpf.amountSanitize( value ) ),
id = $this.parent().data( 'field-id' ),
placeholder = $( '#wpforms-field-option-' + id + '-placeholder' ).val().trim(),
$preview = $( '#wpforms-field-' + id ),
newValue = value === '' && placeholder !== '' ? '' : formatted;
$preview.find( '.primary-input' ).val( newValue );
$preview.find( '.price' ).text( wpf.amountFormatCurrency( value ) );
} );
// Real-time updates for Single Item field "Minimum Price" option.
$builder.on( 'input', '.wpforms-field-option-row-min_price input', function() {
const $this = $( this ),
amount = $this.val(),
sanitized = wpf.amountSanitize( amount ),
isEmpty = sanitized <= 0,
isValid = sanitized >= $this.data( 'minimum-price' ),
$fieldOptionRow = $this.parent(),
$preview = $( '#wpforms-field-' + $fieldOptionRow.data( 'field-id' ) );
$fieldOptionRow.find( '.wpforms-item-minimum-price-alert' ).toggleClass( 'wpforms-hidden', isValid );
$preview.find( '.item-min-price' ).toggleClass( 'wpforms-hidden', isEmpty );
$preview.toggleClass( 'min-price-warning', ! isValid );
$preview.find( '.fa-exclamation-triangle' ).toggleClass( 'wpforms-hidden', isValid );
if ( isEmpty ) {
return;
}
$preview.find( '.min-price' ).text( wpf.amountFormatCurrency( amount ) );
} );
// Real-time updates for price label for single item field.
$builder.on( 'input', '.wpforms-single-item-price-label-display', function() {
const $this = $( this ),
value = wpf.sanitizeHTML( $this.val(), '<>' ),
id = $this.parent().data( 'field-id' ),
$preview = $( `#wpforms-field-${ id }` ),
$price = wpf.amountFormatCurrency( $( `#wpforms-field-option-${ id }-price` ).val() );
if ( ! value ) {
$this.val( '{price}' );
$preview.find( '.price-label' ).html( `<span class="price"> ${ $price } </span>` );
return;
}
$preview.find( '.price-label' ).html( value.replaceAll( '{price}', `<span class="price"> ${ $price } </span>` ) );
} );
// Real-time updates for payment CC icons
$builder.on( 'change', '.wpforms-field-option-credit-card .payment-icons input', function() {
const $this = $( this ),
card = $this.data( 'card' ),
id = $this.parent().data( 'field-id' );
$( '#wpforms-field-' + id ).find( 'img.icon-' + card ).toggleClass( 'card_hide' );
} );
// Generic updates for various additional placeholder fields (at least Stripe's "Name on Card").
$builder.on( 'input', '.wpforms-field-option input.placeholder-update', function() {
const $this = $( this ),
value = $this.val(),
id = $this.data( 'field-id' ),
subfield = $this.data( 'subfield' );
$( '#wpforms-field-' + id ).find( '.wpforms-' + subfield + ' input' ).attr( 'placeholder', value );
} );
// Toggle Choice Layout advanced field option.
$builder.on( 'change', '.wpforms-field-option-row-input_columns select', function() {
const $this = $( this ),
value = $this.val(),
id = $this.parent().data( 'field-id' );
let cls = '';
if ( value === '2' ) {
cls = 'wpforms-list-2-columns';
} else if ( value === '3' ) {
cls = 'wpforms-list-3-columns';
} else if ( value === 'inline' ) {
cls = 'wpforms-list-inline';
}
$( '#wpforms-field-' + id ).removeClass( 'wpforms-list-2-columns wpforms-list-3-columns wpforms-list-inline' ).addClass( cls );
} );
// Toggle the toggle field.
$builder.on( 'change', '.wpforms-field-option-row .wpforms-toggle-control input', function() {
const $check = $( this ),
$control = $check.closest( '.wpforms-toggle-control' ),
$status = $control.find( '.wpforms-toggle-control-status' ),
state = $check.is( ':checked' ) ? 'on' : 'off';
$status.html( $status.data( state ) );
} );
// Real-time updates for "Dynamic Choices" field option, for Dropdown,
// Checkboxes, and Multiple choice fields
$builder.on( 'change', '.wpforms-field-option-row-dynamic_choices select', function() {
app.fieldDynamicChoiceToggle( $( this ) );
} );
// Real-time updates for the "Dynamic [type] Source" field option, for Dropdown,
// Checkboxes, and Multiple choice fields
$builder.on( 'change', '.wpforms-field-option-row-dynamic_taxonomy select, .wpforms-field-option-row-dynamic_post_type select', function() {
app.fieldDynamicChoiceSource( $( this ) );
} );
// Toggle Layout selector
$builder.on( 'click', '.toggle-layout-selector-display', function( e ) {
e.preventDefault();
app.fieldLayoutSelectorToggle( this );
} );
$builder.on( 'click', '.layout-selector-display-layout', function( e ) {
e.preventDefault();
app.fieldLayoutSelectorLayout( this );
} );
$builder.on( 'click', '.layout-selector-display-columns span', function( e ) {
e.preventDefault();
app.fieldLayoutSelectorInsert( this );
} );
// Real-time updates for a Rating field scale option.
$( document ).on( 'change', '.wpforms-field-option-row-scale select', function() {
const $this = $( this ),
value = $this.val(),
id = $this.parent().data( 'field-id' ),
$icons = $( '#wpforms-field-' + id + ' .rating-icon' );
let x = 1;
$icons.each( function() {
if ( x <= value ) {
$( this ).show();
} else {
$( this ).hide();
}
x++;
} );
} );
// Real-time updates for a Rating field icon option.
$( document ).on( 'change', '.wpforms-field-option-row-icon select', function() {
const $this = $( this ),
value = $this.val(),
id = $this.parent().data( 'field-id' ),
$icons = $( '#wpforms-field-' + id + ' .rating-icon' );
let iconClass = 'fa-star';
if ( 'heart' === value ) {
iconClass = 'fa-heart';
} else if ( 'thumb' === value ) {
iconClass = 'fa-thumbs-up';
} else if ( 'smiley' === value ) {
iconClass = 'fa-smile-o';
}
$icons.removeClass( 'fa-star fa-heart fa-thumbs-up fa-smile-o' ).addClass( iconClass );
} );
// Real-time updates for a Rating field icon size option.
$( document ).on( 'change', '.wpforms-field-option-row-icon_size select', function() {
const $this = $( this ),
value = $this.val(),
id = $this.parent().data( 'field-id' ),
$icons = $( '#wpforms-field-' + id + ' .rating-icon' );
let fontSize = '28';
if ( 'small' === value ) {
fontSize = '18';
} else if ( 'large' === value ) {
fontSize = '38';
}
$icons.css( 'font-size', fontSize + 'px' );
} );
// Real-time updates for a Rating field icon color option.
$( document ).on( 'input', '.wpforms-field-option-row-icon_color input.wpforms-color-picker', function() {
const $this = $( this ),
id = $this.closest( '.wpforms-field-option-row' ).data( 'field-id' ),
$icons = $( `#wpforms-field-${ id } .wpforms-rating-field-icons i.fa` );
$icons.css( 'color', app.getValidColorPickerValue( $this ) );
} );
// Real-time updates for a Checkbox field Disclaimer option.
$( document ).on( 'change', '.wpforms-field-option-row-disclaimer_format input', function() {
const $this = $( this ),
id = $this.closest( '.wpforms-field-option-row' ).data( 'field-id' ),
$desc = $( '#wpforms-field-' + id + ' .description' );
$desc.toggleClass( 'disclaimer' );
} );
$builder.on(
'change',
'.wpforms-field-option-row-limit_enabled input',
function( event ) {
app.updateTextFieldsLimitControls( $( event.target ).closest( '.wpforms-field-option-row-limit_enabled' ).data().fieldId, event.target.checked );
}
);
$builder.on(
'change',
'.wpforms-field-option-row-date_disable_past_dates input',
function( event ) {
app.updateDisableTodaysDateControls( $( event.target ).closest( '.wpforms-field-option-row-date_disable_past_dates' ).data().fieldId, event.target?.checked );
}
);
$builder.on( 'change', '.wpforms-field-option-row-password-visibility input', app.togglePasswordEyeIcon );
$builder.on(
'change',
'.wpforms-field-option-row-password-strength input',
function( event ) {
app.updatePasswordStrengthControls( $( event.target ).parents( '.wpforms-field-option-row-password-strength' ).data().fieldId, event.target.checked );
}
);
$builder.on(
'change',
'.wpforms-field-option-richtext .wpforms-field-option-row-media_enabled input',
app.updateRichTextMediaFieldsLimitControls
);
$builder.on(
'change',
'.wpforms-field-option-richtext .wpforms-field-option-row-style select',
app.updateRichTextStylePreview
);
// File uploader - change style.
$builder
.on(
'change',
'.wpforms-field-option-file-upload .wpforms-field-option-row-style select, .wpforms-field-option-file-upload .wpforms-field-option-row-max_file_number input, .wpforms-field-option-file-upload .wpforms-field-option-row-camera input',
function( event ) {
app.fieldFileUploadPreviewUpdate( event.target );
}
);
// Real-time updates for the Number Slider field.
app.numberSliderEvents( $builder );
// Hide image and icon choices if dynamic choices are not off.
app.fieldDynamicChoiceToggleImageChoices();
app.fieldDynamicChoiceToggleIconChoices();
// Real-time updates for Payment field's 'Show price after item label' option.
$builder.on( 'change', '.wpforms-field-option-row-show_price_after_labels input', function() {
const $input = $( this ),
$list = $input.closest( '.wpforms-field-option-group-basic' ).find( '.wpforms-field-option-row-choices .choices-list' );
app.fieldChoiceUpdate( $list.data( 'field-type' ), $list.data( 'field-id' ) );
} );
$builder
.on( 'input', '.wpforms-field-option-row-preview-notice textarea', app.updatePreviewNotice )
.on( 'change', '.wpforms-field-option-row-preview-notice-enable input', app.toggleEntryPreviewNotice )
.on( 'wpformsFieldAdd', app.maybeLockEntryPreviewGroupOnAdd )
.on( 'wpformsFieldMove', app.maybeLockEntryPreviewGroupOnMove )
.on( 'click', '.wpforms-entry-preview-block', app.entryPreviewBlockField );
app.defaultStateEntryPreviewNotice();
},
/**
* Event handler for the Read-Only toggle.
*
* @since 1.9.8
*/
fieldReadOnlyToggleChange() {
const $input = $( this );
const fieldId = Number( $input.closest( '.wpforms-field-option-row' ).data( 'field-id' ) );
const isChecked = $input.is( ':checked' );
const $dependentToggles = $( `#wpforms-field-option-row-${ fieldId }-required, #wpforms-field-option-row-${ fieldId }-unique_answer` );
const $requiredInput = $dependentToggles.filter( '.wpforms-field-option-row-required' ).find( 'input' );
// Disable/Enable the dependent toggles.
app.fieldReadOnlyToggleDependentToggles( $dependentToggles, isChecked );
// Toggle CSS classes in the field preview container.
$( `#wpforms-field-${ fieldId }` )
.toggleClass( 'readonly', isChecked )
.toggleClass( 'required', $requiredInput.is( ':checked' ) );
},
/**
* Disable/Enable the dependent toggles.
*
* @since 1.9.8
*
* @param {Object} $toggles jQuery object with toggles to operate.
* @param {boolean} disable True to disable, false to enable.
*/
fieldReadOnlyToggleDependentToggles( $toggles, disable = true ) {
$toggles.each( function() {
const $optionRow = $( this );
const $optionInput = $optionRow.find( ':input' );
const setDataChecked = disable ? $optionInput.is( ':checked' ) : null;
const setChecked = disable ? false : $optionInput.data( 'enabled-state-checked' );
const setTitle = disable ? wp.i18n.__( 'Disabled because this field is set to Read-Only in the Advanced tab.', 'wpforms' ) : '';
$optionRow
.toggleClass( 'wpforms-disabled', disable )
.find( '.wpforms-toggle-control' )
.attr( 'title', setTitle );
$optionInput
.data( 'enabled-state-checked', setDataChecked )
.prop( 'checked', setChecked )
.prop( 'disabled', disable );
} );
},
/**
* Check if we had focusout event from certain fields.
*
* @since 1.7.5
*/
focusOutEvent() {
if ( elements.$focusOutTarget === null ) {
return;
}
if ( elements.$defaultEmail.is( elements.$focusOutTarget ) ) {
const $field = elements.$focusOutTarget;
$field.next( '.wpforms-alert' ).remove();
if ( $field.val() === '' ) {
return;
}
$.get(
wpforms_builder.ajax_url,
{
nonce: wpforms_builder.nonce,
content: $field.val(),
action: 'wpforms_sanitize_default_email',
},
function( res ) {
if ( res.success ) {
$field.val( res.data );
$field.trigger( 'input' );
if ( ! res.data ) {
$field.after( '<div class="wpforms-alert-warning wpforms-alert"><p>' + wpforms_builder.restricted_default_email + '</p></div>' );
}
}
}
);
}
elements.$focusOutTarget = null;
},
/**
* Determine if the field is disabled for selection/duplication/deletion.
*
* @since 1.7.1
*
* @param {any} el DOM element or jQuery object of some container on the field preview.
*
* @return {boolean} True if actions are disabled.
*/
isFieldPreviewActionsDisabled( el ) {
return app.isFormPreviewActionsDisabled( el ) ||
$( el ).closest( '.wpforms-field' ).hasClass( 'ui-sortable-disabled' );
},
/**
* Determine if form wrapper has sorting locked.
*
* @since 1.7.6
*
* @param {any} el DOM element or jQuery object of some container on the field preview.
*
* @return {boolean} True when form preview wrapper sorting is disabled.
*/
isFormPreviewActionsDisabled( el ) {
return $( el ).closest( '.wpforms-field-wrap' ).hasClass( 'ui-sortable-disabled' );
},
/**
* Toggle field group visibility in the field sidebar.
*
* @since 1.0.0
*
* @param {any} el DOM element or jQuery object.
* @param {string} action Action.
*/
fieldGroupToggle( el, action ) {
/** @type {JQ|jQuery} */
const $this = $( el );
let $buttons = $this.next( '.wpforms-add-fields-buttons' );
const $group = $buttons.parent();
let $icon = $this.find( 'i' ),
groupName = $this.data( 'group' ),
cookieName = 'wpforms_field_group_' + groupName;
if ( action === 'click' ) {
if ( $group.hasClass( 'wpforms-closed' ) ) {
wpCookies.remove( cookieName );
} else {
wpCookies.set( cookieName, 'true', 2592000 ); // 1 month
}
$icon.toggleClass( 'wpforms-angle-right' );
$buttons.stop().slideToggle( '', function() {
$group.toggleClass( 'wpforms-closed' );
} );
return;
}
if ( action === 'load' ) {
$buttons = $this.find( '.wpforms-add-fields-buttons' );
$icon = $this.find( '.wpforms-add-fields-heading i' );
groupName = $this.find( '.wpforms-add-fields-heading' ).data( 'group' );
cookieName = 'wpforms_field_group_' + groupName;
if ( wpCookies.get( cookieName ) === 'true' ) {
$icon.toggleClass( 'wpforms-angle-right' );
$buttons.hide();
$this.toggleClass( 'wpforms-closed' );
}
}
},
/**
* Update description.
*
* @since 1.6.9
*
* @param {jQuery} $el Element.
* @param {string} value Value.
*/
updateDescription( $el, value ) {
if ( $el.hasClass( 'nl2br' ) ) {
value = value.replace( /\n/g, '<br>' );
}
$el.html( value );
},
/**
* Set the default state for the entry preview notice field.
*
* @since 1.6.9
*/
defaultStateEntryPreviewNotice() {
$( '.wpforms-field-option-row-preview-notice-enable input' ).each( function() {
$( this ).trigger( 'change' );
} );
},
/**
* Update a preview notice for the field preview.
*
* @since 1.6.9
*/
updatePreviewNotice() {
const $this = $( this );
let value = wpf.sanitizeHTML( $this.val() ).trim();
const id = $this.parent().data( 'field-id' ),
$field = $( '#wpforms-field-' + id ).find( '.wpforms-entry-preview-notice' );
value = value ? value : wpforms_builder.entry_preview_default_notice;
app.updateDescription( $field, value );
},
/**
* Show/hide entry preview notice for the field preview.
*
* @since 1.6.9
*/
toggleEntryPreviewNotice() {
const $this = $( this ),
id = $this.closest( '.wpforms-field-option' ).data( 'field-id' ),
$field = $( '#wpforms-field-' + id ),
$noticeField = $( '#wpforms-field-option-' + id + ' .wpforms-field-option-row-preview-notice' ),
$notice = $field.find( '.wpforms-entry-preview-notice' ),
$defaultNotice = $field.find( '.wpforms-alert-info' );
if ( $this.is( ':checked' ) ) {
$defaultNotice.hide();
$notice.show();
$noticeField.show();
return;
}
$noticeField.hide();
$notice.hide();
$defaultNotice.show();
},
/**
* Delete a field.
*
* @param {number} id Field ID.
*
* @since 1.0.0
* @since 1.6.9 Add the entry preview logic.
*/
fieldDelete( id ) {
const $field = $( '#wpforms-field-' + id ),
type = $field.data( 'field-type' );
if ( type === 'pagebreak' && $field.hasClass( 'wpforms-field-entry-preview-not-deleted' ) ) {
app.youCantRemovePageBreakFieldPopup();
return;
}
if ( $field.hasClass( 'no-delete' ) ) {
app.youCantRemoveFieldPopup();
return;
}
app.confirmFieldDeletion( id, type );
},
/**
* Show the error message in the popup that you cannot remove the page break field.
*
* @since 1.6.9
*/
youCantRemovePageBreakFieldPopup() {
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.entry_preview_require_page_break,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Show the error message in the popup that you cannot reorder the field.
*
* @since 1.7.1
* @since 1.7.7 Deprecated.
*
* @deprecated Use `WPForms.Admin.Builder.DragFields.youCantReorderFieldPopup()` instead.
*/
youCantReorderFieldPopup() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.youCantReorderFieldPopup()" has been deprecated, please use the new "WPForms.Admin.Builder.DragFields.youCantReorderFieldPopup()" function instead!' );
WPForms.Admin.Builder.DragFields.youCantReorderFieldPopup();
},
/**
* Show the error message in the popup that you cannot remove the field.
*
* @since 1.6.9
*/
youCantRemoveFieldPopup() {
$.alert( {
title: wpforms_builder.field_locked,
content: wpforms_builder.field_locked_no_delete_msg,
icon: 'fa fa-info-circle',
type: 'blue',
buttons: {
confirm: {
text: wpforms_builder.close,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Error alert displayed for invalid From Email Notification field.
*
* @since 1.8.1
* @deprecated 1.9.5
*
* @param {string} msg Message.
*/
validationErrorNotificationPopup( msg ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.validationErrorNotificationPopup()" has been deprecated.' );
$.alert( {
title: wpforms_builder.heads_up,
content: msg,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.close,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Show the confirmation popup before the field deletion.
*
* @param {number} id Field ID.
* @param {string} type Field type.
*
* @since 1.6.9
*/
confirmFieldDeletion( id, type ) {
const fieldData = {
id,
message: wpforms_builder.delete_confirm,
};
const event = WPFormsUtils.triggerEvent( $builder, 'wpformsBeforeFieldDeleteAlert', [ fieldData, type ] );
// Allow callbacks on `wpformsBeforeFieldDeleteAlert` to prevent field deletion by triggering `event.preventDefault()`.
if ( event.isDefaultPrevented() ) {
return;
}
$.confirm( {
title: false,
content: fieldData.message,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
app.fieldDeleteById( id );
},
},
cancel: {
text: wpforms_builder.cancel,
},
},
} );
},
/**
* Remove the field by ID.
*
* @since 1.6.9
*
* @param {number} id Field ID.
* @param {string} type Field type (deprecated)
* @param {number} duration Duration of animation.
*/
fieldDeleteById( id, type = '', duration = 400 ) {
$( `#wpforms-field-${ id }` ).fadeOut( duration, function() {
const $field = $( this );
const $layoutParents = $field.parents( '.wpforms-field-layout-columns' );
type = $field.data( 'field-type' );
$builder.trigger( 'wpformsBeforeFieldDelete', [ id, type ] );
$field.remove();
$( '#wpforms-field-option-' + id ).remove();
$( '.wpforms-field, .wpforms-title-desc' ).removeClass( 'active' );
app.fieldTabToggle( 'add-fields' );
const $fieldsOptions = $( '.wpforms-field-option' ),
$submitButton = $builder.find( '.wpforms-field-submit' );
// No fields remains.
if ( $fieldsOptions.length < 1 ) {
elements.$sortableFieldsWrap.append( elements.$noFieldsPreview.clone() );
elements.$fieldOptions.append( elements.$noFieldsOptions.clone() );
$submitButton.hide();
}
// Only Layout fields remains.
if ( ! $fieldsOptions.filter( ':not(.wpforms-field-option-layout)' ).length ) {
$submitButton.hide();
}
$builder.trigger( 'wpformsFieldDelete', [ id, type, $layoutParents ] );
} );
},
/**
* Determine which sections to activate for each panel.
*
* @since 1.9.3
*/
determineActiveSections() {
const sectionFromUrl = wpf.getQueryString( 'section' );
// Gets the section to activate based on the URL.
const getSectionFromUrl = ( $panel, sectionFromUrl ) => {
if ( ! sectionFromUrl || ! $panel.hasClass( 'active' ) ) {
return null;
}
const $sectionElement = $panel.find( `.wpforms-panel-sidebar-section[data-section="${ sectionFromUrl }"]` );
// Skip non-existing or non-accessible section.
if ( $sectionElement.length === 0 || $sectionElement.hasClass( 'wpforms-panel-sidebar-section-no-access' ) ) {
return null;
}
return $sectionElement;
};
// Gets the configured section within a panel to activate, if available.
const getConfiguredSection = ( $panel ) => {
const $configuredSection = $panel.find( '.wpforms-panel-sidebar-section.configured' ).first();
return $configuredSection.length ? $configuredSection : null;
};
// Gets the first available section in the sidebar to activate.
const getFirstAvailableSection = ( $panel ) => {
return $panel.find( '.wpforms-panel-sidebar-section:first-of-type' );
};
// Activates the specified section within a panel and its corresponding content section.
const activateSection = ( $panel, $sectionToActivate ) => {
if ( ! $sectionToActivate ) {
return;
}
const sectionNameToActivate = $sectionToActivate.data( 'section' );
$sectionToActivate.addClass( 'active' );
const $contentSection = $panel.find( `.wpforms-panel-content-section-${ sectionNameToActivate }` );
if ( $contentSection.length ) {
$contentSection.show().addClass( 'active' );
$panel.find( '.wpforms-panel-content-section-default' ).toggle( sectionNameToActivate === 'default' );
} else {
$panel.find( '.wpforms-panel-content-section-default' ).show().addClass( 'active' );
}
WPFormsUtils.triggerEvent( $builder, 'wpformsPanelSectionSwitch', sectionNameToActivate );
};
// Iterate through each panel and determine which section to activate.
$( '.wpforms-panel' ).each( function() {
const $panel = $( this );
const $sectionToActivate = getSectionFromUrl( $panel, sectionFromUrl ) ||
getConfiguredSection( $panel ) ||
getFirstAvailableSection( $panel );
activateSection( $panel, $sectionToActivate );
} );
},
/**
* Load entry preview fields.
*
* @since 1.6.9
*/
loadEntryPreviewFields() {
const $fields = $( '#wpforms-panel-fields .wpforms-field-wrap .wpforms-field-entry-preview' );
if ( ! $fields.length ) {
return;
}
$fields.each( function() {
app.lockEntryPreviewFieldsPosition( $( this ).data( 'field-id' ) );
} );
},
/**
* Delete the entry preview field from the form preview.
*
* @since 1.6.9
*
* @param {Event} event Event.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
fieldEntryPreviewDelete( event, id, type ) {
if ( 'entry-preview' !== type ) {
return;
}
const $field = $( '#wpforms-field-' + id ),
$previousPageBreakField = $field.prevAll( '.wpforms-field-pagebreak' ).first(),
$nextPageBreakField = $field.nextAll( '.wpforms-field-pagebreak' ).first(),
nextPageBreakId = $nextPageBreakField.data( 'field-id' ),
$nextPageBreakOptions = $( '#wpforms-field-option-' + nextPageBreakId );
$previousPageBreakField.removeClass( 'wpforms-field-not-draggable wpforms-field-entry-preview-not-deleted' );
$nextPageBreakOptions.find( '.wpforms-entry-preview-block' ).removeClass( 'wpforms-entry-preview-block' );
$builder.trigger( 'wpformsFieldDragToggle', [ $previousPageBreakField.data( 'field-id' ), $previousPageBreakField.data( 'field-type' ) ] );
},
/**
* Maybe lock the entry preview and fields nearby after the move event.
*
* @since 1.6.9
*
* @param {Event} e Event.
* @param {Object} ui UI sortable object.
*/
maybeLockEntryPreviewGroupOnMove( e, ui ) {
if ( ! ui.item.hasClass( 'wpforms-field-pagebreak' ) ) {
return;
}
app.maybeLockEntryPreviewGroupOnAdd( e, ui.item.data( 'field-id' ), 'pagebreak' );
},
/**
* Maybe lock the entry preview and fields nearby after adding the event.
*
* @since 1.6.9
*
* @param {Event} e Event.
* @param {number} fieldId Field id.
* @param {string} type Field type.
*/
maybeLockEntryPreviewGroupOnAdd( e, fieldId, type ) {
if ( type !== 'pagebreak' ) {
return;
}
const $currentField = $( '#wpforms-field-' + fieldId ),
$prevField = $currentField.prevAll( '.wpforms-field-entry-preview,.wpforms-field-pagebreak' ).first(),
$nextField = $currentField.nextAll( '.wpforms-field-entry-preview,.wpforms-field-pagebreak' ).first();
if ( ! $prevField.hasClass( 'wpforms-field-entry-preview' ) && ! $nextField.hasClass( 'wpforms-field-entry-preview' ) ) {
return;
}
const $currentFieldPrevToggle = $( '#wpforms-field-option-' + fieldId + ' .wpforms-field-option-row-prev_toggle' ),
$currentFieldPrevToggleField = $currentFieldPrevToggle.find( 'input' ),
$nextFieldPrevToggle = $( '#wpforms-field-option-' + $nextField.data( 'field-id' ) + ' .wpforms-field-option-row-prev_toggle' );
if ( $prevField.hasClass( 'wpforms-field-entry-preview' ) ) {
$currentFieldPrevToggleField.attr( 'checked', 'checked' ).trigger( 'change' );
$currentFieldPrevToggle.addClass( 'wpforms-entry-preview-block' );
$nextFieldPrevToggle.removeClass( 'wpforms-entry-preview-block' );
return;
}
const prevFieldId = $prevField.data( 'field-id' ),
$prevFieldPrevToggle = $( '#wpforms-field-option-' + prevFieldId + ' .wpforms-field-option-row-prev_toggle' ),
$prevFieldPrevToggleField = $prevFieldPrevToggle.find( 'input' );
$currentField.addClass( 'wpforms-field-not-draggable wpforms-field-entry-preview-not-deleted' );
$builder.trigger( 'wpformsFieldDragToggle', [ fieldId, type ] );
$prevField.removeClass( 'wpforms-field-not-draggable wpforms-field-entry-preview-not-deleted' );
$builder.trigger( 'wpformsFieldDragToggle', [ prevFieldId, $prevField.data( 'field-type' ) ] );
if ( $prevField.prevAll( '.wpforms-field-entry-preview,.wpforms-field-pagebreak' ).first().hasClass( 'wpforms-field-entry-preview' ) ) {
$prevFieldPrevToggleField.attr( 'checked', 'checked' ).trigger( 'change' );
$prevFieldPrevToggle.addClass( 'wpforms-entry-preview-block' );
}
},
/**
* Show the error popup that the entry preview field blocks the field.
*
* @since 1.6.9
*
* @param {Event} e Event.
*/
entryPreviewBlockField( e ) {
e.preventDefault();
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.entry_preview_require_previous_button,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Is it an entry preview field that should be checked before adding?
*
* @since 1.6.9
*
* @param {string} type Field type.
* @param {Object} options Field options.
*
* @return {boolean} True when we should check it.
*/
isUncheckedEntryPreviewField( type, options ) {
// eslint-disable-next-line no-mixed-operators
return type === 'entry-preview' && ( ! options || options && ! options.passed );
},
/**
* Add an entry preview field to the form preview.
*
* @since 1.6.9
*
* @param {string} type Field type.
* @param {Object} options Field options.
*/
addEntryPreviewField( type, options ) { // eslint-disable-line complexity
const addButton = $( '#wpforms-add-fields-entry-preview' );
if ( addButton.hasClass( 'wpforms-entry-preview-adding' ) ) {
return;
}
const $fields = $( '#wpforms-panel-fields .wpforms-field-wrap > .wpforms-field' ),
position = options?.position ? options.position : $fields.length,
needPageBreakBefore = app.isEntryPreviewFieldRequiresPageBreakBefore( $fields, position ),
needPageBreakAfter = app.isEntryPreviewFieldRequiresPageBreakAfter( $fields, position );
addButton.addClass( 'wpforms-entry-preview-adding' );
if ( ! options ) {
options = {};
}
options.passed = true;
if ( ! needPageBreakBefore && ! needPageBreakAfter ) {
app.fieldAdd( 'entry-preview', options ).done( function( res ) {
app.lockEntryPreviewFieldsPosition( res.data.field.id );
} );
return;
}
if ( needPageBreakBefore ) {
app.addPageBreakAndEntryPreviewFields( options, position );
return;
}
app.addEntryPreviewAndPageBreakFields( options, position );
},
/**
* Add the entry preview field after the page break field.
* We should wait for the page break adding to avoid id duplication.
*
* @since 1.6.9
*
* @param {Object} options Field options.
*/
addEntryPreviewFieldAfterPageBreak( options ) {
const checkExist = setInterval( function() {
if ( $( '#wpforms-panel-fields .wpforms-field-wrap' ).find( '.wpforms-pagebreak-bottom, .wpforms-pagebreak-top' ).length === 2 ) {
app.fieldAdd( 'entry-preview', options ).done( function( res ) {
app.lockEntryPreviewFieldsPosition( res.data.field.id );
} );
clearInterval( checkExist );
}
}, 100 );
},
/**
* Add the entry preview field after the page break field.
*
* @since 1.6.9
*
* @param {Object} options Field options.
* @param {number} position The field position.
*/
addPageBreakAndEntryPreviewFields( options, position ) {
const hasPageBreak = $( '#wpforms-panel-fields .wpforms-field-wrap > .wpforms-field-pagebreak' ).length >= 3;
app.fieldAdd( 'pagebreak', { position } ).done( function( res ) {
options.position = hasPageBreak ? position + 1 : position + 2;
app.addEntryPreviewFieldAfterPageBreak( options );
const $pageBreakOptions = $( '#wpforms-field-option-' + res.data.field.id ),
$pageBreakPrevToggle = $pageBreakOptions.find( '.wpforms-field-option-row-prev_toggle' ),
$pageBreakPrevToggleField = $pageBreakPrevToggle.find( 'input' );
$pageBreakPrevToggleField.attr( 'checked', 'checked' ).trigger( 'change' );
$pageBreakPrevToggle.addClass( 'wpforms-entry-preview-block' );
} );
},
/**
* Duplicate field.
*
* @since 1.2.9
*
* @param {string} id Field id.
*/
fieldDuplicate( id ) {
const $field = $( `#wpforms-field-${ id }` );
if ( $field.hasClass( 'no-duplicate' ) ) {
$.alert( {
title: wpforms_builder.field_locked,
content: wpforms_builder.field_locked_no_duplicate_msg,
icon: 'fa fa-info-circle',
type: 'blue',
buttons: {
confirm: {
text: wpforms_builder.close,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
return;
}
$.confirm( {
title: false,
content: wpforms_builder.duplicate_confirm,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
// Disable the current button to avoid firing multiple click events.
// By default, "jconfirm" tends to destroy any modal DOM element upon button click.
this.$$confirm.prop( 'disabled', true );
const beforeEvent = WPFormsUtils.triggerEvent( $builder, 'wpformsBeforeFieldDuplicate', [ id, $field ] );
// Allow callbacks on `wpformsFieldBeforeDuplicate` to cancel field duplication.
if ( beforeEvent.isDefaultPrevented() ) {
return;
}
const newFieldId = app.fieldDuplicateRoutine( id, true ),
$newField = $( `#wpforms-field-${ newFieldId }` );
// Lastly, update the next ID stored in the database.
app.increaseNextFieldIdAjaxRequest();
WPFormsUtils.triggerEvent( $builder, 'wpformsFieldDuplicated', [ id, $field, newFieldId, $newField ] );
},
},
cancel: {
text: wpforms_builder.cancel,
},
},
} );
},
/**
* Update the next ID stored in the database.
*
* @since 1.7.7
*/
increaseNextFieldIdAjaxRequest() {
/* eslint-disable camelcase */
$.post(
wpforms_builder.ajax_url,
{
form_id: s.formID,
field_id: elements.$nextFieldId.val(),
nonce: wpforms_builder.nonce,
action: 'wpforms_builder_increase_next_field_id',
}
);
},
/**
* Duplicate field routine.
*
* @since 1.7.7
*
* @param {number|string} id Field ID.
* @param {boolean} changeLabel Is it necessary to change the label and add a copy suffix.
*
* @return {number} New field ID.
*/
fieldDuplicateRoutine( id, changeLabel = true ) { // eslint-disable-line max-lines-per-function, complexity
const $field = $( `#wpforms-field-${ id }` ),
$fieldOptions = $( `#wpforms-field-option-${ id }` ),
$fieldActive = elements.$sortableFieldsWrap.find( '>.active' ),
$visibleOptions = elements.$fieldOptions.find( '>:visible' ),
$visibleTab = $visibleOptions.find( '>.active' ),
type = $field.data( 'field-type' ),
fieldOptionsClass = $fieldOptions.attr( 'class' ),
isModernDropdown = app.dropdownField.helpers.isModernSelect( $field.find( '> .choices .primary-input' ) );
// Restore tooltips before cloning.
wpf.restoreTooltips( $fieldOptions );
// Force Modern Dropdown conversion to classic before cloning.
if ( isModernDropdown ) {
app.dropdownField.helpers.convertModernToClassic( id );
}
let newFieldOptions = $fieldOptions.html();
const $newField = $field.clone(),
newFieldID = parseInt( elements.$nextFieldId.val(), 10 ),
$fieldLabel = $( `#wpforms-field-option-${ id }-label` ),
fieldLabelVal = $fieldLabel.length ? $fieldLabel.val() : $( `#wpforms-field-option-${ id }-name` ).val(),
nextID = newFieldID + 1,
regex = {};
const newFieldLabel = fieldLabelVal !== ''
? `${ fieldLabelVal } ${ wpforms_builder.duplicate_copy }`
: `${ wpforms_builder.field } #${ id } ${ wpforms_builder.duplicate_copy }`;
regex.fieldOptionsID = new RegExp( 'ID #' + id, 'g' );
regex.fieldID = new RegExp( 'fields\\[' + id + '\\]', 'g' );
regex.dataFieldID = new RegExp( 'data-field-id="' + id + '"', 'g' );
regex.referenceID = new RegExp( 'data-reference="' + id + '"', 'g' );
regex.elementID = new RegExp( '\\b(id|for)="wpforms-(.*?)' + id + '(.*?)"', 'ig' );
// Toggle visibility states.
$field.after( $newField );
$fieldActive.removeClass( 'active' );
$newField.addClass( 'active' ).attr( {
id: `wpforms-field-${ newFieldID }`,
'data-field-id': newFieldID,
} );
// Various regexes to adjust the field options to work with the new field ID.
regex.elementIdReplace = function( match, p1, p2, p3 ) {
return `${ p1 }="wpforms-${ p2 }${ newFieldID }${ p3 }"`;
};
newFieldOptions = newFieldOptions.replace( regex.fieldOptionsID, `ID #${ newFieldID }` );
newFieldOptions = newFieldOptions.replace( regex.fieldID, `fields[${ newFieldID }]` );
newFieldOptions = newFieldOptions.replace( regex.dataFieldID, `data-field-id="${ newFieldID }"` );
newFieldOptions = newFieldOptions.replace( regex.referenceID, `data-reference="${ newFieldID }"` );
newFieldOptions = newFieldOptions.replace( regex.elementID, regex.elementIdReplace );
// Hide all field options panels.
$visibleOptions.hide();
// Add a new field options panel.
$fieldOptions.after( `<div class="${ fieldOptionsClass }" id="wpforms-field-option-${ newFieldID }" data-field-id="${ newFieldID }">${ newFieldOptions }</div>` );
// Get a new field options panel.
const $newFieldOptions = $( `#wpforms-field-option-${ newFieldID }` );
// If the user duplicates an active field.
if ( $fieldActive.data( 'field-id' ) === id && $visibleTab.length ) {
// The following will help identify which tab from the sidebar panel settings is currently being viewed,
// i.e., "General," "Advanced," "Smart Logic," etc.
const visibleTabClassName = $visibleTab.attr( 'class' ).match( /wpforms-field-option-group-\S*/i )[ 0 ];
const $newFieldOptionsTab = $newFieldOptions.find( `>.${ visibleTabClassName }` );
// Remove any left-over state from previously duplicated options.
$newFieldOptions.find( '>' ).removeClass( 'active' );
// Set the active tab to the same tab that was active before the duplication.
$newFieldOptionsTab.addClass( 'active' );
}
// If the user duplicates an inactive field.
if ( $fieldActive.data( 'field-id' ) !== id && $visibleTab.length ) {
// Remove the active class from the current active tab.
$newFieldOptions.find( '>' ).removeClass( 'active' );
// Set the active tab to "General".
$newFieldOptions.find( '>.wpforms-field-option-group-basic' ).addClass( 'active' );
}
// Copy over values.
$fieldOptions.find( ':input' ).each( function() { // eslint-disable-line complexity
const $this = $( this ),
name = $this.attr( 'name' );
if ( ! name ) {
return 'continue';
}
const newName = name.replace( regex.fieldID, `fields[${ newFieldID }]` ),
type = $this.attr( 'type' );
if ( type === 'checkbox' || type === 'radio' ) {
if ( $this.is( ':checked' ) ) {
$newFieldOptions.find( `[name="${ newName }"]` )
.prop( 'checked', true )
.attr( 'checked', 'checked' );
} else {
$newFieldOptions.find( `[name="${ newName }"]` )
.prop( 'checked', false )
.attr( 'checked', false );
}
return;
}
if ( $this.is( 'select' ) ) {
if ( $this.find( 'option:selected' ).length ) {
const optionVal = $this.find( 'option:selected' ).val();
$newFieldOptions.find( `[name="${ newName }"]` )
.find( `[value="${ optionVal }"]` )
.prop( 'selected', true );
}
return;
}
const value = $this.val();
if ( value === '' && $this.hasClass( 'wpforms-money-input' ) ) {
$newFieldOptions.find( `[name="${ newName }"]` ).val(
wpf.numberFormat( '0', wpforms_builder.currency_decimals, wpforms_builder.currency_decimal, wpforms_builder.currency_thousands )
);
} else {
// We've removed the empty value check here.
// If we are duplicating a field with no value, we should respect that.
$newFieldOptions.find( `[name="${ newName }"]` ).val( value );
}
} );
// ID adjustments.
$newFieldOptions.find( '.wpforms-field-option-hidden-id' ).val( newFieldID );
elements.$nextFieldId.val( nextID );
const $newFieldLabel = type === 'html' ? $( `#wpforms-field-option-${ newFieldID }-name` ) : $( `#wpforms-field-option-${ newFieldID }-label` );
// Adjust the label to indicate this is a copy.
if ( changeLabel ) {
$newFieldLabel.val( newFieldLabel ).trigger( 'input' );
}
// Fire field adds custom event.
$builder.trigger( 'wpformsFieldAdd', [ newFieldID, type ] );
// Re-init tooltips for a new field options panel.
wpf.initTooltips();
// Re-init Modern Dropdown.
if ( isModernDropdown ) {
app.dropdownField.helpers.convertClassicToModern( id );
app.dropdownField.helpers.convertClassicToModern( newFieldID );
}
// Re-init instance in choices related fields.
app.fieldChoiceUpdate( $newField.data( 'field-type' ), newFieldID );
// Re-init color pickers.
app.loadColorPickers();
return newFieldID;
},
/**
* Add the entry preview field before the page break field.
*
* @since 1.6.9
*
* @param {Object} options Field options.
* @param {number} position The field position.
*/
addEntryPreviewAndPageBreakFields( options, position ) {
app.fieldAdd( 'entry-preview', options ).done( function( res ) {
const entryPreviewId = res.data.field.id;
app.fieldAdd( 'pagebreak', { position: position + 1 } ).done( function( res ) {
app.lockEntryPreviewFieldsPosition( entryPreviewId );
const $pageBreakField = $( '#wpforms-field-' + res.data.field.id ),
$nextField = $pageBreakField.nextAll( '.wpforms-field-pagebreak, .wpforms-field-entry-preview' ).first();
if ( $nextField.hasClass( 'wpforms-field-entry-preview' ) ) {
app.lockEntryPreviewFieldsPosition( $nextField.data( 'field-id' ) );
}
} );
} );
},
/**
* Stick an entry preview field after adding.
*
* @since 1.6.9
*
* @param {number} id ID.
*/
lockEntryPreviewFieldsPosition( id ) {
const $entryPreviewField = $( '#wpforms-field-' + id ),
$pageBreakField = $entryPreviewField.prevAll( '.wpforms-field-pagebreak:not(.wpforms-pagebreak-bottom)' ).first(),
$nextPageBreakField = $entryPreviewField.nextAll( '.wpforms-field-pagebreak' ).first(),
nextPageBreakFieldId = $nextPageBreakField.data( 'field-id' ),
$pageBreakOptions = $( '#wpforms-field-option-' + nextPageBreakFieldId ),
$pageBreakPrevToggle = $pageBreakOptions.find( '.wpforms-field-option-row-prev_toggle' ),
$pageBreakPrevToggleField = $pageBreakPrevToggle.find( 'input' );
$entryPreviewField.addClass( 'wpforms-field-not-draggable' );
$pageBreakField.addClass( 'wpforms-field-not-draggable wpforms-field-entry-preview-not-deleted' );
$pageBreakPrevToggleField.prop( 'checked', 'checked' ).trigger( 'change' );
$pageBreakPrevToggle.addClass( 'wpforms-entry-preview-block' );
$( '#wpforms-add-fields-entry-preview' ).removeClass( 'wpforms-entry-preview-adding' );
$builder.trigger( 'wpformsFieldDragToggle', [ id, $entryPreviewField.data( 'field-type' ) ] );
$builder.trigger( 'wpformsFieldDragToggle', [ $pageBreakField.data( 'field-id' ), $pageBreakField.data( 'field-type' ) ] );
},
/**
* An entry preview field requires a page break that locates before.
*
* @since 1.6.9
*
* @param {jQuery} $fields List of fields in the form preview.
* @param {number} position The field position.
*
* @return {boolean} True if we need to add a page break field before.
*/
isEntryPreviewFieldRequiresPageBreakBefore( $fields, position ) {
const $beforeFields = $fields.slice( 0, position ).filter( '.wpforms-field-pagebreak,.wpforms-field-entry-preview' );
let needPageBreakBefore = true;
if ( ! $beforeFields.length ) {
return needPageBreakBefore;
}
$( $beforeFields.get().reverse() ).each( function() {
const $this = $( this );
if ( $this.hasClass( 'wpforms-field-entry-preview' ) ) {
return false;
}
if ( $this.hasClass( 'wpforms-field-pagebreak' ) && ! $this.hasClass( 'wpforms-field-stick' ) ) {
needPageBreakBefore = false;
return false;
}
} );
return needPageBreakBefore;
},
/**
* An entry preview field requires a page break that locates after.
*
* @since 1.6.9
*
* @param {jQuery} $fields List of fields in the form preview.
* @param {number} position The field position.
*
* @return {boolean} True if we need to add a page break field after.
*/
isEntryPreviewFieldRequiresPageBreakAfter( $fields, position ) {
const $afterFields = $fields.slice( position ).filter( '.wpforms-field-pagebreak,.wpforms-field-entry-preview' );
let needPageBreakAfter = Boolean( $afterFields.length );
if ( ! $afterFields.length ) {
return needPageBreakAfter;
}
$afterFields.each( function() {
const $this = $( this );
if ( $this.hasClass( 'wpforms-field-entry-preview' ) ) {
return false;
}
if ( $this.hasClass( 'wpforms-field-pagebreak' ) ) {
needPageBreakAfter = false;
return false;
}
} );
return needPageBreakAfter;
},
/**
* Add a new field.
*
* @since 1.0.0
* @since 1.6.4 Added hCaptcha support.
*
* @param {string} type Field type.
* @param {Object} options Additional options.
*
* @return {Promise|void|jqXHR} jQuery.post() promise interface.
*/
fieldAdd( type, options ) { // eslint-disable-line max-lines-per-function
const $btn = $( `#wpforms-add-fields-${ type }` );
if ( $btn.hasClass( 'upgrade-modal' ) || $btn.hasClass( 'education-modal' ) || $btn.hasClass( 'warning-modal' ) ) {
return;
}
if ( [ 'captcha_turnstile', 'captcha_hcaptcha', 'captcha_recaptcha', 'captcha_none' ].includes( type ) ) {
app.captchaUpdate();
return;
}
adding = true;
WPForms.Admin.Builder.DragFields.disableDragAndDrop();
app.disableFormActions();
if ( app.isUncheckedEntryPreviewField( type, options ) ) {
app.addEntryPreviewField( type, options );
return;
}
const defaults = {
position: 'bottom',
$sortable: 'base',
placeholder: false,
scroll: true,
defaults: false,
};
options = $.extend( {}, defaults, options );
const data = {
action: 'wpforms_new_field_' + type,
id: s.formID,
type,
defaults: options.defaults,
nonce: wpforms_builder.nonce,
};
return $.post( wpforms_builder.ajax_url, data, function( res ) { // eslint-disable-line complexity
if ( ! res.success ) {
wpf.debug( 'Add field AJAX call is unsuccessful:', res );
return;
}
const $baseFieldsContainer = elements.$sortableFieldsWrap,
/** @type {JQ|jQuery} */ $newField = $( res.data.preview ),
/** @type {JQ|jQuery} */ $newOptions = $( res.data.options );
let $fieldContainer = options.$sortable;
adding = false;
$newField.css( 'display', 'none' );
if ( options.placeholder ) {
options.placeholder.remove();
}
if ( options.$sortable === 'default' || ! options.$sortable.length ) {
$fieldContainer = $baseFieldsContainer.find( '.wpforms-fields-sortable-default' );
}
if ( options.$sortable === 'base' || ! $fieldContainer.length ) {
$fieldContainer = $baseFieldsContainer;
}
const event = WPFormsUtils.triggerEvent(
$builder,
'wpformsBeforeFieldAddToDOM',
[ options, $newField, $newOptions, $fieldContainer ]
);
// Allow callbacks on `wpformsBeforeFieldAddToDOM` to cancel adding field
// by triggering `event.preventDefault()`.
if ( event.isDefaultPrevented() ) {
return;
}
// Add a field to the base level of fields.
// Allow callbacks on `wpformsBeforeFieldAddToDOM` to skip adding field to the base level
// by setting `event.skipAddFieldToBaseLevel = true`.
if ( ! event.skipAddFieldToBaseLevel ) {
app.fieldAddToBaseLevel( options, $newField, $newOptions );
}
$newField.fadeIn();
$builder.find( '.no-fields, .no-fields-preview' ).remove();
if ( $( '.wpforms-field-option:not(.wpforms-field-option-layout)' ).length ) {
$builder.find( '.wpforms-field-submit' ).show();
}
// Scroll to the added field.
if ( options.scroll && options.position.length ) {
app.scrollPreviewToField( res.data.field.id );
}
// Update next field id hidden input value.
elements.$nextFieldId.val( res.data.field.id + 1 );
wpf.initTooltips();
app.loadColorPickers();
app.toggleAllOptionGroups();
$builder.trigger( 'wpformsFieldAdd', [ res.data.field.id, type ] );
} ).fail( function( xhr ) {
adding = false;
wpf.debug( 'Add field AJAX call failed:', xhr.responseText );
} ).always( function() {
if ( ! adding ) {
WPForms.Admin.Builder.DragFields.enableDragAndDrop();
app.enableFormActions();
}
} );
},
/**
* Add a new field to the base level of fields.
*
* @since 1.7.7
*
* @param {Options} options Field add additional options.
* @param {jQuery} $newField New field preview object.
* @param {jQuery} $newOptions New field options object.
*/
fieldAddToBaseLevel( options, $newField, $newOptions ) { // eslint-disable-line complexity
const $baseFieldsContainer = elements.$sortableFieldsWrap,
$baseFields = $baseFieldsContainer.children( ':not(.wpforms-field-drag-pending, .no-fields-preview)' ),
totalBaseFields = $baseFields.length;
const $fieldOptions = elements.$fieldOptions;
if ( options.position === 'top' ) {
// Add a field to the top of base level fields.
$baseFieldsContainer.prepend( $newField );
$fieldOptions.prepend( $newOptions );
return;
}
const $lastBaseField = $baseFields.last();
if (
options.position === 'bottom' && (
! $lastBaseField.length ||
! $lastBaseField.hasClass( 'wpforms-field-stick' )
)
) {
// Add a field to the bottom of base level fields.
$baseFieldsContainer.append( $newField );
$fieldOptions.append( $newOptions );
return;
}
if ( options.position === 'bottom' ) {
options.position = totalBaseFields;
}
if (
options.position === totalBaseFields &&
$lastBaseField.length && $lastBaseField.hasClass( 'wpforms-field-stick' )
) {
const lastBaseFieldId = $lastBaseField.data( 'field-id' );
// Check to see if the last field we have is configured to
// be stuck to the bottom, if so, add the field above it.
$lastBaseField.before( $newField );
$fieldOptions.find( `#wpforms-field-option-${ lastBaseFieldId }` ).before( $newOptions );
return;
}
const $fieldInPosition = $baseFields.eq( options.position );
if ( $fieldInPosition.length ) {
const fieldInPositionId = $fieldInPosition.data( 'field-id' );
// Add a field to a specific location.
$fieldInPosition.before( $newField );
$fieldOptions.find( `#wpforms-field-option-${ fieldInPositionId }` ).before( $newOptions );
return;
}
// Something is wrong. Add the field. This should never occur.
$baseFieldsContainer.append( $newField );
$fieldOptions.append( $newOptions );
},
/**
* Scroll the preview panel to the desired field.
*
* @since 1.7.7
*
* @param {number} fieldId Field ID.
*/
scrollPreviewToField( fieldId ) {
const $field = $( `#wpforms-field-${ fieldId }` ),
scrollTop = elements.$fieldsPreviewWrap.scrollTop(),
$layoutField = $field.closest( '.wpforms-field-layout' );
let fieldPosition = $field.position().top;
if ( $layoutField.length ) {
fieldPosition = $layoutField.position().top + fieldPosition + 20;
}
const scrollAmount = fieldPosition > scrollTop ? fieldPosition - scrollTop : fieldPosition + scrollTop;
elements.$fieldsPreviewWrap.scrollTop( scrollAmount );
},
/**
* Update CAPTCHA form setting.
*
* @since 1.6.4
*
* @return {Object} jqXHR.
*/
captchaUpdate() {
const data = {
action: 'wpforms_update_field_captcha',
id: s.formID,
nonce: wpforms_builder.nonce,
};
return $.post( wpforms_builder.ajax_url, data, function( res ) {
if ( res.success ) {
const args = {
title: false,
content: false,
icon: 'fa fa-exclamation-circle',
type: 'orange',
boxWidth: '450px',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
},
$enableCheckbox = $( '#wpforms-panel-field-settings-recaptcha' );
let caseName = res.data.current;
$enableCheckbox.data( 'provider', res.data.provider );
// Possible cases:
//
// not_configured - IF CAPTCHA is not configured in the WPForms plugin settings
// configured_not_enabled - IF CAPTCHA is configured in WPForms plugin settings, but wasn't set in form settings
// configured_enabled - IF CAPTCHA is configured in WPForms plugin and form settings
if ( 'configured_not_enabled' === caseName || 'configured_enabled' === caseName ) {
// Get a correct case name.
caseName = $enableCheckbox.prop( 'checked' ) ? 'configured_enabled' : 'configured_not_enabled';
// Check/uncheck a `CAPTCHA` checkbox in form setting.
args.buttons.confirm.action = function() {
$enableCheckbox.prop( 'checked', ( 'configured_not_enabled' === caseName ) ).trigger( 'change' );
};
}
args.title = res.data.cases[ caseName ].title;
args.content = res.data.cases[ caseName ].content;
// Do you need a Cancel button?
if ( res.data.cases[ caseName ].cancel ) {
args.buttons.cancel = {
text: wpforms_builder.cancel,
keys: [ 'esc' ],
};
}
// Call a Confirm modal.
$.confirm( args );
} else {
// eslint-disable-next-line no-console
console.log( res );
}
} ).fail( function( xhr ) {
// eslint-disable-next-line no-console
console.log( xhr.responseText );
} );
},
/**
* Disable drag & drop.
*
* @since 1.7.1
* @since 1.7.7 Deprecated.
*
* @deprecated Use `WPForms.Admin.Builder.DragFields.disableDragAndDrop()` instead.
*/
disableDragAndDrop() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.disableDragAndDrop()" has been deprecated, please use the new "WPForms.Admin.Builder.DragFields.disableDragAndDrop()" function instead!' );
WPForms.Admin.Builder.DragFields.disableDragAndDrop();
},
/**
* Enable drag & drop.
*
* @since 1.7.1
* @since 1.7.7 Deprecated.
*
* @deprecated Use `WPForms.Admin.Builder.DragFields.enableDragAndDrop()` instead.
*/
enableDragAndDrop() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.enableDragAndDrop()" has been deprecated, please use the new "WPForms.Admin.Builder.DragFields.enableDragAndDrop()" function instead!' );
WPForms.Admin.Builder.DragFields.enableDragAndDrop();
},
/**
* Disable Preview, Embed, Save form actions, and Form Builder exit button.
*
* @since 1.7.4
*/
disableFormActions() {
$.each(
[
elements.$previewButton,
elements.$embedButton,
elements.$saveButton,
elements.$exitButton,
],
function( _index, button ) {
button.prop( 'disabled', true ).addClass( 'wpforms-disabled' );
}
);
},
/**
* Enable Preview, Embed, Save form actions, and Form Builder exit button.
*
* @since 1.7.4
*/
enableFormActions() {
$.each(
[
elements.$previewButton,
elements.$embedButton,
elements.$saveButton,
elements.$exitButton,
],
function( _index, button ) {
button.prop( 'disabled', false ).removeClass( 'wpforms-disabled' );
}
);
},
/**
* Sortable fields in the builder form the preview area.
*
* @since 1.0.0
* @since 1.7.7 Deprecated.
*
* @deprecated Use `WPForms.Admin.Builder.DragFields.initSortableFields()` instead.
*/
fieldSortable() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.fieldSortable()" has been deprecated, please use the new "WPForms.Admin.Builder.DragFields.initSortableFields()" function instead!' );
WPForms.Admin.Builder.DragFields.initSortableFields();
},
/**
* Show a popup in case if the field is not draggable and cancel moving.
*
* @since 1.7.5
* @since 1.7.6 The showPopUp parameter added.
* @since 1.7.7 Deprecated.
*
* @deprecated Use `WPForms.Admin.Builder.DragFields.fieldDragDisable()` instead.
*
* @param {jQuery} $field A field or list of fields.
* @param {boolean} showPopUp Whether the pop-up should be displayed on a dragging attempt.
*/
fieldDragDisable( $field, showPopUp = true ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.fieldDragDisable()" has been deprecated, please use the new "WPForms.Admin.Builder.DragFields.fieldDragDisable()" function instead!' );
WPForms.Admin.Builder.DragFields.fieldDragDisable( $field, showPopUp );
},
/**
* Allow field dragging.
*
* @since 1.7.5
* @since 1.7.7 Deprecated.
*
* @deprecated Use `WPForms.Admin.Builder.DragFields.fieldDragEnable()` instead.
*
* @param {jQuery} $field A field or list of fields.
*/
fieldDragEnable( $field ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsBuilder.fieldDragEnable()" has been deprecated, please use the new "WPForms.Admin.Builder.DragFields.fieldDragEnable()" function instead!' );
WPForms.Admin.Builder.DragFields.fieldDragEnable( $field );
},
/**
* Add a new field choice.
*
* @since 1.0.0
*
* @param {Event} event Event.
* @param {Element} el Element.
*/
fieldChoiceAdd( event, el ) {
event.preventDefault();
const $this = $( el ),
$parent = $this.parent(),
checked = $parent.find( 'input.default' ).is( ':checked' ),
fieldID = $this.closest( '.wpforms-field-option-row-choices' ).data( 'field-id' );
let id = $parent.parent().attr( 'data-next-id' );
const type = $parent.parent().data( 'field-type' ),
$choice = $parent.clone().insertAfter( $parent );
$choice.attr( 'data-key', id );
$choice.find( '.wpforms-icon-select .ic-fa-preview' ).removeClass().addClass( 'ic-fa-preview ic-fa-' + wpforms_builder.icon_choices.default_icon_style + ' ic-fa-' + wpforms_builder.icon_choices.default_icon );
$choice.find( '.wpforms-icon-select .ic-fa-preview + span' ).text( wpforms_builder.icon_choices.default_icon );
$choice.find( '.preview' ).empty();
$choice.find( '.wpforms-image-upload-add' ).show();
$choice.find( '.wpforms-money-input' ).trigger( 'focusout' );
$choice.find( 'input, select' ).each( function() {
const $field = $( this ),
type = $field.attr( 'type' );
$field.attr( 'name', $( this ).attr( 'name' ).replace( /\[choices]\[(\d+)]/g, `[choices][${ id }]` ) );
if ( type === 'radio' || type === 'checkbox' ) {
$field.prop( 'checked', false );
} else {
$field.val( '' );
}
} );
$choice.find( '.wpforms-icon-select input.source-icon' ).val( wpforms_builder.icon_choices.default_icon );
$choice.find( '.wpforms-icon-select input.source-icon-style' ).val( wpforms_builder.icon_choices.default_icon_style );
if ( checked === true ) {
$parent.find( 'input.default' ).prop( 'checked', true );
}
id++;
$parent.parent().attr( 'data-next-id', id );
$builder.trigger( 'wpformsFieldChoiceAdd', [ fieldID ] );
app.fieldChoiceUpdate( type, fieldID );
},
/**
* Delete field choice.
*
* @since 1.0.0
*
* @param {Event} e Event.
* @param {Element} el Element.
*/
fieldChoiceDelete( e, el ) {
e.preventDefault();
const $this = $( el ),
$list = $this.parent().parent(),
total = $list.find( 'li' ).not( '.wpforms-choice-other-option' ).length,
fieldData = {
id: $list.data( 'field-id' ),
choiceId: $this.closest( 'li' ).data( 'key' ),
message: '<strong>' + wpforms_builder.delete_choice_confirm + '</strong>',
trigger: false,
};
$builder.trigger( 'wpformsBeforeFieldDeleteAlert', [ fieldData ] );
if ( total === 1 ) {
app.fieldChoiceDeleteAlert();
} else {
const deleteChoice = function() {
$this.parent().remove();
app.fieldChoiceUpdate( $list.data( 'field-type' ), $list.data( 'field-id' ) );
$builder.trigger( 'wpformsFieldChoiceDelete', [ $list.data( 'field-id' ) ] );
};
if ( ! fieldData.trigger ) {
deleteChoice();
return;
}
$.confirm( {
title: false,
content: fieldData.message,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
deleteChoice();
},
},
cancel: {
text: wpforms_builder.cancel,
},
},
} );
}
},
/**
* Field choice delete error alert.
*
* @since 1.6.7
*/
fieldChoiceDeleteAlert() {
$.alert( {
title: false,
content: wpforms_builder.error_choice,
icon: 'fa fa-info-circle',
type: 'blue',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Make field choices sortable.
* Currently used for select, radio, and checkboxes field types.
*
* @since 1.0.0
*
* @param {string} type Type.
* @param {string|undefined} selector Selector.
*/
fieldChoiceSortable( type, selector = undefined ) {
selector = typeof selector !== 'undefined' ? selector : '.wpforms-field-option-' + type + ' .wpforms-field-option-row-choices ul';
$( selector ).one( 'mouseenter', function() { // eslint-disable-line max-lines-per-function
$( this ).sortable( {
items: 'li:not(.not-draggable)',
axis: 'y',
delay: 100,
opacity: 0.6,
handle: '.move',
stop( e, ui ) {
const id = ui.item.parent().data( 'field-id' );
app.fieldChoiceUpdate( type, id );
$builder.trigger( 'wpformsFieldChoiceMove', ui );
},
update() {
},
} );
} );
},
/**
* Generate Choice label.
* Used in a field preview template.
*
* @since 1.6.2
*
* @param {Object} data Template data.
* @param {number} choiceID Choice ID.
*
* @return {string} Label.
*/
fieldChoiceLabel( data, choiceID ) { // eslint-disable-line complexity
const isPaymentChoice = [ 'payment-multiple', 'payment-checkbox' ].includes( data.settings.type ),
isIconImageChoice = data.settings.choices_icons || data.settings.choices_images,
isEmptyLabel = typeof data.settings.choices[ choiceID ].label === 'undefined' || data.settings.choices[ choiceID ].label.length === 0;
// Do not set a placeholder for an empty label in Icon and Image choices except for payment fields.
if ( isEmptyLabel && ! isPaymentChoice && isIconImageChoice ) {
return '';
}
const placeholder = isPaymentChoice ? wpforms_builder.payment_choice_empty_label_tpl : wpforms_builder.choice_empty_label_tpl;
let label = ! isEmptyLabel
? wpf.sanitizeHTML( data.settings.choices[ choiceID ].label, wpforms_builder.allowed_label_html_tags )
: placeholder.replace( '{number}', choiceID );
if ( data.settings.show_price_after_labels ) {
label += ' - ' + wpf.amountFormatCurrency( data.settings.choices[ choiceID ].value );
}
return label;
},
/**
* Update field choices in the preview area for the Fields panel.
*
* Currently used for select, radio, and checkboxes field types.
*
* @param {string} type Field type.
* @param {string|number} id Field ID.
* @param {number} count Number of choices to show, -1 if not set.
*
* @since 1.0.0
*/
fieldChoiceUpdate: ( type, id, count = -1 ) => { // eslint-disable-line complexity, max-lines-per-function
const isDynamicChoices = app.dropdownField.helpers.isDynamicChoices( id );
if ( app.replaceChoicesWithTemplate( type, id, isDynamicChoices ) ) {
return;
}
if ( count === -1 ) {
count = app.settings.choicesLimitLong;
}
// Dropdown payment choices are of a select type.
if ( 'payment-select' === type ) {
type = 'select';
}
const $primary = $( '#wpforms-field-' + id + ' .primary-input' );
let newChoice = '';
if ( 'select' === type ) {
if ( ! isDynamicChoices ) {
newChoice = '<option value="{label}">{label}</option>';
$primary.find( 'option' ).not( '.placeholder' ).remove();
}
} else if ( 'radio' === type || 'checkbox' === type || 'gdpr-checkbox' === type ) {
type = 'gdpr-checkbox' === type ? 'checkbox' : type;
$primary.find( 'li' ).remove();
newChoice = '<li><input type="' + type + '" disabled>{label}</li>';
}
// Building an inner content for the Primary field.
const $choicesList = $( `#wpforms-field-option-row-${ id }-choices .choices-list` ),
$choicesToRender = $choicesList.find( 'li' ).slice( 0, count ),
hasDefaults = !! $choicesList.find( 'input.default:checked' ).length,
modernSelectChoices = [],
showPriceAfterLabels = $( '#wpforms-field-option-' + id + '-show_price_after_labels' ).prop( 'checked' ),
isModernSelect = app.dropdownField.helpers.isModernSelect( $primary );
$choicesToRender.get().forEach( function( item ) {// eslint-disable-line complexity
const $this = $( item ),
value = $this.find( 'input.value' ).val(),
choiceID = $this.data( 'key' );
let label = wpf.sanitizeHTML( $this.find( 'input.label' ).val().trim(), wpforms_builder.allowed_label_html_tags ),
$choice;
label = label !== '' ? label : wpforms_builder.choice_empty_label_tpl.replace( '{number}', choiceID );
label += ( showPriceAfterLabels && value ) ? ' - ' + wpf.amountFormatCurrency( value ) : '';
// Append a new choice.
if ( ! isModernSelect ) {
if ( ! isDynamicChoices ) {
$choice = $( newChoice.replace( /{label}/g, label ) );
$primary.append( $choice );
}
} else {
modernSelectChoices.push(
{
value: label,
label,
}
);
}
const selected = $this.find( 'input.default' ).is( ':checked' );
if ( true === selected ) {
switch ( type ) {
case 'select':
if ( ! isModernSelect ) {
app.setClassicSelectedChoice( $choice );
} else {
modernSelectChoices[ modernSelectChoices.length - 1 ].selected = true;
}
break;
case 'radio':
case 'checkbox':
$choice.find( 'input' ).prop( 'checked', 'true' );
break;
}
}
} );
if ( isModernSelect ) {
const placeholderClass = $primary.prop( 'multiple' ) ? 'input.choices__input' : '.choices__inner .choices__placeholder',
choicesjsInstance = app.dropdownField.helpers.getInstance( $primary );
if ( ! isDynamicChoices ) {
choicesjsInstance.removeActiveItems();
}
choicesjsInstance.setChoices( modernSelectChoices, 'value', 'label', true );
// Re-initialize the modern dropdown to properly determine and update the placeholder.
app.dropdownField.helpers.update( id, isDynamicChoices );
// Hide/show a placeholder for Modern select if it has or not default choices.
$primary
.closest( '.choices' )
.find( placeholderClass )
.toggleClass( 'wpforms-hidden', hasDefaults );
}
},
/**
* Generate Choice label.
* Used in a field preview template.
*
* @since 1.8.6
*
* @param {string} type Field type.
* @param {number} id Field ID.
* @param {boolean} isDynamicChoices Whether the field has dynamic choices.
*
* @return {boolean} True if the template was used.
*/
replaceChoicesWithTemplate: ( type, id, isDynamicChoices ) => { // eslint-disable-line complexity
// Radio, Checkbox, and Payment Multiple/Checkbox use _ template.
if ( 'radio' !== type && 'checkbox' !== type && 'payment-multiple' !== type && 'payment-checkbox' !== type ) {
return false;
}
const order = wpf.getChoicesOrder( id ),
tmpl = wp.template( 'wpforms-field-preview-checkbox-radio-payment-multiple' );
const fieldSettings = wpf.getField( id ),
slicedChoices = {},
slicedOrder = order.slice( 0, app.settings.choicesLimit ),
data = {
settings: fieldSettings,
order: slicedOrder,
type: 'radio',
};
// If Icon Choices is on, get the valid color.
if ( fieldSettings.choices_icons ) {
// eslint-disable-next-line camelcase
data.settings.choices_icons_color = app.getValidColorPickerValue( $( '#wpforms-field-option-' + id + '-choices_icons_color' ) );
}
// Slice choices for preview.
slicedOrder.forEach( function( entry ) {
slicedChoices[ entry ] = fieldSettings.choices[ entry ];
} );
fieldSettings.choices = slicedChoices;
if ( 'checkbox' === type || 'payment-checkbox' === type ) {
data.type = 'checkbox';
}
if ( ! isDynamicChoices ) {
$( '#wpforms-field-' + id ).find( 'ul.primary-input' ).replaceWith( tmpl( data ) );
}
// Toggle limit choices alert message.
app.firstNChoicesAlert( id, order.length );
return true;
},
/**
* Set the classic selected choice.
*
* @since 1.8.2.3
*
* @param {jQuery|undefined} $choice Choice option.
*/
setClassicSelectedChoice( $choice ) {
if ( $choice === undefined ) {
return;
}
$choice.prop( 'selected', 'true' );
},
/**
* Field choice bulk add toggling.
*
* @since 1.3.7
*
* @param {Object} el jQuery object.
*/
fieldChoiceBulkAddToggle( el ) {
const /** @type {JQ|jQuery} */ $this = $( el ),
/** @type {JQ|jQuery} */ $label = $this.closest( 'label' );
if ( $this.hasClass( 'bulk-add-showing' ) ) {
// "Import details" is showing, so hide/remove it.
const $selector = $label.next( '.bulk-add-display' );
$selector.slideUp( 400, function() {
$selector.remove();
} );
$this.find( 'span' ).text( wpforms_builder.bulk_add_show );
} else {
let importOptions = '<div class="bulk-add-display unfoldable-cont">';
importOptions += '<p class="heading wpforms-clear">' + wpforms_builder.bulk_add_heading + ' <a href="#" class="toggle-bulk-add-presets">' + wpforms_builder.bulk_add_presets_show + '</a></p>';
importOptions += '<ul>';
for ( const key in wpforms_preset_choices ) {
importOptions += '<li><a href="#" data-preset="' + key + '" class="bulk-add-preset-insert">' + wpforms_preset_choices[ key ].name + '</a></li>';
}
importOptions += '</ul>';
importOptions += '<textarea placeholder="' + wpforms_builder.bulk_add_placeholder + '"></textarea>';
importOptions += '<button class="bulk-add-insert wpforms-btn wpforms-btn-sm wpforms-btn-blue">' + wpforms_builder.bulk_add_button + '</button>';
importOptions += '</div>';
$label.after( importOptions );
$label.next( '.bulk-add-display' ).slideDown( 400, function() {
$( this ).find( 'textarea' ).trigger( 'focus' );
} );
$this.find( 'span' ).text( wpforms_builder.bulk_add_hide );
}
$this.toggleClass( 'bulk-add-showing' );
},
/**
* Field choice bulk insert the new choices.
*
* @since 1.3.7
*
* @param {Object} el DOM element.
*/
fieldChoiceBulkAddInsert( el ) {
const $this = $( el ),
$container = $this.closest( '.wpforms-field-option-row' ),
$textarea = $container.find( 'textarea' ),
$list = $container.find( '.choices-list' ),
$choice = $list.find( 'li:first-of-type' ).clone().wrap( '<div>' ).parent();
let choice = '';
const fieldID = $container.data( 'field-id' ),
type = $list.data( 'field-type' );
let nextID = Number( $list.attr( 'data-next-id' ) );
const newValues = $textarea.val().split( '\n' );
let newChoices = '';
$this.prop( 'disabled', true ).html( $this.html() + ' ' + s.spinner );
$choice.find( 'input.value,input.label' ).attr( 'value', '' );
$choice.find( 'input.default' ).attr( 'checked', false );
$choice.find( 'input.source-icon' ).attr( 'value', wpforms_builder.icon_choices.default_icon );
$choice.find( 'input.source-icon-style' ).attr( 'value', wpforms_builder.icon_choices.default_icon_style );
$choice.find( '.ic-fa-preview' ).removeClass().addClass( `ic-fa-preview ic-fa-${ wpforms_builder.icon_choices.default_icon_style } ic-fa-${ wpforms_builder.icon_choices.default_icon }` );
$choice.find( '.ic-fa-preview + span' ).text( wpforms_builder.icon_choices.default_icon );
choice = $choice.html();
for ( const key in newValues ) {
if ( ! newValues.hasOwnProperty( key ) ) {
continue;
}
const value = wpf.sanitizeHTML( newValues[ key ] ).trim().replace( /"/g, '&quot;' );
let newChoice = choice;
newChoice = newChoice.replace( /\[choices]\[(\d+)]/g, '[choices][' + nextID + ']' );
newChoice = newChoice.replace( /data-key="(\d+)"/g, 'data-key="' + nextID + '"' );
newChoice = newChoice.replace( /value="" class="label"/g, 'value="' + value + '" class="label"' );
// For some reason, IE has its own attribute order.
newChoice = newChoice.replace( /class="label" type="text" value=""/g, 'class="label" type="text" value="' + value + '"' );
newChoices += newChoice;
nextID++;
}
$list.attr( 'data-next-id', nextID );
// Ensure the "Other" choice remains the last item in the list.
const $other = $list.find( 'li.wpforms-choice-other-option' ).first();
if ( $other.length ) {
$other.before( newChoices );
} else {
$list.append( newChoices );
}
app.fieldChoiceUpdate( type, fieldID, nextID );
$builder.trigger( 'wpformsFieldChoiceAdd' );
app.fieldChoiceBulkAddToggle( $container.find( '.toggle-bulk-add-display' ) );
},
/**
* Trigger $builder event.
*
* @since 1.9.1
*
* @param {string} event Event name.
*/
triggerBuilderEvent( event ) {
$builder.trigger( event );
},
/**
* Toggle fields tabs (Add Fields, Field Options).
*
* @since 1.0.0
*
* @param {number|string} id Field ID or `add-fields` or `field-options`.
*
* @return {false|void} False if event is prevented.
*/
fieldTabToggle( id ) {
const event = WPFormsUtils.triggerEvent( $builder, 'wpformsFieldTabToggle', [ id ] );
// Allow callbacks on `wpformsFieldTabToggle` to cancel tab toggle by triggering `event.preventDefault()`.
if ( event.isDefaultPrevented() ) {
return false;
}
$( '.wpforms-tab a' ).removeClass( 'active' );
$( '.wpforms-field, .wpforms-title-desc' ).removeClass( 'active' );
if ( id === 'add-fields' ) {
elements.$addFieldsTab.addClass( 'active' );
$( '.wpforms-field-options' ).hide();
$( '.wpforms-add-fields' ).show();
} else {
$( '#field-options a' ).addClass( 'active' );
if ( id === 'field-options' ) {
const $field = $( '.wpforms-field' ).first();
$field.addClass( 'active' );
id = $field.data( 'field-id' );
} else {
$( '#wpforms-field-' + id ).addClass( 'active' );
}
$( '.wpforms-field-option' ).hide();
$( '#wpforms-field-option-' + id ).show();
$( '.wpforms-add-fields' ).hide();
$( '.wpforms-field-options' ).show();
$builder.trigger( 'wpformsFieldOptionTabToggle', [ id ] );
}
},
/**
* Watches fields being added and listens for a pagebreak field.
*
* If a pagebreak field is added, and it's the first one, then we
* automatically add the top and bottom pagebreak elements to the
* builder.
*
* @param {Object} event Current DOM event.
* @param {number} id Field ID.
* @param {string} type Field type.
*
* @since 1.2.1
*/
fieldPagebreakAdd( event, id, type ) {
/* eslint-disable camelcase */
if ( 'pagebreak' !== type ) {
return;
}
let options;
if ( ! s.pagebreakTop ) {
s.pagebreakTop = true;
options = {
position: 'top',
scroll: false,
defaults: {
position: 'top',
nav_align: 'left',
},
};
app.fieldAdd( 'pagebreak', options ).done( function( res ) {
s.pagebreakTop = res.data.field.id;
const $preview = $( '#wpforms-field-' + res.data.field.id ),
$options = $( '#wpforms-field-option-' + res.data.field.id );
$options.find( '.wpforms-field-option-group' ).addClass( 'wpforms-pagebreak-top' );
$preview.addClass( 'wpforms-field-stick wpforms-pagebreak-top' );
} );
} else if ( ! s.pagebreakBottom ) {
s.pagebreakBottom = true;
options = {
position: 'bottom',
scroll: false,
defaults: {
position: 'bottom',
},
};
app.fieldAdd( 'pagebreak', options ).done( function( res ) {
s.pagebreakBottom = res.data.field.id;
const $preview = $( '#wpforms-field-' + res.data.field.id ),
$options = $( '#wpforms-field-option-' + res.data.field.id );
$options.find( '.wpforms-field-option-group' ).addClass( 'wpforms-pagebreak-bottom' );
$preview.addClass( 'wpforms-field-stick wpforms-pagebreak-bottom' );
} );
}
},
/**
* Watches fields being deleted and listens for a pagebreak field.
*
* If a pagebreak field is added, and it's the first one, then we
* automatically add the top and bottom pagebreak elements to the
* builder.
*
* @param {Object} event Current DOM event.
* @param {number} id Field ID.
* @param {string} type Field type.
*
* @since 1.2.1
*/
fieldPagebreakDelete( event, id, type ) {
if ( 'pagebreak' !== type ) {
return;
}
const pagebreaksRemaining = $( '#wpforms-panel-fields .wpforms-field-pagebreak' ).not( '.wpforms-pagebreak-top, .wpforms-pagebreak-bottom' ).length;
if ( pagebreaksRemaining ) {
return;
}
// All pagebreaks, excluding top/bottom, are gone.
// So we need to remove the top and bottom pagebreak.
const $preview = $( '#wpforms-panel-fields .wpforms-preview-wrap' ),
$top = $preview.find( '.wpforms-pagebreak-top' ),
topID = $top.data( 'field-id' ),
$bottom = $preview.find( '.wpforms-pagebreak-bottom' ),
bottomID = $bottom.data( 'field-id' );
$top.remove();
$( '#wpforms-field-option-' + topID ).remove();
s.pagebreakTop = false;
$bottom.remove();
$( '#wpforms-field-option-' + bottomID ).remove();
s.pagebreakBottom = false;
},
/**
* Init Display Previous option for Pagebreak field.
*
* @since 1.5.8
*
* @param {jQuery} $field Page Break field jQuery object.
*/
fieldPageBreakInitDisplayPrevious( $field ) {
const id = $field.data( 'field-id' ),
$prevToggle = $( '#wpforms-field-option-row-' + id + '-prev_toggle' ),
$prev = $( '#wpforms-field-option-row-' + id + '-prev' ),
$prevBtn = $field.find( '.wpforms-pagebreak-prev' );
if ( $field.prevAll( '.wpforms-field-pagebreak.wpforms-pagebreak-normal' ).length > 0 ) {
$prevToggle.removeClass( 'hidden' );
$prev.removeClass( 'hidden' );
if ( $prevToggle.find( 'input' ).is( ':checked' ) ) {
$prevBtn.removeClass( 'wpforms-hidden' ).text( $prev.find( 'input' ).val() );
}
} else {
$prevToggle.addClass( 'hidden' );
$prev.addClass( 'hidden' );
$prevBtn.addClass( 'wpforms-hidden' );
}
},
/**
* Field Dynamic Choice toggle.
*
* @since 1.2.8
*
* @param {Element} el Element.
*/
fieldDynamicChoiceToggle( el ) { // eslint-disable-line complexity, max-lines-per-function
let optionHTML;
const $this = $( el ),
$thisOption = $this.parent(),
value = $this.val(),
id = $thisOption.data( 'field-id' );
const $choices = $( '#wpforms-field-option-row-' + id + '-choices' ),
$images = $( '#wpforms-field-option-' + id + '-choices_images' ),
$icons = $( '#wpforms-field-option-' + id + '-choices_icons' ),
$otherOption = $( '#wpforms-field-option-' + id + '-choices_other' ),
$basicOptions = $( `#wpforms-field-option-basic-${ id }` );
// Hide image and icon choices if "dynamic choices" is not off.
app.fieldDynamicChoiceToggleImageChoices();
app.fieldDynamicChoiceToggleIconChoices();
app.fieldDynamicChoiceToggleOtherOption();
// Fire an event when a field's dynamic choices option was changed.
$builder.trigger( 'wpformsFieldDynamicChoiceToggle', [ id ] );
// Loading
wpf.fieldOptionLoading( $thisOption );
// Remove previous dynamic post-type or taxonomy source options.
$( '#wpforms-field-option-row-' + id + '-dynamic_post_type' ).remove();
$( '#wpforms-field-option-row-' + id + '-dynamic_taxonomy' ).remove();
/*
* Post type- or Taxonomy-based dynamic populating.
*/
if ( '' !== value ) {
// Hide choice images and icons options, not applicable.
$images.addClass( 'wpforms-hidden' );
$icons.addClass( 'wpforms-hidden' );
$otherOption.addClass( 'wpforms-hidden' );
// Hide `Bulk Add` toggle.
$choices.find( '.toggle-bulk-add-display' ).addClass( 'wpforms-hidden' );
// Hide the AI Choices button.
$basicOptions.find( '.wpforms-ai-choices-button' ).addClass( 'wpforms-hidden' );
// Hide tooltip.
$choices.find( '.wpforms-help-tooltip' ).addClass( 'wpforms-hidden' );
const data = {
type: value,
field_id: id, // eslint-disable-line camelcase
action: 'wpforms_builder_dynamic_choices',
nonce: wpforms_builder.nonce,
};
$.post( wpforms_builder.ajax_url, data, function( res ) {
if ( res.success ) {
// New option markup.
$thisOption.after( res.data.markup );
} else {
// eslint-disable-next-line no-console
console.log( res );
}
// Hide the loading indicator.
wpf.fieldOptionLoading( $thisOption, true );
// Re-init tooltips for a new field.
wpf.initTooltips();
// Trigger Dynamic source updates.
const $dynamicValue = $( '#wpforms-field-option-' + id + '-dynamic_' + value );
$dynamicValue.find( 'option' ).first().prop( 'selected', true );
$dynamicValue.trigger( 'change' );
} ).fail( function( xhr ) {
// eslint-disable-next-line no-console
console.log( xhr.responseText );
} );
return; // Nothing more for dynamic populating.
}
/*
* "Off" - no dynamic populating.
*/
let type = $( '#wpforms-field-option-' + id ).find( '.wpforms-field-option-hidden-type' ).val();
// Show choice images and icon options.
$images.removeClass( 'wpforms-hidden' );
$icons.removeClass( 'wpforms-hidden' );
$otherOption.removeClass( 'wpforms-hidden' );
// Show `Bulk Add` toggle.
$choices.find( '.toggle-bulk-add-display' ).removeClass( 'wpforms-hidden' );
// Show the AI Choices button.
$basicOptions.find( '.wpforms-ai-choices-button' ).removeClass( 'wpforms-hidden' );
// Show tooltip.
$choices.find( '.wpforms-help-tooltip' ).removeClass( 'wpforms-hidden' );
const $wpformsField = $( '#wpforms-field-' + id );
$wpformsField.find( '.wpforms-alert' ).remove();
if ( [ 'checkbox', 'radio', 'payment-multiple', 'payment-checkbox' ].indexOf( type ) > -1 ) {
app.fieldChoiceUpdate( type, id );
// Toggle elements and hide the loading indicator.
$choices.find( 'ul' ).removeClass( 'wpforms-hidden' );
$choices.find( '.wpforms-alert' ).addClass( 'wpforms-hidden' );
wpf.fieldOptionLoading( $thisOption, true );
return; // Nothing more for those types.
}
// Get original field choices.
const $field = $wpformsField;
const choices = [],
$primary = $field.find( '.primary-input' );
let key;
$( `#wpforms-field-option-row-${ id }-choices li` ).each( function() {
const $this = $( this );
choices.push( {
label: wpf.sanitizeHTML( $this.find( '.label' ).val() ),
selected: $this.find( '.default' ).is( ':checked' ),
} );
} );
// Restore field to display original field choices.
if ( $field.hasClass( 'wpforms-field-select' ) ) {
const isModernSelect = app.dropdownField.helpers.isModernSelect( $primary );
let selected = false;
// Remove previous items.
$primary.find( 'option' ).not( '.placeholder' ).remove();
// Update Modern Dropdown.
if ( isModernSelect && choices.length ) {
app.dropdownField.helpers.update( id, false );
} else {
// Update Classic select field.
for ( key in choices ) {
selected = choices[ key ].selected;
optionHTML = '<option';
optionHTML += selected ? ' selected>' : '>';
optionHTML += choices[ key ].label + '</option>';
$primary.append( optionHTML );
}
}
} else {
type = 'radio';
if ( $field.hasClass( 'wpforms-field-checkbox' ) ) {
type = 'checkbox';
}
// Remove previous items.
$primary.empty();
// Add new items to the radio or checkbox field.
for ( key in choices ) {
optionHTML = '<li><input type="' + type + '" disabled';
optionHTML += choices[ key ].selected ? ' selected>' : '>';
optionHTML += choices[ key ].label + '</li>';
$primary.append( optionHTML );
}
}
// Toggle elements and hide the loading indicator.
$choices.find( 'ul' ).removeClass( 'wpforms-hidden' );
$choices.find( '.wpforms-alert' ).addClass( 'wpforms-hidden' );
$primary.removeClass( 'wpforms-hidden' );
wpf.fieldOptionLoading( $thisOption, true );
},
/**
* Field Dynamic Choice Source toggle.
*
* @since 1.2.8
*
* @param {Element} el Element.
*/
fieldDynamicChoiceSource( el ) { // eslint-disable-line max-lines-per-function
/* eslint-disable camelcase */
const $this = $( el ),
$thisOption = $this.parent(),
value = $this.val(),
id = $thisOption.data( 'field-id' ),
form_id = $( '#wpforms-builder-form' ).data( 'id' ),
$choices = $( '#wpforms-field-option-row-' + id + '-choices' ),
$field = $( '#wpforms-field-' + id ),
type = $( `#wpforms-field-option-${ id }-dynamic_choices option:selected` ).val();
let limit = 20;
// Loading.
wpf.fieldOptionLoading( $thisOption );
const data = {
type,
source: value,
field_id: id,
form_id,
action: 'wpforms_builder_dynamic_source',
nonce: wpforms_builder.nonce,
};
/**
* @typedef {Object} DynamicSourceResponse
* @property {boolean} success Success flag.
* @property {{ source_name: string, type_name: string }} data Response data.
*/
/**
* @param {DynamicSourceResponse} res
*/
$.post( wpforms_builder.ajax_url, data, function( res ) {
if ( ! res.success ) {
// eslint-disable-next-line no-console
console.log( res );
// Toggle elements and hide the loading indicator.
wpf.fieldOptionLoading( $thisOption, true );
return;
}
// Update the info box and remove old choices.
$choices.find( '.dynamic-name' ).text( res.data.source_name );
$choices.find( '.dynamic-type' ).text( res.data.type_name );
$choices.find( 'ul' ).addClass( 'wpforms-hidden' );
$choices.find( '.wpforms-alert' ).removeClass( 'wpforms-hidden' );
// Update items.
app.fieldDynamicChoiceSourceItems( $field, res.data.items );
if ( $field.hasClass( 'wpforms-field-select' ) ) {
limit = 200;
}
// Remove any previous empty message.
$field.find( '.wpforms-notice-dynamic-empty' ).remove();
// If the source has more items than the field type can
// ideally handle alert the user.
if ( Number( res.data.total ) > limit ) {
let msg = wpforms_builder.dynamic_choices.limit_message;
msg = msg.replace( '{source}', res.data.source_name );
msg = msg.replace( '{type}', res.data.type_name );
msg = msg.replace( '{limit}', limit );
msg = msg.replace( '{total}', res.data.total );
$.alert( {
title: wpforms_builder.heads_up,
content: msg,
icon: 'fa fa-info-circle',
type: 'blue',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
}
// Toggle limit choices alert message.
app.firstNChoicesAlert( id, res.data.total );
// Toggle empty choices to notice a message.
if ( Number( res.data.total ) === 0 ) {
app.emptyChoicesNotice( id, res.data.source_name, res.data.type );
}
// Toggle elements and hide the loading indicator.
wpf.fieldOptionLoading( $thisOption, true );
} ).fail( function( xhr ) {
// eslint-disable-next-line no-console
console.log( xhr.responseText );
} );
},
/**
* Update a Field Items when `Dynamic Choice` Source is toggled.
*
* @since 1.6.1
*
* @param {Object} $field jQuery selector for current field.
* @param {Object} items Items collection.
*/
fieldDynamicChoiceSourceItems( $field, items ) {
const $primary = $field.find( '.primary-input' );
let key = 0;
if ( $field.hasClass( 'wpforms-field-select' ) ) {
const isModernSelect = app.dropdownField.helpers.isModernSelect( $primary );
if ( isModernSelect ) {
app.fieldDynamicChoiceSourceForModernSelect( $primary, items );
} else {
app.fieldDynamicChoiceSourceForClassicSelect( $primary, items );
}
} else {
let type = 'radio';
if ( $field.hasClass( 'wpforms-field-checkbox' ) ) {
type = 'checkbox';
}
// Remove previous items.
$primary.empty();
// Add new items to the radio or checkbox field.
for ( key in items ) {
$primary.append( '<li><input type="' + type + '" disabled> ' + wpf.sanitizeHTML( items[ key ] ) + '</li>' );
}
}
},
/**
* Update options for Modern style select when `Dynamic Choice` Source is toggled.
*
* @since 1.6.1
*
* @param {Object} $jquerySelector jQuery selector for primary input.
* @param {Object} items Items collection.
*/
fieldDynamicChoiceSourceForModernSelect( $jquerySelector, items ) {
const instance = app.dropdownField.helpers.getInstance( $jquerySelector ),
fieldId = $jquerySelector.closest( '.wpforms-field' ).data().fieldId;
// Destroy the instance of Choices.js.
instance.destroy();
// Update a placeholder.
app.dropdownField.helpers.updatePlaceholderChoice( instance, fieldId );
// Update options.
app.fieldDynamicChoiceSourceForClassicSelect( $jquerySelector, items );
// Choices.js init.
app.dropdownField.events.choicesInit( $jquerySelector );
},
/**
* Update options for Classic style select when `Dynamic Choice` Source is toggled.
*
* @since 1.6.1
*
* @param {Object} $jquerySelector jQuery selector for primary input.
* @param {Object} items Items collection.
*/
fieldDynamicChoiceSourceForClassicSelect( $jquerySelector, items ) {
let index = 0;
const itemsSize = items.length;
// Clear.
$jquerySelector.find( 'option' ).not( '.placeholder' ).remove();
// Add options (items) to a single <select> field.
for ( ; index < itemsSize; index++ ) {
const item = wpf.sanitizeHTML( items[ index ] );
$jquerySelector.append( '<option value="' + item + '">' + item + '</option>' );
}
$jquerySelector.toggleClass( 'wpforms-hidden', ! itemsSize );
},
/**
* Image choice toggle, hide image choices, image choices style, choices if Dynamic choices are not OFF.
*
* @since 1.5.8
*/
fieldDynamicChoiceToggleImageChoices() {
// noinspection CssUnusedSymbol
$( `#wpforms-builder .wpforms-field-options .wpforms-field-option-checkbox,
#wpforms-builder .wpforms-field-options .wpforms-field-option-radio` ).each( function( key, value ) {
const $option = $( value ),
dynamicChoices = $option.find( '.wpforms-field-option-row-dynamic_choices select' ).val(),
isDynamicChoices = typeof dynamicChoices !== 'undefined' && '' !== dynamicChoices,
isImageChoices = $option.find( '.wpforms-field-option-row-choices_images input' ).is( ':checked' );
$option
.find( '.wpforms-field-option-row-choices_images' )
.toggleClass( 'wpforms-hidden', isDynamicChoices );
if ( ! isImageChoices || isDynamicChoices ) {
$option
.find( '.wpforms-field-option-row-choices_images_style' )
.addClass( 'wpforms-hidden' );
}
} );
},
/**
* Hide icon choice toggle, icon choices, color, size, and style options if Dynamic choices are not OFF.
*
* @since 1.7.9
*/
fieldDynamicChoiceToggleIconChoices() {
// noinspection CssUnusedSymbol
$( `#wpforms-builder .wpforms-field-options .wpforms-field-option-checkbox,
#wpforms-builder .wpforms-field-options .wpforms-field-option-radio` ).each( function( key, value ) {
const $option = $( value ),
dynamicChoices = $option.find( '.wpforms-field-option-row-dynamic_choices select' ).val(),
isDynamicChoices = typeof dynamicChoices !== 'undefined' && '' !== dynamicChoices,
isIconChoices = $option.find( '.wpforms-field-option-row-choices_icons input' ).is( ':checked' );
$option
.find( '.wpforms-field-option-row-choices_icons' )
.toggleClass( 'wpforms-hidden', isDynamicChoices );
if ( ! isIconChoices || isDynamicChoices ) {
$option
.find( '.wpforms-field-option-row-choices_icons_color' )
.addClass( 'wpforms-hidden' );
$option
.find( '.wpforms-field-option-row-choices_icons_size' )
.addClass( 'wpforms-hidden' );
$option
.find( '.wpforms-field-option-row-choices_icons_style' )
.addClass( 'wpforms-hidden' );
}
} );
},
/**
* Hide other option toggle, input placeholder color and size if Dynamic choices are not OFF.
*
* @since 1.9.8.3
*/
fieldDynamicChoiceToggleOtherOption() {
// noinspection CssUnusedSymbol
$( `#wpforms-builder .wpforms-field-options .wpforms-field-option-radio` ).each( function( key, value ) {
const $option = $( value ),
$dynamicChoices = $option.find( '.wpforms-field-option-row-dynamic_choices select' ),
dynamicChoicesValue = $dynamicChoices.val(),
isDynamicChoices = typeof dynamicChoicesValue !== 'undefined' && '' !== dynamicChoicesValue,
isOtherOption = $option.find( '.wpforms-field-option-row-choices_other input' ).is( ':checked' );
const fieldID = $dynamicChoices.closest( '.wpforms-field-option-row' ).data( 'field-id' );
const $fieldOptions = $( '#wpforms-field-option-' + fieldID );
const $otherToggle = $fieldOptions.find( '.wpforms-field-option-row-choices_other input' );
// Only force-uncheck the Add Other Choice toggle when Dynamic Choices is enabled.
if ( isDynamicChoices ) {
if ( $otherToggle.is( ':checked' ) ) {
$otherToggle.prop( 'checked', false ).trigger( 'change' );
}
} else {
// When Dynamic Choices is OFF, do not alter the saved toggle state; just ensure the preview is synced.
WPForms.Admin.Builder.MultipleChoices.updatePreviewState( fieldID );
}
$option
.find( '.wpforms-field-option-row-choices_other' )
.toggleClass( 'wpforms-hidden', isDynamicChoices );
if ( ! isOtherOption || isDynamicChoices ) {
$option
.find( '.wpforms-field-option-row-other_placeholder' )
.addClass( 'wpforms-hidden' );
$option
.find( '.wpforms-field-option-row-other_size' )
.addClass( 'wpforms-hidden' );
}
} );
},
/**
* Show choices limit an alert message.
*
* @since 1.6.9
*
* @param {number} fieldId Field ID.
* @param {number} total Total number of choices.
*/
firstNChoicesAlert: ( fieldId, total ) => {
const $field = $( '#wpforms-field-' + fieldId );
// Don't show a message for select fields.
if ( $field.hasClass( 'wpforms-field-select' ) ) {
return;
}
const tmpl = wp.template( 'wpforms-choices-limit-message' ),
data = {
total,
},
limit = app.settings.choicesLimit;
$field.find( '.wpforms-alert-dynamic' ).remove();
if ( total > limit ) {
$field.find( '.primary-input' ).after( tmpl( data ) );
}
},
/**
* Show an empty choice notice message.
*
* @since 1.8.2
*
* @param {number} fieldId Field ID.
* @param {string} source Source name.
* @param {string} type Source type.
*/
emptyChoicesNotice( fieldId, source, type ) {
const field = $( '#wpforms-field-' + fieldId );
const emptyDynamicChoicesMessage = wpforms_builder.dynamic_choices.empty_message
.replace( '{source}', source )
.replace( '{type}', wpforms_builder.dynamic_choices.entities[ type ] );
const template = wp.template( 'wpforms-empty-choice-message' ),
data = {
message: emptyDynamicChoicesMessage,
};
field.find( '.label-title' ).after( template( data ) );
},
/**
* Field layout selector toggling.
*
* @since 1.3.7
*
* @param {Element} el Layout selector toggle of a link element.
*/
fieldLayoutSelectorToggle( el ) {
const /** @type {JQ|jQuery} */ $this = $( el ),
$layoutSelectorDisplay = $this.closest( 'label' ).next( '.layout-selector-display' );
if ( $this.hasClass( 'layout-selector-showing' ) ) {
// Selector is showing, so hide it
$layoutSelectorDisplay.stop().slideUp( 400 );
$this.find( 'span' ).text( wpforms_builder.layout_selector_show );
} else {
$layoutSelectorDisplay.stop().slideDown();
$this.find( 'span' ).text( wpforms_builder.layout_selector_hide );
}
$this.toggleClass( 'layout-selector-showing' );
},
/**
* Init legacy field layout selector.
*
* @since 1.7.7
*
* @param {number} fieldId Field id.
*/
fieldLayoutSelectorInit( fieldId ) { // eslint-disable-line max-lines-per-function
const $layoutSelector = $( `#wpforms-field-option-row-${ fieldId }-css > .layout-selector-display` );
// Bail if already initialized.
if ( $layoutSelector.length ) {
return;
}
const layouts = {
'layout-1': [
{
class: 'one-half',
data: 'wpforms-one-half wpforms-first',
},
{
class: 'one-half',
data: 'wpforms-one-half',
},
],
'layout-2': [
{
class: 'one-third',
data: 'wpforms-one-third wpforms-first',
},
{
class: 'one-third',
data: 'wpforms-one-third',
},
{
class: 'one-third',
data: 'wpforms-one-third',
},
],
'layout-3': [
{
class: 'one-fourth',
data: 'wpforms-one-fourth wpforms-first',
},
{
class: 'one-fourth',
data: 'wpforms-one-fourth',
},
{
class: 'one-fourth',
data: 'wpforms-one-fourth',
},
{
class: 'one-fourth',
data: 'wpforms-one-fourth',
},
],
'layout-4': [
{
class: 'one-third',
data: 'wpforms-one-third wpforms-first',
},
{
class: 'two-third',
data: 'wpforms-two-thirds',
},
],
'layout-5': [
{
class: 'two-third',
data: 'wpforms-two-thirds wpforms-first',
},
{
class: 'one-third',
data: 'wpforms-one-third',
},
],
'layout-6': [
{
class: 'one-fourth',
data: 'wpforms-one-fourth wpforms-first',
},
{
class: 'one-fourth',
data: 'wpforms-one-fourth',
},
{
class: 'two-fourth',
data: 'wpforms-two-fourths',
},
],
'layout-7': [
{
class: 'two-fourth',
data: 'wpforms-two-fourths wpforms-first',
},
{
class: 'one-fourth',
data: 'wpforms-one-fourth',
},
{
class: 'one-fourth',
data: 'wpforms-one-fourth',
},
],
'layout-8': [
{
class: 'one-fourth',
data: 'wpforms-one-fourth wpforms-first',
},
{
class: 'two-fourth',
data: 'wpforms-two-fourths',
},
{
class: 'one-fourth',
data: 'wpforms-one-fourth',
},
],
};
// Create selector options.
let layout,
layoutOptions = `<div class="layout-selector-display unfoldable-cont">
<p class="heading">${ wpforms_builder.layout_selector_layout }</p>
<div class="layouts">`;
for ( const key in layouts ) {
layout = layouts[ key ];
layoutOptions += '<div class="layout-selector-display-layout">';
for ( const i in layout ) {
layoutOptions += `<span class="${ layout[ i ].class }" data-classes="${ layout[ i ].data }"></span>`;
}
layoutOptions += '</div>';
}
layoutOptions += '</div></div>';
$( `#wpforms-field-option-row-${ fieldId }-css > label` ).after( layoutOptions );
},
/**
* Legacy field layout selector, selecting a layout.
*
* @since 1.3.7
*
* @param {Element} el Layout selector toggle link.
*/
fieldLayoutSelectorLayout( el ) {
const $this = $( el );
$this.parent().find( '.layout-selector-display-layout' ).not( $this ).remove();
$this.parent().find( '.heading' ).text( wpforms_builder.layout_selector_column );
$this.toggleClass( 'layout-selector-display-layout layout-selector-display-columns' );
},
/**
* Field layout selector, insert into a class field.
*
* @since 1.3.7
*
* @param {Element} el Element.
*/
fieldLayoutSelectorInsert( el ) {
const /** @type {JQ|jQuery} */ $this = $( el ),
$selector = $this.closest( '.layout-selector-display' ),
$parent = $selector.parent(),
$id = $parent.data( 'field-id' ),
$label = $parent.find( 'label' ),
$input = $parent.find( 'input[type=text]' ),
layoutClassList = [
'wpforms-one-half',
'wpforms-first',
'wpforms-one-third',
'wpforms-one-fourth',
'wpforms-two-thirds',
'wpforms-two-fourths',
];
let classes = $this.data( 'classes' ),
inputVal = $input.val();
if ( inputVal ) {
// Remove existing wpforms layout classes before adding new.
layoutClassList.forEach( ( cls ) => {
inputVal = inputVal.replace( new RegExp( '\\b' + cls + '\\b', 'gi' ), '' );
} );
// Remove any extra spaces.
inputVal = inputVal.replace( /\s\s+/g, ' ' ).trim();
// Add new layout classes.
classes += ' ' + inputVal;
}
$input.val( classes );
// Remove the list, all done!
$selector.slideUp( 400, function() {
$selector.remove();
app.fieldLayoutSelectorInit( $id );
} );
$label.find( '.toggle-layout-selector-display' ).removeClass( 'layout-selector-showing' );
$label.find( '.toggle-layout-selector-display span' ).text( wpforms_builder.layout_selector_show );
},
/**
* Toggle an Order Summary Confirmation settings option.
*
* @since 1.8.7
*
* @param {Event} event Event.
* @param {string} id Field ID.
* @param {string} type Field type.
*/
toggleOrderSummaryConfirmation( event, id, type ) {
if ( type !== 'payment-total' ) {
return;
}
$( '.wpforms-confirmation' ).each( function() {
$( this ).find( '.wpforms-panel-field-confirmations-message_order_summary' ).closest( '.wpforms-panel-field' ).toggle( $( '#wpforms-panel-fields .wpforms-field-payment-total' ).length !== 0 );
} );
},
//--------------------------------------------------------------------//
// Settings Panel
//--------------------------------------------------------------------//
/**
* Element bindings for Settings panel.
*
* @since 1.0.0
*/
bindUIActionsSettings() { // eslint-disable-line max-lines-per-function
// Clicking form title/desc opens Settings panel.
$builder.on( 'click', '#wpforms-panel-fields .wpforms-title-desc, #wpforms-panel-fields .wpforms-field-submit-button, .wpforms-center-form-name', function( e ) {
e.preventDefault();
app.panelSwitch( 'settings' );
if ( $( this ).hasClass( 'wpforms-center-form-name' ) || $( this ).hasClass( 'wpforms-title-desc' ) ) {
setTimeout( function() {
$( '#wpforms-panel-field-settings-form_title' ).trigger( 'focus' );
}, 300 );
}
} );
// Clicking the previous page break button.
$builder.on( 'click', '.wpforms-field-pagebreak-last button', function( e ) {
e.preventDefault();
app.panelSwitch( 'settings' );
$( '#wpforms-panel-field-settings-pagebreak_prev' ).trigger( 'focus' );
} );
// Trigger Custom Captcha adding when clicking on its block in the Also Available section.
$builder.on( 'click', '.wpforms-panel-content-also-available-item-add-captcha', function( e ) {
e.preventDefault();
const customCaptcha = $builder.find( '#wpforms-add-fields-captcha' );
// Show educational modal if Custom Captcha is not installed or activated.
if ( customCaptcha.data( 'action' ) ) {
customCaptcha.trigger( 'click' );
return;
}
app.fieldAdd( 'captcha', {} ).done( function( res ) {
app.panelSwitch( 'fields' );
$( `#wpforms-field-${ res.data.field.id }` ).trigger( 'click' );
} );
} );
// Clicking form last page break button.
$builder.on( 'input', '#wpforms-panel-field-settings-pagebreak_prev', function() {
$( '.wpforms-field-pagebreak-last button' ).text( $( this ).val() );
} );
// Real-time updates for editing the form title.
$builder.on( 'input', '#wpforms-panel-field-settings-form_title, #wpforms-setup-name', function() {
const title = $( this ).val().toString().trim();
$( '.wpforms-preview .wpforms-form-name' ).text( title );
$( '.wpforms-center-form-name.wpforms-form-name' ).text( title );
app.trimFormTitle();
} );
// Real-time updates for editing the form description.
$builder.on( 'input', '#wpforms-panel-field-settings-form_desc', function() {
$( '.wpforms-form-desc' ).text( $( this ).val() );
} );
// Real-time updates for editing the form submit button.
$builder.on( 'input', '#wpforms-panel-field-settings-submit_text', function() {
const submitText = $( this ).val() || wpforms_builder.submit_text;
$( '.wpforms-field-submit input[type=submit]' ).val( submitText );
} );
// Toggle form reCAPTCHA setting.
$builder.on( 'change', '#wpforms-panel-field-settings-recaptcha', function() {
app.captchaToggle();
} );
// Toggle form confirmation setting fields.
$builder.on( 'change', '.wpforms-panel-field-confirmations-type', function() {
app.confirmationFieldsToggle( $( this ) );
} );
$builder.on( 'change', '.wpforms-panel-field-confirmations-message_entry_preview', app.confirmationEntryPreviewToggle );
// Toggle form notification setting fields.
$builder.on( 'change', '#wpforms-panel-field-settings-notification_enable', app.notificationToggle );
// Add a new settings block.
$builder.on( 'click', '.wpforms-builder-settings-block-add', function( e ) {
e.preventDefault();
if ( ! wpforms_builder.pro ) {
return;
}
app.settingsBlockAdd( $( this ) );
} );
// Edit settings block name.
$builder.on( 'click', '.wpforms-builder-settings-block-edit', function( e ) {
e.preventDefault();
const $el = $( this );
if ( $el.parents( '.wpforms-builder-settings-block-header' ).find( '.wpforms-builder-settings-block-name' ).hasClass( 'editing' ) ) {
app.settingsBlockNameEditingHide( $el );
} else {
app.settingsBlockNameEditingShow( $el );
}
} );
// Update settings block name and close editing interface.
$builder.on( 'blur', '.wpforms-builder-settings-block-name-edit input', function( e ) {
// Do not fire if for onBlur user clicked on the edit button - it has its own event processing.
if ( ! $( e.relatedTarget ).hasClass( 'wpforms-builder-settings-block-edit' ) ) {
app.settingsBlockNameEditingHide( $( this ) );
}
} );
// Close the settings block editing interface with pressed Enter.
$builder.on( 'keypress', '.wpforms-builder-settings-block-name-edit input', function( e ) {
// On Enter - hide editing interface.
if ( e.keyCode === 13 ) {
app.settingsBlockNameEditingHide( $( this ) );
// We need this preventDefault() to stop jumping to form name editing input.
e.preventDefault();
}
} );
// Clone settings block.
$builder.on( 'click', '.wpforms-builder-settings-block-clone', function( e ) {
e.preventDefault();
app.settingsBlockPanelClone( $( this ) );
} );
// Toggle the settings block - slide up or down.
$builder.on( 'click', '.wpforms-builder-settings-block-toggle', function( e ) {
e.preventDefault();
app.settingsBlockPanelToggle( $( this ) );
} );
// Remove settings block.
$builder.on( 'click', '.wpforms-builder-settings-block-delete', function( e ) {
e.preventDefault();
app.settingsBlockDelete( $( this ) );
} );
$builder.on( 'wpformsSettingsBlockAdded wpformsSettingsBlockCloned', function( e, $element ) {
if ( ! $element.hasClass( 'wpforms-notification' ) ) {
return;
}
app.notificationUpdateStatus( $element );
} );
$builder.on( 'click', '.wpforms-status-button', function() {
// Notification block has a different HTML structure.
if ( $( this ).hasClass( 'wpforms-notification-status-button' ) ) {
app.notificationChangeStatus( $( this ) );
return;
}
app.handleStatusButton( $( this ) );
} );
// Toggle Open Confirmations in New Tab options on an AJAX form submit setting change.
$builder.on( 'change', '#wpforms-panel-field-settings-ajax_submit', function() {
app.hideOpenConfirmationsInNewTabOptions( ! $( this ).is( ':checked' ) );
} );
},
/**
* Toggle Open Confirmations in New Tab options.
*
* @since 1.9.2
*
* @param {boolean} hide Whether to hide the options.
*/
hideOpenConfirmationsInNewTabOptions( hide ) {
const $options = $( '.wpforms-panel-field-confirmations-redirect_new_tab' );
if ( ! $options.length ) {
return;
}
$options.each( function() {
const $option = $( this ),
$block = $option.closest( '.wpforms-builder-settings-block' ),
type = $block.find( '.wpforms-panel-field-confirmations-type' ).val();
$option.closest( '.wpforms-panel-field' ).toggle( ! hide && [ 'redirect', 'page' ].includes( type ) );
} );
},
/**
* Toggle displaying the CAPTCHA.
*
* @since 1.6.4
*/
captchaToggle() {
const $preview = $builder.find( '.wpforms-field-recaptcha' ),
$setting = $( '#wpforms-panel-field-settings-recaptcha' );
let provider = $setting.data( 'provider' );
provider = provider || 'recaptcha';
if ( ! $preview.length ) {
return;
}
if ( $setting.is( ':checked' ) ) {
$preview
.show()
.toggleClass( 'is-recaptcha', 'recaptcha' === provider );
} else {
$preview.hide();
}
},
/**
* Init confirmations' type.
*
* @since 1.9.2
*/
initConfirmationsType() {
// Toggle the setting fields in each confirmation block.
$( '.wpforms-panel-field-confirmations-type' ).each( function() {
app.confirmationFieldsToggle( $( this ) );
} );
},
/**
* Init TinyMCE in given elements.
*
* @since 1.9.2
*
* @param {jQuery} $elements Elements.
* @param {Object} settings Settings.
*/
initElementsTinyMCE( $elements, settings = {} ) {
if ( typeof tinymce === 'undefined' || typeof wp.editor === 'undefined' ) {
return;
}
// Respect the default settings.
const tinymceSettings = { ...s.tinymceDefaults, ...settings };
const originalSetupCallback = tinymceSettings.tinymce.setup;
// Register the Insert Smart Tag button.
if ( ! tinymceSettings.tinymce.toolbar1.includes( 'wpf_insert_smart_tag' ) ) {
tinymceSettings.tinymce.toolbar1 += ',wpf_insert_smart_tag';
}
// Add a button to the toolbar.
tinymceSettings.tinymce.setup = function( editor ) {
originalSetupCallback?.call( this, editor );
editor.addButton( 'wpf_insert_smart_tag', {
text: '',
tooltip: wpforms_builder.smart_tags_dropdown_title,
icon: false,
image: wpforms_builder.smart_tags_dropdown_mce_icon,
classes: 'wpforms-smart-tags-mce-button',
} );
};
// Init TinyMCE for each element.
$elements.each( function() {
const id = $( this ).attr( 'id' );
// Destroy previously initialized editor.
wp.editor.remove( id );
// Initialize new editor.
wp.editor.initialize( id, tinymceSettings );
} );
},
/**
* Set up the Confirmation blocks.
*
* @since 1.4.8
*/
confirmationsSetup() {
// Toggle the setting fields in each confirmation block.
app.initConfirmationsType();
// Init TinyMCE in each confirmation block.
app.initElementsTinyMCE( $( '.wpforms-panel-field-confirmations-message' ) );
// Validate Confirmation Redirect URL.
$builder.on( 'focusout', '.wpforms-panel-field-confirmations-redirect', function() {
const $field = $( this );
const url = $field.val().trim();
$field.val( url );
// The value is either a valid URL or empty, we're done.
if ( wpf.isURL( url ) || url === '' ) {
return;
}
// Show the error modal and focus the field.
app.confirmationRedirectValidationError( function() {
$field.trigger( 'focus' );
} );
} );
// Make sure Confirmation Redirect URL is not empty, verify before leaving the panel or saving.
$builder.on( 'wpformsBeforeSave wpformsPanelSectionSwitch wpformsPanelSwitch', function( event ) {
const $confirmations = $( '.wpforms-confirmation' );
$confirmations.each( function( _index, confirmation ) {
const $confirmation = $( confirmation );
const $urlField = $confirmation.find( '.wpforms-panel-field-confirmations-redirect' );
// We're starting on a panel other than Settings > Confirmations, bail.
if ( $urlField.is( ':hidden' ) ) {
return;
}
const $typeField = $confirmation.find( '.wpforms-panel-field-confirmations-type' );
// The Confirmation type is not redirect, bail.
// If the URL value is non-empty, `focusout` validation takes over.
if ( $typeField.val() !== 'redirect' || $urlField.val().trim().length > 0 ) {
return;
}
app.confirmationRedirectValidationError( function() {
$urlField.trigger( 'focus' );
} );
event.stopImmediatePropagation();
event.preventDefault();
return false;
} );
} );
},
/**
* Display a confirmation popup for empty or invalid Confirmation Redirect URL.
*
* @since 1.7.6
*
* @param {Function} onDestroyCallback Callback to execute when popup is closed and removed from DOM.
*/
confirmationRedirectValidationError( onDestroyCallback ) {
$.confirm( {
title: wpforms_builder.heads_up,
content: wpforms_builder.redirect_url_field_error,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
onDestroy: onDestroyCallback,
} );
},
/**
* Toggle the different form Confirmation setting fields.
*
* @since 1.4.8
*
* @param {jQuery} $el Element.
*/
confirmationFieldsToggle( $el ) {
if ( ! $el.length ) {
return;
}
const type = $el.val(),
$block = $el.closest( '.wpforms-builder-settings-block-content' );
$block.find( '.wpforms-panel-field' )
.not( $el.parent() )
.not( '.wpforms-conditionals-enable-toggle' )
.hide();
$block.find( '.wpforms-panel-field-confirmations-' + type ).closest( '.wpforms-panel-field' ).show();
if ( type === 'message' ) {
$block.find( '.wpforms-panel-field-confirmations-message_scroll' ).closest( '.wpforms-panel-field' ).show();
$block.find( '.wpforms-panel-field-confirmations-message_entry_preview' ).trigger( 'change' ).closest( '.wpforms-panel-field' ).show();
$block.find( '.wpforms-panel-field-confirmations-message_order_summary' ).closest( '.wpforms-panel-field' ).toggle( $( '#wpforms-panel-fields .wpforms-field-payment-total' ).length !== 0 );
}
if ( type === 'page' ) {
$block.find( '.wpforms-panel-field-confirmations-page-url-parameters' ).closest( '.wpforms-panel-field' ).show();
}
if ( $( '#wpforms-panel-field-settings-ajax_submit' ).is( ':checked' ) ) {
$block.find( '.wpforms-panel-field-confirmations-redirect_new_tab' ).closest( '.wpforms-panel-field' ).toggle( [ 'redirect', 'page' ].includes( type ) );
}
},
/**
* Show/hide an entry preview message.
*
* @since 1.6.9
*/
confirmationEntryPreviewToggle() {
const $this = $( this ),
$styleField = $this.closest( '.wpforms-builder-settings-block-content' ).find( '.wpforms-panel-field-confirmations-message_entry_preview_style' ).parent();
$this.is( ':checked' ) ? $styleField.show() : $styleField.hide();
},
/**
* Toggle the displaying notification settings depending on if the
* notifications are enabled.
*
* @since 1.1.9
*/
notificationToggle() {
const $notification = $( '#wpforms-panel-field-settings-notification_enable' ),
$settingsBlock = $notification.closest( '.wpforms-panel-content-section' ).find( '.wpforms-builder-settings-block' ),
$enabled = $notification.is( ':checked' );
// Toggle Add new notification button.
$( '.wpforms-notifications-add' ).toggleClass( 'wpforms-hidden', ! $enabled );
$builder.trigger( 'wpformsNotificationsToggle', [ $enabled ] );
$enabled ? $settingsBlock.show() : $settingsBlock.hide();
},
/**
* Notifications by status alerts.
*
* @since 1.6.6
*/
notificationsByStatusAlerts() {
$builder.on( 'change', '.wpforms-panel-content-section-notifications .wpforms-notification-by-status-alert', function() {
const $input = $( this );
if ( ! $input.prop( 'checked' ) ) {
return;
}
const $enabled = $( '.wpforms-radio-group-' + $input.attr( 'data-radio-group' ) + ':checked:not(#' + $input.attr( 'id' ) + ')' );
let alertText;
if ( $enabled.length === 0 ) {
alertText = wpforms_builder.notification_by_status_enable_alert;
alertText = alertText.replace( /%1\$s/g, $input.data( 'provider-title' ) );
} else {
alertText = wpforms_builder.notification_by_status_switch_alert;
alertText = alertText.replace( /%2\$s/g, $enabled.data( 'provider-title' ) );
alertText = alertText.replace( /%1\$s/g, $input.data( 'provider-title' ) );
}
$.confirm( {
title: wpforms_builder.heads_up,
content: alertText,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
},
},
} );
} );
},
/**
* Add new settings block.
*
* @since 1.4.8
* @since 1.6.1 Added processing for Field Map table.
* @since 1.6.1.2 Registered `wpformsSettingsBlockAdded` trigger.
*
* @param {jQuery} $el Settings Block jQuery object.
*/
settingsBlockAdd( $el ) { // eslint-disable-line max-lines-per-function
const nextID = Number( $el.attr( 'data-next-id' ) ),
panelID = $el.closest( '.wpforms-panel-content-section' ).data( 'panel' ),
blockType = $el.data( 'block-type' ),
defaultName = $el.data( 'default-name' ) ?? '',
namePrompt = wpforms_builder[ blockType + '_prompt' ],
nameField = `<input autofocus="" type="text" id="settings-block-name" placeholder="${ wpforms_builder[ blockType + '_ph' ] }" value="${ defaultName }">`,
nameError = '<p class="error">' + wpforms_builder[ blockType + '_error' ] + '</p>',
modalContent = namePrompt + nameField + nameError;
const modal = $.confirm( {
container: $builder,
title: false,
content: modalContent,
icon: 'fa fa-info-circle',
type: 'blue',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() { // eslint-disable-line complexity, max-lines-per-function
const settingsBlockName = this.$content.find( 'input#settings-block-name' ).val().toString().trim(),
error = this.$content.find( '.error' );
if ( settingsBlockName === '' ) {
error.show();
return false;
}
const $firstSettingsBlock = $el.closest( '.wpforms-panel-content-section' ).find( '.wpforms-builder-settings-block' ).first();
// Restore tooltips before cloning.
wpf.restoreTooltips( $firstSettingsBlock );
// Check if we have a template for this block type.
const templateSelector = '#wpforms-' + blockType + '-template-block';
const $template = $( templateSelector );
let $newSettingsBlock;
let blockID;
// If we have a template and no existing blocks to clone from.
if ( $template.length ) {
// Get the template HTML
let templateHTML = $template.text();
// Replace variables in the template.
templateHTML = templateHTML
.replace( /{CLONE}/g, nextID )
.replace( /CLONE/g, nextID );
// Create a new jQuery object from the template.
$newSettingsBlock = $( templateHTML );
blockID = nextID;
} else {
// Use the regular clone approach when no template exists.
$newSettingsBlock = $firstSettingsBlock.clone();
blockID = $firstSettingsBlock.data( 'block-id' );
}
let newSettingsBlock;
$newSettingsBlock.attr( 'data-block-id', nextID );
$newSettingsBlock.find( '.wpforms-builder-settings-block-name-holder span' ).text( settingsBlockName );
/**
* Fires to reset settings block elements on adding a new settings block.
*
* @param {jQuery} $element jQuery object of an element.
*/
const resetFormElement = function( $element ) {
if ( $element.attr( 'name' ) ) {
$element.val( '' ).attr( 'name', $element.attr( 'name' ).replace( /\[(\d+)]/, '[' + nextID + ']' ) );
if ( $element.is( 'select' ) ) {
$element.find( 'option' ).prop( 'selected', false ).attr( 'selected', false );
$element.find( 'option' ).first().prop( 'selected', true ).attr( 'selected', 'selected' );
} else if ( $element.attr( 'type' ) === 'checkbox' ) {
const isChecked = $element.closest( '.wpforms-panel-field' ).hasClass( 'js-wpforms-enabled-notification' );
$element.prop( 'checked', isChecked ).attr( 'checked', isChecked ).val( '1' );
} else {
$element.val( '' ).attr( 'value', '' );
}
}
};
$newSettingsBlock.find( 'input, textarea, select' ).each( function() {
const $this = $( this );
const $parent = $this.parent();
if ( $this.hasClass( 'wpforms-disabled' ) && ( $parent.hasClass( 'from-name' ) || $parent.hasClass( 'from-email' ) ) ) {
return;
}
if ( $parent.hasClass( 'wpforms-pdf-file-name' ) ) {
return;
}
resetFormElement( $this );
} );
// Update elements IDs.
const idPrefixPanel = 'wpforms-panel-field-' + panelID + '-',
idPrefixBlock = idPrefixPanel + blockID;
$newSettingsBlock.find( '[id^="' + idPrefixBlock + '"], [for^="' + idPrefixBlock + '"]' ).each( function() {
const $el = $( this ),
attr = $el.prop( 'tagName' ) === 'LABEL' ? 'for' : 'id',
elID = $el.attr( attr ).replace( new RegExp( idPrefixBlock, 'g' ), idPrefixPanel + nextID );
$el.attr( attr, elID );
} );
// Update `notification by status` checkboxes.
const radioGroup = blockID + '-notification-by-status';
$newSettingsBlock.find( '[data-radio-group="' + radioGroup + '"]' ).each( function() {
$( this )
.removeClass( 'wpforms-radio-group-' + radioGroup )
.addClass( 'wpforms-radio-group-' + nextID + '-notification-by-status' )
.attr( 'data-radio-group', nextID + '-notification-by-status' );
} );
$newSettingsBlock.find( '.wpforms-builder-settings-block-name-holder input' ).val( settingsBlockName ).attr( 'value', settingsBlockName );
if ( blockType === 'notification' ) {
$newSettingsBlock.find( '.email-msg textarea' ).val( '{all_fields}' ).text( '{all_fields}' ).attr( 'value', '{all_fields}' );
$newSettingsBlock.find( '.email-recipient input' ).val( '{admin_email}' ).attr( 'value', '{admin_email}' );
}
$newSettingsBlock.removeClass( 'wpforms-builder-settings-block-default' );
if ( blockType === 'confirmation' ) {
$newSettingsBlock.find( '.wpforms-panel-field-tinymce' ).remove();
if ( typeof WPForms !== 'undefined' ) {
$newSettingsBlock.find( '.wpforms-panel-field-confirmations-type-wrap' )
.after( WPForms.Admin.Builder.Templates
.get( 'wpforms-builder-confirmations-message-field' )( {
id: nextID,
} )
);
}
}
// Conditional logic, if present
const $conditionalLogic = $newSettingsBlock.find( '.wpforms-conditional-block' );
if ( $conditionalLogic.length && typeof WPForms !== 'undefined' ) {
$conditionalLogic
.html( WPForms.Admin.Builder.Templates
.get( 'wpforms-builder-conditional-logic-toggle-field' )( {
id: nextID,
type: blockType,
actions: JSON.stringify( $newSettingsBlock.find( '.wpforms-panel-field-conditional_logic-checkbox' ).data( 'actions' ) ),
actionDesc: $newSettingsBlock.find( '.wpforms-panel-field-conditional_logic-checkbox' ).data( 'action-desc' ),
reference: $newSettingsBlock.find( '.wpforms-panel-field-conditional_logic-checkbox' ).data( 'reference' ),
} )
);
}
// Fields Map Table, if present.
const $fieldsMapTable = $newSettingsBlock.find( '.wpforms-field-map-table' );
if ( $fieldsMapTable.length ) {
$fieldsMapTable.each( function( index, el ) {
const $table = $( el );
// Clean table fields.
$table.find( 'tr:not(:first-child)' ).remove();
const $input = $table.find( '.key input' ),
$select = $table.find( '.field select' ),
name = $select.data( 'name' );
$input.attr( 'value', '' );
$select
.attr( 'name', '' )
.attr( 'data-name', name.replace( /\[(\d+)]/, '[' + nextID + ']' ) );
} );
}
newSettingsBlock = $newSettingsBlock.wrap( '<div>' ).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( '<i class="fa fa-chevron-circle-up"></i>' );
} else {
$el.html( '<i class="fa fa-chevron-circle-down"></i>' );
}
},
};
$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: '<p>' + error + '</p><p>' + wpforms_builder.error_contact_support + '</p>',
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( '<img src="' + mediaAttachment.url + '" alt=""><a href="#" title="' + wpforms_builder.upload_image_remove + '" class="wpforms-image-upload-remove"><i class="fa fa-trash-o"></i></a>' );
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 = '<ul class="smart-tags-list-display unfoldable-cont">';
smartTagList += app.getSmartTagsListFieldsElements( $el );
smartTagList += app.getSmartTagsListOtherElements( $el, isFieldOption );
smartTagList += '</ul>';
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 '<li class="heading">' + wpforms_builder.fields_unavailable + '</li>';
}
let smartTagListElements = '';
smartTagListElements += '<li class="heading">' + wpforms_builder.fields_available + '</li>';
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 = `<li><a href="#" data-type="field" data-meta="${ field.id }">${ label }</a></li>`;
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 += `<li><a href="#" data-type="field" data-meta="${ field.id }" data-additional='${ additionalTag }'>${ label } ${ additionalTagLabel }</a></li>`;
} );
}
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 = '<li class="heading">' + wpforms_builder.other + '</li>';
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 += '<li><a href="#" data-type="other" data-meta=\'' + smartTagKey + '\'>' + wpforms_builder.smart_tags[ smartTagKey ] + '</a></li>';
}
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 }
<span class="wpforms-icon-picker-description">${ wpforms_builder.icon_choices.strings.icon_picker_description }</span>
<input type="text" placeholder="${ wpforms_builder.icon_choices.strings.icon_picker_search_placeholder }" class="search" id="wpforms-icon-picker-search">
`;
const content = `
<div class="wpforms-icon-picker-container" id="wpforms-icon-picker-icons">
<ul class="wpforms-icon-picker-icons" data-field-id="${ data.fieldId }" data-choice-id="${ data.choiceId }"></ul>
<ul class="wpforms-icon-picker-pagination"></ul>
<p class="wpforms-icon-picker-not-found wpforms-hidden" data-message="${ wpforms_builder.icon_choices.strings.icon_picker_not_found }"></>
</div>`;
$.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 `
<li data-icon="${ values.icon }" data-icon-style="${ values.style }"${ maybeSelectedClass }>
<i class="ic-fa-${ values.style } ic-fa-${ values.icon }"></i>
<span class="name">${ values.icon }</span>
</li>`;
},
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( '<i>alt</i><i>s</i>' );
}
},
} );
},
/**
* 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();