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,109 @@
/* global wpforms_builder_themes_no_access */
/**
* @param wpforms_builder_themes_no_access.strings.permission_modal.confirm
* @param wpforms_builder_themes_no_access.strings.permission_modal.content
* @param wpforms_builder_themes_no_access.strings.permission_modal.title
*/
// noinspection ES6ConvertVarToLetConst
/**
* WPForms Form builder themes - Non-admin version.
*
* @since 1.9.8
*/
var WPForms = window.WPForms || {}; // eslint-disable-line no-var
WPForms.Admin = WPForms.Admin || {};
WPForms.Admin.Builder = WPForms.Admin.Builder || {};
WPForms.Admin.Builder.ThemesNoAccess = WPForms.Admin.Builder.ThemesNoAccess || ( function( document, window, $ ) {
/**
* Elements holder.
*
* @since 1.9.8
*
* @type {Object}
*/
const el = {};
/**
* Public functions and properties.
*
* @since 1.9.8
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.8
*/
init() {
$( app.ready );
},
/**
* Start the engine.
*
* @since 1.9.8
*/
ready() {
app.setup();
app.events();
},
/**
* Setup. Prepare some variables.
*
* @since 1.9.8
*/
setup() {
// Cache DOM elements.
el.$builder = $( '#wpforms-builder' );
},
/**
* Setup events.
*
* @since 1.9.8
*/
events() {
el.$builder.on( 'wpformsPanelSectionSwitch', app.handlePanelSectionSwitch );
},
/**
* Handle panel section switch and show permission modal.
*
* @since 1.9.8
*
* @param {Object} _event The event object.
* @param {string} section The section that was switched to.
*/
handlePanelSectionSwitch( _event, section ) {
if ( section === 'themes' ) {
$.alert( {
title: wpforms_builder_themes_no_access.strings.permission_modal.title,
content: wpforms_builder_themes_no_access.strings.permission_modal.content,
icon: 'fa fa-exclamation-triangle',
type: 'red',
theme: 'modern',
buttons: {
confirm: {
text: wpforms_builder_themes_no_access.strings.permission_modal.confirm,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
_event.preventDefault();
}
},
};
// Return the public-facing methods.
return app;
}( document, window, jQuery ) );
// Initialize.
WPForms.Admin.Builder.ThemesNoAccess.init();
@@ -0,0 +1 @@
var WPForms=window.WPForms||{};WPForms.Admin=WPForms.Admin||{},WPForms.Admin.Builder=WPForms.Admin.Builder||{},WPForms.Admin.Builder.ThemesNoAccess=WPForms.Admin.Builder.ThemesNoAccess||(n=>{let e={},s={init(){n(s.ready)},ready(){s.setup(),s.events()},setup(){e.$builder=n("#wpforms-builder")},events(){e.$builder.on("wpformsPanelSectionSwitch",s.handlePanelSectionSwitch)},handlePanelSectionSwitch(e,s){"themes"===s&&(n.alert({title:wpforms_builder_themes_no_access.strings.permission_modal.title,content:wpforms_builder_themes_no_access.strings.permission_modal.content,icon:"fa fa-exclamation-triangle",type:"red",theme:"modern",buttons:{confirm:{text:wpforms_builder_themes_no_access.strings.permission_modal.confirm,btnClass:"btn-confirm",keys:["enter"]}}}),e.preventDefault())}};return s})((document,window,jQuery)),WPForms.Admin.Builder.ThemesNoAccess.init();
@@ -0,0 +1,264 @@
/* global wpforms_builder_themes, wpf, WPFormsUtils */
// noinspection ES6ConvertVarToLetConst
/**
* WPForms Form builder themes.
*
* @since 1.9.7
*/
var WPForms = window.WPForms || {}; // eslint-disable-line no-var
WPForms.Admin = WPForms.Admin || {};
WPForms.Admin.Builder = WPForms.Admin.Builder || {};
WPForms.Admin.Builder.Themes = WPForms.Admin.Builder.Themes || ( function( document, window, $ ) {
/**
* Elements holder.
*
* @since 1.9.7
*
* @type {Object}
*/
const el = {};
/**
* Public functions and properties.
*
* @since 1.9.7
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.7
*/
init() {
$( app.ready );
},
/**
* Start the engine.
*
* @since 1.9.7
*/
ready() {
app.setup();
app.loadModules();
},
/**
* Setup. Prepare some variables.
*
* @since 1.9.7
*/
setup() {
// Cache DOM elements.
el.$builder = $( '#wpforms-builder' );
},
/**
* Centralized reactive store for theme style settings in the Form Builder.
*
* Provides a simple pub/sub mechanism to track and respond to setting changes.
* Automatically initializes its state from all inputs/selects matching `name="settings[themes][...]"`.
*
* Usage examples:
*
* // Subscribe to a specific setting change.
* app.store.subscribe('buttonTextColor', (value) => {
* console.log('Button text color changed to:', value);
* } );
*
* // Subscribe to all setting changes.
* app.store.subscribeAll(( value, key) => {
* console.log( Setting ${ key } changed to:`, value);
* });
*
* // Get current value.
* const padding = app.store.get('containerPadding');
*
* // Manually update a setting (will trigger listeners).
* // Use the 3rd argument as 'true' to set in a 'silent' mode when state and input will update,
* // but no listeners will be run.
* app.store.set('fieldSize', 'medium');
*
* @since 1.9.7
*
* @type {Object}
* @property {Function} get Get current value of a setting by key.
* @property {Function} set Set value of a setting by key and notify listeners.
* @property {Function} subscribe Subscribe to a specific setting change: (key, callback) => void
* @property {Function} subscribeAll Subscribe to all setting changes: (callback) => void
* @property {Function} initFromDOM Initialize the store from DOM inputs/selects.
* @property {Object} state Raw internal state object (mostly for debugging).
*/
store: ( () => {
const state = {};
const keyListeners = new Map();
const inputElements = new Map();
const globalListeners = [];
const debouncedSetters = {};
const DEBOUNCE_DELAY = 50;
// Settings getter.
const get = ( key ) => state[ key ];
// Settings setter.
const set = ( key, value, silent = false ) => {
if ( state[ key ] === value ) {
return;
}
state[ key ] = value;
const $input = inputElements.get( key );
if ( $input && $input.val() !== value ) {
$input.val( value );
if ( ! silent ) {
$input.trigger( 'input' );
}
}
if ( silent ) {
return;
}
if ( keyListeners.has( key ) ) {
$.each( keyListeners.get( key ), ( _, cb ) => cb( value, key ) );
}
$.each( globalListeners, ( _, cb ) => cb( value, key ) );
};
// Get bounced Setter.
const getDebouncedSetter = ( key ) => {
if ( ! debouncedSetters[ key ] ) {
debouncedSetters[ key ] = _.debounce( ( value ) => set( key, value ), DEBOUNCE_DELAY );
}
return debouncedSetters[ key ];
};
// Allow subscribing to specific setting change.
const subscribe = ( key, callback ) => {
if ( ! keyListeners.has( key ) ) {
keyListeners.set( key, [] );
}
keyListeners.get( key ).push( callback );
};
// Allow listening all settings change.
const subscribeAll = ( callback ) => {
globalListeners.push( callback );
};
// Initialize from DOM (should be called once during app init).
const initFromDOM = () => {
$( '[name^="settings[themes]"]' ).each( function() {
const $input = $( this );
const nameMatch = $input.attr( 'name' ).match( /\[themes]\[(.*?)]/ );
if ( ! nameMatch ) {
return;
}
const key = nameMatch[ 1 ];
state[ key ] = $input.val();
inputElements.set( key, $input );
const tag = $input.prop( 'tagName' ).toLowerCase();
const type = ( $input.attr( 'type' ) || '' ).toLowerCase();
const isDebouncedInput = (
( tag === 'input' && ( type === 'text' || type === 'number' || type === 'hidden' ) ) ||
tag === 'textarea'
);
$input.on( isDebouncedInput ? 'input' : 'change', function() {
const value = $( this ).val();
if ( isDebouncedInput ) {
getDebouncedSetter( key )( value );
} else {
set( key, value );
}
} );
} );
};
return {
get,
set,
subscribe,
subscribeAll,
initFromDOM,
inputElements,
state,
};
}
)(),
/**
* Get setting.
*
* @since 1.9.7
*
* @param {null|string} key Settings key.
*
* @return {Object} Setting value.
*/
getSettings( key = null ) {
if ( key ) {
return app.store.get( key );
}
return app.store.state;
},
/**
* Get controls.
*
* @since 1.9.7
*
* @param {null|string} key Settings key.
*
* @return {Object} Control
*/
getControls( key = null ) {
if ( key ) {
return app.store.inputElements.get( key );
}
return app.store.inputElements;
},
/**
* Load modules.
*
* @since 1.9.7
*/
loadModules() {
const modules = wpforms_builder_themes.modules || [];
// Import all modules dynamically.
Promise.all( modules.map( ( module ) => import( module.path ) ) )
.then( ( importedModules ) => {
importedModules.forEach( ( module, index ) => {
const moduleName = modules[ index ].name;
app[ moduleName ] = module.default( document, window, $ );
// Initialize module on `wpformsBuilderThemesLoaded` event.
el.$builder.on( `wpformsBuilderThemesLoaded`, app[ moduleName ].init );
} );
// Trigger `wpformsBuilderThemesLoaded` event.
WPFormsUtils.triggerEvent( el.$builder, 'wpformsBuilderThemesLoaded', [ importedModules ] );
} )
.catch( ( error ) => {
wpf.debug( 'Error importing modules:', error );
} );
},
};
// Return the public-facing methods.
return app;
}( document, window, jQuery ) );
// Initialize.
WPForms.Admin.Builder.Themes.init();
@@ -0,0 +1 @@
var WPForms=window.WPForms||{};WPForms.Admin=WPForms.Admin||{},WPForms.Admin.Builder=WPForms.Admin.Builder||{},WPForms.Admin.Builder.Themes=WPForms.Admin.Builder.Themes||((s,i,d)=>{let n={},o={init(){d(o.ready)},ready(){o.setup(),o.loadModules()},setup(){n.$builder=d("#wpforms-builder")},store:(()=>{let n={},i=new Map,o=new Map,m=[],a={},l=50;let u=(r,s,e=!1)=>{var t;n[r]===s||(n[r]=s,(t=o.get(r))&&t.val()!==s&&(t.val(s),e||t.trigger("input")),e)||(i.has(r)&&d.each(i.get(r),(e,t)=>t(s,r)),d.each(m,(e,t)=>t(s,r)))};return{get:e=>n[e],set:u,subscribe:(e,t)=>{i.has(e)||i.set(e,[]),i.get(e).push(t)},subscribeAll:e=>{m.push(e)},initFromDOM:()=>{d('[name^="settings[themes]"]').each(function(){var e=d(this),t=e.attr("name").match(/\[themes]\[(.*?)]/);if(t){let r=t[1];n[r]=e.val(),o.set(r,e);var t=e.prop("tagName").toLowerCase(),i=(e.attr("type")||"").toLowerCase();let s="input"===t&&("text"===i||"number"===i||"hidden"===i)||"textarea"===t;e.on(s?"input":"change",function(){var t,e=d(this).val();s?(t=r,a[t]||(a[t]=_.debounce(e=>u(t,e),l)),a[t])(e):u(r,e)})}})},inputElements:o,state:n}})(),getSettings(e=null){return e?o.store.get(e):o.store.state},getControls(e=null){return e?o.store.inputElements.get(e):o.store.inputElements},loadModules(){let r=wpforms_builder_themes.modules||[];Promise.all(r.map(e=>import(e.path))).then(e=>{e.forEach((e,t)=>{t=r[t].name;o[t]=e.default(s,i,d),n.$builder.on("wpformsBuilderThemesLoaded",o[t].init)}),WPFormsUtils.triggerEvent(n.$builder,"wpformsBuilderThemesLoaded",[e])}).catch(e=>{wpf.debug("Error importing modules:",e)})}};return o})(document,window,jQuery),WPForms.Admin.Builder.Themes.init();
@@ -0,0 +1,221 @@
/* global wpforms_builder_themes */
/**
* WPForms Form Builder Themes: Advanced settings module.
*
* @since 1.9.7
*
* @param {Object} document Document object.
* @param {Object} window Window object.
* @param {jQuery} $ jQuery object.
*
* @return {Object} Public functions and properties.
*/
export default function( document, window, $ ) { // eslint-disable-line max-lines-per-function
const WPForms = window.WPForms || {};
const WPFormsBuilderThemes = WPForms.Admin.Builder.Themes || {};
/**
* Elements holder.
*
* @since 1.9.7
*
* @type {Object}
*/
const el = {};
/**
* Public functions and properties.
*
* @since 1.9.7
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.7
*/
init() {
app.setup();
app.events();
// Subscribe to all settings change.
WPFormsBuilderThemes.store.subscribeAll( ( value, key ) => {
app.updateCopyPasteContent( value, key );
} );
app.disableSpellCheck();
app.updateCopyPasteContent();
},
/**
* Setup.
*
* @since 1.9.7
*/
setup() {
el.$builder = $( '#wpforms-builder' );
},
/**
* Setup.
*
* @since 1.9.7
*/
events() {
},
/**
* Get the list of the settings key allowed to show in the Copy/paste field.
*
* @since 1.9.7
* @return {Array} List of allowed settings.
*/
getAllowedKeys() {
const allowedKeys = [ 'themeName', 'isCustomTheme', 'wpformsTheme', 'customCss' ];
const styleSettings = WPFormsBuilderThemes.common.getStyleAttributesKeys();
return allowedKeys.concat( styleSettings );
},
/**
* Update the content of the "Copy/Paste" field.
*
* @since 1.9.7
*
* @param {string} value Setting value
* @param {string} key Setting key.
*/
updateCopyPasteContent( value = '', key = '' ) {
if ( key === 'copyPasteJsonValue' ) {
app.pasteSettings( value );
return;
}
const content = {};
const allowedKeys = app.getAllowedKeys();
const settings = WPFormsBuilderThemes.getSettings();
allowedKeys.forEach( ( settingKey ) => {
content[ settingKey ] = settings[ settingKey ];
} );
// Update field content in a 'silent' mode.
WPFormsBuilderThemes.store.set( 'copyPasteJsonValue', JSON.stringify( content ), true );
},
/**
* Paste settings handler.
*
* @since 1.9.7
*
* @param {string} value New attribute value.
*/
pasteSettings( value ) {
value = value.trim();
const pasteAttributes = app.parseValidateJson( value );
// Show the error modal if JSON is broken.
if ( ! pasteAttributes ) {
if ( value ) {
app.showJsonErrorModal();
}
return;
}
const themeSlug = pasteAttributes?.wpformsTheme ?? pasteAttributes?.theme;
const currentThemeSlug = WPFormsBuilderThemes.store.get( 'wpformsTheme' );
const theme = WPFormsBuilderThemes.themes.getTheme( themeSlug );
// If the theme already exists - set it.
if ( theme && themeSlug !== currentThemeSlug ) {
WPFormsBuilderThemes.themes.setFormTheme( themeSlug );
WPFormsBuilderThemes.themes.updateThemesList();
return;
}
// For not existed theme - parse and set settings.
const allowedKeys = app.getAllowedKeys();
allowedKeys.forEach( ( settingKey ) => {
if ( pasteAttributes[ settingKey ] !== undefined ) {
let settingValue = pasteAttributes[ settingKey ];
settingValue = typeof settingValue === 'string'
? settingValue.replace( /px$/, '' )
: settingValue;
WPFormsBuilderThemes.store.set( settingKey, settingValue );
}
} );
},
/**
* Parse and validate JSON string.
*
* @since 1.9.7
*
* @param {string} value JSON string.
*
* @return {boolean|object} Parsed JSON object OR false on error.
*/
parseValidateJson( value ) {
if ( typeof value !== 'string' ) {
return false;
}
let atts;
try {
atts = JSON.parse( value.trim() );
} catch ( error ) {
atts = false;
}
return atts;
},
/**
* Show the error when pasted JSON is broken.
*
* @since 1.9.7
*/
showJsonErrorModal() {
$.alert( {
title: wpforms_builder_themes.strings.uhoh,
content: wpforms_builder_themes.strings.copy_paste_error,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
cancel: {
text: wpforms_builder_themes.strings.close,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Disable spellcheck for the textarea settings fields.
*
* @since 1.9.7
*/
disableSpellCheck() {
const customCssControl = WPFormsBuilderThemes.getControls( 'customCss' );
const copyPasteControl = WPFormsBuilderThemes.getControls( 'copyPasteJsonValue' );
if ( ! customCssControl || ! copyPasteControl ) {
return;
}
copyPasteControl.attr( 'spellcheck', 'false' );
customCssControl.attr( 'spellcheck', 'false' );
},
};
return app;
}
@@ -0,0 +1 @@
export default function(e,t,s){let o=(t.WPForms||{}).Admin.Builder.Themes||{},r={},l={init(){l.setup(),l.events(),o.store.subscribeAll((e,t)=>{l.updateCopyPasteContent(e,t)}),l.disableSpellCheck(),l.updateCopyPasteContent()},setup(){r.$builder=s("#wpforms-builder")},events(){},getAllowedKeys(){var e=o.common.getStyleAttributesKeys();return["themeName","isCustomTheme","wpformsTheme","customCss"].concat(e)},updateCopyPasteContent(e="",r=""){if("copyPasteJsonValue"===r)l.pasteSettings(e);else{let t={};r=l.getAllowedKeys();let s=o.getSettings();r.forEach(e=>{t[e]=s[e]}),o.store.set("copyPasteJsonValue",JSON.stringify(t),!0)}},pasteSettings(e){e=e.trim();let s=l.parseValidateJson(e);var t,r;s?(t=s?.wpformsTheme??s?.theme,r=o.store.get("wpformsTheme"),o.themes.getTheme(t)&&t!==r?(o.themes.setFormTheme(t),o.themes.updateThemesList()):l.getAllowedKeys().forEach(t=>{if(void 0!==s[t]){let e=s[t];e="string"==typeof e?e.replace(/px$/,""):e,o.store.set(t,e)}})):e&&l.showJsonErrorModal()},parseValidateJson(e){if("string"!=typeof e)return!1;let t;try{t=JSON.parse(e.trim())}catch(e){t=!1}return t},showJsonErrorModal(){s.alert({title:wpforms_builder_themes.strings.uhoh,content:wpforms_builder_themes.strings.copy_paste_error,icon:"fa fa-exclamation-circle",type:"red",buttons:{cancel:{text:wpforms_builder_themes.strings.close,btnClass:"btn-confirm",keys:["enter"]}}})},disableSpellCheck(){var e=o.getControls("customCss"),t=o.getControls("copyPasteJsonValue");e&&t&&(t.attr("spellcheck","false"),e.attr("spellcheck","false"))}};return l}
@@ -0,0 +1,367 @@
/**
* WPForms Form Builder Themes: Background module.
*
* @since 1.9.7
*
* @param {Object} document Document object.
* @param {Object} window Window object.
* @param {jQuery} $ jQuery object.
*
* @return {Object} Public functions and properties.
*/
export default function( document, window, $ ) {// eslint-disable-line max-lines-per-function
const WPForms = window.WPForms || {};
const WPFormsBuilderThemes = WPForms.Admin.Builder.Themes || {};
/**
* Elements holder.
*
* @since 1.9.7
*
* @type {Object}
*/
const el = {};
/**
* Public functions and properties.
*
* @since 1.9.7
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.7
*/
init() {
app.setup();
app.events();
WPFormsBuilderThemes.store.subscribe( 'backgroundUrl', ( value ) => {
app.setImagePreview( value );
app.maybeShowChooseButton();
} );
WPFormsBuilderThemes.store.subscribe( 'backgroundImage', ( value ) => {
app.maybeShowImageSelector( value );
app.maybeShowChooseButton();
} );
WPFormsBuilderThemes.store.subscribe( 'backgroundSizeMode', ( value ) => {
app.handleSizeFromDimensions( value );
} );
},
/**
* Setup.
*
* @since 1.9.7
*/
setup() {
el.$builder = $( '#wpforms-builder' );
el.$preview = $( '#wpforms-builder-themes-preview' );
el.$imageSelector = $( '.wpforms-builder-themes-background-selector' );
el.$imagePreview = el.$imageSelector.find( '.wpforms-builder-themes-bg-image-preview' );
el.$chooseButton = el.$imageSelector.find( '.wpforms-builder-themes-bg-image-choose' );
el.$removeButton = el.$imageSelector.find( '.wpforms-builder-themes-bg-image-remove' );
app.initImageSelector();
},
/**
* Events.
*
* @since 1.9.7
*/
events() {
el.$builder
.on( 'click', '.wpforms-builder-themes-bg-image-remove', app.removeImage )
.on( 'click', '.wpforms-builder-themes-bg-image-choose, .wpforms-builder-themes-bg-image-preview', app.chooseImage );
},
/**
* Init the Image Selector control.
*
* @since 1.9.7
*/
initImageSelector() {
const settings = WPFormsBuilderThemes.getSettings();
el.$imageSelector.removeClass( 'wpforms-hidden' );
app.setImagePreview( settings.backgroundUrl );
app.maybeShowChooseButton();
},
/**
* Handle image selector control state.
*
* @since 1.9.7
* @param {string} value `backgroundImage` setting value.
*/
maybeShowImageSelector( value ) {
if ( value === 'none' ) {
el.$imageSelector.addClass( 'wpforms-hidden' );
} else {
el.$imageSelector.removeClass( 'wpforms-hidden' );
const backgroundUrl = WPFormsBuilderThemes.store.get( 'backgroundUrl' );
// Here we need to clean the url value and set a new one.
// Otherwise, the picture preview won't be updated.
WPFormsBuilderThemes.store.set( 'backgroundUrl', 'url()' );
WPFormsBuilderThemes.store.set( 'backgroundUrl', backgroundUrl );
}
},
/**
* Remove an image button handler.
*
* @param {Object} e Event object
*
* @since 1.9.7
*/
removeImage( e ) {
e.preventDefault();
WPFormsBuilderThemes.store.set( 'backgroundUrl', 'url()' );
el.$chooseButton.removeClass( 'wpforms-hidden' );
},
/**
* Choose an image button handler.
*
* @param {Object} e Event object
*
* @since 1.9.7
*/
chooseImage( e ) {
e.preventDefault();
const settings = WPFormsBuilderThemes.getSettings();
if ( settings.backgroundImage === 'library' ) {
app.openMediaLibrary();
} else {
WPFormsBuilderThemes.stockPhotos.openModal( 'bg-styles' );
}
app.maybeShowChooseButton();
},
/**
* Set the image preview.
*
* @param {null|string} value Image preview url value.
*
* @since 1.9.7
*/
setImagePreview( value = null ) {
const isHidden = ! value || value === 'url()';
const imageValue = isHidden ? 'url()' : `url(${ value })`;
el.$imagePreview.css( 'background-image', imageValue );
el.$imagePreview.toggleClass( 'wpforms-hidden', isHidden );
el.$removeButton.toggleClass( 'wpforms-hidden', isHidden );
},
/**
* Conditionally show or hide the `Choose Image` button.
*
* @since 1.9.7
*/
maybeShowChooseButton() {
const settings = WPFormsBuilderThemes.getSettings();
if ( settings.backgroundImage !== 'none' && settings.backgroundUrl === 'url()' ) {
el.$chooseButton.removeClass( 'wpforms-hidden' );
} else {
el.$chooseButton.addClass( 'wpforms-hidden' );
}
},
/**
* Open media library modal and handle image selection.
*
* @since 1.9.7
*/
openMediaLibrary() {
const frame = wp.media( {
multiple: false,
library: {
type: 'image',
},
} );
frame.on( 'select', () => {
const attachment = frame.state().get( 'selection' ).first().toJSON();
if ( attachment.url ) {
WPFormsBuilderThemes.store.set( 'backgroundUrl', attachment.url );
}
} );
frame.open();
},
/**
* Handle the real size from image dimensions.
*
* @since 1.9.7
*
* @param {string} value Value.
*/
handleSizeFromDimensions( value ) {
const settings = WPFormsBuilderThemes.getSettings();
const $container = el.$preview.find( '.wpforms-container' )[ 0 ];
const backgroundWidth = WPFormsBuilderThemes.common.prepareComplexAttrValues( settings.backgroundWidth, 'backgroundWidth' );
const backgroundHeight = WPFormsBuilderThemes.common.prepareComplexAttrValues( settings.backgroundHeight, 'backgroundHeight' );
const $backgroundSizeControl = WPFormsBuilderThemes.getControls( 'backgroundSize' );
if ( value === 'cover' ) {
app.setContainerBackgroundWidth( $container, backgroundWidth );
app.setContainerBackgroundHeight( $container, backgroundHeight );
$container.style.setProperty( `--wpforms-background-size`, 'cover' );
$backgroundSizeControl.val( 'cover' );
} else {
$container.style.setProperty( `--wpforms-background-size`, backgroundWidth + ' ' + backgroundHeight );
$backgroundSizeControl.val( backgroundWidth + ' ' + backgroundHeight );
}
$backgroundSizeControl.trigger( 'input' );
},
/**
* Handle real size from height.
*
* @since 1.9.7
*
* @param {HTMLElement} container Form preview container
* @param {string} value Value.
* @param {Object} atts Form style settings.
*/
handleSizeFromHeight( container, value, atts ) {
const backgroundWidth = WPFormsBuilderThemes.common.prepareComplexAttrValues( atts.backgroundWidth, 'backgroundWidth' );
const $backgroundSizeControl = WPFormsBuilderThemes.getControls( 'backgroundSize' );
app.setContainerBackgroundHeight( container, value );
if ( atts.backgroundSizeMode !== 'cover' ) {
$backgroundSizeControl.val( backgroundWidth + ' ' + value );
container.style.setProperty( `--wpforms-background-size`, backgroundWidth + ' ' + value );
$backgroundSizeControl.trigger( 'input' );
}
},
/**
* Handle real size from width.
*
* @since 1.9.7
*
* @param {HTMLElement} container Form preview container
* @param {string} value Value.
* @param {Object} atts Form style settings.
*/
handleSizeFromWidth( container, value, atts ) {
const backgroundWidth = WPFormsBuilderThemes.common.prepareComplexAttrValues( atts.backgroundWidth, 'backgroundWidth' );
const backgroundHeight = WPFormsBuilderThemes.common.prepareComplexAttrValues( atts.backgroundHeight, 'backgroundHeight' );
const $backgroundSizeControl = WPFormsBuilderThemes.getControls( 'backgroundSize' );
app.setContainerBackgroundWidth( container, backgroundWidth );
if ( atts.backgroundSizeMode !== 'cover' ) {
$backgroundSizeControl.val( value + ' ' + backgroundHeight );
container.style.setProperty( `--wpforms-background-size`, value + ' ' + backgroundHeight );
$backgroundSizeControl.trigger( 'input' );
}
},
/**
* Set the container background color.
*
* @since 1.9.7
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*/
setBackgroundColor( container, value ) {
container.style.setProperty( `--wpforms-background-color`, value );
},
/**
* Set the container background url.
*
* @since 1.9.7
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*/
setBackgroundUrl( container, value ) {
container.style.setProperty( `--wpforms-background-url`, value );
},
/**
* Set the container background height.
*
* @since 1.9.7
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*/
setContainerBackgroundHeight( container, value ) {
container.style.setProperty( `--wpforms-background-height`, value );
},
/**
* Set the container background image.
*
* @since 1.9.7
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*/
setContainerBackgroundImage( container, value ) {
if ( value === 'none' ) {
container.style.setProperty( `--wpforms-background-url`, 'url()' );
}
},
/**
* Set the container background position.
*
* @since 1.9.7
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*/
setContainerBackgroundPosition( container, value ) {
container.style.setProperty( `--wpforms-background-position`, value );
},
/**
* Set container background repeat.
*
* @since 1.9.7
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*/
setContainerBackgroundRepeat( container, value ) {
container.style.setProperty( `--wpforms-background-repeat`, value );
},
/**
* Set the container background width.
*
* @since 1.9.7
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*/
setContainerBackgroundWidth( container, value ) {
container.style.setProperty( `--wpforms-background-width`, value );
},
};
return app;
}
@@ -0,0 +1 @@
export default function(e,r,o){let n=(r.WPForms||{}).Admin.Builder.Themes||{},a={},i={init(){i.setup(),i.events(),n.store.subscribe("backgroundUrl",e=>{i.setImagePreview(e),i.maybeShowChooseButton()}),n.store.subscribe("backgroundImage",e=>{i.maybeShowImageSelector(e),i.maybeShowChooseButton()}),n.store.subscribe("backgroundSizeMode",e=>{i.handleSizeFromDimensions(e)})},setup(){a.$builder=o("#wpforms-builder"),a.$preview=o("#wpforms-builder-themes-preview"),a.$imageSelector=o(".wpforms-builder-themes-background-selector"),a.$imagePreview=a.$imageSelector.find(".wpforms-builder-themes-bg-image-preview"),a.$chooseButton=a.$imageSelector.find(".wpforms-builder-themes-bg-image-choose"),a.$removeButton=a.$imageSelector.find(".wpforms-builder-themes-bg-image-remove"),i.initImageSelector()},events(){a.$builder.on("click",".wpforms-builder-themes-bg-image-remove",i.removeImage).on("click",".wpforms-builder-themes-bg-image-choose, .wpforms-builder-themes-bg-image-preview",i.chooseImage)},initImageSelector(){var e=n.getSettings();a.$imageSelector.removeClass("wpforms-hidden"),i.setImagePreview(e.backgroundUrl),i.maybeShowChooseButton()},maybeShowImageSelector(e){"none"===e?a.$imageSelector.addClass("wpforms-hidden"):(a.$imageSelector.removeClass("wpforms-hidden"),e=n.store.get("backgroundUrl"),n.store.set("backgroundUrl","url()"),n.store.set("backgroundUrl",e))},removeImage(e){e.preventDefault(),n.store.set("backgroundUrl","url()"),a.$chooseButton.removeClass("wpforms-hidden")},chooseImage(e){e.preventDefault(),"library"===n.getSettings().backgroundImage?i.openMediaLibrary():n.stockPhotos.openModal("bg-styles"),i.maybeShowChooseButton()},setImagePreview(e=null){var r=!e||"url()"===e,e=r?"url()":`url(${e})`;a.$imagePreview.css("background-image",e),a.$imagePreview.toggleClass("wpforms-hidden",r),a.$removeButton.toggleClass("wpforms-hidden",r)},maybeShowChooseButton(){var e=n.getSettings();"none"!==e.backgroundImage&&"url()"===e.backgroundUrl?a.$chooseButton.removeClass("wpforms-hidden"):a.$chooseButton.addClass("wpforms-hidden")},openMediaLibrary(){let r=wp.media({multiple:!1,library:{type:"image"}});r.on("select",()=>{var e=r.state().get("selection").first().toJSON();e.url&&n.store.set("backgroundUrl",e.url)}),r.open()},handleSizeFromDimensions(e){var r=n.getSettings(),o=a.$preview.find(".wpforms-container")[0],t=n.common.prepareComplexAttrValues(r.backgroundWidth,"backgroundWidth"),r=n.common.prepareComplexAttrValues(r.backgroundHeight,"backgroundHeight"),s=n.getControls("backgroundSize");"cover"===e?(i.setContainerBackgroundWidth(o,t),i.setContainerBackgroundHeight(o,r),o.style.setProperty("--wpforms-background-size","cover"),s.val("cover")):(o.style.setProperty("--wpforms-background-size",t+" "+r),s.val(t+" "+r)),s.trigger("input")},handleSizeFromHeight(e,r,o){var t=n.common.prepareComplexAttrValues(o.backgroundWidth,"backgroundWidth"),s=n.getControls("backgroundSize");i.setContainerBackgroundHeight(e,r),"cover"!==o.backgroundSizeMode&&(s.val(t+" "+r),e.style.setProperty("--wpforms-background-size",t+" "+r),s.trigger("input"))},handleSizeFromWidth(e,r,o){var t=n.common.prepareComplexAttrValues(o.backgroundWidth,"backgroundWidth"),s=n.common.prepareComplexAttrValues(o.backgroundHeight,"backgroundHeight"),a=n.getControls("backgroundSize");i.setContainerBackgroundWidth(e,t),"cover"!==o.backgroundSizeMode&&(a.val(r+" "+s),e.style.setProperty("--wpforms-background-size",r+" "+s),a.trigger("input"))},setBackgroundColor(e,r){e.style.setProperty("--wpforms-background-color",r)},setBackgroundUrl(e,r){e.style.setProperty("--wpforms-background-url",r)},setContainerBackgroundHeight(e,r){e.style.setProperty("--wpforms-background-height",r)},setContainerBackgroundImage(e,r){"none"===r&&e.style.setProperty("--wpforms-background-url","url()")},setContainerBackgroundPosition(e,r){e.style.setProperty("--wpforms-background-position",r)},setContainerBackgroundRepeat(e,r){e.style.setProperty("--wpforms-background-repeat",r)},setContainerBackgroundWidth(e,r){e.style.setProperty("--wpforms-background-width",r)}};return i}
@@ -0,0 +1,955 @@
/* global wpf, wpforms_builder_themes, WPFormsBuilder, wpforms_education, WPFormsEducation, WPFormsUtils */
/**
* WPForms Form Builder Themes: Common module.
*
* @since 1.9.7
*
* @param {Object} document Document object.
* @param {Object} window Window object.
* @param {jQuery} $ jQuery object.
*
* @return {Object} Public functions and properties.
*/
export default function( document, window, $ ) {// eslint-disable-line max-lines-per-function
const WPForms = window.WPForms || {};
const WPFormsBuilderThemes = WPForms.Admin.Builder.Themes || {};
/**
* Localized data aliases.
*
* @since 1.9.7
*/
const { isPro, isLicenseActive, isModern, isFullStyles, isLowFormPagesVersion, strings } = wpforms_builder_themes;
/**
* Elements holder.
*
* @since 1.9.7
*
* @type {Object}
*/
const el = {};
/**
* Field dependencies configuration.
*
* @since 1.9.7
*
* @type {Object}
*/
const fieldDependencies = {
fieldBorderStyle: {
none: {
disable: [ 'fieldBorderSize', 'fieldBorderColor' ],
},
},
buttonBorderStyle: {
none: {
disable: [ 'buttonBorderSize', 'buttonBorderColor' ],
},
},
containerBorderStyle: {
none: {
disable: [ 'containerBorderWidth', 'containerBorderColor' ],
},
},
backgroundImage: {
none: {
hide: [ 'backgroundPosition', 'backgroundRepeat', 'backgroundSizeMode', 'backgroundWidth', 'backgroundHeight' ],
},
},
backgroundSizeMode: {
cover: {
hide: [ 'backgroundWidth', 'backgroundHeight' ],
},
},
};
/**
* Public functions and properties.
*
* @since 1.9.7
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.7
*/
init() {
app.setup();
app.events();
// Maybe show the sidebar after page reload.
app.handlePanelSwitch();
// Init color pickers.
app.loadColorPickers();
// Init settings store.
WPFormsBuilderThemes.store.initFromDOM();
// Subscribe to all settings change.
WPFormsBuilderThemes.store.subscribeAll( ( value, key ) => {
app.changeStyleSettings( value, key );
app.handleFieldDependencies( key, value );
} );
// Render already saved settings.
app.renderSavedSettings();
// Apply initial dependencies.
app.applyAllDependencies();
// Block PRO controls.
app.blockProSections();
// Run checks.
app.runChecks();
},
/**
* Setup.
*
* @since 1.9.7
*/
setup() {
el.$builder = $( '#wpforms-builder' );
el.$settings = $( '.wpforms-panel-content-section-themes' );
el.$sidebar = $( '#wpforms-builder-themes-sidebar' );
el.$preview = $( '#wpforms-builder-themes-preview' );
el.$tabs = $( '#wpforms-builder-themes-sidebar-tabs > a' );
// Set the custom class to sidebar content for macOS.
if ( app.isMac() ) {
el.$sidebar.find( '.wpforms-builder-themes-sidebar-content' ).addClass( 'wpforms-is-mac' );
}
},
/**
* Setup.
*
* @since 1.9.7
*/
events() {
el.$builder
.on( 'click', '#wpforms-builder-themes-back', app.handleClosePreviewSidebar )
.on( 'click', '.wpforms-panel-sidebar-section-themes', app.handleOpenPreviewSidebar )
.on( 'wpformsPanelSwitched', '.wpforms-panel-sidebar-section-themes', app.handlePanelSwitch )
.on( 'wpformsPanelSectionSwitch', app.handlePanelSectionSwitch )
.on( 'click', '.wpforms-panel-settings-button.active[data-panel="settings"]', app.handleSettingsTabClick );
el.$tabs.on( 'click', app.handleTabClick );
},
/**
* Handle sidebar closing when the 'Settings' tab button is clicked.
*
* @since 1.9.7
*/
handleSettingsTabClick() {
if ( el.$sidebar.hasClass( 'wpforms-hidden' ) ) {
return;
}
app.handleClosePreviewSidebar( null );
},
/**
* Handle field dependencies when a field value changes.
*
* @since 1.9.7
*
* @param {string} fieldKey The field key that changed.
* @param {string} fieldValue The new field value.
*/
handleFieldDependencies( fieldKey, fieldValue ) {
// After handling the specific field dependency, re-apply all dependencies
// to ensure all conditions are properly evaluated with current values.
app.applyFieldDependency( fieldKey, fieldValue );
app.applyAllDependencies();
},
/**
* Apply dependency for a specific field.
*
* @since 1.9.7
*
* @param {string} fieldKey The field key that changed.
* @param {string} fieldValue The new field value.
*/
applyFieldDependency( fieldKey, fieldValue ) {
if ( ! fieldDependencies[ fieldKey ] ) {
return;
}
const dependencies = fieldDependencies[ fieldKey ];
// Check each condition for the field.
// eslint-disable-next-line complexity
Object.keys( dependencies ).forEach( ( conditionValue ) => {
const condition = dependencies[ conditionValue ];
const shouldApply = fieldValue === conditionValue;
// Handle disable conditions.
if ( condition.disable && shouldApply ) {
condition.disable.forEach( ( dependentField ) => {
app.disableField( dependentField );
} );
} else if ( condition.disable ) {
condition.disable.forEach( ( dependentField ) => {
app.enableField( dependentField );
} );
}
// Handle enable conditions.
if ( condition.enable && shouldApply ) {
condition.enable.forEach( ( dependentField ) => {
app.enableField( dependentField );
} );
} else if ( condition.enable ) {
condition.enable.forEach( ( dependentField ) => {
app.disableField( dependentField );
} );
}
// Handle hide conditions.
if ( condition.hide && shouldApply ) {
condition.hide.forEach( ( dependentField ) => {
app.hideField( dependentField );
} );
} else if ( condition.hide ) {
condition.hide.forEach( ( dependentField ) => {
app.showField( dependentField );
} );
}
// Handle show conditions.
if ( condition.show && shouldApply ) {
condition.show.forEach( ( dependentField ) => {
app.showField( dependentField );
} );
} else if ( condition.show ) {
condition.show.forEach( ( dependentField ) => {
app.hideField( dependentField );
} );
}
} );
},
/**
* Apply all dependencies based on current settings.
*
* @since 1.9.7
*/
applyAllDependencies() {
const settings = WPFormsBuilderThemes.getSettings();
Object.keys( fieldDependencies ).forEach( ( fieldKey ) => {
const fieldValue = settings[ fieldKey ];
if ( fieldValue !== undefined ) {
app.applyFieldDependency( fieldKey, fieldValue );
}
} );
},
/**
* Disable a field and its wrapper.
*
* @since 1.9.7
*
* @param {string} fieldKey The field key to disable.
*/
disableField( fieldKey ) {
const $field = el.$sidebar.find( `[name*="${ fieldKey }"]` );
if ( $field.length ) {
$field.addClass( 'wpforms-builder-themes-disabled' );
}
},
/**
* Enable a field and its wrapper.
*
* @since 1.9.7
*
* @param {string} fieldKey The field key to enable.
*/
enableField( fieldKey ) {
const $field = el.$sidebar.find( `[name*="${ fieldKey }"]` );
if ( $field.length ) {
$field.removeClass( 'wpforms-builder-themes-disabled' );
}
},
/**
* Hide a field and its wrapper.
*
* @since 1.9.7
*
* @param {string} fieldKey The field key to hide.
*/
hideField( fieldKey ) {
const $field = el.$sidebar.find( `[name*="${ fieldKey }"]` );
const $wrapper = $field.parent().parent().hasClass( 'wpforms-builder-themes-conditional-hide' )
? $field.parent().parent()
: $field.parent( '.wpforms-panel-field' );
if ( $field.length ) {
$field.prop( 'disabled', true );
$wrapper.addClass( 'wpforms-builder-themes-hidden' );
}
},
/**
* Show a field and its wrapper.
*
* @since 1.9.7
*
* @param {string} fieldKey The field key to show.
*/
showField( fieldKey ) {
const $field = el.$sidebar.find( `[name*="${ fieldKey }"]` );
const $wrapper = $field.parent().parent().hasClass( 'wpforms-builder-themes-conditional-hide' )
? $field.parent().parent()
: $field.parent( '.wpforms-panel-field' );
if ( $field.length ) {
$field.prop( 'disabled', false );
$wrapper.removeClass( 'wpforms-builder-themes-hidden' );
}
},
/**
* Handle opening the custom settings sidebar.
*
* @since 1.9.7
*
* @param {Object} event The event object.
*/
handleOpenPreviewSidebar( event ) {
el.$sidebar?.removeClass( 'wpforms-hidden' );
event?.preventDefault();
},
/**
* Handle closing the custom settings sidebar.
*
* @since 1.9.7
*
* @param {Object} event The event object.
*/
handleClosePreviewSidebar( event ) {
el.$sidebar?.addClass( 'wpforms-hidden' );
event?.preventDefault();
},
/**
* Handle panel switch and maybe open the sidebar.
*
* @since 1.9.7
*/
handlePanelSwitch() {
if ( wpf.getQueryString( 'section' ) === 'themes' ) {
app.handleOpenPreviewSidebar( null );
}
},
/**
* Handle panel section switch and maybe open the sidebar.
*
* @since 1.9.7
*
* @param {Object} _event The event object.
* @param {string} section The section that was switched to.
*/
handlePanelSectionSwitch( _event, section ) {
if ( section === 'themes' ) {
app.checkForFormFeatures();
}
},
/**
* Handle tabs click.
*
* @since 1.9.7
*
* @param {Object} e Event object.
*/
handleTabClick( e ) {
e.preventDefault();
el.$tabs.toggleClass( 'active' );
$( '.wpforms-builder-themes-sidebar-tab-content' ).toggleClass( 'wpforms-hidden' );
},
/**
* Get a list of the style settings keys.
*
* @since 1.9.7
*
* @return {Array} Settings keys
*/
getStyleAttributesKeys() {
return [
'containerPadding',
'containerBorderStyle',
'containerBorderWidth',
'containerBorderRadius',
'containerShadowSize',
'containerBorderColor',
'fieldSize',
'fieldBorderStyle',
'fieldBorderRadius',
'fieldBorderSize',
'fieldBackgroundColor',
'fieldBorderColor',
'fieldTextColor',
'fieldMenuColor',
'pageBreakColor',
'labelSize',
'labelColor',
'labelSublabelColor',
'labelErrorColor',
'buttonSize',
'buttonBorderStyle',
'buttonBorderSize',
'buttonBorderRadius',
'buttonBackgroundColor',
'buttonBorderColor',
'buttonTextColor',
'backgroundColor',
'backgroundPosition',
'backgroundUrl',
'backgroundRepeat',
'backgroundSize',
'backgroundSizeMode',
'backgroundWidth',
'backgroundHeight',
'backgroundImage',
];
},
/**
* Get style handlers.
*
* @since 1.9.7
*
* @return {Object} Style handlers.
*/
getStyleHandlers() {
return {
'background-url': WPFormsBuilderThemes.background.setBackgroundUrl,
'background-image': WPFormsBuilderThemes.background.setContainerBackgroundImage,
'background-position': WPFormsBuilderThemes.background.setContainerBackgroundPosition,
'background-repeat': WPFormsBuilderThemes.background.setContainerBackgroundRepeat,
'background-color': WPFormsBuilderThemes.background.setBackgroundColor,
'background-height': WPFormsBuilderThemes.background.handleSizeFromHeight,
'background-width': WPFormsBuilderThemes.background.handleSizeFromWidth,
};
},
/**
* Change style setting handler.
*
* @since 1.9.7
*
* @param {string} settingValue Setting value.
* @param {string} settingKey Setting key.
*/
changeStyleSettings( settingValue, settingKey ) {// eslint-disable-line complexity
const wpformsContainer = el.$preview.find( '.wpforms-container' )[ 0 ];
if ( ! wpformsContainer ) {
return;
}
// Process only styles related settings
if ( ! app.getStyleAttributesKeys().includes( settingKey ) ) {
return;
}
const settings = WPFormsBuilderThemes.getSettings();
/**
* @type {Object}
*/
const property = settingKey.replace( /[A-Z]/g, ( letter ) => `-${ letter.toLowerCase() }` );
settingValue = app.prepareComplexAttrValues( settingValue, settingKey );
// Check for custom handlers.
if ( typeof app.getStyleHandlers()[ property ] === 'function' ) {
app.getStyleHandlers()[ property ]( wpformsContainer, settingValue, settings );
return;
}
switch ( property ) {
case 'field-size':
case 'label-size':
case 'button-size':
case 'container-shadow-size':
for ( const key in wpforms_builder_themes.sizes[ property ][ settingValue ] ) {
wpformsContainer.style.setProperty(
`--wpforms-${ property }-${ key }`,
wpforms_builder_themes.sizes[ property ][ settingValue ][ key ],
);
}
break;
case 'button-background-color':
app.maybeUpdateAccentColor( settings.buttonBorderColor, settingValue, wpformsContainer );
settingValue = app.maybeSetButtonAltBackgroundColor( settingValue, settings.buttonBorderColor, wpformsContainer );
app.maybeSetButtonAltTextColor( settings.buttonTextColor, settingValue, settings.buttonBorderColor, wpformsContainer );
wpformsContainer.style.setProperty( `--wpforms-${ property }`, settingValue );
break;
case 'button-border-color':
app.maybeUpdateAccentColor( settingValue, settings.buttonBackgroundColor, wpformsContainer );
app.maybeSetButtonAltTextColor( settings.buttonTextColor, settings.buttonBackgroundColor, settingValue, wpformsContainer );
wpformsContainer.style.setProperty( `--wpforms-${ property }`, settingValue );
break;
case 'button-text-color':
app.maybeSetButtonAltTextColor( settingValue, settings.buttonBackgroundColor, settings.buttonBorderColor, wpformsContainer );
wpformsContainer.style.setProperty( `--wpforms-${ property }`, settingValue );
break;
default:
wpformsContainer.style.setProperty( `--wpforms-${ property }`, settingValue );
wpformsContainer.style.setProperty( `--wpforms-${ property }-spare`, settingValue );
}
},
/**
* Maybe update accent color.
*
* @since 1.9.7
*
* @param {string} color Color value.
* @param {string} buttonBackgroundColor Button background color.
* @param {Object} container Form container.
*/
maybeUpdateAccentColor( color, buttonBackgroundColor, container ) {
// Setting the CSS property value to the child element overrides the parent property value.
const formWrapper = container.querySelector( '#builder-themes-form-preview-wrapper' );
// Fallback to the default color if the border color is transparent.
color = WPFormsUtils.cssColorsUtils.isTransparentColor( color ) ? '#066aab' : color;
if ( WPFormsUtils.cssColorsUtils.isTransparentColor( buttonBackgroundColor ) ) {
formWrapper.style.setProperty( '--wpforms-button-background-color-alt', 'rgba( 0, 0, 0, 0 )' );
formWrapper.style.setProperty( '--wpforms-button-background-color', color );
} else {
container.style.setProperty( '--wpforms-button-background-color-alt', buttonBackgroundColor );
formWrapper.style.setProperty( '--wpforms-button-background-color-alt', null );
formWrapper.style.setProperty( '--wpforms-button-background-color', null );
}
},
/**
* Maybe set the button's alternative background color.
*
* @since 1.9.7
*
* @param {string} value Setting value.
* @param {string} buttonBorderColor Button border color.
* @param {Object} container Form container.
*
* @return {string|*} New background color.
*/
maybeSetButtonAltBackgroundColor( value, buttonBorderColor, container ) {
// Setting the CSS property value to the child element overrides the parent property value.
const formWrapper = container.querySelector( '#builder-themes-form-preview-wrapper' );
formWrapper.style.setProperty( '--wpforms-button-background-color-alt', value );
if ( WPFormsUtils.cssColorsUtils.isTransparentColor( value ) ) {
return WPFormsUtils.cssColorsUtils.isTransparentColor( buttonBorderColor ) ? '#066aab' : buttonBorderColor;
}
return value;
},
/**
* Maybe set the button's alternative text color.
*
* @since 1.9.7
*
* @param {string} value Setting value.
* @param {string} buttonBackgroundColor Button background color.
* @param {string} buttonBorderColor Button border color.
* @param {Object} container Form container.
*/
maybeSetButtonAltTextColor( value, buttonBackgroundColor, buttonBorderColor, container ) {
const formWrapper = container.querySelector( '#builder-themes-form-preview-wrapper' );
let altColor = null;
value = value.toLowerCase();
if (
WPFormsUtils.cssColorsUtils.isTransparentColor( value ) ||
value === buttonBackgroundColor ||
(
WPFormsUtils.cssColorsUtils.isTransparentColor( buttonBackgroundColor ) &&
value === buttonBorderColor
)
) {
altColor = WPFormsUtils.cssColorsUtils.getContrastColor( buttonBackgroundColor );
}
container.style.setProperty( `--wpforms-button-text-color-alt`, value );
formWrapper.style.setProperty( `--wpforms-button-text-color-alt`, altColor );
},
/**
* Prepare complex setting values.
*
* @since 1.9.7
*
* @param {string|Object} value Setting value.
* @param {string} key Attribute key.
*
* @return {string|Object} Prepared setting value.
*/
prepareComplexAttrValues( value, key ) {
const pxItems = [
'fieldBorderRadius',
'fieldBorderSize',
'buttonBorderRadius',
'buttonBorderSize',
'containerPadding',
'containerBorderWidth',
'containerBorderRadius',
'backgroundWidth',
'backgroundHeight',
];
if ( pxItems.includes( key ) ) {
if ( typeof value === 'number' || ( typeof value === 'string' && ! value.trim().endsWith( 'px' ) ) ) {
value = `${ value }px`;
}
}
if ( key === 'backgroundUrl' ) {
if ( typeof value === 'string' && ! value.trim().startsWith( 'url(' ) ) {
value = value ? `url( ${ value } )` : 'url()';
}
}
// Remove spaces after/before braces in rgb/rgba colors.
value = app.removeRgbaSpaces( value );
return value;
},
/**
* Remove extra spaces in rgba/rgb values.
*
* @since 1.9.7
*
* @param {string} value Setting value.
*
* @return {string} Prepared setting value.
*/
removeRgbaSpaces( value ) {
if ( typeof value !== 'string' || ! value.includes( 'rgb' ) ) {
return value;
}
return value
.replace( /\(\s*/g, '(' )
.replace( /\s*\)/g, ')' );
},
/**
* Render already saved settings.
*
* @since 1.9.7
*/
renderSavedSettings() {
const wpformsContainer = el.$preview.find( '.wpforms-container' )[ 0 ];
if ( ! wpformsContainer ) {
return;
}
const settings = WPFormsBuilderThemes.getSettings();
_.each( settings, ( value, key ) => {
app.changeStyleSettings( value, key );
} );
},
/**
* Custom loader for color pickers.
*
* @since 1.9.7
*/
loadColorPickers() {
WPFormsBuilder.loadColorPickers( el.$sidebar, {
position: 'top left',
} );
},
/**
* Disable PRO sections.
*
* @since 1.9.7
*/
blockProSections() {
if ( isPro && isLicenseActive ) {
return;
}
const $proSectionsHeadings = $( '.wpforms-add-fields-heading[data-group="background_styles"], .wpforms-add-fields-heading[data-group="container_styles"]' );
const proSections = $( '.wpforms-builder-themes-pro-section' );
// Disable sections and show the PRO badge.
proSections.addClass( 'wpforms-builder-themes-disabled-pro' );
$proSectionsHeadings.addClass( 'wpforms-builder-themes-pro-blocked' );
// Disable clicks on blocked sections.
proSections.off( 'click' ).on( 'click', app.handleProSectionClick );
},
/**
* Disable all sections.
*
* @since 1.9.7
* @param {boolean} unblock Need to unblock status.
*/
blockAllSections( unblock = false ) {
const sections = el.$sidebar.find( '.wpforms-add-fields-buttons, .wpforms-builder-themes-sidebar-advanced' );
// Disable/Enable all sections.
if ( ! unblock ) {
sections.addClass( 'wpforms-builder-themes-disabled' );
} else {
sections.removeClass( 'wpforms-builder-themes-disabled' );
}
},
/**
* Handle the PRO section click.
*
* @since 1.9.7
*/
handleProSectionClick() {
const section = $( this ).prev( 'a' ).data( 'group' )?.replace( '_styles', '' );
if ( ! isPro ) {
app.showProModal( section, strings.pro_sections[ section ] );
return;
}
if ( ! isLicenseActive ) {
app.showLicenseModal( strings.pro_sections[ section ], strings.pro_sections[ section ], 'pro-section' );
}
},
/**
* Open the educational popup for users with no Pro license.
*
* @since 1.9.7
*
* @param {string} panel Panel slug.
* @param {string} feature Feature name.
*/
showProModal( panel, feature ) {
const type = 'pro';
const message = wpforms_education.upgrade[ type ].message_plural.replace( /%name%/g, feature );
const utmContent = {
container: 'General Container Styles',
background: 'General Background Styles',
themes: 'General Pro Themes',
};
$.alert( {
backgroundDismiss: true,
title: feature + ' ' + wpforms_education.upgrade[ type ].title_plural,
icon: 'fa fa-lock',
content: message,
boxWidth: '550px',
theme: 'modern,wpforms-education',
closeIcon: true,
onOpenBefore: function() { // eslint-disable-line object-shorthand
this.$btnc.after( '<div class="discount-note">' + wpforms_education.upgrade_bonus + '</div>' );
this.$btnc.after( wpforms_education.upgrade[ type ].doc.replace( /%25name%25/g, 'AP - ' + feature ) );
this.$body.find( '.jconfirm-content' ).addClass( 'lite-upgrade' );
},
buttons: {
confirm: {
text: wpforms_education.upgrade[ type ].button,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action: () => {
window.open( WPFormsEducation.core.getUpgradeURL( utmContent[ panel ], type ), '_blank' );
WPFormsEducation.core.upgradeModalThankYou( type );
},
},
},
} );
},
/**
* Open license modal.
*
* @since 1.9.7
*
* @param {string} feature Feature name.
* @param {string} fieldName Field name.
* @param {string} utmContent UTM content.
*/
showLicenseModal( feature, fieldName, utmContent ) {
WPFormsEducation.proCore.licenseModal( feature, fieldName, utmContent );
},
/**
* Run custom checks.
*
* @since 1.9.7
*/
runChecks() {
app.checkForClassicStyles();
if ( isPro && isLicenseActive && isModern && isFullStyles ) {
app.checkForFormFeatures();
}
app.checkForOldFP();
},
/**
* Conditionally show/hide classic styles notice and block/unblock controls.
*
* @since 1.9.7
*/
checkForClassicStyles() {
const $notice = $( '.wpforms-builder-themes-style-notice' );
const $previewNotice = $( '.wpforms-builder-themes-preview-notice' );
if ( ! isModern || ! isFullStyles ) {
app.blockAllSections();
$notice.removeClass( 'wpforms-hidden' );
$previewNotice.addClass( 'wpforms-hidden' );
}
},
/**
* Check both Lead Forms and Conversational Forms states and update the UI accordingly.
*
* @since 1.9.7
*/
checkForFormFeatures() {
const $LFSwitch = $( '#wpforms-panel-field-lead_forms-enable' );
const $CFSwitch = $( '#wpforms-panel-field-settings-conversational_forms_enable' );
const isLFEnabled = $LFSwitch.prop( 'checked' ) ?? false;
const isCFEnabled = $CFSwitch.prop( 'checked' ) ?? false;
const $LFNotice = $( '.wpforms-builder-themes-lf-notice' );
const $CFNotice = $( '.wpforms-builder-themes-cf-notice' );
const $previewNotice = $( '.wpforms-builder-themes-preview-notice' );
// Handle Lead Forms notice visibility
if ( isLFEnabled ) {
$LFNotice.removeClass( 'wpforms-hidden' );
} else {
$LFNotice.addClass( 'wpforms-hidden' );
}
// Handle Conversational Forms notice visibility
if ( isCFEnabled ) {
$CFNotice.removeClass( 'wpforms-hidden' );
} else {
$CFNotice.addClass( 'wpforms-hidden' );
}
// If either feature is enabled, hide preview and block sections
if ( isLFEnabled || isCFEnabled ) {
$previewNotice.addClass( 'wpforms-hidden' );
el.$preview.addClass( 'wpforms-hidden' );
app.blockAllSections();
} else {
// Only if both features are disabled, show preview and unblock sections
el.$preview.removeClass( 'wpforms-hidden' );
if ( isModern && isFullStyles ) {
app.blockAllSections( true );
$previewNotice.removeClass( 'wpforms-hidden' );
}
}
// Set up event handlers if they haven't been set up yet
app.setupFormFeatureEventHandlers();
},
/**
* Set up event handlers for Lead Forms and Conversational Forms switches.
*
* @since 1.9.7
*/
setupFormFeatureEventHandlers() {
// Set up notice link handlers
$( '.wpforms-builder-themes-lf-notice a' ).off( 'click', app.openLFSettings ).on( 'click', app.openLFSettings );
$( '.wpforms-builder-themes-cf-notice a' ).off( 'click', app.openCFSettings ).on( 'click', app.openCFSettings );
},
/**
* Shows the notice if the Form Pages addons version is low.
*
* @since 1.9.7
*/
checkForOldFP() {
const $FPContent = $( '#wpforms-form-pages-content-block' );
const $notice = $( '#wpforms-page-forms-fbst-notice' );
if ( $FPContent.length ) {
if ( isLowFormPagesVersion ) {
$FPContent.prepend( $notice );
$notice.removeClass( 'wpforms-hidden' );
}
}
},
/**
* Open the Lead Forms settings page.
*
* @since 1.9.7
*
* @param {Object} event Event object.
*/
openLFSettings( event ) {
app.handleClosePreviewSidebar( event );
$( 'a.wpforms-panel-sidebar-section-lead_forms' ).click();
},
/**
* Open the Conversational Forms settings page.
*
* @since 1.9.7
*
* @param {Object} event Event object.
*/
openCFSettings( event ) {
app.handleClosePreviewSidebar( event );
$( 'a.wpforms-panel-sidebar-section-conversational_forms' ).click();
},
/**
* 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;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,356 @@
/* global wpforms_builder_themes */
/**
* WPForms Form Builder Themes: Stock Photos module.
*
* @since 1.9.7
*
* @param {Object} document Document object.
* @param {Object} window Window object.
* @param {jQuery} $ jQuery object.
*
* @return {Object} Public functions and properties.
*/
// eslint-disable-next-line max-lines-per-function
export default function( document, window, $ ) { // eslint-disable-line no-unused-vars
const WPForms = window.WPForms || {};
const WPFormsBuilderThemes = WPForms.Admin.Builder.Themes || {};
/**
* Localized data aliases.
*
* @since 1.9.7
*/
const strings = wpforms_builder_themes.strings;
const routeNamespace = wpforms_builder_themes.route_namespace;
const pictureUrlPath = wpforms_builder_themes.stockPhotos?.urlPath;
/**
* Elements holder.
*
* @since 1.9.7
*
* @type {Object}
*/
const el = {};
/**
* Spinner markup.
*
* @since 1.9.7
*
* @type {string}
*/
const spinner = '<i class="wpforms-loading-spinner wpforms-loading-white wpforms-loading-inline"></i>';
/**
* Runtime state.
*
* @since 1.9.7
*
* @type {Object}
*/
const state = {};
/**
* Stock photos pictures' list.
*
* @since 1.9.7
*
* @type {Array}
*/
let pictures = wpforms_builder_themes.stockPhotos?.pictures;
/**
* Stock photos picture selector markup.
*
* @since 1.9.7
*
* @type {string}
*/
let picturesMarkup = '';
/**
* Public functions and properties.
*
* @since 1.9.7
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.7
*/
init() {
app.setup();
app.events();
},
/**
* Setup.
*
* @since 1.9.7
*/
setup() {
el.$builder = $( '#wpforms-builder' );
},
/**
* Setup.
*
* @since 1.9.7
*/
events() {
},
/**
* Open stock photos modal.
*
* @since 1.9.7
*
* @param {string} from From where the modal was triggered, `themes` or `bg-styles`.
*/
openModal( from ) {
if ( app.isPicturesAvailable() ) {
app.picturesModal();
return;
}
app.installModal( from );
},
/**
* Open stock photos install modal on a select theme.
*
* @since 1.9.7
*
* @param {string} themeSlug The theme slug.
*/
onSelectTheme( themeSlug ) {
const themesModule = WPFormsBuilderThemes.themes;
if ( app.isPicturesAvailable() ) {
return;
}
// Check only WPForms themes.
if ( ! themesModule?.isWPFormsTheme( themeSlug ) ) {
return;
}
const theme = themesModule?.getTheme( themeSlug );
const bgUrl = theme.settings?.backgroundUrl;
if ( bgUrl?.length && bgUrl !== 'url()' ) {
app.installModal( 'themes' );
}
},
/**
* Open a modal prompting to download and install the Stock Photos.
*
* @since 1.9.7
*
* @param {string} from From where the modal was triggered, `themes` or `bg-styles`.
*/
installModal( from ) {
const installStr = from === 'themes' ? strings.stockInstallTheme : strings.stockInstallBg;
$.confirm( {
title: strings.heads_up,
content: installStr + ' ' + strings.stockInstall,
icon: 'wpforms-exclamation-circle',
type: 'orange wpforms-builder-themes-modal',
buttons: {
continue: {
text: strings.continue,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
// noinspection JSUnresolvedReference
this.$$continue.prop( 'disabled', true )
.html( spinner + strings.installing );
// noinspection JSUnresolvedReference
this.$$cancel
.prop( 'disabled', true );
app.install( this, from );
return false;
},
},
cancel: {
text: strings.cancel,
keys: [ 'esc' ],
},
},
} );
},
/**
* Display the modal window with an error message.
*
* @since 1.9.7
*
* @param {string} error Error message.
*/
errorModal( error ) {
$.alert( {
title: strings.uhoh,
content: error || strings.commonError,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
cancel: {
text : strings.close,
btnClass: 'btn-confirm',
keys : [ 'enter' ],
},
},
} );
},
/**
* Display the modal window with pictures.
*
* @since 1.9.7
*/
picturesModal() {
state.picturesModal = $.alert( {
title : `${ strings.picturesTitle }<p>${ strings.picturesSubTitle }</p>`,
content: app.getPictureMarkup(),
type: 'picture-selector wpforms-builder-themes-modal',
boxWidth: '800px',
closeIcon: true,
animation: 'opacity',
closeAnimation: 'opacity',
buttons: false,
onOpen() {
this.$content
.off( 'click' )
.on( 'click', '.wpforms-builder-stock-photos-picture', app.selectPicture );
},
} );
},
/**
* Install stock photos.
*
* @since 1.9.7
*
* @param {Object} modal The jQuery-confirm a modal window object.
* @param {string} from From where the modal was triggered, `themes` or `bg-styles`.
*/
install( modal, from ) {
// If a fetch is already in progress, exit the function.
if ( state.isInstalling ) {
return;
}
// Set the flag to true indicating a fetch is in progress.
state.isInstalling = true;
try {
// Fetch themes data.
wp.apiFetch( {
path: routeNamespace + 'stock-photos/install/',
method: 'POST',
cache: 'no-cache',
} ).then( ( response ) => {
if ( ! response.result ) {
app.errorModal( response.error );
return;
}
// Store the pictures' data.
pictures = response.pictures || [];
// Update the theme or open the picture selector modal.
if ( from === 'themes' ) {
WPFormsBuilderThemes.store.set( 'backgroundUrl', 'url()' );
WPFormsBuilderThemes.themes.setFormTheme( WPFormsBuilderThemes.store.get( 'wpformsTheme' ) );
} else {
app.picturesModal();
}
} ).catch( ( error ) => {
// eslint-disable-next-line no-console
console.error( error?.message );
app.errorModal( `<p>${ strings.commonError }</p><p>${ error?.message }</p>` );
} ).finally( () => {
state.isInstalling = false;
// Close the modal window.
modal.close();
} );
} catch ( error ) {
state.isInstalling = false;
// eslint-disable-next-line no-console
console.error( error );
app.errorModal( strings.commonError + '<br>' + error );
}
},
/**
* Detect whether pictures' data available.
*
* @since 1.9.7
*
* @return {boolean} True if pictures' data available, false otherwise.
*/
isPicturesAvailable() {
return Boolean( pictures?.length );
},
/**
* Generate the pictures' selector markup.
*
* @since 1.9.7
*
* @return {string} Pictures' selector markup.
*/
getPictureMarkup() {
if ( ! app.isPicturesAvailable() ) {
return '';
}
if ( picturesMarkup !== '' ) {
return picturesMarkup;
}
pictures.forEach( ( picture ) => {
const pictureUrl = pictureUrlPath + picture;
picturesMarkup += `<div class="wpforms-builder-stock-photos-picture"
data-url="${ pictureUrl }"
style="background-image: url( '${ pictureUrl }' )"
></div>`;
} );
picturesMarkup = `<div class="wpforms-builder-stock-photos-pictures-wrap">${ picturesMarkup }</div>`;
return picturesMarkup;
},
/**
* Select picture event handler.
*
* @since 1.9.7
*/
selectPicture() {
const pictureUrl = $( this ).data( 'url' );
// Update the settings.
WPFormsBuilderThemes.store.set( 'backgroundUrl', pictureUrl );
// Close the modal window.
state.picturesModal?.close();
},
};
return app;
}
@@ -0,0 +1,4 @@
export default function(e,t,s){let r=(t.WPForms||{}).Admin.Builder.Themes||{},o=wpforms_builder_themes.strings,l=wpforms_builder_themes.route_namespace,i=wpforms_builder_themes.stockPhotos?.urlPath,n={},c={},a=wpforms_builder_themes.stockPhotos?.pictures,u="",p={init(){p.setup(),p.events()},setup(){n.$builder=s("#wpforms-builder")},events(){},openModal(e){p.isPicturesAvailable()?p.picturesModal():p.installModal(e)},onSelectTheme(e){var t=r.themes;p.isPicturesAvailable()||t?.isWPFormsTheme(e)&&(t=(t?.getTheme(e)).settings?.backgroundUrl)?.length&&"url()"!==t&&p.installModal("themes")},installModal(e){var t="themes"===e?o.stockInstallTheme:o.stockInstallBg;s.confirm({title:o.heads_up,content:t+" "+o.stockInstall,icon:"wpforms-exclamation-circle",type:"orange wpforms-builder-themes-modal",buttons:{continue:{text:o.continue,btnClass:"btn-confirm",keys:["enter"],action(){return this.$$continue.prop("disabled",!0).html('<i class="wpforms-loading-spinner wpforms-loading-white wpforms-loading-inline"></i>'+o.installing),this.$$cancel.prop("disabled",!0),p.install(this,e),!1}},cancel:{text:o.cancel,keys:["esc"]}}})},errorModal(e){s.alert({title:o.uhoh,content:e||o.commonError,icon:"fa fa-exclamation-circle",type:"red",buttons:{cancel:{text:o.close,btnClass:"btn-confirm",keys:["enter"]}}})},picturesModal(){c.picturesModal=s.alert({title:`${o.picturesTitle}<p>${o.picturesSubTitle}</p>`,content:p.getPictureMarkup(),type:"picture-selector wpforms-builder-themes-modal",boxWidth:"800px",closeIcon:!0,animation:"opacity",closeAnimation:"opacity",buttons:!1,onOpen(){this.$content.off("click").on("click",".wpforms-builder-stock-photos-picture",p.selectPicture)}})},install(e,t){if(!c.isInstalling){c.isInstalling=!0;try{wp.apiFetch({path:l+"stock-photos/install/",method:"POST",cache:"no-cache"}).then(e=>{e.result?(a=e.pictures||[],"themes"===t?(r.store.set("backgroundUrl","url()"),r.themes.setFormTheme(r.store.get("wpformsTheme"))):p.picturesModal()):p.errorModal(e.error)}).catch(e=>{console.error(e?.message),p.errorModal(`<p>${o.commonError}</p><p>${e?.message}</p>`)}).finally(()=>{c.isInstalling=!1,e.close()})}catch(e){c.isInstalling=!1,console.error(e),p.errorModal(o.commonError+"<br>"+e)}}},isPicturesAvailable(){return Boolean(a?.length)},getPictureMarkup(){return p.isPicturesAvailable()?(""===u&&(a.forEach(e=>{e=i+e;u+=`<div class="wpforms-builder-stock-photos-picture"
data-url="${e}"
style="background-image: url( '${e}' )"
></div>`}),u=`<div class="wpforms-builder-stock-photos-pictures-wrap">${u}</div>`),u):""},selectPicture(){var e=s(this).data("url");r.store.set("backgroundUrl",e),c.picturesModal?.close()}};return p}
@@ -0,0 +1,908 @@
/* global wpforms_builder_themes */
/**
* WPForms Form Builder Themes: Theme module.
*
* @since 1.9.7
*
* @param {Object} document Document object.
* @param {Object} window Window object.
* @param {jQuery} $ jQuery object.
*
* @return {Object} Public functions and properties.
*/
export default function( document, window, $ ) { // eslint-disable-line max-lines-per-function
const WPForms = window.WPForms || {};
const WPFormsBuilderThemes = WPForms.Admin.Builder.Themes || {};
/**
* Localized data aliases.
*
* @since 1.9.7
*/
const { isAdmin, isPro, isLicenseActive, strings, route_namespace: routeNamespace } = wpforms_builder_themes;
/**
* Runtime state.
*
* @since 1.9.7
*
* @type {Object}
*/
const state = {};
/**
* Themes data.
*
* @since 1.9.7
*
* @type {Object}
*/
const themesData = {
wpforms: null,
custom: null,
};
/**
* Enabled themes.
*
* @since 1.9.7
*
* @type {Object}
*/
let enabledThemes = null;
/**
* Elements holder.
*
* @since 1.9.7
*
* @type {Object}
*/
const el = {};
/**
* Public functions and properties.
*
* @since 1.9.7
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.7
*/
init() {
app.fetchThemesData();
app.setup();
app.events();
},
/**
* Setup.
*
* @since 1.9.7
*/
setup() {
el.$builder = $( '#wpforms-builder' );
el.$themesControl = el.$builder.find( '.wpforms-builder-themes-control' );
el.$customThemeRenamer = el.$builder.find( '#wpforms-panel-field-themes-themeName-wrap' );
el.$customThemeRemover = el.$builder.find( '#wpforms-builder-themer-remove-theme' );
el.$window = $( window );
},
/**
* Setup.
*
* @since 1.9.7
*/
events() {
el.$window.on( 'wpformsBuilderThemesDataLoaded', app.themesControlSetup );
el.$builder.on( 'wpformsSaved', app.saveCustomThemes );
},
/**
* Set up the Themes Select control.
*
* @since 1.9.7
*/
themesControlSetup() {
// Debounce custom themes update and creation.
const debouncedMaybeCreate = _.debounce( ( key ) => {
app.maybeCreateCustomTheme();
app.maybeUpdateCustomTheme( key );
}, 300 );
// Listen for all settings changes.
WPFormsBuilderThemes.store.subscribeAll( ( value, key ) => {
const allowedKeys = WPFormsBuilderThemes.common.getStyleAttributesKeys();
if ( ! allowedKeys.includes( key ) ) {
return;
}
debouncedMaybeCreate( key );
} );
// Listen for the theme name change.
WPFormsBuilderThemes.store.subscribe( 'themeName', ( value ) => {
app.changeThemeName( value );
app.updateThemesList();
} );
// Listen for the isCustomTheme setting change.
WPFormsBuilderThemes.store.subscribe( 'isCustomTheme', () => {
app.toggleCustomThemeSettings();
} );
// Check if the selected theme exists. If no, create a new one.
app.maybeCreateCustomTheme();
app.toggleCustomThemeSettings();
app.updateThemesList();
},
/**
* Update themes list.
*
* @since 1.9.7
*/
updateThemesList() {
const selectedTheme = WPFormsBuilderThemes.store.get( 'wpformsTheme' ) ?? 'default';
// Get all themes.
const html = app.getThemesListMarkup( selectedTheme );
el.$themesControl.html( html );
app.addThemesEvents();
},
/**
* Get the Themes control markup.
*
* @since 1.9.7
*
* @param {string} selectedTheme Selected theme slug.
*
* @return {string} Themes items HTML.
*/
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-builder-themes-radio-group">
${ itemsHtml }
</div>`;
return html;
},
/**
* Get the theme item markup.
*
* @since 1.9.7
*
* @param {Object} theme Theme data.
* @param {string} slug Theme slug.
* @param {string} selectedTheme Selected theme slug.
*
* @return {string} Theme item HTML.
*/
getThemesItemMarkup( theme, slug, selectedTheme ) {
if ( ! theme ) {
return '';
}
const title = theme.name?.length > 0 ? theme.name : strings.theme_noname;
let radioClasses = 'wpforms-builder-themes-radio ';
const buttonClass = slug === selectedTheme ? 'is-active' : '';
radioClasses += app.isDisabledTheme( slug ) ? 'wpforms-builder-themes-radio-disabled' : ' wpforms-builder-themes-radio-enabled';
return `<button type="button" class="${ buttonClass }" value="${ slug }" role="radio">
<div class="wpforms-builder-themes-radio ${ radioClasses }">
<div class="wpforms-builder-themes-radio-title">${ title }</div>
</div>
<div class="wpforms-builder-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>`;
},
/**
* Show or hide the custom theme rename input.
*
* @since 1.9.7
*/
toggleCustomThemeSettings() {
if ( ! isAdmin ) {
return;
}
const value = WPFormsBuilderThemes.store.get( 'isCustomTheme' ) ?? '';
const shouldShow = value === 'true';
el.$customThemeRenamer.toggleClass( 'wpforms-hidden', ! shouldShow );
el.$customThemeRemover.toggleClass( 'wpforms-hidden', ! shouldShow );
},
/**
* On settings change event handler.
*
* @since 1.9.7
*/
addThemesEvents() {
const $radioButtons = el.$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.
el.$customThemeRemover
.off( 'click' )
.on( 'click', app.deleteThemeModal );
},
/**
* Select theme event handler.
*
* @since 1.9.7
*
* @param {string} value New attribute value.
*/
selectTheme( value ) {
if ( ! app.setFormTheme( value ) ) {
return;
}
app.onSelectThemeWithBG( value );
},
/**
* Set the form theme.
*
* @since 1.9.7
*
* @param {string} themeSlug The theme slug.
*
* @return {boolean} True on success.
*/
setFormTheme( themeSlug ) {
if ( app.maybeDisplayUpgradeModal( themeSlug ) ) {
return false;
}
const theme = app.getTheme( themeSlug );
if ( ! theme?.settings ) {
return false;
}
const attributes = Object.keys( theme.settings );
const isCustomTheme = !! themesData.custom[ themeSlug ];
// Set the theme settings.
WPFormsBuilderThemes.store.set( 'wpformsTheme', themeSlug );
WPFormsBuilderThemes.store.set( 'isCustomTheme', isCustomTheme ? 'true' : '' );
WPFormsBuilderThemes.store.set( 'themeName', isCustomTheme ? themesData.custom[ themeSlug ].name : '' );
// Clean up the settings.
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 );
//Reinit color pickers.
WPFormsBuilderThemes.common.loadColorPickers();
return true;
},
/**
* Open stock photos install modal on the select theme.
*
* @since 1.9.7
*
* @param {string} themeSlug The theme slug.
*/
onSelectThemeWithBG( themeSlug ) {
if ( WPFormsBuilderThemes.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()' ) {
WPFormsBuilderThemes.stockPhotos.installModal( 'themes' );
}
},
/**
* Update styles atts.
*
* @since 1.9.7
*
* @param {Object} themeSettings Theme settings.
*/
updateStylesAtts( themeSettings ) {
const allowedKeys = WPFormsBuilderThemes.common.getStyleAttributesKeys();
const validSettings = {};
for ( const key in themeSettings ) {
if ( ! allowedKeys.includes( key ) ) {
continue;
}
let value = themeSettings[ key ];
if ( key === 'backgroundUrl' && typeof value === 'string' ) {
value = app.getBackgroundUrl( value );
}
validSettings[ key ] = value;
}
// Update the settings.
if ( Object.keys( validSettings ).length ) {
Object.entries( validSettings ).forEach( ( [ key, value ] ) => {
WPFormsBuilderThemes.store.set( key, value );
} );
}
},
/**
* Extract the background URL from the string.
*
* @since 1.9.7
*
* @param {string} value Background value.
*
* @return {string} Extracted background image url.
*/
getBackgroundUrl( value ) {
const match = value.match( /^url\(\s*['"]?(.*?)['"]?\s*\)$/i );
return match?.[ 1 ] || 'url()';
},
/**
* Get all themes data.
*
* @since 1.9.7
*
* @return {Object} Themes data.
*/
getAllThemes() {
return { ...( themesData.custom || {} ), ...( themesData.wpforms || {} ) };
},
/**
* Get theme data.
*
* @since 1.9.7
*
* @param {string} slug Theme slug.
*
* @return {Object|null} Theme settings.
*/
getTheme( slug ) {
return app.getAllThemes()[ slug ] || null;
},
/**
* Get enabled themes data.
*
* @since 1.9.7
*
* @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.7
*
* @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.7
*
* @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.7
*
* @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 Rest API.
*
* @since 1.9.7
*/
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 + 'themes/',
method: 'GET',
cache: 'no-cache',
} )
.then( ( response ) => {
themesData.wpforms = response.wpforms || {};
themesData.custom = response.custom || {};
el.$window.trigger( 'wpformsBuilderThemesDataLoaded' );
} )
.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 custom themes.
*
* @since 1.9.7
*/
saveCustomThemes() {
// Custom themes do not exist.
if ( state.isSavingThemes || ! themesData.custom || ! isAdmin ) {
return;
}
// Set the flag to true indicating a saving is in progress.
state.isSavingThemes = true;
try {
// Save themes.
wp.apiFetch( {
path: routeNamespace + '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.7
*
* @param {Object} settings Settings.
*
* @return {Array} Current style attributes.
*/
getCurrentStyleAttributes( settings ) {
const defaultAttributes = Object.keys( themesData.wpforms.default?.settings );
const currentStyleAttributes = {};
for ( const key in defaultAttributes ) {
const attr = defaultAttributes[ key ];
currentStyleAttributes[ attr ] = WPFormsBuilderThemes.common.prepareComplexAttrValues( settings[ attr ], attr ) ?? '';
}
return currentStyleAttributes;
},
/**
* Maybe create a custom theme.
*
* @since 1.9.7
*
*
* @return {boolean} Whether the custom theme is created.
*/
maybeCreateCustomTheme() {
const settings = WPFormsBuilderThemes.getSettings();
const currentStyles = app.getCurrentStyleAttributes( settings );
const isWPFormsTheme = !! themesData.wpforms[ settings.wpformsTheme ];
const isCustomTheme = !! themesData.custom[ settings.wpformsTheme ];
// It is one of the default themes without any changes.
if (
isWPFormsTheme &&
app.getPreparedDefaultThemeSettings( themesData.wpforms[ settings.wpformsTheme ]?.settings ) === JSON.stringify( currentStyles )
) {
return false;
}
// It is a modified default theme OR unknown custom theme.
if ( isWPFormsTheme || ! isCustomTheme ) {
app.createCustomTheme( settings, currentStyles );
}
return true;
},
/**
* Prepare default theme settings for comparing.
*
* @since 1.9.7
*
* @param {Object} settings Theme properties.
*
* @return {string} Whether the custom theme is created.
*/
getPreparedDefaultThemeSettings( settings ) {
const preparedSettings = {};
Object.keys( settings ).forEach( ( key ) => {
preparedSettings[ key ] = WPFormsBuilderThemes.common.removeRgbaSpaces( settings[ key ] );
} );
return JSON.stringify( preparedSettings );
},
/**
* Create a custom theme.
*
* @since 1.9.7
*
* @param {Object} settings Style settings.
* @param {Object} currentStyles Current style settings.
*
* @return {boolean} Whether the custom theme is created.
*/
createCustomTheme( settings, currentStyles = null ) {
let counter = 0;
let themeSlug = settings.wpformsTheme;
const baseTheme = app.getTheme( settings.wpformsTheme ) || themesData.wpforms.default;
let themeName = baseTheme.name;
themesData.custom = themesData.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 + ')';
// Add the new custom theme.
themesData.custom[ themeSlug ] = {
name: themeName,
settings: currentStyles || app.getCurrentStyleAttributes( settings ),
};
app.updateEnabledThemes( themeSlug, themesData.custom[ themeSlug ] );
// Update the settings with the new custom theme settings.
WPFormsBuilderThemes.store.set( 'wpformsTheme', themeSlug );
WPFormsBuilderThemes.store.set( 'isCustomTheme', 'true' );
WPFormsBuilderThemes.store.set( 'themeName', themeName );
app.updateThemesList();
return true;
},
/**
* Update custom theme.
*
* @since 1.9.7
*
* @param {string} attribute Attribute name.
* @param {string} value New attribute value.
*/
updateCustomThemeAttribute( attribute, value ) {
const settings = WPFormsBuilderThemes.getSettings();
const themeSlug = settings.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;
app.maybeUpdateColorIndicator( attribute, value );
}
},
/**
* Maybe update the custom theme settings.
*
* @param {string} key Setting key.
*
* @since 1.9.7
*/
maybeUpdateCustomTheme( key ) {
const settings = WPFormsBuilderThemes.getSettings();
const isCustomTheme = settings.isCustomTheme === 'true';
if ( ! isCustomTheme ) {
return;
}
const attrValue = WPFormsBuilderThemes.common.prepareComplexAttrValues( settings[ key ], key );
app.updateCustomThemeAttribute( key, attrValue );
},
/**
* Maybe update the color indicators for the custom theme.
*
* @param {string} settingKey Setting key.
* @param {string} settingValue Setting value.
*
* @since 1.9.7
*/
maybeUpdateColorIndicator( settingKey, settingValue ) {
const colorSettingKeys = [ 'buttonBackgroundColor', 'buttonTextColor', 'labelColor', 'labelSublabelColor', 'fieldBorderColor' ];
if ( ! colorSettingKeys.includes( settingKey ) ) {
return;
}
const $indicators = el.$themesControl.find( 'button.is-active .wpforms-builder-themes-indicators' );
const indicatorIndex = colorSettingKeys.indexOf( settingKey );
const $indicator = $indicators.find( `.component-color-indicator[data-index="${ indicatorIndex }"]` );
if ( $indicator.length ) {
$indicator.css( 'background-color', settingValue );
}
},
/**
* Maybe display upgrades modal in Lite.
*
* @since 1.9.7
*
* @param {string} themeSlug The theme slug.
*
* @return {boolean} True if modal was displayed.
*/
maybeDisplayUpgradeModal( themeSlug ) {
if ( ! app.isDisabledTheme( themeSlug ) ) {
return false;
}
if ( ! isPro ) {
WPFormsBuilderThemes.common.showProModal( 'themes', strings.pro_sections.themes );
return true;
}
if ( ! isLicenseActive ) {
WPFormsBuilderThemes.common.showLicenseModal( 'themes', strings.pro_sections.themes, 'select-theme' );
return true;
}
return false;
},
/**
* Change theme name event handler.
*
* @since 1.9.7
*
* @param {string} value New attribute value.
*/
changeThemeName( value ) {
app.updateCustomThemeAttribute( 'themeName', value );
},
/**
* Delete theme event handler.
*
* @param {string} deleteThemeSlug Theme slug.
*
* @since 1.9.7
*/
deleteTheme( deleteThemeSlug ) {
// Remove theme from the theme storage.
delete themesData.custom[ deleteThemeSlug ];
},
/**
* Open the theme delete the confirmation modal window.
*
* @since 1.9.7
*
* @param {Object} e Event object.
*/
deleteThemeModal( e ) {
e.preventDefault();
const settings = WPFormsBuilderThemes.getSettings();
const selectedThemeSlug = settings.wpformsTheme;
const selectedThemeName = app.getTheme( selectedThemeSlug )?.name;
const confirm = strings.theme_delete_confirm.replace( '%1$s', `<b>${ _.escape( selectedThemeName ) }</b>` );
const content = `<p class="wpforms-theme-delete-text">${ confirm } ${ strings.theme_delete_cant_undone }</p>`;
$.confirm( {
title: strings.theme_delete_title,
content,
icon: 'wpforms-exclamation-circle',
type: 'red wpforms-builder-themes-modal',
buttons: {
confirm: {
text: strings.theme_delete_yes,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
// Delete the theme and switch to the default theme.
app.deleteTheme( selectedThemeSlug );
app.selectTheme( 'default' );
},
},
cancel: {
text: strings.cancel,
keys: [ 'esc' ],
},
},
} );
},
};
return app;
}
File diff suppressed because one or more lines are too long