{"version":3,"file":"scroll-restoration.cjs","names":[],"sources":["../../src/scroll-restoration.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport { functionalUpdate, isPlainObject } from './utils'\nimport type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\nimport type { NonNullableUpdater } from './utils'\n\nexport type ScrollRestorationEntry = { scrollX: number; scrollY: number }\n\ntype ScrollRestorationByElement = Record<string, ScrollRestorationEntry>\n\ntype ScrollRestorationByKey = Record<string, ScrollRestorationByElement>\n\ntype ScrollRestorationCache = {\n  readonly state: ScrollRestorationByKey\n  set: (updater: NonNullableUpdater<ScrollRestorationByKey>) => void\n  persist: () => void\n}\n\nexport type ScrollRestorationOptions = {\n  getKey?: (location: ParsedLocation) => string\n  scrollBehavior?: ScrollToOptions['behavior']\n}\n\nfunction getSafeSessionStorage() {\n  try {\n    return typeof window !== 'undefined' &&\n      typeof window.sessionStorage === 'object'\n      ? window.sessionStorage\n      : undefined\n  } catch {\n    // silent\n    return undefined\n  }\n}\n\n// SessionStorage key used to store scroll positions across navigations.\nexport const storageKey = 'tsr-scroll-restoration-v1_3'\n\nfunction createScrollRestorationCache(): ScrollRestorationCache | null {\n  const safeSessionStorage = getSafeSessionStorage()\n  if (!safeSessionStorage) {\n    return null\n  }\n\n  let state: ScrollRestorationByKey = {}\n\n  try {\n    const parsed = JSON.parse(safeSessionStorage.getItem(storageKey) || '{}')\n    if (isPlainObject(parsed)) {\n      state = parsed as ScrollRestorationByKey\n    }\n  } catch {\n    // ignore invalid session storage payloads\n  }\n\n  const persist = () => {\n    try {\n      safeSessionStorage.setItem(storageKey, JSON.stringify(state))\n    } catch {\n      if (process.env.NODE_ENV !== 'production') {\n        console.warn(\n          '[ts-router] Could not persist scroll restoration state to sessionStorage.',\n        )\n      }\n    }\n  }\n\n  return {\n    get state() {\n      return state\n    },\n    set: (updater) => {\n      state = functionalUpdate(updater, state) || state\n    },\n    persist,\n  }\n}\n\nexport const scrollRestorationCache = createScrollRestorationCache()\n\n/**\n * The default `getKey` function for `useScrollRestoration`.\n * It returns the `key` from the location state or the `href` of the location.\n *\n * The `location.href` is used as a fallback to support the use case where the location state is not available like the initial render.\n */\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n  return location.state.__TSR_key! || location.href\n}\n\nfunction getCssSelector(el: any): string {\n  const path = []\n  let parent: HTMLElement\n  while ((parent = el.parentNode)) {\n    path.push(\n      `${el.tagName}:nth-child(${Array.prototype.indexOf.call(parent.children, el) + 1})`,\n    )\n    el = parent\n  }\n  return `${path.reverse().join(' > ')}`.toLowerCase()\n}\n\nexport function getElementScrollRestorationEntry(\n  router: AnyRouter,\n  options: (\n    | {\n        id: string\n        getElement?: () => Window | Element | undefined | null\n      }\n    | {\n        id?: string\n        getElement: () => Window | Element | undefined | null\n      }\n  ) & {\n    getKey?: (location: ParsedLocation) => string\n  },\n): ScrollRestorationEntry | undefined {\n  const getKey = options.getKey || defaultGetScrollRestorationKey\n  const restoreKey = getKey(router.latestLocation)\n\n  if (options.id) {\n    return scrollRestorationCache?.state[restoreKey]?.[\n      `[${scrollRestorationIdAttribute}=\"${options.id}\"]`\n    ]\n  }\n\n  const element = options.getElement?.()\n  if (!element) {\n    return\n  }\n\n  return scrollRestorationCache?.state[restoreKey]?.[\n    element instanceof Window ? windowScrollTarget : getCssSelector(element)\n  ]\n}\n\nlet ignoreScroll = false\nconst windowScrollTarget = 'window'\nconst scrollRestorationIdAttribute = 'data-scroll-restoration-id'\ntype ScrollTarget = typeof windowScrollTarget | Element\n\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n  if (!scrollRestorationCache && !(isServer ?? router.isServer)) {\n    return\n  }\n\n  const cache = scrollRestorationCache\n\n  const shouldScrollRestoration =\n    force ?? router.options.scrollRestoration ?? false\n\n  if (shouldScrollRestoration) {\n    router.isScrollRestoring = true\n  }\n\n  if (\n    (isServer ?? router.isServer) ||\n    router.isScrollRestorationSetup ||\n    !cache\n  ) {\n    return\n  }\n\n  router.isScrollRestorationSetup = true\n  ignoreScroll = false\n\n  const getKey =\n    router.options.getScrollRestorationKey || defaultGetScrollRestorationKey\n  const trackedScrollEntries = new Map<ScrollTarget, ScrollRestorationEntry>()\n\n  window.history.scrollRestoration = 'manual'\n\n  const onScroll = (event: Event) => {\n    if (ignoreScroll || !router.isScrollRestoring) {\n      return\n    }\n\n    if (event.target === document || event.target === window) {\n      trackedScrollEntries.set(windowScrollTarget, {\n        scrollX: window.scrollX || 0,\n        scrollY: window.scrollY || 0,\n      })\n    } else {\n      const target = event.target as Element\n      trackedScrollEntries.set(target, {\n        scrollX: target.scrollLeft || 0,\n        scrollY: target.scrollTop || 0,\n      })\n    }\n  }\n\n  // Snapshot the current page's tracked scroll targets before navigation or unload.\n  const snapshotCurrentScrollTargets = (restoreKey?: string) => {\n    if (\n      !router.isScrollRestoring ||\n      !restoreKey ||\n      trackedScrollEntries.size === 0 ||\n      !cache\n    ) {\n      return\n    }\n\n    const keyEntry = (cache.state[restoreKey] ||=\n      {} as ScrollRestorationByElement)\n\n    for (const [target, position] of trackedScrollEntries) {\n      let selector: string | undefined\n\n      if (target === windowScrollTarget) {\n        selector = windowScrollTarget\n      } else if (target.isConnected) {\n        const attrId = target.getAttribute(scrollRestorationIdAttribute)\n        selector = attrId\n          ? `[${scrollRestorationIdAttribute}=\"${attrId}\"]`\n          : getCssSelector(target)\n      }\n\n      if (!selector) {\n        continue\n      }\n\n      keyEntry[selector] = position\n    }\n  }\n\n  document.addEventListener('scroll', onScroll, true)\n  router.subscribe('onBeforeLoad', (event) => {\n    snapshotCurrentScrollTargets(\n      event.fromLocation ? getKey(event.fromLocation) : undefined,\n    )\n    trackedScrollEntries.clear()\n  })\n  window.addEventListener('pagehide', () => {\n    snapshotCurrentScrollTargets(\n      getKey(\n        router.stores.resolvedLocation.get() ?? router.stores.location.get(),\n      ),\n    )\n    cache.persist()\n  })\n\n  // Restore destination scroll after the new route has rendered.\n  router.subscribe('onRendered', (event) => {\n    const cacheKey = getKey(event.toLocation)\n    const behavior = router.options.scrollRestorationBehavior\n    const scrollToTopSelectors = router.options.scrollToTopSelectors\n    trackedScrollEntries.clear()\n\n    if (!router.resetNextScroll) {\n      router.resetNextScroll = true\n      return\n    }\n\n    if (\n      typeof router.options.scrollRestoration === 'function' &&\n      !router.options.scrollRestoration({ location: router.latestLocation })\n    ) {\n      return\n    }\n\n    ignoreScroll = true\n\n    try {\n      const elementEntries = router.isScrollRestoring\n        ? cache.state[cacheKey]\n        : undefined\n      let restored = false\n\n      if (elementEntries) {\n        for (const elementSelector in elementEntries) {\n          const entry = elementEntries[elementSelector]\n\n          if (!isPlainObject(entry)) {\n            continue\n          }\n\n          const { scrollX, scrollY } = entry as {\n            scrollX?: unknown\n            scrollY?: unknown\n          }\n\n          if (!Number.isFinite(scrollX) || !Number.isFinite(scrollY)) {\n            continue\n          }\n\n          if (elementSelector === windowScrollTarget) {\n            window.scrollTo({\n              top: scrollY as number,\n              left: scrollX as number,\n              behavior,\n            })\n            restored = true\n          } else if (elementSelector) {\n            let element\n\n            try {\n              element = document.querySelector(elementSelector)\n            } catch {\n              continue\n            }\n\n            if (element) {\n              element.scrollLeft = scrollX as number\n              element.scrollTop = scrollY as number\n              restored = true\n            }\n          }\n        }\n      }\n\n      if (!restored) {\n        const hash = router.history.location.hash.slice(1)\n\n        if (hash) {\n          const hashScrollIntoViewOptions =\n            window.history.state?.__hashScrollIntoViewOptions ?? true\n\n          if (hashScrollIntoViewOptions) {\n            const el = document.getElementById(hash)\n            if (el) {\n              el.scrollIntoView(hashScrollIntoViewOptions)\n            }\n          }\n        } else {\n          const scrollOptions = {\n            top: 0,\n            left: 0,\n            behavior,\n          }\n\n          window.scrollTo(scrollOptions)\n          if (scrollToTopSelectors) {\n            for (const selector of scrollToTopSelectors) {\n              if (selector === windowScrollTarget) continue\n              const element =\n                typeof selector === 'function'\n                  ? selector()\n                  : document.querySelector(selector)\n              if (element) {\n                element.scrollTo(scrollOptions)\n              }\n            }\n          }\n        }\n      }\n    } finally {\n      ignoreScroll = false\n    }\n\n    if (router.isScrollRestoring) {\n      cache.set((state) => {\n        state[cacheKey] ||= {} as ScrollRestorationByElement\n        return state\n      })\n    }\n  })\n}\n"],"mappings":"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"}