UNPKG

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
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)); } } }