mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +08:00
583 lines
15 KiB
TypeScript
583 lines
15 KiB
TypeScript
import { ComponentRef, ElementRef, EventEmitter, Output, Renderer } from '@angular/core';
|
|
|
|
import { isPresent, assert } from '../util/util';
|
|
import { NavController } from './nav-controller';
|
|
import { NavOptions, STATE_NEW, STATE_INITIALIZED, STATE_ATTACHED, STATE_DESTROYED } from './nav-util';
|
|
import { NavParams } from './nav-params';
|
|
import { Content, Footer, Header, Navbar } from './nav-interfaces';
|
|
|
|
|
|
/**
|
|
* @name ViewController
|
|
* @description
|
|
* Access various features and information about the current view.
|
|
* @usage
|
|
* ```ts
|
|
* import { Component } from '@angular/core';
|
|
* import { ViewController } from 'ionic-angular';
|
|
*
|
|
* @Component({...})
|
|
* export class MyPage{
|
|
*
|
|
* constructor(public viewCtrl: ViewController) {}
|
|
*
|
|
* }
|
|
* ```
|
|
*/
|
|
export class ViewController {
|
|
|
|
private _cntDir: any;
|
|
private _cntRef: ElementRef;
|
|
private _ionCntDir: Content;
|
|
private _ionCntRef: ElementRef;
|
|
private _hdrDir: Header;
|
|
private _ftrDir: Footer;
|
|
private _isHidden: boolean = false;
|
|
private _leavingOpts: NavOptions;
|
|
private _nb: Navbar;
|
|
private _onDidDismiss: (data: any, role: string) => void;
|
|
private _onWillDismiss: (data: any, role: string) => void;
|
|
private _dismissData: any;
|
|
private _dismissRole: string;
|
|
private _detached: boolean;
|
|
|
|
_cmp: ComponentRef<any>;
|
|
_nav: NavController;
|
|
_zIndex: number;
|
|
_state: number = STATE_NEW;
|
|
_cssClass: string;
|
|
|
|
/**
|
|
* Observable to be subscribed to when the current component will become active
|
|
* @returns {Observable} Returns an observable
|
|
*/
|
|
willEnter: EventEmitter<any> = new EventEmitter();
|
|
|
|
/**
|
|
* Observable to be subscribed to when the current component has become active
|
|
* @returns {Observable} Returns an observable
|
|
*/
|
|
didEnter: EventEmitter<any> = new EventEmitter();
|
|
|
|
/**
|
|
* Observable to be subscribed to when the current component will no longer be active
|
|
* @returns {Observable} Returns an observable
|
|
*/
|
|
willLeave: EventEmitter<any> = new EventEmitter();
|
|
|
|
/**
|
|
* Observable to be subscribed to when the current component is no long active
|
|
* @returns {Observable} Returns an observable
|
|
*/
|
|
didLeave: EventEmitter<any> = new EventEmitter();
|
|
|
|
/**
|
|
* Observable to be subscribed to when the current component has been destroyed
|
|
* @returns {Observable} Returns an observable
|
|
*/
|
|
willUnload: EventEmitter<any> = new EventEmitter();
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
readReady: EventEmitter<any> = new EventEmitter<any>();
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
writeReady: EventEmitter<any> = new EventEmitter<any>();
|
|
|
|
/** @hidden */
|
|
data: any;
|
|
|
|
/** @hidden */
|
|
instance: any;
|
|
|
|
/** @hidden */
|
|
id: string;
|
|
|
|
/** @hidden */
|
|
isOverlay: boolean = false;
|
|
|
|
/** @hidden */
|
|
@Output() private _emitter: EventEmitter<any> = new EventEmitter();
|
|
|
|
constructor(
|
|
public component?: any,
|
|
data?: any,
|
|
rootCssClass: string = DEFAULT_CSS_CLASS
|
|
) {
|
|
// passed in data could be NavParams, but all we care about is its data object
|
|
this.data = (data instanceof NavParams ? data.data : (isPresent(data) ? data : {}));
|
|
|
|
this._cssClass = rootCssClass;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
init(componentRef: ComponentRef<any>) {
|
|
assert(componentRef, 'componentRef can not be null');
|
|
|
|
this._cmp = componentRef;
|
|
this.instance = this.instance || componentRef.instance;
|
|
this._detached = false;
|
|
}
|
|
|
|
_setNav(navCtrl: NavController) {
|
|
this._nav = navCtrl;
|
|
}
|
|
|
|
_setInstance(instance: any) {
|
|
this.instance = instance;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
subscribe(generatorOrNext?: any): any {
|
|
return this._emitter.subscribe(generatorOrNext);
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
emit(data?: any) {
|
|
this._emitter.emit(data);
|
|
}
|
|
|
|
/**
|
|
* Called when the current viewController has be successfully dismissed
|
|
*/
|
|
onDidDismiss(callback: (data: any, role: string) => void) {
|
|
this._onDidDismiss = callback;
|
|
}
|
|
|
|
/**
|
|
* Called when the current viewController will be dismissed
|
|
*/
|
|
onWillDismiss(callback: (data: any, role: string) => void) {
|
|
this._onWillDismiss = callback;
|
|
}
|
|
|
|
/**
|
|
* Dismiss the current viewController
|
|
* @param {any} [data] Data that you want to return when the viewController is dismissed.
|
|
* @param {any} [role ]
|
|
* @param {NavOptions} navOptions Options for the dismiss navigation.
|
|
* @returns {any} data Returns the data passed in, if any.
|
|
*/
|
|
dismiss(data?: any, role?: string, navOptions: NavOptions = {}): Promise<any> {
|
|
if (!this._nav) {
|
|
assert(this._state === STATE_DESTROYED, 'ViewController does not have a valid _nav');
|
|
return Promise.resolve(false);
|
|
}
|
|
if (this.isOverlay && !navOptions.minClickBlockDuration) {
|
|
// This is a Modal being dismissed so we need
|
|
// to add the minClickBlockDuration option
|
|
// for UIWebView
|
|
navOptions.minClickBlockDuration = 400;
|
|
}
|
|
this._dismissData = data;
|
|
this._dismissRole = role;
|
|
|
|
const options = Object.assign({}, this._leavingOpts, navOptions);
|
|
return this._nav.removeView(this, options).then(() => data);
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getNav(): NavController {
|
|
return this._nav;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getTransitionName(direction: string): string {
|
|
return this._nav && this._nav.config.get('pageTransition');
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getNavParams(): NavParams {
|
|
return new NavParams(this.data);
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
setLeavingOpts(opts: NavOptions) {
|
|
this._leavingOpts = opts;
|
|
}
|
|
|
|
/**
|
|
* Check to see if you can go back in the navigation stack.
|
|
* @returns {boolean} Returns if it's possible to go back from this Page.
|
|
*/
|
|
enableBack(): boolean {
|
|
// update if it's possible to go back from this nav item
|
|
if (!this._nav) {
|
|
return false;
|
|
}
|
|
// the previous view may exist, but if it's about to be destroyed
|
|
// it shouldn't be able to go back to
|
|
const previousItem = this._nav.getPrevious(this);
|
|
return !!(previousItem);
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
get name(): string {
|
|
return (this.component ? this.component.name : '');
|
|
}
|
|
|
|
/**
|
|
* Get the index of the current component in the current navigation stack.
|
|
* @returns {number} Returns the index of this page within its `NavController`.
|
|
*/
|
|
get index(): number {
|
|
return (this._nav ? this._nav.indexOf(this) : -1);
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean} Returns if this Page is the first in the stack of pages within its NavController.
|
|
*/
|
|
isFirst(): boolean {
|
|
return (this._nav ? this._nav.first() === this : false);
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean} Returns if this Page is the last in the stack of pages within its NavController.
|
|
*/
|
|
isLast(): boolean {
|
|
return (this._nav ? this._nav.last() === this : false);
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
* DOM WRITE
|
|
*/
|
|
_domShow(shouldShow: boolean, renderer: Renderer) {
|
|
// using hidden element attribute to display:none and not render views
|
|
// _hidden value of '' means the hidden attribute will be added
|
|
// _hidden value of null means the hidden attribute will be removed
|
|
// doing checks to make sure we only update the DOM when actually needed
|
|
// if it should render, then the hidden attribute should not be on the element
|
|
if (this._cmp && shouldShow === this._isHidden) {
|
|
this._isHidden = !shouldShow;
|
|
let value = (shouldShow ? null : '');
|
|
// ******** DOM WRITE ****************
|
|
renderer.setElementAttribute(this.pageRef().nativeElement, 'hidden', value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getZIndex(): number {
|
|
return this._zIndex;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
* DOM WRITE
|
|
*/
|
|
_setZIndex(zIndex: number, renderer: Renderer) {
|
|
if (zIndex !== this._zIndex) {
|
|
this._zIndex = zIndex;
|
|
const pageRef = this.pageRef();
|
|
if (pageRef) {
|
|
// ******** DOM WRITE ****************
|
|
renderer.setElementStyle(pageRef.nativeElement, 'z-index', (<any>zIndex));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {ElementRef} Returns the Page's ElementRef.
|
|
*/
|
|
pageRef(): ElementRef {
|
|
return this._cmp && this._cmp.location;
|
|
}
|
|
|
|
_setContent(directive: any) {
|
|
this._cntDir = directive;
|
|
}
|
|
|
|
/**
|
|
* @returns {component} Returns the Page's Content component reference.
|
|
*/
|
|
getContent(): any {
|
|
return this._cntDir;
|
|
}
|
|
|
|
_setContentRef(elementRef: ElementRef) {
|
|
this._cntRef = elementRef;
|
|
}
|
|
|
|
/**
|
|
* @returns {ElementRef} Returns the Content's ElementRef.
|
|
*/
|
|
contentRef(): ElementRef {
|
|
return this._cntRef;
|
|
}
|
|
|
|
_setIONContent(content: Content) {
|
|
this._setContent(content);
|
|
this._ionCntDir = content;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getIONContent(): Content {
|
|
return this._ionCntDir;
|
|
}
|
|
|
|
_setIONContentRef(elementRef: ElementRef) {
|
|
this._setContentRef(elementRef);
|
|
this._ionCntRef = elementRef;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getIONContentRef(): ElementRef {
|
|
return this._ionCntRef;
|
|
}
|
|
|
|
_setHeader(directive: Header) {
|
|
this._hdrDir = directive;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getHeader(): Header {
|
|
return this._hdrDir;
|
|
}
|
|
|
|
_setFooter(directive: Footer) {
|
|
this._ftrDir = directive;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getFooter(): Footer {
|
|
return this._ftrDir;
|
|
}
|
|
|
|
_setNavbar(directive: Navbar) {
|
|
this._nb = directive;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
getNavbar(): Navbar {
|
|
return this._nb;
|
|
}
|
|
|
|
/**
|
|
* Find out if the current component has a NavBar or not. Be sure
|
|
* to wrap this in an `ionViewWillEnter` method in order to make sure
|
|
* the view has rendered fully.
|
|
* @returns {boolean} Returns a boolean if this Page has a navbar or not.
|
|
*/
|
|
hasNavbar(): boolean {
|
|
return !!this._nb;
|
|
}
|
|
|
|
/**
|
|
* Change the title of the back-button. Be sure to call this
|
|
* after `ionViewWillEnter` to make sure the DOM has been rendered.
|
|
* @param {string} val Set the back button text.
|
|
*/
|
|
setBackButtonText(val: string) {
|
|
this._nb && this._nb.setBackButtonText(val);
|
|
}
|
|
|
|
/**
|
|
* Set if the back button for the current view is visible or not. Be sure to call this
|
|
* after `ionViewWillEnter` to make sure the DOM has been rendered.
|
|
* @param {boolean} Set if this Page's back button should show or not.
|
|
*/
|
|
showBackButton(shouldShow: boolean) {
|
|
if (this._nb) {
|
|
this._nb.hideBackButton = !shouldShow;
|
|
}
|
|
}
|
|
|
|
_preLoad() {
|
|
assert(this._state === STATE_INITIALIZED, 'view state must be INITIALIZED');
|
|
this._lifecycle('PreLoad');
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
* The view has loaded. This event only happens once per view will be created.
|
|
* This event is fired before the component and his children have been initialized.
|
|
*/
|
|
_willLoad() {
|
|
assert(this._state === STATE_INITIALIZED, 'view state must be INITIALIZED');
|
|
this._lifecycle('WillLoad');
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
* The view has loaded. This event only happens once per view being
|
|
* created. If a view leaves but is cached, then this will not
|
|
* fire again on a subsequent viewing. This method is a good place
|
|
* to put your setup code for the view; however, it is not the
|
|
* recommended method to use when a view becomes active.
|
|
*/
|
|
_didLoad() {
|
|
assert(this._state === STATE_ATTACHED, 'view state must be ATTACHED');
|
|
this._lifecycle('DidLoad');
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
* The view is about to enter and become the active view.
|
|
*/
|
|
_willEnter() {
|
|
assert(this._state === STATE_ATTACHED, 'view state must be ATTACHED');
|
|
|
|
if (this._detached && this._cmp) {
|
|
// ensure this has been re-attached to the change detector
|
|
this._cmp.changeDetectorRef.reattach();
|
|
this._detached = false;
|
|
}
|
|
|
|
this.willEnter.emit(null);
|
|
this._lifecycle('WillEnter');
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
* The view has fully entered and is now the active view. This
|
|
* will fire, whether it was the first load or loaded from the cache.
|
|
*/
|
|
_didEnter() {
|
|
assert(this._state === STATE_ATTACHED, 'view state must be ATTACHED');
|
|
|
|
this._nb && this._nb.didEnter();
|
|
this.didEnter.emit(null);
|
|
this._lifecycle('DidEnter');
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
* The view is about to leave and no longer be the active view.
|
|
*/
|
|
_willLeave(willUnload: boolean) {
|
|
this.willLeave.emit(null);
|
|
this._lifecycle('WillLeave');
|
|
|
|
if (willUnload && this._onWillDismiss) {
|
|
this._onWillDismiss(this._dismissData, this._dismissRole);
|
|
this._onWillDismiss = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
* The view has finished leaving and is no longer the active view. This
|
|
* will fire, whether it is cached or unloaded.
|
|
*/
|
|
_didLeave() {
|
|
this.didLeave.emit(null);
|
|
this._lifecycle('DidLeave');
|
|
|
|
// when this is not the active page
|
|
// we no longer need to detect changes
|
|
if (!this._detached && this._cmp) {
|
|
this._cmp.changeDetectorRef.detach();
|
|
this._detached = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
_willUnload() {
|
|
this.willUnload.emit(null);
|
|
this._lifecycle('WillUnload');
|
|
|
|
this._onDidDismiss && this._onDidDismiss(this._dismissData, this._dismissRole);
|
|
this._onDidDismiss = null;
|
|
this._dismissData = null;
|
|
this._dismissRole = null;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
* DOM WRITE
|
|
*/
|
|
_destroy(renderer: Renderer) {
|
|
assert(this._state !== STATE_DESTROYED, 'view state must be ATTACHED');
|
|
|
|
if (this._cmp) {
|
|
if (renderer) {
|
|
// ensure the element is cleaned up for when the view pool reuses this element
|
|
// ******** DOM WRITE ****************
|
|
var cmpEle = this._cmp.location.nativeElement;
|
|
renderer.setElementAttribute(cmpEle, 'class', null);
|
|
renderer.setElementAttribute(cmpEle, 'style', null);
|
|
}
|
|
|
|
// completely destroy this component. boom.
|
|
this._cmp.destroy();
|
|
}
|
|
|
|
this._nav = this._cmp = this.instance = this._cntDir = this._cntRef = this._leavingOpts = this._hdrDir = this._ftrDir = this._nb = this._onDidDismiss = this._onWillDismiss = null;
|
|
this._state = STATE_DESTROYED;
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
_lifecycleTest(lifecycle: string): Promise<boolean> {
|
|
const instance = this.instance;
|
|
const methodName = 'ionViewCan' + lifecycle;
|
|
if (instance && instance[methodName]) {
|
|
try {
|
|
var result = instance[methodName]();
|
|
if (result instanceof Promise) {
|
|
return result;
|
|
} else {
|
|
// Any value but explitic false, should be true
|
|
return Promise.resolve(result !== false);
|
|
}
|
|
|
|
} catch (e) {
|
|
return Promise.reject(`${this.name} ${methodName} error: ${e.message}`);
|
|
}
|
|
}
|
|
return Promise.resolve(true);
|
|
}
|
|
|
|
/**
|
|
* @hidden
|
|
*/
|
|
_lifecycle(lifecycle: string) {
|
|
const instance = this.instance;
|
|
const methodName = 'ionView' + lifecycle;
|
|
if (instance && instance[methodName]) {
|
|
instance[methodName]();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
export function isViewController(viewCtrl: any): boolean {
|
|
return !!(viewCtrl && (<ViewController>viewCtrl)._didLoad && (<ViewController>viewCtrl)._willUnload);
|
|
}
|
|
|
|
const DEFAULT_CSS_CLASS = 'ion-page';
|