single-post.js

I’ll build this as a complete single-post.js for your RX Theme, focused on useful single-article features without needing external libraries. It will be safe vanilla JavaScript, modular, and easy to enable from your theme.

My dear friend, create this file:

rx-theme/inc/js/single-post.js

Then paste the full code below.

/*!
 * RX Theme - Single Post JavaScript
 * File: inc/js/single-post.js
 * Author: RxHarun
 * Description:
 * Advanced single post UX features for RX Theme:
 * - Reading progress bar
 * - Estimated reading time
 * - Table of contents
 * - Smooth anchor scrolling
 * - Active heading highlight
 * - Back to top button
 * - Copy current post link
 * - Native share button
 * - Copy code button
 * - External link safety
 * - Image lightbox
 * - Lazy iframe loading
 * - Responsive video wrapper
 * - Footnote back links
 * - Print article button
 * - Font size controls
 * - Dark reading mode toggle
 * - Sticky article tools
 * - Selection share helper
 * - Comment form helper
 * - Heading anchor links
 */

(function () {
  'use strict';

  const RXSinglePost = {
    config: {
      contentSelector: '.entry-content, .post-content, article .content, article',
      articleSelector: 'article',
      tocContainerSelector: '#rx-table-of-contents',
      tocListSelector: '#rx-toc-list',
      progressBarId: 'rx-reading-progress-bar',
      backToTopId: 'rx-back-to-top',
      minHeadingsForToc: 3,
      headingSelector: 'h2, h3',
      scrollOffset: 90,
      readingWordsPerMinute: 220,
      storagePrefix: 'rx_theme_single_',
    },

    state: {
      content: null,
      article: null,
      headings: [],
      ticking: false,
      activeHeadingId: null,
      selectionPopup: null,
    },

    init() {
      this.state.content = document.querySelector(this.config.contentSelector);
      this.state.article = document.querySelector(this.config.articleSelector);

      if (!this.state.content) {
        return;
      }

      this.createReadingProgressBar();
      this.createReadingTime();
      this.createTableOfContents();
      this.createHeadingAnchors();
      this.enableSmoothScrolling();
      this.enableActiveTocHighlight();
      this.createBackToTopButton();
      this.enableCopyCurrentLink();
      this.enableNativeShare();
      this.enableCopyCodeButtons();
      this.fixExternalLinks();
      this.enableImageLightbox();
      this.lazyLoadIframes();
      this.wrapResponsiveVideos();
      this.improveTables();
      this.enableFootnoteBacklinks();
      this.createPrintButton();
      this.createFontSizeControls();
      this.createDarkReadingMode();
      this.createStickyTools();
      this.createSelectionShare();
      this.improveCommentForm();
      this.restoreReadingPosition();
      this.saveReadingPosition();
      this.addKeyboardShortcuts();

      window.addEventListener('scroll', () => this.onScroll(), { passive: true });
      window.addEventListener('resize', () => this.onResize(), { passive: true });
    },

    qs(selector, parent = document) {
      return parent.querySelector(selector);
    },

    qsa(selector, parent = document) {
      return Array.prototype.slice.call(parent.querySelectorAll(selector));
    },

    createEl(tag, className, text) {
      const el = document.createElement(tag);

      if (className) {
        el.className = className;
      }

      if (text) {
        el.textContent = text;
      }

      return el;
    },

    slugify(text) {
      return text
        .toString()
        .toLowerCase()
        .trim()
        .replace(/&/g, 'and')
        .replace(/[^a-z0-9\u0980-\u09FF]+/g, '-')
        .replace(/^-+|-+$/g, '');
    },

    uniqueId(base) {
      let id = base || 'rx-heading';
      let counter = 1;

      while (document.getElementById(id)) {
        id = `${base}-${counter}`;
        counter += 1;
      }

      return id;
    },

    createReadingProgressBar() {
      if (document.getElementById(this.config.progressBarId)) {
        return;
      }

      const bar = this.createEl('div', 'rx-reading-progress-bar');
      bar.id = this.config.progressBarId;
      bar.setAttribute('aria-hidden', 'true');

      document.body.appendChild(bar);
    },

    updateReadingProgress() {
      const bar = document.getElementById(this.config.progressBarId);

      if (!bar || !this.state.article) {
        return;
      }

      const articleRect = this.state.article.getBoundingClientRect();
      const articleTop = window.scrollY + articleRect.top;
      const articleHeight = this.state.article.offsetHeight;
      const windowHeight = window.innerHeight;
      const scrollPosition = window.scrollY - articleTop;
      const readableHeight = Math.max(articleHeight - windowHeight, 1);
      const progress = Math.min(Math.max(scrollPosition / readableHeight, 0), 1);

      bar.style.transform = `scaleX(${progress})`;
    },

    createReadingTime() {
      const existing = document.querySelector('.rx-reading-time');

      if (existing) {
        return;
      }

      const text = this.state.content.innerText || '';
      const words = text.trim().split(/\s+/).filter(Boolean).length;
      const minutes = Math.max(1, Math.ceil(words / this.config.readingWordsPerMinute));

      const readingTime = this.createEl(
        'div',
        'rx-reading-time',
        `${minutes} min read`
      );

      readingTime.setAttribute('aria-label', `Estimated reading time ${minutes} minutes`);

      const title = document.querySelector('.entry-title, .post-title, h1');

      if (title && title.parentNode) {
        title.parentNode.insertBefore(readingTime, title.nextSibling);
      } else {
        this.state.content.insertBefore(readingTime, this.state.content.firstChild);
      }
    },

    createTableOfContents() {
      this.state.headings = this.qsa(this.config.headingSelector, this.state.content)
        .filter((heading) => heading.textContent.trim().length > 0);

      if (this.state.headings.length < this.config.minHeadingsForToc) {
        return;
      }

      this.state.headings.forEach((heading) => {
        if (!heading.id) {
          const base = this.slugify(heading.textContent) || 'rx-heading';
          heading.id = this.uniqueId(base);
        }
      });

      let tocContainer = document.querySelector(this.config.tocContainerSelector);

      if (!tocContainer) {
        tocContainer = this.createEl('nav', 'rx-table-of-contents');
        tocContainer.id = 'rx-table-of-contents';
        tocContainer.setAttribute('aria-label', 'Table of contents');

        const tocTitle = this.createEl('button', 'rx-toc-title', 'Table of Contents');
        tocTitle.type = 'button';
        tocTitle.setAttribute('aria-expanded', 'true');

        const tocList = this.createEl('ol', 'rx-toc-list');
        tocList.id = 'rx-toc-list';

        tocContainer.appendChild(tocTitle);
        tocContainer.appendChild(tocList);

        const firstParagraph = this.qs('p', this.state.content);

        if (firstParagraph && firstParagraph.parentNode) {
          firstParagraph.parentNode.insertBefore(tocContainer, firstParagraph);
        } else {
          this.state.content.insertBefore(tocContainer, this.state.content.firstChild);
        }

        tocTitle.addEventListener('click', () => {
          const expanded = tocTitle.getAttribute('aria-expanded') === 'true';
          tocTitle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
          tocContainer.classList.toggle('is-collapsed', expanded);
        });
      }

      const tocList = tocContainer.querySelector('ol') || this.createEl('ol', 'rx-toc-list');
      tocList.innerHTML = '';

      this.state.headings.forEach((heading) => {
        const level = heading.tagName.toLowerCase() === 'h3' ? 'rx-toc-level-3' : 'rx-toc-level-2';

        const li = this.createEl('li', `rx-toc-item ${level}`);
        const link = this.createEl('a', 'rx-toc-link', heading.textContent.trim());

        link.href = `#${heading.id}`;
        link.dataset.target = heading.id;

        li.appendChild(link);
        tocList.appendChild(li);
      });

      if (!tocList.parentNode) {
        tocContainer.appendChild(tocList);
      }
    },

    createHeadingAnchors() {
      const headings = this.qsa('h2, h3, h4, h5, h6', this.state.content);

      headings.forEach((heading) => {
        if (!heading.id) {
          const base = this.slugify(heading.textContent) || 'rx-heading';
          heading.id = this.uniqueId(base);
        }

        if (heading.querySelector('.rx-heading-anchor')) {
          return;
        }

        const anchor = this.createEl('a', 'rx-heading-anchor', '#');
        anchor.href = `#${heading.id}`;
        anchor.setAttribute('aria-label', `Link to ${heading.textContent.trim()}`);
        anchor.title = 'Copy heading link';

        anchor.addEventListener('click', (event) => {
          event.preventDefault();
          const url = `${window.location.origin}${window.location.pathname}#${heading.id}`;
          this.copyText(url);
          this.showToast('Heading link copied');
          history.replaceState(null, '', `#${heading.id}`);
        });

        heading.appendChild(anchor);
      });
    },

    enableSmoothScrolling() {
      document.addEventListener('click', (event) => {
        const link = event.target.closest('a[href^="#"]');

        if (!link) {
          return;
        }

        const hash = link.getAttribute('href');

        if (!hash || hash === '#') {
          return;
        }

        const target = document.getElementById(hash.substring(1));

        if (!target) {
          return;
        }

        event.preventDefault();
        this.scrollToElement(target);
        history.pushState(null, '', hash);
      });
    },

    scrollToElement(element) {
      const top =
        window.scrollY +
        element.getBoundingClientRect().top -
        this.config.scrollOffset;

      window.scrollTo({
        top,
        behavior: 'smooth',
      });
    },

    enableActiveTocHighlight() {
      if (!('IntersectionObserver' in window)) {
        return;
      }

      const tocLinks = this.qsa('.rx-toc-link');

      if (!tocLinks.length || !this.state.headings.length) {
        return;
      }

      const observer = new IntersectionObserver(
        (entries) => {
          entries.forEach((entry) => {
            if (entry.isIntersecting) {
              this.setActiveToc(entry.target.id);
            }
          });
        },
        {
          rootMargin: '-20% 0px -70% 0px',
          threshold: 0,
        }
      );

      this.state.headings.forEach((heading) => observer.observe(heading));
    },

    setActiveToc(id) {
      if (!id || this.state.activeHeadingId === id) {
        return;
      }

      this.state.activeHeadingId = id;

      this.qsa('.rx-toc-link').forEach((link) => {
        link.classList.toggle('is-active', link.dataset.target === id);
      });
    },

    createBackToTopButton() {
      if (document.getElementById(this.config.backToTopId)) {
        return;
      }

      const button = this.createEl('button', 'rx-back-to-top', '↑');
      button.id = this.config.backToTopId;
      button.type = 'button';
      button.setAttribute('aria-label', 'Back to top');
      button.title = 'Back to top';

      button.addEventListener('click', () => {
        window.scrollTo({
          top: 0,
          behavior: 'smooth',
        });
      });

      document.body.appendChild(button);
    },

    updateBackToTopButton() {
      const button = document.getElementById(this.config.backToTopId);

      if (!button) {
        return;
      }

      button.classList.toggle('is-visible', window.scrollY > 500);
    },

    enableCopyCurrentLink() {
      document.addEventListener('click', (event) => {
        const button = event.target.closest('[data-rx-copy-link]');

        if (!button) {
          return;
        }

        event.preventDefault();
        this.copyText(window.location.href);
        this.showToast('Post link copied');
      });
    },

    enableNativeShare() {
      document.addEventListener('click', (event) => {
        const button = event.target.closest('[data-rx-share]');

        if (!button) {
          return;
        }

        event.preventDefault();

        const shareData = {
          title: document.title,
          text: this.getMetaDescription(),
          url: window.location.href,
        };

        if (navigator.share) {
          navigator.share(shareData).catch(() => {
            this.copyText(window.location.href);
            this.showToast('Link copied');
          });
        } else {
          this.copyText(window.location.href);
          this.showToast('Link copied');
        }
      });
    },

    getMetaDescription() {
      const meta = document.querySelector('meta[name="description"]');
      return meta ? meta.getAttribute('content') : '';
    },

    enableCopyCodeButtons() {
      const codeBlocks = this.qsa('pre');

      codeBlocks.forEach((pre) => {
        if (pre.classList.contains('rx-code-ready')) {
          return;
        }

        pre.classList.add('rx-code-ready');

        const button = this.createEl('button', 'rx-copy-code-button', 'Copy');
        button.type = 'button';
        button.setAttribute('aria-label', 'Copy code');

        button.addEventListener('click', () => {
          const code = pre.querySelector('code') || pre;
          this.copyText(code.innerText);
          button.textContent = 'Copied';

          window.setTimeout(() => {
            button.textContent = 'Copy';
          }, 1800);
        });

        pre.appendChild(button);
      });
    },

    fixExternalLinks() {
      const links = this.qsa('a[href]', this.state.content);
      const currentHost = window.location.hostname;

      links.forEach((link) => {
        try {
          const url = new URL(link.href);

          if (url.hostname && url.hostname !== currentHost) {
            link.setAttribute('target', '_blank');
            link.setAttribute('rel', 'nofollow noopener noreferrer external');
            link.classList.add('rx-external-link');

            if (!link.getAttribute('aria-label')) {
              link.setAttribute(
                'aria-label',
                `${link.textContent.trim()} opens in a new tab`
              );
            }
          }
        } catch (error) {
          // Invalid URL, ignore safely.
        }
      });
    },

    enableImageLightbox() {
      const images = this.qsa('img', this.state.content);

      if (!images.length) {
        return;
      }

      let overlay = document.querySelector('.rx-lightbox-overlay');

      if (!overlay) {
        overlay = this.createEl('div', 'rx-lightbox-overlay');
        overlay.setAttribute('role', 'dialog');
        overlay.setAttribute('aria-modal', 'true');
        overlay.setAttribute('aria-hidden', 'true');

        const close = this.createEl('button', 'rx-lightbox-close', '×');
        close.type = 'button';
        close.setAttribute('aria-label', 'Close image preview');

        const img = document.createElement('img');
        img.className = 'rx-lightbox-image';
        img.alt = '';

        const caption = this.createEl('div', 'rx-lightbox-caption');

        overlay.appendChild(close);
        overlay.appendChild(img);
        overlay.appendChild(caption);
        document.body.appendChild(overlay);

        close.addEventListener('click', () => this.closeLightbox());
        overlay.addEventListener('click', (event) => {
          if (event.target === overlay) {
            this.closeLightbox();
          }
        });
      }

      images.forEach((image) => {
        if (image.closest('a')) {
          return;
        }

        image.classList.add('rx-clickable-image');
        image.setAttribute('tabindex', '0');
        image.setAttribute('role', 'button');
        image.setAttribute('aria-label', 'Open image preview');

        image.addEventListener('click', () => this.openLightbox(image));
        image.addEventListener('keydown', (event) => {
          if (event.key === 'Enter' || event.key === ' ') {
            event.preventDefault();
            this.openLightbox(image);
          }
        });
      });
    },

    openLightbox(image) {
      const overlay = document.querySelector('.rx-lightbox-overlay');

      if (!overlay) {
        return;
      }

      const preview = overlay.querySelector('.rx-lightbox-image');
      const caption = overlay.querySelector('.rx-lightbox-caption');

      preview.src = image.currentSrc || image.src;
      preview.alt = image.alt || '';

      caption.textContent =
        image.getAttribute('data-caption') ||
        image.alt ||
        image.closest('figure')?.querySelector('figcaption')?.textContent ||
        '';

      overlay.setAttribute('aria-hidden', 'false');
      overlay.classList.add('is-open');
      document.body.classList.add('rx-lightbox-open');
    },

    closeLightbox() {
      const overlay = document.querySelector('.rx-lightbox-overlay');

      if (!overlay) {
        return;
      }

      overlay.setAttribute('aria-hidden', 'true');
      overlay.classList.remove('is-open');
      document.body.classList.remove('rx-lightbox-open');
    },

    lazyLoadIframes() {
      const iframes = this.qsa('iframe', this.state.content);

      iframes.forEach((iframe) => {
        iframe.loading = 'lazy';

        if (!iframe.getAttribute('title')) {
          iframe.setAttribute('title', 'Embedded content');
        }
      });
    },

    wrapResponsiveVideos() {
      const selectors = [
        'iframe[src*="youtube.com"]',
        'iframe[src*="youtu.be"]',
        'iframe[src*="vimeo.com"]',
        'iframe[src*="dailymotion.com"]',
      ];

      const videos = this.qsa(selectors.join(','), this.state.content);

      videos.forEach((video) => {
        if (video.parentElement && video.parentElement.classList.contains('rx-video-wrapper')) {
          return;
        }

        const wrapper = this.createEl('div', 'rx-video-wrapper');
        video.parentNode.insertBefore(wrapper, video);
        wrapper.appendChild(video);
      });
    },

    improveTables() {
      const tables = this.qsa('table', this.state.content);

      tables.forEach((table) => {
        if (table.parentElement && table.parentElement.classList.contains('rx-table-wrapper')) {
          return;
        }

        const wrapper = this.createEl('div', 'rx-table-wrapper');
        wrapper.setAttribute('tabindex', '0');
        wrapper.setAttribute('role', 'region');
        wrapper.setAttribute('aria-label', 'Scrollable table');

        table.parentNode.insertBefore(wrapper, table);
        wrapper.appendChild(table);
      });
    },

    enableFootnoteBacklinks() {
      const footnotes = this.qsa('a[href^="#fn"], a[href^="#footnote"]', this.state.content);

      footnotes.forEach((link) => {
        const targetId = link.getAttribute('href')?.substring(1);
        const target = targetId ? document.getElementById(targetId) : null;

        if (!target) {
          return;
        }

        const backlinkId = link.id || this.uniqueId('rx-footnote-ref');
        link.id = backlinkId;

        if (!target.querySelector('.rx-footnote-back')) {
          const back = this.createEl('a', 'rx-footnote-back', '↩ Back');
          back.href = `#${backlinkId}`;
          target.appendChild(document.createTextNode(' '));
          target.appendChild(back);
        }
      });
    },

    createPrintButton() {
      if (document.querySelector('[data-rx-print-created]')) {
        return;
      }

      const button = this.createEl('button', 'rx-print-button', 'Print');
      button.type = 'button';
      button.setAttribute('data-rx-print-created', 'true');
      button.setAttribute('aria-label', 'Print this article');

      button.addEventListener('click', () => {
        window.print();
      });

      this.insertIntoPostTools(button);
    },

    createFontSizeControls() {
      if (document.querySelector('.rx-font-size-controls')) {
        return;
      }

      const wrapper = this.createEl('div', 'rx-font-size-controls');

      const decrease = this.createEl('button', 'rx-font-size-button', 'A−');
      const reset = this.createEl('button', 'rx-font-size-button', 'A');
      const increase = this.createEl('button', 'rx-font-size-button', 'A+');

      decrease.type = 'button';
      reset.type = 'button';
      increase.type = 'button';

      decrease.setAttribute('aria-label', 'Decrease article font size');
      reset.setAttribute('aria-label', 'Reset article font size');
      increase.setAttribute('aria-label', 'Increase article font size');

      const applyFontSize = (size) => {
        this.state.content.style.fontSize = size ? `${size}px` : '';
        if (size) {
          localStorage.setItem(`${this.config.storagePrefix}font_size`, String(size));
        } else {
          localStorage.removeItem(`${this.config.storagePrefix}font_size`);
        }
      };

      const currentSaved = localStorage.getItem(`${this.config.storagePrefix}font_size`);

      if (currentSaved) {
        applyFontSize(parseInt(currentSaved, 10));
      }

      decrease.addEventListener('click', () => {
        const current = parseInt(window.getComputedStyle(this.state.content).fontSize, 10);
        applyFontSize(Math.max(current - 1, 14));
      });

      reset.addEventListener('click', () => {
        applyFontSize(null);
      });

      increase.addEventListener('click', () => {
        const current = parseInt(window.getComputedStyle(this.state.content).fontSize, 10);
        applyFontSize(Math.min(current + 1, 24));
      });

      wrapper.appendChild(decrease);
      wrapper.appendChild(reset);
      wrapper.appendChild(increase);

      this.insertIntoPostTools(wrapper);
    },

    createDarkReadingMode() {
      if (document.querySelector('[data-rx-reading-mode]')) {
        return;
      }

      const button = this.createEl('button', 'rx-reading-mode-button', 'Reading Mode');
      button.type = 'button';
      button.setAttribute('data-rx-reading-mode', 'true');
      button.setAttribute('aria-pressed', 'false');

      const saved = localStorage.getItem(`${this.config.storagePrefix}reading_mode`);

      if (saved === 'dark') {
        document.body.classList.add('rx-dark-reading-mode');
        button.setAttribute('aria-pressed', 'true');
      }

      button.addEventListener('click', () => {
        const enabled = document.body.classList.toggle('rx-dark-reading-mode');
        button.setAttribute('aria-pressed', enabled ? 'true' : 'false');

        if (enabled) {
          localStorage.setItem(`${this.config.storagePrefix}reading_mode`, 'dark');
        } else {
          localStorage.removeItem(`${this.config.storagePrefix}reading_mode`);
        }
      });

      this.insertIntoPostTools(button);
    },

    createStickyTools() {
      if (document.querySelector('.rx-sticky-post-tools')) {
        return;
      }

      const wrapper = this.createEl('div', 'rx-sticky-post-tools');
      wrapper.setAttribute('aria-label', 'Post tools');

      const copy = this.createEl('button', 'rx-sticky-tool', 'Copy Link');
      copy.type = 'button';
      copy.setAttribute('data-rx-copy-link', 'true');

      const share = this.createEl('button', 'rx-sticky-tool', 'Share');
      share.type = 'button';
      share.setAttribute('data-rx-share', 'true');

      const print = this.createEl('button', 'rx-sticky-tool', 'Print');
      print.type = 'button';
      print.addEventListener('click', () => window.print());

      wrapper.appendChild(copy);
      wrapper.appendChild(share);
      wrapper.appendChild(print);

      document.body.appendChild(wrapper);
    },

    insertIntoPostTools(element) {
      let tools = document.querySelector('.rx-post-tools');

      if (!tools) {
        tools = this.createEl('div', 'rx-post-tools');
        tools.setAttribute('aria-label', 'Article tools');

        const title = document.querySelector('.entry-title, .post-title, h1');

        if (title && title.parentNode) {
          title.parentNode.insertBefore(tools, title.nextSibling);
        } else {
          this.state.content.insertBefore(tools, this.state.content.firstChild);
        }
      }

      tools.appendChild(element);
    },

    createSelectionShare() {
      if (this.state.selectionPopup) {
        return;
      }

      const popup = this.createEl('div', 'rx-selection-popup');
      popup.setAttribute('aria-hidden', 'true');

      const copyButton = this.createEl('button', 'rx-selection-copy', 'Copy');
      copyButton.type = 'button';

      const shareButton = this.createEl('button', 'rx-selection-share', 'Share');
      shareButton.type = 'button';

      popup.appendChild(copyButton);
      popup.appendChild(shareButton);
      document.body.appendChild(popup);

      this.state.selectionPopup = popup;

      document.addEventListener('mouseup', () => {
        window.setTimeout(() => this.updateSelectionPopup(), 20);
      });

      document.addEventListener('keyup', () => {
        this.updateSelectionPopup();
      });

      document.addEventListener('click', (event) => {
        if (!popup.contains(event.target)) {
          popup.classList.remove('is-visible');
          popup.setAttribute('aria-hidden', 'true');
        }
      });

      copyButton.addEventListener('click', () => {
        const selected = window.getSelection().toString().trim();

        if (selected) {
          this.copyText(`${selected}\n\nSource: ${window.location.href}`);
          this.showToast('Selected text copied');
        }
      });

      shareButton.addEventListener('click', () => {
        const selected = window.getSelection().toString().trim();

        if (!selected) {
          return;
        }

        if (navigator.share) {
          navigator.share({
            title: document.title,
            text: selected,
            url: window.location.href,
          }).catch(() => {});
        } else {
          this.copyText(`${selected}\n\nSource: ${window.location.href}`);
          this.showToast('Selected text copied');
        }
      });
    },

    updateSelectionPopup() {
      const popup = this.state.selectionPopup;
      const selection = window.getSelection();

      if (!popup || !selection || selection.rangeCount === 0) {
        return;
      }

      const selectedText = selection.toString().trim();

      if (!selectedText || selectedText.length < 8) {
        popup.classList.remove('is-visible');
        popup.setAttribute('aria-hidden', 'true');
        return;
      }

      const range = selection.getRangeAt(0);
      const rect = range.getBoundingClientRect();

      if (!rect.width && !rect.height) {
        return;
      }

      popup.style.left = `${window.scrollX + rect.left + rect.width / 2}px`;
      popup.style.top = `${window.scrollY + rect.top - 45}px`;
      popup.classList.add('is-visible');
      popup.setAttribute('aria-hidden', 'false');
    },

    improveCommentForm() {
      const form = document.querySelector('#commentform');

      if (!form) {
        return;
      }

      const textarea = form.querySelector('textarea');

      if (textarea) {
        const counter = this.createEl('div', 'rx-comment-counter', '0 characters');
        textarea.parentNode.appendChild(counter);

        textarea.addEventListener('input', () => {
          counter.textContent = `${textarea.value.length} characters`;
        });
      }

      form.addEventListener('submit', (event) => {
        const comment = form.querySelector('textarea');

        if (comment && comment.value.trim().length < 3) {
          event.preventDefault();
          this.showToast('Please write a longer comment');
          comment.focus();
        }
      });
    },

    restoreReadingPosition() {
      const key = `${this.config.storagePrefix}position_${window.location.pathname}`;
      const saved = sessionStorage.getItem(key);

      if (!saved || window.location.hash) {
        return;
      }

      const position = parseInt(saved, 10);

      if (position > 300) {
        const restore = window.confirm('Do you want to continue reading from where you left?');

        if (restore) {
          window.scrollTo({
            top: position,
            behavior: 'smooth',
          });
        }
      }
    },

    saveReadingPosition() {
      const key = `${this.config.storagePrefix}position_${window.location.pathname}`;

      window.addEventListener(
        'beforeunload',
        () => {
          sessionStorage.setItem(key, String(window.scrollY));
        },
        { passive: true }
      );
    },

    addKeyboardShortcuts() {
      document.addEventListener('keydown', (event) => {
        const tag = document.activeElement?.tagName?.toLowerCase();

        if (tag === 'input' || tag === 'textarea' || tag === 'select') {
          return;
        }

        if (event.key === 'Escape') {
          this.closeLightbox();
        }

        if (event.key.toLowerCase() === 't') {
          window.scrollTo({
            top: 0,
            behavior: 'smooth',
          });
        }

        if (event.key.toLowerCase() === 'p' && event.ctrlKey) {
          return;
        }
      });
    },

    copyText(text) {
      if (navigator.clipboard && window.isSecureContext) {
        return navigator.clipboard.writeText(text).catch(() => {
          this.fallbackCopyText(text);
        });
      }

      this.fallbackCopyText(text);
      return Promise.resolve();
    },

    fallbackCopyText(text) {
      const textarea = document.createElement('textarea');
      textarea.value = text;
      textarea.setAttribute('readonly', '');
      textarea.style.position = 'fixed';
      textarea.style.top = '-9999px';
      textarea.style.left = '-9999px';

      document.body.appendChild(textarea);
      textarea.select();

      try {
        document.execCommand('copy');
      } catch (error) {
        console.warn('Copy failed', error);
      }

      document.body.removeChild(textarea);
    },

    showToast(message) {
      let toast = document.querySelector('.rx-toast');

      if (!toast) {
        toast = this.createEl('div', 'rx-toast');
        toast.setAttribute('role', 'status');
        toast.setAttribute('aria-live', 'polite');
        document.body.appendChild(toast);
      }

      toast.textContent = message;
      toast.classList.add('is-visible');

      window.clearTimeout(toast._rxTimeout);

      toast._rxTimeout = window.setTimeout(() => {
        toast.classList.remove('is-visible');
      }, 2200);
    },

    onScroll() {
      if (this.state.ticking) {
        return;
      }

      this.state.ticking = true;

      window.requestAnimationFrame(() => {
        this.updateReadingProgress();
        this.updateBackToTopButton();
        this.state.ticking = false;
      });
    },

    onResize() {
      this.updateReadingProgress();
    },
  };

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

Add this CSS also, otherwise many features will work but not look beautiful.

File:

rx-theme/inc/css/single-post.css

/* RX Theme Single Post UI */

.rx-reading-progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 99999;
  width: 100%;
  height: 4px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  pointer-events: none;
}

.rx-reading-time {
  margin: 10px 0 20px;
  font-size: 14px;
  opacity: 0.75;
}

.rx-table-of-contents {
  margin: 24px 0;
  padding: 20px;
  border: 1px solid rgba(0, 0, 0, 0.12);
  border-radius: 12px;
  background: rgba(0, 0, 0, 0.03);
}

.rx-toc-title {
  width: 100%;
  border: 0;
  background: transparent;
  font-weight: 700;
  font-size: 18px;
  text-align: left;
  cursor: pointer;
}

.rx-toc-list {
  margin: 16px 0 0;
  padding-left: 22px;
}

.rx-table-of-contents.is-collapsed .rx-toc-list {
  display: none;
}

.rx-toc-item {
  margin: 8px 0;
}

.rx-toc-level-3 {
  margin-left: 18px;
  font-size: 95%;
}

.rx-toc-link {
  text-decoration: none;
}

.rx-toc-link.is-active {
  font-weight: 700;
  text-decoration: underline;
}

.rx-heading-anchor {
  margin-left: 8px;
  font-size: 70%;
  opacity: 0;
  text-decoration: none;
}

h2:hover .rx-heading-anchor,
h3:hover .rx-heading-anchor,
h4:hover .rx-heading-anchor,
h5:hover .rx-heading-anchor,
h6:hover .rx-heading-anchor {
  opacity: 0.65;
}

.rx-back-to-top {
  position: fixed;
  right: 20px;
  bottom: 24px;
  z-index: 9999;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 999px;
  opacity: 0;
  visibility: hidden;
  cursor: pointer;
  transform: translateY(12px);
  transition: 0.25s ease;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}

.rx-back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.rx-post-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 16px 0;
}

.rx-post-tools button,
.rx-font-size-button,
.rx-print-button,
.rx-reading-mode-button,
.rx-sticky-tool,
.rx-copy-code-button,
.rx-selection-popup button {
  border: 1px solid rgba(0, 0, 0, 0.14);
  border-radius: 999px;
  padding: 8px 14px;
  background: #fff;
  cursor: pointer;
  font-size: 14px;
}

.rx-sticky-post-tools {
  position: fixed;
  left: 18px;
  top: 45%;
  z-index: 9998;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.rx-sticky-tool {
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14);
}

.rx-code-ready {
  position: relative;
}

.rx-copy-code-button {
  position: absolute;
  top: 8px;
  right: 8px;
}

.rx-external-link::after {
  content: "↗";
  margin-left: 4px;
  font-size: 80%;
}

.rx-clickable-image {
  cursor: zoom-in;
}

.rx-lightbox-overlay {
  position: fixed;
  inset: 0;
  z-index: 100000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(0, 0, 0, 0.86);
}

.rx-lightbox-overlay.is-open {
  display: flex;
}

.rx-lightbox-image {
  max-width: 92vw;
  max-height: 82vh;
  object-fit: contain;
}

.rx-lightbox-close {
  position: fixed;
  top: 18px;
  right: 22px;
  border: 0;
  background: transparent;
  color: #fff;
  font-size: 42px;
  cursor: pointer;
}

.rx-lightbox-caption {
  position: fixed;
  left: 24px;
  right: 24px;
  bottom: 18px;
  color: #fff;
  text-align: center;
}

.rx-lightbox-open {
  overflow: hidden;
}

.rx-video-wrapper {
  position: relative;
  width: 100%;
  padding-bottom: 56.25%;
  margin: 24px 0;
  overflow: hidden;
  border-radius: 12px;
}

.rx-video-wrapper iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.rx-table-wrapper {
  width: 100%;
  overflow-x: auto;
  margin: 24px 0;
}

.rx-table-wrapper table {
  min-width: 680px;
}

.rx-toast {
  position: fixed;
  left: 50%;
  bottom: 28px;
  z-index: 100001;
  padding: 10px 16px;
  border-radius: 999px;
  background: #111;
  color: #fff;
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, 14px);
  transition: 0.25s ease;
}

.rx-toast.is-visible {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
}

.rx-selection-popup {
  position: absolute;
  z-index: 100002;
  display: none;
  gap: 6px;
  padding: 6px;
  border-radius: 999px;
  background: #111;
  transform: translateX(-50%);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.25);
}

.rx-selection-popup.is-visible {
  display: flex;
}

.rx-selection-popup button {
  border-color: transparent;
  background: #fff;
}

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

.rx-dark-reading-mode {
  background: #111827;
  color: #f9fafb;
}

.rx-dark-reading-mode article,
.rx-dark-reading-mode .entry-content,
.rx-dark-reading-mode .post-content {
  color: #f9fafb;
}

.rx-dark-reading-mode a {
  color: #93c5fd;
}

.rx-dark-reading-mode .rx-table-of-contents {
  border-color: rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.06);
}

.rx-dark-reading-mode .rx-post-tools button,
.rx-dark-reading-mode .rx-font-size-button,
.rx-dark-reading-mode .rx-print-button,
.rx-dark-reading-mode .rx-reading-mode-button,
.rx-dark-reading-mode .rx-sticky-tool {
  background: #1f2937;
  color: #f9fafb;
  border-color: rgba(255, 255, 255, 0.18);
}

@media (max-width: 768px) {
  .rx-sticky-post-tools {
    left: 10px;
    right: 10px;
    top: auto;
    bottom: 78px;
    flex-direction: row;
    justify-content: center;
  }

  .rx-sticky-tool {
    padding: 7px 10px;
    font-size: 12px;
  }

  .rx-back-to-top {
    right: 14px;
    bottom: 18px;
  }

  .rx-table-of-contents {
    padding: 16px;
  }
}

@media print {
  .rx-reading-progress-bar,
  .rx-back-to-top,
  .rx-post-tools,
  .rx-sticky-post-tools,
  .rx-copy-code-button,
  .rx-selection-popup,
  .rx-toast,
  .rx-lightbox-overlay {
    display: none !important;
  }
}

Now enqueue both files from your functions.php or your enqueue file.

function rx_theme_single_post_assets() {
    if ( is_single() ) {
        wp_enqueue_style(
            'rx-single-post',
            get_template_directory_uri() . '/inc/css/single-post.css',
            array(),
            '1.0.0'
        );

        wp_enqueue_script(
            'rx-single-post',
            get_template_directory_uri() . '/inc/js/single-post.js',
            array(),
            '1.0.0',
            true
        );
    }
}
add_action( 'wp_enqueue_scripts', 'rx_theme_single_post_assets' );

This file will give your single post pages a very advanced reader experience: progress bar, table of contents, reading time, image popup, copy buttons, share tools, reading mode, font size controls, video responsiveness, table scroll support, and more.

Leave a Reply

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