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
);
}