chunk-016-forms.js

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>

Leave a Reply

Your email address will not be published. Required fields are marked *