| /** |
| * React Router DOM v6.27.0 |
| * |
| * Copyright (c) Remix Software Inc. |
| * |
| * This source code is licensed under the MIT license found in the |
| * LICENSE.md file in the root directory of this source tree. |
| * |
| * @license MIT |
| */ |
| import * as React from 'react'; |
| import * as ReactDOM from 'react-dom'; |
| import { UNSAFE_mapRouteProperties, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, Router, UNSAFE_useRoutesImpl, UNSAFE_NavigationContext, useHref, useResolvedPath, useLocation, useNavigate, createPath, UNSAFE_useRouteId, UNSAFE_RouteContext, useMatches, useNavigation, useBlocker } from 'react-router'; |
| export { AbortedDeferredError, Await, MemoryRouter, Navigate, NavigationType, Outlet, Route, Router, Routes, UNSAFE_DataRouterContext, UNSAFE_DataRouterStateContext, UNSAFE_LocationContext, UNSAFE_NavigationContext, UNSAFE_RouteContext, UNSAFE_useRouteId, createMemoryRouter, createPath, createRoutesFromChildren, createRoutesFromElements, defer, generatePath, isRouteErrorResponse, json, matchPath, matchRoutes, parsePath, redirect, redirectDocument, renderMatches, replace, resolvePath, useActionData, useAsyncError, useAsyncValue, useBlocker, useHref, useInRouterContext, useLoaderData, useLocation, useMatch, useMatches, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRouteLoaderData, useRoutes } from 'react-router'; |
| import { stripBasename, UNSAFE_warning, createRouter, createBrowserHistory, createHashHistory, UNSAFE_ErrorResponseImpl, UNSAFE_invariant, joinPaths, IDLE_FETCHER, matchPath } from '@remix-run/router'; |
| export { UNSAFE_ErrorResponseImpl } from '@remix-run/router'; |
| |
| const defaultMethod = "get"; |
| const defaultEncType = "application/x-www-form-urlencoded"; |
| function isHtmlElement(object) { |
| return object != null && typeof object.tagName === "string"; |
| } |
| function isButtonElement(object) { |
| return isHtmlElement(object) && object.tagName.toLowerCase() === "button"; |
| } |
| function isFormElement(object) { |
| return isHtmlElement(object) && object.tagName.toLowerCase() === "form"; |
| } |
| function isInputElement(object) { |
| return isHtmlElement(object) && object.tagName.toLowerCase() === "input"; |
| } |
| function isModifiedEvent(event) { |
| return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); |
| } |
| function shouldProcessLinkClick(event, target) { |
| return event.button === 0 && ( |
| // Ignore everything but left clicks |
| !target || target === "_self") && |
| // Let browser handle "target=_blank" etc. |
| !isModifiedEvent(event) // Ignore clicks with modifier keys |
| ; |
| } |
| |
| /** |
| * Creates a URLSearchParams object using the given initializer. |
| * |
| * This is identical to `new URLSearchParams(init)` except it also |
| * supports arrays as values in the object form of the initializer |
| * instead of just strings. This is convenient when you need multiple |
| * values for a given key, but don't want to use an array initializer. |
| * |
| * For example, instead of: |
| * |
| * let searchParams = new URLSearchParams([ |
| * ['sort', 'name'], |
| * ['sort', 'price'] |
| * ]); |
| * |
| * you can do: |
| * |
| * let searchParams = createSearchParams({ |
| * sort: ['name', 'price'] |
| * }); |
| */ |
| function createSearchParams(init = "") { |
| return new URLSearchParams(typeof init === "string" || Array.isArray(init) || init instanceof URLSearchParams ? init : Object.keys(init).reduce((memo, key) => { |
| let value = init[key]; |
| return memo.concat(Array.isArray(value) ? value.map(v => [key, v]) : [[key, value]]); |
| }, [])); |
| } |
| function getSearchParamsForLocation(locationSearch, defaultSearchParams) { |
| let searchParams = createSearchParams(locationSearch); |
| if (defaultSearchParams) { |
| // Use `defaultSearchParams.forEach(...)` here instead of iterating of |
| // `defaultSearchParams.keys()` to work-around a bug in Firefox related to |
| // web extensions. Relevant Bugzilla tickets: |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=1414602 |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=1023984 |
| defaultSearchParams.forEach((_, key) => { |
| if (!searchParams.has(key)) { |
| defaultSearchParams.getAll(key).forEach(value => { |
| searchParams.append(key, value); |
| }); |
| } |
| }); |
| } |
| return searchParams; |
| } |
| |
| // Thanks https://github.com/sindresorhus/type-fest! |
| |
| // One-time check for submitter support |
| let _formDataSupportsSubmitter = null; |
| function isFormDataSubmitterSupported() { |
| if (_formDataSupportsSubmitter === null) { |
| try { |
| new FormData(document.createElement("form"), |
| // @ts-expect-error if FormData supports the submitter parameter, this will throw |
| 0); |
| _formDataSupportsSubmitter = false; |
| } catch (e) { |
| _formDataSupportsSubmitter = true; |
| } |
| } |
| return _formDataSupportsSubmitter; |
| } |
| |
| /** |
| * Submit options shared by both navigations and fetchers |
| */ |
| |
| /** |
| * Submit options available to fetchers |
| */ |
| |
| /** |
| * Submit options available to navigations |
| */ |
| |
| const supportedFormEncTypes = new Set(["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"]); |
| function getFormEncType(encType) { |
| if (encType != null && !supportedFormEncTypes.has(encType)) { |
| UNSAFE_warning(false, `"${encType}" is not a valid \`encType\` for \`<Form>\`/\`<fetcher.Form>\` ` + `and will default to "${defaultEncType}"`) ; |
| return null; |
| } |
| return encType; |
| } |
| function getFormSubmissionInfo(target, basename) { |
| let method; |
| let action; |
| let encType; |
| let formData; |
| let body; |
| if (isFormElement(target)) { |
| // When grabbing the action from the element, it will have had the basename |
| // prefixed to ensure non-JS scenarios work, so strip it since we'll |
| // re-prefix in the router |
| let attr = target.getAttribute("action"); |
| action = attr ? stripBasename(attr, basename) : null; |
| method = target.getAttribute("method") || defaultMethod; |
| encType = getFormEncType(target.getAttribute("enctype")) || defaultEncType; |
| formData = new FormData(target); |
| } else if (isButtonElement(target) || isInputElement(target) && (target.type === "submit" || target.type === "image")) { |
| let form = target.form; |
| if (form == null) { |
| throw new Error(`Cannot submit a <button> or <input type="submit"> without a <form>`); |
| } |
| |
| // <button>/<input type="submit"> may override attributes of <form> |
| |
| // When grabbing the action from the element, it will have had the basename |
| // prefixed to ensure non-JS scenarios work, so strip it since we'll |
| // re-prefix in the router |
| let attr = target.getAttribute("formaction") || form.getAttribute("action"); |
| action = attr ? stripBasename(attr, basename) : null; |
| method = target.getAttribute("formmethod") || form.getAttribute("method") || defaultMethod; |
| encType = getFormEncType(target.getAttribute("formenctype")) || getFormEncType(form.getAttribute("enctype")) || defaultEncType; |
| |
| // Build a FormData object populated from a form and submitter |
| formData = new FormData(form, target); |
| |
| // If this browser doesn't support the `FormData(el, submitter)` format, |
| // then tack on the submitter value at the end. This is a lightweight |
| // solution that is not 100% spec compliant. For complete support in older |
| // browsers, consider using the `formdata-submitter-polyfill` package |
| if (!isFormDataSubmitterSupported()) { |
| let { |
| name, |
| type, |
| value |
| } = target; |
| if (type === "image") { |
| let prefix = name ? `${name}.` : ""; |
| formData.append(`${prefix}x`, "0"); |
| formData.append(`${prefix}y`, "0"); |
| } else if (name) { |
| formData.append(name, value); |
| } |
| } |
| } else if (isHtmlElement(target)) { |
| throw new Error(`Cannot submit element that is not <form>, <button>, or ` + `<input type="submit|image">`); |
| } else { |
| method = defaultMethod; |
| action = null; |
| encType = defaultEncType; |
| body = target; |
| } |
| |
| // Send body for <Form encType="text/plain" so we encode it into text |
| if (formData && encType === "text/plain") { |
| body = formData; |
| formData = undefined; |
| } |
| return { |
| action, |
| method: method.toLowerCase(), |
| encType, |
| formData, |
| body |
| }; |
| } |
| |
| /** |
| * NOTE: If you refactor this to split up the modules into separate files, |
| * you'll need to update the rollup config for react-router-dom-v5-compat. |
| */ |
| //#endregion |
| // HEY YOU! DON'T TOUCH THIS VARIABLE! |
| // |
| // It is replaced with the proper version at build time via a babel plugin in |
| // the rollup config. |
| // |
| // Export a global property onto the window for React Router detection by the |
| // Core Web Vitals Technology Report. This way they can configure the `wappalyzer` |
| // to detect and properly classify live websites as being built with React Router: |
| // https://github.com/HTTPArchive/wappalyzer/blob/main/src/technologies/r.json |
| const REACT_ROUTER_VERSION = "6"; |
| try { |
| window.__reactRouterVersion = REACT_ROUTER_VERSION; |
| } catch (e) { |
| // no-op |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| //#region Routers |
| //////////////////////////////////////////////////////////////////////////////// |
| function createBrowserRouter(routes, opts) { |
| return createRouter({ |
| basename: opts?.basename, |
| future: { |
| ...opts?.future, |
| v7_prependBasename: true |
| }, |
| history: createBrowserHistory({ |
| window: opts?.window |
| }), |
| hydrationData: opts?.hydrationData || parseHydrationData(), |
| routes, |
| mapRouteProperties: UNSAFE_mapRouteProperties, |
| dataStrategy: opts?.dataStrategy, |
| patchRoutesOnNavigation: opts?.patchRoutesOnNavigation, |
| window: opts?.window |
| }).initialize(); |
| } |
| function createHashRouter(routes, opts) { |
| return createRouter({ |
| basename: opts?.basename, |
| future: { |
| ...opts?.future, |
| v7_prependBasename: true |
| }, |
| history: createHashHistory({ |
| window: opts?.window |
| }), |
| hydrationData: opts?.hydrationData || parseHydrationData(), |
| routes, |
| mapRouteProperties: UNSAFE_mapRouteProperties, |
| dataStrategy: opts?.dataStrategy, |
| patchRoutesOnNavigation: opts?.patchRoutesOnNavigation, |
| window: opts?.window |
| }).initialize(); |
| } |
| function parseHydrationData() { |
| let state = window?.__staticRouterHydrationData; |
| if (state && state.errors) { |
| state = { |
| ...state, |
| errors: deserializeErrors(state.errors) |
| }; |
| } |
| return state; |
| } |
| function deserializeErrors(errors) { |
| if (!errors) return null; |
| let entries = Object.entries(errors); |
| let serialized = {}; |
| for (let [key, val] of entries) { |
| // Hey you! If you change this, please change the corresponding logic in |
| // serializeErrors in react-router-dom/server.tsx :) |
| if (val && val.__type === "RouteErrorResponse") { |
| serialized[key] = new UNSAFE_ErrorResponseImpl(val.status, val.statusText, val.data, val.internal === true); |
| } else if (val && val.__type === "Error") { |
| // Attempt to reconstruct the right type of Error (i.e., ReferenceError) |
| if (val.__subType) { |
| let ErrorConstructor = window[val.__subType]; |
| if (typeof ErrorConstructor === "function") { |
| try { |
| // @ts-expect-error |
| let error = new ErrorConstructor(val.message); |
| // Wipe away the client-side stack trace. Nothing to fill it in with |
| // because we don't serialize SSR stack traces for security reasons |
| error.stack = ""; |
| serialized[key] = error; |
| } catch (e) { |
| // no-op - fall through and create a normal Error |
| } |
| } |
| } |
| if (serialized[key] == null) { |
| let error = new Error(val.message); |
| // Wipe away the client-side stack trace. Nothing to fill it in with |
| // because we don't serialize SSR stack traces for security reasons |
| error.stack = ""; |
| serialized[key] = error; |
| } |
| } else { |
| serialized[key] = val; |
| } |
| } |
| return serialized; |
| } |
| |
| //#endregion |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| //#region Contexts |
| //////////////////////////////////////////////////////////////////////////////// |
| const ViewTransitionContext = /*#__PURE__*/React.createContext({ |
| isTransitioning: false |
| }); |
| { |
| ViewTransitionContext.displayName = "ViewTransition"; |
| } |
| |
| // TODO: (v7) Change the useFetcher data from `any` to `unknown` |
| |
| const FetchersContext = /*#__PURE__*/React.createContext(new Map()); |
| { |
| FetchersContext.displayName = "Fetchers"; |
| } |
| |
| //#endregion |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| //#region Components |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| Webpack + React 17 fails to compile on any of the following because webpack |
| complains that `startTransition` doesn't exist in `React`: |
| * import { startTransition } from "react" |
| * import * as React from from "react"; |
| "startTransition" in React ? React.startTransition(() => setState()) : setState() |
| * import * as React from from "react"; |
| "startTransition" in React ? React["startTransition"](() => setState()) : setState() |
| |
| Moving it to a constant such as the following solves the Webpack/React 17 issue: |
| * import * as React from from "react"; |
| const START_TRANSITION = "startTransition"; |
| START_TRANSITION in React ? React[START_TRANSITION](() => setState()) : setState() |
| |
| However, that introduces webpack/terser minification issues in production builds |
| in React 18 where minification/obfuscation ends up removing the call of |
| React.startTransition entirely from the first half of the ternary. Grabbing |
| this exported reference once up front resolves that issue. |
| |
| See https://github.com/remix-run/react-router/issues/10579 |
| */ |
| const START_TRANSITION = "startTransition"; |
| const startTransitionImpl = React[START_TRANSITION]; |
| const FLUSH_SYNC = "flushSync"; |
| const flushSyncImpl = ReactDOM[FLUSH_SYNC]; |
| const USE_ID = "useId"; |
| const useIdImpl = React[USE_ID]; |
| function startTransitionSafe(cb) { |
| if (startTransitionImpl) { |
| startTransitionImpl(cb); |
| } else { |
| cb(); |
| } |
| } |
| function flushSyncSafe(cb) { |
| if (flushSyncImpl) { |
| flushSyncImpl(cb); |
| } else { |
| cb(); |
| } |
| } |
| class Deferred { |
| status = "pending"; |
| |
| // @ts-expect-error - no initializer |
| |
| // @ts-expect-error - no initializer |
| |
| constructor() { |
| this.promise = new Promise((resolve, reject) => { |
| this.resolve = value => { |
| if (this.status === "pending") { |
| this.status = "resolved"; |
| resolve(value); |
| } |
| }; |
| this.reject = reason => { |
| if (this.status === "pending") { |
| this.status = "rejected"; |
| reject(reason); |
| } |
| }; |
| }); |
| } |
| } |
| |
| /** |
| * Given a Remix Router instance, render the appropriate UI |
| */ |
| function RouterProvider({ |
| fallbackElement, |
| router, |
| future |
| }) { |
| let [state, setStateImpl] = React.useState(router.state); |
| let [pendingState, setPendingState] = React.useState(); |
| let [vtContext, setVtContext] = React.useState({ |
| isTransitioning: false |
| }); |
| let [renderDfd, setRenderDfd] = React.useState(); |
| let [transition, setTransition] = React.useState(); |
| let [interruption, setInterruption] = React.useState(); |
| let fetcherData = React.useRef(new Map()); |
| let { |
| v7_startTransition |
| } = future || {}; |
| let optInStartTransition = React.useCallback(cb => { |
| if (v7_startTransition) { |
| startTransitionSafe(cb); |
| } else { |
| cb(); |
| } |
| }, [v7_startTransition]); |
| let setState = React.useCallback((newState, { |
| deletedFetchers, |
| flushSync: flushSync, |
| viewTransitionOpts: viewTransitionOpts |
| }) => { |
| deletedFetchers.forEach(key => fetcherData.current.delete(key)); |
| newState.fetchers.forEach((fetcher, key) => { |
| if (fetcher.data !== undefined) { |
| fetcherData.current.set(key, fetcher.data); |
| } |
| }); |
| let isViewTransitionUnavailable = router.window == null || router.window.document == null || typeof router.window.document.startViewTransition !== "function"; |
| |
| // If this isn't a view transition or it's not available in this browser, |
| // just update and be done with it |
| if (!viewTransitionOpts || isViewTransitionUnavailable) { |
| if (flushSync) { |
| flushSyncSafe(() => setStateImpl(newState)); |
| } else { |
| optInStartTransition(() => setStateImpl(newState)); |
| } |
| return; |
| } |
| |
| // flushSync + startViewTransition |
| if (flushSync) { |
| // Flush through the context to mark DOM elements as transition=ing |
| flushSyncSafe(() => { |
| // Cancel any pending transitions |
| if (transition) { |
| renderDfd && renderDfd.resolve(); |
| transition.skipTransition(); |
| } |
| setVtContext({ |
| isTransitioning: true, |
| flushSync: true, |
| currentLocation: viewTransitionOpts.currentLocation, |
| nextLocation: viewTransitionOpts.nextLocation |
| }); |
| }); |
| |
| // Update the DOM |
| let t = router.window.document.startViewTransition(() => { |
| flushSyncSafe(() => setStateImpl(newState)); |
| }); |
| |
| // Clean up after the animation completes |
| t.finished.finally(() => { |
| flushSyncSafe(() => { |
| setRenderDfd(undefined); |
| setTransition(undefined); |
| setPendingState(undefined); |
| setVtContext({ |
| isTransitioning: false |
| }); |
| }); |
| }); |
| flushSyncSafe(() => setTransition(t)); |
| return; |
| } |
| |
| // startTransition + startViewTransition |
| if (transition) { |
| // Interrupting an in-progress transition, cancel and let everything flush |
| // out, and then kick off a new transition from the interruption state |
| renderDfd && renderDfd.resolve(); |
| transition.skipTransition(); |
| setInterruption({ |
| state: newState, |
| currentLocation: viewTransitionOpts.currentLocation, |
| nextLocation: viewTransitionOpts.nextLocation |
| }); |
| } else { |
| // Completed navigation update with opted-in view transitions, let 'er rip |
| setPendingState(newState); |
| setVtContext({ |
| isTransitioning: true, |
| flushSync: false, |
| currentLocation: viewTransitionOpts.currentLocation, |
| nextLocation: viewTransitionOpts.nextLocation |
| }); |
| } |
| }, [router.window, transition, renderDfd, fetcherData, optInStartTransition]); |
| |
| // Need to use a layout effect here so we are subscribed early enough to |
| // pick up on any render-driven redirects/navigations (useEffect/<Navigate>) |
| React.useLayoutEffect(() => router.subscribe(setState), [router, setState]); |
| |
| // When we start a view transition, create a Deferred we can use for the |
| // eventual "completed" render |
| React.useEffect(() => { |
| if (vtContext.isTransitioning && !vtContext.flushSync) { |
| setRenderDfd(new Deferred()); |
| } |
| }, [vtContext]); |
| |
| // Once the deferred is created, kick off startViewTransition() to update the |
| // DOM and then wait on the Deferred to resolve (indicating the DOM update has |
| // happened) |
| React.useEffect(() => { |
| if (renderDfd && pendingState && router.window) { |
| let newState = pendingState; |
| let renderPromise = renderDfd.promise; |
| let transition = router.window.document.startViewTransition(async () => { |
| optInStartTransition(() => setStateImpl(newState)); |
| await renderPromise; |
| }); |
| transition.finished.finally(() => { |
| setRenderDfd(undefined); |
| setTransition(undefined); |
| setPendingState(undefined); |
| setVtContext({ |
| isTransitioning: false |
| }); |
| }); |
| setTransition(transition); |
| } |
| }, [optInStartTransition, pendingState, renderDfd, router.window]); |
| |
| // When the new location finally renders and is committed to the DOM, this |
| // effect will run to resolve the transition |
| React.useEffect(() => { |
| if (renderDfd && pendingState && state.location.key === pendingState.location.key) { |
| renderDfd.resolve(); |
| } |
| }, [renderDfd, transition, state.location, pendingState]); |
| |
| // If we get interrupted with a new navigation during a transition, we skip |
| // the active transition, let it cleanup, then kick it off again here |
| React.useEffect(() => { |
| if (!vtContext.isTransitioning && interruption) { |
| setPendingState(interruption.state); |
| setVtContext({ |
| isTransitioning: true, |
| flushSync: false, |
| currentLocation: interruption.currentLocation, |
| nextLocation: interruption.nextLocation |
| }); |
| setInterruption(undefined); |
| } |
| }, [vtContext.isTransitioning, interruption]); |
| React.useEffect(() => { |
| UNSAFE_warning(fallbackElement == null || !router.future.v7_partialHydration, "`<RouterProvider fallbackElement>` is deprecated when using " + "`v7_partialHydration`, use a `HydrateFallback` component instead") ; |
| // Only log this once on initial mount |
| // eslint-disable-next-line react-hooks/exhaustive-deps |
| }, []); |
| let navigator = React.useMemo(() => { |
| return { |
| createHref: router.createHref, |
| encodeLocation: router.encodeLocation, |
| go: n => router.navigate(n), |
| push: (to, state, opts) => router.navigate(to, { |
| state, |
| preventScrollReset: opts?.preventScrollReset |
| }), |
| replace: (to, state, opts) => router.navigate(to, { |
| replace: true, |
| state, |
| preventScrollReset: opts?.preventScrollReset |
| }) |
| }; |
| }, [router]); |
| let basename = router.basename || "/"; |
| let dataRouterContext = React.useMemo(() => ({ |
| router, |
| navigator, |
| static: false, |
| basename |
| }), [router, navigator, basename]); |
| let routerFuture = React.useMemo(() => ({ |
| v7_relativeSplatPath: router.future.v7_relativeSplatPath |
| }), [router.future.v7_relativeSplatPath]); |
| |
| // The fragment and {null} here are important! We need them to keep React 18's |
| // useId happy when we are server-rendering since we may have a <script> here |
| // containing the hydrated server-side staticContext (from StaticRouterProvider). |
| // useId relies on the component tree structure to generate deterministic id's |
| // so we need to ensure it remains the same on the client even though |
| // we don't need the <script> tag |
| return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(UNSAFE_DataRouterContext.Provider, { |
| value: dataRouterContext |
| }, /*#__PURE__*/React.createElement(UNSAFE_DataRouterStateContext.Provider, { |
| value: state |
| }, /*#__PURE__*/React.createElement(FetchersContext.Provider, { |
| value: fetcherData.current |
| }, /*#__PURE__*/React.createElement(ViewTransitionContext.Provider, { |
| value: vtContext |
| }, /*#__PURE__*/React.createElement(Router, { |
| basename: basename, |
| location: state.location, |
| navigationType: state.historyAction, |
| navigator: navigator, |
| future: routerFuture |
| }, state.initialized || router.future.v7_partialHydration ? /*#__PURE__*/React.createElement(MemoizedDataRoutes, { |
| routes: router.routes, |
| future: router.future, |
| state: state |
| }) : fallbackElement))))), null); |
| } |
| |
| // Memoize to avoid re-renders when updating `ViewTransitionContext` |
| const MemoizedDataRoutes = /*#__PURE__*/React.memo(DataRoutes); |
| function DataRoutes({ |
| routes, |
| future, |
| state |
| }) { |
| return UNSAFE_useRoutesImpl(routes, undefined, state, future); |
| } |
| /** |
| * A `<Router>` for use in web browsers. Provides the cleanest URLs. |
| */ |
| function BrowserRouter({ |
| basename, |
| children, |
| future, |
| window |
| }) { |
| let historyRef = React.useRef(); |
| if (historyRef.current == null) { |
| historyRef.current = createBrowserHistory({ |
| window, |
| v5Compat: true |
| }); |
| } |
| let history = historyRef.current; |
| let [state, setStateImpl] = React.useState({ |
| action: history.action, |
| location: history.location |
| }); |
| let { |
| v7_startTransition |
| } = future || {}; |
| let setState = React.useCallback(newState => { |
| v7_startTransition && startTransitionImpl ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); |
| }, [setStateImpl, v7_startTransition]); |
| React.useLayoutEffect(() => history.listen(setState), [history, setState]); |
| return /*#__PURE__*/React.createElement(Router, { |
| basename: basename, |
| children: children, |
| location: state.location, |
| navigationType: state.action, |
| navigator: history, |
| future: future |
| }); |
| } |
| /** |
| * A `<Router>` for use in web browsers. Stores the location in the hash |
| * portion of the URL so it is not sent to the server. |
| */ |
| function HashRouter({ |
| basename, |
| children, |
| future, |
| window |
| }) { |
| let historyRef = React.useRef(); |
| if (historyRef.current == null) { |
| historyRef.current = createHashHistory({ |
| window, |
| v5Compat: true |
| }); |
| } |
| let history = historyRef.current; |
| let [state, setStateImpl] = React.useState({ |
| action: history.action, |
| location: history.location |
| }); |
| let { |
| v7_startTransition |
| } = future || {}; |
| let setState = React.useCallback(newState => { |
| v7_startTransition && startTransitionImpl ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); |
| }, [setStateImpl, v7_startTransition]); |
| React.useLayoutEffect(() => history.listen(setState), [history, setState]); |
| return /*#__PURE__*/React.createElement(Router, { |
| basename: basename, |
| children: children, |
| location: state.location, |
| navigationType: state.action, |
| navigator: history, |
| future: future |
| }); |
| } |
| /** |
| * A `<Router>` that accepts a pre-instantiated history object. It's important |
| * to note that using your own history object is highly discouraged and may add |
| * two versions of the history library to your bundles unless you use the same |
| * version of the history library that React Router uses internally. |
| */ |
| function HistoryRouter({ |
| basename, |
| children, |
| future, |
| history |
| }) { |
| let [state, setStateImpl] = React.useState({ |
| action: history.action, |
| location: history.location |
| }); |
| let { |
| v7_startTransition |
| } = future || {}; |
| let setState = React.useCallback(newState => { |
| v7_startTransition && startTransitionImpl ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); |
| }, [setStateImpl, v7_startTransition]); |
| React.useLayoutEffect(() => history.listen(setState), [history, setState]); |
| return /*#__PURE__*/React.createElement(Router, { |
| basename: basename, |
| children: children, |
| location: state.location, |
| navigationType: state.action, |
| navigator: history, |
| future: future |
| }); |
| } |
| { |
| HistoryRouter.displayName = "unstable_HistoryRouter"; |
| } |
| const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined"; |
| const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; |
| |
| /** |
| * The public API for rendering a history-aware `<a>`. |
| */ |
| const Link = /*#__PURE__*/React.forwardRef(function LinkWithRef({ |
| onClick, |
| relative, |
| reloadDocument, |
| replace, |
| state, |
| target, |
| to, |
| preventScrollReset, |
| viewTransition, |
| ...rest |
| }, ref) { |
| let { |
| basename |
| } = React.useContext(UNSAFE_NavigationContext); |
| |
| // Rendered into <a href> for absolute URLs |
| let absoluteHref; |
| let isExternal = false; |
| if (typeof to === "string" && ABSOLUTE_URL_REGEX.test(to)) { |
| // Render the absolute href server- and client-side |
| absoluteHref = to; |
| |
| // Only check for external origins client-side |
| if (isBrowser) { |
| try { |
| let currentUrl = new URL(window.location.href); |
| let targetUrl = to.startsWith("//") ? new URL(currentUrl.protocol + to) : new URL(to); |
| let path = stripBasename(targetUrl.pathname, basename); |
| if (targetUrl.origin === currentUrl.origin && path != null) { |
| // Strip the protocol/origin/basename for same-origin absolute URLs |
| to = path + targetUrl.search + targetUrl.hash; |
| } else { |
| isExternal = true; |
| } |
| } catch (e) { |
| // We can't do external URL detection without a valid URL |
| UNSAFE_warning(false, `<Link to="${to}"> contains an invalid URL which will probably break ` + `when clicked - please update to a valid URL path.`) ; |
| } |
| } |
| } |
| |
| // Rendered into <a href> for relative URLs |
| let href = useHref(to, { |
| relative |
| }); |
| let internalOnClick = useLinkClickHandler(to, { |
| replace, |
| state, |
| target, |
| preventScrollReset, |
| relative, |
| viewTransition |
| }); |
| function handleClick(event) { |
| if (onClick) onClick(event); |
| if (!event.defaultPrevented) { |
| internalOnClick(event); |
| } |
| } |
| return ( |
| /*#__PURE__*/ |
| // eslint-disable-next-line jsx-a11y/anchor-has-content |
| React.createElement("a", Object.assign({}, rest, { |
| href: absoluteHref || href, |
| onClick: isExternal || reloadDocument ? onClick : handleClick, |
| ref: ref, |
| target: target |
| })) |
| ); |
| }); |
| { |
| Link.displayName = "Link"; |
| } |
| /** |
| * A `<Link>` wrapper that knows if it's "active" or not. |
| */ |
| const NavLink = /*#__PURE__*/React.forwardRef(function NavLinkWithRef({ |
| "aria-current": ariaCurrentProp = "page", |
| caseSensitive = false, |
| className: classNameProp = "", |
| end = false, |
| style: styleProp, |
| to, |
| viewTransition, |
| children, |
| ...rest |
| }, ref) { |
| let path = useResolvedPath(to, { |
| relative: rest.relative |
| }); |
| let location = useLocation(); |
| let routerState = React.useContext(UNSAFE_DataRouterStateContext); |
| let { |
| navigator, |
| basename |
| } = React.useContext(UNSAFE_NavigationContext); |
| let isTransitioning = routerState != null && |
| // Conditional usage is OK here because the usage of a data router is static |
| // eslint-disable-next-line react-hooks/rules-of-hooks |
| useViewTransitionState(path) && viewTransition === true; |
| let toPathname = navigator.encodeLocation ? navigator.encodeLocation(path).pathname : path.pathname; |
| let locationPathname = location.pathname; |
| let nextLocationPathname = routerState && routerState.navigation && routerState.navigation.location ? routerState.navigation.location.pathname : null; |
| if (!caseSensitive) { |
| locationPathname = locationPathname.toLowerCase(); |
| nextLocationPathname = nextLocationPathname ? nextLocationPathname.toLowerCase() : null; |
| toPathname = toPathname.toLowerCase(); |
| } |
| if (nextLocationPathname && basename) { |
| nextLocationPathname = stripBasename(nextLocationPathname, basename) || nextLocationPathname; |
| } |
| |
| // If the `to` has a trailing slash, look at that exact spot. Otherwise, |
| // we're looking for a slash _after_ what's in `to`. For example: |
| // |
| // <NavLink to="/users"> and <NavLink to="/users/"> |
| // both want to look for a / at index 6 to match URL `/users/matt` |
| const endSlashPosition = toPathname !== "/" && toPathname.endsWith("/") ? toPathname.length - 1 : toPathname.length; |
| let isActive = locationPathname === toPathname || !end && locationPathname.startsWith(toPathname) && locationPathname.charAt(endSlashPosition) === "/"; |
| let isPending = nextLocationPathname != null && (nextLocationPathname === toPathname || !end && nextLocationPathname.startsWith(toPathname) && nextLocationPathname.charAt(toPathname.length) === "/"); |
| let renderProps = { |
| isActive, |
| isPending, |
| isTransitioning |
| }; |
| let ariaCurrent = isActive ? ariaCurrentProp : undefined; |
| let className; |
| if (typeof classNameProp === "function") { |
| className = classNameProp(renderProps); |
| } else { |
| // If the className prop is not a function, we use a default `active` |
| // class for <NavLink />s that are active. In v5 `active` was the default |
| // value for `activeClassName`, but we are removing that API and can still |
| // use the old default behavior for a cleaner upgrade path and keep the |
| // simple styling rules working as they currently do. |
| className = [classNameProp, isActive ? "active" : null, isPending ? "pending" : null, isTransitioning ? "transitioning" : null].filter(Boolean).join(" "); |
| } |
| let style = typeof styleProp === "function" ? styleProp(renderProps) : styleProp; |
| return /*#__PURE__*/React.createElement(Link, Object.assign({}, rest, { |
| "aria-current": ariaCurrent, |
| className: className, |
| ref: ref, |
| style: style, |
| to: to, |
| viewTransition: viewTransition |
| }), typeof children === "function" ? children(renderProps) : children); |
| }); |
| { |
| NavLink.displayName = "NavLink"; |
| } |
| |
| /** |
| * Form props shared by navigations and fetchers |
| */ |
| |
| /** |
| * Form props available to fetchers |
| */ |
| |
| /** |
| * Form props available to navigations |
| */ |
| |
| /** |
| * A `@remix-run/router`-aware `<form>`. It behaves like a normal form except |
| * that the interaction with the server is with `fetch` instead of new document |
| * requests, allowing components to add nicer UX to the page as the form is |
| * submitted and returns with data. |
| */ |
| const Form = /*#__PURE__*/React.forwardRef(({ |
| fetcherKey, |
| navigate, |
| reloadDocument, |
| replace, |
| state, |
| method: _method = defaultMethod, |
| action, |
| onSubmit, |
| relative, |
| preventScrollReset, |
| viewTransition, |
| ...props |
| }, forwardedRef) => { |
| let submit = useSubmit(); |
| let formAction = useFormAction(action, { |
| relative |
| }); |
| let formMethod = _method.toLowerCase() === "get" ? "get" : "post"; |
| let submitHandler = event => { |
| onSubmit && onSubmit(event); |
| if (event.defaultPrevented) return; |
| event.preventDefault(); |
| let submitter = event.nativeEvent.submitter; |
| let submitMethod = submitter?.getAttribute("formmethod") || _method; |
| submit(submitter || event.currentTarget, { |
| fetcherKey, |
| method: submitMethod, |
| navigate, |
| replace, |
| state, |
| relative, |
| preventScrollReset, |
| viewTransition |
| }); |
| }; |
| return /*#__PURE__*/React.createElement("form", Object.assign({ |
| ref: forwardedRef, |
| method: formMethod, |
| action: formAction, |
| onSubmit: reloadDocument ? onSubmit : submitHandler |
| }, props)); |
| }); |
| { |
| Form.displayName = "Form"; |
| } |
| /** |
| * This component will emulate the browser's scroll restoration on location |
| * changes. |
| */ |
| function ScrollRestoration({ |
| getKey, |
| storageKey |
| }) { |
| useScrollRestoration({ |
| getKey, |
| storageKey |
| }); |
| return null; |
| } |
| { |
| ScrollRestoration.displayName = "ScrollRestoration"; |
| } |
| //#endregion |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| //#region Hooks |
| //////////////////////////////////////////////////////////////////////////////// |
| var DataRouterHook = /*#__PURE__*/function (DataRouterHook) { |
| DataRouterHook["UseScrollRestoration"] = "useScrollRestoration"; |
| DataRouterHook["UseSubmit"] = "useSubmit"; |
| DataRouterHook["UseSubmitFetcher"] = "useSubmitFetcher"; |
| DataRouterHook["UseFetcher"] = "useFetcher"; |
| DataRouterHook["useViewTransitionState"] = "useViewTransitionState"; |
| return DataRouterHook; |
| }(DataRouterHook || {}); |
| var DataRouterStateHook = /*#__PURE__*/function (DataRouterStateHook) { |
| DataRouterStateHook["UseFetcher"] = "useFetcher"; |
| DataRouterStateHook["UseFetchers"] = "useFetchers"; |
| DataRouterStateHook["UseScrollRestoration"] = "useScrollRestoration"; |
| return DataRouterStateHook; |
| }(DataRouterStateHook || {}); // Internal hooks |
| function getDataRouterConsoleError(hookName) { |
| return `${hookName} must be used within a data router. See https://reactrouter.com/routers/picking-a-router.`; |
| } |
| function useDataRouterContext(hookName) { |
| let ctx = React.useContext(UNSAFE_DataRouterContext); |
| !ctx ? UNSAFE_invariant(false, getDataRouterConsoleError(hookName)) : void 0; |
| return ctx; |
| } |
| function useDataRouterState(hookName) { |
| let state = React.useContext(UNSAFE_DataRouterStateContext); |
| !state ? UNSAFE_invariant(false, getDataRouterConsoleError(hookName)) : void 0; |
| return state; |
| } |
| |
| // External hooks |
| |
| /** |
| * Handles the click behavior for router `<Link>` components. This is useful if |
| * you need to create custom `<Link>` components with the same click behavior we |
| * use in our exported `<Link>`. |
| */ |
| function useLinkClickHandler(to, { |
| target, |
| replace: replaceProp, |
| state, |
| preventScrollReset, |
| relative, |
| viewTransition |
| } = {}) { |
| let navigate = useNavigate(); |
| let location = useLocation(); |
| let path = useResolvedPath(to, { |
| relative |
| }); |
| return React.useCallback(event => { |
| if (shouldProcessLinkClick(event, target)) { |
| event.preventDefault(); |
| |
| // If the URL hasn't changed, a regular <a> will do a replace instead of |
| // a push, so do the same here unless the replace prop is explicitly set |
| let replace = replaceProp !== undefined ? replaceProp : createPath(location) === createPath(path); |
| navigate(to, { |
| replace, |
| state, |
| preventScrollReset, |
| relative, |
| viewTransition |
| }); |
| } |
| }, [location, navigate, path, replaceProp, state, target, to, preventScrollReset, relative, viewTransition]); |
| } |
| |
| /** |
| * A convenient wrapper for reading and writing search parameters via the |
| * URLSearchParams interface. |
| */ |
| function useSearchParams(defaultInit) { |
| UNSAFE_warning(typeof URLSearchParams !== "undefined", `You cannot use the \`useSearchParams\` hook in a browser that does not ` + `support the URLSearchParams API. If you need to support Internet ` + `Explorer 11, we recommend you load a polyfill such as ` + `https://github.com/ungap/url-search-params.`) ; |
| let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit)); |
| let hasSetSearchParamsRef = React.useRef(false); |
| let location = useLocation(); |
| let searchParams = React.useMemo(() => |
| // Only merge in the defaults if we haven't yet called setSearchParams. |
| // Once we call that we want those to take precedence, otherwise you can't |
| // remove a param with setSearchParams({}) if it has an initial value |
| getSearchParamsForLocation(location.search, hasSetSearchParamsRef.current ? null : defaultSearchParamsRef.current), [location.search]); |
| let navigate = useNavigate(); |
| let setSearchParams = React.useCallback((nextInit, navigateOptions) => { |
| const newSearchParams = createSearchParams(typeof nextInit === "function" ? nextInit(searchParams) : nextInit); |
| hasSetSearchParamsRef.current = true; |
| navigate("?" + newSearchParams, navigateOptions); |
| }, [navigate, searchParams]); |
| return [searchParams, setSearchParams]; |
| } |
| |
| /** |
| * Submits a HTML `<form>` to the server without reloading the page. |
| */ |
| |
| /** |
| * Submits a fetcher `<form>` to the server without reloading the page. |
| */ |
| |
| function validateClientSideSubmission() { |
| if (typeof document === "undefined") { |
| throw new Error("You are calling submit during the server render. " + "Try calling submit within a `useEffect` or callback instead."); |
| } |
| } |
| let fetcherId = 0; |
| let getUniqueFetcherId = () => `__${String(++fetcherId)}__`; |
| |
| /** |
| * Returns a function that may be used to programmatically submit a form (or |
| * some arbitrary data) to the server. |
| */ |
| function useSubmit() { |
| let { |
| router |
| } = useDataRouterContext(DataRouterHook.UseSubmit); |
| let { |
| basename |
| } = React.useContext(UNSAFE_NavigationContext); |
| let currentRouteId = UNSAFE_useRouteId(); |
| return React.useCallback((target, options = {}) => { |
| validateClientSideSubmission(); |
| let { |
| action, |
| method, |
| encType, |
| formData, |
| body |
| } = getFormSubmissionInfo(target, basename); |
| if (options.navigate === false) { |
| let key = options.fetcherKey || getUniqueFetcherId(); |
| router.fetch(key, currentRouteId, options.action || action, { |
| preventScrollReset: options.preventScrollReset, |
| formData, |
| body, |
| formMethod: options.method || method, |
| formEncType: options.encType || encType, |
| flushSync: options.flushSync |
| }); |
| } else { |
| router.navigate(options.action || action, { |
| preventScrollReset: options.preventScrollReset, |
| formData, |
| body, |
| formMethod: options.method || method, |
| formEncType: options.encType || encType, |
| replace: options.replace, |
| state: options.state, |
| fromRouteId: currentRouteId, |
| flushSync: options.flushSync, |
| viewTransition: options.viewTransition |
| }); |
| } |
| }, [router, basename, currentRouteId]); |
| } |
| |
| // v7: Eventually we should deprecate this entirely in favor of using the |
| // router method directly? |
| function useFormAction(action, { |
| relative |
| } = {}) { |
| let { |
| basename |
| } = React.useContext(UNSAFE_NavigationContext); |
| let routeContext = React.useContext(UNSAFE_RouteContext); |
| !routeContext ? UNSAFE_invariant(false, "useFormAction must be used inside a RouteContext") : void 0; |
| let [match] = routeContext.matches.slice(-1); |
| // Shallow clone path so we can modify it below, otherwise we modify the |
| // object referenced by useMemo inside useResolvedPath |
| let path = { |
| ...useResolvedPath(action ? action : ".", { |
| relative |
| }) |
| }; |
| |
| // If no action was specified, browsers will persist current search params |
| // when determining the path, so match that behavior |
| // https://github.com/remix-run/remix/issues/927 |
| let location = useLocation(); |
| if (action == null) { |
| // Safe to write to this directly here since if action was undefined, we |
| // would have called useResolvedPath(".") which will never include a search |
| path.search = location.search; |
| |
| // When grabbing search params from the URL, remove any included ?index param |
| // since it might not apply to our contextual route. We add it back based |
| // on match.route.index below |
| let params = new URLSearchParams(path.search); |
| let indexValues = params.getAll("index"); |
| let hasNakedIndexParam = indexValues.some(v => v === ""); |
| if (hasNakedIndexParam) { |
| params.delete("index"); |
| indexValues.filter(v => v).forEach(v => params.append("index", v)); |
| let qs = params.toString(); |
| path.search = qs ? `?${qs}` : ""; |
| } |
| } |
| if ((!action || action === ".") && match.route.index) { |
| path.search = path.search ? path.search.replace(/^\?/, "?index&") : "?index"; |
| } |
| |
| // If we're operating within a basename, prepend it to the pathname prior |
| // to creating the form action. If this is a root navigation, then just use |
| // the raw basename which allows the basename to have full control over the |
| // presence of a trailing slash on root actions |
| if (basename !== "/") { |
| path.pathname = path.pathname === "/" ? basename : joinPaths([basename, path.pathname]); |
| } |
| return createPath(path); |
| } |
| // TODO: (v7) Change the useFetcher generic default from `any` to `unknown` |
| /** |
| * Interacts with route loaders and actions without causing a navigation. Great |
| * for any interaction that stays on the same page. |
| */ |
| function useFetcher({ |
| key |
| } = {}) { |
| let { |
| router |
| } = useDataRouterContext(DataRouterHook.UseFetcher); |
| let state = useDataRouterState(DataRouterStateHook.UseFetcher); |
| let fetcherData = React.useContext(FetchersContext); |
| let route = React.useContext(UNSAFE_RouteContext); |
| let routeId = route.matches[route.matches.length - 1]?.route.id; |
| !fetcherData ? UNSAFE_invariant(false, `useFetcher must be used inside a FetchersContext`) : void 0; |
| !route ? UNSAFE_invariant(false, `useFetcher must be used inside a RouteContext`) : void 0; |
| !(routeId != null) ? UNSAFE_invariant(false, `useFetcher can only be used on routes that contain a unique "id"`) : void 0; |
| |
| // Fetcher key handling |
| // OK to call conditionally to feature detect `useId` |
| // eslint-disable-next-line react-hooks/rules-of-hooks |
| let defaultKey = useIdImpl ? useIdImpl() : ""; |
| let [fetcherKey, setFetcherKey] = React.useState(key || defaultKey); |
| if (key && key !== fetcherKey) { |
| setFetcherKey(key); |
| } else if (!fetcherKey) { |
| // We will only fall through here when `useId` is not available |
| setFetcherKey(getUniqueFetcherId()); |
| } |
| |
| // Registration/cleanup |
| React.useEffect(() => { |
| router.getFetcher(fetcherKey); |
| return () => { |
| // Tell the router we've unmounted - if v7_fetcherPersist is enabled this |
| // will not delete immediately but instead queue up a delete after the |
| // fetcher returns to an `idle` state |
| router.deleteFetcher(fetcherKey); |
| }; |
| }, [router, fetcherKey]); |
| |
| // Fetcher additions |
| let load = React.useCallback((href, opts) => { |
| !routeId ? UNSAFE_invariant(false, "No routeId available for fetcher.load()") : void 0; |
| router.fetch(fetcherKey, routeId, href, opts); |
| }, [fetcherKey, routeId, router]); |
| let submitImpl = useSubmit(); |
| let submit = React.useCallback((target, opts) => { |
| submitImpl(target, { |
| ...opts, |
| navigate: false, |
| fetcherKey |
| }); |
| }, [fetcherKey, submitImpl]); |
| let FetcherForm = React.useMemo(() => { |
| let FetcherForm = /*#__PURE__*/React.forwardRef((props, ref) => { |
| return /*#__PURE__*/React.createElement(Form, Object.assign({}, props, { |
| navigate: false, |
| fetcherKey: fetcherKey, |
| ref: ref |
| })); |
| }); |
| { |
| FetcherForm.displayName = "fetcher.Form"; |
| } |
| return FetcherForm; |
| }, [fetcherKey]); |
| |
| // Exposed FetcherWithComponents |
| let fetcher = state.fetchers.get(fetcherKey) || IDLE_FETCHER; |
| let data = fetcherData.get(fetcherKey); |
| let fetcherWithComponents = React.useMemo(() => ({ |
| Form: FetcherForm, |
| submit, |
| load, |
| ...fetcher, |
| data |
| }), [FetcherForm, submit, load, fetcher, data]); |
| return fetcherWithComponents; |
| } |
| |
| /** |
| * Provides all fetchers currently on the page. Useful for layouts and parent |
| * routes that need to provide pending/optimistic UI regarding the fetch. |
| */ |
| function useFetchers() { |
| let state = useDataRouterState(DataRouterStateHook.UseFetchers); |
| return Array.from(state.fetchers.entries()).map(([key, fetcher]) => ({ |
| ...fetcher, |
| key |
| })); |
| } |
| const SCROLL_RESTORATION_STORAGE_KEY = "react-router-scroll-positions"; |
| let savedScrollPositions = {}; |
| |
| /** |
| * When rendered inside a RouterProvider, will restore scroll positions on navigations |
| */ |
| function useScrollRestoration({ |
| getKey, |
| storageKey |
| } = {}) { |
| let { |
| router |
| } = useDataRouterContext(DataRouterHook.UseScrollRestoration); |
| let { |
| restoreScrollPosition, |
| preventScrollReset |
| } = useDataRouterState(DataRouterStateHook.UseScrollRestoration); |
| let { |
| basename |
| } = React.useContext(UNSAFE_NavigationContext); |
| let location = useLocation(); |
| let matches = useMatches(); |
| let navigation = useNavigation(); |
| |
| // Trigger manual scroll restoration while we're active |
| React.useEffect(() => { |
| window.history.scrollRestoration = "manual"; |
| return () => { |
| window.history.scrollRestoration = "auto"; |
| }; |
| }, []); |
| |
| // Save positions on pagehide |
| usePageHide(React.useCallback(() => { |
| if (navigation.state === "idle") { |
| let key = (getKey ? getKey(location, matches) : null) || location.key; |
| savedScrollPositions[key] = window.scrollY; |
| } |
| try { |
| sessionStorage.setItem(storageKey || SCROLL_RESTORATION_STORAGE_KEY, JSON.stringify(savedScrollPositions)); |
| } catch (error) { |
| UNSAFE_warning(false, `Failed to save scroll positions in sessionStorage, <ScrollRestoration /> will not work properly (${error}).`) ; |
| } |
| window.history.scrollRestoration = "auto"; |
| }, [storageKey, getKey, navigation.state, location, matches])); |
| |
| // Read in any saved scroll locations |
| if (typeof document !== "undefined") { |
| // eslint-disable-next-line react-hooks/rules-of-hooks |
| React.useLayoutEffect(() => { |
| try { |
| let sessionPositions = sessionStorage.getItem(storageKey || SCROLL_RESTORATION_STORAGE_KEY); |
| if (sessionPositions) { |
| savedScrollPositions = JSON.parse(sessionPositions); |
| } |
| } catch (e) { |
| // no-op, use default empty object |
| } |
| }, [storageKey]); |
| |
| // Enable scroll restoration in the router |
| // eslint-disable-next-line react-hooks/rules-of-hooks |
| React.useLayoutEffect(() => { |
| let getKeyWithoutBasename = getKey && basename !== "/" ? (location, matches) => getKey( |
| // Strip the basename to match useLocation() |
| { |
| ...location, |
| pathname: stripBasename(location.pathname, basename) || location.pathname |
| }, matches) : getKey; |
| let disableScrollRestoration = router?.enableScrollRestoration(savedScrollPositions, () => window.scrollY, getKeyWithoutBasename); |
| return () => disableScrollRestoration && disableScrollRestoration(); |
| }, [router, basename, getKey]); |
| |
| // Restore scrolling when state.restoreScrollPosition changes |
| // eslint-disable-next-line react-hooks/rules-of-hooks |
| React.useLayoutEffect(() => { |
| // Explicit false means don't do anything (used for submissions) |
| if (restoreScrollPosition === false) { |
| return; |
| } |
| |
| // been here before, scroll to it |
| if (typeof restoreScrollPosition === "number") { |
| window.scrollTo(0, restoreScrollPosition); |
| return; |
| } |
| |
| // try to scroll to the hash |
| if (location.hash) { |
| let el = document.getElementById(decodeURIComponent(location.hash.slice(1))); |
| if (el) { |
| el.scrollIntoView(); |
| return; |
| } |
| } |
| |
| // Don't reset if this navigation opted out |
| if (preventScrollReset === true) { |
| return; |
| } |
| |
| // otherwise go to the top on new locations |
| window.scrollTo(0, 0); |
| }, [location, restoreScrollPosition, preventScrollReset]); |
| } |
| } |
| |
| /** |
| * Setup a callback to be fired on the window's `beforeunload` event. This is |
| * useful for saving some data to `window.localStorage` just before the page |
| * refreshes. |
| * |
| * Note: The `callback` argument should be a function created with |
| * `React.useCallback()`. |
| */ |
| function useBeforeUnload(callback, options) { |
| let { |
| capture |
| } = options || {}; |
| React.useEffect(() => { |
| let opts = capture != null ? { |
| capture |
| } : undefined; |
| window.addEventListener("beforeunload", callback, opts); |
| return () => { |
| window.removeEventListener("beforeunload", callback, opts); |
| }; |
| }, [callback, capture]); |
| } |
| |
| /** |
| * Setup a callback to be fired on the window's `pagehide` event. This is |
| * useful for saving some data to `window.localStorage` just before the page |
| * refreshes. This event is better supported than beforeunload across browsers. |
| * |
| * Note: The `callback` argument should be a function created with |
| * `React.useCallback()`. |
| */ |
| function usePageHide(callback, options) { |
| let { |
| capture |
| } = options || {}; |
| React.useEffect(() => { |
| let opts = capture != null ? { |
| capture |
| } : undefined; |
| window.addEventListener("pagehide", callback, opts); |
| return () => { |
| window.removeEventListener("pagehide", callback, opts); |
| }; |
| }, [callback, capture]); |
| } |
| |
| /** |
| * Wrapper around useBlocker to show a window.confirm prompt to users instead |
| * of building a custom UI with useBlocker. |
| * |
| * Warning: This has *a lot of rough edges* and behaves very differently (and |
| * very incorrectly in some cases) across browsers if user click addition |
| * back/forward navigations while the confirm is open. Use at your own risk. |
| */ |
| function usePrompt({ |
| when, |
| message |
| }) { |
| let blocker = useBlocker(when); |
| React.useEffect(() => { |
| if (blocker.state === "blocked") { |
| let proceed = window.confirm(message); |
| if (proceed) { |
| // This timeout is needed to avoid a weird "race" on POP navigations |
| // between the `window.history` revert navigation and the result of |
| // `window.confirm` |
| setTimeout(blocker.proceed, 0); |
| } else { |
| blocker.reset(); |
| } |
| } |
| }, [blocker, message]); |
| React.useEffect(() => { |
| if (blocker.state === "blocked" && !when) { |
| blocker.reset(); |
| } |
| }, [blocker, when]); |
| } |
| |
| /** |
| * Return a boolean indicating if there is an active view transition to the |
| * given href. You can use this value to render CSS classes or viewTransitionName |
| * styles onto your elements |
| * |
| * @param href The destination href |
| * @param [opts.relative] Relative routing type ("route" | "path") |
| */ |
| function useViewTransitionState(to, opts = {}) { |
| let vtContext = React.useContext(ViewTransitionContext); |
| !(vtContext != null) ? UNSAFE_invariant(false, "`useViewTransitionState` must be used within `react-router-dom`'s `RouterProvider`. " + "Did you accidentally import `RouterProvider` from `react-router`?") : void 0; |
| let { |
| basename |
| } = useDataRouterContext(DataRouterHook.useViewTransitionState); |
| let path = useResolvedPath(to, { |
| relative: opts.relative |
| }); |
| if (!vtContext.isTransitioning) { |
| return false; |
| } |
| let currentPath = stripBasename(vtContext.currentLocation.pathname, basename) || vtContext.currentLocation.pathname; |
| let nextPath = stripBasename(vtContext.nextLocation.pathname, basename) || vtContext.nextLocation.pathname; |
| |
| // Transition is active if we're going to or coming from the indicated |
| // destination. This ensures that other PUSH navigations that reverse |
| // an indicated transition apply. I.e., on the list view you have: |
| // |
| // <NavLink to="/details/1" viewTransition> |
| // |
| // If you click the breadcrumb back to the list view: |
| // |
| // <NavLink to="/list" viewTransition> |
| // |
| // We should apply the transition because it's indicated as active going |
| // from /list -> /details/1 and therefore should be active on the reverse |
| // (even though this isn't strictly a POP reverse) |
| return matchPath(path.pathname, nextPath) != null || matchPath(path.pathname, currentPath) != null; |
| } |
| |
| //#endregion |
| |
| export { BrowserRouter, Form, HashRouter, Link, NavLink, RouterProvider, ScrollRestoration, FetchersContext as UNSAFE_FetchersContext, ViewTransitionContext as UNSAFE_ViewTransitionContext, useScrollRestoration as UNSAFE_useScrollRestoration, createBrowserRouter, createHashRouter, createSearchParams, HistoryRouter as unstable_HistoryRouter, usePrompt as unstable_usePrompt, useBeforeUnload, useFetcher, useFetchers, useFormAction, useLinkClickHandler, useSearchParams, useSubmit, useViewTransitionState }; |
| //# sourceMappingURL=react-router-dom.development.js.map |