chunk-015-comments.js

I’ll build this as a standalone advanced comments chunk for RX Theme: frontend comment UX, validation, reply handling, accessibility, lazy behavior, and safe progressive enhancement without breaking WordPress defaults.

Below is a full advanced chunk-015-comments.js file for your RX Theme.

File path:

assets/static-js/static-chunks-js/chunk-015-comments.js
/*!
 * RX Theme - Comments Advanced Chunk
 * File: assets/static-js/static-chunks-js/chunk-015-comments.js
 * Version: 1.0.0
 *
 * Purpose:
 * - Improve WordPress comment form UX
 * - Add safe client-side validation
 * - Improve reply/cancel-reply behavior
 * - Add accessibility helpers
 * - Add character counter
 * - Add auto-resize textarea
 * - Add comment form protection helpers
 * - Add smooth scroll to comments/reply form
 * - Add progressive enhancement only, no dependency required
 *
 * Important:
 * - This file does not replace WordPress server-side validation.
 * - WordPress/PHP must still sanitize, validate, and verify all comments.
 */

(function () {
  'use strict';

  /**
   * ------------------------------------------------------------
   * RX Comments Config
   * ------------------------------------------------------------
   */

  const RXCommentsConfig = {
    selectors: {
      commentsArea: '#comments, .comments-area, .rx-comments-area',
      commentList: '.comment-list, .rx-comment-list',
      commentItem: '.comment, .rx-comment',
      commentForm: '#commentform, .comment-form, .rx-comment-form',
      commentTextarea: '#comment, textarea[name="comment"], .rx-comment-textarea',
      authorInput: '#author, input[name="author"]',
      emailInput: '#email, input[name="email"]',
      urlInput: '#url, input[name="url"]',
      submitButton: '#submit, .form-submit input[type="submit"], .rx-comment-submit',
      replyLinks: '.comment-reply-link',
      cancelReplyLink: '#cancel-comment-reply-link',
      respond: '#respond',
      commentParent: '#comment_parent',
      commentPostId: '#comment_post_ID',
      loggedInAs: '.logged-in-as',
      mustLogIn: '.must-log-in',
      notesBefore: '.comment-notes',
      notesAfter: '.form-allowed-tags',
      formCookiesConsent: '.comment-form-cookies-consent',
      moderationMessage: '.comment-awaiting-moderation'
    },

    classes: {
      initialized: 'rx-comments-initialized',
      formReady: 'rx-comment-form-ready',
      fieldError: 'rx-field-error',
      fieldSuccess: 'rx-field-success',
      errorMessage: 'rx-comment-error-message',
      helpMessage: 'rx-comment-help-message',
      charCounter: 'rx-comment-char-counter',
      charCounterWarning: 'rx-comment-char-counter-warning',
      charCounterDanger: 'rx-comment-char-counter-danger',
      isSubmitting: 'rx-comment-is-submitting',
      disabled: 'rx-is-disabled',
      stickyReply: 'rx-comment-reply-active',
      highlight: 'rx-comment-highlight',
      visible: 'rx-is-visible',
      hidden: 'rx-is-hidden',
      srOnly: 'rx-sr-only'
    },

    attributes: {
      enhanced: 'data-rx-comments-enhanced',
      errorFor: 'data-rx-error-for',
      originalText: 'data-rx-original-text',
      replyTarget: 'data-rx-reply-target',
      fieldTouched: 'data-rx-field-touched'
    },

    limits: {
      minCommentLength: 5,
      maxCommentLength: 3000,
      maxAuthorLength: 80,
      maxEmailLength: 120,
      maxUrlLength: 200,
      maxRepeatedCharacters: 12
    },

    behavior: {
      smoothScroll: true,
      focusAfterReplyClick: true,
      enableAutoResize: true,
      enableCharCounter: true,
      enableLocalDraft: true,
      enableSpamTrap: true,
      enableSubmitLock: true,
      enableCommentHighlight: true,
      enableExternalLinkSecurity: true,
      enableKeyboardShortcuts: true,
      enableFormDirtyWarning: false,
      autoSaveDelay: 700,
      highlightDuration: 2200,
      submitLockDelay: 1200
    },

    messages: {
      commentRequired: 'Please write your comment.',
      commentTooShort: 'Please write a little more before submitting.',
      commentTooLong: 'Your comment is too long. Please shorten it.',
      authorRequired: 'Please enter your name.',
      authorTooLong: 'Your name is too long.',
      emailRequired: 'Please enter your email address.',
      emailInvalid: 'Please enter a valid email address.',
      emailTooLong: 'Your email address is too long.',
      urlInvalid: 'Please enter a valid website URL.',
      urlTooLong: 'Your website URL is too long.',
      repeatedText: 'Please avoid repeated characters or spam-like text.',
      submitWorking: 'Posting...',
      submitReady: 'Post Comment',
      savedDraft: 'Draft saved in this browser.',
      restoredDraft: 'Your saved comment draft was restored.',
      clearedDraft: 'Saved draft cleared.',
      replyMode: 'Reply mode is active.',
      cancelReply: 'Reply cancelled.'
    },

    storage: {
      draftKeyPrefix: 'rx_comment_draft_',
      authorKey: 'rx_comment_author',
      emailKey: 'rx_comment_email',
      urlKey: 'rx_comment_url'
    }
  };

  /**
   * ------------------------------------------------------------
   * Utility Helpers
   * ------------------------------------------------------------
   */

  const RXCommentUtils = {
    qs(selector, context = document) {
      if (!selector || !context) return null;
      return context.querySelector(selector);
    },

    qsa(selector, context = document) {
      if (!selector || !context) return [];
      return Array.prototype.slice.call(context.querySelectorAll(selector));
    },

    closest(element, selector) {
      if (!element || !selector) return null;
      if (element.closest) return element.closest(selector);

      let current = element;
      while (current && current.nodeType === 1) {
        if (RXCommentUtils.matches(current, selector)) return current;
        current = current.parentElement;
      }

      return null;
    },

    matches(element, selector) {
      if (!element || element.nodeType !== 1) return false;

      const proto = Element.prototype;
      const fn =
        proto.matches ||
        proto.matchesSelector ||
        proto.msMatchesSelector ||
        proto.webkitMatchesSelector;

      if (!fn) return false;
      return fn.call(element, selector);
    },

    on(element, eventName, handler, options) {
      if (!element || !eventName || typeof handler !== 'function') return;
      element.addEventListener(eventName, handler, options || false);
    },

    off(element, eventName, handler, options) {
      if (!element || !eventName || typeof handler !== 'function') return;
      element.removeEventListener(eventName, handler, options || false);
    },

    debounce(fn, delay) {
      let timer = null;

      return function debounced() {
        const context = this;
        const args = arguments;

        window.clearTimeout(timer);
        timer = window.setTimeout(function () {
          fn.apply(context, args);
        }, delay);
      };
    },

    throttle(fn, delay) {
      let last = 0;
      let timer = null;

      return function throttled() {
        const now = Date.now();
        const remaining = delay - (now - last);
        const context = this;
        const args = arguments;

        if (remaining <= 0) {
          window.clearTimeout(timer);
          timer = null;
          last = now;
          fn.apply(context, args);
        } else if (!timer) {
          timer = window.setTimeout(function () {
            last = Date.now();
            timer = null;
            fn.apply(context, args);
          }, remaining);
        }
      };
    },

    trim(value) {
      return String(value || '').replace(/^\s+|\s+$/g, '');
    },

    normalizeSpaces(value) {
      return String(value || '').replace(/\s+/g, ' ').trim();
    },

    escapeHTML(value) {
      const div = document.createElement('div');
      div.textContent = String(value || '');
      return div.innerHTML;
    },

    hasClass(element, className) {
      if (!element || !className) return false;
      return element.classList.contains(className);
    },

    addClass(element, className) {
      if (!element || !className) return;
      element.classList.add(className);
    },

    removeClass(element, className) {
      if (!element || !className) return;
      element.classList.remove(className);
    },

    toggleClass(element, className, force) {
      if (!element || !className) return;
      element.classList.toggle(className, force);
    },

    setAttr(element, name, value) {
      if (!element || !name) return;
      element.setAttribute(name, value);
    },

    removeAttr(element, name) {
      if (!element || !name) return;
      element.removeAttribute(name);
    },

    getAttr(element, name, fallback = '') {
      if (!element || !name) return fallback;
      const value = element.getAttribute(name);
      return value === null ? fallback : value;
    },

    isVisible(element) {
      if (!element) return false;
      return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
    },

    scrollToElement(element, offset = 90) {
      if (!element) return;

      const top =
        element.getBoundingClientRect().top +
        window.pageYOffset -
        Number(offset || 0);

      if (RXCommentsConfig.behavior.smoothScroll && 'scrollBehavior' in document.documentElement.style) {
        window.scrollTo({
          top,
          behavior: 'smooth'
        });
      } else {
        window.scrollTo(0, top);
      }
    },

    safeLocalStorageGet(key) {
      try {
        return window.localStorage.getItem(key);
      } catch (error) {
        return null;
      }
    },

    safeLocalStorageSet(key, value) {
      try {
        window.localStorage.setItem(key, value);
        return true;
      } catch (error) {
        return false;
      }
    },

    safeLocalStorageRemove(key) {
      try {
        window.localStorage.removeItem(key);
        return true;
      } catch (error) {
        return false;
      }
    },

    generateId(prefix = 'rx-id') {
      return prefix + '-' + Math.random().toString(36).slice(2, 10);
    },

    getPostId(form) {
      const postInput = RXCommentUtils.qs(RXCommentsConfig.selectors.commentPostId, form);
      return postInput ? postInput.value || 'global' : 'global';
    },

    isEmail(value) {
      const email = RXCommentUtils.trim(value);
      if (!email) return false;

      return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email);
    },

    isURL(value) {
      const url = RXCommentUtils.trim(value);
      if (!url) return true;

      try {
        const normalized = /^https?:\/\//i.test(url) ? url : 'https://' + url;
        const parsed = new URL(normalized);
        return parsed.protocol === 'http:' || parsed.protocol === 'https:';
      } catch (error) {
        return false;
      }
    },

    hasSpamLikeRepeats(value) {
      const text = String(value || '');
      const max = RXCommentsConfig.limits.maxRepeatedCharacters;

      const repeatedPattern = new RegExp('(.)\\1{' + max + ',}', 'i');
      return repeatedPattern.test(text);
    },

    countChars(value) {
      return String(value || '').length;
    }
  };

  /**
   * ------------------------------------------------------------
   * Accessibility Helpers
   * ------------------------------------------------------------
   */

  const RXCommentA11y = {
    liveRegion: null,

    init() {
      if (this.liveRegion) return;

      const region = document.createElement('div');
      region.className = 'rx-comment-live-region rx-sr-only';
      region.setAttribute('aria-live', 'polite');
      region.setAttribute('aria-atomic', 'true');

      document.body.appendChild(region);
      this.liveRegion = region;
    },

    announce(message) {
      if (!message) return;

      this.init();

      this.liveRegion.textContent = '';

      window.setTimeout(() => {
        this.liveRegion.textContent = message;
      }, 40);
    },

    ensureDescribedBy(field, messageId) {
      if (!field || !messageId) return;

      const current = RXCommentUtils.getAttr(field, 'aria-describedby', '');
      const parts = current ? current.split(/\s+/) : [];

      if (parts.indexOf(messageId) === -1) {
        parts.push(messageId);
      }

      RXCommentUtils.setAttr(field, 'aria-describedby', parts.join(' '));
    },

    removeDescribedBy(field, messageId) {
      if (!field || !messageId) return;

      const current = RXCommentUtils.getAttr(field, 'aria-describedby', '');
      if (!current) return;

      const parts = current.split(/\s+/).filter((item) => item !== messageId);
      if (parts.length) {
        RXCommentUtils.setAttr(field, 'aria-describedby', parts.join(' '));
      } else {
        RXCommentUtils.removeAttr(field, 'aria-describedby');
      }
    }
  };

  /**
   * ------------------------------------------------------------
   * Form Message Manager
   * ------------------------------------------------------------
   */

  const RXCommentMessages = {
    getMessageElement(field) {
      if (!field) return null;

      const fieldId = field.id || field.name;
      if (!fieldId) return null;

      const selector = '[' + RXCommentsConfig.attributes.errorFor + '="' + fieldId + '"]';
      return RXCommentUtils.qs(selector, field.parentElement || document);
    },

    createMessageElement(field) {
      if (!field) return null;

      const fieldId = field.id || field.name || RXCommentUtils.generateId('rx-comment-field');
      if (!field.id) field.id = fieldId;

      let message = this.getMessageElement(field);

      if (!message) {
        message = document.createElement('div');
        message.className = RXCommentsConfig.classes.errorMessage;
        message.id = fieldId + '-error';
        message.setAttribute(RXCommentsConfig.attributes.errorFor, fieldId);
        message.setAttribute('role', 'alert');
        message.hidden = true;

        const parent = field.parentElement;
        if (parent) {
          parent.appendChild(message);
        }
      }

      RXCommentA11y.ensureDescribedBy(field, message.id);

      return message;
    },

    showError(field, text) {
      if (!field || !text) return;

      const message = this.createMessageElement(field);

      RXCommentUtils.addClass(field, RXCommentsConfig.classes.fieldError);
      RXCommentUtils.removeClass(field, RXCommentsConfig.classes.fieldSuccess);
      RXCommentUtils.setAttr(field, 'aria-invalid', 'true');

      if (message) {
        message.textContent = text;
        message.hidden = false;
      }
    },

    showSuccess(field) {
      if (!field) return;

      const message = this.getMessageElement(field);

      RXCommentUtils.removeClass(field, RXCommentsConfig.classes.fieldError);
      RXCommentUtils.addClass(field, RXCommentsConfig.classes.fieldSuccess);
      RXCommentUtils.setAttr(field, 'aria-invalid', 'false');

      if (message) {
        message.textContent = '';
        message.hidden = true;
      }
    },

    clear(field) {
      if (!field) return;

      const message = this.getMessageElement(field);

      RXCommentUtils.removeClass(field, RXCommentsConfig.classes.fieldError);
      RXCommentUtils.removeClass(field, RXCommentsConfig.classes.fieldSuccess);
      RXCommentUtils.removeAttr(field, 'aria-invalid');

      if (message) {
        message.textContent = '';
        message.hidden = true;
      }
    }
  };

  /**
   * ------------------------------------------------------------
   * Validation
   * ------------------------------------------------------------
   */

  const RXCommentValidation = {
    validateComment(textarea, show = true) {
      if (!textarea) return true;

      const value = RXCommentUtils.trim(textarea.value);
      const length = RXCommentUtils.countChars(value);
      const limits = RXCommentsConfig.limits;
      const messages = RXCommentsConfig.messages;

      if (!value) {
        if (show) RXCommentMessages.showError(textarea, messages.commentRequired);
        return false;
      }

      if (length < limits.minCommentLength) {
        if (show) RXCommentMessages.showError(textarea, messages.commentTooShort);
        return false;
      }

      if (length > limits.maxCommentLength) {
        if (show) RXCommentMessages.showError(textarea, messages.commentTooLong);
        return false;
      }

      if (RXCommentUtils.hasSpamLikeRepeats(value)) {
        if (show) RXCommentMessages.showError(textarea, messages.repeatedText);
        return false;
      }

      if (show) RXCommentMessages.showSuccess(textarea);
      return true;
    },

    validateAuthor(input, show = true) {
      if (!input) return true;

      const required = input.hasAttribute('required') || input.getAttribute('aria-required') === 'true';
      const value = RXCommentUtils.normalizeSpaces(input.value);
      const messages = RXCommentsConfig.messages;

      if (required && !value) {
        if (show) RXCommentMessages.showError(input, messages.authorRequired);
        return false;
      }

      if (value.length > RXCommentsConfig.limits.maxAuthorLength) {
        if (show) RXCommentMessages.showError(input, messages.authorTooLong);
        return false;
      }

      if (show && value) RXCommentMessages.showSuccess(input);
      if (show && !value) RXCommentMessages.clear(input);

      return true;
    },

    validateEmail(input, show = true) {
      if (!input) return true;

      const required = input.hasAttribute('required') || input.getAttribute('aria-required') === 'true';
      const value = RXCommentUtils.trim(input.value);
      const messages = RXCommentsConfig.messages;

      if (required && !value) {
        if (show) RXCommentMessages.showError(input, messages.emailRequired);
        return false;
      }

      if (value && value.length > RXCommentsConfig.limits.maxEmailLength) {
        if (show) RXCommentMessages.showError(input, messages.emailTooLong);
        return false;
      }

      if (value && !RXCommentUtils.isEmail(value)) {
        if (show) RXCommentMessages.showError(input, messages.emailInvalid);
        return false;
      }

      if (show && value) RXCommentMessages.showSuccess(input);
      if (show && !value) RXCommentMessages.clear(input);

      return true;
    },

    validateUrl(input, show = true) {
      if (!input) return true;

      const value = RXCommentUtils.trim(input.value);
      const messages = RXCommentsConfig.messages;

      if (value && value.length > RXCommentsConfig.limits.maxUrlLength) {
        if (show) RXCommentMessages.showError(input, messages.urlTooLong);
        return false;
      }

      if (value && !RXCommentUtils.isURL(value)) {
        if (show) RXCommentMessages.showError(input, messages.urlInvalid);
        return false;
      }

      if (show && value) RXCommentMessages.showSuccess(input);
      if (show && !value) RXCommentMessages.clear(input);

      return true;
    },

    validateForm(form, show = true) {
      if (!form) return true;

      const textarea = RXCommentUtils.qs(RXCommentsConfig.selectors.commentTextarea, form);
      const author = RXCommentUtils.qs(RXCommentsConfig.selectors.authorInput, form);
      const email = RXCommentUtils.qs(RXCommentsConfig.selectors.emailInput, form);
      const url = RXCommentUtils.qs(RXCommentsConfig.selectors.urlInput, form);

      const results = [
        this.validateComment(textarea, show),
        this.validateAuthor(author, show),
        this.validateEmail(email, show),
        this.validateUrl(url, show)
      ];

      const valid = results.every(Boolean);

      if (!valid && show) {
        const firstError = RXCommentUtils.qs('.' + RXCommentsConfig.classes.fieldError, form);
        if (firstError) {
          firstError.focus({ preventScroll: true });
          RXCommentUtils.scrollToElement(firstError, 120);
        }
      }

      return valid;
    }
  };

  /**
   * ------------------------------------------------------------
   * Character Counter
   * ------------------------------------------------------------
   */

  const RXCommentCounter = {
    init(textarea) {
      if (!textarea || !RXCommentsConfig.behavior.enableCharCounter) return;

      const existing = textarea.parentElement
        ? RXCommentUtils.qs('.' + RXCommentsConfig.classes.charCounter, textarea.parentElement)
        : null;

      if (existing) {
        this.update(textarea, existing);
        return;
      }

      const counter = document.createElement('div');
      counter.className = RXCommentsConfig.classes.charCounter;
      counter.id = (textarea.id || 'comment') + '-counter';

      textarea.parentElement.appendChild(counter);
      RXCommentA11y.ensureDescribedBy(textarea, counter.id);

      this.update(textarea, counter);

      RXCommentUtils.on(textarea, 'input', () => {
        this.update(textarea, counter);
      });
    },

    update(textarea, counter) {
      if (!textarea || !counter) return;

      const length = RXCommentUtils.countChars(textarea.value);
      const max = RXCommentsConfig.limits.maxCommentLength;
      const remaining = max - length;

      counter.textContent = length + ' / ' + max + ' characters';

      RXCommentUtils.toggleClass(
        counter,
        RXCommentsConfig.classes.charCounterWarning,
        remaining <= 300 && remaining > 100
      );

      RXCommentUtils.toggleClass(
        counter,
        RXCommentsConfig.classes.charCounterDanger,
        remaining <= 100
      );
    }
  };

  /**
   * ------------------------------------------------------------
   * Auto Resize Textarea
   * ------------------------------------------------------------
   */

  const RXCommentAutoResize = {
    init(textarea) {
      if (!textarea || !RXCommentsConfig.behavior.enableAutoResize) return;

      const resize = RXCommentUtils.throttle(() => {
        this.resize(textarea);
      }, 60);

      RXCommentUtils.on(textarea, 'input', resize);
      RXCommentUtils.on(window, 'resize', resize);

      this.resize(textarea);
    },

    resize(textarea) {
      if (!textarea) return;

      textarea.style.height = 'auto';

      const minHeight = parseInt(window.getComputedStyle(textarea).minHeight, 10) || 120;
      const nextHeight = Math.max(textarea.scrollHeight, minHeight);

      textarea.style.height = nextHeight + 'px';
    }
  };

  /**
   * ------------------------------------------------------------
   * Local Draft Save
   * ------------------------------------------------------------
   */

  const RXCommentDraft = {
    getDraftKey(form) {
      const postId = RXCommentUtils.getPostId(form);
      return RXCommentsConfig.storage.draftKeyPrefix + postId;
    },

    init(form) {
      if (!form || !RXCommentsConfig.behavior.enableLocalDraft) return;

      const textarea = RXCommentUtils.qs(RXCommentsConfig.selectors.commentTextarea, form);
      const author = RXCommentUtils.qs(RXCommentsConfig.selectors.authorInput, form);
      const email = RXCommentUtils.qs(RXCommentsConfig.selectors.emailInput, form);
      const url = RXCommentUtils.qs(RXCommentsConfig.selectors.urlInput, form);

      this.restore(form, textarea, author, email, url);

      const save = RXCommentUtils.debounce(() => {
        this.save(form, textarea, author, email, url);
      }, RXCommentsConfig.behavior.autoSaveDelay);

      [textarea, author, email, url].forEach((field) => {
        if (field) {
          RXCommentUtils.on(field, 'input', save);
          RXCommentUtils.on(field, 'change', save);
        }
      });

      RXCommentUtils.on(form, 'submit', () => {
        this.clear(form);
      });
    },

    save(form, textarea, author, email, url) {
      if (!form) return;

      const data = {
        comment: textarea ? textarea.value : '',
        author: author ? author.value : '',
        email: email ? email.value : '',
        url: url ? url.value : '',
        time: Date.now()
      };

      const hasContent =
        RXCommentUtils.trim(data.comment) ||
        RXCommentUtils.trim(data.author) ||
        RXCommentUtils.trim(data.email) ||
        RXCommentUtils.trim(data.url);

      if (!hasContent) {
        this.clear(form);
        return;
      }

      RXCommentUtils.safeLocalStorageSet(this.getDraftKey(form), JSON.stringify(data));

      if (author && author.value) {
        RXCommentUtils.safeLocalStorageSet(RXCommentsConfig.storage.authorKey, author.value);
      }

      if (email && email.value) {
        RXCommentUtils.safeLocalStorageSet(RXCommentsConfig.storage.emailKey, email.value);
      }

      if (url && url.value) {
        RXCommentUtils.safeLocalStorageSet(RXCommentsConfig.storage.urlKey, url.value);
      }
    },

    restore(form, textarea, author, email, url) {
      if (!form) return;

      const raw = RXCommentUtils.safeLocalStorageGet(this.getDraftKey(form));

      if (raw) {
        try {
          const data = JSON.parse(raw);

          if (textarea && !textarea.value && data.comment) textarea.value = data.comment;
          if (author && !author.value && data.author) author.value = data.author;
          if (email && !email.value && data.email) email.value = data.email;
          if (url && !url.value && data.url) url.value = data.url;

          if (textarea && data.comment) {
            RXCommentAutoResize.resize(textarea);
            RXCommentA11y.announce(RXCommentsConfig.messages.restoredDraft);
          }
        } catch (error) {
          this.clear(form);
        }
      } else {
        const savedAuthor = RXCommentUtils.safeLocalStorageGet(RXCommentsConfig.storage.authorKey);
        const savedEmail = RXCommentUtils.safeLocalStorageGet(RXCommentsConfig.storage.emailKey);
        const savedUrl = RXCommentUtils.safeLocalStorageGet(RXCommentsConfig.storage.urlKey);

        if (author && !author.value && savedAuthor) author.value = savedAuthor;
        if (email && !email.value && savedEmail) email.value = savedEmail;
        if (url && !url.value && savedUrl) url.value = savedUrl;
      }
    },

    clear(form) {
      if (!form) return;
      RXCommentUtils.safeLocalStorageRemove(this.getDraftKey(form));
    }
  };

  /**
   * ------------------------------------------------------------
   * Anti-Spam Progressive Trap
   * ------------------------------------------------------------
   */

  const RXCommentSpamTrap = {
    init(form) {
      if (!form || !RXCommentsConfig.behavior.enableSpamTrap) return;

      if (RXCommentUtils.qs('input[name="rx_comment_time"]', form)) return;

      const timeInput = document.createElement('input');
      timeInput.type = 'hidden';
      timeInput.name = 'rx_comment_time';
      timeInput.value = String(Date.now());

      const trapWrap = document.createElement('div');
      trapWrap.className = RXCommentsConfig.classes.srOnly;
      trapWrap.setAttribute('aria-hidden', 'true');

      const trapLabel = document.createElement('label');
      trapLabel.textContent = 'Leave this field empty';

      const trapInput = document.createElement('input');
      trapInput.type = 'text';
      trapInput.name = 'rx_comment_website_confirm';
      trapInput.tabIndex = -1;
      trapInput.autocomplete = 'off';

      trapLabel.appendChild(trapInput);
      trapWrap.appendChild(trapLabel);

      form.appendChild(timeInput);
      form.appendChild(trapWrap);

      RXCommentUtils.on(form, 'submit', function (event) {
        if (trapInput.value) {
          event.preventDefault();
          event.stopPropagation();
          return false;
        }

        return true;
      });
    }
  };

  /**
   * ------------------------------------------------------------
   * Submit Button State
   * ------------------------------------------------------------
   */

  const RXCommentSubmit = {
    init(form) {
      if (!form) return;

      const submit = RXCommentUtils.qs(RXCommentsConfig.selectors.submitButton, form);
      if (!submit) return;

      if (!submit.getAttribute(RXCommentsConfig.attributes.originalText)) {
        submit.setAttribute(
          RXCommentsConfig.attributes.originalText,
          submit.value || submit.textContent || RXCommentsConfig.messages.submitReady
        );
      }

      RXCommentUtils.on(form, 'submit', (event) => {
        const valid = RXCommentValidation.validateForm(form, true);

        if (!valid) {
          event.preventDefault();
          event.stopPropagation();
          return false;
        }

        if (RXCommentsConfig.behavior.enableSubmitLock) {
          this.lock(form, submit);
        }

        return true;
      });
    },

    lock(form, submit) {
      if (!form || !submit) return;

      RXCommentUtils.addClass(form, RXCommentsConfig.classes.isSubmitting);
      RXCommentUtils.addClass(submit, RXCommentsConfig.classes.disabled);

      if ('disabled' in submit) {
        submit.disabled = true;
      }

      if (submit.tagName === 'INPUT') {
        submit.value = RXCommentsConfig.messages.submitWorking;
      } else {
        submit.textContent = RXCommentsConfig.messages.submitWorking;
      }

      window.setTimeout(() => {
        this.unlock(form, submit);
      }, RXCommentsConfig.behavior.submitLockDelay);
    },

    unlock(form, submit) {
      if (!form || !submit) return;

      const original =
        submit.getAttribute(RXCommentsConfig.attributes.originalText) ||
        RXCommentsConfig.messages.submitReady;

      RXCommentUtils.removeClass(form, RXCommentsConfig.classes.isSubmitting);
      RXCommentUtils.removeClass(submit, RXCommentsConfig.classes.disabled);

      if ('disabled' in submit) {
        submit.disabled = false;
      }

      if (submit.tagName === 'INPUT') {
        submit.value = original;
      } else {
        submit.textContent = original;
      }
    }
  };

  /**
   * ------------------------------------------------------------
   * Reply Link Enhancement
   * ------------------------------------------------------------
   */

  const RXCommentReply = {
    init() {
      const replyLinks = RXCommentUtils.qsa(RXCommentsConfig.selectors.replyLinks);
      const cancelLink = RXCommentUtils.qs(RXCommentsConfig.selectors.cancelReplyLink);

      replyLinks.forEach((link) => {
        RXCommentUtils.on(link, 'click', () => {
          this.handleReplyClick(link);
        });
      });

      if (cancelLink) {
        RXCommentUtils.on(cancelLink, 'click', () => {
          this.handleCancelReply();
        });
      }
    },

    handleReplyClick(link) {
      const comment = RXCommentUtils.closest(link, RXCommentsConfig.selectors.commentItem);
      const respond = RXCommentUtils.qs(RXCommentsConfig.selectors.respond);
      const textarea = respond
        ? RXCommentUtils.qs(RXCommentsConfig.selectors.commentTextarea, respond)
        : RXCommentUtils.qs(RXCommentsConfig.selectors.commentTextarea);

      document.body.classList.add(RXCommentsConfig.classes.stickyReply);

      if (comment) {
        const commentId = comment.id || '';
        if (respond) {
          respond.setAttribute(RXCommentsConfig.attributes.replyTarget, commentId);
        }

        this.highlightComment(comment);
      }

      window.setTimeout(() => {
        if (respond) {
          RXCommentUtils.scrollToElement(respond, 100);
        }

        if (textarea && RXCommentsConfig.behavior.focusAfterReplyClick) {
          textarea.focus({ preventScroll: true });
        }

        RXCommentA11y.announce(RXCommentsConfig.messages.replyMode);
      }, 120);
    },

    handleCancelReply() {
      const respond = RXCommentUtils.qs(RXCommentsConfig.selectors.respond);

      document.body.classList.remove(RXCommentsConfig.classes.stickyReply);

      if (respond) {
        respond.removeAttribute(RXCommentsConfig.attributes.replyTarget);
      }

      RXCommentA11y.announce(RXCommentsConfig.messages.cancelReply);
    },

    highlightComment(comment) {
      if (!comment || !RXCommentsConfig.behavior.enableCommentHighlight) return;

      RXCommentUtils.addClass(comment, RXCommentsConfig.classes.highlight);

      window.setTimeout(() => {
        RXCommentUtils.removeClass(comment, RXCommentsConfig.classes.highlight);
      }, RXCommentsConfig.behavior.highlightDuration);
    }
  };

  /**
   * ------------------------------------------------------------
   * Comment Hash Highlight
   * ------------------------------------------------------------
   */

  const RXCommentHash = {
    init() {
      if (!RXCommentsConfig.behavior.enableCommentHighlight) return;

      this.highlightFromHash();

      RXCommentUtils.on(window, 'hashchange', () => {
        this.highlightFromHash();
      });
    },

    highlightFromHash() {
      const hash = window.location.hash;
      if (!hash || hash.indexOf('#comment-') !== 0) return;

      const id = decodeURIComponent(hash.slice(1));
      const comment = document.getElementById(id);

      if (!comment) return;

      RXCommentUtils.scrollToElement(comment, 100);
      RXCommentReply.highlightComment(comment);
    }
  };

  /**
   * ------------------------------------------------------------
   * External Link Security inside Comments
   * ------------------------------------------------------------
   */

  const RXCommentLinks = {
    init() {
      if (!RXCommentsConfig.behavior.enableExternalLinkSecurity) return;

      const commentsArea = RXCommentUtils.qs(RXCommentsConfig.selectors.commentsArea);
      if (!commentsArea) return;

      const links = RXCommentUtils.qsa('a[href]', commentsArea);

      links.forEach((link) => {
        this.secureLink(link);
      });
    },

    secureLink(link) {
      if (!link) return;

      const href = link.getAttribute('href') || '';
      if (!href || href.charAt(0) === '#') return;

      try {
        const url = new URL(href, window.location.href);

        if (url.hostname && url.hostname !== window.location.hostname) {
          link.setAttribute('target', '_blank');

          const rel = link.getAttribute('rel') || '';
          const relParts = rel.split(/\s+/).filter(Boolean);

          ['noopener', 'noreferrer', 'ugc', 'nofollow'].forEach((item) => {
            if (relParts.indexOf(item) === -1) relParts.push(item);
          });

          link.setAttribute('rel', relParts.join(' '));
        }
      } catch (error) {
        return;
      }
    }
  };

  /**
   * ------------------------------------------------------------
   * Keyboard Shortcuts
   * ------------------------------------------------------------
   */

  const RXCommentKeyboard = {
    init(form) {
      if (!form || !RXCommentsConfig.behavior.enableKeyboardShortcuts) return;

      const textarea = RXCommentUtils.qs(RXCommentsConfig.selectors.commentTextarea, form);

      if (!textarea) return;

      RXCommentUtils.on(textarea, 'keydown', (event) => {
        const isSubmitShortcut =
          (event.ctrlKey || event.metaKey) &&
          (event.key === 'Enter' || event.keyCode === 13);

        if (!isSubmitShortcut) return;

        event.preventDefault();

        if (RXCommentValidation.validateForm(form, true)) {
          if (typeof form.requestSubmit === 'function') {
            form.requestSubmit();
          } else {
            form.submit();
          }
        }
      });
    }
  };

  /**
   * ------------------------------------------------------------
   * Dirty Form Warning
   * Disabled by default
   * ------------------------------------------------------------
   */

  const RXCommentDirtyWarning = {
    dirty: false,

    init(form) {
      if (!form || !RXCommentsConfig.behavior.enableFormDirtyWarning) return;

      const fields = RXCommentUtils.qsa('input, textarea, select', form);

      fields.forEach((field) => {
        RXCommentUtils.on(field, 'input', () => {
          this.dirty = true;
        });
      });

      RXCommentUtils.on(form, 'submit', () => {
        this.dirty = false;
      });

      RXCommentUtils.on(window, 'beforeunload', (event) => {
        if (!this.dirty) return undefined;

        event.preventDefault();
        event.returnValue = '';

        return '';
      });
    }
  };

  /**
   * ------------------------------------------------------------
   * Field Events
   * ------------------------------------------------------------
   */

  const RXCommentFieldEvents = {
    init(form) {
      if (!form) return;

      const textarea = RXCommentUtils.qs(RXCommentsConfig.selectors.commentTextarea, form);
      const author = RXCommentUtils.qs(RXCommentsConfig.selectors.authorInput, form);
      const email = RXCommentUtils.qs(RXCommentsConfig.selectors.emailInput, form);
      const url = RXCommentUtils.qs(RXCommentsConfig.selectors.urlInput, form);

      if (textarea) {
        RXCommentUtils.on(textarea, 'blur', () => {
          RXCommentValidation.validateComment(textarea, true);
        });

        RXCommentUtils.on(
          textarea,
          'input',
          RXCommentUtils.debounce(() => {
            if (textarea.getAttribute(RXCommentsConfig.attributes.fieldTouched) === 'true') {
              RXCommentValidation.validateComment(textarea, true);
            }
          }, 300)
        );

        RXCommentUtils.on(textarea, 'focus', () => {
          textarea.setAttribute(RXCommentsConfig.attributes.fieldTouched, 'true');
        });
      }

      if (author) {
        RXCommentUtils.on(author, 'blur', () => RXCommentValidation.validateAuthor(author, true));
        RXCommentUtils.on(author, 'input', RXCommentUtils.debounce(() => RXCommentValidation.validateAuthor(author, true), 300));
      }

      if (email) {
        RXCommentUtils.on(email, 'blur', () => RXCommentValidation.validateEmail(email, true));
        RXCommentUtils.on(email, 'input', RXCommentUtils.debounce(() => RXCommentValidation.validateEmail(email, true), 300));
      }

      if (url) {
        RXCommentUtils.on(url, 'blur', () => RXCommentValidation.validateUrl(url, true));
        RXCommentUtils.on(url, 'input', RXCommentUtils.debounce(() => RXCommentValidation.validateUrl(url, true), 300));
      }
    }
  };

  /**
   * ------------------------------------------------------------
   * Form Enhancer
   * ------------------------------------------------------------
   */

  const RXCommentForm = {
    initAll() {
      const forms = RXCommentUtils.qsa(RXCommentsConfig.selectors.commentForm);

      forms.forEach((form) => {
        this.init(form);
      });
    },

    init(form) {
      if (!form) return;

      if (form.getAttribute(RXCommentsConfig.attributes.enhanced) === 'true') return;

      form.setAttribute(RXCommentsConfig.attributes.enhanced, 'true');
      RXCommentUtils.addClass(form, RXCommentsConfig.classes.formReady);

      const textarea = RXCommentUtils.qs(RXCommentsConfig.selectors.commentTextarea, form);

      if (textarea) {
        RXCommentAutoResize.init(textarea);
        RXCommentCounter.init(textarea);
      }

      RXCommentFieldEvents.init(form);
      RXCommentDraft.init(form);
      RXCommentSpamTrap.init(form);
      RXCommentSubmit.init(form);
      RXCommentKeyboard.init(form);
      RXCommentDirtyWarning.init(form);
    }
  };

  /**
   * ------------------------------------------------------------
   * Comment Area Enhancer
   * ------------------------------------------------------------
   */

  const RXCommentArea = {
    init() {
      const commentsArea = RXCommentUtils.qs(RXCommentsConfig.selectors.commentsArea);

      if (!commentsArea) return;
      if (commentsArea.getAttribute(RXCommentsConfig.attributes.enhanced) === 'true') return;

      commentsArea.setAttribute(RXCommentsConfig.attributes.enhanced, 'true');
      RXCommentUtils.addClass(commentsArea, RXCommentsConfig.classes.initialized);

      this.enhanceModerationMessages(commentsArea);
      this.enhanceCommentItems(commentsArea);
    },

    enhanceModerationMessages(area) {
      const messages = RXCommentUtils.qsa(RXCommentsConfig.selectors.moderationMessage, area);

      messages.forEach((message) => {
        message.setAttribute('role', 'status');
        message.setAttribute('aria-live', 'polite');
      });
    },

    enhanceCommentItems(area) {
      const comments = RXCommentUtils.qsa(RXCommentsConfig.selectors.commentItem, area);

      comments.forEach((comment, index) => {
        if (!comment.id) {
          comment.id = 'rx-comment-auto-' + (index + 1);
        }

        comment.setAttribute('data-rx-comment-index', String(index + 1));
      });
    }
  };

  /**
   * ------------------------------------------------------------
   * Public API
   * ------------------------------------------------------------
   */

  const RXComments = {
    config: RXCommentsConfig,
    utils: RXCommentUtils,
    validation: RXCommentValidation,

    init() {
      RXCommentA11y.init();
      RXCommentArea.init();
      RXCommentForm.initAll();
      RXCommentReply.init();
      RXCommentHash.init();
      RXCommentLinks.init();

      document.documentElement.classList.add('rx-comments-js-ready');

      this.dispatchReadyEvent();
    },

    refresh() {
      RXCommentArea.init();
      RXCommentForm.initAll();
      RXCommentReply.init();
      RXCommentLinks.init();

      this.dispatchReadyEvent('rx-comments-refresh');
    },

    dispatchReadyEvent(name = 'rx-comments-ready') {
      let event;

      if (typeof window.CustomEvent === 'function') {
        event = new CustomEvent(name, {
          bubbles: true,
          detail: {
            source: 'RX Theme',
            chunk: 'chunk-015-comments.js'
          }
        });
      } else {
        event = document.createEvent('CustomEvent');
        event.initCustomEvent(name, true, false, {
          source: 'RX Theme',
          chunk: 'chunk-015-comments.js'
        });
      }

      document.dispatchEvent(event);
    }
  };

  /**
   * ------------------------------------------------------------
   * Boot
   * ------------------------------------------------------------
   */

  function bootRXComments() {
    RXComments.init();
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', bootRXComments);
  } else {
    bootRXComments();
  }

  window.RXComments = RXComments;
})();

Add this small CSS later in your comments CSS file so error messages and highlights look good:

.rx-sr-only {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

.rx-field-error {
  border-color: #d63638 !important;
  outline-color: #d63638 !important;
}

.rx-field-success {
  border-color: #00a32a;
}

.rx-comment-error-message {
  margin-top: 6px;
  font-size: 14px;
  line-height: 1.5;
  color: #d63638;
}

.rx-comment-char-counter {
  margin-top: 6px;
  font-size: 13px;
  opacity: 0.75;
}

.rx-comment-char-counter-warning {
  color: #996800;
}

.rx-comment-char-counter-danger {
  color: #d63638;
  font-weight: 600;
}

.rx-comment-highlight {
  animation: rxCommentHighlight 2.2s ease;
}

@keyframes rxCommentHighlight {
  0% {
    background: rgba(255, 235, 59, 0.35);
  }

  100% {
    background: transparent;
  }
}

.rx-comment-is-submitting {
  opacity: 0.75;
}

.rx-is-disabled {
  cursor: wait !important;
}

Recommended enqueue for functions.php:

wp_enqueue_script(
    'rx-theme-comments-chunk',
    get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-015-comments.js',
    array(),
    wp_get_theme()->get('Version'),
    true
);

For better performance, load it only on single posts/pages where comments are open:

if ( is_singular() && comments_open() ) {
    wp_enqueue_script(
        'rx-theme-comments-chunk',
        get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-015-comments.js',
        array(),
        wp_get_theme()->get('Version'),
        true
    );
}

Leave a Reply

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