UNPKG

react-navi

Version:

A batteries-included router for react.

670 lines (654 loc) 30.4 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('navi')) : typeof define === 'function' && define.amd ? define(['exports', 'react', 'navi'], factory) : (global = global || self, factory(global.ReactNavi = {}, global.React, global.Navi)); }(this, function (exports, React, navi) { 'use strict'; var HashScrollContext = React.createContext('auto'); function HashScroll(props) { if (!props.behavior) { return React.createElement(React.Fragment, null, props.children); } return (React.createElement(HashScrollContext.Provider, { value: props.behavior }, props.children)); } function smoothScroll(left, top) { try { window.scroll({ top: top, left: left, behavior: 'smooth', }); } catch (e) { window.scroll(left, top); } } var behaviors = { none: function () { }, auto: function (hash) { if (hash) { var id = document.getElementById(hash.slice(1)); if (id) { var _a = id.getBoundingClientRect(), top_1 = _a.top, left = _a.left; window.scroll(left + window.pageXOffset, top_1 + window.pageYOffset); } } else { window.scroll(0, 0); } }, smooth: function (hash) { if (hash) { var id = document.getElementById(hash.slice(1)); if (id) { var _a = id.getBoundingClientRect(), top_2 = _a.top, left = _a.left; smoothScroll(left + window.pageXOffset, top_2 + window.pageYOffset); // Focus the element, as default behavior is cancelled. // https://css-tricks.com/snippets/jquery/smooth-scrolling/ id.focus(); } } else { smoothScroll(0, 0); } }, }; function scrollToHash(hash, behavior) { if (behavior === void 0) { behavior = 'auto'; } var fn = typeof behavior === 'string' ? behaviors[behavior] : behavior; fn(hash); } var NaviContext = React.createContext({}); var NavConsumer = NaviContext.Consumer; var NavContextProvider = NaviContext.Provider; var __assign = (undefined && undefined.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __rest = (undefined && undefined.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; function isExternalHref(href) { // If this is an external link, return undefined so that the native // response will be used. return (!href || (typeof href === 'string' && (href.indexOf('://') !== -1 || href.indexOf('mailto:') === 0))); } function getLinkURL(href, routeURL) { if (!isExternalHref(href)) { // Resolve relative to the current "directory" if (routeURL && typeof href === 'string') { href = href[0] === '/' ? href : navi.joinPaths('/', routeURL.pathname, href); } return navi.createURLDescriptor(href); } } /** * Returns a boolean that indicates whether the user is currently * viewing the specified href. * @param href * @param options.exact If false, will match any URL underneath this href * @param options.loading If true, will match even if the route is currently loading */ var useActive = function (href, _a) { var _b = _a === void 0 ? {} : _a, _c = _b.exact, exact = _c === void 0 ? true : _c, _d = _b.loading, loading = _d === void 0 ? false : _d; var context = React.useContext(NaviContext); var route = loading ? context.busyRoute || context.steadyRoute : context.steadyRoute || context.busyRoute; var routeURL = route && route.url; var linkURL = getLinkURL(href, routeURL); return !!(linkURL && routeURL && (exact ? linkURL.pathname === routeURL.pathname : navi.modifyTrailingSlash(routeURL.pathname, 'add').indexOf(linkURL.pathname) === 0)); }; var useLinkProps = function (_a) { var disabled = _a.disabled, hashScrollBehavior = _a.hashScrollBehavior, href = _a.href, prefetch = _a.prefetch, state = _a.state, onClick = _a.onClick, onMouseEnter = _a.onMouseEnter; var _b, _c; if (prefetch && state) { prefetch = false; { console.warn("Warning: A <Link> component received both \"prefetch\" and \"state\" " + "props, but links with state cannot be prefetched. Skipping prefetch."); } } if (prefetch === true) { prefetch = 'mount'; { console.warn("Warning: A <Link> component received a \"prefetch\" value of \"true\". " + "This value is no longer supported - please set it to \"mount\" instead."); } } // Prefetch on hover by default. if (prefetch === undefined) { prefetch = 'hover'; } var hashScrollBehaviorFromContext = React.useContext(HashScrollContext); var context = React.useContext(NaviContext); var navigation = context.navigation; if (hashScrollBehavior === undefined) { hashScrollBehavior = hashScrollBehaviorFromContext; } var route = context.steadyRoute || context.busyRoute; var routeURL = React.useMemo(function () { return route && route.url; }, [(_b = route) === null || _b === void 0 ? void 0 : _b.url.href]); var linkURL = getLinkURL(href, routeURL); if (!isExternalHref(href)) { var resolvedHref = href; // Resolve relative to the current "directory" if (routeURL && typeof href === 'string') { resolvedHref = href[0] === '/' ? href : navi.joinPaths('/', routeURL.pathname, href); } linkURL = navi.createURLDescriptor(resolvedHref); } // We need a URL descriptor that stays referentially equal so that we don't // trigger prefetches more than we'd like. var memoizedLinkURL = React.useMemo(function () { return linkURL; }, [(_c = linkURL) === null || _c === void 0 ? void 0 : _c.href]); var doPrefetch = React.useMemo(function () { var hasPrefetched = false; return function () { if (!hasPrefetched && memoizedLinkURL && memoizedLinkURL.pathname && navigation) { hasPrefetched = true; navigation.prefetch(memoizedLinkURL).catch(function (e) { console.warn("A <Link> tried to prefetch \"" + memoizedLinkURL.pathname + "\", but the " + "router was unable to fetch this path."); }); } }; }, [memoizedLinkURL, navigation]); // Prefetch on mount if required, or if `prefetch` becomes `true`. React.useEffect(function () { if (prefetch === 'mount') { doPrefetch(); } }, [prefetch, doPrefetch]); var handleMouseEnter = React.useCallback(function (event) { if (prefetch === 'hover') { if (onMouseEnter) { onMouseEnter(event); } if (disabled) { event.preventDefault(); return; } if (!event.defaultPrevented) { doPrefetch(); } } }, [disabled, doPrefetch, onMouseEnter, prefetch]); var handleClick = React.useCallback(function (event) { // Let the browser handle the event directly if: // - The user used the middle/right mouse button // - The user was holding a modifier key // - A `target` property is set (which may cause the browser to open the // link in another tab) if (event.button === 0 && !(event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)) { if (disabled) { event.preventDefault(); return; } if (onClick) { onClick(event); } // Sanity check if (!routeURL) { return; } if (!event.defaultPrevented && linkURL) { event.preventDefault(); var isSamePathname = navi.modifyTrailingSlash(linkURL.pathname, 'remove') === navi.modifyTrailingSlash(routeURL.pathname, 'remove'); navigation.navigate(linkURL, state ? { state: state } : undefined); if ((isSamePathname || linkURL.pathname === '') && linkURL.hash === routeURL.hash && linkURL.hash) { scrollToHash(routeURL.hash, hashScrollBehavior); } } } }, [disabled, onClick, linkURL && linkURL.href, routeURL && routeURL.href]); return { onClick: handleClick, onMouseEnter: handleMouseEnter, href: linkURL ? linkURL.href : href, }; }; // Need to include this type definition, as the automatically generated one // is incompatible with some versions of the react typings. var Link = React.forwardRef(function (props, anchorRef) { var active = props.active, _a = props.activeClassName, activeClassName = _a === void 0 ? '' : _a, _b = props.activeStyle, activeStyle = _b === void 0 ? {} : _b, _c = props.className, className = _c === void 0 ? '' : _c, disabled = props.disabled, exact = props.exact, hashScrollBehavior = props.hashScrollBehavior, hrefProp = props.href, onClickProp = props.onClick, onMouseEnterProp = props.onMouseEnter, prefetch = props.prefetch, state = props.state, _d = props.style, style = _d === void 0 ? {} : _d, rest = __rest(props, ["active", "activeClassName", "activeStyle", "className", "disabled", "exact", "hashScrollBehavior", "href", "onClick", "onMouseEnter", "prefetch", "state", "style"]); var _e = useLinkProps({ hashScrollBehavior: hashScrollBehavior, href: hrefProp, onClick: onClickProp, onMouseEnter: onMouseEnterProp, prefetch: prefetch, state: state, }), onClick = _e.onClick, onMouseEnter = _e.onMouseEnter, linkProps = __rest(_e, ["onClick", "onMouseEnter"]); var actualActive = useActive(linkProps.href, { exact: !!exact }); if (active === undefined) { active = actualActive; } return (React.createElement("a", __assign({ ref: anchorRef, className: className + " " + (active ? activeClassName : ''), style: __assign(__assign({}, style), (active ? activeStyle : {})) }, rest, linkProps, { // Don't handle events on links with a `target` prop. onClick: props.target ? onClickProp : onClick, onMouseEnter: props.target ? onMouseEnterProp : onMouseEnter }))); }); var __extends = (undefined && undefined.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var NaviProvider = /** @class */ (function (_super) { __extends(NaviProvider, _super); function NaviProvider(props) { var _this = _super.call(this, props) || this; _this.handleNavigationSnapshot = function (route) { if (route.type !== 'busy') { _this.setState({ steadyRoute: route, busyRoute: undefined, }); } else { _this.setState({ busyRoute: route, }); } }; _this.handleError = function (error) { throw error; }; _this.state = {}; return _this; } NaviProvider.getDerivedStateFromProps = function (props, state) { if (state.navigation !== props.navigation) { var route = props.navigation.getCurrentValue(); return route.type === 'busy' ? { steadyRoute: state.steadyRoute, busyRoute: route, navigation: props.navigation, } : { steadyRoute: route, busyRoute: undefined, navigation: props.navigation, }; } return null; }; NaviProvider.prototype.render = function () { return (React.createElement(HashScroll, { behavior: this.props.hashScrollBehavior }, React.createElement(NaviContext.Provider, { value: this.state }, this.props.children))); }; NaviProvider.prototype.componentDidMount = function () { this.subscribe(); }; NaviProvider.prototype.componentDidUpdate = function (prevProps) { if (prevProps.navigation !== this.props.navigation) { this.unsubscribe(); this.subscribe(); } }; NaviProvider.prototype.componentWillUnmount = function () { this.unsubscribe(); }; NaviProvider.prototype.subscribe = function () { if (!this.props.navigation) { throw new Error("A <NaviProvider> component must receive a \"navigation\" prop."); } this.subscription = this.props.navigation.subscribe(this.handleNavigationSnapshot, this.handleError); }; NaviProvider.prototype.unsubscribe = function () { if (this.subscription) { this.subscription.unsubscribe(); delete this.subscription; } }; return NaviProvider; }(React.Component)); var __extends$1 = (undefined && undefined.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign$1 = (undefined && undefined.__assign) || function () { __assign$1 = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign$1.apply(this, arguments); }; var NotFoundBoundary = function ErrorBoundary(props) { return (React.createElement(NaviContext.Consumer, null, function (context) { return React.createElement(InnerNotFoundBoundary, __assign$1({ context: context }, props)); })); }; var InnerNotFoundBoundary = /** @class */ (function (_super) { __extends$1(InnerNotFoundBoundary, _super); function InnerNotFoundBoundary(props) { var _this = _super.call(this, props) || this; _this.state = {}; return _this; } InnerNotFoundBoundary.getDerivedStateFromProps = function (props, state) { if (state.error && props.context.steadyRoute.url.pathname !== state.errorPathname) { return { error: undefined, errorPathname: undefined, errorInfo: undefined, }; } return null; }; InnerNotFoundBoundary.prototype.componentDidCatch = function (error, errorInfo) { if (error instanceof navi.NotFoundError) { this.setState({ error: error, errorInfo: errorInfo, errorPathname: this.props.context.steadyRoute.url.pathname, }); } else { throw error; } }; InnerNotFoundBoundary.prototype.componentDidUpdate = function (prevProps, prevState) { if (this.state.error && !prevState.error) ; }; InnerNotFoundBoundary.prototype.render = function () { if (this.state.error) { return this.props.render(this.state.error); } return this.props.children; }; return InnerNotFoundBoundary; }(React.Component)); var ViewHeadRendererContext = React.createContext(null); var __assign$2 = (undefined && undefined.__assign) || function () { __assign$2 = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign$2.apply(this, arguments); }; function defaultUseViewChunkPredicate(chunk) { return chunk.type === 'view'; } function useViewElement(options) { if (options === void 0) { options = {}; } var result = useView(options); return result && result.element; } function useView(_a) { var _b = _a === void 0 ? {} : _a, _c = _b.disableScrolling, disableScrolling = _c === void 0 ? false : _c, hashScrollBehavior = _b.hashScrollBehavior, renderHead = _b.renderHead, _d = _b.where, where = _d === void 0 ? defaultUseViewChunkPredicate : _d; var hashScrollBehaviorFromContext = React.useContext(HashScrollContext); var renderHeadFromContext = React.useContext(ViewHeadRendererContext); var context = React.useContext(NaviContext); if (hashScrollBehavior === undefined) { hashScrollBehavior = hashScrollBehaviorFromContext; } if (renderHead === undefined && renderHeadFromContext) { renderHead = renderHeadFromContext; } var route = context.steadyRoute || context.busyRoute; if (!route) { throw new Error('react-navi: A <View> component cannot be rendered outside of a <Router> or <NaviProvider> component.'); } var unconsumedChunks = context.unconsumedSteadyRouteChunks || route.chunks; var index = unconsumedChunks.findIndex(where); var view = index !== -1 && unconsumedChunks[index].view; // Find any other chunks that come before this chunk, or after this one if // this is the final view chunk. // // Don't treat this as the final chunk is there is an error, as that means // we don't know whether this is really meant to be the final chunk, and we // don't want to throw an error before rendering whatever views we can. var final = index === -1 || (!unconsumedChunks.slice(index + 1).find(where) && route.type !== 'error'); var chunks = React.useMemo(function () { return (final ? unconsumedChunks : unconsumedChunks.slice(0, index + 1)); }, [final, unconsumedChunks, index]); // Look for an error amongst any route chunks that haven't already been used // by a `useView()` and throw it. var errorChunk = chunks.find(function (chunk) { return chunk.type === 'error'; }); if (errorChunk) { throw errorChunk.error || new Error('Unknown routing error'); } // If there's no steady route, then we'll need to wait until a steady // route becomes available using Supsense. if (!view && !context.steadyRoute) { throw context.navigation.getRoute(); } var childContext = React.useMemo(function () { return (__assign$2(__assign$2({}, context), { unconsumedSteadyRouteChunks: final ? [] : unconsumedChunks.slice(index + 1) })); }, [context, unconsumedChunks, index]); var connect = React.useCallback(function (children) { return (React.createElement(NaviContext.Provider, { value: childContext }, // Clone the content to force a re-render even if content hasn't // changed, as Provider is a PureComponent. React.isValidElement(children) ? React.cloneElement(children) : children)); }, [childContext]); var content = React.useMemo(function () { return typeof view === 'function' ? React.createElement(view, { route: context.steadyRoute, }) : view || null; }, [view, context.steadyRoute]); var head = React.useMemo(function () { return (!renderHead ? null : renderHead(chunks)); }, [ renderHead, chunks, ]); // Scroll to hash or top of page if appropriate. var lastRouteRef = React.useRef(); React.useEffect(function () { var nextRoute = route; var prevRoute = lastRouteRef.current; lastRouteRef.current = route; if (final && route && unconsumedChunks.length !== 0) { if (nextRoute && nextRoute.type !== 'busy') { if (prevRoute && nextRoute.url.pathname === prevRoute.url.pathname && nextRoute.url.search === prevRoute.url.search && nextRoute.url.hash === prevRoute.url.hash) { return; } if (!disableScrolling && (!prevRoute || !prevRoute.url || prevRoute.url.hash !== nextRoute.url.hash || prevRoute.url.pathname !== nextRoute.url.pathname)) { scrollToHash(nextRoute.url.hash, prevRoute && prevRoute.url && prevRoute.url.pathname === nextRoute.url.pathname ? hashScrollBehavior : 'auto'); } } } }, [route]); var result = React.useMemo(function () { return ({ chunks: chunks, connect: connect, content: content, element: connect(React.createElement(React.Fragment, null, head, content)), final: final, head: head, }); }, [chunks, connect, content, final, head]); return unconsumedChunks.length === 0 ? null : result; } var View = function View(_a) { var disableScrolling = _a.disableScrolling, hashScrollBehavior = _a.hashScrollBehavior, renderHead = _a.renderHead, where = _a.where; var result = useView({ disableScrolling: disableScrolling, hashScrollBehavior: hashScrollBehavior, renderHead: renderHead, where: where, }); if (!result) { throw new Error('A Navi <View> was not able to find a view to render.'); } return result.element; }; var __extends$2 = (undefined && undefined.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var Router = /** @class */ (function (_super) { __extends$2(Router, _super); function Router(props) { var _this = _super.call(this, props) || this; if (props.navigation) { if (props.basename) { console.warn("Warning: <Router> can't receive both a \"basename\" and a \"navigation\" prop. Proceeding by ignoring \"basename\"."); } if (props.routes) { console.warn("Warning: <Router> can't receive both a \"routes\" and a \"navigation\" prop. Proceeding by ignoring \"routes\"."); } if (props.history) { console.warn("Warning: <Router> can't receive both a \"history\" and a \"navigation\" prop. Proceeding by ignoring \"history\"."); } } _this.navigation = props.navigation || navi.createBrowserNavigation({ basename: props.basename, context: props.context, history: props.history, routes: props.routes, }); return _this; } Router.prototype.render = function () { var _a = this.props, children = _a.children, hashScrollBehavior = _a.hashScrollBehavior; return (React.createElement(NaviProvider, { navigation: this.navigation, hashScrollBehavior: hashScrollBehavior }, children || React.createElement(View, null))); }; Router.prototype.componentDidMount = function () { if (this.props.navigation && this.props.context) { this.props.navigation.setContext(this.props.context); } }; Router.prototype.componentDidUpdate = function (prevProps) { if (shallowDiffers(prevProps.context || {}, this.props.context || {})) { this.navigation.setContext(this.props.context || {}); } }; Router.prototype.componentWillUnmount = function () { // Clean up any navigation object that we've created. if (!this.props.navigation) { this.navigation.dispose(); } delete this.navigation; }; Router.defaultProps = { fallback: undefined, }; return Router; }(React.Component)); // Pulled from react-compat // https://github.com/developit/preact-compat/blob/7c5de00e7c85e2ffd011bf3af02899b63f699d3a/src/index.js#L349 function shallowDiffers(a, b) { for (var i in a) if (!(i in b)) return true; for (var i in b) if (a[i] !== b[i]) return true; return false; } function useNavigation() { return React.useContext(NaviContext).navigation; } function useHistory() { { console.warn("Deprecation Warning: \"useHistory()\" is deprecated. It will be removed in a future version."); } return React.useContext(NaviContext).navigation._history; } function History(props) { React.useEffect(function () { { console.warn("Deprecation Warning: \"<History>\" is deprecated. It will be removed in a future version."); } }, []); return (React.createElement(NaviContext.Consumer, null, function (context) { return props.children(context.navigation._history); })); } function useLoadingRoute() { return React.useContext(NaviContext).busyRoute; } function useCurrentRoute() { var _a = React.useContext(NaviContext), steadyRoute = _a.steadyRoute, busyRoute = _a.busyRoute; return (steadyRoute || busyRoute); } exports.HashScroll = HashScroll; exports.useLinkProps = useLinkProps; exports.useActive = useActive; exports.Link = Link; exports.NaviProvider = NaviProvider; exports.NotFoundBoundary = NotFoundBoundary; exports.Router = Router; exports.useNavigation = useNavigation; exports.useHistory = useHistory; exports.History = History; exports.useLoadingRoute = useLoadingRoute; exports.useCurrentRoute = useCurrentRoute; exports.View = View; exports.useView = useView; exports.useViewElement = useViewElement; exports.ViewHeadRendererContext = ViewHeadRendererContext; Object.defineProperty(exports, '__esModule', { value: true }); }));