import { Component, Optional, ElementRef, EventEmitter, Input, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core'; import { NgControl } from '@angular/forms'; 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 { DomController } from '../../platform/dom-controller'; import { Form, IonicFormInput } from '../../util/form'; import { Ion } from '../ion'; import { isString, isTrueProperty } from '../../util/util'; import { Item } from '../item/item'; import { NativeInput, NextInput } from './native-input'; import { NavController } from '../../navigation/nav-controller'; import { NavControllerBase } from '../../navigation/nav-controller-base'; import { Platform } from '../../platform/platform'; /** * @name Input * @description * * `ion-input` is meant for text type inputs only, such as `text`, * `password`, `email`, `number`, `search`, `tel`, and `url`. Ionic * still uses an actual `` HTML element within the * component, however, with Ionic wrapping the native HTML input * element it's able to better handle the user experience and * interactivity. * * Similarily, `` should be used in place of `' + '' + '' + '
', encapsulation: ViewEncapsulation.None, }) export class TextInput extends Ion implements IonicFormInput { _autoComplete: string; _autoCorrect: string; _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 = ''; /** @private */ 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} The placeholder for the input */ @Input() placeholder: string = ''; /** * @input {boolean} A clear icon will appear in the input when there is a value. Clicking it clears the input. */ @Input() get clearInput() { 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); } /** * @input {string} The HTML input type (text, password, email, number, search, tel, or url) */ @Input() get type() { 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; } } } } /** * @input {boolean} If the input should be disabled or not */ @Input() get disabled() { return this._disabled; } set disabled(val: boolean) { this.setDisabled(this._disabled = isTrueProperty(val)); } /** * @private */ 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); } /** * @private */ setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; } /** * @input {boolean} If the input should be readonly or not */ @Input() get readonly() { return this._readonly; } set readonly(val: boolean) { this._readonly = isTrueProperty(val); } /** * @input {string} The mode to apply to this component. */ @Input() set mode(val: string) { this._setMode(val); } /** * @input {boolean} whether to clear the input upon editing or not */ @Input() get clearOnEdit() { return this._clearOnEdit; } set clearOnEdit(val: any) { 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); } /** * @private */ setMin(val: any) { this._native && this._native.setMin(val); } /** * @input {any} The maximum value, which must not be less than its minimum (min attribute) value. */ @Input() get max() { return this._max; } set max(val: any) { this.setMax(this._max = val); } /** * @private */ setMax(val: any) { this._native && this._native.setMax(val); } /** * @input {any} Works with the min and max attributes to limit the increments at which a value can be set. */ @Input() get step() { return this._step; } set step(val: any) { this.setStep(this._step = val); } /** * @private */ setStep(val: any) { this._native && this._native.setStep(val); } /** * @private */ @ViewChild('input', { read: NativeInput }) set _nativeInput(nativeInput: NativeInput) { if (this.type !== TEXTAREA) { this.setNativeInput(nativeInput); } } /** * @private */ @ViewChild('textarea', { read: NativeInput }) set _nativeTextarea(nativeInput: NativeInput) { if (this.type === TEXTAREA) { this.setNativeInput(nativeInput); } } /** * @private */ @ViewChild(NextInput) set _nextInput(nextInput: NextInput) { if (nextInput) { nextInput.focused.subscribe(() => { this._form.tabFocus(this); }); } } /** * @output {event} Expression to call when the input no longer has focus */ @Output() blur: EventEmitter = new EventEmitter(); /** * @output {event} Expression to call when the input has focus */ @Output() focus: EventEmitter = new EventEmitter(); /** * @private */ 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); } 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); if (ionInputEle.hasAttribute('autofocus')) { // the ion-input element has the autofocus attributes 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(() => { nativeInputEle.focus(); }, 650); } // 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); } /** * @private */ 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(); } } /** * @private */ 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; }); } /** * @private */ 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); }); } } /** * @private */ inputBlurred(ev: UIEvent) { this.blur.emit(ev); } /** * @private */ inputFocused(ev: UIEvent) { this.focus.emit(ev); } /** * @private */ writeValue(val: any) { this._value = val; this.checkHasValue(val); } /** * @private */ onChange(val: any) { this.checkHasValue(val); } /** * @private */ onKeydown(val: any) { if (this._clearOnEdit) { this.checkClearOnEdit(val); } } /** * @private */ onTouched(val: any) {} /** * @private */ hasFocus(): boolean { // check if an input has focus or not return this._plt.hasFocus(this._native.element()); } /** * @private */ hasValue(): boolean { const inputValue = this._value; return (inputValue !== null && inputValue !== undefined && inputValue !== ''); } /** * @private */ checkHasValue(inputValue: any) { if (this._item) { var hasValue = (inputValue !== null && inputValue !== undefined && inputValue !== ''); this._item.setElementClass('input-has-value', hasValue); } } /** * @private */ 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); } // If clearOnEdit is enabled and the input blurred but has a value, set a flag if (this._clearOnEdit && !inputHasFocus && this.hasValue()) { this._didBlurAfterEdit = true; } } /** * @private */ pointerStart(ev: UIEvent) { // input cover touchstart if (ev.type === 'touchstart') { this._isTouch = true; } if ((this._isTouch || (!this._isTouch && ev.type === 'mousedown')) && this._app.isEnabled()) { // remember where the touchstart/mousedown started this._coord = pointerCoord(ev); } console.debug(`input-base, pointerStart, type: ${ev.type}`); } /** * @private */ pointerEnd(ev: UIEvent) { // input cover touchend/mouseup console.debug(`input-base, pointerEnd, type: ${ev.type}`); 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(); } else if (this._coord) { // get where the touchend/mouseup ended let 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()) { ev.preventDefault(); ev.stopPropagation(); // begin the input focus process this.initFocus(); } } this._coord = null; } /** * @private */ 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); } // Set the control classes on the native input if (nativeInput && inputControl) { setControlCss(nativeInput, inputControl); } } /** * @private */ ngOnInit() { const item = this._item; if (item) { if (this.type === TEXTAREA) { item.setElementClass('item-textarea', true); } 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; } } /** * @private */ ngAfterContentChecked() { this.setItemInputControlCss(); } /** * @private */ ngOnDestroy() { this._form.deregister(this); // only stop listening to content scroll events if there is content if (this._content) { this._scrollStart.unsubscribe(); this._scrollEnd.unsubscribe(); } } /** * @private */ clearTextInput() { console.debug('Should clear input'); this._value = ''; this.onChange(this._value); this.writeValue(this._value); } /** * Check if we need to clear the text input if clearOnEdit is enabled * @private */ checkClearOnEdit(inputValue: string) { if (!this._clearOnEdit) { return; } // Did the input value change after it was blurred and edited? if (this._didBlurAfterEdit && this.hasValue()) { // Clear the input this.clearTextInput(); } // Reset the flag this._didBlurAfterEdit = false; } /** * @private * 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; } /** * @private * 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; } /** * @private */ focusNext() { this._form.tabFocus(this); } } /** * @name TextArea * @description * * `ion-textarea` is is used for multi-line text inputs. Ionic still * uses an actual `