{"version":3,"file":"scroll-restoration.js","sources":["../../src/scroll-restoration.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport { functionalUpdate } from './utils'\nimport type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\nimport type { NonNullableUpdater } from './utils'\nimport type { HistoryLocation } from '@tanstack/history'\n\nexport type ScrollRestorationEntry = { scrollX: number; scrollY: number }\n\nexport type ScrollRestorationByElement = Record<string, ScrollRestorationEntry>\n\nexport type ScrollRestorationByKey = Record<string, ScrollRestorationByElement>\n\nexport type ScrollRestorationCache = {\n  state: ScrollRestorationByKey\n  set: (updater: NonNullableUpdater<ScrollRestorationByKey>) => void\n}\nexport type ScrollRestorationOptions = {\n  getKey?: (location: ParsedLocation) => string\n  scrollBehavior?: ScrollToOptions['behavior']\n}\n\nfunction getSafeSessionStorage() {\n  try {\n    if (\n      typeof window !== 'undefined' &&\n      typeof window.sessionStorage === 'object'\n    ) {\n      return window.sessionStorage\n    }\n  } catch {\n    // silent\n  }\n  return undefined\n}\n\n/** SessionStorage key used to persist scroll restoration state. */\n/** SessionStorage key used to store scroll positions across navigations. */\n/** SessionStorage key used to store scroll positions across navigations. */\nexport const storageKey = 'tsr-scroll-restoration-v1_3'\n\nconst throttle = (fn: (...args: Array<any>) => void, wait: number) => {\n  let timeout: any\n  return (...args: Array<any>) => {\n    if (!timeout) {\n      timeout = setTimeout(() => {\n        fn(...args)\n        timeout = null\n      }, wait)\n    }\n  }\n}\n\nfunction createScrollRestorationCache(): ScrollRestorationCache | null {\n  const safeSessionStorage = getSafeSessionStorage()\n  if (!safeSessionStorage) {\n    return null\n  }\n\n  const persistedState = safeSessionStorage.getItem(storageKey)\n  let state: ScrollRestorationByKey = persistedState\n    ? JSON.parse(persistedState)\n    : {}\n\n  return {\n    state,\n    // This setter is simply to make sure that we set the sessionStorage right\n    // after the state is updated. It doesn't necessarily need to be a functional\n    // update.\n    set: (updater) => {\n      state = functionalUpdate(updater, state) || state\n      try {\n        safeSessionStorage.setItem(storageKey, JSON.stringify(state))\n      } catch {\n        console.warn(\n          '[ts-router] Could not persist scroll restoration state to sessionStorage.',\n        )\n      }\n    },\n  }\n}\n\n/** In-memory handle to the persisted scroll restoration cache. */\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 */\n\n/**\n * Default scroll restoration cache key: location state key or full href.\n */\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n  return location.state.__TSR_key! || location.href\n}\n\n/** Best-effort nth-child CSS selector for a given element. */\nexport function 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\nlet ignoreScroll = false\n\n// NOTE: This function must remain pure and not use any outside variables\n// unless they are passed in as arguments. Why? Because we need to be able to\n// toString() it into a script tag to execute as early as possible in the browser\n// during SSR. Additionally, we also call it from within the router lifecycle\nexport function restoreScroll({\n  storageKey,\n  key,\n  behavior,\n  shouldScrollRestoration,\n  scrollToTopSelectors,\n  location,\n}: {\n  storageKey: string\n  key?: string\n  behavior?: ScrollToOptions['behavior']\n  shouldScrollRestoration?: boolean\n  scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>\n  location?: HistoryLocation\n}) {\n  let byKey: ScrollRestorationByKey\n\n  try {\n    byKey = JSON.parse(sessionStorage.getItem(storageKey) || '{}')\n  } catch (error) {\n    console.error(error)\n    return\n  }\n\n  const resolvedKey = key || window.history.state?.__TSR_key\n  const elementEntries = byKey[resolvedKey]\n\n  //\n  ignoreScroll = true\n\n  //\n  scroll: {\n    // If we have a cached entry for this location state,\n    // we always need to prefer that over the hash scroll.\n    if (\n      shouldScrollRestoration &&\n      elementEntries &&\n      Object.keys(elementEntries).length > 0\n    ) {\n      for (const elementSelector in elementEntries) {\n        const entry = elementEntries[elementSelector]!\n        if (elementSelector === 'window') {\n          window.scrollTo({\n            top: entry.scrollY,\n            left: entry.scrollX,\n            behavior,\n          })\n        } else if (elementSelector) {\n          const element = document.querySelector(elementSelector)\n          if (element) {\n            element.scrollLeft = entry.scrollX\n            element.scrollTop = entry.scrollY\n          }\n        }\n      }\n\n      break scroll\n    }\n\n    // If we don't have a cached entry for the hash,\n    // Which means we've never seen this location before,\n    // we need to check if there is a hash in the URL.\n    // If there is, we need to scroll it's ID into view.\n    const hash = (location ?? window.location).hash.split('#', 2)[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\n      break scroll\n    }\n\n    // If there is no cached entry for the hash and there is no hash in the URL,\n    // we need to scroll to the top of the page for every scrollToTop element\n    const scrollOptions = { top: 0, left: 0, behavior }\n    window.scrollTo(scrollOptions)\n    if (scrollToTopSelectors) {\n      for (const selector of scrollToTopSelectors) {\n        if (selector === 'window') continue\n        const element =\n          typeof selector === 'function'\n            ? selector()\n            : document.querySelector(selector)\n        if (element) element.scrollTo(scrollOptions)\n      }\n    }\n  }\n\n  //\n  ignoreScroll = false\n}\n\n/** Setup global listeners and hooks to support scroll restoration. */\n/** Setup global listeners and hooks to support scroll restoration. */\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n  if (!scrollRestorationCache && !(isServer ?? router.isServer)) {\n    return\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    !scrollRestorationCache\n  ) {\n    return\n  }\n\n  router.isScrollRestorationSetup = true\n\n  //\n  ignoreScroll = false\n\n  const getKey =\n    router.options.getScrollRestorationKey || defaultGetScrollRestorationKey\n\n  window.history.scrollRestoration = 'manual'\n\n  // // Create a MutationObserver to monitor DOM changes\n  // const mutationObserver = new MutationObserver(() => {\n  //   ;ignoreScroll = true\n  //   requestAnimationFrame(() => {\n  //     ;ignoreScroll = false\n\n  //     // Attempt to restore scroll position on each dom\n  //     // mutation until the user scrolls. We do this\n  //     // because dynamic content may come in at different\n  //     // ticks after the initial render and we want to\n  //     // keep up with that content as much as possible.\n  //     // As soon as the user scrolls, we no longer need\n  //     // to attempt router.\n  //     // console.log('mutation observer restoreScroll')\n  //     restoreScroll(\n  //       storageKey,\n  //       getKey(router.state.location),\n  //       router.options.scrollRestorationBehavior,\n  //     )\n  //   })\n  // })\n\n  // const observeDom = () => {\n  //   // Observe changes to the entire document\n  //   mutationObserver.observe(document, {\n  //     childList: true, // Detect added or removed child nodes\n  //     subtree: true, // Monitor all descendants\n  //     characterData: true, // Detect text content changes\n  //   })\n  // }\n\n  // const unobserveDom = () => {\n  //   mutationObserver.disconnect()\n  // }\n\n  // observeDom()\n\n  const onScroll = (event: Event) => {\n    // unobserveDom()\n\n    if (ignoreScroll || !router.isScrollRestoring) {\n      return\n    }\n\n    let elementSelector = ''\n\n    if (event.target === document || event.target === window) {\n      elementSelector = 'window'\n    } else {\n      const attrId = (event.target as Element).getAttribute(\n        'data-scroll-restoration-id',\n      )\n\n      if (attrId) {\n        elementSelector = `[data-scroll-restoration-id=\"${attrId}\"]`\n      } else {\n        elementSelector = getCssSelector(event.target)\n      }\n    }\n\n    const restoreKey = getKey(router.state.location)\n\n    scrollRestorationCache.set((state) => {\n      const keyEntry = (state[restoreKey] ||= {} as ScrollRestorationByElement)\n\n      const elementEntry = (keyEntry[elementSelector] ||=\n        {} as ScrollRestorationEntry)\n\n      if (elementSelector === 'window') {\n        elementEntry.scrollX = window.scrollX || 0\n        elementEntry.scrollY = window.scrollY || 0\n      } else if (elementSelector) {\n        const element = document.querySelector(elementSelector)\n        if (element) {\n          elementEntry.scrollX = element.scrollLeft || 0\n          elementEntry.scrollY = element.scrollTop || 0\n        }\n      }\n\n      return state\n    })\n  }\n\n  // Throttle the scroll event to avoid excessive updates\n  if (typeof document !== 'undefined') {\n    document.addEventListener('scroll', throttle(onScroll, 100), true)\n  }\n\n  router.subscribe('onRendered', (event) => {\n    // unobserveDom()\n\n    const cacheKey = getKey(event.toLocation)\n\n    // If the user doesn't want to restore the scroll position,\n    // we don't need to do anything.\n    if (!router.resetNextScroll) {\n      router.resetNextScroll = true\n      return\n    }\n    if (typeof router.options.scrollRestoration === 'function') {\n      const shouldRestore = router.options.scrollRestoration({\n        location: router.latestLocation,\n      })\n      if (!shouldRestore) {\n        return\n      }\n    }\n\n    restoreScroll({\n      storageKey,\n      key: cacheKey,\n      behavior: router.options.scrollRestorationBehavior,\n      shouldScrollRestoration: router.isScrollRestoring,\n      scrollToTopSelectors: router.options.scrollToTopSelectors,\n      location: router.history.location,\n    })\n\n    if (router.isScrollRestoring) {\n      // Mark the location as having been seen\n      scrollRestorationCache.set((state) => {\n        state[cacheKey] ||= {} as ScrollRestorationByElement\n\n        return state\n      })\n    }\n  })\n}\n\n/**\n * @private\n * Handles hash-based scrolling after navigation completes.\n * To be used in framework-specific <Transitioner> components during the onResolved event.\n *\n * Provides hash scrolling for programmatic navigation when default browser handling is prevented.\n * @param router The router instance containing current location and state\n */\n/**\n * @private\n * Handles hash-based scrolling after navigation completes.\n * To be used in framework-specific Transitioners.\n */\nexport function handleHashScroll(router: AnyRouter) {\n  if (typeof document !== 'undefined' && (document as any).querySelector) {\n    const hashScrollIntoViewOptions =\n      router.state.location.state.__hashScrollIntoViewOptions ?? true\n\n    if (hashScrollIntoViewOptions && router.state.location.hash !== '') {\n      const el = document.getElementById(router.state.location.hash)\n      if (el) {\n        el.scrollIntoView(hashScrollIntoViewOptions)\n      }\n    }\n  }\n}\n"],"names":["storageKey"],"mappings":"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"}