# notificationx/3.2.13/blocks/countdown/frontend.js

NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner &amp; Floating Notification Bar, version 3.2.13. 209 lines.

- Page: https://pluginprobe.com/plugins/notificationx/3.2.13/code/blocks/countdown/frontend.js
- Raw: https://pluginprobe.com/plugins/notificationx/3.2.13/raw/blocks/countdown/frontend.js
- Modified: 2026-06-24T12:26:04+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/notificationx/3.2.13/code/blocks/countdown/frontend.js#L10-L20`.

```javascript
/**
 * NotificationX Countdown Timer — Gutenberg block (frontend).
 *
 * Vanilla port of assets/public/js/nx-countdown.js (which is Elementor-only).
 * Initializes every block-rendered `.nx-countdown-wrapper` on the page and runs
 * the identical due-date / evergreen / expiry logic, without jQuery or Elementor.
 */
(function () {
  "use strict";

  function pad(n) {
    return String(n).padStart(2, "0");
  }

  function initCountdown(wrapper) {
    var widgetId = wrapper.getAttribute("data-countdown-id") || "";
    var countdownType = wrapper.getAttribute("data-countdown-type") || "due_date";
    var expireType = wrapper.getAttribute("data-expire-type") || "none";
    var expireTitle = wrapper.getAttribute("data-expiry-title") || "";
    var expireText = wrapper.getAttribute("data-expiry-text") || "";
    var redirectUrl = wrapper.getAttribute("data-redirect-url") || "";
    var evergreenTime = parseInt(wrapper.getAttribute("data-evergreen-time") || 0, 10);
    var recurring = wrapper.getAttribute("data-evergreen-recurring");
    var recurringStop = wrapper.getAttribute("data-evergreen-recurring-stop") || "";

    var list = wrapper.querySelector(".nx-countdown-container");
    if (!list) return;

    var daysEl = list.querySelector("[data-days]");
    var hoursEl = list.querySelector("[data-hours]");
    var minutesEl = list.querySelector("[data-minutes]");
    var secondsEl = list.querySelector("[data-seconds]");

    var targetTime;

    if (countdownType === "evergreen") {
      var storageKeyTime = "nx_countdown_evergreen_time_" + widgetId;
      var storageKeyInterval = "nx_countdown_evergreen_interval_" + widgetId;
      var storedTime = localStorage.getItem(storageKeyTime);
      var storedInterval = localStorage.getItem(storageKeyInterval);
      var HOUR_MS = 60 * 60 * 1000;

      if (
        storedTime === null ||
        storedInterval === null ||
        parseInt(storedInterval, 10) !== evergreenTime
      ) {
        storedTime = Date.now() + evergreenTime * 1000;
        localStorage.setItem(storageKeyInterval, String(evergreenTime));
        localStorage.setItem(storageKeyTime, String(storedTime));
      }

      storedTime = parseInt(storedTime, 10);

      if (recurring !== null && recurring !== "" && recurring !== "false") {
        var recurringAfterMs = parseFloat(recurring) * HOUR_MS;
        if (storedTime + recurringAfterMs < Date.now()) {
          storedTime = Date.now() + evergreenTime * 1000;
          localStorage.setItem(storageKeyTime, String(storedTime));
        }
        if (recurringStop) {
          var stopTs = new Date(recurringStop).getTime();
          if (!isNaN(stopTs) && stopTs < storedTime) {
            storedTime = stopTs;
          }
        }
      }

      targetTime = storedTime;
    } else {
      var dateStr = list.getAttribute("data-date") || "";
      targetTime = dateStr ? new Date(dateStr).getTime() : 0;
    }

    if (!targetTime || isNaN(targetTime)) return;

    var intervalId;

    function handleExpiry() {
      clearInterval(intervalId);
      if (expireType === "text") {
        list.innerHTML =
          '<div class="nx-countdown-finish-message">' +
          '<h4 class="nx-expiry-title">' +
          expireTitle +
          "</h4>" +
          '<div class="nx-expiry-text">' +
          expireText +
          "</div>" +
          "</div>";
      } else if (expireType === "url" && redirectUrl) {
        window.location.href = redirectUrl;
      }
    }

    function tick() {
      var diff = targetTime - Date.now();

      if (diff <= 0) {
        if (daysEl) daysEl.textContent = "00";
        if (hoursEl) hoursEl.textContent = "00";
        if (minutesEl) minutesEl.textContent = "00";
        if (secondsEl) secondsEl.textContent = "00";
        handleExpiry();
        return;
      }

      var days = Math.floor(diff / (1000 * 60 * 60 * 24));
      var hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
      var minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
      var seconds = Math.floor((diff % (1000 * 60)) / 1000);

      if (daysEl) daysEl.textContent = pad(days);
      if (hoursEl) hoursEl.textContent = pad(hours);
      if (minutesEl) minutesEl.textContent = pad(minutes);
      if (secondsEl) secondsEl.textContent = pad(seconds);
    }

    tick();
    intervalId = setInterval(tick, 1000);
  }

  function initAll() {
    var wrappers = document.querySelectorAll(".nx-countdown-wrapper");
    for (var i = 0; i < wrappers.length; i++) {
      var wrapper = wrappers[i];
      // Leave Elementor-rendered timers to the Elementor script.
      if (wrapper.closest && wrapper.closest(".elementor-widget")) continue;
      if (wrapper.getAttribute("data-nx-countdown-init")) continue;
      wrapper.setAttribute("data-nx-countdown-init", "1");
      initCountdown(wrapper);
    }
  }

  // Run once on initial page load.
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", initAll);
  } else {
    initAll();
  }

  // NotificationX injects press bars into the DOM after page load and calls
  // window.ebRunCountDown() once a bar is mounted. Expose initAll under that
  // hook so block countdowns inside a dynamically-injected bar — e.g. on the
  // preview screen, where the bar's assets aren't re-loaded — get initialized.
  // Chain any pre-existing hook (e.g. Essential Blocks) instead of clobbering
  // it; initAll() is idempotent via the data-nx-countdown-init guard.
  var prevRunCountDown =
    typeof window.ebRunCountDown === "function" ? window.ebRunCountDown : null;
  window.ebRunCountDown = function () {
    if (prevRunCountDown) {
      try {
        prevRunCountDown();
      } catch (e) {}
    }
    initAll();
  };

  // Fallback: watch for late-injected countdowns directly, in case a bar is
  // mounted without the hook firing. Guarded to a single observer per page and
  // scoped so it only does work when a countdown wrapper actually appears.
  if (
    !window.__nxCountdownObserverAttached &&
    typeof MutationObserver !== "undefined"
  ) {
    window.__nxCountdownObserverAttached = true;

    var hasCountdown = function (node) {
      if (!node || node.nodeType !== 1) return false;
      if (node.classList && node.classList.contains("nx-countdown-wrapper")) {
        return true;
      }
      return (
        typeof node.querySelector === "function" &&
        !!node.querySelector(".nx-countdown-wrapper")
      );
    };

    var pending = false;
    var observer = new MutationObserver(function (mutations) {
      if (pending) return;
      for (var i = 0; i < mutations.length; i++) {
        var added = mutations[i].addedNodes;
        for (var j = 0; j < added.length; j++) {
          if (hasCountdown(added[j])) {
            pending = true;
            setTimeout(function () {
              pending = false;
              initAll();
            }, 50);
            return;
          }
        }
      }
    });

    var startObserving = function () {
      if (document.body) {
        observer.observe(document.body, { childList: true, subtree: true });
      }
    };
    if (document.body) {
      startObserving();
    } else {
      document.addEventListener("DOMContentLoaded", startObserving);
    }
  }
})();

```
