diff --git a/src/components/action-sheet/action-sheet-component.ts b/src/components/action-sheet/action-sheet-component.ts index 0438a6c25a..b5aa9ad255 100644 --- a/src/components/action-sheet/action-sheet-component.ts +++ b/src/components/action-sheet/action-sheet-component.ts @@ -1,7 +1,7 @@ import { Component, Renderer, ElementRef, HostListener, ViewEncapsulation } from '@angular/core'; import { Config } from '../../config/config'; -import { Form } from '../../util/form'; +import { focusOutActiveElement } from '../../util/dom'; import { Key } from '../../util/key'; import { NavParams } from '../../navigation/nav-params'; import { ViewController } from '../../navigation/view-controller'; @@ -58,16 +58,15 @@ export class ActionSheetCmp { constructor( private _viewCtrl: ViewController, - private _config: Config, + config: Config, private _elementRef: ElementRef, - private _form: Form, gestureCtrl: GestureController, params: NavParams, renderer: Renderer ) { this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL); this.d = params.data; - this.mode = _config.get('mode'); + this.mode = config.get('mode'); renderer.setElementClass(_elementRef.nativeElement, `action-sheet-${this.mode}`, true); if (this.d.cssClass) { @@ -123,7 +122,7 @@ export class ActionSheetCmp { } ionViewDidEnter() { - this._form.focusOut(); + focusOutActiveElement(); let focusableEle = this._elementRef.nativeElement.querySelector('button'); if (focusableEle) { @@ -142,7 +141,7 @@ export class ActionSheetCmp { } } - click(button: any, dismissDelay?: number) { + click(button: any) { if (! this.enabled ) { return; } @@ -158,16 +157,14 @@ export class ActionSheetCmp { } if (shouldDismiss) { - setTimeout(() => { - this.dismiss(button.role); - }, dismissDelay || this._config.get('pageTransitionDelay')); + this.dismiss(button.role); } } bdClick() { if (this.enabled && this.d.enableBackdropDismiss) { if (this.d.cancelButton) { - this.click(this.d.cancelButton, 1); + this.click(this.d.cancelButton); } else { this.dismiss('backdrop'); diff --git a/src/components/alert/alert-component.ts b/src/components/alert/alert-component.ts index a10ae732f7..8e542e88a6 100644 --- a/src/components/alert/alert-component.ts +++ b/src/components/alert/alert-component.ts @@ -1,11 +1,14 @@ import { Component, ElementRef, HostListener, Renderer, ViewEncapsulation } from '@angular/core'; import { Config } from '../../config/config'; +import { focusOutActiveElement, NON_TEXT_INPUT_REGEX } from '../../util/dom'; +import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller'; import { isPresent, assert } from '../../util/util'; import { Key } from '../../util/key'; import { NavParams } from '../../navigation/nav-params'; +import { Platform } from '../../platform/platform'; import { ViewController } from '../../navigation/view-controller'; -import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller'; + /** * @private @@ -91,21 +94,22 @@ export class AlertCmp { constructor( public _viewCtrl: ViewController, public _elementRef: ElementRef, - public _config: Config, + config: Config, gestureCtrl: GestureController, params: NavParams, - renderer: Renderer + private _renderer: Renderer, + private _platform: Platform ) { // gesture blocker is used to disable gestures dynamically this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL); this.d = params.data; - this.mode = _config.get('mode'); - renderer.setElementClass(_elementRef.nativeElement, `alert-${this.mode}`, true); + this.mode = config.get('mode'); + _renderer.setElementClass(_elementRef.nativeElement, `alert-${this.mode}`, true); if (this.d.cssClass) { this.d.cssClass.split(' ').forEach(cssClass => { // Make sure the class isn't whitespace, otherwise it throws exceptions - if (cssClass.trim() !== '') renderer.setElementClass(_elementRef.nativeElement, cssClass, true); + if (cssClass.trim() !== '') _renderer.setElementClass(_elementRef.nativeElement, cssClass, true); }); } @@ -131,7 +135,7 @@ export class AlertCmp { ionViewDidLoad() { // normalize the data - let data = this.d; + const data = this.d; data.buttons = data.buttons.map(button => { if (typeof button === 'string') { @@ -149,7 +153,7 @@ export class AlertCmp { label: input.label, checked: !!input.checked, disabled: !!input.disabled, - id: 'alert-input-' + this.id + '-' + index, + id: `alert-input-${this.id}-${index}`, handler: isPresent(input.handler) ? input.handler : null, }; }); @@ -157,7 +161,7 @@ export class AlertCmp { // An alert can be created with several different inputs. Radios, // checkboxes and inputs are all accepted, but they cannot be mixed. - let inputTypes: any[] = []; + const inputTypes: string[] = []; data.inputs.forEach(input => { if (inputTypes.indexOf(input.type) < 0) { inputTypes.push(input.type); @@ -165,15 +169,24 @@ export class AlertCmp { }); if (inputTypes.length > 1 && (inputTypes.indexOf('checkbox') > -1 || inputTypes.indexOf('radio') > -1)) { - console.warn('Alert cannot mix input types: ' + (inputTypes.join('/')) + '. Please see alert docs for more info.'); + console.warn(`Alert cannot mix input types: ${(inputTypes.join('/'))}. Please see alert docs for more info.`); } this.inputType = inputTypes.length ? inputTypes[0] : null; - let checkedInput = this.d.inputs.find(input => input.checked); + const checkedInput = this.d.inputs.find(input => input.checked); if (checkedInput) { this.activeId = checkedInput.id; } + + const hasTextInput = (this.d.inputs.length && this.d.inputs.some(i => !(NON_TEXT_INPUT_REGEX.test(i.type)))); + if (hasTextInput && this._platform.is('mobile')) { + // this alert has a text input and it's on a mobile device so we should align + // the alert up high because we need to leave space for the virtual keboard + // this also helps prevent the layout getting all messed up from + // the browser trying to scroll the input into a safe area + this._renderer.setElementClass(this._elementRef.nativeElement, 'alert-top', true); + } } ionViewWillEnter() { @@ -185,12 +198,14 @@ export class AlertCmp { } ionViewDidEnter() { - let activeElement: any = document.activeElement; - if (document.activeElement) { - activeElement.blur(); - } + // focus out of the active element + focusOutActiveElement(); - let focusableEle = this._elementRef.nativeElement.querySelector('input,button'); + // set focus on the first input or button in the alert + // note that this does not always work and bring up the keyboard on + // devices since the focus command must come from the user's touch event + // and ionViewDidEnter is not in the same callstack as the touch event :( + const focusableEle = this._elementRef.nativeElement.querySelector('input,button'); if (focusableEle) { focusableEle.focus(); } @@ -206,19 +221,19 @@ export class AlertCmp { // this can happen when the button has focus and used the enter // key to click the button. However, both the click handler and // this keyup event will fire, so only allow one of them to go. - console.debug('alert, enter button'); + console.debug(`alert, enter button`); let button = this.d.buttons[this.d.buttons.length - 1]; this.btnClick(button); } } else if (ev.keyCode === Key.ESCAPE) { - console.debug('alert, escape button'); + console.debug(`alert, escape button`); this.bdClick(); } } } - btnClick(button: any, dismissDelay?: number) { + btnClick(button: any) { if (!this.enabled) { return; } @@ -238,9 +253,8 @@ export class AlertCmp { } if (shouldDismiss) { - setTimeout(() => { - this.dismiss(button.role); - }, dismissDelay || this._config.get('pageTransitionDelay')); + this.dismiss(button.role); + focusOutActiveElement(); } } @@ -271,7 +285,7 @@ export class AlertCmp { if (this.enabled && this.d.enableBackdropDismiss) { let cancelBtn = this.d.buttons.find(b => b.role === 'cancel'); if (cancelBtn) { - this.btnClick(cancelBtn, 1); + this.btnClick(cancelBtn); } else { this.dismiss('backdrop'); @@ -280,14 +294,15 @@ export class AlertCmp { } dismiss(role: any): Promise { + focusOutActiveElement(); return this._viewCtrl.dismiss(this.getValues(), role); } - getValues() { + getValues(): any { if (this.inputType === 'radio') { // this is an alert with radio buttons (single value select) // return the one value which is checked, otherwise undefined - let checkedInput = this.d.inputs.find(i => i.checked); + const checkedInput = this.d.inputs.find(i => i.checked); return checkedInput ? checkedInput.value : undefined; } @@ -299,7 +314,7 @@ export class AlertCmp { // this is an alert with text inputs // return an object of all the values with the input name as the key - let values: {[k: string]: string} = {}; + const values: {[k: string]: string} = {}; this.d.inputs.forEach(i => { values[i.name] = i.value; }); diff --git a/src/components/alert/alert.scss b/src/components/alert/alert.scss index b77584fc71..146668c287 100644 --- a/src/components/alert/alert.scss +++ b/src/components/alert/alert.scss @@ -24,6 +24,12 @@ ion-alert { justify-content: center; } +ion-alert.alert-top { + align-items: flex-start; + + padding-top: 50px; +} + ion-alert input { width: 100%; } diff --git a/src/components/infinite-scroll/infinite-scroll.ts b/src/components/infinite-scroll/infinite-scroll.ts index 63505b215d..dbe7877142 100644 --- a/src/components/infinite-scroll/infinite-scroll.ts +++ b/src/components/infinite-scroll/infinite-scroll.ts @@ -35,7 +35,7 @@ import { Content } from '../content/content'; * items = []; * * constructor() { - * for (var i = 0; i < 30; i++) { + * for (let i = 0; i < 30; i++) { * this.items.push( this.items.length ); * } * } @@ -44,7 +44,7 @@ import { Content } from '../content/content'; * console.log('Begin async operation'); * * setTimeout(() => { - * for (var i = 0; i < 30; i++) { + * for (let i = 0; i < 30; i++) { * this.items.push( this.items.length ); * } * diff --git a/src/components/picker/picker-component.ts b/src/components/picker/picker-component.ts index 30d277f525..c42b4bc1a7 100644 --- a/src/components/picker/picker-component.ts +++ b/src/components/picker/picker-component.ts @@ -461,14 +461,14 @@ export class PickerCmp { constructor( private _viewCtrl: ViewController, private _elementRef: ElementRef, - private _config: Config, + config: Config, gestureCtrl: GestureController, params: NavParams, renderer: Renderer ) { this._gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL); this.d = params.data; - this.mode = _config.get('mode'); + this.mode = config.get('mode'); renderer.setElementClass(_elementRef.nativeElement, `picker-${this.mode}`, true); if (this.d.cssClass) { @@ -579,7 +579,7 @@ export class PickerCmp { this.enabled = true; } - btnClick(button: any, dismissDelay?: number) { + btnClick(button: any) { if (!this.enabled) { return; } @@ -599,9 +599,7 @@ export class PickerCmp { } if (shouldDismiss) { - setTimeout(() => { - this.dismiss(button.role); - }, dismissDelay || this._config.get('pageTransitionDelay')); + this.dismiss(button.role); } } diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index f0d8724b31..47d69874b2 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -137,7 +137,7 @@ import { ViewController } from '../../navigation/view-controller'; * *```ts * switchTabs() { - * this.navCtrl.parent.switch(2); + * this.navCtrl.parent.select(2); * } *``` * @demo /docs/v2/demos/src/tabs/ diff --git a/src/config/config.ts b/src/config/config.ts index 514a187aef..49cd66e634 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -108,15 +108,14 @@ import { isObject, isDefined, isFunction, isArray } from '../util/util'; * | `menuType` | `string` | Type of menu to display. Available options: `"overlay"`, `"reveal"`, `"push"`. | * | `modalEnter` | `string` | The name of the transition to use while a modal is presented. | * | `modalLeave` | `string` | The name of the transition to use while a modal is dismiss. | - * | `mode` | `string` | The mode to use throughout the application. | + * | `mode` | `string` | The mode to use throughout the application. | * | `pageTransition` | `string` | The name of the transition to use while changing pages. | - * | `pageTransitionDelay` | `number` | The delay in milliseconds before the transition starts while changing pages. | * | `pickerEnter` | `string` | The name of the transition to use while a picker is presented. | * | `pickerLeave` | `string` | The name of the transition to use while a picker is dismissed. | * | `popoverEnter` | `string` | The name of the transition to use while a popover is presented. | * | `popoverLeave` | `string` | The name of the transition to use while a popover is dismissed. | * | `spinner` | `string` | The default spinner to use when a name is not defined. | - * | `swipeBackEnabled` | `boolean` | Whether native iOS swipe to go back functionality is enabled. + * | `swipeBackEnabled` | `boolean` | Whether native iOS swipe to go back functionality is enabled. | * | `tabsHighlight` | `boolean` | Whether to show a highlight line under the tab when it is selected. | * | `tabsLayout` | `string` | The layout to use for all tabs. Available options: `"icon-top"`, `"icon-left"`, `"icon-right"`, `"icon-bottom"`, `"icon-hide"`, `"title-hide"`. | * | `tabsPlacement` | `string` | The position of the tabs relative to the content. Available options: `"top"`, `"bottom"` | diff --git a/src/config/mode-registry.ts b/src/config/mode-registry.ts index 2bce29f4f2..38937f8171 100644 --- a/src/config/mode-registry.ts +++ b/src/config/mode-registry.ts @@ -24,7 +24,6 @@ export const MODE_IOS: any = { modalLeave: 'modal-slide-out', pageTransition: 'ios-transition', - pageTransitionDelay: 16, pickerEnter: 'picker-slide-in', pickerLeave: 'picker-slide-out', @@ -68,7 +67,6 @@ export const MODE_MD: any = { modalLeave: 'modal-md-slide-out', pageTransition: 'md-transition', - pageTransitionDelay: 64, pickerEnter: 'picker-slide-in', pickerLeave: 'picker-slide-out', @@ -112,7 +110,6 @@ export const MODE_WP: any = { modalLeave: 'modal-md-slide-out', pageTransition: 'wp-transition', - pageTransitionDelay: 96, pickerEnter: 'picker-slide-in', pickerLeave: 'picker-slide-out', diff --git a/src/util/datetime-util.ts b/src/util/datetime-util.ts index c8ad95c101..13efb5cc28 100644 --- a/src/util/datetime-util.ts +++ b/src/util/datetime-util.ts @@ -278,7 +278,7 @@ export function updateDate(existingData: DateTimeData, newData: any) { export function parseTemplate(template: string): string[] { - let formats: string[] = []; + const formats: string[] = []; template = template.replace(/[^\w\s]/gi, ' '); @@ -288,17 +288,19 @@ export function parseTemplate(template: string): string[] { } }); - let words = template.split(' ').filter(w => w.length > 0); + const words = template.split(' ').filter(w => w.length > 0); words.forEach((word, i) => { FORMAT_KEYS.forEach(format => { if (word === format.f) { if (word === FORMAT_A || word === FORMAT_a) { // this format is an am/pm format, so it's an "a" or "A" + console.log(`word: ${word}, words[i - 1]: ${words[i - 1]}`); + if ((formats.indexOf(FORMAT_h) < 0 && formats.indexOf(FORMAT_hh) < 0) || - (words[i - 1] !== FORMAT_m && words[i - 1] !== FORMAT_mm)) { + VALID_AMPM_PREFIX.indexOf(words[i - 1]) === -1) { // template does not already have a 12-hour format - // or this am/pm format doesn't have a minute format immediately before it - // so do not treat this word "a" or "A" as an am/pm format + // or this am/pm format doesn't have a hour, minute, or second format immediately before it + // so do not treat this word "a" or "A" as the am/pm format return; } } @@ -515,3 +517,7 @@ const MONTH_SHORT_NAMES = [ 'Nov', 'Dec', ]; + +const VALID_AMPM_PREFIX = [ + FORMAT_hh, FORMAT_h, FORMAT_mm, FORMAT_m, FORMAT_ss, FORMAT_s +]; diff --git a/src/util/dom.ts b/src/util/dom.ts index a3d7d8a43c..23eee9fa52 100644 --- a/src/util/dom.ts +++ b/src/util/dom.ts @@ -251,9 +251,11 @@ export function isTextInput(ele: any) { return !!ele && (ele.tagName === 'TEXTAREA' || ele.contentEditable === 'true' || - (ele.tagName === 'INPUT' && !(/^(radio|checkbox|range|file|submit|reset|color|image|button)$/i).test(ele.type))); + (ele.tagName === 'INPUT' && !(NON_TEXT_INPUT_REGEX.test(ele.type)))); } +export const NON_TEXT_INPUT_REGEX = /^(radio|checkbox|range|file|submit|reset|color|image|button)$/i; + export function hasFocusedTextInput() { const ele = document.activeElement; if (isTextInput(ele)) { @@ -262,6 +264,11 @@ export function hasFocusedTextInput() { return false; } +export function focusOutActiveElement() { + const activeElement = document.activeElement; + activeElement && activeElement.blur && activeElement.blur(); +} + const skipInputAttrsReg = /^(value|checked|disabled|type|class|style|id|autofocus|autocomplete|autocorrect)$/i; export function copyInputAttributes(srcElement: HTMLElement, destElement: HTMLElement) { // copy attributes from one element to another diff --git a/src/util/form.ts b/src/util/form.ts index bde732a4c1..120b73def1 100644 --- a/src/util/form.ts +++ b/src/util/form.ts @@ -24,11 +24,6 @@ export class Form { } } - focusOut() { - const activeElement = document.activeElement; - activeElement && activeElement.blur && activeElement.blur(); - } - setAsFocused(input: any) { this._focused = input; } diff --git a/src/util/keyboard.ts b/src/util/keyboard.ts index 8b347d761d..7ac5a6fc54 100644 --- a/src/util/keyboard.ts +++ b/src/util/keyboard.ts @@ -1,8 +1,7 @@ import { Injectable, NgZone } from '@angular/core'; import { Config } from '../config/config'; -import { Form } from './form'; -import { hasFocusedTextInput, nativeRaf, nativeTimeout, zoneRafFrames } from './dom'; +import { focusOutActiveElement, hasFocusedTextInput, nativeRaf, nativeTimeout, zoneRafFrames } from './dom'; import { Key } from './key'; @@ -24,7 +23,7 @@ import { Key } from './key'; @Injectable() export class Keyboard { - constructor(config: Config, private _form: Form, private _zone: NgZone) { + constructor(config: Config, private _zone: NgZone) { _zone.runOutsideAngular(() => { this.focusOutline(config.get('focusOutline'), document); @@ -33,7 +32,7 @@ export class Keyboard { // useful when the virtual keyboard is closed natively // https://github.com/driftyco/ionic-plugin-keyboard if (hasFocusedTextInput()) { - this._form.focusOut(); + focusOutActiveElement(); } }); }); @@ -117,7 +116,7 @@ export class Keyboard { nativeRaf(() => { if (hasFocusedTextInput()) { // only focus out when a text input has focus - this._form.focusOut(); + focusOutActiveElement(); } }); } diff --git a/src/util/mock-providers.ts b/src/util/mock-providers.ts index f8730ef9af..166bd753e6 100644 --- a/src/util/mock-providers.ts +++ b/src/util/mock-providers.ts @@ -6,7 +6,6 @@ import { App } from '../components/app/app'; import { IonicApp } from '../components/app/app-root'; import { Config } from '../config/config'; import { DeepLinker } from '../navigation/deep-linker'; -import { Form } from './form'; import { GestureController } from '../gestures/gesture-controller'; import { Keyboard } from './keyboard'; import { Menu } from '../components/menu/menu'; @@ -231,11 +230,9 @@ export const mockNavController = function(): NavControllerBase { let app = mockApp(config, platform); - let form = new Form(); - let zone = mockZone(); - let keyboard = new Keyboard(config, form, zone); + let keyboard = new Keyboard(config, zone); let elementRef = mockElementRef(); @@ -281,11 +278,9 @@ export const mockNavController = function(): NavControllerBase { }; export const mockOverlayPortal = function(app: App, config: Config, platform: Platform): OverlayPortal { - let form = new Form(); - let zone = mockZone(); - let keyboard = new Keyboard(config, form, zone); + let keyboard = new Keyboard(config, zone); let elementRef = mockElementRef(); @@ -323,11 +318,9 @@ export const mockTab = function(parentTabs: Tabs): Tab { let app = (parentTabs)._app || mockApp(config, platform); - let form = new Form(); - let zone = mockZone(); - let keyboard = new Keyboard(config, form, zone); + let keyboard = new Keyboard(config, zone); let elementRef = mockElementRef(); diff --git a/src/util/test/datetime-util.spec.ts b/src/util/test/datetime-util.spec.ts index f42acf5080..850a145318 100644 --- a/src/util/test/datetime-util.spec.ts +++ b/src/util/test/datetime-util.spec.ts @@ -321,6 +321,13 @@ describe('parseTemplate', () => { expect(formats[2]).toEqual('a'); }); + it('should allow am/pm when using only 12-hour', () => { + var formats = datetime.parseTemplate('hh a'); + expect(formats.length).toEqual(2); + expect(formats[0]).toEqual('hh'); + expect(formats[1]).toEqual('a'); + }); + it('should allow am/pm when using 12-hour', () => { var formats = datetime.parseTemplate('hh:mm a'); expect(formats.length).toEqual(3); @@ -329,7 +336,7 @@ describe('parseTemplate', () => { expect(formats[2]).toEqual('a'); }); - it('should not add am/pm when not using 24-hour', () => { + it('should not add am/pm when using 24-hour', () => { var formats = datetime.parseTemplate('HH:mm a'); expect(formats.length).toEqual(2); expect(formats[0]).toEqual('HH');