@ionic/angular
Version:
Angular specific wrappers for @ionic/core
1,311 lines (1,299 loc) • 106 kB
JavaScript
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 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 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 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];
}
}
// 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 childInjector = Injector.create({
providers: getProviders(params),
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 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 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 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,