From d8e2849be85ea773a1843dc58a07a78aa9cf5208 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 14 Apr 2016 09:03:00 -0500 Subject: [PATCH 001/102] feat(Input): added functionality for clear input option on ion-input Implemented function to handle when the clearInput button is pressed on an ion-input element --- ionic/components/input/input.ts | 7 +++++++ .../components/input/test/clear-input/e2e.ts | 5 +++++ .../input/test/clear-input/index.ts | 11 ++++++++++ .../input/test/clear-input/main.html | 20 +++++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 ionic/components/input/test/clear-input/e2e.ts create mode 100644 ionic/components/input/test/clear-input/index.ts create mode 100644 ionic/components/input/test/clear-input/main.html diff --git a/ionic/components/input/input.ts b/ionic/components/input/input.ts index 27f2dfe3c0..22d8454239 100644 --- a/ionic/components/input/input.ts +++ b/ionic/components/input/input.ts @@ -105,6 +105,13 @@ export class TextInput extends InputBase { inputFocused(event) { this.focus.emit(event); } + + /** + * @private + */ + clearTextInput() { + this._value = ''; + } } diff --git a/ionic/components/input/test/clear-input/e2e.ts b/ionic/components/input/test/clear-input/e2e.ts new file mode 100644 index 0000000000..be86073f5e --- /dev/null +++ b/ionic/components/input/test/clear-input/e2e.ts @@ -0,0 +1,5 @@ + +it('should clear input', function() { + element(by.css('.e2eClearInput')).click(); + expect(by.css('.e2eClearInput').getText()).toEqual(''); +}); diff --git a/ionic/components/input/test/clear-input/index.ts b/ionic/components/input/test/clear-input/index.ts new file mode 100644 index 0000000000..fb0b644bf0 --- /dev/null +++ b/ionic/components/input/test/clear-input/index.ts @@ -0,0 +1,11 @@ +import {App} from 'ionic-angular'; + + +@App({ + templateUrl: 'main.html' +}) +class E2EApp { + constructor() { + this.myValue = 'value'; + } +} diff --git a/ionic/components/input/test/clear-input/main.html b/ionic/components/input/test/clear-input/main.html new file mode 100644 index 0000000000..e3f743570e --- /dev/null +++ b/ionic/components/input/test/clear-input/main.html @@ -0,0 +1,20 @@ + + + Clear Input + + + + + + + + + + Text 1: + + + + + + + From 41174f6b27f69b74f8545daba8b8e93cea6a8036 Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Fri, 29 Apr 2016 11:51:31 -0400 Subject: [PATCH 002/102] docs(spinner): make methods private to dgeni --- ionic/components/spinner/spinner.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ionic/components/spinner/spinner.ts b/ionic/components/spinner/spinner.ts index 048055e795..6746f41e0e 100644 --- a/ionic/components/spinner/spinner.ts +++ b/ionic/components/spinner/spinner.ts @@ -155,11 +155,17 @@ export class Spinner { constructor(private _config: Config) {} + /** + * @private + */ ngOnInit() { this._init = true; this.load(); } + /** + * @private + */ load() { if (this._init) { this._l = []; From aa7c0bfb1f9c9cbae2cf948d90fab6d66e8798d4 Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Fri, 29 Apr 2016 12:47:07 -0400 Subject: [PATCH 003/102] docs(tabs): update to use ViewChild --- ionic/components/tabs/tabs.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ionic/components/tabs/tabs.ts b/ionic/components/tabs/tabs.ts index 502e915be9..7018b6f39f 100644 --- a/ionic/components/tabs/tabs.ts +++ b/ionic/components/tabs/tabs.ts @@ -103,7 +103,7 @@ import {isBlank, isTrueProperty} from '../../util/util'; * example, set the value of `id` to `myTabs`: * * ```html - * + * * * * @@ -111,17 +111,16 @@ import {isBlank, isTrueProperty} from '../../util/util'; * ``` * * Then in your class you can grab the `Tabs` instance and call `select()`, - * passing the index of the tab as the argument. In the following code `app` is - * of type [`IonicApp`](../../app/IonicApp/): + * passing the index of the tab as the argument. Here we're grabbing the tabs + * by using ViewChild. * *```ts - * constructor(app: IonicApp) { - * this.app = app; + * constructor() { + * @ViewChild('myTabs) tabRef: Tabs * } * * onPageDidEnter() { - * let tabs = this.app.getComponent('myTabs'); - * tabs.select(2); + * this.tabRef.select(2); * } *``` * From 3ca7d1a4ef6178909d02b98c48fed7e334fe81dd Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Fri, 29 Apr 2016 12:56:53 -0400 Subject: [PATCH 004/102] docs(tabs): fix ViewChild example --- ionic/components/tabs/tabs.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ionic/components/tabs/tabs.ts b/ionic/components/tabs/tabs.ts index 7018b6f39f..0e1d073dfa 100644 --- a/ionic/components/tabs/tabs.ts +++ b/ionic/components/tabs/tabs.ts @@ -115,12 +115,14 @@ import {isBlank, isTrueProperty} from '../../util/util'; * by using ViewChild. * *```ts - * constructor() { - * @ViewChild('myTabs) tabRef: Tabs - * } + * export class TabsPage { + * + * @ViewChild('myTabs) tabRef: Tabs * * onPageDidEnter() { * this.tabRef.select(2); + * } + * * } *``` * From 35dee45219fcc09f0b0765e59477152b655614f0 Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Fri, 29 Apr 2016 12:57:29 -0400 Subject: [PATCH 005/102] docs(menu): split out menu and menu controller expose menu input properties references driftyco/ionic-site#393 --- ionic/components/menu/menu-controller.ts | 114 +++++------- ionic/components/menu/menu.ts | 210 +++++++++++++++++++---- 2 files changed, 222 insertions(+), 102 deletions(-) diff --git a/ionic/components/menu/menu-controller.ts b/ionic/components/menu/menu-controller.ts index 38ba18d2dc..783a9f1039 100644 --- a/ionic/components/menu/menu-controller.ts +++ b/ionic/components/menu/menu-controller.ts @@ -3,17 +3,18 @@ import {MenuType} from './menu-types'; /** - * @name Menu + * @name MenuController * @description - * Menu is a side-menu interface that can be dragged and toggled to open or close. - * An Ionic app can have numerous menus, all of which can be controlled within - * template HTML, or programmatically. + * The MenuController is a provider which makes it easy to control a [Menu](../Menu). + * Its methods can be used to display the menu, enable the menu, toggle the menu, and more. + * The controller will grab a reference to the menu by the `side`, `id`, or, if neither + * of these are passed to it, it will grab the first menu it finds. + * * * @usage - * In order to use Menu, you must specify a [reference](https://angular.io/docs/ts/latest/guide/user-input.html#local-variables) - * to the content element that Menu should listen on for drag events, using the `content` property. - * This is telling the menu which content the menu is attached to, so it knows which element to - * move over, and to respond to drag events. Note that a **menu is a sibling to its content**. + * + * Add a basic menu component to start with. See the [Menu](../Menu) API docs + * for more information on adding menu components. * * ```html * @@ -27,45 +28,44 @@ import {MenuType} from './menu-types'; * * ``` * - * By default, Menus are on the left, but this can be overridden with the `side` - * property: - * - * ```html - * ... - * ``` - * - * - * ### Programmatic Interaction - * - * To programmatically interact with any menu, you can inject the `MenuController` - * provider into any component or directive. This makes it easy get ahold of and - * control the correct menu instance. By default Ionic will find the app's menu - * without requiring a menu ID. + * To call the controller methods, inject the `MenuController` provider + * into the page. Then, create some methods for opening, closing, and + * toggling the menu. * * ```ts * import{Page, MenuController} from 'ionic-angular'; + * * @Page({...}) * export class MyPage { - * constructor(menu: MenuController) { - * this.menu = menu; + * + * constructor(private menu: MenuController) { + * * } * * openMenu() { * this.menu.open(); * } * + * closeMenu() { + * this.menu.close(); + * } + * + * toggleMenu() { + * this.menu.toggle(); + * } + * * } * ``` * - * Note that if you want to easily toggle or close a menu just from a page's - * template, you can use `menuToggle` and/or `menuClose` to accomplish the same - * tasks as above. + * Since only one menu exists, the `MenuController` will grab the + * correct menu and call the correct method for each. * * - * ### Apps With Left And Right Menus + * ### Multiple Menus on Different Sides * - * For apps with a left and right menu, you can control the desired - * menu by passing in the side of the menu. + * For applications with both a left and right menu, the desired menu can be + * grabbed by passing the `side` of the menu. If nothing is passed, it will + * default to the `"left"` menu. * * ```html * ... @@ -74,24 +74,22 @@ import {MenuType} from './menu-types'; * ``` * * ```ts - * openLeftMenu() { - * this.menu.open('left'); + * toggleLeftMenu() { + * this.menu.toggle(); * } * - * closeRightMenu() { - * this.menu.close('right'); + * toggleRightMenu() { + * this.menu.toggle('right'); * } * ``` * * - * ### Apps With Multiple, Same Side Menus + * ### Multiple Menus on the Same Side * - * Since more than one menu on a the same side is possible, and you wouldn't want - * both to be open at the same time, an app can decide which menu should be enabled. - * For apps with multiple menus on the same side, it's required to give each menu a - * unique ID. In the example below, we're saying that the left menu with the - * `authenticated` id should be enabled, and the left menu with the `unauthenticated` - * id be disabled. + * An application can have multiple menus on the same side. In order to determine + * the menu to control, an `id` should be passed. In the example below, the menu + * with the `authenticated` id will be enabled, and the menu with the `unauthenticated` + * id will be disabled. * * ```html * ... @@ -106,43 +104,13 @@ import {MenuType} from './menu-types'; * } * ``` * - * Note that if an app only had one menu, there is no reason to pass a menu id. + * Note: if an app only has one menu, there is no reason to pass an `id`. * * - * ### Menu Types - * - * Menu supports two display types: `overlay`, `reveal` and `push`. Overlay - * is the traditional Material Design drawer type, and Reveal is the traditional - * iOS type. By default, menus will use to the correct type for the platform, - * but this can be overriden using the `type` property: - * - * ```html - * ... - * ``` - * - * - * ### Persistent Menus - * - * By default, menus, and specifically their menu toggle buttons in the navbar, - * only show on the root page within its `NavController`. For example, on Page 1 - * the menu toggle will show in the navbar. However, when navigating to Page 2, - * because it is not the root Page for that `NavController`, the menu toggle - * will not show in the navbar. - * - * Not showing the menu toggle button in the navbar is commonly seen within - * native apps after navigating past the root Page. However, it is still possible - * to always show the menu toggle button in the navbar by setting - * `persistent="true"` on the `ion-menu` component. - * - * ```html - * ... - * ``` - * * @demo /docs/v2/demos/menu/ * * @see {@link /docs/v2/components#menus Menu Component Docs} - * @see {@link /docs/v2/components#navigation Navigation Component Docs} - * @see {@link ../../nav/Nav Nav API Docs} + * @see {@link ../Menu Menu API Docs} * */ export class MenuController { diff --git a/ionic/components/menu/menu.ts b/ionic/components/menu/menu.ts index a020f5c3bf..e83a4b6cd4 100644 --- a/ionic/components/menu/menu.ts +++ b/ionic/components/menu/menu.ts @@ -11,7 +11,169 @@ import {isTrueProperty} from '../../util/util'; /** - * @private + * @name Menu + * @description + * The Menu component is a navigation drawer that slides in from the side of the current + * view. By default, it slides in from the left, but the side can be overridden. The menu + * will be displayed differently based on the mode, however the display type can be changed + * to any of the available [menu types](#menu-types). The menu element should be a sibling + * to the app's content element. There can be any number of menus attached to the content. + * These can be controlled from the templates, or programmatically using the [MenuController](../MenuController). + * + * + * ### Opening/Closing Menus + * + * There are several ways to open or close a menu. The menu can be **toggled** open or closed + * from the template using the [MenuToggle](../MenuToggle) directive. It can also be + * **closed** from the template using the [MenuClose](../MenuClose) directive. To display a menu + * programmatically, inject the [MenuController](../MenuController) provider and call any of the + * `MenuController` methods. + * + * + * ### Menu Types + * + * The menu supports several display types: `overlay`, `reveal` and `push`. By default, + * it will use the correct type based on the mode, but this can be changed. The default + * type for both Material Design and Windows mode is `overlay`, and `reveal` is the default + * type for iOS mode. The menu type can be changed in the app's [config](../../config/Config) + * via the `menuType` property, or passed in the `type` property on the `` element. + * See [usage](#usage) below for examples of changing the menu type. + * + * + * ### Navigation Bar Behavior + * + * If a [MenuToggle](../MenuToggle) button is added to the [NavBar](../../nav/NavBar) of + * a page, the button will only appear when the page it's in is currently a root page. The + * root page is the initial page loaded in the app, or it can be set using the [setRoot](../../nav/NavController/#setRoot) + * method on the [NavController](../../nav/NavController). + * + * For example, say the application has two pages, `Page1` and `Page2`, and both have a + * `MenuToggle` button in their navigation bars. Assume the initial page loaded into the app + * is `Page1`, making it the root page. `Page1` will display the `MenuToggle` button, but once + * `Page2` is pushed onto the navigation stack, the `MenuToggle` will not be displayed. + * + * + * ### Persistent Menus + * + * Persistent menus display the [MenuToggle](../MenuToggle) button in the [NavBar](../../nav/NavBar) + * on all pages in the navigation stack. To make a menu persistent set `persistent` to `true` on the + * `` element. Note that this will only affect the `MenuToggle` button in the `NavBar` attached + * to the `Menu` with `persistent` set to true, any other `MenuToggle` buttons will not be affected. + * + * + * @usage + * + * To add a menu to an application, the `` element should be added as a sibling to + * the content it belongs to. A [local variable](https://angular.io/docs/ts/latest/guide/user-input.html#local-variables) + * should be added to the content element and passed to the menu element in the `content` property. + * This tells the menu which content it is attached to, so it knows which element to watch for + * gestures. In the below example, `content` is using [property binding](https://angular.io/docs/ts/latest/guide/template-syntax.html#!#property-binding) + * because `mycontent` is a reference to the `` element, and not a string. + * + * ```html + * + * + * + * ... + * + * + * + * + * + * ``` + * + * ### Menu Side + * + * By default, menus slide in from the left, but this can be overridden by passing `right` + * to the `side` property: + * + * ```html + * ... + * ``` + * + * + * ### Menu Type + * + * The menu type can be changed by passing the value to `type` on the ``: + * + * ```html + * ... + * ``` + * + * It can also be set in the app's config. The below will set the menu type to + * `push` for all modes, and then set the type to `overlay` for the `ios` mode. + * + * ```ts + * @App({ + * templateUrl: 'build/app.html', + * config: { + * menuType: 'push', + * platforms: { + * ios: { + * menuType: 'overlay', + * } + * } + * } + * }) + * ``` + * + * + * ### Displaying the Menu + * + * To toggle a menu from the template, add a button with the `menuToggle` + * directive anywhere in the page's template: + * + * ```html + * + * ``` + * + * To close a menu, add the `menuClose` button. It can be added anywhere + * in the content, or even the menu itself. Below it is added to the menu's + * content: + * + * ```html + * + * + * + * + * + * + * + * ``` + * + * See the [MenuToggle](../MenuToggle) and [MenuClose](../MenuClose) docs + * for more information on these directives. + * + * The menu can also be controlled from the Page by using the `MenuController`. + * Inject the `MenuController` provider into the page and then call any of its + * methods. In the below example, the `openMenu` method will open the menu + * when it is called. + * + * ```ts + * import{Page, MenuController} from 'ionic-angular'; + * + * @Page({...}) + * export class MyPage { + * constructor(private menu: MenuController) { + * + * } + * + * openMenu() { + * this.menu.open(); + * } + * } + * ``` + * + * See the [MenuController](../MenuController) API docs for all of the methods + * and usage information. + * + * + * @demo /docs/v2/demos/menu/ + * + * @see {@link /docs/v2/components#menus Menu Component Docs} + * @see {@link ../MenuController MenuController API Docs} + * @see {@link ../../nav/Nav Nav API Docs} + * @see {@link ../../nav/NavController NavController API Docs} */ @Component({ selector: 'ion-menu', @@ -53,27 +215,29 @@ export class Menu extends Ion { onContentClick: EventListener; /** - * @private + * @input {any} A reference to the content element the menu should use. */ @Input() content: any; /** - * @private + * @input {string} An id for the menu. */ @Input() id: string; /** - * @private + * @input {string} Which side of the view the menu should be placed. Default `"left"`. */ @Input() side: string; /** - * @private + * @input {string} The display type of the menu. Default varies based on the mode, + * see the `menuType` in the [config](../../config/Config). Available options: + * `"overlay"`, `"reveal"`, `"push"`. */ @Input() type: string; /** - * @private + * @input {boolean} Whether or not the menu should be enabled. Default `true`. */ @Input() get enabled(): boolean { @@ -86,7 +250,7 @@ export class Menu extends Ion { } /** - * @private + * @input {boolean} Whether or not swiping the menu should be enabled. Default `true`. */ @Input() get swipeEnabled(): boolean { @@ -99,7 +263,7 @@ export class Menu extends Ion { } /** - * @private + * @input {string} Whether or not the menu should persist on child pages. Default `false`. */ @Input() get persistent(): boolean { @@ -116,7 +280,7 @@ export class Menu extends Ion { @Input() maxEdgeStart: number; /** - * @private + * @output {event} When the menu is being dragged open. */ @Output() opening: EventEmitter = new EventEmitter(); @@ -231,13 +395,11 @@ export class Menu extends Ion { } /** - * Sets the state of the Menu to open or not. - * @param {boolean} shouldOpen If the Menu is open or not. - * @return {Promise} returns a promise once set + * @private */ setOpen(shouldOpen: boolean): Promise { // _isPrevented is used to prevent unwanted opening/closing after swiping open/close - // or swiping open the menu while pressing down on the menuToggle button + // or swiping open the menu while pressing down on the MenuToggle button if ((shouldOpen && this.isOpen) || this._isPrevented()) { return Promise.resolve(this.isOpen); } @@ -335,7 +497,7 @@ export class Menu extends Ion { */ private _prevent() { // used to prevent unwanted opening/closing after swiping open/close - // or swiping open the menu while pressing down on the menuToggle + // or swiping open the menu while pressing down on the MenuToggle this._preventTime = Date.now() + 20; } @@ -347,36 +509,28 @@ export class Menu extends Ion { } /** - * Progamatically open the Menu. - * @return {Promise} returns a promise when the menu is fully opened. + * @private */ open() { return this.setOpen(true); } /** - * Progamatically close the Menu. - * @return {Promise} returns a promise when the menu is fully closed. + * @private */ close() { return this.setOpen(false); } /** - * Toggle the menu. If it's closed, it will open, and if opened, it will close. - * @return {Promise} returns a promise when the menu has been toggled. + * @private */ toggle() { return this.setOpen(!this.isOpen); } /** - * Used to enable or disable a menu. For example, there could be multiple - * left menus, but only one of them should be able to be opened at the same - * time. If there are multiple menus on the same side, then enabling one menu - * will also automatically disable all the others that are on the same side. - * @param {boolean} shouldEnable True if it should be enabled, false if not. - * @return {Menu} Returns the instance of the menu, which is useful for chaining. + * @private */ enable(shouldEnable: boolean): Menu { this.enabled = shouldEnable; @@ -399,9 +553,7 @@ export class Menu extends Ion { } /** - * Used to enable or disable the ability to swipe open the menu. - * @param {boolean} shouldEnable True if it should be swipe-able, false if not. - * @return {Menu} Returns the instance of the menu, which is useful for chaining. + * @private */ swipeEnable(shouldEnable: boolean): Menu { this.swipeEnabled = shouldEnable; From e2366bbb1157cabaa57d353a8b691469af4a40f3 Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Fri, 29 Apr 2016 14:24:31 -0400 Subject: [PATCH 006/102] docs(menu): update the docs for toggle and close closes driftyco/ionic-site#393 --- ionic/components/menu/menu-close.ts | 6 ++++-- ionic/components/menu/menu-toggle.ts | 10 ++++++++-- ionic/components/menu/menu.ts | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ionic/components/menu/menu-close.ts b/ionic/components/menu/menu-close.ts index b8ffea7760..775a399b2c 100644 --- a/ionic/components/menu/menu-close.ts +++ b/ionic/components/menu/menu-close.ts @@ -6,10 +6,12 @@ import {MenuController} from './menu-controller'; /** * @name MenuClose * @description - * The `menuClose` directive can be placed on any button to - * automatically close an open menu. + * The `menuClose` directive can be placed on any button to close an open menu. * * @usage + * + * A simple `menuClose` button can be added using the following markup: + * * ```html * * ``` diff --git a/ionic/components/menu/menu-toggle.ts b/ionic/components/menu/menu-toggle.ts index f90a2bca3e..c1ffd4928f 100644 --- a/ionic/components/menu/menu-toggle.ts +++ b/ionic/components/menu/menu-toggle.ts @@ -7,10 +7,16 @@ import {MenuController} from './menu-controller'; /** * @name MenuToggle * @description - * The `menuToggle` directive can be placed on any button to - * automatically close an open menu. + * The `menuToggle` directive can be placed on any button to toggle a menu open or closed. + * If it is added to the [NavBar](../../nav/NavBar) of a page, the button will only appear + * when the page it's in is currently a root page. See the [Menu Navigation Bar Behavior](../Menu#navigation-bar-behavior) + * docs for more information. + * * * @usage + * + * A simple `menuToggle` button can be added using the following markup: + * * ```html * * ``` diff --git a/ionic/components/menu/menu.ts b/ionic/components/menu/menu.ts index e83a4b6cd4..ca97332994 100644 --- a/ionic/components/menu/menu.ts +++ b/ionic/components/menu/menu.ts @@ -44,8 +44,8 @@ import {isTrueProperty} from '../../util/util'; * * If a [MenuToggle](../MenuToggle) button is added to the [NavBar](../../nav/NavBar) of * a page, the button will only appear when the page it's in is currently a root page. The - * root page is the initial page loaded in the app, or it can be set using the [setRoot](../../nav/NavController/#setRoot) - * method on the [NavController](../../nav/NavController). + * root page is the initial page loaded in the app, or a page that has been set as the root + * using the [setRoot](../../nav/NavController/#setRoot) method on the [NavController](../../nav/NavController). * * For example, say the application has two pages, `Page1` and `Page2`, and both have a * `MenuToggle` button in their navigation bars. Assume the initial page loaded into the app From 77b21b29c556419c54a0da042cd216799a34d963 Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Fri, 29 Apr 2016 14:20:56 -0500 Subject: [PATCH 007/102] chore(e2e): fix systemJS map for e2e paths This lets us have relative Ionic imports in the e2e tests, so they can be type checked. --- scripts/e2e/e2e.template.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/e2e/e2e.template.html b/scripts/e2e/e2e.template.html index 7aa33f569e..8fc4e30115 100644 --- a/scripts/e2e/e2e.template.html +++ b/scripts/e2e/e2e.template.html @@ -42,7 +42,8 @@ + + - - - - diff --git a/scripts/karma/karma.conf.js b/scripts/karma/karma.conf.js index 539a36d286..d7a7365c2f 100644 --- a/scripts/karma/karma.conf.js +++ b/scripts/karma/karma.conf.js @@ -10,17 +10,17 @@ module.exports = function(config) { frameworks: ['jasmine'], files: [ + 'node_modules/es6-shim/es6-shim.min.js', + 'node_modules/systemjs/node_modules/es6-module-loader/dist/es6-module-loader.js', //npm2 'node_modules/es6-module-loader/dist/es6-module-loader.js', //npm3 + 'node_modules/reflect-metadata/Reflect.js', + 'node_modules/zone.js/dist/zone.js', 'node_modules/systemjs/dist/system.js', 'scripts/karma/system.config.js', - 'node_modules/angular2/bundles/angular2-polyfills.min.js', - 'node_modules/angular2/bundles/angular2.min.js', - 'node_modules/angular2/bundles/router.min.js', - 'node_modules/angular2/bundles/http.min.js', 'node_modules/rxjs/bundles/Rx.min.js', 'dist/bundles/ionic.system.js', - //'node_modules/angular2/bundles/test_lib.js', + { pattern: 'node_modules/@angular/**/*.js', included: false}, { pattern: 'dist/tests/**/*.spec.js', included: false }, 'scripts/karma/test-main.js' ], diff --git a/scripts/karma/system.config.js b/scripts/karma/system.config.js index 01c9ac6c5d..1a3cbda8ba 100644 --- a/scripts/karma/system.config.js +++ b/scripts/karma/system.config.js @@ -1,11 +1,60 @@ System.config({ + baseURL: '/base', map: { - 'angular2': '/base/angular2', - 'ionic-angular': '/base/ionic' + 'ionic-angular': 'ionic', + '@angular': 'node_modules/@angular', }, packages: { 'ionic-angular': { main: 'index' + }, + 'rxjs': { + defaultExtension: 'js' + }, + '@angular/core': { + main: 'index.js', + defaultExtension: 'js' + }, + '@angular/compiler': { + main: 'index.js', + defaultExtension: 'js' + }, + '@angular/common': { + main: 'index.js', + defaultExtension: 'js' + }, + // remove after all tests imports are fixed + '@angular/facade': { + main: 'index.js', + defaultExtension: 'js' + }, + '@angular/router': { + main: 'index.js', + defaultExtension: 'js' + }, + '@angular/router-deprecated': { + main: 'index.js', + defaultExtension: 'js' + }, + '@angular/http': { + main: 'index.js', + defaultExtension: 'js' + }, + '@angular/upgrade': { + main: 'index.js', + defaultExtension: 'js' + }, + '@angular/platform-browser': { + main: 'index.js', + defaultExtension: 'js' + }, + '@angular/platform-browser-dynamic': { + main: 'index.js', + defaultExtension: 'js' + }, + '@angular/platform-server': { + main: 'index.js', + defaultExtension: 'js' } } -}); \ No newline at end of file +}); diff --git a/scripts/karma/test-main.js b/scripts/karma/test-main.js index 85a3701cfd..b88aa8afc1 100644 --- a/scripts/karma/test-main.js +++ b/scripts/karma/test-main.js @@ -4,8 +4,13 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 50; // we will call `__karma__.start()` later, once all the specs are loaded. __karma__.loaded = function() {}; -System.import('angular2/src/platform/browser/browser_adapter').then(function(browser_adapter) { - browser_adapter.BrowserDomAdapter.makeCurrent(); +System.import('@angular/core/testing').then(function(coreTesting) { + return System.import('@angular/platform-browser-dynamic/testing').then(function(browserTesting) { + coreTesting.setBaseTestProviders( + browserTesting.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, + browserTesting.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS + ); + }); }).then(function() { return Promise.all( Object.keys(window.__karma__.files) // All files served by Karma. From 71cd29775174f35eb4d50f66d9cd8383e6d67f72 Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Thu, 12 May 2016 18:55:24 -0400 Subject: [PATCH 044/102] fix(input): remove old clearInput code and clean up UI, added onChange calls references #6514 --- ionic/components/input/input-base.ts | 8 ----- ionic/components/input/input.ios.scss | 1 + ionic/components/input/input.md.scss | 2 +- ionic/components/input/input.scss | 5 ++++ ionic/components/input/input.ts | 5 +++- ionic/components/input/input.wp.scss | 2 +- .../input/test/form-inputs/main.html | 30 +++++++++---------- 7 files changed, 27 insertions(+), 26 deletions(-) diff --git a/ionic/components/input/input-base.ts b/ionic/components/input/input-base.ts index bac4b9dc80..bf6e047ffe 100644 --- a/ionic/components/input/input-base.ts +++ b/ionic/components/input/input-base.ts @@ -401,14 +401,6 @@ export class InputBase { } } - /** - * @private - */ - clearTextInput() { - console.debug('Should clear input'); - this._value = ''; - } - /** * @private */ diff --git a/ionic/components/input/input.ios.scss b/ionic/components/input/input.ios.scss index 9f07c0e8b1..dabb02dd45 100644 --- a/ionic/components/input/input.ios.scss +++ b/ionic/components/input/input.ios.scss @@ -80,6 +80,7 @@ ion-input[clearInput] { bottom: 0; width: $text-input-ios-input-clear-icon-width; + height: 34px; background-size: $text-input-ios-input-clear-icon-size; } diff --git a/ionic/components/input/input.md.scss b/ionic/components/input/input.md.scss index ebdabd3ffa..464c93a3e3 100644 --- a/ionic/components/input/input.md.scss +++ b/ionic/components/input/input.md.scss @@ -112,7 +112,7 @@ ion-input[clearInput] { @include svg-background-image($text-input-md-input-clear-icon-svg); right: ($item-md-padding-right / 2); - bottom: 2px; + bottom: 4px; width: $text-input-md-input-clear-icon-width; diff --git a/ionic/components/input/input.scss b/ionic/components/input/input.scss index e42c2de2bf..2080e8ecd0 100644 --- a/ionic/components/input/input.scss +++ b/ionic/components/input/input.scss @@ -127,6 +127,7 @@ input.text-input:-webkit-autofill { .text-input-clear-icon { position: absolute; + display: none; margin: 0; padding: 0; @@ -135,6 +136,10 @@ input.text-input:-webkit-autofill { background-position: center; } +.input-has-value .text-input-clear-icon { + display: block; +} + // Cloned Input // -------------------------------------------------- diff --git a/ionic/components/input/input.ts b/ionic/components/input/input.ts index 03586d6b9d..b724ef0c07 100644 --- a/ionic/components/input/input.ts +++ b/ionic/components/input/input.ts @@ -66,7 +66,7 @@ import {Platform} from '../../platform/platform'; template: '' + '' + - '' + + '' + '
', directives: [ NextInput, @@ -107,7 +107,10 @@ export class TextInput extends InputBase { * @private */ clearTextInput() { + console.debug("Should clear input"); this._value = ''; + this.onChange(this._value); + this.writeValue(this._value); } } diff --git a/ionic/components/input/input.wp.scss b/ionic/components/input/input.wp.scss index cb71558a36..37cdecd5b6 100644 --- a/ionic/components/input/input.wp.scss +++ b/ionic/components/input/input.wp.scss @@ -108,7 +108,7 @@ ion-input[clearInput] { @include svg-background-image($text-input-wp-input-clear-icon-svg); right: ($item-wp-padding-right / 2); - bottom: 2px; + bottom: 7px; width: $text-input-wp-input-clear-icon-width; diff --git a/ionic/components/input/test/form-inputs/main.html b/ionic/components/input/test/form-inputs/main.html index 205cc14755..357d9594ae 100644 --- a/ionic/components/input/test/form-inputs/main.html +++ b/ionic/components/input/test/form-inputs/main.html @@ -7,24 +7,24 @@
- + Email - + - + Username - + - + Password - + - + Comments - Comment value + Comment value
@@ -43,15 +43,15 @@ - + Username - + Password -
+
@@ -64,22 +64,22 @@ - + Email - + Username - + Password - + Comments Comment value From 809dc477f6b4a85858090b271f9e355376ec1431 Mon Sep 17 00:00:00 2001 From: Mateus Silva Date: Fri, 13 May 2016 14:00:34 +0100 Subject: [PATCH 045/102] docs(): fix typo (#6523) --- ionic/components/list/list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ionic/components/list/list.ts b/ionic/components/list/list.ts index 1435db4784..dc4fe40443 100644 --- a/ionic/components/list/list.ts +++ b/ionic/components/list/list.ts @@ -93,7 +93,7 @@ export class List extends Ion { * export class MyClass { * @ViewChild(List) list: List; * constructor(){} - * closeItmes(){ + * closeItems(){ * this.list.closeSlidingItems(); * } * } From 306289de22b4fb7e6af1d8aeae5f6b1a36a20a86 Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Fri, 13 May 2016 12:38:21 -0400 Subject: [PATCH 046/102] style(input): fix linter errors --- ionic/components/input/input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ionic/components/input/input.ts b/ionic/components/input/input.ts index b724ef0c07..40491711b0 100644 --- a/ionic/components/input/input.ts +++ b/ionic/components/input/input.ts @@ -107,7 +107,7 @@ export class TextInput extends InputBase { * @private */ clearTextInput() { - console.debug("Should clear input"); + console.debug('Should clear input'); this._value = ''; this.onChange(this._value); this.writeValue(this._value); From a1a594d9b662a01f7d17894b5a8be3f32011b785 Mon Sep 17 00:00:00 2001 From: Dan Bucholtz Date: Fri, 13 May 2016 15:01:57 -0500 Subject: [PATCH 047/102] feat(modal): start of inset modals start of inset modals --- ionic/components/modal/modal.scss | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ionic/components/modal/modal.scss b/ionic/components/modal/modal.scss index 94d2602903..9784b3af22 100644 --- a/ionic/components/modal/modal.scss +++ b/ionic/components/modal/modal.scss @@ -8,4 +8,22 @@ ion-page.modal { // hidden by default to prevent flickers, the animation will show it transform: translate3d(0, 100%, 0); + + @media only screen and (min-width: 768px) and (min-height: 600px){ + position: absolute; + top: calc(50% - 250px); + left: calc(50% - 300px); + + width: 600px; + height: 500px; + } + + @media only screen and (min-width: 768px) and (min-height: 768px){ + position: absolute; + top: calc(50% - 300px); + left: calc(50% - 300px); + + width: 600px; + height: 600px; + } } From af2085ed3d55de8f12948c77720ef3976dcfb22f Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Fri, 13 May 2016 18:37:50 -0400 Subject: [PATCH 048/102] fix(tabs): move border to top for windows positioned bottom tabs fixes #6526 --- ionic/components/tabs/tabs.wp.scss | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ionic/components/tabs/tabs.wp.scss b/ionic/components/tabs/tabs.wp.scss index 3a8e9385db..09dda92531 100644 --- a/ionic/components/tabs/tabs.wp.scss +++ b/ionic/components/tabs/tabs.wp.scss @@ -35,7 +35,7 @@ tabbar { opacity: .7; &[aria-selected=true] { - border-bottom: 2px solid $tab-button-wp-active-color; + border-bottom-color: $tab-button-wp-active-color; color: $tab-button-wp-active-color; opacity: 1; } @@ -77,6 +77,17 @@ tabbar { padding: 6px 10px; } +[tabbarPlacement=bottom] .tab-button { + border-top: 2px solid transparent; + border-bottom-width: 0; + + &[aria-selected=true] { + border-top-color: $tab-button-wp-active-color; + } +} + + + // Windows Tabbar Color Mixin // -------------------------------------------------- From 1e331c9ca079e9ec2d8a5cb672f40cbf07f9b473 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Fri, 13 May 2016 21:00:47 -0500 Subject: [PATCH 049/102] feat(datetime): add ion-datetime --- ionic/components.ios.scss | 1 + ionic/components.ts | 1 + ionic/components/datetime/datetime.ios.scss | 15 + ionic/components/datetime/datetime.scss | 30 + ionic/components/datetime/datetime.ts | 888 ++++++++++++++++++ ionic/components/datetime/test/basic/e2e.ts | 8 + ionic/components/datetime/test/basic/index.ts | 41 + .../components/datetime/test/basic/main.html | 68 ++ .../components/datetime/test/datetime.spec.ts | 518 ++++++++++ ionic/components/item/item.ts | 2 +- ionic/components/picker/picker.ios.scss | 11 +- ionic/components/picker/picker.md.scss | 8 +- ionic/components/picker/picker.scss | 17 +- ionic/components/picker/picker.ts | 190 ++-- ionic/components/picker/picker.wp.scss | 8 +- ionic/components/select/select.ts | 10 +- ionic/config/directives.ts | 5 +- ionic/util.ts | 1 + ionic/util/datetime-util.ts | 500 ++++++++++ ionic/util/test/datetime-util.spec.ts | 792 ++++++++++++++++ ionic/util/test/util.spec.ts | 685 +++++++------- 21 files changed, 3374 insertions(+), 425 deletions(-) create mode 100644 ionic/components/datetime/datetime.ios.scss create mode 100644 ionic/components/datetime/datetime.scss create mode 100644 ionic/components/datetime/datetime.ts create mode 100644 ionic/components/datetime/test/basic/e2e.ts create mode 100644 ionic/components/datetime/test/basic/index.ts create mode 100644 ionic/components/datetime/test/basic/main.html create mode 100644 ionic/components/datetime/test/datetime.spec.ts create mode 100644 ionic/util/datetime-util.ts create mode 100644 ionic/util/test/datetime-util.spec.ts diff --git a/ionic/components.ios.scss b/ionic/components.ios.scss index 797def8943..d92468af94 100644 --- a/ionic/components.ios.scss +++ b/ionic/components.ios.scss @@ -14,6 +14,7 @@ "components/checkbox/checkbox.ios", "components/chip/chip.ios", "components/content/content.ios", + "components/datetime/datetime.ios", "components/input/input.ios", "components/item/item.ios", "components/label/label.ios", diff --git a/ionic/components.ts b/ionic/components.ts index cb5185b5d3..75898f1580 100644 --- a/ionic/components.ts +++ b/ionic/components.ts @@ -5,6 +5,7 @@ export * from './components/badge/badge'; export * from './components/button/button'; export * from './components/checkbox/checkbox'; export * from './components/content/content'; +export * from './components/datetime/datetime'; export * from './components/icon/icon'; export * from './components/img/img'; export * from './components/infinite-scroll/infinite-scroll'; diff --git a/ionic/components/datetime/datetime.ios.scss b/ionic/components/datetime/datetime.ios.scss new file mode 100644 index 0000000000..a51b089746 --- /dev/null +++ b/ionic/components/datetime/datetime.ios.scss @@ -0,0 +1,15 @@ +@import "../../globals.ios"; +@import "./datetime"; + +// iOS DateTime +// -------------------------------------------------- + +$datetime-ios-padding-top: $item-ios-padding-top !default; +$datetime-ios-padding-right: ($item-ios-padding-right / 2) !default; +$datetime-ios-padding-bottom: $item-ios-padding-bottom !default; +$datetime-ios-padding-left: $item-ios-padding-left !default; + + +ion-datetime { + padding: $datetime-ios-padding-top $datetime-ios-padding-right $datetime-ios-padding-bottom $datetime-ios-padding-left; +} diff --git a/ionic/components/datetime/datetime.scss b/ionic/components/datetime/datetime.scss new file mode 100644 index 0000000000..4abc2cc8a2 --- /dev/null +++ b/ionic/components/datetime/datetime.scss @@ -0,0 +1,30 @@ +@import "../../globals.core"; + +// DateTime +// -------------------------------------------------- + +ion-datetime { + display: flex; + overflow: hidden; + + max-width: 45%; +} + +.datetime-text { + overflow: hidden; + + flex: 1; + + min-width: 16px; + + font-size: inherit; + text-overflow: ellipsis; + white-space: nowrap; +} + +.datetime-disabled, +.item-datetime-disabled ion-label { + opacity: .4; + + pointer-events: none; +} diff --git a/ionic/components/datetime/datetime.ts b/ionic/components/datetime/datetime.ts new file mode 100644 index 0000000000..a870ea5f46 --- /dev/null +++ b/ionic/components/datetime/datetime.ts @@ -0,0 +1,888 @@ +import {Component, Optional, ElementRef, Renderer, Input, Output, Provider, forwardRef, EventEmitter, HostListener, ViewEncapsulation} from 'angular2/core'; +import {NG_VALUE_ACCESSOR} from 'angular2/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'; + +const DATETIME_VALUE_ACCESSOR = new Provider( + NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => DateTime), multi: true}); + + +/** + * @name DateTime + * @description + * The `ion-datetime` component is similar to an HTML `` + * input, however, Ionic's datetime component makes it easier for developers to + * display an exact datetime input format and manage values within JavaScript. + * Additionally, the datetime component makes it easier for users to scroll through + * and individually select parts of date and time values from an easy to user interface. + * + * ```html + * + * Date + * + * + * + * ``` + * + * + * ### Display and Picker Formats + * + * How datetime values can be displayed can come in many variations formats, + * therefore it is best to let the app decide exactly how to display it. To do + * so, `ion-datetime` uses a common format seen in many other libraries and + * programming languages: + * + * | Format | Description | Examples | + * |----------|---------------------|----------------| + * | `YYYY` | Year, 4 digits | `2018` | + * | `YY` | Year, 2 digits | `18` | + * | `M` | Month, 1 digit | `1` .. `12` | + * | `MM` | Month, 2 digit | `01` .. `12` | + * | `MMM` | Month, short name | `Jan` * | + * | `MMMM` | Month, full name | `January` * | + * | `D` | Day, 1 digit | `1` .. `31` | + * | `DD` | Day, 2 digit | `01` .. `31` | + * | `DDD` | Day, short name | `Fri` * | + * | `DDDD` | Day, full name | `Friday` * | + * | `H` | 24-hour, 1 digit | `0` .. `23` | + * | `HH` | 24-hour, 2 digit | `00` .. `23` | + * | `h` | 12-hour, 1 digit | `1` .. `12` | + * | `hh` | 12-hour, 2 digit | `01` .. `12` | + * | `a` | am/pm, lower case | `am` `pm` | + * | `A` | AM/PM, upper case | `AM` `PM` | + * | `m` | minute, 1 digit | `1` .. `59` | + * | `mm` | minute, 2 digit | `01` .. `59` | + * | `s` | seconds, 1 digit | `1` .. `59` | + * | `ss` | seconds, 2 digit | `01` .. `59` | + * | `Z` | UTC Timezone Offset | | + * + * * See the "Month Names and Day of the Week Names" section below on how to + * use names other than the default English month and day names. + * + * The `displayFormat` input allows developers to specify how a date's value + * should be displayed within the `ion-datetime`. The `pickerFormat` decides + * which datetime picker columns should be shown, the order of the columns, and + * which format to display the value in. If a `pickerFormat` is not provided + * then it'll use the `displayFormat` instead. In most cases only providing the + * `displayFormat` is needed. + * + * In the example below, the datetime's display would use the month's short + * name, the 1 digit day in the month, and a 4 digit year. + * + * ```html + * + * Date + * + * + * + * ``` + * + * In this example, the datetime's display would only show hours and minutes, + * and the hours would be in the 24-hour format. Note that the divider between + * the hours and minutes, in this case the `:` character, can be have any + * character which the app chooses to use as the divider. + * + * ```html + * + * Date + * + * + * + * ``` + * + * + * ### Datetime Data + * + * Historically, handling datetime data within JavaScript, or even within HTML + * inputs, has always been a challenge. Specifically, JavaScript's `Date` object is + * notoriously difficult to correctly parse apart datetime strings or to format + * datetime values. Even worse is how different browsers and JavaScript versions + * parse various datetime strings differently, especially per locale. Additional, + * developers face even more challenges when dealing with timezones using + * JavaScript's core `Date` object. + * + * But no worries, all is not lost! Ionic's datetime input has been designed so + * developers can avoid the common pitfalls, allowing developers to easily format + * datetime data within the input, and give the user a simple datetime picker for a + * great user experience. Oddly enough, one of the best ways to work with datetime + * values in JavaScript is to not use the `Date` object at all. + * + * ##### ISO 8601 Datetime Format: YYYY-MM-DDTHH:mmZ + * + * For all the reasons above, and how datetime data is commonly saved within databases, + * Ionic uses the [ISO 8601 datetime format](https://www.w3.org/TR/NOTE-datetime) + * for both its input value, and output value. The value is simply a string, rather + * than using JavaScript's `Date` object, and it strictly follows the standardized + * ISO 8601 format. Additionally, when using the ISO datetime string format, it makes + * it easier on developers when passing data within JSON objects, and sending databases + * a standardized datetime format which it can be easily parse apart and formatted. + * Because of the strict adherence to the ISO 8601 format, and not involving the hundreds + * of other format possibilities and locales, this approach actually makes it easier + * for Ionic apps and backend-services to manage datetime data. + * + * An ISO format can be used as a simple year, or just the hour and minute, or get more + * detailed down to the millisecond and timezone. Any of the ISO formats below can be used, + * and after a user selects a new date, Ionic will continue to use the same ISO format + * which datetime value was originally given as. + * + * | Description | Format | Datetime Value Example | + * |----------------------|------------------------|------------------------------| + * | Year | YYYY | 1994 | + * | Year and Month | YYYY-MM | 1994-12 | + * | Complete Date | YYYY-MM-DD | 1994-12-15 | + * | Date and Time | YYYY-MM-DDTHH:mm | 1994-12-15T13:47 | + * | UTC Timezone | YYYY-MM-DDTHH:mm:ssTZD | 1994-12-15T13:47:20.789Z | + * | Timezone Offset | YYYY-MM-DDTHH:mm:ssTZD | 1994-12-15T13:47:20.789+5:00 | + * | Hour and Minute | HH:mm | 13:47 | + * | Hour, Minute, Second | HH:mm:ss | 13:47:20 | + * + * Note that the year is always four-digits, milliseconds (if it's added) is always + * three-digits, and all others are always two-digits. So the number representing + * January always has a leading zero, such as `01`. Additionally, the hour is always + * in the 24-hour format, so `00` is `12am` on a 12-hour clock, `13` means `1pm`, + * and `23` means `11pm`. + * + * It's also important to note that neither the `displayFormat` or `pickerFormat` can + * set the datetime value's output, which is the value that sent the the component's + * `ngModel`. The format's are merely for displaying the value as text and the picker's + * interface, but the datetime's value is always persisted as a valid ISO 8601 datetime + * string. + * + * + * ### Min and Max Datetimes + * + * Dates are infinite in either direction, so for a user selection there should be at + * least some form of restricting the dates can be selected. Be default, the maximum + * date is to the end of the current year, and the minimum date is from the beginning + * of the year that was 100 years ago. + * + * To customize the minimum and maximum datetime values, the `min` and `max` component + * inputs can be provided which may make more sense for the app's use-case, rather + * than the default of the last 100 years. Following the same IS0 8601 format listed + * in the table above, each component can restrict which dates can be selected by the + * user. Below is an example of restricting the date selection between the beginning + * of 2016, and October 31st of 2020: + * + * ```html + * + * Date + * + * + * + * ``` + * + * + * ### Month Names and Day of the Week Names + * + * At this time, there is no one-size-fits-all standard to automatically choose the correct + * language/spelling for a month name, or day of the week name, depending on the language + * or locale. Good news is that there is an + * [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat) + * standard which *most* browsers have adopted. However, at this time the standard has not + * been fully implemented by all popular browsers so Ionic is unavailable to take advantage + * of it *yet*. Additionally, Angular also provides an internationalization service, but it + * is still under heavy development so Ionic does not depend on it at this time. + * + * All things considered, the by far easiest solution is to just provide an array of names + * if the app needs to use names other than the default English version of month and day + * names. The month names and day names can be either configurated at the app level, or + * individual `ion-datetime` level. + * + * ##### App Config Level + * + * ```ts + * @App({ + * config: { + * monthNames: ['janeiro, 'fevereiro', 'mar\u00e7o', ... ], + * monthShortNames: ['jan', 'fev', 'mar', ... ], + * dayNames: ['domingo', 'segunda-feira', 'ter\u00e7a-feira', ... ], + * dayShortNames: ['dom', 'seg', 'ter', ... ], + * } + * }) + * ``` + * + * ##### Component Input Level + * + * ```html + * + * Período + * + * + * ``` + * + * + * ### Advanced Datetime Validation and Manipulation + * + * The datetime picker provides the simplicity of selecting an exact format, and persists + * the datetime values as a string using the standardized + * [ISO 8601 datetime format](https://www.w3.org/TR/NOTE-datetime). + * However, it's important to note that `ion-datetime` does not attempt to solve all + * situtations when validating and manipulating datetime values. If datetime values need + * to be parsed from a certain format, or manipulated (such as adding 5 days to a date, + * subtracting 30 minutes), or even formatting data to a specific locale, then we highly + * recommend using [moment.js](http://momentjs.com/) to "Parse, validate, manipulate, and + * display dates in JavaScript". [Moment.js](http://momentjs.com/) has quickly become + * our goto standard when dealing with datetimes within JavaScript, but Ionic does not + * prepackage this dependency since most apps will not require it, and its locale + * configuration should be decided by the end-developer. + * + * + */ +@Component({ + selector: 'ion-datetime', + template: + '
{{_text}}
' + + '', + host: { + '[class.datetime-disabled]': '_disabled' + }, + providers: [DATETIME_VALUE_ACCESSOR], + encapsulation: ViewEncapsulation.None, +}) +export class DateTime { + private _disabled: any = false; + private _labelId: string; + private _text: string = ''; + private _fn: Function; + private _isOpen: boolean = false; + private _min: DateTimeData; + private _max: DateTimeData; + private _value: DateTimeData = {}; + private _locale: LocaleData = {}; + + /** + * @private + */ + id: string; + + /** + * @input {string} The minimum datetime allowed. Value must be a date string + * following the + * [ISO 8601 datetime format standard](https://www.w3.org/TR/NOTE-datetime), + * such as `1996-12-19`. The format does not have to be specific to an exact + * datetime. For example, the minimum could just be the year, such as `1994`. + * Defaults to the beginning of the year, 100 years ago from today. + */ + @Input() min: string; + + /** + * @input {string} The maximum datetime allowed. Value must be a date string + * following the + * [ISO 8601 datetime format standard](https://www.w3.org/TR/NOTE-datetime), + * `1996-12-19`. The format does not have to be specific to an exact + * datetime. For example, the maximum could just be the year, such as `1994`. + * Defaults to the end of this year. + */ + @Input() max: string; + + /** + * @input {string} The display format of the date and time as text that shows + * within the item. When the `pickerFormat` input is not used, then the + * `displayFormat` is used for both display the formatted text, and determining + * the datetime picker's columns. See the `pickerFormat` input description for + * more info. Defaults to `MMM D, YYYY`. + */ + @Input() displayFormat: string = 'MMM D, YYYY'; + + /** + * @input {string} The format of the date and time picker columns the user selects. + * A datetime input can have one or many datetime parts, each getting their + * own column which allow individual selection of that particular datetime part. For + * example, year and month columns are two individually selectable columns which help + * choose an exact date from the datetime picker. Each column follows the string + * parse format. Defaults to use `displayFormat`. + */ + @Input() pickerFormat: string; + + /** + * @input {string} The text to display on the picker's cancel button. Default: `Cancel`. + */ + @Input() cancelText: string = 'Cancel'; + + /** + * @input {string} The text to display on the picker's "Done" button. Default: `Done`. + */ + @Input() doneText: string = 'Done'; + + /** + * @input {array | string} Values used to create the list of selectable years. By default + * the year values range between the `min` and `max` datetime inputs. However, to + * control exactly which years to display, the `yearValues` input can take either an array + * of numbers, or string of comma separated numbers. For example, to show upcoming and + * recent leap years, then this input's value would be `yearValues="2024,2020,2016,2012,2008"`. + */ + @Input() yearValues: any; + + /** + * @input {array | string} Values used to create the list of selectable months. By default + * the month values range from `1` to `12`. However, to control exactly which months to + * display, the `monthValues` input can take either an array of numbers, or string of + * comma separated numbers. For example, if only summer months should be shown, then this + * input value would be `monthValues="6,7,8"`. Note that month numbers do *not* have a + * zero-based index, meaning January's value is `1`, and December's is `12`. + */ + @Input() monthValues: any; + + /** + * @input {array | string} Values used to create the list of selectable days. By default + * every day is shown for the given month. However, to control exactly which days of + * the month to display, the `dayValues` input can take either an array of numbers, or + * string of comma separated numbers. Note that even if the array days have an invalid + * number for the selected month, like `31` in February, it will correctly not show + * days which are not valid for the selected month. + */ + @Input() dayValues: any; + + /** + * @input {array | string} Values used to create the list of selectable hours. By default + * the hour values range from `1` to `23` for 24-hour, or `1` to `12` for 12-hour. However, + * to control exactly which hours to display, the `hourValues` input can take either an + * array of numbers, or string of comma separated numbers. + */ + @Input() hourValues: any; + + /** + * @input {array | string} Values used to create the list of selectable minutes. By default + * the mintues range from `1` to `59`. However, to control exactly which minutes to display, + * the `minuteValues` input can take either an array of numbers, or string of comma separated + * numbers. For example, if the minute selections should only be every 15 minutes, then + * this input value would be `minuteValues="0,15,30,45"`. + */ + @Input() minuteValues: any; + + /** + * @input {array} Full names for each month name. This can be used to provide + * locale month names. Defaults to English. + */ + @Input() monthNames: any; + + /** + * @input {array} Short abbreviated names for each month name. This can be used to provide + * locale month names. Defaults to English. + */ + @Input() monthShortNames: any; + + /** + * @input {array} Full day of the week names. This can be used to provide + * locale names for each day in the week. Defaults to English. + */ + @Input() dayNames: any; + + /** + * @input {array} Short abbreviated day of the week names. This can be used to provide + * locale names for each day in the week. Defaults to English. + */ + @Input() dayShortNames: any; + + /** + * @input {any} Any addition options that the picker interface can accept. + * See the [Picker API docs](../../picker/Picker) for the picker options. + */ + @Input() pickerOptions: any = {}; + + /** + * @output {any} Any expression to evaluate when the datetime selection has changed. + */ + @Output() change: EventEmitter = new EventEmitter(); + + /** + * @output {any} Any expression to evaluate when the datetime selection was cancelled. + */ + @Output() cancel: EventEmitter = new EventEmitter(); + + constructor( + private _form: Form, + private _config: Config, + @Optional() private _item: Item, + @Optional() private _nav: NavController + ) { + this._form.register(this); + if (_item) { + this.id = 'dt-' + _item.registerInput('datetime'); + this._labelId = 'lbl-' + _item.id; + this._item.setCssClass('item-datetime', true); + } + + if (!_nav) { + console.error('parent required for '); + } + } + + @HostListener('click', ['$event']) + private _click(ev) { + if (ev.detail === 0) { + // do not continue if the click event came from a form submit + return; + } + ev.preventDefault(); + ev.stopPropagation(); + this.open(); + } + + @HostListener('keyup.space', ['$event']) + private _keyup(ev) { + if (!this._isOpen) { + this.open(); + } + } + + /** + * @private + */ + open() { + if (this._disabled) { + return; + } + + console.debug('datetime, open picker'); + + // the user may have assigned some options specifically for the alert + let pickerOptions = merge({}, this.pickerOptions); + + let picker = Picker.create(pickerOptions); + pickerOptions.buttons = [ + { + text: this.cancelText, + role: 'cancel', + handler: () => { + this.cancel.emit(null); + } + }, + { + text: this.doneText, + handler: (data) => { + console.log('datetime, done', data); + this.onChange(data); + this.change.emit(data); + } + } + ]; + + this.generate(picker); + this.validate(picker); + + picker.change.subscribe(() => { + this.validate(picker); + }); + + this._nav.present(picker, pickerOptions); + + this._isOpen = true; + picker.onDismiss(() => { + this._isOpen = false; + }); + } + + /** + * @private + */ + generate(picker: Picker) { + // if a picker format wasn't provided, then fallback + // to use the display format + let template = this.pickerFormat || this.displayFormat; + + if (isPresent(template)) { + // make sure we've got up to date sizing information + this.calcMinMax(); + + // does not support selecting by day name + // automaticallly remove any day name formats + template = template.replace('DDDD', '{~}').replace('DDD', '{~}'); + if (template.indexOf('D') === -1) { + // there is not a day in the template + // replace the day name with a numeric one if it exists + template = template.replace('{~}', 'D'); + } + // make sure no day name replacer is left in the string + template = template.replace(/{~}/g, ''); + + // parse apart the given template into an array of "formats" + parseTemplate(template).forEach(format => { + // loop through each format in the template + // create a new picker column to build up with data + let key = convertFormatToKey(format); + let values: any[]; + + // first see if they have exact values to use for this input + if (isPresent(this[key + 'Values'])) { + // user provide exact values for this date part + values = convertToArrayOfNumbers(this[key + 'Values'], key); + + } else { + // use the default date part values + values = dateValueRange(format, this._min, this._max); + } + + let column: PickerColumn = { + name: key, + options: values.map(val => { + return { + value: val, + text: renderTextFormat(format, val, null, this._locale), + }; + }) + }; + + if (column.options.length) { + // cool, we've loaded up the columns with options + // preselect the option for this column + var selected = column.options.find(opt => opt.value === getValueFromFormat(this._value, format)); + if (selected) { + // set the select index for this column's options + column.selectedIndex = column.options.indexOf(selected); + } + + // add our newly created column to the picker + picker.addColumn(column); + } + }); + + this.divyColumns(picker); + } + } + + /** + * @private + */ + validate(picker: Picker) { + let i: number; + let today = new Date(); + let columns = picker.getColumns(); + + // find the columns used + let yearCol = columns.find(col => col.name === 'year'); + let monthCol = columns.find(col => col.name === 'month'); + let dayCol = columns.find(col => col.name === 'day'); + + let yearOpt: PickerColumnOption; + let monthOpt: PickerColumnOption; + let dayOpt: PickerColumnOption; + + // default to assuming today's year + let selectedYear = today.getFullYear(); + if (yearCol) { + yearOpt = yearCol.options[yearCol.selectedIndex]; + if (yearOpt) { + // they have a selected year value + selectedYear = yearOpt.value; + } + } + + // default to assuming this month has 31 days + let numDaysInMonth = 31; + let selectedMonth; + if (monthCol) { + monthOpt = monthCol.options[monthCol.selectedIndex]; + if (monthOpt) { + // they have a selected month value + selectedMonth = monthOpt.value; + + // calculate how many days are in this month + numDaysInMonth = daysInMonth(selectedMonth, selectedYear); + } + } + + // create sort values for the min/max datetimes + let minCompareVal = dateDataSortValue(this._min); + let maxCompareVal = dateDataSortValue(this._max); + + if (monthCol) { + // enable/disable which months are valid + // to show within the min/max date range + for (i = 0; i < monthCol.options.length; i++) { + monthOpt = monthCol.options[i]; + + // loop through each month and see if it + // is within the min/max date range + monthOpt.disabled = (dateSortValue(selectedYear, monthOpt.value, 31) < minCompareVal || + dateSortValue(selectedYear, monthOpt.value, 1) > maxCompareVal); + } + } + + if (dayCol) { + if (isPresent(selectedMonth)) { + // enable/disable which days are valid + // to show within the min/max date range + for (i = 0; i < 31; i++) { + dayOpt = dayCol.options[i]; + + // loop through each day and see if it + // is within the min/max date range + var compareVal = dateSortValue(selectedYear, selectedMonth, dayOpt.value); + + dayOpt.disabled = (compareVal < minCompareVal || + compareVal > maxCompareVal || + numDaysInMonth <= i); + } + + } else { + // enable/disable which numbers of days to show in this month + for (i = 0; i < 31; i++) { + dayCol.options[i].disabled = (numDaysInMonth <= i); + } + } + } + + picker.refresh(); + } + + /** + * @private + */ + divyColumns(picker: Picker) { + let pickerColumns = picker.getColumns(); + let columns = []; + + pickerColumns.forEach((col, i) => { + columns.push(0); + + col.options.forEach(opt => { + if (opt.text.length > columns[i]) { + columns[i] = opt.text.length; + } + }); + + }); + + if (columns.length === 2) { + var width = Math.max(columns[0], columns[1]); + pickerColumns[0].columnWidth = pickerColumns[1].columnWidth = `${width * 16}px`; + + } else if (columns.length === 3) { + var width = Math.max(columns[0], columns[2]); + pickerColumns[1].columnWidth = `${columns[1] * 16}px`; + pickerColumns[0].columnWidth = pickerColumns[2].columnWidth = `${width * 16}px`; + + } else if (columns.length > 3) { + columns.forEach((col, i) => { + pickerColumns[i].columnWidth = `${col * 12}px`; + }); + } + } + + /** + * @private + */ + setValue(newData: any) { + updateDate(this._value, newData); + } + + /** + * @private + */ + getValue(): DateTimeData { + return this._value; + } + + /** + * @private + */ + updateText() { + // create the text of the formatted data + this._text = renderDateTime(this.displayFormat, this._value, this._locale); + } + + /** + * @private + */ + calcMinMax() { + let todaysYear = new Date().getFullYear(); + + if (isBlank(this.min)) { + if (isPresent(this.yearValues)) { + this.min = Math.min.apply(Math, convertToArrayOfNumbers(this.yearValues, 'year')); + + } else { + this.min = (todaysYear - 100).toString(); + } + } + + if (isBlank(this.max)) { + if (isPresent(this.yearValues)) { + this.max = Math.max.apply(Math, convertToArrayOfNumbers(this.yearValues, 'year')); + + } else { + this.max = todaysYear.toString(); + } + } + + let min = this._min = parseDate(this.min); + let max = this._max = parseDate(this.max); + + min.month = min.month || 1; + min.day = min.day || 1; + min.hour = min.hour || 0; + min.minute = min.minute || 0; + min.second = min.second || 0; + + max.month = max.month || 12; + max.day = max.day || 31; + max.hour = max.hour || 23; + max.minute = max.minute || 59; + max.second = max.second || 59; + } + + /** + * @input {boolean} Whether or not the datetime component is disabled. Default `false`. + */ + @Input() + get disabled() { + return this._disabled; + } + + set disabled(val) { + this._disabled = isTrueProperty(val); + this._item && this._item.setCssClass('item-datetime-disabled', this._disabled); + } + + /** + * @private + */ + writeValue(val: any) { + console.debug('datetime, writeValue', val); + this.setValue(val); + this.updateText(); + } + + /** + * @private + */ + ngAfterContentInit() { + // first see if locale names were provided in the inputs + // then check to see if they're in the config + // if neither were provided then it will use default English names + ['monthNames', 'monthShortNames', 'dayNames', 'dayShortNames'].forEach(type => { + this._locale[type] = convertToArrayOfStrings(isPresent(this[type]) ? this[type] : this._config.get(type), type); + }); + + // update how the datetime value is displayed as formatted text + this.updateText(); + } + + /** + * @private + */ + registerOnChange(fn: Function): void { + this._fn = fn; + this.onChange = (val: any) => { + console.debug('datetime, onChange', val); + this.setValue(val); + this.updateText(); + + // convert DateTimeData value to iso datetime format + fn(convertDataToISO(this._value)); + + this.onTouched(); + }; + } + + /** + * @private + */ + registerOnTouched(fn) { this.onTouched = fn; } + + /** + * @private + */ + onChange(val: any) { + // onChange used when there is not an ngControl + console.debug('datetime, onChange w/out ngControl', val); + this.setValue(val); + this.updateText(); + this.onTouched(); + } + + /** + * @private + */ + onTouched() { } + + /** + * @private + */ + ngOnDestroy() { + this._form.deregister(this); + } +} + +/** + * @private + * Use to convert a string of comma separated numbers or + * an array of numbers, and clean up any user input + */ +function convertToArrayOfNumbers(input: any, type: string): number[] { + var values: number[] = []; + + if (isString(input)) { + // convert the string to an array of strings + // auto remove any whitespace and [] characters + input = input.replace(/\[|\]|\s/g, '').split(','); + } + + if (isArray(input)) { + // ensure each value is an actual number in the returned array + input.forEach(num => { + num = parseInt(num, 10); + if (!isNaN(num)) { + values.push(num); + } + }); + } + + if (!values.length) { + console.warn(`Invalid "${type}Values". Must be an array of numbers, or a comma separated string of numbers.`); + } + + return values; +} + +/** + * @private + * Use to convert a string of comma separated strings or + * an array of strings, and clean up any user input + */ +function convertToArrayOfStrings(input: any, type: string): string[] { + if (isPresent(input)) { + var values: string[] = []; + + if (isString(input)) { + // convert the string to an array of strings + // auto remove any [] characters + input = input.replace(/\[|\]/g, '').split(','); + } + + if (isArray(input)) { + // trim up each string value + input.forEach(val => { + val = val.trim(); + if (val) { + values.push(val); + } + }); + } + + if (!values.length) { + console.warn(`Invalid "${type}Names". Must be an array of strings, or a comma separated string.`); + } + + return values; + } +} diff --git a/ionic/components/datetime/test/basic/e2e.ts b/ionic/components/datetime/test/basic/e2e.ts new file mode 100644 index 0000000000..d572f477d2 --- /dev/null +++ b/ionic/components/datetime/test/basic/e2e.ts @@ -0,0 +1,8 @@ + +it('should open basic datetime picker', function() { + element(by.css('.e2eOpenMMDDYYYY')).click(); +}); + +it('should close with Done button click', function() { + element(by.css('.picker-button:last-child')).click(); +}); diff --git a/ionic/components/datetime/test/basic/index.ts b/ionic/components/datetime/test/basic/index.ts new file mode 100644 index 0000000000..474f203e30 --- /dev/null +++ b/ionic/components/datetime/test/basic/index.ts @@ -0,0 +1,41 @@ +import {App, Page} from '../../../../../ionic'; + + +@Page({ + templateUrl: 'main.html' +}) +class E2EPage { + wwwInvented = '1989'; + time = '13:47'; + netscapeReleased = '1994-12-15T13:47:20.789'; + operaReleased = '1995-04-15'; + firefoxReleased = '2002-09-23T15:03:46.789'; + webkitOpenSourced = '2005-06-17T11:06Z'; + chromeReleased = '2008-09-02'; + leapYearsSummerMonths = ''; + + leapYearsArray = [2020, 2016, 2008, 2004, 2000, 1996]; + + customShortDay = [ + 's\u00f8n', + 'man', + 'tir', + 'ons', + 'tor', + 'fre', + 'l\u00f8r' + ]; + +} + + +@App({ + template: '' +}) +class E2EApp { + root; + + constructor() { + this.root = E2EPage; + } +} diff --git a/ionic/components/datetime/test/basic/main.html b/ionic/components/datetime/test/basic/main.html new file mode 100644 index 0000000000..46969834a5 --- /dev/null +++ b/ionic/components/datetime/test/basic/main.html @@ -0,0 +1,68 @@ + + Datetime + + + + + + YYYY + + + + + MMMM YY + + + + + MMM DD, YYYY + + + + + DDD. MM/DD/YY (locale day) + + + + + D MMM YYYY H:mm + + + + + DDDD MMM D, YYYY + + + + + HH:mm + + + + + h:mm a + + + + + hh:mm A (15 min steps) + + + + + Leap years, summer months + + + + + + \ No newline at end of file diff --git a/ionic/components/datetime/test/datetime.spec.ts b/ionic/components/datetime/test/datetime.spec.ts new file mode 100644 index 0000000000..c8da46a3ce --- /dev/null +++ b/ionic/components/datetime/test/datetime.spec.ts @@ -0,0 +1,518 @@ +import {DateTime, Form, Picker, Config, NavController} from '../../../../ionic'; +import * as datetime from '../../../../ionic/util/datetime-util'; + +export function run() { + +describe('DateTime', () => { + + describe('validate', () => { + + it('should restrict January 1-14, 2000 from selection, then allow it, and restrict December 15-31, 2001', () => { + datetime.max = '2001-12-15'; + datetime.min = '2000-01-15'; + datetime.pickerFormat = 'MM DD YYYY'; + var picker = new Picker(); + datetime.generate(picker); + + var columns = picker.getColumns(); + columns[0].selectedIndex = 0; // January + columns[1].selectedIndex = 0; // January 1st + columns[2].selectedIndex = 1; // January 1st, 2000 + + datetime.validate(picker); + + expect(columns[1].options[0].disabled).toEqual(true); + expect(columns[1].options[13].disabled).toEqual(true); + expect(columns[1].options[14].disabled).toEqual(false); + + columns[0].selectedIndex = 11; // December + columns[2].selectedIndex = 0; // December 1st, 2001 + + datetime.validate(picker); + + expect(columns[0].options[11].disabled).toEqual(false); + + expect(columns[1].options[14].disabled).toEqual(false); + expect(columns[1].options[15].disabled).toEqual(true); + expect(columns[1].options[30].disabled).toEqual(true); + }); + + it('should restrict January 2000 from selection, then allow it, and restrict December 2010', () => { + datetime.max = '2010-11-15'; + datetime.min = '2000-02-15'; + datetime.pickerFormat = 'MM DD YYYY'; + var picker = new Picker(); + datetime.generate(picker); + + var columns = picker.getColumns(); + columns[0].selectedIndex = 1; // February + columns[1].selectedIndex = 0; // February 1st + columns[2].selectedIndex = columns[2].options.length - 1; // February 1st, 2000 + + datetime.validate(picker); + + expect(columns[0].options[0].disabled).toEqual(true); + expect(columns[0].options[1].disabled).toEqual(false); + expect(columns[0].options[11].disabled).toEqual(false); + + columns[2].selectedIndex = 0; // December 1st, 2010 + + datetime.validate(picker); + + expect(columns[0].options[0].disabled).toEqual(false); + expect(columns[0].options[10].disabled).toEqual(false); + expect(columns[0].options[11].disabled).toEqual(true); + }); + + it('should only show 31 valid days in the selected 31 day month, then reset for 28 day, then to 30', () => { + datetime.max = '2010-12-31'; + datetime.min = '2000-01-01'; + datetime.pickerFormat = 'MM DD YYYY'; + + var picker = new Picker(); + datetime.generate(picker); + + var columns = picker.getColumns(); + columns[0].selectedIndex = 0; // January + columns[1].selectedIndex = 0; // January 1st + columns[2].selectedIndex = 0; // January 1st, 2010 + + datetime.validate(picker); + + for (var i = 0; i < 31; i++) { + expect(columns[1].options[i].disabled).toEqual(false); + } + + columns[0].selectedIndex = 1; // February + datetime.validate(picker); + + for (var i = 0; i < 28; i++) { + expect(columns[1].options[i].disabled).toEqual(false); + } + expect(columns[1].options[28].disabled).toEqual(true); + expect(columns[1].options[29].disabled).toEqual(true); + expect(columns[1].options[30].disabled).toEqual(true); + + columns[0].selectedIndex = 3; // April + datetime.validate(picker); + + for (var i = 0; i < 30; i++) { + expect(columns[1].options[i].disabled).toEqual(false); + } + expect(columns[1].options[30].disabled).toEqual(true); + }); + + }); + + describe('generate', () => { + + it('should generate with custom locale short month names from input property', () => { + datetime.monthShortNames = customLocale.monthShortNames; + datetime.ngAfterContentInit(); + datetime.pickerFormat = 'MMM YYYY'; + datetime.setValue('1994-12-15T13:47:20.789Z'); + + var picker = new Picker(); + datetime.generate(picker); + var columns = picker.getColumns(); + + expect(columns.length).toEqual(2); + expect(columns[0].name).toEqual('month'); + expect(columns[0].options[0].value).toEqual(1); + expect(columns[0].options[0].text).toEqual('jan'); + }); + + it('should generate with custom locale full month names from input property', () => { + datetime.monthNames = customLocale.monthNames; + datetime.ngAfterContentInit(); + datetime.pickerFormat = 'MMMM YYYY'; + datetime.setValue('1994-12-15T13:47:20.789Z'); + + var picker = new Picker(); + datetime.generate(picker); + var columns = picker.getColumns(); + + expect(columns.length).toEqual(2); + expect(columns[0].name).toEqual('month'); + expect(columns[0].options[0].value).toEqual(1); + expect(columns[0].options[0].text).toEqual('janeiro'); + }); + + it('should replace a picker format with both a day name and a numeric day to use only the numeric day', () => { + datetime.pickerFormat = 'DDDD D M YYYY'; + datetime.setValue('1994-12-15T13:47:20.789Z'); + + var picker = new Picker(); + datetime.generate(picker); + var columns = picker.getColumns(); + + expect(columns.length).toEqual(3); + expect(columns[0].name).toEqual('day'); + expect(columns[0].options[0].value).toEqual(1); + expect(columns[0].options[0].text).toEqual('1'); + }); + + it('should replace a picker format with only a day name to use a numeric day instead', () => { + datetime.pickerFormat = 'DDDD M YYYY'; + datetime.setValue('1994-12-15T13:47:20.789Z'); + + var picker = new Picker(); + datetime.generate(picker); + var columns = picker.getColumns(); + + expect(columns.length).toEqual(3); + expect(columns[0].name).toEqual('day'); + expect(columns[0].options[0].value).toEqual(1); + expect(columns[0].options[0].text).toEqual('1'); + }); + + it('should generate MM DD YYYY pickerFormat with min/max', () => { + datetime.max = '2010-12-31'; + datetime.min = '2000-01-01'; + datetime.pickerFormat = 'MM DD YYYY'; + + var picker = new Picker(); + datetime.generate(picker); + var columns = picker.getColumns(); + + expect(columns.length).toEqual(3); + expect(columns[0].options.length).toEqual(12); + expect(columns[0].options[0].value).toEqual(1); + expect(columns[0].options[11].value).toEqual(12); + + expect(columns[1].options.length).toEqual(31); + expect(columns[1].options[0].value).toEqual(1); + expect(columns[1].options[30].value).toEqual(31); + + expect(columns[2].options.length).toEqual(11); + expect(columns[2].options[0].value).toEqual(2010); + expect(columns[2].options[10].value).toEqual(2000); + }); + + it('should generate YYYY pickerFormat with min/max', () => { + datetime.max = '2010-01-01'; + datetime.min = '2000-01-01'; + datetime.pickerFormat = 'YYYY'; + + var picker = new Picker(); + datetime.generate(picker); + var columns = picker.getColumns(); + + expect(columns.length).toEqual(1); + expect(columns[0].options.length).toEqual(11); + expect(columns[0].options[0].value).toEqual(2010); + expect(columns[0].options[10].value).toEqual(2000); + }); + + }); + + describe('calcMinMax', () => { + + it('should max date with no max input, but has yearValues input', () => { + datetime.yearValues = '2000,1996,1992'; + datetime.calcMinMax(); + expect(datetime._max.year).toEqual(2000); + expect(datetime._max.month).toEqual(12); + expect(datetime._max.day).toEqual(31); + expect(datetime._max.hour).toEqual(23); + expect(datetime._max.minute).toEqual(59); + expect(datetime._max.second).toEqual(59); + }); + + it('should min date with no min input, but has yearValues input', () => { + datetime.yearValues = '2000,1996,1992'; + datetime.calcMinMax(); + expect(datetime._min.year).toEqual(1992); + expect(datetime._min.month).toEqual(1); + expect(datetime._min.day).toEqual(1); + expect(datetime._min.hour).toEqual(0); + expect(datetime._min.minute).toEqual(0); + expect(datetime._min.second).toEqual(0); + }); + + it('should min date with only YYYY', () => { + datetime.min = '1994'; + datetime.calcMinMax(); + expect(datetime._min.year).toEqual(1994); + expect(datetime._min.month).toEqual(1); + expect(datetime._min.day).toEqual(1); + expect(datetime._min.hour).toEqual(0); + expect(datetime._min.minute).toEqual(0); + expect(datetime._min.second).toEqual(0); + }); + + it('should max date with only YYYY', () => { + datetime.max = '1994'; + datetime.calcMinMax(); + expect(datetime._max.year).toEqual(1994); + expect(datetime._max.month).toEqual(12); + expect(datetime._max.day).toEqual(31); + expect(datetime._max.hour).toEqual(23); + expect(datetime._max.minute).toEqual(59); + expect(datetime._max.second).toEqual(59); + }); + + it('should max date from max input string', () => { + datetime.max = '1994-12-15T13:47:20.789Z'; + datetime.calcMinMax(); + expect(datetime._max.year).toEqual(1994); + expect(datetime._max.month).toEqual(12); + expect(datetime._max.day).toEqual(15); + expect(datetime._max.hour).toEqual(13); + expect(datetime._max.minute).toEqual(47); + expect(datetime._max.second).toEqual(20); + expect(datetime._max.millisecond).toEqual(789); + }); + + it('should min date from max input string', () => { + datetime.min = '0123-01-05T00:05:00.009Z'; + datetime.calcMinMax(); + expect(datetime._min.year).toEqual(123); + expect(datetime._min.month).toEqual(1); + expect(datetime._min.day).toEqual(5); + expect(datetime._min.hour).toEqual(0); + expect(datetime._min.minute).toEqual(5); + expect(datetime._min.second).toEqual(0); + expect(datetime._min.millisecond).toEqual(9); + }); + + it('should default max date when not set', () => { + datetime.calcMinMax(); + expect(datetime._max.year).toEqual(new Date().getFullYear()); + expect(datetime._max.month).toEqual(12); + expect(datetime._max.day).toEqual(31); + expect(datetime._max.hour).toEqual(23); + expect(datetime._max.minute).toEqual(59); + expect(datetime._max.second).toEqual(59); + }); + + it('should default min date when not set', () => { + datetime.calcMinMax(); + expect(datetime._min.year).toEqual(new Date().getFullYear() - 100); + expect(datetime._min.month).toEqual(1); + expect(datetime._min.day).toEqual(1); + expect(datetime._min.hour).toEqual(0); + expect(datetime._min.minute).toEqual(0); + expect(datetime._min.second).toEqual(0); + }); + + }); + + describe('setValue', () => { + + it('should update existing time value with 12-hour PM DateTimeData value', () => { + var d = '13:47:20.789Z'; + datetime.setValue(d); + + var dateTimeData = { + hour: { + text: '12', + value: 12, + }, + minute: { + text: '09', + value: 9, + }, + ampm: { + text: 'pm', + value: 'pm', + }, + }; + datetime.setValue(dateTimeData); + + expect(datetime.getValue().hour).toEqual(12); + expect(datetime.getValue().minute).toEqual(9); + expect(datetime.getValue().second).toEqual(20); + + dateTimeData.hour.value = 1; + datetime.setValue(dateTimeData); + + expect(datetime.getValue().hour).toEqual(13); + expect(datetime.getValue().minute).toEqual(9); + expect(datetime.getValue().second).toEqual(20); + }); + + it('should update existing time value with 12-hour AM DateTimeData value', () => { + var d = '13:47:20.789Z'; + datetime.setValue(d); + + var dateTimeData = { + hour: { + text: '12', + value: 12, + }, + minute: { + text: '09', + value: 9, + }, + ampm: { + text: 'am', + value: 'am', + }, + }; + datetime.setValue(dateTimeData); + + expect(datetime.getValue().hour).toEqual(0); + expect(datetime.getValue().minute).toEqual(9); + expect(datetime.getValue().second).toEqual(20); + + dateTimeData.hour.value = 11; + datetime.setValue(dateTimeData); + + expect(datetime.getValue().hour).toEqual(11); + expect(datetime.getValue().minute).toEqual(9); + expect(datetime.getValue().second).toEqual(20); + }); + + it('should update existing time value with new DateTimeData value', () => { + var d = '13:47:20.789Z'; + datetime.setValue(d); + + expect(datetime.getValue().hour).toEqual(13); + expect(datetime.getValue().minute).toEqual(47); + expect(datetime.getValue().second).toEqual(20); + + var dateTimeData = { + hour: { + text: '15', + value: 15, + }, + minute: { + text: '09', + value: 9, + }, + }; + datetime.setValue(dateTimeData); + + expect(datetime.getValue().year).toEqual(null); + expect(datetime.getValue().month).toEqual(null); + expect(datetime.getValue().day).toEqual(null); + expect(datetime.getValue().hour).toEqual(15); + expect(datetime.getValue().minute).toEqual(9); + expect(datetime.getValue().second).toEqual(20); + }); + + it('should update existing DateTimeData value with new DateTimeData value', () => { + var d = '1994-12-15T13:47:20.789Z'; + datetime.setValue(d); + + expect(datetime.getValue().year).toEqual(1994); + + var dateTimeData = { + year: { + text: '1995', + value: 1995, + }, + month: { + text: 'December', + value: 12, + }, + day: { + text: '20', + value: 20 + }, + whatevaIDoWhatIWant: -99, + }; + datetime.setValue(dateTimeData); + + expect(datetime.getValue().year).toEqual(1995); + expect(datetime.getValue().month).toEqual(12); + expect(datetime.getValue().day).toEqual(20); + expect(datetime.getValue().hour).toEqual(13); + expect(datetime.getValue().minute).toEqual(47); + }); + + it('should parse a ISO date string with no existing DateTimeData value', () => { + var d = '1994-12-15T13:47:20.789Z'; + datetime.setValue(d); + expect(datetime.getValue().year).toEqual(1994); + expect(datetime.getValue().month).toEqual(12); + expect(datetime.getValue().day).toEqual(15); + }); + + it('should not parse a Date object', () => { + var d = new Date(1994, 11, 15); + datetime.setValue(d); + expect(datetime.getValue()).toEqual({}); + }); + + it('should not parse a value with bad data', () => { + var d = 'umm 1994 i think'; + datetime.setValue(d); + expect(datetime.getValue()).toEqual({}); + }); + + it('should not parse a value with blank value', () => { + datetime.setValue(null); + expect(datetime.getValue()).toEqual({}); + + datetime.setValue(undefined); + expect(datetime.getValue()).toEqual({}); + + datetime.setValue(''); + expect(datetime.getValue()).toEqual({}); + }); + + }); + + var datetime: DateTime; + + beforeEach(() => { + datetime = new DateTime(new Form(), new Config(), null, {}); + }); + + console.warn = function(){}; + + // pt-br + var customLocale: datetime.LocaleData = { + dayShort: [ + 'domingo', + 'segunda-feira', + 'ter\u00e7a-feira', + 'quarta-feira', + 'quinta-feira', + 'sexta-feira', + 's\u00e1bado' + ], + dayShortNames: [ + 'dom', + 'seg', + 'ter', + 'qua', + 'qui', + 'sex', + 's\u00e1b' + ], + monthNames: [ + 'janeiro', + 'fevereiro', + 'mar\u00e7o', + 'abril', + 'maio', + 'junho', + 'julho', + 'agosto', + 'setembro', + 'outubro', + 'novembro', + 'dezembro' + ], + monthShortNames: [ + 'jan', + 'fev', + 'mar', + 'abr', + 'mai', + 'jun', + 'jul', + 'ago', + 'set', + 'out', + 'nov', + 'dez' + ], + }; + +}); + +} diff --git a/ionic/components/item/item.ts b/ionic/components/item/item.ts index f5d0568c98..e55ad09097 100644 --- a/ionic/components/item/item.ts +++ b/ionic/components/item/item.ts @@ -48,7 +48,7 @@ import {Label} from '../label/label'; '' + '' + '' + - '' + + '' + '
' + '' + '
' + diff --git a/ionic/components/picker/picker.ios.scss b/ionic/components/picker/picker.ios.scss index 6e58e3811e..a254a7230b 100644 --- a/ionic/components/picker/picker.ios.scss +++ b/ionic/components/picker/picker.ios.scss @@ -15,11 +15,12 @@ $picker-ios-button-height: $picker-ios-toolbar-height !defau $picker-ios-button-text-color: $link-ios-color !default; $picker-ios-button-background-color: transparent !default; -$picker-ios-column-padding: 0 12px !default; +$picker-ios-column-padding: 0 4px !default; +$picker-ios-column-perspective: 1000px !default; -$picker-ios-option-padding: 0 10px !default; +$picker-ios-option-padding: 0 !default; $picker-ios-option-text-color: $list-ios-text-color !default; -$picker-ios-option-font-size: 22px !default; +$picker-ios-option-font-size: 20px !default; $picker-ios-option-height: 42px !default; $picker-ios-option-offset-y: (($picker-ios-height - $picker-ios-toolbar-height) / 2) - ($picker-ios-option-height / 2) - 10 !default; @@ -74,7 +75,7 @@ $picker-highlight-opacity: .8 !default; .picker-columns { height: $picker-ios-height - $picker-ios-toolbar-height; - perspective: 1800px; + perspective: $picker-ios-column-perspective; } .picker-col { @@ -101,8 +102,6 @@ $picker-highlight-opacity: .8 !default; margin: 0; padding: $picker-ios-option-padding; - width: calc(100% - 24px); - font-size: $picker-ios-option-font-size; line-height: $picker-ios-option-height; diff --git a/ionic/components/picker/picker.md.scss b/ionic/components/picker/picker.md.scss index 825c72d3ca..8c10eb7335 100644 --- a/ionic/components/picker/picker.md.scss +++ b/ionic/components/picker/picker.md.scss @@ -15,15 +15,15 @@ $picker-md-button-height: $picker-md-toolbar-height !default $picker-md-button-text-color: $link-md-color !default; $picker-md-button-background-color: transparent !default; -$picker-md-column-padding: 0 12px !default; +$picker-md-column-padding: 0 8px !default; -$picker-md-option-padding: 0 10px !default; +$picker-md-option-padding: 0 !default; $picker-md-option-text-color: $list-md-text-color !default; $picker-md-option-font-size: 18px !default; $picker-md-option-height: 42px !default; $picker-md-option-offset-y: (($picker-md-height - $picker-md-toolbar-height) / 2) - ($picker-md-option-height / 2) - 10 !default; -$picker-md-option-selected-font-size: 24px !default; +$picker-md-option-selected-font-size: 22px !default; $picker-md-option-selected-color: $link-md-color !default; $picker-highlight-opacity: .8 !default; @@ -98,8 +98,6 @@ $picker-highlight-opacity: .8 !default; margin: 0; padding: $picker-md-option-padding; - width: calc(100% - 24px); - font-size: $picker-md-option-font-size; line-height: $picker-md-option-height; diff --git a/ionic/components/picker/picker.scss b/ionic/components/picker/picker.scss index 792f3698fe..2eaf0cbcc4 100644 --- a/ionic/components/picker/picker.scss +++ b/ionic/components/picker/picker.scss @@ -99,10 +99,25 @@ ion-picker-cmp { flex: 1; width: 100%; +} + +.picker-opt .button-inner { + display: block; + + overflow: hidden; - text-align: center; text-overflow: ellipsis; white-space: nowrap; + + transition: opacity 150ms ease-in-out; +} + +.picker-opt.picker-opt-disabled { + pointer-events: none; +} + +.picker-opt-disabled .button-inner { + opacity: 0; } .picker-opts-left .button-inner { diff --git a/ionic/components/picker/picker.ts b/ionic/components/picker/picker.ts index 0c0f2c9758..0d58c58ac6 100644 --- a/ionic/components/picker/picker.ts +++ b/ionic/components/picker/picker.ts @@ -1,12 +1,12 @@ -import {Component, ElementRef, Input, ViewChild, Renderer, HostListener, ViewEncapsulation} from 'angular2/core'; +import {Component, ElementRef, Input, Output, EventEmitter, ViewChildren, QueryList, ViewChild, Renderer, HostListener, ViewEncapsulation} from 'angular2/core'; import {Animation} from '../../animations/animation'; import {Transition, TransitionOptions} from '../../transitions/transition'; import {Config} from '../../config/config'; -import {isPresent, isString, isNumber} from '../../util/util'; +import {isPresent, isString, isNumber, clamp} from '../../util/util'; import {NavParams} from '../nav/nav-params'; import {ViewController} from '../nav/view-controller'; -import {nativeRaf, cancelRaf, CSS, pointerCoord} from '../../util/dom'; +import {raf, cancelRaf, CSS, pointerCoord} from '../../util/dom'; /** @@ -16,6 +16,8 @@ import {nativeRaf, cancelRaf, CSS, pointerCoord} from '../../util/dom'; */ export class Picker extends ViewController { + @Output() change: EventEmitter; + constructor(opts: PickerOptions = {}) { opts.columns = opts.columns || []; opts.buttons = opts.buttons || []; @@ -25,6 +27,8 @@ export class Picker extends ViewController { this.viewType = 'picker'; this.isOverlay = true; + this.change = new EventEmitter(); + // by default, pickers should not fire lifecycle events of other views // for example, when an picker enters, the current active view should // not fire its lifecycle events because it's not conceptually leaving @@ -54,6 +58,14 @@ export class Picker extends ViewController { this.data.columns.push(column); } + getColumns(): PickerColumn[] { + return this.data.columns; + } + + refresh() { + this.instance.refresh && this.instance.refresh(); + } + /** * @param {string} cssClass CSS class name to add to the picker's outer wrapper. */ @@ -76,7 +88,7 @@ export class Picker extends ViewController { template: '
{{col.prefix}}
' + '
' + - '' + '
' + @@ -91,7 +103,6 @@ export class Picker extends ViewController { '(mousedown)': 'pointerStart($event)', '(mousemove)': 'pointerMove($event)', '(body:mouseup)': 'pointerEnd($event)', - '(body:mouseout)': 'mouseOut($event)', } }) class PickerColumnCmp { @@ -106,8 +117,11 @@ class PickerColumnCmp { startY: number = null; rafId: number; bounceFrom: number; + minY: number; maxY: number; rotateFactor: number; + lastIndex: number; + @Output() change: EventEmitter = new EventEmitter(); constructor(config: Config) { this.rotateFactor = config.getNumber('pickerRotateFactor', 0); @@ -123,8 +137,7 @@ class PickerColumnCmp { this.optHeight = (colEle.firstElementChild ? colEle.firstElementChild.clientHeight : 0); // set the scroll position for the selected option - let selectedIndex = this.col.options.indexOf(this.col.selected); - this.setSelected(selectedIndex, 0); + this.setSelected(this.col.selectedIndex, 0); } pointerStart(ev) { @@ -145,7 +158,24 @@ class PickerColumnCmp { this.velocity = 0; this.pos.length = 0; this.pos.push(this.startY, Date.now()); - this.maxY = (this.optHeight * (this.col.options.length - 1)) * -1; + + 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) { + continue; + } + if (i < minY) { + minY = i; + } + if (i > maxY) { + maxY = i; + } + } + + this.minY = (minY * this.optHeight * -1); + this.maxY = (maxY * this.optHeight * -1); } pointerMove(ev) { @@ -163,21 +193,21 @@ class PickerColumnCmp { // update the scroll position relative to pointer start position var y = this.y + (currentY - this.startY); - if (y > 0) { + 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 = y + Math.pow(this.maxY - y, 0.9); + y += Math.pow(this.maxY - y, 0.9); this.bounceFrom = y; } else { this.bounceFrom = 0; } - this.update(y, 0, false); + this.update(y, 0, false, false); } } @@ -190,11 +220,11 @@ class PickerColumnCmp { if (this.bounceFrom > 0) { // bounce back up - this.update(0, 100, true); + this.update(this.minY, 100, true, true); } else if (this.bounceFrom < 0) { // bounce back down - this.update(this.maxY, 100, true); + this.update(this.maxY, 100, true, true); } else if (this.startY !== null) { var endY = pointerCoord(ev).y; @@ -226,7 +256,7 @@ class PickerColumnCmp { ev.stopPropagation(); var y = this.y + (endY - this.startY); - this.update(y, 0, true); + this.update(y, 0, true, true); } } @@ -235,19 +265,13 @@ class PickerColumnCmp { this.decelerate(); } - mouseOut(ev) { - if (ev.target.classList.contains('picker-col')) { - this.pointerEnd(ev); - } - } - decelerate() { - var y = 0; + let y = 0; cancelRaf(this.rafId); if (isNaN(this.y) || !this.optHeight) { // fallback in case numbers get outta wack - this.update(y, 0, true); + this.update(y, 0, true, true); } else if (Math.abs(this.velocity) > 0) { // still decelerating @@ -258,9 +282,9 @@ class PickerColumnCmp { y = Math.round(this.y - this.velocity); - if (y > 0) { + if (y > this.minY) { // whoops, it's trying to scroll up farther than the options we have! - y = 0; + y = this.minY; this.velocity = 0; } else if (y < this.maxY) { @@ -271,11 +295,13 @@ class PickerColumnCmp { console.log(`decelerate y: ${y}, velocity: ${this.velocity}, optHeight: ${this.optHeight}`); - this.update(y, 0, true); + var notLockedIn = (y % this.optHeight !== 0 || Math.abs(this.velocity) > 1); - if (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 = nativeRaf(this.decelerate.bind(this)); + this.rafId = raf(this.decelerate.bind(this)); } } else if (this.y % this.optHeight !== 0) { @@ -307,22 +333,17 @@ class PickerColumnCmp { this.velocity = 0; // so what y position we're at - this.update(y, duration, true); + this.update(y, duration, true, true); } - update(y: number, duration: number, saveY: boolean) { + update(y: number, duration: number, saveY: boolean, emitChange: boolean) { // ensure we've got a good round number :) y = Math.round(y); - let selectedIndex = Math.abs(Math.round(y / this.optHeight)); + this.col.selectedIndex = Math.max(Math.abs(Math.round(y / this.optHeight)), 0); - this.col.selected = this.col.options[selectedIndex]; - - let colEle: HTMLElement = this.colEle.nativeElement; - let optElements: any = colEle.querySelectorAll('.picker-opt'); - - for (var i = 0; i < optElements.length; i++) { - var optEle: HTMLElement = optElements[i]; + 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); @@ -332,7 +353,7 @@ class PickerColumnCmp { var translateZ = 0; if (this.rotateFactor !== 0) { - translateX = 10; + translateX = 0; translateZ = 90; if (rotateX > 90 || rotateX < -90) { translateX = -9999; @@ -343,17 +364,50 @@ class PickerColumnCmp { translateY = optOffset; } - optEle.style[CSS.transform] = `rotateX(${rotateX}deg) translate3d(${translateX}px,${translateY}px,${translateZ}px)`; - - optEle.style[CSS.transitionDuration] = (duration > 0 ? duration + 'ms' : ''); - - optEle.classList[i === selectedIndex ? 'add' : 'remove']('picker-opt-selected'); - + opt._trans = `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.change.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++) { + var opt = this.col.options[i]; + if (!opt.disabled) { + if (i < min) { + min = i; + } + if (i > 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); + } } isPrevented(ev) { @@ -390,7 +444,7 @@ class PickerColumnCmp { '
' + '
' + '
' + - '
' + + '
' + '
' + '
' + '', @@ -401,6 +455,7 @@ class PickerColumnCmp { encapsulation: ViewEncapsulation.None, }) class PickerDisplayCmp { + @ViewChildren(PickerColumnCmp) private _cols: QueryList; private d: PickerOptions; private created: number; private lastClick: number; @@ -452,12 +507,13 @@ class PickerDisplayCmp { column.options = column.options.map(inputOpt => { let opt: PickerColumnOption = { text: '', - value: '' + value: '', + disabled: inputOpt.disabled, }; if (isPresent(inputOpt)) { if (isString(inputOpt) || isNumber(inputOpt)) { - opt.text = inputOpt; + opt.text = inputOpt.toString(); opt.value = inputOpt; } else { @@ -472,6 +528,18 @@ class PickerDisplayCmp { }); } + 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.change.emit(this.getSelected()); + } + @HostListener('body:keyup', ['$event']) private _keyUp(ev: KeyboardEvent) { if (this.isEnabled() && this._viewCtrl.isLast()) { @@ -518,7 +586,7 @@ class PickerDisplayCmp { 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 (button.handler(this.getSelected()) === false) { // if the return value of the handler is false then do not dismiss shouldDismiss = false; } @@ -538,17 +606,20 @@ class PickerDisplayCmp { } dismiss(role): Promise { - return this._viewCtrl.dismiss(this.getValues(), role); + return this._viewCtrl.dismiss(this.getSelected(), role); } - getValues() { - // this is an alert with text inputs - // return an object of all the values with the input name as the key - let values = {}; - this.d.columns.forEach(col => { - values[col.name] = col.selected ? col.selected.value : null; + getSelected(): any { + let selected = {}; + 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 values; + return selected; } isEnabled() { @@ -566,10 +637,10 @@ export interface PickerOptions { export interface PickerColumn { name?: string; - selected?: PickerColumnOption; + selectedIndex?: number; prefix?: string; suffix?: string; - options: PickerColumnOption[]; + options?: PickerColumnOption[]; cssClass?: string; columnWidth?: string; prefixWidth?: string; @@ -578,8 +649,9 @@ export interface PickerColumn { } export interface PickerColumnOption { + text?: string; value?: any; - text?: any; + disabled?: boolean; } diff --git a/ionic/components/picker/picker.wp.scss b/ionic/components/picker/picker.wp.scss index 85fdb18cf4..a1603409bc 100644 --- a/ionic/components/picker/picker.wp.scss +++ b/ionic/components/picker/picker.wp.scss @@ -15,15 +15,15 @@ $picker-wp-button-height: $picker-wp-toolbar-height !default $picker-wp-button-text-color: $link-wp-color !default; $picker-wp-button-background-color: transparent !default; -$picker-wp-column-padding: 0 12px !default; +$picker-wp-column-padding: 0 4px !default; -$picker-wp-option-padding: 0 10px !default; +$picker-wp-option-padding: 0 !default; $picker-wp-option-text-color: $list-wp-text-color !default; $picker-wp-option-font-size: 18px !default; $picker-wp-option-height: 42px !default; $picker-wp-option-offset-y: (($picker-wp-height - $picker-wp-toolbar-height) / 2) - ($picker-wp-option-height / 2) - 10 !default; -$picker-wp-option-selected-font-size: 24px !default; +$picker-wp-option-selected-font-size: 22px !default; $picker-wp-option-selected-color: $link-wp-color !default; $picker-highlight-opacity: .8 !default; @@ -110,8 +110,6 @@ $picker-highlight-opacity: .8 !default; margin: 0; padding: $picker-wp-option-padding; - width: calc(100% - 24px); - font-size: $picker-wp-option-font-size; line-height: $picker-wp-option-height; diff --git a/ionic/components/select/select.ts b/ionic/components/select/select.ts index 0daf458bd0..6a816e65bf 100644 --- a/ionic/components/select/select.ts +++ b/ionic/components/select/select.ts @@ -138,8 +138,8 @@ export class Select { private _labelId: string; private _multi: boolean = false; private _options: QueryList