mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-08 23:58:13 +08:00
258 lines
6.9 KiB
TypeScript
258 lines
6.9 KiB
TypeScript
import { shallowRef } from "vue";
|
|
import type { RouteLocationMatched, Router } from "vue-router";
|
|
|
|
import type { RouteInfo, ViewItem, ViewStacks } from "./types";
|
|
import { generateId } from "./utils";
|
|
|
|
export const createViewStacks = (router: Router) => {
|
|
const viewStacks: ViewStacks = {};
|
|
|
|
/**
|
|
* Returns the number of active stacks.
|
|
* This is useful for determining if an app
|
|
* is using linear navigation only or non-linear
|
|
* navigation. Multiple stacks indiciate an app
|
|
* is using non-linear navigation.
|
|
*/
|
|
const size = () => Object.keys(viewStacks).length;
|
|
|
|
const clear = (outletId: number) => {
|
|
delete viewStacks[outletId];
|
|
};
|
|
|
|
const getViewStack = (outletId: number) => {
|
|
return viewStacks[outletId];
|
|
};
|
|
|
|
const registerIonPage = (viewItem: ViewItem, ionPage: HTMLElement) => {
|
|
viewItem.ionPageElement = ionPage;
|
|
viewItem.ionRoute = true;
|
|
|
|
/**
|
|
* This is needed otherwise Vue Router
|
|
* will not consider this component mounted
|
|
* and will not run route guards that
|
|
* are written in the component.
|
|
*/
|
|
viewItem.matchedRoute.instances = {
|
|
default: viewItem.vueComponentRef.value,
|
|
};
|
|
};
|
|
|
|
const findViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number) => {
|
|
return findViewItemByPath(routeInfo.pathname, outletId, false);
|
|
};
|
|
|
|
const findLeavingViewItemByRouteInfo = (
|
|
routeInfo: RouteInfo,
|
|
outletId?: number,
|
|
mustBeIonRoute = true
|
|
) => {
|
|
return findViewItemByPath(routeInfo.lastPathname, outletId, mustBeIonRoute);
|
|
};
|
|
|
|
const findViewItemByPathname = (pathname: string, outletId?: number) => {
|
|
return findViewItemByPath(pathname, outletId, false);
|
|
};
|
|
|
|
const findViewItemInStack = (
|
|
path: string,
|
|
stack: ViewItem[]
|
|
): ViewItem | undefined => {
|
|
return stack.find((viewItem: ViewItem) => {
|
|
if (viewItem.pathname === path) {
|
|
return viewItem;
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
};
|
|
|
|
const findViewItemByPath = (
|
|
path: string,
|
|
outletId?: number,
|
|
mustBeIonRoute = false
|
|
): ViewItem | undefined => {
|
|
const matchView = (viewItem: ViewItem) => {
|
|
if ((mustBeIonRoute && !viewItem.ionRoute) || path === "") {
|
|
return false;
|
|
}
|
|
|
|
const resolvedPath = router.resolve(path);
|
|
const findMatchedRoute = resolvedPath.matched.find(
|
|
(matchedRoute: RouteLocationMatched) =>
|
|
matchedRoute === viewItem.matchedRoute
|
|
);
|
|
|
|
if (findMatchedRoute) {
|
|
/**
|
|
* /page/1 and /page/2 should not match
|
|
* to the same view item otherwise there will
|
|
* be not page transition and we will need to
|
|
* explicitly clear out parameters from page 1
|
|
* so the page 2 params are properly passed
|
|
* to the developer's app.
|
|
*/
|
|
const hasParameter = findMatchedRoute.path.includes(":");
|
|
if (hasParameter && path !== viewItem.pathname) {
|
|
return false;
|
|
}
|
|
|
|
return viewItem;
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
if (outletId) {
|
|
const stack = viewStacks[outletId];
|
|
if (!stack) return undefined;
|
|
|
|
const match = router
|
|
? stack.find(matchView)
|
|
: findViewItemInStack(path, stack);
|
|
if (match) return match;
|
|
} else {
|
|
for (const outletId in viewStacks) {
|
|
const stack = viewStacks[outletId];
|
|
const viewItem = findViewItemInStack(path, stack);
|
|
if (viewItem) {
|
|
return viewItem;
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
// TODO(FW-2969): type
|
|
const createViewItem = (
|
|
outletId: number,
|
|
vueComponent: any,
|
|
matchedRoute: RouteLocationMatched,
|
|
routeInfo: RouteInfo,
|
|
ionPage?: HTMLElement
|
|
): ViewItem => {
|
|
return {
|
|
id: generateId("viewItem"),
|
|
pathname: routeInfo.pathname,
|
|
outletId,
|
|
matchedRoute,
|
|
ionPageElement: ionPage,
|
|
vueComponent,
|
|
vueComponentRef: shallowRef(),
|
|
ionRoute: false,
|
|
mount: false,
|
|
exact: routeInfo.pathname === matchedRoute.path,
|
|
params: routeInfo.params,
|
|
vueComponentData: {},
|
|
};
|
|
};
|
|
|
|
const add = (viewItem: ViewItem): void => {
|
|
const { outletId } = viewItem;
|
|
if (!viewStacks[outletId]) {
|
|
viewStacks[outletId] = [viewItem];
|
|
} else {
|
|
viewStacks[outletId].push(viewItem);
|
|
}
|
|
};
|
|
|
|
const remove = (viewItem: ViewItem, outletId?: number): void => {
|
|
if (!outletId) {
|
|
throw Error("outletId required");
|
|
}
|
|
|
|
const viewStack = viewStacks[outletId];
|
|
if (viewStack) {
|
|
viewStacks[outletId] = viewStack.filter(
|
|
(item) => item.id !== viewItem.id
|
|
);
|
|
}
|
|
};
|
|
|
|
const getChildrenToRender = (outletId: number): ViewItem[] => {
|
|
const viewStack = viewStacks[outletId];
|
|
if (viewStack) {
|
|
const components = viewStacks[outletId].filter((v) => v.mount);
|
|
return components;
|
|
}
|
|
return [];
|
|
};
|
|
|
|
/**
|
|
* When navigating backwards, we need to clean up and
|
|
* leaving pages so that they are re-created if
|
|
* we ever navigate back to them. This is especially
|
|
* important when using router.go and stepping back
|
|
* multiple pages at a time.
|
|
*/
|
|
const unmountLeavingViews = (
|
|
outletId: number,
|
|
viewItem: ViewItem,
|
|
delta = 1
|
|
) => {
|
|
const viewStack = viewStacks[outletId];
|
|
if (!viewStack) return;
|
|
|
|
const startIndex = viewStack.findIndex((v) => v === viewItem);
|
|
|
|
for (let i = startIndex + 1; i < startIndex - delta; i++) {
|
|
const viewItem = viewStack[i];
|
|
viewItem.mount = false;
|
|
viewItem.ionPageElement = undefined;
|
|
viewItem.ionRoute = false;
|
|
viewItem.matchedRoute.instances = {};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* When navigating forward it is possible for
|
|
* developers to step forward over multiple views.
|
|
* The intermediary views need to be remounted so that
|
|
* swipe to go back works properly.
|
|
* We need to account for the delta value here too because
|
|
* we do not want to remount an unrelated view.
|
|
* Example:
|
|
* /home --> /page2 --> router.back() --> /page3
|
|
* Going to /page3 would remount /page2 since we do
|
|
* not prune /page2 from the stack. However, /page2
|
|
* needs to remain in the stack.
|
|
* Example:
|
|
* /home --> /page2 --> /page3 --> router.go(-2) --> router.go(2)
|
|
* We would end up on /page3, but users need to be able to swipe
|
|
* to go back to /page2 and /home, so we need both pages mounted
|
|
* in the DOM.
|
|
*/
|
|
const mountIntermediaryViews = (
|
|
outletId: number,
|
|
viewItem: ViewItem,
|
|
delta = 1
|
|
) => {
|
|
const viewStack = viewStacks[outletId];
|
|
if (!viewStack) return;
|
|
|
|
const startIndex = viewStack.findIndex((v) => v === viewItem);
|
|
|
|
for (let i = startIndex + 1; i < startIndex + delta; i++) {
|
|
viewStack[i].mount = true;
|
|
}
|
|
};
|
|
|
|
return {
|
|
unmountLeavingViews,
|
|
mountIntermediaryViews,
|
|
clear,
|
|
findViewItemByRouteInfo,
|
|
findLeavingViewItemByRouteInfo,
|
|
findViewItemByPathname,
|
|
createViewItem,
|
|
getChildrenToRender,
|
|
add,
|
|
remove,
|
|
registerIonPage,
|
|
getViewStack,
|
|
size,
|
|
};
|
|
};
|