mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 03:00:58 +08:00
409 lines
14 KiB
TypeScript
409 lines
14 KiB
TypeScript
import {
|
|
parseQuery,
|
|
Router,
|
|
RouteLocationNormalized,
|
|
NavigationFailure,
|
|
RouteLocationRaw
|
|
} from 'vue-router';
|
|
import { createLocationHistory } from './locationHistory';
|
|
import { generateId } from './utils';
|
|
import {
|
|
ExternalNavigationOptions,
|
|
RouteInfo,
|
|
RouteParams,
|
|
RouteAction,
|
|
RouteDirection,
|
|
IonicVueRouterOptions,
|
|
NavigationInformation
|
|
} from './types';
|
|
import { AnimationBuilder } from '@ionic/vue';
|
|
|
|
export const createIonRouter = (opts: IonicVueRouterOptions, router: Router) => {
|
|
let currentNavigationInfo: NavigationInformation = { direction: undefined, action: undefined, delta: undefined };
|
|
|
|
/**
|
|
* Ionic Vue should only react to navigation
|
|
* changes once they have been confirmed and should
|
|
* never affect the outcome of navigation (with the
|
|
* exception of going back or selecting a tab).
|
|
* As a result, we should do our work in afterEach
|
|
* which is fired once navigation is confirmed
|
|
* and any user guards have run.
|
|
*/
|
|
router.afterEach((to: RouteLocationNormalized, _: RouteLocationNormalized, failure?: NavigationFailure) => {
|
|
if (failure) return;
|
|
|
|
const { direction, action, delta } = currentNavigationInfo;
|
|
|
|
/**
|
|
* When calling router.replace, we are not informed
|
|
* about the replace action in opts.history.listen
|
|
* but we can check to see if the latest routing action
|
|
* was a replace action by looking at the history state.
|
|
* We need to use opts.history rather than window.history
|
|
* because window.history will be undefined when using SSR.
|
|
*/
|
|
|
|
currentHistoryPosition = opts.history.state.position as number;
|
|
|
|
const replaceAction = opts.history.state.replaced ? 'replace' : undefined;
|
|
handleHistoryChange(to, action || replaceAction, direction, delta);
|
|
|
|
currentNavigationInfo = { direction: undefined, action: undefined, delta: undefined };
|
|
});
|
|
|
|
const locationHistory = createLocationHistory();
|
|
|
|
/**
|
|
* Keeping track of the history position
|
|
* allows us to determine if a user is pushing
|
|
* new pages or updating history via the forward
|
|
* and back browser buttons.
|
|
*/
|
|
const initialHistoryPosition = opts.history.state.position as number;
|
|
let currentHistoryPosition = opts.history.state.position as number;
|
|
|
|
let currentRouteInfo: RouteInfo;
|
|
let incomingRouteParams: RouteParams;
|
|
let currentTab: string | undefined;
|
|
|
|
// TODO types
|
|
let historyChangeListeners: any[] = [];
|
|
|
|
if (typeof (document as any) !== 'undefined') {
|
|
document.addEventListener('ionBackButton', (ev: Event) => {
|
|
(ev as any).detail.register(0, (processNextHandler: () => void) => {
|
|
opts.history.go(-1);
|
|
processNextHandler();
|
|
});
|
|
})
|
|
}
|
|
|
|
opts.history.listen((_: any, _x: any, info: any) => {
|
|
/**
|
|
* history.listen only fires on certain
|
|
* event such as when the user clicks the
|
|
* browser back button. It also gives us
|
|
* additional information as to the type
|
|
* of navigation (forward, backward, etc).
|
|
*
|
|
* We can use this to better handle the
|
|
* `handleHistoryChange` call in
|
|
* router.beforeEach
|
|
*/
|
|
currentNavigationInfo = {
|
|
delta: info.delta,
|
|
|
|
/**
|
|
* Both the browser forward and backward actions
|
|
* are considered "pop" actions, but when going forward
|
|
* we want to make sure the forward animation is used.
|
|
*/
|
|
action: (info.type === 'pop' && info.delta >= 1) ? 'push' : info.type,
|
|
direction: info.direction === '' ? 'forward' : info.direction
|
|
};
|
|
});
|
|
|
|
const handleNavigateBack = (defaultHref?: string, routerAnimation?: AnimationBuilder) => {
|
|
// todo grab default back button href from config
|
|
const routeInfo = locationHistory.current(initialHistoryPosition, currentHistoryPosition);
|
|
if (routeInfo && routeInfo.pushedByRoute) {
|
|
const prevInfo = locationHistory.findLastLocation(routeInfo);
|
|
if (prevInfo) {
|
|
incomingRouteParams = { ...prevInfo, routerAction: 'pop', routerDirection: 'back', routerAnimation: routerAnimation || routeInfo.routerAnimation };
|
|
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 === ''
|
|
)
|
|
) {
|
|
router.back();
|
|
} else {
|
|
router.replace({ path: prevInfo.pathname, query: parseQuery(prevInfo.search) });
|
|
}
|
|
} else {
|
|
handleNavigate(defaultHref, 'pop', 'back');
|
|
}
|
|
} else {
|
|
handleNavigate(defaultHref, 'pop', 'back');
|
|
}
|
|
}
|
|
|
|
const handleNavigate = (path: RouteLocationRaw, routerAction?: RouteAction, routerDirection?: RouteDirection, routerAnimation?: AnimationBuilder, tab?: string) => {
|
|
setIncomingRouteParams(routerAction, routerDirection, routerAnimation, tab);
|
|
|
|
if (routerAction === 'push') {
|
|
router.push(path);
|
|
} else {
|
|
router.replace(path);
|
|
}
|
|
}
|
|
|
|
// TODO RouteLocationNormalized
|
|
const handleHistoryChange = (
|
|
location: any,
|
|
action?: RouteAction,
|
|
direction?: RouteDirection,
|
|
delta?: number
|
|
) => {
|
|
let leavingLocationInfo: RouteInfo;
|
|
if (incomingRouteParams) {
|
|
if (incomingRouteParams.routerAction === 'replace') {
|
|
leavingLocationInfo = locationHistory.previous();
|
|
} else if (incomingRouteParams.routerAction === 'pop') {
|
|
leavingLocationInfo = locationHistory.current(initialHistoryPosition, currentHistoryPosition + 1);
|
|
} else {
|
|
leavingLocationInfo = locationHistory.current(initialHistoryPosition, currentHistoryPosition - 1);
|
|
}
|
|
} else {
|
|
leavingLocationInfo = currentRouteInfo;
|
|
}
|
|
|
|
if (!leavingLocationInfo) {
|
|
leavingLocationInfo = {
|
|
pathname: '',
|
|
search: ''
|
|
}
|
|
}
|
|
|
|
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
|
|
if (leavingUrl !== location.fullPath) {
|
|
if (!incomingRouteParams) {
|
|
if (action === 'replace') {
|
|
incomingRouteParams = {
|
|
routerAction: 'replace',
|
|
routerDirection: 'none',
|
|
tab: currentTab
|
|
}
|
|
} else if (action === 'pop') {
|
|
const routeInfo = locationHistory.current(initialHistoryPosition, currentHistoryPosition - delta);
|
|
|
|
if (routeInfo && routeInfo.pushedByRoute) {
|
|
const prevRouteInfo = locationHistory.findLastLocation(routeInfo, delta);
|
|
incomingRouteParams = {
|
|
...prevRouteInfo,
|
|
routerAction: 'pop',
|
|
routerDirection: 'back'
|
|
};
|
|
} else {
|
|
incomingRouteParams = {
|
|
routerAction: 'pop',
|
|
routerDirection: 'none',
|
|
tab: currentTab
|
|
}
|
|
}
|
|
}
|
|
if (!incomingRouteParams) {
|
|
incomingRouteParams = {
|
|
routerAction: 'push',
|
|
routerDirection: direction || 'forward',
|
|
tab: currentTab
|
|
}
|
|
}
|
|
}
|
|
|
|
let routeInfo: RouteInfo;
|
|
if (incomingRouteParams?.id) {
|
|
routeInfo = {
|
|
...incomingRouteParams,
|
|
lastPathname: leavingLocationInfo.pathname
|
|
}
|
|
|
|
} else {
|
|
const isPushed = incomingRouteParams.routerAction === 'push' && incomingRouteParams.routerDirection === 'forward';
|
|
routeInfo = {
|
|
id: generateId('routeInfo'),
|
|
...incomingRouteParams,
|
|
lastPathname: leavingLocationInfo.pathname,
|
|
pathname: location.path,
|
|
search: location.fullPath && location.fullPath.split('?')[1] || '',
|
|
params: location.params && location.params,
|
|
prevRouteLastPathname: leavingLocationInfo.lastPathname
|
|
}
|
|
|
|
if (isPushed) {
|
|
routeInfo.tab = leavingLocationInfo.tab;
|
|
routeInfo.pushedByRoute = (leavingLocationInfo.pathname !== '') ? leavingLocationInfo.pathname : undefined;
|
|
} else if (routeInfo.routerAction === 'pop') {
|
|
const route = locationHistory.findLastLocation(routeInfo);
|
|
routeInfo.pushedByRoute = route?.pushedByRoute;
|
|
} else if (routeInfo.routerAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
|
|
const lastRoute = locationHistory.getCurrentRouteInfoForTab(routeInfo.tab);
|
|
routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
|
|
} else if (routeInfo.routerAction === 'replace') {
|
|
const currentRouteInfo = locationHistory.last();
|
|
|
|
/**
|
|
* 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?.pushedByRoute;
|
|
const pushedByRoute = (currentPushedBy !== undefined && currentPushedBy !== routeInfo.pathname) ? currentPushedBy : routeInfo.pushedByRoute;
|
|
|
|
routeInfo.lastPathname = currentRouteInfo?.pathname || routeInfo.lastPathname;
|
|
routeInfo.pushedByRoute = pushedByRoute;
|
|
routeInfo.routerDirection = currentRouteInfo?.routerDirection || routeInfo.routerDirection;
|
|
routeInfo.routerAnimation = currentRouteInfo?.routerAnimation || routeInfo.routerAnimation;
|
|
routeInfo.prevRouteLastPathname = currentRouteInfo?.lastPathname;
|
|
}
|
|
|
|
}
|
|
|
|
routeInfo.position = currentHistoryPosition;
|
|
routeInfo.delta = delta;
|
|
const historySize = locationHistory.size();
|
|
const historyDiff = currentHistoryPosition - initialHistoryPosition;
|
|
|
|
/**
|
|
* If the size of location history is greater
|
|
* than the difference between the current history
|
|
* position and the initial history position
|
|
* then we are guaranteed to already have a history
|
|
* item for this route. In other words, a user
|
|
* is navigating within the history without pushing
|
|
* new items within the stack.
|
|
*/
|
|
if (historySize > historyDiff && routeInfo.tab === undefined) {
|
|
/**
|
|
* When going from /a --> /a/1 --> /b, then going
|
|
* back to /a, then going /a --> /a/2 --> /b, clicking
|
|
* the ion-back-button should return us to /a/2, not /a/1.
|
|
* However, since the route entry for /b already exists,
|
|
* we need to update other information such as the "pushedByRoute"
|
|
* so we know which route pushed this new route.
|
|
*
|
|
* However, when using router.go with a stride of >1 or <-1,
|
|
* we should not update this additional information because
|
|
* we are traversing through the history, not pushing new states.
|
|
* Going from /a --> /b --> /c, then doing router.go(-2), then doing
|
|
* router.go(2) to go from /a --> /c should not update the route
|
|
* listing to say that /c was pushed by /a.
|
|
*/
|
|
const hasDeltaStride = delta !== undefined && Math.abs(delta) !== 1;
|
|
locationHistory.updateByHistoryPosition(routeInfo, !hasDeltaStride);
|
|
} else {
|
|
locationHistory.add(routeInfo);
|
|
}
|
|
|
|
currentRouteInfo = routeInfo;
|
|
}
|
|
incomingRouteParams = undefined;
|
|
historyChangeListeners.forEach(cb => cb(currentRouteInfo));
|
|
}
|
|
|
|
const getCurrentRouteInfo = () => currentRouteInfo;
|
|
|
|
const canGoBack = (deep: number = 1) => locationHistory.canGoBack(deep, initialHistoryPosition, currentHistoryPosition);
|
|
|
|
const navigate = (navigationOptions: ExternalNavigationOptions) => {
|
|
const { routerAnimation, routerDirection, routerLink } = navigationOptions;
|
|
|
|
setIncomingRouteParams('push', routerDirection, routerAnimation);
|
|
|
|
router.push(routerLink);
|
|
}
|
|
|
|
const resetTab = (tab: string, originalHref: string) => {
|
|
const routeInfo = locationHistory.getFirstRouteInfoForTab(tab);
|
|
if (routeInfo) {
|
|
const newRouteInfo = { ...routeInfo };
|
|
newRouteInfo.pathname = originalHref;
|
|
incomingRouteParams = { ...newRouteInfo, routerAction: 'pop', routerDirection: 'back' };
|
|
router.push({ path: newRouteInfo.pathname, query: parseQuery(newRouteInfo.search) });
|
|
}
|
|
}
|
|
|
|
const changeTab = (tab: string, path?: string) => {
|
|
if (!path) return;
|
|
|
|
const routeInfo = locationHistory.getCurrentRouteInfoForTab(tab);
|
|
const [pathname] = path.split('?');
|
|
|
|
if (routeInfo) {
|
|
incomingRouteParams = {
|
|
...incomingRouteParams,
|
|
routerAction: 'push',
|
|
routerDirection: 'none',
|
|
tab
|
|
}
|
|
|
|
/**
|
|
* When going back to a tab
|
|
* you just left, it's possible
|
|
* for the route info to be incorrect
|
|
* as the tab you want is not the
|
|
* tab you are on.
|
|
*/
|
|
if (routeInfo.pathname === pathname) {
|
|
router.push({ path: routeInfo.pathname, query: parseQuery(routeInfo.search) });
|
|
} else {
|
|
router.push({ path: pathname, query: parseQuery(routeInfo.search) });
|
|
}
|
|
}
|
|
else {
|
|
handleNavigate(pathname, 'push', 'none', undefined, tab);
|
|
}
|
|
}
|
|
|
|
const handleSetCurrentTab = (tab: string) => {
|
|
currentTab = tab;
|
|
|
|
const ri = { ...locationHistory.last() };
|
|
if (ri.tab !== tab) {
|
|
ri.tab = tab;
|
|
locationHistory.update(ri);
|
|
}
|
|
}
|
|
|
|
// TODO types
|
|
const registerHistoryChangeListener = (cb: any) => {
|
|
historyChangeListeners.push(cb);
|
|
}
|
|
|
|
const setIncomingRouteParams = (routerAction: RouteAction = 'push', routerDirection: RouteDirection = 'forward', routerAnimation?: AnimationBuilder, tab?: string) => {
|
|
incomingRouteParams = {
|
|
routerAction,
|
|
routerDirection,
|
|
routerAnimation,
|
|
tab
|
|
};
|
|
}
|
|
|
|
const goBack = (routerAnimation?: AnimationBuilder) => {
|
|
setIncomingRouteParams('pop', 'back', routerAnimation);
|
|
router.back()
|
|
};
|
|
|
|
const goForward = (routerAnimation?: AnimationBuilder) => {
|
|
setIncomingRouteParams('push', 'forward', routerAnimation);
|
|
router.forward();
|
|
}
|
|
|
|
const getLeavingRouteInfo = () => {
|
|
return locationHistory.current(initialHistoryPosition, currentHistoryPosition);
|
|
}
|
|
|
|
return {
|
|
handleNavigate,
|
|
getLeavingRouteInfo,
|
|
handleNavigateBack,
|
|
handleSetCurrentTab,
|
|
getCurrentRouteInfo,
|
|
canGoBack,
|
|
navigate,
|
|
resetTab,
|
|
changeTab,
|
|
registerHistoryChangeListener,
|
|
goBack,
|
|
goForward
|
|
}
|
|
}
|