{"version":3,"file":"countdown-clock-section.min.js","mappings":"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","sources":["webpack://lange/./lib/countdown-timer.ts","webpack://lange/./sections/countdown-clock/countdown-clock/index.ts","webpack://lange/./sections/countdown-clock/countdown-clock-face-circle/index.ts","webpack://lange/./sections/countdown-clock/countdown-clock-face-square/index.ts","webpack://lange/./lib/dom-ready.ts","webpack://lange/./sections/countdown-clock/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 { startTimer, computeTimeLeft } from '../../../lib/countdown-timer.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * A countdown timer with an optional title, description, and CTA, used to count down to the end of\n * a promotion.\n *\n * It can show the remaining time as plain numbers or, when a face style is set, as circle or square\n * flip faces. For those it swaps each time unit for a countdown clock face child element and feeds\n * it the current value.\n *\n * It updates on a shared timer tick broadcast rather than running its own interval, and hides\n * itself when the current time is outside the configured start and end window.\n */\nclass CountdownClock extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return [\n      'data-end-time',\n      'data-start-time'\n    ];\n  }\n\n  declare readonly dataset: {\n    /**\n     * The text of the call to action.\n     */\n    ctaText: string;\n\n    /**\n     * The url to navigate to when a visitor clicks the call to action.\n     */\n    ctaUrl: string;\n\n    /**\n     * Countdown clock element description.\n     */\n    description: string;\n\n    /**\n     * Stop time as unix epoch time.\n     *\n     * @see https://www.epochconverter.com\n     */\n    endTime: string;\n\n    /**\n     * Set visual style for countdown clock, change it from default numbers to square flip or\n     * circles.\n     *\n     * @example 'circle' | 'square'\n     */\n    faceStyle: string;\n\n    /**\n     * Start time as unix epoch time.\n     *\n     * @see https://www.epochconverter.com\n     */\n    startTime: string;\n\n    /**\n     * Countdown element background color, hex format.\n     */\n    themeBgColor: string;\n\n    /**\n     * CTA button text and border color, hex format.\n     */\n    themeCtaColor: string;\n\n    /**\n     * Countdown element description color, hex format.\n     */\n    themeDescColor: string;\n\n    /**\n     * Text color for the countdown clock element, hex format.\n     */\n    themeTextColor: string;\n\n    /**\n     * Countdown element title color, hex format.\n     */\n    themeTitleColor: string;\n\n    /**\n     * Countdown clock element title.\n     */\n    title: string;\n  };\n\n  private declare days?: string;\n  private declare hours?: string;\n  private declare minutes?: string;\n  private declare seconds?: string;\n  private remainingText = '';\n  private shouldShowTimer = false;\n  private initialized = false;\n\n  private onTimerTickBound = this.onTimerTick.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    if (!this.dataset.endTime || !this.dataset.startTime) {\n      return;\n    }\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('timer-tick', this.onTimerTickBound);\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  private onTimerTick(event: WindowEventMap['timer-tick']) {\n    const endTime = parseInt(this.dataset.endTime, 10) * 1000;\n\n    const timeLeft = computeTimeLeft(event.detail.timestamp, endTime);\n\n    if (timeLeft) {\n      // Display the result\n      this.days = normalizeTimeItem(timeLeft.days);\n      this.hours = normalizeTimeItem(timeLeft.hours);\n      this.minutes = normalizeTimeItem(timeLeft.minutes);\n      this.seconds = normalizeTimeItem(timeLeft.seconds);\n      this.remainingText = timeLeft.text;\n      this.shouldShowTimer = true;\n\n      if (this.dataset.faceStyle === 'circle' || this.dataset.faceStyle === 'square') {\n        this.runFaceTimer(timeLeft.days, timeLeft.hours, timeLeft.minutes, timeLeft.seconds);\n      }\n    } else {\n      this.shouldShowTimer = false;\n    }\n\n    this.render();\n  }\n\n  private runFaceTimer(days: number, hours: number, minutes: number, seconds: number) {\n    const startTime = parseInt(this.dataset.startTime, 10) * 1000;\n    const endTime = parseInt(this.dataset.endTime, 10) * 1000;\n\n    const daysElement = this.shadowRoot.querySelector<HTMLElement>('.days');\n    daysElement.dataset.value = `${days}`;\n    daysElement.dataset.total = `${Math.trunc(endTime - startTime) / (24 * 60 * 60 * 1000)}`;\n\n    const hoursElement = this.shadowRoot.querySelector<HTMLElement>('.hours');\n    hoursElement.dataset.value = `${hours}`;\n    hoursElement.dataset.total = '24';\n\n    const minutesElement = this.shadowRoot.querySelector<HTMLElement>('.minutes');\n    minutesElement.dataset.value = `${minutes}`;\n    minutesElement.dataset.total = '60';\n\n    const secondsElement = this.shadowRoot.querySelector<HTMLElement>('.seconds');\n    secondsElement.dataset.value = `${seconds}`;\n    secondsElement.dataset.total = '60';\n  }\n\n  private render() {\n    if (!this.initialized) {\n      this.initialized = true;\n\n      const now = Date.now();\n      const startTime = parseInt(this.dataset.startTime, 10) * 1000;\n      const endTime = parseInt(this.dataset.endTime, 10) * 1000;\n\n      if (now > startTime && now < endTime) {\n        // render face style if set\n        const faceStyle = this.dataset.faceStyle;\n\n        if (faceStyle === 'circle' || faceStyle === 'square') {\n          const renderFn = faceStyle === 'square' ?\n            createSquareFaceElement : createCircleFaceElement;\n\n          const container = this.shadowRoot.querySelector('.container');\n          container.classList.add(faceStyle);\n\n          const clockUnitNames = ['days', 'hours', 'minutes', 'seconds'];\n          for (const unitName of clockUnitNames) {\n            const unit = this.shadowRoot.querySelector(`.value.${unitName}`);\n            unit.parentElement.replaceChild(renderFn(unitName), unit);\n          }\n        }\n\n        startTimer();\n        addEventListener('timer-tick', this.onTimerTickBound);\n      } else {\n        // With no active window there is nothing to count down to, so hide the whole element. The\n        // host sits in the light dom, so the global forced [hidden] rule is what hides it, not the\n        // [hidden] rule in our own stylesheet, which only reaches inside the shadow root. Forced\n        // matters here because our stylesheet also sets display block on the host.\n        this.hidden = true;\n      }\n    }\n\n    const containerElement = this.shadowRoot.querySelector<HTMLElement>('.container');\n    if (!this.shouldShowTimer) {\n      // BUG: For some unclear reason, we see errors in Sentry where sometimes the element is not\n      // found. The error appears as \"Cannot read properties of null (reading 'classList')\". As a\n      // partial mitigation, we explicitly check for truthiness of containerElement.\n      if (containerElement) {\n        containerElement.hidden = true;\n      }\n\n      return;\n    }\n\n    this.renderElement(true, '.value.days', this.days);\n    this.renderElement(true, '.value.hours', this.hours);\n    this.renderElement(true, '.value.minutes', this.minutes);\n    this.renderElement(true, '.value.seconds', this.seconds);\n    this.renderElement(true, '.container', null,\n      { color: this.dataset.themeTextColor, background: this.dataset.themeBgColor });\n    this.renderElement(this.dataset.title, '.title', this.dataset.title,\n      { color: this.dataset.themeTitleColor });\n    this.renderElement(this.dataset.description, '.description', this.dataset.description,\n      { color: this.dataset.themeDescColor });\n\n    this.renderElement(this.dataset.ctaUrl && this.dataset.ctaText, '.cta',\n      this.dataset.ctaText, {\n        color: this.dataset.themeCtaColor,\n        borderColor: this.dataset.themeCtaColor\n      });\n\n    if (this.dataset.ctaUrl && this.dataset.ctaText) {\n      const button = this.shadowRoot.querySelector<HTMLAnchorElement>('.cta');\n      button.href = this.dataset.ctaUrl;\n    }\n\n    const wrapper = this.shadowRoot.querySelector('.wrapper');\n    wrapper?.setAttribute('aria-label', this.remainingText);\n  }\n\n  private renderElement(shouldRender: boolean | string, selector: string, content?: string,\n    style = <Record<string, string>>{}) {\n    if (shouldRender) {\n      const element = this.shadowRoot.querySelector<HTMLElement>(selector);\n      if (!element) {\n        return;\n      }\n\n      if (content && element.textContent !== content) {\n        element.textContent = content;\n      }\n\n      element.hidden = false;\n\n      for (const [prop, value] of Object.entries(style)) {\n        element.style.setProperty(prop, value);\n      }\n    }\n  }\n}\n\nfunction createSquareFaceElement(name: string) {\n  const squareFaceElement = document.createElement('countdown-clock-face-square');\n  squareFaceElement.className = name;\n  return squareFaceElement;\n}\n\nfunction createCircleFaceElement(name: string) {\n  const circleFaceElement = document.createElement('countdown-clock-face-circle');\n  circleFaceElement.className = name;\n  return circleFaceElement;\n}\n\n/**\n * This will normalize time items to show 01:01:01:01 instead of 1:1:1:1\n */\nfunction normalizeTimeItem(value: number) {\n  return value < 10 ? `0${value}` : `${value}`;\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'countdown-clock': CountdownClock;\n  }\n}\n\nif (!customElements.get('countdown-clock')) {\n  customElements.define('countdown-clock', CountdownClock);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * One time unit (e.g. days or minutes) drawn as a circular progress ring.\n *\n * This is one of the two face styles the countdown clock swaps in. It takes the current value and\n * the unit's maximum and draws the ring proportionally.\n */\nclass CountdownClockFaceCircle extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-value'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Helps display max value for currently rendered part of countdown ex. minute\n     */\n    total: string;\n    /**\n     * Represents current day/hour/minute/second value, changing this triggers rendering.\n     */\n    value: string;\n  };\n\n  private radiusInitialized = false;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    this.render();\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && name === 'data-value' && oldValue !== newValue) {\n      this.render();\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  private render() {\n    if (!this.radiusInitialized) {\n      this.radiusInitialized = true;\n\n      // circle radius in pixels\n      const radius = 30;\n      const barElement = this.shadowRoot.querySelector<HTMLElement>('.bar');\n      barElement.style.strokeDasharray = `${2 * Math.PI * radius}`;\n    }\n\n    // circle radius in pixels\n    const radius = 30;\n    const value = parseInt(this.dataset.value, 10);\n    const total = parseInt(this.dataset.total, 10);\n\n    const barElement = this.shadowRoot.querySelector<HTMLElement>('.bar');\n    const valueElement = this.shadowRoot.querySelector('.value');\n    if (!barElement) {\n      return;\n    }\n\n    barElement.style.strokeDashoffset = `${(1 - value / total) * (2 * Math.PI * radius)}`;\n    valueElement.textContent = value < 10 ? `0${value}` : `${value}`;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'countdown-clock-face-circle': CountdownClockFaceCircle;\n  }\n}\n\nif (!customElements.get('countdown-clock-face-circle')) {\n  customElements.define('countdown-clock-face-circle', CountdownClockFaceCircle);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * One time unit (e.g. days or minutes) drawn as a flip card that animates when the value changes.\n *\n * This is one of the two face styles the countdown clock swaps in, taking the current value for the\n * unit.\n */\nclass CountdownClockFaceSquare extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-value'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Represents current day/hour/minute/second value, changing this triggers rendering.\n     */\n    value: string;\n  };\n\n  private declare currentValue?: number;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.render();\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && name === 'data-value' && oldValue !== newValue) {\n      this.render();\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  private render() {\n    const face = this.shadowRoot.querySelector<HTMLElement>('.square');\n    const value = parseInt(this.dataset.value, 10);\n\n    if (value !== this.currentValue) {\n      if (this.currentValue >= 0) {\n        const currentValue =\n          this.currentValue < 10 ? `0${this.currentValue}` : `${this.currentValue}`;\n        const backElement = this.shadowRoot.querySelector<WithValueElement>('.back');\n        backElement.dataset.value = currentValue || '';\n\n        const bottomElement = this.shadowRoot.querySelector<WithValueElement>('.bottom');\n        bottomElement.dataset.value = currentValue || '';\n      }\n\n      this.currentValue = value;\n      const topElement = this.shadowRoot.querySelector('.top');\n      topElement.textContent = value < 10 ? `0${value}` : `${value}`;\n\n      const backBottomElement = this.shadowRoot.querySelector<WithValueElement>('.back .bottom');\n      backBottomElement.dataset.value = value < 10 ? `0${value}` : `${value}`;\n\n      face.classList.remove('flip');\n\n      // forces repaint before reapplying class to make flip animation visible\n      // eslint-disable-next-line @typescript-eslint/no-unused-expressions\n      face.offsetWidth;\n      face.classList.add('flip');\n    }\n  }\n}\n\n/**\n * An element that has a data-value attribute.\n *\n * We are storing a value on various elements within the custom element's template that are not the\n * same as the data-value attribute value of the custom element itself, so this is a simple way to\n * document and introduce the concept of elements that carry a time value in their dataset.\n */\ninterface WithValueElement extends HTMLElement {\n  readonly dataset: {\n    /**\n     * Represents a time value, which is basically an integer.\n     */\n    value: string;\n  };\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'countdown-clock-face-square': CountdownClockFaceSquare;\n  }\n}\n\nif (!customElements.get('countdown-clock-face-square')) {\n  customElements.define('countdown-clock-face-square', CountdownClockFaceSquare);\n}\n","/**\n * Releases the handle returned from the ready() function.\n *\n * This is idempotent. Additional calls are pointless but harmless.\n */\nexport type ReleaseHandle = () => void;\n\n/**\n * A subset of events that pertain to document ready state.\n */\ntype LoadingEventMap = Pick<DocumentEventMap, 'DOMContentLoaded' | 'load'>;\n\n/**\n * Calls the provided callback when the document is ready. This works both when the document is\n * already ready and when it is not, so callers do not have to care. This exists because it ensures\n * that each caller consistently expresses its dependency on document readiness, and to save a bit\n * of boilerplate.\n *\n * A document is considered ready when it has been parsed. This corresponds to the ready state of\n * 'interactive'. When the document has been fully loaded, the ready state is 'complete'. The\n * callback runs either way, so callers do not have to check which one they are in.\n *\n * Ready means the html is parsed and the DOM is there to query. It does not mean the page's other\n * scripts have run. Deferred scripts and module scripts run after the ready state turns\n * 'interactive' and before DOMContentLoaded fires, so a callback here can run before them. When a\n * caller needs some other script to have run already, this is the wrong tool, and it should wait on\n * whatever that script produces instead.\n *\n * That remedy has a catch of its own when the calling bundle is loaded with async. An async bundle\n * can execute after DOMContentLoaded has already fired, so a plain listener for that event never\n * runs and the work silently never happens. Nothing here can detect that, because by the time this\n * file executes the event is long gone. Catching it needs something registered at parse time,\n * before any bundle loads.\n *\n * There is a second trap right next to that one, and it applies only when waiting for\n * DOMContentLoaded. The ready state stays 'interactive' after DOMContentLoaded has fired, all the\n * way until 'complete', so the ready state cannot tell you whether DOMContentLoaded is still coming\n * or has already gone. Waiting for load is not exposed to this, because the ready state turns\n * 'complete' and load fires in the same task, so seeing 'complete' means load has fired too.\n *\n * When the document is already ready, the callback is invoked via setTimeout. This way, if the\n * callback throws, the error surfaces to the global error handler in a way that is similar to what\n * happens when an event listener throws.\n *\n * We might one day want to move this to an NPM library.\n *\n * Beware of a quirk here. setTimeout yields, and the busier the frontend, the longer it takes to\n * fire. Page load is exactly when the frontend is busiest. The timeout is a floor and not a\n * guarantee, so there is no real bound on how long that yield lasts. What this means is that using\n * this library can introduce more delay than you might expect. Compare code that runs on async load\n * and checks ready state and invokes immediately to code that runs after setTimeout. The former\n * runs in the same tick, the latter runs in \"some later tick\". That later tick can be surprisingly\n * much later. That can cause initial paint issues.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event\n * @see https://html.spec.whatwg.org/multipage/parsing.html#the-end\n */\nexport function ready(callback: (event?: Event) => void,\n  type: keyof LoadingEventMap = 'DOMContentLoaded') {\n  if (document.readyState === 'complete' || (type === 'DOMContentLoaded' &&\n    document.readyState === 'interactive')) {\n    const timer = setTimeout(callback, 0);\n    const handle: ReleaseHandle = () => clearTimeout(timer);\n    return handle;\n  } else {\n    addEventListener(type, callback);\n    const handle: ReleaseHandle = () => removeEventListener(type, callback);\n    return handle;\n  }\n}\n","import './countdown-clock/index.js';\nimport './countdown-clock-face-circle/index.js';\nimport './countdown-clock-face-square/index.js';\nimport { ready } from '../../lib/dom-ready.js';\n\nfunction onReady() {\n  // We must query the DOM to determine if a flash sale countdown timer override exists. If none is\n  // found we have no need to continue further and short circuit.\n  const overrideEl = document.querySelector<HTMLElement>('#product-detail-countdown-timer');\n  if (!overrideEl) {\n    return;\n  }\n\n  if (!overrideEl.dataset.startTime || !overrideEl.dataset.endTime) {\n    return;\n  }\n\n  const flashStartTime = parseInt(overrideEl.dataset.startTime, 10);\n  const flashEndTime = parseInt(overrideEl.dataset.endTime, 10);\n  const timestamp = Math.floor(Date.now() / 1000);\n  if (timestamp > flashStartTime && timestamp < flashEndTime) {\n    let appendTimer = false;\n    let clock = document.querySelector('countdown-clock');\n\n    // Since the global countdown timer is uniquely shown on various collection pages we must check\n    // if it exists and create the element to be inserted into the DOM after all dataset attributes\n    // have been updated to ensure proper rendering.\n    if (!clock) {\n      clock = document.createElement('countdown-clock');\n      clock.dataset.themeTextColor = '#000000';\n      clock.dataset.faceStyle = 'default';\n      appendTimer = true;\n    }\n\n    clock.dataset.startTime = overrideEl.dataset.startTime;\n    clock.dataset.endTime = overrideEl.dataset.endTime;\n\n    if (overrideEl.dataset.heading) {\n      clock.dataset.title = overrideEl.dataset.heading;\n    }\n\n    if (appendTimer) {\n      overrideEl.appendChild(clock);\n    }\n  }\n}\n\nready(onReady);\n"],"names":["tick","event","CustomEvent","detail","timestamp","Date","now","dispatchEvent","CountdownClock","HTMLElement","observedAttributes","remainingText","shouldShowTimer","initialized","onTimerTickBound","this","onTimerTick","bind","connectedCallback","initializeShadowRoot","dataset","endTime","startTime","render","disconnectedCallback","removeEventListener","shadowRoot","attachShadow","mode","innerHTML","parseInt","timeLeft","currentTimestamp","endTimestamp","secondsLeft","Math","round","seconds","minutesLeft","minutes","hoursLeft","hours","days","text","toString","join","computeTimeLeft","normalizeTimeItem","faceStyle","runFaceTimer","daysElement","querySelector","value","total","trunc","hoursElement","minutesElement","secondsElement","renderFn","createSquareFaceElement","createCircleFaceElement","classList","add","clockUnitNames","unitName","unit","parentElement","replaceChild","setTimeout","window","countdown_timer_interval_started","setInterval","addEventListener","hidden","containerElement","renderElement","color","themeTextColor","background","themeBgColor","title","themeTitleColor","description","themeDescColor","ctaUrl","ctaText","themeCtaColor","borderColor","href","wrapper","setAttribute","shouldRender","selector","content","style","element","textContent","prop","Object","entries","setProperty","name","squareFaceElement","document","createElement","className","circleFaceElement","customElements","get","define","CountdownClockFaceCircle","radiusInitialized","attributeChangedCallback","oldValue","newValue","isConnected","radius","strokeDasharray","PI","barElement","valueElement","strokeDashoffset","CountdownClockFaceSquare","face","currentValue","remove","offsetWidth","callback","type","readyState","timer","clearTimeout","ready","overrideEl","flashStartTime","flashEndTime","floor","appendTimer","clock","heading","appendChild"],"sourceRoot":""}