{"version":3,"file":"landing-video-hero-section.min.js","mappings":"wZAuBOA,eAAeC,EAAKC,GACzB,OAAOA,EAAMD,MACf,CCPA,MAAME,kBAAkBC,YAMdC,uBAAyBC,KAAKC,kBAAkBC,KAAKF,MACrDG,aAAeH,KAAKI,QAAQF,KAAKF,MACjCK,yBAA2BL,KAAKM,oBAAoBJ,KAAKF,MACzDO,uBAAyBP,KAAKQ,kBAAkBN,KAAKF,MAEtD,iBAAAS,GAELT,KAAKU,uBAEL,MAAMC,EAASX,KAAKY,WAAWC,cAAc,UAC7CF,EAAOG,iBAAiB,QAASd,KAAKG,cAItCH,KAAKY,WAAWE,iBAAiB,OAAQd,KAAKO,wBAAwB,GACtEP,KAAKY,WAAWE,iBAAiB,QAASd,KAAKO,wBAAwB,GAEvEP,KAAKe,WAAaC,WAAW,sBAC7BhB,KAAKe,WAAWD,iBAAiB,SAAUd,KAAKK,0BAEhDL,KAAKiB,cAML,MAAMrB,EAAQI,KAAKY,WAAWC,cAAc,SAC5CF,EAAOO,aAAa,eAAgBtB,EAAMuB,SAAW,QAAU,OACjE,CAEO,oBAAAC,GACUpB,KAAKY,WAAWC,cAAc,UACtCQ,oBAAoB,QAASrB,KAAKG,cAEzCH,KAAKY,WAAWS,oBAAoB,OAAQrB,KAAKO,wBAAwB,GACzEP,KAAKY,WAAWS,oBAAoB,QAASrB,KAAKO,wBAAwB,GAEtEP,KAAKe,YACPf,KAAKe,WAAWM,oBAAoB,SAAUrB,KAAKK,yBAEvD,CAEQ,oBAAAK,GACDV,KAAKY,aACRZ,KAAKsB,aAAa,CAAEC,KAAM,SAC1BvB,KAAKY,WAAWY,UAAY,qmCAEhC,CAOQ,WAAAP,GACN,MAAMQ,EAAWzB,KAAKe,WAAWW,QAAU,UAAY,SACjDC,EAAW3B,KAAKa,cAAc,iCAAiCY,OACrE,KAAME,aAAoBC,qBACxB,OAGF,MACMhC,EAD6B+B,EAASE,QAAQC,WAAU,GACvCjB,cAAc,SACrCjB,EAAMsB,aAAa,cAAe,QAElC,MAAMa,EAAU/B,KAAKY,WAAWC,cAAc,SAE9C,GAAIkB,EAAS,CAE6C,SADzC/B,KAAKY,WAAWC,cAAc,UACtBmB,aAAa,kBAElCpC,EAAMuB,UAAW,GAGnBnB,KAAKY,WAAWqB,aAAarC,EAAOmC,EACtC,MAGMf,WAAW,oCAAoCU,UACjD9B,EAAMuB,UAAW,GAGnBnB,KAAKY,WAAWsB,YAAYtC,GAM1BA,EAAMuB,UACRxB,EAAKC,GAAOuC,MAAMnC,KAAKD,uBAE3B,CAMQ,iBAAAE,CAAkBmC,GACxBC,QAAQC,MAAMF,GACdpC,KAAKuC,mBACP,CAEQ,mBAAAjC,CAAoBkC,GAC1BxC,KAAKiB,aACP,CAOQ,OAAAb,CAAQoC,GACd,MAAM5C,EAAQI,KAAKY,WAAWC,cAAc,SACxCjB,EAAM6C,OACR9C,EAAKC,GAAOuC,MAAME,QAAQC,OAE1B1C,EAAM8C,OAEV,CAEQ,iBAAAlC,CAAkBgC,GACxBxC,KAAKuC,mBACP,CAEQ,iBAAAA,GACN,MAAM5B,EAASX,KAAKY,WAAWC,cAAc,UACvCjB,EAAQI,KAAKY,WAAWC,cAAc,SAC5CF,EAAOO,aAAa,eAAgBtB,EAAM6C,OAAS,OAAS,QAC9D,EASGE,eAAeC,IAAI,eACtBD,eAAeE,OAAO,aAAchD","sources":["webpack://lange/./lib/video.ts","webpack://lange/./sections/landing-video-hero/hero-video/index.ts"],"sourcesContent":["/**\n * Try to play a video, and always report the outcome as a promise.\n *\n * Do not inline this function. Playing a video is inconsistent across browsers in several ways, and\n * wrapping the call in an async function is what smooths all of them over at once.\n *\n * Some browsers have no `play` function on the element at all, so the call throws a TypeError. Some\n * have one that returns undefined instead of a promise, which is how `play` worked before the spec\n * gained a return value. Some return a promise that rejects later, which is the modern behavior and\n * is how a browser reports that it refused to autoplay. And some throw synchronously, right out of\n * the call, rather than rejecting.\n *\n * An async function turns all four into the same thing. Whatever happens inside it, the caller gets\n * a promise back, so a synchronous throw arrives as a rejection and a missing or non-promise `play`\n * still produces a promise that resolves. That means one `catch` on the call site covers every\n * failure mode, and the caller never has to check whether it got a promise before attaching a\n * handler.\n *\n * Nothing here is actionable, because a refusal is the browser enforcing its own autoplay policy.\n * Restrictive in-app browsers and low power mode refuse even after a user gesture, and interrupted\n * or unsupported sources reject too. The video simply stays paused, so callers should keep a debug\n * breadcrumb and never warn.\n */\nexport async function play(video: HTMLVideoElement) {\n  return video.play();\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { play } from '../../../lib/video.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * The background video of the landing video hero, together with its pause button. The video plays\n * on its own and keeps looping, and moving content like that needs a way to stop it for visitors\n * who find it distracting. The button is a toggle, and its aria-pressed state says whether the\n * video is paused.\n *\n * Liquid renders the desktop and the mobile video inside templates, so neither one loads with the\n * page. The element puts only the one for the current screen width on the page, and swaps it when\n * the width crosses the breakpoint, so a visitor downloads a single video.\n *\n * The shadow root holds the button. The button only works once this script runs, so it only exists\n * from then on. The element only looks inside itself, so every hero on a page works on its own.\n */\nclass HeroVideo extends HTMLElement implements CustomElement {\n  /**\n   * Matches from 768px up, where the desktop video shows.\n   */\n  private declare mediaQuery: MediaQueryList;\n\n  private onAutoplayRefusedBound = this.onAutoplayRefused.bind(this);\n  private onClickBound = this.onClick.bind(this);\n  private onMediaQueryChangedBound = this.onMediaQueryChanged.bind(this);\n  private onPlaybackChangedBound = this.onPlaybackChanged.bind(this);\n\n  public connectedCallback() {\n    // The bundle is deferred, so the templates are parsed by the time the element upgrades.\n    this.initializeShadowRoot();\n\n    const button = this.shadowRoot.querySelector('button');\n    button.addEventListener('click', this.onClickBound);\n\n    // Play and pause do not bubble, so we listen in the capture phase to hear them from the video\n    // inside the element.\n    this.shadowRoot.addEventListener('play', this.onPlaybackChangedBound, true);\n    this.shadowRoot.addEventListener('pause', this.onPlaybackChangedBound, true);\n\n    this.mediaQuery = matchMedia('(min-width: 768px)');\n    this.mediaQuery.addEventListener('change', this.onMediaQueryChangedBound);\n\n    this.renderVideo();\n\n    // Autoplay is skipped in the theme editor and for reduced motion, so a video without it starts\n    // out paused. A video that will autoplay may not have started yet, so we go by the attribute\n    // rather than by whether it is playing right now. The play and pause events take over from\n    // there, and so does the browser refusing autoplay.\n    const video = this.shadowRoot.querySelector('video');\n    button.setAttribute('aria-pressed', video.autoplay ? 'false' : 'true');\n  }\n\n  public disconnectedCallback() {\n    const button = this.shadowRoot.querySelector('button');\n    button.removeEventListener('click', this.onClickBound);\n\n    this.shadowRoot.removeEventListener('play', this.onPlaybackChangedBound, true);\n    this.shadowRoot.removeEventListener('pause', this.onPlaybackChangedBound, true);\n\n    if (this.mediaQuery) {\n      this.mediaQuery.removeEventListener('change', this.onMediaQueryChangedBound);\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   * Puts the video for the current screen width on the page, in place of the one already there. The\n   * first video does not autoplay for a visitor who asked for reduced motion. After that, a visitor\n   * who paused the old video expects the new one to stay paused, so it does not autoplay either.\n   */\n  private renderVideo() {\n    const viewport = this.mediaQuery.matches ? 'desktop' : 'mobile';\n    const template = this.querySelector(`template[data-video-viewport=\"${viewport}\"]`);\n    if (!(template instanceof HTMLTemplateElement)) {\n      return;\n    }\n\n    const fragment = <DocumentFragment>template.content.cloneNode(true);\n    const video = fragment.querySelector('video');\n    video.setAttribute('aria-hidden', 'true');\n\n    const current = this.shadowRoot.querySelector('video');\n\n    if (current) {\n      const button = this.shadowRoot.querySelector('button');\n      const pressed = button.getAttribute('aria-pressed') === 'true';\n      if (pressed) {\n        video.autoplay = false;\n      }\n\n      this.shadowRoot.replaceChild(video, current);\n    } else {\n      // A looping background video is the kind of motion the reduced motion setting asks us to hold\n      // back, so the visitor gets it paused and can still start it with the button.\n      if (matchMedia('(prefers-reduced-motion: reduce)').matches) {\n        video.autoplay = false;\n      }\n\n      this.shadowRoot.appendChild(video);\n    }\n\n    // A browser can refuse autoplay, for example in low power mode. The video then stays paused\n    // without firing a pause event, so the button would keep showing it as playing. Asking it to\n    // play ourselves gets us a promise that rejects when that happens.\n    if (video.autoplay) {\n      play(video).catch(this.onAutoplayRefusedBound);\n    }\n  }\n\n  /**\n   * The rejection can also come from the video being paused or swapped out before it started, so we\n   * read the state of whatever video is there now rather than assuming it is paused.\n   */\n  private onAutoplayRefused(error: unknown) {\n    console.debug(error);\n    this.renderPausedState();\n  }\n\n  private onMediaQueryChanged(_event: MediaQueryListEvent) {\n    this.renderVideo();\n  }\n\n  /**\n   * We go by whether the video is actually paused rather than by the button, since a browser can\n   * refuse autoplay, for example in low power mode, and leave a video paused that the button shows\n   * as playing.\n   */\n  private onClick(_event: PointerEvent) {\n    const video = this.shadowRoot.querySelector('video');\n    if (video.paused) {\n      play(video).catch(console.debug);\n    } else {\n      video.pause();\n    }\n  }\n\n  private onPlaybackChanged(_event: Event) {\n    this.renderPausedState();\n  }\n\n  private renderPausedState() {\n    const button = this.shadowRoot.querySelector('button');\n    const video = this.shadowRoot.querySelector('video');\n    button.setAttribute('aria-pressed', video.paused ? 'true' : 'false');\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'hero-video': HeroVideo;\n  }\n}\n\nif (!customElements.get('hero-video')) {\n  customElements.define('hero-video', HeroVideo);\n}\n"],"names":["async","play","video","HeroVideo","HTMLElement","onAutoplayRefusedBound","this","onAutoplayRefused","bind","onClickBound","onClick","onMediaQueryChangedBound","onMediaQueryChanged","onPlaybackChangedBound","onPlaybackChanged","connectedCallback","initializeShadowRoot","button","shadowRoot","querySelector","addEventListener","mediaQuery","matchMedia","renderVideo","setAttribute","autoplay","disconnectedCallback","removeEventListener","attachShadow","mode","innerHTML","viewport","matches","template","HTMLTemplateElement","content","cloneNode","current","getAttribute","replaceChild","appendChild","catch","error","console","debug","renderPausedState","_event","paused","pause","customElements","get","define"],"sourceRoot":""}