@ionic/angular
Version:
Angular specific wrappers for @ionic/core
282 lines • 10.9 kB
JavaScript
import { bindLifecycleEvents } from '../../providers/angular-delegate';
import { computeStackId, destroyView, getUrl, insertView, isTabSwitch, toSegments, } from './stack-utils';
// TODO(FW-2827): types
export class StackController {
containerEl;
router;
navCtrl;
zone;
location;
views = [];
runningTask;
skipTransition = false;
tabsPrefix;
activeView;
nextId = 0;
constructor(tabsPrefix, containerEl, router, navCtrl, zone, location) {
this.containerEl = containerEl;
this.router = router;
this.navCtrl = navCtrl;
this.zone = zone;
this.location = location;
this.tabsPrefix = tabsPrefix !== undefined ? toSegments(tabsPrefix) : undefined;
}
createView(ref, activatedRoute) {
const url = getUrl(this.router, activatedRoute);
const element = ref?.location?.nativeElement;
const unlistenEvents = bindLifecycleEvents(this.zone, ref.changeDetectorRef, ref.instance, element);
return {
id: this.nextId++,
stackId: computeStackId(this.tabsPrefix, url),
unlistenEvents,
element,
ref,
url,
};
}
getExistingView(activatedRoute) {
const activatedUrlKey = getUrl(this.router, activatedRoute);
const view = this.views.find((vw) => vw.url === activatedUrlKey);
if (view) {
view.ref.changeDetectorRef.reattach();
}
return view;
}
setActive(enteringView) {
const consumeResult = this.navCtrl.consumeTransition();
let { direction, animation, animationBuilder } = consumeResult;
const leavingView = this.activeView;
const tabSwitch = isTabSwitch(enteringView, leavingView);
if (tabSwitch) {
direction = 'back';
animation = undefined;
}
const viewsSnapshot = this.views.slice();
let currentNavigation;
const router = this.router;
// Angular >= 7.2.0
if (router.getCurrentNavigation) {
currentNavigation = router.getCurrentNavigation();
// Angular < 7.2.0
}
else if (router.navigations?.value) {
currentNavigation = router.navigations.value;
}
/**
* If the navigation action
* sets `replaceUrl: true`
* then we need to make sure
* we remove the last item
* from our views stack
*/
if (currentNavigation?.extras?.replaceUrl) {
if (this.views.length > 0) {
this.views.splice(-1, 1);
}
}
const reused = this.views.includes(enteringView);
const views = this.insertView(enteringView, direction);
// Trigger change detection before transition starts
// This will call ngOnInit() the first time too, just after the view
// was attached to the dom, but BEFORE the transition starts
if (!reused) {
enteringView.ref.changeDetectorRef.detectChanges();
}
/**
* If we are going back from a page that
* was presented using a custom animation
* we should default to using that
* unless the developer explicitly
* provided another animation.
*/
const customAnimation = enteringView.animationBuilder;
if (animationBuilder === undefined && direction === 'back' && !tabSwitch && customAnimation !== undefined) {
animationBuilder = customAnimation;
}
/**
* Save any custom animation so that navigating
* back will use this custom animation by default.
*/
if (leavingView) {
leavingView.animationBuilder = animationBuilder;
}
// Wait until previous transitions finish
return this.zone.runOutsideAngular(() => {
return this.wait(() => {
// disconnect leaving page from change detection to
// reduce jank during the page transition
if (leavingView) {
leavingView.ref.changeDetectorRef.detach();
}
// In case the enteringView is the same as the leavingPage we need to reattach()
enteringView.ref.changeDetectorRef.reattach();
return this.transition(enteringView, leavingView, animation, this.canGoBack(1), false, animationBuilder)
.then(() => cleanupAsync(enteringView, views, viewsSnapshot, this.location, this.zone))
.then(() => ({
enteringView,
direction,
animation,
tabSwitch,
}));
});
});
}
canGoBack(deep, stackId = this.getActiveStackId()) {
return this.getStack(stackId).length > deep;
}
pop(deep, stackId = this.getActiveStackId()) {
return this.zone.run(() => {
const views = this.getStack(stackId);
if (views.length <= deep) {
return Promise.resolve(false);
}
const view = views[views.length - deep - 1];
let url = view.url;
const viewSavedData = view.savedData;
if (viewSavedData) {
const primaryOutlet = viewSavedData.get('primary');
if (primaryOutlet?.route?._routerState?.snapshot.url) {
url = primaryOutlet.route._routerState.snapshot.url;
}
}
const { animationBuilder } = this.navCtrl.consumeTransition();
return this.navCtrl.navigateBack(url, { ...view.savedExtras, animation: animationBuilder }).then(() => true);
});
}
startBackTransition() {
const leavingView = this.activeView;
if (leavingView) {
const views = this.getStack(leavingView.stackId);
const enteringView = views[views.length - 2];
const customAnimation = enteringView.animationBuilder;
return this.wait(() => {
return this.transition(enteringView, // entering view
leavingView, // leaving view
'back', this.canGoBack(2), true, customAnimation);
});
}
return Promise.resolve();
}
endBackTransition(shouldComplete) {
if (shouldComplete) {
this.skipTransition = true;
this.pop(1);
}
else if (this.activeView) {
cleanup(this.activeView, this.views, this.views, this.location, this.zone);
}
}
getLastUrl(stackId) {
const views = this.getStack(stackId);
return views.length > 0 ? views[views.length - 1] : undefined;
}
/**
* @internal
*/
getRootUrl(stackId) {
const views = this.getStack(stackId);
return views.length > 0 ? views[0] : undefined;
}
getActiveStackId() {
return this.activeView ? this.activeView.stackId : undefined;
}
/**
* @internal
*/
getActiveView() {
return this.activeView;
}
hasRunningTask() {
return this.runningTask !== undefined;
}
destroy() {
this.containerEl = undefined;
this.views.forEach(destroyView);
this.activeView = undefined;
this.views = [];
}
getStack(stackId) {
return this.views.filter((v) => v.stackId === stackId);
}
insertView(enteringView, direction) {
this.activeView = enteringView;
this.views = insertView(this.views, enteringView, direction);
return this.views.slice();
}
transition(enteringView, leavingView, direction, showGoBack, progressAnimation, animationBuilder) {
if (this.skipTransition) {
this.skipTransition = false;
return Promise.resolve(false);
}
if (leavingView === enteringView) {
return Promise.resolve(false);
}
const enteringEl = enteringView ? enteringView.element : undefined;
const leavingEl = leavingView ? leavingView.element : undefined;
const containerEl = this.containerEl;
if (enteringEl && enteringEl !== leavingEl) {
enteringEl.classList.add('ion-page');
enteringEl.classList.add('ion-page-invisible');
// containerEl is set to undefined when the stack controller is destroyed,
// so a transition that resolves after its outlet was destroyed must not
// dereference it (see destroy()).
if (containerEl?.commit) {
return containerEl.commit(enteringEl, leavingEl, {
duration: direction === undefined ? 0 : undefined,
direction,
showGoBack,
progressAnimation,
animationBuilder,
});
}
}
return Promise.resolve(false);
}
async wait(task) {
if (this.runningTask !== undefined) {
await this.runningTask;
this.runningTask = undefined;
}
const promise = (this.runningTask = task());
promise.finally(() => (this.runningTask = undefined));
return promise;
}
}
const cleanupAsync = (activeRoute, views, viewsSnapshot, location, zone) => {
if (typeof requestAnimationFrame === 'function') {
return new Promise((resolve) => {
requestAnimationFrame(() => {
cleanup(activeRoute, views, viewsSnapshot, location, zone);
resolve();
});
});
}
return Promise.resolve();
};
const cleanup = (activeRoute, views, viewsSnapshot, location, zone) => {
/**
* Re-enter the Angular zone when destroying page components. This will allow
* lifecycle events (`ngOnDestroy`) to be run inside the Angular zone.
*/
zone.run(() => viewsSnapshot.filter((view) => !views.includes(view)).forEach(destroyView));
views.forEach((view) => {
/**
* In the event that a user navigated multiple
* times in rapid succession, we want to make sure
* we don't pre-emptively detach a view while
* it is in mid-transition.
*
* In this instance we also do not care about query
* params or fragments as it will be the same view regardless
*/
const locationWithoutParams = location.path().split('?')[0];
const locationWithoutFragment = locationWithoutParams.split('#')[0];
if (view !== activeRoute && view.url !== locationWithoutFragment) {
const element = view.element;
element.setAttribute('aria-hidden', 'true');
element.classList.add('ion-page-hidden');
view.ref.changeDetectorRef.detach();
}
});
};
//# sourceMappingURL=stack-controller.js.map