react-navi
Version:
A batteries-included router for react.
670 lines (654 loc) • 30.4 kB
JavaScript
(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 });
}));