HEX
Server: LiteSpeed
System: Linux server315.web-hosting.com 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: globfdxw (6114)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/globfdxw/www/wp-content/plugins/wpforms-pdf/assets/js/modules/custom.js
/* global WPForms, wpformsPDF, wpforms_builder, WPFormsPDFBuilder, wpf */

/**
 * @function getValue
 * @function setChoices
 * @param    choices._store
 * @param    wpformsPDF.customTemplates
 * @param    templateData.appearance
 * @param    themeData.colors
 * @param    wpforms_builder.pdf.theme.delete_title
 * @param    wpforms_builder.pdf.theme.delete_confirm
 * @param    wpforms_builder.pdf.theme.delete_cant_undone
 * @param    wpforms_builder.pdf.theme.delete_yes
 * @param    wpforms_builder.pdf.template.delete_title
 * @param    wpforms_builder.pdf_template.delete_confirm
 */

/**
 * WPForms PDF: Custom templates/themes module.
 *
 * @since 1.0.0
 *
 * @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 no-unused-vars, max-lines-per-function
	/**
	 * Elements holder.
	 *
	 * @since 1.0.0
	 *
	 * @type {Object}
	 */
	const el = {};

	/**
	 * Public functions and properties.
	 *
	 * @since 1.0.0
	 */
	const app = {

		/**
		 * Altered theme data.
		 *
		 * @since 1.0.0
		 *
		 * @type {Object|null}
		 */
		alteredThemeData: null,

		/**
		 * Initialize module.
		 *
		 * @since 1.0.0
		 */
		init() {
			app.setup();
			app.initAllCustomEntitiesSettings();
			app.bindEvents();
		},

		/**
		 * Setup. Prepare some variables.
		 *
		 * @since 1.0.0
		 */
		setup() {
			// Cache DOM elements.
			el.$builder = $( '#wpforms-builder' );

			// Create a debounced function to update entity data.
			app.updateEntityDataAjaxDebounced = _.debounce( app.updateEntityDataAjax, 200 );
		},

		/**
		 * Bind events.
		 *
		 * @since 1.0.0
		 */
		bindEvents() {
			el.$builder
				.on( 'click', '.wpforms-pdf-custom-entity-rename', app.clickRename )
				.on( 'click', '.wpforms-pdf-custom-entity-delete', app.clickDelete )
				.on( 'click', '.wpforms-pdf-custom-entity-name-save', app.clickRenameSaveButton )
				.on( 'click', '.wpforms-pdf-custom-entity-name-cancel', app.clickRenameCancelButton )
				.on( 'wpformsSaved', app.wpformsSaved )
				.on( 'wpformsPDFReflectorElementUpdate', app.reflectorElementUpdate )
				.on( 'wpformsSettingsBlockAdded wpformsSettingsBlockCloned', app.pdfAdded )
				.on( 'input', '.wpforms-field-pdf-custom-entity-name-field input', app.entityNameChanged )
				.on( 'change', '.wpforms-pdf-template-category select', app.toggleCustomEntityVisibility )
				.on( 'change', '.wpforms-pdf-template-style select', app.toggleCustomEntityVisibility )
				.on( 'change', '.wpforms-pdf-theme-selector select', app.toggleCustomEntityVisibility );
		},

		/**
		 * The PDF block was added or cloned.
		 *
		 * @since 1.0.0
		 *
		 * @param {Event}  e      The event object.
		 * @param {jQuery} $block The block element.
		 */
		pdfAdded( e, $block ) {
			if ( $block.data( 'block-type' ) !== 'pdf' ) {
				return;
			}

			app.initAllCustomEntitiesSettings( $block );
		},

		/**
		 * Initialize all custom entities' settings.
		 *
		 * @since 1.0.0
		 *
		 * @param {jQuery} $block The block element.
		 */
		initAllCustomEntitiesSettings( $block ) {
			$block = $block?.length ? $block : el.$builder;

			$block.find( '.wpforms-pdf-custom-entity-settings' ).each( function() {
				app.initCustomEntitySettings( $( this ) );
			} );
		},

		/**
		 * Initialize custom entity settings.
		 *
		 * @since 1.0.0
		 *
		 * @param {jQuery} $settings Settings element object.
		 */
		initCustomEntitySettings( $settings ) {
			if ( ! $settings?.length ) {
				return;
			}

			const entity = $settings.data( 'entity' );
			const value = $settings.prev( '.wpforms-panel-field-select' ).find( 'select' ).val();
			const data = entity === 'theme' ? app.getThemeData( value ) : app.getTemplateData( value );
			const isCustom = data?.isCustom ?? false;
			const $field = $settings.find( '.wpforms-field-pdf-custom-entity-name-field' );

			$settings.toggleClass( 'wpforms-hidden', ! isCustom );
			$field.addClass( 'wpforms-hidden' );
		},

		/**
		 * Get the current template for the given pdfId.
		 *
		 * @since 1.0.0
		 *
		 * @param {string} pdfId Template slug.
		 *
		 * @return {string} Template slug.
		 */
		getCurrentTemplateSlug( pdfId ) {
			return $( `#wpforms-panel-field-pdfs-${ pdfId }-template_style` ).val();
		},

		/**
		 * Get the current theme for the given pdfId.
		 *
		 * @since 1.0.0
		 *
		 * @param {string} pdfId Template slug.
		 *
		 * @return {string} Theme slug.
		 */
		getCurrentThemeSlug( pdfId ) {
			return $( `#wpforms-panel-field-pdfs-${ pdfId }-theme` ).val();
		},

		/**
		 * Get the template data.
		 *
		 * @since 1.0.0
		 *
		 * @param {string} slug Template slug.
		 *
		 * @return {Object|null} Template data.
		 */
		getTemplateData( slug ) {
			return WPFormsPDFBuilder.templates.getTemplateData( slug );
		},

		/**
		 * Get the theme data.
		 *
		 * @since 1.0.0
		 *
		 * @param {string} slug Theme slug.
		 *
		 * @return {Object|null} Theme data.
		 */
		getThemeData( slug ) {
			return WPFormsPDFBuilder.appearance.getThemeData( slug );
		},

		/**
		 * Toggle custom entity settings visibility.
		 *
		 * @since 1.0.0
		 *
		 * @param {Event}  e       Event object.
		 * @param {jQuery} $select The select element, optional.
		 */
		toggleCustomEntityVisibility( e, $select ) {
			$select = $select ?? $( this );

			const $settings = $select.closest( '.wpforms-panel-field' ).nextAll( '.wpforms-pdf-custom-entity-settings' );
			const entity = $settings.data( 'entity' );
			const value = $select.val();
			const data = entity === 'theme' ? app.getThemeData( value ) : app.getTemplateData( value );
			const isCustom = data?.isCustom ?? false;

			$settings.toggleClass( 'wpforms-hidden', ! isCustom );
		},

		/**
		 * Handle entity name change.
		 *
		 * @since 1.0.0
		 */
		entityNameChanged() {
			const $input = $( this );
			const $saveBtn = $input.closest( '.wpforms-field-pdf-custom-entity-name-field' ).find( '.wpforms-pdf-custom-entity-name-save' );

			$saveBtn.toggleClass( 'wpforms-disabled', ! $input.val() );
		},

		/**
		 * Click rename an action link.
		 *
		 * @since 1.0.0
		 *
		 * @param {Event} e Event object.
		 */
		clickRename( e ) {
			const $button = $( this );
			const $settings = $button.closest( '.wpforms-pdf-custom-entity-settings' );
			const $select = $settings.prevAll( '.wpforms-pdf-template-style, .wpforms-pdf-theme-selector' ).find( 'select' );
			const $field = $settings.find( '.wpforms-field-pdf-custom-entity-name-field' );
			const currentName = $select.find( 'option:selected' ).text();

			$field
				.removeClass( 'wpforms-hidden' )
				.find( 'input' )
				.val( currentName );

			e.preventDefault();
		},

		/**
		 * Click delete action link.
		 *
		 * @since 1.0.0
		 *
		 * @param {Event} e Event object.
		 */
		clickDelete( e ) {
			const $this = $( this );
			const $settings = $this.closest( '.wpforms-pdf-custom-entity-settings' );
			const $select = $settings.prevAll( '.wpforms-pdf-template-style, .wpforms-pdf-theme-selector' ).find( 'select' );
			const entity = $settings.data( 'entity' );

			app.openEntityDeleteModal( $select, entity );

			e.preventDefault();
		},

		/**
		 * Click the rename save button.
		 *
		 * @since 1.0.0
		 *
		 * @param {Event} e Event object.
		 */
		clickRenameSaveButton( e ) {
			const $button = $( this );
			const $settings = $button.closest( '.wpforms-pdf-custom-entity-settings' );
			const $field = $button.closest( '.wpforms-field-pdf-custom-entity-name-field' );
			const $input = $field.find( 'input' );
			const $select = $settings.prevAll( '.wpforms-pdf-template-style, .wpforms-pdf-theme-selector' ).find( 'select' );
			const entity = $settings.data( 'entity' );
			const entityName = $input.val();
			const entitySlug = $select.val();
			let entityData = {};

			$select
				.find( 'option:selected' )
				.text( entityName );

			// Update ChoicesJS option.
			if ( entity === 'theme' ) {
				app.updateThemeSelectorItem( entitySlug, entityName );
				wpformsPDF.customThemes[ entitySlug ].title = entityName;
				entityData = app.getThemeData( entitySlug );
			}

			if ( entity === 'template' ) {
				wpformsPDF.customTemplates[ entitySlug ].title = entityName;
				entityData = app.getTemplateData( entitySlug );
			}

			app.updateEntityData( entityData, entity );

			$field.addClass( 'wpforms-hidden' );
			e.preventDefault();
		},

		/**
		 * Click the rename cancel button.
		 *
		 * @since 1.0.0
		 *
		 * @param {Event} e Event object.
		 */
		clickRenameCancelButton( e ) {
			const $button = $( this );
			const $field = $button.closest( '.wpforms-field-pdf-custom-entity-name-field' );

			$field.addClass( 'wpforms-hidden' );
			e.preventDefault();
		},

		/**
		 * The `wpformsPDFReflectorElementUpdate` event handler.
		 *
		 * @since 1.0.0
		 *
		 * @param {Event}  e                Event object.
		 * @param {Object} reflectorElement Reflector element data object.
		 */
		reflectorElementUpdate( e, reflectorElement ) {
			if ( reflectorElement.context === 'triggered' ) {
				return;
			}

			const templateSlug = app.getCurrentTemplateSlug( reflectorElement.pdfId );
			const templateData = app.getTemplateData( templateSlug );
			const themeData = app.getThemeData( reflectorElement.themeSlug );

			if ( ! templateData || ! themeData ) {
				return;
			}

			if ( app.detectThemeAlternate( reflectorElement, themeData, templateData ) ) {
				app.processThemeAlternate( templateData, reflectorElement.themeSlug, themeData, reflectorElement.themeColorKey, reflectorElement );
				return;
			}

			if ( app.detectTemplateAlternate( reflectorElement, templateData ) ) {
				app.processTemplateAlternate( templateData, reflectorElement );
			}
		},

		/**
		 * Detect theme modification.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} reflectorElement Reflector element data object.
		 * @param {Object} themeData        Theme data object.
		 * @param {Object} templateData     Template data object.
		 *
		 * @return {boolean} `true` if theme modification detected, `false` otherwise.
		 */
		detectThemeAlternate( reflectorElement, themeData, templateData ) { // eslint-disable-line complexity
			const isThemeColorChange = reflectorElement.isThemeColorChange &&
				themeData.colors?.[ reflectorElement.themeColorKey ] !== reflectorElement.value;

			if ( isThemeColorChange ) {
				return true;
			}

			const appearance = {
				...templateData.appearance,
				...( themeData.appearance ?? {} ),
			};

			const isAppearanceChange = appearance[ reflectorElement.setting ] !== undefined &&
				appearance[ reflectorElement.setting ] !== reflectorElement.value;

			if ( isAppearanceChange ) {
				return true;
			}

			return templateData.text[ reflectorElement.setting ] &&
				reflectorElement.setting.includes( '_color' ) &&
				templateData.text[ reflectorElement.setting ] !== reflectorElement.value;
		},

		/**
		 * Detect template modification.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} reflectorElement Reflector element data object.
		 * @param {Object} templateData     Template data object.
		 *
		 * @return {boolean} `true` if template modification detected, `false` otherwise.
		 */
		detectTemplateAlternate( reflectorElement, templateData ) {
			const rawTemplateValue = templateData.text?.[ reflectorElement.setting ];
			const templateValue = app.decodeHTMLEntities( rawTemplateValue ?? '' );

			return typeof rawTemplateValue !== 'undefined' && // It is a text template setting
				! reflectorElement.setting.includes( '_color' ) && // It is not a color setting
				templateValue !== reflectorElement.value; // The value is changed.
		},

		/**
		 * Decode HTML entities.
		 *
		 * @since 1.0.0
		 *
		 * @param {string} html Encoded HTML string.
		 *
		 * @return {string} Decoded HTML string.
		 */
		decodeHTMLEntities( html ) {
			const txt = document.createElement( 'textarea' );

			txt.innerHTML = html;

			return txt.value;
		},

		/**
		 * Create a new unique slug.
		 *
		 * @since 1.0.0
		 *
		 * @param {string} baseSlug Base slug to create a new unique slug from.
		 * @param {string} entity   Entity type, `theme` or `template`.
		 *
		 * @return {string} New unique slug.
		 */
		getNewSlug( baseSlug, entity ) {
			// Start with the original slug.
			let newSlug = baseSlug;
			const data = entity === 'theme' ? wpformsPDF.customThemes : wpformsPDF.customTemplates;

			do {
				// Generate a hash based on the current timestamp.
				const hash = Math.floor( Date.now() / 1000 );

				newSlug = `${ baseSlug }-copy-${ hash }`;
			} while ( data?.[ newSlug ] );

			return newSlug;
		},

		/**
		 * Get the copy number.
		 *
		 * @since 1.0.0
		 *
		 * @param {string} baseSlug Base slug to create a new unique slug from.
		 * @param {string} entity   Entity type, `theme` or `template`.
		 *
		 * @return {number} Number of theme copies.
		 */
		getCopyNumber( baseSlug, entity ) {
			const data = Object.values( entity === 'theme' ? wpformsPDF.customThemes : wpformsPDF.customTemplates );
			const key = entity === 'theme' ? 'baseTheme' : 'baseTemplate';

			// Count all themes where baseTheme matches the provided parameter.
			if ( ! data.length ) {
				return 0;
			}

			const count = data.filter( ( entityData ) => {
				return entityData[ key ] === baseSlug;
			} )?.length;

			return count + 1;
		},

		/**
		 * Get the entity copy title.
		 *
		 * @since 1.0.0
		 *
		 * @param {number} copyNumber Copy number.
		 * @param {Object} data       Entity data.
		 *
		 * @return {string} New copy title.
		 */
		getCopyTitle( copyNumber, data ) {
			return copyNumber > 1
				? `${ data.title } (Copy ${ copyNumber })`
				: `${ data.title } (Copy)`;
		},

		/**
		 * AJAX call to update/delete the entity data.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} entityData Theme data object.
		 * @param {string} entity     Entity type, `theme` or `template`.
		 */
		updateEntityData( entityData, entity ) {
			if ( ! entityData || ! entity ) {
				return;
			}

			app.updateEntityDataAjaxDebounced( entityData, entity );
		},

		/**
		 * Queue update theme data.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} themeData Theme data object.
		 */
		queueUpdateThemeData( themeData ) {
			app.alteredThemeData = themeData;
		},

		/**
		 * The `wpformsSaved` event handler.
		 *
		 * @since 1.0.0
		 */
		wpformsSaved() {
			if ( app.alteredThemeData ) {
				app.updateEntityDataAjax( app.alteredThemeData, 'theme' );
				app.alteredThemeData = null;
			}
		},

		/**
		 * AJAX call to update/delete the entity data.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} entityData Theme data object.
		 * @param {string} entity     Entity type, `theme` or `template`.
		 */
		updateEntityDataAjax( entityData, entity ) {
			if ( ! entityData || ! entityData.slug ) {
				return;
			}

			entity = entity === 'theme' ? entity : 'template';

			// Prepare data for AJAX request.
			const data = {
				action: `wpforms_pdf_update_${ entity }_data`,
				nonce: wpforms_builder.nonce,
				data: entityData,
			};

			const errorMessage = entity.charAt( 0 ).toUpperCase() + entity.slice( 1 ) + ' update error';

			// Make the AJAX call.
			$.post( wpforms_builder.ajax_url, data )
				.done( function( response ) {
					if ( ! response.success ) {
						wpf.debug( errorMessage, response );
					}
				} )
				.fail( function( xhr, textStatus ) {
					wpf.debug( errorMessage, xhr.responseText || textStatus || '' );
				} );
		},

		/**
		 * Open the delete theme confirmation modal.
		 *
		 * @since 1.0.0
		 *
		 * @param {jQuery} $select The select element.
		 * @param {string} entity  Entity name.
		 */
		openEntityDeleteModal( $select, entity ) {
			const strings = wpforms_builder.pdf[ entity ];
			const name = $select?.find( 'option:selected' ).text();
			const confirm = strings.delete_confirm.replace( '%1$s', `<b>${ name }</b>` );
			const content = `<p class="wpforms-entity-delete-text">${ confirm } ${ wpforms_builder.pdf.theme.delete_cant_undone }</p>`;

			$.confirm( {
				title: strings.delete_title,
				content,
				icon: 'fa fa-exclamation-circle wpforms-exclamation-circle',
				type: 'red',
				buttons: {
					confirm: {
						text:  wpforms_builder.pdf.theme.delete_yes,
						btnClass: 'btn-confirm',
						keys: [ 'enter' ],
						action() {
							if ( entity === 'theme' ) {
								app.deleteTheme( $select );
							}

							if ( entity === 'template' ) {
								app.deleteTemplate( $select );
							}

							// Hide Rename | Delete buttons.
							app.toggleCustomEntityVisibility( {}, $select );
						},
					},
					cancel: {
						text: wpforms_builder.cancel,
						keys: [ 'esc' ],
					},
				},
			} );
		},

		/**
		 * -------- Theme processing functions. --------
		 */

		/**
		 * Process theme change.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} templateData     Theme data object.
		 * @param {string} themeSlug        Theme slug.
		 * @param {Object} themeData        Theme data object.
		 * @param {string} themeColorKey    Theme color key.
		 * @param {Object} reflectorElement Reflector element data object.
		 */
		processThemeAlternate( templateData, themeSlug, themeData, themeColorKey, reflectorElement ) {
			if ( ! themeSlug || ! themeData || ! reflectorElement ) {
				return;
			}

			// When the theme is not custom, we should first create the custom theme.
			if ( ! themeData.isCustom ) {
				themeData = app.createNewTheme( templateData, themeSlug, themeData, themeColorKey, reflectorElement );
			} else {
				themeData = app.updateThemeData( templateData, themeSlug, themeColorKey, reflectorElement );
			}

			// Update theme data.
			app.queueUpdateThemeData( themeData );

			// When the theme color is not changed, we should do nothing.
			if ( ! reflectorElement.isThemeColorChange ) {
				return;
			}

			// Update color swatch in the theme selectors.
			app.updateThemeSelectorColorSwatch( themeData, themeColorKey, reflectorElement.value );

			// Apply theme changes.
			WPFormsPDFBuilder.appearance.applyThemeChanges( $( `#wpforms-panel-field-pdfs-${ reflectorElement.pdfId }-theme` ) );
		},

		/**
		 * Create a new custom theme.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} templateData     Theme data object.
		 * @param {string} themeSlug        Theme slug.
		 * @param {Object} themeData        Theme data object.
		 * @param {string} themeColorKey    Theme color key.
		 * @param {Object} reflectorElement Reflector element data object.
		 *
		 * @return {Object} New theme data.
		 */
		createNewTheme( templateData, themeSlug, themeData, themeColorKey, reflectorElement ) {
			const newSlug = app.getNewSlug( themeSlug, 'theme' );
			const copyNumber = app.getCopyNumber( themeSlug, 'theme' );
			const newTitle = app.getCopyTitle( copyNumber, themeData );

			// Create new theme data.
			const newThemeData = {
				colors: { ...themeData.colors },
				title: newTitle,
				appearance: { ...themeData.appearance ?? {} },
				text: {},
				isCustom: true,
				slug: newSlug,
				baseTheme: themeSlug,
			};

			// The default theme in Notifications should have appearance colors inherited from the global Email settings.
			if ( templateData.category === 'notification' && themeSlug === wpformsPDF.defaultTheme ) {
				newThemeData.appearance = { ...themeData.emailAppearance, ...newThemeData.appearance };
			}

			wpformsPDF.customThemes[ newSlug ] = newThemeData;

			app.updateThemeData( templateData, newSlug, themeColorKey, reflectorElement );
			app.addThemeSelectorItem( newThemeData, reflectorElement );

			// Trigger theme change.
			$( `#wpforms-panel-field-pdfs-${ reflectorElement.pdfId }-theme` ).trigger( 'change' );

			return newThemeData;
		},

		/**
		 * Update the theme data.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} templateData     Theme data object.
		 * @param {string} themeSlug        Theme slug.
		 * @param {string} themeColorKey    Theme color key.
		 * @param {Object} reflectorElement Reflector element data object.
		 * @param {string} newTitle         New title, optional.
		 *
		 * @return {Object|null} Theme data.
		 */
		updateThemeData( templateData, themeSlug, themeColorKey, reflectorElement, newTitle = '' ) { // eslint-disable-line complexity
			const themeData = app.getThemeData( themeSlug );

			// When the theme is not custom, we should do nothing.
			if ( ! themeData || ! themeData.isCustom ) {
				return null;
			}

			const value = reflectorElement.value ?? '';

			// Store color setting from the template text OR appearance sections.
			if ( templateData.text[ reflectorElement.setting ] ) {
				themeData.text = themeData.text ?? {};
				themeData.text[ reflectorElement.setting ] = value;
			} else {
				themeData.appearance = themeData.appearance ?? {};
				themeData.appearance[ reflectorElement.setting ] = value;
			}

			// Store color setting from the theme colors editor.
			if ( reflectorElement.isThemeColorChange ) {
				themeData.colors[ themeColorKey ] = value;
			}

			// Update title.
			if ( newTitle ) {
				themeData.title = newTitle;
			}

			wpformsPDF.customThemes[ themeSlug ] = themeData;

			return themeData;
		},

		/**
		 * Update the theme selector item.
		 *
		 * @since 1.0.0
		 *
		 * @param {string} themeSlug Theme slug.
		 * @param {string} newName   New theme name.
		 */
		updateThemeSelectorItem( themeSlug, newName ) {
			$( '.wpforms-pdf-theme-selector select' ).each( function() {
				const $select = $( this );
				const choices = $select?.data( 'choicesjs' );

				if ( ! choices ) {
					return;
				}

				const updatedChoices = choices._store.choices.map( ( choice ) => {
					if ( choice.value === themeSlug ) {
						return { ...choice, label: newName };
					}

					return choice;
				} );

				app.updateThemeSelectorChoices( choices, updatedChoices );
			} );
		},

		/**
		 * Add a new theme to the theme selector.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} themeData        Theme data.
		 * @param {Object} reflectorElement Reflector element data object.
		 */
		addThemeSelectorItem( themeData, reflectorElement ) {
			$( '.wpforms-pdf-theme-selector select' ).each( function() {
				const $select = $( this );
				const choices = $select?.data( 'choicesjs' );

				if ( ! choices ) {
					return;
				}

				const updatedChoices = choices._store.choices;

				updatedChoices.unshift( {
					value: themeData.slug,
					label: themeData.title,
				} );

				// The current PDF theme selector should be set to the new theme.
				const selectorPdfId = $select.closest( '.wpforms-pdf' ).data( 'block-id' );
				const setValue = selectorPdfId === reflectorElement.pdfId ? themeData.slug : $select.val();

				app.updateThemeSelectorChoices( choices, updatedChoices, setValue );
			} );
		},

		/**
		 * Update theme selector choices.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object}      choicesObj ChoicesJS instance.
		 * @param {Array}       choices    Choices.
		 * @param {string|null} setTheme   Set theme after update, optional.
		 */
		updateThemeSelectorChoices( choicesObj, choices, setTheme = null ) {
			if ( ! choicesObj?.setChoices ) {
				return;
			}

			setTheme = setTheme ?? choicesObj.getValue( true );

			WPForms.Admin.Builder?.UndoRedo?.preventRecord( true );

			// noinspection JSVoidFunctionReturnValueUsed
			choicesObj
				.clearStore()
				.setChoices( choices, 'value', 'label', true )
				.setChoiceByValue( setTheme );

			WPForms.Admin.Builder?.UndoRedo?.preventRecord( 'continue' );
		},

		/**
		 * Update theme selector color swatch.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} themeData Theme data.
		 * @param {string} colorKey  Color key.
		 * @param {string} color     Color value.
		 */
		updateThemeSelectorColorSwatch( themeData, colorKey, color ) {
			if ( ! colorKey ) {
				return;
			}

			const keys = Object.keys( themeData.colors );
			const index = keys.indexOf( colorKey );

			// If the color key is not found, OR index is out of range, do nothing.
			if ( index < 0 || index > 4 ) {
				return;
			}

			const $choiceSwatches = $( `.choices__item[data-value="${ themeData.slug }"] .wpforms-theme-color-swatches` );

			$choiceSwatches.each( function() {
				const $swatches = $( this );

				$swatches
					.find( `.wpforms-theme-color-swatch[data-index="${ index }"]` )
					.css( 'background-color', color );
			} );
		},

		/**
		 * Delete theme.
		 *
		 * @since 1.0.0
		 *
		 * @param {jQuery} $select The select element.
		 */
		deleteTheme( $select ) {
			const themeSlug = $select.val();

			// Delete the theme from the runtime object.
			delete wpformsPDF.customThemes[ themeSlug ];

			// Delete the theme from the server.
			app.updateEntityData( { slug: themeSlug, delete: true }, 'theme' );

			const choices = $select?.data( 'choicesjs' );

			if ( ! choices ) {
				return;
			}

			const updatedChoices = choices._store.choices;

			// Delete theme from the choices.
			for ( const choice of updatedChoices ) {
				if ( choice.value === themeSlug ) {
					delete updatedChoices[ updatedChoices.indexOf( choice ) ];
				}
			}

			// Update theme selectors in all PDFs.
			$( '.wpforms-pdf-theme-selector select' ).each( function() {
				const eachChoices = $( this ).data( 'choicesjs' );
				app.updateThemeSelectorChoices( eachChoices, updatedChoices, wpformsPDF.defaultTheme );
			} );

			// Apply theme changes.
			WPFormsPDFBuilder.appearance.applyThemeChanges( $select );
		},

		/**
		 * -------- Template processing functions. --------
		 */

		/**
		 * Process template change.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} templateData     Theme data object.
		 * @param {Object} reflectorElement Reflector element data object.
		 */
		processTemplateAlternate( templateData, reflectorElement ) {
			if ( ! templateData || ! reflectorElement ) {
				return;
			}

			// When the template is not custom, we should first create the custom template.
			if ( ! templateData.isCustom ) {
				templateData = app.createNewTemplate( templateData, reflectorElement );
			} else {
				templateData = app.updateTemplateData( templateData.slug, reflectorElement );
			}

			app.updateEntityData( templateData, 'template' );
		},

		/**
		 * Create a new template.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} templateData     Theme data object.
		 * @param {Object} reflectorElement Reflector element data object.
		 *
		 * @return {Object|null} New theme data.
		 */
		createNewTemplate( templateData, reflectorElement ) {
			const templateSlug = templateData.slug ?? '';

			if ( ! templateSlug ) {
				return null;
			}

			const newSlug = app.getNewSlug( templateSlug, 'template' );
			const copyNumber = app.getCopyNumber( templateSlug, 'template' );
			const newTitle = app.getCopyTitle( copyNumber, templateData );

			// Create new template data.
			const newTemplateData = {
				category: templateData.category ?? '',
				title: newTitle,
				appearance: { ...templateData.appearance },
				text: { ...templateData.text },
				isCustom: true,
				slug: newSlug,
				baseTemplate: templateSlug,
			};

			// Preserve email_style from the base template (e.g., 'compact' for table layout).
			if ( templateData.email_style ) {
				// eslint-disable-next-line camelcase
				newTemplateData.email_style = templateData.email_style;
			}

			wpformsPDF.customTemplates[ newSlug ] = newTemplateData;

			app.updateTemplateData( newSlug, reflectorElement );
			app.addTemplateSelectorItem( newTemplateData, reflectorElement );

			app.toggleCustomEntityVisibility( {}, $( `#wpforms-panel-field-pdfs-${ reflectorElement.pdfId }-template_style` ) );

			return newTemplateData;
		},

		/**
		 * Update the template data.
		 *
		 * @since 1.0.0
		 *
		 * @param {string} templateSlug     Theme slug.
		 * @param {Object} reflectorElement Reflector element data object.
		 * @param {string} newTitle         New title, optional.
		 *
		 * @return {Object|null} Theme data.
		 */
		updateTemplateData( templateSlug, reflectorElement, newTitle = '' ) { // eslint-disable-line complexity
			const templateData = app.getTemplateData( templateSlug );

			// When the template is not custom, we should do nothing.
			if ( ! templateData || ! templateData.isCustom ) {
				return null;
			}

			templateData.text[ reflectorElement.setting ] = reflectorElement.value ?? '';

			if ( newTitle ) {
				templateData.title = newTitle;
			}

			wpformsPDF.customTemplates[ templateSlug ] = templateData;

			return templateData;
		},

		/**
		 * Add a new template to the Template selector.
		 *
		 * @since 1.0.0
		 *
		 * @param {Object} templateData     Template data.
		 * @param {Object} reflectorElement Reflector element data object.
		 */
		addTemplateSelectorItem( templateData, reflectorElement ) {
			// language=HTML
			const $option = $( '<option></option>' )
				.attr( 'value', templateData.slug )
				.text( templateData.title );

			$( '.wpforms-pdf-template-style select' ).each( function() {
				const $select = $( this );
				const $categorySelect = $select
					.closest( '.wpforms-panel-fields-group-inner' )
					.find( '.wpforms-pdf-template-category select' );

				if ( $categorySelect.val() !== templateData.category ) {
					return;
				}

				$select.prepend( $option.clone() );

				// The current PDF template selector should be set to the new template.
				const selectorPdfId = $select.closest( '.wpforms-pdf' ).data( 'block-id' );
				const setValue = selectorPdfId === reflectorElement.pdfId ? templateData.slug : $select.val();

				$select.val( setValue );
			} );
		},

		/**
		 * Delete theme.
		 *
		 * @since 1.0.0
		 *
		 * @param {jQuery} $select The select element.
		 */
		deleteTemplate( $select ) {
			const templateSlug = $select.val();
			const baseTemplate = app.getTemplateData( templateSlug )?.baseTemplate;

			// Delete the template from the runtime object.
			delete wpformsPDF.customTemplates[ templateSlug ];

			// Delete the template from the server.
			app.updateEntityData( { slug: templateSlug, delete: true }, 'template' );

			// Update template selectors in all PDFs.
			$( '.wpforms-pdf-template-style select' ).each( function() {
				const $eachSelect = $( this );
				const isCurrentTemplate = $eachSelect.val() === templateSlug;

				$eachSelect.find( `option[value="${ templateSlug }"]` ).remove();

				if ( ! isCurrentTemplate ) {
					return;
				}

				$eachSelect.val( baseTemplate );
			} );

			$select.trigger( 'change' );
		},
	};

	// Return the public-facing methods.
	return app;
}