import type { CountdownConfig, CountdownLabels, CountdownUnit, DayLabels, } from "./Countdown.shared"; import { BaseCountdownStatic, calculateState, countdownUnits, getVisibleUnits, normalizeDayLabels, } from "./Countdown.shared"; import { ATTR_COUNTDOWN_UNIT, CLASS_FLIP, SELECTOR_LABEL, SELECTOR_UNIT, SELECTOR_VALUE_LAYERS, } from "./constants"; interface CountdownLeaf { element: HTMLElement; unit: CountdownUnit; labels: HTMLElement[]; isFlipping: boolean; flipTimeoutId?: number; } const defaultDayLabels: DayLabels = { many: "dni", few: "dni", one: "deň", }; const defaultLabels: Required = { days: defaultDayLabels, hours: "hod", minutes: "min", seconds: "sek", }; const defaultConfig: CountdownConfig = { targetDate: "", units: [...countdownUnits], labels: defaultLabels, }; const FLIP_DURATION_MS = 600; const pad = (value: number): string => String(Math.max(0, value)).padStart(2, "0"); const getUnitLabel = ( unit: CountdownUnit, value: number, labels: Required, ): string => { if (unit !== "days") { return labels[unit]; } const { many, few, one } = normalizeDayLabels(labels.days, defaultDayLabels); if (value === 1) return one; if (value >= 2 && value <= 4) return few; return many; }; export default class CountdownStatic extends BaseCountdownStatic { leaves!: Map; constructor(element: HTMLElement, config?: Partial) { super(element, config, { defaultConfig, instanceKey: "ODS_Countdown", }); this.leaves = new Map(); this.init(); } private getLabels(): Required { return { ...defaultLabels, ...(this.config.labels ?? {}), days: normalizeDayLabels(this.config.labels?.days, defaultDayLabels), }; } protected collectElements(): void { this.leaves.clear(); const visibleUnits = getVisibleUnits(this.config.units); Array.from( this.element.querySelectorAll(SELECTOR_UNIT), ).forEach((leaf) => { const unit = leaf.getAttribute( ATTR_COUNTDOWN_UNIT, ) as CountdownUnit | null; if (!unit || !visibleUnits.includes(unit)) return; this.leaves.set(unit, { element: leaf, unit, labels: Array.from(leaf.querySelectorAll(SELECTOR_VALUE_LAYERS)), isFlipping: false, }); }); } private setLeafValue(leaf: CountdownLeaf, value: string): void { leaf.labels.forEach((label) => { label.replaceChildren(document.createTextNode(value)); }); } private flipLeaf(leaf: CountdownLeaf, value: string): void { const [topLabel, frontLabel, backLabel, bottomLabel] = leaf.labels; if (!topLabel || !frontLabel || !backLabel || !bottomLabel) { return; } if (!topLabel.textContent || topLabel.textContent === "00") { this.setLeafValue(leaf, value); return; } if (leaf.isFlipping || topLabel.textContent === value) { return; } if (leaf.flipTimeoutId) { window.clearTimeout(leaf.flipTimeoutId); } leaf.isFlipping = true; topLabel.replaceChildren(document.createTextNode(value)); backLabel.replaceChildren(document.createTextNode(value)); leaf.element.classList.add(CLASS_FLIP); leaf.flipTimeoutId = window.setTimeout(() => { frontLabel.replaceChildren(document.createTextNode(value)); bottomLabel.replaceChildren(document.createTextNode(value)); leaf.element.classList.remove(CLASS_FLIP); leaf.isFlipping = false; leaf.flipTimeoutId = undefined; }, FLIP_DURATION_MS); } protected render(): void { const units = getVisibleUnits(this.config.units); const labels = this.getLabels(); const state = calculateState(this.config.targetDate, units); units.forEach((unit) => { const leaf = this.leaves.get(unit); if (!leaf) { return; } const value = state[unit]; const label = getUnitLabel(unit, value, labels); this.flipLeaf(leaf, pad(value)); const labelElement = leaf.element.querySelector(SELECTOR_LABEL); if (labelElement) { labelElement.textContent = label; } }); if (this.liveRegion) { const announcement = units .map( (unit) => `${state[unit]} ${getUnitLabel(unit, state[unit], labels)}`, ) .join(", "); this.liveRegion.textContent = announcement; } } protected destroyElements(): void { this.leaves.forEach((leaf) => { if (leaf.flipTimeoutId) { window.clearTimeout(leaf.flipTimeoutId); leaf.flipTimeoutId = undefined; } leaf.isFlipping = false; leaf.element.classList.remove(CLASS_FLIP); }); } static getInstance(el: HTMLElement): CountdownStatic | null { return el && (el as any).ODS_Countdown ? (el as any).ODS_Countdown : null; } } export type { CountdownConfig, CountdownLabels, CountdownUnit }; export { countdownUnits };