mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2675 lines
104 KiB
2675 lines
104 KiB
import * as i0 from '@angular/core';
|
|
import { Injectable, Inject, Optional, InjectionToken, inject, NgZone, ApplicationRef, Injector, createComponent, TemplateRef, Directive, ContentChild, EventEmitter, ViewContainerRef, EnvironmentInjector, Attribute, SkipSelf, Input, Output, reflectComponentType, HostListener, ElementRef, ViewChild } from '@angular/core';
|
|
import * as i3 from '@angular/router';
|
|
import { NavigationStart, PRIMARY_OUTLET, ChildrenOutletContexts, ActivatedRoute, Router } from '@angular/router';
|
|
import * as i1 from '@angular/common';
|
|
import { DOCUMENT } from '@angular/common';
|
|
import { isPlatform, getPlatforms, LIFECYCLE_WILL_ENTER, LIFECYCLE_DID_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_UNLOAD, componentOnReady } from '@ionic/core/components';
|
|
import { Subject, fromEvent, BehaviorSubject, combineLatest, of } from 'rxjs';
|
|
import { __decorate } from 'tslib';
|
|
import { filter, switchMap, distinctUntilChanged } from 'rxjs/operators';
|
|
import { NgControl } from '@angular/forms';
|
|
|
|
class DomController {
|
|
/**
|
|
* Schedules a task to run during the READ phase of the next frame.
|
|
* This task should only read the DOM, but never modify it.
|
|
*/
|
|
read(cb) {
|
|
getQueue().read(cb);
|
|
}
|
|
/**
|
|
* Schedules a task to run during the WRITE phase of the next frame.
|
|
* This task should write the DOM, but never READ it.
|
|
*/
|
|
write(cb) {
|
|
getQueue().write(cb);
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DomController, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
/** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DomController, providedIn: 'root' });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DomController, decorators: [{
|
|
type: Injectable,
|
|
args: [{
|
|
providedIn: 'root',
|
|
}]
|
|
}] });
|
|
const getQueue = () => {
|
|
const win = typeof window !== 'undefined' ? window : null;
|
|
if (win != null) {
|
|
const Ionic = win.Ionic;
|
|
if (Ionic?.queue) {
|
|
return Ionic.queue;
|
|
}
|
|
return {
|
|
read: (cb) => win.requestAnimationFrame(cb),
|
|
write: (cb) => win.requestAnimationFrame(cb),
|
|
};
|
|
}
|
|
return {
|
|
read: (cb) => cb(),
|
|
write: (cb) => cb(),
|
|
};
|
|
};
|
|
|
|
class MenuController {
|
|
menuController;
|
|
constructor(menuController) {
|
|
this.menuController = menuController;
|
|
}
|
|
/**
|
|
* Programmatically open the Menu.
|
|
* @param [menuId] Optionally get the menu by its id, or side.
|
|
* @return returns a promise when the menu is fully opened
|
|
*/
|
|
open(menuId) {
|
|
return this.menuController.open(menuId);
|
|
}
|
|
/**
|
|
* Programmatically close the Menu. If no `menuId` is given as the first
|
|
* argument then it'll close any menu which is open. If a `menuId`
|
|
* is given then it'll close that exact menu.
|
|
* @param [menuId] Optionally get the menu by its id, or side.
|
|
* @return returns a promise when the menu is fully closed
|
|
*/
|
|
close(menuId) {
|
|
return this.menuController.close(menuId);
|
|
}
|
|
/**
|
|
* Toggle the menu. If it's closed, it will open, and if opened, it
|
|
* will close.
|
|
* @param [menuId] Optionally get the menu by its id, or side.
|
|
* @return returns a promise when the menu has been toggled
|
|
*/
|
|
toggle(menuId) {
|
|
return this.menuController.toggle(menuId);
|
|
}
|
|
/**
|
|
* Used to enable or disable a menu. For example, there could be multiple
|
|
* left menus, but only one of them should be able to be opened at the same
|
|
* time. If there are multiple menus on the same side, then enabling one menu
|
|
* will also automatically disable all the others that are on the same side.
|
|
* @param [menuId] Optionally get the menu by its id, or side.
|
|
* @return Returns the instance of the menu, which is useful for chaining.
|
|
*/
|
|
enable(shouldEnable, menuId) {
|
|
return this.menuController.enable(shouldEnable, menuId);
|
|
}
|
|
/**
|
|
* Used to enable or disable the ability to swipe open the menu.
|
|
* @param shouldEnable True if it should be swipe-able, false if not.
|
|
* @param [menuId] Optionally get the menu by its id, or side.
|
|
* @return Returns the instance of the menu, which is useful for chaining.
|
|
*/
|
|
swipeGesture(shouldEnable, menuId) {
|
|
return this.menuController.swipeGesture(shouldEnable, menuId);
|
|
}
|
|
/**
|
|
* @param [menuId] Optionally get the menu by its id, or side.
|
|
* @return Returns true if the specified menu is currently open, otherwise false.
|
|
* If the menuId is not specified, it returns true if ANY menu is currenly open.
|
|
*/
|
|
isOpen(menuId) {
|
|
return this.menuController.isOpen(menuId);
|
|
}
|
|
/**
|
|
* @param [menuId] Optionally get the menu by its id, or side.
|
|
* @return Returns true if the menu is currently enabled, otherwise false.
|
|
*/
|
|
isEnabled(menuId) {
|
|
return this.menuController.isEnabled(menuId);
|
|
}
|
|
/**
|
|
* Used to get a menu instance. If a `menuId` is not provided then it'll
|
|
* return the first menu found. If a `menuId` is `left` or `right`, then
|
|
* it'll return the enabled menu on that side. Otherwise, if a `menuId` is
|
|
* provided, then it'll try to find the menu using the menu's `id`
|
|
* property. If a menu is not found then it'll return `null`.
|
|
* @param [menuId] Optionally get the menu by its id, or side.
|
|
* @return Returns the instance of the menu if found, otherwise `null`.
|
|
*/
|
|
get(menuId) {
|
|
return this.menuController.get(menuId);
|
|
}
|
|
/**
|
|
* @return Returns the instance of the menu already opened, otherwise `null`.
|
|
*/
|
|
getOpen() {
|
|
return this.menuController.getOpen();
|
|
}
|
|
/**
|
|
* @return Returns an array of all menu instances.
|
|
*/
|
|
getMenus() {
|
|
return this.menuController.getMenus();
|
|
}
|
|
registerAnimation(name, animation) {
|
|
return this.menuController.registerAnimation(name, animation);
|
|
}
|
|
isAnimating() {
|
|
return this.menuController.isAnimating();
|
|
}
|
|
_getOpenSync() {
|
|
return this.menuController._getOpenSync();
|
|
}
|
|
_createAnimation(type, menuCmp) {
|
|
return this.menuController._createAnimation(type, menuCmp);
|
|
}
|
|
_register(menu) {
|
|
return this.menuController._register(menu);
|
|
}
|
|
_unregister(menu) {
|
|
return this.menuController._unregister(menu);
|
|
}
|
|
_setOpen(menu, shouldOpen, animated) {
|
|
return this.menuController._setOpen(menu, shouldOpen, animated);
|
|
}
|
|
}
|
|
|
|
class Platform {
|
|
doc;
|
|
_readyPromise;
|
|
win;
|
|
/**
|
|
* @hidden
|
|
*/
|
|
backButton = new Subject();
|
|
/**
|
|
* The keyboardDidShow event emits when the
|
|
* on-screen keyboard is presented.
|
|
*/
|
|
keyboardDidShow = new Subject();
|
|
/**
|
|
* The keyboardDidHide event emits when the
|
|
* on-screen keyboard is hidden.
|
|
*/
|
|
keyboardDidHide = new Subject();
|
|
/**
|
|
* The pause event emits when the native platform puts the application
|
|
* into the background, typically when the user switches to a different
|
|
* application. This event would emit when a Cordova app is put into
|
|
* the background, however, it would not fire on a standard web browser.
|
|
*/
|
|
pause = new Subject();
|
|
/**
|
|
* The resume event emits when the native platform pulls the application
|
|
* out from the background. This event would emit when a Cordova app comes
|
|
* out from the background, however, it would not fire on a standard web browser.
|
|
*/
|
|
resume = new Subject();
|
|
/**
|
|
* The resize event emits when the browser window has changed dimensions. This
|
|
* could be from a browser window being physically resized, or from a device
|
|
* changing orientation.
|
|
*/
|
|
resize = new Subject();
|
|
constructor(doc, zone) {
|
|
this.doc = doc;
|
|
zone.run(() => {
|
|
this.win = doc.defaultView;
|
|
this.backButton.subscribeWithPriority = function (priority, callback) {
|
|
return this.subscribe((ev) => {
|
|
return ev.register(priority, (processNextHandler) => zone.run(() => callback(processNextHandler)));
|
|
});
|
|
};
|
|
proxyEvent(this.pause, doc, 'pause', zone);
|
|
proxyEvent(this.resume, doc, 'resume', zone);
|
|
proxyEvent(this.backButton, doc, 'ionBackButton', zone);
|
|
proxyEvent(this.resize, this.win, 'resize', zone);
|
|
proxyEvent(this.keyboardDidShow, this.win, 'ionKeyboardDidShow', zone);
|
|
proxyEvent(this.keyboardDidHide, this.win, 'ionKeyboardDidHide', zone);
|
|
let readyResolve;
|
|
this._readyPromise = new Promise((res) => {
|
|
readyResolve = res;
|
|
});
|
|
if (this.win?.['cordova']) {
|
|
doc.addEventListener('deviceready', () => {
|
|
readyResolve('cordova');
|
|
}, { once: true });
|
|
}
|
|
else {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
readyResolve('dom');
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* @returns returns true/false based on platform.
|
|
* @description
|
|
* Depending on the platform the user is on, `is(platformName)` will
|
|
* return `true` or `false`. Note that the same app can return `true`
|
|
* for more than one platform name. For example, an app running from
|
|
* an iPad would return `true` for the platform names: `mobile`,
|
|
* `ios`, `ipad`, and `tablet`. Additionally, if the app was running
|
|
* from Cordova then `cordova` would be true, and if it was running
|
|
* from a web browser on the iPad then `mobileweb` would be `true`.
|
|
*
|
|
* ```
|
|
* import { Platform } from 'ionic-angular';
|
|
*
|
|
* @Component({...})
|
|
* export MyPage {
|
|
* constructor(public platform: Platform) {
|
|
* if (this.platform.is('ios')) {
|
|
* // This will only print when on iOS
|
|
* console.log('I am an iOS device!');
|
|
* }
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* | Platform Name | Description |
|
|
* |-----------------|------------------------------------|
|
|
* | android | on a device running Android. |
|
|
* | capacitor | on a device running Capacitor. |
|
|
* | cordova | on a device running Cordova. |
|
|
* | ios | on a device running iOS. |
|
|
* | ipad | on an iPad device. |
|
|
* | iphone | on an iPhone device. |
|
|
* | phablet | on a phablet device. |
|
|
* | tablet | on a tablet device. |
|
|
* | electron | in Electron on a desktop device. |
|
|
* | pwa | as a PWA app. |
|
|
* | mobile | on a mobile device. |
|
|
* | mobileweb | on a mobile device in a browser. |
|
|
* | desktop | on a desktop device. |
|
|
* | hybrid | is a cordova or capacitor app. |
|
|
*
|
|
*/
|
|
is(platformName) {
|
|
return isPlatform(this.win, platformName);
|
|
}
|
|
/**
|
|
* @returns the array of platforms
|
|
* @description
|
|
* Depending on what device you are on, `platforms` can return multiple values.
|
|
* Each possible value is a hierarchy of platforms. For example, on an iPhone,
|
|
* it would return `mobile`, `ios`, and `iphone`.
|
|
*
|
|
* ```
|
|
* import { Platform } from 'ionic-angular';
|
|
*
|
|
* @Component({...})
|
|
* export MyPage {
|
|
* constructor(public platform: Platform) {
|
|
* // This will print an array of the current platforms
|
|
* console.log(this.platform.platforms());
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
platforms() {
|
|
return getPlatforms(this.win);
|
|
}
|
|
/**
|
|
* Returns a promise when the platform is ready and native functionality
|
|
* can be called. If the app is running from within a web browser, then
|
|
* the promise will resolve when the DOM is ready. When the app is running
|
|
* from an application engine such as Cordova, then the promise will
|
|
* resolve when Cordova triggers the `deviceready` event.
|
|
*
|
|
* The resolved value is the `readySource`, which states which platform
|
|
* ready was used. For example, when Cordova is ready, the resolved ready
|
|
* source is `cordova`. The default ready source value will be `dom`. The
|
|
* `readySource` is useful if different logic should run depending on the
|
|
* platform the app is running from. For example, only Cordova can execute
|
|
* the status bar plugin, so the web should not run status bar plugin logic.
|
|
*
|
|
* ```
|
|
* import { Component } from '@angular/core';
|
|
* import { Platform } from 'ionic-angular';
|
|
*
|
|
* @Component({...})
|
|
* export MyApp {
|
|
* constructor(public platform: Platform) {
|
|
* this.platform.ready().then((readySource) => {
|
|
* console.log('Platform ready from', readySource);
|
|
* // Platform now ready, execute any required native code
|
|
* });
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
ready() {
|
|
return this._readyPromise;
|
|
}
|
|
/**
|
|
* Returns if this app is using right-to-left language direction or not.
|
|
* We recommend the app's `index.html` file already has the correct `dir`
|
|
* attribute value set, such as `<html dir="ltr">` or `<html dir="rtl">`.
|
|
* [W3C: Structural markup and right-to-left text in HTML](http://www.w3.org/International/questions/qa-html-dir)
|
|
*/
|
|
get isRTL() {
|
|
return this.doc.dir === 'rtl';
|
|
}
|
|
/**
|
|
* Get the query string parameter
|
|
*/
|
|
getQueryParam(key) {
|
|
return readQueryParam(this.win.location.href, key);
|
|
}
|
|
/**
|
|
* Returns `true` if the app is in landscape mode.
|
|
*/
|
|
isLandscape() {
|
|
return !this.isPortrait();
|
|
}
|
|
/**
|
|
* Returns `true` if the app is in portrait mode.
|
|
*/
|
|
isPortrait() {
|
|
return this.win.matchMedia?.('(orientation: portrait)').matches;
|
|
}
|
|
testUserAgent(expression) {
|
|
const nav = this.win.navigator;
|
|
return !!(nav?.userAgent && nav.userAgent.indexOf(expression) >= 0);
|
|
}
|
|
/**
|
|
* Get the current url.
|
|
*/
|
|
url() {
|
|
return this.win.location.href;
|
|
}
|
|
/**
|
|
* Gets the width of the platform's viewport using `window.innerWidth`.
|
|
*/
|
|
width() {
|
|
return this.win.innerWidth;
|
|
}
|
|
/**
|
|
* Gets the height of the platform's viewport using `window.innerHeight`.
|
|
*/
|
|
height() {
|
|
return this.win.innerHeight;
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Platform, deps: [{ token: DOCUMENT }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
/** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Platform, providedIn: 'root' });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Platform, decorators: [{
|
|
type: Injectable,
|
|
args: [{
|
|
providedIn: 'root',
|
|
}]
|
|
}], ctorParameters: function () { return [{ type: undefined, decorators: [{
|
|
type: Inject,
|
|
args: [DOCUMENT]
|
|
}] }, { type: i0.NgZone }]; } });
|
|
const readQueryParam = (url, key) => {
|
|
key = key.replace(/[[\]\\]/g, '\\$&');
|
|
const regex = new RegExp('[\\?&]' + key + '=([^&#]*)');
|
|
const results = regex.exec(url);
|
|
return results ? decodeURIComponent(results[1].replace(/\+/g, ' ')) : null;
|
|
};
|
|
const proxyEvent = (emitter, el, eventName, zone) => {
|
|
if (el) {
|
|
el.addEventListener(eventName, (ev) => {
|
|
/**
|
|
* `zone.run` is required to make sure that we are running inside the Angular zone
|
|
* at all times. This is necessary since an app that has Capacitor will
|
|
* override the `document.addEventListener` with its own implementation.
|
|
* The override causes the event to no longer be in the Angular zone.
|
|
*/
|
|
zone.run(() => {
|
|
// ?? cordova might emit "null" events
|
|
const value = ev != null ? ev.detail : undefined;
|
|
emitter.next(value);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
class NavController {
|
|
location;
|
|
serializer;
|
|
router;
|
|
topOutlet;
|
|
direction = DEFAULT_DIRECTION;
|
|
animated = DEFAULT_ANIMATED;
|
|
animationBuilder;
|
|
guessDirection = 'forward';
|
|
guessAnimation;
|
|
lastNavId = -1;
|
|
constructor(platform, location, serializer, router) {
|
|
this.location = location;
|
|
this.serializer = serializer;
|
|
this.router = router;
|
|
// Subscribe to router events to detect direction
|
|
if (router) {
|
|
router.events.subscribe((ev) => {
|
|
if (ev instanceof NavigationStart) {
|
|
// restoredState is set if the browser back/forward button is used
|
|
const id = ev.restoredState ? ev.restoredState.navigationId : ev.id;
|
|
this.guessDirection = this.guessAnimation = id < this.lastNavId ? 'back' : 'forward';
|
|
this.lastNavId = this.guessDirection === 'forward' ? ev.id : id;
|
|
}
|
|
});
|
|
}
|
|
// Subscribe to backButton events
|
|
platform.backButton.subscribeWithPriority(0, (processNextHandler) => {
|
|
this.pop();
|
|
processNextHandler();
|
|
});
|
|
}
|
|
/**
|
|
* This method uses Angular's [Router](https://angular.io/api/router/Router) under the hood,
|
|
* it's equivalent to calling `this.router.navigateByUrl()`, but it's explicit about the **direction** of the transition.
|
|
*
|
|
* Going **forward** means that a new page is going to be pushed to the stack of the outlet (ion-router-outlet),
|
|
* and that it will show a "forward" animation by default.
|
|
*
|
|
* Navigating forward can also be triggered in a declarative manner by using the `[routerDirection]` directive:
|
|
*
|
|
* ```html
|
|
* <a routerLink="/path/to/page" routerDirection="forward">Link</a>
|
|
* ```
|
|
*/
|
|
navigateForward(url, options = {}) {
|
|
this.setDirection('forward', options.animated, options.animationDirection, options.animation);
|
|
return this.navigate(url, options);
|
|
}
|
|
/**
|
|
* This method uses Angular's [Router](https://angular.io/api/router/Router) under the hood,
|
|
* it's equivalent to calling:
|
|
*
|
|
* ```ts
|
|
* this.navController.setDirection('back');
|
|
* this.router.navigateByUrl(path);
|
|
* ```
|
|
*
|
|
* Going **back** means that all the pages in the stack until the navigated page is found will be popped,
|
|
* and that it will show a "back" animation by default.
|
|
*
|
|
* Navigating back can also be triggered in a declarative manner by using the `[routerDirection]` directive:
|
|
*
|
|
* ```html
|
|
* <a routerLink="/path/to/page" routerDirection="back">Link</a>
|
|
* ```
|
|
*/
|
|
navigateBack(url, options = {}) {
|
|
this.setDirection('back', options.animated, options.animationDirection, options.animation);
|
|
return this.navigate(url, options);
|
|
}
|
|
/**
|
|
* This method uses Angular's [Router](https://angular.io/api/router/Router) under the hood,
|
|
* it's equivalent to calling:
|
|
*
|
|
* ```ts
|
|
* this.navController.setDirection('root');
|
|
* this.router.navigateByUrl(path);
|
|
* ```
|
|
*
|
|
* Going **root** means that all existing pages in the stack will be removed,
|
|
* and the navigated page will become the single page in the stack.
|
|
*
|
|
* Navigating root can also be triggered in a declarative manner by using the `[routerDirection]` directive:
|
|
*
|
|
* ```html
|
|
* <a routerLink="/path/to/page" routerDirection="root">Link</a>
|
|
* ```
|
|
*/
|
|
navigateRoot(url, options = {}) {
|
|
this.setDirection('root', options.animated, options.animationDirection, options.animation);
|
|
return this.navigate(url, options);
|
|
}
|
|
/**
|
|
* Same as [Location](https://angular.io/api/common/Location)'s back() method.
|
|
* It will use the standard `window.history.back()` under the hood, but featuring a `back` animation
|
|
* by default.
|
|
*/
|
|
back(options = { animated: true, animationDirection: 'back' }) {
|
|
this.setDirection('back', options.animated, options.animationDirection, options.animation);
|
|
return this.location.back();
|
|
}
|
|
/**
|
|
* This methods goes back in the context of Ionic's stack navigation.
|
|
*
|
|
* It recursively finds the top active `ion-router-outlet` and calls `pop()`.
|
|
* This is the recommended way to go back when you are using `ion-router-outlet`.
|
|
*
|
|
* Resolves to `true` if it was able to pop.
|
|
*/
|
|
async pop() {
|
|
let outlet = this.topOutlet;
|
|
while (outlet) {
|
|
if (await outlet.pop()) {
|
|
return true;
|
|
}
|
|
else {
|
|
outlet = outlet.parentOutlet;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* This methods specifies the direction of the next navigation performed by the Angular router.
|
|
*
|
|
* `setDirection()` does not trigger any transition, it just sets some flags to be consumed by `ion-router-outlet`.
|
|
*
|
|
* It's recommended to use `navigateForward()`, `navigateBack()` and `navigateRoot()` instead of `setDirection()`.
|
|
*/
|
|
setDirection(direction, animated, animationDirection, animationBuilder) {
|
|
this.direction = direction;
|
|
this.animated = getAnimation(direction, animated, animationDirection);
|
|
this.animationBuilder = animationBuilder;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
setTopOutlet(outlet) {
|
|
this.topOutlet = outlet;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
consumeTransition() {
|
|
let direction = 'root';
|
|
let animation;
|
|
const animationBuilder = this.animationBuilder;
|
|
if (this.direction === 'auto') {
|
|
direction = this.guessDirection;
|
|
animation = this.guessAnimation;
|
|
}
|
|
else {
|
|
animation = this.animated;
|
|
direction = this.direction;
|
|
}
|
|
this.direction = DEFAULT_DIRECTION;
|
|
this.animated = DEFAULT_ANIMATED;
|
|
this.animationBuilder = undefined;
|
|
return {
|
|
direction,
|
|
animation,
|
|
animationBuilder,
|
|
};
|
|
}
|
|
navigate(url, options) {
|
|
if (Array.isArray(url)) {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
return this.router.navigate(url, options);
|
|
}
|
|
else {
|
|
/**
|
|
* navigateByUrl ignores any properties that
|
|
* would change the url, so things like queryParams
|
|
* would be ignored unless we create a url tree
|
|
* More Info: https://github.com/angular/angular/issues/18798
|
|
*/
|
|
const urlTree = this.serializer.parse(url.toString());
|
|
if (options.queryParams !== undefined) {
|
|
urlTree.queryParams = { ...options.queryParams };
|
|
}
|
|
if (options.fragment !== undefined) {
|
|
urlTree.fragment = options.fragment;
|
|
}
|
|
/**
|
|
* `navigateByUrl` will still apply `NavigationExtras` properties
|
|
* that do not modify the url, such as `replaceUrl` which is why
|
|
* `options` is passed in here.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
return this.router.navigateByUrl(urlTree, options);
|
|
}
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavController, deps: [{ token: Platform }, { token: i1.Location }, { token: i3.UrlSerializer }, { token: i3.Router, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
/** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavController, providedIn: 'root' });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavController, decorators: [{
|
|
type: Injectable,
|
|
args: [{
|
|
providedIn: 'root',
|
|
}]
|
|
}], ctorParameters: function () { return [{ type: Platform }, { type: i1.Location }, { type: i3.UrlSerializer }, { type: i3.Router, decorators: [{
|
|
type: Optional
|
|
}] }]; } });
|
|
const getAnimation = (direction, animated, animationDirection) => {
|
|
if (animated === false) {
|
|
return undefined;
|
|
}
|
|
if (animationDirection !== undefined) {
|
|
return animationDirection;
|
|
}
|
|
if (direction === 'forward' || direction === 'back') {
|
|
return direction;
|
|
}
|
|
else if (direction === 'root' && animated === true) {
|
|
return 'forward';
|
|
}
|
|
return undefined;
|
|
};
|
|
const DEFAULT_DIRECTION = 'auto';
|
|
const DEFAULT_ANIMATED = undefined;
|
|
|
|
class Config {
|
|
get(key, fallback) {
|
|
const c = getConfig();
|
|
if (c) {
|
|
return c.get(key, fallback);
|
|
}
|
|
return null;
|
|
}
|
|
getBoolean(key, fallback) {
|
|
const c = getConfig();
|
|
if (c) {
|
|
return c.getBoolean(key, fallback);
|
|
}
|
|
return false;
|
|
}
|
|
getNumber(key, fallback) {
|
|
const c = getConfig();
|
|
if (c) {
|
|
return c.getNumber(key, fallback);
|
|
}
|
|
return 0;
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Config, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
/** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Config, providedIn: 'root' });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Config, decorators: [{
|
|
type: Injectable,
|
|
args: [{
|
|
providedIn: 'root',
|
|
}]
|
|
}] });
|
|
const ConfigToken = new InjectionToken('USERCONFIG');
|
|
const getConfig = () => {
|
|
if (typeof window !== 'undefined') {
|
|
const Ionic = window.Ionic;
|
|
if (Ionic?.config) {
|
|
return Ionic.config;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* @description
|
|
* NavParams are an object that exists on a page and can contain data for that particular view.
|
|
* Similar to how data was pass to a view in V1 with `$stateParams`, NavParams offer a much more flexible
|
|
* option with a simple `get` method.
|
|
*
|
|
* @usage
|
|
* ```ts
|
|
* import { NavParams } from '@ionic/angular';
|
|
*
|
|
* export class MyClass{
|
|
*
|
|
* constructor(navParams: NavParams){
|
|
* // userParams is an object we have in our nav-parameters
|
|
* navParams.get('userParams');
|
|
* }
|
|
*
|
|
* }
|
|
* ```
|
|
*/
|
|
class NavParams {
|
|
data;
|
|
constructor(data = {}) {
|
|
this.data = data;
|
|
console.warn(`[Ionic Warning]: NavParams has been deprecated in favor of using Angular's input API. Developers should migrate to either the @Input decorator or the Signals-based input API.`);
|
|
}
|
|
/**
|
|
* Get the value of a nav-parameter for the current view
|
|
*
|
|
* ```ts
|
|
* import { NavParams } from 'ionic-angular';
|
|
*
|
|
* export class MyClass{
|
|
* constructor(public navParams: NavParams){
|
|
* // userParams is an object we have in our nav-parameters
|
|
* this.navParams.get('userParams');
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @param param Which param you want to look up
|
|
*/
|
|
get(param) {
|
|
return this.data[param];
|
|
}
|
|
}
|
|
|
|
// Token for injecting the modal element
|
|
const IonModalToken = new InjectionToken('IonModalToken');
|
|
// TODO(FW-2827): types
|
|
class AngularDelegate {
|
|
zone = inject(NgZone);
|
|
applicationRef = inject(ApplicationRef);
|
|
config = inject(ConfigToken);
|
|
create(environmentInjector, injector, elementReferenceKey) {
|
|
return new AngularFrameworkDelegate(environmentInjector, injector, this.applicationRef, this.zone, elementReferenceKey, this.config.useSetInputAPI ?? false);
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AngularDelegate, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
/** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AngularDelegate });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AngularDelegate, decorators: [{
|
|
type: Injectable
|
|
}] });
|
|
class AngularFrameworkDelegate {
|
|
environmentInjector;
|
|
injector;
|
|
applicationRef;
|
|
zone;
|
|
elementReferenceKey;
|
|
enableSignalsSupport;
|
|
elRefMap = new WeakMap();
|
|
elEventsMap = new WeakMap();
|
|
constructor(environmentInjector, injector, applicationRef, zone, elementReferenceKey, enableSignalsSupport) {
|
|
this.environmentInjector = environmentInjector;
|
|
this.injector = injector;
|
|
this.applicationRef = applicationRef;
|
|
this.zone = zone;
|
|
this.elementReferenceKey = elementReferenceKey;
|
|
this.enableSignalsSupport = enableSignalsSupport;
|
|
}
|
|
attachViewToDom(container, component, params, cssClasses) {
|
|
return this.zone.run(() => {
|
|
return new Promise((resolve) => {
|
|
const componentProps = {
|
|
...params,
|
|
};
|
|
/**
|
|
* Ionic Angular passes a reference to a modal
|
|
* or popover that can be accessed using a
|
|
* variable in the overlay component. If
|
|
* elementReferenceKey is defined, then we should
|
|
* pass a reference to the component using
|
|
* elementReferenceKey as the key.
|
|
*/
|
|
if (this.elementReferenceKey !== undefined) {
|
|
componentProps[this.elementReferenceKey] = container;
|
|
}
|
|
const el = attachView(this.zone, this.environmentInjector, this.injector, this.applicationRef, this.elRefMap, this.elEventsMap, container, component, componentProps, cssClasses, this.elementReferenceKey, this.enableSignalsSupport);
|
|
resolve(el);
|
|
});
|
|
});
|
|
}
|
|
removeViewFromDom(_container, component) {
|
|
return this.zone.run(() => {
|
|
return new Promise((resolve) => {
|
|
const componentRef = this.elRefMap.get(component);
|
|
if (componentRef) {
|
|
componentRef.destroy();
|
|
this.elRefMap.delete(component);
|
|
const unbindEvents = this.elEventsMap.get(component);
|
|
if (unbindEvents) {
|
|
unbindEvents();
|
|
this.elEventsMap.delete(component);
|
|
}
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
const attachView = (zone, environmentInjector, injector, applicationRef, elRefMap, elEventsMap, container, component, params, cssClasses, elementReferenceKey, enableSignalsSupport) => {
|
|
/**
|
|
* Wraps the injector with a custom injector that
|
|
* provides NavParams to the component.
|
|
*
|
|
* NavParams is a legacy feature from Ionic v3 that allows
|
|
* Angular developers to provide data to a component
|
|
* and access it by providing NavParams as a dependency
|
|
* in the constructor.
|
|
*
|
|
* The modern approach is to access the data directly
|
|
* from the component's class instance.
|
|
*/
|
|
const providers = getProviders(params);
|
|
// If this is an ion-modal, provide the modal element as an injectable
|
|
// so components inside the modal can inject it directly
|
|
if (container.tagName.toLowerCase() === 'ion-modal') {
|
|
providers.push({
|
|
provide: IonModalToken,
|
|
useValue: container,
|
|
});
|
|
}
|
|
const childInjector = Injector.create({
|
|
providers,
|
|
parent: injector,
|
|
});
|
|
const componentRef = createComponent(component, {
|
|
environmentInjector,
|
|
elementInjector: childInjector,
|
|
});
|
|
const instance = componentRef.instance;
|
|
const hostElement = componentRef.location.nativeElement;
|
|
if (params) {
|
|
/**
|
|
* For modals and popovers, a reference to the component is
|
|
* added to `params` during the call to attachViewToDom. If
|
|
* a reference using this name is already set, this means
|
|
* the app is trying to use the name as a component prop,
|
|
* which will cause collisions.
|
|
*/
|
|
if (elementReferenceKey && instance[elementReferenceKey] !== undefined) {
|
|
console.error(`[Ionic Error]: ${elementReferenceKey} is a reserved property when using ${container.tagName.toLowerCase()}. Rename or remove the "${elementReferenceKey}" property from ${component.name}.`);
|
|
}
|
|
/**
|
|
* Angular 14.1 added support for setInput
|
|
* so we need to fall back to Object.assign
|
|
* for Angular 14.0.
|
|
*/
|
|
if (enableSignalsSupport === true && componentRef.setInput !== undefined) {
|
|
const { modal, popover, ...otherParams } = params;
|
|
/**
|
|
* Any key/value pairs set in componentProps
|
|
* must be set as inputs on the component instance.
|
|
*/
|
|
for (const key in otherParams) {
|
|
componentRef.setInput(key, otherParams[key]);
|
|
}
|
|
/**
|
|
* Using setInput will cause an error when
|
|
* setting modal/popover on a component that
|
|
* does not define them as an input. For backwards
|
|
* compatibility purposes we fall back to using
|
|
* Object.assign for these properties.
|
|
*/
|
|
if (modal !== undefined) {
|
|
Object.assign(instance, { modal });
|
|
}
|
|
if (popover !== undefined) {
|
|
Object.assign(instance, { popover });
|
|
}
|
|
}
|
|
else {
|
|
Object.assign(instance, params);
|
|
}
|
|
}
|
|
if (cssClasses) {
|
|
for (const cssClass of cssClasses) {
|
|
hostElement.classList.add(cssClass);
|
|
}
|
|
}
|
|
const unbindEvents = bindLifecycleEvents(zone, instance, hostElement);
|
|
container.appendChild(hostElement);
|
|
applicationRef.attachView(componentRef.hostView);
|
|
elRefMap.set(hostElement, componentRef);
|
|
elEventsMap.set(hostElement, unbindEvents);
|
|
return hostElement;
|
|
};
|
|
const LIFECYCLES = [
|
|
LIFECYCLE_WILL_ENTER,
|
|
LIFECYCLE_DID_ENTER,
|
|
LIFECYCLE_WILL_LEAVE,
|
|
LIFECYCLE_DID_LEAVE,
|
|
LIFECYCLE_WILL_UNLOAD,
|
|
];
|
|
const bindLifecycleEvents = (zone, instance, element) => {
|
|
return zone.run(() => {
|
|
const unregisters = LIFECYCLES.filter((eventName) => typeof instance[eventName] === 'function').map((eventName) => {
|
|
const handler = (ev) => instance[eventName](ev.detail);
|
|
element.addEventListener(eventName, handler);
|
|
return () => element.removeEventListener(eventName, handler);
|
|
});
|
|
return () => unregisters.forEach((fn) => fn());
|
|
});
|
|
};
|
|
const NavParamsToken = new InjectionToken('NavParamsToken');
|
|
const getProviders = (params) => {
|
|
return [
|
|
{
|
|
provide: NavParamsToken,
|
|
useValue: params,
|
|
},
|
|
{
|
|
provide: NavParams,
|
|
useFactory: provideNavParamsInjectable,
|
|
deps: [NavParamsToken],
|
|
},
|
|
];
|
|
};
|
|
const provideNavParamsInjectable = (params) => {
|
|
return new NavParams(params);
|
|
};
|
|
|
|
// TODO: Is there a way we can grab this from angular-component-lib instead?
|
|
/* eslint-disable */
|
|
/* tslint:disable */
|
|
const proxyInputs = (Cmp, inputs) => {
|
|
const Prototype = Cmp.prototype;
|
|
inputs.forEach((item) => {
|
|
Object.defineProperty(Prototype, item, {
|
|
get() {
|
|
return this.el[item];
|
|
},
|
|
set(val) {
|
|
this.z.runOutsideAngular(() => (this.el[item] = val));
|
|
},
|
|
});
|
|
});
|
|
};
|
|
const proxyMethods = (Cmp, methods) => {
|
|
const Prototype = Cmp.prototype;
|
|
methods.forEach((methodName) => {
|
|
Prototype[methodName] = function () {
|
|
const args = arguments;
|
|
return this.z.runOutsideAngular(() => this.el[methodName].apply(this.el, args));
|
|
};
|
|
});
|
|
};
|
|
const proxyOutputs = (instance, el, events) => {
|
|
events.forEach((eventName) => (instance[eventName] = fromEvent(el, eventName)));
|
|
};
|
|
// tslint:disable-next-line: only-arrow-functions
|
|
function ProxyCmp(opts) {
|
|
const decorator = function (cls) {
|
|
const { defineCustomElementFn, inputs, methods } = opts;
|
|
if (defineCustomElementFn !== undefined) {
|
|
defineCustomElementFn();
|
|
}
|
|
if (inputs) {
|
|
proxyInputs(cls, inputs);
|
|
}
|
|
if (methods) {
|
|
proxyMethods(cls, methods);
|
|
}
|
|
return cls;
|
|
};
|
|
return decorator;
|
|
}
|
|
|
|
const MODAL_INPUTS = [
|
|
'animated',
|
|
'keepContentsMounted',
|
|
'backdropBreakpoint',
|
|
'backdropDismiss',
|
|
'breakpoints',
|
|
'canDismiss',
|
|
'cssClass',
|
|
'enterAnimation',
|
|
'expandToScroll',
|
|
'event',
|
|
'focusTrap',
|
|
'handle',
|
|
'handleBehavior',
|
|
'initialBreakpoint',
|
|
'isOpen',
|
|
'keyboardClose',
|
|
'leaveAnimation',
|
|
'mode',
|
|
'presentingElement',
|
|
'showBackdrop',
|
|
'translucent',
|
|
'trigger',
|
|
];
|
|
const MODAL_METHODS = [
|
|
'present',
|
|
'dismiss',
|
|
'onDidDismiss',
|
|
'onWillDismiss',
|
|
'setCurrentBreakpoint',
|
|
'getCurrentBreakpoint',
|
|
];
|
|
let IonModal = class IonModal {
|
|
z;
|
|
// TODO(FW-2827): type
|
|
template;
|
|
isCmpOpen = false;
|
|
el;
|
|
constructor(c, r, z) {
|
|
this.z = z;
|
|
this.el = r.nativeElement;
|
|
this.el.addEventListener('ionMount', () => {
|
|
this.isCmpOpen = true;
|
|
c.detectChanges();
|
|
});
|
|
this.el.addEventListener('didDismiss', () => {
|
|
this.isCmpOpen = false;
|
|
c.detectChanges();
|
|
});
|
|
proxyOutputs(this, this.el, [
|
|
'ionModalDidPresent',
|
|
'ionModalWillPresent',
|
|
'ionModalWillDismiss',
|
|
'ionModalDidDismiss',
|
|
'ionBreakpointDidChange',
|
|
'didPresent',
|
|
'willPresent',
|
|
'willDismiss',
|
|
'didDismiss',
|
|
]);
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonModal, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive });
|
|
/** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: IonModal, selector: "ion-modal", inputs: { animated: "animated", keepContentsMounted: "keepContentsMounted", backdropBreakpoint: "backdropBreakpoint", backdropDismiss: "backdropDismiss", breakpoints: "breakpoints", canDismiss: "canDismiss", cssClass: "cssClass", enterAnimation: "enterAnimation", expandToScroll: "expandToScroll", event: "event", focusTrap: "focusTrap", handle: "handle", handleBehavior: "handleBehavior", initialBreakpoint: "initialBreakpoint", isOpen: "isOpen", keyboardClose: "keyboardClose", leaveAnimation: "leaveAnimation", mode: "mode", presentingElement: "presentingElement", showBackdrop: "showBackdrop", translucent: "translucent", trigger: "trigger" }, queries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true }], ngImport: i0 });
|
|
};
|
|
IonModal = __decorate([
|
|
ProxyCmp({
|
|
inputs: MODAL_INPUTS,
|
|
methods: MODAL_METHODS,
|
|
})
|
|
/**
|
|
* @Component extends from @Directive
|
|
* so by defining the inputs here we
|
|
* do not need to re-define them for the
|
|
* lazy loaded popover.
|
|
*/
|
|
], IonModal);
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonModal, decorators: [{
|
|
type: Directive,
|
|
args: [{
|
|
selector: 'ion-modal',
|
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
|
inputs: MODAL_INPUTS,
|
|
}]
|
|
}], ctorParameters: function () { return [{ type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.NgZone }]; }, propDecorators: { template: [{
|
|
type: ContentChild,
|
|
args: [TemplateRef, { static: false }]
|
|
}] } });
|
|
|
|
const POPOVER_INPUTS = [
|
|
'alignment',
|
|
'animated',
|
|
'arrow',
|
|
'keepContentsMounted',
|
|
'backdropDismiss',
|
|
'cssClass',
|
|
'dismissOnSelect',
|
|
'enterAnimation',
|
|
'event',
|
|
'focusTrap',
|
|
'isOpen',
|
|
'keyboardClose',
|
|
'leaveAnimation',
|
|
'mode',
|
|
'showBackdrop',
|
|
'translucent',
|
|
'trigger',
|
|
'triggerAction',
|
|
'reference',
|
|
'size',
|
|
'side',
|
|
];
|
|
const POPOVER_METHODS = ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss'];
|
|
let IonPopover = class IonPopover {
|
|
z;
|
|
// TODO(FW-2827): type
|
|
template;
|
|
isCmpOpen = false;
|
|
el;
|
|
constructor(c, r, z) {
|
|
this.z = z;
|
|
this.el = r.nativeElement;
|
|
this.el.addEventListener('ionMount', () => {
|
|
this.isCmpOpen = true;
|
|
c.detectChanges();
|
|
});
|
|
this.el.addEventListener('didDismiss', () => {
|
|
this.isCmpOpen = false;
|
|
c.detectChanges();
|
|
});
|
|
proxyOutputs(this, this.el, [
|
|
'ionPopoverDidPresent',
|
|
'ionPopoverWillPresent',
|
|
'ionPopoverWillDismiss',
|
|
'ionPopoverDidDismiss',
|
|
'didPresent',
|
|
'willPresent',
|
|
'willDismiss',
|
|
'didDismiss',
|
|
]);
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonPopover, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive });
|
|
/** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: IonPopover, selector: "ion-popover", inputs: { alignment: "alignment", animated: "animated", arrow: "arrow", keepContentsMounted: "keepContentsMounted", backdropDismiss: "backdropDismiss", cssClass: "cssClass", dismissOnSelect: "dismissOnSelect", enterAnimation: "enterAnimation", event: "event", focusTrap: "focusTrap", isOpen: "isOpen", keyboardClose: "keyboardClose", leaveAnimation: "leaveAnimation", mode: "mode", showBackdrop: "showBackdrop", translucent: "translucent", trigger: "trigger", triggerAction: "triggerAction", reference: "reference", size: "size", side: "side" }, queries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true }], ngImport: i0 });
|
|
};
|
|
IonPopover = __decorate([
|
|
ProxyCmp({
|
|
inputs: POPOVER_INPUTS,
|
|
methods: POPOVER_METHODS,
|
|
})
|
|
/**
|
|
* @Component extends from @Directive
|
|
* so by defining the inputs here we
|
|
* do not need to re-define them for the
|
|
* lazy loaded popover.
|
|
*/
|
|
], IonPopover);
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonPopover, decorators: [{
|
|
type: Directive,
|
|
args: [{
|
|
selector: 'ion-popover',
|
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
|
inputs: POPOVER_INPUTS,
|
|
}]
|
|
}], ctorParameters: function () { return [{ type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.NgZone }]; }, propDecorators: { template: [{
|
|
type: ContentChild,
|
|
args: [TemplateRef, { static: false }]
|
|
}] } });
|
|
|
|
const insertView = (views, view, direction) => {
|
|
if (direction === 'root') {
|
|
return setRoot(views, view);
|
|
}
|
|
else if (direction === 'forward') {
|
|
return setForward(views, view);
|
|
}
|
|
else {
|
|
return setBack(views, view);
|
|
}
|
|
};
|
|
const setRoot = (views, view) => {
|
|
views = views.filter((v) => v.stackId !== view.stackId);
|
|
views.push(view);
|
|
return views;
|
|
};
|
|
const setForward = (views, view) => {
|
|
const index = views.indexOf(view);
|
|
if (index >= 0) {
|
|
views = views.filter((v) => v.stackId !== view.stackId || v.id <= view.id);
|
|
}
|
|
else {
|
|
views.push(view);
|
|
}
|
|
return views;
|
|
};
|
|
const setBack = (views, view) => {
|
|
const index = views.indexOf(view);
|
|
if (index >= 0) {
|
|
return views.filter((v) => v.stackId !== view.stackId || v.id <= view.id);
|
|
}
|
|
else {
|
|
return setRoot(views, view);
|
|
}
|
|
};
|
|
const getUrl = (router, activatedRoute) => {
|
|
const urlTree = router.createUrlTree(['.'], { relativeTo: activatedRoute });
|
|
return router.serializeUrl(urlTree);
|
|
};
|
|
const isTabSwitch = (enteringView, leavingView) => {
|
|
if (!leavingView) {
|
|
return true;
|
|
}
|
|
return enteringView.stackId !== leavingView.stackId;
|
|
};
|
|
const computeStackId = (prefixUrl, url) => {
|
|
if (!prefixUrl) {
|
|
return undefined;
|
|
}
|
|
const segments = toSegments(url);
|
|
for (let i = 0; i < segments.length; i++) {
|
|
if (i >= prefixUrl.length) {
|
|
return segments[i];
|
|
}
|
|
if (segments[i] !== prefixUrl[i]) {
|
|
return undefined;
|
|
}
|
|
}
|
|
return undefined;
|
|
};
|
|
const toSegments = (path) => {
|
|
return path
|
|
.split('/')
|
|
.map((s) => s.trim())
|
|
.filter((s) => s !== '');
|
|
};
|
|
const destroyView = (view) => {
|
|
if (view) {
|
|
view.ref.destroy();
|
|
view.unlistenEvents();
|
|
}
|
|
};
|
|
|
|
// TODO(FW-2827): types
|
|
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.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() {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
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');
|
|
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();
|
|
}
|
|
});
|
|
};
|
|
|
|
// TODO(FW-2827): types
|
|
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
|
class IonRouterOutlet {
|
|
parentOutlet;
|
|
nativeEl;
|
|
activatedView = null;
|
|
tabsPrefix;
|
|
_swipeGesture;
|
|
stackCtrl;
|
|
// Maintain map of activated route proxies for each component instance
|
|
proxyMap = new WeakMap();
|
|
// Keep the latest activated route in a subject for the proxy routes to switch map to
|
|
currentActivatedRoute$ = new BehaviorSubject(null);
|
|
activated = null;
|
|
/** @internal */
|
|
get activatedComponentRef() {
|
|
return this.activated;
|
|
}
|
|
_activatedRoute = null;
|
|
/**
|
|
* The name of the outlet
|
|
*/
|
|
name = PRIMARY_OUTLET;
|
|
/** @internal */
|
|
stackWillChange = new EventEmitter();
|
|
/** @internal */
|
|
stackDidChange = new EventEmitter();
|
|
// eslint-disable-next-line @angular-eslint/no-output-rename
|
|
activateEvents = new EventEmitter();
|
|
// eslint-disable-next-line @angular-eslint/no-output-rename
|
|
deactivateEvents = new EventEmitter();
|
|
parentContexts = inject(ChildrenOutletContexts);
|
|
location = inject(ViewContainerRef);
|
|
environmentInjector = inject(EnvironmentInjector);
|
|
inputBinder = inject(INPUT_BINDER, { optional: true });
|
|
/** @nodoc */
|
|
supportsBindingToComponentInputs = true;
|
|
// Ionic providers
|
|
config = inject(Config);
|
|
navCtrl = inject(NavController);
|
|
set animation(animation) {
|
|
this.nativeEl.animation = animation;
|
|
}
|
|
set animated(animated) {
|
|
this.nativeEl.animated = animated;
|
|
}
|
|
set swipeGesture(swipe) {
|
|
this._swipeGesture = swipe;
|
|
this.nativeEl.swipeHandler = swipe
|
|
? {
|
|
canStart: () => this.stackCtrl.canGoBack(1) && !this.stackCtrl.hasRunningTask(),
|
|
onStart: () => this.stackCtrl.startBackTransition(),
|
|
onEnd: (shouldContinue) => this.stackCtrl.endBackTransition(shouldContinue),
|
|
}
|
|
: undefined;
|
|
}
|
|
constructor(name, tabs, commonLocation, elementRef, router, zone, activatedRoute, parentOutlet) {
|
|
this.parentOutlet = parentOutlet;
|
|
this.nativeEl = elementRef.nativeElement;
|
|
this.name = name || PRIMARY_OUTLET;
|
|
this.tabsPrefix = tabs === 'true' ? getUrl(router, activatedRoute) : undefined;
|
|
this.stackCtrl = new StackController(this.tabsPrefix, this.nativeEl, router, this.navCtrl, zone, commonLocation);
|
|
this.parentContexts.onChildOutletCreated(this.name, this);
|
|
}
|
|
ngOnDestroy() {
|
|
this.stackCtrl.destroy();
|
|
this.inputBinder?.unsubscribeFromRouteData(this);
|
|
}
|
|
getContext() {
|
|
return this.parentContexts.getContext(this.name);
|
|
}
|
|
ngOnInit() {
|
|
this.initializeOutletWithName();
|
|
}
|
|
// Note: Ionic deviates from the Angular Router implementation here
|
|
initializeOutletWithName() {
|
|
if (!this.activated) {
|
|
// If the outlet was not instantiated at the time the route got activated we need to populate
|
|
// the outlet when it is initialized (ie inside a NgIf)
|
|
const context = this.getContext();
|
|
if (context?.route) {
|
|
this.activateWith(context.route, context.injector);
|
|
}
|
|
}
|
|
new Promise((resolve) => componentOnReady(this.nativeEl, resolve)).then(() => {
|
|
if (this._swipeGesture === undefined) {
|
|
this.swipeGesture = this.config.getBoolean('swipeBackEnabled', this.nativeEl.mode === 'ios');
|
|
}
|
|
});
|
|
}
|
|
get isActivated() {
|
|
return !!this.activated;
|
|
}
|
|
get component() {
|
|
if (!this.activated) {
|
|
throw new Error('Outlet is not activated');
|
|
}
|
|
return this.activated.instance;
|
|
}
|
|
get activatedRoute() {
|
|
if (!this.activated) {
|
|
throw new Error('Outlet is not activated');
|
|
}
|
|
return this._activatedRoute;
|
|
}
|
|
get activatedRouteData() {
|
|
if (this._activatedRoute) {
|
|
return this._activatedRoute.snapshot.data;
|
|
}
|
|
return {};
|
|
}
|
|
/**
|
|
* Called when the `RouteReuseStrategy` instructs to detach the subtree
|
|
*/
|
|
detach() {
|
|
throw new Error('incompatible reuse strategy');
|
|
}
|
|
/**
|
|
* Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
attach(_ref, _activatedRoute) {
|
|
throw new Error('incompatible reuse strategy');
|
|
}
|
|
deactivate() {
|
|
if (this.activated) {
|
|
if (this.activatedView) {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const context = this.getContext();
|
|
this.activatedView.savedData = new Map(context.children['contexts']);
|
|
/**
|
|
* Angular v11.2.10 introduced a change
|
|
* where this route context is cleared out when
|
|
* a router-outlet is deactivated, However,
|
|
* we need this route information in order to
|
|
* return a user back to the correct tab when
|
|
* leaving and then going back to the tab context.
|
|
*/
|
|
const primaryOutlet = this.activatedView.savedData.get('primary');
|
|
if (primaryOutlet && context.route) {
|
|
primaryOutlet.route = { ...context.route };
|
|
}
|
|
/**
|
|
* Ensure we are saving the NavigationExtras
|
|
* data otherwise it will be lost
|
|
*/
|
|
this.activatedView.savedExtras = {};
|
|
if (context.route) {
|
|
const contextSnapshot = context.route.snapshot;
|
|
this.activatedView.savedExtras.queryParams = contextSnapshot.queryParams;
|
|
this.activatedView.savedExtras.fragment = contextSnapshot.fragment;
|
|
}
|
|
}
|
|
const c = this.component;
|
|
this.activatedView = null;
|
|
this.activated = null;
|
|
this._activatedRoute = null;
|
|
this.deactivateEvents.emit(c);
|
|
}
|
|
}
|
|
activateWith(activatedRoute, environmentInjector) {
|
|
if (this.isActivated) {
|
|
throw new Error('Cannot activate an already activated outlet');
|
|
}
|
|
this._activatedRoute = activatedRoute;
|
|
let cmpRef;
|
|
let enteringView = this.stackCtrl.getExistingView(activatedRoute);
|
|
if (enteringView) {
|
|
cmpRef = this.activated = enteringView.ref;
|
|
const saved = enteringView.savedData;
|
|
if (saved) {
|
|
// self-restore
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const context = this.getContext();
|
|
context.children['contexts'] = saved;
|
|
}
|
|
// Updated activated route proxy for this component
|
|
this.updateActivatedRouteProxy(cmpRef.instance, activatedRoute);
|
|
}
|
|
else {
|
|
const snapshot = activatedRoute._futureSnapshot;
|
|
/**
|
|
* Angular 14 introduces a new `loadComponent` property to the route config.
|
|
* This function will assign a `component` property to the route snapshot.
|
|
* We check for the presence of this property to determine if the route is
|
|
* using standalone components.
|
|
*/
|
|
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
|
|
// We create an activated route proxy object that will maintain future updates for this component
|
|
// over its lifecycle in the stack.
|
|
const component$ = new BehaviorSubject(null);
|
|
const activatedRouteProxy = this.createActivatedRouteProxy(component$, activatedRoute);
|
|
const injector = new OutletInjector(activatedRouteProxy, childContexts, this.location.injector);
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const component = snapshot.routeConfig.component ?? snapshot.component;
|
|
/**
|
|
* View components need to be added as a child of ion-router-outlet
|
|
* for page transitions and swipe to go back.
|
|
* However, createComponent mounts components as siblings of the
|
|
* ViewContainerRef. As a result, outletContent must reference
|
|
* an ng-container inside of ion-router-outlet and not
|
|
* ion-router-outlet itself.
|
|
*/
|
|
cmpRef = this.activated = this.outletContent.createComponent(component, {
|
|
index: this.outletContent.length,
|
|
injector,
|
|
environmentInjector: environmentInjector ?? this.environmentInjector,
|
|
});
|
|
// Once the component is created we can push it to our local subject supplied to the proxy
|
|
component$.next(cmpRef.instance);
|
|
// Calling `markForCheck` to make sure we will run the change detection when the
|
|
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
|
|
/**
|
|
* At this point this.activated has been set earlier
|
|
* in this function, so it is guaranteed to be non-null.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
enteringView = this.stackCtrl.createView(this.activated, activatedRoute);
|
|
// Store references to the proxy by component
|
|
this.proxyMap.set(cmpRef.instance, activatedRouteProxy);
|
|
this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute });
|
|
}
|
|
this.inputBinder?.bindActivatedRouteToOutletComponent(this);
|
|
this.activatedView = enteringView;
|
|
/**
|
|
* The top outlet is set prior to the entering view's transition completing,
|
|
* so that when we have nested outlets (e.g. ion-tabs inside an ion-router-outlet),
|
|
* the tabs outlet will be assigned as the top outlet when a view inside tabs is
|
|
* activated.
|
|
*
|
|
* In this scenario, activeWith is called for both the tabs and the root router outlet.
|
|
* To avoid a race condition, we assign the top outlet synchronously.
|
|
*/
|
|
this.navCtrl.setTopOutlet(this);
|
|
const leavingView = this.stackCtrl.getActiveView();
|
|
this.stackWillChange.emit({
|
|
enteringView,
|
|
tabSwitch: isTabSwitch(enteringView, leavingView),
|
|
});
|
|
this.stackCtrl.setActive(enteringView).then((data) => {
|
|
this.activateEvents.emit(cmpRef.instance);
|
|
this.stackDidChange.emit(data);
|
|
});
|
|
}
|
|
/**
|
|
* Returns `true` if there are pages in the stack to go back.
|
|
*/
|
|
canGoBack(deep = 1, stackId) {
|
|
return this.stackCtrl.canGoBack(deep, stackId);
|
|
}
|
|
/**
|
|
* Resolves to `true` if it the outlet was able to sucessfully pop the last N pages.
|
|
*/
|
|
pop(deep = 1, stackId) {
|
|
return this.stackCtrl.pop(deep, stackId);
|
|
}
|
|
/**
|
|
* Returns the URL of the active page of each stack.
|
|
*/
|
|
getLastUrl(stackId) {
|
|
const active = this.stackCtrl.getLastUrl(stackId);
|
|
return active ? active.url : undefined;
|
|
}
|
|
/**
|
|
* Returns the RouteView of the active page of each stack.
|
|
* @internal
|
|
*/
|
|
getLastRouteView(stackId) {
|
|
return this.stackCtrl.getLastUrl(stackId);
|
|
}
|
|
/**
|
|
* Returns the root view in the tab stack.
|
|
* @internal
|
|
*/
|
|
getRootView(stackId) {
|
|
return this.stackCtrl.getRootUrl(stackId);
|
|
}
|
|
/**
|
|
* Returns the active stack ID. In the context of ion-tabs, it means the active tab.
|
|
*/
|
|
getActiveStackId() {
|
|
return this.stackCtrl.getActiveStackId();
|
|
}
|
|
/**
|
|
* Since the activated route can change over the life time of a component in an ion router outlet, we create
|
|
* a proxy so that we can update the values over time as a user navigates back to components already in the stack.
|
|
*/
|
|
createActivatedRouteProxy(component$, activatedRoute) {
|
|
const proxy = new ActivatedRoute();
|
|
proxy._futureSnapshot = activatedRoute._futureSnapshot;
|
|
proxy._routerState = activatedRoute._routerState;
|
|
proxy.snapshot = activatedRoute.snapshot;
|
|
proxy.outlet = activatedRoute.outlet;
|
|
proxy.component = activatedRoute.component;
|
|
// Setup wrappers for the observables so consumers don't have to worry about switching to new observables as the state updates
|
|
proxy._paramMap = this.proxyObservable(component$, 'paramMap');
|
|
proxy._queryParamMap = this.proxyObservable(component$, 'queryParamMap');
|
|
proxy.url = this.proxyObservable(component$, 'url');
|
|
proxy.params = this.proxyObservable(component$, 'params');
|
|
proxy.queryParams = this.proxyObservable(component$, 'queryParams');
|
|
proxy.fragment = this.proxyObservable(component$, 'fragment');
|
|
proxy.data = this.proxyObservable(component$, 'data');
|
|
return proxy;
|
|
}
|
|
/**
|
|
* Create a wrapped observable that will switch to the latest activated route matched by the given component
|
|
*/
|
|
proxyObservable(component$, path) {
|
|
return component$.pipe(
|
|
// First wait until the component instance is pushed
|
|
filter((component) => !!component), switchMap((component) => this.currentActivatedRoute$.pipe(filter((current) => current !== null && current.component === component), switchMap((current) => current && current.activatedRoute[path]), distinctUntilChanged())));
|
|
}
|
|
/**
|
|
* Updates the activated route proxy for the given component to the new incoming router state
|
|
*/
|
|
updateActivatedRouteProxy(component, activatedRoute) {
|
|
const proxy = this.proxyMap.get(component);
|
|
if (!proxy) {
|
|
throw new Error(`Could not find activated route proxy for view`);
|
|
}
|
|
proxy._futureSnapshot = activatedRoute._futureSnapshot;
|
|
proxy._routerState = activatedRoute._routerState;
|
|
proxy.snapshot = activatedRoute.snapshot;
|
|
proxy.outlet = activatedRoute.outlet;
|
|
proxy.component = activatedRoute.component;
|
|
this.currentActivatedRoute$.next({ component, activatedRoute });
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonRouterOutlet, deps: [{ token: 'name', attribute: true }, { token: 'tabs', attribute: true, optional: true }, { token: i1.Location }, { token: i0.ElementRef }, { token: i3.Router }, { token: i0.NgZone }, { token: i3.ActivatedRoute }, { token: IonRouterOutlet, optional: true, skipSelf: true }], target: i0.ɵɵFactoryTarget.Directive });
|
|
/** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: IonRouterOutlet, selector: "ion-router-outlet", inputs: { animated: "animated", animation: "animation", mode: "mode", swipeGesture: "swipeGesture", name: "name" }, outputs: { stackWillChange: "stackWillChange", stackDidChange: "stackDidChange", activateEvents: "activate", deactivateEvents: "deactivate" }, exportAs: ["outlet"], ngImport: i0 });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonRouterOutlet, decorators: [{
|
|
type: Directive,
|
|
args: [{
|
|
selector: 'ion-router-outlet',
|
|
exportAs: 'outlet',
|
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
|
inputs: ['animated', 'animation', 'mode', 'swipeGesture'],
|
|
}]
|
|
}], ctorParameters: function () { return [{ type: undefined, decorators: [{
|
|
type: Attribute,
|
|
args: ['name']
|
|
}] }, { type: undefined, decorators: [{
|
|
type: Optional
|
|
}, {
|
|
type: Attribute,
|
|
args: ['tabs']
|
|
}] }, { type: i1.Location }, { type: i0.ElementRef }, { type: i3.Router }, { type: i0.NgZone }, { type: i3.ActivatedRoute }, { type: IonRouterOutlet, decorators: [{
|
|
type: SkipSelf
|
|
}, {
|
|
type: Optional
|
|
}] }]; }, propDecorators: { name: [{
|
|
type: Input
|
|
}], stackWillChange: [{
|
|
type: Output
|
|
}], stackDidChange: [{
|
|
type: Output
|
|
}], activateEvents: [{
|
|
type: Output,
|
|
args: ['activate']
|
|
}], deactivateEvents: [{
|
|
type: Output,
|
|
args: ['deactivate']
|
|
}] } });
|
|
class OutletInjector {
|
|
route;
|
|
childContexts;
|
|
parent;
|
|
constructor(route, childContexts, parent) {
|
|
this.route = route;
|
|
this.childContexts = childContexts;
|
|
this.parent = parent;
|
|
}
|
|
get(token, notFoundValue) {
|
|
if (token === ActivatedRoute) {
|
|
return this.route;
|
|
}
|
|
if (token === ChildrenOutletContexts) {
|
|
return this.childContexts;
|
|
}
|
|
return this.parent.get(token, notFoundValue);
|
|
}
|
|
}
|
|
// TODO: FW-4785 - Remove this once Angular 15 support is dropped
|
|
const INPUT_BINDER = new InjectionToken('');
|
|
/**
|
|
* Injectable used as a tree-shakable provider for opting in to binding router data to component
|
|
* inputs.
|
|
*
|
|
* The RouterOutlet registers itself with this service when an `ActivatedRoute` is attached or
|
|
* activated. When this happens, the service subscribes to the `ActivatedRoute` observables (params,
|
|
* queryParams, data) and sets the inputs of the component using `ComponentRef.setInput`.
|
|
* Importantly, when an input does not have an item in the route data with a matching key, this
|
|
* input is set to `undefined`. If it were not done this way, the previous information would be
|
|
* retained if the data got removed from the route (i.e. if a query parameter is removed).
|
|
*
|
|
* The `RouterOutlet` should unregister itself when destroyed via `unsubscribeFromRouteData` so that
|
|
* the subscriptions are cleaned up.
|
|
*/
|
|
class RoutedComponentInputBinder {
|
|
outletDataSubscriptions = new Map();
|
|
bindActivatedRouteToOutletComponent(outlet) {
|
|
this.unsubscribeFromRouteData(outlet);
|
|
this.subscribeToRouteData(outlet);
|
|
}
|
|
unsubscribeFromRouteData(outlet) {
|
|
this.outletDataSubscriptions.get(outlet)?.unsubscribe();
|
|
this.outletDataSubscriptions.delete(outlet);
|
|
}
|
|
subscribeToRouteData(outlet) {
|
|
const { activatedRoute } = outlet;
|
|
const dataSubscription = combineLatest([activatedRoute.queryParams, activatedRoute.params, activatedRoute.data])
|
|
.pipe(switchMap(([queryParams, params, data], index) => {
|
|
data = { ...queryParams, ...params, ...data };
|
|
// Get the first result from the data subscription synchronously so it's available to
|
|
// the component as soon as possible (and doesn't require a second change detection).
|
|
if (index === 0) {
|
|
return of(data);
|
|
}
|
|
// Promise.resolve is used to avoid synchronously writing the wrong data when
|
|
// two of the Observables in the `combineLatest` stream emit one after
|
|
// another.
|
|
return Promise.resolve(data);
|
|
}))
|
|
.subscribe((data) => {
|
|
// Outlet may have been deactivated or changed names to be associated with a different
|
|
// route
|
|
if (!outlet.isActivated ||
|
|
!outlet.activatedComponentRef ||
|
|
outlet.activatedRoute !== activatedRoute ||
|
|
activatedRoute.component === null) {
|
|
this.unsubscribeFromRouteData(outlet);
|
|
return;
|
|
}
|
|
const mirror = reflectComponentType(activatedRoute.component);
|
|
if (!mirror) {
|
|
this.unsubscribeFromRouteData(outlet);
|
|
return;
|
|
}
|
|
for (const { templateName } of mirror.inputs) {
|
|
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
|
|
}
|
|
});
|
|
this.outletDataSubscriptions.set(outlet, dataSubscription);
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RoutedComponentInputBinder, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
/** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RoutedComponentInputBinder });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RoutedComponentInputBinder, decorators: [{
|
|
type: Injectable
|
|
}] });
|
|
const provideComponentInputBinding = () => {
|
|
return {
|
|
provide: INPUT_BINDER,
|
|
useFactory: componentInputBindingFactory,
|
|
deps: [Router],
|
|
};
|
|
};
|
|
function componentInputBindingFactory(router) {
|
|
/**
|
|
* We cast the router to any here, since the componentInputBindingEnabled
|
|
* property is not available until Angular v16.
|
|
*/
|
|
if (router?.componentInputBindingEnabled) {
|
|
return new RoutedComponentInputBinder();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const raf = (h) => {
|
|
if (typeof __zone_symbol__requestAnimationFrame === 'function') {
|
|
return __zone_symbol__requestAnimationFrame(h);
|
|
}
|
|
if (typeof requestAnimationFrame === 'function') {
|
|
return requestAnimationFrame(h);
|
|
}
|
|
return setTimeout(h);
|
|
};
|
|
|
|
// TODO(FW-2827): types
|
|
class ValueAccessor {
|
|
injector;
|
|
elementRef;
|
|
onChange = () => {
|
|
/**/
|
|
};
|
|
onTouched = () => {
|
|
/**/
|
|
};
|
|
lastValue;
|
|
statusChanges;
|
|
constructor(injector, elementRef) {
|
|
this.injector = injector;
|
|
this.elementRef = elementRef;
|
|
}
|
|
writeValue(value) {
|
|
this.elementRef.nativeElement.value = this.lastValue = value;
|
|
setIonicClasses(this.elementRef);
|
|
}
|
|
/**
|
|
* Notifies the ControlValueAccessor of a change in the value of the control.
|
|
*
|
|
* This is called by each of the ValueAccessor directives when we want to update
|
|
* the status and validity of the form control. For example with text components this
|
|
* is called when the ionInput event is fired. For select components this is called
|
|
* when the ionChange event is fired.
|
|
*
|
|
* This also updates the Ionic form status classes on the element.
|
|
*
|
|
* @param el The component element.
|
|
* @param value The new value of the control.
|
|
*/
|
|
handleValueChange(el, value) {
|
|
if (el === this.elementRef.nativeElement) {
|
|
if (value !== this.lastValue) {
|
|
this.lastValue = value;
|
|
this.onChange(value);
|
|
}
|
|
setIonicClasses(this.elementRef);
|
|
}
|
|
}
|
|
_handleBlurEvent(el) {
|
|
if (el === this.elementRef.nativeElement) {
|
|
this.onTouched();
|
|
setIonicClasses(this.elementRef);
|
|
// When ion-radio is blurred, el and this.elementRef.nativeElement are
|
|
// different so we need to check if the closest ion-radio-group is the same
|
|
// as this.elementRef.nativeElement and if so, we need to mark the radio group
|
|
// as touched
|
|
}
|
|
else if (el.closest('ion-radio-group') === this.elementRef.nativeElement) {
|
|
this.onTouched();
|
|
}
|
|
}
|
|
registerOnChange(fn) {
|
|
this.onChange = fn;
|
|
}
|
|
registerOnTouched(fn) {
|
|
this.onTouched = fn;
|
|
}
|
|
setDisabledState(isDisabled) {
|
|
this.elementRef.nativeElement.disabled = isDisabled;
|
|
}
|
|
ngOnDestroy() {
|
|
if (this.statusChanges) {
|
|
this.statusChanges.unsubscribe();
|
|
}
|
|
}
|
|
ngAfterViewInit() {
|
|
let ngControl;
|
|
try {
|
|
ngControl = this.injector.get(NgControl);
|
|
}
|
|
catch {
|
|
/* No FormControl or ngModel binding */
|
|
}
|
|
if (!ngControl) {
|
|
return;
|
|
}
|
|
// Listen for changes in validity, disabled, or pending states
|
|
if (ngControl.statusChanges) {
|
|
this.statusChanges = ngControl.statusChanges.subscribe(() => setIonicClasses(this.elementRef));
|
|
}
|
|
/**
|
|
* TODO FW-2787: Remove this in favor of https://github.com/angular/angular/issues/10887
|
|
* whenever it is implemented.
|
|
*/
|
|
const formControl = ngControl.control;
|
|
if (formControl) {
|
|
const methodsToPatch = ['markAsTouched', 'markAllAsTouched', 'markAsUntouched', 'markAsDirty', 'markAsPristine'];
|
|
methodsToPatch.forEach((method) => {
|
|
if (typeof formControl[method] !== 'undefined') {
|
|
const oldFn = formControl[method].bind(formControl);
|
|
formControl[method] = (...params) => {
|
|
oldFn(...params);
|
|
setIonicClasses(this.elementRef);
|
|
};
|
|
}
|
|
});
|
|
}
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ValueAccessor, deps: [{ token: i0.Injector }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
/** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ValueAccessor, host: { listeners: { "ionBlur": "_handleBlurEvent($event.target)" } }, ngImport: i0 });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ValueAccessor, decorators: [{
|
|
type: Directive
|
|
}], ctorParameters: function () { return [{ type: i0.Injector }, { type: i0.ElementRef }]; }, propDecorators: { _handleBlurEvent: [{
|
|
type: HostListener,
|
|
args: ['ionBlur', ['$event.target']]
|
|
}] } });
|
|
const setIonicClasses = (element) => {
|
|
raf(() => {
|
|
const input = element.nativeElement;
|
|
const hasValue = input.value != null && input.value.toString().length > 0;
|
|
const classes = getClasses(input);
|
|
setClasses(input, classes);
|
|
const item = input.closest('ion-item');
|
|
if (item) {
|
|
if (hasValue) {
|
|
setClasses(item, [...classes, 'item-has-value']);
|
|
}
|
|
else {
|
|
setClasses(item, classes);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
const getClasses = (element) => {
|
|
const classList = element.classList;
|
|
const classes = [];
|
|
for (let i = 0; i < classList.length; i++) {
|
|
const item = classList.item(i);
|
|
if (item !== null && startsWith(item, 'ng-')) {
|
|
classes.push(`ion-${item.substring(3)}`);
|
|
}
|
|
}
|
|
return classes;
|
|
};
|
|
const setClasses = (element, classes) => {
|
|
const classList = element.classList;
|
|
classList.remove('ion-valid', 'ion-invalid', 'ion-touched', 'ion-untouched', 'ion-dirty', 'ion-pristine');
|
|
classList.add(...classes);
|
|
};
|
|
const startsWith = (input, search) => {
|
|
return input.substring(0, search.length) === search;
|
|
};
|
|
|
|
const BACK_BUTTON_INPUTS = ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type'];
|
|
let IonBackButton = class IonBackButton {
|
|
routerOutlet;
|
|
navCtrl;
|
|
config;
|
|
r;
|
|
z;
|
|
el;
|
|
constructor(routerOutlet, navCtrl, config, r, z, c) {
|
|
this.routerOutlet = routerOutlet;
|
|
this.navCtrl = navCtrl;
|
|
this.config = config;
|
|
this.r = r;
|
|
this.z = z;
|
|
c.detach();
|
|
this.el = this.r.nativeElement;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
onClick(ev) {
|
|
const defaultHref = this.defaultHref || this.config.get('backButtonDefaultHref');
|
|
if (this.routerOutlet?.canGoBack()) {
|
|
this.navCtrl.setDirection('back', undefined, undefined, this.routerAnimation);
|
|
this.routerOutlet.pop();
|
|
ev.preventDefault();
|
|
}
|
|
else if (defaultHref != null) {
|
|
this.navCtrl.navigateBack(defaultHref, { animation: this.routerAnimation });
|
|
ev.preventDefault();
|
|
}
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonBackButton, deps: [{ token: IonRouterOutlet, optional: true }, { token: NavController }, { token: Config }, { token: i0.ElementRef }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
/** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: IonBackButton, inputs: { color: "color", defaultHref: "defaultHref", disabled: "disabled", icon: "icon", mode: "mode", routerAnimation: "routerAnimation", text: "text", type: "type" }, host: { listeners: { "click": "onClick($event)" } }, ngImport: i0 });
|
|
};
|
|
IonBackButton = __decorate([
|
|
ProxyCmp({
|
|
inputs: BACK_BUTTON_INPUTS,
|
|
})
|
|
], IonBackButton);
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonBackButton, decorators: [{
|
|
type: Directive,
|
|
args: [{
|
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
|
inputs: BACK_BUTTON_INPUTS,
|
|
}]
|
|
}], ctorParameters: function () { return [{ type: IonRouterOutlet, decorators: [{
|
|
type: Optional
|
|
}] }, { type: NavController }, { type: Config }, { type: i0.ElementRef }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { onClick: [{
|
|
type: HostListener,
|
|
args: ['click', ['$event']]
|
|
}] } });
|
|
|
|
const NAV_INPUTS = ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'];
|
|
const NAV_METHODS = [
|
|
'push',
|
|
'insert',
|
|
'insertPages',
|
|
'pop',
|
|
'popTo',
|
|
'popToRoot',
|
|
'removeIndex',
|
|
'setRoot',
|
|
'setPages',
|
|
'getActive',
|
|
'getByIndex',
|
|
'canGoBack',
|
|
'getPrevious',
|
|
];
|
|
let IonNav = class IonNav {
|
|
z;
|
|
el;
|
|
constructor(ref, environmentInjector, injector, angularDelegate, z, c) {
|
|
this.z = z;
|
|
c.detach();
|
|
this.el = ref.nativeElement;
|
|
ref.nativeElement.delegate = angularDelegate.create(environmentInjector, injector);
|
|
proxyOutputs(this, this.el, ['ionNavDidChange', 'ionNavWillChange']);
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonNav, deps: [{ token: i0.ElementRef }, { token: i0.EnvironmentInjector }, { token: i0.Injector }, { token: AngularDelegate }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
/** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: IonNav, inputs: { animated: "animated", animation: "animation", root: "root", rootParams: "rootParams", swipeGesture: "swipeGesture" }, ngImport: i0 });
|
|
};
|
|
IonNav = __decorate([
|
|
ProxyCmp({
|
|
inputs: NAV_INPUTS,
|
|
methods: NAV_METHODS,
|
|
})
|
|
], IonNav);
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonNav, decorators: [{
|
|
type: Directive,
|
|
args: [{
|
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
|
inputs: NAV_INPUTS,
|
|
}]
|
|
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.EnvironmentInjector }, { type: i0.Injector }, { type: AngularDelegate }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }]; } });
|
|
|
|
/**
|
|
* Adds support for Ionic routing directions and animations to the base Angular router link directive.
|
|
*
|
|
* When the router link is clicked, the directive will assign the direction and
|
|
* animation so that the routing integration will transition correctly.
|
|
*/
|
|
class RouterLinkDelegateDirective {
|
|
locationStrategy;
|
|
navCtrl;
|
|
elementRef;
|
|
router;
|
|
routerLink;
|
|
routerDirection = 'forward';
|
|
routerAnimation;
|
|
constructor(locationStrategy, navCtrl, elementRef, router, routerLink) {
|
|
this.locationStrategy = locationStrategy;
|
|
this.navCtrl = navCtrl;
|
|
this.elementRef = elementRef;
|
|
this.router = router;
|
|
this.routerLink = routerLink;
|
|
}
|
|
ngOnInit() {
|
|
this.updateTargetUrlAndHref();
|
|
this.updateTabindex();
|
|
}
|
|
ngOnChanges() {
|
|
this.updateTargetUrlAndHref();
|
|
}
|
|
/**
|
|
* The `tabindex` is set to `0` by default on the host element when
|
|
* the `routerLink` directive is used. This causes issues with Ionic
|
|
* components that wrap an `a` or `button` element, such as `ion-item`.
|
|
* See issue https://github.com/angular/angular/issues/28345
|
|
*
|
|
* This method removes the `tabindex` attribute from the host element
|
|
* to allow the Ionic component to manage the focus state correctly.
|
|
*/
|
|
updateTabindex() {
|
|
// Ionic components that render a native anchor or button element
|
|
const ionicComponents = [
|
|
'ION-BACK-BUTTON',
|
|
'ION-BREADCRUMB',
|
|
'ION-BUTTON',
|
|
'ION-CARD',
|
|
'ION-FAB-BUTTON',
|
|
'ION-ITEM',
|
|
'ION-ITEM-OPTION',
|
|
'ION-MENU-BUTTON',
|
|
'ION-SEGMENT-BUTTON',
|
|
'ION-TAB-BUTTON',
|
|
];
|
|
const hostElement = this.elementRef.nativeElement;
|
|
if (ionicComponents.includes(hostElement.tagName)) {
|
|
if (hostElement.getAttribute('tabindex') === '0') {
|
|
hostElement.removeAttribute('tabindex');
|
|
}
|
|
}
|
|
}
|
|
updateTargetUrlAndHref() {
|
|
if (this.routerLink?.urlTree) {
|
|
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
|
|
this.elementRef.nativeElement.href = href;
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
onClick(ev) {
|
|
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
|
|
/**
|
|
* This prevents the browser from
|
|
* performing a page reload when pressing
|
|
* an Ionic component with routerLink.
|
|
* The page reload interferes with routing
|
|
* and causes ion-back-button to disappear
|
|
* since the local history is wiped on reload.
|
|
*/
|
|
ev.preventDefault();
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RouterLinkDelegateDirective, deps: [{ token: i1.LocationStrategy }, { token: NavController }, { token: i0.ElementRef }, { token: i3.Router }, { token: i3.RouterLink, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
|
|
/** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: RouterLinkDelegateDirective, selector: ":not(a):not(area)[routerLink]", inputs: { routerDirection: "routerDirection", routerAnimation: "routerAnimation" }, host: { listeners: { "click": "onClick($event)" } }, usesOnChanges: true, ngImport: i0 });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RouterLinkDelegateDirective, decorators: [{
|
|
type: Directive,
|
|
args: [{
|
|
selector: ':not(a):not(area)[routerLink]',
|
|
}]
|
|
}], ctorParameters: function () { return [{ type: i1.LocationStrategy }, { type: NavController }, { type: i0.ElementRef }, { type: i3.Router }, { type: i3.RouterLink, decorators: [{
|
|
type: Optional
|
|
}] }]; }, propDecorators: { routerDirection: [{
|
|
type: Input
|
|
}], routerAnimation: [{
|
|
type: Input
|
|
}], onClick: [{
|
|
type: HostListener,
|
|
args: ['click', ['$event']]
|
|
}] } });
|
|
class RouterLinkWithHrefDelegateDirective {
|
|
locationStrategy;
|
|
navCtrl;
|
|
elementRef;
|
|
router;
|
|
routerLink;
|
|
routerDirection = 'forward';
|
|
routerAnimation;
|
|
constructor(locationStrategy, navCtrl, elementRef, router, routerLink) {
|
|
this.locationStrategy = locationStrategy;
|
|
this.navCtrl = navCtrl;
|
|
this.elementRef = elementRef;
|
|
this.router = router;
|
|
this.routerLink = routerLink;
|
|
}
|
|
ngOnInit() {
|
|
this.updateTargetUrlAndHref();
|
|
}
|
|
ngOnChanges() {
|
|
this.updateTargetUrlAndHref();
|
|
}
|
|
updateTargetUrlAndHref() {
|
|
if (this.routerLink?.urlTree) {
|
|
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
|
|
this.elementRef.nativeElement.href = href;
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
onClick() {
|
|
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RouterLinkWithHrefDelegateDirective, deps: [{ token: i1.LocationStrategy }, { token: NavController }, { token: i0.ElementRef }, { token: i3.Router }, { token: i3.RouterLink, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
|
|
/** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: RouterLinkWithHrefDelegateDirective, selector: "a[routerLink],area[routerLink]", inputs: { routerDirection: "routerDirection", routerAnimation: "routerAnimation" }, host: { listeners: { "click": "onClick()" } }, usesOnChanges: true, ngImport: i0 });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RouterLinkWithHrefDelegateDirective, decorators: [{
|
|
type: Directive,
|
|
args: [{
|
|
selector: 'a[routerLink],area[routerLink]',
|
|
}]
|
|
}], ctorParameters: function () { return [{ type: i1.LocationStrategy }, { type: NavController }, { type: i0.ElementRef }, { type: i3.Router }, { type: i3.RouterLink, decorators: [{
|
|
type: Optional
|
|
}] }]; }, propDecorators: { routerDirection: [{
|
|
type: Input
|
|
}], routerAnimation: [{
|
|
type: Input
|
|
}], onClick: [{
|
|
type: HostListener,
|
|
args: ['click']
|
|
}] } });
|
|
|
|
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
|
class IonTabs {
|
|
navCtrl;
|
|
tabsInner;
|
|
/**
|
|
* Emitted before the tab view is changed.
|
|
*/
|
|
ionTabsWillChange = new EventEmitter();
|
|
/**
|
|
* Emitted after the tab view is changed.
|
|
*/
|
|
ionTabsDidChange = new EventEmitter();
|
|
tabBarSlot = 'bottom';
|
|
hasTab = false;
|
|
selectedTab;
|
|
leavingTab;
|
|
constructor(navCtrl) {
|
|
this.navCtrl = navCtrl;
|
|
}
|
|
ngAfterViewInit() {
|
|
/**
|
|
* Developers must pass at least one ion-tab
|
|
* inside of ion-tabs if they want to use a
|
|
* basic tab-based navigation without the
|
|
* history stack or URL updates associated
|
|
* with the router.
|
|
*/
|
|
const firstTab = this.tabs.length > 0 ? this.tabs.first : undefined;
|
|
if (firstTab) {
|
|
this.hasTab = true;
|
|
this.setActiveTab(firstTab.tab);
|
|
this.tabSwitch();
|
|
}
|
|
}
|
|
ngAfterContentInit() {
|
|
this.detectSlotChanges();
|
|
}
|
|
ngAfterContentChecked() {
|
|
this.detectSlotChanges();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
onStackWillChange({ enteringView, tabSwitch }) {
|
|
const stackId = enteringView.stackId;
|
|
if (tabSwitch && stackId !== undefined) {
|
|
this.ionTabsWillChange.emit({ tab: stackId });
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
onStackDidChange({ enteringView, tabSwitch }) {
|
|
const stackId = enteringView.stackId;
|
|
if (tabSwitch && stackId !== undefined) {
|
|
if (this.tabBar) {
|
|
this.tabBar.selectedTab = stackId;
|
|
}
|
|
this.ionTabsDidChange.emit({ tab: stackId });
|
|
}
|
|
}
|
|
/**
|
|
* When a tab button is clicked, there are several scenarios:
|
|
* 1. If the selected tab is currently active (the tab button has been clicked
|
|
* again), then it should go to the root view for that tab.
|
|
*
|
|
* a. Get the saved root view from the router outlet. If the saved root view
|
|
* matches the tabRootUrl, set the route view to this view including the
|
|
* navigation extras.
|
|
* b. If the saved root view from the router outlet does
|
|
* not match, navigate to the tabRootUrl. No navigation extras are
|
|
* included.
|
|
*
|
|
* 2. If the current tab tab is not currently selected, get the last route
|
|
* view from the router outlet.
|
|
*
|
|
* a. If the last route view exists, navigate to that view including any
|
|
* navigation extras
|
|
* b. If the last route view doesn't exist, then navigate
|
|
* to the default tabRootUrl
|
|
*/
|
|
select(tabOrEvent) {
|
|
const isTabString = typeof tabOrEvent === 'string';
|
|
const tab = isTabString ? tabOrEvent : tabOrEvent.detail.tab;
|
|
/**
|
|
* If the tabs are not using the router, then
|
|
* the tab switch logic is handled by the tabs
|
|
* component itself.
|
|
*/
|
|
if (this.hasTab) {
|
|
this.setActiveTab(tab);
|
|
this.tabSwitch();
|
|
return;
|
|
}
|
|
const alreadySelected = this.outlet.getActiveStackId() === tab;
|
|
const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`;
|
|
/**
|
|
* If this is a nested tab, prevent the event
|
|
* from bubbling otherwise the outer tabs
|
|
* will respond to this event too, causing
|
|
* the app to get directed to the wrong place.
|
|
*/
|
|
if (!isTabString) {
|
|
tabOrEvent.stopPropagation();
|
|
}
|
|
if (alreadySelected) {
|
|
const activeStackId = this.outlet.getActiveStackId();
|
|
const activeView = this.outlet.getLastRouteView(activeStackId);
|
|
// If on root tab, do not navigate to root tab again
|
|
if (activeView?.url === tabRootUrl) {
|
|
return;
|
|
}
|
|
const rootView = this.outlet.getRootView(tab);
|
|
const navigationExtras = rootView && tabRootUrl === rootView.url && rootView.savedExtras;
|
|
return this.navCtrl.navigateRoot(tabRootUrl, {
|
|
...navigationExtras,
|
|
animated: true,
|
|
animationDirection: 'back',
|
|
});
|
|
}
|
|
else {
|
|
const lastRoute = this.outlet.getLastRouteView(tab);
|
|
/**
|
|
* If there is a lastRoute, goto that, otherwise goto the fallback url of the
|
|
* selected tab
|
|
*/
|
|
const url = lastRoute?.url || tabRootUrl;
|
|
const navigationExtras = lastRoute?.savedExtras;
|
|
return this.navCtrl.navigateRoot(url, {
|
|
...navigationExtras,
|
|
animated: true,
|
|
animationDirection: 'back',
|
|
});
|
|
}
|
|
}
|
|
setActiveTab(tab) {
|
|
const tabs = this.tabs;
|
|
const selectedTab = tabs.find((t) => t.tab === tab);
|
|
if (!selectedTab) {
|
|
console.error(`[Ionic Error]: Tab with id: "${tab}" does not exist`);
|
|
return;
|
|
}
|
|
this.leavingTab = this.selectedTab;
|
|
this.selectedTab = selectedTab;
|
|
this.ionTabsWillChange.emit({ tab });
|
|
selectedTab.el.active = true;
|
|
}
|
|
tabSwitch() {
|
|
const { selectedTab, leavingTab } = this;
|
|
if (this.tabBar && selectedTab) {
|
|
this.tabBar.selectedTab = selectedTab.tab;
|
|
}
|
|
if (leavingTab?.tab !== selectedTab?.tab) {
|
|
if (leavingTab?.el) {
|
|
leavingTab.el.active = false;
|
|
}
|
|
}
|
|
if (selectedTab) {
|
|
this.ionTabsDidChange.emit({ tab: selectedTab.tab });
|
|
}
|
|
}
|
|
getSelected() {
|
|
if (this.hasTab) {
|
|
return this.selectedTab?.tab;
|
|
}
|
|
return this.outlet.getActiveStackId();
|
|
}
|
|
/**
|
|
* Detects changes to the slot attribute of the tab bar.
|
|
*
|
|
* If the slot attribute has changed, then the tab bar
|
|
* should be relocated to the new slot position.
|
|
*/
|
|
detectSlotChanges() {
|
|
this.tabBars.forEach((tabBar) => {
|
|
// el is a protected attribute from the generated component wrapper
|
|
const currentSlot = tabBar.el.getAttribute('slot');
|
|
if (currentSlot !== this.tabBarSlot) {
|
|
this.tabBarSlot = currentSlot;
|
|
this.relocateTabBar();
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Relocates the tab bar to the new slot position.
|
|
*/
|
|
relocateTabBar() {
|
|
/**
|
|
* `el` is a protected attribute from the generated component wrapper.
|
|
* To avoid having to manually create the wrapper for tab bar, we
|
|
* cast the tab bar to any and access the protected attribute.
|
|
*/
|
|
const tabBar = this.tabBar.el;
|
|
if (this.tabBarSlot === 'top') {
|
|
/**
|
|
* A tab bar with a slot of "top" should be inserted
|
|
* at the top of the container.
|
|
*/
|
|
this.tabsInner.nativeElement.before(tabBar);
|
|
}
|
|
else {
|
|
/**
|
|
* A tab bar with a slot of "bottom" or without a slot
|
|
* should be inserted at the end of the container.
|
|
*/
|
|
this.tabsInner.nativeElement.after(tabBar);
|
|
}
|
|
}
|
|
/** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonTabs, deps: [{ token: NavController }], target: i0.ɵɵFactoryTarget.Directive });
|
|
/** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: IonTabs, selector: "ion-tabs", outputs: { ionTabsWillChange: "ionTabsWillChange", ionTabsDidChange: "ionTabsDidChange" }, host: { listeners: { "ionTabButtonClick": "select($event)" } }, viewQueries: [{ propertyName: "tabsInner", first: true, predicate: ["tabsInner"], descendants: true, read: ElementRef, static: true }], ngImport: i0 });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonTabs, decorators: [{
|
|
type: Directive,
|
|
args: [{
|
|
selector: 'ion-tabs',
|
|
}]
|
|
}], ctorParameters: function () { return [{ type: NavController }]; }, propDecorators: { tabsInner: [{
|
|
type: ViewChild,
|
|
args: ['tabsInner', { read: ElementRef, static: true }]
|
|
}], ionTabsWillChange: [{
|
|
type: Output
|
|
}], ionTabsDidChange: [{
|
|
type: Output
|
|
}], select: [{
|
|
type: HostListener,
|
|
args: ['ionTabButtonClick', ['$event']]
|
|
}] } });
|
|
|
|
// TODO(FW-2827): types
|
|
class OverlayBaseController {
|
|
ctrl;
|
|
constructor(ctrl) {
|
|
this.ctrl = ctrl;
|
|
}
|
|
/**
|
|
* Creates a new overlay
|
|
*/
|
|
create(opts) {
|
|
return this.ctrl.create((opts || {}));
|
|
}
|
|
/**
|
|
* When `id` is not provided, it dismisses the top overlay.
|
|
*/
|
|
dismiss(data, role, id) {
|
|
return this.ctrl.dismiss(data, role, id);
|
|
}
|
|
/**
|
|
* Returns the top overlay.
|
|
*/
|
|
getTop() {
|
|
return this.ctrl.getTop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides a way to customize when activated routes get reused.
|
|
*/
|
|
class IonicRouteStrategy {
|
|
/**
|
|
* Whether the given route should detach for later reuse.
|
|
*/
|
|
shouldDetach(_route) {
|
|
return false;
|
|
}
|
|
/**
|
|
* Returns `false`, meaning the route (and its subtree) is never reattached
|
|
*/
|
|
shouldAttach(_route) {
|
|
return false;
|
|
}
|
|
/**
|
|
* A no-op; the route is never stored since this strategy never detaches routes for later re-use.
|
|
*/
|
|
store(_route, _detachedTree) {
|
|
return;
|
|
}
|
|
/**
|
|
* Returns `null` because this strategy does not store routes for later re-use.
|
|
*/
|
|
retrieve(_route) {
|
|
return null;
|
|
}
|
|
/**
|
|
* Determines if a route should be reused.
|
|
* This strategy returns `true` when the future route config and
|
|
* current route config are identical and all route parameters are identical.
|
|
*/
|
|
shouldReuseRoute(future, curr) {
|
|
if (future.routeConfig !== curr.routeConfig) {
|
|
return false;
|
|
}
|
|
// checking router params
|
|
const futureParams = future.params;
|
|
const currentParams = curr.params;
|
|
const keysA = Object.keys(futureParams);
|
|
const keysB = Object.keys(currentParams);
|
|
if (keysA.length !== keysB.length) {
|
|
return false;
|
|
}
|
|
// Test for A's keys different from B.
|
|
for (const key of keysA) {
|
|
if (currentParams[key] !== futureParams[key]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generated bundle index. Do not edit.
|
|
*/
|
|
|
|
export { AngularDelegate, Config, ConfigToken, DomController, IonBackButton, IonModal, IonModalToken, IonNav, IonPopover, IonRouterOutlet, IonTabs, IonicRouteStrategy, MenuController, NavController, NavParams, OverlayBaseController, Platform, ProxyCmp, RouterLinkDelegateDirective, RouterLinkWithHrefDelegateDirective, ValueAccessor, bindLifecycleEvents, provideComponentInputBinding, raf, setIonicClasses };
|
|
//# sourceMappingURL=ionic-angular-common.mjs.map
|
|
|