diff --git a/src/animations/animation.ts b/src/animations/animation.ts index e6b0d175e5..aabeb857be 100644 --- a/src/animations/animation.ts +++ b/src/animations/animation.ts @@ -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; } diff --git a/src/components/app/app.ts b/src/components/app/app.ts index 11e47c8615..fc8cf93b0a 100644 --- a/src/components/app/app.ts +++ b/src/components/app/app.ts @@ -89,8 +89,9 @@ export class App { runInDev(() => { // During developement, navPop can be triggered by calling - if (!(_plt.win())['HWBackButton']) { - (_plt.win())['HWBackButton'] = () => { + const win = _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 { 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; diff --git a/src/components/loading/test/basic/pages/e2e-page/e2e-page.ts b/src/components/loading/test/basic/pages/e2e-page/e2e-page.ts index c4de81b41d..cefe15f693 100644 --- a/src/components/loading/test/basic/pages/e2e-page/e2e-page.ts +++ b/src/components/loading/test/basic/pages/e2e-page/e2e-page.ts @@ -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(); + } } diff --git a/src/components/loading/test/basic/pages/e2e-page/main.html b/src/components/loading/test/basic/pages/e2e-page/main.html index 4f86675fe4..182d7cfd9a 100644 --- a/src/components/loading/test/basic/pages/e2e-page/main.html +++ b/src/components/loading/test/basic/pages/e2e-page/main.html @@ -22,6 +22,7 @@ + diff --git a/src/components/menu/menu-controller.ts b/src/components/menu/menu-controller.ts index 698e3280f9..d9fd882907 100644 --- a/src/components/menu/menu-controller.ts +++ b/src/components/menu/menu-controller.ts @@ -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 */ diff --git a/src/components/menu/menu-toggle.ts b/src/components/menu/menu-toggle.ts index 5b1569cf45..ff1a62d36e 100644 --- a/src/components/menu/menu-toggle.ts +++ b/src/components/menu/menu-toggle.ts @@ -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 diff --git a/src/components/menu/menu-types.ts b/src/components/menu/menu-types.ts index 3f9d458a0b..9ca1a59d8b 100644 --- a/src/components/menu/menu-types.ts +++ b/src/components/menu/menu-types.ts @@ -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(); } } diff --git a/src/components/menu/menu.ts b/src/components/menu/menu.ts index c1d2ef064e..aa1503474f 100644 --- a/src/components/menu/menu.ts +++ b/src/components/menu/menu.ts @@ -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 { // 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 */ diff --git a/src/components/menu/test/enable-disable/app.module.ts b/src/components/menu/test/enable-disable/app.module.ts index 05b582d2c7..5b855af438 100644 --- a/src/components/menu/test/enable-disable/app.module.ts +++ b/src/components/menu/test/enable-disable/app.module.ts @@ -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'); } } diff --git a/src/components/menu/test/multiple/app.module.ts b/src/components/menu/test/multiple/app.module.ts index 09d4590a57..c5030a4a64 100644 --- a/src/components/menu/test/multiple/app.module.ts +++ b/src/components/menu/test/multiple/app.module.ts @@ -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'); diff --git a/src/components/menu/test/multiple/main.html b/src/components/menu/test/multiple/main.html index ace2c5772d..47c8783aeb 100644 --- a/src/components/menu/test/multiple/main.html +++ b/src/components/menu/test/multiple/main.html @@ -1,4 +1,4 @@ - + @@ -17,7 +17,7 @@ - + diff --git a/src/components/nav/nav.ts b/src/components/nav/nav.ts index a27bce7999..5007dfac1f 100644 --- a/src/components/nav/nav.ts +++ b/src/components/nav/nav.ts @@ -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'; '
' + '', 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(); + } + } + } diff --git a/src/components/nav/overlay-portal.ts b/src/components/nav/overlay-portal.ts index 96ef72a9ba..ea1f119365 100644 --- a/src/components/nav/overlay-portal.ts +++ b/src/components/nav/overlay-portal.ts @@ -49,5 +49,9 @@ export class OverlayPortal extends NavControllerBase { this._zIndexOffset = (val || 0); } + ngOnDestroy() { + this.destroy(); + } + } diff --git a/src/components/popover/test/basic/pages/main/main.html b/src/components/popover/test/basic/pages/main/main.html index 2aeb0a626a..21a2cebe22 100644 --- a/src/components/popover/test/basic/pages/main/main.html +++ b/src/components/popover/test/basic/pages/main/main.html @@ -62,6 +62,11 @@
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;
+ + + diff --git a/src/components/refresher/refresher.ts b/src/components/refresher/refresher.ts index 893ca3bee3..1f36596b18 100644 --- a/src/components/refresher/refresher.ts +++ b/src/components/refresher/refresher.ts @@ -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); } } diff --git a/src/components/slides/slides.ts b/src/components/slides/slides.ts index 054fe5b5d5..86a9e7aac8 100644 --- a/src/components/slides/slides.ts +++ b/src/components/slides/slides.ts @@ -107,7 +107,7 @@ import { ViewController } from '../../navigation/view-controller'; * ```ts * import { ViewChild } from '@angular/core'; * import { Slides } from 'ionic-angular'; - + * class MyPage { * @ViewChild(Slides) slides: Slides; * @@ -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. * diff --git a/src/components/slides/swiper/swiper-events.ts b/src/components/slides/swiper/swiper-events.ts index f8f20cf3a2..0dc1b8e73e 100644 --- a/src/components/slides/swiper/swiper-events.ts +++ b/src/components/slides/swiper/swiper-events.ts @@ -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; diff --git a/src/components/split-pane/split-pane.ios.scss b/src/components/split-pane/split-pane.ios.scss new file mode 100644 index 0000000000..59daf3d997 --- /dev/null +++ b/src/components/split-pane/split-pane.ios.scss @@ -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; +} diff --git a/src/components/split-pane/split-pane.md.scss b/src/components/split-pane/split-pane.md.scss new file mode 100644 index 0000000000..e9a76e2d9b --- /dev/null +++ b/src/components/split-pane/split-pane.md.scss @@ -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; +} + diff --git a/src/components/split-pane/split-pane.scss b/src/components/split-pane/split-pane.scss new file mode 100644 index 0000000000..539d998234 --- /dev/null +++ b/src/components/split-pane/split-pane.scss @@ -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; + } +} diff --git a/src/components/split-pane/split-pane.ts b/src/components/split-pane/split-pane.ts new file mode 100644 index 0000000000..4c931470f7 --- /dev/null +++ b/src/components/split-pane/split-pane.ts @@ -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 + * + * + * + * + * + * Menu + * + * + * + * + * + * + * + * ``` + * + * 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. + * + * ``` + * + * + * + * + * .... + * + * + * + * + * + * ``` + * + * SplitPane also provides some predefined media queries that can be used. + * + * ```html + * + * + * ... + * + * ``` + * + * + * | 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 + * + * ... + * + * ``` + * + * ```ts + * class MyClass { + * public isLarge = false; + * constructor(){} + * } + * ``` + * + * Or + * + * ```html + * + * ... + * + * ``` + * + * ```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) { + 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 = new EventEmitter(); + + 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; + } + +} diff --git a/src/components/split-pane/split-pane.wp.scss b/src/components/split-pane/split-pane.wp.scss new file mode 100644 index 0000000000..49f91aa36b --- /dev/null +++ b/src/components/split-pane/split-pane.wp.scss @@ -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; +} diff --git a/src/components/split-pane/test/basic/app.module.ts b/src/components/split-pane/test/basic/app.module.ts new file mode 100644 index 0000000000..ce1cd257a7 --- /dev/null +++ b/src/components/split-pane/test/basic/app.module.ts @@ -0,0 +1,120 @@ +import { Component, NgModule } from '@angular/core'; +import { IonicApp, IonicModule, NavController, MenuController, SplitPane } from '../../../../../ionic-angular'; + + +@Component({ + template: ` + + Navigation + + + + Hola 1 + Hola 2 + Hola 3 + + Hola + Hola + Hola + + + + ` +}) +export class SidePage { + constructor(public navCtrl: NavController) { } + push() { + this.navCtrl.push(SidePage); + } +} + +@Component({ + template: ` + + + + Page 2 + + + +

Page 2

+
+ ` +}) +export class E2EPage2 {} + + +@Component({ + template: ` + + + + Navigation + + + +

Page 1

+ + +
+
+
+
+ +
+ ` +}) +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 {} + diff --git a/src/components/split-pane/test/basic/e2e.ts b/src/components/split-pane/test/basic/e2e.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/components/split-pane/test/basic/e2e.ts @@ -0,0 +1 @@ + diff --git a/src/components/split-pane/test/basic/main.html b/src/components/split-pane/test/basic/main.html new file mode 100644 index 0000000000..0ccaf46b3d --- /dev/null +++ b/src/components/split-pane/test/basic/main.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/split-pane/test/menus/app.module.ts b/src/components/split-pane/test/menus/app.module.ts new file mode 100644 index 0000000000..7d817418ce --- /dev/null +++ b/src/components/split-pane/test/menus/app.module.ts @@ -0,0 +1,102 @@ +import { Component, NgModule } from '@angular/core'; +import { IonicApp, IonicModule, NavController, MenuController, SplitPane } from '../../../../../ionic-angular'; + +@Component({ + template: ` + + + + Page 2 + + + +

Page 2

+
+ ` +}) +export class E2EPage2 {} + + +@Component({ + template: ` + + + + Navigation + + + +

Page 1

+ + + + + + +
+
+
+
+ +
+ ` +}) +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 {} + diff --git a/src/components/split-pane/test/menus/e2e.ts b/src/components/split-pane/test/menus/e2e.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/components/split-pane/test/menus/e2e.ts @@ -0,0 +1 @@ + diff --git a/src/components/split-pane/test/menus/main.html b/src/components/split-pane/test/menus/main.html new file mode 100644 index 0000000000..353e613197 --- /dev/null +++ b/src/components/split-pane/test/menus/main.html @@ -0,0 +1,52 @@ + + + + + + + Menu 1 + + + + + + Example + + + + + + + + + + + Menu 2 + + + + + + Example + + + + + + + + + + Menu 3 + + + + + + Example + + + + + + \ No newline at end of file diff --git a/src/components/split-pane/test/nested/app.module.ts b/src/components/split-pane/test/nested/app.module.ts new file mode 100644 index 0000000000..30827fcb39 --- /dev/null +++ b/src/components/split-pane/test/nested/app.module.ts @@ -0,0 +1,162 @@ +import { Component, NgModule } from '@angular/core'; +import { IonicApp, IonicModule, NavController } from '../../../../../ionic-angular'; + + +@Component({ + template: ` + + Nested 1 + + + +
+
+
+
+
+ ` +}) +export class E2ENested { + constructor( + public navCtrl: NavController, + ) { } + + push() { + this.navCtrl.push(E2ENested); + } +} + +@Component({ + template: ` + + Nested 2 + + + +
+
+
+
+
+ ` +}) +export class E2ENested2 { + constructor( + public navCtrl: NavController, + ) { } + + push() { + this.navCtrl.push(E2ENested2); + } +} + +@Component({ + template: ` + + + + Nested 3 + + + + +
+
+
+
+
+ ` +}) +export class E2ENested3 { + constructor( + public navCtrl: NavController, + ) { } + + push() { + this.navCtrl.push(E2ENested3); + } +} + + +@Component({ + template: ` + + Navigation + + + + Hola + Hola + Hola + + Hola + Hola + Hola + + + + ` +}) +export class SidePage { + constructor(public navCtrl: NavController) { } + push() { + this.navCtrl.push(SidePage); + } +} + +@Component({ + template: ` + + + + + + + + + + ` +}) +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 {} + diff --git a/src/components/split-pane/test/nested/e2e.ts b/src/components/split-pane/test/nested/e2e.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/components/split-pane/test/nested/e2e.ts @@ -0,0 +1 @@ + diff --git a/src/components/split-pane/test/nested/main.html b/src/components/split-pane/test/nested/main.html new file mode 100644 index 0000000000..b7c24ede8d --- /dev/null +++ b/src/components/split-pane/test/nested/main.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/components/split-pane/test/tabs/app.module.ts b/src/components/split-pane/test/tabs/app.module.ts new file mode 100644 index 0000000000..c28f385002 --- /dev/null +++ b/src/components/split-pane/test/tabs/app.module.ts @@ -0,0 +1,98 @@ +import { Component, NgModule } from '@angular/core'; +import { IonicApp, IonicModule, NavController, MenuController } from '../../../../../ionic-angular'; + + +@Component({ + template: ` + + Navigation + + + + + +

Slide 1

+
+ + +

Slide 2

+
+ + +

Slide 3

+
+ +
+
+ ` +}) +export class SidePage { + constructor(public navCtrl: NavController) { } + push() { + this.navCtrl.push(SidePage); + } +} + + +@Component({ + template: ` + + + + Navigation + + + + +
+
+
+
+ +
+ ` +}) +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 {} + diff --git a/src/components/split-pane/test/tabs/e2e.ts b/src/components/split-pane/test/tabs/e2e.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/components/split-pane/test/tabs/e2e.ts @@ -0,0 +1 @@ + diff --git a/src/components/split-pane/test/tabs/main.html b/src/components/split-pane/test/tabs/main.html new file mode 100644 index 0000000000..342508f5a6 --- /dev/null +++ b/src/components/split-pane/test/tabs/main.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/components/tabs/tab-button.ts b/src/components/tabs/tab-button.ts index 7ef63750f6..fe33a89e12 100644 --- a/src/components/tabs/tab-button.ts +++ b/src/components/tabs/tab-button.ts @@ -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: + '' + + '{{tab.tabTitle}}' + + '{{tab.tabBadge}}' + + '
', 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 { diff --git a/src/components/tabs/tab.ts b/src/components/tabs/tab.ts index efdd9fc995..bb5549feb9 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -314,17 +314,24 @@ export class Tab extends NavControllerBase { // to refresh the tabbar and content dimensions to be sure // they're lined up correctly this._dom.read(() => { - const active = this.getActive(); - if (!active) { - return; - } - const content = active.getIONContent(); - content && content.resize(); + this.resize(); }); done(true); } } + /** + * @private + */ + resize() { + const active = this.getActive(); + if (!active) { + return; + } + const content = active.getIONContent(); + content && content.resize(); + } + /** * @private */ @@ -385,7 +392,7 @@ export class Tab extends NavControllerBase { /** * @private */ - destroy() { + ngOnDestroy() { this.destroy(); } diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 956cf3da77..665ec5e995 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -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: '' + '' + '
', 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; diff --git a/src/index.ts b/src/index.ts index 886adc2066..024b037792 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/navigation/nav-controller-base.ts b/src/navigation/nav-controller-base.ts index 5408bcae35..be0f861452 100644 --- a/src/navigation/nav-controller-base.ts +++ b/src/navigation/nav-controller-base.ts @@ -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; diff --git a/src/navigation/nav-controller.ts b/src/navigation/nav-controller.ts index cb9959959b..1b77e61b19 100644 --- a/src/navigation/nav-controller.ts +++ b/src/navigation/nav-controller.ts @@ -609,4 +609,9 @@ export abstract class NavController { * @private */ abstract registerChildNav(nav: any): void; + + /** + * @private + */ + abstract resize(): void; } diff --git a/src/navigation/nav-util.ts b/src/navigation/nav-util.ts index 3f81d9b8f0..158a592429 100644 --- a/src/navigation/nav-util.ts +++ b/src/navigation/nav-util.ts @@ -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; diff --git a/src/navigation/test/view-controller.spec.ts b/src/navigation/test/view-controller.spec.ts index e4ed040d47..3cf991666d 100644 --- a/src/navigation/test/view-controller.spec.ts +++ b/src/navigation/test/view-controller.spec.ts @@ -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); }); diff --git a/src/navigation/view-controller.ts b/src/navigation/view-controller.ts index 5bd3ca5090..e40ce51606 100644 --- a/src/navigation/view-controller.ts +++ b/src/navigation/view-controller.ts @@ -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; _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 { 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,14 +263,12 @@ 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) { - this._isHidden = !shouldShow; - let value = (shouldShow ? null : ''); - // ******** DOM WRITE **************** - renderer.setElementAttribute(this.pageRef().nativeElement, 'hidden', value); - } + // 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); } } @@ -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) { diff --git a/src/themes/ionic.components.scss b/src/themes/ionic.components.scss index 9c6bdbaf04..3477a394ce 100644 --- a/src/themes/ionic.components.scss +++ b/src/themes/ionic.components.scss @@ -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", diff --git a/src/util/mock-providers.ts b/src/util/mock-providers.ts index 652c826393..d5d99531b7 100644 --- a/src/util/mock-providers.ts +++ b/src/util/mock-providers.ts @@ -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 { @@ -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 { diff --git a/src/util/util.ts b/src/util/util.ts index bfbe21c6f1..b2fb6488e8 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -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);