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: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
2}">
+
+
+
+ `,
+ 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:
- '' +
- '
' +
- '
' +
- '' +
- '' +
- '
' +
- '' +
- '
' +
-
- '' +
- '
' +
- '' +
- '
' +
- '' +
-
- '' +
- '
' +
- '' +
- '
' +
- '' +
-
- '' +
- '
' +
- '
' +
- '' +
- '
' +
- '
' +
- '' +
-
- '
' +
- '
2}">' +
- '' +
- '
' +
- '
',
- 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: `
+