AK-21/Graphite-Industrial-Intelligence
0
1import { parse as parsePattern } from "regexparam";2 3import {4 useBrowserLocation,5 useSearch as useBrowserSearch,6} from "./use-browser-location.js";7 8import {9 useRef,10 useContext,11 createContext,12 isValidElement,13 cloneElement,14 createElement as h,15 Fragment,16 forwardRef,17 useIsomorphicLayoutEffect,18 useEvent,19 useMemo,20} from "./react-deps.js";21import { absolutePath, relativePath, sanitizeSearch } from "./paths.js";22 23/*24 * Router and router context. Router is a lightweight object that represents the current25 * routing options: how location is managed, base path etc.26 *27 * There is a default router present for most of the use cases, however it can be overridden28 * via the <Router /> component.29 */30 31const defaultRouter = {32 hook: useBrowserLocation,33 searchHook: useBrowserSearch,34 parser: parsePattern,35 base: "",36 // this option is used to override the current location during SSR37 ssrPath: undefined,38 ssrSearch: undefined,39 // optional context to track render state during SSR40 ssrContext: undefined,41 // customizes how `href` props are transformed for <Link />42 hrefs: (x) => x,43 // wraps navigate calls, useful for view transitions44 aroundNav: (n, t, o) => n(t, o),45};46 47const RouterCtx = createContext(defaultRouter);48 49// gets the closest parent router from the context50export const useRouter = () => useContext(RouterCtx);51 52/**53 * Parameters context. Used by `useParams()` to get the54 * matched params from the innermost `Route` component.55 */56 57const Params0 = {},58 ParamsCtx = createContext(Params0);59 60export const useParams = () => useContext(ParamsCtx);61 62/*63 * Part 1, Hooks API: useRoute and useLocation64 */65 66// Internal version of useLocation to avoid redundant useRouter calls67 68const useLocationFromRouter = (router) => {69 const [location, navigate] = router.hook(router);70 71 // the function reference should stay the same between re-renders, so that72 // it can be passed down as an element prop without any performance concerns.73 // (This is achieved via `useEvent`.)74 return [75 relativePath(router.base, location),76 useEvent((to, opts) =>77 router.aroundNav(navigate, absolutePath(to, router.base), opts)78 ),79 ];80};81 82export const useLocation = () => useLocationFromRouter(useRouter());83 84export const useSearch = () => {85 const router = useRouter();86 return sanitizeSearch(router.searchHook(router));87};88 89export const matchRoute = (parser, route, path, loose) => {90 // if the input is a regexp, skip parsing91 const { pattern, keys } =92 route instanceof RegExp93 ? { keys: false, pattern: route }94 : parser(route || "*", loose);95 96 // array destructuring loses keys, so this is done in two steps97 const result = pattern.exec(path) || [];98 99 // when parser is in "loose" mode, `$base` is equal to the100 // first part of the route that matches the pattern101 // (e.g. for pattern `/a/:b` and path `/a/1/2/3` the `$base` is `a/1`)102 // we use this for route nesting103 const [$base, ...matches] = result;104 105 return $base !== undefined106 ? [107 true,108 109 (() => {110 // for regex paths, `keys` will always be false111 112 // an object with parameters matched, e.g. { foo: "bar" } for "/:foo"113 // we "zip" two arrays here to construct the object114 // ["foo"], ["bar"] โ { foo: "bar" }115 const groups =116 keys !== false117 ? Object.fromEntries(keys.map((key, i) => [key, matches[i]]))118 : result.groups;119 120 // convert the array to an instance of object121 // this makes it easier to integrate with the existing param implementation122 let obj = { ...matches };123 124 // merge named capture groups with matches array125 groups && Object.assign(obj, groups);126 127 return obj;128 })(),129 130 // the third value if only present when parser is in "loose" mode,131 // so that we can extract the base path for nested routes132 ...(loose ? [$base] : []),133 ]134 : [false, null];135};136 137export const useRoute = (pattern) =>138 matchRoute(useRouter().parser, pattern, useLocation()[0]);139 140/*141 * Part 2, Low Carb Router API: Router, Route, Link, Switch142 */143 144export const Router = ({ children, ...props }) => {145 // the router we will inherit from - it is the closest router in the tree,146 // unless the custom `hook` is provided (in that case it's the default one)147 const parent_ = useRouter();148 const parent = props.hook ? defaultRouter : parent_;149 150 // holds to the context value: the router object151 let value = parent;152 153 // when `ssrPath` contains a `?` character, we can extract the search from it.154 // also, ensure ssrSearch is always defined when ssrPath is provided, so that155 // useSearch behavior matches usePathname (proper SSR hydration when client156 // renders <Router> without props after server rendered with ssrPath/ssrSearch)157 const [path, search = props.ssrSearch ?? ""] =158 props.ssrPath?.split("?") ?? [];159 if (path) (props.ssrSearch = search), (props.ssrPath = path);160 161 // hooks can define their own `href` formatter (e.g. for hash location)162 props.hrefs = props.hrefs ?? props.hook?.hrefs;163 164 // hooks can define their own search hook (e.g. for memory location)165 props.searchHook = props.searchHook ?? props.hook?.searchHook;166 167 // what is happening below: to avoid unnecessary rerenders in child components,168 // we ensure that the router object reference is stable, unless there are any169 // changes that require reload (e.g. `base` prop changes -> all components that170 // get the router from the context should rerender, even if the component is memoized).171 // the expected behaviour is:172 //173 // 1) when the resulted router is no different from the parent, use parent174 // 2) if the custom `hook` prop is provided, we always inherit from the175 // default router instead. this resets all previously overridden options.176 // 3) when the router is customized here, it should stay stable between renders177 let ref = useRef({}),178 prev = ref.current,179 next = prev;180 181 for (let k in parent) {182 const option =183 k === "base"184 ? /* base is special case, it is appended to the parent's base */185 parent[k] + (props[k] ?? "")186 : props[k] ?? parent[k];187 188 if (prev === next && option !== next[k]) {189 ref.current = next = { ...next };190 }191 192 next[k] = option;193 194 // the new router is no different from the parent or from the memoized value, use parent195 if (option !== parent[k] || option !== value[k]) value = next;196 }197 198 return h(RouterCtx.Provider, { value, children });199};200 201const h_route = ({ children, component }, params) => {202 // React-Router style `component` prop203 if (component) return h(component, { params });204 205 // support render prop or plain children206 return typeof children === "function" ? children(params) : children;207};208 209// Cache params object between renders if values are shallow equal210const useCachedParams = (value) => {211 let prev = useRef(Params0);212 const curr = prev.current;213 return (prev.current =214 // Update cache if number of params changed or any value changed215 Object.keys(value).length !== Object.keys(curr).length ||216 Object.entries(value).some(([k, v]) => v !== curr[k])217 ? value // Return new value if there are changes218 : curr); // Return cached value if nothing changed219};220 221export function useSearchParams() {222 const [location, navigate] = useLocation();223 224 const search = useSearch();225 const searchParams = useMemo(() => new URLSearchParams(search), [search]);226 227 // cached value before next render, so you can call setSearchParams multiple times228 let tempSearchParams = searchParams;229 230 const setSearchParams = useEvent((nextInit, options) => {231 tempSearchParams = new URLSearchParams(232 typeof nextInit === "function" ? nextInit(tempSearchParams) : nextInit233 );234 navigate(235 location + (tempSearchParams.size ? "?" + tempSearchParams : ""),236 options237 );238 });239 240 return [searchParams, setSearchParams];241}242 243export const Route = ({ path, nest, match, ...renderProps }) => {244 const router = useRouter();245 const [location] = useLocationFromRouter(router);246 247 const [matches, routeParams, base] =248 // `match` is a special prop to give up control to the parent,249 // it is used by the `Switch` to avoid double matching250 match ?? matchRoute(router.parser, path, location, nest);251 252 // when `routeParams` is `null` (there was no match), the argument253 // below becomes {...null} = {}, see the Object Spread specs254 // https://tc39.es/proposal-object-rest-spread/#AbstractOperations-CopyDataProperties255 const params = useCachedParams({ ...useParams(), ...routeParams });256 257 if (!matches) return null;258 259 const children = base260 ? h(Router, { base }, h_route(renderProps, params))261 : h_route(renderProps, params);262 263 return h(ParamsCtx.Provider, { value: params, children });264};265 266export const Link = forwardRef((props, ref) => {267 const router = useRouter();268 const [currentPath, navigate] = useLocationFromRouter(router);269 270 const {271 to = "",272 href: targetPath = to,273 onClick: _onClick,274 asChild,275 children,276 className: cls,277 /* eslint-disable no-unused-vars */278 replace /* ignore nav props */,279 state /* ignore nav props */,280 transition /* ignore nav props */,281 /* eslint-enable no-unused-vars */282 283 ...restProps284 } = props;285 286 const onClick = useEvent((event) => {287 // ignores the navigation when clicked using right mouse button or288 // by holding a special modifier key: ctrl, command, win, alt, shift289 if (290 event.ctrlKey ||291 event.metaKey ||292 event.altKey ||293 event.shiftKey ||294 event.button !== 0295 )296 return;297 298 _onClick?.(event);299 if (!event.defaultPrevented) {300 event.preventDefault();301 navigate(targetPath, props);302 }303 });304 305 // handle nested routers and absolute paths306 const href = router.hrefs(307 targetPath[0] === "~" ? targetPath.slice(1) : router.base + targetPath,308 router // pass router as a second argument for convinience309 );310 311 return asChild && isValidElement(children)312 ? cloneElement(children, { onClick, href })313 : h("a", {314 ...restProps,315 onClick,316 href,317 // `className` can be a function to apply the class if this link is active318 className: cls?.call ? cls(currentPath === targetPath) : cls,319 children,320 ref,321 });322});323 324const flattenChildren = (children) =>325 Array.isArray(children)326 ? children.flatMap((c) =>327 flattenChildren(c && c.type === Fragment ? c.props.children : c)328 )329 : [children];330 331export const Switch = ({ children, location }) => {332 const router = useRouter();333 const [originalLocation] = useLocationFromRouter(router);334 335 for (const element of flattenChildren(children)) {336 let match = 0;337 338 if (339 isValidElement(element) &&340 // we don't require an element to be of type Route,341 // but we do require it to contain a truthy `path` prop.342 // this allows to use different components that wrap Route343 // inside of a switch, for example <AnimatedRoute />.344 (match = matchRoute(345 router.parser,346 element.props.path,347 location || originalLocation,348 element.props.nest349 ))[0]350 )351 return cloneElement(element, { match });352 }353 354 return null;355};356 357export const Redirect = (props) => {358 const { to, href = to } = props;359 const router = useRouter();360 const [, navigate] = useLocationFromRouter(router);361 const redirect = useEvent(() => navigate(to || href, props));362 const { ssrContext } = router;363 364 // redirect is guaranteed to be stable since it is returned from useEvent365 useIsomorphicLayoutEffect(() => {366 redirect();367 }, []); // eslint-disable-line react-hooks/exhaustive-deps368 369 if (ssrContext) {370 ssrContext.redirectTo = to;371 }372 373 return null;374};375 