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 `` or ``. * [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 * Link * ``` */ 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 * Link * ``` */ 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 * Link * ``` */ 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, 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 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']] }] } }); /** * 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'] }] } }); 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 }]; } }); // 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']] }] } }); 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; }; /** * 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; } } // 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(); } } /** * Generated bundle index. Do not edit. */ export { AngularDelegate, Config, ConfigToken, DomController, IonBackButton, IonModal, IonNav, IonPopover, IonRouterOutlet, IonTabs, IonicRouteStrategy, MenuController, NavController, NavParams, OverlayBaseController, Platform, ProxyCmp, RouterLinkDelegateDirective, RouterLinkWithHrefDelegateDirective, ValueAccessor, bindLifecycleEvents, provideComponentInputBinding, raf, setIonicClasses }; //# sourceMappingURL=ionic-angular-common.mjs.map