/** * Contact Forms - Form Settings Editor * * Handles real-time preview updates and form settings saving. */ jQuery(function($){ 'use strict'; // ======================================================================== // Form Editor Page Initialization // ======================================================================== // Message override radio buttons - toggle WP editor visibility $('input[type=radio]').on('change', function() { var name = $(this).attr('name'); if ($(this).val() === '1') { $('#' + name + ' .defalut_message').hide(); $('#' + name + ' .wp-editor-wrap').show(); } else { if ($(this).val() !== '-1') { $('#' + name + ' .defalut_message').show(); } else { $('#' + name + ' .defalut_message').hide(); } $('#' + name + ' .wp-editor-wrap').hide(); } }); // Initialize message field visibility $.each(['success_message', 'error_message', 'admin_emails_message', 'confirmation_emails_message'], function(i, key) { var value = $('#accua_form_' + key + ' .accua_form_check_override:checked').val(); if (value !== undefined && value != 0) { if (value != -1) { $('#accua_form_' + key + ' .wp-editor-wrap').show(); } else { $('#accua_form_' + key + ' .wp-editor-wrap').hide(); } $('#accua_form_' + key + ' .defalut_message').hide(); } else { $('#accua_form_' + key + ' .wp-editor-wrap').hide(); $('#accua_form_' + key + ' .defalut_message').show(); } }); // Retention override toggle $('#accua_form_retention_override').on('change', function() { $('#accua_form_retention_fields').toggle(this.checked); }); // Preview area - resizable and auto-resize iframe on load var $previewWrapper = $('#accua_form_preview_area_wrapper'); var $previewIframe = $('#accua_form_preview_area'); var previewWrapperResizable = false; if ($previewWrapper.length && $.fn.resizable) { $previewWrapper.resizable({ handles: 's' }); $previewIframe.css({ width: '100%', height: '100%' }); previewWrapperResizable = true; } /** * The iframe src is rendered server-side, so on a fast response the load * event may fire before any handler in this file is attached. The initial * about:blank document also reports readyState 'complete' but has an empty * body; require a non-empty body to tell the real preview apart. */ function previewFrameAlreadyLoaded() { if (!$previewIframe.length) return false; try { var doc = $previewIframe[0].contentDocument; return !!(doc && doc.readyState === 'complete' && doc.body && doc.body.children.length); } catch (e) { return false; // cross-origin } } function resizePreviewToContent() { try { var h = $previewIframe[0].contentWindow.document.documentElement.scrollHeight + 200; if (previewWrapperResizable) { // The iframe fills the wrapper (width/height 100%) and jQuery UI // resizable manages the wrapper's height, so size the wrapper; // a pixel height on the iframe itself would detach it from manual // wrapper resizing. $previewWrapper.height(h); } else { $previewIframe[0].style.height = h + 'px'; } } catch (e) { /* cross-origin */ } } if ($previewIframe.length) { $previewIframe.on('load', resizePreviewToContent); if (previewFrameAlreadyLoaded()) { resizePreviewToContent(); } } // ======================================================================== // Configuration // ======================================================================== var DEBOUNCE_DELAY = 100; // ms delay for debounced updates var PREVIEW_LOAD_DELAY = 300; // ms delay after iframe load before updating // All style fields that support override checkbox + value var ALL_STYLE_FIELDS = { // Form container styles form: [ 'style_margin', 'style_border_color', 'style_border_width', 'style_border_radius', 'style_background_color', 'style_padding', 'style_color', 'style_font_size' ], // Field styles field: [ 'style_field_spacing', 'style_field_border_color', 'style_field_border_width', 'style_field_border_radius', 'style_field_background_color', 'style_field_padding', 'style_field_color' ], // Submit button styles submit: [ 'style_submit_border_color', 'style_submit_border_width', 'style_submit_border_radius', 'style_submit_background_color', 'style_submit_padding', 'style_submit_color', 'style_submit_font_size' ] }; // Color fields that use wpColorPicker var COLOR_FIELDS = [ 'style_border_color', 'style_background_color', 'style_color', 'style_field_border_color', 'style_field_background_color', 'style_field_color', 'style_submit_border_color', 'style_submit_background_color', 'style_submit_color' ]; // ======================================================================== // State // ======================================================================== var previewReady = false; var $saveButton = $('.accua_form_save_settings_button'); // ======================================================================== // Utility Functions // ======================================================================== /** * Simple debounce function */ function debounce(func, wait) { var timeout; return function() { var context = this, args = arguments; clearTimeout(timeout); timeout = setTimeout(function() { func.apply(context, args); }, wait); }; } /** * Get TinyMCE content or fallback to textarea value */ function getTinyMCEContent(name) { if ($('#' + name + ' .wp-editor-wrap').hasClass('tmce-active') && typeof tinyMCE !== 'undefined') { var editor = tinyMCE.get(name + '_textarea'); return editor ? editor.getContent() : $('#' + name + '_textarea').val(); } return $('#' + name + '_textarea').val(); } /** * Parse a numeric value and add px suffix if needed */ function parseStyleValue(value) { if (!value || value === '') return ''; value = String(value).trim(); // If it's a pure number, add px if (/^-?\d+(\.\d+)?$/.test(value)) { return value + 'px'; } return value; } // ======================================================================== // Preview Document Access // ======================================================================== /** * Safely get the preview iframe document */ function getPreviewDocument() { var iframe = document.getElementById('accua_form_preview_area'); if (!iframe) return null; try { var doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document); return doc && doc.body ? doc : null; } catch (e) { return null; } } /** * Get the form element from preview */ function getPreviewForm() { var doc = getPreviewDocument(); return doc ? doc.querySelector('form.accua-form') : null; } // ======================================================================== // Style Value Retrieval // ======================================================================== /** * Get style value if the override checkbox is checked */ function getStyleValue(key) { var $container = $('#accua_form_' + key); var $checkbox = $container.find('.accua_form_check_override'); // Check if override is enabled if (!$checkbox.length || !$checkbox.is(':checked')) { return ''; } // For color pickers, get value from the hidden input (wpColorPicker syncs to it) var $valueInput = $container.find('.accua_form_value'); return $valueInput.val() || ''; } // ======================================================================== // Layout Preview // ======================================================================== /** * Reload preview iframe with new layout parameter. * Layout changes require HTML structure rebuild, not just CSS class toggling. * Different layouts (inline, toplabel, sidebyside) generate different HTML. */ function reloadPreviewWithLayout(layout) { var formId = $('#accua_form_save_settings_id').val(); var $iframe = $('#accua_form_preview_area'); var $wrapper = $('#accua_form_preview_area_wrapper'); previewReady = false; // Show loading state $wrapper.addClass('accua-form-preview-loading'); // Build preview URL with layout override parameter var previewUrl = 'admin-ajax.php?action=accua_forms_preview&fid=' + formId + '&_wpnonce=' + accua_forms_nonces.preview_nonce; // Always pass layout parameter - use 'default' when empty to signal global default should be used var layoutParam = (layout && layout !== '') ? layout : 'default'; previewUrl += '&preview_layout=' + encodeURIComponent(layoutParam); $iframe[0].src = previewUrl; } /** * Update preview layout class based on dropdown selection * NOTE: This only toggles CSS classes - used for CSS-only style changes. * For actual layout changes, use reloadPreviewWithLayout() instead. */ function updatePreviewLayout() { var form = getPreviewForm(); if (!form) return; var layout = $('#accua_form_layout .accua_form_value').val() || ''; // Remove all layout classes form.classList.remove( 'accua-form-view-standard', 'accua-form-view-sidebyside', 'accua-form-view-inlinelabel' ); // Add appropriate class based on selection switch (layout) { case 'toplabel': form.classList.add('accua-form-view-standard'); break; case 'inlinelabel': form.classList.add('accua-form-view-inlinelabel'); break; case 'sidebyside': form.classList.add('accua-form-view-sidebyside'); break; default: // Empty/default - use sidebyside as fallback for backwards compatibility form.classList.add('accua-form-view-sidebyside'); break; } } // ======================================================================== // Styles Preview // ======================================================================== /** * Build inline style string from style object */ function buildStyleString(styles) { var parts = []; for (var prop in styles) { if (styles.hasOwnProperty(prop) && styles[prop]) { parts.push(prop + ':' + styles[prop]); } } return parts.join(';'); } /** * Update all preview styles */ function updatePreviewStyles() { var form = getPreviewForm(); if (!form) return; var doc = getPreviewDocument(); if (!doc) return; // ---- Form Container Styles ---- var formStyles = {}; var margin = getStyleValue('style_margin'); if (margin) formStyles['margin'] = parseStyleValue(margin); var borderColor = getStyleValue('style_border_color'); if (borderColor) formStyles['border-color'] = borderColor; var borderWidth = getStyleValue('style_border_width'); if (borderWidth) { formStyles['border-width'] = parseStyleValue(borderWidth); formStyles['border-style'] = 'solid'; } var borderRadius = getStyleValue('style_border_radius'); if (borderRadius) formStyles['border-radius'] = parseStyleValue(borderRadius); var backgroundColor = getStyleValue('style_background_color'); if (backgroundColor) formStyles['background-color'] = backgroundColor; var padding = getStyleValue('style_padding'); if (padding) formStyles['padding'] = parseStyleValue(padding); var color = getStyleValue('style_color'); if (color) formStyles['color'] = color; var fontSize = getStyleValue('style_font_size'); if (fontSize) formStyles['font-size'] = parseStyleValue(fontSize); form.style.cssText = buildStyleString(formStyles); // ---- Field Styles ---- var fieldStyles = {}; var fieldBorderColor = getStyleValue('style_field_border_color'); if (fieldBorderColor) fieldStyles['border-color'] = fieldBorderColor; var fieldBorderWidth = getStyleValue('style_field_border_width'); if (fieldBorderWidth) { fieldStyles['border-width'] = parseStyleValue(fieldBorderWidth); fieldStyles['border-style'] = 'solid'; } var fieldBorderRadius = getStyleValue('style_field_border_radius'); if (fieldBorderRadius) fieldStyles['border-radius'] = parseStyleValue(fieldBorderRadius); var fieldBackgroundColor = getStyleValue('style_field_background_color'); if (fieldBackgroundColor) fieldStyles['background-color'] = fieldBackgroundColor; var fieldPadding = getStyleValue('style_field_padding'); if (fieldPadding) fieldStyles['padding'] = parseStyleValue(fieldPadding); var fieldColor = getStyleValue('style_field_color'); if (fieldColor) fieldStyles['color'] = fieldColor; var fieldStyleString = buildStyleString(fieldStyles); var fields = doc.querySelectorAll('.pfbc-textbox, .pfbc-textarea, .pfbc-select, input[type="text"], input[type="email"], input[type="date"], input[type="password"], textarea, select'); for (var i = 0; i < fields.length; i++) { fields[i].style.cssText = fieldStyleString; } // Field spacing (margin-bottom on pfbc-element) var fieldSpacing = getStyleValue('style_field_spacing'); if (fieldSpacing) { var elements = doc.querySelectorAll('.pfbc-element'); var spacingValue = parseStyleValue(fieldSpacing); for (var j = 0; j < elements.length; j++) { elements[j].style.marginBottom = spacingValue; } } // ---- Submit Button Styles ---- var submitStyles = {}; var submitBorderColor = getStyleValue('style_submit_border_color'); if (submitBorderColor) submitStyles['border-color'] = submitBorderColor; var submitBorderWidth = getStyleValue('style_submit_border_width'); if (submitBorderWidth) { submitStyles['border-width'] = parseStyleValue(submitBorderWidth); submitStyles['border-style'] = 'solid'; } var submitBorderRadius = getStyleValue('style_submit_border_radius'); if (submitBorderRadius) submitStyles['border-radius'] = parseStyleValue(submitBorderRadius); var submitBackgroundColor = getStyleValue('style_submit_background_color'); if (submitBackgroundColor) submitStyles['background-color'] = submitBackgroundColor; var submitPadding = getStyleValue('style_submit_padding'); if (submitPadding) submitStyles['padding'] = parseStyleValue(submitPadding); var submitColor = getStyleValue('style_submit_color'); if (submitColor) submitStyles['color'] = submitColor; var submitFontSize = getStyleValue('style_submit_font_size'); if (submitFontSize) submitStyles['font-size'] = parseStyleValue(submitFontSize); var submitButton = doc.querySelector('.pfbc-buttons button, .pfbc-buttons input[type="submit"], button[type="submit"], input[type="submit"]'); if (submitButton) { submitButton.style.cssText = buildStyleString(submitStyles); } } // Create debounced version for smoother UX var updatePreviewStylesDebounced = debounce(updatePreviewStyles, DEBOUNCE_DELAY); // ======================================================================== // Combined Preview Update // ======================================================================== /** * Update preview styles after iframe load. * Layout is NOT toggled here - the server-rendered iframe already has the * correct layout class and HTML structure. Layout changes go through * reloadPreviewWithLayout() which rebuilds the iframe entirely. */ function updateFullPreview() { if (!previewReady) return; updatePreviewStyles(); } // ======================================================================== // Color Picker Initialization // ======================================================================== /** * Initialize a color picker. * * Opening and closing is left entirely to wpColorPicker. Its open() already * closes every other picker (it fires click.wpcolorpicker on body), so no * custom "only one open at a time" handling is needed here - and any that * hides .iris-picker directly breaks the widget, because both open() and * close() call iris('toggle'), which flips on the element's current * visibility rather than on the widget's own state. Hiding the panel behind * the widget's back therefore inverts the next click. * * The hex input is kept visible at all times for keyboard access; admin.css * overrides the .hidden class WordPress puts on .wp-picker-input-wrap. */ function initColorPicker(fieldKey) { var $input = $('#accua_form_' + fieldKey + ' .accua_form_value'); if (!$input.length) return; $input.wpColorPicker({ change: function(event, ui) { // wpColorPicker change event - update preview updatePreviewStylesDebounced(); }, clear: function() { updatePreviewStylesDebounced(); } }); // Iris registers a one-shot "show the panel on first focus" handler on the // input. It calls iris show() directly, so it opens the panel without // wpColorPicker marking the picker open. In stock WordPress that never // fires, because the hex input only exists while the picker is already // open; here the input is always visible, so a plain click into the field // would desync the widget. Drop it - the swatch button opens the panel, // and typing a hex value still works through Iris's change/keyup listeners. // Iris binds it without a namespace, so this can only be an unqualified // off() and must stay directly after the wpColorPicker() call above, while // that handler is still the only focus handler on the input. $input.off('focus'); } // Initialize all color pickers $.each(COLOR_FIELDS, function(i, key) { initColorPicker(key); }); // ======================================================================== // Event Listeners - Style Fields // ======================================================================== /** * Attach event listeners for a style field */ function attachStyleFieldListeners(fieldKey, isColorField) { var $container = $('#accua_form_' + fieldKey); if (!$container.length) return; // Checkbox toggle - always triggers preview update $container.find('.accua_form_check_override').on('change', function() { updatePreviewStylesDebounced(); }); // Value input - for non-color fields if (!isColorField) { $container.find('.accua_form_value').on('input change', function() { updatePreviewStylesDebounced(); }); } } // Attach listeners to all form style fields $.each(ALL_STYLE_FIELDS.form, function(i, key) { attachStyleFieldListeners(key, COLOR_FIELDS.indexOf(key) !== -1); }); // Attach listeners to all field style fields $.each(ALL_STYLE_FIELDS.field, function(i, key) { attachStyleFieldListeners(key, COLOR_FIELDS.indexOf(key) !== -1); }); // Attach listeners to all submit style fields $.each(ALL_STYLE_FIELDS.submit, function(i, key) { attachStyleFieldListeners(key, COLOR_FIELDS.indexOf(key) !== -1); }); // ======================================================================== // Event Listeners - Layout // ======================================================================== $('#accua_form_layout .accua_form_value').on('change', function() { // Reload preview with new layout - layout changes require HTML rebuild // (different layouts generate different HTML structure, not just CSS) reloadPreviewWithLayout($(this).val()); }); // ======================================================================== // Preview Iframe Load Handler // ======================================================================== function handlePreviewLoaded() { previewReady = true; // Remove loading state $('#accua_form_preview_area_wrapper').removeClass('accua-form-preview-loading'); // Small delay to ensure iframe content is fully rendered setTimeout(function() { updateFullPreview(); }, PREVIEW_LOAD_DELAY); } $('#accua_form_preview_area').on('load', handlePreviewLoaded); // If the server-rendered iframe finished loading before the handler above // was attached, the load event has already fired: without this the preview // never becomes ready and live style updates stay disabled until a reload. if (previewFrameAlreadyLoaded()) { handlePreviewLoaded(); } // ======================================================================== // Save Handler // ======================================================================== $('.accua_form_save_settings_button').on('click', function(e) { e.preventDefault(); var $clickedButton = $(this); // Store original button text and add saving state with "Saving..." text var originalText = $clickedButton.val(); $saveButton.addClass('accua-saving').prop('disabled', true); $clickedButton.val(accua_forms_i18n.saving || 'Saving...').addClass('accua-saving-active'); var formId = $('#accua_form_save_settings_id').val(); // Build data object var data = { 'action': 'accua-save-form-settings', 'form-id': formId, '_nonce_edit_form': $('#_nonce_edit_form').val(), 'title': $('#title').val(), 'use_ajax': $('#accua_form_use_ajax .accua_form_value').is(':checked') ? 1 : 0 }; // Layout var layout = $('#accua_form_layout .accua_form_value').val(); if (layout === 'toplabel' || layout === 'sidebyside' || layout === 'inlinelabel') { data.layout = layout; } else { data.layout = ''; } // GADS conversion tracking var gads = $('#gads_conversion_code_input').val(); if (gads && gads !== 'undefined' && gads !== 'null') { data.gads_conversion_tracking_code = gads; } // Collect all override fields var overrideFields = [ 'success_message', 'error_message', 'emails_from_name', 'emails_from', 'admin_emails_to', 'emails_bcc', 'admin_emails_subject', 'admin_emails_message', 'confirmation_emails_subject', 'confirmation_emails_message' ]; // Add all style fields $.each(ALL_STYLE_FIELDS.form, function(i, key) { overrideFields.push(key); }); $.each(ALL_STYLE_FIELDS.field, function(i, key) { overrideFields.push(key); }); $.each(ALL_STYLE_FIELDS.submit, function(i, key) { overrideFields.push(key); }); $.each(overrideFields, function(i, key) { var $checkbox = $('#accua_form_' + key + ' .accua_form_check_override:checked'); var checkValue = $checkbox.val(); if (checkValue !== undefined && checkValue != 0) { if (checkValue == -1) { // "No message" option data[key] = ''; data[key + '_no_message'] = 1; } else { var $element = $('#accua_form_' + key + ' .accua_form_value'); if ($element.is('textarea')) { data[key] = getTinyMCEContent('accua_form_' + key); } else { data[key] = $element.val(); } } } }); // Data Retention fields data.submission_retention_override = $('#accua_form_retention_override').is(':checked') ? 1 : 0; data.submission_retention_value = $('#submission_retention_value').val(); data.submission_retention_unit = $('#submission_retention_unit').val(); data.submission_retention_mode = $('input[name="submission_retention_mode"]:checked').val(); // The per-widget field saves, the order save and the settings save below // all read-modify-write the same draft transient server-side. They must // run strictly in sequence: fired concurrently they overwrite each // other's draft (lost update - e.g. a renamed title silently reverting). var runDraftSaveChain = function(done) { if (typeof accuaWidgets === 'undefined' || !accuaWidgets) { done(); return; } var $widgets = $('#widgets-right div.widget'); var saveWidgetAt = function(i) { if (i >= $widgets.length) { if (accuaWidgets.saveOrder) { accuaWidgets.saveOrder(null, function() { done(); }); } else { done(); } return; } accuaWidgets.save($widgets.eq(i), 0, 1, 0, function() { saveWidgetAt(i + 1); }); }; saveWidgetAt(0); }; runDraftSaveChain(function() { // Step 1: Save settings to draft $.ajax({ url: ajaxurl, type: 'POST', data: data, success: function() { // Step 2: Publish draft to live database $.ajax({ url: ajaxurl, type: 'POST', data: { 'action': 'accua-publish-form-draft', 'form-id': formId, '_nonce_edit_form': $('#_nonce_edit_form').val() }, success: function() { // Reload preview to show saved state (include current layout) reloadPreviewWithLayout($('#accua_form_layout .accua_form_value').val()); // Update URL without reload try { if (history.pushState && window.location.search.indexOf('page=accua_forms_list') === -1) { history.pushState('', document.title, 'admin.php?page=accua_forms_list&fid=' + formId); window.onpopstate = function() { location.reload(); }; } } catch (e) {} // Restore button text immediately (was cleared to show spinner) $clickedButton.val(originalText); // Show success state on button briefly $saveButton.removeClass('accua-saving').addClass('accua-saved').prop('disabled', false); $clickedButton.removeClass('accua-saving-active').addClass('accua-saved-active'); // Remove success visual state after 1.5 seconds setTimeout(function() { $saveButton.removeClass('accua-saved'); $clickedButton.removeClass('accua-saved-active'); }, 1500); }, error: function() { // Show error state $saveButton.removeClass('accua-saving').prop('disabled', false); $clickedButton.removeClass('accua-saving-active').val(originalText).addClass('accua-error'); setTimeout(function() { $clickedButton.removeClass('accua-error'); }, 3000); } }); }, error: function() { // Show error state $saveButton.removeClass('accua-saving').prop('disabled', false); $clickedButton.removeClass('accua-saving-active').val(originalText).addClass('accua-error'); setTimeout(function() { $clickedButton.removeClass('accua-error'); }, 3000); } }); }); // end runDraftSaveChain }); // ======================================================================== // Messages Tab: Override Checkboxes Enable/Disable Their Text Input // ======================================================================== /** * The email override inputs (To, Bcc, Subject, From name, From email) render * disabled until their "Customize" checkbox is checked. The checkbox name * matches its container id (e.g. accua_form_admin_emails_to). Select by * data-tab, not panel #id - tabs.js renames panel IDs at init. Appearance * tab rows need no JS: admin.css :has() rules show/hide those inputs. */ $('.accua-tabs__panel[data-tab="messages"] input.accua_form_check_override[type="checkbox"]').on('change', function() { $('#' + this.name).find('.accua_form_value').first().prop('disabled', !this.checked); }); // ======================================================================== // Unsaved Changes Warning (WordPress standard beforeunload pattern) // ======================================================================== var formDirty = false; function markDirty() { formDirty = true; } function markClean() { formDirty = false; } // Expose globally so form-fields.js (and other scripts) can mark dirty window.accuaFormsEditorDirty = { mark: markDirty, clean: markClean, isDirty: function() { return formDirty; } }; // Track changes on all form inputs within the edit page $('#accua_forms_edit_page').on('input change', 'input, textarea, select', markDirty); // Track color picker changes (wpColorPicker fires irischange on the document) $(document).on('irischange', '#accua_forms_edit_page .wp-picker-container input', markDirty); // Note: Field drag/drop/reorder is tracked via window.accuaFormsEditorDirty.mark() // called directly from form-fields.js sortable stop and droppable drop handlers. // TinyMCE editors: mark dirty on content change function bindTinyMCEDirty(editor) { editor.on('input change keyup', markDirty); } $(document).on('tinymce-editor-init', function(event, editor) { bindTinyMCEDirty(editor); }); // Also bind to any editors already initialized (race condition safety) if (typeof tinyMCE !== 'undefined' && tinyMCE.editors) { $.each(tinyMCE.editors, function(i, editor) { if (editor) { bindTinyMCEDirty(editor); } }); } // Mark clean after successful save (publish draft) $(document).ajaxComplete(function(event, xhr, settings) { if (settings && settings.data && typeof settings.data === 'string' && settings.data.indexOf('action=accua-publish-form-draft') !== -1) { if (xhr.status === 200) { markClean(); } } }); // Allow intentional form submissions (e.g. delete form) without warning $('#accua_forms_edit_page').on('submit', 'form', markClean); // Browser beforeunload warning $(window).on('beforeunload', function() { if (formDirty) { return (typeof accua_forms_i18n !== 'undefined' && accua_forms_i18n.unsaved_changes) ? accua_forms_i18n.unsaved_changes : true; } }); });