import React, { useCallback, useEffect, useMemo, useRef } from "react"; import { NavigateOptions } from "next/dist/shared/lib/app-router-context.shared-runtime"; import { usePathname, useSearchParams, useRouter as useNextRouter } from "next/navigation"; import NProgress from "nprogress"; import { getAnchorProperty, hasPreventProgressAttribute } from "./utils/getAnchorProperty"; import { isSameURL, isSameURLWithoutSearch } from "./utils/sameURL"; import { ProgressBarProps, RouterNProgressOptions } from "."; type PushStateInput = [data: any, unused: string, url?: string | URL | null | undefined]; export const AppProgressBar = React.memo( ({ color = "#0A2FFF", height = "2px", options, shallowRouting = false, disableSameURL = true, startPosition = 0, delay = 0, stopDelay = 0, style, nonce, targetPreprocessor, disableAnchorClick = false, }: ProgressBarProps) => { const styles = ( ); NProgress.configure(options || {}); // eslint-disable-next-line no-undef let progressDoneTimer: NodeJS.Timeout; const pathname = usePathname(); const searchParams = useSearchParams(); useEffect(() => { if (progressDoneTimer) clearTimeout(progressDoneTimer); progressDoneTimer = setTimeout(() => { NProgress.done(); }, stopDelay); }, [pathname, searchParams]); const elementsWithAttachedHandlers = useRef<(HTMLAnchorElement | SVGAElement)[]>([]); useEffect(() => { if (disableAnchorClick) { return; } // eslint-disable-next-line no-undef let timer: NodeJS.Timeout; const startProgress = () => { timer = setTimeout(() => { if (startPosition > 0) NProgress.set(startPosition); NProgress.start(); }, delay); }; const stopProgress = () => { if (timer) clearTimeout(timer); timer = setTimeout(() => { NProgress.done(); }, stopDelay); }; const handleAnchorClick: any = (event: MouseEvent) => { // Skip preventDefault if (event.defaultPrevented) return; const anchorElement = event.currentTarget as HTMLAnchorElement | SVGAElement; const target = event.target as HTMLElement | Element; // Check if the target or any of its parents have the attribute const preventProgress = hasPreventProgressAttribute(target) || anchorElement?.getAttribute("data-prevent-nprogress") === "true"; if (preventProgress) return; const anchorTarget = getAnchorProperty(anchorElement, "target"); // Skip anchors with target="_blank" if (anchorTarget === "_blank") return; // Skip control/command/option/alt+click if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; const targetHref = getAnchorProperty(anchorElement, "href"); const targetUrl = targetPreprocessor ? targetPreprocessor(new URL(targetHref)) : new URL(targetHref); const currentUrl = new URL(location.href); if (shallowRouting && isSameURLWithoutSearch(targetUrl, currentUrl) && disableSameURL) return; if (isSameURL(targetUrl, currentUrl) && disableSameURL) return; startProgress(); }; // eslint-disable-next-line no-undef const handleMutation: MutationCallback = () => { const anchorElements = Array.from(document.querySelectorAll("a")) as (HTMLAnchorElement | SVGAElement)[]; const validAnchorElements = anchorElements.filter((anchor) => { const href = getAnchorProperty(anchor, "href"); const isNProgressDisabled = anchor.getAttribute("data-disable-nprogress") === "true"; const isNotTelOrMailto = href && !href.startsWith("tel:") && !href.startsWith("mailto:") && !href.startsWith("blob:") && !href.startsWith("javascript:"); return !isNProgressDisabled && isNotTelOrMailto && getAnchorProperty(anchor, "target") !== "_blank"; }); validAnchorElements.forEach((anchor) => anchor.addEventListener("click", handleAnchorClick)); elementsWithAttachedHandlers.current = validAnchorElements; }; const mutationObserver = new MutationObserver(handleMutation); mutationObserver.observe(document, { childList: true, subtree: true }); const originalWindowHistoryPushState = window.history.pushState; // eslint-disable-next-line no-undef window.history.pushState = new Proxy(window.history.pushState, { apply: (target, thisArg, argArray: PushStateInput) => { stopProgress(); return target.apply(thisArg, argArray); }, }); return () => { mutationObserver.disconnect(); elementsWithAttachedHandlers.current.forEach((anchor) => { anchor.removeEventListener("click", handleAnchorClick); }); elementsWithAttachedHandlers.current = []; window.history.pushState = originalWindowHistoryPushState; }; }, []); return styles; }, (prevProps, nextProps) => { if (nextProps?.memo === false) { return false; } if (!nextProps?.shouldCompareComplexProps) { return true; } return ( prevProps?.color === nextProps?.color && prevProps?.height === nextProps?.height && prevProps?.shallowRouting === nextProps?.shallowRouting && prevProps?.startPosition === nextProps?.startPosition && prevProps?.delay === nextProps?.delay && prevProps?.disableSameURL === nextProps?.disableSameURL && prevProps?.stopDelay === nextProps?.stopDelay && prevProps?.nonce === nextProps?.nonce && JSON.stringify(prevProps?.options) === JSON.stringify(nextProps?.options) && prevProps?.style === nextProps?.style && prevProps.disableAnchorClick === nextProps.disableAnchorClick ); } ); AppProgressBar.displayName = "AppProgressBar"; export function useRouter() { const router = useNextRouter(); const startProgress = useCallback( (startPosition?: number) => { if (startPosition && startPosition > 0) NProgress.set(startPosition); NProgress.start(); }, [router] ); const progress = useCallback( (href: string, options?: NavigateOptions, NProgressOptions?: RouterNProgressOptions) => { if (NProgressOptions?.showProgressBar === false) { return router.push(href, options); } const currentUrl = new URL(location.href); const targetUrl = new URL(href, location.href); if (isSameURL(targetUrl, currentUrl) && NProgressOptions?.disableSameURL !== false) return router.push(href, options); startProgress(NProgressOptions?.startPosition); }, [router] ); const push = useCallback( (href: string, options?: NavigateOptions, NProgressOptions?: RouterNProgressOptions) => { progress(href, options, NProgressOptions); return router.push(href, options); }, [router, startProgress] ); const replace = useCallback( (href: string, options?: NavigateOptions, NProgressOptions?: RouterNProgressOptions) => { progress(href, options, NProgressOptions); return router.replace(href, options); }, [router, startProgress] ); const back = useCallback( (NProgressOptions?: RouterNProgressOptions) => { if (NProgressOptions?.showProgressBar === false) return router.back(); startProgress(NProgressOptions?.startPosition); return router.back(); }, [router] ); const enhancedRouter = useMemo(() => ({ ...router, push, replace, back }), [router, push, replace, back]); return enhancedRouter; }