diff --git a/src/components/checkbox/checkbox.ts b/src/components/checkbox/checkbox.ts index 86377f4c0c4..4ddd2b177d5 100644 --- a/src/components/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox.ts @@ -90,13 +90,6 @@ export class Checkbox extends BaseInput implements IonicTapInput, OnDes super(config, elementRef, renderer, 'checkbox', false, form, item, null); } - /** - * @hidden - */ - initFocus() { - this._elementRef.nativeElement.querySelector('button').focus(); - } - /** * @hidden */ @@ -117,8 +110,8 @@ export class Checkbox extends BaseInput implements IonicTapInput, OnDes /** * @hidden */ - _inputCheckHasValue(val: boolean) { - this._item && this._item.setElementClass('item-checkbox-checked', val); + _inputUpdated() { + this._item && this._item.setElementClass('item-checkbox-checked', this._value); } } diff --git a/src/components/input/input.ios.scss b/src/components/input/input.ios.scss index 4b0f8be8154..8c6e5b38768 100644 --- a/src/components/input/input.ios.scss +++ b/src/components/input/input.ios.scss @@ -52,7 +52,7 @@ $text-input-ios-highlight-color-invalid: $text-input-highlight-color-invalid ! // iOS Default Input // -------------------------------------------------- -.text-input-ios { +.input-ios .text-input { margin: $text-input-ios-margin-top $text-input-ios-margin-right $text-input-ios-margin-bottom $text-input-ios-margin-left; padding: 0; diff --git a/src/components/input/input.md.scss b/src/components/input/input.md.scss index 8caebfce061..75a171d2eb0 100644 --- a/src/components/input/input.md.scss +++ b/src/components/input/input.md.scss @@ -52,7 +52,7 @@ $text-input-md-highlight-color-invalid: $text-input-highlight-color-invalid // Material Design Default Input // -------------------------------------------------- -.text-input-md { +.input-md .text-input { margin: $text-input-md-margin-top $text-input-md-margin-right $text-input-md-margin-bottom $text-input-md-margin-left; padding: 0; diff --git a/src/components/input/input.ts b/src/components/input/input.ts index d6bbe6c8a17..e654cc729b5 100644 --- a/src/components/input/input.ts +++ b/src/components/input/input.ts @@ -1,19 +1,19 @@ import { Component, Optional, ElementRef, EventEmitter, Input, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core'; import { NgControl } from '@angular/forms'; +import { Subject } from 'rxjs/Subject'; +import 'rxjs/add/operator/takeUntil'; + import { App } from '../app/app'; import { Config } from '../../config/config'; import { Content, ContentDimensions, ScrollEvent } from '../content/content'; -import { copyInputAttributes, PointerCoordinates, hasPointerMoved, pointerCoord } from '../../util/dom'; +import { PointerCoordinates, hasPointerMoved, pointerCoord } from '../../util/dom'; import { DomController } from '../../platform/dom-controller'; import { Form, IonicFormInput } from '../../util/form'; -import { Ion } from '../ion'; -import { isString, isTrueProperty } from '../../util/util'; +import { BaseInput } from '../../util/base-input'; +import { UIEventManager } from '../../gestures/ui-event-manager'; +import { isString, isTrueProperty, assert } from '../../util/util'; import { Item } from '../item/item'; -import { NativeInput } from './native-input'; -import { NextInput } from './next-input'; -import { NavController } from '../../navigation/nav-controller'; -import { NavControllerBase } from '../../navigation/nav-controller-base'; import { Platform } from '../../platform/platform'; @@ -84,92 +84,42 @@ import { Platform } from '../../platform/platform'; @Component({ selector: 'ion-input,ion-textarea', template: - '' + - '' + - '' + - '' + - '
', + '' + + + '' + + + '' + + '', encapsulation: ViewEncapsulation.None, }) -export class TextInput extends Ion implements IonicFormInput { - _autoComplete: string; - _autoCorrect: string; +export class TextInput extends BaseInput implements IonicFormInput { + _autoFocusAssist: string; _clearInput: boolean = false; _clearOnEdit: boolean; - _coord: PointerCoordinates; _didBlurAfterEdit: boolean; - _disabled: boolean = false; _readonly: boolean = false; - _isTouch: boolean; _keyboardHeight: number; - _min: any; - _max: any; - _step: any; - _native: NativeInput; - _nav: NavControllerBase; - _scrollStart: any; - _scrollEnd: any; _type: string = 'text'; - _useAssist: boolean; - _usePadding: boolean; - _value: any = ''; - - /** @hidden */ - inputControl: NgControl; - - constructor( - config: Config, - private _plt: Platform, - private _form: Form, - private _app: App, - elementRef: ElementRef, - renderer: Renderer, - @Optional() private _content: Content, - @Optional() private _item: Item, - @Optional() nav: NavController, - @Optional() public ngControl: NgControl, - private _dom: DomController - ) { - super(config, elementRef, renderer, 'input'); - - this._nav = nav; - - this._autoFocusAssist = config.get('autoFocusAssist', 'delay'); - this._autoComplete = config.get('autocomplete', 'off'); - this._autoCorrect = config.get('autocorrect', 'off'); - this._keyboardHeight = config.getNumber('keyboardHeight'); - this._useAssist = config.getBoolean('scrollAssist', false); - this._usePadding = config.getBoolean('scrollPadding', this._useAssist); - - if (elementRef.nativeElement.tagName === 'ION-TEXTAREA') { - this._type = TEXTAREA; - } - - if (ngControl) { - ngControl.valueAccessor = this; - this.inputControl = ngControl; - } - - _form.register(this); - - // only listen to content scroll events if there is content - if (_content) { - this._scrollStart = _content.ionScrollStart.subscribe((ev: ScrollEvent) => { - this.scrollHideFocus(ev, true); - }); - this._scrollEnd = _content.ionScrollEnd.subscribe((ev: ScrollEvent) => { - this.scrollHideFocus(ev, false); - }); - } - - this.mode = config.get('mode'); - } - - /** - * @input {string} Instructional text that shows before the input has a value. - */ - @Input() placeholder: string = ''; + _scrollData: ScrollData; + _isTextarea: boolean = false; + _clone: boolean; + _onDestroy: Subject = new Subject(); /** * @input {boolean} If true, a clear icon will appear in the input when there is a value. Clicking it clears the input. @@ -179,19 +129,7 @@ export class TextInput extends Ion implements IonicFormInput { return this._clearInput; } set clearInput(val: any) { - this._clearInput = (this._type !== TEXTAREA && isTrueProperty(val)); - } - - /** - * @input {string} The text value of the input. - */ - @Input() - get value() { - return this._value; - } - set value(val: any) { - this._value = val; - this.checkHasValue(val); + this._clearInput = (!this._isTextarea && isTrueProperty(val)); } /** @@ -202,46 +140,18 @@ export class TextInput extends Ion implements IonicFormInput { return this._type; } set type(val: any) { - if (this._type !== TEXTAREA) { - this._type = 'text'; - - if (isString(val)) { - val = val.toLowerCase(); - - if (TEXT_TYPE_REGEX.test(val)) { - this._type = val; - } + if (this._isTextarea) { + return; + } + this._type = 'text'; + if (isString(val)) { + val = val.toLowerCase(); + if (ALLOWED_TYPES.indexOf(val) >= 0) { + this._type = val; } } } - /** - * @input {boolean} If true, the user cannot interact with this element. - */ - @Input() - get disabled(): boolean { - return this._disabled; - } - set disabled(val: boolean) { - this.setDisabled(this._disabled = isTrueProperty(val)); - } - - /** - * @hidden - */ - setDisabled(val: boolean) { - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'disabled', val ? '' : null); - this._item && this._item.setElementClass('item-input-disabled', val); - this._native && this._native.isDisabled(val); - } - - /** - * @hidden - */ - setDisabledState(isDisabled: boolean) { - this.disabled = isDisabled; - } - /** * @input {boolean} If true, the user cannot modify the value. */ @@ -264,524 +174,414 @@ export class TextInput extends Ion implements IonicFormInput { this._clearOnEdit = isTrueProperty(val); } - /** - * @input {any} The minimum value, which must not be greater than its maximum (max attribute) value. - */ - @Input() - get min() { - return this._min; - } - set min(val: any) { - this.setMin(this._min = val); - } - /** * @hidden */ - setMin(val: any) { - this._native && this._native.setMin(val); - } + @ViewChild('input') _native: ElementRef; /** - * @input {any} The maximum value, which must not be less than its minimum (min attribute) value. + * @input {string} Instructional text that shows before the input has a value. */ - @Input() - get max() { - return this._max; - } - set max(val: any) { - this.setMax(this._max = val); - } + @Input() autocomplete: string = ''; /** - * @hidden + * @input {string} Instructional text that shows before the input has a value. */ - setMax(val: any) { - this._native && this._native.setMax(val); - } + @Input() autocorrect: string = ''; /** - * @input {any} Works with the min and max attributes to limit the increments at which a value can be set. + * @input {string} Instructional text that shows before the input has a value. */ - @Input() - get step() { - return this._step; - } - set step(val: any) { - this.setStep(this._step = val); - } + @Input() placeholder: string = ''; /** - * @hidden + * @input {any} The minimum value, which must not be greater than its maximum (max attribute) value. */ - setStep(val: any) { - this._native && this._native.setStep(val); - } + @Input() min: number|string; /** - * @hidden + * @input {any} The maximum value, which must not be less than its minimum (min attribute) value. */ - @ViewChild('input', { read: NativeInput }) - set _nativeInput(nativeInput: NativeInput) { - if (this.type !== TEXTAREA) { - this.setNativeInput(nativeInput); - } - } + @Input() max: number|string; /** - * @hidden + * @input {any} Works with the min and max attributes to limit the increments at which a value can be set. */ - @ViewChild('textarea', { read: NativeInput }) - set _nativeTextarea(nativeInput: NativeInput) { - if (this.type === TEXTAREA) { - this.setNativeInput(nativeInput); + @Input() step: number|string; + + + constructor( + config: Config, + private _plt: Platform, + private form: Form, + private _app: App, + elementRef: ElementRef, + renderer: Renderer, + @Optional() private _content: Content, + @Optional() private item: Item, + @Optional() public ngControl: NgControl, + private _dom: DomController + ) { + super(config, elementRef, renderer, + elementRef.nativeElement.tagName === 'ION-TEXTAREA' ? 'textarea' : 'input', '', form, item, ngControl); + + this.autocomplete = config.get('autocomplete', 'off'); + this.autocorrect = config.get('autocorrect', 'off'); + this._autoFocusAssist = config.get('autoFocusAssist', 'delay'); + this._keyboardHeight = config.getNumber('keyboardHeight'); + this._isTextarea = elementRef.nativeElement.tagName === 'ION-TEXTAREA'; + + const useAssist = config.getBoolean('scrollAssist', false); + if (useAssist) { + this._enableScrollAssist(); } - } - /** - * @hidden - */ - @ViewChild(NextInput) - set _nextInput(nextInput: NextInput) { - if (nextInput) { - nextInput.focused.subscribe(() => { - this._form.tabFocus(this); - }); + const usePadding = config.getBoolean('scrollPadding', useAssist); + if (usePadding && _content) { + this._enableScrollPadding(); } - } - /** - * @output {event} Emitted when the input no longer has focus. - */ - @Output() blur: EventEmitter = new EventEmitter(); + const blurring = config.getBoolean('inputBlurring', false); + if (blurring) { + this._enableInputBlurring(); + } - /** - * @output {event} Emitted when the input has focus. - */ - @Output() focus: EventEmitter = new EventEmitter(); + const blurOnScroll = config.getBoolean('blurOnFocus', false); + if (blurOnScroll && _content) { + this._enableBlurOnScrolling(); + } + } /** * @hidden */ - setNativeInput(nativeInput: NativeInput) { - this._native = nativeInput; - nativeInput.setValue(this._value); - nativeInput.setMin(this._min); - nativeInput.setMax(this._max); - nativeInput.setStep(this._step); - nativeInput.isDisabled(this.disabled); - - if (this._item && this._item.labelId !== null) { - nativeInput.labelledBy(this._item.labelId); + ngOnInit() { + // By default, password inputs clear after focus when they have content + if (this.clearOnEdit !== false && this.type === 'password') { + this.clearOnEdit = true; } - - nativeInput.valueChange.subscribe((inputValue: any) => { - this.onChange(inputValue); - this.checkHasValue(inputValue); - }); - - nativeInput.keydown.subscribe((inputValue: any) => { - this.onKeydown(inputValue); - }); - - this.focusChange(this.hasFocus()); - nativeInput.focusChange.subscribe((textInputHasFocus: any) => { - this.focusChange(textInputHasFocus); - this.checkHasValue(nativeInput.getValue()); - if (!textInputHasFocus) { - this.onTouched(textInputHasFocus); - } - }); - - this.checkHasValue(nativeInput.getValue()); - - var ionInputEle: HTMLElement = this._elementRef.nativeElement; - var nativeInputEle: HTMLElement = nativeInput.element(); - - // copy ion-input attributes to the native input element - copyInputAttributes(ionInputEle, nativeInputEle); + const ionInputEle: HTMLElement = this._elementRef.nativeElement; if (ionInputEle.hasAttribute('autofocus')) { // the ion-input element has the autofocus attributes + const nativeInputEle: HTMLElement = this._native.nativeElement; ionInputEle.removeAttribute('autofocus'); - - if (this._autoFocusAssist === 'immediate') { - // config says to immediate focus on the input - // works best on android devices - nativeInputEle.focus(); - - } else if (this._autoFocusAssist === 'delay') { - // config says to chill out a bit and focus on the input after transitions - // works best on desktop - this._plt.timeout(() => { + switch (this._autoFocusAssist) { + case 'immediate': + // config says to immediate focus on the input + // works best on android devices nativeInputEle.focus(); - }, 650); + break; + case 'delay': + // config says to chill out a bit and focus on the input after transitions + // works best on desktop + this._plt.timeout(() => nativeInputEle.focus(), 650); + break; } - // traditionally iOS has big issues with autofocus on actual devices // autoFocus is disabled by default with the iOS mode config } - - // by default set autocomplete="off" unless specified by the input - if (ionInputEle.hasAttribute('autocomplete')) { - this._autoComplete = ionInputEle.getAttribute('autocomplete'); - } - nativeInputEle.setAttribute('autocomplete', this._autoComplete); - - // by default set autocorrect="off" unless specified by the input - if (ionInputEle.hasAttribute('autocorrect')) { - this._autoCorrect = ionInputEle.getAttribute('autocorrect'); - } - nativeInputEle.setAttribute('autocorrect', this._autoCorrect); } /** * @hidden */ - initFocus() { - // begin the process of setting focus to the inner input element - const app = this._app; - const content = this._content; - const nav = this._nav; - const nativeInput = this._native; - - console.debug(`input-base, initFocus(), scrollView: ${!!content}`); - - if (content) { - // this input is inside of a scroll view - // find out if text input should be manually scrolled into view - - // get container of this input, probably an ion-item a few nodes up - var ele: HTMLElement = this._elementRef.nativeElement; - ele = ele.closest('ion-item,[ion-item]') || ele; - - var scrollData = getScrollData(ele.offsetTop, ele.offsetHeight, content.getContentDimensions(), this._keyboardHeight, this._plt.height()); - if (Math.abs(scrollData.scrollAmount) < 4) { - // the text input is in a safe position that doesn't - // require it to be scrolled into view, just set focus now - this.setFocus(); - - // all good, allow clicks again - app.setEnabled(true); - nav && nav.setTransitioning(false); - - if (this._usePadding) { - content.clearScrollPaddingFocusOut(); - } - return; - } - - if (this._usePadding) { - // add padding to the bottom of the scroll view (if needed) - content.addScrollPadding(scrollData.scrollPadding); - } - - // manually scroll the text input to the top - // do not allow any clicks while it's scrolling - var scrollDuration = getScrollAssistDuration(scrollData.scrollAmount); - app.setEnabled(false, scrollDuration); - nav && nav.setTransitioning(true); - - // temporarily move the focus to the focus holder so the browser - // doesn't freak out while it's trying to get the input in place - // at this point the native text input still does not have focus - nativeInput.beginFocus(true, scrollData.inputSafeY); - - // scroll the input into place - content.scrollTo(0, scrollData.scrollTo, scrollDuration, () => { - console.debug(`input-base, scrollTo completed, scrollTo: ${scrollData.scrollTo}, scrollDuration: ${scrollDuration}`); - // the scroll view is in the correct position now - // give the native text input focus - nativeInput.beginFocus(false, 0); - - // ensure this is the focused input - this.setFocus(); - - // all good, allow clicks again - app.setEnabled(true); - nav && nav.setTransitioning(false); - - if (this._usePadding) { - content.clearScrollPaddingFocusOut(); - } - }); - - } else { - // not inside of a scroll view, just focus it - this.setFocus(); - } + ngOnDestroy() { + super.ngOnDestroy(); + this._onDestroy.next(true); + this._onDestroy = null; } /** * @hidden */ - setFocus() { - // immediately set focus - this._form.setAsFocused(this); - - // set focus on the actual input element - console.debug(`input-base, setFocus ${this._native.element().value}`); - this._native.setFocus(); - - // ensure the body hasn't scrolled down - this._dom.write(() => { - this._plt.doc().body.scrollTop = 0; - }); + initFocus() { + this.setFocus(); } /** * @hidden */ - scrollHideFocus(ev: ScrollEvent, shouldHideFocus: boolean) { - // do not continue if there's no nav, or it's transitioning - if (this._nav && this.hasFocus()) { - // if it does have focus, then do the dom write - this._dom.write(() => { - this._native.hideFocus(shouldHideFocus); - }); + setFocus() { + // let's set focus to the element + // but only if it does not already have focus + if (this.isFocus()) { + this._native.nativeElement.focus(); } } - /** - * @hidden - */ - inputBlurred(ev: UIEvent) { - this.blur.emit(ev); - } /** * @hidden */ - inputFocused(ev: UIEvent) { - this.focus.emit(ev); + onKeydown(ev: any) { + if (ev && this._clearOnEdit) { + this.checkClearOnEdit(ev.target.value); + } } /** * @hidden */ - writeValue(val: any) { - this._value = val; - this.checkHasValue(val); + _inputFocusChanged(hasFocus: boolean) { + if (this._item) { + this._item.setElementClass('input-has-focus', hasFocus); + } + + // If clearOnEdit is enabled and the input blurred but has a value, set a flag + if (this._clearOnEdit && !hasFocus && this.hasValue()) { + this._didBlurAfterEdit = true; + } } /** * @hidden */ - onChange(val: any) { - this.checkHasValue(val); + clearTextInput() { + this.value = ''; } /** - * @hidden - */ - onKeydown(val: any) { - if (this._clearOnEdit) { - this.checkClearOnEdit(val); + * Check if we need to clear the text input if clearOnEdit is enabled + * @hidden + */ + checkClearOnEdit(inputValue: string) { + if (!this._clearOnEdit) { + return; } - } - /** - * @hidden - */ - onTouched(val: any) {} + // Did the input value change after it was blurred and edited? + if (this._didBlurAfterEdit && this.hasValue()) { + // Clear the input + this.clearTextInput(); + } - /** - * @hidden - */ - hasFocus(): boolean { - // check if an input has focus or not - return this._plt.hasFocus(this._native.element()); + // Reset the flag + this._didBlurAfterEdit = false; } - /** - * @hidden - */ - hasValue(): boolean { - const inputValue = this._value; - return (inputValue !== null && inputValue !== undefined && inputValue !== ''); - } + _enableInputBlurring() { + console.debug('Input: enableInputBlurring'); + + const self = this; + let unrefBlur: Function; + this.ionFocus.subscribe(() => { + // automatically blur input if: + // 1) this input has focus + // 2) the newly tapped document element is not an input + const plt = self._plt; + unrefBlur = plt.registerListener(plt.doc(), 'touchend', (ev: TouchEvent) => { + const tapped = ev.target; + const ele = self._native.nativeElement; + if (tapped && ele) { + if (tapped.tagName !== 'INPUT' && tapped.tagName !== 'TEXTAREA' && !tapped.classList.contains('input-cover')) { + ele.blur(); + } + } + }, { capture: true, zone: false }); + }); - /** - * @hidden - */ - checkHasValue(inputValue: any) { - if (this._item) { - var hasValue = (inputValue !== null && inputValue !== undefined && inputValue !== ''); - this._item.setElementClass('input-has-value', hasValue); - } + this.ionBlur.subscribe(() => { + unrefBlur && unrefBlur(); + }); } - /** - * @hidden - */ - focusChange(inputHasFocus: boolean) { - if (this._item) { - console.debug(`input-base, focusChange, inputHasFocus: ${inputHasFocus}, ${this._item.getNativeElement().nodeName}.${this._item.getNativeElement().className}`); - this._item.setElementClass('input-has-focus', inputHasFocus); - } + _enableScrollPadding() { + console.debug('Input: enableScrollPadding'); - // If clearOnEdit is enabled and the input blurred but has a value, set a flag - if (this._clearOnEdit && !inputHasFocus && this.hasValue()) { - this._didBlurAfterEdit = true; - } + this.ionFocus.subscribe(() => { + this._dom.write(() => { + this._plt.doc().body.scrollTop = 0; + }); + const content = this._content; + + // add padding to the bottom of the scroll view (if needed) + content.addScrollPadding(this._scrollData.scrollPadding); + content.clearScrollPaddingFocusOut(); + }); } - /** - * @hidden - */ - pointerStart(ev: UIEvent) { - // input cover touchstart - if (ev.type === 'touchstart') { - this._isTouch = true; + _enableBlurOnScrolling() { + console.debug('Input: enableBlurOnScrolling'); + + const self = this; + const content = this._content; + + content.ionScrollStart + .takeUntil(this._onDestroy) + .subscribe(() => scrollHideFocus(true)); + + content.ionScrollEnd + .takeUntil(this._onDestroy) + .subscribe(() => scrollHideFocus(false)); + + this.ionBlur.subscribe(() => hideFocus(false)); + + function scrollHideFocus(shouldHideFocus: boolean) { + assert(self._content, 'content must be valid'); + + // do not continue if there's no nav, or it's transitioning + if (self.isFocus()) { + // if it does have focus, then do the dom write + self._dom.write(() => hideFocus(shouldHideFocus)); + } } - if ((this._isTouch || (!this._isTouch && ev.type === 'mousedown')) && this._app.isEnabled()) { - // remember where the touchstart/mousedown started - this._coord = pointerCoord(ev); + function hideFocus(shouldHideFocus: boolean) { + const platform = self._plt; + const focusedInputEle = self._native.nativeElement; + console.debug(`native-input, hideFocus, shouldHideFocus: ${shouldHideFocus}, input value: ${focusedInputEle.value}`); + + if (shouldHideFocus) { + cloneInputComponent(platform, focusedInputEle); + (focusedInputEle.style)[platform.Css.transform] = 'scale(0)'; + + } else { + removeClone(platform, focusedInputEle); + } } - console.debug(`input-base, pointerStart, type: ${ev.type}`); } - /** - * @hidden - */ - pointerEnd(ev: UIEvent) { - // input cover touchend/mouseup - console.debug(`input-base, pointerEnd, type: ${ev.type}`); + _enableScrollAssist() { + console.debug('Input: enableScrollAssist'); - if ((this._isTouch && ev.type === 'mouseup') || !this._app.isEnabled()) { - // the app is actively doing something right now - // don't try to scroll in the input - ev.preventDefault(); - ev.stopPropagation(); + const self = this; + let coord: PointerCoordinates; + let relocated: boolean = false; + const clone = this._config.getBoolean('inputCloning', false); + const events = new UIEventManager(this._plt); + events.pointerEvents({ + element: this.getNativeElement(), + pointerDown: pointerDown, + pointerUp: pointerUp, + capture: true, + zone: false + }); - } else if (this._coord) { - // get where the touchend/mouseup ended - let endCoord = pointerCoord(ev); + this._onDestroy.subscribe(() => events.destroy()); + + function pointerDown(ev: any) { + if (self._app.isEnabled()) { + coord = pointerCoord(ev); + return true; + } + return false; + } + + function pointerUp(ev: any) { + if (!self._app.isEnabled()) { + // the app is actively doing something right now + // don't try to scroll in the input + ev.preventDefault(); + ev.stopPropagation(); + return; + } + assert(coord, 'coord must be valid'); + const endCoord = pointerCoord(ev); // focus this input if the pointer hasn't moved XX pixels // and the input doesn't already have focus - if (!hasPointerMoved(8, this._coord, endCoord) && !this.hasFocus()) { + if (!hasPointerMoved(8, coord, endCoord) && !self.isFocus()) { ev.preventDefault(); ev.stopPropagation(); // begin the input focus process - this.initFocus(); + initFocus(); } + coord = null; } - this._coord = null; - } - /** - * @hidden - */ - setItemInputControlCss() { - let item = this._item; - let nativeInput = this._native; - let inputControl = this.inputControl; - - // Set the control classes on the item - if (item && inputControl) { - setControlCss(item, inputControl); - } + function initFocus() { + // begin the process of setting focus to the inner input element + const content = self._content; + console.debug(`input-base, initFocus(), scrollView: ${!!content}`); - // Set the control classes on the native input - if (nativeInput && inputControl) { - setControlCss(nativeInput, inputControl); - } - } - - /** - * @hidden - */ - ngOnInit() { - const item = this._item; - if (item) { - if (this.type === TEXTAREA) { - item.setElementClass('item-textarea', true); + // not inside of a scroll view, just focus it + if (!content) { + self.setFocus(); + return; } - item.setElementClass('item-input', true); - item.registerInput(this.type); - } - - // By default, password inputs clear after focus when they have content - if (this.type === 'password' && this.clearOnEdit !== false) { - this.clearOnEdit = true; - } - } - - /** - * @hidden - */ - ngAfterContentChecked() { - this.setItemInputControlCss(); - } + // this input is inside of a scroll view + // find out if text input should be manually scrolled into view + const app = self._app; - /** - * @hidden - */ - ngOnDestroy() { - this._form.deregister(this); + // get container of this input, probably an ion-item a few nodes up + let ele: HTMLElement = self._elementRef.nativeElement; + ele = ele.closest('ion-item,[ion-item]') || ele; - // only stop listening to content scroll events if there is content - if (this._content) { - this._scrollStart.unsubscribe(); - this._scrollEnd.unsubscribe(); - } - } + const scrollData = self._scrollData = getScrollData(ele.offsetTop, ele.offsetHeight, content.getContentDimensions(), self._keyboardHeight, self._plt.height()); + if (Math.abs(scrollData.scrollAmount) < 4) { + // the text input is in a safe position that doesn't + // require it to be scrolled into view, just set focus now + self.setFocus(); - /** - * @hidden - */ - clearTextInput() { - console.debug('Should clear input'); - this._value = ''; - this.onChange(this._value); - this.writeValue(this._value); - } + // all good, allow clicks again + app.setEnabled(true); + return; + } - /** - * Check if we need to clear the text input if clearOnEdit is enabled - * @hidden - */ - checkClearOnEdit(inputValue: string) { - if (!this._clearOnEdit) { - return; - } + // manually scroll the text input to the top + // do not allow any clicks while it's scrolling + const scrollDuration = getScrollAssistDuration(scrollData.scrollAmount); + app.setEnabled(false, scrollDuration); - // Did the input value change after it was blurred and edited? - if (this._didBlurAfterEdit && this.hasValue()) { - // Clear the input - this.clearTextInput(); - } + if (clone) { + // temporarily move the focus to the focus holder so the browser + // doesn't freak out while it's trying to get the input in place + // at this point the native text input still does not have focus + beginFocus(true, scrollData.inputSafeY); + } - // Reset the flag - this._didBlurAfterEdit = false; - } + // let's now set focus to the actual native element + // at this point it is safe to assume the browser will not attempt + // to scroll the input into view itself (screwing up headers/footers) + self.setFocus(); - /** - * @hidden - * Angular2 Forms API method called by the view (formControlName) to register the - * onChange event handler that updates the model (Control). - * @param {Function} fn the onChange event handler. - */ - registerOnChange(fn: any) { this.onChange = fn; } + // scroll the input into place + content.scrollTo(0, scrollData.scrollTo, scrollDuration, () => { + console.debug(`input-base, scrollTo completed, scrollTo: ${scrollData.scrollTo}, scrollDuration: ${scrollDuration}`); + if (clone) { + // the scroll view is in the correct position now + // give the native text input focus + beginFocus(false, 0); + } - /** - * @hidden - * Angular2 Forms API method called by the view (formControlName) to register - * the onTouched event handler that marks model (Control) as touched. - * @param {Function} fn onTouched event handler. - */ - registerOnTouched(fn: any) { this.onTouched = fn; } + // all good, allow clicks again + app.setEnabled(true); + }); + } + function beginFocus(shouldFocus: boolean, inputRelativeY: number) { + if (relocated === shouldFocus) { + return; + } + relocated = shouldFocus; + + const focusedInputEle = this._native.nativeElement; + if (shouldFocus) { + // this platform needs the input to be cloned + // this allows for the actual input to receive the focus from + // the user's touch event, but before it receives focus, it + // moves the actual input to a location that will not screw + // up the app's layout, and does not allow the native browser + // to attempt to scroll the input into place (messing up headers/footers) + // the cloned input fills the area of where native input should be + // while the native input fakes out the browser by relocating itself + // before it receives the actual focus event + cloneInputComponent(this._plt, focusedInputEle); + + // move the native input to a location safe to receive focus + // according to the browser, the native input receives focus in an + // area which doesn't require the browser to scroll the input into place + (focusedInputEle.style)[this._plt.Css.transform] = `translate3d(-9999px,${inputRelativeY}px,0)`; + focusedInputEle.style.opacity = '0'; + + } else { + removeClone(this._plt, focusedInputEle); + } + } - /** - * @hidden - */ - focusNext() { - this._form.tabFocus(this); } } @@ -833,8 +633,7 @@ export class TextInput extends Ion implements IonicFormInput { const SCROLL_ASSIST_SPEED = 0.3; -const TEXTAREA = 'textarea'; -const TEXT_TYPE_REGEX = /password|email|number|search|tel|url|date|month|time|week/; +const ALLOWED_TYPES = ['password', 'email', 'number', 'search', 'tel', 'url', 'date', 'month', 'time', 'week']; /** @@ -842,20 +641,20 @@ const TEXT_TYPE_REGEX = /password|email|number|search|tel|url|date|month|time|we */ export function getScrollData(inputOffsetTop: number, inputOffsetHeight: number, scrollViewDimensions: ContentDimensions, keyboardHeight: number, plaformHeight: number) { // compute input's Y values relative to the body - let inputTop = (inputOffsetTop + scrollViewDimensions.contentTop - scrollViewDimensions.scrollTop); - let inputBottom = (inputTop + inputOffsetHeight); + const inputTop = (inputOffsetTop + scrollViewDimensions.contentTop - scrollViewDimensions.scrollTop); + const inputBottom = (inputTop + inputOffsetHeight); // compute the safe area which is the viewable content area when the soft keyboard is up - let safeAreaTop = scrollViewDimensions.contentTop; - let safeAreaHeight = (plaformHeight - keyboardHeight - safeAreaTop) / 2; - let safeAreaBottom = safeAreaTop + safeAreaHeight; + const safeAreaTop = scrollViewDimensions.contentTop; + const safeAreaHeight = (plaformHeight - keyboardHeight - safeAreaTop) / 2; + const safeAreaBottom = safeAreaTop + safeAreaHeight; // figure out if each edge of teh input is within the safe area - let inputTopWithinSafeArea = (inputTop >= safeAreaTop && inputTop <= safeAreaBottom); - let inputTopAboveSafeArea = (inputTop < safeAreaTop); - let inputTopBelowSafeArea = (inputTop > safeAreaBottom); - let inputBottomWithinSafeArea = (inputBottom >= safeAreaTop && inputBottom <= safeAreaBottom); - let inputBottomBelowSafeArea = (inputBottom > safeAreaBottom); + const inputTopWithinSafeArea = (inputTop >= safeAreaTop && inputTop <= safeAreaBottom); + const inputTopAboveSafeArea = (inputTop < safeAreaTop); + const inputTopBelowSafeArea = (inputTop > safeAreaBottom); + const inputBottomWithinSafeArea = (inputBottom >= safeAreaTop && inputBottom <= safeAreaBottom); + const inputBottomBelowSafeArea = (inputBottom > safeAreaBottom); /* Text Input Scroll To Scenarios @@ -942,21 +741,64 @@ export function getScrollData(inputOffsetTop: number, inputOffsetHeight: number, return scrollData; } -function setControlCss(element: any, control: NgControl) { - element.setElementClass('ng-untouched', control.untouched); - element.setElementClass('ng-touched', control.touched); - element.setElementClass('ng-pristine', control.pristine); - element.setElementClass('ng-dirty', control.dirty); - element.setElementClass('ng-valid', control.valid); - element.setElementClass('ng-invalid', !control.valid); -} - function getScrollAssistDuration(distanceToScroll: number) { distanceToScroll = Math.abs(distanceToScroll); - let duration = distanceToScroll / SCROLL_ASSIST_SPEED; + const duration = distanceToScroll / SCROLL_ASSIST_SPEED; return Math.min(400, Math.max(150, duration)); } +function cloneInputComponent(plt: Platform, srcNativeInputEle: HTMLInputElement) { + // given a native or