# contact-forms/2.3.6/assets/js/admin/fields-page.js

Contact Forms by Cimatti, version 2.3.6. 421 lines.

- Page: https://pluginprobe.com/plugins/contact-forms/2.3.6/code/assets/js/admin/fields-page.js
- Raw: https://pluginprobe.com/plugins/contact-forms/2.3.6/raw/assets/js/admin/fields-page.js
- Modified: 2026-08-21T08:39:40+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/contact-forms/2.3.6/code/assets/js/admin/fields-page.js#L10-L20`.

```javascript
/**
 * Fields admin page - section visibility toggling and client-side validation.
 *
 * @package ContactForms
 * @since 2.2.5
 */
/* global accuaFieldsPage */
jQuery( function( $ ) {
	var typeSelect = $( '#parent' );
	var l10n = accuaFieldsPage.l10n;

	// Types shipped with the plugin. Anything else was registered by an
	// extension plugin through the accua_forms_field_types filter and gets the
	// generic sections the form editor offers for custom types (default value
	// and custom required message).
	var builtinTypes = [ 'textfield', 'textarea', 'email', 'autoreply_email', 'telephone', 'checkbox', 'select', 'radio', 'multiselect', 'multicheckbox', 'post-select', 'post-multicheckbox', 'colorpicker', 'hidden', 'file', 'submit', 'html', 'captcha', 'captcha_v3', 'cap', 'turnstile', 'password', 'password-and-confirm', 'date' ];

	// Built-in field types that support each section. For 'html' the default
	// value holds the HTML content rendered in the form (labels swapped below).
	var hasDefaultValue = [ 'textfield', 'textarea', 'email', 'autoreply_email', 'telephone', 'checkbox', 'select', 'radio', 'multiselect', 'multicheckbox', 'post-select', 'post-multicheckbox', 'colorpicker', 'hidden', 'html', 'password', 'password-and-confirm' ];
	var hasAllowedValues = [ 'select', 'radio', 'multiselect', 'multicheckbox', 'post-select', 'post-multicheckbox', 'file' ];
	var hasDate = [ 'date' ];
	var hasCustomRequired = [ 'textfield', 'textarea', 'email', 'autoreply_email', 'telephone', 'checkbox', 'select', 'radio', 'multiselect', 'multicheckbox', 'post-select', 'post-multicheckbox', 'colorpicker', 'file', 'password', 'password-and-confirm', 'date' ];
	var hasCustomFormat = [ 'email', 'autoreply_email', 'telephone' ];
	// Types that never store a submission value: no submissions-list column
	// can exist for them, so the essential-column option is meaningless there.
	var hasNoSubmissionColumn = [ 'submit', 'html', 'captcha', 'captcha_v3', 'cap', 'turnstile' ];

	function updateVisibility() {
		var type = typeSelect.val();
		var isBuiltin = builtinTypes.indexOf( type ) !== -1;
		// The description is definition metadata shown in the fields list for
		// every type, so it is always left visible/editable.
		$( '#field-section-default-value' ).toggle( ! isBuiltin || hasDefaultValue.indexOf( type ) !== -1 );
		$( '#field-section-allowed-values' ).toggle( hasAllowedValues.indexOf( type ) !== -1 );
		$( '#field-section-date' ).toggle( hasDate.indexOf( type ) !== -1 );
		$( '#field-section-custom-required' ).toggle( ! isBuiltin || hasCustomRequired.indexOf( type ) !== -1 );
		$( '#field-section-custom-format' ).toggle( hasCustomFormat.indexOf( type ) !== -1 );
		$( '#field-section-essential-column' ).toggle( ! isBuiltin || hasNoSubmissionColumn.indexOf( type ) === -1 );

		// The allowed-values textarea doubles as extensions list for file
		// fields and query parameters for post fields - same keys, same
		// wording as the form editor widgets.
		if ( type === 'file' ) {
			$( '#allowed-values-label' ).text( l10n.allowedExtensionsLabel );
			$( '#allowed-values-help' ).text( l10n.allowedExtensionsHelp );
		} else if ( type === 'post-select' || type === 'post-multicheckbox' ) {
			$( '#allowed-values-label' ).text( l10n.queryParamsLabel );
			$( '#allowed-values-help' ).text( l10n.queryParamsHelp );
		} else {
			$( '#allowed-values-label' ).text( l10n.allowedValuesLabel );
			$( '#allowed-values-help' ).text( l10n.allowedValuesHelp );
		}

		// The default-value textarea holds the HTML content for html fields
		if ( type === 'html' ) {
			$( '#default-value-label' ).text( l10n.customHtmlLabel );
			$( '#default-value-help' ).text( l10n.customHtmlHelp );
		} else {
			$( '#default-value-label' ).text( l10n.defaultValueLabel );
			$( '#default-value-help' ).text( l10n.defaultValueHelp );
		}
	}

	typeSelect.on( 'change', updateVisibility );
	updateVisibility();

	// Client-side validation
	var typesNeedingAllowedValues = [ 'select', 'radio', 'multiselect', 'multicheckbox' ];

	$( '#addtag' ).on( 'submit', function( e ) {
		var valid = true;
		var errors = [];

		// Remove previous error highlights and messages
		$( this ).find( '.form-invalid' ).removeClass( 'form-invalid' );
		$( this ).prev( '.field-error-message' ).remove();

		// Validate label
		var $label = $( '#tag-name' );
		if ( $.trim( $label.val() ) === '' ) {
			$label.closest( '.form-field' ).addClass( 'form-invalid' );
			errors.push( l10n.errorLabelRequired );
			valid = false;
		}

		// Validate slug (only when adding)
		var $slug = $( '#tag-slug' );
		if ( ! $slug.prop( 'disabled' ) ) {
			var slugValue = $.trim( $slug.val() );
			if ( slugValue === '' ) {
				$slug.closest( '.form-field' ).addClass( 'form-invalid' );
				errors.push( l10n.errorSlugRequired );
				valid = false;
			} else {
				var liveSlugError = slugError( slugValue );
				if ( liveSlugError !== '' ) {
					$slug.closest( '.form-field' ).addClass( 'form-invalid' );
					errors.push( liveSlugError );
					valid = false;
				}
			}
		}

		// Validate allowed values (when visible and required)
		var type = typeSelect.val();
		if ( typesNeedingAllowedValues.indexOf( type ) !== -1 ) {
			var $allowed = $( '#form-field-allowed-values' );
			if ( $.trim( $allowed.val() ) === '' ) {
				$allowed.closest( '.form-field' ).addClass( 'form-invalid' );
				errors.push( l10n.errorAllowedValuesRequired );
				valid = false;
			}
		}

		if ( ! valid ) {
			e.preventDefault();
			// Show error message
			var errorHtml = '<div class="notice notice-error field-error-message" role="alert"><p>' + errors.join( '</p><p>' ) + '</p></div>';
			$( this ).before( errorHtml );
			// Announce for screen readers
			if ( window.wp && wp.a11y && wp.a11y.speak ) {
				wp.a11y.speak( errors.join( '. ' ), 'assertive' );
			}
			// Focus first invalid field
			var $firstInvalid = $( this ).find( '.form-invalid:first' );
			if ( $firstInvalid.length ) {
				$firstInvalid.find( 'input, textarea, select' ).first().trigger( 'focus' );
			}
		}
	} );

	// Remove error highlight on input
	$( '#addtag' ).on( 'input change', 'input, textarea, select', function() {
		$( this ).closest( '.form-field' ).removeClass( 'form-invalid' );
	} );

	// ------------------------------------------------------------------
	// Slug suggestion: while adding, the slug follows the label until the
	// user edits the slug manually (same idea as the core permalink slug).
	// ------------------------------------------------------------------
	var $slugInput = $( '#tag-slug' );
	var slugTouched = $slugInput.prop( 'disabled' ) || $.trim( $slugInput.val() ) !== '';
	$slugInput.on( 'input', function() {
		// New slugs are lowercase (the server lowercases them on save, like
		// core does with term slugs), so show what will actually be stored
		// instead of letting the input disagree with the result.
		var typed = $( this ).val();
		if ( typed !== typed.toLowerCase() ) {
			var caret = this.selectionStart;
			$( this ).val( typed.toLowerCase() );
			if ( caret !== null && typeof this.setSelectionRange === 'function' ) {
				this.setSelectionRange( caret, caret );
			}
		}
		// A manual edit stops the suggestion, but emptying the slug
		// re-enables it: an empty slug always follows the label again.
		slugTouched = $.trim( $( this ).val() ) !== '';
	} );
	$( '#tag-name' ).on( 'input', function() {
		if ( slugTouched ) {
			return;
		}
		var slug = $( this ).val().toLowerCase()
			.normalize( 'NFD' ).replace( /[\u0300-\u036f]/g, '' ) // fold accents: "à" -> "a"
			.replace( /[^a-z0-9_-]+/g, '-' )
			.replace( /^-+|-+$/g, '' )
			.substring( 0, 70 );
		$slugInput.val( slug );
		validateSlugField(); // a suggested slug can collide with an existing field
	} );

	// ------------------------------------------------------------------
	// Inline slug validation while adding: mirrors the server-side rules
	// and also flags a slug already in use, before anything is submitted.
	// The server-side validation on save stays authoritative.
	// ------------------------------------------------------------------
	var existingSlugs = accuaFieldsPage.existingSlugs || [];
	var $slugFeedback = $( '#slug-feedback' );

	function slugError( slug ) {
		if ( slug === '' ) {
			return ''; // emptiness is reported on submit, not while typing
		}
		if ( ! /^[a-z0-9_-]+$/i.test( slug ) ) {
			return l10n.slugInvalidChars;
		}
		if ( slug.indexOf( '__' ) === 0 ) {
			return l10n.slugDoubleUnderscore;
		}
		if ( slug.length > 70 ) {
			return l10n.slugTooLong;
		}
		if ( existingSlugs.indexOf( slug ) !== -1 ) {
			return l10n.slugExists;
		}
		return '';
	}

	function validateSlugField() {
		if ( $slugInput.prop( 'disabled' ) || ! $slugFeedback.length ) {
			return true;
		}
		var err = slugError( $.trim( $slugInput.val() ) );
		$slugInput.closest( '.form-field' ).toggleClass( 'form-invalid', err !== '' );
		$slugFeedback.text( err ).prop( 'hidden', err === '' );
		return err === '';
	}

	// Delegated on purpose: the generic error-highlight removal above is
	// delegated too and bound first, so this re-applies the highlight after
	// it runs for the same input event.
	$( '#addtag' ).on( 'input', '#tag-slug', validateSlugField );

	// ------------------------------------------------------------------
	// Delete confirmation
	// ------------------------------------------------------------------
	$( '#delete-field' ).on( 'click', function( e ) {
		var slug = $( '#addtag input[name="form-field-id"]' ).val() || '';
		if ( ! window.confirm( l10n.confirmDelete.replace( '%s', slug ) ) ) {
			e.preventDefault();
		}
	} );

	// ------------------------------------------------------------------
	// Live preview: POSTs the current (unsaved) settings to the
	// accua_forms_field_preview AJAX action and writes the returned document
	// into the preview iframe (document.write instead of a real navigation:
	// no browser history entries, frontend CSS + JS still execute). Text
	// input is debounced; selects and date pickers refresh immediately.
	// ------------------------------------------------------------------
	var $previewFrame = $( '#accua-field-preview' );
	var $previewWrapper = $( '#accua-field-preview-wrapper' );
	var $previewPanel = $( '#accua-field-preview-panel' );
	var previewTimer = null;
	var previewXhr = null;
	var previewSettleTimers = [];

	function clearPreviewSettleTimers() {
		previewSettleTimers.forEach( clearTimeout );
		previewSettleTimers = [];
	}

	function previewPayload() {
		var payload = [
			{ name: 'action', value: 'accua_forms_field_preview' },
			{ name: '_wpnonce', value: accuaFieldsPage.previewNonce }
		];
		$( '#addtag' ).serializeArray().forEach( function( pair ) {
			// The editor form's own action and nonces must not leak into the
			// preview request.
			if ( pair.name === 'action' || pair.name.indexOf( '_wpnonce' ) === 0 || pair.name === '_wp_http_referer' ) {
				return;
			}
			payload.push( pair );
		} );
		return payload;
	}

	function resizePreview() {
		try {
			var doc = $previewFrame[ 0 ].contentDocument;
			if ( doc && doc.body ) {
				// Neither documentElement.scrollHeight nor the body box can be
				// trusted: both are stretched to the iframe's own height (the
				// frontend styles give body 100% height), so the preview could
				// grow but never shrink back to fit a small field. Measure the
				// actual content instead - the lowest bottom edge among the
				// body's children. The +16 leaves room for focus outlines.
				var win = doc.defaultView;
				var bottom = 0;
				Array.prototype.forEach.call( doc.body.children, function ( el ) {
					var rect = el.getBoundingClientRect();
					if ( rect.bottom > bottom ) {
						bottom = rect.bottom;
					}
				} );
				bottom += win.pageYOffset || 0;
				var needed = Math.ceil( bottom ) + 16;
				var height = Math.min( 700, Math.max( 80, needed ) );
				$previewFrame.css( 'height', height + 'px' );
				// The document is written with overflow hidden so no scrollbar
				// flashes while the frame animates to this height; scrolling is
				// only given back when the content exceeds the height cap.
				doc.documentElement.style.overflowY = needed > 700 ? 'auto' : '';
			}
		} catch ( e ) {
			// Cross-origin (should not happen) - keep the CSS height.
		}
	}

	var previewObserver = null;

	// Follow every later content change (inline validation errors appearing on
	// blur, async widgets settling) so the frame always fits its content. The
	// body itself is stretched to 100% height, so observe its children (the
	// form, the response-messages div, the empty-preview note).
	function observePreviewBody() {
		try {
			var frameWin = $previewFrame[ 0 ].contentWindow;
			if ( previewObserver ) {
				previewObserver.disconnect();
				previewObserver = null;
			}
			if ( frameWin && frameWin.ResizeObserver && frameWin.document.body ) {
				previewObserver = new frameWin.ResizeObserver( resizePreview );
				Array.prototype.forEach.call( frameWin.document.body.children, function ( el ) {
					previewObserver.observe( el );
				} );
			}
		} catch ( e ) {
			// No observer - the timed re-measures below still apply.
		}
	}

	function refreshPreview() {
		if ( ! $previewFrame.length ) {
			return;
		}
		// Nothing meaningful to preview until the field has a label.
		if ( $.trim( $( '#tag-name' ).val() ) === '' ) {
			$previewPanel.hide();
			return;
		}
		$previewPanel.show();
		$previewWrapper.addClass( 'accua-form-preview-loading' );
		if ( previewXhr ) {
			previewXhr.abort();
		}
		// A superseded refresh must not resize against the document this one
		// is about to replace.
		clearPreviewSettleTimers();
		previewXhr = $.post( accuaFieldsPage.ajaxUrl, $.param( previewPayload() ) )
			.done( function( html ) {
				var doc = $previewFrame[ 0 ].contentDocument;
				// The old document's observed nodes are about to be detached -
				// their final resize must not measure the new, not-yet-styled
				// content.
				if ( previewObserver ) {
					previewObserver.disconnect();
					previewObserver = null;
				}
				// Measuring right after document.write would size the still
				// unstyled content (the frontend stylesheets are only loading),
				// grow the frame, then shrink it back once the CSS applies - a
				// visible jump. Keep the previous height and the loading
				// overlay until the written document (stylesheets included)
				// has loaded, then resize once to the final height. The timer
				// is a safety net in case a subresource hangs and the frame's
				// load event never fires.
				var settled = false;
				var settle = function() {
					if ( settled ) {
						return;
					}
					settled = true;
					observePreviewBody();
					resizePreview();
					$previewWrapper.removeClass( 'accua-form-preview-loading' );
					// Async widgets (post-select options, captcha iframes)
					// settle late - fallback when ResizeObserver is
					// unavailable (it re-fires on those changes anyway).
					previewSettleTimers.push( setTimeout( resizePreview, 600 ) );
					previewSettleTimers.push( setTimeout( resizePreview, 1500 ) );
				};
				// Bound on the frame element (not the frame window, which
				// document.open() resets) and before doc.close(), so the load
				// event cannot be missed; re-bound on every refresh.
				$previewFrame.off( 'load' ).one( 'load', settle );
				doc.open();
				doc.write( html );
				doc.close();
				previewSettleTimers.push( setTimeout( settle, 1500 ) );
			} )
			.always( function( dataOrXhr, status ) {
				if ( status !== 'abort' ) {
					previewXhr = null;
					if ( status !== 'success' ) {
						$previewWrapper.removeClass( 'accua-form-preview-loading' );
					}
				}
			} );
	}

	function schedulePreviewRefresh() {
		clearTimeout( previewTimer );
		previewTimer = setTimeout( refreshPreview, 700 );
	}

	if ( $previewFrame.length ) {
		$( '#addtag' ).on( 'input', 'input, textarea', schedulePreviewRefresh );
		$( '#addtag' ).on( 'change', 'select, input[type="date"]', function() {
			clearTimeout( previewTimer );
			refreshPreview();
		} );
		// Once the editor form is submitted the page is about to navigate:
		// a debounced refresh firing in that window would be wasted work
		// racing the unload (an AJAX render plus a document.write into the
		// iframe of a disappearing page). If the client-side validation
		// blocks the submit instead, the next input event simply schedules
		// a fresh refresh.
		$( '#addtag' ).on( 'submit', function() {
			clearTimeout( previewTimer );
			clearPreviewSettleTimers();
			if ( previewXhr ) {
				previewXhr.abort();
				previewXhr = null;
			}
		} );
		// Hide the panel as soon as the label is cleared (showing again is
		// handled by the debounced refresh).
		$( '#tag-name' ).on( 'input', function() {
			if ( $.trim( $( this ).val() ) === '' ) {
				clearTimeout( previewTimer );
				$previewPanel.hide();
			}
		} );
		refreshPreview();
	}
} );

```
