tabs.js 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. import { Component, ElementRef, EventEmitter, Input, Optional, Output, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation, forwardRef } from '@angular/core';
  2. import { Subject } from 'rxjs/Subject';
  3. import 'rxjs/add/operator/takeUntil';
  4. import { App } from '../app/app';
  5. import { Config } from '../../config/config';
  6. import { DeepLinker } from '../../navigation/deep-linker';
  7. import { Ion } from '../ion';
  8. import { isBlank, isPresent } from '../../util/util';
  9. import { Keyboard } from '../../platform/keyboard';
  10. import { NavController } from '../../navigation/nav-controller';
  11. import { DIRECTION_SWITCH, getComponent } from '../../navigation/nav-util';
  12. import { formatUrlPart } from '../../navigation/url-serializer';
  13. import { RootNode } from '../split-pane/split-pane';
  14. import { Platform } from '../../platform/platform';
  15. import { TabHighlight } from './tab-highlight';
  16. import { ViewController } from '../../navigation/view-controller';
  17. /**
  18. * @name Tabs
  19. * @description
  20. * Tabs make it easy to navigate between different pages or functional
  21. * aspects of an app. The Tabs component, written as `<ion-tabs>`, is
  22. * a container of individual [Tab](../Tab/) components. Each individual `ion-tab`
  23. * is a declarative component for a [NavController](../../../navigation/NavController/)
  24. *
  25. * For more information on using nav controllers like Tab or [Nav](../../nav/Nav/),
  26. * take a look at the [NavController API Docs](../../../navigation/NavController/).
  27. *
  28. * ### Placement
  29. *
  30. * The position of the tabs relative to the content varies based on
  31. * the mode. The tabs are placed at the bottom of the screen
  32. * for iOS and Android, and at the top for Windows by default. The position can
  33. * be configured using the `tabsPlacement` attribute on the `<ion-tabs>` component,
  34. * or in an app's [config](../../config/Config/).
  35. * See the [Input Properties](#input-properties) below for the available
  36. * values of `tabsPlacement`.
  37. *
  38. * ### Layout
  39. *
  40. * The layout for all of the tabs can be defined using the `tabsLayout`
  41. * property. If the individual tab has a title and icon, the icons will
  42. * show on top of the title by default. All tabs can be changed by setting
  43. * the value of `tabsLayout` on the `<ion-tabs>` element, or in your
  44. * app's [config](../../config/Config/). For example, this is useful if
  45. * you want to show tabs with a title only on Android, but show icons
  46. * and a title for iOS. See the [Input Properties](#input-properties)
  47. * below for the available values of `tabsLayout`.
  48. *
  49. * ### Selecting a Tab
  50. *
  51. * There are different ways you can select a specific tab from the tabs
  52. * component. You can use the `selectedIndex` property to set the index
  53. * on the `<ion-tabs>` element, or you can call `select()` from the `Tabs`
  54. * instance after creation. See [usage](#usage) below for more information.
  55. *
  56. * @usage
  57. *
  58. * You can add a basic tabs template to a `@Component` using the following
  59. * template:
  60. *
  61. * ```html
  62. * <ion-tabs>
  63. * <ion-tab [root]="tab1Root"></ion-tab>
  64. * <ion-tab [root]="tab2Root"></ion-tab>
  65. * <ion-tab [root]="tab3Root"></ion-tab>
  66. * </ion-tabs>
  67. * ```
  68. *
  69. * Where `tab1Root`, `tab2Root`, and `tab3Root` are each a page:
  70. *
  71. *```ts
  72. * @Component({
  73. * templateUrl: 'build/pages/tabs/tabs.html'
  74. * })
  75. * export class TabsPage {
  76. * // this tells the tabs component which Pages
  77. * // should be each tab's root Page
  78. * tab1Root = Page1;
  79. * tab2Root = Page2;
  80. * tab3Root = Page3;
  81. *
  82. * constructor() {
  83. *
  84. * }
  85. * }
  86. *```
  87. *
  88. * By default, the first tab will be selected upon navigation to the
  89. * Tabs page. We can change the selected tab by using `selectedIndex`
  90. * on the `<ion-tabs>` element:
  91. *
  92. * ```html
  93. * <ion-tabs selectedIndex="2">
  94. * <ion-tab [root]="tab1Root"></ion-tab>
  95. * <ion-tab [root]="tab2Root"></ion-tab>
  96. * <ion-tab [root]="tab3Root"></ion-tab>
  97. * </ion-tabs>
  98. * ```
  99. *
  100. * Since the index starts at `0`, this will select the 3rd tab which has
  101. * root set to `tab3Root`. If you wanted to change it dynamically from
  102. * your class, you could use [property binding](https://angular.io/docs/ts/latest/guide/template-syntax.html#!#property-binding).
  103. *
  104. * Alternatively, you can grab the `Tabs` instance and call the `select()`
  105. * method. This requires the `<ion-tabs>` element to have an `id`. For
  106. * example, set the value of `id` to `myTabs`:
  107. *
  108. * ```html
  109. * <ion-tabs #myTabs>
  110. * <ion-tab [root]="tab1Root"></ion-tab>
  111. * <ion-tab [root]="tab2Root"></ion-tab>
  112. * <ion-tab [root]="tab3Root"></ion-tab>
  113. * </ion-tabs>
  114. * ```
  115. *
  116. * Then in your class you can grab the `Tabs` instance and call `select()`,
  117. * passing the index of the tab as the argument. Here we're grabbing the tabs
  118. * by using ViewChild.
  119. *
  120. *```ts
  121. * export class TabsPage {
  122. *
  123. * @ViewChild('myTabs') tabRef: Tabs;
  124. *
  125. * ionViewDidEnter() {
  126. * this.tabRef.select(2);
  127. * }
  128. *
  129. * }
  130. *```
  131. *
  132. * You can also switch tabs from a child component by calling `select()` on the
  133. * parent view using the `NavController` instance. For example, assuming you have
  134. * a `TabsPage` component, you could call the following from any of the child
  135. * components to switch to `TabsRoot3`:
  136. *
  137. *```ts
  138. * switchTabs() {
  139. * this.navCtrl.parent.select(2);
  140. * }
  141. *```
  142. * @demo /docs/demos/src/tabs/
  143. *
  144. * @see {@link /docs/components#tabs Tabs Component Docs}
  145. * @see {@link ../Tab Tab API Docs}
  146. * @see {@link ../../config/Config Config API Docs}
  147. *
  148. */
  149. export class Tabs extends Ion {
  150. constructor(parent, viewCtrl, _app, config, elementRef, _plt, renderer, _linker, keyboard) {
  151. super(config, elementRef, renderer, 'tabs');
  152. this.viewCtrl = viewCtrl;
  153. this._app = _app;
  154. this._plt = _plt;
  155. this._linker = _linker;
  156. /** @internal */
  157. this._ids = -1;
  158. /** @internal */
  159. this._tabs = [];
  160. /** @internal */
  161. this._selectHistory = [];
  162. /** @internal */
  163. this._onDestroy = new Subject();
  164. /**
  165. * @output {any} Emitted when the tab changes.
  166. */
  167. this.ionChange = new EventEmitter();
  168. this.parent = parent;
  169. this.id = 't' + (++tabIds);
  170. this._sbPadding = config.getBoolean('statusbarPadding');
  171. this.tabsHighlight = config.getBoolean('tabsHighlight');
  172. if (this.parent) {
  173. // this Tabs has a parent Nav
  174. this.parent.registerChildNav(this);
  175. }
  176. else if (viewCtrl && viewCtrl.getNav()) {
  177. // this Nav was opened from a modal
  178. this.parent = viewCtrl.getNav();
  179. this.parent.registerChildNav(this);
  180. }
  181. else if (this._app) {
  182. // this is the root navcontroller for the entire app
  183. this._app.registerRootNav(this);
  184. }
  185. // Tabs may also be an actual ViewController which was navigated to
  186. // if Tabs is static and not navigated to within a NavController
  187. // then skip this and don't treat it as it's own ViewController
  188. if (viewCtrl) {
  189. viewCtrl._setContent(this);
  190. viewCtrl._setContentRef(elementRef);
  191. }
  192. const keyboardResizes = config.getBoolean('keyboardResizes', false);
  193. if (keyboard && keyboardResizes) {
  194. keyboard.willHide
  195. .takeUntil(this._onDestroy)
  196. .subscribe(() => {
  197. this._plt.timeout(() => this.setTabbarHidden(false), 50);
  198. });
  199. keyboard.willShow
  200. .takeUntil(this._onDestroy)
  201. .subscribe(() => this.setTabbarHidden(true));
  202. }
  203. }
  204. /**
  205. * @internal
  206. */
  207. setTabbarHidden(tabbarHidden) {
  208. this.setElementClass('tabbar-hidden', tabbarHidden);
  209. this.resize();
  210. }
  211. /**
  212. * @internal
  213. */
  214. ngOnDestroy() {
  215. this._onDestroy.next();
  216. if (this.parent) {
  217. this.parent.unregisterChildNav(this);
  218. }
  219. else {
  220. this._app.unregisterRootNav(this);
  221. }
  222. }
  223. /**
  224. * @internal
  225. */
  226. ngAfterViewInit() {
  227. this._setConfig('tabsPlacement', 'bottom');
  228. this._setConfig('tabsLayout', 'icon-top');
  229. this._setConfig('tabsHighlight', this.tabsHighlight);
  230. if (this.tabsHighlight) {
  231. this._plt.resize
  232. .takeUntil(this._onDestroy)
  233. .subscribe(() => this._highlight.select(this.getSelected()));
  234. }
  235. this.initTabs();
  236. }
  237. /**
  238. * @internal
  239. */
  240. initTabs() {
  241. // get the selected index from the input
  242. // otherwise default it to use the first index
  243. let selectedIndex = (isBlank(this.selectedIndex) ? 0 : parseInt(this.selectedIndex, 10));
  244. // now see if the deep linker can find a tab index
  245. const tabsSegment = this._linker.getSegmentByNavIdOrName(this.id, this.name);
  246. if (tabsSegment) {
  247. // we found a segment which probably represents which tab to select
  248. selectedIndex = this._getSelectedTabIndex(tabsSegment.secondaryId, selectedIndex);
  249. }
  250. // get the selectedIndex and ensure it isn't hidden or disabled
  251. let selectedTab = this._tabs.find((t, i) => i === selectedIndex && t.enabled && t.show);
  252. if (!selectedTab) {
  253. // wasn't able to select the tab they wanted
  254. // try to find the first tab that's available
  255. selectedTab = this._tabs.find(t => t.enabled && t.show);
  256. }
  257. let promise = Promise.resolve();
  258. if (selectedTab) {
  259. selectedTab._segment = tabsSegment;
  260. promise = this.select(selectedTab);
  261. }
  262. return promise.then(() => {
  263. // set the initial href attribute values for each tab
  264. this._tabs.forEach(t => {
  265. t.updateHref(t.root, t.rootParams);
  266. });
  267. });
  268. }
  269. /**
  270. * @internal
  271. */
  272. _setConfig(attrKey, fallback) {
  273. let val = this[attrKey];
  274. if (isBlank(val)) {
  275. val = this._config.get(attrKey, fallback);
  276. }
  277. this.setElementAttribute(attrKey, val);
  278. }
  279. /**
  280. * @hidden
  281. */
  282. add(tab) {
  283. this._tabs.push(tab);
  284. return this.id + '-' + (++this._ids);
  285. }
  286. /**
  287. * @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select.
  288. */
  289. select(tabOrIndex, opts = {}, fromUrl = false) {
  290. const selectedTab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex);
  291. if (isBlank(selectedTab)) {
  292. return Promise.resolve();
  293. }
  294. // If the selected tab is the current selected tab, we do not switch
  295. const currentTab = this.getSelected();
  296. if (selectedTab === currentTab && currentTab.getActive()) {
  297. return this._updateCurrentTab(selectedTab, fromUrl);
  298. }
  299. // If the selected tab does not have a root, we do not switch (#9392)
  300. // it's possible the tab is only for opening modal's or signing out
  301. // and doesn't actually have content. In the case there's no content
  302. // for a tab then do nothing and leave the current view as is
  303. if (selectedTab.root) {
  304. // At this point we are going to perform a page switch
  305. // Let's fire willLeave in the current tab page
  306. var currentPage;
  307. if (currentTab) {
  308. currentPage = currentTab.getActive();
  309. currentPage && currentPage._willLeave(false);
  310. }
  311. // Fire willEnter in the new selected tab
  312. const selectedPage = selectedTab.getActive();
  313. selectedPage && selectedPage._willEnter();
  314. // Let's start the transition
  315. opts.animate = false;
  316. return selectedTab.load(opts).then(() => {
  317. this._tabSwitchEnd(selectedTab, selectedPage, currentPage);
  318. if (opts.updateUrl !== false) {
  319. this._linker.navChange(DIRECTION_SWITCH);
  320. }
  321. (void 0) /* assert */;
  322. this._fireChangeEvent(selectedTab);
  323. });
  324. }
  325. else {
  326. this._fireChangeEvent(selectedTab);
  327. return Promise.resolve();
  328. }
  329. }
  330. _fireChangeEvent(selectedTab) {
  331. selectedTab.ionSelect.emit(selectedTab);
  332. this.ionChange.emit(selectedTab);
  333. }
  334. _tabSwitchEnd(selectedTab, selectedPage, currentPage) {
  335. (void 0) /* assert */;
  336. (void 0) /* assert */;
  337. // Update tabs selection state
  338. const tabs = this._tabs;
  339. let tab;
  340. for (var i = 0; i < tabs.length; i++) {
  341. tab = tabs[i];
  342. tab.setSelected(tab === selectedTab);
  343. }
  344. if (this.tabsHighlight) {
  345. this._highlight.select(selectedTab);
  346. }
  347. // Fire didEnter/didLeave lifecycle events
  348. if (selectedPage) {
  349. selectedPage._didEnter();
  350. this._app.viewDidEnter.emit(selectedPage);
  351. }
  352. if (currentPage) {
  353. currentPage && currentPage._didLeave();
  354. this._app.viewDidLeave.emit(currentPage);
  355. }
  356. // track the order of which tabs have been selected, by their index
  357. // do not track if the tab index is the same as the previous
  358. if (this._selectHistory[this._selectHistory.length - 1] !== selectedTab.id) {
  359. this._selectHistory.push(selectedTab.id);
  360. }
  361. }
  362. /**
  363. * Get the previously selected Tab which is currently not disabled or hidden.
  364. * @param {boolean} trimHistory If the selection history should be trimmed up to the previous tab selection or not.
  365. * @returns {Tab}
  366. */
  367. previousTab(trimHistory = true) {
  368. // walk backwards through the tab selection history
  369. // and find the first previous tab that is enabled and shown
  370. (void 0) /* console.debug */;
  371. for (var i = this._selectHistory.length - 2; i >= 0; i--) {
  372. var tab = this._tabs.find(t => t.id === this._selectHistory[i]);
  373. if (tab && tab.enabled && tab.show) {
  374. if (trimHistory) {
  375. this._selectHistory.splice(i + 1);
  376. }
  377. return tab;
  378. }
  379. }
  380. return null;
  381. }
  382. /**
  383. * @param {number} index Index of the tab you want to get
  384. * @returns {Tab} Returns the tab who's index matches the one passed
  385. */
  386. getByIndex(index) {
  387. return this._tabs[index];
  388. }
  389. /**
  390. * @return {Tab} Returns the currently selected tab
  391. */
  392. getSelected() {
  393. const tabs = this._tabs;
  394. for (var i = 0; i < tabs.length; i++) {
  395. if (tabs[i].isSelected) {
  396. return tabs[i];
  397. }
  398. }
  399. return null;
  400. }
  401. /**
  402. * @internal
  403. */
  404. getActiveChildNavs() {
  405. const selected = this.getSelected();
  406. return selected ? [selected] : [];
  407. }
  408. /**
  409. * @internal
  410. */
  411. getAllChildNavs() {
  412. return this._tabs;
  413. }
  414. /**
  415. * @internal
  416. */
  417. getIndex(tab) {
  418. return this._tabs.indexOf(tab);
  419. }
  420. /**
  421. * @internal
  422. */
  423. length() {
  424. return this._tabs.length;
  425. }
  426. /**
  427. * "Touch" the active tab, going back to the root view of the tab
  428. * or optionally letting the tab handle the event
  429. */
  430. _updateCurrentTab(tab, fromUrl) {
  431. const active = tab.getActive();
  432. if (active) {
  433. if (fromUrl && tab._segment) {
  434. // see if the view controller exists
  435. const vc = tab.getViewById(tab._segment.name);
  436. if (vc) {
  437. // the view is already in the stack
  438. return tab.popTo(vc, {
  439. animate: false,
  440. updateUrl: false,
  441. });
  442. }
  443. else if (tab._views.length === 0 && tab._segment.defaultHistory && tab._segment.defaultHistory.length) {
  444. return this._linker.initViews(tab._segment).then((views) => {
  445. return tab.setPages(views, {
  446. animate: false, updateUrl: false
  447. });
  448. }).then(() => {
  449. tab._segment = null;
  450. });
  451. }
  452. else {
  453. return tab.setRoot(tab._segment.name, tab._segment.data, {
  454. animate: false, updateUrl: false
  455. }).then(() => {
  456. tab._segment = null;
  457. });
  458. }
  459. }
  460. else if (active._cmp && active._cmp.instance.ionSelected) {
  461. // if they have a custom tab selected handler, call it
  462. active._cmp.instance.ionSelected();
  463. return Promise.resolve();
  464. }
  465. else if (tab.length() > 1) {
  466. // if we're a few pages deep, pop to root
  467. return tab.popToRoot();
  468. }
  469. else {
  470. return getComponent(this._linker, tab.root).then(viewController => {
  471. if (viewController.component !== active.component) {
  472. // Otherwise, if the page we're on is not our real root
  473. // reset it to our default root type
  474. return tab.setRoot(tab.root);
  475. }
  476. }).catch(() => {
  477. (void 0) /* console.debug */;
  478. });
  479. }
  480. }
  481. }
  482. /**
  483. * @internal
  484. * DOM WRITE
  485. */
  486. setTabbarPosition(top, bottom) {
  487. if (this._top !== top || this._bottom !== bottom) {
  488. var tabbarEle = this._tabbar.nativeElement;
  489. tabbarEle.style.top = (top > -1 ? top + 'px' : '');
  490. tabbarEle.style.bottom = (bottom > -1 ? bottom + 'px' : '');
  491. tabbarEle.classList.add('show-tabbar');
  492. this._top = top;
  493. this._bottom = bottom;
  494. }
  495. }
  496. /**
  497. * @internal
  498. */
  499. resize() {
  500. const tab = this.getSelected();
  501. tab && tab.resize();
  502. }
  503. /**
  504. * @internal
  505. */
  506. initPane() {
  507. const isMain = this._elementRef.nativeElement.hasAttribute('main');
  508. return isMain;
  509. }
  510. /**
  511. * @internal
  512. */
  513. paneChanged(isPane) {
  514. if (isPane) {
  515. this.resize();
  516. }
  517. }
  518. goToRoot(opts) {
  519. if (this._tabs.length) {
  520. return this.select(this._tabs[0], opts);
  521. }
  522. }
  523. /*
  524. * @private
  525. */
  526. getType() {
  527. return 'tabs';
  528. }
  529. /*
  530. * @private
  531. */
  532. getSecondaryIdentifier() {
  533. const tabs = this.getActiveChildNavs();
  534. if (tabs && tabs.length) {
  535. return this._linker._getTabSelector(tabs[0]);
  536. }
  537. return '';
  538. }
  539. /**
  540. * @private
  541. */
  542. _getSelectedTabIndex(secondaryId = '', fallbackIndex = 0) {
  543. // we found a segment which probably represents which tab to select
  544. const indexMatch = secondaryId.match(/tab-(\d+)/);
  545. if (indexMatch) {
  546. // awesome, the segment name was something "tab-0", and
  547. // the numbe represents which tab to select
  548. return parseInt(indexMatch[1], 10);
  549. }
  550. // wasn't in the "tab-0" format so maybe it's using a word
  551. const tab = this._tabs.find(t => {
  552. return (isPresent(t.tabUrlPath) && t.tabUrlPath === secondaryId) ||
  553. (isPresent(t.tabTitle) && formatUrlPart(t.tabTitle) === secondaryId);
  554. });
  555. return isPresent(tab) ? tab.index : fallbackIndex;
  556. }
  557. }
  558. Tabs.decorators = [
  559. { type: Component, args: [{
  560. selector: 'ion-tabs',
  561. template: '<div class="tabbar" role="tablist" #tabbar>' +
  562. '<a *ngFor="let t of _tabs" [tab]="t" class="tab-button" role="tab" href="#" (ionSelect)="select(t)"></a>' +
  563. '<div class="tab-highlight"></div>' +
  564. '</div>' +
  565. '<ng-content></ng-content>' +
  566. '<div #portal tab-portal></div>',
  567. encapsulation: ViewEncapsulation.None,
  568. providers: [{ provide: RootNode, useExisting: forwardRef(() => Tabs) }]
  569. },] },
  570. ];
  571. /** @nocollapse */
  572. Tabs.ctorParameters = () => [
  573. { type: NavController, decorators: [{ type: Optional },] },
  574. { type: ViewController, decorators: [{ type: Optional },] },
  575. { type: App, },
  576. { type: Config, },
  577. { type: ElementRef, },
  578. { type: Platform, },
  579. { type: Renderer, },
  580. { type: DeepLinker, },
  581. { type: Keyboard, },
  582. ];
  583. Tabs.propDecorators = {
  584. 'name': [{ type: Input },],
  585. 'selectedIndex': [{ type: Input },],
  586. 'tabsLayout': [{ type: Input },],
  587. 'tabsPlacement': [{ type: Input },],
  588. 'tabsHighlight': [{ type: Input },],
  589. 'ionChange': [{ type: Output },],
  590. '_highlight': [{ type: ViewChild, args: [TabHighlight,] },],
  591. '_tabbar': [{ type: ViewChild, args: ['tabbar',] },],
  592. 'portal': [{ type: ViewChild, args: ['portal', { read: ViewContainerRef },] },],
  593. };
  594. let tabIds = -1;
  595. //# sourceMappingURL=tabs.js.map