I’ll build this as a standalone advanced forms chunk for RX Theme, focused on validation, accessibility, UX helpers, spam protection, autosave, AJAX-ready hooks, and safe WordPress-theme integration.
Below is a strong advanced-level file for:
assets/static-js/static-chunks-js/chunk-016-forms.js
It is written as vanilla JavaScript, so it can work inside your RX Theme without React/Vue. It supports normal forms, search forms, comment forms, contact forms, newsletter forms, AJAX forms, accessibility, validation, autosave, password toggle, character counter, file validation, anti-spam honeypot, loading states, and custom events.
/*!
* RX Theme - Forms Advanced Chunk
* File: assets/static-js/static-chunks-js/chunk-016-forms.js
* Theme: RX Theme
* Author: RxHarun
* Description:
* Advanced form enhancement system for WordPress theme forms.
* Includes validation, accessibility, UX helpers, autosave, AJAX-ready hooks,
* password toggle, character counter, file checks, input formatting,
* honeypot protection, and custom events.
*/
(function () {
'use strict';
/**
* Prevent duplicate initialization if script loads more than once.
*/
if (window.RXThemeForms && window.RXThemeForms.__initialized) {
return;
}
/**
* Main namespace.
*/
var RXThemeForms = {
__initialized: false,
version: '1.0.0',
selectors: {
form: 'form',
enhancedForm: '[data-rx-form]',
ajaxForm: '[data-rx-ajax-form="true"]',
validateForm: '[data-rx-validate="true"]',
autoSaveForm: '[data-rx-autosave="true"]',
resetButton: '[data-rx-form-reset]',
submitButton: 'button[type="submit"], input[type="submit"]',
passwordToggle: '[data-rx-password-toggle]',
characterCounter: '[data-rx-counter]',
fileInput: 'input[type="file"][data-rx-file]',
honeypot: '[data-rx-honeypot]',
requiredFields: '[required]',
liveRegion: '[data-rx-form-live]',
clearField: '[data-rx-clear-field]',
copyField: '[data-rx-copy-field]',
revealField: '[data-rx-reveal-field]',
conditionalField: '[data-rx-condition]',
searchForm: 'form[role="search"], .search-form',
commentForm: '#commentform',
newsletterForm: '[data-rx-newsletter-form]'
},
classes: {
initialized: 'rx-form-initialized',
fieldValid: 'rx-field-valid',
fieldInvalid: 'rx-field-invalid',
formLoading: 'rx-form-loading',
formSubmitted: 'rx-form-submitted',
formDirty: 'rx-form-dirty',
formSaved: 'rx-form-saved',
hidden: 'rx-hidden',
visible: 'rx-visible',
errorMessage: 'rx-field-error-message',
successMessage: 'rx-form-success-message',
loadingMessage: 'rx-form-loading-message'
},
messages: {
required: 'This field is required.',
email: 'Please enter a valid email address.',
url: 'Please enter a valid URL.',
tel: 'Please enter a valid phone number.',
number: 'Please enter a valid number.',
min: 'Value is too small.',
max: 'Value is too large.',
minLength: 'Please enter more characters.',
maxLength: 'Please enter fewer characters.',
pattern: 'Please match the requested format.',
passwordWeak: 'Password is too weak.',
fileTooLarge: 'The selected file is too large.',
fileType: 'This file type is not allowed.',
mismatch: 'The values do not match.',
success: 'Form submitted successfully.',
error: 'Please check the form and try again.',
saving: 'Saving...',
saved: 'Saved.',
copied: 'Copied.'
},
settings: {
validateOnInput: true,
validateOnBlur: true,
scrollToError: true,
focusFirstError: true,
autosaveDelay: 700,
ajaxTimeout: 20000,
maxFileSizeMB: 5,
allowedFileTypes: [
'image/jpeg',
'image/png',
'image/webp',
'image/gif',
'application/pdf',
'text/plain'
],
phonePattern: /^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/,
emailPattern: /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/,
urlPattern: /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?$/i
},
timers: {},
autosaveCache: {},
init: function () {
if (this.__initialized) {
return;
}
this.__initialized = true;
this.bindGlobalEvents();
this.prepareForms();
this.preparePasswordToggles();
this.prepareCharacterCounters();
this.prepareFileInputs();
this.prepareClearButtons();
this.prepareCopyButtons();
this.prepareConditionalFields();
this.prepareSearchForms();
this.prepareCommentForm();
this.prepareNewsletterForms();
this.restoreAutosavedForms();
this.dispatch('rxFormsReady', {
version: this.version
});
},
bindGlobalEvents: function () {
var self = this;
document.addEventListener('submit', function (event) {
var form = event.target;
if (!self.isForm(form)) {
return;
}
self.handleFormSubmit(event, form);
}, true);
document.addEventListener('input', function (event) {
var field = event.target;
if (!self.isField(field)) {
return;
}
var form = field.form;
if (form) {
self.markFormDirty(form);
if (self.settings.validateOnInput && self.shouldValidateForm(form)) {
self.validateField(field, false);
}
if (self.isAutosaveForm(form)) {
self.queueAutosave(form);
}
}
self.handleLiveFormatting(field);
self.updateCharacterCounter(field);
}, true);
document.addEventListener('blur', function (event) {
var field = event.target;
if (!self.isField(field)) {
return;
}
var form = field.form;
if (form && self.settings.validateOnBlur && self.shouldValidateForm(form)) {
self.validateField(field, true);
}
}, true);
document.addEventListener('change', function (event) {
var field = event.target;
if (!self.isField(field)) {
return;
}
var form = field.form;
if (form) {
self.markFormDirty(form);
if (self.shouldValidateForm(form)) {
self.validateField(field, true);
}
if (self.isAutosaveForm(form)) {
self.queueAutosave(form);
}
}
if (field.matches(self.selectors.fileInput)) {
self.validateFileInput(field);
}
self.prepareConditionalFields();
}, true);
document.addEventListener('click', function (event) {
var passwordToggle = event.target.closest(self.selectors.passwordToggle);
var resetButton = event.target.closest(self.selectors.resetButton);
var clearField = event.target.closest(self.selectors.clearField);
var copyField = event.target.closest(self.selectors.copyField);
if (passwordToggle) {
event.preventDefault();
self.togglePassword(passwordToggle);
}
if (resetButton) {
self.handleResetButton(event, resetButton);
}
if (clearField) {
event.preventDefault();
self.clearTargetField(clearField);
}
if (copyField) {
event.preventDefault();
self.copyTargetField(copyField);
}
}, true);
window.addEventListener('pageshow', function () {
self.resetLoadingForms();
});
window.addEventListener('beforeunload', function (event) {
var dirtyForms = document.querySelectorAll('form.' + self.classes.formDirty + '[data-rx-warn-unsaved="true"]');
if (dirtyForms.length > 0) {
event.preventDefault();
event.returnValue = '';
}
});
},
prepareForms: function () {
var forms = document.querySelectorAll(this.selectors.form);
for (var i = 0; i < forms.length; i++) {
this.prepareForm(forms[i]);
}
},
prepareForm: function (form) {
if (!this.isForm(form)) {
return;
}
if (form.classList.contains(this.classes.initialized)) {
return;
}
form.classList.add(this.classes.initialized);
this.ensureLiveRegion(form);
this.prepareRequiredLabels(form);
this.prepareAriaDescriptions(form);
this.prepareSubmitButtonState(form);
if (this.shouldValidateForm(form)) {
form.setAttribute('novalidate', 'novalidate');
}
this.dispatch('rxFormPrepared', {
form: form
});
},
prepareRequiredLabels: function (form) {
var fields = form.querySelectorAll(this.selectors.requiredFields);
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
var label = this.getFieldLabel(field);
if (!label) {
continue;
}
if (label.querySelector('.rx-required-mark')) {
continue;
}
var mark = document.createElement('span');
mark.className = 'rx-required-mark';
mark.setAttribute('aria-hidden', 'true');
mark.textContent = ' *';
label.appendChild(mark);
}
},
prepareAriaDescriptions: function (form) {
var fields = form.querySelectorAll('input, select, textarea');
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
if (!field.id) {
field.id = this.uniqueId('rx-field');
}
var help = form.querySelector('[data-rx-help-for="' + field.id + '"]');
if (help && !help.id) {
help.id = this.uniqueId('rx-help');
}
if (help) {
this.addAriaDescribedBy(field, help.id);
}
}
},
prepareSubmitButtonState: function (form) {
var buttons = form.querySelectorAll(this.selectors.submitButton);
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
if (!button.dataset.rxOriginalText) {
button.dataset.rxOriginalText = button.value || button.textContent || 'Submit';
}
}
},
preparePasswordToggles: function () {
var toggles = document.querySelectorAll(this.selectors.passwordToggle);
for (var i = 0; i < toggles.length; i++) {
var toggle = toggles[i];
if (toggle.dataset.rxPasswordReady === 'true') {
continue;
}
toggle.dataset.rxPasswordReady = 'true';
toggle.setAttribute('aria-pressed', 'false');
if (!toggle.getAttribute('aria-label')) {
toggle.setAttribute('aria-label', 'Show password');
}
}
},
prepareCharacterCounters: function () {
var counters = document.querySelectorAll(this.selectors.characterCounter);
for (var i = 0; i < counters.length; i++) {
var counter = counters[i];
var targetSelector = counter.getAttribute('data-rx-counter');
if (!targetSelector) {
continue;
}
var field = document.querySelector(targetSelector);
if (!field) {
continue;
}
if (!counter.id) {
counter.id = this.uniqueId('rx-counter');
}
this.addAriaDescribedBy(field, counter.id);
this.updateCharacterCounter(field);
}
},
prepareFileInputs: function () {
var inputs = document.querySelectorAll(this.selectors.fileInput);
for (var i = 0; i < inputs.length; i++) {
var input = inputs[i];
if (input.dataset.rxFileReady === 'true') {
continue;
}
input.dataset.rxFileReady = 'true';
if (!input.getAttribute('data-rx-max-size')) {
input.setAttribute('data-rx-max-size', String(this.settings.maxFileSizeMB));
}
if (!input.getAttribute('data-rx-allowed-types')) {
input.setAttribute('data-rx-allowed-types', this.settings.allowedFileTypes.join(','));
}
}
},
prepareClearButtons: function () {
var buttons = document.querySelectorAll(this.selectors.clearField);
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
if (!button.getAttribute('aria-label')) {
button.setAttribute('aria-label', 'Clear field');
}
}
},
prepareCopyButtons: function () {
var buttons = document.querySelectorAll(this.selectors.copyField);
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
if (!button.getAttribute('aria-label')) {
button.setAttribute('aria-label', 'Copy field value');
}
}
},
prepareConditionalFields: function () {
var conditionalItems = document.querySelectorAll(this.selectors.conditionalField);
for (var i = 0; i < conditionalItems.length; i++) {
this.handleConditionalItem(conditionalItems[i]);
}
},
prepareSearchForms: function () {
var forms = document.querySelectorAll(this.selectors.searchForm);
for (var i = 0; i < forms.length; i++) {
var form = forms[i];
if (form.dataset.rxSearchReady === 'true') {
continue;
}
form.dataset.rxSearchReady = 'true';
var input = form.querySelector('input[type="search"], input[name="s"]');
if (!input) {
continue;
}
input.setAttribute('autocomplete', 'off');
if (!input.getAttribute('aria-label') && !this.getFieldLabel(input)) {
input.setAttribute('aria-label', 'Search');
}
}
},
prepareCommentForm: function () {
var form = document.querySelector(this.selectors.commentForm);
if (!form || form.dataset.rxCommentReady === 'true') {
return;
}
form.dataset.rxCommentReady = 'true';
form.setAttribute('data-rx-validate', 'true');
var comment = form.querySelector('#comment');
if (comment && !comment.getAttribute('maxlength')) {
comment.setAttribute('maxlength', '5000');
}
},
prepareNewsletterForms: function () {
var forms = document.querySelectorAll(this.selectors.newsletterForm);
for (var i = 0; i < forms.length; i++) {
var form = forms[i];
if (form.dataset.rxNewsletterReady === 'true') {
continue;
}
form.dataset.rxNewsletterReady = 'true';
form.setAttribute('data-rx-validate', 'true');
var email = form.querySelector('input[type="email"]');
if (email) {
email.setAttribute('autocomplete', 'email');
email.setAttribute('inputmode', 'email');
}
}
},
handleFormSubmit: function (event, form) {
this.prepareForm(form);
if (this.isSpamSubmission(form)) {
event.preventDefault();
this.announce(form, this.messages.error);
return false;
}
if (this.shouldValidateForm(form)) {
var isValid = this.validateForm(form);
if (!isValid) {
event.preventDefault();
event.stopPropagation();
this.announce(form, this.messages.error);
if (this.settings.scrollToError) {
this.scrollToFirstError(form);
}
if (this.settings.focusFirstError) {
this.focusFirstError(form);
}
this.dispatch('rxFormValidationFailed', {
form: form
});
return false;
}
}
if (this.isAjaxForm(form)) {
event.preventDefault();
this.submitAjaxForm(form);
return false;
}
this.setFormLoading(form, true);
this.dispatch('rxFormBeforeSubmit', {
form: form
});
return true;
},
validateForm: function (form) {
var fields = form.querySelectorAll('input, select, textarea');
var isValid = true;
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
if (!this.shouldValidateField(field)) {
continue;
}
var fieldValid = this.validateField(field, true);
if (!fieldValid) {
isValid = false;
}
}
return isValid;
},
validateField: function (field, showMessage) {
if (!this.shouldValidateField(field)) {
return true;
}
var result = this.getFieldValidationResult(field);
if (result.valid) {
this.setFieldValid(field);
} else {
this.setFieldInvalid(field, result.message, showMessage);
}
this.dispatch('rxFieldValidated', {
field: field,
valid: result.valid,
message: result.message
});
return result.valid;
},
getFieldValidationResult: function (field) {
var value = this.getFieldValue(field);
var type = (field.getAttribute('type') || '').toLowerCase();
var tag = field.tagName.toLowerCase();
if (field.disabled || field.readOnly) {
return this.validResult();
}
if (field.required && !this.hasValue(field)) {
return this.invalidResult(this.getMessage(field, 'required'));
}
if (!this.hasValue(field)) {
return this.validResult();
}
if (type === 'email' && !this.settings.emailPattern.test(value)) {
return this.invalidResult(this.getMessage(field, 'email'));
}
if (type === 'url' && !this.settings.urlPattern.test(value)) {
return this.invalidResult(this.getMessage(field, 'url'));
}
if (type === 'tel' && !this.settings.phonePattern.test(value)) {
return this.invalidResult(this.getMessage(field, 'tel'));
}
if (type === 'number' && isNaN(Number(value))) {
return this.invalidResult(this.getMessage(field, 'number'));
}
if (field.min && type === 'number' && Number(value) < Number(field.min)) {
return this.invalidResult(this.getMessage(field, 'min'));
}
if (field.max && type === 'number' && Number(value) > Number(field.max)) {
return this.invalidResult(this.getMessage(field, 'max'));
}
if (field.minLength > 0 && value.length < field.minLength) {
return this.invalidResult(this.getMessage(field, 'minLength'));
}
if (field.maxLength > 0 && value.length > field.maxLength) {
return this.invalidResult(this.getMessage(field, 'maxLength'));
}
if (field.pattern) {
try {
var pattern = new RegExp('^(?:' + field.pattern + ')$');
if (!pattern.test(value)) {
return this.invalidResult(this.getMessage(field, 'pattern'));
}
} catch (error) {
console.warn('RX Theme Forms: Invalid pattern:', field.pattern);
}
}
if (type === 'password' && field.getAttribute('data-rx-password-strength') === 'true') {
if (this.getPasswordScore(value) < 3) {
return this.invalidResult(this.getMessage(field, 'passwordWeak'));
}
}
var matchSelector = field.getAttribute('data-rx-match');
if (matchSelector) {
var matchField = document.querySelector(matchSelector);
if (matchField && value !== this.getFieldValue(matchField)) {
return this.invalidResult(this.getMessage(field, 'mismatch'));
}
}
if (type === 'file') {
var fileResult = this.getFileValidationResult(field);
if (!fileResult.valid) {
return fileResult;
}
}
if (tag === 'select' && field.required && field.selectedIndex < 0) {
return this.invalidResult(this.getMessage(field, 'required'));
}
return this.validResult();
},
validResult: function () {
return {
valid: true,
message: ''
};
},
invalidResult: function (message) {
return {
valid: false,
message: message || this.messages.error
};
},
setFieldValid: function (field) {
field.classList.remove(this.classes.fieldInvalid);
field.classList.add(this.classes.fieldValid);
field.setAttribute('aria-invalid', 'false');
this.removeFieldError(field);
},
setFieldInvalid: function (field, message, showMessage) {
field.classList.remove(this.classes.fieldValid);
field.classList.add(this.classes.fieldInvalid);
field.setAttribute('aria-invalid', 'true');
if (showMessage) {
this.showFieldError(field, message);
}
},
showFieldError: function (field, message) {
var error = this.getFieldError(field);
if (!error) {
error = document.createElement('div');
error.className = this.classes.errorMessage;
error.setAttribute('role', 'alert');
if (!error.id) {
error.id = this.uniqueId('rx-error');
}
field.insertAdjacentElement('afterend', error);
}
error.textContent = message;
this.addAriaDescribedBy(field, error.id);
},
removeFieldError: function (field) {
var error = this.getFieldError(field);
if (error) {
error.remove();
}
},
getFieldError: function (field) {
var next = field.nextElementSibling;
if (next && next.classList.contains(this.classes.errorMessage)) {
return next;
}
if (field.getAttribute('aria-describedby')) {
var ids = field.getAttribute('aria-describedby').split(/\s+/);
for (var i = 0; i < ids.length; i++) {
var item = document.getElementById(ids[i]);
if (item && item.classList.contains(this.classes.errorMessage)) {
return item;
}
}
}
return null;
},
getFileValidationResult: function (field) {
if (!field.files || field.files.length === 0) {
return this.validResult();
}
var maxSizeMB = Number(field.getAttribute('data-rx-max-size') || this.settings.maxFileSizeMB);
var maxSizeBytes = maxSizeMB * 1024 * 1024;
var allowedTypesRaw = field.getAttribute('data-rx-allowed-types') || this.settings.allowedFileTypes.join(',');
var allowedTypes = allowedTypesRaw.split(',').map(function (type) {
return type.trim();
}).filter(Boolean);
for (var i = 0; i < field.files.length; i++) {
var file = field.files[i];
if (file.size > maxSizeBytes) {
return this.invalidResult(this.getMessage(field, 'fileTooLarge'));
}
if (allowedTypes.length > 0 && allowedTypes.indexOf(file.type) === -1) {
return this.invalidResult(this.getMessage(field, 'fileType'));
}
}
return this.validResult();
},
validateFileInput: function (field) {
var result = this.getFileValidationResult(field);
if (result.valid) {
this.setFieldValid(field);
} else {
this.setFieldInvalid(field, result.message, true);
}
this.renderFileList(field);
},
renderFileList: function (field) {
if (field.getAttribute('data-rx-file-list') !== 'true') {
return;
}
var listId = field.id + '-file-list';
var list = document.getElementById(listId);
if (!list) {
list = document.createElement('ul');
list.id = listId;
list.className = 'rx-file-list';
field.insertAdjacentElement('afterend', list);
}
list.innerHTML = '';
if (!field.files || field.files.length === 0) {
return;
}
for (var i = 0; i < field.files.length; i++) {
var file = field.files[i];
var item = document.createElement('li');
item.textContent = file.name + ' — ' + this.formatBytes(file.size);
list.appendChild(item);
}
},
submitAjaxForm: function (form) {
var self = this;
var action = form.getAttribute('action') || window.location.href;
var method = (form.getAttribute('method') || 'POST').toUpperCase();
var formData = new FormData(form);
var controller = null;
var timeoutId = null;
if (window.AbortController) {
controller = new AbortController();
timeoutId = window.setTimeout(function () {
controller.abort();
}, this.settings.ajaxTimeout);
}
this.setFormLoading(form, true);
this.announce(form, form.getAttribute('data-rx-loading-message') || 'Submitting...');
this.dispatch('rxFormAjaxBeforeSubmit', {
form: form,
formData: formData
});
fetch(action, {
method: method,
body: method === 'GET' ? null : formData,
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
signal: controller ? controller.signal : undefined
})
.then(function (response) {
if (!response.ok) {
throw new Error('Form request failed.');
}
var contentType = response.headers.get('content-type') || '';
if (contentType.indexOf('application/json') !== -1) {
return response.json();
}
return response.text();
})
.then(function (data) {
self.setFormLoading(form, false);
self.markFormClean(form);
form.classList.add(self.classes.formSubmitted);
var successMessage = form.getAttribute('data-rx-success-message') || self.messages.success;
self.showFormSuccess(form, successMessage);
self.announce(form, successMessage);
if (form.getAttribute('data-rx-reset-after-submit') === 'true') {
form.reset();
}
self.clearAutosave(form);
self.dispatch('rxFormAjaxSuccess', {
form: form,
response: data
});
})
.catch(function (error) {
self.setFormLoading(form, false);
var message = form.getAttribute('data-rx-error-message') || self.messages.error;
if (error && error.name === 'AbortError') {
message = 'Request timed out. Please try again.';
}
self.showFormError(form, message);
self.announce(form, message);
self.dispatch('rxFormAjaxError', {
form: form,
error: error
});
})
.finally(function () {
if (timeoutId) {
window.clearTimeout(timeoutId);
}
});
},
setFormLoading: function (form, loading) {
var buttons = form.querySelectorAll(this.selectors.submitButton);
if (loading) {
form.classList.add(this.classes.formLoading);
form.setAttribute('aria-busy', 'true');
} else {
form.classList.remove(this.classes.formLoading);
form.setAttribute('aria-busy', 'false');
}
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
var original = button.dataset.rxOriginalText || button.value || button.textContent || 'Submit';
var loadingText = form.getAttribute('data-rx-submit-loading') || 'Submitting...';
button.disabled = loading;
if (button.tagName.toLowerCase() === 'input') {
button.value = loading ? loadingText : original;
} else {
button.textContent = loading ? loadingText : original;
}
}
},
resetLoadingForms: function () {
var forms = document.querySelectorAll('form.' + this.classes.formLoading);
for (var i = 0; i < forms.length; i++) {
this.setFormLoading(forms[i], false);
}
},
showFormSuccess: function (form, message) {
this.removeFormMessages(form);
var box = document.createElement('div');
box.className = this.classes.successMessage;
box.setAttribute('role', 'status');
box.textContent = message;
form.insertAdjacentElement('afterbegin', box);
},
showFormError: function (form, message) {
this.removeFormMessages(form);
var box = document.createElement('div');
box.className = this.classes.errorMessage;
box.setAttribute('role', 'alert');
box.textContent = message;
form.insertAdjacentElement('afterbegin', box);
},
removeFormMessages: function (form) {
var messages = form.querySelectorAll(
'.' + this.classes.successMessage + ', .' + this.classes.errorMessage + ':not(input):not(textarea):not(select)'
);
for (var i = 0; i < messages.length; i++) {
if (!messages[i].matches('input, textarea, select')) {
messages[i].remove();
}
}
},
ensureLiveRegion: function (form) {
var region = form.querySelector(this.selectors.liveRegion);
if (region) {
return region;
}
region = document.createElement('div');
region.className = 'rx-form-live-region screen-reader-text';
region.setAttribute('data-rx-form-live', 'true');
region.setAttribute('aria-live', 'polite');
region.setAttribute('aria-atomic', 'true');
form.appendChild(region);
return region;
},
announce: function (form, message) {
var region = form.querySelector(this.selectors.liveRegion) || this.ensureLiveRegion(form);
region.textContent = '';
window.setTimeout(function () {
region.textContent = message;
}, 30);
},
togglePassword: function (button) {
var selector = button.getAttribute('data-rx-password-toggle');
var field = selector ? document.querySelector(selector) : null;
if (!field) {
var wrapper = button.closest('.rx-password-field, .password-field, .form-field');
if (wrapper) {
field = wrapper.querySelector('input[type="password"], input[type="text"]');
}
}
if (!field) {
return;
}
var isPassword = field.getAttribute('type') === 'password';
field.setAttribute('type', isPassword ? 'text' : 'password');
button.setAttribute('aria-pressed', isPassword ? 'true' : 'false');
button.setAttribute('aria-label', isPassword ? 'Hide password' : 'Show password');
var showText = button.getAttribute('data-rx-show-text') || 'Show';
var hideText = button.getAttribute('data-rx-hide-text') || 'Hide';
if (button.getAttribute('data-rx-update-text') === 'true') {
button.textContent = isPassword ? hideText : showText;
}
},
updateCharacterCounter: function (field) {
if (!field.id) {
return;
}
var counter = document.querySelector('[data-rx-counter="#' + field.id + '"]');
if (!counter) {
return;
}
var value = this.getFieldValue(field);
var length = value.length;
var max = field.getAttribute('maxlength');
var min = field.getAttribute('minlength');
if (max) {
counter.textContent = length + ' / ' + max;
} else if (min) {
counter.textContent = length + ' characters. Minimum ' + min + '.';
} else {
counter.textContent = length + ' characters';
}
},
clearTargetField: function (button) {
var selector = button.getAttribute('data-rx-clear-field');
var field = selector ? document.querySelector(selector) : null;
if (!field) {
return;
}
field.value = '';
field.dispatchEvent(new Event('input', { bubbles: true }));
field.focus();
},
copyTargetField: function (button) {
var selector = button.getAttribute('data-rx-copy-field');
var field = selector ? document.querySelector(selector) : null;
if (!field) {
return;
}
var value = this.getFieldValue(field);
var self = this;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(value).then(function () {
self.afterCopy(button);
});
} else {
field.select();
try {
document.execCommand('copy');
self.afterCopy(button);
} catch (error) {
console.warn('RX Theme Forms: Copy failed.', error);
}
}
},
afterCopy: function (button) {
var original = button.dataset.rxOriginalText || button.textContent;
button.dataset.rxOriginalText = original;
button.textContent = button.getAttribute('data-rx-copied-text') || this.messages.copied;
window.setTimeout(function () {
button.textContent = original;
}, 1400);
},
handleConditionalItem: function (item) {
var raw = item.getAttribute('data-rx-condition');
if (!raw) {
return;
}
var config = this.parseCondition(raw);
if (!config || !config.field) {
return;
}
var field = document.querySelector(config.field);
if (!field) {
return;
}
var fieldValue = this.getFieldValue(field);
var shouldShow = this.evaluateCondition(fieldValue, config);
item.hidden = !shouldShow;
item.classList.toggle(this.classes.hidden, !shouldShow);
item.classList.toggle(this.classes.visible, shouldShow);
var inputs = item.querySelectorAll('input, select, textarea, button');
for (var i = 0; i < inputs.length; i++) {
inputs[i].disabled = !shouldShow;
}
},
parseCondition: function (raw) {
try {
return JSON.parse(raw);
} catch (error) {
var parts = raw.split(':');
if (parts.length >= 2) {
return {
field: parts[0],
equals: parts.slice(1).join(':')
};
}
return null;
}
},
evaluateCondition: function (value, config) {
if (Object.prototype.hasOwnProperty.call(config, 'equals')) {
return String(value) === String(config.equals);
}
if (Object.prototype.hasOwnProperty.call(config, 'not')) {
return String(value) !== String(config.not);
}
if (Object.prototype.hasOwnProperty.call(config, 'contains')) {
return String(value).indexOf(String(config.contains)) !== -1;
}
if (Object.prototype.hasOwnProperty.call(config, 'empty')) {
return config.empty ? !value : !!value;
}
return !!value;
},
handleResetButton: function (event, button) {
var form = button.closest('form');
if (!form) {
return;
}
if (button.getAttribute('data-rx-confirm-reset') === 'true') {
var message = button.getAttribute('data-rx-confirm-message') || 'Clear this form?';
if (!window.confirm(message)) {
event.preventDefault();
return;
}
}
window.setTimeout(function () {
RXThemeForms.clearFormValidation(form);
RXThemeForms.markFormClean(form);
}, 0);
},
clearFormValidation: function (form) {
var fields = form.querySelectorAll('input, select, textarea');
for (var i = 0; i < fields.length; i++) {
fields[i].classList.remove(this.classes.fieldValid, this.classes.fieldInvalid);
fields[i].removeAttribute('aria-invalid');
this.removeFieldError(fields[i]);
}
this.removeFormMessages(form);
},
handleLiveFormatting: function (field) {
var format = field.getAttribute('data-rx-format');
if (!format) {
return;
}
var caret = field.selectionStart;
var originalLength = field.value.length;
if (format === 'lowercase') {
field.value = field.value.toLowerCase();
}
if (format === 'uppercase') {
field.value = field.value.toUpperCase();
}
if (format === 'slug') {
field.value = this.toSlug(field.value);
}
if (format === 'digits') {
field.value = field.value.replace(/[^\d]/g, '');
}
if (format === 'trim-start') {
field.value = field.value.replace(/^\s+/, '');
}
var newLength = field.value.length;
if (document.activeElement === field && typeof caret === 'number') {
var diff = newLength - originalLength;
field.setSelectionRange(Math.max(0, caret + diff), Math.max(0, caret + diff));
}
},
queueAutosave: function (form) {
var id = this.getFormStorageId(form);
var self = this;
if (!id) {
return;
}
if (this.timers[id]) {
window.clearTimeout(this.timers[id]);
}
this.timers[id] = window.setTimeout(function () {
self.saveForm(form);
}, this.settings.autosaveDelay);
},
saveForm: function (form) {
var id = this.getFormStorageId(form);
if (!id || !window.localStorage) {
return;
}
var data = this.serializeForm(form);
try {
window.localStorage.setItem(id, JSON.stringify(data));
form.classList.add(this.classes.formSaved);
this.announce(form, this.messages.saved);
this.dispatch('rxFormAutosaved', {
form: form,
data: data
});
} catch (error) {
console.warn('RX Theme Forms: Autosave failed.', error);
}
},
restoreAutosavedForms: function () {
var forms = document.querySelectorAll(this.selectors.autoSaveForm);
for (var i = 0; i < forms.length; i++) {
this.restoreForm(forms[i]);
}
},
restoreForm: function (form) {
var id = this.getFormStorageId(form);
if (!id || !window.localStorage) {
return;
}
var raw = window.localStorage.getItem(id);
if (!raw) {
return;
}
try {
var data = JSON.parse(raw);
this.fillForm(form, data);
this.dispatch('rxFormAutosaveRestored', {
form: form,
data: data
});
} catch (error) {
console.warn('RX Theme Forms: Autosave restore failed.', error);
}
},
clearAutosave: function (form) {
var id = this.getFormStorageId(form);
if (!id || !window.localStorage) {
return;
}
window.localStorage.removeItem(id);
},
serializeForm: function (form) {
var data = {};
var fields = form.querySelectorAll('input, select, textarea');
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
if (!field.name || field.type === 'password' || field.type === 'file') {
continue;
}
if (field.type === 'checkbox') {
data[field.name] = field.checked;
continue;
}
if (field.type === 'radio') {
if (field.checked) {
data[field.name] = field.value;
}
continue;
}
if (field.tagName.toLowerCase() === 'select' && field.multiple) {
data[field.name] = Array.prototype.slice.call(field.options)
.filter(function (option) {
return option.selected;
})
.map(function (option) {
return option.value;
});
continue;
}
data[field.name] = field.value;
}
return data;
},
fillForm: function (form, data) {
var fields = form.querySelectorAll('input, select, textarea');
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
if (!field.name || !Object.prototype.hasOwnProperty.call(data, field.name)) {
continue;
}
var value = data[field.name];
if (field.type === 'checkbox') {
field.checked = !!value;
continue;
}
if (field.type === 'radio') {
field.checked = field.value === value;
continue;
}
if (field.tagName.toLowerCase() === 'select' && field.multiple && Array.isArray(value)) {
for (var j = 0; j < field.options.length; j++) {
field.options[j].selected = value.indexOf(field.options[j].value) !== -1;
}
continue;
}
field.value = value;
}
},
getFormStorageId: function (form) {
var explicit = form.getAttribute('data-rx-autosave-id');
if (explicit) {
return 'rx-theme-form-' + explicit;
}
if (form.id) {
return 'rx-theme-form-' + form.id;
}
var action = form.getAttribute('action') || window.location.pathname;
return 'rx-theme-form-' + this.toSlug(action);
},
isSpamSubmission: function (form) {
var honeypots = form.querySelectorAll(this.selectors.honeypot);
for (var i = 0; i < honeypots.length; i++) {
if (honeypots[i].value.trim() !== '') {
return true;
}
}
var startedAt = form.getAttribute('data-rx-started-at');
if (!startedAt) {
form.setAttribute('data-rx-started-at', String(Date.now()));
return false;
}
var minTime = Number(form.getAttribute('data-rx-min-time') || 0);
if (minTime > 0 && Date.now() - Number(startedAt) < minTime) {
return true;
}
return false;
},
markFormDirty: function (form) {
form.classList.add(this.classes.formDirty);
form.classList.remove(this.classes.formSaved);
},
markFormClean: function (form) {
form.classList.remove(this.classes.formDirty);
},
scrollToFirstError: function (form) {
var first = form.querySelector('.' + this.classes.fieldInvalid);
if (!first) {
return;
}
var offset = Number(form.getAttribute('data-rx-scroll-offset') || 90);
var rect = first.getBoundingClientRect();
var top = rect.top + window.pageYOffset - offset;
window.scrollTo({
top: top,
behavior: 'smooth'
});
},
focusFirstError: function (form) {
var first = form.querySelector('.' + this.classes.fieldInvalid);
if (first && typeof first.focus === 'function') {
window.setTimeout(function () {
first.focus();
}, 250);
}
},
getPasswordScore: function (password) {
var score = 0;
if (!password) {
return score;
}
if (password.length >= 8) {
score++;
}
if (password.length >= 12) {
score++;
}
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) {
score++;
}
if (/\d/.test(password)) {
score++;
}
if (/[^A-Za-z0-9]/.test(password)) {
score++;
}
return score;
},
getFieldValue: function (field) {
if (!field) {
return '';
}
if (field.type === 'checkbox') {
return field.checked ? field.value || '1' : '';
}
if (field.type === 'radio') {
var checked = field.form
? field.form.querySelector('input[name="' + CSS.escape(field.name) + '"]:checked')
: document.querySelector('input[name="' + CSS.escape(field.name) + '"]:checked');
return checked ? checked.value : '';
}
return String(field.value || '').trim();
},
hasValue: function (field) {
if (field.type === 'checkbox') {
return field.checked;
}
if (field.type === 'radio') {
return !!this.getFieldValue(field);
}
if (field.type === 'file') {
return field.files && field.files.length > 0;
}
return this.getFieldValue(field) !== '';
},
getFieldLabel: function (field) {
if (!field) {
return null;
}
if (field.id) {
var label = document.querySelector('label[for="' + field.id + '"]');
if (label) {
return label;
}
}
return field.closest('label');
},
addAriaDescribedBy: function (field, id) {
if (!field || !id) {
return;
}
var current = field.getAttribute('aria-describedby') || '';
var ids = current.split(/\s+/).filter(Boolean);
if (ids.indexOf(id) === -1) {
ids.push(id);
}
field.setAttribute('aria-describedby', ids.join(' '));
},
getMessage: function (field, key) {
var attr = 'data-rx-message-' + key.replace(/[A-Z]/g, function (letter) {
return '-' + letter.toLowerCase();
});
return field.getAttribute(attr) || this.messages[key] || this.messages.error;
},
isForm: function (element) {
return element && element.tagName && element.tagName.toLowerCase() === 'form';
},
isField: function (element) {
if (!element || !element.tagName) {
return false;
}
var tag = element.tagName.toLowerCase();
return tag === 'input' || tag === 'select' || tag === 'textarea';
},
shouldValidateForm: function (form) {
return form.matches(this.selectors.validateForm) ||
form.matches(this.selectors.commentForm) ||
form.matches(this.selectors.newsletterForm) ||
form.hasAttribute('data-rx-form');
},
shouldValidateField: function (field) {
if (!field || field.disabled || field.type === 'hidden' || field.matches(this.selectors.honeypot)) {
return false;
}
if (field.getAttribute('data-rx-no-validate') === 'true') {
return false;
}
return true;
},
isAjaxForm: function (form) {
return form.matches(this.selectors.ajaxForm);
},
isAutosaveForm: function (form) {
return form.matches(this.selectors.autoSaveForm);
},
toSlug: function (value) {
return String(value || '')
.toLowerCase()
.trim()
.replace(/https?:\/\//g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
},
formatBytes: function (bytes) {
if (bytes === 0) {
return '0 Bytes';
}
var k = 1024;
var sizes = ['Bytes', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
uniqueId: function (prefix) {
return prefix + '-' + Math.random().toString(36).slice(2, 9);
},
dispatch: function (name, detail) {
document.dispatchEvent(new CustomEvent(name, {
detail: detail || {}
}));
}
};
window.RXThemeForms = RXThemeForms;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
RXThemeForms.init();
});
} else {
RXThemeForms.init();
}
})();
Use it in your theme with something like this in functions.php:
wp_enqueue_script(
'rx-theme-forms',
get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-016-forms.js',
array(),
'1.0.0',
true
);
Example form usage:
<form data-rx-form data-rx-validate="true" data-rx-autosave="true">
<label for="rx-name">Name</label>
<input id="rx-name" name="name" type="text" required minlength="2">
<label for="rx-email">Email</label>
<input id="rx-email" name="email" type="email" required>
<label for="rx-message">Message</label>
<textarea id="rx-message" name="message" maxlength="500" required></textarea>
<div data-rx-counter="#rx-message"></div>
<input type="text" data-rx-honeypot hidden tabindex="-1" autocomplete="off">
<button type="submit">Submit</button>
</form>
For password toggle:
<input id="rx-password" type="password" data-rx-password-strength="true" required>
<button data-rx-password-toggle="#rx-password" data-rx-update-text="true">Show</button>