easy-page-router
Version:
easy-page-router is a lightweight and easy-to-use JavaScript routing package that simplifies navigation in vanilla JavaScript, React, and React Native applications.
165 lines (164 loc) • 6.67 kB
JavaScript
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
const isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
const createHref = isReactNative ? "/" : "http://localhost/";
const createPageKey = "default";
const createRouterContext = {
index: 0,
pages: [{ pageKey: createPageKey, href: createHref }],
push: () => { },
back: () => { },
forward: () => { },
};
const PageContext = createContext(getPageProps(createPageKey, createHref, 0, 0));
const RouterContext = createContext(createRouterContext);
function getHref() {
if (isReactNative)
return createHref; // TODO: open app in different state
if (!global.window)
console.error("Error: No default location for SSR!");
return global.window?.location?.href || createHref;
}
function getDefaultHistory() {
return {
index: 0,
pages: [{ pageKey: createPageKey, href: getHref() }],
};
}
export function RouterProvider({ children, onPageChange, scrollSpeed = 500, scrollAlways = false, noScroll = false, }) {
const [history, setHistory] = useState(getDefaultHistory());
const pageActual = history.pages[history.index];
useEffect(() => {
if (onPageChange && pageActual?.href)
onPageChange(pageActual.href);
}, [pageActual?.href]);
const { handleChange, push, back, forward } = useMemo(() => {
const handleChange = (newHref) => {
setHistory((history) => {
const pageActual = history.pages[history.index];
const pageBack = history.pages[history.index + 1];
const pageForward = history.pages[history.index + 1];
if (newHref === pageActual?.href)
return history;
!noScroll && scrollAlways && scrollToTop(scrollSpeed);
if (newHref === pageBack?.href) {
return { ...history, index: history.index + 1 };
}
if (newHref === pageForward?.href) {
return { ...history, index: history.index - 1 };
}
// remove forwarded pages
const pages = history.pages.slice(history.index);
const newPage = {
pageKey: new Date().toISOString(),
href: newHref,
};
!noScroll && !scrollAlways && scrollToTop(scrollSpeed);
return {
index: 0,
pages: [newPage, ...pages],
};
});
};
const push = (to) => {
if (isReactNative) {
handleChange(to);
}
else {
window.history.pushState({ to }, "", to);
handleChange(window.location.href);
}
};
const back = () => {
setHistory((history) => {
let index = history.index + 1;
if (index >= history.pages.length) {
index = history.pages.length - 1;
}
!noScroll && scrollAlways && scrollToTop(scrollSpeed);
return { ...history, index };
});
};
const forward = () => {
setHistory((history) => {
let index = history.index - 1;
if (index < 0)
index = 0;
!noScroll && scrollAlways && scrollToTop(scrollSpeed);
return { ...history, index };
});
};
return { handleChange, push, back, forward };
}, [scrollSpeed, scrollAlways, noScroll]);
// listening browser url change (only for react)
useEffect(() => {
if (isReactNative)
return;
function listener() {
handleChange(window.location.href);
}
window.addEventListener("popstate", listener);
window.addEventListener("hashchange", listener);
return () => {
window.removeEventListener("popstate", listener);
window.removeEventListener("hashchange", listener);
};
}, [handleChange]);
// for useRouter outside Router
const actualPageProps = getPageProps(pageActual.pageKey, pageActual.href, history.index, history.index);
return React.createElement(RouterContext.Provider, { value: { ...history, push, back, forward } },
React.createElement(PageContext.Provider, { value: actualPageProps }, children));
}
export function Router({ renderPage, renderAnimation }) {
const { index, pages } = useContext(RouterContext);
const Page = renderPage;
const Animation = renderAnimation;
if (Animation) {
return React.createElement(React.Fragment, null, pages.map((page, i) => {
const pageProps = getPageProps(page.pageKey, page.href, i, index);
return (React.createElement(PageContext.Provider, { key: page.pageKey, value: pageProps },
React.createElement(Animation, { ...pageProps, page: React.createElement(Page, { ...pageProps }) })));
}).reverse());
}
else {
const activePage = pages[index];
const pageProps = getPageProps(activePage.pageKey, activePage.href, index, index);
return React.createElement(PageContext.Provider, { value: pageProps },
React.createElement(Page, { ...pageProps }));
}
}
export function getPageProps(pageKey, href, index, actualIndex) {
const url = new URL(href);
const searchParams = {};
url.searchParams.forEach((value, key) => searchParams[key] = value);
return {
pageKey,
href,
to: url.pathname + url.search,
path: url.pathname.split("/").map((p) => decodeURIComponent(p)).filter(Boolean),
searchParams,
state: index === actualIndex
? "active"
: index > actualIndex
? "back"
: "forward",
};
}
export function useRouter() {
const { push, back, forward } = useContext(RouterContext);
const pageProps = useContext(PageContext);
return { ...pageProps, push, back, forward };
}
export function scrollToTop(time, startScroll, startTime = new Date().getTime()) {
if (isReactNative) {
// TODO: implement
}
else {
if (window.scrollY > 0) {
if (!startScroll)
startScroll = window.scrollY;
const ratio = (new Date().getTime() - startTime) / time;
window.scrollTo(0, startScroll * (1 - ratio));
window.requestAnimationFrame(() => scrollToTop(time, startScroll, startTime));
}
}
}