PluginProbe
Contact Forms by Cimatti / 2.3.6
Contact Forms by Cimatti v2.3.6
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.6, at assets/js/admin/form-settings.js

835 lines 30.6 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 * Initialize a color picker.
469 *
470 * Opening and closing is left entirely to wpColorPicker. Its open() already
471 * closes every other picker (it fires click.wpcolorpicker on body), so no
472 * custom "only one open at a time" handling is needed here - and any that
473 * hides .iris-picker directly breaks the widget, because both open() and
474 * close() call iris('toggle'), which flips on the element's current
475 * visibility rather than on the widget's own state. Hiding the panel behind
476 * the widget's back therefore inverts the next click.
477 *
478 * The hex input is kept visible at all times for keyboard access; admin.css
479 * overrides the .hidden class WordPress puts on .wp-picker-input-wrap.
480 */
481 function initColorPicker(fieldKey) {
482 var $input = $('#accua_form_' + fieldKey + ' .accua_form_value');
483 if (!$input.length) return;
484
485 $input.wpColorPicker({
486 change: function(event, ui) {
487 // wpColorPicker change event - update preview
488 updatePreviewStylesDebounced();
489 },
490 clear: function() {
491 updatePreviewStylesDebounced();
492 }
493 });
494
495 // Iris registers a one-shot "show the panel on first focus" handler on the
496 // input. It calls iris show() directly, so it opens the panel without
497 // wpColorPicker marking the picker open. In stock WordPress that never
498 // fires, because the hex input only exists while the picker is already
499 // open; here the input is always visible, so a plain click into the field
500 // would desync the widget. Drop it - the swatch button opens the panel,
501 // and typing a hex value still works through Iris's change/keyup listeners.
502 // Iris binds it without a namespace, so this can only be an unqualified
503 // off() and must stay directly after the wpColorPicker() call above, while
504 // that handler is still the only focus handler on the input.
505 $input.off('focus');
506 }
507
508 // Initialize all color pickers
509 $.each(COLOR_FIELDS, function(i, key) {
510 initColorPicker(key);
511 });
512
513 // ========================================================================
514 // Event Listeners - Style Fields
515 // ========================================================================
516
517 /**
518 * Attach event listeners for a style field
519 */
520 function attachStyleFieldListeners(fieldKey, isColorField) {
521 var $container = $('#accua_form_' + fieldKey);
522 if (!$container.length) return;
523
524 // Checkbox toggle - always triggers preview update
525 $container.find('.accua_form_check_override').on('change', function() {
526 updatePreviewStylesDebounced();
527 });
528
529 // Value input - for non-color fields
530 if (!isColorField) {
531 $container.find('.accua_form_value').on('input change', function() {
532 updatePreviewStylesDebounced();
533 });
534 }
535 }
536
537 // Attach listeners to all form style fields
538 $.each(ALL_STYLE_FIELDS.form, function(i, key) {
539 attachStyleFieldListeners(key, COLOR_FIELDS.indexOf(key) !== -1);
540 });
541
542 // Attach listeners to all field style fields
543 $.each(ALL_STYLE_FIELDS.field, function(i, key) {
544 attachStyleFieldListeners(key, COLOR_FIELDS.indexOf(key) !== -1);
545 });
546
547 // Attach listeners to all submit style fields
548 $.each(ALL_STYLE_FIELDS.submit, function(i, key) {
549 attachStyleFieldListeners(key, COLOR_FIELDS.indexOf(key) !== -1);
550 });
551
552 // ========================================================================
553 // Event Listeners - Layout
554 // ========================================================================
555
556 $('#accua_form_layout .accua_form_value').on('change', function() {
557 // Reload preview with new layout - layout changes require HTML rebuild
558 // (different layouts generate different HTML structure, not just CSS)
559 reloadPreviewWithLayout($(this).val());
560 });
561
562 // ========================================================================
563 // Preview Iframe Load Handler
564 // ========================================================================
565
566 function handlePreviewLoaded() {
567 previewReady = true;
568
569 // Remove loading state
570 $('#accua_form_preview_area_wrapper').removeClass('accua-form-preview-loading');
571
572 // Small delay to ensure iframe content is fully rendered
573 setTimeout(function() {
574 updateFullPreview();
575 }, PREVIEW_LOAD_DELAY);
576 }
577
578 $('#accua_form_preview_area').on('load', handlePreviewLoaded);
579
580 // If the server-rendered iframe finished loading before the handler above
581 // was attached, the load event has already fired: without this the preview
582 // never becomes ready and live style updates stay disabled until a reload.
583 if (previewFrameAlreadyLoaded()) {
584 handlePreviewLoaded();
585 }
586
587 // ========================================================================
588 // Save Handler
589 // ========================================================================
590
591 $('.accua_form_save_settings_button').on('click', function(e) {
592 e.preventDefault();
593
594 var $clickedButton = $(this);
595
596 // Store original button text and add saving state with "Saving..." text
597 var originalText = $clickedButton.val();
598 $saveButton.addClass('accua-saving').prop('disabled', true);
599 $clickedButton.val(accua_forms_i18n.saving || 'Saving...').addClass('accua-saving-active');
600
601 var formId = $('#accua_form_save_settings_id').val();
602
603 // Build data object
604 var data = {
605 'action': 'accua-save-form-settings',
606 'form-id': formId,
607 '_nonce_edit_form': $('#_nonce_edit_form').val(),
608 'title': $('#title').val(),
609 'use_ajax': $('#accua_form_use_ajax .accua_form_value').is(':checked') ? 1 : 0
610 };
611
612 // Layout
613 var layout = $('#accua_form_layout .accua_form_value').val();
614 if (layout === 'toplabel' || layout === 'sidebyside' || layout === 'inlinelabel') {
615 data.layout = layout;
616 } else {
617 data.layout = '';
618 }
619
620 // GADS conversion tracking
621 var gads = $('#gads_conversion_code_input').val();
622 if (gads && gads !== 'undefined' && gads !== 'null') {
623 data.gads_conversion_tracking_code = gads;
624 }
625
626 // Collect all override fields
627 var overrideFields = [
628 'success_message', 'error_message',
629 'emails_from_name', 'emails_from', 'admin_emails_to', 'emails_bcc',
630 'admin_emails_subject', 'admin_emails_message',
631 'confirmation_emails_subject', 'confirmation_emails_message'
632 ];
633
634 // Add all style fields
635 $.each(ALL_STYLE_FIELDS.form, function(i, key) { overrideFields.push(key); });
636 $.each(ALL_STYLE_FIELDS.field, function(i, key) { overrideFields.push(key); });
637 $.each(ALL_STYLE_FIELDS.submit, function(i, key) { overrideFields.push(key); });
638
639 $.each(overrideFields, function(i, key) {
640 var $checkbox = $('#accua_form_' + key + ' .accua_form_check_override:checked');
641 var checkValue = $checkbox.val();
642
643 if (checkValue !== undefined && checkValue != 0) {
644 if (checkValue == -1) {
645 // "No message" option
646 data[key] = '';
647 data[key + '_no_message'] = 1;
648 } else {
649 var $element = $('#accua_form_' + key + ' .accua_form_value');
650 if ($element.is('textarea')) {
651 data[key] = getTinyMCEContent('accua_form_' + key);
652 } else {
653 data[key] = $element.val();
654 }
655 }
656 }
657 });
658
659 // Data Retention fields
660 data.submission_retention_override = $('#accua_form_retention_override').is(':checked') ? 1 : 0;
661 data.submission_retention_value = $('#submission_retention_value').val();
662 data.submission_retention_unit = $('#submission_retention_unit').val();
663 data.submission_retention_mode = $('input[name="submission_retention_mode"]:checked').val();
664
665 // The per-widget field saves, the order save and the settings save below
666 // all read-modify-write the same draft transient server-side. They must
667 // run strictly in sequence: fired concurrently they overwrite each
668 // other's draft (lost update - e.g. a renamed title silently reverting).
669 var runDraftSaveChain = function(done) {
670 if (typeof accuaWidgets === 'undefined' || !accuaWidgets) {
671 done();
672 return;
673 }
674 var $widgets = $('#widgets-right div.widget');
675 var saveWidgetAt = function(i) {
676 if (i >= $widgets.length) {
677 if (accuaWidgets.saveOrder) {
678 accuaWidgets.saveOrder(null, function() { done(); });
679 } else {
680 done();
681 }
682 return;
683 }
684 accuaWidgets.save($widgets.eq(i), 0, 1, 0, function() { saveWidgetAt(i + 1); });
685 };
686 saveWidgetAt(0);
687 };
688
689 runDraftSaveChain(function() {
690
691 // Step 1: Save settings to draft
692 $.ajax({
693 url: ajaxurl,
694 type: 'POST',
695 data: data,
696 success: function() {
697 // Step 2: Publish draft to live database
698 $.ajax({
699 url: ajaxurl,
700 type: 'POST',
701 data: {
702 'action': 'accua-publish-form-draft',
703 'form-id': formId,
704 '_nonce_edit_form': $('#_nonce_edit_form').val()
705 },
706 success: function() {
707 // Reload preview to show saved state (include current layout)
708 reloadPreviewWithLayout($('#accua_form_layout .accua_form_value').val());
709
710 // Update URL without reload
711 try {
712 if (history.pushState && window.location.search.indexOf('page=accua_forms_list') === -1) {
713 history.pushState('', document.title, 'admin.php?page=accua_forms_list&fid=' + formId);
714 window.onpopstate = function() { location.reload(); };
715 }
716 } catch (e) {}
717
718 // Restore button text immediately (was cleared to show spinner)
719 $clickedButton.val(originalText);
720
721 // Show success state on button briefly
722 $saveButton.removeClass('accua-saving').addClass('accua-saved').prop('disabled', false);
723 $clickedButton.removeClass('accua-saving-active').addClass('accua-saved-active');
724
725 // Remove success visual state after 1.5 seconds
726 setTimeout(function() {
727 $saveButton.removeClass('accua-saved');
728 $clickedButton.removeClass('accua-saved-active');
729 }, 1500);
730 },
731 error: function() {
732 // Show error state
733 $saveButton.removeClass('accua-saving').prop('disabled', false);
734 $clickedButton.removeClass('accua-saving-active').val(originalText).addClass('accua-error');
735 setTimeout(function() {
736 $clickedButton.removeClass('accua-error');
737 }, 3000);
738 }
739 });
740 },
741 error: function() {
742 // Show error state
743 $saveButton.removeClass('accua-saving').prop('disabled', false);
744 $clickedButton.removeClass('accua-saving-active').val(originalText).addClass('accua-error');
745 setTimeout(function() {
746 $clickedButton.removeClass('accua-error');
747 }, 3000);
748 }
749 });
750
751 }); // end runDraftSaveChain
752 });
753
754 // ========================================================================
755 // Messages Tab: Override Checkboxes Enable/Disable Their Text Input
756 // ========================================================================
757
758 /**
759 * The email override inputs (To, Bcc, Subject, From name, From email) render
760 * disabled until their "Customize" checkbox is checked. The checkbox name
761 * matches its container id (e.g. accua_form_admin_emails_to). Select by
762 * data-tab, not panel #id - tabs.js renames panel IDs at init. Appearance
763 * tab rows need no JS: admin.css :has() rules show/hide those inputs.
764 */
765 $('.accua-tabs__panel[data-tab="messages"] input.accua_form_check_override[type="checkbox"]').on('change', function() {
766 $('#' + this.name).find('.accua_form_value').first().prop('disabled', !this.checked);
767 });
768
769 // ========================================================================
770 // Unsaved Changes Warning (WordPress standard beforeunload pattern)
771 // ========================================================================
772
773 var formDirty = false;
774
775 function markDirty() {
776 formDirty = true;
777 }
778
779 function markClean() {
780 formDirty = false;
781 }
782
783 // Expose globally so form-fields.js (and other scripts) can mark dirty
784 window.accuaFormsEditorDirty = {
785 mark: markDirty,
786 clean: markClean,
787 isDirty: function() { return formDirty; }
788 };
789
790 // Track changes on all form inputs within the edit page
791 $('#accua_forms_edit_page').on('input change', 'input, textarea, select', markDirty);
792
793 // Track color picker changes (wpColorPicker fires irischange on the document)
794 $(document).on('irischange', '#accua_forms_edit_page .wp-picker-container input', markDirty);
795
796 // Note: Field drag/drop/reorder is tracked via window.accuaFormsEditorDirty.mark()
797 // called directly from form-fields.js sortable stop and droppable drop handlers.
798
799 // TinyMCE editors: mark dirty on content change
800 function bindTinyMCEDirty(editor) {
801 editor.on('input change keyup', markDirty);
802 }
803 $(document).on('tinymce-editor-init', function(event, editor) {
804 bindTinyMCEDirty(editor);
805 });
806 // Also bind to any editors already initialized (race condition safety)
807 if (typeof tinyMCE !== 'undefined' && tinyMCE.editors) {
808 $.each(tinyMCE.editors, function(i, editor) {
809 if (editor) { bindTinyMCEDirty(editor); }
810 });
811 }
812
813 // Mark clean after successful save (publish draft)
814 $(document).ajaxComplete(function(event, xhr, settings) {
815 if (settings && settings.data && typeof settings.data === 'string' && settings.data.indexOf('action=accua-publish-form-draft') !== -1) {
816 if (xhr.status === 200) {
817 markClean();
818 }
819 }
820 });
821
822 // Allow intentional form submissions (e.g. delete form) without warning
823 $('#accua_forms_edit_page').on('submit', 'form', markClean);
824
825 // Browser beforeunload warning
826 $(window).on('beforeunload', function() {
827 if (formDirty) {
828 return (typeof accua_forms_i18n !== 'undefined' && accua_forms_i18n.unsaved_changes)
829 ? accua_forms_i18n.unsaved_changes
830 : true;
831 }
832 });
833
834 });
835