Merge branch 'master' into 3.0

This commit is contained in:
Manu Mtz.-Almeida
2017-03-07 20:57:38 +01:00
46 changed files with 1402 additions and 160 deletions

View File

@ -351,6 +351,18 @@ export class Animation {
});
}
syncPlay() {
// If the animation was already invalidated (it did finish), do nothing
if (!this.plt) {
return;
}
const opts = { duration: 0 };
this._isAsync = false;
this._clearAsync();
this._playInit(opts);
this._playDomInspect(opts);
}
/**
* @private
* DOM WRITE
@ -569,7 +581,6 @@ export class Animation {
return true;
}
}
return false;
}
@ -589,7 +600,6 @@ export class Animation {
return true;
}
}
return false;
}

View File

@ -89,8 +89,9 @@ export class App {
runInDev(() => {
// During developement, navPop can be triggered by calling
if (!(<any>_plt.win())['HWBackButton']) {
(<any>_plt.win())['HWBackButton'] = () => {
const win = <any>_plt.win();
if (!win['HWBackButton']) {
win['HWBackButton'] = () => {
let p = this.goBack();
p && p.catch(() => console.debug('hardware go back cancelled'));
return p;
@ -227,6 +228,8 @@ export class App {
present(enteringView: ViewController, opts: NavOptions, appPortal?: number): Promise<any> {
const portal = this._appRoot._getPortal(appPortal);
// Set Nav must be set here in order to dimiss() work synchnously.
// TODO: move _setNav() to the earlier stages of NavController. _queueTrns()
enteringView._setNav(portal);
opts.keyboardClose = false;

View File

@ -255,4 +255,19 @@ export class E2EPage {
this.navCtrl.push('page2');
}, 500);
}
presentLoadingOpenDismiss() {
// debugger;
const loading = this.loadingCtrl.create({
content: 'Loading 1'
});
loading.present();
loading.dismiss();
const loading2 = this.loadingCtrl.create({
content: 'Loading 2'
});
loading2.present();
loading2.dismiss();
}
}

View File

@ -22,6 +22,7 @@
<button ion-button block (click)="presentLoadingMultiple()" color="danger">Multiple Loading</button>
<button ion-button block (click)="presentLoadingMultipleNav()" color="danger">Multiple Nav Loading</button>
<button ion-button block (click)="presentLoadingDismissNav()">Dismiss Page Change</button>
<button ion-button block (click)="presentLoadingOpenDismiss()">Open->Dismiss (x2)</button>
</ion-content>

View File

@ -309,6 +309,23 @@ export class MenuController {
removeArrayItem(this._menus, menu);
}
/**
* @private
*/
_setActiveMenu(menu: Menu) {
assert(menu.enabled, 'menu must be enabled');
assert(this._menus.indexOf(menu) >= 0, 'menu is not registered');
// if this menu should be enabled
// then find all the other menus on this same side
// and automatically disable other same side menus
const side = menu.side;
this._menus
.filter(m => m.side === side && m !== menu)
.map(m => m.enable(false));
}
/**
* @private
*/

View File

@ -129,7 +129,7 @@ export class MenuToggle {
*/
@HostListener('click')
toggle() {
let menu = this._menu.get(this.menuToggle);
const menu = this._menu.get(this.menuToggle);
menu && menu.toggle();
}
@ -137,13 +137,17 @@ export class MenuToggle {
* @private
*/
get isHidden() {
const menu = this._menu.get(this.menuToggle);
if (this._inNavbar && this._viewCtrl) {
if (!menu || !menu._canOpen()) {
return true;
}
if (this._viewCtrl.isFirst()) {
// this is the first view, so it should always show
return false;
}
let menu = this._menu.get(this.menuToggle);
if (menu) {
// this is not the root view, so see if this menu
// is configured to still be enabled if it's not the root view

View File

@ -24,14 +24,14 @@ export class MenuType {
}
setOpen(shouldOpen: boolean, animated: boolean, done: Function) {
let ani = this.ani
.onFinish(done, true)
const ani = this.ani
.onFinish(done, true, true)
.reverse(!shouldOpen);
if (animated) {
ani.play();
} else {
ani.play({ duration: 0 });
ani.syncPlay();
}
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, EventEmitter, Input, NgZone, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, EventEmitter, forwardRef, Input, NgZone, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
import { App } from '../app/app';
import { Backdrop } from '../backdrop/backdrop';
@ -11,8 +11,10 @@ import { Keyboard } from '../../platform/keyboard';
import { MenuContentGesture } from './menu-gestures';
import { MenuController } from './menu-controller';
import { MenuType } from './menu-types';
import { Nav } from '../nav/nav';
import { Platform } from '../../platform/platform';
import { UIEventManager } from '../../gestures/ui-event-manager';
import { RootNode } from '../split-pane/split-pane';
/**
* @name Menu
@ -188,19 +190,21 @@ import { UIEventManager } from '../../gestures/ui-event-manager';
},
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
providers: [{provide: RootNode, useExisting: forwardRef(() => Menu) }]
})
export class Menu {
export class Menu implements RootNode {
private _cntEle: HTMLElement;
private _gesture: MenuContentGesture;
private _type: MenuType;
private _isEnabled: boolean = true;
private _isEnabled: boolean;
private _isSwipeEnabled: boolean = true;
private _isAnimating: boolean = false;
private _isPersistent: boolean = false;
private _init: boolean = false;
private _events: UIEventManager;
private _gestureBlocker: BlockerDelegate;
private _isPane: boolean = false;
/**
* @private
@ -217,6 +221,11 @@ export class Menu {
*/
@ContentChild(Content) menuContent: Content;
/**
* @private
*/
@ContentChild(Nav) menuNav: Nav;
/**
* @input {any} A reference to the content element the menu should use.
*/
@ -248,8 +257,8 @@ export class Menu {
}
set enabled(val: boolean) {
this._isEnabled = isTrueProperty(val);
this._setListeners();
const isEnabled = isTrueProperty(val);
this.enable(isEnabled);
}
/**
@ -261,8 +270,8 @@ export class Menu {
}
set swipeEnabled(val: boolean) {
this._isSwipeEnabled = isTrueProperty(val);
this._setListeners();
const isEnabled = isTrueProperty(val);
this.swipeEnable(isEnabled);
}
/**
@ -344,22 +353,22 @@ export class Menu {
// add the gestures
this._gesture = new MenuContentGesture(this._plt, this, this._gestureCtrl, this._domCtrl);
// register listeners if this menu is enabled
// check if more than one menu is on the same side
let hasEnabledSameSideMenu = this._menuCtrl.getMenus().some(m => {
return m.side === this.side && m.enabled;
});
if (hasEnabledSameSideMenu) {
// auto-disable if another menu on the same side is already enabled
this._isEnabled = false;
}
this._setListeners();
// add menu's content classes
this._cntEle.classList.add('menu-content');
this._cntEle.classList.add('menu-content-' + this.type);
let isEnabled = this._isEnabled;
if (isEnabled === true || typeof isEnabled === 'undefined') {
// check if more than one menu is on the same side
isEnabled = !this._menuCtrl.getMenus().some(m => {
return m.side === this.side && m.enabled;
});
}
// register this menu with the app's menu controller
this._menuCtrl._register(this);
// mask it as enabled / disabled
this.enable(isEnabled);
}
/**
@ -371,27 +380,6 @@ export class Menu {
this._menuCtrl.close();
}
/**
* @private
*/
private _setListeners() {
if (!this._init) {
return;
}
const gesture = this._gesture;
// only listen/unlisten if the menu has initialized
if (this._isEnabled && this._isSwipeEnabled && !gesture.isListening) {
// should listen, but is not currently listening
console.debug('menu, gesture listen', this.side);
gesture.listen();
} else if (gesture.isListening && (!this._isEnabled || !this._isSwipeEnabled)) {
// should not listen, but is currently listening
console.debug('menu, gesture unlisten', this.side);
gesture.unlisten();
}
}
/**
* @private
*/
@ -411,13 +399,11 @@ export class Menu {
*/
setOpen(shouldOpen: boolean, animated: boolean = true): Promise<boolean> {
// If the menu is disabled or it is currenly being animated, let's do nothing
if ((shouldOpen === this.isOpen) || !this._isEnabled || this._isAnimating) {
if ((shouldOpen === this.isOpen) || !this._canOpen() || this._isAnimating) {
return Promise.resolve(this.isOpen);
}
this._before();
return new Promise(resolve => {
this._before();
this._getType().setOpen(shouldOpen, animated, () => {
this._after(shouldOpen);
resolve(this.isOpen);
@ -425,13 +411,21 @@ export class Menu {
});
}
_forceClosing() {
assert(this.isOpen, 'menu cannot be closed');
this._isAnimating = true;
this._getType().setOpen(false, false, () => {
this._after(false);
});
}
/**
* @private
*/
canSwipe(): boolean {
return this._isEnabled &&
this._isSwipeEnabled &&
return this._isSwipeEnabled &&
!this._isAnimating &&
this._canOpen() &&
this._app.isEnabled();
}
@ -442,6 +436,7 @@ export class Menu {
return this._isAnimating;
}
_swipeBeforeStart() {
if (!this.canSwipe()) {
assert(false, 'canSwipe() has to be true');
@ -500,7 +495,7 @@ export class Menu {
// this css class doesn't actually kick off any animations
this.setElementClass('show-menu', true);
this.backdrop.setElementClass('show-backdrop', true);
this.menuContent && this.menuContent.resize();
this.resize();
this._keyboard.close();
this._isAnimating = true;
}
@ -554,6 +549,16 @@ export class Menu {
return this.setOpen(false);
}
/**
* @private
*/
resize() {
const content: Content | Nav = this.menuContent
? this.menuContent
: this.menuNav;
content && content.resize();
}
/**
* @private
*/
@ -561,38 +566,81 @@ export class Menu {
return this.setOpen(!this.isOpen);
}
_canOpen(): boolean {
return this._isEnabled && !this._isPane;
}
/**
* @private
*/
_updateState() {
const canOpen = this._canOpen();
// Close menu inmediately
if (!canOpen && this.isOpen) {
assert(this._init, 'menu must be initialized');
// close if this menu is open, and should not be enabled
this._forceClosing();
}
if (this._isEnabled && this._menuCtrl) {
this._menuCtrl._setActiveMenu(this);
}
if (!this._init) {
return;
}
const gesture = this._gesture;
// only listen/unlisten if the menu has initialized
if (canOpen && this._isSwipeEnabled && !gesture.isListening) {
// should listen, but is not currently listening
console.debug('menu, gesture listen', this.side);
gesture.listen();
} else if (gesture.isListening && (!canOpen || !this._isSwipeEnabled)) {
// should not listen, but is currently listening
console.debug('menu, gesture unlisten', this.side);
gesture.unlisten();
}
if (this.isOpen || (this._isPane && this._isEnabled)) {
this.resize();
}
assert(!this._isAnimating, 'can not be animating');
}
/**
* @private
*/
enable(shouldEnable: boolean): Menu {
this.enabled = shouldEnable;
if (!shouldEnable && this.isOpen) {
// close if this menu is open, and should not be enabled
this.close();
}
if (shouldEnable) {
// if this menu should be enabled
// then find all the other menus on this same side
// and automatically disable other same side menus
this._menuCtrl.getMenus()
.filter(m => m.side === this.side && m !== this)
.map(m => m.enabled = false);
}
// TODO
// what happens if menu is disabled while swipping?
this._isEnabled = shouldEnable;
this.setElementClass('menu-enabled', shouldEnable);
this._updateState();
return this;
}
/**
* @internal
*/
initPane(): boolean {
return false;
}
/**
* @internal
*/
paneChanged(isPane: boolean) {
this._isPane = isPane;
this._updateState();
}
/**
* @private
*/
swipeEnable(shouldEnable: boolean): Menu {
this.swipeEnabled = shouldEnable;
// TODO
// what happens if menu swipe is disabled while swipping?
this._isSwipeEnabled = shouldEnable;
this._updateState();
return this;
}
@ -652,6 +700,13 @@ export class Menu {
this._renderer.setElementAttribute(this._elementRef.nativeElement, attributeName, value);
}
/**
* @private
*/
getElementRef(): ElementRef {
return this._elementRef;
}
/**
* @private
*/

View File

@ -38,17 +38,11 @@ export class E2EApp {
menu1Active() {
this.menuCtrl.enable(true, 'menu1');
this.menuCtrl.enable(false, 'menu2');
this.menuCtrl.enable(false, 'menu3');
}
menu2Active() {
this.menuCtrl.enable(false, 'menu1');
this.menuCtrl.enable(true, 'menu2');
this.menuCtrl.enable(false, 'menu3');
}
menu3Active() {
this.menuCtrl.enable(false, 'menu1');
this.menuCtrl.enable(false, 'menu2');
this.menuCtrl.enable(true, 'menu3');
}
}

View File

@ -7,11 +7,10 @@ import { IonicApp, IonicModule, MenuController } from '../../../..';
templateUrl: 'page1.html'
})
export class Page1 {
activeMenu: string;
activeMenu: string = 'none';
constructor(private menu: MenuController) { }
constructor(private menu: MenuController) {
this.menu1Active();
}
menu1Active() {
this.activeMenu = 'menu1';
this.menu.enable(true, 'menu1');

View File

@ -1,4 +1,4 @@
<ion-menu [content]="content" id="menu1">
<ion-menu [content]="content" id="menu1" enabled="false">
<ion-header>
<ion-toolbar color="secondary">
@ -17,7 +17,7 @@
</ion-menu>
<ion-menu [content]="content" id="menu2">
<ion-menu [content]="content" id="menu2" enabled="false">
<ion-header>
<ion-toolbar color="danger">

View File

@ -1,4 +1,4 @@
import { AfterViewInit, Component, ComponentFactoryResolver, ElementRef, Input, Optional, NgZone, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { AfterViewInit, Component, ComponentFactoryResolver, ElementRef, forwardRef, Input, Optional, NgZone, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { App } from '../app/app';
import { Config } from '../../config/config';
@ -13,6 +13,7 @@ import { NavOptions } from '../../navigation/nav-util';
import { Platform } from '../../platform/platform';
import { TransitionController } from '../../transitions/transition-controller';
import { ViewController } from '../../navigation/view-controller';
import { RootNode } from '../split-pane/split-pane';
/**
* @name Nav
@ -52,8 +53,9 @@ import { ViewController } from '../../navigation/view-controller';
'<div #viewport nav-viewport></div>' +
'<div class="nav-decor"></div>',
encapsulation: ViewEncapsulation.None,
providers: [{provide: RootNode, useExisting: forwardRef(() => Nav) }]
})
export class Nav extends NavControllerBase implements AfterViewInit {
export class Nav extends NavControllerBase implements AfterViewInit, RootNode {
private _root: any;
private _hasInit: boolean = false;
@ -162,8 +164,19 @@ export class Nav extends NavControllerBase implements AfterViewInit {
/**
* @private
*/
destroy() {
ngOnDestroy() {
this.destroy();
}
initPane(): boolean {
const isMain = this._elementRef.nativeElement.hasAttribute('main');
return isMain;
}
paneChanged(isPane: boolean) {
if (isPane) {
this.resize();
}
}
}

View File

@ -49,5 +49,9 @@ export class OverlayPortal extends NavControllerBase {
this._zIndexOffset = (val || 0);
}
ngOnDestroy() {
this.destroy();
}
}

View File

@ -62,6 +62,11 @@
<div>Aenean rhoncus urna at interdum blandit. Donec ac massa nec libero vehicula tincidunt. Sed sit amet hendrerit risus. Aliquam vitae vestibulum ipsum, non feugiat orci. Vivamus eu rutrum elit. Nulla dapibus tortor non dignissim pretium. Nulla in luctus turpis. Etiam non mattis tortor, at aliquet ex. Nunc ut ante varius, auctor dui vel, volutpat elit. Nunc laoreet augue sit amet ultrices porta. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum pellentesque lobortis est, ut tincidunt ligula mollis sit amet. In porta risus arcu, quis pellentesque dolor mattis non. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;</div>
</div>
<button ion-button block color="secondary" (click)="presentToast()">
Open Toast
</button>
</ion-content>

View File

@ -477,7 +477,7 @@ export class Refresher {
}
_setListeners(shouldListen: boolean) {
this._events.destroy();
this._events.unlistenAll();
this._pointerEvents = null;
if (shouldListen) {
this._pointerEvents = this._events.pointerEvents({
@ -503,9 +503,9 @@ export class Refresher {
* @private
*/
ngOnDestroy() {
this._setListeners(false);
this._events.destroy();
this._gesture.destroy();
this._setListeners(false);
}
}

View File

@ -890,7 +890,14 @@ export class Slides extends Ion {
constructor(config: Config, private _plt: Platform, zone: NgZone, @Optional() viewCtrl: ViewController, elementRef: ElementRef, renderer: Renderer) {
constructor(
config: Config,
private _plt: Platform,
zone: NgZone,
@Optional() viewCtrl: ViewController,
elementRef: ElementRef,
renderer: Renderer,
) {
super(config, elementRef, renderer, 'slides');
this._zone = zone;
@ -964,6 +971,12 @@ export class Slides extends Ion {
}
}
resize() {
if (this._init) {
}
}
/**
* Transition to the specified slide.
*

View File

@ -85,7 +85,6 @@ export function initEvents(s: Slides, plt: Platform): Function {
// onresize
let resizeObs = plt.resize.subscribe(() => onResize(s, plt, false));
// Next, Prev, Index
if (s.nextButton) {
plt.registerListener(s.nextButton, 'click', (ev) => {
@ -817,7 +816,18 @@ function onTouchEnd(s: Slides, plt: Platform, ev: SlideUIEvent) {
/*=========================
Resize Handler
===========================*/
let resizeId: number;
function onResize(s: Slides, plt: Platform, forceUpdatePagination: boolean) {
// TODO: hacky, we should use Resize Observer in the future
if (resizeId) {
plt.cancelTimeout(resizeId);
resizeId = null;
}
resizeId = plt.timeout(() => doResize(s, plt, forceUpdatePagination), 200);
}
function doResize(s: Slides, plt: Platform, forceUpdatePagination: boolean) {
resizeId = null;
// Disable locks on resize
var allowSwipeToPrev = s._allowSwipeToPrev;
var allowSwipeToNext = s._allowSwipeToNext;

View File

@ -0,0 +1,12 @@
@import "../../themes/ionic.globals.ios";
// Split Pane
// --------------------------------------------------
.split-pane-ios.split-pane-visible >.split-pane-side {
min-width: 270px;
max-width: 28%;
border-right: $hairlines-width solid $list-ios-border-color;
}

View File

@ -0,0 +1,13 @@
@import "../../themes/ionic.globals.md";
// Split Pane
// --------------------------------------------------
.split-pane-md.split-pane-visible >.split-pane-side {
min-width: 270px;
max-width: 28%;
border-right: 1px solid $list-md-border-color;
}

View File

@ -0,0 +1,74 @@
@import "../../themes/ionic.globals";
// Split Pane
// --------------------------------------------------
ion-split-pane {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-wrap: nowrap;
contain: strict;
}
.split-pane-side:not(ion-menu) {
display: none;
}
.split-pane-visible >.split-pane-side,
.split-pane-visible >.split-pane-main {
// scss-lint:disable ImportantRule
position: relative;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
flex: 1;
box-shadow: none !important;
}
.split-pane-visible >.split-pane-side {
flex-shrink: 0;
order: -1;
}
.split-pane-visible >.split-pane-main,
.split-pane-visible >ion-nav.split-pane-side,
.split-pane-visible >ion-tabs.split-pane-side,
.split-pane-visible >ion-menu.menu-enabled {
display: block;
}
.split-pane-visible >ion-split-pane.split-pane-side,
.split-pane-visible >ion-split-pane.split-pane-main {
display: flex;
}
.split-pane-visible >ion-menu.menu-enabled {
>.menu-inner {
// scss-lint:disable ImportantRule
right: 0;
left: 0;
width: auto;
box-shadow: none !important;
transform: none !important;
}
>.ion-backdrop {
// scss-lint:disable ImportantRule
display: hidden !important;
}
}

View File

@ -0,0 +1,332 @@
import { ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, Input, Output, QueryList, NgZone, Renderer } from '@angular/core';
import { Ion } from '../ion';
import { assert } from '../../util/util';
import { Config } from '../../config/config';
import { Platform } from '../../platform/platform';
const QUERY: { [key: string]: string } = {
xs: '(min-width: 0px)',
sm: '(min-width: 576px)',
md: '(min-width: 768px)',
lg: '(min-width: 992px)',
xl: '(min-width: 1200px)',
never: ''
};
/**
* @private
*/
export abstract class RootNode {
abstract getElementRef(): ElementRef;
abstract initPane(): boolean;
abstract paneChanged?(visible: boolean): void;
}
/**
* @name SplitPane
*
* @description
* SplitPane is a component that makes it possible to create multi-view layout.
* Similar to iPad apps, SplitPane allows UI elements, like Menus, to be
* displayed as the viewport increases.
*
* If the devices screen size is below a certain size, the SplitPane will
* collapse and the menu will become hidden again. This is especially useful when
* creating an app that will be served over a browser or deployed through the app
* store to phones and tablets.
*
* @usage
* To use SplitPane, simply add the component around your root component.
* In this example, we'll be using a sidemenu layout, similar to what is
* provided from the sidemenu starter template.
*
* ```html
* <ion-split-pane>
* <!-- our side menu -->
* <ion-menu [content]="content">
* <ion-header>
* <ion-toolbar>
* <ion-title>Menu</ion-title>
* </ion-toolbar>
* </ion-header>
* </ion-menu>
*
* <!-- the main content -->
* <ion-nav [root]="root" main #content></ion-nav>
* </ion-split-pane>
* ```
*
* Here, SplitPane will look for the element with the `main` attribute and make
* that the central component on larger screens. The `main` component can be any
* Ionic component (`ion-nav` or `ion-tabs`) except `ion-menu`.
*
* ### Setting breakpoints
*
* By default, SplitPane will expand when the screen is larger than 768px.
* If you want to customize this, use the `when` input. The `when` input can
* accept any valid media query, as it uses `matchMedia()` underneath.
*
* ```
* <ion-split-pane when="(min-width: 475px)">
*
* <!-- our side menu -->
* <ion-menu [content]="content">
* ....
* </ion-menu>
*
* <!-- the main content -->
* <ion-nav [root]="root" main #content></ion-nav>
* </ion-split-pane>
* ```
*
* SplitPane also provides some predefined media queries that can be used.
*
* ```html
* <!-- could be "xs", "sm", "md", "lg", or "xl" -->
* <ion-split-pane when="lg">
* ...
* </ion-split-pane>
* ```
*
*
* | Size | Value | Description |
* |------|-----------------------|-----------------------------------------------------------------------|
* | `xs` | `(min-width: 0px)` | Show the split-pane when the min-width is 0px (meaning, always) |
* | `sm` | `(min-width: 576px)` | Show the split-pane when the min-width is 576px |
* | `md` | `(min-width: 768px)` | Show the split-pane when the min-width is 768px (default break point) |
* | `lg` | `(min-width: 992px)` | Show the split-pane when the min-width is 992px |
* | `xl` | `(min-width: 1200px)` | Show the split-pane when the min-width is 1200px |
*
* You can also pass in boolean values that will trigger SplitPane when the value
* or expression evaluates to true.
*
*
* ```html
* <ion-split-pane [when]="isLarge">
* ...
* </ion-split-pane>
* ```
*
* ```ts
* class MyClass {
* public isLarge = false;
* constructor(){}
* }
* ```
*
* Or
*
* ```html
* <ion-split-pane [when]="shouldShow()">
* ...
* </ion-split-pane>
* ```
*
* ```ts
* class MyClass {
* constructor(){}
* shouldShow(){
* if(conditionA){
* return true
* } else {
* return false
* }
* }
* }
* ```
*
*/
@Directive({
selector: 'ion-split-pane',
providers: [{provide: RootNode, useExisting: forwardRef(() => SplitPane) }]
})
export class SplitPane extends Ion implements RootNode {
_rmListener: any;
_visible: boolean = false;
_init: boolean = false;
_mediaQuery: string | boolean = QUERY['md'];
_children: RootNode[];
/**
* @private
*/
sideContent: RootNode = null;
/**
* @private
*/
mainContent: RootNode = null;
/**
* @private
*/
@ContentChildren(RootNode, { descendants: false })
set _setchildren(query: QueryList<RootNode>) {
const children = this._children = query.filter((child => child !== this));
children.forEach(child => {
var isMain = child.initPane();
this._setPaneCSSClass(child.getElementRef(), isMain);
});
}
/**
* @input {string | boolean} When the split-pane should be shown.
* Can be a CSS media query expression, or a shortcut expression.
* Can aslo be a boolean expression.
*/
@Input()
set when(query: string | boolean) {
if (typeof query === 'boolean') {
this._mediaQuery = query;
} else {
const defaultQuery = QUERY[query];
this._mediaQuery = (defaultQuery)
? defaultQuery
: query;
}
this._update();
}
get when(): string | boolean {
return this._mediaQuery;
}
/**
* @output {any} Expression to be called when the split-pane visibility has changed
*/
@Output() ionChange: EventEmitter<SplitPane> = new EventEmitter<SplitPane>();
constructor(
private _zone: NgZone,
private _plt: Platform,
config: Config,
elementRef: ElementRef,
renderer: Renderer,
) {
super(config, elementRef, renderer, 'split-pane');
}
/**
* @private
*/
_register(node: RootNode, isMain: boolean, callback: Function): boolean {
if (this.getElementRef().nativeElement !== node.getElementRef().nativeElement.parentNode) {
return false;
}
this._setPaneCSSClass(node.getElementRef(), isMain);
if (callback) {
this.ionChange.subscribe(callback);
}
if (isMain) {
if (this.mainContent) {
console.error('split pane: main content was already set');
}
this.mainContent = node;
}
return true;
}
/**
* @private
*/
ngAfterViewInit() {
this._init = true;
this._update();
}
/**
* @private
*/
_update() {
if (!this._init) {
return;
}
// Unlisten
this._rmListener && this._rmListener();
this._rmListener = null;
const query = this._mediaQuery;
if (typeof query === 'boolean') {
this._setVisible(query);
return;
}
if (query && query.length > 0) {
// Listen
const callback = (query: MediaQueryList) => this._setVisible(query.matches);
const mediaList = this._plt.win().matchMedia(query);
mediaList.addListener(callback);
this._setVisible(mediaList.matches);
this._rmListener = function () {
mediaList.removeListener(callback);
};
} else {
this._setVisible(false);
}
}
/**
* @private
*/
_updateChildren() {
this.mainContent = null;
this.sideContent = null;
const visible = this._visible;
this._children.forEach(child => child.paneChanged && child.paneChanged(visible));
}
/**
* @private
*/
_setVisible(visible: boolean) {
if (this._visible === visible) {
return;
}
this._visible = visible;
this.setElementClass('split-pane-visible', visible);
this._updateChildren();
this._zone.run(() => {
this.ionChange.emit(this);
});
}
/**
* @private
*/
isVisible(): boolean {
return this._visible;
}
/**
* @private
*/
setElementClass(className: string, add: boolean) {
this._renderer.setElementClass(this._elementRef.nativeElement, className, add);
}
/**
* @private
*/
_setPaneCSSClass(elementRef: ElementRef, isMain: boolean) {
const ele = elementRef.nativeElement;
this._renderer.setElementClass(ele, 'split-pane-main', isMain);
this._renderer.setElementClass(ele, 'split-pane-side', !isMain);
}
/**
* @private
*/
ngOnDestroy() {
assert(this._rmListener, 'at this point _rmListerner should be valid');
this._rmListener && this._rmListener();
this._rmListener = null;
}
/**
* @private
*/
initPane(): boolean {
return true;
}
}

View File

@ -0,0 +1,12 @@
@import "../../themes/ionic.globals.wp";
// Split Pane
// --------------------------------------------------
.split-pane-wp.split-pane-visible >.split-pane-side {
min-width: 270px;
max-width: 28%;
border-right: 1px solid $list-wp-border-color;
}

View File

@ -0,0 +1,120 @@
import { Component, NgModule } from '@angular/core';
import { IonicApp, IonicModule, NavController, MenuController, SplitPane } from '../../../../../ionic-angular';
@Component({
template: `
<ion-header>
<ion-navbar><ion-title>Navigation</ion-title></ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>Hola 1</ion-item>
<ion-item>Hola 2</ion-item>
<ion-item>Hola 3</ion-item>
<button ion-item (click)="push()">Push</button>
<ion-item>Hola</ion-item>
<ion-item>Hola</ion-item>
<ion-item>Hola</ion-item>
</ion-list>
</ion-content>
`
})
export class SidePage {
constructor(public navCtrl: NavController) { }
push() {
this.navCtrl.push(SidePage);
}
}
@Component({
template: `
<ion-header>
<ion-navbar>
<button ion-button menuToggle>
<ion-icon name="menu"></ion-icon>
</button>
<ion-title>Page 2</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<h1>Page 2</h1>
</ion-content>
`
})
export class E2EPage2 {}
@Component({
template: `
<ion-header>
<ion-navbar>
<button ion-button menuToggle>
<ion-icon name="menu"></ion-icon>
</button>
<ion-title>Navigation</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<h1>Page 1</h1>
<button ion-button (click)="push()">Push</button>
<button ion-button (click)="menu()">Disable/enable menu</button>
<div f></div>
<div f></div>
<div f></div>
<div f></div>
</ion-content>
`
})
export class E2EPage {
constructor(
public navCtrl: NavController,
public menuCtrl: MenuController,
) { }
push() {
this.navCtrl.push(E2EPage2);
}
menu() {
this.menuCtrl.enable(!this.menuCtrl.isEnabled());
}
}
@Component({
templateUrl: 'main.html'
})
export class E2EApp {
root = E2EPage;
root2 = SidePage;
splitPaneChanged(splitPane: SplitPane) {
console.log('Split pane changed, visible: ', splitPane.isVisible());
}
}
@NgModule({
declarations: [
E2EApp,
E2EPage,
E2EPage2,
SidePage,
],
imports: [
IonicModule.forRoot(E2EApp, {
swipeBackEnabled: true
})
],
bootstrap: [IonicApp],
entryComponents: [
E2EApp,
E2EPage,
E2EPage2,
SidePage,
]
})
export class AppModule {}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,7 @@
<ion-split-pane (ionChange)="splitPaneChanged($event)">
<ion-menu [content]="content">
<ion-nav [root]="root2"></ion-nav>
</ion-menu>
<ion-nav [root]="root" main #content></ion-nav>
</ion-split-pane>

View File

@ -0,0 +1,102 @@
import { Component, NgModule } from '@angular/core';
import { IonicApp, IonicModule, NavController, MenuController, SplitPane } from '../../../../../ionic-angular';
@Component({
template: `
<ion-header>
<ion-navbar>
<button ion-button menuToggle>
<ion-icon name="menu"></ion-icon>
</button>
<ion-title>Page 2</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<h1>Page 2</h1>
</ion-content>
`
})
export class E2EPage2 {}
@Component({
template: `
<ion-header>
<ion-navbar>
<button ion-button menuToggle>
<ion-icon name="menu"></ion-icon>
</button>
<ion-title>Navigation</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<h1>Page 1</h1>
<button ion-button (click)="push()">Push</button>
<button ion-button (click)="menu1Active()">Enable menu 1</button>
<button ion-button (click)="menu2Active()">Enable menu 2</button>
<button ion-button (click)="menu3Active()">Enable menu 3</button>
<button ion-button (click)="disableAll()">Disable all</button>
<div f></div>
<div f></div>
<div f></div>
<div f></div>
</ion-content>
`
})
export class E2EPage {
constructor(
public navCtrl: NavController,
public menuCtrl: MenuController,
) { }
push() {
this.navCtrl.push(E2EPage2);
}
menu1Active() {
this.menuCtrl.enable(true, 'menu1');
}
menu2Active() {
this.menuCtrl.enable(true, 'menu2');
}
menu3Active() {
this.menuCtrl.enable(true, 'menu3');
}
disableAll() {
this.menuCtrl.enable(false);
}
}
@Component({
templateUrl: 'main.html'
})
export class E2EApp {
root = E2EPage;
splitPaneChanged(splitPane: SplitPane) {
console.log('Split pane changed, visible: ', splitPane.isVisible());
}
}
@NgModule({
declarations: [
E2EApp,
E2EPage,
E2EPage2,
],
imports: [
IonicModule.forRoot(E2EApp, {
swipeBackEnabled: true
})
],
bootstrap: [IonicApp],
entryComponents: [
E2EApp,
E2EPage,
E2EPage2,
]
})
export class AppModule {}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,52 @@
<ion-split-pane (ionChange)="splitPaneChanged($event)">
<ion-menu [content]="content" id="menu1">
<ion-header>
<ion-toolbar color="secondary">
<ion-title>Menu 1</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>Example</ion-item>
</ion-list>
</ion-content>
</ion-menu>
<ion-menu [content]="content" id="menu2">
<ion-header>
<ion-toolbar color="danger">
<ion-title>Menu 2</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>Example</ion-item>
</ion-list>
</ion-content>
</ion-menu>
<ion-menu [content]="content" id="menu3">
<ion-header>
<ion-toolbar color="primary">
<ion-title>Menu 3</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>Example</ion-item>
</ion-list>
</ion-content>
</ion-menu>
<ion-nav [root]="root" main #content></ion-nav>
</ion-split-pane>

View File

@ -0,0 +1,162 @@
import { Component, NgModule } from '@angular/core';
import { IonicApp, IonicModule, NavController } from '../../../../../ionic-angular';
@Component({
template: `
<ion-header>
<ion-navbar><ion-title>Nested 1</ion-title></ion-navbar>
</ion-header>
<ion-content padding>
<button ion-button (click)="push()">Push</button>
<div f></div>
<div f></div>
<div f></div>
<div f></div>
</ion-content>
`
})
export class E2ENested {
constructor(
public navCtrl: NavController,
) { }
push() {
this.navCtrl.push(E2ENested);
}
}
@Component({
template: `
<ion-header>
<ion-navbar><ion-title>Nested 2</ion-title></ion-navbar>
</ion-header>
<ion-content padding>
<button ion-button (click)="push()">Push</button>
<div f></div>
<div f></div>
<div f></div>
<div f></div>
</ion-content>
`
})
export class E2ENested2 {
constructor(
public navCtrl: NavController,
) { }
push() {
this.navCtrl.push(E2ENested2);
}
}
@Component({
template: `
<ion-header>
<ion-navbar>
<button ion-button menuToggle>
<ion-icon name="menu"></ion-icon>
</button>
<ion-title>Nested 3</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<button ion-button (click)="push()">Push</button>
<div f></div>
<div f></div>
<div f></div>
<div f></div>
</ion-content>
`
})
export class E2ENested3 {
constructor(
public navCtrl: NavController,
) { }
push() {
this.navCtrl.push(E2ENested3);
}
}
@Component({
template: `
<ion-header>
<ion-navbar><ion-title>Navigation</ion-title></ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>Hola</ion-item>
<ion-item>Hola</ion-item>
<ion-item>Hola</ion-item>
<button ion-item (click)="push()">Push</button>
<ion-item>Hola</ion-item>
<ion-item>Hola</ion-item>
<ion-item>Hola</ion-item>
</ion-list>
</ion-content>
`
})
export class SidePage {
constructor(public navCtrl: NavController) { }
push() {
this.navCtrl.push(SidePage);
}
}
@Component({
template: `
<ion-split-pane>
<ion-nav [root]="root"></ion-nav>
<ion-split-pane when="lg" main >
<ion-nav [root]="root2"></ion-nav>
<ion-nav [root]="root3" main ></ion-nav>
</ion-split-pane>
</ion-split-pane>
`
})
export class E2EPage {
root = E2ENested;
root2 = E2ENested2;
root3 = E2ENested3;
}
@Component({
templateUrl: 'main.html'
})
export class E2EApp {
root = E2EPage;
root2 = SidePage;
}
@NgModule({
declarations: [
E2EApp,
E2EPage,
SidePage,
E2ENested,
E2ENested2,
E2ENested3
],
imports: [
IonicModule.forRoot(E2EApp, {
swipeBackEnabled: true
})
],
bootstrap: [IonicApp],
entryComponents: [
E2EApp,
E2EPage,
SidePage,
E2ENested,
E2ENested2,
E2ENested3
]
})
export class AppModule {}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,10 @@
<ion-split-pane when="sm">
<ion-menu [content]="content">
<ion-nav [root]="root2"></ion-nav>
</ion-menu>
<ion-nav [root]="root" main #content></ion-nav>
</ion-split-pane>

View File

@ -0,0 +1,98 @@
import { Component, NgModule } from '@angular/core';
import { IonicApp, IonicModule, NavController, MenuController } from '../../../../../ionic-angular';
@Component({
template: `
<ion-header>
<ion-navbar><ion-title>Navigation</ion-title></ion-navbar>
</ion-header>
<ion-content>
<ion-slides style="background: black"
pager="true"
effect="flip">
<ion-slide style="background: red; color: white;">
<h1>Slide 1</h1>
</ion-slide>
<ion-slide style="background: white; color: blue;">
<h1>Slide 2</h1>
</ion-slide>
<ion-slide style="background: blue; color: white;">
<h1>Slide 3</h1>
</ion-slide>
</ion-slides>
</ion-content>
`
})
export class SidePage {
constructor(public navCtrl: NavController) { }
push() {
this.navCtrl.push(SidePage);
}
}
@Component({
template: `
<ion-header>
<ion-navbar>
<button ion-button menuToggle>
<ion-icon name="menu"></ion-icon>
</button>
<ion-title>Navigation</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<button ion-button (click)="push()">Push</button>
<div f></div>
<div f></div>
<div f></div>
<div f></div>
</ion-content>
`
})
export class E2EPage {
constructor(
public navCtrl: NavController,
public menuCtrl: MenuController,
) { }
push() {
this.navCtrl.push(E2EPage);
}
}
@Component({
templateUrl: 'main.html'
})
export class E2EApp {
root = E2EPage;
root2 = SidePage;
}
@NgModule({
declarations: [
E2EApp,
E2EPage,
SidePage,
],
imports: [
IonicModule.forRoot(E2EApp, {
swipeBackEnabled: true
})
],
bootstrap: [IonicApp],
entryComponents: [
E2EApp,
E2EPage,
SidePage,
]
})
export class AppModule {}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,8 @@
<ion-split-pane>
<ion-nav [root]="root2" #content main></ion-nav>
<ion-tabs>
<ion-tab [root]="root" tabTitle="Page1"></ion-tab>
<ion-tab [root]="root" tabTitle="Page2"></ion-tab>
</ion-tabs>
</ion-split-pane>

View File

@ -1,4 +1,4 @@
import { Directive, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, Renderer } from '@angular/core';
import { Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, Renderer } from '@angular/core';
import { Config } from '../../config/config';
import { Ion } from '../ion';
@ -7,8 +7,13 @@ import { Tab } from './tab';
/**
* @private
*/
@Directive({
@Component({
selector: '.tab-button',
template:
'<ion-icon *ngIf="tab.tabIcon" [name]="tab.tabIcon" [isActive]="tab.isSelected" class="tab-button-icon"></ion-icon>' +
'<span *ngIf="tab.tabTitle" class="tab-button-text">{{tab.tabTitle}}</span>' +
'<ion-badge *ngIf="tab.tabBadge" class="tab-badge" [color]="tab.tabBadgeStyle">{{tab.tabBadge}}</ion-badge>' +
'<div class="button-effect"></div>',
host: {
'[attr.id]': 'tab._btnId',
'[attr.aria-controls]': 'tab._tabId',
@ -18,7 +23,9 @@ import { Tab } from './tab';
'[class.has-title-only]': 'hasTitleOnly',
'[class.icon-only]': 'hasIconOnly',
'[class.has-badge]': 'hasBadge',
'[class.disable-hover]': 'disHover'
'[class.disable-hover]': 'disHover',
'[class.tab-disabled]': '!tab.enabled',
'[class.tab-hidden]': '!tab.show',
}
})
export class TabButton extends Ion implements OnInit {

View File

@ -314,15 +314,22 @@ export class Tab extends NavControllerBase {
// to refresh the tabbar and content dimensions to be sure
// they're lined up correctly
this._dom.read(() => {
this.resize();
});
done(true);
}
}
/**
* @private
*/
resize() {
const active = this.getActive();
if (!active) {
return;
}
const content = active.getIONContent();
content && content.resize();
});
done(true);
}
}
/**
@ -385,7 +392,7 @@ export class Tab extends NavControllerBase {
/**
* @private
*/
destroy() {
ngOnDestroy() {
this.destroy();
}

View File

@ -1,4 +1,4 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, Optional, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { AfterViewInit, Component, ElementRef, EventEmitter, forwardRef, Input, Output, Optional, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { App } from '../app/app';
import { Config } from '../../config/config';
@ -8,6 +8,7 @@ import { isBlank, assert } from '../../util/util';
import { NavController } from '../../navigation/nav-controller';
import { NavControllerBase } from '../../navigation/nav-controller-base';
import { getComponent, NavOptions, DIRECTION_SWITCH } from '../../navigation/nav-util';
import { RootNode } from '../split-pane/split-pane';
import { Platform } from '../../platform/platform';
import { Tab } from './tab';
import { TabHighlight } from './tab-highlight';
@ -150,19 +151,15 @@ import { ViewController } from '../../navigation/view-controller';
selector: 'ion-tabs',
template:
'<div class="tabbar" role="tablist" #tabbar>' +
'<a *ngFor="let t of _tabs" [tab]="t" class="tab-button" [class.tab-disabled]="!t.enabled" [class.tab-hidden]="!t.show" role="tab" href="#" (ionSelect)="select($event)">' +
'<ion-icon *ngIf="t.tabIcon" [name]="t.tabIcon" [isActive]="t.isSelected" class="tab-button-icon"></ion-icon>' +
'<span *ngIf="t.tabTitle" class="tab-button-text">{{t.tabTitle}}</span>' +
'<ion-badge *ngIf="t.tabBadge" class="tab-badge" [color]="t.tabBadgeStyle">{{t.tabBadge}}</ion-badge>' +
'<div class="button-effect"></div>' +
'</a>' +
'<a *ngFor="let t of _tabs" [tab]="t" class="tab-button" role="tab" href="#" (ionSelect)="select(t)"></a>' +
'<div class="tab-highlight"></div>' +
'</div>' +
'<ng-content></ng-content>' +
'<div #portal tab-portal></div>',
encapsulation: ViewEncapsulation.None,
providers: [{provide: RootNode, useExisting: forwardRef(() => Tabs) }]
})
export class Tabs extends Ion implements AfterViewInit {
export class Tabs extends Ion implements AfterViewInit, RootNode {
/** @internal */
_ids: number = -1;
/** @internal */
@ -562,6 +559,31 @@ export class Tabs extends Ion implements AfterViewInit {
}
}
/**
* @internal
*/
resize() {
const tab = this.getSelected();
tab && tab.resize();
}
/**
* @internal
*/
initPane(): boolean {
const isMain = this._elementRef.nativeElement.hasAttribute('main');
return isMain;
}
/**
* @internal
*/
paneChanged(isPane: boolean) {
if (isPane) {
this.resize();
}
}
}
let tabIds = -1;

View File

@ -127,6 +127,7 @@ import { HideWhen } from './components/show-hide-when/hide-when';
import { Slide } from './components/slides/slide';
import { Slides } from './components/slides/slides';
import { Spinner } from './components/spinner/spinner';
import { SplitPane } from './components/split-pane/split-pane';
import { Tab } from './components/tabs/tab';
import { Tabs } from './components/tabs/tabs';
import { TabButton } from './components/tabs/tab-button';
@ -241,6 +242,7 @@ export { HideWhen } from './components/show-hide-when/hide-when';
export { Slide } from './components/slides/slide';
export { Slides } from './components/slides/slides';
export { Spinner } from './components/spinner/spinner';
export { SplitPane, RootNode } from './components/split-pane/split-pane';
export { Tab } from './components/tabs/tab';
export { TabButton } from './components/tabs/tab-button';
export { TabHighlight } from './components/tabs/tab-highlight';
@ -313,6 +315,9 @@ export { reorderArray } from './util/util';
export { Animation, AnimationOptions, EffectProperty, EffectState, PlayOptions } from './animations/animation';
export { PageTransition } from './transitions/page-transition';
export { Transition } from './transitions/transition';
export { PlatformConfigToken } from './platform/platform-registry';
export { registerModeConfigs } from './config/mode-registry';
export { IonicGestureConfig } from './gestures/gesture-config';
@ -432,6 +437,7 @@ export { Transition } from './transitions/transition';
Slide,
Slides,
Spinner,
SplitPane,
Tab,
Tabs,
TabButton,
@ -526,6 +532,7 @@ export { Transition } from './transitions/transition';
Slide,
Slides,
Spinner,
SplitPane,
Tab,
Tabs,
TabButton,

View File

@ -4,7 +4,7 @@ import { AnimationOptions } from '../animations/animation';
import { App } from '../components/app/app';
import { Config } from '../config/config';
import { convertToView, convertToViews, NavOptions, DIRECTION_BACK, DIRECTION_FORWARD, INIT_ZINDEX,
TransitionResolveFn, TransitionInstruction, STATE_INITIALIZED, STATE_LOADED, STATE_PRE_RENDERED } from './nav-util';
TransitionResolveFn, TransitionInstruction, STATE_NEW, STATE_INITIALIZED, STATE_ATTACHED, STATE_DESTROYED } from './nav-util';
import { setZIndex } from './nav-util';
import { DeepLinker } from './deep-linker';
import { DomController } from '../platform/dom-controller';
@ -213,8 +213,8 @@ export class NavControllerBase extends Ion implements NavController {
});
}
// ti.resolve() is called when the navigation transition is finished successfully
ti.resolve = (hasCompleted: boolean, isAsync: boolean, enteringName: string, leavingName: string, direction: string) => {
// transition has successfully resolved
this._trnsId = null;
this._init = true;
resolve && resolve(hasCompleted, isAsync, enteringName, leavingName, direction);
@ -225,23 +225,22 @@ export class NavControllerBase extends Ion implements NavController {
this._nextTrns();
};
ti.reject = (rejectReason: any, trns: Transition) => {
// rut row raggy, something rejected this transition
// ti.reject() is called when the navigation transition fails. ie. it is rejected at some point.
ti.reject = (rejectReason: any, transition: Transition) => {
this._trnsId = null;
this._queue.length = 0;
while (trns) {
if (trns.enteringView && (trns.enteringView._state !== STATE_LOADED)) {
// destroy the entering views and all of their hopes and dreams
this._destroyView(trns.enteringView);
// walk through the transition views so they are destroyed
while (transition) {
var enteringView = transition.enteringView;
if (enteringView && (enteringView._state === STATE_ATTACHED)) {
this._destroyView(enteringView);
}
if (!trns.parent) {
if (transition.isRoot()) {
this._trnsCtrl.destroy(transition.trnsId);
break;
}
}
if (trns) {
this._trnsCtrl.destroy(trns.trnsId);
transition = transition.parent;
}
reject && reject(false, false, rejectReason);
@ -289,7 +288,19 @@ export class NavControllerBase extends Ion implements NavController {
return false;
}
// Get entering and leaving views
// ensure any of the inserted view are used
const insertViews = ti.insertViews;
if (insertViews) {
for (var i = 0; i < insertViews.length; i++) {
var nav = insertViews[i]._nav;
if (nav && nav !== this || insertViews[i]._state === STATE_DESTROYED) {
ti.reject('leavingView and enteringView are null. stack is already empty');
return false;
}
}
}
// get entering and leaving views
const leavingView = this.getActive();
const enteringView = this._getEnteringView(ti, leavingView);
@ -302,7 +313,7 @@ export class NavControllerBase extends Ion implements NavController {
this.setTransitioning(true);
// Initialize enteringView
if (enteringView && isBlank(enteringView._state)) {
if (enteringView && enteringView._state === STATE_NEW) {
// render the entering view, and all child navs and views
// ******** DOM WRITE ****************
this._viewInit(enteringView);
@ -314,7 +325,6 @@ export class NavControllerBase extends Ion implements NavController {
// views have been initialized, now let's test
// to see if the transition is even allowed or not
return this._viewTest(enteringView, leavingView, ti);
} else {
return this._postViewInit(enteringView, leavingView, ti);
}
@ -430,7 +440,6 @@ export class NavControllerBase extends Ion implements NavController {
// add the views to the
for (i = 0; i < insertViews.length; i++) {
view = insertViews[i];
assert(view, 'view must be non null');
this._insertViewAt(view, ti.insertStart + i);
}
@ -488,6 +497,9 @@ export class NavControllerBase extends Ion implements NavController {
* DOM WRITE
*/
_viewInit(enteringView: ViewController) {
assert(enteringView, 'enteringView must be non null');
assert(enteringView._state === STATE_NEW, 'enteringView state must be NEW');
// entering view has not been initialized yet
const componentProviders = ReflectiveInjector.resolve([
{ provide: NavController, useValue: this },
@ -512,7 +524,7 @@ export class NavControllerBase extends Ion implements NavController {
// render the component ref instance to the DOM
// ******** DOM WRITE ****************
viewport.insert(componentRef.hostView, viewport.length);
view._state = STATE_PRE_RENDERED;
view._state = STATE_ATTACHED;
if (view._cssClass) {
// the ElementRef of the actual ion-page created
@ -615,14 +627,12 @@ export class NavControllerBase extends Ion implements NavController {
}
});
if (enteringView && enteringView._state === STATE_INITIALIZED) {
if (enteringView && (enteringView._state === STATE_INITIALIZED)) {
// render the entering component in the DOM
// this would also render new child navs/views
// which may have their very own async canEnter/Leave tests
// ******** DOM WRITE ****************
this._viewAttachToDOM(enteringView, enteringView._cmp, this._viewport);
} else {
console.debug('enteringView state is not INITIALIZED', enteringView);
}
if (!transition.hasChildren) {
@ -773,9 +783,10 @@ export class NavControllerBase extends Ion implements NavController {
if (existingIndex > -1) {
// this view is already in the stack!!
// move it to its new location
assert(view._nav === this, 'view is not part of the nav');
this._views.splice(index, 0, this._views.splice(existingIndex, 1)[0]);
} else {
assert(!view._nav || (this._isPortal && view._nav === this), 'nav is used');
// this is a new view to add to the stack
// create the new entering view
view._setNav(this);
@ -792,6 +803,8 @@ export class NavControllerBase extends Ion implements NavController {
}
_removeView(view: ViewController) {
assert(view._state === STATE_ATTACHED || view._state === STATE_DESTROYED, 'view state should be loaded or destroyed');
const views = this._views;
const index = views.indexOf(view);
assert(index > -1, 'view must be part of the stack');
@ -811,7 +824,7 @@ export class NavControllerBase extends Ion implements NavController {
_cleanup(activeView: ViewController) {
// ok, cleanup time!! Destroy all of the views that are
// INACTIVE and come after the active view
const activeViewIndex = this.indexOf(activeView);
const activeViewIndex = this._views.indexOf(activeView);
const views = this._views;
let reorderZIndexes = false;
let view: ViewController;
@ -1030,7 +1043,8 @@ export class NavControllerBase extends Ion implements NavController {
if (!view) {
view = this.getActive();
}
return this._views[this.indexOf(view) - 1];
const views = this._views;
return views[views.indexOf(view) - 1];
}
first(): ViewController {
@ -1066,7 +1080,7 @@ export class NavControllerBase extends Ion implements NavController {
dismissPageChangeViews() {
for (let view of this._views) {
if (view.data && view.data.dismissOnPageChange) {
view.dismiss();
view.dismiss().catch(null);
}
}
}
@ -1075,6 +1089,15 @@ export class NavControllerBase extends Ion implements NavController {
this._viewport = val;
}
resize() {
const active = this.getActive();
if (!active) {
return;
}
const content = active.getIONContent();
content && content.resize();
}
}
let ctrlIds = -1;

View File

@ -609,4 +609,9 @@ export abstract class NavController {
* @private
*/
abstract registerChildNav(nav: any): void;
/**
* @private
*/
abstract resize(): void;
}

View File

@ -208,10 +208,10 @@ export interface TransitionInstruction {
requiresTransition?: boolean;
}
export const STATE_INITIALIZED = 1;
export const STATE_PRE_RENDERED = 2;
export const STATE_LOADED = 3;
export const STATE_NEW = 1;
export const STATE_INITIALIZED = 2;
export const STATE_ATTACHED = 3;
export const STATE_DESTROYED = 4;
export const INIT_ZINDEX = 100;

View File

@ -1,5 +1,5 @@
import { mockNavController, mockView, mockViews } from '../../util/mock-providers';
import { STATE_ATTACHED } from '../nav-util';
describe('ViewController', () => {
@ -16,6 +16,7 @@ describe('ViewController', () => {
});
// act
viewController._state = STATE_ATTACHED;
viewController._willEnter();
}, 10000);
});
@ -33,6 +34,7 @@ describe('ViewController', () => {
});
// act
viewController._state = STATE_ATTACHED;
viewController._didEnter();
}, 10000);
});
@ -50,6 +52,7 @@ describe('ViewController', () => {
});
// act
viewController._state = STATE_ATTACHED;
viewController._willLeave(false);
}, 10000);
});

View File

@ -2,10 +2,10 @@ import { ComponentRef, ElementRef, EventEmitter, Output, Renderer } from '@angul
import { Footer } from '../components/toolbar/toolbar-footer';
import { Header } from '../components/toolbar/toolbar-header';
import { isPresent } from '../util/util';
import { isPresent, assert } from '../util/util';
import { Navbar } from '../components/navbar/navbar';
import { NavController } from './nav-controller';
import { NavOptions } from './nav-util';
import { NavOptions, STATE_NEW, STATE_INITIALIZED, STATE_ATTACHED, STATE_DESTROYED } from './nav-util';
import { NavParams } from './nav-params';
import { Content } from '../components/content/content';
@ -47,7 +47,7 @@ export class ViewController {
_cmp: ComponentRef<any>;
_nav: NavController;
_zIndex: number;
_state: number;
_state: number = STATE_NEW;
_cssClass: string;
/**
@ -166,6 +166,7 @@ export class ViewController {
*/
dismiss(data?: any, role?: any, 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) {
@ -228,7 +229,7 @@ export class ViewController {
* @private
*/
get name(): string {
return this.component ? this.component.name : '';
return (this.component ? this.component.name : '');
}
/**
@ -262,16 +263,14 @@ export class ViewController {
// _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 (this._cmp) {
// if it should render, then the hidden attribute should not be on the element
if (shouldShow === this._isHidden) {
if (this._cmp && shouldShow === this._isHidden) {
this._isHidden = !shouldShow;
let value = (shouldShow ? null : '');
// ******** DOM WRITE ****************
renderer.setElementAttribute(this.pageRef().nativeElement, 'hidden', value);
}
}
}
/**
* @private
@ -412,6 +411,7 @@ export class ViewController {
}
_preLoad() {
assert(this._state === STATE_INITIALIZED, 'view state must be INITIALIZED');
this._lifecycle('PreLoad');
}
@ -421,6 +421,7 @@ export class ViewController {
* 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');
}
@ -433,6 +434,7 @@ export class ViewController {
* recommended method to use when a view becomes active.
*/
_didLoad() {
assert(this._state === STATE_ATTACHED, 'view state must be ATTACHED');
this._lifecycle('DidLoad');
}
@ -441,6 +443,8 @@ export class ViewController {
* 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();
@ -457,6 +461,8 @@ export class ViewController {
* 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');
@ -511,6 +517,8 @@ export class ViewController {
* 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
@ -525,6 +533,7 @@ export class ViewController {
}
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;
}
/**
@ -535,7 +544,7 @@ export class ViewController {
const methodName = 'ionViewCan' + lifecycle;
if (instance && instance[methodName]) {
try {
let result = instance[methodName]();
var result = instance[methodName]();
if (result === false) {
return false;
} else if (result instanceof Promise) {

View File

@ -190,6 +190,12 @@
@import
"../components/slides/slides";
@import
"../components/split-pane/split-pane",
"../components/split-pane/split-pane.ios",
"../components/split-pane/split-pane.md",
"../components/split-pane/split-pane.wp";
@import
"../components/spinner/spinner",
"../components/spinner/spinner.ios",

View File

@ -351,7 +351,9 @@ export function mockView(component?: any, data?: any) {
export function mockViews(nav: NavControllerBase, views: ViewController[]) {
nav._views = views;
views.forEach(v => v._setNav(nav));
views.forEach(v => {
v._setNav(nav);
});
}
export function mockComponentRef(): ComponentRef<any> {
@ -499,7 +501,9 @@ export function mockMenu(): Menu {
let app = mockApp();
let gestureCtrl = new GestureController(app);
let dom = mockDomController();
return new Menu(null, null, null, null, null, null, null, gestureCtrl, dom, app);
let elementRef = mockElementRef();
let renderer = mockRenderer();
return new Menu(null, elementRef, null, null, renderer, null, null, gestureCtrl, dom, app);
}
export function mockDeepLinkConfig(links?: any[]): DeepLinkConfig {

View File

@ -170,7 +170,7 @@ function _runInDev(fn: Function) {
/** @private */
function _assert(actual: any, reason?: string) {
function _assert(actual: any, reason: string) {
if (!actual && ASSERT_ENABLED === true) {
let message = 'IONIC ASSERT: ' + reason;
console.error(message);