chunk-037-marquee.js

I’ll build this as a standalone advanced marquee chunk for your RX Theme: safe, reusable, accessible, responsive, pauseable, and compatible with WordPress front-end loading.

Yes dear friend. Create this file:

assets/static-js/static-chunks-js/chunk-037-marquee.js

This is an advanced RX Theme marquee chunk. It supports auto marquee, hover pause, focus pause, drag pause, reduced-motion safety, RTL direction, duplicate-track animation, speed control, pause button, accessibility, mutation refresh, and WordPress-friendly initialization.

/*!
 * RX Theme - Advanced Marquee Chunk
 * File: assets/static-js/static-chunks-js/chunk-037-marquee.js
 * Version: 1.0.0
 * Author: RX Theme
 *
 * Usage HTML:
 *
 * <div class="rx-marquee" data-rx-marquee data-speed="60" data-direction="left" data-pause-on-hover="true">
 *   <div class="rx-marquee__track">
 *     <span class="rx-marquee__item">Breaking medical update</span>
 *     <span class="rx-marquee__item">Health news</span>
 *     <span class="rx-marquee__item">RX Harun</span>
 *   </div>
 * </div>
 */

(function () {
  'use strict';

  var RXThemeMarquee = {
    version: '1.0.0',
    selector: '[data-rx-marquee]',
    instances: [],
    resizeTimer: null,
    mutationObserver: null,
    defaults: {
      speed: 60,
      direction: 'left',
      pauseOnHover: true,
      pauseOnFocus: true,
      pauseOnTouch: true,
      pauseOnDrag: true,
      pauseOnHidden: true,
      duplicate: true,
      gap: 32,
      minItems: 2,
      reverseOnRtl: false,
      respectReducedMotion: true,
      ariaLive: 'off',
      button: false,
      buttonTextPause: 'Pause marquee',
      buttonTextPlay: 'Play marquee'
    }
  };

  function toBoolean(value, fallback) {
    if (value === undefined || value === null || value === '') {
      return fallback;
    }

    if (typeof value === 'boolean') {
      return value;
    }

    value = String(value).toLowerCase().trim();

    if (value === 'true' || value === '1' || value === 'yes' || value === 'on') {
      return true;
    }

    if (value === 'false' || value === '0' || value === 'no' || value === 'off') {
      return false;
    }

    return fallback;
  }

  function toNumber(value, fallback) {
    var number = Number(value);

    if (Number.isFinite(number)) {
      return number;
    }

    return fallback;
  }

  function clamp(number, min, max) {
    return Math.min(Math.max(number, min), max);
  }

  function getDatasetOptions(element) {
    return {
      speed: clamp(toNumber(element.getAttribute('data-speed'), RXThemeMarquee.defaults.speed), 5, 500),
      direction: element.getAttribute('data-direction') || RXThemeMarquee.defaults.direction,
      pauseOnHover: toBoolean(element.getAttribute('data-pause-on-hover'), RXThemeMarquee.defaults.pauseOnHover),
      pauseOnFocus: toBoolean(element.getAttribute('data-pause-on-focus'), RXThemeMarquee.defaults.pauseOnFocus),
      pauseOnTouch: toBoolean(element.getAttribute('data-pause-on-touch'), RXThemeMarquee.defaults.pauseOnTouch),
      pauseOnDrag: toBoolean(element.getAttribute('data-pause-on-drag'), RXThemeMarquee.defaults.pauseOnDrag),
      pauseOnHidden: toBoolean(element.getAttribute('data-pause-on-hidden'), RXThemeMarquee.defaults.pauseOnHidden),
      duplicate: toBoolean(element.getAttribute('data-duplicate'), RXThemeMarquee.defaults.duplicate),
      gap: clamp(toNumber(element.getAttribute('data-gap'), RXThemeMarquee.defaults.gap), 0, 300),
      minItems: clamp(toNumber(element.getAttribute('data-min-items'), RXThemeMarquee.defaults.minItems), 1, 20),
      reverseOnRtl: toBoolean(element.getAttribute('data-reverse-on-rtl'), RXThemeMarquee.defaults.reverseOnRtl),
      respectReducedMotion: toBoolean(element.getAttribute('data-respect-reduced-motion'), RXThemeMarquee.defaults.respectReducedMotion),
      ariaLive: element.getAttribute('data-aria-live') || RXThemeMarquee.defaults.ariaLive,
      button: toBoolean(element.getAttribute('data-button'), RXThemeMarquee.defaults.button),
      buttonTextPause: element.getAttribute('data-button-text-pause') || RXThemeMarquee.defaults.buttonTextPause,
      buttonTextPlay: element.getAttribute('data-button-text-play') || RXThemeMarquee.defaults.buttonTextPlay
    };
  }

  function prefersReducedMotion() {
    return window.matchMedia &&
      window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  }

  function isRTL(element) {
    var direction = window.getComputedStyle(element).direction;
    return direction === 'rtl' || document.documentElement.getAttribute('dir') === 'rtl';
  }

  function createElement(tag, className, attributes) {
    var element = document.createElement(tag);

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

    if (attributes) {
      Object.keys(attributes).forEach(function (key) {
        element.setAttribute(key, attributes[key]);
      });
    }

    return element;
  }

  function setStyles(element, styles) {
    Object.keys(styles).forEach(function (property) {
      element.style[property] = styles[property];
    });
  }

  function getTrack(element) {
    return element.querySelector('.rx-marquee__track');
  }

  function getItems(track) {
    return Array.prototype.slice.call(track.children).filter(function (child) {
      return !child.hasAttribute('data-rx-clone') && !child.classList.contains('rx-marquee__clone');
    });
  }

  function removeClones(track) {
    var clones = track.querySelectorAll('[data-rx-clone="true"], .rx-marquee__clone');

    Array.prototype.forEach.call(clones, function (clone) {
      clone.parentNode.removeChild(clone);
    });
  }

  function duplicateItems(track, options) {
    var originalItems = getItems(track);

    if (!originalItems.length) {
      return;
    }

    var cloneFragment = document.createDocumentFragment();
    var cloneCount = Math.max(options.minItems, originalItems.length);

    for (var i = 0; i < cloneCount; i++) {
      var item = originalItems[i % originalItems.length];
      var clone = item.cloneNode(true);

      clone.setAttribute('aria-hidden', 'true');
      clone.setAttribute('data-rx-clone', 'true');
      clone.classList.add('rx-marquee__clone');

      cloneFragment.appendChild(clone);
    }

    track.appendChild(cloneFragment);
  }

  function injectBaseStyles() {
    if (document.getElementById('rx-theme-marquee-style')) {
      return;
    }

    var style = document.createElement('style');
    style.id = 'rx-theme-marquee-style';

    style.textContent = [
      '.rx-marquee{',
      'position:relative;',
      'overflow:hidden;',
      'width:100%;',
      '--rx-marquee-gap:32px;',
      '--rx-marquee-duration:30s;',
      '--rx-marquee-distance:-50%;',
      '}',
      '.rx-marquee__viewport{',
      'overflow:hidden;',
      'width:100%;',
      '}',
      '.rx-marquee__track{',
      'display:flex;',
      'align-items:center;',
      'width:max-content;',
      'gap:var(--rx-marquee-gap);',
      'will-change:transform;',
      'animation:rxMarqueeMove var(--rx-marquee-duration) linear infinite;',
      '}',
      '.rx-marquee.is-paused .rx-marquee__track,',
      '.rx-marquee.is-reduced-motion .rx-marquee__track{',
      'animation-play-state:paused;',
      '}',
      '.rx-marquee.is-reduced-motion .rx-marquee__track{',
      'transform:none !important;',
      '}',
      '.rx-marquee__item{',
      'flex:0 0 auto;',
      'white-space:nowrap;',
      '}',
      '.rx-marquee__toggle{',
      'position:absolute;',
      'right:8px;',
      'top:50%;',
      'transform:translateY(-50%);',
      'z-index:2;',
      'cursor:pointer;',
      '}',
      '@keyframes rxMarqueeMove{',
      'from{transform:translate3d(0,0,0);}',
      'to{transform:translate3d(var(--rx-marquee-distance),0,0);}',
      '}'
    ].join('');

    document.head.appendChild(style);
  }

  function Marquee(element) {
    this.element = element;
    this.options = getDatasetOptions(element);
    this.track = null;
    this.viewport = null;
    this.button = null;
    this.isPaused = false;
    this.isDestroyed = false;
    this.isPointerDown = false;
    this.pointerStartX = 0;
    this.pointerCurrentX = 0;
    this.dragDistance = 0;
    this.visibilityHandler = null;
    this.boundHandlers = {};
    this.init();
  }

  Marquee.prototype.init = function () {
    if (!this.element || this.element.__rxMarqueeInitialized) {
      return;
    }

    injectBaseStyles();

    this.element.__rxMarqueeInitialized = true;
    this.element.classList.add('rx-marquee');

    this.prepareStructure();
    this.prepareAccessibility();
    this.setup();
    this.bindEvents();

    if (this.options.button) {
      this.createToggleButton();
    }

    if (this.options.respectReducedMotion && prefersReducedMotion()) {
      this.element.classList.add('is-reduced-motion');
      this.pause();
    }

    this.dispatch('rx-marquee:init');
  };

  Marquee.prototype.prepareStructure = function () {
    this.track = getTrack(this.element);

    if (!this.track) {
      this.track = createElement('div', 'rx-marquee__track');

      while (this.element.firstChild) {
        this.track.appendChild(this.element.firstChild);
      }

      this.element.appendChild(this.track);
    }

    if (!this.track.parentElement.classList.contains('rx-marquee__viewport')) {
      this.viewport = createElement('div', 'rx-marquee__viewport');
      this.track.parentNode.insertBefore(this.viewport, this.track);
      this.viewport.appendChild(this.track);
    } else {
      this.viewport = this.track.parentElement;
    }

    this.track.classList.add('rx-marquee__track');

    Array.prototype.forEach.call(this.track.children, function (child) {
      if (!child.classList.contains('rx-marquee__item') && !child.hasAttribute('data-rx-clone')) {
        child.classList.add('rx-marquee__item');
      }
    });
  };

  Marquee.prototype.prepareAccessibility = function () {
    this.element.setAttribute('role', this.element.getAttribute('role') || 'region');
    this.element.setAttribute('aria-live', this.options.ariaLive);
    this.element.setAttribute('data-rx-marquee-ready', 'true');

    if (!this.element.getAttribute('aria-label')) {
      this.element.setAttribute('aria-label', 'Scrolling content');
    }
  };

  Marquee.prototype.setup = function () {
    if (!this.track) {
      return;
    }

    removeClones(this.track);

    if (this.options.duplicate) {
      duplicateItems(this.track, this.options);
    }

    this.applyLayout();
  };

  Marquee.prototype.applyLayout = function () {
    var direction = this.options.direction;
    var rtl = isRTL(this.element);

    if (this.options.reverseOnRtl && rtl) {
      if (direction === 'left') {
        direction = 'right';
      } else if (direction === 'right') {
        direction = 'left';
      }
    }

    var distance = '-50%';

    if (direction === 'right') {
      distance = '50%';
    }

    var trackWidth = this.track.scrollWidth || 1;
    var speed = this.options.speed || RXThemeMarquee.defaults.speed;
    var duration = Math.max(trackWidth / speed, 4);

    this.element.style.setProperty('--rx-marquee-gap', this.options.gap + 'px');
    this.element.style.setProperty('--rx-marquee-duration', duration + 's');
    this.element.style.setProperty('--rx-marquee-distance', distance);

    setStyles(this.track, {
      gap: this.options.gap + 'px'
    });

    this.element.setAttribute('data-current-direction', direction);
    this.element.setAttribute('data-current-speed', String(speed));
  };

  Marquee.prototype.bindEvents = function () {
    var self = this;

    this.boundHandlers.mouseenter = function () {
      if (self.options.pauseOnHover) {
        self.pause();
      }
    };

    this.boundHandlers.mouseleave = function () {
      if (self.options.pauseOnHover) {
        self.play();
      }
    };

    this.boundHandlers.focusin = function () {
      if (self.options.pauseOnFocus) {
        self.pause();
      }
    };

    this.boundHandlers.focusout = function () {
      if (self.options.pauseOnFocus) {
        self.play();
      }
    };

    this.boundHandlers.pointerdown = function (event) {
      if (!self.options.pauseOnDrag && !self.options.pauseOnTouch) {
        return;
      }

      self.isPointerDown = true;
      self.pointerStartX = event.clientX || 0;
      self.pointerCurrentX = self.pointerStartX;
      self.dragDistance = 0;

      if (self.options.pauseOnTouch || self.options.pauseOnDrag) {
        self.pause();
      }
    };

    this.boundHandlers.pointermove = function (event) {
      if (!self.isPointerDown) {
        return;
      }

      self.pointerCurrentX = event.clientX || 0;
      self.dragDistance = Math.abs(self.pointerCurrentX - self.pointerStartX);

      if (self.dragDistance > 8) {
        self.element.classList.add('is-dragging');
      }
    };

    this.boundHandlers.pointerup = function () {
      if (!self.isPointerDown) {
        return;
      }

      self.isPointerDown = false;
      self.element.classList.remove('is-dragging');

      if (self.options.pauseOnTouch || self.options.pauseOnDrag) {
        self.play();
      }
    };

    this.boundHandlers.resize = function () {
      clearTimeout(RXThemeMarquee.resizeTimer);
      RXThemeMarquee.resizeTimer = setTimeout(function () {
        self.refresh();
      }, 150);
    };

    this.element.addEventListener('mouseenter', this.boundHandlers.mouseenter);
    this.element.addEventListener('mouseleave', this.boundHandlers.mouseleave);
    this.element.addEventListener('focusin', this.boundHandlers.focusin);
    this.element.addEventListener('focusout', this.boundHandlers.focusout);
    this.element.addEventListener('pointerdown', this.boundHandlers.pointerdown);
    window.addEventListener('pointermove', this.boundHandlers.pointermove);
    window.addEventListener('pointerup', this.boundHandlers.pointerup);
    window.addEventListener('resize', this.boundHandlers.resize);

    if (this.options.pauseOnHidden) {
      this.visibilityHandler = function () {
        if (document.hidden) {
          self.pause();
        } else {
          self.play();
        }
      };

      document.addEventListener('visibilitychange', this.visibilityHandler);
    }
  };

  Marquee.prototype.createToggleButton = function () {
    var self = this;

    if (this.button) {
      return;
    }

    this.button = createElement('button', 'rx-marquee__toggle', {
      type: 'button',
      'aria-pressed': 'false',
      'aria-label': this.options.buttonTextPause
    });

    this.button.textContent = '❚❚';

    this.button.addEventListener('click', function () {
      if (self.isPaused) {
        self.play();
      } else {
        self.pause();
      }
    });

    this.element.appendChild(this.button);
  };

  Marquee.prototype.updateButton = function () {
    if (!this.button) {
      return;
    }

    if (this.isPaused) {
      this.button.setAttribute('aria-pressed', 'true');
      this.button.setAttribute('aria-label', this.options.buttonTextPlay);
      this.button.textContent = '▶';
    } else {
      this.button.setAttribute('aria-pressed', 'false');
      this.button.setAttribute('aria-label', this.options.buttonTextPause);
      this.button.textContent = '❚❚';
    }
  };

  Marquee.prototype.pause = function () {
    if (this.isDestroyed) {
      return;
    }

    this.isPaused = true;
    this.element.classList.add('is-paused');
    this.updateButton();
    this.dispatch('rx-marquee:pause');
  };

  Marquee.prototype.play = function () {
    if (this.isDestroyed) {
      return;
    }

    if (this.options.respectReducedMotion && prefersReducedMotion()) {
      this.pause();
      return;
    }

    this.isPaused = false;
    this.element.classList.remove('is-paused');
    this.updateButton();
    this.dispatch('rx-marquee:play');
  };

  Marquee.prototype.refresh = function () {
    if (this.isDestroyed) {
      return;
    }

    this.options = getDatasetOptions(this.element);
    this.setup();

    if (this.isPaused) {
      this.pause();
    } else {
      this.play();
    }

    this.dispatch('rx-marquee:refresh');
  };

  Marquee.prototype.setSpeed = function (speed) {
    speed = clamp(toNumber(speed, this.options.speed), 5, 500);
    this.options.speed = speed;
    this.element.setAttribute('data-speed', String(speed));
    this.applyLayout();
    this.dispatch('rx-marquee:speed');
  };

  Marquee.prototype.setDirection = function (direction) {
    if (['left', 'right'].indexOf(direction) === -1) {
      return;
    }

    this.options.direction = direction;
    this.element.setAttribute('data-direction', direction);
    this.applyLayout();
    this.dispatch('rx-marquee:direction');
  };

  Marquee.prototype.destroy = function () {
    if (this.isDestroyed) {
      return;
    }

    this.isDestroyed = true;

    removeClones(this.track);

    this.element.removeEventListener('mouseenter', this.boundHandlers.mouseenter);
    this.element.removeEventListener('mouseleave', this.boundHandlers.mouseleave);
    this.element.removeEventListener('focusin', this.boundHandlers.focusin);
    this.element.removeEventListener('focusout', this.boundHandlers.focusout);
    this.element.removeEventListener('pointerdown', this.boundHandlers.pointerdown);
    window.removeEventListener('pointermove', this.boundHandlers.pointermove);
    window.removeEventListener('pointerup', this.boundHandlers.pointerup);
    window.removeEventListener('resize', this.boundHandlers.resize);

    if (this.visibilityHandler) {
      document.removeEventListener('visibilitychange', this.visibilityHandler);
    }

    if (this.button && this.button.parentNode) {
      this.button.parentNode.removeChild(this.button);
    }

    this.element.classList.remove('is-paused', 'is-dragging', 'is-reduced-motion');
    this.element.removeAttribute('data-rx-marquee-ready');
    this.element.__rxMarqueeInitialized = false;

    this.dispatch('rx-marquee:destroy');
  };

  Marquee.prototype.dispatch = function (name) {
    var event;

    try {
      event = new CustomEvent(name, {
        bubbles: true,
        detail: {
          instance: this,
          element: this.element,
          options: this.options
        }
      });
    } catch (error) {
      event = document.createEvent('CustomEvent');
      event.initCustomEvent(name, true, false, {
        instance: this,
        element: this.element,
        options: this.options
      });
    }

    this.element.dispatchEvent(event);
  };

  RXThemeMarquee.init = function (context) {
    context = context || document;

    var elements = context.querySelectorAll
      ? context.querySelectorAll(RXThemeMarquee.selector)
      : [];

    Array.prototype.forEach.call(elements, function (element) {
      if (!element.__rxMarqueeInitialized) {
        var instance = new Marquee(element);
        RXThemeMarquee.instances.push(instance);
      }
    });

    RXThemeMarquee.observeNewMarquees();
  };

  RXThemeMarquee.refresh = function () {
    RXThemeMarquee.instances.forEach(function (instance) {
      if (instance && !instance.isDestroyed) {
        instance.refresh();
      }
    });
  };

  RXThemeMarquee.pauseAll = function () {
    RXThemeMarquee.instances.forEach(function (instance) {
      if (instance && !instance.isDestroyed) {
        instance.pause();
      }
    });
  };

  RXThemeMarquee.playAll = function () {
    RXThemeMarquee.instances.forEach(function (instance) {
      if (instance && !instance.isDestroyed) {
        instance.play();
      }
    });
  };

  RXThemeMarquee.destroyAll = function () {
    RXThemeMarquee.instances.forEach(function (instance) {
      if (instance && !instance.isDestroyed) {
        instance.destroy();
      }
    });

    RXThemeMarquee.instances = [];
  };

  RXThemeMarquee.getInstance = function (element) {
    for (var i = 0; i < RXThemeMarquee.instances.length; i++) {
      if (RXThemeMarquee.instances[i].element === element) {
        return RXThemeMarquee.instances[i];
      }
    }

    return null;
  };

  RXThemeMarquee.observeNewMarquees = function () {
    if (RXThemeMarquee.mutationObserver || !window.MutationObserver) {
      return;
    }

    RXThemeMarquee.mutationObserver = new MutationObserver(function (mutations) {
      mutations.forEach(function (mutation) {
        Array.prototype.forEach.call(mutation.addedNodes, function (node) {
          if (!node || node.nodeType !== 1) {
            return;
          }

          if (node.matches && node.matches(RXThemeMarquee.selector)) {
            RXThemeMarquee.init(node.parentNode || document);
            return;
          }

          if (node.querySelector && node.querySelector(RXThemeMarquee.selector)) {
            RXThemeMarquee.init(node);
          }
        });
      });
    });

    RXThemeMarquee.mutationObserver.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
  };

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

  domReady(function () {
    RXThemeMarquee.init(document);
  });

  window.RXThemeMarquee = RXThemeMarquee;
})();

Example HTML

<div
  class="rx-marquee"
  data-rx-marquee
  data-speed="70"
  data-direction="left"
  data-gap="40"
  data-pause-on-hover="true"
  data-pause-on-focus="true"
  data-button="true"
  aria-label="Latest RX medical updates"
>
  <div class="rx-marquee__track">
    <span class="rx-marquee__item">Latest medical articles</span>
    <span class="rx-marquee__item">Evidence based health knowledge</span>
    <span class="rx-marquee__item">A global war against illness</span>
    <span class="rx-marquee__item">RX Harun medical website</span>
  </div>
</div>

Enqueue in functions.php

function rx_theme_enqueue_static_chunks() {
    wp_enqueue_script(
        'rx-chunk-037-marquee',
        get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-037-marquee.js',
        array(),
        '1.0.0',
        true
    );
}
add_action( 'wp_enqueue_scripts', 'rx_theme_enqueue_static_chunks' );

This file is safe as a static chunk JS because it only runs when it finds:

data-rx-marquee

So it will not affect other parts of your RX Theme.

Leave a Reply

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