{"version":3,"file":"blog-article-section.min.js","mappings":"wZAEA,SAASA,IAMP,MALwB,CACtBC,MAAOC,SAASD,MAChBE,IAAKC,SAASC,KAIlB,CAQA,IAAIC,GAAe,EAEnB,SAASC,EAAmBC,GAC1B,IAAKF,EAAc,CACjBA,GAAe,EACf,MAAMG,EAAOT,IAKbU,UAAUC,MAAMF,GACbG,MAAMC,GACNC,QAAQ,KACPR,GAAe,GAErB,CACF,CAEA,SAASO,EAAcE,GACjBA,aAAiBC,cAA+B,eAAfD,EAAME,MAGzCC,QAAQC,KAAKJ,EAEjB,CAOA,SAASK,EAAmBC,GAC1B,KAAMA,EAAMC,yBAAyBC,mBACnC,OAGF,MAAMC,EAAUH,EAAMC,cAAcG,QAAqB,mBACzD,IAAKD,EACH,OAGF,MAAME,EAASF,EAAQG,cAAc,UACrC,IAAKD,EACH,OAGF,MAAMvB,EAAM,IAAIyB,IAAIF,EAAOG,QAAQC,KACnC3B,EAAI4B,aAAaC,IAAI,WAAY,KACjCN,EAAOI,IAAM3B,EAAI8B,WACjBT,EAAQK,QAAQK,QAAU,MAC5B,ECTO,SAAeC,EACpBC,GACA,QAD2B,IAA3BA,IAAAA,EAA8B,oBACF,aAAxBlC,SAASmC,YAAuC,qBAATD,GACjB,gBAAxBlC,SAASmC,WAA+B,CACxC,MAAMC,EAAQC,WAAWJ,EAAU,GAEnC,MAD8B,IAAMK,aAAaF,EAEnD,CACEG,iBAAiBL,EAAMD,EAI3B,CDgBAO,CAjBA,WAEE,GAAIhC,UAAUiC,UAAYjC,UAAUiC,SAAS3C,KAAiB,CAC5D,MAAM4C,EAAe1C,SAAS2C,iBAA8B,8BAC5D,IAAK,MAAMC,KAAUF,EACnBE,EAAOC,QAAS,EAChBD,EAAOL,iBAAiB,QAASlC,EAErC,CAGA,MAAMyC,EAAc9C,SAASyB,cAA2B,0BACpDqB,GACFA,EAAYP,iBAAiB,QAASrB,EAE1C","sources":["webpack://lange/./sections/blog-article/index.ts","webpack://lange/./lib/dom-ready.ts"],"sourcesContent":["import { ready } from '../../lib/dom-ready.js';\n\nfunction getShareData() {\n  const data: ShareData = {\n    title: document.title,\n    url: location.href\n  };\n\n  return data;\n}\n\n/**\n * The share sheet stays open until the user picks a target or dismisses it. The browser allows only\n * one share at a time. A fast second click while the first sheet is still open makes the next call\n * to navigator.share fail with a DOMException, so we need to ignore clicks while a share is already\n * pending. This is a simple boolean guard we use to prevent that.\n */\nlet sharePending = false;\n\nfunction onShareButtonClick(_event: MouseEvent) {\n  if (!sharePending) {\n    sharePending = true;\n    const data = getShareData();\n\n    // promise rejected with AbortError means the user dismissed the share sheet without sharing;\n    // promise fulfilled means the user shared successfully - unfortunately, the Web Share API does\n    // not provide a way to know which target the user picked, so we can't track that\n    navigator.share(data)\n      .catch(onShareFailed)\n      .finally(() => {\n        sharePending = false;\n      });\n  }\n}\n\nfunction onShareFailed(error: unknown) {\n  if (error instanceof DOMException && error.name === 'AbortError') {\n    // Share was aborted by the user, no need to log an error\n  } else {\n    console.warn(error);\n  }\n}\n\n/**\n * Articles with an embedded YouTube video have a poster image that is shown until the user clicks\n * it. When the user clicks the poster, we set the iframe's src to the YouTube URL with autoplay=1,\n * which starts the video playing and we replace the poster with the actual video iframe\n */\nfunction onEmbedPosterClick(event: MouseEvent) {\n  if (!(event.currentTarget instanceof HTMLButtonElement)) {\n    return;\n  }\n\n  const wrapper = event.currentTarget.closest<HTMLElement>('.iframe-wrapper');\n  if (!wrapper) {\n    return;\n  }\n\n  const iframe = wrapper.querySelector('iframe');\n  if (!iframe) {\n    return;\n  }\n\n  const url = new URL(iframe.dataset.src);\n  url.searchParams.set('autoplay', '1');\n  iframe.src = url.toString();\n  wrapper.dataset.playing = 'true';\n}\n\nfunction onReady() {\n  // Only show the share buttons if the browser supports the Web Share API\n  if (navigator.canShare && navigator.canShare(getShareData())) {\n    const shareButtons = document.querySelectorAll<HTMLElement>('.social-links button.share');\n    for (const button of shareButtons) {\n      button.hidden = false;\n      button.addEventListener('click', onShareButtonClick);\n    }\n  }\n\n  // Articles with no embed have no poster button to wire up\n  const embedPoster = document.querySelector<HTMLElement>('.howto-article .poster');\n  if (embedPoster) {\n    embedPoster.addEventListener('click', onEmbedPosterClick);\n  }\n}\n\nready(onReady);\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"],"names":["getShareData","title","document","url","location","href","sharePending","onShareButtonClick","_event","data","navigator","share","catch","onShareFailed","finally","error","DOMException","name","console","warn","onEmbedPosterClick","event","currentTarget","HTMLButtonElement","wrapper","closest","iframe","querySelector","URL","dataset","src","searchParams","set","toString","playing","callback","type","readyState","timer","setTimeout","clearTimeout","addEventListener","ready","canShare","shareButtons","querySelectorAll","button","hidden","embedPoster"],"sourceRoot":""}