PluginProbe
Contact Forms by Cimatti / 2.3.0
Contact Forms by Cimatti v2.3.0
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / assets / js / admin / form-settings.js

form-settings.js in Contact Forms by Cimatti 2.3.0, at assets/js/admin/form-settings.js

888 lines 31.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Contact Forms - Form Settings Editor
3 *
4 * Handles real-time preview updates and form settings saving.
5 */
6 jQuery(function($){
7 'use strict';
8
9 // ========================================================================
10 // Form Editor Page Initialization
11 // ========================================================================
12
13 // Message override radio buttons — toggle WP editor visibility
14 $('input[type=radio]').on('change', function() {
15 var name = $(this).attr('name');
16 if ($(this).val() === '1') {
17 $('#' + name + ' .defalut_message').hide();
18 $('#' + name + ' .wp-editor-wrap').show();
19 } else {
20 if ($(this).val() !== '-1') {
21 $('#' + name + ' .defalut_message').show();
22 } else {
23 $('#' + name + ' .defalut_message').hide();
24 }
25 $('#' + name + ' .wp-editor-wrap').hide();
26 }
27 });
28
29 // Initialize message field visibility
30 $.each(['success_message', 'error_message', 'admin_emails_message', 'confirmation_emails_message'], function(i, key) {
31 var value = $('#accua_form_' + key + ' .accua_form_check_override:checked').val();
32 if (value !== undefined && value != 0) {
33 if (value != -1) {
34 $('#accua_form_' + key + ' .wp-editor-wrap').show();
35 } else {
36 $('#accua_form_' + key + ' .wp-editor-wrap').hide();
37 }
38 $('#accua_form_' + key + ' .defalut_message').hide();
39 } else {
40 $('#accua_form_' + key + ' .wp-editor-wrap').hide();
41 $('#accua_form_' + key + ' .defalut_message').show();
42 }
43 });
44
45 // Retention override toggle
46 $('#accua_form_retention_override').on('change', function() {
47 $('#accua_form_retention_fields').toggle(this.checked);
48 });
49
50 // Preview area — resizable and auto-resize iframe on load
51 var $previewWrapper = $('#accua_form_preview_area_wrapper');
52 var $previewIframe = $('#accua_form_preview_area');
53 var previewWrapperResizable = false;
54 if ($previewWrapper.length && $.fn.resizable) {
55 $previewWrapper.resizable({ handles: 's' });
56 $previewIframe.css({ width: '100%', height: '100%' });
57 previewWrapperResizable = true;
58 }
59
60 /**
61 * The iframe src is rendered server-side, so on a fast response the load
62 * event may fire before any handler in this file is attached. The initial
63 * about:blank document also reports readyState 'complete' but has an empty
64 * body; require a non-empty body to tell the real preview apart.
65 */
66 function previewFrameAlreadyLoaded() {
67 if (!$previewIframe.length) return false;
68 try {
69 var doc = $previewIframe[0].contentDocument;
70 return !!(doc && doc.readyState === 'complete' && doc.body && doc.body.children.length);
71 } catch (e) {
72 return false; // cross-origin
73 }
74 }
75
76 function resizePreviewToContent() {
77 try {
78 var h = $previewIframe[0].contentWindow.document.documentElement.scrollHeight + 200;
79 if (previewWrapperResizable) {
80 // The iframe fills the wrapper (width/height 100%) and jQuery UI
81 // resizable manages the wrapper's height, so size the wrapper;
82 // a pixel height on the iframe itself would detach it from manual
83 // wrapper resizing.
84 $previewWrapper.height(h);
85 } else {
86 $previewIframe[0].style.height = h + 'px';
87 }
88 } catch (e) { /* cross-origin */ }
89 }
90
91 if ($previewIframe.length) {
92 $previewIframe.on('load', resizePreviewToContent);
93 if (previewFrameAlreadyLoaded()) {
94 resizePreviewToContent();
95 }
96 }
97
98 // ========================================================================
99 // Configuration
100 // ========================================================================
101 var DEBOUNCE_DELAY = 100; // ms delay for debounced updates
102 var PREVIEW_LOAD_DELAY = 300; // ms delay after iframe load before updating
103
104 // All style fields that support override checkbox + value
105 var ALL_STYLE_FIELDS = {
106 // Form container styles
107 form: [
108 'style_margin',
109 'style_border_color',
110 'style_border_width',
111 'style_border_radius',
112 'style_background_color',
113 'style_padding',
114 'style_color',
115 'style_font_size'
116 ],
117 // Field styles
118 field: [
119 'style_field_spacing',
120 'style_field_border_color',
121 'style_field_border_width',
122 'style_field_border_radius',
123 'style_field_background_color',
124 'style_field_padding',
125 'style_field_color'
126 ],
127 // Submit button styles
128 submit: [
129 'style_submit_border_color',
130 'style_submit_border_width',
131 'style_submit_border_radius',
132 'style_submit_background_color',
133 'style_submit_padding',
134 'style_submit_color',
135 'style_submit_font_size'
136 ]
137 };
138
139 // Color fields that use wpColorPicker
140 var COLOR_FIELDS = [
141 'style_border_color',
142 'style_background_color',
143 'style_color',
144 'style_field_border_color',
145 'style_field_background_color',
146 'style_field_color',
147 'style_submit_border_color',
148 'style_submit_background_color',
149 'style_submit_color'
150 ];
151
152 // ========================================================================
153 // State
154 // ========================================================================
155 var previewReady = false;
156 var $saveButton = $('.accua_form_save_settings_button');
157
158 // ========================================================================
159 // Utility Functions
160 // ========================================================================
161
162 /**
163 * Simple debounce function
164 */
165 function debounce(func, wait) {
166 var timeout;
167 return function() {
168 var context = this, args = arguments;
169 clearTimeout(timeout);
170 timeout = setTimeout(function() {
171 func.apply(context, args);
172 }, wait);
173 };
174 }
175
176 /**
177 * Get TinyMCE content or fallback to textarea value
178 */
179 function getTinyMCEContent(name) {
180 if ($('#' + name + ' .wp-editor-wrap').hasClass('tmce-active') && typeof tinyMCE !== 'undefined') {
181 var editor = tinyMCE.get(name + '_textarea');
182 return editor ? editor.getContent() : $('#' + name + '_textarea').val();
183 }
184 return $('#' + name + '_textarea').val();
185 }
186
187 /**
188 * Parse a numeric value and add px suffix if needed
189 */
190 function parseStyleValue(value) {
191 if (!value || value === '') return '';
192 value = String(value).trim();
193 // If it's a pure number, add px
194 if (/^-?\d+(\.\d+)?$/.test(value)) {
195 return value + 'px';
196 }
197 return value;
198 }
199
200 // ========================================================================
201 // Preview Document Access
202 // ========================================================================
203
204 /**
205 * Safely get the preview iframe document
206 */
207 function getPreviewDocument() {
208 var iframe = document.getElementById('accua_form_preview_area');
209 if (!iframe) return null;
210 try {
211 var doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
212 return doc && doc.body ? doc : null;
213 } catch (e) {
214 return null;
215 }
216 }
217
218 /**
219 * Get the form element from preview
220 */
221 function getPreviewForm() {
222 var doc = getPreviewDocument();
223 return doc ? doc.querySelector('form.accua-form') : null;
224 }
225
226 // ========================================================================
227 // Style Value Retrieval
228 // ========================================================================
229
230 /**
231 * Get style value if the override checkbox is checked
232 */
233 function getStyleValue(key) {
234 var $container = $('#accua_form_' + key);
235 var $checkbox = $container.find('.accua_form_check_override');
236
237 // Check if override is enabled
238 if (!$checkbox.length || !$checkbox.is(':checked')) {
239 return '';
240 }
241
242 // For color pickers, get value from the hidden input (wpColorPicker syncs to it)
243 var $valueInput = $container.find('.accua_form_value');
244 return $valueInput.val() || '';
245 }
246
247 // ========================================================================
248 // Layout Preview
249 // ========================================================================
250
251 /**
252 * Reload preview iframe with new layout parameter.
253 * Layout changes require HTML structure rebuild, not just CSS class toggling.
254 * Different layouts (inline, toplabel, sidebyside) generate different HTML.
255 */
256 function reloadPreviewWithLayout(layout) {
257 var formId = $('#accua_form_save_settings_id').val();
258 var $iframe = $('#accua_form_preview_area');
259 var $wrapper = $('#accua_form_preview_area_wrapper');
260
261 previewReady = false;
262
263 // Show loading state
264 $wrapper.addClass('accua-form-preview-loading');
265
266 // Build preview URL with layout override parameter
267 var previewUrl = 'admin-ajax.php?action=accua_forms_preview&fid=' + formId +
268 '&_wpnonce=' + accua_forms_nonces.preview_nonce;
269
270 // Always pass layout parameter - use 'default' when empty to signal global default should be used
271 var layoutParam = (layout && layout !== '') ? layout : 'default';
272 previewUrl += '&preview_layout=' + encodeURIComponent(layoutParam);
273
274 $iframe[0].src = previewUrl;
275 }
276
277 /**
278 * Update preview layout class based on dropdown selection
279 * NOTE: This only toggles CSS classes - used for CSS-only style changes.
280 * For actual layout changes, use reloadPreviewWithLayout() instead.
281 */
282 function updatePreviewLayout() {
283 var form = getPreviewForm();
284 if (!form) return;
285
286 var layout = $('#accua_form_layout .accua_form_value').val() || '';
287
288 // Remove all layout classes
289 form.classList.remove(
290 'accua-form-view-standard',
291 'accua-form-view-sidebyside',
292 'accua-form-view-inlinelabel'
293 );
294
295 // Add appropriate class based on selection
296 switch (layout) {
297 case 'toplabel':
298 form.classList.add('accua-form-view-standard');
299 break;
300 case 'inlinelabel':
301 form.classList.add('accua-form-view-inlinelabel');
302 break;
303 case 'sidebyside':
304 form.classList.add('accua-form-view-sidebyside');
305 break;
306 default:
307 // Empty/default - use sidebyside as fallback for backwards compatibility
308 form.classList.add('accua-form-view-sidebyside');
309 break;
310 }
311 }
312
313 // ========================================================================
314 // Styles Preview
315 // ========================================================================
316
317 /**
318 * Build inline style string from style object
319 */
320 function buildStyleString(styles) {
321 var parts = [];
322 for (var prop in styles) {
323 if (styles.hasOwnProperty(prop) && styles[prop]) {
324 parts.push(prop + ':' + styles[prop]);
325 }
326 }
327 return parts.join(';');
328 }
329
330 /**
331 * Update all preview styles
332 */
333 function updatePreviewStyles() {
334 var form = getPreviewForm();
335 if (!form) return;
336
337 var doc = getPreviewDocument();
338 if (!doc) return;
339
340 // ---- Form Container Styles ----
341 var formStyles = {};
342
343 var margin = getStyleValue('style_margin');
344 if (margin) formStyles['margin'] = parseStyleValue(margin);
345
346 var borderColor = getStyleValue('style_border_color');
347 if (borderColor) formStyles['border-color'] = borderColor;
348
349 var borderWidth = getStyleValue('style_border_width');
350 if (borderWidth) {
351 formStyles['border-width'] = parseStyleValue(borderWidth);
352 formStyles['border-style'] = 'solid';
353 }
354
355 var borderRadius = getStyleValue('style_border_radius');
356 if (borderRadius) formStyles['border-radius'] = parseStyleValue(borderRadius);
357
358 var backgroundColor = getStyleValue('style_background_color');
359 if (backgroundColor) formStyles['background-color'] = backgroundColor;
360
361 var padding = getStyleValue('style_padding');
362 if (padding) formStyles['padding'] = parseStyleValue(padding);
363
364 var color = getStyleValue('style_color');
365 if (color) formStyles['color'] = color;
366
367 var fontSize = getStyleValue('style_font_size');
368 if (fontSize) formStyles['font-size'] = parseStyleValue(fontSize);
369
370 form.style.cssText = buildStyleString(formStyles);
371
372 // ---- Field Styles ----
373 var fieldStyles = {};
374
375 var fieldBorderColor = getStyleValue('style_field_border_color');
376 if (fieldBorderColor) fieldStyles['border-color'] = fieldBorderColor;
377
378 var fieldBorderWidth = getStyleValue('style_field_border_width');
379 if (fieldBorderWidth) {
380 fieldStyles['border-width'] = parseStyleValue(fieldBorderWidth);
381 fieldStyles['border-style'] = 'solid';
382 }
383
384 var fieldBorderRadius = getStyleValue('style_field_border_radius');
385 if (fieldBorderRadius) fieldStyles['border-radius'] = parseStyleValue(fieldBorderRadius);
386
387 var fieldBackgroundColor = getStyleValue('style_field_background_color');
388 if (fieldBackgroundColor) fieldStyles['background-color'] = fieldBackgroundColor;
389
390 var fieldPadding = getStyleValue('style_field_padding');
391 if (fieldPadding) fieldStyles['padding'] = parseStyleValue(fieldPadding);
392
393 var fieldColor = getStyleValue('style_field_color');
394 if (fieldColor) fieldStyles['color'] = fieldColor;
395
396 var fieldStyleString = buildStyleString(fieldStyles);
397 var fields = doc.querySelectorAll('.pfbc-textbox, .pfbc-textarea, .pfbc-select, input[type="text"], input[type="email"], input[type="date"], input[type="password"], textarea, select');
398 for (var i = 0; i < fields.length; i++) {
399 fields[i].style.cssText = fieldStyleString;
400 }
401
402 // Field spacing (margin-bottom on pfbc-element)
403 var fieldSpacing = getStyleValue('style_field_spacing');
404 if (fieldSpacing) {
405 var elements = doc.querySelectorAll('.pfbc-element');
406 var spacingValue = parseStyleValue(fieldSpacing);
407 for (var j = 0; j < elements.length; j++) {
408 elements[j].style.marginBottom = spacingValue;
409 }
410 }
411
412 // ---- Submit Button Styles ----
413 var submitStyles = {};
414
415 var submitBorderColor = getStyleValue('style_submit_border_color');
416 if (submitBorderColor) submitStyles['border-color'] = submitBorderColor;
417
418 var submitBorderWidth = getStyleValue('style_submit_border_width');
419 if (submitBorderWidth) {
420 submitStyles['border-width'] = parseStyleValue(submitBorderWidth);
421 submitStyles['border-style'] = 'solid';
422 }
423
424 var submitBorderRadius = getStyleValue('style_submit_border_radius');
425 if (submitBorderRadius) submitStyles['border-radius'] = parseStyleValue(submitBorderRadius);
426
427 var submitBackgroundColor = getStyleValue('style_submit_background_color');
428 if (submitBackgroundColor) submitStyles['background-color'] = submitBackgroundColor;
429
430 var submitPadding = getStyleValue('style_submit_padding');
431 if (submitPadding) submitStyles['padding'] = parseStyleValue(submitPadding);
432
433 var submitColor = getStyleValue('style_submit_color');
434 if (submitColor) submitStyles['color'] = submitColor;
435
436 var submitFontSize = getStyleValue('style_submit_font_size');
437 if (submitFontSize) submitStyles['font-size'] = parseStyleValue(submitFontSize);
438
439 var submitButton = doc.querySelector('.pfbc-buttons button, .pfbc-buttons input[type="submit"], button[type="submit"], input[type="submit"]');
440 if (submitButton) {
441 submitButton.style.cssText = buildStyleString(submitStyles);
442 }
443 }
444
445 // Create debounced version for smoother UX
446 var updatePreviewStylesDebounced = debounce(updatePreviewStyles, DEBOUNCE_DELAY);
447
448 // ========================================================================
449 // Combined Preview Update
450 // ========================================================================
451
452 /**
453 * Update preview styles after iframe load.
454 * Layout is NOT toggled here — the server-rendered iframe already has the
455 * correct layout class and HTML structure. Layout changes go through
456 * reloadPreviewWithLayout() which rebuilds the iframe entirely.
457 */
458 function updateFullPreview() {
459 if (!previewReady) return;
460 updatePreviewStyles();
461 }
462
463 // ========================================================================
464 // Color Picker Initialization
465 // ========================================================================
466
467 /**
468 * Close all open Iris color pickers
469 * WordPress Iris adds .wp-picker-active class to open pickers
470 */
471 function closeAllColorPickers() {
472 $('.accua-style-content .wp-picker-container').each(function() {
473 var $container = $(this);
474 var $holder = $container.find('.wp-picker-holder');
475 var $iris = $holder.find('.iris-picker');
476
477 // Hide the Iris picker
478 if ($iris.length) {
479 $iris.hide();
480 }
481
482 // Remove the active state
483 $container.removeClass('wp-picker-active');
484 });
485 }
486
487 /**
488 * Initialize a color picker with change handler
489 * Follows WordPress admin UI guidelines:
490 * - Only one color picker open at a time
491 * - Hex input always visible for keyboard accessibility
492 */
493 function initColorPicker(fieldKey) {
494 var $input = $('#accua_form_' + fieldKey + ' .accua_form_value');
495 if (!$input.length) return;
496
497 $input.wpColorPicker({
498 change: function(event, ui) {
499 // wpColorPicker change event - update preview
500 updatePreviewStylesDebounced();
501 },
502 clear: function() {
503 updatePreviewStylesDebounced();
504 }
505 });
506
507 // Get the container elements after wpColorPicker initialization
508 var $container = $input.closest('.wp-picker-container');
509 var $inputWrap = $container.find('.wp-picker-input-wrap');
510 var $hexInput = $container.find('.wp-color-picker');
511 var $colorButton = $container.find('.wp-color-result');
512 var $holder = $container.find('.wp-picker-holder');
513
514 // Always show the hex input field for accessibility (keyboard-accessible text input)
515 // WordPress hides it by default and only shows when picker is open
516 $inputWrap.css('display', '');
517 $hexInput.css('display', '').removeAttr('disabled');
518
519 // Close other pickers when this color button is clicked
520 $colorButton.on('click', function(e) {
521 // Check if this picker is about to open (will become active)
522 var wasActive = $container.hasClass('wp-picker-active');
523
524 if (!wasActive) {
525 // Close all other pickers before this one opens
526 $('.accua-style-content .wp-picker-container').not($container).each(function() {
527 var $other = $(this);
528 var $otherIris = $other.find('.iris-picker');
529 if ($otherIris.length) {
530 $otherIris.hide();
531 }
532 $other.removeClass('wp-picker-active');
533 });
534 }
535 });
536
537 // Also close other pickers when hex input is focused
538 $hexInput.on('focus', function() {
539 // Close all other pickers
540 $('.accua-style-content .wp-picker-container').not($container).each(function() {
541 var $other = $(this);
542 var $otherIris = $other.find('.iris-picker');
543 if ($otherIris.length) {
544 $otherIris.hide();
545 }
546 $other.removeClass('wp-picker-active');
547 });
548 });
549 }
550
551 // Initialize all color pickers
552 $.each(COLOR_FIELDS, function(i, key) {
553 initColorPicker(key);
554 });
555
556 // Close all color pickers when clicking outside
557 $(document).on('click', function(e) {
558 var $target = $(e.target);
559
560 // If click is not inside a color picker container, close all pickers
561 if (!$target.closest('.wp-picker-container').length) {
562 closeAllColorPickers();
563 }
564 });
565
566 // ========================================================================
567 // Event Listeners - Style Fields
568 // ========================================================================
569
570 /**
571 * Attach event listeners for a style field
572 */
573 function attachStyleFieldListeners(fieldKey, isColorField) {
574 var $container = $('#accua_form_' + fieldKey);
575 if (!$container.length) return;
576
577 // Checkbox toggle - always triggers preview update
578 $container.find('.accua_form_check_override').on('change', function() {
579 updatePreviewStylesDebounced();
580 });
581
582 // Value input - for non-color fields
583 if (!isColorField) {
584 $container.find('.accua_form_value').on('input change', function() {
585 updatePreviewStylesDebounced();
586 });
587 }
588 }
589
590 // Attach listeners to all form style fields
591 $.each(ALL_STYLE_FIELDS.form, function(i, key) {
592 attachStyleFieldListeners(key, COLOR_FIELDS.indexOf(key) !== -1);
593 });
594
595 // Attach listeners to all field style fields
596 $.each(ALL_STYLE_FIELDS.field, function(i, key) {
597 attachStyleFieldListeners(key, COLOR_FIELDS.indexOf(key) !== -1);
598 });
599
600 // Attach listeners to all submit style fields
601 $.each(ALL_STYLE_FIELDS.submit, function(i, key) {
602 attachStyleFieldListeners(key, COLOR_FIELDS.indexOf(key) !== -1);
603 });
604
605 // ========================================================================
606 // Event Listeners - Layout
607 // ========================================================================
608
609 $('#accua_form_layout .accua_form_value').on('change', function() {
610 // Reload preview with new layout - layout changes require HTML rebuild
611 // (different layouts generate different HTML structure, not just CSS)
612 reloadPreviewWithLayout($(this).val());
613 });
614
615 // ========================================================================
616 // Preview Iframe Load Handler
617 // ========================================================================
618
619 function handlePreviewLoaded() {
620 previewReady = true;
621
622 // Remove loading state
623 $('#accua_form_preview_area_wrapper').removeClass('accua-form-preview-loading');
624
625 // Small delay to ensure iframe content is fully rendered
626 setTimeout(function() {
627 updateFullPreview();
628 }, PREVIEW_LOAD_DELAY);
629 }
630
631 $('#accua_form_preview_area').on('load', handlePreviewLoaded);
632
633 // If the server-rendered iframe finished loading before the handler above
634 // was attached, the load event has already fired: without this the preview
635 // never becomes ready and live style updates stay disabled until a reload.
636 if (previewFrameAlreadyLoaded()) {
637 handlePreviewLoaded();
638 }
639
640 // ========================================================================
641 // Save Handler
642 // ========================================================================
643
644 $('.accua_form_save_settings_button').on('click', function(e) {
645 e.preventDefault();
646
647 var $clickedButton = $(this);
648
649 // Store original button text and add saving state with "Saving..." text
650 var originalText = $clickedButton.val();
651 $saveButton.addClass('accua-saving').prop('disabled', true);
652 $clickedButton.val(accua_forms_i18n.saving || 'Saving...').addClass('accua-saving-active');
653
654 var formId = $('#accua_form_save_settings_id').val();
655
656 // Build data object
657 var data = {
658 'action': 'accua-save-form-settings',
659 'form-id': formId,
660 '_nonce_edit_form': $('#_nonce_edit_form').val(),
661 'title': $('#title').val(),
662 'use_ajax': $('#accua_form_use_ajax .accua_form_value').is(':checked') ? 1 : 0
663 };
664
665 // Layout
666 var layout = $('#accua_form_layout .accua_form_value').val();
667 if (layout === 'toplabel' || layout === 'sidebyside' || layout === 'inlinelabel') {
668 data.layout = layout;
669 } else {
670 data.layout = '';
671 }
672
673 // GADS conversion tracking
674 var gads = $('#gads_conversion_code_input').val();
675 if (gads && gads !== 'undefined' && gads !== 'null') {
676 data.gads_conversion_tracking_code = gads;
677 }
678
679 // Collect all override fields
680 var overrideFields = [
681 'success_message', 'error_message',
682 'emails_from_name', 'emails_from', 'admin_emails_to', 'emails_bcc',
683 'admin_emails_subject', 'admin_emails_message',
684 'confirmation_emails_subject', 'confirmation_emails_message'
685 ];
686
687 // Add all style fields
688 $.each(ALL_STYLE_FIELDS.form, function(i, key) { overrideFields.push(key); });
689 $.each(ALL_STYLE_FIELDS.field, function(i, key) { overrideFields.push(key); });
690 $.each(ALL_STYLE_FIELDS.submit, function(i, key) { overrideFields.push(key); });
691
692 $.each(overrideFields, function(i, key) {
693 var $checkbox = $('#accua_form_' + key + ' .accua_form_check_override:checked');
694 var checkValue = $checkbox.val();
695
696 if (checkValue !== undefined && checkValue != 0) {
697 if (checkValue == -1) {
698 // "No message" option
699 data[key] = '';
700 data[key + '_no_message'] = 1;
701 } else {
702 var $element = $('#accua_form_' + key + ' .accua_form_value');
703 if ($element.is('textarea')) {
704 data[key] = getTinyMCEContent('accua_form_' + key);
705 } else {
706 data[key] = $element.val();
707 }
708 }
709 }
710 });
711
712 // Data Retention fields
713 data.submission_retention_override = $('#accua_form_retention_override').is(':checked') ? 1 : 0;
714 data.submission_retention_value = $('#submission_retention_value').val();
715 data.submission_retention_unit = $('#submission_retention_unit').val();
716 data.submission_retention_mode = $('input[name="submission_retention_mode"]:checked').val();
717
718 // The per-widget field saves, the order save and the settings save below
719 // all read-modify-write the same draft transient server-side. They must
720 // run strictly in sequence: fired concurrently they overwrite each
721 // other's draft (lost update — e.g. a renamed title silently reverting).
722 var runDraftSaveChain = function(done) {
723 if (typeof accuaWidgets === 'undefined' || !accuaWidgets) {
724 done();
725 return;
726 }
727 var $widgets = $('#widgets-right div.widget');
728 var saveWidgetAt = function(i) {
729 if (i >= $widgets.length) {
730 if (accuaWidgets.saveOrder) {
731 accuaWidgets.saveOrder(null, function() { done(); });
732 } else {
733 done();
734 }
735 return;
736 }
737 accuaWidgets.save($widgets.eq(i), 0, 1, 0, function() { saveWidgetAt(i + 1); });
738 };
739 saveWidgetAt(0);
740 };
741
742 runDraftSaveChain(function() {
743
744 // Step 1: Save settings to draft
745 $.ajax({
746 url: ajaxurl,
747 type: 'POST',
748 data: data,
749 success: function() {
750 // Step 2: Publish draft to live database
751 $.ajax({
752 url: ajaxurl,
753 type: 'POST',
754 data: {
755 'action': 'accua-publish-form-draft',
756 'form-id': formId,
757 '_nonce_edit_form': $('#_nonce_edit_form').val()
758 },
759 success: function() {
760 // Reload preview to show saved state (include current layout)
761 reloadPreviewWithLayout($('#accua_form_layout .accua_form_value').val());
762
763 // Update URL without reload
764 try {
765 if (history.pushState && window.location.search.indexOf('page=accua_forms_list') === -1) {
766 history.pushState('', document.title, 'admin.php?page=accua_forms_list&fid=' + formId);
767 window.onpopstate = function() { location.reload(); };
768 }
769 } catch (e) {}
770
771 // Restore button text immediately (was cleared to show spinner)
772 $clickedButton.val(originalText);
773
774 // Show success state on button briefly
775 $saveButton.removeClass('accua-saving').addClass('accua-saved').prop('disabled', false);
776 $clickedButton.removeClass('accua-saving-active').addClass('accua-saved-active');
777
778 // Remove success visual state after 1.5 seconds
779 setTimeout(function() {
780 $saveButton.removeClass('accua-saved');
781 $clickedButton.removeClass('accua-saved-active');
782 }, 1500);
783 },
784 error: function() {
785 // Show error state
786 $saveButton.removeClass('accua-saving').prop('disabled', false);
787 $clickedButton.removeClass('accua-saving-active').val(originalText).addClass('accua-error');
788 setTimeout(function() {
789 $clickedButton.removeClass('accua-error');
790 }, 3000);
791 }
792 });
793 },
794 error: function() {
795 // Show error state
796 $saveButton.removeClass('accua-saving').prop('disabled', false);
797 $clickedButton.removeClass('accua-saving-active').val(originalText).addClass('accua-error');
798 setTimeout(function() {
799 $clickedButton.removeClass('accua-error');
800 }, 3000);
801 }
802 });
803
804 }); // end runDraftSaveChain
805 });
806
807 // ========================================================================
808 // Messages Tab: Override Checkboxes Enable/Disable Their Text Input
809 // ========================================================================
810
811 /**
812 * The email override inputs (To, Bcc, Subject, From name, From email) render
813 * disabled until their "Customize" checkbox is checked. The checkbox name
814 * matches its container id (e.g. accua_form_admin_emails_to). Select by
815 * data-tab, not panel #id — tabs.js renames panel IDs at init. Appearance
816 * tab rows need no JS: admin.css :has() rules show/hide those inputs.
817 */
818 $('.accua-tabs__panel[data-tab="messages"] input.accua_form_check_override[type="checkbox"]').on('change', function() {
819 $('#' + this.name).find('.accua_form_value').first().prop('disabled', !this.checked);
820 });
821
822 // ========================================================================
823 // Unsaved Changes Warning (WordPress standard beforeunload pattern)
824 // ========================================================================
825
826 var formDirty = false;
827
828 function markDirty() {
829 formDirty = true;
830 }
831
832 function markClean() {
833 formDirty = false;
834 }
835
836 // Expose globally so form-fields.js (and other scripts) can mark dirty
837 window.accuaFormsEditorDirty = {
838 mark: markDirty,
839 clean: markClean,
840 isDirty: function() { return formDirty; }
841 };
842
843 // Track changes on all form inputs within the edit page
844 $('#accua_forms_edit_page').on('input change', 'input, textarea, select', markDirty);
845
846 // Track color picker changes (wpColorPicker fires irischange on the document)
847 $(document).on('irischange', '#accua_forms_edit_page .wp-picker-container input', markDirty);
848
849 // Note: Field drag/drop/reorder is tracked via window.accuaFormsEditorDirty.mark()
850 // called directly from form-fields.js sortable stop and droppable drop handlers.
851
852 // TinyMCE editors: mark dirty on content change
853 function bindTinyMCEDirty(editor) {
854 editor.on('input change keyup', markDirty);
855 }
856 $(document).on('tinymce-editor-init', function(event, editor) {
857 bindTinyMCEDirty(editor);
858 });
859 // Also bind to any editors already initialized (race condition safety)
860 if (typeof tinyMCE !== 'undefined' && tinyMCE.editors) {
861 $.each(tinyMCE.editors, function(i, editor) {
862 if (editor) { bindTinyMCEDirty(editor); }
863 });
864 }
865
866 // Mark clean after successful save (publish draft)
867 $(document).ajaxComplete(function(event, xhr, settings) {
868 if (settings && settings.data && typeof settings.data === 'string' && settings.data.indexOf('action=accua-publish-form-draft') !== -1) {
869 if (xhr.status === 200) {
870 markClean();
871 }
872 }
873 });
874
875 // Allow intentional form submissions (e.g. delete form) without warning
876 $('#accua_forms_edit_page').on('submit', 'form', markClean);
877
878 // Browser beforeunload warning
879 $(window).on('beforeunload', function() {
880 if (formDirty) {
881 return (typeof accua_forms_i18n !== 'undefined' && accua_forms_i18n.unsaved_changes)
882 ? accua_forms_i18n.unsaved_changes
883 : true;
884 }
885 });
886
887 });
888