@pageblock/utils
Version:
A modern utility library for PageBlock and Webflow, providing reusable components and utilities for web applications and Webflow sites
358 lines (357 loc) • 13.7 kB
JavaScript
const CONSTANTS = {
STORAGE_PREFIX: "pb_countdown_",
UPDATE_INTERVAL: 1e3,
ATTRIBUTES: {
COUNTDOWN: "data-pb-countdown",
DAILY: "data-pb-countdown-daily",
STORE: "data-pb-countdown-store",
ID: "data-pb-countdown-id",
MINUTES: "data-pb-countdown-minutes",
END: "data-pb-countdown-end",
SHOW: "data-pb-countdown-show",
HIDE: "data-pb-countdown-hide",
SHOW_TARGET: "data-pb-countdown-show-target",
MESSAGE_TARGET: "data-pb-countdown-message-target",
FREEZE: "data-pb-countdown-freeze"
},
EVENTS: {
COMPLETE: "countdown:complete",
PAUSE: "countdown:pause",
RESUME: "countdown:resume"
}
};
class Countdown {
/**
* @param {Object} options - Countdown options
* @param {boolean} [options.debug=false] - Enable debug mode
*/
constructor(options = {}) {
this.debug = options.debug || false;
this.countdowns = /* @__PURE__ */ new Map();
this.init();
}
/**
* Debug logging function
* @private
*/
log(...args) {
if (this.debug) {
console.log("[Countdown]", ...args);
}
}
/**
* Validate countdown configuration
* @private
* @param {CountdownConfig} config - Configuration to validate
* @returns {string[]} Array of error messages
*/
validateConfig(config) {
const errors = [];
if (config.storageKey && !config.id) {
errors.push("ID is required when storage is enabled");
}
if (config.expiresDaily && (config.messageTarget || config.storageKey)) {
errors.push("Daily countdown cannot use message target or storage features");
}
if (config.endDate && isNaN(new Date(config.endDate).getTime())) {
errors.push("Invalid end date format");
}
return errors;
}
init() {
const countdownElements = document.querySelectorAll("[data-pb-countdown]");
countdownElements.forEach((element) => this.setupCountdown(element));
}
setupCountdown(element) {
try {
if (!element) {
console.error("Invalid countdown element");
return;
}
if (element.hasAttribute("data-pb-countdown-freeze")) {
element.setAttribute("data-pb-countdown-store", "");
}
if (element.hasAttribute("data-pb-countdown-daily") && (element.hasAttribute("data-pb-countdown-message-target") || element.hasAttribute("data-pb-countdown-store"))) {
console.warn("Daily countdown cannot use message target or storage features");
element.removeAttribute("data-pb-countdown-message-target");
element.removeAttribute("data-pb-countdown-store");
}
if ((element.hasAttribute("data-pb-countdown-store") || element.hasAttribute("data-pb-countdown-freeze")) && !element.hasAttribute("data-pb-countdown-id")) {
console.error("Storage/freeze enabled but no ID provided. Add data-pb-countdown-id attribute.");
return;
}
const config = this.parseConfig(element);
const now = /* @__PURE__ */ new Date();
let endTime = this.getEndTime(config, now);
if (!endTime || isNaN(endTime.getTime())) {
console.error("Invalid end time configuration. Please check your date format or minutes value.");
return;
}
if (config.showDate) {
const showAtDate = new Date(config.showDate);
if (isNaN(showAtDate.getTime())) {
console.error("Invalid show date configuration. Please use a valid date format.");
return;
}
if (now < showAtDate) {
element.style.display = "none";
}
}
const countdown = {
element,
config,
endTime,
timer: null,
displayElement: element.querySelector("[data-pb-countdown-display]"),
unitElements: {
days: element.querySelector('[data-pb-countdown-unit="days"] [data-pb-countdown-display]'),
hours: element.querySelector('[data-pb-countdown-unit="hours"] [data-pb-countdown-display]'),
minutes: element.querySelector('[data-pb-countdown-unit="minutes"] [data-pb-countdown-display]'),
seconds: element.querySelector('[data-pb-countdown-unit="seconds"] [data-pb-countdown-display]')
}
};
if (!countdown.displayElement && !Object.values(countdown.unitElements).some((el) => el)) {
console.error("No display elements found");
return;
}
this.countdowns.set(element, countdown);
this.startCountdown(countdown);
} catch (error) {
console.error("Error setting up countdown:", error);
}
}
parseConfig(element) {
return {
settings: JSON.parse(element.getAttribute("data-pb-countdown-settings") || "{}"),
endDate: element.getAttribute("data-pb-countdown-end"),
startDate: element.getAttribute("data-pb-countdown-start"),
showDate: element.getAttribute("data-pb-countdown-show"),
hideOnComplete: element.getAttribute("data-pb-countdown-hide") !== null,
showTarget: element.getAttribute("data-pb-countdown-show-target"),
messageTarget: element.getAttribute("data-pb-countdown-message-target"),
expiresDaily: element.getAttribute("data-pb-countdown-daily") !== null,
minutes: parseInt(element.getAttribute("data-pb-countdown-minutes")) || null,
storageKey: element.getAttribute("data-pb-countdown-store") !== null,
id: element.getAttribute("data-pb-countdown-id"),
freeze: element.getAttribute("data-pb-countdown-freeze") !== null
};
}
getEndTime(config, now) {
let endTime;
if (config.storageKey && config.id) {
endTime = this.getStoredEndTime(config.id, now);
}
if (!endTime || isNaN(endTime.getTime())) {
endTime = this.calculateEndTime(config, now);
if (config.storageKey && config.id && endTime) {
this.storeEndTime(config.id, now, endTime);
}
}
return endTime;
}
getStoredEndTime(id, now) {
try {
const storedData = localStorage.getItem(`${CONSTANTS.STORAGE_PREFIX}${id}`);
if (storedData) {
const data = JSON.parse(storedData);
const endTime = new Date(data.endTime);
if (now >= endTime) {
localStorage.removeItem(`${CONSTANTS.STORAGE_PREFIX}${id}`);
return null;
}
return endTime;
}
} catch (error) {
console.warn("Error reading stored countdown data:", error);
}
return null;
}
storeEndTime(id, startTime, endTime) {
try {
localStorage.setItem(`${CONSTANTS.STORAGE_PREFIX}${id}`, JSON.stringify({
startTime: startTime.toISOString(),
endTime: endTime.toISOString()
}));
} catch (error) {
console.warn("Error storing countdown data:", error);
}
}
calculateEndTime(config, now) {
if (config.minutes) {
return new Date(now.getTime() + config.minutes * 6e4);
} else if (config.expiresDaily) {
const endTime = new Date(now);
endTime.setHours(23, 59, 59, 999);
return endTime;
} else if (config.endDate) {
const endTime = new Date(config.endDate);
return isNaN(endTime.getTime()) ? null : endTime;
}
return new Date(now.getTime() + 24 * 60 * 60 * 1e3);
}
startCountdown(countdown) {
const updateTimer = () => {
const now = /* @__PURE__ */ new Date();
const distance = countdown.endTime - now;
if (distance < 0) {
this.handleCountdownEnd(countdown);
return;
}
const timeUnits = this.calculateTimeUnits(distance);
this.updateDisplay(countdown, timeUnits);
if (countdown.config.freeze && countdown.config.storageKey && countdown.config.id) {
this.storeFrozenState(countdown.config.id, timeUnits);
}
};
if (countdown.config.freeze && countdown.config.storageKey && countdown.config.id) {
const frozenState = this.getFrozenState(countdown.config.id);
if (frozenState) {
const totalSeconds = frozenState.days * 24 * 60 * 60 + frozenState.hours * 60 * 60 + frozenState.minutes * 60 + frozenState.seconds;
countdown.endTime = new Date((/* @__PURE__ */ new Date()).getTime() + totalSeconds * 1e3);
this.updateDisplay(countdown, frozenState);
}
}
updateTimer();
countdown.timer = setInterval(updateTimer, 1e3);
}
calculateTimeUnits(distance) {
return {
days: Math.floor(distance / (1e3 * 60 * 60 * 24)),
hours: Math.floor(distance % (1e3 * 60 * 60 * 24) / (1e3 * 60 * 60)),
minutes: Math.floor(distance % (1e3 * 60 * 60) / (1e3 * 60)),
seconds: Math.floor(distance % (1e3 * 60) / 1e3)
};
}
updateDisplay(countdown, timeUnits) {
const { days, hours, minutes, seconds } = timeUnits;
if (Object.values(countdown.unitElements).some((el) => el)) {
if (countdown.unitElements.days) countdown.unitElements.days.textContent = days.toString().padStart(2, "0");
if (countdown.unitElements.hours) countdown.unitElements.hours.textContent = hours.toString().padStart(2, "0");
if (countdown.unitElements.minutes) countdown.unitElements.minutes.textContent = minutes.toString().padStart(2, "0");
if (countdown.unitElements.seconds) countdown.unitElements.seconds.textContent = seconds.toString().padStart(2, "0");
} else if (countdown.displayElement) {
let display = "";
if (days > 0) {
display = `${days.toString().padStart(2, "0")}:`;
}
display += `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
countdown.displayElement.textContent = display;
} else {
console.warn("No display elements found for countdown. Please add either a [data-pb-countdown-display] element or individual unit elements.");
}
}
handleCountdownEnd(countdown) {
clearInterval(countdown.timer);
if (countdown.config.hideOnComplete) {
countdown.element.style.display = "none";
}
if (countdown.config.showTarget) {
const targetElement = document.querySelector(countdown.config.showTarget);
if (targetElement) {
targetElement.style.display = "block";
countdown.element.style.display = "none";
}
}
if (countdown.config.messageTarget && !countdown.config.expiresDaily) {
const messageElement = document.querySelector(countdown.config.messageTarget);
if (messageElement) {
messageElement.style.display = "block";
}
}
if (countdown.config.storageKey && countdown.config.id) {
try {
if (!countdown.config.freeze) {
localStorage.removeItem(`${CONSTANTS.STORAGE_PREFIX}${countdown.config.id}`);
}
} catch (error) {
this.log("Error clearing stored countdown:", error);
}
}
if (countdown.config.expiresDaily) {
countdown.endTime = /* @__PURE__ */ new Date();
countdown.endTime.setHours(23, 59, 59, 999);
this.startCountdown(countdown);
} else {
this.countdowns.delete(countdown.element);
}
countdown.element.dispatchEvent(new CustomEvent("countdown:complete", {
detail: { countdown }
}));
}
/**
* Pause a specific countdown
* @param {string} countdownId - ID of the countdown to pause
*/
pauseCountdown(countdownId) {
const countdown = Array.from(this.countdowns.values()).find((c) => c.config.id === countdownId);
if (countdown) {
clearInterval(countdown.timer);
countdown.pausedAt = /* @__PURE__ */ new Date();
this.log(`Paused countdown: ${countdownId}`);
countdown.element.dispatchEvent(new CustomEvent(CONSTANTS.EVENTS.PAUSE, {
detail: { countdown }
}));
}
}
/**
* Resume a paused countdown
* @param {string} countdownId - ID of the countdown to resume
*/
resumeCountdown(countdownId) {
const countdown = Array.from(this.countdowns.values()).find((c) => c.config.id === countdownId);
if (countdown && countdown.pausedAt) {
const pauseDuration = /* @__PURE__ */ new Date() - countdown.pausedAt;
countdown.endTime = new Date(countdown.endTime.getTime() + pauseDuration);
delete countdown.pausedAt;
this.startCountdown(countdown);
this.log(`Resumed countdown: ${countdownId}`);
countdown.element.dispatchEvent(new CustomEvent(CONSTANTS.EVENTS.RESUME, {
detail: { countdown }
}));
}
}
storeFrozenState(id, timeUnits) {
try {
localStorage.setItem(`${CONSTANTS.STORAGE_PREFIX}${id}_frozen`, JSON.stringify(timeUnits));
} catch (error) {
console.warn("Error storing frozen state:", error);
}
}
getFrozenState(id) {
try {
const frozenData = localStorage.getItem(`${CONSTANTS.STORAGE_PREFIX}${id}_frozen`);
if (frozenData) {
return JSON.parse(frozenData);
}
} catch (error) {
console.warn("Error reading frozen state:", error);
}
return null;
}
destroy() {
this.countdowns.forEach((countdown) => {
clearInterval(countdown.timer);
if (countdown.config.storageKey && countdown.config.id) {
try {
if (!countdown.config.freeze) {
localStorage.removeItem(`${CONSTANTS.STORAGE_PREFIX}${countdown.config.id}`);
}
} catch (error) {
this.log("Error clearing stored countdown:", error);
}
}
countdown.element.removeEventListener(CONSTANTS.EVENTS.COMPLETE, null);
countdown.element.removeEventListener(CONSTANTS.EVENTS.PAUSE, null);
countdown.element.removeEventListener(CONSTANTS.EVENTS.RESUME, null);
countdown.displayElement = null;
countdown.unitElements = null;
});
this.countdowns.clear();
this.log("All countdowns destroyed");
}
}
export {
Countdown,
Countdown as default
};
//# sourceMappingURL=countdown.js.map