diff --git a/src/wc-timer.js b/src/wc-timer.js
index c9a3c30..faf20e5 100644
--- a/src/wc-timer.js
+++ b/src/wc-timer.js
@@ -1,94 +1,69 @@
/**
- * wc-timer.js — Web Component
- * ---------------------------------------------
- * A tiny countdown/uptimer that renders as plain text
- * (light DOM). Text is selectable and styles with normal CSS.
- *
- * Features:
- * - Presets: auto | hms | dhms | ydhms | date
- * - Auto locale (with override via locale/lang attrib)
- * - Uses Intl.DurationFormat when available (fallback included)
- * - “Compact” mode hides empty high units (e.g., 0 years)
- * - One global, synced tick for all instances (no drift, low CPU)
- * - Public JS API (properties): target, preset, labels, compact, locale
- *
- * Usage (HTML):
- *
- *
- *
- *
- * Styling:
- * wc-timer { font-size: 20px; color: #4da3ff; }
- *
- * JS control:
- * const t = document.querySelector('wc-timer');
- * t.target = '2025-10-12T00:30:00Z'; // string | Date | ms number
- * t.preset = 'dhms'; // 'auto' | 'hms' | 'dhms' | 'ydhms' | 'date'
- * t.labels = 'short'; // 'none' | 'short' | 'long'
- * t.compact = true; // boolean
- * t.locale = 'pl'; // locale override
- *
- * Notes:
- * - Don’t mutate private fields (those starting with “_”).
- * Use public setters (e.g., el.target = ...).
- * - “auto” shows HH:MM:SS for near targets and adds days for far targets.
- * - If DurationFormat is missing, we use a small fallback with plural rules.
+ * wc-timer.js — Web Component (Refactored)
*/
-/* ---------------- Global synced tick ----------------
- * One scheduler for all instances. It fires once per second,
- * aligned to the next real second boundary.
- * Saves CPU and avoids per-element setInterval drift.
+/* ---------------- Memoizacja Intl ----------------
+ * Obiekty Intl są kosztowne. Tworzymy je raz i używamy wielokrotnie.
+ * Kluczem cache jest locale i zserializowane opcje.
*/
+const IntlCache = (() => {
+ const cache = new Map();
+
+ function get(Ctor, locale, opts = {}) {
+ // Tworzymy unikalny klucz dla kombinacji konstruktora, języka i opcji.
+ const key = `${Ctor.name}:${locale}:${JSON.stringify(opts)}`;
+ if (!cache.has(key)) {
+ cache.set(key, new Ctor(locale, opts));
+ }
+ return cache.get(key);
+ }
+
+ return {
+ dtf: (loc, opts) => get(Intl.DateTimeFormat, loc, opts),
+ pr: (loc) => get(Intl.PluralRules, loc),
+ df: (loc, opts) => get(Intl.DurationFormat, loc, opts),
+ // Sprawdzamy wsparcie tylko raz
+ hasIDF: typeof Intl !== 'undefined' && 'DurationFormat' in Intl
+ };
+})();
+
+/* ---------------- Global synced tick ---------------- */
const TickHub = (() => {
const subs = new Set();
let timer = null;
function scheduleNext() {
- // Align next tick to the next full second.
+ if (subs.size === 0) return;
const now = performance.now();
+ // Wyrównanie do pełnej sekundy
const msToNext = 1000 - (Math.floor(now) % 1000);
+
timer = setTimeout(() => {
const t = Date.now();
- // Notify all subscribers. Errors are isolated.
- subs.forEach(cb => { try { cb(t); } catch { } });
+ // Kopiujemy Set, aby uniknąć problemów przy dodawaniu/usuwaniu w trakcie pętli
+ for (const cb of [...subs]) {
+ try { cb(t); } catch (e) { console.error(e); }
+ }
scheduleNext();
}, msToNext);
}
return {
- /**
- * sub(cb): subscribe to ticks.
- * Returns an unsubscribe function.
- */
sub(cb) {
- if (subs.size === 0) scheduleNext();
subs.add(cb);
+ if (!timer) scheduleNext();
return () => {
subs.delete(cb);
- if (!subs.size && timer) { clearTimeout(timer); timer = null; }
+ if (subs.size === 0 && timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
};
}
};
})();
-/* ---------------- Locale helpers ----------------
- * We prefer: explicit element attribute -> nearest [lang] ->
- * -> navigator.languages[0] -> navigator.language -> 'en'
- */
-const hasIDF = typeof Intl !== 'undefined' && 'DurationFormat' in Intl;
-
-function resolveLocale(el) {
- const attr = el.getAttribute('locale') || el.getAttribute('lang');
- const tree = el.closest?.('[lang]')?.getAttribute('lang') || document.documentElement.lang;
- const nav = Array.isArray(navigator.languages) && navigator.languages.length
- ? navigator.languages[0]
- : navigator.language;
- return (attr || tree || nav || 'en').toLowerCase();
-}
-
-// Fallback label dictionaries (English + Polish).
-// Used only when Intl.DurationFormat is not available.
+/* ---------------- Locale & Helpers ---------------- */
const LABELS = {
en: {
y: { long: { one: 'year', other: 'years' }, short: 'y' },
@@ -107,348 +82,318 @@ const LABELS = {
s: { long: { one: 'sekunda', few: 'sekundy', many: 'sekund', other: 'sekund' }, short: 's' },
}
};
-const clampLocale = loc => (LABELS[loc.slice(0, 2)] ? loc.slice(0, 2) : 'en');
-/* ---------------- Diff with smart collapsing ----------------
- * We compute calendar years/months only if those units are shown.
- * If not shown, their time “falls down” into lower units (days/hours/...).
- */
-function extractYearsMonthsCalendar(a, b, wantY, wantM) {
+function resolveLocale(el) {
+ const attr = el.getAttribute('locale') || el.getAttribute('lang');
+ if (attr) return attr.toLowerCase();
+ return (navigator.language || 'en').toLowerCase();
+}
+
+/* ---------------- Algorytmy dat ---------------- */
+
+// Obliczanie różnicy kalendarzowej bez pętli (O(1))
+function diffCalendar(a, b, wantY, wantM) {
let anchor = new Date(a);
- let y = 0, M = 0;
+ let y = 0;
+ let M = 0;
- // Add full years while not passing end.
if (wantY) {
- while (new Date(
- anchor.getFullYear() + 1, anchor.getMonth(), anchor.getDate(),
- anchor.getHours(), anchor.getMinutes(), anchor.getSeconds()
- ) <= b) { anchor.setFullYear(anchor.getFullYear() + 1); y++; }
+ y = b.getFullYear() - anchor.getFullYear();
+ // Sprawdzamy, czy "przeskoczyliśmy" datę końcową
+ const probe = new Date(anchor);
+ probe.setFullYear(anchor.getFullYear() + y);
+ if (probe > b) y--;
+ anchor.setFullYear(anchor.getFullYear() + y);
}
- // Then add full months while not passing end.
if (wantM) {
- while (new Date(
- anchor.getFullYear(), anchor.getMonth() + 1, anchor.getDate(),
- anchor.getHours(), anchor.getMinutes(), anchor.getSeconds()
- ) <= b) { anchor.setMonth(anchor.getMonth() + 1); M++; }
+ // Różnica w miesiącach całkowitych
+ M = (b.getFullYear() - anchor.getFullYear()) * 12 + (b.getMonth() - anchor.getMonth());
+ const probe = new Date(anchor);
+ probe.setMonth(anchor.getMonth() + M);
+ if (probe > b) M--;
+ anchor.setMonth(anchor.getMonth() + M);
}
return { y, M, anchor };
}
function collapseForUnits(start, end, show) {
- const { y, M, anchor } = extractYearsMonthsCalendar(start, end, !!show.y, !!show.M);
-
- // If we showed Y/M, base the remaining ms on the anchor (after adding Y/M).
- // Otherwise, consume everything from the start.
+ const { y, M, anchor } = diffCalendar(start, end, !!show.y, !!show.M);
+
+ // Jeśli liczyliśmy Y/M, resztę liczymy od 'anchor'. Jeśli nie, od 'start'.
const base = (show.y || show.M) ? anchor : start;
let rest = Math.max(0, end - base);
- // Greedy consume into D/H/m/s only for units we plan to display.
- const dayMs = 86400000, hourMs = 3600000, minMs = 60000, secMs = 1000;
- let d = 0, h = 0, m = 0, s = 0;
+ const MS = { d: 86400000, h: 3600000, m: 60000, s: 1000 };
+ const res = { years: y, months: M, days: 0, hours: 0, minutes: 0, seconds: 0 };
- if (show.d) { d = Math.floor(rest / dayMs); rest -= d * dayMs; }
- if (show.h) { h = Math.floor(rest / hourMs); rest -= h * hourMs; }
- if (show.m) { m = Math.floor(rest / minMs); rest -= m * minMs; }
- if (show.s) { s = Math.floor(rest / secMs); rest -= s * secMs; }
+ if (show.d) { res.days = Math.floor(rest / MS.d); rest %= MS.d; }
+ if (show.h) { res.hours = Math.floor(rest / MS.h); rest %= MS.h; }
+ if (show.m) { res.minutes = Math.floor(rest / MS.m); rest %= MS.m; }
+ if (show.s) { res.seconds = Math.floor(rest / MS.s); }
- return { years: y, months: M, days: d, hours: h, minutes: m, seconds: s };
+ return res;
}
-/* ---------------- Formatters ----------------
- * pad2: always pad for H/M/S “digital” look.
- * formatHMS: joins visible HMS with ':'.
- * formatDurationSmart: uses DurationFormat if available; otherwise fallback.
- */
-function pad2(n) { return String(n ?? 0).padStart(2, '0'); }
-
-function formatHMS(show, p) {
- const segs = [];
- if (show.h) segs.push(pad2(p.hours));
- if (show.m) segs.push(pad2(p.minutes));
- if (show.s) segs.push(pad2(p.seconds));
- return segs.join(':');
-}
+/* ---------------- Formatter ---------------- */
+const pad2 = (n) => String(n ?? 0).padStart(2, '0');
function formatDurationSmart(locale, labelsMode, show, parts) {
- const lang2 = LABELS[locale.slice(0, 2)] ? locale.slice(0, 2) : 'en';
-
- // labels="none" → head units as numbers, HMS as HH:MM:SS
+ // Tryb: none (czysty tekst z dwukropkami)
if (labelsMode === 'none') {
const head = [];
- if (show.y) head.push(String(parts.years));
- if (show.M) head.push(String(parts.months));
- if (show.d) head.push(String(parts.days));
- const tail = formatHMS(show, parts);
- return [head.join(' '), tail].filter(Boolean).join(head.length && tail ? ' ' : '');
+ if (show.y) head.push(parts.years);
+ if (show.M) head.push(parts.months);
+ if (show.d) head.push(parts.days);
+
+ const hms = [pad2(parts.hours), pad2(parts.minutes), pad2(parts.seconds)];
+ if (!show.h) hms.shift(); // jeśli ukryte godziny, usuń
+
+ return [...head, hms.join(':')].join(' ');
}
- // Prefer native DurationFormat, it handles pluralization and spacing.
- if (hasIDF) {
- const style = labelsMode === 'short' ? 'short' : 'long';
- const opts = { style };
+ // Tryb: Intl.DurationFormat (jeśli dostępny)
+ if (IntlCache.hasIDF) {
+ const opts = { style: labelsMode === 'short' ? 'short' : 'long' };
if (show.h) opts.hours = '2-digit';
if (show.m) opts.minutes = '2-digit';
if (show.s) opts.seconds = '2-digit';
- const df = new Intl.DurationFormat(locale, opts);
+ // Przekazujemy tylko niezerowe/wymagane jednostki
const inParts = {};
- if (show.y) inParts.years = parts.years;
- if (show.M) inParts.months = parts.months;
- if (show.d) inParts.days = parts.days;
- if (show.h) inParts.hours = parts.hours;
- if (show.m) inParts.minutes = parts.minutes;
- if (show.s) inParts.seconds = parts.seconds;
- return df.format(inParts);
+ for (const key in parts) {
+ // Mapowanie kluczy (years -> years) jest 1:1 w tym przypadku
+ if (show[key.slice(0, 1)] || show[key.slice(0, 2)]) inParts[key] = parts[key];
+ }
+ return IntlCache.df(locale, opts).format(inParts);
}
- // Fallback: plural rules + our dictionaries
- const PR = new Intl.PluralRules(locale);
+ // Tryb: Fallback (słownikowy)
+ const lang2 = LABELS[locale.slice(0, 2)] ? locale.slice(0, 2) : 'en';
const L = LABELS[lang2];
+ const pr = IntlCache.pr(locale);
+ const out = [];
- const head = [];
- const push = (key, val) => {
- const cat = PR.select(val);
- const word = L[key].long[cat] ?? L[key].long.other;
- head.push(labelsMode === 'short' ? `${val} ${L[key].short}` : `${val} ${word}`);
+ const add = (k, v, forcePad = false) => {
+ const cat = pr.select(v);
+ const word = L[k].long[cat] ?? L[k].long.other;
+ const num = forcePad ? pad2(v) : v;
+ const suffix = labelsMode === 'short' ? L[k].short : word;
+ out.push(`${num} ${suffix}`);
};
- if (show.y) push('y', parts.years);
- if (show.M) push('M', parts.months);
- if (show.d) push('d', parts.days);
- const hms = [];
- const pushH = (key, val) => {
- const cat = PR.select(val);
- const word = L[key].long[cat] ?? L[key].long.other;
- if (labelsMode === 'short') hms.push(`${pad2(val)} ${L[key].short}`);
- else hms.push(`${pad2(val)} ${word}`);
- };
- if (show.h) pushH('h', parts.hours);
- if (show.m) pushH('m', parts.minutes);
- if (show.s) pushH('s', parts.seconds);
+ if (show.y) add('y', parts.years);
+ if (show.M) add('M', parts.months);
+ if (show.d) add('d', parts.days);
+ if (show.h) add('h', parts.hours, true);
+ if (show.m) add('m', parts.minutes, true);
+ if (show.s) add('s', parts.seconds, true);
- return [head.join(' '), hms.join(' ')].filter(Boolean).join(head.length && hms.length ? ' ' : '');
+ return out.join(' ');
}
-/* ---------------- Presets ----------------
- * hms → HH:MM:SS
- * dhms → D + HH:MM:SS
- * ydhms → Y + M + D + HH:MM:SS (calendar-aware)
- * auto → HH:MM:SS when near; D + HH:MM:SS when far (>= 30 days)
- * default → dhms
- */
-function unitsForPreset(preset, start, end) {
- if (preset === 'hms') return { y: false, M: false, d: false, h: true, m: true, s: true };
- if (preset === 'dhms') return { y: false, M: false, d: true, h: true, m: true, s: true };
- if (preset === 'ydhms') return { y: true, M: true, d: true, h: true, m: true, s: true };
- if (preset === 'auto') {
- const diff = Math.max(0, end - start);
- const dayMs = 86400000;
- return diff >= 30 * dayMs
- ? { y: false, M: false, d: true, h: true, m: true, s: true }
- : { y: false, M: false, d: false, h: true, m: true, s: true };
+function getShowConfig(preset, diffMs) {
+ const base = { y: false, M: false, d: true, h: true, m: true, s: true };
+
+ switch (preset) {
+ case 'hms': return { ...base, d: false };
+ case 'dhms': return base;
+ case 'ydhms': return { y: true, M: true, d: true, h: true, m: true, s: true };
+ case 'auto':
+ // Jeśli więcej niż 30 dni, pokaż dni.
+ return diffMs >= 2592000000 ? base : { ...base, d: false };
+ default: return base;
}
- return { y: false, M: false, d: true, h: true, m: true, s: true };
}
-/* ---------------- Web Component ----------------
- * Light-DOM: visible text is a single Text node inside .
- * We never replace that node; we only update its .data → selection is stable.
- */
+/* ---------------- Web Component ---------------- */
+// Style współdzielone (wydajność pamięci)
+const sheet = new CSSStyleSheet();
+sheet.replaceSync(`
+ :host {
+ display:inline-block;
+ color: inherit;
+ user-select: text; -webkit-user-select: text;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+ }
+`);
+
class WcTimer extends HTMLElement {
static get observedAttributes() { return ['target', 'preset', 'labels', 'locale', 'lang', 'compact']; }
+
+ // Pola prywatne
+ #target = new Date();
+ #preset = 'auto';
+ #labels = 'short';
+ #compact = false;
+ #locale = 'en';
+ #unsub = null;
+ #textNode = null;
+ #isAttrUpdate = false;
constructor() {
super();
- this.attachShadow({ mode: 'open' });
-
- // Shadow only defines style and a default slot.
- // Text is in light DOM (selectable, styleable).
- const style = document.createElement('style');
- style.textContent = `
- :host {
- display:inline-block;
- color: inherit;
- user-select: text; -webkit-user-select: text;
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
- font-variant-numeric: tabular-nums;
- white-space: nowrap;
- }
- ::slotted(*) { all: unset; }
- `;
- const slot = document.createElement('slot');
- this.shadowRoot.append(style, slot);
-
- // Single managed text node in light DOM.
- this._text = null;
-
- // Internal state.
- this._unsub = null;
- this._updatingAttr = false; // guard against attribute echo from setters
- this._target = new Date();
- this._preset = 'auto';
- this._labels = 'short'; // 'none' | 'short' | 'long'
- this._compact = false;
- this._locale = 'en';
+ const shadow = this.attachShadow({ mode: 'open' });
+ shadow.adoptedStyleSheets = [sheet];
+ shadow.appendChild(document.createElement('slot'));
}
connectedCallback() {
- // Ensure the first child is our Text node.
- if (!this._text) {
- if (this.firstChild && this.firstChild.nodeType === Node.TEXT_NODE) {
- this._text = this.firstChild;
- } else {
- this.textContent = '';
- this._text = document.createTextNode('');
- this.appendChild(this._text);
- }
+ // Inicjalizacja węzła tekstowego w Light DOM
+ if (!this.firstChild || this.firstChild.nodeType !== Node.TEXT_NODE) {
+ this.textContent = '';
+ this.#textNode = document.createTextNode('');
+ this.appendChild(this.#textNode);
+ } else {
+ this.#textNode = this.firstChild;
}
- this._syncFromAttrs();
- this._renderOnce(Date.now());
- this._unsub = TickHub.sub(t => this._renderOnce(t));
+ this.#syncAttrs();
+ this.#startTicking();
}
disconnectedCallback() {
- if (this._unsub) { this._unsub(); this._unsub = null; }
+ this.#stopTicking();
}
attributeChangedCallback() {
- // Ignore updates triggered by our own setters.
- if (this._updatingAttr) return;
- this._syncFromAttrs();
- this._renderOnce(Date.now());
+ if (this.#isAttrUpdate) return;
+ this.#syncAttrs();
+ // Renderujemy natychmiast po zmianie atrybutu
+ this.#render(Date.now());
+ // Jeśli zmieniono cel na przyszły, upewnij się, że zegar tyka
+ this.#checkRestart();
}
- // Pull current attributes into state.
- _syncFromAttrs() {
- const raw = this.getAttribute('target');
- const t = raw ? new Date(raw) : new Date();
- this._target = Number.isNaN(t.getTime()) ? new Date() : t;
+ #syncAttrs() {
+ const tRaw = this.getAttribute('target');
+ this.#target = tRaw ? new Date(tRaw) : new Date();
+ if (isNaN(this.#target)) this.#target = new Date();
- this._preset = (this.getAttribute('preset') || 'auto').toLowerCase(); // 'auto'|'hms'|'dhms'|'ydhms'|'date'
- this._labels = (this.getAttribute('labels') || 'short').toLowerCase(); // 'none'|'short'|'long'
- this._compact = this.hasAttribute('compact');
-
- this._locale = resolveLocale(this);
+ this.#preset = (this.getAttribute('preset') || 'auto').toLowerCase();
+ this.#labels = (this.getAttribute('labels') || 'short').toLowerCase();
+ this.#compact = this.hasAttribute('compact');
+ this.#locale = resolveLocale(this);
}
- // Compute and render the string once.
- _renderOnce(nowMs) {
- if (!this._text) return;
+ #startTicking() {
+ if (!this.#unsub) {
+ this.#unsub = TickHub.sub((now) => this.#render(now));
+ this.#render(Date.now());
+ }
+ }
- // Date-only preset: defer to Intl.DateTimeFormat.
- if (this._preset === 'date') {
- const dtf = new Intl.DateTimeFormat(this._locale, {
+ #stopTicking() {
+ if (this.#unsub) {
+ this.#unsub();
+ this.#unsub = null;
+ }
+ }
+
+ #checkRestart() {
+ // Jeśli mamy przyszły target, a zegar stoi -> wznów go
+ const now = Date.now();
+ if (this.#target > now && !this.#unsub) {
+ this.#startTicking();
+ }
+ }
+
+ #render(nowMs) {
+ if (!this.#textNode) return;
+
+ // Tryb daty statycznej
+ if (this.#preset === 'date') {
+ const dtf = IntlCache.dtf(this.#locale, {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
});
- const out = dtf.format(this._target);
- if (this._text.data !== out) this._text.data = out;
+ const txt = dtf.format(this.#target);
+ if (this.#textNode.data !== txt) this.#textNode.data = txt;
return;
}
- // Build the time window depending on future/past.
const now = new Date(nowMs);
- const future = this._target - now >= 0;
- const start = future ? now : this._target;
- const end = future ? this._target : now;
+ const diff = this.#target - now;
+ const isFuture = diff >= 0;
+
+ // Auto-stop, jeśli minęliśmy czas
+ if (!isFuture && this.#unsub) {
+ this.#stopTicking();
+ }
- // Decide what to show based on preset.
- let show = unitsForPreset(this._preset, start, end);
+ const start = isFuture ? now : this.#target;
+ const end = isFuture ? this.#target : now;
- // compact: hide empty high units (Y/M/D), and 0 hours when days are shown.
- if (this._compact) {
+ let show = getShowConfig(this.#preset, Math.abs(diff));
+
+ // Logika compact (ukrywanie zerowych jednostek wysokiego rzędu)
+ if (this.#compact) {
const probe = collapseForUnits(start, end, show);
- if (show.y && probe.years === 0) show = { ...show, y: false };
- if (show.M && !show.y && probe.months === 0) show = { ...show, M: false };
- if (show.d && !show.y && !show.M && probe.days === 0) show = { ...show, d: false };
+ if (show.y && probe.years === 0) show.y = false;
+ if (show.M && !show.y && probe.months === 0) show.M = false;
+ if (show.d && !show.y && !show.M && probe.days === 0) show.d = false;
+ // Przelicz ponownie po redukcji jednostek
const probe2 = collapseForUnits(start, end, show);
- if (show.d && show.h && probe2.hours === 0) show = { ...show, h: false };
+ if (show.d && show.h && probe2.hours === 0) show.h = false;
}
const parts = collapseForUnits(start, end, show);
-
- // If countdown reached zero, stop ticking to save CPU.
- if (future && end - now <= 0 && this._unsub) { this._unsub(); this._unsub = null; }
-
- // Format the output string and update only TextNode.data.
- const out = formatDurationSmart(this._locale, this._labels, show, parts);
- if (this._text.data !== out) this._text.data = out;
+ const txt = formatDurationSmart(this.#locale, this.#labels, show, parts);
+
+ if (this.#textNode.data !== txt) this.#textNode.data = txt;
}
- /* =======================
- * Public API (properties)
- * =======================
- * Use these instead of touching private fields.
- */
-
- // target: Date | string | number(ms)
- get target() { return this._target; }
- set target(v) {
- const d = this._coerceDate(v);
- if (!d) return;
- this._target = d;
- this._updatingAttr = true; // prevent attributeChanged echo
- this.setAttribute('target', d.toISOString());
- this._updatingAttr = false;
- this._renderOnce(Date.now());
+ /* Public API */
+ get target() { return new Date(this.#target); }
+ set target(val) {
+ const d = new Date(val);
+ if (isNaN(d)) return;
+ this.#target = d;
+ this.#reflect('target', d.toISOString());
+ this.#render(Date.now());
+ this.#checkRestart();
}
- // preset: 'auto' | 'hms' | 'dhms' | 'ydhms' | 'date'
- get preset() { return this._preset; }
- set preset(v) {
- const p = String(v || '').toLowerCase();
- this._preset = p;
- this._updatingAttr = true;
- this.setAttribute('preset', p);
- this._updatingAttr = false;
- this._renderOnce(Date.now());
+ get preset() { return this.#preset; }
+ set preset(val) {
+ this.#preset = String(val).toLowerCase();
+ this.#reflect('preset', this.#preset);
+ this.#render(Date.now());
}
- // labels: 'none' | 'short' | 'long'
- get labels() { return this._labels; }
- set labels(v) {
- const l = String(v || '').toLowerCase();
- this._labels = l;
- this._updatingAttr = true;
- this.setAttribute('labels', l);
- this._updatingAttr = false;
- this._renderOnce(Date.now());
+ get labels() { return this.#labels; }
+ set labels(val) {
+ this.#labels = String(val).toLowerCase();
+ this.#reflect('labels', this.#labels);
+ this.#render(Date.now());
}
- // compact: boolean
- get compact() { return this._compact; }
- set compact(v) {
- const on = !!v;
- this._compact = on;
- this._updatingAttr = true;
- this.toggleAttribute('compact', on);
- this._updatingAttr = false;
- this._renderOnce(Date.now());
+ get compact() { return this.#compact; }
+ set compact(val) {
+ this.#compact = !!val;
+ if (this.#compact) this.setAttribute('compact', '');
+ else this.removeAttribute('compact');
+ this.#isAttrUpdate = true; // Flaga dla AttributeChanged
+ this.#render(Date.now());
+ this.#isAttrUpdate = false;
}
- // locale: IETF BCP 47 (e.g., 'pl', 'en-GB'); overrides auto detection
- get locale() { return this._locale; }
- set locale(v) {
- const loc = (v || '').toString();
- if (!loc) return;
- this._locale = loc.toLowerCase();
- this._updatingAttr = true;
- this.setAttribute('locale', this._locale);
- this._updatingAttr = false;
- this._renderOnce(Date.now());
+ set locale(val) {
+ if (!val) return;
+ this.#locale = String(val).toLowerCase();
+ this.#reflect('locale', this.#locale);
+ this.#render(Date.now());
}
- // --- helpers ---
- _coerceDate(v) {
- if (v instanceof Date) return isNaN(v.getTime()) ? null : v;
- if (typeof v === 'number') { const d = new Date(v); return isNaN(d.getTime()) ? null : d; }
- if (typeof v === 'string') { const d = new Date(v); return isNaN(d.getTime()) ? null : d; }
- return null;
+ #reflect(name, val) {
+ this.#isAttrUpdate = true;
+ this.setAttribute(name, val);
+ this.#isAttrUpdate = false;
}
}
-customElements.define('wc-timer', WcTimer);
+customElements.define('wc-timer', WcTimer);
\ No newline at end of file