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>
This commit is contained in:
root
2026-04-29 15:32:23 +00:00
parent 57b752f54e
commit b6df4dbb92
5385 changed files with 838580 additions and 2416 deletions
@@ -0,0 +1,70 @@
/* global $e, elementor */
// noinspection ES6ConvertVarToLetConst
/**
* WPForms script for editor context.
*
* @since 1.9.6
*/
var WPFormsElementorEditorContext = window.WPFormsElementorEditorContext || ( function( document, window, $ ) { // eslint-disable-line no-var
// noinspection JSUnusedGlobalSymbols
/**
* Public functions and properties.
*
* @since 1.9.6
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.6
*/
init() {
app.events();
},
/**
* Register JS events.
*
* @since 1.9.6
*/
events() {
$( window ).on( 'elementor/init', function() {
// To add action on save event, we should use hookUI.After.
$e.hooks.registerUIAfter( new class extends $e.modules.hookUI.After {
// noinspection JSUnusedGlobalSymbols
getCommand() {
return 'document/save/save';
}
// noinspection JSUnusedGlobalSymbols
getId() {
return 'wpforms-elementor-editor-context-after-save';
}
// noinspection JSUnusedGlobalSymbols
getConditions() {
return true;
}
apply() {
// Save custom themes in a preview window.
const previewWindow = elementor.$preview[ 0 ]?.contentWindow;
if ( previewWindow ) {
previewWindow.WPFormsElementorThemes.saveCustomThemes();
}
}
} );
} );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsElementorEditorContext.init();
@@ -0,0 +1 @@
var WPFormsElementorEditorContext=window.WPFormsElementorEditorContext||((e,t)=>{let o={init(){o.events()},events(){t(e).on("elementor/init",function(){$e.hooks.registerUIAfter(new class extends $e.modules.hookUI.After{getCommand(){return"document/save/save"}getId(){return"wpforms-elementor-editor-context-after-save"}getConditions(){return!0}apply(){var e=elementor.$preview[0]?.contentWindow;e&&e.WPFormsElementorThemes.saveCustomThemes()}})})}};return o})((document,window),jQuery);WPFormsElementorEditorContext.init();
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,623 @@
/* global wpformsElementorVars, elementor, elementorFrontend */
'use strict';
/**
* WPForms integration with Elementor in the editor.
*
* @since 1.6.0
* @since 1.6.2 Moved frontend integration to `wpforms-elementor-frontend.js`
*/
var WPFormsElementor = window.WPFormsElementor || ( function( document, window, $ ) {
/**
* Runtime variables.
*
* @since 1.6.2
*
* @type {object}
*/
var vars = {};
/**
* Public functions and properties.
*
* @since 1.6.0
*
* @type {object}
*/
var app = {
/**
* Start the engine.
*
* @since 1.6.0
*/
init: function() {
app.events();
},
/**
* Register JS events.
*
* @since 1.6.0
*/
events: function() {
// Widget events.
$( window ).on( 'elementor/frontend/init', function( event, id, instance ) {
// Widget buttons click.
elementor.channels.editor.on( 'elementorWPFormsAddFormBtnClick', app.addFormBtnClick );
// Widget frontend events.
elementorFrontend.hooks.addAction( 'frontend/element_ready/wpforms.default', app.widgetPreviewEvents );
// Initialize widget controls.
elementor.hooks.addAction( 'panel/open_editor/widget/wpforms', app.widgetPanelOpen );
// Initialize choiceJS.
elementorFrontend.hooks.addAction( 'frontend/element_ready/wpforms.default', app.loadChoicesJS );
} );
},
/**
* Init Modern style Dropdown fields (<select>) with choiceJS.
*
* @since 1.9.0
*
* @param {Object} $scope Elementor scope object.
*/
loadChoicesJS( $scope ) {
// Loads if function exists.
if ( typeof parent.Choices !== 'function' ) {
return;
}
const $elements = $scope.find( '.wpforms-field .choicesjs-select' );
const config = window.wpforms_choicesjs_config || {};
// Initialize ChoicesJS.
$elements.each( function( index, el ) {
if ( ! ( el instanceof parent.HTMLSelectElement ) ) {
return;
}
const $el = $( el );
if ( $el.data( 'choicesjs' ) ) {
return;
}
const $field = $el.closest( '.wpforms-field' );
config.callbackOnInit = function() {
const self = this,
$element = $( self.passedElement.element ),
$input = $( self.input.element ),
sizeClass = $element.data( 'size-class' );
// Add CSS-class for size.
if ( sizeClass ) {
$( self.containerOuter.element ).addClass( sizeClass );
}
/**
* If a multiple select has selected choices - hide a placeholder text.
* In case if select is empty - we return placeholder text.
*/
if ( $element.prop( 'multiple' ) ) {
// On init event.
$input.data( 'placeholder', $input.attr( 'placeholder' ) );
if ( self.getValue( true ).length ) {
$input.hide();
}
}
this.disable();
$field.find( '.is-disabled' ).removeClass( 'is-disabled' );
};
$el.data( 'choicesjs', new parent.Choices( el, config ) );
} );
},
/**
* Widget events.
*
* @since 1.6.2
*
* @param {jQuery} $scope The current element wrapped with jQuery.
*/
widgetPreviewEvents: function( $scope ) {
$scope
.on( 'click', '.wpforms-btn', app.addFormBtnClick )
.on( 'click', '.wpforms-admin-no-forms-container a', app.clickLinkInPreview )
.on( 'change', '.wpforms-elementor-form-selector select', app.selectFormInPreview )
.on( 'click mousedown focus keydown submit', '.wpforms-container *', app.disableEvents )
.on( 'click', '.wpforms-comprehensive-link', app.openComprehensiveLink );
app.updateSameForms( $scope );
},
/**
* Update all the same forms on the preview.
*
* @since 1.6.3
*
* @param {jQuery} $scope The current element wrapped with jQuery.
*/
updateSameForms: function( $scope ) {
var elementId = $scope.data( 'id' ),
$formContainer = $scope.find( '.wpforms-container' ),
formContainerHtml = $formContainer.html(),
formContainerId = $formContainer.attr( 'id' );
$scope
.closest( '.elementor-editor-active' )
.find( '.elementor-widget-wpforms:not(.elementor-element-' + elementId + ')' )
.each( function() {
var $anotherFormContainer = $( this ).find( '.wpforms-container' );
if ( $anotherFormContainer.attr( 'id' ) === formContainerId ) {
$anotherFormContainer.html( formContainerHtml );
}
} );
},
/**
* Initialize widget controls when widget is activated.
*
* @since 1.6.2
*
* @param {object} panel Panel object.
* @param {object} model Model object.
*/
widgetPanelOpen: function( panel, model ) {
vars.widgetId = model.attributes.id;
vars.formId = model.attributes.settings.attributes.form_id;
app.widgetPanelInit( panel );
app.widgetPanelObserver.init( panel );
},
/**
* Initialize widget controls when widget is activated.
*
* @since 1.6.2
*
* @param {object} panel Panel object.
*/
widgetPanelInit: function( panel ) {
var $formSelectControl = panel.$el.find( '.elementor-control.elementor-control-form_id' ),
$formSelect = $formSelectControl.find( 'select' ),
$addFormNoticeControl = panel.$el.find( '.elementor-control.elementor-control-add_form_notice' ),
$testFormNoticeControl = panel.$el.find( '.elementor-control.elementor-control-test_form_notice' );
// Update form select options if it is available after adding the form.
if ( vars.formSelectOptions ) {
$formSelect.html( vars.formSelectOptions );
}
// Update form select value.
if ( vars.formId && vars.formId !== '' ) {
$formSelect.val( vars.formId );
}
// Hide not needed controls.
if ( $formSelect.find( 'option' ).length > 0 ) {
$addFormNoticeControl.hide();
} else {
$formSelectControl.hide();
$testFormNoticeControl.hide();
}
// Show needed controls.
if ( parseInt( $formSelect.val(), 10 ) > 0 ) {
$testFormNoticeControl.show();
}
// Select form.
panel.$el.find( '.elementor-control.elementor-control-form_id' ).on( 'change', 'select', function() {
// Update `vars.formId` to be able to restore selected value after options update.
vars.formId = $( this ).val();
} );
// Click on the `Edit the selected form` link.
panel.$el.find( '.elementor-control.elementor-control-edit_form' ).on( 'click', 'a', app.editFormLinkClick );
},
/**
* The observer needed to re-init controls when the widget panel section and tabs switches.
*
* @since 1.6.3
*
* @member {object}
*/
widgetPanelObserver: {
/**
* Initialize observer.
*
* @since 1.6.3
*
* @param {object} panel Panel object.
*/
init: function( panel ) {
// Skip if observer for current widget already initialized.
if ( vars.observerWidgetId === vars.widgetId ) {
return;
}
// Disconnect previous widget observer.
if ( typeof vars.observer !== 'undefined' && typeof vars.observer.disconnect === 'function' ) {
vars.observer.disconnect();
}
var obs = {
targetNode : panel.$el.find( '#elementor-panel-content-wrapper' )[0],
config : {
childList: true,
subtree: true,
attributes: true,
},
};
app.widgetPanelObserver.panel = panel;
obs.observer = new MutationObserver( app.widgetPanelObserver.callback );
obs.observer.observe( obs.targetNode, obs.config );
vars.observerWidgetId = vars.widgetId;
vars.observer = obs.observer;
},
/**
* Observer callback.
*
* @since 1.6.3
*
* @param {Array} mutationsList Mutation list.
*/
callback: function( mutationsList ) {
var mutation,
quit = false;
for ( var i in mutationsList ) {
mutation = mutationsList[ i ];
if ( mutation.type === 'childList' && mutation.addedNodes.length > 0 ) {
quit = app.widgetPanelObserver.callbackMutationChildList( mutation );
}
if ( mutation.type === 'attributes' ) {
quit = app.widgetPanelObserver.callbackMutationAttributes( mutation );
}
if ( quit ) {
return;
}
}
},
/**
* Process 'childList' mutation.
*
* @since 1.6.3
*
* @param {MutationRecord} mutation Mutation record.
*
* @returns {boolean} True if detect needed node.
*/
callbackMutationChildList: function( mutation ) {
var addedNodes = mutation.addedNodes || [],
node;
for ( var n in addedNodes ) {
node = addedNodes[ n ];
if ( node && node.classList && node.classList.contains( 'elementor-control-section_form' ) ) {
app.widgetPanelInit( app.widgetPanelObserver.panel );
return true;
}
}
return false;
},
/**
* Process 'attributes' mutation.
*
* @since 1.6.3
*
* @param {MutationRecord} mutation Mutation record.
*
* @returns {boolean} True if detect needed target.
*/
callbackMutationAttributes: function( mutation ) {
if (
mutation.target &&
mutation.target.classList &&
mutation.target.classList.contains( 'elementor-tab-control-content' )
) {
app.widgetPanelInit( app.widgetPanelObserver.panel );
return true;
}
return false;
},
},
/**
* Edit selected form button click event handler.
*
* @since 1.6.2
*
* @param {object} event Event object.
*/
editFormLinkClick: function( event ) {
app.findFormSelector( event );
app.openBuilderPopup( vars.$select.val() );
},
/**
* Add a new form button click event handler.
*
* @since 1.6.2
*
* @param {object} event Event object.
*/
addFormBtnClick: function( event ) {
app.findFormSelector( event );
app.openBuilderPopup( 0 );
},
/**
* Find and store the form selector control wrapped in jQuery object.
*
* @since 1.6.2
*
* @param {object} event Event object.
*/
findFormSelector: function( event ) {
let view = elementor.getPanelView().getCurrentPageView();
// We need to be sure that we are on the widget Content section.
if ( view.activeSection && view.activeSection !== 'section_form' ) {
$( view.ui.tabs[0] ).trigger( 'click' );
}
vars.$select = event && event.$el ?
event.$el.closest( '#elementor-controls' ).find( 'select[data-setting="form_id"]' ) :
window.parent.jQuery( '#elementor-controls select[data-setting="form_id"]' );
},
/**
* Preview: Form selector event handler.
*
* @since 1.6.2
*/
selectFormInPreview: function() {
vars.formId = $( this ).val();
app.findFormSelector();
// To be sure, that both form selector selects are in sync.
app.refreshFormsList( null, vars.formId );
},
/**
* Preview: Click on the link event handler.
*
* @since 1.6.2
*
* @param {object} event Event object.
*/
clickLinkInPreview: function( event ) {
if ( event.target && event.target.href ) {
window.open( event.target.href, '_blank', 'noopener,noreferrer' );
}
},
/**
* Disable events.
*
* @since 1.6.2
*
* @param {object} event Event object.
*
* @returns {boolean} Always false.
*/
disableEvents: function( event ) {
event.preventDefault();
event.stopImmediatePropagation();
return false;
},
/**
* Open the compreshenvie guide link,
* as elementor disables all links in the preview.
*
* @since 1.8.3
*
* @param {object} event Event object.
*/
openComprehensiveLink: function( event ) {
const url = $( this ).attr( 'href' );
// Open the url in a new tab with JS bc elementor doesn't allow links in the preview.
window.open( url, '_blank' ).focus();
},
/**
* Open builder popup.
*
* @since 1.6.2
*
* @param {number} formId Form id. 0 for create new form.
*/
openBuilderPopup: function( formId ) {
formId = parseInt( formId || '0', 10 );
if ( ! vars.$popup ) {
// We need to add popup markup to the editor top document.
var $elementor = window.parent.jQuery( '#elementor-editor-wrapper' ),
popupTpl = wp.template( 'wpforms-builder-elementor-popup' );
$elementor.after( popupTpl() );
vars.$popup = $elementor.siblings( '#wpforms-builder-elementor-popup' );
}
var url = formId > 0 ? wpformsElementorVars.edit_form_url + formId : wpformsElementorVars.add_form_url,
$iframe = vars.$popup.find( 'iframe' );
app.builderCloseButtonEvent();
$iframe.attr( 'src', url );
vars.$popup.fadeIn();
},
/**
* Close button (inside the form builder) click event.
*
* @since 1.6.2
*/
builderCloseButtonEvent: function() {
vars.$popup
.off( 'wpformsBuilderInPopupClose' )
.on( 'wpformsBuilderInPopupClose', function( e, action, formId ) {
if ( action !== 'saved' || ! formId ) {
return;
}
app.refreshFormsList( null, formId );
} );
},
/**
* Refresh forms list event handler.
*
* @since 1.6.2
*
* @param {object} event Event object.
* @param {number} setFormId Set selected form to.
*/
refreshFormsList: function( event, setFormId ) {
if ( event ) {
event.preventDefault();
}
app.findFormSelector();
var data = {
action: 'wpforms_admin_get_form_selector_options',
nonce : wpformsElementorVars.nonce,
};
vars.$select.prop( 'disabled', true );
$.post( wpformsElementorVars.ajax_url, data )
.done( function( response ) {
if ( ! response.success ) {
app.debug( response );
return;
}
vars.formSelectOptions = response.data;
vars.$select.html( response.data );
if ( setFormId ) {
vars.formId = setFormId;
}
if ( vars.formId && vars.formId !== '' ) {
vars.$select.val( vars.formId ).trigger( 'change' );
}
} )
.fail( function( xhr, textStatus ) {
app.debug( {
xhr: xhr,
textStatus: textStatus,
} );
} )
.always( function() {
if ( ! vars.$select || vars.$select.length < 1 ) {
return;
}
vars.$select.prop( 'disabled', false );
var $formSelectOptions = vars.$select.find( 'option' ),
$formSelectControl = vars.$select.closest( '.elementor-control' );
if ( $formSelectOptions.length > 0 ) {
$formSelectControl.show();
$formSelectControl.siblings( '.elementor-control-add_form_notice' ).hide();
}
if ( parseInt( vars.$select.val(), 10 ) > 0 ) {
$formSelectControl.siblings( '.elementor-control-test_form_notice' ).show();
}
} );
},
/**
* Debug output helper.
*
* @since 1.6.2
*
* @param {mixed} msg Debug message.
*/
debug: function( msg ) {
if ( app.isDebug() ) {
console.log( 'WPForms Debug:', msg );
}
},
/**
* Is debug mode.
*
* @since 1.6.2
*
* @returns {boolean} True if the debug enabled.
*/
isDebug: function() {
return ( ( window.top.location.hash && '#wpformsdebug' === window.top.location.hash ) || wpformsElementorVars.debug );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsElementor.init();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,154 @@
/* global wpforms, wpformsElementorVars, wpformsModernFileUpload, wpformsRecaptchaLoad, grecaptcha, WPFormsRepeaterField, WPFormsStripePaymentElement */
/**
* WPForms integration with Elementor on the frontend.
*
* @since 1.6.2 Moved from `wpforms-elementor.js`
*/
var WPFormsElementorFrontend = window.WPFormsElementorFrontend || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.6.2
*
* @type {Object}
*/
var app = {
/**
* Flag to force load ChoicesJS.
*
* @since 1.9.0
*
* @type {boolean}
*/
forceLoadChoices: false,
/**
* Flag to force set Stripe.
*
* @since 1.9.3
*
* @type {boolean}
*/
forceSetStripe: false,
/**
* Start the engine.
*
* @since 1.6.2
*/
init() {
app.events();
},
/**
* Register JS events.
*
* @since 1.6.2
*/
events() {
window.addEventListener( 'elementor/popup/show', function( event ) {
const $modal = $( '#elementor-popup-modal-' + event.detail.id ),
$form = $modal.find( '.wpforms-form' );
if ( ! $form.length ) {
return;
}
app.forceSetStripe = true;
app.initFields( $form );
} );
// Add Elementor popup support for text limit.
window.addEventListener( 'elementor/popup/show', function() {
window.WPFormsTextLimit?.initHint( '.elementor-popup-modal' );
} );
// Force load ChoicesJS for elementor popup.
$( document ).on( 'elementor/popup/show', () => {
app.forceLoadChoices = true;
wpforms.loadChoicesJS();
} );
$( document ).on( 'wpformsBeforeLoadElementChoices', ( event, el ) => {
// Do not initialize on elementor popup.
if ( ! app.isFormInElementorPopup( el ) || app.forceLoadChoices ) {
return;
}
event.preventDefault();
} );
$( document ).on( 'wpformsBeforeStripePaymentElementSetup', ( event, el ) => {
// Do not initialize on elementor popup.
if ( ! app.isFormInElementorPopup( el ) || app.forceSetStripe ) {
return;
}
event.preventDefault();
} );
},
/**
* Check if the form is in Elementor popup.
*
* @since 1.9.3
*
* @param {Object} form Form element.
*
* @return {boolean} True if the form is in Elementor popup, false otherwise.
*/
isFormInElementorPopup( form ) {
return $( form ).parents( 'div[data-elementor-type="popup"]' ).length;
},
/**
* Init all things for WPForms.
*
* @since 1.6.2
*
* @param {Object} $form jQuery selector.
*/
initFields( $form ) { // eslint-disable-line complexity
// Init WPForms things.
wpforms.ready();
// Init `Modern File Upload` field.
if ( 'undefined' !== typeof wpformsModernFileUpload ) {
wpformsModernFileUpload.init();
}
// Init CAPTCHA.
if ( 'undefined' !== typeof wpformsRecaptchaLoad ) {
if ( 'recaptcha' === wpformsElementorVars.captcha_provider && 'v3' === wpformsElementorVars.recaptcha_type ) {
if ( 'undefined' !== typeof grecaptcha ) {
grecaptcha.ready( wpformsRecaptchaLoad );
}
} else {
wpformsRecaptchaLoad();
}
}
// Init Repeater fields.
if ( 'undefined' !== typeof WPFormsRepeaterField ) {
WPFormsRepeaterField.ready();
}
// Init Stripe payment.
if ( 'undefined' !== typeof WPFormsStripePaymentElement ) {
WPFormsStripePaymentElement.setupStripeForm( $form );
}
// Register a custom event.
$( document ).trigger( 'wpforms_elementor_form_fields_initialized', [ $form ] );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsElementorFrontend.init();
@@ -0,0 +1 @@
var WPFormsElementorFrontend=window.WPFormsElementorFrontend||((o,e,r)=>{var t={forceLoadChoices:!1,forceSetStripe:!1,init(){t.events()},events(){e.addEventListener("elementor/popup/show",function(e){e=r("#elementor-popup-modal-"+e.detail.id).find(".wpforms-form");e.length&&(t.forceSetStripe=!0,t.initFields(e))}),e.addEventListener("elementor/popup/show",function(){e.WPFormsTextLimit?.initHint(".elementor-popup-modal")}),r(o).on("elementor/popup/show",()=>{t.forceLoadChoices=!0,wpforms.loadChoicesJS()}),r(o).on("wpformsBeforeLoadElementChoices",(e,o)=>{t.isFormInElementorPopup(o)&&!t.forceLoadChoices&&e.preventDefault()}),r(o).on("wpformsBeforeStripePaymentElementSetup",(e,o)=>{t.isFormInElementorPopup(o)&&!t.forceSetStripe&&e.preventDefault()})},isFormInElementorPopup(e){return r(e).parents('div[data-elementor-type="popup"]').length},initFields(e){wpforms.ready(),"undefined"!=typeof wpformsModernFileUpload&&wpformsModernFileUpload.init(),"undefined"!=typeof wpformsRecaptchaLoad&&("recaptcha"===wpformsElementorVars.captcha_provider&&"v3"===wpformsElementorVars.recaptcha_type?"undefined"!=typeof grecaptcha&&grecaptcha.ready(wpformsRecaptchaLoad):wpformsRecaptchaLoad()),"undefined"!=typeof WPFormsRepeaterField&&WPFormsRepeaterField.ready(),"undefined"!=typeof WPFormsStripePaymentElement&&WPFormsStripePaymentElement.setupStripeForm(e),r(o).trigger("wpforms_elementor_form_fields_initialized",[e])}};return t})(document,window,jQuery);WPFormsElementorFrontend.init();
@@ -0,0 +1,965 @@
/* global elementor, elementorCommon, wpformsElementorVars, WPFormsElementorModern */
// noinspection TypeScriptUMDGlobal
/**
* @param wpformsElementorVars.route_namespace
* @param strings.form_themes
* @param strings.theme_name
* @param strings.theme_delete
* @param strings.theme_delete_title
* @param strings.theme_delete_confirm
* @param strings.theme_delete_cant_undone
* @param strings.theme_delete_yes
* @param strings.theme_copy
* @param strings.theme_custom
* @param strings.theme_noname
* @param strings.themes_error
* @param strings.button_background
* @param strings.button_text
* @param strings.field_label
* @param strings.field_sublabel
* @param strings.field_border
*/
// noinspection ES6ConvertVarToLetConst
/**
* WPForms integration with Elementor (modern widget).
*
* @since 1.9.6
*/
var WPFormsElementorThemes = window.WPFormsElementorThemes || ( function( document, window, $ ) { // eslint-disable-line no-var
/**
* Localized data aliases.
*
* @since 1.9.6
*/
const { isAdmin, isPro, isLicenseActive, strings, route_namespace: routeNamespace } = wpformsElementorVars;
/**
* Runtime state.
*
* @since 1.9.6
*
* @type {Object}
*/
const state = {};
/**
* Themes data.
*
* @since 1.9.6
*
* @type {Object}
*/
const themesData = {
wpforms: null,
custom: null,
};
/**
* Enabled themes.
*
* @since 1.9.6
*
* @type {Object}
*/
let enabledThemes = null;
/**
* Elements holder.
*
* @since 1.9.6
*
* @type {Object}
*/
const el = {};
// noinspection JSUnusedGlobalSymbols
/**
* Public functions and properties.
*
* @since 1.9.6
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.6
*/
init() {
el.$window = $( window );
app.fetchThemesData();
app.events();
},
/**
* Register JS events.
*
* @since 1.9.6
*/
events() {
// noinspection JSUnusedLocalSymbols
$( window )
.on( 'elementor/frontend/init', function() {
elementor.channels.editor.on( 'section:activated', app.themesControlSetup );
} );
},
/**
* Get all themes data.
*
* @since 1.9.6
*
* @return {Object} Themes data.
*/
getAllThemes() {
return { ...( themesData.custom || {} ), ...( themesData.wpforms || {} ) };
},
/**
* On section change event handler.
*
* @since 1.9.6
*
* @param {string} sectionName The current section name.
* @param {Object} editor Editor instance.
*/
themesControlSetup( sectionName, editor ) {
if ( sectionName !== 'themes' || editor.model.attributes.widgetType !== 'wpforms' ) {
return;
}
const $panelContent = editor.$childViewContainer[ 0 ];
const $themesControl = $( $panelContent ).find( '.wpforms-elementor-themes-control' );
// Scrollbar fix for Mac.
if ( app.isMac() ) {
$themesControl.addClass( 'wpforms-is-mac' );
}
app.updateThemesList( editor, $themesControl );
},
/**
* Update themes list.
*
* @since 1.9.6
* @param {Object} editor Editor instance.
* @param {Object} $themesControl Themes control object.
*/
updateThemesList( editor, $themesControl ) {
const selectedTheme = editor.model.attributes.settings.attributes.wpformsTheme ?? 'default';
// Get all themes.
const html = app.getThemesListMarkup( selectedTheme );
$themesControl.html( html );
app.addThemesEvents( $themesControl, editor );
},
/**
* On settings change event handler.
*
* @since 1.9.6
*
* @param {Object} $themesControl Themes control element.
* @param {Object} editor Editor instance.
*/
addThemesEvents( $themesControl, editor ) {
const debouncedMaybeCreate = _.debounce( ( settings ) => {
app.maybeCreateCustomTheme( settings );
}, 300 );
const settingsModel = editor.model.get( 'settings' );
if ( settingsModel.attributes.isMigrated !== 'true' ) {
app.maybeMigrateToCustomTheme( settingsModel, $themesControl, editor );
}
settingsModel.on( 'change', ( one ) => {
debouncedMaybeCreate( one.attributes );
app.maybeUpdateCustomTheme( one );
} );
const $radioButtons = $themesControl.find( '[role="radio"]' );
// Add event listeners to the radio buttons.
$radioButtons.off( 'click' ).on( 'click', function() {
$radioButtons.removeClass( 'is-active' );
$( this ).addClass( 'is-active' );
const selectedValue = $( this ).val();
app.selectTheme( selectedValue );
} );
// Add event listeners to the theme delete button.
elementor.channels.editor
.off( 'WPFormsDeleteThemeButtonClick' )
.on( 'WPFormsDeleteThemeButtonClick', () => {
app.deleteThemeModal( editor.model.attributes.settings.attributes, editor );
} );
// Listen for the theme name change.
editor.model.get( 'settings' )
.off( 'change:customThemeName' )
.on( 'change:customThemeName', function( model ) {
const newName = model.get( 'customThemeName' );
app.changeThemeName( newName, model );
app.updateThemesList( editor, $themesControl );
} );
},
/**
* Maybe migrate to the custom theme.
*
* @since 1.9.6
*
* @param {Object} settingsModel Settings model.
* @param {Object} $themesControl Themes Control object.
* @param {Object} editor Editor object.
*/
maybeMigrateToCustomTheme( settingsModel, $themesControl, editor ) {
const previousSettings = settingsModel._previousAttributes;
const atts = settingsModel.attributes;
if ( 'copyPasteJsonValue' in previousSettings && ! previousSettings.wpformsTheme && ! atts.isCustomTheme ) {
const currentStyles = app.getCurrentStyleAttributes( settingsModel.attributes );
app.createCustomTheme( settingsModel.attributes, currentStyles, true );
app.updateThemesList( editor, $themesControl );
}
settingsModel.setExternalChange( {
isMigrated: 'true',
} );
},
/**
* Maybe update the custom theme settings.
*
* @since 1.9.6
*
* @param {Object} model Settings model.
*/
maybeUpdateCustomTheme( model ) {
const atts = model.attributes;
const isCustomTheme = atts.isCustomTheme === 'true';
if ( ! isCustomTheme ) {
return;
}
const changedAtts = model.changed;
const allowedKeys = WPFormsElementorModern.getStyleAttributesKeys();
// Update only allowed attributes.
for ( const element in changedAtts ) {
if ( ! allowedKeys.includes( element ) ) {
continue;
}
const attrValue = WPFormsElementorModern.prepareComplexAttrValues( changedAtts[ element ], element );
app.updateCustomThemeAttribute( element, attrValue, atts );
}
},
/**
* Get the Themes control markup.
*
* @since 1.9.6
*
* @param {string} selectedTheme Selected theme slug.
*
* @return {string} Themes items HTML.
*/
// eslint-disable-next-line complexity
getThemesListMarkup( selectedTheme ) {
if ( ! themesData.wpforms ) {
app.fetchThemesData();
// Return markup with an error message if themes are not available.
return `<div class="wpforms-no-themes">${ strings.themes_error }</div>`;
}
const allThemes = app.getAllThemes();
if ( ! allThemes ) {
return '';
}
const themes = Object.keys( allThemes );
let theme, firstThemeSlug;
let html = '';
let itemsHtml = '';
if ( ! app.isWPFormsTheme( selectedTheme ) ) {
firstThemeSlug = selectedTheme;
itemsHtml += app.getThemesItemMarkup( app.getTheme( firstThemeSlug ), firstThemeSlug, firstThemeSlug );
}
for ( const key in themes ) {
const slug = themes[ key ];
// Skip the first theme.
if ( firstThemeSlug && firstThemeSlug === slug ) {
continue;
}
// Ensure that all the theme settings are present.
theme = { ...allThemes.default, ...( allThemes[ slug ] || {} ) };
theme.settings = { ...allThemes.default.settings, ...( theme.settings || {} ) };
itemsHtml += app.getThemesItemMarkup( theme, slug, selectedTheme );
}
html = `<div role="radiogroup" class="wpforms-elementor-themes-radio-group">
${ itemsHtml }
</div>`;
return html;
},
/**
* Get the Themes list item markup.
*
* @since 1.9.6
*
* @param {Object} theme Theme properties.
* @param {string} slug Theme slug.
* @param {string} selectedTheme Selected theme slug.
*
* @return {string} Themes items HTML.
*/
getThemesItemMarkup( theme, slug, selectedTheme ) {
if ( ! theme ) {
return '';
}
const title = theme.name?.length > 0 ? theme.name : strings.theme_noname;
let radioClasses = 'wpforms-elementor-themes-radio ';
const buttonClass = slug === selectedTheme ? 'is-active' : '';
radioClasses += app.isDisabledTheme( slug ) ? 'wpforms-elementor-themes-radio-disabled' : ' wpforms-elementor-themes-radio-enabled';
return `<button type="button" class="${ buttonClass }" value="${ slug }" role="radio">
<div class="wpforms-elementor-themes-radio ${ radioClasses }">
<div class="wpforms-elementor-themes-radio-title">${ title }</div>
</div>
<div class="wpforms-elementor-themes-indicators">
<span class="component-color-indicator" title="${ strings.button_background }" style="background: ${ theme.settings.buttonBackgroundColor };" data-index="0"></span>
<span class="component-color-indicator" title="${ strings.button_text }" style="background: ${ theme.settings.buttonTextColor }" data-index="1"></span>
<span class="component-color-indicator" title="${ strings.field_label }" style="background: ${ theme.settings.labelColor };" data-index="2"></span>
<span class="component-color-indicator" title="${ strings.field_sublabel } " style="background: ${ theme.settings.labelSublabelColor };" data-index="3"></span>
<span class="component-color-indicator" title="${ strings.field_border }" style="background: ${ theme.settings.fieldBorderColor };" data-index="4"></span>
</div>
</button>`;
},
/**
* Get theme data.
*
* @since 1.9.6
*
* @param {string} slug Theme slug.
*
* @return {Object|null} Theme settings.
*/
getTheme( slug ) {
return app.getAllThemes()[ slug ] || null;
},
/**
* Get enabled themes data.
*
* @since 1.9.6
*
* @return {Object} Themes data.
*/
getEnabledThemes() {
if ( enabledThemes ) {
return enabledThemes;
}
const allThemes = app.getAllThemes();
if ( isPro && isLicenseActive ) {
return allThemes;
}
enabledThemes = Object.keys( allThemes ).reduce( ( acc, key ) => {
if ( allThemes[ key ].settings?.fieldSize && ! allThemes[ key ].disabled ) {
acc[ key ] = allThemes[ key ];
}
return acc;
}, {} );
return enabledThemes;
},
/**
* Update enabled themes.
*
* @since 1.9.6
*
* @param {string} slug Theme slug.
* @param {Object} theme Theme settings.
*/
updateEnabledThemes( slug, theme ) {
if ( ! enabledThemes ) {
return;
}
enabledThemes = {
...enabledThemes,
[ slug ]: theme,
};
},
/**
* Whether the theme is disabled.
*
* @since 1.9.6
*
* @param {string} slug Theme slug.
*
* @return {boolean} True if the theme is disabled.
*/
isDisabledTheme( slug ) {
return ! app.getEnabledThemes()?.[ slug ];
},
/**
* Whether the theme is one of the WPForms themes.
*
* @since 1.9.6
*
* @param {string} slug Theme slug.
*
* @return {boolean} True if the theme is one of the WPForms themes.
*/
isWPFormsTheme( slug ) {
return Boolean( themesData.wpforms[ slug ]?.settings );
},
/**
* Fetch themes data from API.
*
* @since 1.9.6
*/
fetchThemesData() {
// If a fetch is already in progress, exit the function.
if ( state.isFetchingThemes || themesData.wpforms ) {
return;
}
// Set the flag to true indicating a fetch is in progress.
state.isFetchingThemes = true;
try {
// Fetch themes data.
wp.apiFetch( {
path: routeNamespace + 'elementor/themes/',
method: 'GET',
cache: 'no-cache',
} )
.then( ( response ) => {
themesData.wpforms = response.wpforms || {};
themesData.custom = response.custom || {};
} )
.catch( ( error ) => {
// eslint-disable-next-line no-console
console.error( error?.message );
} )
.finally( () => {
state.isFetchingThemes = false;
} );
} catch ( error ) {
// eslint-disable-next-line no-console
console.error( error );
}
},
/**
* Save the custom themes.
*
* @since 1.9.6
*/
saveCustomThemes() {
if ( ! isAdmin ) {
return;
}
// Custom themes do not exist.
if ( state.isSavingThemes || ! themesData.custom ) {
return;
}
// Set the flag to true indicating a saving is in progress.
state.isSavingThemes = true;
try {
// Save themes.
wp.apiFetch( {
path: routeNamespace + 'elementor/themes/custom/',
method: 'POST',
data: { customThemes: themesData.custom },
} )
.then( ( response ) => {
if ( ! response?.result ) {
// eslint-disable-next-line no-console
console.log( response?.error );
}
} )
.catch( ( error ) => {
// eslint-disable-next-line no-console
console.error( error?.message );
} )
.finally( () => {
state.isSavingThemes = false;
} );
} catch ( error ) {
// eslint-disable-next-line no-console
console.error( error );
}
},
/**
* Get the current style attributes state.
*
* @since 1.9.6
*
* @param {Object} atts Widget attributes.
*
* @return {Object} Whether the custom theme is created.
*/
getCurrentStyleAttributes( atts ) {
const defaultAttributes = Object.keys( themesData.wpforms.default?.settings );
const currentStyleAttributes = {};
for ( const key in defaultAttributes ) {
const attr = defaultAttributes[ key ];
currentStyleAttributes[ attr ] = WPFormsElementorModern.prepareComplexAttrValues( atts[ attr ], defaultAttributes[ key ] ) ?? '';
}
return currentStyleAttributes;
},
/**
* Maybe create a custom theme.
*
* @since 1.9.6
*
* @param {Object} atts Widget attributes.
*
* @return {boolean} Whether the custom theme is created.
*/
// eslint-disable-next-line complexity
maybeCreateCustomTheme( atts ) {
const currentStyles = app.getCurrentStyleAttributes( atts );
const isWPFormsTheme = !! themesData.wpforms[ atts.wpformsTheme ];
const isCustomTheme = !! themesData.custom[ atts.wpformsTheme ];
// It is one of the default themes without any changes.
if (
isWPFormsTheme &&
JSON.stringify( themesData.wpforms[ atts.wpformsTheme ]?.settings ) === JSON.stringify( currentStyles )
) {
return false;
}
// It is a modified default theme OR unknown custom theme.
if ( isWPFormsTheme || ! isCustomTheme ) {
app.createCustomTheme( atts, currentStyles );
}
return true;
},
/**
* Create a custom theme.
*
* @since 1.9.6
*
* @param {Object} atts Widget properties.
* @param {Object} currentStyles Current style settings.
* @param {boolean} migrateToCustomTheme Whether it is necessary to migrate to custom theme.
*
* @return {boolean} Whether the custom theme is created.
*/
createCustomTheme( atts, currentStyles = null, migrateToCustomTheme = false ) { // eslint-disable-line complexity
let counter = 0;
let themeSlug = atts.wpformsTheme;
const baseTheme = app.getTheme( atts.wpformsTheme ) || themesData.wpforms.default;
let themeName = baseTheme.name;
themesData.custom = themesData.custom || {};
if ( migrateToCustomTheme ) {
themeSlug = 'custom';
themeName = strings.theme_custom;
}
// Determine the theme slug and the number of copies.
do {
counter++;
themeSlug = themeSlug + '-copy-' + counter;
} while ( themesData.custom[ themeSlug ] && counter < 10000 );
const copyStr = counter < 2 ? strings.theme_copy : strings.theme_copy + ' ' + counter;
themeName += ' (' + copyStr + ')';
// The first migrated Custom Theme should be without a ` (Copy)` suffix.
themeName = migrateToCustomTheme && counter < 2 ? strings.theme_custom : themeName;
// Add the new custom theme.
themesData.custom[ themeSlug ] = {
name: themeName,
settings: currentStyles || app.getCurrentStyleAttributes( atts ),
};
app.updateEnabledThemes( themeSlug, themesData.custom[ themeSlug ] );
const widget = elementor.getPanelView().getCurrentPageView().getOption( 'editedElementView' );
const settingsModel = widget.model.get( 'settings' );
settingsModel.setExternalChange( {
wpformsTheme: themeSlug,
isCustomTheme: 'true',
customThemeName: themeName,
} );
return true;
},
/**
* Maybe create a custom theme by given attributes.
*
* @since 1.9.6
*
* @param {Object} attributes Widget attributes.
*
* @return {string} New theme's slug.
*/
maybeCreateCustomThemeFromAttributes( attributes ) { // eslint-disable-line complexity
const newThemeSlug = attributes.theme;
/**
* @type {Object|null}
* @property {Object} settings Theme settings.
*/
const existingTheme = app.getTheme( attributes.theme );
const keys = Object.keys( attributes );
let isExistingTheme = Boolean( existingTheme?.settings );
// Check if the theme already exists and has the same settings.
if ( isExistingTheme ) {
for ( const i in keys ) {
const key = keys[ i ];
if ( ! existingTheme.settings[ key ] || existingTheme.settings[ key ] !== attributes[ key ] ) {
isExistingTheme = false;
break;
}
}
}
// The theme exists and has the same settings.
if ( isExistingTheme ) {
return newThemeSlug;
}
// The theme doesn't exist.
// Normalize the attributes to the default theme settings.
const defaultAttributes = Object.keys( themesData.wpforms.default.settings );
const newSettings = {};
for ( const i in defaultAttributes ) {
const attr = defaultAttributes[ i ];
newSettings[ attr ] = attributes[ attr ] ?? '';
}
// Create a new custom theme.
themesData.custom[ newThemeSlug ] = {
name: attributes.themeName ?? strings.theme_custom,
settings: newSettings,
};
app.updateEnabledThemes( newThemeSlug, themesData.custom[ newThemeSlug ] );
return newThemeSlug;
},
/**
* Update custom theme.
*
* @since 1.9.6
*
* @param {string} attribute Attribute name.
* @param {string} value New attribute value.
* @param {Object} atts Widget properties.
*/
updateCustomThemeAttribute( attribute, value, atts ) { // eslint-disable-line complexity
const themeSlug = atts.wpformsTheme;
// Skip if it is one of the WPForms themes OR the attribute is not in the theme settings.
if (
themesData.wpforms[ themeSlug ] ||
(
attribute !== 'themeName' &&
! themesData.wpforms.default.settings[ attribute ]
)
) {
return;
}
// Skip if the custom theme doesn't exist in some rare cases.
if ( ! themesData.custom[ themeSlug ] ) {
return;
}
// Update the theme data.
if ( attribute === 'themeName' ) {
themesData.custom[ themeSlug ].name = value;
} else {
themesData.custom[ themeSlug ].settings = themesData.custom[ themeSlug ].settings || themesData.wpforms.default.settings;
themesData.custom[ themeSlug ].settings[ attribute ] = value;
}
},
/**
* Set the widget theme.
*
* @since 1.9.6
*
* @param {string} themeSlug The theme slug.
*
* @return {boolean} True on success.
*/
setWidgetTheme( themeSlug ) { // eslint-disable-line complexity
if ( app.maybeDisplayUpgradeModal( themeSlug ) ) {
return false;
}
const theme = app.getTheme( themeSlug );
if ( ! theme?.settings ) {
return false;
}
const attributes = Object.keys( theme.settings );
const widget = elementor.getPanelView().getCurrentPageView().getOption( 'editedElementView' );
const settingsModel = widget.model.get( 'settings' );
const isCustomTheme = !! themesData.custom[ themeSlug ];
// Set the theme attribute.
settingsModel.setExternalChange( {
wpformsTheme: themeSlug,
isCustomTheme: isCustomTheme ? 'true' : '',
customThemeName: isCustomTheme ? themesData.custom[ themeSlug ].name : '',
} );
// Clean up the attributes.
const cleanSettings = {};
for ( const key in attributes ) {
const attr = attributes[ key ];
const value = theme.settings[ attr ];
cleanSettings[ attr ] = typeof value === 'string'
? value.replace( /px$/, '' )
: value;
}
// Update the theme settings.
app.updateStylesAtts( cleanSettings, settingsModel );
// Activate the Publish button.
const $pageView = elementor.getPanelView().getCurrentPageView().$el;
$pageView.find( '.elementor-control-isCustomTheme input' ).trigger( 'input' );
return true;
},
/**
* Update styles atts.
*
* @since 1.9.6
*
* @param {Object} themeSettings Theme settings.
* @param {Object} settingsModel Settings model.
*/
// eslint-disable-next-line complexity
updateStylesAtts( themeSettings, settingsModel ) {
const allowedKeys = WPFormsElementorModern.getStyleAttributesKeys();
const validSettings = {};
for ( const key in themeSettings ) {
if ( ! allowedKeys.includes( key ) ) {
continue;
}
let value = themeSettings[ key ];
if ( key === 'backgroundUrl' && typeof value === 'string' ) {
const match = value.match( /^url\(\s*['"]?(.*?)['"]?\s*\)$/i );
if ( match && match[ 1 ] ) {
value = { id: '', url: match[ 1 ] };
} else {
value = '';
}
}
validSettings[ key ] = value;
}
// Update the widget settings.
if ( Object.keys( validSettings ).length ) {
settingsModel.setExternalChange( validSettings );
}
},
/**
* Maybe display upgrades modal in Lite.
*
* @since 1.9.6
*
* @param {string} themeSlug The theme slug.
*
* @return {boolean} True if modal was displayed.
*/
maybeDisplayUpgradeModal( themeSlug ) {
if ( ! app.isDisabledTheme( themeSlug ) ) {
return false;
}
if ( ! isPro ) {
WPFormsElementorModern.showProModal( 'themes', strings.form_themes );
return true;
}
if ( ! isLicenseActive ) {
WPFormsElementorModern.showLicenseModal( strings.form_themes );
return true;
}
return false;
},
/**
* Select widget theme event handler.
*
* @since 1.9.6
*
* @param {string} value New attribute value.
*/
selectTheme( value ) {
if ( ! app.setWidgetTheme( value ) ) {
return;
}
app.onSelectThemeWithBG( value );
},
/**
* Change theme name event handler.
*
* @since 1.9.6
*
* @param {string} value New attribute value.
* @param {Object} model Model object.
*/
changeThemeName( value, model ) {
app.updateCustomThemeAttribute( 'themeName', value, model.attributes );
},
/**
* Open the theme delete confirmation window.
*
* @since 1.9.6
*
* @param {Object} atts Widget properties.
* @param {Object} editor Editor object.
*/
deleteThemeModal( atts, editor ) {
const themeName = app.getTheme( atts.wpformsTheme )?.name;
const confirm = strings.theme_delete_confirm.replace( '%1$s', `<b>${ themeName }</b>` );
const content = `<p class="wpforms-theme-delete-text">${ confirm } ${ strings.theme_delete_cant_undone }</p>`;
const $panelContent = editor.$childViewContainer[ 0 ];
const $themesControl = $( $panelContent ).find( '.wpforms-elementor-themes-control' );
const dialog = elementorCommon.dialogsManager.createWidget( 'confirm', {
message: content,
headerMessage: strings.theme_delete_title,
onConfirm: () => {
// Remove theme from the theme storage.
delete themesData.custom[ atts.wpformsTheme ];
app.selectTheme( 'default' );
app.updateThemesList( editor, $themesControl );
},
} );
dialog.show();
},
/**
* Open stock photos install modal on the select theme.
*
* @since 1.9.6
*
* @param {string} themeSlug The theme slug.
*/
onSelectThemeWithBG( themeSlug ) {
if ( WPFormsElementorModern.stockPhotos.isPicturesAvailable() ) {
return;
}
// Check only WPForms themes.
if ( ! app.isWPFormsTheme( themeSlug ) ) {
return;
}
/**
* @type {Object|null}
* @property {Object|null} settings Settings.
*/
const theme = app.getTheme( themeSlug );
const bgUrl = theme.settings?.backgroundUrl;
if ( bgUrl?.length && bgUrl !== 'url()' ) {
WPFormsElementorModern.stockPhotos.installModal( 'themes' );
}
},
/**
* Determine if the user is on a Mac.
*
* @return {boolean} True if the user is on a Mac.
*/
isMac() {
return navigator.userAgent.includes( 'Macintosh' );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsElementorThemes.init();
@@ -0,0 +1,15 @@
var WPFormsElementorThemes=window.WPFormsElementorThemes||((e,m)=>{let{isAdmin:t,isPro:r,isLicenseActive:o,strings:u,route_namespace:s}=wpformsElementorVars,n={},h={wpforms:null,custom:null},a=null,i={},c={init(){i.$window=m(e),c.fetchThemesData(),c.events()},events(){m(e).on("elementor/frontend/init",function(){elementor.channels.editor.on("section:activated",c.themesControlSetup)})},getAllThemes(){return{...h.custom||{},...h.wpforms||{}}},themesControlSetup(e,t){"themes"===e&&"wpforms"===t.model.attributes.widgetType&&(e=t.$childViewContainer[0],e=m(e).find(".wpforms-elementor-themes-control"),c.isMac()&&e.addClass("wpforms-is-mac"),c.updateThemesList(t,e))},updateThemesList(e,t){var s=e.model.attributes.settings.attributes.wpformsTheme??"default",s=c.getThemesListMarkup(s);t.html(s),c.addThemesEvents(t,e)},addThemesEvents(s,r){let t=_.debounce(e=>{c.maybeCreateCustomTheme(e)},300);var e=r.model.get("settings");"true"!==e.attributes.isMigrated&&c.maybeMigrateToCustomTheme(e,s,r),e.on("change",e=>{t(e.attributes),c.maybeUpdateCustomTheme(e)});let o=s.find('[role="radio"]');o.off("click").on("click",function(){o.removeClass("is-active"),m(this).addClass("is-active");var e=m(this).val();c.selectTheme(e)}),elementor.channels.editor.off("WPFormsDeleteThemeButtonClick").on("WPFormsDeleteThemeButtonClick",()=>{c.deleteThemeModal(r.model.attributes.settings.attributes,r)}),r.model.get("settings").off("change:customThemeName").on("change:customThemeName",function(e){var t=e.get("customThemeName");c.changeThemeName(t,e),c.updateThemesList(r,s)})},maybeMigrateToCustomTheme(e,t,s){var r=e._previousAttributes,o=e.attributes;"copyPasteJsonValue"in r&&!r.wpformsTheme&&!o.isCustomTheme&&(r=c.getCurrentStyleAttributes(e.attributes),c.createCustomTheme(e.attributes,r,!0),c.updateThemesList(s,t)),e.setExternalChange({isMigrated:"true"})},maybeUpdateCustomTheme(e){var t=e.attributes;if("true"===t.isCustomTheme){var s,r,o=e.changed,m=WPFormsElementorModern.getStyleAttributesKeys();for(s in o)m.includes(s)&&(r=WPFormsElementorModern.prepareComplexAttrValues(o[s],s),c.updateCustomThemeAttribute(s,r,t))}},getThemesListMarkup(e){if(!h.wpforms)return c.fetchThemesData(),`<div class="wpforms-no-themes">${u.themes_error}</div>`;var t=c.getAllThemes();if(!t)return"";var s=Object.keys(t);let r,o;let m="";for(var n in c.isWPFormsTheme(e)||(o=e,m+=c.getThemesItemMarkup(c.getTheme(o),o,o)),s){n=s[n];o&&o===n||((r={...t.default,...t[n]||{}}).settings={...t.default.settings,...r.settings||{}},m+=c.getThemesItemMarkup(r,n,e))}return`<div role="radiogroup" class="wpforms-elementor-themes-radio-group">
${m}
</div>`},getThemesItemMarkup(e,t,s){var r,o;return e?(r=0<e.name?.length?e.name:u.theme_noname,o="wpforms-elementor-themes-radio ",`<button type="button" class="${t===s?"is-active":""}" value="${t}" role="radio">
<div class="wpforms-elementor-themes-radio ${o+=c.isDisabledTheme(t)?"wpforms-elementor-themes-radio-disabled":" wpforms-elementor-themes-radio-enabled"}">
<div class="wpforms-elementor-themes-radio-title">${r}</div>
</div>
<div class="wpforms-elementor-themes-indicators">
<span class="component-color-indicator" title="${u.button_background}" style="background: ${e.settings.buttonBackgroundColor};" data-index="0"></span>
<span class="component-color-indicator" title="${u.button_text}" style="background: ${e.settings.buttonTextColor}" data-index="1"></span>
<span class="component-color-indicator" title="${u.field_label}" style="background: ${e.settings.labelColor};" data-index="2"></span>
<span class="component-color-indicator" title="${u.field_sublabel} " style="background: ${e.settings.labelSublabelColor};" data-index="3"></span>
<span class="component-color-indicator" title="${u.field_border}" style="background: ${e.settings.fieldBorderColor};" data-index="4"></span>
</div>
</button>`):""},getTheme(e){return c.getAllThemes()[e]||null},getEnabledThemes(){if(!a){let s=c.getAllThemes();if(r&&o)return s;a=Object.keys(s).reduce((e,t)=>(s[t].settings?.fieldSize&&!s[t].disabled&&(e[t]=s[t]),e),{})}return a},updateEnabledThemes(e,t){a=a&&{...a,[e]:t}},isDisabledTheme(e){return!c.getEnabledThemes()?.[e]},isWPFormsTheme(e){return Boolean(h.wpforms[e]?.settings)},fetchThemesData(){if(!n.isFetchingThemes&&!h.wpforms){n.isFetchingThemes=!0;try{wp.apiFetch({path:s+"elementor/themes/",method:"GET",cache:"no-cache"}).then(e=>{h.wpforms=e.wpforms||{},h.custom=e.custom||{}}).catch(e=>{console.error(e?.message)}).finally(()=>{n.isFetchingThemes=!1})}catch(e){console.error(e)}}},saveCustomThemes(){if(t&&!n.isSavingThemes&&h.custom){n.isSavingThemes=!0;try{wp.apiFetch({path:s+"elementor/themes/custom/",method:"POST",data:{customThemes:h.custom}}).then(e=>{e?.result||console.log(e?.error)}).catch(e=>{console.error(e?.message)}).finally(()=>{n.isSavingThemes=!1})}catch(e){console.error(e)}}},getCurrentStyleAttributes(e){var t,s=Object.keys(h.wpforms.default?.settings),r={};for(t in s){var o=s[t];r[o]=WPFormsElementorModern.prepareComplexAttrValues(e[o],s[t])??""}return r},maybeCreateCustomTheme(e){var t=c.getCurrentStyleAttributes(e),s=!!h.wpforms[e.wpformsTheme],r=!!h.custom[e.wpformsTheme];return!(s&&JSON.stringify(h.wpforms[e.wpformsTheme]?.settings)===JSON.stringify(t)||(!s&&r||c.createCustomTheme(e,t),0))},createCustomTheme(e,t=null,s=!1){let r=0,o=e.wpformsTheme;let m=(c.getTheme(e.wpformsTheme)||h.wpforms.default).name;for(h.custom=h.custom||{},s&&(o="custom",m=u.theme_custom);r++,o=o+"-copy-"+r,h.custom[o]&&r<1e4;);var n=r<2?u.theme_copy:u.theme_copy+" "+r;return m+=" ("+n+")",m=s&&r<2?u.theme_custom:m,h.custom[o]={name:m,settings:t||c.getCurrentStyleAttributes(e)},c.updateEnabledThemes(o,h.custom[o]),elementor.getPanelView().getCurrentPageView().getOption("editedElementView").model.get("settings").setExternalChange({wpformsTheme:o,isCustomTheme:"true",customThemeName:m}),!0},maybeCreateCustomThemeFromAttributes(e){var t=e.theme,s=c.getTheme(e.theme),r=Object.keys(e);let o=Boolean(s?.settings);if(o)for(var m in r){m=r[m];if(!s.settings[m]||s.settings[m]!==e[m]){o=!1;break}}if(!o){var n,a=Object.keys(h.wpforms.default.settings),i={};for(n in a){var l=a[n];i[l]=e[l]??""}h.custom[t]={name:e.themeName??u.theme_custom,settings:i},c.updateEnabledThemes(t,h.custom[t])}return t},updateCustomThemeAttribute(e,t,s){s=s.wpformsTheme;h.wpforms[s]||"themeName"!==e&&!h.wpforms.default.settings[e]||h.custom[s]&&("themeName"===e?h.custom[s].name=t:(h.custom[s].settings=h.custom[s].settings||h.wpforms.default.settings,h.custom[s].settings[e]=t))},setWidgetTheme(e){if(c.maybeDisplayUpgradeModal(e))return!1;var t=c.getTheme(e);if(!t?.settings)return!1;var s,r=Object.keys(t.settings),o=elementor.getPanelView().getCurrentPageView().getOption("editedElementView").model.get("settings"),m=!!h.custom[e],n=(o.setExternalChange({wpformsTheme:e,isCustomTheme:m?"true":"",customThemeName:m?h.custom[e].name:""}),{});for(s in r){var a=r[s],i=t.settings[a];n[a]="string"==typeof i?i.replace(/px$/,""):i}return c.updateStylesAtts(n,o),elementor.getPanelView().getCurrentPageView().$el.find(".elementor-control-isCustomTheme input").trigger("input"),!0},updateStylesAtts(t,e){var s,r,o=WPFormsElementorModern.getStyleAttributesKeys(),m={};for(s in t)if(o.includes(s)){let e=t[s];"backgroundUrl"===s&&"string"==typeof e&&(r=e.match(/^url\(\s*['"]?(.*?)['"]?\s*\)$/i),e=r&&r[1]?{id:"",url:r[1]}:""),m[s]=e}Object.keys(m).length&&e.setExternalChange(m)},maybeDisplayUpgradeModal(e){return!(!c.isDisabledTheme(e)||(r?o||(WPFormsElementorModern.showLicenseModal(u.form_themes),0):(WPFormsElementorModern.showProModal("themes",u.form_themes),0)))},selectTheme(e){c.setWidgetTheme(e)&&c.onSelectThemeWithBG(e)},changeThemeName(e,t){c.updateCustomThemeAttribute("themeName",e,t.attributes)},deleteThemeModal(e,t){var s=c.getTheme(e.wpformsTheme)?.name,s=`<p class="wpforms-theme-delete-text">${u.theme_delete_confirm.replace("%1$s",`<b>${s}</b>`)} ${u.theme_delete_cant_undone}</p>`,r=t.$childViewContainer[0];let o=m(r).find(".wpforms-elementor-themes-control");elementorCommon.dialogsManager.createWidget("confirm",{message:s,headerMessage:u.theme_delete_title,onConfirm:()=>{delete h.custom[e.wpformsTheme],c.selectTheme("default"),c.updateThemesList(t,o)}}).show()},onSelectThemeWithBG(e){WPFormsElementorModern.stockPhotos.isPicturesAvailable()||c.isWPFormsTheme(e)&&(e=c.getTheme(e).settings?.backgroundUrl)?.length&&"url()"!==e&&WPFormsElementorModern.stockPhotos.installModal("themes")},isMac(){return navigator.userAgent.includes("Macintosh")}};return c})((document,window),jQuery);WPFormsElementorThemes.init();