@ionic/react-router
Version:
React Router wrapper for @ionic/react
946 lines (935 loc) • 44 kB
JavaScript
import { __rest } from 'tslib';
import { createBrowserHistory, createHashHistory } from 'history';
import React from 'react';
import { withRouter, Router } from 'react-router-dom';
import { ViewStacks, generateId, IonRoute, ViewLifeCycleManager, StackContext, RouteManagerContext, getConfig, LocationHistory, NavManager } from '@ionic/react';
import { Route, matchPath as matchPath$1, Router as Router$1 } from 'react-router';
class IonRouteInner extends React.PureComponent {
render() {
return (React.createElement(Route, Object.assign({ path: this.props.path, exact: this.props.exact, render: this.props.render }, (this.props.computedMatch !== undefined
? {
computedMatch: this.props.computedMatch,
}
: {}))));
}
}
/**
* @see https://v5.reactrouter.com/web/api/matchPath
*/
const matchPath = ({ pathname, componentProps, }) => {
const { exact, component } = componentProps;
const path = componentProps.path || componentProps.from;
/***
* The props to match against, they are identical
* to the matching props `Route` accepts. It could also be a string
* or an array of strings as shortcut for `{ path }`.
*/
const matchProps = {
exact,
path,
component,
};
const match = matchPath$1(pathname, matchProps);
if (!match) {
return false;
}
return match;
};
class ReactRouterViewStack extends ViewStacks {
constructor() {
super();
this.createViewItem = this.createViewItem.bind(this);
this.findViewItemByRouteInfo = this.findViewItemByRouteInfo.bind(this);
this.findLeavingViewItemByRouteInfo = this.findLeavingViewItemByRouteInfo.bind(this);
this.getChildrenToRender = this.getChildrenToRender.bind(this);
this.findViewItemByPathname = this.findViewItemByPathname.bind(this);
}
createViewItem(outletId, reactElement, routeInfo, page) {
const viewItem = {
id: generateId('viewItem'),
outletId,
ionPageElement: page,
reactElement,
mount: true,
ionRoute: false,
};
if (reactElement.type === IonRoute) {
viewItem.ionRoute = true;
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
}
viewItem.routeData = {
match: matchPath({
pathname: routeInfo.pathname,
componentProps: reactElement.props,
}),
childProps: reactElement.props,
};
return viewItem;
}
getChildrenToRender(outletId, ionRouterOutlet, routeInfo) {
const viewItems = this.getViewItemsForOutlet(outletId);
// Sync latest routes with viewItems
React.Children.forEach(ionRouterOutlet.props.children, (child) => {
const viewItem = viewItems.find((v) => {
return matchComponent$1(child, v.routeData.childProps.path || v.routeData.childProps.from);
});
if (viewItem) {
viewItem.reactElement = child;
}
});
const children = viewItems.map((viewItem) => {
let clonedChild;
if (viewItem.ionRoute && !viewItem.disableIonPageManagement) {
clonedChild = (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) }, React.cloneElement(viewItem.reactElement, {
computedMatch: viewItem.routeData.match,
})));
}
else {
const match = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
clonedChild = (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) }, React.cloneElement(viewItem.reactElement, {
computedMatch: viewItem.routeData.match,
})));
if (!match && viewItem.routeData.match) {
viewItem.routeData.match = undefined;
viewItem.mount = false;
}
}
return clonedChild;
});
return children;
}
findViewItemByRouteInfo(routeInfo, outletId, updateMatch) {
const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
const shouldUpdateMatch = updateMatch === undefined || updateMatch === true;
if (shouldUpdateMatch && viewItem && match) {
viewItem.routeData.match = match;
}
return viewItem;
}
findLeavingViewItemByRouteInfo(routeInfo, outletId, mustBeIonRoute = true) {
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
return viewItem;
}
findViewItemByPathname(pathname, outletId) {
const { viewItem } = this.findViewItemByPath(pathname, outletId);
return viewItem;
}
/**
* Returns the matching view item and the match result for a given pathname.
*/
findViewItemByPath(pathname, outletId, mustBeIonRoute) {
let viewItem;
let match;
let viewStack;
if (outletId) {
viewStack = this.getViewItemsForOutlet(outletId);
viewStack.some(matchView);
if (!viewItem) {
viewStack.some(matchDefaultRoute);
}
}
else {
const viewItems = this.getAllViewItems();
viewItems.some(matchView);
if (!viewItem) {
viewItems.some(matchDefaultRoute);
}
}
return { viewItem, match };
function matchView(v) {
var _a, _b;
if (mustBeIonRoute && !v.ionRoute) {
return false;
}
match = matchPath({
pathname,
componentProps: v.routeData.childProps,
});
if (match) {
/**
* Even though we have a match from react-router, we do not know if the match
* is for this specific view item.
*
* To validate this, we need to check if the path and url match the view item's route data.
*/
const hasParameter = match.path.includes(':');
if (!hasParameter || (hasParameter && match.url === ((_b = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match) === null || _b === void 0 ? void 0 : _b.url))) {
viewItem = v;
return true;
}
}
return false;
}
function matchDefaultRoute(v) {
// try to find a route that doesn't have a path or from prop, that will be our default route
if (!v.routeData.childProps.path && !v.routeData.childProps.from) {
match = {
path: pathname,
url: pathname,
isExact: true,
params: {},
};
viewItem = v;
return true;
}
return false;
}
}
}
function matchComponent$1(node, pathname) {
return matchPath({
pathname,
componentProps: node.props,
});
}
function clonePageElement(leavingViewHtml) {
let html;
if (typeof leavingViewHtml === 'string') {
html = leavingViewHtml;
}
else {
html = leavingViewHtml.outerHTML;
}
if (document) {
const newEl = document.createElement('div');
newEl.innerHTML = html;
newEl.style.zIndex = '';
// Remove an existing back button so the new element doesn't get two of them
const ionBackButton = newEl.getElementsByTagName('ion-back-button');
if (ionBackButton[0]) {
ionBackButton[0].remove();
}
return newEl.firstChild;
}
return undefined;
}
const isViewVisible = (el) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden');
class StackManager extends React.PureComponent {
constructor(props) {
super(props);
this.stackContextValue = {
registerIonPage: this.registerIonPage.bind(this),
isInOutlet: () => true,
};
this.pendingPageTransition = false;
this.registerIonPage = this.registerIonPage.bind(this);
this.transitionPage = this.transitionPage.bind(this);
this.handlePageTransition = this.handlePageTransition.bind(this);
this.id = generateId('routerOutlet');
this.prevProps = undefined;
this.skipTransition = false;
}
componentDidMount() {
if (this.clearOutletTimeout) {
/**
* The clearOutlet integration with React Router is a bit hacky.
* It uses a timeout to clear the outlet after a transition.
* In React v18, components are mounted and unmounted in development mode
* to check for side effects.
*
* This clearTimeout prevents the outlet from being cleared when the component is re-mounted,
* which should only happen in development mode and as a result of a hot reload.
*/
clearTimeout(this.clearOutletTimeout);
}
if (this.routerOutletElement) {
this.setupRouterOutlet(this.routerOutletElement);
this.handlePageTransition(this.props.routeInfo);
}
}
componentDidUpdate(prevProps) {
const { pathname } = this.props.routeInfo;
const { pathname: prevPathname } = prevProps.routeInfo;
if (pathname !== prevPathname) {
this.prevProps = prevProps;
this.handlePageTransition(this.props.routeInfo);
}
else if (this.pendingPageTransition) {
this.handlePageTransition(this.props.routeInfo);
this.pendingPageTransition = false;
}
}
componentWillUnmount() {
this.clearOutletTimeout = this.context.clearOutlet(this.id);
}
async handlePageTransition(routeInfo) {
var _a, _b;
if (!this.routerOutletElement || !this.routerOutletElement.commit) {
/**
* The route outlet has not mounted yet. We need to wait for it to render
* before we can transition the page.
*
* Set a flag to indicate that we should transition the page after
* the component has updated.
*/
this.pendingPageTransition = true;
}
else {
let enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
}
// Check if leavingViewItem should be unmounted
if (leavingViewItem) {
if (routeInfo.routeAction === 'replace') {
leavingViewItem.mount = false;
}
else if (!(routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward')) {
if (routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
leavingViewItem.mount = false;
}
}
else if ((_a = routeInfo.routeOptions) === null || _a === void 0 ? void 0 : _a.unmount) {
leavingViewItem.mount = false;
}
}
const enteringRoute = matchRoute((_b = this.ionRouterOutlet) === null || _b === void 0 ? void 0 : _b.props.children, routeInfo);
if (enteringViewItem) {
enteringViewItem.reactElement = enteringRoute;
}
else if (enteringRoute) {
enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
this.context.addViewItem(enteringViewItem);
}
if (enteringViewItem && enteringViewItem.ionPageElement) {
/**
* If the entering view item is the same as the leaving view item,
* then we don't need to transition.
*/
if (enteringViewItem === leavingViewItem) {
/**
* If the entering view item is the same as the leaving view item,
* we are either transitioning using parameterized routes to the same view
* or a parent router outlet is re-rendering as a result of React props changing.
*
* If the route data does not match the current path, the parent router outlet
* is attempting to transition and we cancel the operation.
*/
if (enteringViewItem.routeData.match.url !== routeInfo.pathname) {
return;
}
}
/**
* If there isn't a leaving view item, but the route info indicates
* that the user has routed from a previous path, then we need
* to find the leaving view item to transition between.
*/
if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
}
/**
* If the entering view is already visible and the leaving view is not, the transition does not need to occur.
*/
if (isViewVisible(enteringViewItem.ionPageElement) &&
leavingViewItem !== undefined &&
!isViewVisible(leavingViewItem.ionPageElement)) {
return;
}
/**
* The view should only be transitioned in the following cases:
* 1. Performing a replace or pop action, such as a swipe to go back gesture
* to animation the leaving view off the screen.
*
* 2. Navigating between top-level router outlets, such as /page-1 to /page-2;
* or navigating within a nested outlet, such as /tabs/tab-1 to /tabs/tab-2.
*
* 3. The entering view is an ion-router-outlet containing a page
* matching the current route and that hasn't already transitioned in.
*
* This should only happen when navigating directly to a nested router outlet
* route or on an initial page load (i.e. refreshing). In cases when loading
* /tabs/tab-1, we need to transition the /tabs page element into the view.
*/
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
}
else if (leavingViewItem && !enteringRoute && !enteringViewItem) {
// If we have a leavingView but no entering view/route, we are probably leaving to
// another outlet, so hide this leavingView. We do it in a timeout to give time for a
// transition to finish.
// setTimeout(() => {
if (leavingViewItem.ionPageElement) {
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
}
// }, 250);
}
this.forceUpdate();
}
}
registerIonPage(page, routeInfo) {
const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
if (foundView) {
const oldPageElement = foundView.ionPageElement;
foundView.ionPageElement = page;
foundView.ionRoute = true;
/**
* React 18 will unmount and remount IonPage
* elements in development mode when using createRoot.
* This can cause duplicate page transitions to occur.
*/
if (oldPageElement === page) {
return;
}
}
this.handlePageTransition(routeInfo);
}
async setupRouterOutlet(routerOutlet) {
const canStart = () => {
const config = getConfig();
const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
if (!swipeEnabled) {
return false;
}
const { routeInfo } = this.props;
const propsToUse = this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
? this.prevProps.routeInfo
: { pathname: routeInfo.pushedByRoute || '' };
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
return (!!enteringViewItem &&
/**
* The root url '/' is treated as
* the first view item (but is never mounted),
* so we do not want to swipe back to the
* root url.
*/
enteringViewItem.mount &&
/**
* When on the first page (whatever view
* you land on after the root url) it
* is possible for findViewItemByRouteInfo to
* return the exact same view you are currently on.
* Make sure that we are not swiping back to the same
* instances of a view.
*/
enteringViewItem.routeData.match.path !== routeInfo.pathname);
};
const onStart = async () => {
const { routeInfo } = this.props;
const propsToUse = this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
? this.prevProps.routeInfo
: { pathname: routeInfo.pushedByRoute || '' };
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
/**
* When the gesture starts, kick off
* a transition that is controlled
* via a swipe gesture.
*/
if (enteringViewItem && leavingViewItem) {
await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
}
return Promise.resolve();
};
const onEnd = (shouldContinue) => {
if (shouldContinue) {
this.skipTransition = true;
this.context.goBack();
}
else {
/**
* In the event that the swipe
* gesture was aborted, we should
* re-hide the page that was going to enter.
*/
const { routeInfo } = this.props;
const propsToUse = this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
? this.prevProps.routeInfo
: { pathname: routeInfo.pushedByRoute || '' };
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
/**
* Ionic React has a design defect where it
* a) Unmounts the leaving view item when using parameterized routes
* b) Considers the current view to be the entering view when using
* parameterized routes
*
* As a result, we should not hide the view item here
* as it will cause the current view to be hidden.
*/
if (enteringViewItem !== leavingViewItem && (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) !== undefined) {
const { ionPageElement } = enteringViewItem;
ionPageElement.setAttribute('aria-hidden', 'true');
ionPageElement.classList.add('ion-page-hidden');
}
}
};
routerOutlet.swipeHandler = {
canStart,
onStart,
onEnd,
};
}
async transitionPage(routeInfo, enteringViewItem, leavingViewItem, direction, progressAnimation = false) {
const runCommit = async (enteringEl, leavingEl) => {
const skipTransition = this.skipTransition;
/**
* If the transition was handled
* via the swipe to go back gesture,
* then we do not want to perform
* another transition.
*
* We skip adding ion-page or ion-page-invisible
* because the entering view already exists in the DOM.
* If we added the classes, there would be a flicker where
* the view would be briefly hidden.
*/
if (skipTransition) {
/**
* We need to reset skipTransition before
* we call routerOutlet.commit otherwise
* the transition triggered by the swipe
* to go back gesture would reset it. In
* that case you would see a duplicate
* transition triggered by handlePageTransition
* in componentDidUpdate.
*/
this.skipTransition = false;
}
else {
enteringEl.classList.add('ion-page');
enteringEl.classList.add('ion-page-invisible');
}
await routerOutlet.commit(enteringEl, leavingEl, {
duration: skipTransition || directionToUse === undefined ? 0 : undefined,
direction: directionToUse,
showGoBack: !!routeInfo.pushedByRoute,
progressAnimation,
animationBuilder: routeInfo.routeAnimation,
});
};
const routerOutlet = this.routerOutletElement;
const routeInfoFallbackDirection = routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root' ? undefined : routeInfo.routeDirection;
const directionToUse = direction !== null && direction !== void 0 ? direction : routeInfoFallbackDirection;
if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
// If a page is transitioning to another version of itself
// we clone it so we can have an animation to show
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true);
if (match) {
const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
if (newLeavingElement) {
this.routerOutletElement.appendChild(newLeavingElement);
await runCommit(enteringViewItem.ionPageElement, newLeavingElement);
this.routerOutletElement.removeChild(newLeavingElement);
}
}
else {
await runCommit(enteringViewItem.ionPageElement, undefined);
}
}
else {
await runCommit(enteringViewItem.ionPageElement, leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) {
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
}
}
}
}
render() {
const { children } = this.props;
const ionRouterOutlet = React.Children.only(children);
this.ionRouterOutlet = ionRouterOutlet;
const components = this.context.getChildrenToRender(this.id, this.ionRouterOutlet, this.props.routeInfo, () => {
this.forceUpdate();
});
return (React.createElement(StackContext.Provider, { value: this.stackContextValue }, React.cloneElement(ionRouterOutlet, {
ref: (node) => {
if (ionRouterOutlet.props.setRef) {
ionRouterOutlet.props.setRef(node);
}
if (ionRouterOutlet.props.forwardedRef) {
ionRouterOutlet.props.forwardedRef.current = node;
}
this.routerOutletElement = node;
const { ref } = ionRouterOutlet;
if (typeof ref === 'function') {
ref(node);
}
},
}, components)));
}
static get contextType() {
return RouteManagerContext;
}
}
function matchRoute(node, routeInfo) {
let matchedNode;
React.Children.forEach(node, (child) => {
const match = matchPath({
pathname: routeInfo.pathname,
componentProps: child.props,
});
if (match) {
matchedNode = child;
}
});
if (matchedNode) {
return matchedNode;
}
// If we haven't found a node
// try to find one that doesn't have a path or from prop, that will be our not found route
React.Children.forEach(node, (child) => {
if (!(child.props.path || child.props.from)) {
matchedNode = child;
}
});
return matchedNode;
}
function matchComponent(node, pathname, forceExact) {
return matchPath({
pathname,
componentProps: Object.assign(Object.assign({}, node.props), { exact: forceExact }),
});
}
class IonRouterInner extends React.PureComponent {
constructor(props) {
super(props);
this.exitViewFromOtherOutletHandlers = [];
this.locationHistory = new LocationHistory();
this.viewStack = new ReactRouterViewStack();
this.routeMangerContextState = {
canGoBack: () => this.locationHistory.canGoBack(),
clearOutlet: this.viewStack.clear,
findViewItemByPathname: this.viewStack.findViewItemByPathname,
getChildrenToRender: this.viewStack.getChildrenToRender,
goBack: () => this.handleNavigateBack(),
createViewItem: this.viewStack.createViewItem,
findViewItemByRouteInfo: this.viewStack.findViewItemByRouteInfo,
findLeavingViewItemByRouteInfo: this.viewStack.findLeavingViewItemByRouteInfo,
addViewItem: this.viewStack.add,
unMountViewItem: this.viewStack.remove,
};
const routeInfo = {
id: generateId('routeInfo'),
pathname: this.props.location.pathname,
search: this.props.location.search,
};
this.locationHistory.add(routeInfo);
this.handleChangeTab = this.handleChangeTab.bind(this);
this.handleResetTab = this.handleResetTab.bind(this);
this.handleNativeBack = this.handleNativeBack.bind(this);
this.handleNavigate = this.handleNavigate.bind(this);
this.handleNavigateBack = this.handleNavigateBack.bind(this);
this.props.registerHistoryListener(this.handleHistoryChange.bind(this));
this.handleSetCurrentTab = this.handleSetCurrentTab.bind(this);
this.state = {
routeInfo,
};
}
handleChangeTab(tab, path, routeOptions) {
if (!path) {
return;
}
const routeInfo = this.locationHistory.getCurrentRouteInfoForTab(tab);
const [pathname, search] = path.split('?');
if (routeInfo) {
this.incomingRouteParams = Object.assign(Object.assign({}, routeInfo), { routeAction: 'push', routeDirection: 'none' });
if (routeInfo.pathname === pathname) {
this.incomingRouteParams.routeOptions = routeOptions;
this.props.history.push(routeInfo.pathname + (routeInfo.search || ''));
}
else {
this.incomingRouteParams.pathname = pathname;
this.incomingRouteParams.search = search ? '?' + search : undefined;
this.incomingRouteParams.routeOptions = routeOptions;
this.props.history.push(pathname + (search ? '?' + search : ''));
}
}
else {
this.handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
}
}
handleHistoryChange(location, action) {
var _a, _b, _c;
let leavingLocationInfo;
if (this.incomingRouteParams) {
if (this.incomingRouteParams.routeAction === 'replace') {
leavingLocationInfo = this.locationHistory.previous();
}
else {
leavingLocationInfo = this.locationHistory.current();
}
}
else {
leavingLocationInfo = this.locationHistory.current();
}
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
if (leavingUrl !== location.pathname) {
if (!this.incomingRouteParams) {
if (action === 'REPLACE') {
this.incomingRouteParams = {
routeAction: 'replace',
routeDirection: 'none',
tab: this.currentTab,
};
}
if (action === 'POP') {
const currentRoute = this.locationHistory.current();
if (currentRoute && currentRoute.pushedByRoute) {
const prevInfo = this.locationHistory.findLastLocation(currentRoute);
this.incomingRouteParams = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
}
else {
this.incomingRouteParams = {
routeAction: 'pop',
routeDirection: 'none',
tab: this.currentTab,
};
}
}
if (!this.incomingRouteParams) {
this.incomingRouteParams = {
routeAction: 'push',
routeDirection: ((_a = location.state) === null || _a === void 0 ? void 0 : _a.direction) || 'forward',
routeOptions: (_b = location.state) === null || _b === void 0 ? void 0 : _b.routerOptions,
tab: this.currentTab,
};
}
}
let routeInfo;
if ((_c = this.incomingRouteParams) === null || _c === void 0 ? void 0 : _c.id) {
routeInfo = Object.assign(Object.assign({}, this.incomingRouteParams), { lastPathname: leavingLocationInfo.pathname });
this.locationHistory.add(routeInfo);
}
else {
const isPushed = this.incomingRouteParams.routeAction === 'push' && this.incomingRouteParams.routeDirection === 'forward';
routeInfo = Object.assign(Object.assign({ id: generateId('routeInfo') }, this.incomingRouteParams), { lastPathname: leavingLocationInfo.pathname, pathname: location.pathname, search: location.search, params: this.props.match.params, prevRouteLastPathname: leavingLocationInfo.lastPathname });
if (isPushed) {
routeInfo.tab = leavingLocationInfo.tab;
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
}
else if (routeInfo.routeAction === 'pop') {
const r = this.locationHistory.findLastLocation(routeInfo);
routeInfo.pushedByRoute = r === null || r === void 0 ? void 0 : r.pushedByRoute;
}
else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
// If we are switching tabs grab the last route info for the tab and use its pushedByRoute
const lastRoute = this.locationHistory.getCurrentRouteInfoForTab(routeInfo.tab);
routeInfo.pushedByRoute = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute;
}
else if (routeInfo.routeAction === 'replace') {
// Make sure to set the lastPathname, etc.. to the current route so the page transitions out
const currentRouteInfo = this.locationHistory.current();
/**
* If going from /home to /child, then replacing from
* /child to /home, we don't want the route info to
* say that /home was pushed by /home which is not correct.
*/
const currentPushedBy = currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.pushedByRoute;
const pushedByRoute = currentPushedBy !== undefined && currentPushedBy !== routeInfo.pathname
? currentPushedBy
: routeInfo.pushedByRoute;
routeInfo.lastPathname = (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.pathname) || routeInfo.lastPathname;
routeInfo.prevRouteLastPathname = currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.lastPathname;
routeInfo.pushedByRoute = pushedByRoute;
/**
* When replacing routes we should still prefer
* any custom direction/animation that the developer
* has specified when navigating first instead of relying
* on previously used directions/animations.
*/
routeInfo.routeDirection = routeInfo.routeDirection || (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.routeDirection);
routeInfo.routeAnimation = routeInfo.routeAnimation || (currentRouteInfo === null || currentRouteInfo === void 0 ? void 0 : currentRouteInfo.routeAnimation);
}
this.locationHistory.add(routeInfo);
}
this.setState({
routeInfo,
});
}
this.incomingRouteParams = undefined;
}
/**
* history@4.x uses goBack(), history@5.x uses back()
* TODO: If support for React Router <=5 is dropped
* this logic is no longer needed. We can just
* assume back() is available.
*/
handleNativeBack() {
const history = this.props.history;
const goBack = history.goBack || history.back;
goBack();
}
handleNavigate(path, routeAction, routeDirection, routeAnimation, routeOptions, tab) {
this.incomingRouteParams = Object.assign(this.incomingRouteParams || {}, {
routeAction,
routeDirection,
routeOptions,
routeAnimation,
tab,
});
if (routeAction === 'push') {
this.props.history.push(path);
}
else {
this.props.history.replace(path);
}
}
handleNavigateBack(defaultHref = '/', routeAnimation) {
const config = getConfig();
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref');
const routeInfo = this.locationHistory.current();
if (routeInfo && routeInfo.pushedByRoute) {
const prevInfo = this.locationHistory.findLastLocation(routeInfo);
if (prevInfo) {
/**
* This needs to be passed to handleNavigate
* otherwise incomingRouteParams.routeAnimation
* will be overridden.
*/
const incomingAnimation = routeAnimation || routeInfo.routeAnimation;
this.incomingRouteParams = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back', routeAnimation: incomingAnimation });
if (routeInfo.lastPathname === routeInfo.pushedByRoute ||
/**
* We need to exclude tab switches/tab
* context changes here because tabbed
* navigation is not linear, but router.back()
* will go back in a linear fashion.
*/
(prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '')) {
/**
* history@4.x uses goBack(), history@5.x uses back()
* TODO: If support for React Router <=5 is dropped
* this logic is no longer needed. We can just
* assume back() is available.
*/
const history = this.props.history;
const goBack = history.goBack || history.back;
goBack();
}
else {
this.handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
}
}
else {
this.handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
}
}
else {
this.handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
}
}
handleResetTab(tab, originalHref, originalRouteOptions) {
const routeInfo = this.locationHistory.getFirstRouteInfoForTab(tab);
if (routeInfo) {
const newRouteInfo = Object.assign({}, routeInfo);
newRouteInfo.pathname = originalHref;
newRouteInfo.routeOptions = originalRouteOptions;
this.incomingRouteParams = Object.assign(Object.assign({}, newRouteInfo), { routeAction: 'pop', routeDirection: 'back' });
this.props.history.push(newRouteInfo.pathname + (newRouteInfo.search || ''));
}
}
handleSetCurrentTab(tab) {
this.currentTab = tab;
const ri = Object.assign({}, this.locationHistory.current());
if (ri.tab !== tab) {
ri.tab = tab;
this.locationHistory.update(ri);
}
}
render() {
return (React.createElement(RouteManagerContext.Provider, { value: this.routeMangerContextState },
React.createElement(NavManager, { ionRoute: IonRouteInner, ionRedirect: {}, stackManager: StackManager, routeInfo: this.state.routeInfo, onNativeBack: this.handleNativeBack, onNavigateBack: this.handleNavigateBack, onNavigate: this.handleNavigate, onSetCurrentTab: this.handleSetCurrentTab, onChangeTab: this.handleChangeTab, onResetTab: this.handleResetTab, locationHistory: this.locationHistory }, this.props.children)));
}
}
const IonRouter = withRouter(IonRouterInner);
IonRouter.displayName = 'IonRouter';
class IonReactRouter extends React.Component {
constructor(props) {
super(props);
const { history } = props, rest = __rest(props, ["history"]);
this.history = history || createBrowserHistory(rest);
this.history.listen(this.handleHistoryChange.bind(this));
this.registerHistoryListener = this.registerHistoryListener.bind(this);
}
/**
* history@4.x passes separate location and action
* params. history@5.x passes location and action
* together as a single object.
* TODO: If support for React Router <=5 is dropped
* this logic is no longer needed. We can just assume
* a single object with both location and action.
*/
handleHistoryChange(location, action) {
const locationValue = location.location || location;
const actionValue = location.action || action;
if (this.historyListenHandler) {
this.historyListenHandler(locationValue, actionValue);
}
}
registerHistoryListener(cb) {
this.historyListenHandler = cb;
}
render() {
const _a = this.props, { children } = _a, props = __rest(_a, ["children"]);
return (React.createElement(Router, Object.assign({ history: this.history }, props),
React.createElement(IonRouter, { registerHistoryListener: this.registerHistoryListener }, children)));
}
}
class IonReactMemoryRouter extends React.Component {
constructor(props) {
super(props);
this.history = props.history;
this.history.listen(this.handleHistoryChange.bind(this));
this.registerHistoryListener = this.registerHistoryListener.bind(this);
}
/**
* history@4.x passes separate location and action
* params. history@5.x passes location and action
* together as a single object.
* TODO: If support for React Router <=5 is dropped
* this logic is no longer needed. We can just assume
* a single object with both location and action.
*/
handleHistoryChange(location, action) {
const locationValue = location.location || location;
const actionValue = location.action || action;
if (this.historyListenHandler) {
this.historyListenHandler(locationValue, actionValue);
}
}
registerHistoryListener(cb) {
this.historyListenHandler = cb;
}
render() {
const _a = this.props, { children } = _a, props = __rest(_a, ["children"]);
return (React.createElement(Router$1, Object.assign({}, props),
React.createElement(IonRouter, { registerHistoryListener: this.registerHistoryListener }, children)));
}
}
class IonReactHashRouter extends React.Component {
constructor(props) {
super(props);
const { history } = props, rest = __rest(props, ["history"]);
this.history = history || createHashHistory(rest);
this.history.listen(this.handleHistoryChange.bind(this));
this.registerHistoryListener = this.registerHistoryListener.bind(this);
}
/**
* history@4.x passes separate location and action
* params. history@5.x passes location and action
* together as a single object.
* TODO: If support for React Router <=5 is dropped
* this logic is no longer needed. We can just assume
* a single object with both location and action.
*/
handleHistoryChange(location, action) {
const locationValue = location.location || location;
const actionValue = location.action || action;
if (this.historyListenHandler) {
this.historyListenHandler(locationValue, actionValue);
}
}
registerHistoryListener(cb) {
this.historyListenHandler = cb;
}
render() {
const _a = this.props, { children } = _a, props = __rest(_a, ["children"]);
return (React.createElement(Router, Object.assign({ history: this.history }, props),
React.createElement(IonRouter, { registerHistoryListener: this.registerHistoryListener }, children)));
}
}
export { IonReactHashRouter, IonReactMemoryRouter, IonReactRouter };
//# sourceMappingURL=index.js.map