@ionic/angular
Version:
Angular specific wrappers for @ionic/core
290 lines • 11.7 kB
JavaScript
import { Directive, ElementRef, EventEmitter, HostListener, Output, ViewChild, } from '@angular/core';
import * as i0 from "@angular/core";
import * as i1 from "../../providers/nav-controller";
/**
* Extracts `queryParams` and `fragment` from a tab button's href for use
* as Angular `NavigationExtras`. Returns `undefined` when neither is present.
*/
const parseHrefExtras = (href) => {
if (!href) {
return undefined;
}
const hashIndex = href.indexOf('#');
// Treat a bare `#` (no fragment text) as no fragment.
const fragment = hashIndex >= 0 && hashIndex < href.length - 1 ? href.slice(hashIndex + 1) : undefined;
const beforeHash = hashIndex >= 0 ? href.slice(0, hashIndex) : href;
const queryIndex = beforeHash.indexOf('?');
const search = queryIndex >= 0 ? beforeHash.slice(queryIndex + 1) : '';
let queryParams;
if (search) {
const params = new URLSearchParams(search);
queryParams = {};
for (const key of new Set(params.keys())) {
const all = params.getAll(key);
queryParams[key] = all.length > 1 ? all : all[0];
}
}
if (!queryParams && fragment === undefined) {
return undefined;
}
/**
* Build the result with only the populated keys so that a spread of the
* returned object does not overwrite saved `queryParams`/`fragment` with
* `undefined` (which `Object.assign`/spread would copy as a real key).
*/
const extras = {};
if (queryParams)
extras.queryParams = queryParams;
if (fragment !== undefined)
extras.fragment = fragment;
return extras;
};
export 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 });
}
}
/**
* Host listener for the `ionTabButtonClick` event. Angular 22 enabled stricter
* host-binding type checking, which types `$event` as the DOM `Event`. That is
* not assignable to `select`'s public `string | CustomEvent` parameter, so this
* thin wrapper narrows the event before forwarding to keep `select`'s public
* signature intact.
*/
onTabButtonClick(ev) {
return this.select(ev);
}
/**
* 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. Any `queryParams` or `fragment` declared on the tab
* button's `href` are also forwarded.
* b. If the saved root view from the router outlet does not match, navigate
* to the tabRootUrl, forwarding any `queryParams`/`fragment` declared on
* the tab button's `href`.
*
* 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, forwarding any `queryParams`/`fragment` declared on the
* tab button's `href`.
*/
select(tabOrEvent) {
const isTabString = typeof tabOrEvent === 'string';
const tab = isTabString ? tabOrEvent : tabOrEvent.detail.tab;
const href = isTabString ? undefined : tabOrEvent.detail.href;
/**
* 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}`;
/**
* The href pathname is ignored here; tab routing is driven by `tabsPrefix/tab`.
* Only the query and fragment are forwarded as navigation extras.
*/
const hrefExtras = parseHrefExtras(href);
/**
* 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);
// Keep the explicit rootView null-guard; an optional-chain rewrite changes the short-circuit value spread below.
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
const navigationExtras = rootView && tabRootUrl === rootView.url && rootView.savedExtras;
return this.navCtrl.navigateRoot(tabRootUrl, {
...navigationExtras,
...hrefExtras,
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. When falling back to the tab root, honor query params and
* fragment declared on the tab button's href.
*/
const url = lastRoute?.url || tabRootUrl;
const navigationExtras = lastRoute?.savedExtras ?? (url === tabRootUrl ? hrefExtras : undefined);
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: "22.0.0", ngImport: i0, type: IonTabs, deps: [{ token: i1.NavController }], target: i0.ɵɵFactoryTarget.Directive });
/** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.0", type: IonTabs, isStandalone: true, selector: "ion-tabs", outputs: { ionTabsWillChange: "ionTabsWillChange", ionTabsDidChange: "ionTabsDidChange" }, host: { listeners: { "ionTabButtonClick": "onTabButtonClick($event)" } }, viewQueries: [{ propertyName: "tabsInner", first: true, predicate: ["tabsInner"], descendants: true, read: ElementRef, static: true }], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: IonTabs, decorators: [{
type: Directive,
args: [{
selector: 'ion-tabs',
}]
}], ctorParameters: () => [{ type: i1.NavController }], propDecorators: { tabsInner: [{
type: ViewChild,
args: ['tabsInner', { read: ElementRef, static: true }]
}], ionTabsWillChange: [{
type: Output
}], ionTabsDidChange: [{
type: Output
}], onTabButtonClick: [{
type: HostListener,
args: ['ionTabButtonClick', ['$event']]
}] } });
//# sourceMappingURL=tabs.js.map