{"version":3,"file":"countdown-clock-v2-snippet.min.js","mappings":"wZAiJA,SAASA,IACP,MAAMC,EAAQ,IAAIC,YAAsC,aAAc,CACpEC,OAAQ,CACNC,UAAWC,KAAKC,SAGpBC,cAAcN,EAChB,CCjIA,MAAMO,yBAAyBC,YAC7B,6BAAkBC,GAChB,MAAO,CAAC,gBAAiB,kBAC3B,CAmBQC,iBAAmBC,KAAKC,YAAYC,KAAKF,MAE1C,iBAAAG,GACLH,KAAKI,uBACLJ,KAAKK,OACP,CAEO,oBAAAC,GACLC,oBAAoB,aAAcP,KAAKD,iBACzC,CAEO,wBAAAS,CAAyBC,EAAeC,EAAkBC,GAC/DX,KAAKI,uBAEDJ,KAAKY,aAAeF,IAAaC,GACnCX,KAAKK,OAET,CAEQ,oBAAAD,GACDJ,KAAKa,aACRb,KAAKc,aAAa,CAAEC,KAAM,SAC1Bf,KAAKa,WAAWG,UAAY,2wBAEhC,CAOQ,KAAAX,GACNE,oBAAoB,aAAcP,KAAKD,kBACvCkB,iBAAiB,aAAcjB,KAAKD,kBD+CtCmB,WAAW9B,EAAM,GAEZ+B,OAAOC,mCACVD,OAAOC,kCAAmC,EAC1CC,YAAYjC,EAAM,MCjDlBY,KAAKsB,OAAO7B,KAAKC,MACnB,CAEQ,WAAAO,CAAYZ,GAClBW,KAAKsB,OAAOjC,EAAME,OAAOC,UAC3B,CAEQ,MAAA8B,CAAO9B,GACb,MAAM+B,EAA+C,IAArCC,SAASxB,KAAKyB,QAAQF,QAAS,IACzCG,EAAmD,IAAvCF,SAASxB,KAAKyB,QAAQC,UAAW,IAI7CC,ED/BH,SAAyBC,EAA0BC,GACxD,GAAID,EAAmBC,IAAiBA,EACtC,OAAO,KAGT,MAAMC,EAAcC,KAAKC,OAAOH,EAAeD,GAAoB,KAC7DK,EAAUH,EAAc,GAExBI,GAAeJ,EAAcG,GAAW,GACxCE,EAAUD,EAAc,GAExBE,GAAaF,EAAcC,GAAW,GACtCE,EAAQD,EAAY,GAEpBE,GAAQF,EAAYC,GAAS,GAE7BE,EAAO,CACXD,EAAKE,WACI,IAATF,EAAa,MAAQ,OACrBD,EAAMG,WACI,IAAVH,EAAc,OAAS,QACvBF,EAAQK,WACI,IAAZL,EAAgB,SAAW,UAC3BF,EAAQO,WACI,IAAZP,EAAgB,SAAW,WAC3BQ,KAAK,KAUP,MAR2B,CACzBH,OACAD,QACAF,UACAF,UACAM,OAIJ,CCLqBG,CAAgBlD,EAAW+B,GAC5C,IAAKI,EAGH,OAFApB,oBAAoB,aAAcP,KAAKD,uBACvCC,KAAK2C,QAAS,GAIhB,GAAInD,EAAYkC,EAEd,YADA1B,KAAK2C,QAAS,GAIhB3C,KAAK2C,QAAS,EACd3C,KAAK4C,WAAW,OAAQjB,EAASW,MACjCtC,KAAK4C,WAAW,QAASjB,EAASU,OAClCrC,KAAK4C,WAAW,UAAWjB,EAASQ,SACpCnC,KAAK4C,WAAW,UAAWjB,EAASM,SAEvBjC,KAAKa,WAAWgC,eAAe,QACvCC,aAAa,aAAcnB,EAASY,KAC3C,CAEQ,UAAAK,CAAWG,EAAYC,GAC7B,MAAMT,EAAOS,EAAQ,GAAK,IAAIA,IAAU,GAAGA,IACrCC,EAAOjD,KAAKa,WAAWgC,eAAeE,GACxCE,EAAKC,cAAgBX,IACvBU,EAAKC,YAAcX,EAEvB,EASGY,eAAeC,IAAI,uBACtBD,eAAeE,OAAO,qBAAsBzD","sources":["webpack://lange/./lib/countdown-timer.ts","webpack://lange/./snippets/countdown-clock-v2/index.ts"],"sourcesContent":["// We have now more than one element that displays a countdown clock on the page. When the elements\n// use separate `setInterval()` calls to display their time the effect of them not being\n// synchronized can be seen even if the interval is low (50-100ms).\n//\n// This module provides a very simple synchronization method and some shared utilities for timestamp\n// calculations.\n//\n// Clock synchronization is based on a single `setInterval()` call. This starts a timer which emits\n// `timer-tick` events. Each page element can listen to the events to update the displayed time at\n// the very same moment other elements do.\n//\n// The timer has to be initialized with the `startTimer()` function. There is no possibility to stop\n// the timer, as we would have to implement a reference counter which would be prone to problems,\n// i.e. a bug in a simple component could stop all timers on the page. Instead of stopping the timer\n// the components should stop listening to the events.\n\ndeclare global {\n  /**\n   * Sentinel marking that the shared tick interval has been started, so repeated `startTimer()`\n   * calls do not each spin up their own interval.\n   *\n   * This lives on the global scope rather than in a module variable on purpose. Each section is its\n   * own webpack entry and we do not share chunks, so this file is copied into every bundle that\n   * imports it. A module variable would therefore be per-bundle, letting every section on the page\n   * start its own interval and defeating the synchronization. The tick events are already\n   * dispatched globally, so the guard has to be global too.\n   */\n  var countdown_timer_interval_started: boolean | undefined;\n}\n\n/**\n * The remaining time between two timestamps, broken down into whole units plus a human readable\n * summary. Each numeric component is the remainder within its larger unit, so `hours`, `minutes`,\n * and `seconds` never exceed their natural maximums (e.g. `seconds` is always 0-59).\n */\ninterface TimeLeft {\n  /** Whole days remaining. */\n  days?: number;\n\n  /** Whole hours remaining within the current day (0-23). */\n  hours?: number;\n\n  /** Whole minutes remaining within the current hour (0-59). */\n  minutes?: number;\n\n  /** Whole seconds remaining within the current minute (0-59). */\n  seconds?: number;\n\n  /** Human readable summary of the remaining time, e.g. `\"1 day 2 hours 3 minutes 4 seconds\"`. */\n  text?: string;\n}\n\n/**\n * Calculate the remaining time between currentTimestamp and endTimestamp and express it as an\n * object with number of days, hours, minutes, seconds as integers and a human readable `text`\n *\n * Timestamps should be in milliseconds.\n *\n * This is not guaranteed to return an object. It may return null if the current time is past the\n * end time or if the end time is falsy. In other words, a null return value indicates that either\n * there is no time left or it is not possible to compute it.\n */\nexport function computeTimeLeft(currentTimestamp: number, endTimestamp: number) {\n  if (currentTimestamp > endTimestamp || !endTimestamp) {\n    return null;\n  }\n\n  const secondsLeft = Math.round((endTimestamp - currentTimestamp) / 1000);\n  const seconds = secondsLeft % 60;\n\n  const minutesLeft = (secondsLeft - seconds) / 60;\n  const minutes = minutesLeft % 60;\n\n  const hoursLeft = (minutesLeft - minutes) / 60;\n  const hours = hoursLeft % 24;\n\n  const days = (hoursLeft - hours) / 24;\n\n  const text = [\n    days.toString(),\n    days === 1 ? 'day' : 'days',\n    hours.toString(),\n    hours === 1 ? 'hour' : 'hours',\n    minutes.toString(),\n    minutes === 1 ? 'minute' : 'minutes',\n    seconds.toString(),\n    seconds === 1 ? 'second' : 'seconds'\n  ].join(' ');\n\n  const timeLeft: TimeLeft = {\n    days,\n    hours,\n    minutes,\n    seconds,\n    text\n  };\n\n  return timeLeft;\n}\n\n/**\n * Start the shared heartbeat that drives every countdown on the page.\n *\n * Once started, a tick event fires on the global scope once a second carrying the current\n * timestamp. Each countdown section listens for that event and repaints from the same timestamp, so\n * every countdown on the page advances at the exact same instant instead of drifting out of step\n * with each other.\n *\n * Any section that wants a countdown may call this, in any order, as many times as it likes. Only\n * the very first call anywhere on the page actually creates the interval. Every later call finds\n * the guard already set and skips that part. The guard lives on the global scope rather than in a\n * module variable on purpose. This file is copied into each webpack bundle because we do not share\n * chunks, so a module variable would be per bundle and would let every section start its own\n * interval, which would bring back the drift this module exists to prevent. The sentinel\n * declaration near the top of the file explains that reasoning in full.\n *\n * The immediate tick deliberately sits outside the guard so it runs on every call rather than only\n * the first. Some sections paint their starting value themselves when they connect, but others set\n * their displayed time only inside their tick listener. A section of that second kind that connects\n * after the interval is already running would otherwise show an empty clock for up to a second,\n * until the next regular beat arrives. Firing one immediate tick per caller hands each one a value\n * straight away. The extra repaints this causes are cheap because a section only touches the DOM\n * when its displayed value actually changed.\n */\nexport function startTimer() {\n  setTimeout(tick, 0);\n\n  if (!window.countdown_timer_interval_started) {\n    window.countdown_timer_interval_started = true;\n    setInterval(tick, 1000);\n  }\n}\n\n/**\n * Emit a single tick event stamped with the current time.\n *\n * This is the callback for both the one-shot immediate timeout and the recurring interval that\n * startTimer sets up. The order in which those two fire never matters. Each event carries its own\n * timestamp, and every listener recomputes its whole display from whatever timestamp it receives,\n * holding no state from one tick to the next. So a tick that arrives out of order, or an extra tick\n * from a second caller, at worst causes one redundant repaint, which a section ignores when its\n * displayed value has not actually changed. That is also why there is no timer id to capture or\n * clear here. The immediate timeout has already elapsed by the time this runs, and the interval is\n * meant to run for the life of the page.\n */\nfunction tick() {\n  const event = new CustomEvent<TimerTickEvent['detail']>('timer-tick', {\n    detail: {\n      timestamp: Date.now()\n    }\n  });\n  dispatchEvent(event);\n}\n\n/**\n * This event is fired per tick of the countdown timer. It represents one tick. It captures the\n * precise point in time at which the tick occurred.\n *\n * Events have a native timeStamp property, which at first glance suggests this does not need a\n * custom property as a simple Event would suffice. However, event.timeStamp is not an epoch, it is\n * a high resolution timestamp relative to the page load. Listeners typically compare this event's\n * timestamp to Date.now(). In order to do that comparison using the native event.timeStamp, they\n * have to use performance.timeOrigin + event.timeStamp. We have seen the performance global have\n * issues, the comparison involves more boilerplate, and there is a mental tax on using it because\n * of its subtleties and misleading name. Not to mention the fact that most people write \"timestamp\"\n * in all lowercase but some camel case fanatic thought better. Using a custom property actually\n * reduces the complexity despite its reinvention of the wheel.\n *\n * The name \"timestamp\" conventionally refers to an ISO string. However, given that the native APIs\n * have adopted the term (e.g. the DOM High Resolution Time API), we are stuck with the bad name in\n * the browser setting, and must live with it.\n */\ntype TimerTickEvent = CustomEvent<{\n  /**\n   * The precise point in time at which the tick occurred. Measures as milliseconds since epoch.\n   *\n   * Despite the name, this has nothing to do with event.timeStamp. Do not confuse the two.\n   *\n   * Despite the name, this is not an ISO string. It is a number representing milliseconds since\n   * epoch.\n   */\n  timestamp: number;\n}>;\n\ndeclare global {\n  interface WindowEventMap {\n    'timer-tick': TimerTickEvent;\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { computeTimeLeft, startTimer } from '../../lib/countdown-timer.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * A countdown to an end time, shown as zero-padded days, hours, minutes, and seconds, like\n * \"02:13:05:42\". It's meant to become the one countdown every section uses, replacing the separate\n * timers that each section built for itself.\n *\n * A section renders it with the end time, and optionally a start time, as unix epoch seconds. The\n * element hides itself whenever the current time is outside that window, so it shows up on its own\n * once the start time passes and goes away once the end time passes. A heading such as \"Ends in\"\n * goes in a child with `slot=\"heading\"`, and it shows and hides along with the clock. The clock\n * takes its font and color from the element, so a section styles it with a rule on the element. To\n * draw a box around each number, the section sets the `--countdown-unit-*` variables the element's\n * stylesheet reads, and `--countdown-gap` for the space around the colons.\n *\n * @example\n * <countdown-clock-v2 data-start-time=\"1793000000\" data-end-time=\"1793600000\">\n *   <span slot=\"heading\">Ends in</span>\n * </countdown-clock-v2>\n */\nclass CountdownClockV2 extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-end-time', 'data-start-time'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * When the countdown reaches zero, as unix epoch seconds. The element stays hidden without it.\n     *\n     * @see https://www.epochconverter.com\n     */\n    endTime: string;\n\n    /**\n     * When the countdown first shows, as unix epoch seconds. Without it the countdown shows any\n     * time before the end time.\n     *\n     * @see https://www.epochconverter.com\n     */\n    startTime: string;\n  };\n\n  private onTimerTickBound = this.onTimerTick.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.start();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('timer-tick', this.onTimerTickBound);\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && oldValue !== newValue) {\n      this.start();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  /**\n   * Listens for the shared tick, which keeps this clock in step with every other countdown on the\n   * page, and paints the current time right away rather than waiting up to a second for the first\n   * tick.\n   */\n  private start() {\n    removeEventListener('timer-tick', this.onTimerTickBound);\n    addEventListener('timer-tick', this.onTimerTickBound);\n    startTimer();\n    this.render(Date.now());\n  }\n\n  private onTimerTick(event: WindowEventMap['timer-tick']) {\n    this.render(event.detail.timestamp);\n  }\n\n  private render(timestamp: number) {\n    const endTime = parseInt(this.dataset.endTime, 10) * 1000;\n    const startTime = parseInt(this.dataset.startTime, 10) * 1000;\n\n    // We stop listening once the end time passes, since nothing will change after that. Changing\n    // the end time starts us listening again.\n    const timeLeft = computeTimeLeft(timestamp, endTime);\n    if (!timeLeft) {\n      removeEventListener('timer-tick', this.onTimerTickBound);\n      this.hidden = true;\n      return;\n    }\n\n    if (timestamp < startTime) {\n      this.hidden = true;\n      return;\n    }\n\n    this.hidden = false;\n    this.renderUnit('days', timeLeft.days);\n    this.renderUnit('hours', timeLeft.hours);\n    this.renderUnit('minutes', timeLeft.minutes);\n    this.renderUnit('seconds', timeLeft.seconds);\n\n    const time = this.shadowRoot.getElementById('time');\n    time.setAttribute('aria-label', timeLeft.text);\n  }\n\n  private renderUnit(id: string, value: number) {\n    const text = value < 10 ? `0${value}` : `${value}`;\n    const unit = this.shadowRoot.getElementById(id);\n    if (unit.textContent !== text) {\n      unit.textContent = text;\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'countdown-clock-v2': CountdownClockV2;\n  }\n}\n\nif (!customElements.get('countdown-clock-v2')) {\n  customElements.define('countdown-clock-v2', CountdownClockV2);\n}\n"],"names":["tick","event","CustomEvent","detail","timestamp","Date","now","dispatchEvent","CountdownClockV2","HTMLElement","observedAttributes","onTimerTickBound","this","onTimerTick","bind","connectedCallback","initializeShadowRoot","start","disconnectedCallback","removeEventListener","attributeChangedCallback","_name","oldValue","newValue","isConnected","shadowRoot","attachShadow","mode","innerHTML","addEventListener","setTimeout","window","countdown_timer_interval_started","setInterval","render","endTime","parseInt","dataset","startTime","timeLeft","currentTimestamp","endTimestamp","secondsLeft","Math","round","seconds","minutesLeft","minutes","hoursLeft","hours","days","text","toString","join","computeTimeLeft","hidden","renderUnit","getElementById","setAttribute","id","value","unit","textContent","customElements","get","define"],"sourceRoot":""}