// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
Component, Input, Output, EventEmitter, OnInit, OnChanges, OnDestroy, AfterViewInit, ViewChild, ElementRef,
SimpleChange
} from '@angular/core';
import { CoreTabComponent } from './tab';
import { Content, Slides } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
* This component displays some tabs that usually share data between them.
*
* If your tabs don't share any data then you should probably use ion-tabs. This component doesn't use different ion-nav
* for each tab, so it will not load pages.
*
* Example usage:
*
*
*
*
*
*
*
*
*
* Obviously, the tab contents will only be shown if that tab is selected.
*/
@Component({
selector: 'core-tabs',
templateUrl: 'core-tabs.html'
})
export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
@Input() selectedIndex = 0; // Index of the tab to select.
@Input() hideUntil = true; // Determine when should the contents be shown.
@Input() parentScrollable = false; // Determine if the scroll should be in the parent content or the tab itself.
@Output() ionChange: EventEmitter = new EventEmitter(); // Emitted when the tab changes.
@ViewChild('originalTabs') originalTabsRef: ElementRef;
@ViewChild('topTabs') topTabs: ElementRef;
@ViewChild(Slides) slides: Slides;
tabs: CoreTabComponent[] = []; // List of tabs.
selected: number; // Selected tab number.
showPrevButton: boolean;
showNextButton: boolean;
maxSlides = 3;
slidesShown = this.maxSlides;
numTabsShown = 0;
protected originalTabsContainer: HTMLElement; // The container of the original tabs. It will include each tab's content.
protected initialized = false;
protected afterViewInitTriggered = false;
protected topTabsElement: HTMLElement; // The container of the original tabs. It will include each tab's content.
protected tabBarHeight;
protected tabBarElement: HTMLElement; // Host element.
protected tabsShown = true;
protected resizeFunction;
protected isDestroyed = false;
protected isCurrentView = true;
protected shouldSlideToInitial = false; // Whether we need to slide to the initial slide because it's out of view.
protected hasSliddenToInitial = false; // Whether we've already slidden to the initial slide or there was no need.
constructor(element: ElementRef, protected content: Content, protected domUtils: CoreDomUtilsProvider) {
this.tabBarElement = element.nativeElement;
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.originalTabsContainer = this.originalTabsRef.nativeElement;
this.topTabsElement = this.topTabs.nativeElement;
}
/**
* View has been initialized.
*/
ngAfterViewInit(): void {
if (this.isDestroyed) {
return;
}
this.afterViewInitTriggered = true;
if (!this.initialized && this.hideUntil) {
// Tabs should be shown, initialize them.
this.initializeTabs();
}
this.resizeFunction = this.calculateSlides.bind(this);
window.addEventListener('resize', this.resizeFunction);
}
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
// We need to wait for ngAfterViewInit because we need core-tab components to be executed.
if (!this.initialized && this.hideUntil && this.afterViewInitTriggered) {
// Tabs should be shown, initialize them.
// Use a setTimeout so child core-tab update their inputs before initializing the tabs.
setTimeout(() => {
this.initializeTabs();
});
}
}
/**
* User entered the page that contains the component.
*/
ionViewDidEnter(): void {
this.isCurrentView = true;
if (this.initialized) {
this.calculateSlides();
}
}
/**
* User left the page that contains the component.
*/
ionViewDidLeave(): void {
this.isCurrentView = false;
}
/**
* Add a new tab if it isn't already in the list of tabs.
*
* @param {CoreTabComponent} tab The tab to add.
*/
addTab(tab: CoreTabComponent): void {
// Check if tab is already in the list.
if (this.getIndex(tab) == -1) {
this.tabs.push(tab);
this.sortTabs();
this.calculateSlides();
if (this.initialized && this.tabs.length > 1 && this.tabBarHeight == 0) {
// Calculate the tabBarHeight again now that there is more than 1 tab and the bar will be seen.
// Use timeout to wait for the view to be rendered. 0 ms should be enough, use 50 to be sure.
setTimeout(() => {
this.calculateTabBarHeight();
}, 50);
}
}
}
/**
* Calculate slides.
*/
calculateSlides(): void {
if (!this.isCurrentView || !this.tabsShown) {
// Don't calculate if component isn't in current view, the calculations are wrong.
return;
}
setTimeout(() => {
this.calculateMaxSlides();
this.updateSlides();
});
}
/**
* Calculate the tab bar height.
*/
calculateTabBarHeight(): void {
this.tabBarHeight = this.topTabsElement.offsetHeight;
this.originalTabsContainer.style.paddingBottom = this.tabBarHeight + 'px';
}
/**
* Get the index of tab.
*
* @param {any} tab [description]
* @return {number} [description]
*/
getIndex(tab: any): number {
for (let i = 0; i < this.tabs.length; i++) {
const t = this.tabs[i];
if (t === tab || (typeof t.id != 'undefined' && t.id === tab.id)) {
return i;
}
}
return -1;
}
/**
* Get the current selected tab.
*
* @return {CoreTabComponent} Selected tab.
*/
getSelected(): CoreTabComponent {
return this.tabs[this.selected];
}
/**
* Initialize the tabs, determining the first tab to be shown.
*/
protected initializeTabs(): void {
let selectedIndex = this.selectedIndex || 0,
selectedTab = this.tabs[selectedIndex];
if (!selectedTab || !selectedTab.enabled || !selectedTab.show) {
// The tab is not enabled or not shown. Get the first tab that is enabled.
selectedTab = this.tabs.find((tab, index) => {
if (tab.enabled && tab.show) {
selectedIndex = index;
return true;
}
return false;
});
}
if (selectedTab) {
this.selectTab(selectedIndex);
}
// Setup tab scrolling.
this.calculateTabBarHeight();
if (this.content) {
if (!this.parentScrollable) {
// Parent scroll element (if core-tabs is inside a ion-content).
const scroll = this.content.getScrollElement();
if (scroll) {
scroll.classList.add('no-scroll');
}
} else {
this.originalTabsContainer.classList.add('no-scroll');
}
}
// Check which arrows should be shown.
this.calculateSlides();
this.initialized = true;
}
/**
* Method executed when the slides are changed.
*/
slideChanged(): void {
const currentIndex = this.slides.getActiveIndex();
if (this.slidesShown >= this.numTabsShown) {
this.showPrevButton = false;
this.showNextButton = false;
} else if (typeof currentIndex !== 'undefined') {
this.showPrevButton = currentIndex > 0;
this.showNextButton = currentIndex < this.numTabsShown - this.slidesShown;
} else {
this.showPrevButton = false;
this.showNextButton = this.numTabsShown > this.slidesShown;
}
if (this.shouldSlideToInitial && currentIndex != this.selected) {
// Current tab has changed, don't slide to initial anymore.
this.shouldSlideToInitial = false;
}
}
/**
* Update slides.
*/
protected updateSlides(): void {
this.numTabsShown = this.tabs.reduce((prev: number, current: any) => {
return current.show ? prev + 1 : prev;
}, 0);
this.slidesShown = Math.min(this.maxSlides, this.numTabsShown);
this.slideChanged();
setTimeout(() => {
this.slides.update();
this.slides.resize();
if (!this.hasSliddenToInitial && this.selected && this.selected >= this.slidesShown) {
this.hasSliddenToInitial = true;
this.shouldSlideToInitial = true;
setTimeout(() => {
if (this.shouldSlideToInitial) {
this.slides.slideTo(this.selected, 0);
this.shouldSlideToInitial = false;
}
}, 400);
} else if (this.selected) {
this.hasSliddenToInitial = true;
}
});
}
protected calculateMaxSlides(): void {
if (this.slides) {
const width = this.domUtils.getElementWidth(this.slides.getNativeElement()) || this.slides.renderedWidth;
if (width) {
this.maxSlides = Math.floor(width / 120);
return;
}
}
this.maxSlides = 3;
}
/**
* Method that shows the next slide.
*/
slideNext(): void {
if (this.showNextButton) {
this.slides.slideNext();
}
}
/**
* Method that shows the previous slide.
*/
slidePrev(): void {
if (this.showPrevButton) {
this.slides.slidePrev();
}
}
/**
* Show or hide the tabs. This is used when the user is scrolling inside a tab.
*
* @param {any} e Scroll event.
*/
showHideTabs(e: any): void {
if (!this.tabBarHeight) {
// We don't have the tab bar height, this means the tab bar isn't shown.
return;
}
if (this.tabsShown && e.target.scrollTop - this.tabBarHeight > this.tabBarHeight) {
this.tabBarElement.classList.add('tabs-hidden');
this.tabsShown = false;
} else if (!this.tabsShown && e.target.scrollTop < this.tabBarHeight) {
this.tabBarElement.classList.remove('tabs-hidden');
this.tabsShown = true;
this.calculateSlides();
}
}
/**
* Remove a tab from the list of tabs.
*
* @param {CoreTabComponent} tab The tab to remove.
*/
removeTab(tab: CoreTabComponent): void {
const index = this.getIndex(tab);
this.tabs.splice(index, 1);
this.calculateSlides();
}
/**
* Select a certain tab.
*
* @param {number} index The index of the tab to select.
*/
selectTab(index: number): void {
if (index == this.selected) {
// Already selected.
return;
}
if (index < 0 || index >= this.tabs.length) {
// Index isn't valid, select the first one.
index = 0;
}
const currentTab = this.getSelected(),
newTab = this.tabs[index];
if (!newTab.enabled || !newTab.show) {
// The tab isn't enabled or shown, stop.
return;
}
if (currentTab) {
// Unselect previous selected tab.
currentTab.unselectTab();
}
if (this.selected) {
this.slides.slideTo(index);
}
this.selected = index;
newTab.selectTab();
this.ionChange.emit(newTab);
}
/**
* Sort the tabs, keeping the same order as in the original list.
*/
protected sortTabs(): void {
if (this.originalTabsContainer) {
const newTabs = [];
this.tabs.forEach((tab, index) => {
const originalIndex = Array.prototype.indexOf.call(this.originalTabsContainer.children, tab.element);
if (originalIndex != -1) {
newTabs[originalIndex] = tab;
}
});
this.tabs = newTabs;
}
}
/**
* Function to call when the visibility of a tab has changed.
*/
tabVisibilityChanged(): void {
this.calculateSlides();
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
if (this.resizeFunction) {
window.removeEventListener('resize', this.resizeFunction);
}
}
}