mirror of
https://github.com/makeplane/plane.git
synced 2025-12-25 16:19:43 +01:00
* chore: add n-progress lib. * chore: prevent unwanted n-progress from projects and notifications. * fix: lint errors.
304 lines
9.6 KiB
TypeScript
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;
|
|
}
|