diff --git a/src/components.ts b/src/components.ts index 1ba48ed55d..8526034e73 100644 --- a/src/components.ts +++ b/src/components.ts @@ -1,5 +1,7 @@ -export { ActionSheet, ActionSheetOptions } from './components/action-sheet/action-sheet'; -export { Alert, AlertOptions, AlertInputOptions } from './components/alert/alert'; +export { ActionSheet, ActionSheetController } from './components/action-sheet/action-sheet'; +export { ActionSheetOptions } from './components/action-sheet/action-sheet-options'; +export { Alert, AlertController } from './components/alert/alert'; +export { AlertOptions, AlertInputOptions } from './components/alert/alert-options'; export { App } from './components/app/app'; export { Backdrop } from './components/backdrop/backdrop'; export { Badge } from './components/badge/badge'; @@ -17,23 +19,28 @@ export { ItemReorder } from './components/item/item-reorder'; export { ItemSliding, ItemOptions, ItemSideFlags } from './components/item/item-sliding'; export { Label } from './components/label/label'; export { List, ListHeader } from './components/list/list'; -export { Loading, LoadingOptions } from './components/loading/loading'; +export { Loading, LoadingController } from './components/loading/loading'; +export { LoadingOptions } from './components/loading/loading-options'; export { Menu } from './components/menu/menu'; export { MenuClose } from './components/menu/menu-close'; export { MenuController } from './components/menu/menu-controller'; export { MenuToggle } from './components/menu/menu-toggle'; export { MenuType } from './components/menu/menu-types'; -export { Modal, ModalOptions } from './components/modal/modal'; +export { Modal, ModalController } from './components/modal/modal'; +export { ModalOptions } from './components/modal/modal-options'; export { Nav } from './components/nav/nav'; -export { NavController, NavOptions } from './components/nav/nav-controller'; +export { NavController } from './components/nav/nav-controller'; +export { NavOptions } from './components/nav/nav-options'; export { NavParams } from './components/nav/nav-params'; export { NavPop } from './components/nav/nav-pop'; export { NavPush } from './components/nav/nav-push'; export { ViewController } from './components/nav/view-controller'; export { Navbar, NavbarTemplate } from './components/navbar/navbar'; export { Option } from './components/option/option'; -export { Picker, PickerOptions, PickerColumn, PickerColumnOption } from './components/picker/picker'; -export { Popover, PopoverOptions } from './components/popover/popover'; +export { Picker, PickerController } from './components/picker/picker'; +export { PickerOptions } from './components/picker/picker-options'; +export { Popover, PopoverController } from './components/popover/popover'; +export { PopoverOptions } from './components/popover/popover-options'; export { RadioButton } from './components/radio/radio-button'; export { RadioGroup } from './components/radio/radio-group'; export { Range, RangeKnob, ClientRect } from './components/range/range'; @@ -49,7 +56,8 @@ export { Spinner } from './components/spinner/spinner'; export { Tab } from './components/tabs/tab'; export { Tabs } from './components/tabs/tabs'; export { TapClick, isActivatable } from './components/tap-click/tap-click'; -export { Toast, ToastOptions } from './components/toast/toast'; +export { Toast, ToastController } from './components/toast/toast'; +export { ToastOptions } from './components/toast/toast-options'; export { Toggle } from './components/toggle/toggle'; export { Toolbar, ToolbarBase, Header, Footer } from './components/toolbar/toolbar'; export { VirtualScroll } from './components/virtual-scroll/virtual-scroll'; diff --git a/src/components/action-sheet/action-sheet-component.ts b/src/components/action-sheet/action-sheet-component.ts new file mode 100644 index 0000000000..91affbf9f3 --- /dev/null +++ b/src/components/action-sheet/action-sheet-component.ts @@ -0,0 +1,262 @@ +import { Component, Renderer, ElementRef, HostListener, ViewEncapsulation } from '@angular/core'; + +import { Animation } from '../../animations/animation'; +import { Config } from '../../config/config'; +import { Form } from '../../util/form'; +import { Key } from '../../util/key'; +import { NavParams } from '../nav/nav-params'; +import { Transition, TransitionOptions } from '../../transitions/transition'; +import { ViewController } from '../nav/view-controller'; + + +/** + * @private + */ +@Component({ + selector: 'ion-action-sheet', + template: ` + +
+
+
+
{{d.title}}
+
{{d.subTitle}}
+ +
+
+ +
+
+
+ `, + host: { + 'role': 'dialog', + '[attr.aria-labelledby]': 'hdrId', + '[attr.aria-describedby]': 'descId' + }, + encapsulation: ViewEncapsulation.None, +}) +export class ActionSheetCmp { + private d: any; + private descId: string; + private enabled: boolean; + private hdrId: string; + private id: number; + + constructor( + private _viewCtrl: ViewController, + private _config: Config, + private _elementRef: ElementRef, + private _form: Form, + params: NavParams, + renderer: Renderer + ) { + this.d = params.data; + + if (this.d.cssClass) { + renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true); + } + + this.id = (++actionSheetIds); + if (this.d.title) { + this.hdrId = 'acst-hdr-' + this.id; + } + if (this.d.subTitle) { + this.descId = 'acst-subhdr-' + this.id; + } + } + + ionViewLoaded() { + // normalize the data + let buttons: any[] = []; + + this.d.buttons.forEach((button: any) => { + if (typeof button === 'string') { + button = { text: button }; + } + if (!button.cssClass) { + button.cssClass = ''; + } + + if (button.role === 'cancel') { + this.d.cancelButton = button; + + } else { + if (button.role === 'destructive') { + button.cssClass = (button.cssClass + ' ' || '') + 'action-sheet-destructive'; + } else if (button.role === 'selected') { + button.cssClass = (button.cssClass + ' ' || '') + 'action-sheet-selected'; + } + buttons.push(button); + } + }); + + this.d.buttons = buttons; + } + + ionViewDidEnter() { + this._form.focusOut(); + + let focusableEle = this._elementRef.nativeElement.querySelector('button'); + if (focusableEle) { + focusableEle.focus(); + } + this.enabled = true; + } + + @HostListener('body:keyup', ['$event']) + private _keyUp(ev: KeyboardEvent) { + if (this.enabled && this._viewCtrl.isLast()) { + if (ev.keyCode === Key.ESCAPE) { + console.debug('actionsheet, escape button'); + this.bdClick(); + } + } + } + + click(button: any, dismissDelay?: number) { + if (! this.enabled ) { + return; + } + + let shouldDismiss = true; + + if (button.handler) { + // a handler has been provided, execute it + if (button.handler() === false) { + // if the return value of the handler is false then do not dismiss + shouldDismiss = false; + } + } + + if (shouldDismiss) { + setTimeout(() => { + this.dismiss(button.role); + }, dismissDelay || this._config.get('pageTransitionDelay')); + } + } + + bdClick() { + if (this.enabled && this.d.enableBackdropDismiss) { + if (this.d.cancelButton) { + this.click(this.d.cancelButton, 1); + + } else { + this.dismiss('backdrop'); + } + } + } + + dismiss(role: any): Promise { + return this._viewCtrl.dismiss(null, role); + } +} + + +class ActionSheetSlideIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); + + backdrop.fromTo('opacity', 0.01, 0.4); + wrapper.fromTo('translateY', '100%', '0%'); + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper); + } +} +Transition.register('action-sheet-slide-in', ActionSheetSlideIn); + + +class ActionSheetSlideOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); + + backdrop.fromTo('opacity', 0.4, 0); + wrapper.fromTo('translateY', '0%', '100%'); + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(300).add(backdrop).add(wrapper); + } +} +Transition.register('action-sheet-slide-out', ActionSheetSlideOut); + + +class ActionSheetMdSlideIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); + + backdrop.fromTo('opacity', 0.01, 0.26); + wrapper.fromTo('translateY', '100%', '0%'); + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper); + } +} +Transition.register('action-sheet-md-slide-in', ActionSheetMdSlideIn); + + +class ActionSheetMdSlideOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); + + backdrop.fromTo('opacity', 0.26, 0); + wrapper.fromTo('translateY', '0%', '100%'); + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(backdrop).add(wrapper); + } +} +Transition.register('action-sheet-md-slide-out', ActionSheetMdSlideOut); + +class ActionSheetWpSlideIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); + + backdrop.fromTo('opacity', 0.01, 0.16); + wrapper.fromTo('translateY', '100%', '0%'); + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper); + } +} +Transition.register('action-sheet-wp-slide-in', ActionSheetWpSlideIn); + + +class ActionSheetWpSlideOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); + + backdrop.fromTo('opacity', 0.1, 0); + wrapper.fromTo('translateY', '0%', '100%'); + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(backdrop).add(wrapper); + } +} +Transition.register('action-sheet-wp-slide-out', ActionSheetWpSlideOut); + +let actionSheetIds = -1; diff --git a/src/components/action-sheet/action-sheet-options.ts b/src/components/action-sheet/action-sheet-options.ts new file mode 100644 index 0000000000..669367262e --- /dev/null +++ b/src/components/action-sheet/action-sheet-options.ts @@ -0,0 +1,8 @@ + +export interface ActionSheetOptions { + title?: string; + subTitle?: string; + cssClass?: string; + buttons?: Array; + enableBackdropDismiss?: boolean; +} diff --git a/src/components/action-sheet/action-sheet.ts b/src/components/action-sheet/action-sheet.ts index 90a7e87ed1..acd4384276 100644 --- a/src/components/action-sheet/action-sheet.ts +++ b/src/components/action-sheet/action-sheet.ts @@ -1,17 +1,85 @@ -import {Component, Renderer, ElementRef, HostListener, ViewEncapsulation} from '@angular/core'; +import { Injectable } from '@angular/core'; -import {Animation} from '../../animations/animation'; -import {Transition, TransitionOptions} from '../../transitions/transition'; -import {Config} from '../../config/config'; -import {Icon} from '../icon/icon'; -import {isPresent} from '../../util/util'; -import {Key} from '../../util/key'; -import {NavParams} from '../nav/nav-params'; -import {ViewController} from '../nav/view-controller'; +import { ActionSheetCmp } from './action-sheet-component'; +import { ActionSheetOptions } from './action-sheet-options'; +import { App } from '../app/app'; +import { isPresent } from '../../util/util'; +import { NavOptions } from '../nav/nav-options'; +import { ViewController } from '../nav/view-controller'; + +/** + * @private + */ +export class ActionSheet extends ViewController { + private _app: App; + + constructor(app: App, opts: ActionSheetOptions) { + opts.buttons = opts.buttons || []; + opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; + + super(ActionSheetCmp, opts); + this._app = app; + this.isOverlay = true; + + // by default, actionsheets should not fire lifecycle events of other views + // for example, when an actionsheets enters, the current active view should + // not fire its lifecycle events because it's not conceptually leaving + this.fireOtherLifecycles = false; + } + + /** + * @private + */ + getTransitionName(direction: string) { + let key = 'actionSheet' + (direction === 'back' ? 'Leave' : 'Enter'); + return this._nav && this._nav.config.get(key); + } + + /** + * @param {string} title Action sheet title + */ + setTitle(title: string) { + this.data.title = title; + } + + /** + * @param {string} subTitle Action sheet subtitle + */ + setSubTitle(subTitle: string) { + this.data.subTitle = subTitle; + } + + /** + * @param {object} button Action sheet button + */ + addButton(button: any) { + this.data.buttons.push(button); + } + + /** + * Present the action sheet instance. + * + * @param {NavOptions} [opts={}] Nav options to go with this transition. + * @returns {Promise} Returns a promise which is resolved when the transition has completed. + */ + present(navOptions: NavOptions = {}) { + return this._app.present(this, navOptions); + } + + /** + * @private + * DEPRECATED: Please inject ActionSheetController instead + */ + private static create(opt: any) { + // deprecated warning: added beta.11 2016-06-27 + console.warn('ActionSheet.create(..) has been deprecated. Please inject ActionSheetController instead'); + } + +} /** - * @name ActionSheet + * @name ActionSheetController * @description * An Action Sheet is a dialog that lets the user choose from a set of * options. It appears on top of the app's content, and must be manually @@ -40,12 +108,12 @@ import {ViewController} from '../nav/view-controller'; * * @usage * ```ts - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private actionSheetCtrl: ActionSheetController) { + * * } * * presentActionSheet() { - * let actionSheet = ActionSheet.create({ + * let actionSheet = this.actionSheetCtrl.create({ * title: 'Modify your album', * buttons: [ * { @@ -71,7 +139,7 @@ import {ViewController} from '../nav/view-controller'; * ] * }); * - * this.nav.present(actionSheet); + * actionSheet.present(); * } * ``` * @@ -95,7 +163,7 @@ import {ViewController} from '../nav/view-controller'; * out first, *then* start the next transition. * * ```ts - * let actionSheet = ActionSheet.create({ + * let actionSheet = this.actionSheetCtrl.create({ * title: 'Hello', * buttons: [{ * text: 'Ok', @@ -119,7 +187,7 @@ import {ViewController} from '../nav/view-controller'; * }] * }); * - * this.nav.present(actionSheet); + * actionSheet.present(); * ``` * * It's important to note that the handler returns `false`. A feature of @@ -134,344 +202,38 @@ import {ViewController} from '../nav/view-controller'; * @demo /docs/v2/demos/action-sheet/ * @see {@link /docs/v2/components#action-sheets ActionSheet Component Docs} */ -export class ActionSheet extends ViewController { +@Injectable() +export class ActionSheetController { - constructor(opts: ActionSheetOptions = {}) { - opts.buttons = opts.buttons || []; - opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; + constructor(private _app: App) {} - super(ActionSheetCmp, opts); - this.isOverlay = true; - - // by default, actionsheets should not fire lifecycle events of other views - // for example, when an actionsheets enters, the current active view should - // not fire its lifecycle events because it's not conceptually leaving - this.fireOtherLifecycles = false; - } - - /** - * @private + /** + * Open an action sheet with the following options + * + * | Option | Type | Description | + * |-----------------------|------------|-----------------------------------------------------------------| + * | title |`string` | The title for the actionsheet | + * | subTitle |`string` | The sub-title for the actionsheet | + * | cssClass |`string` | An additional class for custom styles | + * | enableBackdropDismiss |`boolean` | If the actionsheet should close when the user taps the backdrop | + * | buttons |`array`| An array of buttons to display | + * + * For the buttons: + * + * | Option | Type | Description | + * |----------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------| + * | text | `string` | The buttons text | + * | icon | `icon` | The buttons icons | + * | handler | `any` | An express the button should evaluate | + * | cssClass | `string` | An additional class for custom styles | + * | role | `string` | How the button should be displayed, `destructive` or `cancel`. If not role is provided, it will display the button without any additional styles | + * + * + * + * @param {ActionSheetOptions} opts Action sheet options */ - getTransitionName(direction: string) { - let key = 'actionSheet' + (direction === 'back' ? 'Leave' : 'Enter'); - return this._nav && this._nav.config.get(key); - } - - /** - * @param {string} title Action sheet title - */ - setTitle(title: string) { - this.data.title = title; - } - - /** - * @param {string} subTitle Action sheet subtitle - */ - setSubTitle(subTitle: string) { - this.data.subTitle = subTitle; - } - - /** - * @param {object} button Action sheet button - */ - addButton(button: any) { - this.data.buttons.push(button); - } - - /** - * Open an action sheet with the following options - * - * | Option | Type | Description | - * |-----------------------|------------|-----------------------------------------------------------------| - * | title |`string` | The title for the actionsheet | - * | subTitle |`string` | The sub-title for the actionsheet | - * | cssClass |`string` | An additional class for custom styles | - * | enableBackdropDismiss |`boolean` | If the actionsheet should close when the user taps the backdrop | - * | buttons |`array`| An array of buttons to display | - * - * For the buttons: - * - * | Option | Type | Description | - * |----------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------| - * | text | `string` | The buttons text | - * | icon | `icon` | The buttons icons | - * | handler | `any` | An express the button should evaluate | - * | cssClass | `string` | An additional class for custom styles | - * | role | `string` | How the button should be displayed, `destructive` or `cancel`. If not role is provided, it will display the button without any additional styles | - * - * - * - * @param {object} opts Action sheet options - */ - static create(opts: ActionSheetOptions = {}) { - return new ActionSheet(opts); - } - - } - -/** -* @private -*/ -@Component({ - selector: 'ion-action-sheet', - template: - '' + - '
' + - '
' + - '
' + - '
{{d.title}}
' + - '
{{d.subTitle}}
' + - '' + - '
' + - '
' + - '' + - '
' + - '
' + - '
', - host: { - 'role': 'dialog', - '[attr.aria-labelledby]': 'hdrId', - '[attr.aria-describedby]': 'descId' - }, - encapsulation: ViewEncapsulation.None, -}) -class ActionSheetCmp { - private d: any; - private descId: string; - private enabled: boolean; - private hdrId: string; - private id: number; - - constructor( - private _viewCtrl: ViewController, - private _config: Config, - private _elementRef: ElementRef, - params: NavParams, - renderer: Renderer - ) { - this.d = params.data; - - if (this.d.cssClass) { - renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true); - } - - this.id = (++actionSheetIds); - if (this.d.title) { - this.hdrId = 'acst-hdr-' + this.id; - } - if (this.d.subTitle) { - this.descId = 'acst-subhdr-' + this.id; - } + create(opts: ActionSheetOptions = {}): ActionSheet { + return new ActionSheet(this._app, opts); } - ionViewLoaded() { - // normalize the data - let buttons: any[] = []; - - this.d.buttons.forEach((button: any) => { - if (typeof button === 'string') { - button = { text: button }; - } - if (!button.cssClass) { - button.cssClass = ''; - } - - // deprecated warning - if (button.style) { - console.warn('Action sheet "style" property has been renamed to "role"'); - button.role = button.style; - } - - if (button.role === 'cancel') { - this.d.cancelButton = button; - - } else { - if (button.role === 'destructive') { - button.cssClass = (button.cssClass + ' ' || '') + 'action-sheet-destructive'; - } else if (button.role === 'selected') { - button.cssClass = (button.cssClass + ' ' || '') + 'action-sheet-selected'; - } - buttons.push(button); - } - }); - - this.d.buttons = buttons; - } - - ionViewDidEnter() { - let activeElement: any = document.activeElement; - if (document.activeElement) { - activeElement.blur(); - } - - let focusableEle = this._elementRef.nativeElement.querySelector('button'); - if (focusableEle) { - focusableEle.focus(); - } - this.enabled = true; - } - - @HostListener('body:keyup', ['$event']) - private _keyUp(ev: KeyboardEvent) { - if (this.enabled && this._viewCtrl.isLast()) { - if (ev.keyCode === Key.ESCAPE) { - console.debug('actionsheet, escape button'); - this.bdClick(); - } - } - } - - click(button: any, dismissDelay?: number) { - if (! this.enabled ) { - return; - } - - let shouldDismiss = true; - - if (button.handler) { - // a handler has been provided, execute it - if (button.handler() === false) { - // if the return value of the handler is false then do not dismiss - shouldDismiss = false; - } - } - - if (shouldDismiss) { - setTimeout(() => { - this.dismiss(button.role); - }, dismissDelay || this._config.get('pageTransitionDelay')); - } - } - - bdClick() { - if (this.enabled && this.d.enableBackdropDismiss) { - if (this.d.cancelButton) { - this.click(this.d.cancelButton, 1); - - } else { - this.dismiss('backdrop'); - } - } - } - - dismiss(role: any): Promise { - return this._viewCtrl.dismiss(null, role); - } } - -export interface ActionSheetOptions { - title?: string; - subTitle?: string; - cssClass?: string; - buttons?: Array; - enableBackdropDismiss?: boolean; -} - - -class ActionSheetSlideIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); - - backdrop.fromTo('opacity', 0.01, 0.4); - wrapper.fromTo('translateY', '100%', '0%'); - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper); - } -} -Transition.register('action-sheet-slide-in', ActionSheetSlideIn); - - -class ActionSheetSlideOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); - - backdrop.fromTo('opacity', 0.4, 0); - wrapper.fromTo('translateY', '0%', '100%'); - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(300).add(backdrop).add(wrapper); - } -} -Transition.register('action-sheet-slide-out', ActionSheetSlideOut); - - -class ActionSheetMdSlideIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); - - backdrop.fromTo('opacity', 0.01, 0.26); - wrapper.fromTo('translateY', '100%', '0%'); - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper); - } -} -Transition.register('action-sheet-md-slide-in', ActionSheetMdSlideIn); - - -class ActionSheetMdSlideOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); - - backdrop.fromTo('opacity', 0.26, 0); - wrapper.fromTo('translateY', '0%', '100%'); - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(backdrop).add(wrapper); - } -} -Transition.register('action-sheet-md-slide-out', ActionSheetMdSlideOut); - -class ActionSheetWpSlideIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); - - backdrop.fromTo('opacity', 0.01, 0.16); - wrapper.fromTo('translateY', '100%', '0%'); - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper); - } -} -Transition.register('action-sheet-wp-slide-in', ActionSheetWpSlideIn); - - -class ActionSheetWpSlideOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.action-sheet-wrapper')); - - backdrop.fromTo('opacity', 0.1, 0); - wrapper.fromTo('translateY', '0%', '100%'); - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(backdrop).add(wrapper); - } -} -Transition.register('action-sheet-wp-slide-out', ActionSheetWpSlideOut); - -let actionSheetIds = -1; diff --git a/src/components/alert/alert-component.ts b/src/components/alert/alert-component.ts new file mode 100644 index 0000000000..df33f69c71 --- /dev/null +++ b/src/components/alert/alert-component.ts @@ -0,0 +1,416 @@ +import {Component, ElementRef, Renderer, HostListener, ViewEncapsulation} from '@angular/core'; + +import {Animation} from '../../animations/animation'; +import {Config} from '../../config/config'; +import {isPresent} from '../../util/util'; +import {Key} from '../../util/key'; +import {NavParams} from '../nav/nav-params'; +import {Transition, TransitionOptions} from '../../transitions/transition'; +import {ViewController} from '../nav/view-controller'; + + +/** + * @private + */ +@Component({ + selector: 'ion-alert', + template: ` + +
+
+

+

+
+
+
+ + + + + + + +
+
+ +
+
+ `, + host: { + 'role': 'dialog', + '[attr.aria-labelledby]': 'hdrId', + '[attr.aria-describedby]': 'descId' + }, + encapsulation: ViewEncapsulation.None, +}) +export class AlertCmp { + private activeId: string; + private descId: string; + private d: { + cssClass?: string; + message?: string; + subTitle?: string; + buttons?: any[]; + inputs?: any[]; + enableBackdropDismiss?: boolean; + }; + private enabled: boolean; + private hdrId: string; + private id: number; + private inputType: string; + private lastClick: number; + private msgId: string; + private subHdrId: string; + + constructor( + private _viewCtrl: ViewController, + private _elementRef: ElementRef, + private _config: Config, + params: NavParams, + renderer: Renderer + ) { + this.d = params.data; + + if (this.d.cssClass) { + this.d.cssClass.split(' ').forEach(cssClass => { + renderer.setElementClass(_elementRef.nativeElement, cssClass, true); + }); + } + + this.id = (++alertIds); + this.descId = ''; + this.hdrId = 'alert-hdr-' + this.id; + this.subHdrId = 'alert-subhdr-' + this.id; + this.msgId = 'alert-msg-' + this.id; + this.activeId = ''; + this.lastClick = 0; + + if (this.d.message) { + this.descId = this.msgId; + + } else if (this.d.subTitle) { + this.descId = this.subHdrId; + } + + if (!this.d.message) { + this.d.message = ''; + } + } + + ionViewLoaded() { + // normalize the data + let data = this.d; + + data.buttons = data.buttons.map(button => { + if (typeof button === 'string') { + return { text: button }; + } + return button; + }); + + data.inputs = data.inputs.map((input, index) => { + return { + type: input.type || 'text', + name: isPresent(input.name) ? input.name : index, + placeholder: isPresent(input.placeholder) ? input.placeholder : '', + value: isPresent(input.value) ? input.value : '', + label: input.label, + checked: !!input.checked, + id: 'alert-input-' + this.id + '-' + index + }; + }); + + + // An alert can be created with several different inputs. Radios, + // checkboxes and inputs are all accepted, but they cannot be mixed. + let inputTypes: any[] = []; + data.inputs.forEach(input => { + if (inputTypes.indexOf(input.type) < 0) { + inputTypes.push(input.type); + } + }); + + if (inputTypes.length > 1 && (inputTypes.indexOf('checkbox') > -1 || inputTypes.indexOf('radio') > -1)) { + console.warn('Alert cannot mix input types: ' + (inputTypes.join('/')) + '. Please see alert docs for more info.'); + } + + this.inputType = inputTypes.length ? inputTypes[0] : null; + + let checkedInput = this.d.inputs.find(input => input.checked); + if (checkedInput) { + this.activeId = checkedInput.id; + } + } + + @HostListener('body:keyup', ['$event']) + private _keyUp(ev: KeyboardEvent) { + if (this.enabled && this._viewCtrl.isLast()) { + if (ev.keyCode === Key.ENTER) { + if (this.lastClick + 1000 < Date.now()) { + // do not fire this click if there recently was already a click + // this can happen when the button has focus and used the enter + // key to click the button. However, both the click handler and + // this keyup event will fire, so only allow one of them to go. + console.debug('alert, enter button'); + let button = this.d.buttons[this.d.buttons.length - 1]; + this.btnClick(button); + } + + } else if (ev.keyCode === Key.ESCAPE) { + console.debug('alert, escape button'); + this.bdClick(); + } + } + } + + ionViewDidEnter() { + let activeElement: any = document.activeElement; + if (document.activeElement) { + activeElement.blur(); + } + + let focusableEle = this._elementRef.nativeElement.querySelector('input,button'); + if (focusableEle) { + focusableEle.focus(); + } + this.enabled = true; + } + + btnClick(button: any, dismissDelay?: number) { + if (!this.enabled) { + return; + } + + // keep the time of the most recent button click + this.lastClick = Date.now(); + + let shouldDismiss = true; + + if (button.handler) { + // a handler has been provided, execute it + // pass the handler the values from the inputs + if (button.handler(this.getValues()) === false) { + // if the return value of the handler is false then do not dismiss + shouldDismiss = false; + } + } + + if (shouldDismiss) { + setTimeout(() => { + this.dismiss(button.role); + }, dismissDelay || this._config.get('pageTransitionDelay')); + } + } + + rbClick(checkedInput: any) { + if (this.enabled) { + this.d.inputs.forEach(input => { + input.checked = (checkedInput === input); + }); + this.activeId = checkedInput.id; + } + } + + cbClick(checkedInput: any) { + if (this.enabled) { + checkedInput.checked = !checkedInput.checked; + } + } + + bdClick() { + if (this.enabled && this.d.enableBackdropDismiss) { + let cancelBtn = this.d.buttons.find(b => b.role === 'cancel'); + if (cancelBtn) { + this.btnClick(cancelBtn, 1); + + } else { + this.dismiss('backdrop'); + } + } + } + + dismiss(role: any): Promise { + return this._viewCtrl.dismiss(this.getValues(), role); + } + + getValues() { + if (this.inputType === 'radio') { + // this is an alert with radio buttons (single value select) + // return the one value which is checked, otherwise undefined + let checkedInput = this.d.inputs.find(i => i.checked); + return checkedInput ? checkedInput.value : undefined; + } + + if (this.inputType === 'checkbox') { + // this is an alert with checkboxes (multiple value select) + // return an array of all the checked values + return this.d.inputs.filter(i => i.checked).map(i => i.value); + } + + // this is an alert with text inputs + // return an object of all the values with the input name as the key + let values: {[k: string]: string} = {}; + this.d.inputs.forEach(i => { + values[i.name] = i.value; + }); + return values; + } +} + + +/** + * Animations for alerts + */ +class AlertPopIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.alert-wrapper')); + + wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.1, 1); + backdrop.fromTo('opacity', 0.01, 0.3); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } +} +Transition.register('alert-pop-in', AlertPopIn); + + +class AlertPopOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.alert-wrapper')); + + wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 0.9); + backdrop.fromTo('opacity', 0.3, 0); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } +} +Transition.register('alert-pop-out', AlertPopOut); + + +class AlertMdPopIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.alert-wrapper')); + + wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.1, 1); + backdrop.fromTo('opacity', 0.01, 0.5); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } +} +Transition.register('alert-md-pop-in', AlertMdPopIn); + + +class AlertMdPopOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.alert-wrapper')); + + wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 0.9); + backdrop.fromTo('opacity', 0.5, 0); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } +} +Transition.register('alert-md-pop-out', AlertMdPopOut); + + + +class AlertWpPopIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.alert-wrapper')); + + wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.3, 1); + backdrop.fromTo('opacity', 0.01, 0.5); + + this + .easing('cubic-bezier(0,0 0.05,1)') + .duration(200) + .add(backdrop) + .add(wrapper); + } +} +Transition.register('alert-wp-pop-in', AlertWpPopIn); + + +class AlertWpPopOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.alert-wrapper')); + + wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 1.3); + backdrop.fromTo('opacity', 0.5, 0); + + this + .easing('ease-out') + .duration(150) + .add(backdrop) + .add(wrapper); + } +} +Transition.register('alert-wp-pop-out', AlertWpPopOut); + +let alertIds = -1; diff --git a/src/components/alert/alert-options.ts b/src/components/alert/alert-options.ts new file mode 100644 index 0000000000..3417460b2a --- /dev/null +++ b/src/components/alert/alert-options.ts @@ -0,0 +1,20 @@ + +export interface AlertOptions { + title?: string; + subTitle?: string; + message?: string; + cssClass?: string; + inputs?: Array; + buttons?: Array; + enableBackdropDismiss?: boolean; +} + +export interface AlertInputOptions { + type?: string; + name?: string; + placeholder?: string; + value?: string; + label?: string; + checked?: boolean; + id?: string; +} diff --git a/src/components/alert/alert.ts b/src/components/alert/alert.ts index fb6ea9b901..d35decee0c 100644 --- a/src/components/alert/alert.ts +++ b/src/components/alert/alert.ts @@ -1,199 +1,26 @@ -import {Component, ElementRef, Renderer, HostListener, ViewEncapsulation} from '@angular/core'; +import { Injectable } from '@angular/core'; -import {Animation} from '../../animations/animation'; -import {Transition, TransitionOptions} from '../../transitions/transition'; -import {Config} from '../../config/config'; -import {isPresent} from '../../util/util'; -import {Key} from '../../util/key'; -import {NavParams} from '../nav/nav-params'; -import {ViewController} from '../nav/view-controller'; +import { App } from '../app/app'; +import { AlertCmp } from './alert-component'; +import { AlertOptions, AlertInputOptions } from './alert-options'; +import { isPresent } from '../../util/util'; +import { NavOptions } from '../nav/nav-options'; +import { ViewController } from '../nav/view-controller'; /** - * @name Alert - * @description - * An Alert is a dialog that presents users with information or collects - * information from the user using inputs. An alert appears on top - * of the app's content, and must be manually dismissed by the user before - * they can resume interaction with the app. It can also optionally have a - * `title`, `subTitle` and `message`. - * - * You can pass all of the alert's options in the first argument of - * the create method: `Alert.create(opts)`. Otherwise the alert's instance - * has methods to add options, such as `setTitle()` or `addButton()`. - * - * - * ### Alert Buttons - * - * In the array of `buttons`, each button includes properties for its `text`, - * and optionally a `handler`. If a handler returns `false` then the alert - * will not automatically be dismissed when the button is clicked. All - * buttons will show up in the order they have been added to the `buttons` - * array, from left to right. Note: The right most button (the last one in - * the array) is the main button. - * - * Optionally, a `role` property can be added to a button, such as `cancel`. - * If a `cancel` role is on one of the buttons, then if the alert is - * dismissed by tapping the backdrop, then it will fire the handler from - * the button with a cancel role. - * - * - * ### Alert Inputs - * - * Alerts can also include several different inputs whose data can be passed - * back to the app. Inputs can be used as a simple way to prompt users for - * information. Radios, checkboxes and text inputs are all accepted, but they - * cannot be mixed. For example, an alert could have all radio button inputs, - * or all checkbox inputs, but the same alert cannot mix radio and checkbox - * inputs. Do note however, different types of "text"" inputs can be mixed, - * such as `url`, `email`, `text`, etc. If you require a complex form UI - * which doesn't fit within the guidelines of an alert then we recommend - * building the form within a modal instead. - * - * - * @usage - * ```ts - * constructor(nav: NavController) { - * this.nav = nav; - * } - * - * presentAlert() { - * let alert = Alert.create({ - * title: 'Low battery', - * subTitle: '10% of battery remaining', - * buttons: ['Dismiss'] - * }); - * this.nav.present(alert); - * } - * - * presentConfirm() { - * let alert = Alert.create({ - * title: 'Confirm purchase', - * message: 'Do you want to buy this book?', - * buttons: [ - * { - * text: 'Cancel', - * role: 'cancel', - * handler: () => { - * console.log('Cancel clicked'); - * } - * }, - * { - * text: 'Buy', - * handler: () => { - * console.log('Buy clicked'); - * } - * } - * ] - * }); - * this.nav.present(alert); - * } - * - * presentPrompt() { - * let alert = Alert.create({ - * title: 'Login', - * inputs: [ - * { - * name: 'username', - * placeholder: 'Username' - * }, - * { - * name: 'password', - * placeholder: 'Password', - * type: 'password' - * } - * ], - * buttons: [ - * { - * text: 'Cancel', - * role: 'cancel', - * handler: data => { - * console.log('Cancel clicked'); - * } - * }, - * { - * text: 'Login', - * handler: data => { - * if (User.isValid(data.username, data.password)) { - * // logged in! - * } else { - * // invalid login - * return false; - * } - * } - * } - * ] - * }); - * this.nav.present(alert); - * } - * ``` - * - * - * ### Dismissing And Async Navigation - * - * After an alert has been dismissed, the app may need to also transition - * to another page depending on the handler's logic. However, because multiple - * transitions were fired at roughly the same time, it's difficult for the - * nav controller to cleanly animate multiple transitions that may - * have been kicked off asynchronously. This is further described in the - * [`Nav Transition Promises`](../../nav/NavController) section. For alerts, - * this means it's best to wait for the alert to finish its transition - * out before starting a new transition on the same nav controller. - * - * In the example below, after the alert button has been clicked, its handler - * waits on async operation to complete, *then* it uses `pop` to navigate - * back a page in the same stack. The potential problem is that the async operation - * may have been completed before the alert has even finished its transition - * out. In this case, it's best to ensure the alert has finished its transition - * out first, *then* start the next transition. - * - * ```ts - * let alert = Alert.create({ - * title: 'Hello', - * buttons: [{ - * text: 'Ok', - * handler: () => { - * // user has clicked the alert button - * // begin the alert's dismiss transition - * let navTransition = alert.dismiss(); - * - * // start some async method - * someAsyncOperation().then(() => { - * // once the async operation has completed - * // then run the next nav transition after the - * // first transition has finished animating out - * - * navTransition.then(() => { - * this.nav.pop(); - * }); - * }); - * return false; - * } - * }] - * }); - * - * this.nav.present(alert); - * ``` - * - * It's important to note that the handler returns `false`. A feature of - * button handlers is that they automatically dismiss the alert when their button - * was clicked, however, we'll need more control regarding the transition. Because - * the handler returns `false`, then the alert does not automatically dismiss - * itself. Instead, you now have complete control of when the alert has finished - * transitioning, and the ability to wait for the alert to finish transitioning - * out before starting a new transition. - * - * - * @demo /docs/v2/demos/alert/ + * @private */ export class Alert extends ViewController { + private _app: App; - constructor(opts: AlertOptions = {}) { + constructor(app: App, opts: AlertOptions = {}) { opts.inputs = opts.inputs || []; opts.buttons = opts.buttons || []; opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; super(AlertCmp, opts); + this._app = app; this.isOverlay = true; // by default, alerts should not fire lifecycle events of other views @@ -261,6 +88,209 @@ export class Alert extends ViewController { this.data.cssClass = cssClass; } + /** + * Present the alert instance. + * + * @param {NavOptions} [opts={}] Nav options to go with this transition. + * @returns {Promise} Returns a promise which is resolved when the transition has completed. + */ + present(navOptions: NavOptions = {}) { + return this._app.present(this, navOptions); + } + + /** + * @private + * DEPRECATED: Please inject AlertController instead + */ + private static create(opt: any) { + // deprecated warning: added beta.11 2016-06-27 + console.warn('Alert.create(..) has been deprecated. Please inject AlertController instead'); + } + +} + + +/** + * @name AlertController + * @description + * An Alert is a dialog that presents users with information or collects + * information from the user using inputs. An alert appears on top + * of the app's content, and must be manually dismissed by the user before + * they can resume interaction with the app. It can also optionally have a + * `title`, `subTitle` and `message`. + * + * You can pass all of the alert's options in the first argument of + * the create method: `create(opts)`. Otherwise the alert's instance + * has methods to add options, such as `setTitle()` or `addButton()`. + * + * + * ### Alert Buttons + * + * In the array of `buttons`, each button includes properties for its `text`, + * and optionally a `handler`. If a handler returns `false` then the alert + * will not automatically be dismissed when the button is clicked. All + * buttons will show up in the order they have been added to the `buttons` + * array, from left to right. Note: The right most button (the last one in + * the array) is the main button. + * + * Optionally, a `role` property can be added to a button, such as `cancel`. + * If a `cancel` role is on one of the buttons, then if the alert is + * dismissed by tapping the backdrop, then it will fire the handler from + * the button with a cancel role. + * + * + * ### Alert Inputs + * + * Alerts can also include several different inputs whose data can be passed + * back to the app. Inputs can be used as a simple way to prompt users for + * information. Radios, checkboxes and text inputs are all accepted, but they + * cannot be mixed. For example, an alert could have all radio button inputs, + * or all checkbox inputs, but the same alert cannot mix radio and checkbox + * inputs. Do note however, different types of "text"" inputs can be mixed, + * such as `url`, `email`, `text`, etc. If you require a complex form UI + * which doesn't fit within the guidelines of an alert then we recommend + * building the form within a modal instead. + * + * + * @usage + * ```ts + * constructor(private alertCtrl: AlertController) { + * + * } + * + * presentAlert() { + * let alert = this.alertCtrl.create({ + * title: 'Low battery', + * subTitle: '10% of battery remaining', + * buttons: ['Dismiss'] + * }); + * alert.present(); + * } + * + * presentConfirm() { + * let alert = this.alertCtrl.create({ + * title: 'Confirm purchase', + * message: 'Do you want to buy this book?', + * buttons: [ + * { + * text: 'Cancel', + * role: 'cancel', + * handler: () => { + * console.log('Cancel clicked'); + * } + * }, + * { + * text: 'Buy', + * handler: () => { + * console.log('Buy clicked'); + * } + * } + * ] + * }); + * alert.present(); + * } + * + * presentPrompt() { + * let alert = this.alertCtrl.create({ + * title: 'Login', + * inputs: [ + * { + * name: 'username', + * placeholder: 'Username' + * }, + * { + * name: 'password', + * placeholder: 'Password', + * type: 'password' + * } + * ], + * buttons: [ + * { + * text: 'Cancel', + * role: 'cancel', + * handler: data => { + * console.log('Cancel clicked'); + * } + * }, + * { + * text: 'Login', + * handler: data => { + * if (User.isValid(data.username, data.password)) { + * // logged in! + * } else { + * // invalid login + * return false; + * } + * } + * } + * ] + * }); + * alert.present(); + * } + * ``` + * + * + * ### Dismissing And Async Navigation + * + * After an alert has been dismissed, the app may need to also transition + * to another page depending on the handler's logic. However, because multiple + * transitions were fired at roughly the same time, it's difficult for the + * nav controller to cleanly animate multiple transitions that may + * have been kicked off asynchronously. This is further described in the + * [`Nav Transition Promises`](../../nav/NavController) section. For alerts, + * this means it's best to wait for the alert to finish its transition + * out before starting a new transition on the same nav controller. + * + * In the example below, after the alert button has been clicked, its handler + * waits on async operation to complete, *then* it uses `pop` to navigate + * back a page in the same stack. The potential problem is that the async operation + * may have been completed before the alert has even finished its transition + * out. In this case, it's best to ensure the alert has finished its transition + * out first, *then* start the next transition. + * + * ```ts + * let alert = this.alertCtrl.create({ + * title: 'Hello', + * buttons: [{ + * text: 'Ok', + * handler: () => { + * // user has clicked the alert button + * // begin the alert's dismiss transition + * let navTransition = alert.dismiss(); + * + * // start some async method + * someAsyncOperation().then(() => { + * // once the async operation has completed + * // then run the next nav transition after the + * // first transition has finished animating out + * + * navTransition.then(() => { + * this.nav.pop(); + * }); + * }); + * return false; + * } + * }] + * }); + * + * alert.present(); + * ``` + * + * It's important to note that the handler returns `false`. A feature of + * button handlers is that they automatically dismiss the alert when their button + * was clicked, however, we'll need more control regarding the transition. Because + * the handler returns `false`, then the alert does not automatically dismiss + * itself. Instead, you now have complete control of when the alert has finished + * transitioning, and the ability to wait for the alert to finish transitioning + * out before starting a new transition. + * + * + * @demo /docs/v2/demos/alert/ + */ +@Injectable() +export class AlertController { + + constructor(private _app: App) {} /** * * Alert options @@ -297,435 +327,10 @@ export class Alert extends ViewController { * | cssClass | `string` | An additional CSS class for the button | * | role | `string` | The buttons role, null or `cancel` | * - * @param {object} opts Alert. See the table above + * @param {AlertOptions} opts Alert. See the table above */ - static create(opts: AlertOptions = {}) { - return new Alert(opts); + create(opts: AlertOptions = {}): Alert { + return new Alert(this._app, opts); } } - -/** - * @private - */ -@Component({ - selector: 'ion-alert', - template: - '' + - '
' + - '
' + - '

' + - '

' + - '
' + - '
' + - '
' + - - '' + - - '' + - - '' + - - '
' + - '
' + - '' + - '
' + - '
', - host: { - 'role': 'dialog', - '[attr.aria-labelledby]': 'hdrId', - '[attr.aria-describedby]': 'descId' - }, - encapsulation: ViewEncapsulation.None, -}) -class AlertCmp { - private activeId: string; - private descId: string; - private d: { - cssClass?: string; - message?: string; - subTitle?: string; - buttons?: any[]; - inputs?: any[]; - enableBackdropDismiss?: boolean; - }; - private enabled: boolean; - private hdrId: string; - private id: number; - private inputType: string; - private lastClick: number; - private msgId: string; - private subHdrId: string; - - constructor( - private _viewCtrl: ViewController, - private _elementRef: ElementRef, - private _config: Config, - params: NavParams, - renderer: Renderer - ) { - this.d = params.data; - - if (this.d.cssClass) { - this.d.cssClass.split(' ').forEach(cssClass => { - renderer.setElementClass(_elementRef.nativeElement, cssClass, true); - }); - } - - this.id = (++alertIds); - this.descId = ''; - this.hdrId = 'alert-hdr-' + this.id; - this.subHdrId = 'alert-subhdr-' + this.id; - this.msgId = 'alert-msg-' + this.id; - this.activeId = ''; - this.lastClick = 0; - - if (this.d.message) { - this.descId = this.msgId; - - } else if (this.d.subTitle) { - this.descId = this.subHdrId; - } - - if (!this.d.message) { - this.d.message = ''; - } - } - - ionViewLoaded() { - // normalize the data - let data = this.d; - - data.buttons = data.buttons.map(button => { - if (typeof button === 'string') { - return { text: button }; - } - return button; - }); - - data.inputs = data.inputs.map((input, index) => { - return { - type: input.type || 'text', - name: isPresent(input.name) ? input.name : index, - placeholder: isPresent(input.placeholder) ? input.placeholder : '', - value: isPresent(input.value) ? input.value : '', - label: input.label, - checked: !!input.checked, - id: 'alert-input-' + this.id + '-' + index - }; - }); - - - // An alert can be created with several different inputs. Radios, - // checkboxes and inputs are all accepted, but they cannot be mixed. - let inputTypes: any[] = []; - data.inputs.forEach(input => { - if (inputTypes.indexOf(input.type) < 0) { - inputTypes.push(input.type); - } - }); - - if (inputTypes.length > 1 && (inputTypes.indexOf('checkbox') > -1 || inputTypes.indexOf('radio') > -1)) { - console.warn('Alert cannot mix input types: ' + (inputTypes.join('/')) + '. Please see alert docs for more info.'); - } - - this.inputType = inputTypes.length ? inputTypes[0] : null; - - let checkedInput = this.d.inputs.find(input => input.checked); - if (checkedInput) { - this.activeId = checkedInput.id; - } - } - - @HostListener('body:keyup', ['$event']) - private _keyUp(ev: KeyboardEvent) { - if (this.enabled && this._viewCtrl.isLast()) { - if (ev.keyCode === Key.ENTER) { - if (this.lastClick + 1000 < Date.now()) { - // do not fire this click if there recently was already a click - // this can happen when the button has focus and used the enter - // key to click the button. However, both the click handler and - // this keyup event will fire, so only allow one of them to go. - console.debug('alert, enter button'); - let button = this.d.buttons[this.d.buttons.length - 1]; - this.btnClick(button); - } - - } else if (ev.keyCode === Key.ESCAPE) { - console.debug('alert, escape button'); - this.bdClick(); - } - } - } - - ionViewDidEnter() { - let activeElement: any = document.activeElement; - if (document.activeElement) { - activeElement.blur(); - } - - let focusableEle = this._elementRef.nativeElement.querySelector('input,button'); - if (focusableEle) { - focusableEle.focus(); - } - this.enabled = true; - } - - btnClick(button: any, dismissDelay?: number) { - if (!this.enabled) { - return; - } - - // keep the time of the most recent button click - this.lastClick = Date.now(); - - let shouldDismiss = true; - - if (button.handler) { - // a handler has been provided, execute it - // pass the handler the values from the inputs - if (button.handler(this.getValues()) === false) { - // if the return value of the handler is false then do not dismiss - shouldDismiss = false; - } - } - - if (shouldDismiss) { - setTimeout(() => { - this.dismiss(button.role); - }, dismissDelay || this._config.get('pageTransitionDelay')); - } - } - - rbClick(checkedInput: any) { - if (this.enabled) { - this.d.inputs.forEach(input => { - input.checked = (checkedInput === input); - }); - this.activeId = checkedInput.id; - } - } - - cbClick(checkedInput: any) { - if (this.enabled) { - checkedInput.checked = !checkedInput.checked; - } - } - - bdClick() { - if (this.enabled && this.d.enableBackdropDismiss) { - let cancelBtn = this.d.buttons.find(b => b.role === 'cancel'); - if (cancelBtn) { - this.btnClick(cancelBtn, 1); - - } else { - this.dismiss('backdrop'); - } - } - } - - dismiss(role: any): Promise { - return this._viewCtrl.dismiss(this.getValues(), role); - } - - getValues() { - if (this.inputType === 'radio') { - // this is an alert with radio buttons (single value select) - // return the one value which is checked, otherwise undefined - let checkedInput = this.d.inputs.find(i => i.checked); - return checkedInput ? checkedInput.value : undefined; - } - - if (this.inputType === 'checkbox') { - // this is an alert with checkboxes (multiple value select) - // return an array of all the checked values - return this.d.inputs.filter(i => i.checked).map(i => i.value); - } - - // this is an alert with text inputs - // return an object of all the values with the input name as the key - let values: {[k: string]: string} = {}; - this.d.inputs.forEach(i => { - values[i.name] = i.value; - }); - return values; - } -} - -export interface AlertOptions { - title?: string; - subTitle?: string; - message?: string; - cssClass?: string; - inputs?: Array; - buttons?: Array; - enableBackdropDismiss?: boolean; -} - -export interface AlertInputOptions { - type?: string; - name?: string; - placeholder?: string; - value?: string; - label?: string; - checked?: boolean; - id?: string; -} - - -/** - * Animations for alerts - */ -class AlertPopIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.alert-wrapper')); - - wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.1, 1); - backdrop.fromTo('opacity', 0.01, 0.3); - - this - .easing('ease-in-out') - .duration(200) - .add(backdrop) - .add(wrapper); - } -} -Transition.register('alert-pop-in', AlertPopIn); - - -class AlertPopOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.alert-wrapper')); - - wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 0.9); - backdrop.fromTo('opacity', 0.3, 0); - - this - .easing('ease-in-out') - .duration(200) - .add(backdrop) - .add(wrapper); - } -} -Transition.register('alert-pop-out', AlertPopOut); - - -class AlertMdPopIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.alert-wrapper')); - - wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.1, 1); - backdrop.fromTo('opacity', 0.01, 0.5); - - this - .easing('ease-in-out') - .duration(200) - .add(backdrop) - .add(wrapper); - } -} -Transition.register('alert-md-pop-in', AlertMdPopIn); - - -class AlertMdPopOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.alert-wrapper')); - - wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 0.9); - backdrop.fromTo('opacity', 0.5, 0); - - this - .easing('ease-in-out') - .duration(200) - .add(backdrop) - .add(wrapper); - } -} -Transition.register('alert-md-pop-out', AlertMdPopOut); - - - -class AlertWpPopIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.alert-wrapper')); - - wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.3, 1); - backdrop.fromTo('opacity', 0.01, 0.5); - - this - .easing('cubic-bezier(0,0 0.05,1)') - .duration(200) - .add(backdrop) - .add(wrapper); - } -} -Transition.register('alert-wp-pop-in', AlertWpPopIn); - - -class AlertWpPopOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.alert-wrapper')); - - wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 1.3); - backdrop.fromTo('opacity', 0.5, 0); - - this - .easing('ease-out') - .duration(150) - .add(backdrop) - .add(wrapper); - } -} -Transition.register('alert-wp-pop-out', AlertWpPopOut); - -let alertIds = -1; diff --git a/src/components/app/app.ts b/src/components/app/app.ts index 4e574f4f32..ea0ff269d1 100644 --- a/src/components/app/app.ts +++ b/src/components/app/app.ts @@ -1,11 +1,17 @@ -import {Injectable, Injector} from '@angular/core'; -import {Title} from '@angular/platform-browser'; +import { Component, ComponentResolver, EventEmitter, Injectable, Renderer, ViewChild, ViewContainerRef } from '@angular/core'; +import { Title } from '@angular/platform-browser'; -import {ClickBlock} from '../../util/click-block'; -import {Config} from '../../config/config'; -import {NavController} from '../nav/nav-controller'; -import {Platform} from '../../platform/platform'; -import {Tabs} from '../tabs/tabs'; +import { ClickBlock } from '../../util/click-block'; +import { Config } from '../../config/config'; +import { NavController } from '../nav/nav-controller'; +import { NavOptions } from '../nav/nav-options'; +import { NavPortal } from '../nav/nav-portal'; +import { Platform } from '../../platform/platform'; + +/** + * @private + */ +export abstract class UserComponent {} /** @@ -18,11 +24,23 @@ export class App { private _title: string = ''; private _titleSrv: Title = new Title(); private _rootNav: NavController = null; - private _appInjector: Injector; + private _portal: NavPortal; + + /** + * @private + */ + clickBlock: ClickBlock; + + viewDidLoad: EventEmitter = new EventEmitter(); + viewWillEnter: EventEmitter = new EventEmitter(); + viewDidEnter: EventEmitter = new EventEmitter(); + viewWillLeave: EventEmitter = new EventEmitter(); + viewDidLeave: EventEmitter = new EventEmitter(); + viewWillUnload: EventEmitter = new EventEmitter(); + viewDidUnload: EventEmitter = new EventEmitter(); constructor( private _config: Config, - private _clickBlock: ClickBlock, private _platform: Platform ) { // listen for hardware back button events @@ -55,14 +73,15 @@ export class App { */ setEnabled(isEnabled: boolean, duration: number = 700) { this._disTime = (isEnabled ? 0 : Date.now() + duration); - const CLICK_BLOCK_BUFFER_IN_MILLIS = 64; - if (this._clickBlock) { - if ( isEnabled || duration <= 32 ) { + + if (this.clickBlock) { + if (isEnabled || duration <= 32) { // disable the click block if it's enabled, or the duration is tiny - this._clickBlock.show(false, 0); + this.clickBlock.activate(false, 0); + } else { // show the click block for duration + some number - this._clickBlock.show(true, duration + CLICK_BLOCK_BUFFER_IN_MILLIS); + this.clickBlock.activate(true, duration + CLICK_BLOCK_BUFFER_IN_MILLIS); } } } @@ -123,6 +142,36 @@ export class App { this._rootNav = nav; } + /** + * @private + */ + setPortal(portal: NavPortal) { + this._portal = portal; + } + + /** + * @private + */ + present(enteringView: any, opts: NavOptions = {}): Promise { + enteringView.setNav(this._portal); + + opts.keyboardClose = false; + opts.direction = 'forward'; + + if (!opts.animation) { + opts.animation = enteringView.getTransitionName('forward'); + } + + enteringView.setLeavingOpts({ + keyboardClose: false, + direction: 'back', + animation: enteringView.getTransitionName('back'), + ev: opts.ev + }); + + return this._portal.insertViews(-1, [enteringView], opts); + } + /** * @private */ @@ -162,12 +211,11 @@ export class App { // first check if the root navigation has any overlays // opened in it's portal, like alert/actionsheet/popup - let portal = this._rootNav.getPortal && this._rootNav.getPortal(); - if (portal && portal.length() > 0) { + if (this._portal && this._portal.length() > 0) { // there is an overlay view in the portal // let's pop this one off to go back console.debug('app, goBack pop overlay'); - return portal.pop(); + return this._portal.pop(); } // next get the active nav, check itself and climb up all @@ -207,19 +255,48 @@ export class App { 'Please use Angular\'s ViewChild annotation instead:\n\nhttp://learnangular2.com/viewChild/'); } - /** - * Set the global app injector that contains references to all of the instantiated providers - * @param injector - */ - setAppInjector(injector: Injector) { - this._appInjector = injector; - } - /** * Get an instance of the global app injector that contains references to all of the instantiated providers * @returns {Injector} */ - getAppInjector(): Injector { - return this._appInjector; + getAppInjector(): any { + // deprecated warning: added 2016-06-27, beta10 + console.warn('Recent Angular2 versions should no longer require App.getAppInjector()'); } } + + +/** + * @private + */ +@Component({ + selector: 'ion-app', + template: ` +
+ + `, + directives: [NavPortal, ClickBlock] +}) +export class AppRoot { + + @ViewChild('anchor', {read: ViewContainerRef}) private _viewport: ViewContainerRef; + + constructor( + private _cmp: UserComponent, + private _cr: ComponentResolver, + private _renderer: Renderer) {} + + ngAfterViewInit() { + // load the user app's root component + this._cr.resolveComponent(this._cmp).then(componentFactory => { + let appEle: HTMLElement = this._renderer.createElement(null, componentFactory.selector || 'div', null); + appEle.setAttribute('class', 'app-root'); + + let componentRef = componentFactory.create(this._viewport.injector, null, appEle); + this._viewport.insert(componentRef.hostView, 0); + }); + } + +} + +const CLICK_BLOCK_BUFFER_IN_MILLIS = 64; diff --git a/src/components/app/structure.scss b/src/components/app/structure.scss index 12cef99a36..29de61797c 100644 --- a/src/components/app/structure.scss +++ b/src/components/app/structure.scss @@ -55,7 +55,8 @@ body { ion-app, ion-nav, ion-tab, -ion-tabs { +ion-tabs, +.app-root { position: absolute; top: 0; left: 0; diff --git a/src/components/datetime/datetime.ts b/src/components/datetime/datetime.ts index 535d7cdd3a..688e267322 100644 --- a/src/components/datetime/datetime.ts +++ b/src/components/datetime/datetime.ts @@ -1,13 +1,14 @@ -import {Component, Optional, ElementRef, Renderer, Input, Output, Provider, forwardRef, EventEmitter, HostListener, ViewEncapsulation} from '@angular/core'; -import {NG_VALUE_ACCESSOR} from '@angular/common'; +import { Component, EventEmitter, forwardRef, HostListener, Input, Optional, Output, Provider, ViewEncapsulation } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/common'; -import {Config} from '../../config/config'; -import {Picker, PickerColumn, PickerColumnOption} from '../picker/picker'; -import {Form} from '../../util/form'; -import {Item} from '../item/item'; -import {merge, isBlank, isPresent, isTrueProperty, isArray, isString} from '../../util/util'; -import {dateValueRange, renderDateTime, renderTextFormat, convertFormatToKey, getValueFromFormat, parseTemplate, parseDate, updateDate, DateTimeData, convertDataToISO, daysInMonth, dateSortValue, dateDataSortValue, LocaleData} from '../../util/datetime-util'; -import {NavController} from '../nav/nav-controller'; +import { Config } from '../../config/config'; +import { Picker, PickerController } from '../picker/picker'; +import { PickerColumn, PickerColumnOption } from '../picker/picker-options'; +import { Form } from '../../util/form'; +import { Item } from '../item/item'; +import { merge, isBlank, isPresent, isTrueProperty, isArray, isString } from '../../util/util'; +import { dateValueRange, renderDateTime, renderTextFormat, convertFormatToKey, getValueFromFormat, parseTemplate, parseDate, updateDate, DateTimeData, convertDataToISO, daysInMonth, dateSortValue, dateDataSortValue, LocaleData } from '../../util/datetime-util'; +import { NavController } from '../nav/nav-controller'; const DATETIME_VALUE_ACCESSOR = new Provider( NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => DateTime), multi: true}); @@ -193,7 +194,7 @@ const DATETIME_VALUE_ACCESSOR = new Provider( * ### App Config Level * * ```ts - * import {ionicBootstrap} from 'ionic-angular'; + * import { ionicBootstrap } from 'ionic-angular'; * * ionicBootstrap(MyApp, customProviders, { * monthNames: ['janeiro', 'fevereiro', 'mar\u00e7o', ... ], @@ -418,7 +419,7 @@ export class DateTime { private _form: Form, private _config: Config, @Optional() private _item: Item, - @Optional() private _nav: NavController + @Optional() private _pickerCtrl: PickerController ) { this._form.register(this); if (_item) { @@ -426,10 +427,6 @@ export class DateTime { this._labelId = 'lbl-' + _item.id; this._item.setCssClass('item-datetime', true); } - - if (!_nav) { - console.error('parent required for '); - } } @HostListener('click', ['$event']) @@ -463,7 +460,7 @@ export class DateTime { // the user may have assigned some options specifically for the alert let pickerOptions = merge({}, this.pickerOptions); - let picker = Picker.create(pickerOptions); + let picker = this._pickerCtrl.create(pickerOptions); pickerOptions.buttons = [ { text: this.cancelText, @@ -489,7 +486,7 @@ export class DateTime { this.validate(picker); }); - this._nav.present(picker, pickerOptions); + picker.present(pickerOptions); this._isOpen = true; picker.onDismiss(() => { diff --git a/src/components/loading/loading-component.ts b/src/components/loading/loading-component.ts new file mode 100644 index 0000000000..f51465fc4a --- /dev/null +++ b/src/components/loading/loading-component.ts @@ -0,0 +1,205 @@ +import { Component, ElementRef, Renderer, ViewEncapsulation } from '@angular/core'; + +import { Animation } from '../../animations/animation'; +import { Config } from '../../config/config'; +import { isDefined, isPresent, isUndefined } from '../../util/util'; +import { NavParams } from '../nav/nav-params'; +import { Transition, TransitionOptions } from '../../transitions/transition'; +import { ViewController} from '../nav/view-controller'; + + +/** +* @private +*/ +@Component({ + selector: 'ion-loading', + template: + '' + + '
' + + '
' + + '' + + '
' + + '
' + + '
', + host: { + 'role': 'dialog' + }, + encapsulation: ViewEncapsulation.None, +}) +export class LoadingCmp { + private d: any; + private id: number; + private showSpinner: boolean; + + constructor( + private _viewCtrl: ViewController, + private _config: Config, + private _elementRef: ElementRef, + params: NavParams, + renderer: Renderer + ) { + this.d = params.data; + + if (this.d.cssClass) { + renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true); + } + + this.id = (++loadingIds); + } + + ngOnInit() { + // If no spinner was passed in loading options we need to fall back + // to the loadingSpinner in the app's config, then the mode spinner + if (isUndefined(this.d.spinner)) { + this.d.spinner = this._config.get('loadingSpinner', this._config.get('spinner', 'ios')); + } + + // If the user passed hide to the spinner we don't want to show it + this.showSpinner = isDefined(this.d.spinner) && this.d.spinner !== 'hide'; + } + + ionViewDidEnter() { + let activeElement: any = document.activeElement; + if (document.activeElement) { + activeElement.blur(); + } + + // If there is a duration, dismiss after that amount of time + this.d.duration ? setTimeout(() => this.dismiss('backdrop'), this.d.duration) : null; + } + + dismiss(role: any): Promise { + return this._viewCtrl.dismiss(null, role); + } +} + + +/** + * Animations for loading + */ + class LoadingPopIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.1, 1); + backdrop.fromTo('opacity', 0.01, 0.3); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-pop-in', LoadingPopIn); + + + class LoadingPopOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 0.9); + backdrop.fromTo('opacity', 0.3, 0); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-pop-out', LoadingPopOut); + + + class LoadingMdPopIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.1, 1); + backdrop.fromTo('opacity', 0.01, 0.5); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-md-pop-in', LoadingMdPopIn); + + + class LoadingMdPopOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 0.9); + backdrop.fromTo('opacity', 0.5, 0); + + this + .easing('ease-in-out') + .duration(200) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-md-pop-out', LoadingMdPopOut); + + + class LoadingWpPopIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.3, 1); + backdrop.fromTo('opacity', 0.01, 0.16); + + this + .easing('cubic-bezier(0,0 0.05,1)') + .duration(200) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-wp-pop-in', LoadingWpPopIn); + + + class LoadingWpPopOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.loading-wrapper')); + + wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 1.3); + backdrop.fromTo('opacity', 0.16, 0); + + this + .easing('ease-out') + .duration(150) + .add(backdrop) + .add(wrapper); + } + } + Transition.register('loading-wp-pop-out', LoadingWpPopOut); + +let loadingIds = -1; diff --git a/src/components/loading/loading-options.ts b/src/components/loading/loading-options.ts new file mode 100644 index 0000000000..3e2713e743 --- /dev/null +++ b/src/components/loading/loading-options.ts @@ -0,0 +1,10 @@ + +export interface LoadingOptions { + spinner?: string; + content?: string; + cssClass?: string; + showBackdrop?: boolean; + dismissOnPageChange?: boolean; + delay?: number; + duration?: number; +} diff --git a/src/components/loading/loading.ts b/src/components/loading/loading.ts index 7a70c5d5ce..95cf8935d1 100644 --- a/src/components/loading/loading.ts +++ b/src/components/loading/loading.ts @@ -1,15 +1,65 @@ -import { Component, ElementRef, Renderer, ViewEncapsulation } from '@angular/core'; +import { Injectable } from '@angular/core'; -import { Animation } from '../../animations/animation'; +import { App } from '../app/app'; import { Config } from '../../config/config'; -import { isDefined, isPresent, isUndefined } from '../../util/util'; -import { NavParams } from '../nav/nav-params'; -import { Transition, TransitionOptions } from '../../transitions/transition'; +import { isPresent } from '../../util/util'; +import { LoadingCmp } from './loading-component'; +import { LoadingOptions } from './loading-options'; +import { NavOptions } from '../nav/nav-options'; import { ViewController} from '../nav/view-controller'; +/** + * @private + */ +export class Loading extends ViewController { + private _app: App; + + constructor(app: App, opts: LoadingOptions = {}) { + opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true; + opts.dismissOnPageChange = isPresent(opts.dismissOnPageChange) ? !!opts.dismissOnPageChange : false; + + super(LoadingCmp, opts); + this._app = app; + this.isOverlay = true; + + // by default, loading indicators should not fire lifecycle events of other views + // for example, when an loading indicators enters, the current active view should + // not fire its lifecycle events because it's not conceptually leaving + this.fireOtherLifecycles = false; + } + + /** + * @private + */ + getTransitionName(direction: string) { + let key = (direction === 'back' ? 'loadingLeave' : 'loadingEnter'); + return this._nav && this._nav.config.get(key); + } + + /** + * Present the loading instance. + * + * @param {NavOptions} [opts={}] Nav options to go with this transition. + * @returns {Promise} Returns a promise which is resolved when the transition has completed. + */ + present(navOptions: NavOptions = {}) { + return this._app.present(this, navOptions); + } + + /** + * @private + * DEPRECATED: Please inject LoadingController instead + */ + private static create(opt: any) { + // deprecated warning: added beta.11 2016-06-27 + console.warn('Loading.create(..) has been deprecated. Please inject LoadingController instead'); + } + +} + /** - * @name Loading + * @name LoadingController * @description * An overlay that can be used to indicate activity while blocking user * interaction. The loading indicator appears on top of the app's content, @@ -19,7 +69,7 @@ import { ViewController} from '../nav/view-controller'; * * ### Creating * You can pass all of the loading options in the first argument of - * the create method: `Loading.create(opts)`. The spinner name should be + * the create method: `create(opts)`. The spinner name should be * passed in the `spinner` property, and any optional HTML can be passed * in the `content` property. If you do not pass a value to `spinner` * the loading indicator will use the spinner specified by the mode. To @@ -50,16 +100,16 @@ import { ViewController} from '../nav/view-controller'; * * @usage * ```ts - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private loadingCtrl: LoadingController) { + * * } * * presentLoadingDefault() { - * let loading = Loading.create({ + * let loading = this.loadingCtrl.create({ * content: 'Please wait...' * }); * - * this.nav.present(loading); + * loading.present(); * * setTimeout(() => { * loading.dismiss(); @@ -67,7 +117,7 @@ import { ViewController} from '../nav/view-controller'; * } * * presentLoadingCustom() { - * let loading = Loading.create({ + * let loading = this.loadingCtrl.create({ * spinner: 'hide', * content: ` *
@@ -80,16 +130,16 @@ import { ViewController} from '../nav/view-controller'; * console.log('Dismissed loading'); * }); * - * this.nav.present(loading); + * loading.present(); * } * * presentLoadingText() { - * let loading = Loading.create({ + * let loading = this.loadingCtrl.create({ * spinner: 'hide', * content: 'Loading Please Wait...' * }); * - * this.nav.present(loading); + * loading.present(); * * setTimeout(() => { * this.nav.push(Page2); @@ -104,252 +154,27 @@ import { ViewController} from '../nav/view-controller'; * @demo /docs/v2/demos/loading/ * @see {@link /docs/v2/api/components/spinner/Spinner Spinner API Docs} */ -export class Loading extends ViewController { +@Injectable() +export class LoadingController { - constructor(opts: LoadingOptions = {}) { - opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true; - opts.dismissOnPageChange = isPresent(opts.dismissOnPageChange) ? !!opts.dismissOnPageChange : false; - - super(LoadingCmp, opts); - this.isOverlay = true; - this.usePortal = true; - - // by default, loading indicators should not fire lifecycle events of other views - // for example, when an loading indicators enters, the current active view should - // not fire its lifecycle events because it's not conceptually leaving - this.fireOtherLifecycles = false; - } - - /** - * @private + constructor(private _app: App) {} + /** + * Create a loading indicator with the following options + * + * | Option | Type | Description | + * |-----------------------|------------|------------------------------------------------------------------------------------------------------------------| + * | spinner |`string` | The name of the SVG spinner for the loading indicator. | + * | content |`string` | The html content for the loading indicator. | + * | cssClass |`string` | An additional class for custom styles. | + * | showBackdrop |`boolean` | Whether to show the backdrop. Default true. | + * | dismissOnPageChange |`boolean` | Whether to dismiss the indicator when navigating to a new page. Default false. | + * | duration |`number` | How many milliseconds to wait before hiding the indicator. By default, it will show until `dismiss()` is called. | + * + * + * @param {LoadingOptions} opts Loading options */ - getTransitionName(direction: string) { - let key = (direction === 'back' ? 'loadingLeave' : 'loadingEnter'); - return this._nav && this._nav.config.get(key); - } - - /** - * Create a loading indicator with the following options - * - * | Option | Type | Description | - * |-----------------------|------------|------------------------------------------------------------------------------------------------------------------| - * | spinner |`string` | The name of the SVG spinner for the loading indicator. | - * | content |`string` | The html content for the loading indicator. | - * | cssClass |`string` | An additional class for custom styles. | - * | showBackdrop |`boolean` | Whether to show the backdrop. Default true. | - * | dismissOnPageChange |`boolean` | Whether to dismiss the indicator when navigating to a new page. Default false. | - * | duration |`number` | How many milliseconds to wait before hiding the indicator. By default, it will show until `dismiss()` is called. | - * - * - * @param {object} opts Loading options - */ - static create(opts: LoadingOptions = {}) { - return new Loading(opts); - } - - } - -/** -* @private -*/ -@Component({ - selector: 'ion-loading', - template: - '' + - '
' + - '
' + - '' + - '
' + - '
' + - '
', - host: { - 'role': 'dialog' - }, - encapsulation: ViewEncapsulation.None, -}) -class LoadingCmp { - private d: any; - private id: number; - private showSpinner: boolean; - - constructor( - private _viewCtrl: ViewController, - private _config: Config, - private _elementRef: ElementRef, - params: NavParams, - renderer: Renderer - ) { - this.d = params.data; - - if (this.d.cssClass) { - renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true); - } - - this.id = (++loadingIds); + create(opts: LoadingOptions = {}) { + return new Loading(this._app, opts); } - ngOnInit() { - // If no spinner was passed in loading options we need to fall back - // to the loadingSpinner in the app's config, then the mode spinner - if (isUndefined(this.d.spinner)) { - this.d.spinner = this._config.get('loadingSpinner', this._config.get('spinner', 'ios')); - } - - // If the user passed hide to the spinner we don't want to show it - this.showSpinner = isDefined(this.d.spinner) && this.d.spinner !== 'hide'; - } - - ionViewDidEnter() { - let activeElement: any = document.activeElement; - if (document.activeElement) { - activeElement.blur(); - } - - // If there is a duration, dismiss after that amount of time - this.d.duration ? setTimeout(() => this.dismiss('backdrop'), this.d.duration) : null; - } - - dismiss(role: any): Promise { - return this._viewCtrl.dismiss(null, role); - } } - -export interface LoadingOptions { - spinner?: string; - content?: string; - cssClass?: string; - showBackdrop?: boolean; - dismissOnPageChange?: boolean; - delay?: number; - duration?: number; -} - -/** - * Animations for loading - */ - class LoadingPopIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.loading-wrapper')); - - wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.1, 1); - backdrop.fromTo('opacity', 0.01, 0.3); - - this - .easing('ease-in-out') - .duration(200) - .add(backdrop) - .add(wrapper); - } - } - Transition.register('loading-pop-in', LoadingPopIn); - - - class LoadingPopOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.loading-wrapper')); - - wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 0.9); - backdrop.fromTo('opacity', 0.3, 0); - - this - .easing('ease-in-out') - .duration(200) - .add(backdrop) - .add(wrapper); - } - } - Transition.register('loading-pop-out', LoadingPopOut); - - - class LoadingMdPopIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.loading-wrapper')); - - wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.1, 1); - backdrop.fromTo('opacity', 0.01, 0.5); - - this - .easing('ease-in-out') - .duration(200) - .add(backdrop) - .add(wrapper); - } - } - Transition.register('loading-md-pop-in', LoadingMdPopIn); - - - class LoadingMdPopOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.loading-wrapper')); - - wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 0.9); - backdrop.fromTo('opacity', 0.5, 0); - - this - .easing('ease-in-out') - .duration(200) - .add(backdrop) - .add(wrapper); - } - } - Transition.register('loading-md-pop-out', LoadingMdPopOut); - - - class LoadingWpPopIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.loading-wrapper')); - - wrapper.fromTo('opacity', 0.01, 1).fromTo('scale', 1.3, 1); - backdrop.fromTo('opacity', 0.01, 0.16); - - this - .easing('cubic-bezier(0,0 0.05,1)') - .duration(200) - .add(backdrop) - .add(wrapper); - } - } - Transition.register('loading-wp-pop-in', LoadingWpPopIn); - - - class LoadingWpPopOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.loading-wrapper')); - - wrapper.fromTo('opacity', 0.99, 0).fromTo('scale', 1, 1.3); - backdrop.fromTo('opacity', 0.16, 0); - - this - .easing('ease-out') - .duration(150) - .add(backdrop) - .add(wrapper); - } - } - Transition.register('loading-wp-pop-out', LoadingWpPopOut); - -let loadingIds = -1; diff --git a/src/components/modal/modal-component.ts b/src/components/modal/modal-component.ts new file mode 100644 index 0000000000..a45504b0a0 --- /dev/null +++ b/src/components/modal/modal-component.ts @@ -0,0 +1,186 @@ +import { Component, ComponentResolver, HostListener, Renderer, ViewChild, ViewContainerRef } from '@angular/core'; + +import { addSelector } from '../../config/bootstrap'; +import { Animation } from '../../animations/animation'; +import { pascalCaseToDashCase } from '../../util/util'; +import { Key } from '../../util/key'; +import { NavParams } from '../nav/nav-params'; +import { PageTransition } from '../../transitions/page-transition'; +import { TransitionOptions } from '../../transitions/transition'; +import { ViewController } from '../nav/view-controller'; +import { windowDimensions } from '../../util/dom'; + + +/** + * @private + */ +@Component({ + selector: 'ion-modal', + template: ` + + + ` +}) +export class ModalCmp { + + @ViewChild('viewport', {read: ViewContainerRef}) viewport: ViewContainerRef; + + private d: any; + private enabled: boolean; + + constructor(private _compiler: ComponentResolver, private _renderer: Renderer, private _navParams: NavParams, private _viewCtrl: ViewController) { + this.d = _navParams.data.opts; + } + + loadComponent(done: Function) { + let componentType = this._navParams.data.componentType; + addSelector(componentType, 'ion-page'); + + this._compiler.resolveComponent(componentType).then((componentFactory) => { + let componentRef = this.viewport.createComponent(componentFactory, this.viewport.length, this.viewport.parentInjector); + this._renderer.setElementClass(componentRef.location.nativeElement, 'show-page', true); + + // auto-add page css className created from component JS class name + let cssClassName = pascalCaseToDashCase(componentType.name); + this._renderer.setElementClass(componentRef.location.nativeElement, cssClassName, true); + this._viewCtrl.setInstance(componentRef.instance); + this.enabled = true; + done(); + }); + } + + ngAfterViewInit() { + // intentionally kept empty + } + + dismiss(role: any): Promise { + return this._viewCtrl.dismiss(null, role); + } + + bdClick() { + if (this.enabled && this.d.enableBackdropDismiss) { + this.dismiss('backdrop'); + } + } + + @HostListener('body:keyup', ['$event']) + private _keyUp(ev: KeyboardEvent) { + if (this.enabled && this._viewCtrl.isLast() && ev.keyCode === Key.ESCAPE ) { + this.bdClick(); + } + } +} + +/** + * Animations for modals + */ + class ModalSlideIn extends PageTransition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdropEle = ele.querySelector('ion-backdrop'); + let backdrop = new Animation(backdropEle); + let wrapper = new Animation(ele.querySelector('.modal-wrapper')); + + backdrop.fromTo('opacity', 0.01, 0.4); + wrapper.fromTo('translateY', '100%', '0%'); + + + this + .element(enteringView.pageRef()) + .easing('cubic-bezier(0.36,0.66,0.04,1)') + .duration(400) + .add(backdrop) + .add(wrapper); + + if (enteringView.hasNavbar()) { + // entering page has a navbar + let enteringNavBar = new Animation(enteringView.navbarRef()); + enteringNavBar.before.addClass('show-navbar'); + this.add(enteringNavBar); + } + } + } + PageTransition.register('modal-slide-in', ModalSlideIn); + + +class ModalSlideOut extends PageTransition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapperEle = ele.querySelector('.modal-wrapper'); + let wrapperEleRect = wrapperEle.getBoundingClientRect(); + let wrapper = new Animation(wrapperEle); + + // height of the screen - top of the container tells us how much to scoot it down + // so it's off-screen + let screenDimensions = windowDimensions(); + wrapper.fromTo('translateY', '0px', `${screenDimensions.height - wrapperEleRect.top}px`); + backdrop.fromTo('opacity', 0.4, 0.0); + + this + .element(leavingView.pageRef()) + .easing('ease-out') + .duration(250) + .add(backdrop) + .add(wrapper); + } +} +PageTransition.register('modal-slide-out', ModalSlideOut); + + +class ModalMDSlideIn extends PageTransition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.modal-wrapper')); + + backdrop.fromTo('opacity', 0.01, 0.4); + wrapper.fromTo('translateY', '40px', '0px'); + wrapper.fromTo('opacity', 0.01, 1); + + const DURATION = 280; + const EASING = 'cubic-bezier(0.36,0.66,0.04,1)'; + this.element(enteringView.pageRef()).easing(EASING).duration(DURATION) + .add(backdrop) + .add(wrapper); + + if (enteringView.hasNavbar()) { + // entering page has a navbar + let enteringNavBar = new Animation(enteringView.navbarRef()); + enteringNavBar.before.addClass('show-navbar'); + this.add(enteringNavBar); + } + } +} +PageTransition.register('modal-md-slide-in', ModalMDSlideIn); + + +class ModalMDSlideOut extends PageTransition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.modal-wrapper')); + + backdrop.fromTo('opacity', 0.4, 0.0); + wrapper.fromTo('translateY', '0px', '40px'); + wrapper.fromTo('opacity', 0.99, 0); + + this + .element(leavingView.pageRef()) + .duration(200) + .easing('cubic-bezier(0.47,0,0.745,0.715)') + .add(wrapper) + .add(backdrop); + } +} +PageTransition.register('modal-md-slide-out', ModalMDSlideOut); diff --git a/src/components/modal/modal-options.ts b/src/components/modal/modal-options.ts new file mode 100644 index 0000000000..255b54c6ff --- /dev/null +++ b/src/components/modal/modal-options.ts @@ -0,0 +1,5 @@ + +export interface ModalOptions { + showBackdrop?: boolean; + enableBackdropDismiss?: boolean; +} diff --git a/src/components/modal/modal.ts b/src/components/modal/modal.ts index 2f82d19ec6..668eac84c6 100644 --- a/src/components/modal/modal.ts +++ b/src/components/modal/modal.ts @@ -1,18 +1,77 @@ -import { Component, ComponentResolver, HostListener, Renderer, ViewChild, ViewContainerRef } from '@angular/core'; +import { Injectable } from '@angular/core'; -import { addSelector } from '../../config/bootstrap'; -import { Animation } from '../../animations/animation'; -import { isPresent, pascalCaseToDashCase } from '../../util/util'; -import { Key } from '../../util/key'; -import { NavParams } from '../nav/nav-params'; -import { PageTransition } from '../../transitions/page-transition'; -import { TransitionOptions } from '../../transitions/transition'; +import { App } from '../app/app'; +import { isPresent } from '../../util/util'; +import { ModalCmp } from './modal-component'; +import { ModalOptions } from './modal-options'; +import { NavOptions } from '../nav/nav-options'; import { ViewController } from '../nav/view-controller'; -import { windowDimensions } from '../../util/dom'; /** - * @name Modal + * @private + */ +export class Modal extends ViewController { + private _app: App; + + constructor(app: App, componentType: any, data: any = {}, opts: ModalOptions = {}) { + data.componentType = componentType; + opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true; + opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; + data.opts = opts; + + super(ModalCmp, data); + this._app = app; + this.isOverlay = true; + } + + /** + * @private + */ + getTransitionName(direction: string) { + let key = (direction === 'back' ? 'modalLeave' : 'modalEnter'); + return this._nav && this._nav.config.get(key); + } + + /** + * @private + * Override the load method and load our child component + */ + loaded(done: Function) { + // grab the instance, and proxy the ngAfterViewInit method + let originalNgAfterViewInit = this.instance.ngAfterViewInit; + + this.instance.ngAfterViewInit = () => { + if (originalNgAfterViewInit) { + originalNgAfterViewInit(); + } + this.instance.loadComponent(done); + }; + } + + /** + * Present the action sheet instance. + * + * @param {NavOptions} [opts={}] Nav options to go with this transition. + * @returns {Promise} Returns a promise which is resolved when the transition has completed. + */ + present(navOptions: NavOptions = {}) { + return this._app.present(this, navOptions); + } + + /** + * @private + * DEPRECATED: Please inject ModalController instead + */ + private static create(cmp: any, opt: any) { + // deprecated warning: added beta.11 2016-06-27 + console.warn('Modal.create(..) has been deprecated. Please inject ModalController instead'); + } +} + + +/** + * @name ModalController * @description * A Modal is a content pane that goes over the user's current page. * Usually it is used for making a choice or editing an item. A modal uses the @@ -37,18 +96,18 @@ import { windowDimensions } from '../../util/dom'; * * @usage * ```ts - * import { Modal, NavController, NavParams } from 'ionic-angular'; + * import { ModalController, NavParams } from 'ionic-angular'; * * @Component(...) * class HomePage { * - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private modalCtrl: ModalController) { + * * } * * presentProfileModal() { - * let profileModal = Modal.create(Profile, { userId: 8675309 }); - * this.nav.present(profileModal); + * let profileModal = this.modalCtrl.create(Profile, { userId: 8675309 }); + * profileModal.present(); * } * * } @@ -70,26 +129,26 @@ import { windowDimensions } from '../../util/dom'; * * ```ts * import { Component } from '@angular/core'; - * import { Modal, NavController, ViewController } from 'ionic-angular'; + * import { ModalController, ViewController } from 'ionic-angular'; * * @Component(...) * class HomePage { * - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private modalCtrl: ModalController) { + * * } * * presentContactModal() { - * let contactModal = Modal.create(ContactUs); - * this.nav.present(contactModal); + * let contactModal = this.modalCtrl.create(ContactUs); + * contactModal.present(); * } * * presentProfileModal() { - * let profileModal = Modal.create(Profile, { userId: 8675309 }); + * let profileModal = this.modalCtrl.create(Profile, { userId: 8675309 }); * profileModal.onDismiss(data => { * console.log(data); * }); - * this.nav.present(profileModal); + * profileModal.present(); * } * * } @@ -97,8 +156,8 @@ import { windowDimensions } from '../../util/dom'; * @Component(...) * class Profile { * - * constructor(viewCtrl: ViewController) { - * this.viewCtrl = viewCtrl; + * constructor(private viewCtrl: ViewController) { + * * } * * dismiss() { @@ -111,27 +170,10 @@ import { windowDimensions } from '../../util/dom'; * @demo /docs/v2/demos/modal/ * @see {@link /docs/v2/components#modals Modal Component Docs} */ -export class Modal extends ViewController { - - constructor(componentType: any, data: any = {}, opts: ModalOptions = {}) { - data.componentType = componentType; - opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true; - opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; - data.opts = opts; - - super(ModalCmp, data); - this.isOverlay = true; - this.usePortal = true; - } - - /** - * @private - */ - getTransitionName(direction: string) { - let key = (direction === 'back' ? 'modalLeave' : 'modalEnter'); - return this._nav && this._nav.config.get(key); - } +@Injectable() +export class ModalController { + constructor(private _app: App) {} /** * Create a modal with the following options * @@ -145,194 +187,7 @@ export class Modal extends ViewController { * @param {object} data Any data to pass to the Modal view * @param {object} opts Modal options */ - static create(componentType: any, data: any = {}, opts: ModalOptions = {}) { - return new Modal(componentType, data, opts); - } - - // Override the load method and load our child component - loaded(done: Function) { - // grab the instance, and proxy the ngAfterViewInit method - let originalNgAfterViewInit = this.instance.ngAfterViewInit; - - this.instance.ngAfterViewInit = () => { - if (originalNgAfterViewInit) { - originalNgAfterViewInit(); - } - this.instance.loadComponent(done); - }; + create(componentType: any, data: any = {}, opts: ModalOptions = {}) { + return new Modal(this._app, componentType, data, opts); } } - -export interface ModalOptions { - showBackdrop?: boolean; - enableBackdropDismiss?: boolean; -} - -@Component({ - selector: 'ion-modal', - template: - '' + - '' -}) -export class ModalCmp { - - @ViewChild('viewport', {read: ViewContainerRef}) viewport: ViewContainerRef; - - private d: any; - private enabled: boolean; - - constructor(private _compiler: ComponentResolver, private _renderer: Renderer, private _navParams: NavParams, private _viewCtrl: ViewController) { - this.d = _navParams.data.opts; - } - - loadComponent(done: Function) { - let componentType = this._navParams.data.componentType; - addSelector(componentType, 'ion-page'); - - this._compiler.resolveComponent(componentType).then((componentFactory) => { - let componentRef = this.viewport.createComponent(componentFactory, this.viewport.length, this.viewport.parentInjector); - this._renderer.setElementClass(componentRef.location.nativeElement, 'show-page', true); - // auto-add page css className created from component JS class name - let cssClassName = pascalCaseToDashCase(componentType.name); - this._renderer.setElementClass(componentRef.location.nativeElement, cssClassName, true); - this._viewCtrl.setInstance(componentRef.instance); - this.enabled = true; - done(); - }); - } - - ngAfterViewInit() { - // intentionally kept empty - } - - dismiss(role: any): Promise { - return this._viewCtrl.dismiss(null, role); - } - - bdClick() { - if (this.enabled && this.d.enableBackdropDismiss) { - this.dismiss('backdrop'); - } - } - - @HostListener('body:keyup', ['$event']) - private _keyUp(ev: KeyboardEvent) { - if (this.enabled && this._viewCtrl.isLast() && ev.keyCode === Key.ESCAPE ) { - this.bdClick(); - } - } -} - -/** - * Animations for modals - */ - class ModalSlideIn extends PageTransition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdropEle = ele.querySelector('ion-backdrop'); - let backdrop = new Animation(backdropEle); - let wrapper = new Animation(ele.querySelector('.modal-wrapper')); - - backdrop.fromTo('opacity', 0.01, 0.4); - wrapper.fromTo('translateY', '100%', '0%'); - - - this - .element(enteringView.pageRef()) - .easing('cubic-bezier(0.36,0.66,0.04,1)') - .duration(400) - .add(backdrop) - .add(wrapper); - - if (enteringView.hasNavbar()) { - // entering page has a navbar - let enteringNavBar = new Animation(enteringView.navbarRef()); - enteringNavBar.before.addClass('show-navbar'); - this.add(enteringNavBar); - } - } - } - PageTransition.register('modal-slide-in', ModalSlideIn); - - -class ModalSlideOut extends PageTransition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapperEle = ele.querySelector('.modal-wrapper'); - let wrapperEleRect = wrapperEle.getBoundingClientRect(); - let wrapper = new Animation(wrapperEle); - - // height of the screen - top of the container tells us how much to scoot it down - // so it's off-screen - let screenDimensions = windowDimensions(); - wrapper.fromTo('translateY', '0px', `${screenDimensions.height - wrapperEleRect.top}px`); - backdrop.fromTo('opacity', 0.4, 0.0); - - this - .element(leavingView.pageRef()) - .easing('ease-out') - .duration(250) - .add(backdrop) - .add(wrapper); - } -} -PageTransition.register('modal-slide-out', ModalSlideOut); - - -class ModalMDSlideIn extends PageTransition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.modal-wrapper')); - - backdrop.fromTo('opacity', 0.01, 0.4); - wrapper.fromTo('translateY', '40px', '0px'); - wrapper.fromTo('opacity', 0.01, 1); - - const DURATION = 280; - const EASING = 'cubic-bezier(0.36,0.66,0.04,1)'; - this.element(enteringView.pageRef()).easing(EASING).duration(DURATION) - .add(backdrop) - .add(wrapper); - - if (enteringView.hasNavbar()) { - // entering page has a navbar - let enteringNavBar = new Animation(enteringView.navbarRef()); - enteringNavBar.before.addClass('show-navbar'); - this.add(enteringNavBar); - } - } -} -PageTransition.register('modal-md-slide-in', ModalMDSlideIn); - - -class ModalMDSlideOut extends PageTransition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.modal-wrapper')); - - backdrop.fromTo('opacity', 0.4, 0.0); - wrapper.fromTo('translateY', '0px', '40px'); - wrapper.fromTo('opacity', 0.99, 0); - - this - .element(leavingView.pageRef()) - .duration(200) - .easing('cubic-bezier(0.47,0,0.745,0.715)') - .add(wrapper) - .add(backdrop); - } -} -PageTransition.register('modal-md-slide-out', ModalMDSlideOut); diff --git a/src/components/nav/nav-controller.ts b/src/components/nav/nav-controller.ts index 2171cce3f9..bcae542c5b 100644 --- a/src/components/nav/nav-controller.ts +++ b/src/components/nav/nav-controller.ts @@ -8,11 +8,12 @@ import { isBlank, pascalCaseToDashCase } from '../../util/util'; import { Keyboard } from '../../util/keyboard'; import { MenuController } from '../menu/menu-controller'; import { NavParams } from './nav-params'; -import { NavPortal } from './nav-portal'; +import { NavOptions } from './nav-options'; import { SwipeBackGesture } from './swipe-back'; import { Transition } from '../../transitions/transition'; import { ViewController } from './view-controller'; + /** * @name NavController * @description @@ -52,8 +53,8 @@ import { ViewController } from './view-controller'; * * ```ts * class MyComponent { - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private nav: NavController) { + * * } * } * ``` @@ -162,7 +163,6 @@ export class NavController extends Ion { private _trans: Transition; private _sbGesture: SwipeBackGesture; private _sbThreshold: number; - private _portal: NavPortal; private _viewport: ViewContainerRef; private _children: any[] = []; @@ -240,20 +240,6 @@ export class NavController extends Ion { this.viewDidUnload = new EventEmitter(); } - /** - * @private - */ - getPortal(): NavController { - return this._portal; - } - - /** - * @private - */ - setPortal(val: NavPortal) { - this._portal = val; - } - /** * @private */ @@ -283,8 +269,8 @@ export class NavController extends Ion { * import {Info } from '../info/info' * * export class Home { - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private nav: NavController) { + * * } * setPages() { * this.nav.setPages([ {page: List}, {page: Detail}, {page:Info} ]); @@ -306,8 +292,8 @@ export class NavController extends Ion { * import {Detail } from '../detail/detail' * * export class Home { - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private nav: NavController) { + * * } * setPages() { * this.nav.setPages([ {page: List}, {page: Detail} ], { @@ -328,8 +314,8 @@ export class NavController extends Ion { * import {Detail } from '../detail/detail'; * * export class Home { - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private nav: NavController) { + * * } * setPages() { * this.nav.setPages([{ @@ -427,8 +413,8 @@ export class NavController extends Ion { * * ```ts * class MyClass{ - * constructor(nav: NavController){ - * this.nav = nav; + * constructor(private nav: NavController){ + * * } * * pushPage(user){ @@ -456,68 +442,14 @@ export class NavController extends Ion { } /** - * Present is how an app display overlays on top of the content, from within the - * root level `NavController`. The `present` method is used by overlays, such - * as `ActionSheet`, `Alert`, and `Modal`. The main difference between `push` - * and `present` is that `present` takes a `ViewController` instance, whereas - * `push` takes a component class which hasn't been instantiated yet. - * Additionally, `present` will place the overlay in the root NavController's - * stack. - * - * ```ts - * class MyClass{ - * constructor(nav: NavController) { - * this.nav = nav; - * } - * - * presentModal() { - * let modal = Modal.create(ProfilePage); - * this.nav.present(modal); - * } - * } - * ``` - * - * @param {ViewController} enteringView The component you want to push on the navigation stack. - * @param {object} [opts={}] Nav options to go with this transition. - * @returns {Promise} Returns a promise which is resolved when the transition has completed. + * @private + * DEPRECATED: Please use inject the overlays controller and use the present method on the instance instead. */ - present(enteringView: ViewController, opts?: NavOptions): Promise { - let rootNav = this.rootNav; - - if (rootNav['_tabs']) { - // TODO: must have until this goes in - // https://github.com/angular/angular/issues/5481 - console.error('A parent is required for ActionSheet/Alert/Modal/Loading'); - return; - } - - if (isBlank(opts)) { - opts = {}; - } - - if (enteringView.usePortal && rootNav._portal) { - return rootNav._portal.present(enteringView, opts); - } - - enteringView.setNav(rootNav); - - opts.keyboardClose = false; - opts.direction = 'forward'; - - if (!opts.animation) { - opts.animation = enteringView.getTransitionName('forward'); - } - - enteringView.setLeavingOpts({ - keyboardClose: false, - direction: 'back', - animation: enteringView.getTransitionName('back'), - ev: opts.ev - }); - - // present() always uses the root nav - // start the transition - return rootNav._insertViews(-1, [enteringView], opts); + private present(enteringView: ViewController, opts?: NavOptions): Promise { + // deprecated warning: added beta.11 2016-06-27 + console.warn('nav.present() has been deprecated.\n' + + 'Please use inject the overlays controller and use the present method on the instance instead.'); + return Promise.resolve(); } /** @@ -526,8 +458,8 @@ export class NavController extends Ion { * * ```ts * export class Detail { - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private nav: NavController) { + * * } * insertPage(){ * this.nav.insert(1, Info); @@ -552,8 +484,8 @@ export class NavController extends Ion { * * ```ts * export class Detail { - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private nav: NavController) { + * * } * insertPages(){ * let pages = [ @@ -577,10 +509,13 @@ export class NavController extends Ion { */ insertPages(insertIndex: number, insertPages: Array<{page: any, params?: any}>, opts?: NavOptions): Promise { let views = insertPages.map(p => new ViewController(p.page, p.params)); - return this._insertViews(insertIndex, views, opts); + return this.insertViews(insertIndex, views, opts); } - private _insertViews(insertIndex: number, insertViews: ViewController[], opts?: NavOptions): Promise { + /** + * @private + */ + insertViews(insertIndex: number, insertViews: ViewController[], opts?: NavOptions): Promise { if (!insertViews || !insertViews.length) { return Promise.reject('invalid pages'); } @@ -764,8 +699,8 @@ export class NavController extends Ion { * * ```ts * export class Detail { - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private nav: NavController) { + * * } * removePage(){ * this.nav.remove(1); @@ -832,10 +767,12 @@ export class NavController extends Ion { // only we're looking for an actual NavController w/ stack of views leavingView.fireWillLeave(); this.viewWillLeave.emit(leavingView); + this._app.viewWillLeave.emit(leavingView); return parentNav.pop(opts).then((rtnVal: boolean) => { leavingView.fireDidLeave(); this.viewDidLeave.emit(leavingView); + this._app.viewDidLeave.emit(leavingView); return rtnVal; }); } @@ -934,6 +871,7 @@ export class NavController extends Ion { view.state = STATE_INIT_LEAVE; view.fireWillUnload(); this.viewWillUnload.emit(view); + this._app.viewWillUnload.emit(view); // from the index of the leaving view, go backwards and // find the first view that is inactive so it can be the entering @@ -967,9 +905,7 @@ export class NavController extends Ion { // apart of any transitions that will eventually happen this._views.filter(v => v.state === STATE_REMOVE).forEach(view => { view.fireWillLeave(); - this.viewWillLeave.emit(view); view.fireDidLeave(); - this.viewDidLeave.emit(view); this._views.splice(this.indexOf(view), 1); view.destroy(); }); @@ -1004,7 +940,6 @@ export class NavController extends Ion { // if no entering view then create a bogus one enteringView = new ViewController(); enteringView.fireLoaded(); - this.viewDidLoad.emit(enteringView); } /* Async steps to complete a transition @@ -1062,6 +997,8 @@ export class NavController extends Ion { this.loadPage(enteringView, this._viewport, opts, () => { enteringView.fireLoaded(); this.viewDidLoad.emit(enteringView); + this._app.viewDidLoad.emit(enteringView); + this._postRender(transId, enteringView, leavingView, isAlreadyTransitioning, opts, done); }); } @@ -1122,6 +1059,7 @@ export class NavController extends Ion { // view hasn't explicitly set not to enteringView.fireWillEnter(); this.viewWillEnter.emit(enteringView); + this._app.viewWillEnter.emit(enteringView); } if (enteringView.fireOtherLifecycles) { @@ -1129,6 +1067,7 @@ export class NavController extends Ion { // view hasn't explicitly set not to leavingView.fireWillLeave(); this.viewWillLeave.emit(leavingView); + this._app.viewWillLeave.emit(leavingView); } } else { @@ -1243,6 +1182,7 @@ export class NavController extends Ion { // view hasn't explicitly set not to enteringView.fireDidEnter(); this.viewDidEnter.emit(enteringView); + this._app.viewDidEnter.emit(enteringView); } if (enteringView.fireOtherLifecycles) { @@ -1250,6 +1190,7 @@ export class NavController extends Ion { // view hasn't explicitly set not to leavingView.fireDidLeave(); this.viewDidLeave.emit(leavingView); + this._app.viewDidLeave.emit(leavingView); } } @@ -1357,14 +1298,6 @@ export class NavController extends Ion { // see if we should add the swipe back gesture listeners or not this._sbCheck(); - if (this._portal) { - this._portal._views.forEach(view => { - if (view.data && view.data.dismissOnPageChange) { - view.dismiss(); - } - }); - } - } else { // darn, so this wasn't the most recent transition // so while this one did end, there's another more recent one @@ -1408,6 +1341,8 @@ export class NavController extends Ion { destroys.forEach(view => { this._views.splice(this.indexOf(view), 1); view.destroy(); + this.viewDidUnload.emit(view); + this._app.viewDidUnload.emit(view); }); // if any z-index goes under 0, then reset them all @@ -1771,6 +1706,17 @@ export class NavController extends Ion { return nav; } + /** + * @private + */ + dismissPageChangeViews() { + this._views.forEach(view => { + if (view.data && view.data.dismissOnPageChange) { + view.dismiss(); + } + }); + } + /** * @private */ @@ -1819,20 +1765,6 @@ export class NavController extends Ion { } -export interface NavOptions { - animate?: boolean; - animation?: string; - direction?: string; - duration?: number; - easing?: string; - keyboardClose?: boolean; - preload?: boolean; - transitionDelay?: number; - progressAnimation?: boolean; - climbNav?: boolean; - ev?: any; -} - const STATE_ACTIVE = 'active'; const STATE_INACTIVE = 'inactive'; const STATE_INIT_ENTER = 'init_enter'; diff --git a/src/components/nav/nav-options.ts b/src/components/nav/nav-options.ts new file mode 100644 index 0000000000..076478540c --- /dev/null +++ b/src/components/nav/nav-options.ts @@ -0,0 +1,14 @@ + +export interface NavOptions { + animate?: boolean; + animation?: string; + direction?: string; + duration?: number; + easing?: string; + keyboardClose?: boolean; + preload?: boolean; + transitionDelay?: number; + progressAnimation?: boolean; + climbNav?: boolean; + ev?: any; +} diff --git a/src/components/nav/nav-portal.ts b/src/components/nav/nav-portal.ts index 1d956c008b..95a95bec4f 100644 --- a/src/components/nav/nav-portal.ts +++ b/src/components/nav/nav-portal.ts @@ -1,10 +1,9 @@ -import {Directive, ElementRef, Optional, NgZone, Renderer, ComponentResolver, ViewContainerRef} from '@angular/core'; +import { ComponentResolver, Directive, ElementRef, forwardRef, Inject, NgZone, Optional, Renderer, ViewContainerRef } from '@angular/core'; -import {App} from '../app/app'; -import {Config} from '../../config/config'; -import {Keyboard} from '../../util/keyboard'; -import {NavController} from './nav-controller'; -import {ViewController} from './view-controller'; +import { App } from '../app/app'; +import { Config } from '../../config/config'; +import { Keyboard } from '../../util/keyboard'; +import { NavController } from '../nav/nav-controller'; /** * @private @@ -14,9 +13,7 @@ import {ViewController} from './view-controller'; }) export class NavPortal extends NavController { constructor( - @Optional() viewCtrl: ViewController, - @Optional() parent: NavController, - app: App, + @Inject(forwardRef(() => App)) app: App, config: Config, keyboard: Keyboard, elementRef: ElementRef, @@ -25,8 +22,14 @@ export class NavPortal extends NavController { compiler: ComponentResolver, viewPort: ViewContainerRef ) { - super(parent, app, config, keyboard, elementRef, zone, renderer, compiler); + super(null, app, config, keyboard, elementRef, zone, renderer, compiler); this.isPortal = true; this.setViewport(viewPort); + app.setPortal(this); + + // on every page change make sure the portal has + // dismissed any views that should be auto dismissed on page change + app.viewDidLeave.subscribe(this.dismissPageChangeViews.bind(this)); } + } diff --git a/src/components/nav/nav.ts b/src/components/nav/nav.ts index b2bdcaa516..59cc949419 100644 --- a/src/components/nav/nav.ts +++ b/src/components/nav/nav.ts @@ -5,7 +5,6 @@ import { Config } from '../../config/config'; import { Keyboard } from '../../util/keyboard'; import { isTrueProperty } from '../../util/util'; import { NavController } from './nav-controller'; -import { NavPortal } from './nav-portal'; import { ViewController } from './view-controller'; /** @@ -108,8 +107,10 @@ import { ViewController } from './view-controller'; */ @Component({ selector: 'ion-nav', - template: '
', - directives: [NavPortal], + template: ` +
+ + `, encapsulation: ViewEncapsulation.None, }) export class Nav extends NavController implements AfterViewInit { @@ -194,8 +195,4 @@ export class Nav extends NavController implements AfterViewInit { this._sbEnabled = isTrueProperty(val); } - @ViewChild(NavPortal) - private set _np(val: NavPortal) { - this.setPortal(val); - } } diff --git a/src/components/nav/test/nav-controller.spec.ts b/src/components/nav/test/nav-controller.spec.ts index fd4ae7fadd..2f6e0f4771 100644 --- a/src/components/nav/test/nav-controller.spec.ts +++ b/src/components/nav/test/nav-controller.spec.ts @@ -1,4 +1,4 @@ -import {NavController, Tabs, NavOptions, Config, ViewController} from '../../../../src'; +import { NavController, Tabs, NavOptions, Config, ViewController, App, Platform } from '../../../../src'; export function run() { describe('NavController', () => { @@ -1219,33 +1219,6 @@ export function run() { }); - describe('present', () => { - - it('should present in portal', () => { - let enteringView = new ViewController(); - enteringView.setPageRef({}); - enteringView.usePortal = true; - - expect(nav._portal.length()).toBe(0); - expect(nav.length()).toBe(0); - nav.present(enteringView); - expect(nav._portal.length()).toBe(1); - expect(nav.length()).toBe(0); - }); - - it('should present in main nav', () => { - let enteringView = new ViewController(); - enteringView.setPageRef({}); - enteringView.usePortal = false; - - expect(nav._portal.length()).toBe(0); - expect(nav.length()).toBe(0); - nav.present(enteringView); - expect(nav._portal.length()).toBe(0); - expect(nav.length()).toBe(1); - }); - }); - describe('getActive', () => { it('should getActive()', () => { expect(nav.getActive()).toBe(null); @@ -1645,6 +1618,7 @@ export function run() { // setup stuff let nav: MockNavController; let config = new Config(); + let platform = new Platform(); class Page1 {} class Page2 {} @@ -1659,7 +1633,8 @@ export function run() { function mockNav(): MockNavController { let elementRef = getElementRef(); - let nav = new MockNavController(null, null, config, null, elementRef, null, null, null); + let app = new App(config, platform); + let nav = new MockNavController(null, app, config, null, elementRef, null, null, null); nav._keyboard = { isOpen: function() { @@ -1680,8 +1655,6 @@ export function run() { setElementStyle: function(){} }; - nav._portal = new MockNavController(null, null, config, null, elementRef, null, null, null); - return nav; } diff --git a/src/components/nav/view-controller.ts b/src/components/nav/view-controller.ts index 692b415656..a1f18e694d 100644 --- a/src/components/nav/view-controller.ts +++ b/src/components/nav/view-controller.ts @@ -1,9 +1,10 @@ -import { ChangeDetectorRef, EventEmitter, ElementRef, Output, Renderer } from '@angular/core'; +import { ChangeDetectorRef, ElementRef, EventEmitter, Output, Renderer } from '@angular/core'; import { Footer, Header } from '../toolbar/toolbar'; import { isPresent, merge } from '../../util/util'; import { Navbar } from '../navbar/navbar'; -import { NavController, NavOptions } from './nav-controller'; +import { NavController } from './nav-controller'; +import { NavOptions } from './nav-options'; import { NavParams } from './nav-params'; @@ -79,11 +80,6 @@ export class ViewController { */ isOverlay: boolean = false; - /** - * @private - */ - usePortal: boolean = false; - /** * @private */ diff --git a/src/components/picker/picker-component.ts b/src/components/picker/picker-component.ts new file mode 100644 index 0000000000..a60143db74 --- /dev/null +++ b/src/components/picker/picker-component.ts @@ -0,0 +1,581 @@ +import { Component, ElementRef, EventEmitter, Input, HostListener, Output, QueryList, Renderer, ViewChild, ViewChildren, ViewEncapsulation } from '@angular/core'; +import { DomSanitizationService } from '@angular/platform-browser'; + +import { Animation } from '../../animations/animation'; +import { cancelRaf, pointerCoord, raf } from '../../util/dom'; +import { clamp, isNumber, isPresent, isString } from '../../util/util'; +import { Config } from '../../config/config'; +import { Key } from '../../util/key'; +import { NavParams } from '../nav/nav-params'; +import { Picker } from './picker'; +import { PickerOptions, PickerColumn, PickerColumnOption } from './picker-options'; +import { Transition, TransitionOptions } from '../../transitions/transition'; +import { UIEventManager } from '../../util/ui-event-manager'; +import { ViewController } from '../nav/view-controller'; + + +/** + * @private + */ +@Component({ + selector: '.picker-col', + template: ` +
{{col.prefix}}
+
+ +
+
{{col.suffix}}
+ `, + host: { + '[style.min-width]': 'col.columnWidth', + '[class.picker-opts-left]': 'col.align=="left"', + '[class.picker-opts-right]': 'col.align=="right"', + } +}) +export class PickerColumnCmp { + @ViewChild('colEle') colEle: ElementRef; + @Input() col: PickerColumn; + y: number = 0; + colHeight: number; + optHeight: number; + velocity: number; + pos: number[] = []; + startY: number = null; + rafId: number; + bounceFrom: number; + minY: number; + maxY: number; + rotateFactor: number; + lastIndex: number; + receivingEvents: boolean = false; + events: UIEventManager = new UIEventManager(); + + @Output() ionChange: EventEmitter = new EventEmitter(); + + constructor(config: Config, private elementRef: ElementRef, private _sanitizer: DomSanitizationService) { + this.rotateFactor = config.getNumber('pickerRotateFactor', 0); + } + + ngAfterViewInit() { + // get the scrollable element within the column + let colEle: HTMLElement = this.colEle.nativeElement; + + this.colHeight = colEle.clientHeight; + + // get the height of one option + this.optHeight = (colEle.firstElementChild ? colEle.firstElementChild.clientHeight : 0); + + // set the scroll position for the selected option + this.setSelected(this.col.selectedIndex, 0); + + // Listening for pointer events + this.events.pointerEventsRef(this.elementRef, + (ev: any) => this.pointerStart(ev), + (ev: any) => this.pointerMove(ev), + (ev: any) => this.pointerEnd(ev) + ); + } + + ngOnDestroy() { + this.events.unlistenAll(); + } + + pointerStart(ev: UIEvent): boolean { + console.debug('picker, pointerStart', ev.type, this.startY); + + // cancel any previous raf's that haven't fired yet + cancelRaf(this.rafId); + + // remember where the pointer started from` + this.startY = pointerCoord(ev).y; + + // reset everything + this.receivingEvents = true; + this.velocity = 0; + this.pos.length = 0; + this.pos.push(this.startY, Date.now()); + + let minY = (this.col.options.length - 1); + let maxY = 0; + + for (var i = 0; i < this.col.options.length; i++) { + if (!this.col.options[i].disabled) { + minY = Math.min(minY, i); + maxY = Math.max(maxY, i); + } + } + + this.minY = (minY * this.optHeight * -1); + this.maxY = (maxY * this.optHeight * -1); + return true; + } + + pointerMove(ev: UIEvent) { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.startY === null) { + return; + } + + var currentY = pointerCoord(ev).y; + this.pos.push(currentY, Date.now()); + + // update the scroll position relative to pointer start position + var y = this.y + (currentY - this.startY); + + if (y > this.minY) { + // scrolling up higher than scroll area + y = Math.pow(y, 0.8); + this.bounceFrom = y; + + } else if (y < this.maxY) { + // scrolling down below scroll area + y += Math.pow(this.maxY - y, 0.9); + this.bounceFrom = y; + + } else { + this.bounceFrom = 0; + } + + this.update(y, 0, false, false); + } + + pointerEnd(ev: UIEvent) { + if (!this.receivingEvents) { + return; + } + this.receivingEvents = false; + this.velocity = 0; + + if (this.bounceFrom > 0) { + // bounce back up + this.update(this.minY, 100, true, true); + + } else if (this.bounceFrom < 0) { + // bounce back down + this.update(this.maxY, 100, true, true); + + } else if (this.startY !== null) { + var endY = pointerCoord(ev).y; + + console.debug('picker, pointerEnd', ev.type, endY); + + this.pos.push(endY, Date.now()); + + var endPos = (this.pos.length - 1); + var startPos = endPos; + var timeRange = (Date.now() - 100); + + // move pointer to position measured 100ms ago + for (var i = endPos; i > 0 && this.pos[i] > timeRange; i -= 2) { + startPos = i; + } + + if (startPos !== endPos) { + // compute relative movement between these two points + var timeOffset = (this.pos[endPos] - this.pos[startPos]); + var movedTop = (this.pos[startPos - 1] - this.pos[endPos - 1]); + + // based on XXms compute the movement to apply for each render step + this.velocity = ((movedTop / timeOffset) * FRAME_MS); + } + + if (Math.abs(endY - this.startY) > 3) { + ev.preventDefault(); + ev.stopPropagation(); + + var y = this.y + (endY - this.startY); + this.update(y, 0, true, true); + } + + } + + this.startY = null; + this.decelerate(); + } + + decelerate() { + let y = 0; + cancelRaf(this.rafId); + + if (isNaN(this.y) || !this.optHeight) { + // fallback in case numbers get outta wack + this.update(y, 0, true, true); + + } else if (Math.abs(this.velocity) > 0) { + // still decelerating + this.velocity *= DECELERATION_FRICTION; + + // do not let it go slower than a velocity of 1 + this.velocity = (this.velocity > 0 ? Math.max(this.velocity, 1) : Math.min(this.velocity, -1)); + + y = Math.round(this.y - this.velocity); + + if (y > this.minY) { + // whoops, it's trying to scroll up farther than the options we have! + y = this.minY; + this.velocity = 0; + + } else if (y < this.maxY) { + // gahh, it's trying to scroll down farther than we can! + y = this.maxY; + this.velocity = 0; + } + + console.log(`decelerate y: ${y}, velocity: ${this.velocity}, optHeight: ${this.optHeight}`); + + var notLockedIn = (y % this.optHeight !== 0 || Math.abs(this.velocity) > 1); + + this.update(y, 0, true, !notLockedIn); + + if (notLockedIn) { + // isn't locked in yet, keep decelerating until it is + this.rafId = raf(this.decelerate.bind(this)); + } + + } else if (this.y % this.optHeight !== 0) { + // needs to still get locked into a position so options line up + var currentPos = Math.abs(this.y % this.optHeight); + + // create a velocity in the direction it needs to scroll + this.velocity = (currentPos > (this.optHeight / 2) ? 1 : -1); + + this.decelerate(); + } + } + + optClick(ev: UIEvent, index: number) { + if (!this.velocity) { + ev.preventDefault(); + ev.stopPropagation(); + + this.setSelected(index, 150); + } + } + + setSelected(selectedIndex: number, duration: number) { + // if there is a selected index, then figure out it's y position + // if there isn't a selected index, then just use the top y position + let y = (selectedIndex > -1) ? ((selectedIndex * this.optHeight) * -1) : 0; + + cancelRaf(this.rafId); + this.velocity = 0; + + // so what y position we're at + this.update(y, duration, true, true); + } + + update(y: number, duration: number, saveY: boolean, emitChange: boolean) { + // ensure we've got a good round number :) + y = Math.round(y); + + this.col.selectedIndex = Math.max(Math.abs(Math.round(y / this.optHeight)), 0); + + for (var i = 0; i < this.col.options.length; i++) { + var opt = this.col.options[i]; + var optTop = (i * this.optHeight); + var optOffset = (optTop + y); + + var rotateX = (optOffset * this.rotateFactor); + var translateX = 0; + var translateY = 0; + var translateZ = 0; + + if (this.rotateFactor !== 0) { + translateX = 0; + translateZ = 90; + if (rotateX > 90 || rotateX < -90) { + translateX = -9999; + rotateX = 0; + } + + } else { + translateY = optOffset; + } + + opt._trans = this._sanitizer.bypassSecurityTrustStyle(`rotateX(${rotateX}deg) translate3d(${translateX}px,${translateY}px,${translateZ}px)`); + opt._dur = (duration > 0 ? duration + 'ms' : ''); + } + + if (saveY) { + this.y = y; + } + + if (emitChange) { + if (this.lastIndex === undefined) { + // have not set a last index yet + this.lastIndex = this.col.selectedIndex; + + } else if (this.lastIndex !== this.col.selectedIndex) { + // new selected index has changed from the last index + // update the lastIndex and emit that it has changed + this.lastIndex = this.col.selectedIndex; + this.ionChange.emit(this.col.options[this.col.selectedIndex]); + } + } + } + + refresh() { + let min = this.col.options.length - 1; + let max = 0; + + for (var i = 0; i < this.col.options.length; i++) { + if (!this.col.options[i].disabled) { + min = Math.min(min, i); + max = Math.max(max, i); + } + } + + var selectedIndex = clamp(min, this.col.selectedIndex, max); + + if (selectedIndex !== this.col.selectedIndex) { + var y = (selectedIndex * this.optHeight) * -1; + this.update(y, 150, true, true); + } + } + +} + + + +/** + * @private + */ +@Component({ + selector: 'ion-picker-cmp', + template: ` + +
+
+
+ +
+
+
+
+
+
+
+
+ `, + host: { + 'role': 'dialog' + }, + directives: [PickerColumnCmp], + encapsulation: ViewEncapsulation.None, +}) +export class PickerCmp { + @ViewChildren(PickerColumnCmp) private _cols: QueryList; + private d: PickerOptions; + private enabled: boolean; + private lastClick: number; + private id: number; + + constructor( + private _viewCtrl: ViewController, + private _elementRef: ElementRef, + private _config: Config, + params: NavParams, + renderer: Renderer + ) { + this.d = params.data; + + if (this.d.cssClass) { + this.d.cssClass.split(' ').forEach(cssClass => { + renderer.setElementClass(_elementRef.nativeElement, cssClass, true); + }); + } + + this.id = (++pickerIds); + this.lastClick = 0; + } + + ionViewLoaded() { + // normalize the data + let data = this.d; + + data.buttons = data.buttons.map(button => { + if (isString(button)) { + return { text: button }; + } + if (button.role) { + button.cssRole = `picker-toolbar-${button.role}`; + } + return button; + }); + + // clean up dat data + data.columns = data.columns.map(column => { + if (!isPresent(column.columnWidth)) { + column.columnWidth = (100 / data.columns.length) + '%'; + } + if (!isPresent(column.options)) { + column.options = []; + } + + column.options = column.options.map(inputOpt => { + let opt: PickerColumnOption = { + text: '', + value: '', + disabled: inputOpt.disabled, + }; + + if (isPresent(inputOpt)) { + if (isString(inputOpt) || isNumber(inputOpt)) { + opt.text = inputOpt.toString(); + opt.value = inputOpt; + + } else { + opt.text = isPresent(inputOpt.text) ? inputOpt.text : inputOpt.value; + opt.value = isPresent(inputOpt.value) ? inputOpt.value : inputOpt.text; + } + } + + return opt; + }); + return column; + }); + } + + refresh() { + this._cols.forEach(column => { + column.refresh(); + }); + } + + private _colChange(selectedOption: PickerColumnOption) { + // one of the columns has changed its selected index + var picker = this._viewCtrl; + picker.ionChange.emit(this.getSelected()); + } + + @HostListener('body:keyup', ['$event']) + private _keyUp(ev: KeyboardEvent) { + if (this.enabled && this._viewCtrl.isLast()) { + if (ev.keyCode === Key.ENTER) { + if (this.lastClick + 1000 < Date.now()) { + // do not fire this click if there recently was already a click + // this can happen when the button has focus and used the enter + // key to click the button. However, both the click handler and + // this keyup event will fire, so only allow one of them to go. + console.debug('picker, enter button'); + let button = this.d.buttons[this.d.buttons.length - 1]; + this.btnClick(button); + } + + } else if (ev.keyCode === Key.ESCAPE) { + console.debug('picker, escape button'); + this.bdClick(); + } + } + } + + ionViewDidEnter() { + let activeElement: any = document.activeElement; + if (activeElement) { + activeElement.blur(); + } + + let focusableEle = this._elementRef.nativeElement.querySelector('button'); + if (focusableEle) { + focusableEle.focus(); + } + this.enabled = true; + } + + btnClick(button: any, dismissDelay?: number) { + if (!this.enabled) { + return; + } + + // keep the time of the most recent button click + this.lastClick = Date.now(); + + let shouldDismiss = true; + + if (button.handler) { + // a handler has been provided, execute it + // pass the handler the values from the inputs + if (button.handler(this.getSelected()) === false) { + // if the return value of the handler is false then do not dismiss + shouldDismiss = false; + } + } + + if (shouldDismiss) { + setTimeout(() => { + this.dismiss(button.role); + }, dismissDelay || this._config.get('pageTransitionDelay')); + } + } + + bdClick() { + if (this.enabled && this.d.enableBackdropDismiss) { + this.dismiss('backdrop'); + } + } + + dismiss(role: any): Promise { + return this._viewCtrl.dismiss(this.getSelected(), role); + } + + getSelected(): any { + let selected: {[k: string]: any} = {}; + this.d.columns.forEach((col, index) => { + let selectedColumn = col.options[col.selectedIndex]; + selected[col.name] = { + text: selectedColumn ? selectedColumn.text : null, + value: selectedColumn ? selectedColumn.value : null, + columnIndex: index, + }; + }); + return selected; + } +} + + +/** + * Animations for pickers + */ +class PickerSlideIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.picker-wrapper')); + + backdrop.fromTo('opacity', 0.01, 0.26); + wrapper.fromTo('translateY', '100%', '0%'); + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper); + } +} +Transition.register('picker-slide-in', PickerSlideIn); + + +class PickerSlideOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.picker-wrapper')); + + backdrop.fromTo('opacity', 0.26, 0); + wrapper.fromTo('translateY', '0%', '100%'); + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(backdrop).add(wrapper); + } +} +Transition.register('picker-slide-out', PickerSlideOut); + + +let pickerIds = -1; +const DECELERATION_FRICTION = 0.97; +const FRAME_MS = (1000 / 60); diff --git a/src/components/picker/picker-options.ts b/src/components/picker/picker-options.ts new file mode 100644 index 0000000000..3facf30e81 --- /dev/null +++ b/src/components/picker/picker-options.ts @@ -0,0 +1,27 @@ + +export interface PickerOptions { + buttons?: any[]; + columns?: PickerColumn[]; + cssClass?: string; + enableBackdropDismiss?: boolean; +} + +export interface PickerColumn { + name?: string; + align?: string; + selectedIndex?: number; + prefix?: string; + suffix?: string; + options?: PickerColumnOption[]; + cssClass?: string; + columnWidth?: string; + prefixWidth?: string; + suffixWidth?: string; + optionsWidth?: string; +} + +export interface PickerColumnOption { + text?: string; + value?: any; + disabled?: boolean; +} diff --git a/src/components/picker/picker.ts b/src/components/picker/picker.ts index a4d415e70b..29c4cf94a5 100644 --- a/src/components/picker/picker.ts +++ b/src/components/picker/picker.ts @@ -1,32 +1,27 @@ -import { Component, ElementRef, EventEmitter, Input, HostListener, Output, QueryList, Renderer, ViewChild, ViewChildren, ViewEncapsulation } from '@angular/core'; -import { DomSanitizationService } from '@angular/platform-browser'; +import { EventEmitter, Injectable, Output } from '@angular/core'; -import { Animation } from '../../animations/animation'; -import { cancelRaf, pointerCoord, raf } from '../../util/dom'; -import { clamp, isNumber, isPresent, isString } from '../../util/util'; -import { Config } from '../../config/config'; -import { Key } from '../../util/key'; -import { NavParams } from '../nav/nav-params'; -import { Transition, TransitionOptions } from '../../transitions/transition'; -import { UIEventManager } from '../../util/ui-event-manager'; +import { App } from '../app/app'; +import { isPresent } from '../../util/util'; +import { NavOptions } from '../nav/nav-options'; +import { PickerCmp } from './picker-component'; +import { PickerOptions, PickerColumn } from './picker-options'; import { ViewController } from '../nav/view-controller'; - /** - * @name Picker - * @description - * + * @private */ export class Picker extends ViewController { + private _app: App; @Output() ionChange: EventEmitter; - constructor(opts: PickerOptions = {}) { + constructor(app: App, opts: PickerOptions = {}) { opts.columns = opts.columns || []; opts.buttons = opts.buttons || []; opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; - super(PickerDisplayCmp, opts); + super(PickerCmp, opts); + this._app = app; this.isOverlay = true; this.ionChange = new EventEmitter(); @@ -35,7 +30,6 @@ export class Picker extends ViewController { // for example, when an picker enters, the current active view should // not fire its lifecycle events because it's not conceptually leaving this.fireOtherLifecycles = false; - this.usePortal = true; } /** @@ -75,599 +69,44 @@ export class Picker extends ViewController { this.data.cssClass = cssClass; } - static create(opts: PickerOptions = {}) { - return new Picker(opts); + /** + * Present the picker instance. + * + * @param {NavOptions} [opts={}] Nav options to go with this transition. + * @returns {Promise} Returns a promise which is resolved when the transition has completed. + */ + present(navOptions: NavOptions = {}) { + return this._app.present(this, navOptions); + } + + /** + * @private + * DEPRECATED: Please inject PickerController instead + */ + private static create(opt: any) { + // deprecated warning: added beta.11 2016-06-27 + console.warn('Picker.create(..) has been deprecated. Please inject PickerController instead'); } } + /** - * @private + * @name PickerController + * @description + * */ -@Component({ - selector: '.picker-col', - template: - '
{{col.prefix}}
' + - '
' + - '' + - '
' + - '
{{col.suffix}}
', - host: { - '[style.min-width]': 'col.columnWidth', - '[class.picker-opts-left]': 'col.align=="left"', - '[class.picker-opts-right]': 'col.align=="right"', - } -}) -class PickerColumnCmp { - @ViewChild('colEle') colEle: ElementRef; - @Input() col: PickerColumn; - y: number = 0; - colHeight: number; - optHeight: number; - velocity: number; - pos: number[] = []; - startY: number = null; - rafId: number; - bounceFrom: number; - minY: number; - maxY: number; - rotateFactor: number; - lastIndex: number; - receivingEvents: boolean = false; - events: UIEventManager = new UIEventManager(); +@Injectable() +export class PickerController { - @Output() ionChange: EventEmitter = new EventEmitter(); + constructor(private _app: App) {} - constructor(config: Config, private elementRef: ElementRef, private _sanitizer: DomSanitizationService) { - this.rotateFactor = config.getNumber('pickerRotateFactor', 0); + /** + * Open a picker. + */ + create(opts: PickerOptions = {}): Picker { + return new Picker(this._app, opts); } - ngAfterViewInit() { - // get the scrollable element within the column - let colEle: HTMLElement = this.colEle.nativeElement; - - this.colHeight = colEle.clientHeight; - - // get the height of one option - this.optHeight = (colEle.firstElementChild ? colEle.firstElementChild.clientHeight : 0); - - // set the scroll position for the selected option - this.setSelected(this.col.selectedIndex, 0); - - // Listening for pointer events - this.events.pointerEventsRef(this.elementRef, - (ev: any) => this.pointerStart(ev), - (ev: any) => this.pointerMove(ev), - (ev: any) => this.pointerEnd(ev) - ); - } - - ngOnDestroy() { - this.events.unlistenAll(); - } - - pointerStart(ev: UIEvent): boolean { - console.debug('picker, pointerStart', ev.type, this.startY); - - // cancel any previous raf's that haven't fired yet - cancelRaf(this.rafId); - - // remember where the pointer started from` - this.startY = pointerCoord(ev).y; - - // reset everything - this.receivingEvents = true; - this.velocity = 0; - this.pos.length = 0; - this.pos.push(this.startY, Date.now()); - - let minY = (this.col.options.length - 1); - let maxY = 0; - - for (var i = 0; i < this.col.options.length; i++) { - if (!this.col.options[i].disabled) { - minY = Math.min(minY, i); - maxY = Math.max(maxY, i); - } - } - - this.minY = (minY * this.optHeight * -1); - this.maxY = (maxY * this.optHeight * -1); - return true; - } - - pointerMove(ev: UIEvent) { - ev.preventDefault(); - ev.stopPropagation(); - - if (this.startY === null) { - return; - } - - var currentY = pointerCoord(ev).y; - this.pos.push(currentY, Date.now()); - - // update the scroll position relative to pointer start position - var y = this.y + (currentY - this.startY); - - if (y > this.minY) { - // scrolling up higher than scroll area - y = Math.pow(y, 0.8); - this.bounceFrom = y; - - } else if (y < this.maxY) { - // scrolling down below scroll area - y += Math.pow(this.maxY - y, 0.9); - this.bounceFrom = y; - - } else { - this.bounceFrom = 0; - } - - this.update(y, 0, false, false); - } - - pointerEnd(ev: UIEvent) { - if (!this.receivingEvents) { - return; - } - this.receivingEvents = false; - this.velocity = 0; - - if (this.bounceFrom > 0) { - // bounce back up - this.update(this.minY, 100, true, true); - - } else if (this.bounceFrom < 0) { - // bounce back down - this.update(this.maxY, 100, true, true); - - } else if (this.startY !== null) { - var endY = pointerCoord(ev).y; - - console.debug('picker, pointerEnd', ev.type, endY); - - this.pos.push(endY, Date.now()); - - var endPos = (this.pos.length - 1); - var startPos = endPos; - var timeRange = (Date.now() - 100); - - // move pointer to position measured 100ms ago - for (var i = endPos; i > 0 && this.pos[i] > timeRange; i -= 2) { - startPos = i; - } - - if (startPos !== endPos) { - // compute relative movement between these two points - var timeOffset = (this.pos[endPos] - this.pos[startPos]); - var movedTop = (this.pos[startPos - 1] - this.pos[endPos - 1]); - - // based on XXms compute the movement to apply for each render step - this.velocity = ((movedTop / timeOffset) * FRAME_MS); - } - - if (Math.abs(endY - this.startY) > 3) { - ev.preventDefault(); - ev.stopPropagation(); - - var y = this.y + (endY - this.startY); - this.update(y, 0, true, true); - } - - } - - this.startY = null; - this.decelerate(); - } - - decelerate() { - let y = 0; - cancelRaf(this.rafId); - - if (isNaN(this.y) || !this.optHeight) { - // fallback in case numbers get outta wack - this.update(y, 0, true, true); - - } else if (Math.abs(this.velocity) > 0) { - // still decelerating - this.velocity *= DECELERATION_FRICTION; - - // do not let it go slower than a velocity of 1 - this.velocity = (this.velocity > 0 ? Math.max(this.velocity, 1) : Math.min(this.velocity, -1)); - - y = Math.round(this.y - this.velocity); - - if (y > this.minY) { - // whoops, it's trying to scroll up farther than the options we have! - y = this.minY; - this.velocity = 0; - - } else if (y < this.maxY) { - // gahh, it's trying to scroll down farther than we can! - y = this.maxY; - this.velocity = 0; - } - - console.log(`decelerate y: ${y}, velocity: ${this.velocity}, optHeight: ${this.optHeight}`); - - var notLockedIn = (y % this.optHeight !== 0 || Math.abs(this.velocity) > 1); - - this.update(y, 0, true, !notLockedIn); - - if (notLockedIn) { - // isn't locked in yet, keep decelerating until it is - this.rafId = raf(this.decelerate.bind(this)); - } - - } else if (this.y % this.optHeight !== 0) { - // needs to still get locked into a position so options line up - var currentPos = Math.abs(this.y % this.optHeight); - - // create a velocity in the direction it needs to scroll - this.velocity = (currentPos > (this.optHeight / 2) ? 1 : -1); - - this.decelerate(); - } - } - - optClick(ev: UIEvent, index: number) { - if (!this.velocity) { - ev.preventDefault(); - ev.stopPropagation(); - - this.setSelected(index, 150); - } - } - - setSelected(selectedIndex: number, duration: number) { - // if there is a selected index, then figure out it's y position - // if there isn't a selected index, then just use the top y position - let y = (selectedIndex > -1) ? ((selectedIndex * this.optHeight) * -1) : 0; - - cancelRaf(this.rafId); - this.velocity = 0; - - // so what y position we're at - this.update(y, duration, true, true); - } - - update(y: number, duration: number, saveY: boolean, emitChange: boolean) { - // ensure we've got a good round number :) - y = Math.round(y); - - this.col.selectedIndex = Math.max(Math.abs(Math.round(y / this.optHeight)), 0); - - for (var i = 0; i < this.col.options.length; i++) { - var opt = this.col.options[i]; - var optTop = (i * this.optHeight); - var optOffset = (optTop + y); - - var rotateX = (optOffset * this.rotateFactor); - var translateX = 0; - var translateY = 0; - var translateZ = 0; - - if (this.rotateFactor !== 0) { - translateX = 0; - translateZ = 90; - if (rotateX > 90 || rotateX < -90) { - translateX = -9999; - rotateX = 0; - } - - } else { - translateY = optOffset; - } - - opt._trans = this._sanitizer.bypassSecurityTrustStyle(`rotateX(${rotateX}deg) translate3d(${translateX}px,${translateY}px,${translateZ}px)`); - opt._dur = (duration > 0 ? duration + 'ms' : ''); - } - - if (saveY) { - this.y = y; - } - - if (emitChange) { - if (this.lastIndex === undefined) { - // have not set a last index yet - this.lastIndex = this.col.selectedIndex; - - } else if (this.lastIndex !== this.col.selectedIndex) { - // new selected index has changed from the last index - // update the lastIndex and emit that it has changed - this.lastIndex = this.col.selectedIndex; - this.ionChange.emit(this.col.options[this.col.selectedIndex]); - } - } - } - - refresh() { - let min = this.col.options.length - 1; - let max = 0; - - for (var i = 0; i < this.col.options.length; i++) { - if (!this.col.options[i].disabled) { - min = Math.min(min, i); - max = Math.max(max, i); - } - } - - var selectedIndex = clamp(min, this.col.selectedIndex, max); - - if (selectedIndex !== this.col.selectedIndex) { - var y = (selectedIndex * this.optHeight) * -1; - this.update(y, 150, true, true); - } - } - -} - - -/** - * @private - */ -@Component({ - selector: 'ion-picker-cmp', - template: - '' + - '
' + - '
' + - '
' + - '' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
', - host: { - 'role': 'dialog' - }, - directives: [PickerColumnCmp], - encapsulation: ViewEncapsulation.None, -}) -class PickerDisplayCmp { - @ViewChildren(PickerColumnCmp) private _cols: QueryList; - private d: PickerOptions; - private enabled: boolean; - private lastClick: number; - private id: number; - - constructor( - private _viewCtrl: ViewController, - private _elementRef: ElementRef, - private _config: Config, - params: NavParams, - renderer: Renderer - ) { - this.d = params.data; - - if (this.d.cssClass) { - this.d.cssClass.split(' ').forEach(cssClass => { - renderer.setElementClass(_elementRef.nativeElement, cssClass, true); - }); - } - - this.id = (++pickerIds); - this.lastClick = 0; - } - - ionViewLoaded() { - // normalize the data - let data = this.d; - - data.buttons = data.buttons.map(button => { - if (isString(button)) { - return { text: button }; - } - if (button.role) { - button.cssRole = `picker-toolbar-${button.role}`; - } - return button; - }); - - // clean up dat data - data.columns = data.columns.map(column => { - if (!isPresent(column.columnWidth)) { - column.columnWidth = (100 / data.columns.length) + '%'; - } - if (!isPresent(column.options)) { - column.options = []; - } - - column.options = column.options.map(inputOpt => { - let opt: PickerColumnOption = { - text: '', - value: '', - disabled: inputOpt.disabled, - }; - - if (isPresent(inputOpt)) { - if (isString(inputOpt) || isNumber(inputOpt)) { - opt.text = inputOpt.toString(); - opt.value = inputOpt; - - } else { - opt.text = isPresent(inputOpt.text) ? inputOpt.text : inputOpt.value; - opt.value = isPresent(inputOpt.value) ? inputOpt.value : inputOpt.text; - } - } - - return opt; - }); - return column; - }); - } - - refresh() { - this._cols.forEach(column => { - column.refresh(); - }); - } - - private _colChange(selectedOption: PickerColumnOption) { - // one of the columns has changed its selected index - var picker = this._viewCtrl; - picker.ionChange.emit(this.getSelected()); - } - - @HostListener('body:keyup', ['$event']) - private _keyUp(ev: KeyboardEvent) { - if (this.enabled && this._viewCtrl.isLast()) { - if (ev.keyCode === Key.ENTER) { - if (this.lastClick + 1000 < Date.now()) { - // do not fire this click if there recently was already a click - // this can happen when the button has focus and used the enter - // key to click the button. However, both the click handler and - // this keyup event will fire, so only allow one of them to go. - console.debug('picker, enter button'); - let button = this.d.buttons[this.d.buttons.length - 1]; - this.btnClick(button); - } - - } else if (ev.keyCode === Key.ESCAPE) { - console.debug('picker, escape button'); - this.bdClick(); - } - } - } - - ionViewDidEnter() { - let activeElement: any = document.activeElement; - if (activeElement) { - activeElement.blur(); - } - - let focusableEle = this._elementRef.nativeElement.querySelector('button'); - if (focusableEle) { - focusableEle.focus(); - } - this.enabled = true; - } - - btnClick(button: any, dismissDelay?: number) { - if (!this.enabled) { - return; - } - - // keep the time of the most recent button click - this.lastClick = Date.now(); - - let shouldDismiss = true; - - if (button.handler) { - // a handler has been provided, execute it - // pass the handler the values from the inputs - if (button.handler(this.getSelected()) === false) { - // if the return value of the handler is false then do not dismiss - shouldDismiss = false; - } - } - - if (shouldDismiss) { - setTimeout(() => { - this.dismiss(button.role); - }, dismissDelay || this._config.get('pageTransitionDelay')); - } - } - - bdClick() { - if (this.enabled && this.d.enableBackdropDismiss) { - this.dismiss('backdrop'); - } - } - - dismiss(role: any): Promise { - return this._viewCtrl.dismiss(this.getSelected(), role); - } - - getSelected(): any { - let selected: {[k: string]: any} = {}; - this.d.columns.forEach((col, index) => { - let selectedColumn = col.options[col.selectedIndex]; - selected[col.name] = { - text: selectedColumn ? selectedColumn.text : null, - value: selectedColumn ? selectedColumn.value : null, - columnIndex: index, - }; - }); - return selected; - } -} - -export interface PickerOptions { - buttons?: any[]; - columns?: PickerColumn[]; - cssClass?: string; - enableBackdropDismiss?: boolean; -} - -export interface PickerColumn { - name?: string; - align?: string; - selectedIndex?: number; - prefix?: string; - suffix?: string; - options?: PickerColumnOption[]; - cssClass?: string; - columnWidth?: string; - prefixWidth?: string; - suffixWidth?: string; - optionsWidth?: string; -} - -export interface PickerColumnOption { - text?: string; - value?: any; - disabled?: boolean; -} - - -/** - * Animations for pickers - */ -class PickerSlideIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.picker-wrapper')); - - backdrop.fromTo('opacity', 0.01, 0.26); - wrapper.fromTo('translateY', '100%', '0%'); - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper); - } -} -Transition.register('picker-slide-in', PickerSlideIn); - - -class PickerSlideOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.picker-wrapper')); - - backdrop.fromTo('opacity', 0.26, 0); - wrapper.fromTo('translateY', '0%', '100%'); - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(backdrop).add(wrapper); - } -} -Transition.register('picker-slide-out', PickerSlideOut); - - -let pickerIds = -1; -const DECELERATION_FRICTION = 0.97; -const FRAME_MS = (1000 / 60); +} \ No newline at end of file diff --git a/src/components/popover/popover-component.ts b/src/components/popover/popover-component.ts new file mode 100644 index 0000000000..79d071bc48 --- /dev/null +++ b/src/components/popover/popover-component.ts @@ -0,0 +1,353 @@ +import { Component, ComponentResolver, ElementRef, HostListener, Renderer, ViewChild, ViewContainerRef } from '@angular/core'; + +import { addSelector } from '../../config/bootstrap'; +import { Animation } from '../../animations/animation'; +import { Config } from '../../config/config'; +import { CSS, nativeRaf } from '../../util/dom'; +import { isPresent, pascalCaseToDashCase } from '../../util/util'; +import { Key } from '../../util/key'; +import { NavParams } from '../nav/nav-params'; +import { PageTransition } from '../../transitions/page-transition'; +import { TransitionOptions } from '../../transitions/transition'; +import { ViewController } from '../nav/view-controller'; + + +/** + * @private + */ +@Component({ + selector: 'ion-popover', + template: ` + +
+
+
+
+
+
+
+
+ ` +}) +export class PopoverCmp { + @ViewChild('viewport', {read: ViewContainerRef}) viewport: ViewContainerRef; + + private d: any; + private enabled: boolean; + private id: number; + private showSpinner: boolean; + + constructor( + private _compiler: ComponentResolver, + private _elementRef: ElementRef, + private _renderer: Renderer, + private _config: Config, + private _navParams: NavParams, + private _viewCtrl: ViewController + ) { + this.d = _navParams.data.opts; + + if (this.d.cssClass) { + _renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true); + } + + this.id = (++popoverIds); + } + + ionViewWillEnter() { + addSelector(this._navParams.data.componentType, 'ion-popover-inner'); + + this._compiler.resolveComponent(this._navParams.data.componentType).then((componentFactory) => { + let componentRef = this.viewport.createComponent(componentFactory, this.viewport.length, this.viewport.parentInjector); + + this._viewCtrl.setInstance(componentRef.instance); + + // manually fire ionViewWillEnter() since PopoverCmp's ionViewWillEnter already happened + this._viewCtrl.fireWillEnter(); + }); + } + + ngAfterViewInit() { + let activeElement: any = document.activeElement; + if (document.activeElement) { + activeElement.blur(); + } + this.enabled = true; + } + + dismiss(role: any): Promise { + return this._viewCtrl.dismiss(null, role); + } + + bdTouch(ev: UIEvent) { + ev.preventDefault(); + ev.stopPropagation(); + } + + bdClick() { + if (this.enabled && this.d.enableBackdropDismiss) { + this.dismiss('backdrop'); + } + } + + @HostListener('body:keyup', ['$event']) + private _keyUp(ev: KeyboardEvent) { + if (this.enabled && ev.keyCode === Key.ESCAPE && this._viewCtrl.isLast()) { + this.bdClick(); + } + } +} + + +/** + * Animations for popover + */ +class PopoverTransition extends PageTransition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + } + + mdPositionView(nativeEle: HTMLElement, ev: any) { + let originY = 'top'; + let originX = 'left'; + + let popoverWrapperEle = nativeEle.querySelector('.popover-wrapper'); + + // Popover content width and height + let popoverEle = nativeEle.querySelector('.popover-content'); + let popoverDim = popoverEle.getBoundingClientRect(); + let popoverWidth = popoverDim.width; + let popoverHeight = popoverDim.height; + + // Window body width and height + let bodyWidth = window.innerWidth; + let bodyHeight = window.innerHeight; + + // If ev was passed, use that for target element + let targetDim = ev && ev.target && ev.target.getBoundingClientRect(); + + let targetTop = (targetDim && 'top' in targetDim) ? targetDim.top : (bodyHeight / 2) - (popoverHeight / 2); + let targetLeft = (targetDim && 'left' in targetDim) ? targetDim.left : (bodyWidth / 2) - (popoverWidth / 2); + + let targetWidth = targetDim && targetDim.width || 0; + let targetHeight = targetDim && targetDim.height || 0; + + let popoverCSS = { + top: targetTop, + left: targetLeft + }; + + // If the popover left is less than the padding it is off screen + // to the left so adjust it, else if the width of the popover + // exceeds the body width it is off screen to the right so adjust + if (popoverCSS.left < POPOVER_MD_BODY_PADDING) { + popoverCSS.left = POPOVER_MD_BODY_PADDING; + } else if (popoverWidth + POPOVER_MD_BODY_PADDING + popoverCSS.left > bodyWidth) { + popoverCSS.left = bodyWidth - popoverWidth - POPOVER_MD_BODY_PADDING; + originX = 'right'; + } + + // If the popover when popped down stretches past bottom of screen, + // make it pop up if there's room above + if (targetTop + targetHeight + popoverHeight > bodyHeight && targetTop - popoverHeight > 0) { + popoverCSS.top = targetTop - popoverHeight; + nativeEle.className = nativeEle.className + ' popover-bottom'; + originY = 'bottom'; + // If there isn't room for it to pop up above the target cut it off + } else if (targetTop + targetHeight + popoverHeight > bodyHeight) { + popoverEle.style.bottom = POPOVER_MD_BODY_PADDING + 'px'; + } + + popoverEle.style.top = popoverCSS.top + 'px'; + popoverEle.style.left = popoverCSS.left + 'px'; + + popoverEle.style[CSS.transformOrigin] = originY + ' ' + originX; + + // Since the transition starts before styling is done we + // want to wait for the styles to apply before showing the wrapper + popoverWrapperEle.style.opacity = '1'; + } + + iosPositionView(nativeEle: HTMLElement, ev: any) { + let originY = 'top'; + let originX = 'left'; + + let popoverWrapperEle = nativeEle.querySelector('.popover-wrapper'); + + // Popover content width and height + let popoverEle = nativeEle.querySelector('.popover-content'); + let popoverDim = popoverEle.getBoundingClientRect(); + let popoverWidth = popoverDim.width; + let popoverHeight = popoverDim.height; + + // Window body width and height + let bodyWidth = window.innerWidth; + let bodyHeight = window.innerHeight; + + // If ev was passed, use that for target element + let targetDim = ev && ev.target && ev.target.getBoundingClientRect(); + + let targetTop = (targetDim && 'top' in targetDim) ? targetDim.top : (bodyHeight / 2) - (popoverHeight / 2); + let targetLeft = (targetDim && 'left' in targetDim) ? targetDim.left : (bodyWidth / 2); + let targetWidth = targetDim && targetDim.width || 0; + let targetHeight = targetDim && targetDim.height || 0; + + // The arrow that shows above the popover on iOS + var arrowEle = nativeEle.querySelector('.popover-arrow'); + let arrowDim = arrowEle.getBoundingClientRect(); + var arrowWidth = arrowDim.width; + var arrowHeight = arrowDim.height; + + // If no ev was passed, hide the arrow + if (!targetDim) { + arrowEle.style.display = 'none'; + } + + let arrowCSS = { + top: targetTop + targetHeight, + left: targetLeft + (targetWidth / 2) - (arrowWidth / 2) + }; + + let popoverCSS = { + top: targetTop + targetHeight + (arrowHeight - 1), + left: targetLeft + (targetWidth / 2) - (popoverWidth / 2) + }; + + // If the popover left is less than the padding it is off screen + // to the left so adjust it, else if the width of the popover + // exceeds the body width it is off screen to the right so adjust + if (popoverCSS.left < POPOVER_IOS_BODY_PADDING) { + popoverCSS.left = POPOVER_IOS_BODY_PADDING; + } else if (popoverWidth + POPOVER_IOS_BODY_PADDING + popoverCSS.left > bodyWidth) { + popoverCSS.left = bodyWidth - popoverWidth - POPOVER_IOS_BODY_PADDING; + originX = 'right'; + } + + // If the popover when popped down stretches past bottom of screen, + // make it pop up if there's room above + if (targetTop + targetHeight + popoverHeight > bodyHeight && targetTop - popoverHeight > 0) { + arrowCSS.top = targetTop - (arrowHeight + 1); + popoverCSS.top = targetTop - popoverHeight - (arrowHeight - 1); + nativeEle.className = nativeEle.className + ' popover-bottom'; + originY = 'bottom'; + // If there isn't room for it to pop up above the target cut it off + } else if (targetTop + targetHeight + popoverHeight > bodyHeight) { + popoverEle.style.bottom = POPOVER_IOS_BODY_PADDING + '%'; + } + + arrowEle.style.top = arrowCSS.top + 'px'; + arrowEle.style.left = arrowCSS.left + 'px'; + + popoverEle.style.top = popoverCSS.top + 'px'; + popoverEle.style.left = popoverCSS.left + 'px'; + + popoverEle.style[CSS.transformOrigin] = originY + ' ' + originX; + + // Since the transition starts before styling is done we + // want to wait for the styles to apply before showing the wrapper + popoverWrapperEle.style.opacity = '1'; + } +} + +class PopoverPopIn extends PopoverTransition { + constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.popover-wrapper')); + + wrapper.fromTo('opacity', 0.01, 1); + backdrop.fromTo('opacity', 0.01, 0.08); + + this + .easing('ease') + .duration(100) + .add(backdrop) + .add(wrapper); + } + + play() { + nativeRaf(() => { + this.iosPositionView(this.enteringView.pageRef().nativeElement, this.opts.ev); + super.play(); + }); + } +} +PageTransition.register('popover-pop-in', PopoverPopIn); + + +class PopoverPopOut extends PopoverTransition { + constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let backdrop = new Animation(ele.querySelector('ion-backdrop')); + let wrapper = new Animation(ele.querySelector('.popover-wrapper')); + + wrapper.fromTo('opacity', 0.99, 0); + backdrop.fromTo('opacity', 0.08, 0); + + this + .easing('ease') + .duration(500) + .add(backdrop) + .add(wrapper); + } +} +PageTransition.register('popover-pop-out', PopoverPopOut); + + +class PopoverMdPopIn extends PopoverTransition { + constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + + let content = new Animation(ele.querySelector('.popover-content')); + let viewport = new Animation(ele.querySelector('.popover-viewport')); + + content.fromTo('scale', 0.001, 1); + viewport.fromTo('opacity', 0.01, 1); + + this + .easing('cubic-bezier(0.36,0.66,0.04,1)') + .duration(300) + .add(content) + .add(viewport); + } + + play() { + nativeRaf(() => { + this.mdPositionView(this.enteringView.pageRef().nativeElement, this.opts.ev); + super.play(); + }); + } +} +PageTransition.register('popover-md-pop-in', PopoverMdPopIn); + + +class PopoverMdPopOut extends PopoverTransition { + constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + let wrapper = new Animation(ele.querySelector('.popover-wrapper')); + + wrapper.fromTo('opacity', 0.99, 0); + + this + .easing('ease') + .duration(500) + .fromTo('opacity', 0.01, 1) + .add(wrapper); + } +} +PageTransition.register('popover-md-pop-out', PopoverMdPopOut); + + +let popoverIds = -1; + +const POPOVER_IOS_BODY_PADDING = 2; +const POPOVER_MD_BODY_PADDING = 12; diff --git a/src/components/popover/popover-options.ts b/src/components/popover/popover-options.ts new file mode 100644 index 0000000000..2d6ec8626a --- /dev/null +++ b/src/components/popover/popover-options.ts @@ -0,0 +1,6 @@ + +export interface PopoverOptions { + cssClass?: string; + showBackdrop?: boolean; + enableBackdropDismiss?: boolean; +} diff --git a/src/components/popover/popover.ts b/src/components/popover/popover.ts index 0f8603ca21..f1d2115e16 100644 --- a/src/components/popover/popover.ts +++ b/src/components/popover/popover.ts @@ -1,21 +1,67 @@ -import { Component, ComponentResolver, ElementRef, HostListener, Renderer, ViewChild, ViewContainerRef } from '@angular/core'; +import { Injectable } from '@angular/core'; -import { addSelector } from '../../config/bootstrap'; -import { Animation } from '../../animations/animation'; -import { Config } from '../../config/config'; -import { CSS, nativeRaf } from '../../util/dom'; -import { isPresent, pascalCaseToDashCase } from '../../util/util'; -import { Key } from '../../util/key'; -import { NavParams } from '../nav/nav-params'; -import { PageTransition } from '../../transitions/page-transition'; -import { TransitionOptions } from '../../transitions/transition'; +import { App } from '../app/app'; +import { isPresent } from '../../util/util'; +import { NavOptions } from '../nav/nav-options'; +import { PopoverCmp } from './popover-component'; +import { PopoverOptions } from './popover-options'; import { ViewController } from '../nav/view-controller'; -const POPOVER_IOS_BODY_PADDING = 2; -const POPOVER_MD_BODY_PADDING = 12; /** - * @name Popover + * @private + */ +export class Popover extends ViewController { + private _app: App; + + constructor(app: App, componentType: any, data: any = {}, opts: PopoverOptions = {}) { + opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true; + opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; + + data.componentType = componentType; + data.opts = opts; + super(PopoverCmp, data); + this._app = app; + this.isOverlay = true; + + // by default, popovers should not fire lifecycle events of other views + // for example, when a popover enters, the current active view should + // not fire its lifecycle events because it's not conceptually leaving + this.fireOtherLifecycles = false; + } + + /** + * @private + */ + getTransitionName(direction: string) { + let key = (direction === 'back' ? 'popoverLeave' : 'popoverEnter'); + return this._nav && this._nav.config.get(key); + } + + /** + * Present the popover instance. + * + * @param {NavOptions} [opts={}] Nav options to go with this transition. + * @returns {Promise} Returns a promise which is resolved when the transition has completed. + */ + present(navOptions: NavOptions = {}) { + return this._app.present(this, navOptions); + } + + /** + * @private + * DEPRECATED: Please inject PopoverController instead + */ + static create(componentType: any, data = {}, opts: PopoverOptions = {}) { + // deprecated warning: added beta.11 2016-06-27 + console.warn('Popover.create(..) has been deprecated. Please inject PopoverController instead'); + } + +} + + +/** + * @name PopoverController * @description * A Popover is a dialog that appears on top of the current page. * It can be used for anything, but generally it is used for overflow @@ -65,11 +111,11 @@ const POPOVER_MD_BODY_PADDING = 12; * ```ts * @Component({}) * class MyPage { - * constructor(private nav: NavController) {} + * constructor(private popoverCtrl: PopoverController) {} * * presentPopover(myEvent) { - * let popover = Popover.create(PopoverPage); - * this.nav.present(popover, { + * let popover = this.popoverCtrl.create(PopoverPage); + * popover.present({ * ev: myEvent * }); * } @@ -104,388 +150,27 @@ const POPOVER_MD_BODY_PADDING = 12; * * @demo /docs/v2/demos/popover/ */ -export class Popover extends ViewController { +@Injectable() +export class PopoverController { - constructor(componentType: any, data: any = {}, opts: PopoverOptions = {}) { - opts.showBackdrop = isPresent(opts.showBackdrop) ? !!opts.showBackdrop : true; - opts.enableBackdropDismiss = isPresent(opts.enableBackdropDismiss) ? !!opts.enableBackdropDismiss : true; - - data.componentType = componentType; - data.opts = opts; - super(PopoverCmp, data); - this.isOverlay = true; - - // by default, popovers should not fire lifecycle events of other views - // for example, when a popover enters, the current active view should - // not fire its lifecycle events because it's not conceptually leaving - this.fireOtherLifecycles = false; - } + constructor(private _app: App) {} /** - * @private + * Create a popover with the following options + * + * | Option | Type | Description | + * |-----------------------|------------|------------------------------------------------------------------------------------------------------------------| + * | cssClass |`string` | An additional class for custom styles. | + * | showBackdrop |`boolean` | Whether to show the backdrop. Default true. | + * | enableBackdropDismiss |`boolean` | Whether the popover should be dismissed by tapping the backdrop. Default true. | + * + * + * @param {object} componentType The Popover + * @param {object} data Any data to pass to the Popover view + * @param {PopoverOptions} opts Popover options */ - getTransitionName(direction: string) { - let key = (direction === 'back' ? 'popoverLeave' : 'popoverEnter'); - return this._nav && this._nav.config.get(key); + create(componentType: any, data = {}, opts: PopoverOptions = {}): Popover { + return new Popover(this._app, componentType, data, opts); } - /** - * Create a popover with the following options - * - * | Option | Type | Description | - * |-----------------------|------------|------------------------------------------------------------------------------------------------------------------| - * | cssClass |`string` | An additional class for custom styles. | - * | showBackdrop |`boolean` | Whether to show the backdrop. Default true. | - * | enableBackdropDismiss |`boolean` | Whether the popover should be dismissed by tapping the backdrop. Default true. | - * - * - * @param {object} componentType The Popover - * @param {object} data Any data to pass to the Popover view - * @param {object} opts Popover options - */ - static create(componentType: any, data = {}, opts: PopoverOptions = {}) { - return new Popover(componentType, data, opts); - } - - } - -/** -* @private -*/ -@Component({ - selector: 'ion-popover', - template: - '' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' -}) -class PopoverCmp { - @ViewChild('viewport', {read: ViewContainerRef}) viewport: ViewContainerRef; - - private d: any; - private enabled: boolean; - private id: number; - private showSpinner: boolean; - - constructor( - private _compiler: ComponentResolver, - private _elementRef: ElementRef, - private _renderer: Renderer, - private _config: Config, - private _navParams: NavParams, - private _viewCtrl: ViewController - ) { - this.d = _navParams.data.opts; - - if (this.d.cssClass) { - _renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true); - } - - this.id = (++popoverIds); - } - - ionViewWillEnter() { - addSelector(this._navParams.data.componentType, 'ion-popover-inner'); - - this._compiler.resolveComponent(this._navParams.data.componentType).then((componentFactory) => { - let componentRef = this.viewport.createComponent(componentFactory, this.viewport.length, this.viewport.parentInjector); - - this._viewCtrl.setInstance(componentRef.instance); - - // manually fire ionViewWillEnter() since PopoverCmp's ionViewWillEnter already happened - this._viewCtrl.fireWillEnter(); - }); - } - - ngAfterViewInit() { - let activeElement: any = document.activeElement; - if (document.activeElement) { - activeElement.blur(); - } - this.enabled = true; - } - - dismiss(role: any): Promise { - return this._viewCtrl.dismiss(null, role); - } - - bdTouch(ev: UIEvent) { - ev.preventDefault(); - ev.stopPropagation(); - } - - bdClick() { - if (this.enabled && this.d.enableBackdropDismiss) { - this.dismiss('backdrop'); - } - } - - @HostListener('body:keyup', ['$event']) - private _keyUp(ev: KeyboardEvent) { - if (this.enabled && ev.keyCode === Key.ESCAPE && this._viewCtrl.isLast()) { - this.bdClick(); - } - } } - -export interface PopoverOptions { - cssClass?: string; - showBackdrop?: boolean; - enableBackdropDismiss?: boolean; -} - -/** - * Animations for popover - */ -class PopoverTransition extends PageTransition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - } - - mdPositionView(nativeEle: HTMLElement, ev: any) { - let originY = 'top'; - let originX = 'left'; - - let popoverWrapperEle = nativeEle.querySelector('.popover-wrapper'); - - // Popover content width and height - let popoverEle = nativeEle.querySelector('.popover-content'); - let popoverDim = popoverEle.getBoundingClientRect(); - let popoverWidth = popoverDim.width; - let popoverHeight = popoverDim.height; - - // Window body width and height - let bodyWidth = window.innerWidth; - let bodyHeight = window.innerHeight; - - // If ev was passed, use that for target element - let targetDim = ev && ev.target && ev.target.getBoundingClientRect(); - - let targetTop = (targetDim && 'top' in targetDim) ? targetDim.top : (bodyHeight / 2) - (popoverHeight / 2); - let targetLeft = (targetDim && 'left' in targetDim) ? targetDim.left : (bodyWidth / 2) - (popoverWidth / 2); - - let targetWidth = targetDim && targetDim.width || 0; - let targetHeight = targetDim && targetDim.height || 0; - - let popoverCSS = { - top: targetTop, - left: targetLeft - }; - - // If the popover left is less than the padding it is off screen - // to the left so adjust it, else if the width of the popover - // exceeds the body width it is off screen to the right so adjust - if (popoverCSS.left < POPOVER_MD_BODY_PADDING) { - popoverCSS.left = POPOVER_MD_BODY_PADDING; - } else if (popoverWidth + POPOVER_MD_BODY_PADDING + popoverCSS.left > bodyWidth) { - popoverCSS.left = bodyWidth - popoverWidth - POPOVER_MD_BODY_PADDING; - originX = 'right'; - } - - // If the popover when popped down stretches past bottom of screen, - // make it pop up if there's room above - if (targetTop + targetHeight + popoverHeight > bodyHeight && targetTop - popoverHeight > 0) { - popoverCSS.top = targetTop - popoverHeight; - nativeEle.className = nativeEle.className + ' popover-bottom'; - originY = 'bottom'; - // If there isn't room for it to pop up above the target cut it off - } else if (targetTop + targetHeight + popoverHeight > bodyHeight) { - popoverEle.style.bottom = POPOVER_MD_BODY_PADDING + 'px'; - } - - popoverEle.style.top = popoverCSS.top + 'px'; - popoverEle.style.left = popoverCSS.left + 'px'; - - popoverEle.style[CSS.transformOrigin] = originY + ' ' + originX; - - // Since the transition starts before styling is done we - // want to wait for the styles to apply before showing the wrapper - popoverWrapperEle.style.opacity = '1'; - } - - iosPositionView(nativeEle: HTMLElement, ev: any) { - let originY = 'top'; - let originX = 'left'; - - let popoverWrapperEle = nativeEle.querySelector('.popover-wrapper'); - - // Popover content width and height - let popoverEle = nativeEle.querySelector('.popover-content'); - let popoverDim = popoverEle.getBoundingClientRect(); - let popoverWidth = popoverDim.width; - let popoverHeight = popoverDim.height; - - // Window body width and height - let bodyWidth = window.innerWidth; - let bodyHeight = window.innerHeight; - - // If ev was passed, use that for target element - let targetDim = ev && ev.target && ev.target.getBoundingClientRect(); - - let targetTop = (targetDim && 'top' in targetDim) ? targetDim.top : (bodyHeight / 2) - (popoverHeight / 2); - let targetLeft = (targetDim && 'left' in targetDim) ? targetDim.left : (bodyWidth / 2); - let targetWidth = targetDim && targetDim.width || 0; - let targetHeight = targetDim && targetDim.height || 0; - - // The arrow that shows above the popover on iOS - var arrowEle = nativeEle.querySelector('.popover-arrow'); - let arrowDim = arrowEle.getBoundingClientRect(); - var arrowWidth = arrowDim.width; - var arrowHeight = arrowDim.height; - - // If no ev was passed, hide the arrow - if (!targetDim) { - arrowEle.style.display = 'none'; - } - - let arrowCSS = { - top: targetTop + targetHeight, - left: targetLeft + (targetWidth / 2) - (arrowWidth / 2) - }; - - let popoverCSS = { - top: targetTop + targetHeight + (arrowHeight - 1), - left: targetLeft + (targetWidth / 2) - (popoverWidth / 2) - }; - - // If the popover left is less than the padding it is off screen - // to the left so adjust it, else if the width of the popover - // exceeds the body width it is off screen to the right so adjust - if (popoverCSS.left < POPOVER_IOS_BODY_PADDING) { - popoverCSS.left = POPOVER_IOS_BODY_PADDING; - } else if (popoverWidth + POPOVER_IOS_BODY_PADDING + popoverCSS.left > bodyWidth) { - popoverCSS.left = bodyWidth - popoverWidth - POPOVER_IOS_BODY_PADDING; - originX = 'right'; - } - - // If the popover when popped down stretches past bottom of screen, - // make it pop up if there's room above - if (targetTop + targetHeight + popoverHeight > bodyHeight && targetTop - popoverHeight > 0) { - arrowCSS.top = targetTop - (arrowHeight + 1); - popoverCSS.top = targetTop - popoverHeight - (arrowHeight - 1); - nativeEle.className = nativeEle.className + ' popover-bottom'; - originY = 'bottom'; - // If there isn't room for it to pop up above the target cut it off - } else if (targetTop + targetHeight + popoverHeight > bodyHeight) { - popoverEle.style.bottom = POPOVER_IOS_BODY_PADDING + '%'; - } - - arrowEle.style.top = arrowCSS.top + 'px'; - arrowEle.style.left = arrowCSS.left + 'px'; - - popoverEle.style.top = popoverCSS.top + 'px'; - popoverEle.style.left = popoverCSS.left + 'px'; - - popoverEle.style[CSS.transformOrigin] = originY + ' ' + originX; - - // Since the transition starts before styling is done we - // want to wait for the styles to apply before showing the wrapper - popoverWrapperEle.style.opacity = '1'; - } -} - -class PopoverPopIn extends PopoverTransition { - constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.popover-wrapper')); - - wrapper.fromTo('opacity', 0.01, 1); - backdrop.fromTo('opacity', 0.01, 0.08); - - this - .easing('ease') - .duration(100) - .add(backdrop) - .add(wrapper); - } - - play() { - nativeRaf(() => { - this.iosPositionView(this.enteringView.pageRef().nativeElement, this.opts.ev); - super.play(); - }); - } -} -PageTransition.register('popover-pop-in', PopoverPopIn); - - -class PopoverPopOut extends PopoverTransition { - constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let backdrop = new Animation(ele.querySelector('ion-backdrop')); - let wrapper = new Animation(ele.querySelector('.popover-wrapper')); - - wrapper.fromTo('opacity', 0.99, 0); - backdrop.fromTo('opacity', 0.08, 0); - - this - .easing('ease') - .duration(500) - .add(backdrop) - .add(wrapper); - } -} -PageTransition.register('popover-pop-out', PopoverPopOut); - - -class PopoverMdPopIn extends PopoverTransition { - constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - - let content = new Animation(ele.querySelector('.popover-content')); - let viewport = new Animation(ele.querySelector('.popover-viewport')); - - content.fromTo('scale', 0.001, 1); - viewport.fromTo('opacity', 0.01, 1); - - this - .easing('cubic-bezier(0.36,0.66,0.04,1)') - .duration(300) - .add(content) - .add(viewport); - } - - play() { - nativeRaf(() => { - this.mdPositionView(this.enteringView.pageRef().nativeElement, this.opts.ev); - super.play(); - }); - } -} -PageTransition.register('popover-md-pop-in', PopoverMdPopIn); - - -class PopoverMdPopOut extends PopoverTransition { - constructor(enteringView: ViewController, leavingView: ViewController, private opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - let wrapper = new Animation(ele.querySelector('.popover-wrapper')); - - wrapper.fromTo('opacity', 0.99, 0); - - this - .easing('ease') - .duration(500) - .fromTo('opacity', 0.01, 1) - .add(wrapper); - } -} -PageTransition.register('popover-md-pop-out', PopoverMdPopOut); - - -let popoverIds = -1; diff --git a/src/components/select/select.ts b/src/components/select/select.ts index 9bddf390b4..711c6a9921 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -3,6 +3,7 @@ import { NG_VALUE_ACCESSOR } from '@angular/common'; import { ActionSheet } from '../action-sheet/action-sheet'; import { Alert } from '../alert/alert'; +import { App } from '../app/app'; import { Form } from '../../util/form'; import { isBlank, isCheckedProperty, isTrueProperty, merge } from '../../util/util'; import { Item } from '../item/item'; @@ -192,6 +193,7 @@ export class Select { @Output() ionCancel: EventEmitter = new EventEmitter(); constructor( + private _app: App, private _form: Form, private _elementRef: ElementRef, private _renderer: Renderer, @@ -205,10 +207,6 @@ export class Select { this._labelId = 'lbl-' + _item.id; this._item.setCssClass('item-select', true); } - - if (!_nav) { - console.error('parent required for '); - } } @HostListener('click', ['$event']) @@ -279,7 +277,7 @@ export class Select { })); alertOptions.cssClass = 'select-action-sheet'; - overlay = ActionSheet.create(alertOptions); + overlay = new ActionSheet(this._app, alertOptions); } else { // default to use the alert interface @@ -297,7 +295,7 @@ export class Select { }); // create the alert instance from our built up alertOptions - overlay = Alert.create(alertOptions); + overlay = new Alert(this._app, alertOptions); if (this._multi) { // use checkboxes @@ -318,7 +316,7 @@ export class Select { } - this._nav.present(overlay, alertOptions); + overlay.present(alertOptions); this._isOpen = true; overlay.onDismiss(() => { diff --git a/src/components/tabs/tab.ts b/src/components/tabs/tab.ts index a99d86b693..56fdb779c4 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -4,7 +4,8 @@ import { App } from '../app/app'; import { Config } from '../../config/config'; import { isTrueProperty} from '../../util/util'; import { Keyboard} from '../../util/keyboard'; -import { NavController, NavOptions} from '../nav/nav-controller'; +import { NavController} from '../nav/nav-controller'; +import { NavOptions} from '../nav/nav-options'; import { TabButton} from './tab-button'; import { Tabs} from './tabs'; import { ViewController} from '../nav/view-controller'; @@ -97,13 +98,13 @@ import { ViewController} from '../nav/view-controller'; * * ```ts * export class Tabs { - * constructor(nav: NavController) { - * this.nav = nav; + * constructor(private modalCtrl: ModalController) { + * * } * * chat() { - * let modal = Modal.create(ChatPage); - * this.nav.present(modal); + * let modal = this.modalCtrl.create(ChatPage); + * modal.present(); * } * } * ``` diff --git a/src/components/toast/test/toast.spec.ts b/src/components/toast/test/toast.spec.ts index c14bdbed05..e87d56e03f 100644 --- a/src/components/toast/test/toast.spec.ts +++ b/src/components/toast/test/toast.spec.ts @@ -1,4 +1,4 @@ -import {Toast} from '../../../../src'; +import { ToastController, App, Platform, Config } from '../../../../src'; export function run() { @@ -7,18 +7,18 @@ describe('Toast', () => { describe('create', () => { it('should create toast with close button', () => { - let toast = Toast.create({ + let toast = toastCtrl.create({ message: 'Please Wait...', showCloseButton: true }); - + expect(toast.data.position).toEqual('bottom'); expect(toast.data.message).toEqual('Please Wait...'); expect(toast.data.showCloseButton).toEqual(true); }); it('should create toast with position top', () => { - let toast = Toast.create({ + let toast = toastCtrl.create({ message: 'Please Wait...', position: 'top' }); @@ -27,7 +27,7 @@ describe('Toast', () => { }); it('should create toast with position middle', () => { - let toast = Toast.create({ + let toast = toastCtrl.create({ message: 'Please Wait...', position: 'middle' }); @@ -36,7 +36,7 @@ describe('Toast', () => { }); it('should create toast with position bottom', () => { - let toast = Toast.create({ + let toast = toastCtrl.create({ message: 'Please Wait...', position: 'bottom' }); @@ -45,7 +45,7 @@ describe('Toast', () => { }); it('should set a duration', () => { - let toast = Toast.create({ + let toast = toastCtrl.create({ message: 'Please Wait...', duration: 3000 }); @@ -53,6 +53,15 @@ describe('Toast', () => { expect(toast.data.duration).toEqual(3000); }); }); + + let toastCtrl: ToastController; + beforeEach(() => { + let config = new Config(); + let platform = new Platform(); + let app = new App(config, platform); + toastCtrl = new ToastController(app); + }); + }); } diff --git a/src/components/toast/toast-component.ts b/src/components/toast/toast-component.ts new file mode 100644 index 0000000000..ef21ab0aa2 --- /dev/null +++ b/src/components/toast/toast-component.ts @@ -0,0 +1,307 @@ +import { AfterViewInit, Component, ElementRef, Renderer } from '@angular/core'; + +import { Animation } from '../../animations/animation'; +import { Config } from '../../config/config'; +import { isPresent } from '../../util/util'; +import { NavController } from '../nav/nav-controller'; +import { NavParams } from '../nav/nav-params'; +import { Transition, TransitionOptions } from '../../transitions/transition'; +import { ViewController } from '../nav/view-controller'; + + +/** +* @private +*/ +@Component({ + selector: 'ion-toast', + template: ` +
+
+
{{d.message}}
+ +
+
+ `, + host: { + 'role': 'dialog', + '[attr.aria-labelledby]': 'hdrId', + '[attr.aria-describedby]': 'descId', + }, +}) +export class ToastCmp implements AfterViewInit { + private d: any; + private descId: string; + private dismissTimeout: number = undefined; + private enabled: boolean; + private hdrId: string; + private id: number; + + constructor( + private _nav: NavController, + private _viewCtrl: ViewController, + private _config: Config, + private _elementRef: ElementRef, + params: NavParams, + renderer: Renderer + ) { + + this.d = params.data; + + if (this.d.cssClass) { + renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true); + } + + this.id = (++toastIds); + if (this.d.message) { + this.hdrId = 'toast-hdr-' + this.id; + } + } + + ngAfterViewInit() { + // if there's a `duration` set, automatically dismiss. + if (this.d.duration) { + this.dismissTimeout = + setTimeout(() => { + this.dismiss('backdrop'); + }, this.d.duration); + } + this.enabled = true; + } + + ionViewDidEnter() { + const { activeElement }: any = document; + if (activeElement) { + activeElement.blur(); + } + + let focusableEle = this._elementRef.nativeElement.querySelector('button'); + + if (focusableEle) { + focusableEle.focus(); + } + } + + cbClick() { + if (this.enabled) { + this.dismiss('close'); + } + } + + dismiss(role: any): Promise { + clearTimeout(this.dismissTimeout); + this.dismissTimeout = undefined; + return this._viewCtrl.dismiss(null, role); + } + +} + + +class ToastSlideIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + // DOM READS + let ele = enteringView.pageRef().nativeElement; + const wrapperEle = ele.querySelector('.toast-wrapper'); + let wrapper = new Animation(wrapperEle); + + if (enteringView.data && enteringView.data.position === TOAST_POSITION_TOP) { + // top + // by default, it is -100% hidden (above the screen) + // so move from that to 10px below top: 0px; + wrapper.fromTo('translateY', '-100%', `${10}px`); + + } else if (enteringView.data && enteringView.data.position === TOAST_POSITION_MIDDLE) { + // Middle + // just center it and fade it in + let topPosition = Math.floor(ele.clientHeight / 2 - wrapperEle.clientHeight / 2); + // DOM WRITE + wrapperEle.style.top = `${topPosition}px`; + wrapper.fromTo('opacity', 0.01, 1); + + } else { + // bottom + // by default, it is 100% hidden (below the screen), + // so move from that to 10 px above bottom: 0px + wrapper.fromTo('translateY', '100%', `${0 - 10}px`); + } + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(wrapper); + } +} + +class ToastSlideOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + const wrapperEle = ele.querySelector('.toast-wrapper'); + let wrapper = new Animation(wrapperEle); + + if (leavingView.data && leavingView.data.position === TOAST_POSITION_TOP) { + // top + // reverse arguments from enter transition + wrapper.fromTo('translateY', `${10}px`, '-100%'); + + } else if (leavingView.data && leavingView.data.position === TOAST_POSITION_MIDDLE) { + // Middle + // just fade it out + wrapper.fromTo('opacity', 0.99, 0); + + } else { + // bottom + // reverse arguments from enter transition + wrapper.fromTo('translateY', `${0 - 10}px`, '100%'); + } + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(300).add(wrapper); + } +} + +class ToastMdSlideIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + // DOM reads + let ele = enteringView.pageRef().nativeElement; + const wrapperEle = ele.querySelector('.toast-wrapper'); + let wrapper = new Animation(wrapperEle); + + if (enteringView.data && enteringView.data.position === TOAST_POSITION_TOP) { + // top + // by default, it is -100% hidden (above the screen) + // so move from that to top: 0px; + wrapper.fromTo('translateY', '-100%', `0%`); + + } else if (enteringView.data && enteringView.data.position === TOAST_POSITION_MIDDLE) { + // Middle + // just center it and fade it in + let topPosition = Math.floor(ele.clientHeight / 2 - wrapperEle.clientHeight / 2); + // DOM WRITE + wrapperEle.style.top = `${topPosition}px`; + wrapper.fromTo('opacity', 0.01, 1); + + } else { + // bottom + // by default, it is 100% hidden (below the screen), + // so move from that to bottom: 0px + wrapper.fromTo('translateY', '100%', `0%`); + } + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(wrapper); + } +} + +class ToastMdSlideOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = leavingView.pageRef().nativeElement; + const wrapperEle = ele.querySelector('.toast-wrapper'); + let wrapper = new Animation(wrapperEle); + + if (leavingView.data && leavingView.data.position === TOAST_POSITION_TOP) { + // top + // reverse arguments from enter transition + wrapper.fromTo('translateY', `${0}%`, '-100%'); + + } else if (leavingView.data && leavingView.data.position === TOAST_POSITION_MIDDLE) { + // Middle + // just fade it out + wrapper.fromTo('opacity', 0.99, 0); + + } else { + // bottom + // reverse arguments from enter transition + wrapper.fromTo('translateY', `${0}%`, '100%'); + } + + this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(wrapper); + } +} + +class ToastWpPopIn extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + let ele = enteringView.pageRef().nativeElement; + const wrapperEle = ele.querySelector('.toast-wrapper'); + let wrapper = new Animation(wrapperEle); + + if (enteringView.data && enteringView.data.position === TOAST_POSITION_TOP) { + // top + wrapper.fromTo('opacity', 0.01, 1); + wrapper.fromTo('scale', 1.3, 1); + + } else if (enteringView.data && enteringView.data.position === TOAST_POSITION_MIDDLE) { + // Middle + // just center it and fade it in + let topPosition = Math.floor(ele.clientHeight / 2 - wrapperEle.clientHeight / 2); + + // DOM WRITE + wrapperEle.style.top = `${topPosition}px`; + wrapper.fromTo('opacity', 0.01, 1); + wrapper.fromTo('scale', 1.3, 1); + + } else { + // bottom + wrapper.fromTo('opacity', 0.01, 1); + wrapper.fromTo('scale', 1.3, 1); + } + + this.easing('cubic-bezier(0,0 0.05,1)').duration(200).add(wrapper); + } +} + +class ToastWpPopOut extends Transition { + constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { + super(enteringView, leavingView, opts); + + // DOM reads + let ele = leavingView.pageRef().nativeElement; + const wrapperEle = ele.querySelector('.toast-wrapper'); + let wrapper = new Animation(wrapperEle); + + if (leavingView.data && leavingView.data.position === TOAST_POSITION_TOP) { + // top + // reverse arguments from enter transition + wrapper.fromTo('opacity', 0.99, 0); + wrapper.fromTo('scale', 1, 1.3); + + } else if (leavingView.data && leavingView.data.position === TOAST_POSITION_MIDDLE) { + // Middle + // just fade it out + wrapper.fromTo('opacity', 0.99, 0); + wrapper.fromTo('scale', 1, 1.3); + + } else { + // bottom + // reverse arguments from enter transition + wrapper.fromTo('opacity', 0.99, 0); + wrapper.fromTo('scale', 1, 1.3); + } + + // DOM writes + const EASE: string = 'ease-out'; + const DURATION: number = 150; + this.easing(EASE).duration(DURATION).add(wrapper); + } +} + + +Transition.register('toast-slide-in', ToastSlideIn); +Transition.register('toast-slide-out', ToastSlideOut); +Transition.register('toast-md-slide-in', ToastMdSlideIn); +Transition.register('toast-md-slide-out', ToastMdSlideOut); +Transition.register('toast-wp-slide-out', ToastWpPopOut); +Transition.register('toast-wp-slide-in', ToastWpPopIn); + +let toastIds = -1; +const TOAST_POSITION_TOP = 'top'; +const TOAST_POSITION_MIDDLE = 'middle'; +const TOAST_POSITION_BOTTOM = 'bottom'; diff --git a/src/components/toast/toast-options.ts b/src/components/toast/toast-options.ts new file mode 100644 index 0000000000..4671f8494a --- /dev/null +++ b/src/components/toast/toast-options.ts @@ -0,0 +1,10 @@ + +export interface ToastOptions { + message?: string; + cssClass?: string; + duration?: number; + showCloseButton?: boolean; + closeButtonText?: string; + dismissOnPageChange?: boolean; + position?: string; +} diff --git a/src/components/toast/toast.ts b/src/components/toast/toast.ts index 1fde8ec0ed..d57a5eaf80 100644 --- a/src/components/toast/toast.ts +++ b/src/components/toast/toast.ts @@ -1,79 +1,29 @@ -import { AfterViewInit, Component, ElementRef, Renderer } from '@angular/core'; +import { Injectable } from '@angular/core'; -import { Animation } from '../../animations/animation'; -import { Config } from '../../config/config'; +import { App } from '../app/app'; import { isPresent } from '../../util/util'; -import { NavController } from '../nav/nav-controller'; -import { NavParams } from '../nav/nav-params'; -import { Transition, TransitionOptions } from '../../transitions/transition'; +import { NavOptions } from '../nav/nav-options'; +import { ToastOptions } from './toast-options'; +import { ToastCmp } from './toast-component'; import { ViewController } from '../nav/view-controller'; /** - * @name Toast - * @description - * A Toast is a subtle notification commonly used in modern applications. - * It can be used to provide feedback about an operation or to - * display a system message. The toast appears on top of the app's content, - * and can be dismissed by the app to resume user interaction with - * the app. - * - * ### Creating - * All of the toast options should be passed in the first argument of - * the create method: `Toast.create(opts)`. The message to display should be - * passed in the `message` property. The `showCloseButton` option can be set to - * true in order to display a close button on the toast. See the [create](#create) - * method below for all available options. - * - * ### Positioning - * Toasts can be positioned at the top, bottom or middle of the - * view port. The position can be passed to the `Toast.create(opts)` method. - * The position option is a string, and the values accepted are `top`, `bottom` and `middle`. - * If the position is not specified, the toast will be displayed at the bottom of the view port. - * - * ### Dismissing - * The toast can be dismissed automatically after a specific amount of time - * by passing the number of milliseconds to display it in the `duration` of - * the toast options. If `showCloseButton` is set to true, then the close button - * will dismiss the toast. To dismiss the toast after creation, call the `dismiss()` - * method on the Toast instance. The `onDismiss` function can be called to perform an action after the toast - * is dismissed. - * - * @usage - * ```ts - * constructor(nav: NavController) { - * this.nav = nav; - * } - * - * presentToast() { - * let toast = Toast.create({ - * message: 'User was added successfully', - * duration: 3000, - * position: 'top' - * }); - * - * toast.onDismiss(() => { - * console.log('Dismissed toast'); - * }); - * - * this.nav.present(toast); - * } - * ``` - * - * @demo /docs/v2/demos/toast/ + * @private */ export class Toast extends ViewController { + private _app: App; - constructor(opts: ToastOptions = {}) { + constructor(app: App, opts: ToastOptions = {}) { opts.dismissOnPageChange = isPresent(opts.dismissOnPageChange) ? !!opts.dismissOnPageChange : false; super(ToastCmp, opts); + this._app = app; // set the position to the bottom if not provided - if (! opts.position || ! this.isValidPosition(opts.position)) { + if (!opts.position || !this.isValidPosition(opts.position)) { opts.position = TOAST_POSITION_BOTTOM; } this.isOverlay = true; - this.usePortal = true; // by default, toasts should not fire lifecycle events of other views // for example, when an toast enters, the current active view should @@ -104,6 +54,86 @@ export class Toast extends ViewController { this.data.message = message; } + /** + * Present the toast instance. + * + * @param {NavOptions} [opts={}] Nav options to go with this transition. + * @returns {Promise} Returns a promise which is resolved when the transition has completed. + */ + present(navOptions: NavOptions = {}) { + return this._app.present(this, navOptions); + } + + /** + * @private + * DEPRECATED: Please inject ToastController instead + */ + private static create(opt: any) { + // deprecated warning: added beta.11 2016-06-27 + console.warn('Toast.create(..) has been deprecated. Please inject ToastController instead'); + } + +} + + +/** + * @name ToastController + * @description + * A Toast is a subtle notification commonly used in modern applications. + * It can be used to provide feedback about an operation or to + * display a system message. The toast appears on top of the app's content, + * and can be dismissed by the app to resume user interaction with + * the app. + * + * ### Creating + * All of the toast options should be passed in the first argument of + * the create method: `create(opts)`. The message to display should be + * passed in the `message` property. The `showCloseButton` option can be set to + * true in order to display a close button on the toast. See the [create](#create) + * method below for all available options. + * + * ### Positioning + * Toasts can be positioned at the top, bottom or middle of the + * view port. The position can be passed to the `Toast.create(opts)` method. + * The position option is a string, and the values accepted are `top`, `bottom` and `middle`. + * If the position is not specified, the toast will be displayed at the bottom of the view port. + * + * ### Dismissing + * The toast can be dismissed automatically after a specific amount of time + * by passing the number of milliseconds to display it in the `duration` of + * the toast options. If `showCloseButton` is set to true, then the close button + * will dismiss the toast. To dismiss the toast after creation, call the `dismiss()` + * method on the Toast instance. The `onDismiss` function can be called to perform an action after the toast + * is dismissed. + * + * @usage + * ```ts + * constructor(private toastCtrl: ToastController) { + * + * } + * + * presentToast() { + * let toast = this.toastCtrl.create({ + * message: 'User was added successfully', + * duration: 3000, + * position: 'top' + * }); + * + * toast.onDismiss(() => { + * console.log('Dismissed toast'); + * }); + * + * toast.present(); + * } + * ``` + * + * @demo /docs/v2/demos/toast/ + */ +@Injectable() +export class ToastController { + + constructor(private _app: App) {} + /** * * Toast options @@ -118,318 +148,14 @@ export class Toast extends ViewController { * | closeButtonText | `string` | "Close" | Text to display in the close button. | * | dismissOnPageChange | `boolean` | false | Whether to dismiss the toast when navigating to a new page. | * - * @param {object} opts Toast options. See the above table for available options. + * @param {ToastOptions} opts Toast options. See the above table for available options. */ - static create(opts: ToastOptions = {}) { - return new Toast(opts); - } -} - -/* Don't expose these for now - let's move to an enum or something long term */ -const TOAST_POSITION_TOP: string = 'top'; -const TOAST_POSITION_MIDDLE: string = 'middle'; -const TOAST_POSITION_BOTTOM: string = 'bottom'; - -/** -* @private -*/ -@Component({ - selector: 'ion-toast', - template: ` -
-
-
{{d.message}}
- -
-
- `, - host: { - 'role': 'dialog', - '[attr.aria-labelledby]': 'hdrId', - '[attr.aria-describedby]': 'descId', - }, -}) -class ToastCmp implements AfterViewInit { - private d: any; - private descId: string; - private dismissTimeout: number = undefined; - private enabled: boolean; - private hdrId: string; - private id: number; - - constructor( - private _nav: NavController, - private _viewCtrl: ViewController, - private _config: Config, - private _elementRef: ElementRef, - params: NavParams, - renderer: Renderer - ) { - - this.d = params.data; - - if (this.d.cssClass) { - renderer.setElementClass(_elementRef.nativeElement, this.d.cssClass, true); - } - - this.id = (++toastIds); - if (this.d.message) { - this.hdrId = 'toast-hdr-' + this.id; - } - } - - ngAfterViewInit() { - // if there's a `duration` set, automatically dismiss. - if (this.d.duration) { - this.dismissTimeout = - setTimeout(() => { - this.dismiss('backdrop'); - }, this.d.duration); - } - this.enabled = true; - } - - ionViewDidEnter() { - const { activeElement }: any = document; - if (activeElement) { - activeElement.blur(); - } - - let focusableEle = this._elementRef.nativeElement.querySelector('button'); - - if (focusableEle) { - focusableEle.focus(); - } - } - - cbClick() { - if (this.enabled) { - this.dismiss('close'); - } - } - - dismiss(role: any): Promise { - clearTimeout(this.dismissTimeout); - this.dismissTimeout = undefined; - return this._viewCtrl.dismiss(null, role); + create(opts: ToastOptions = {}) { + return new Toast(this._app, opts); } } -export interface ToastOptions { - message?: string; - cssClass?: string; - duration?: number; - showCloseButton?: boolean; - closeButtonText?: string; - dismissOnPageChange?: boolean; - position?: string; -} - -class ToastSlideIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - // DOM READS - let ele = enteringView.pageRef().nativeElement; - const wrapperEle = ele.querySelector('.toast-wrapper'); - let wrapper = new Animation(wrapperEle); - - if (enteringView.data && enteringView.data.position === TOAST_POSITION_TOP) { - // top - // by default, it is -100% hidden (above the screen) - // so move from that to 10px below top: 0px; - wrapper.fromTo('translateY', '-100%', `${10}px`); - - } else if (enteringView.data && enteringView.data.position === TOAST_POSITION_MIDDLE) { - // Middle - // just center it and fade it in - let topPosition = Math.floor(ele.clientHeight / 2 - wrapperEle.clientHeight / 2); - // DOM WRITE - wrapperEle.style.top = `${topPosition}px`; - wrapper.fromTo('opacity', 0.01, 1); - - } else { - // bottom - // by default, it is 100% hidden (below the screen), - // so move from that to 10 px above bottom: 0px - wrapper.fromTo('translateY', '100%', `${0 - 10}px`); - } - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(wrapper); - } -} - -class ToastSlideOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - const wrapperEle = ele.querySelector('.toast-wrapper'); - let wrapper = new Animation(wrapperEle); - - if (leavingView.data && leavingView.data.position === TOAST_POSITION_TOP) { - // top - // reverse arguments from enter transition - wrapper.fromTo('translateY', `${10}px`, '-100%'); - - } else if (leavingView.data && leavingView.data.position === TOAST_POSITION_MIDDLE) { - // Middle - // just fade it out - wrapper.fromTo('opacity', 0.99, 0); - - } else { - // bottom - // reverse arguments from enter transition - wrapper.fromTo('translateY', `${0 - 10}px`, '100%'); - } - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(300).add(wrapper); - } -} - -class ToastMdSlideIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - // DOM reads - let ele = enteringView.pageRef().nativeElement; - const wrapperEle = ele.querySelector('.toast-wrapper'); - let wrapper = new Animation(wrapperEle); - - if (enteringView.data && enteringView.data.position === TOAST_POSITION_TOP) { - // top - // by default, it is -100% hidden (above the screen) - // so move from that to top: 0px; - wrapper.fromTo('translateY', '-100%', `0%`); - - } else if (enteringView.data && enteringView.data.position === TOAST_POSITION_MIDDLE) { - // Middle - // just center it and fade it in - let topPosition = Math.floor(ele.clientHeight / 2 - wrapperEle.clientHeight / 2); - // DOM WRITE - wrapperEle.style.top = `${topPosition}px`; - wrapper.fromTo('opacity', 0.01, 1); - - } else { - // bottom - // by default, it is 100% hidden (below the screen), - // so move from that to bottom: 0px - wrapper.fromTo('translateY', '100%', `0%`); - } - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(wrapper); - } -} - -class ToastMdSlideOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = leavingView.pageRef().nativeElement; - const wrapperEle = ele.querySelector('.toast-wrapper'); - let wrapper = new Animation(wrapperEle); - - if (leavingView.data && leavingView.data.position === TOAST_POSITION_TOP) { - // top - // reverse arguments from enter transition - wrapper.fromTo('translateY', `${0}%`, '-100%'); - - } else if (leavingView.data && leavingView.data.position === TOAST_POSITION_MIDDLE) { - // Middle - // just fade it out - wrapper.fromTo('opacity', 0.99, 0); - - } else { - // bottom - // reverse arguments from enter transition - wrapper.fromTo('translateY', `${0}%`, '100%'); - } - - this.easing('cubic-bezier(.36,.66,.04,1)').duration(450).add(wrapper); - } -} - -class ToastWpPopIn extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - let ele = enteringView.pageRef().nativeElement; - const wrapperEle = ele.querySelector('.toast-wrapper'); - let wrapper = new Animation(wrapperEle); - - if (enteringView.data && enteringView.data.position === TOAST_POSITION_TOP) { - // top - wrapper.fromTo('opacity', 0.01, 1); - wrapper.fromTo('scale', 1.3, 1); - - } else if (enteringView.data && enteringView.data.position === TOAST_POSITION_MIDDLE) { - // Middle - // just center it and fade it in - let topPosition = Math.floor(ele.clientHeight / 2 - wrapperEle.clientHeight / 2); - - // DOM WRITE - wrapperEle.style.top = `${topPosition}px`; - wrapper.fromTo('opacity', 0.01, 1); - wrapper.fromTo('scale', 1.3, 1); - - } else { - // bottom - wrapper.fromTo('opacity', 0.01, 1); - wrapper.fromTo('scale', 1.3, 1); - } - - this.easing('cubic-bezier(0,0 0.05,1)').duration(200).add(wrapper); - } -} - -class ToastWpPopOut extends Transition { - constructor(enteringView: ViewController, leavingView: ViewController, opts: TransitionOptions) { - super(enteringView, leavingView, opts); - - // DOM reads - let ele = leavingView.pageRef().nativeElement; - const wrapperEle = ele.querySelector('.toast-wrapper'); - let wrapper = new Animation(wrapperEle); - - if (leavingView.data && leavingView.data.position === TOAST_POSITION_TOP) { - // top - // reverse arguments from enter transition - wrapper.fromTo('opacity', 0.99, 0); - wrapper.fromTo('scale', 1, 1.3); - - } else if (leavingView.data && leavingView.data.position === TOAST_POSITION_MIDDLE) { - // Middle - // just fade it out - wrapper.fromTo('opacity', 0.99, 0); - wrapper.fromTo('scale', 1, 1.3); - - } else { - // bottom - // reverse arguments from enter transition - wrapper.fromTo('opacity', 0.99, 0); - wrapper.fromTo('scale', 1, 1.3); - } - - // DOM writes - const EASE: string = 'ease-out'; - const DURATION: number = 150; - this.easing(EASE).duration(DURATION).add(wrapper); - } -} - - -Transition.register('toast-slide-in', ToastSlideIn); -Transition.register('toast-slide-out', ToastSlideOut); -Transition.register('toast-md-slide-in', ToastMdSlideIn); -Transition.register('toast-md-slide-out', ToastMdSlideOut); -Transition.register('toast-wp-slide-out', ToastWpPopOut); -Transition.register('toast-wp-slide-in', ToastWpPopIn); - -let toastIds = -1; +const TOAST_POSITION_TOP = 'top'; +const TOAST_POSITION_MIDDLE = 'middle'; +const TOAST_POSITION_BOTTOM = 'bottom'; diff --git a/src/config/bootstrap.ts b/src/config/bootstrap.ts index 2facff3185..c7ace63225 100644 --- a/src/config/bootstrap.ts +++ b/src/config/bootstrap.ts @@ -1,22 +1,10 @@ import { bootstrap } from '@angular/platform-browser-dynamic'; -import { ComponentRef, enableProdMode, NgZone, PLATFORM_DIRECTIVES, provide } from '@angular/core'; -import { HTTP_PROVIDERS } from '@angular/http'; +import { ComponentRef, NgZone } from '@angular/core'; -import { App } from '../components/app/app'; -import { ClickBlock } from '../util/click-block'; -import { closest, nativeTimeout, nativeRaf } from '../util/dom'; -import { Config } from './config'; -import { Events } from '../util/events'; -import { FeatureDetect } from '../util/feature-detect'; -import { Form } from '../util/form'; -import { IONIC_DIRECTIVES } from './directives'; -import { isPresent } from '../util/util'; -import { Keyboard } from '../util/keyboard'; -import { MenuController } from '../components/menu/menu-controller'; +import { AppRoot, UserComponent } from '../components/app/app'; +import { nativeRaf } from '../util/dom'; +import { ionicProviders } from './providers'; import { Platform } from '../platform/platform'; -import { ScrollView } from '../util/scroll-view'; -import { TapClick } from '../components/tap-click/tap-click'; -import { Translate } from '../translation/translate'; const _reflect: any = Reflect; @@ -44,13 +32,11 @@ const _reflect: any = Reflect; export function ionicBootstrap(appRootComponent: any, customProviders?: Array, config?: any) { // get all Ionic Providers let providers = ionicProviders(customProviders, config); - - // automatically set "ion-app" selector to users root component - addSelector(appRootComponent, 'ion-app'); + providers.push({provide: UserComponent, useValue: appRootComponent}); cssReady(() => { // call angular bootstrap - bootstrap(appRootComponent, providers).then(ngComponentRef => { + bootstrap(AppRoot, providers).then(ngComponentRef => { // ionic app has finished bootstrapping ionicPostBootstrap(ngComponentRef); }); @@ -62,17 +48,11 @@ export function ionicBootstrap(appRootComponent: any, customProviders?: Array) { - let app: App = ngComponentRef.injector.get(App); - app.setAppInjector(ngComponentRef.injector); - // prepare platform ready let platform: Platform = ngComponentRef.injector.get(Platform); platform.setZone(ngComponentRef.injector.get(NgZone)); platform.prepareReady(); - // TODO: Use PLATFORM_INITIALIZER - ngComponentRef.injector.get(TapClick); - return ngComponentRef; } @@ -92,151 +72,6 @@ function cssReady(done: Function) { } -/** - * @private - */ -export function ionicProviders(customProviders?: Array, config?: any): any[] { - // create an instance of Config - if (!(config instanceof Config)) { - config = new Config(config); - } - - // enable production mode if config set to true - if (config.getBoolean('prodMode')) { - enableProdMode(); - } - - // create an instance of Platform - let platform = new Platform(); - - // initialize platform - platform.setUrl(window.location.href); - platform.setUserAgent(window.navigator.userAgent); - platform.setNavigatorPlatform(window.navigator.platform); - platform.load(config); - config.setPlatform(platform); - - let clickBlock = new ClickBlock(); - let events = new Events(); - let featureDetect = new FeatureDetect(); - - setupDom(window, document, config, platform, clickBlock, featureDetect); - bindEvents(window, document, platform, events); - - let providers: any[] = [ - App, - provide(ClickBlock, {useValue: clickBlock}), - provide(Config, {useValue: config}), - provide(Events, {useValue: events}), - provide(FeatureDetect, {useValue: featureDetect}), - Form, - Keyboard, - MenuController, - provide(Platform, {useValue: platform}), - Translate, - TapClick, - provide(PLATFORM_DIRECTIVES, {useValue: IONIC_DIRECTIVES, multi: true}), - HTTP_PROVIDERS - ]; - - if (isPresent(customProviders)) { - providers.push(customProviders); - } - - return providers; -} - - -function setupDom(window: Window, document: Document, config: Config, platform: Platform, clickBlock: ClickBlock, featureDetect: FeatureDetect) { - let bodyEle = document.body; - let mode = config.get('mode'); - - // if dynamic mode links have been added the fire up the correct one - let modeLinkAttr = mode + '-href'; - let linkEle = document.head.querySelector('link[' + modeLinkAttr + ']'); - if (linkEle) { - let href = linkEle.getAttribute(modeLinkAttr); - linkEle.removeAttribute(modeLinkAttr); - linkEle.href = href; - } - - // set the mode class name - // ios/md/wp - bodyEle.classList.add(mode); - - // language and direction - platform.setDir(document.documentElement.dir, false); - platform.setLang(document.documentElement.lang, false); - - let versions = platform.versions(); - platform.platforms().forEach(platformName => { - // platform-ios - let platformClass = 'platform-' + platformName; - bodyEle.classList.add(platformClass); - - let platformVersion = versions[platformName]; - if (platformVersion) { - // platform-ios9 - platformClass += platformVersion.major; - bodyEle.classList.add(platformClass); - - // platform-ios9_3 - bodyEle.classList.add(platformClass + '_' + platformVersion.minor); - } - }); - - // touch devices should not use :hover CSS pseudo - // enable :hover CSS when the "hoverCSS" setting is not false - if (config.getBoolean('hoverCSS', true) !== false) { - bodyEle.classList.add('enable-hover'); - } - - if (config.getBoolean('clickBlock', true) !== false) { - clickBlock.enable(); - } - - // run feature detection tests - featureDetect.run(window, document); -} - - -/** - * Bind some global events and publish on the 'app' channel - */ -function bindEvents(window: Window, document: Document, platform: Platform, events: Events) { - window.addEventListener('online', (ev) => { - events.publish('app:online', ev); - }, false); - - window.addEventListener('offline', (ev) => { - events.publish('app:offline', ev); - }, false); - - window.addEventListener('orientationchange', (ev) => { - events.publish('app:rotated', ev); - }); - - // When that status taps, we respond - window.addEventListener('statusTap', (ev) => { - // TODO: Make this more better - let el = document.elementFromPoint(platform.width() / 2, platform.height() / 2); - if (!el) { return; } - - let content = closest(el, 'scroll-content'); - if (content) { - var scroll = new ScrollView(content); - scroll.scrollTo(0, 0, 300); - } - }); - - // start listening for resizes XXms after the app starts - nativeTimeout(() => { - window.addEventListener('resize', () => { - platform.windowResize(); - }); - }, 2000); -} - /** * @private */ diff --git a/src/config/providers.ts b/src/config/providers.ts new file mode 100644 index 0000000000..2ea0c98a27 --- /dev/null +++ b/src/config/providers.ts @@ -0,0 +1,170 @@ +import { enableProdMode, PLATFORM_DIRECTIVES, provide } from '@angular/core'; +import { HTTP_PROVIDERS } from '@angular/http'; + +import { ActionSheetController } from '../components/action-sheet/action-sheet'; +import { AlertController } from '../components/alert/alert'; +import { App } from '../components/app/app'; +import { Config } from './config'; +import { closest, nativeTimeout } from '../util/dom'; +import { Events } from '../util/events'; +import { FeatureDetect } from '../util/feature-detect'; +import { Form } from '../util/form'; +import { IONIC_DIRECTIVES } from './directives'; +import { isPresent } from '../util/util'; +import { Keyboard } from '../util/keyboard'; +import { LoadingController } from '../components/loading/loading'; +import { MenuController } from '../components/menu/menu-controller'; +import { ModalController } from '../components/modal/modal'; +import { PickerController } from '../components/picker/picker'; +import { Platform } from '../platform/platform'; +import { PopoverController } from '../components/popover/popover'; +import { ScrollView } from '../util/scroll-view'; +import { TapClick } from '../components/tap-click/tap-click'; +import { ToastController } from '../components/toast/toast'; +import { Translate } from '../translation/translate'; + + +/** + * @private + */ +export function ionicProviders(customProviders?: Array, config?: any): any[] { + // create an instance of Config + if (!(config instanceof Config)) { + config = new Config(config); + } + + // enable production mode if config set to true + if (config.getBoolean('prodMode')) { + enableProdMode(); + } + + // create an instance of Platform + let platform = new Platform(); + + // initialize platform + platform.setUrl(window.location.href); + platform.setUserAgent(window.navigator.userAgent); + platform.setNavigatorPlatform(window.navigator.platform); + platform.load(); + config.setPlatform(platform); + + let events = new Events(); + let featureDetect = new FeatureDetect(); + + setupDom(window, document, config, platform, featureDetect); + bindEvents(window, document, platform, events); + + let providers: any[] = [ + ActionSheetController, + AlertController, + App, + provide(Config, {useValue: config}), + provide(Events, {useValue: events}), + provide(FeatureDetect, {useValue: featureDetect}), + Form, + HTTP_PROVIDERS, + Keyboard, + LoadingController, + MenuController, + ModalController, + PickerController, + PopoverController, + provide(Platform, {useValue: platform}), + provide(PLATFORM_DIRECTIVES, {useValue: IONIC_DIRECTIVES, multi: true}), + TapClick, + ToastController, + Translate, + ]; + + if (isPresent(customProviders)) { + providers.push(customProviders); + } + + return providers; +} + +function setupDom(window: Window, document: Document, config: Config, platform: Platform, featureDetect: FeatureDetect) { + let bodyEle = document.body; + let mode = config.get('mode'); + + // if dynamic mode links have been added the fire up the correct one + let modeLinkAttr = mode + '-href'; + let linkEle = document.head.querySelector('link[' + modeLinkAttr + ']'); + if (linkEle) { + let href = linkEle.getAttribute(modeLinkAttr); + linkEle.removeAttribute(modeLinkAttr); + linkEle.href = href; + } + + // set the mode class name + // ios/md/wp + bodyEle.classList.add(mode); + + // language and direction + platform.setDir(document.documentElement.dir, false); + platform.setLang(document.documentElement.lang, false); + + let versions = platform.versions(); + platform.platforms().forEach(platformName => { + // platform-ios + let platformClass = 'platform-' + platformName; + bodyEle.classList.add(platformClass); + + let platformVersion = versions[platformName]; + if (platformVersion) { + // platform-ios9 + platformClass += platformVersion.major; + bodyEle.classList.add(platformClass); + + // platform-ios9_3 + bodyEle.classList.add(platformClass + '_' + platformVersion.minor); + } + }); + + // touch devices should not use :hover CSS pseudo + // enable :hover CSS when the "hoverCSS" setting is not false + if (config.getBoolean('hoverCSS', true)) { + bodyEle.classList.add('enable-hover'); + } + + // run feature detection tests + featureDetect.run(window, document); +} + + +/** + * Bind some global events and publish on the 'app' channel + */ +function bindEvents(window: Window, document: Document, platform: Platform, events: Events) { + window.addEventListener('online', (ev) => { + events.publish('app:online', ev); + }, false); + + window.addEventListener('offline', (ev) => { + events.publish('app:offline', ev); + }, false); + + window.addEventListener('orientationchange', (ev) => { + events.publish('app:rotated', ev); + }); + + // When that status taps, we respond + window.addEventListener('statusTap', (ev) => { + // TODO: Make this more better + let el = document.elementFromPoint(platform.width() / 2, platform.height() / 2); + if (!el) { return; } + + let content = closest(el, 'scroll-content'); + if (content) { + var scroll = new ScrollView(content); + scroll.scrollTo(0, 0, 300); + } + }); + + // start listening for resizes XXms after the app starts + nativeTimeout(() => { + window.addEventListener('resize', () => { + platform.windowResize(); + }); + }, 2000); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 756a083039..f0aa6922e3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ -export * from './config/bootstrap'; -export * from './config/config'; -export * from './config/directives'; +export { ionicBootstrap, ionicPostBootstrap } from './config/bootstrap'; +export { Config } from './config/config'; +export { IONIC_DIRECTIVES } from './config/directives'; +export { ionicProviders } from './config/providers'; export * from './decorators/page'; diff --git a/src/util/click-block.ts b/src/util/click-block.ts index 008477dc1e..3c4b19ab89 100644 --- a/src/util/click-block.ts +++ b/src/util/click-block.ts @@ -1,55 +1,46 @@ -import {nativeTimeout} from './dom'; +import { Directive, ElementRef, forwardRef, Inject, Renderer } from '@angular/core'; +import { App } from '../components/app/app'; +import { clearNativeTimeout, nativeTimeout } from './dom'; +import { Config } from '../config/config'; -const CSS_CLICK_BLOCK = 'click-block-active'; const DEFAULT_EXPIRE = 330; -let cbEle: HTMLElement; -let fallbackTimerId: number; -let isShowing = false; + /** * @private */ +@Directive({ + selector: 'click-block' +}) export class ClickBlock { - private _enabled: boolean = false; + private _tmrId: number; + private _showing: boolean = false; + isEnabled: boolean; - enable() { - cbEle = document.createElement('click-block'); - document.body.appendChild(cbEle); - cbEle.addEventListener('touchmove', function(ev: UIEvent) { - ev.preventDefault(); - ev.stopPropagation(); - }); - this._enabled = true; + constructor( + @Inject(forwardRef(() => App)) app: App, + config: Config, + private elementRef: ElementRef, + private renderer: Renderer + ) { + app.clickBlock = this; + this.isEnabled = config.getBoolean('clickBlock', true); } - show(shouldShow: boolean, expire: number) { - if (this._enabled) { - if (shouldShow) { - show(expire); + activate(shouldShow: boolean, expire: number) { + if (this.isEnabled) { + clearNativeTimeout(this._tmrId); - } else { - hide(); + if (shouldShow) { + this._tmrId = nativeTimeout(this.activate.bind(this, false), expire || DEFAULT_EXPIRE); + } + + if (this._showing !== shouldShow) { + this.renderer.setElementClass(this.elementRef.nativeElement, 'click-block-active', shouldShow); + this._showing = shouldShow; } } } } - -function show(expire: number) { - clearTimeout(fallbackTimerId); - fallbackTimerId = nativeTimeout(hide, expire || DEFAULT_EXPIRE); - - if (!isShowing) { - cbEle.classList.add(CSS_CLICK_BLOCK); - isShowing = true; - } -} - -function hide() { - clearTimeout(fallbackTimerId); - if (isShowing) { - cbEle.classList.remove(CSS_CLICK_BLOCK); - isShowing = false; - } -} diff --git a/src/util/form.ts b/src/util/form.ts index 86b290f676..cccd832863 100644 --- a/src/util/form.ts +++ b/src/util/form.ts @@ -6,15 +6,10 @@ import {Injectable} from '@angular/core'; */ @Injectable() export class Form { - private _blur: HTMLElement; private _focused: any = null; private _ids: number = -1; private _inputs: any[] = []; - constructor() { - this.focusCtrl(document); - } - register(input: any) { this._inputs.push(input); } @@ -29,25 +24,9 @@ export class Form { } } - focusCtrl(document: any) { - // raw DOM fun - let focusCtrl = document.createElement('focus-ctrl'); - focusCtrl.setAttribute('aria-hidden', true); - - this._blur = document.createElement('button'); - this._blur.tabIndex = -1; - focusCtrl.appendChild(this._blur); - - document.body.appendChild(focusCtrl); - } - focusOut() { - console.debug('focusOut'); - let activeElement: any = document.activeElement; - if (activeElement) { - activeElement.blur(); - } - this._blur.focus(); + let activeElement = document.activeElement; + activeElement && activeElement.blur && activeElement.blur(); } setAsFocused(input: any) {