{"version":3,"file":"before-after-section.min.js","mappings":"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","sources":["webpack://lange/./lib/dom-ready.ts","webpack://lange/./sections/before-after/init-models.ts","webpack://lange/./sections/before-after/init-separators.ts","webpack://lange/./sections/before-after/init-tabs.ts","webpack://lange/./lib/css-dom.ts","webpack://lange/./sections/before-after/init-styles.ts"],"sourcesContent":["/**\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 { ready } from '../../lib/dom-ready.js';\n\n/**\n * Wires up the model picker within each before-after tab: binds click handlers to the thumbnail\n * buttons, hides all model panels, then activates the first model. The thumbnail strip stays hidden\n * unless a wrapper has more than one model.\n */\nfunction onReady() {\n  const selector = '.before-after-section .model-and-navigation-wrapper';\n  const wrappers = document.querySelectorAll<HTMLElement>(selector);\n  for (const wrapper of wrappers) {\n    const thumbsContainer = wrapper.querySelector<HTMLElement>('.model-thumbs');\n    const thumbs = thumbsContainer?.querySelectorAll('button');\n\n    // Because we use optional chaining when assigning a value to thumbs, we need the fallback to\n    // empty array here\n\n    for (const thumb of thumbs || []) {\n      thumb.addEventListener('click', onThumbClicked);\n    }\n\n    // The markup ships every panel hidden so none flash before this runs. Revealing the active one\n    // is left entirely to activateModel below.\n\n    if (thumbs?.length > 0 && thumbs[0]) {\n      activateModel(thumbs[0]);\n    }\n\n    if (thumbs?.length > 1) {\n      thumbsContainer.hidden = false;\n    }\n  }\n}\n\nfunction onThumbClicked(event: MouseEvent) {\n  if (event.currentTarget instanceof Element) {\n    activateModel(event.currentTarget);\n  }\n}\n\n/**\n * Selects the given thumbnail and reveals its model: marks the thumb aria-selected (clearing the\n * others in the same strip) and shows the matching model panel via aria-controls, hiding the rest\n * within the same wrapper.\n */\nfunction activateModel(thumb: Element) {\n  const thumbsAncestor = thumb.closest('.model-thumbs');\n  if (!thumbsAncestor) {\n    return;\n  }\n\n  const allThumbs = thumbsAncestor.querySelectorAll('button');\n  for (const thumbCandidate of allThumbs) {\n    if (thumbCandidate === thumb) {\n      thumbCandidate.setAttribute('aria-selected', 'true');\n    } else {\n      thumbCandidate.setAttribute('aria-selected', 'false');\n    }\n  }\n\n  const wrapper = thumbsAncestor.closest('.model-and-navigation-wrapper');\n  if (!wrapper) {\n    return;\n  }\n\n  const modelId = thumb.getAttribute('aria-controls');\n  const models = wrapper.querySelectorAll<HTMLElement>('[role=\"tabpanel\"]');\n\n  // Visibility is driven purely by the native hidden attribute: a global stylesheet rule hides any\n  // element carrying it, so we flag every panel but the active one. The panels set their own\n  // display, but the global hide rule is forced, so it wins on its own.\n\n  for (const model of models) {\n    model.hidden = model.id !== modelId;\n  }\n}\n\nready(onReady);\n","import { ready } from '../../lib/dom-ready.js';\n\n/**\n * Whether the current touch is dragging the separator rather than scrolling the page\n */\nlet isDraggingSeparator = false;\n\n/**\n * Where the current touch started, to tell a horizontal drag apart from a vertical scroll\n */\nlet initialTouchX = 0;\nlet initialTouchY = 0;\n\n/**\n * Wires up the draggable separator on each model that has one. The markup only renders the\n * separator control when the metaobject turns dragging on and the model has both images.\n */\nfunction onReady() {\n  const selector = '.before-after-section .model[data-draggable-separator=\"true\"]';\n  const models = document.querySelectorAll<HTMLElement>(selector);\n  for (const model of models) {\n    const control = model.querySelector('.separator-control');\n    control.addEventListener('input', onControlInput);\n\n    model.addEventListener('touchstart', onTouchStarted, { passive: true });\n    model.addEventListener('touchmove', onTouchMoved, { passive: false });\n  }\n}\n\nfunction onControlInput(event: Event) {\n  if (event.target instanceof HTMLInputElement) {\n    const model = event.target.closest<HTMLElement>('.model');\n    setSeparatorPosition(model, parseFloat(event.target.value));\n  }\n}\n\nfunction onTouchStarted(event: TouchEvent) {\n  if (event.touches.length > 0) {\n    initialTouchX = event.touches[0].clientX;\n    initialTouchY = event.touches[0].clientY;\n    isDraggingSeparator = false;\n  }\n}\n\nfunction onTouchMoved(event: TouchEvent) {\n  if (event.touches.length === 0 || !(event.currentTarget instanceof HTMLElement)) {\n    return;\n  }\n\n  const touch = event.touches[0];\n  const deltaX = Math.abs(touch.clientX - initialTouchX);\n  const deltaY = Math.abs(touch.clientY - initialTouchY);\n\n  // On the first significant movement, decide whether this touch drags the separator or scrolls the\n  // page, by which direction dominates.\n  if (!isDraggingSeparator && (deltaX > 5 || deltaY > 5)) {\n    isDraggingSeparator = deltaX > deltaY;\n  }\n\n  // A vertical touch is left alone so the page scrolls as usual.\n  if (isDraggingSeparator) {\n    event.preventDefault();\n\n    const model = event.currentTarget;\n    const rect = model.getBoundingClientRect();\n    const percentage = (touch.clientX - rect.left) / rect.width * 100;\n    setSeparatorPosition(model, percentage);\n  }\n}\n\n/**\n * Moves the split between the before and after images to the given percentage of the model's width,\n * and keeps the separator control's value in step with it.\n */\nfunction setSeparatorPosition(model: HTMLElement, percentage: number) {\n  const boundedPercentage = Math.max(0, Math.min(100, percentage));\n\n  const control = model.querySelector<HTMLInputElement>('.separator-control');\n  control.value = boundedPercentage.toString();\n\n  model.style.setProperty('--separator-position', `${boundedPercentage}%`);\n}\n\nready(onReady);\n","import { ready } from '../../lib/dom-ready.js';\n\n/**\n * Wires up the before-after section's tab strip: binds click handlers to each tab, hides all tab\n * panels, then activates the first tab.\n *\n * The tab bar stays hidden unless there is more than one tab.\n */\nfunction onReady() {\n  // Placed via a JSON template, so the section wrapper id varies per page and cannot be hardcoded.\n  // Match an element in the section by a css class instead. The section schema caps it at one per\n  // page, so a single match is safe.\n  const section = document.querySelector('.before-after-section');\n  if (!section) {\n    return;\n  }\n\n  const tabsAncestor = section.querySelector<HTMLElement>('.tabs');\n  if (!tabsAncestor) {\n    return;\n  }\n\n  const tabs = tabsAncestor.querySelectorAll<HTMLElement>('[role=\"tab\"]');\n\n  for (const tab of tabs) {\n    tab.addEventListener('click', onTabClicked);\n  }\n\n  // The markup ships every panel hidden so none flash before this runs. Revealing the active one is\n  // left entirely to activateTab below.\n\n  if (tabs.length > 0 && tabs[0]) {\n    activateTab(tabs[0]);\n  }\n\n  if (tabs.length > 1) {\n    tabsAncestor.hidden = false;\n  }\n}\n\nfunction onTabClicked(event: MouseEvent) {\n  if (event.currentTarget instanceof Element) {\n    activateTab(event.currentTarget);\n  }\n}\n\nfunction activateTab(tab: Element) {\n  // Nothing to do if this tab is already active; bail before touching the DOM. Safe on init because\n  // every tab ships with aria-selected=\"false\", so the first activation still runs.\n  if (isActiveTab(tab)) {\n    return;\n  }\n\n  // Climb to the containing section via its own hook. This element is located within a dynamically\n  // rendered Shopify section, so we cannot determine the section by id. Walk up the DOM instead.\n  const section = tab.closest('.before-after-section');\n  if (!section) {\n    return;\n  }\n\n  const tabs = section.querySelectorAll<HTMLElement>('.tabs [role=\"tab\"]');\n  for (const candidateTab of tabs) {\n    if (candidateTab === tab) {\n      candidateTab.setAttribute('aria-selected', 'true');\n    } else {\n      candidateTab.setAttribute('aria-selected', 'false');\n    }\n  }\n\n  const tabId = tab.getAttribute('aria-controls');\n\n  // This selector has to be careful about the descendants it matches. The nested model elements\n  // within each tab also use role=\"tabpanel\", but do not use the class tabpanel, so we match by\n  // class instead of role to avoid clobbering them.\n\n  const panels = section.querySelectorAll<HTMLElement>('.content .tabpanel');\n\n  // Visibility is driven purely by the native hidden attribute, the same as the model panels. These\n  // panels set their own display, but the global hide rule is forced, so it wins on its own.\n\n  for (const panel of panels) {\n    panel.hidden = panel.id !== tabId;\n  }\n}\n\nfunction isActiveTab(tab: Element) {\n  return tab.getAttribute('aria-selected') === 'true';\n}\n\nready(onReady);\n","/**\n * Generates a style element containing the given content and appends it to the dom.\n *\n * This is an alternative to the webpack style-loader that circumvents the overly complex webpack\n * configuration and plugin behavior. This intentionally avoids using CSSStyleSheet or any other\n * kind of feature that is not broadly supported or accurately polyfilled.\n *\n * This is safe to call before the dom has loaded.\n *\n * This is safe to call on platforms where document.head is not defined.\n *\n * This annotates appended style elements with a data-lazy attribute so that it is trivial to\n * identify which elements were appended to the dom in this manner. This extra annotation is not\n * expected to have a material performance impact.\n */\nexport function appendCSSText(content: string) {\n  const style = document.createElement('style');\n  style.dataset.lazy = 'true';\n  const node = document.createTextNode(content);\n  style.appendChild(node);\n  const parent = document.head || document.body || document.documentElement;\n  parent.appendChild(style);\n}\n","import { appendCSSText } from '../../lib/css-dom.js';\nimport styles from './index.scss';\n\nappendCSSText(styles);\n"],"names":["ready","callback","type","document","readyState","timer","setTimeout","clearTimeout","addEventListener","removeEventListener","onThumbClicked","event","currentTarget","Element","activateModel","thumb","thumbsAncestor","closest","allThumbs","querySelectorAll","thumbCandidate","setAttribute","wrapper","modelId","getAttribute","models","model","hidden","id","wrappers","thumbsContainer","querySelector","thumbs","length","isDraggingSeparator","initialTouchX","initialTouchY","onControlInput","target","HTMLInputElement","setSeparatorPosition","parseFloat","value","onTouchStarted","touches","clientX","clientY","onTouchMoved","HTMLElement","touch","deltaX","Math","abs","deltaY","preventDefault","rect","getBoundingClientRect","left","width","percentage","boundedPercentage","max","min","toString","style","setProperty","passive","onTabClicked","activateTab","tab","isActiveTab","section","tabs","candidateTab","tabId","panels","panel","content","createElement","dataset","lazy","node","createTextNode","appendChild","head","body","documentElement","appendCSSText","tabsAncestor"],"sourceRoot":""}