Files
plane/web/core/lib/n-progress/AppProgressBar.tsx
Prateek Shourya 626464513d chore: add custom n-progress component to fix unwanted n-progress trigger issues. (#4965)
* chore: add n-progress lib.

* chore: prevent unwanted n-progress from projects and notifications.

* fix: lint errors.
2024-06-28 20:39:19 +05:30

304 lines
9.6 KiB
TypeScript

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 = (
<style nonce={nonce}>
{style ||
`
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: ${color};
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: ${height};
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px ${color}, 0 0 5px ${color};
opacity: 1.0;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: ${color};
border-left-color: ${color};
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
);
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;
}