{"version":3,"file":"ScrollMirror.cjs","sources":["../src/support/helpers.ts","../src/support/functions.ts","../src/ScrollMirror.ts"],"sourcesContent":["import type { Progress } from \"./defs.js\";\n\n/** Return a Promise that resolves after the next event loop. */\nexport const nextTick = (): Promise<void> => {\n  return new Promise((resolve) => {\n    requestAnimationFrame(() => resolve());\n  });\n};\n\n/** Check if an element has overflow */\nexport const hasOverflow = ({\n  clientWidth,\n  clientHeight,\n  scrollWidth,\n  scrollHeight,\n}: HTMLElement) => {\n  return scrollHeight > clientHeight || scrollWidth > clientWidth;\n};\n\n/** Check if an element is set to overflow: auto in at least one direction */\nexport const hasCSSOverflow = (element: HTMLElement) => {\n  const overflow = window.getComputedStyle(element)[\"overflow\"];\n  return overflow.includes(\"auto\") || overflow.includes(\"scroll\");\n};\n\n/** Get the scroll progress of an element, between 0-1 */\nexport const getScrollProgress = (el: HTMLElement | undefined): Progress => {\n  if (el == null) {\n    return {\n      x: 0,\n      y: 0,\n    };\n  }\n\n  const {\n    scrollTop,\n    scrollHeight,\n    clientHeight,\n    scrollLeft,\n    scrollWidth,\n    clientWidth,\n  } = el;\n\n  const availableWidth = scrollWidth - clientWidth;\n  const availableHeight = scrollHeight - clientHeight;\n\n  return {\n    x: !!scrollLeft ? scrollLeft / Math.max(0.00001, availableWidth) : 0,\n    y: !!scrollTop ? scrollTop / Math.max(0.00001, availableHeight) : 0,\n  };\n};\n","import type { Logger, Progress } from \"./defs.js\";\nimport { hasCSSOverflow, hasOverflow } from \"./helpers.js\";\n\n/**\n * Get the event target for receiving scroll events\n * - return the window if the element is either the html or body element\n * - otherwise, return the element\n */\nexport function getScrollEventTarget(element: HTMLElement): Window | HTMLElement {\n  return element.matches(\"body *\") ? element : window;\n}\n\n/**\n * Get a minimal logger with a prefix\n */\nexport function getLogger(prefix: string) {\n  return {\n    log: (...args: any[]) => console.log(prefix, ...args),\n    warn: (...args: any[]) => console.warn(prefix, ...args),\n    error: (...args: any[]) => console.error(prefix, ...args),\n  };\n}\n\n/**\n * Make sure the provided elements are valid\n */\nexport function validateElements(\n  elements: HTMLElement[],\n  logger?: Logger\n): void {\n  if (elements.length < 1) {\n    logger?.warn(\"No elements provided.\");\n    return;\n  }\n\n  if (elements.length < 2) {\n    logger?.warn(\"Only one element provided.\", elements);\n  }\n\n  if (elements.some((el) => !el)) {\n    logger?.error(\"Some elements are not defined.\", elements);\n  }\n\n  for (const element of elements) {\n    if (element instanceof HTMLElement && !hasOverflow(element)) {\n      logger?.warn(\"Element doesn't have overflow:\", element);\n    }\n    if (\n      element instanceof HTMLElement &&\n      element.matches(\"body *\") &&\n      !hasCSSOverflow(element)\n    ) {\n      logger?.warn('No \"overflow: auto;\" or \"overflow: scroll;\" set on element:', element); // prettier-ignore\n    }\n  }\n}\n\n/**\n * Validate the progress, log errors for invalid values\n */\nexport function validateProgress(\n  progress: Partial<Progress>,\n  logger?: Logger\n) {\n  let valid = true;\n  for (const [key, value] of Object.entries(progress)) {\n    if (typeof value !== \"number\" || value < 0 || value > 1) {\n      logger?.error(`progress.${key} must be a number between 0-1`);\n      valid = false;\n    }\n  }\n  return valid;\n}","import type { Progress, Options, Logger } from \"./support/defs.js\";\nimport { getScrollProgress, hasOverflow, nextTick } from \"./support/helpers.js\";\n\nimport {\n  getScrollEventTarget,\n  getLogger,\n  validateElements,\n  validateProgress,\n} from \"./support/functions.js\";\n\n/**\n * Mirrors the scroll position of multiple elements on a page\n */\nexport default class ScrollMirror {\n  /** Mirror the scroll positions of these elements */\n  readonly elements: HTMLElement[];\n  /** The default options */\n  readonly defaults: Options = {\n    vertical: true,\n    horizontal: true,\n    debug: true,\n  };\n  /** The parsed options */\n  options: Options;\n  /** Is mirroring paused? */\n  paused: boolean = false;\n  /** a simple logger @internal */\n  logger: Logger | undefined = undefined;\n\n  constructor(\n    elements: NodeListOf<Element> | Element[],\n    options: Partial<Options> = {}\n  ) {\n    this.elements = [...elements]\n      .filter(Boolean)\n      .map((el) => this.getScrollContainer(el));\n\n    /** remove duplicates */\n    this.elements = [...new Set(this.elements)];\n\n    this.options = { ...this.defaults, ...options };\n\n    if (this.options.debug) {\n      this.logger = getLogger(\"[scroll-mirror]\");\n      validateElements(this.elements, this.logger);\n    }\n\n    this.elements.forEach((element) => this.addScrollHandler(element));\n\n    /**\n     * Initially, make sure that elements are mirrored to the\n     * documentElement's scroll position (if provided)\n     */\n    if (this.elements.includes(document.documentElement)) {\n      this.mirrorScrollPositions(\n        getScrollProgress(document.documentElement),\n        document.documentElement\n      );\n    }\n  }\n\n  /** Pause mirroring */\n  pause() {\n    this.paused = true;\n  }\n\n  /** Resume mirroring */\n  resume() {\n    this.paused = false;\n  }\n\n  /** Destroy. Removes all event handlers */\n  destroy() {\n    this.elements.forEach((element) => this.removeScrollHandler(element));\n  }\n\n  /** Add the scroll handler to the element @internal */\n  addScrollHandler(element: HTMLElement) {\n    /** Safeguard to prevent duplicate handlers on elements */\n    this.removeScrollHandler(element);\n\n    const target = getScrollEventTarget(element);\n    target.addEventListener(\"scroll\", this.handleScroll, { passive: true });\n  }\n\n  /** Remove the scroll handler from an element @internal */\n  removeScrollHandler(element: HTMLElement) {\n    const target = getScrollEventTarget(element);\n    target.removeEventListener(\"scroll\", this.handleScroll);\n  }\n\n  /**\n   * Get the scroll container, based on element provided:\n   * - return the element if it's a child of <body>\n   * - otherwise, return the documentElement\n   */\n  getScrollContainer(el: unknown): HTMLElement {\n    if (el instanceof HTMLElement && el.matches(\"body *\")) return el;\n    return document.documentElement;\n  }\n\n  /** Handle a scroll event on an element @internal */\n  handleScroll = async (event: Event) => {\n    if (this.paused) return;\n\n    if (!event.currentTarget) return;\n\n    const scrolledElement = this.getScrollContainer(event.currentTarget);\n\n    await nextTick();\n\n    this.mirrorScrollPositions(\n      getScrollProgress(scrolledElement),\n      scrolledElement\n    );\n  };\n\n  /** Mirror the scroll positions of all elements to a target @internal */\n  mirrorScrollPositions(\n    progress: Progress,\n    ignore: HTMLElement | undefined = undefined\n  ) {\n    this.elements.forEach((element) => {\n      /* Ignore the currently scrolled element  */\n      if (ignore === element) return;\n\n      /* Remove the scroll event listener */\n      this.removeScrollHandler(element);\n\n      this.setScrollPosition(progress, element);\n\n      /* Re-attach the scroll event listener */\n      window.requestAnimationFrame(() => {\n        this.addScrollHandler(element);\n      });\n    });\n  }\n\n  /** Mirror the scroll position from one element to another @internal */\n  setScrollPosition(progress: Progress, target: HTMLElement) {\n    const { vertical, horizontal } = this.options;\n\n    /* Calculate the actual element scroll lengths */\n    const availableScroll = {\n      x: target.scrollWidth - target.clientWidth,\n      y: target.scrollHeight - target.clientHeight,\n    };\n\n    /* Adjust the scroll position accordingly */\n    if (vertical && !!availableScroll.y) {\n      target.scrollTo({\n        top: availableScroll.y * progress.y,\n        behavior: \"instant\",\n      });\n    }\n    if (horizontal && !!availableScroll.x) {\n      target.scrollTo({\n        left: availableScroll.x * progress.x,\n        behavior: \"instant\",\n      });\n    }\n  }\n\n  /**\n   * Get the scroll position from the first container that has overflow\n   */\n  get progress(): Progress {\n    const firstWithOverflow = this.elements.find((el) => hasOverflow(el));\n\n    return getScrollProgress(firstWithOverflow);\n  }\n\n  /**\n   * Set the scroll progress of all mirrored elements\n   *\n   * The progress is an object of { x:number , y: number }, where both x and y are a number\n   * between 0-1\n   *\n   * Examples:\n   *  - `const progress = mirror.progress` — returns something like { x: 0.2, y:0.5 }\n   *  - `mirror.progress = 0.5` — set the scroll position to 50% on both axes\n   *  - `mirror.progress = { y: 0.5 }` — set the scroll position to 50% on the y axis\n   *  - `mirror.progress = { x: 0.2, y: 0.5 }` — set the scroll position on both axes\n   */\n  set progress(value: Partial<Progress> | number) {\n    /** if the value is a number, set both axes to that value */\n    if (typeof value === \"number\") {\n      value = { x: value, y: value };\n    }\n    const progress = { ...this.progress, ...value };\n\n    if (!validateProgress(progress, this.logger)) {\n      return;\n    }\n\n    this.mirrorScrollPositions(progress);\n  }\n}\n"],"names":["nextTick","Promise","resolve","requestAnimationFrame","hasOverflow","clientWidth","clientHeight","scrollWidth","scrollHeight","hasCSSOverflow","element","overflow","window","getComputedStyle","includes","getScrollProgress","el","x","y","scrollTop","scrollLeft","availableHeight","Math","max","getScrollEventTarget","matches","constructor","elements","options","_this","this","prefix","defaults","vertical","horizontal","debug","paused","logger","undefined","handleScroll","event","currentTarget","scrolledElement","getScrollContainer","then","mirrorScrollPositions","e","reject","filter","Boolean","map","Set","log","console","slice","call","arguments","warn","error","length","some","HTMLElement","validateElements","forEach","addScrollHandler","document","documentElement","pause","resume","destroy","removeScrollHandler","addEventListener","passive","removeEventListener","progress","ignore","setScrollPosition","target","availableScroll","scrollTo","top","behavior","left","firstWithOverflow","find","value","valid","key","Object","entries","validateProgress"],"mappings":"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"}