import { Component, ChangeDetectionStrategy, 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 } from '../content/content'; import { hasPointerMoved, pointerCoord } from '../../util/dom'; import { DomController } from '../../platform/dom-controller'; import { Form, IonicFormInput } from '../../util/form'; import { BaseInput } from '../../util/base-input'; import { isTrueProperty, assert } from '../../util/util'; import { Item } from '../item/item'; 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 better able to handle the user experience and * interactivity. * * Similarly, `` should be used in place of `' + '' + '
', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush }) export class TextInput extends BaseInput implements IonicFormInput { _autoFocusAssist: string; _clearInput: boolean = false; _clearOnEdit: boolean; _didBlurAfterEdit: boolean; _readonly: boolean = false; _keyboardHeight: number; _type: string = 'text'; _scrollData: ScrollData; _isTextarea: boolean = false; _onDestroy = new Subject(); _coord: any; _isTouch: boolean; _useAssist = false; _relocated: boolean = false; /** * @input {boolean} If true, 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._isTextarea && isTrueProperty(val)); } /** * @input {string} The type of control to display. The default type is text. * Possible values are: `"text"`, `"password"`, `"email"`, `"number"`, `"search"`, `"tel"`, or `"url"`. */ @Input() get type() { return (this._isTextarea) ? 'text' : this._type; } set type(val: any) { this._type = val; } /** * @input {boolean} If true, the user cannot modify the value. */ @Input() get readonly() { return this._readonly; } set readonly(val: boolean) { this._readonly = isTrueProperty(val); } /** * @input {boolean} If true, the value will be cleared after focus upon edit. * Defaults to `true` when `type` is `"password"`, `false` for all other types. */ @Input() get clearOnEdit() { return this._clearOnEdit; } set clearOnEdit(val: any) { this._clearOnEdit = isTrueProperty(val); } /** * @hidden */ @ViewChild('textInput', { read: ElementRef }) _native: ElementRef; /** * @input {string} Instructional text that shows before the input has a value. */ @Input() autocomplete: string = ''; /** * @input {string} Instructional text that shows before the input has a value. */ @Input() autocorrect: string = ''; /** * @input {string} Specifies whether the element is to have its spelling * and grammar checked or not. */ @Input() spellcheck: string = null; /** * @input {string} controls whether and how the text value for textual form control descendants should be automatically capitalized as it is entered/edited by the user. */ @Input() autocapitalize: string = null; /** * @input {string} Instructional text that shows before the input has a value. */ @Input() placeholder: string = ''; /** * @input {string} The name attribute is used to reference elements in a JavaScript, * or to reference form data after a form is submitted. */ @Input() name: string = null; /** * @input {any} The minimum value, which must not be greater than its maximum (max attribute) value. */ @Input() min: number | string = null; /** * @input {any} The maximum value, which must not be less than its minimum (min attribute) value. */ @Input() max: number | string = null; /** * @input {any} Works with the min and max attributes to limit the increments at which a value can be set. */ @Input() step: number | string = null; /** * @input {any} Specifies the maximum number of characters allowed in the element. */ @Input() maxlength: number | string = null; /** * @hidden */ @Output() input = new EventEmitter(); /** * @hidden */ @Output() blur = new EventEmitter(); /** * @hidden */ @Output() focus = new EventEmitter(); 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'); // If not inside content, let's disable all the hacks if (!_content) { return; } const blurOnScroll = config.getBoolean('hideCaretOnScroll', false); if (blurOnScroll) { this._enableHideCaretOnScroll(); } const resizeAssist = config.getBoolean('resizeAssist', false); if (resizeAssist) { this._keyboardHeight = 60; this._enableResizeAssist(); } else { this._useAssist = config.getBoolean('scrollAssist', false); const usePadding = config.getBoolean('scrollPadding', this._useAssist); if (usePadding) { this._enableScrollPadding(); } } } ngAfterContentInit() { } /** * @hidden */ ngAfterViewInit() { assert(this._native && this._native.nativeElement, 'input element must be valid'); // By default, password inputs clear after focus when they have content if (this.clearOnEdit !== false && this.type === 'password') { this.clearOnEdit = true; } 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'); switch (this._autoFocusAssist) { case 'immediate': // config says to immediate focus on the input // works best on android devices nativeInputEle.focus(); 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 } this._initialize(); if (this.focus.observers.length > 0) { console.warn('(focus) is deprecated in ion-input, use (ionFocus) instead'); } if (this.blur.observers.length > 0) { console.warn('(blur) is deprecated in ion-input, use (ionBlur) instead'); } } /** * @hidden */ ngOnDestroy() { super.ngOnDestroy(); this._onDestroy.next(); this._onDestroy = null; } /** * @hidden */ initFocus() { this.setFocus(); } /** * @hidden */ 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 */ setBlur() { if (this.isFocus()) { this._native.nativeElement.blur(); } } /** * @hidden */ onInput(ev: any) { this.value = ev.target.value; // TODO: deprecate this this.input.emit(ev); } /** * @hidden */ onBlur(ev: UIEvent) { this._fireBlur(); // TODO: deprecate this (06/07/2017) this.blur.emit(ev); this._scrollData = null; if (this._clearOnEdit && this.hasValue()) { this._didBlurAfterEdit = true; } } /** * @hidden */ onFocus(ev: UIEvent) { this._fireFocus(); // TODO: deprecate this (06/07/2017) this.focus.emit(ev); } /** * @hidden */ onKeydown(ev: any) { if (ev && this._clearOnEdit) { this.checkClearOnEdit(ev.target.value); } } /** * @hidden */ _inputUpdated() { super._inputUpdated(); const inputEle = this._native.nativeElement; const value = this._value; if (inputEle.value !== value) { inputEle.value = value; } } /** * @hidden */ clearTextInput() { this.value = ''; } /** * Check if we need to clear the text input if clearOnEdit is enabled * @hidden */ 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; } _getScrollData(): ScrollData { if (!this._content) { return newScrollData(); } // get container of this input, probably an ion-item a few nodes up if (this._scrollData) { return this._scrollData; } let ele: HTMLElement = this._elementRef.nativeElement; ele = ele.closest('ion-item,[ion-item]') || ele; return this._scrollData = getScrollData( ele.offsetTop, ele.offsetHeight, this._content.getContentDimensions(), this._keyboardHeight, this._plt.height()); } _relocateInput(shouldRelocate: boolean) { if (this._relocated === shouldRelocate) { return; } const platform = this._plt; const componentEle = this.getNativeElement(); const focusedInputEle = this._native.nativeElement; console.debug(`native-input, hideCaret, shouldHideCaret: ${shouldRelocate}, input value: ${focusedInputEle.value}`); if (shouldRelocate) { // 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 // We hide the focused input (with the visible caret) invisiable by making it scale(0), cloneInputComponent(platform, componentEle, focusedInputEle); const inputRelativeY = this._getScrollData().inputSafeY; focusedInputEle.style[platform.Css.transform] = `translate3d(-9999px,${inputRelativeY}px,0)`; focusedInputEle.style.opacity = '0'; } else { removeClone(platform, componentEle, focusedInputEle); } this._relocated = shouldRelocate; } _enableScrollPadding() { assert(this._content, 'content is undefined'); console.debug('Input: enableScrollPadding'); this.ionFocus.subscribe(() => { const content = this._content; // add padding to the bottom of the scroll view (if needed) content.addScrollPadding(this._getScrollData().scrollPadding); content.clearScrollPaddingFocusOut(); }); } _enableHideCaretOnScroll() { assert(this._content, 'content is undefined'); const content = this._content; console.debug('Input: enableHideCaretOnScroll'); content.ionScrollStart .takeUntil(this._onDestroy) .subscribe(() => scrollHideCaret(true)); content.ionScrollEnd .takeUntil(this._onDestroy) .subscribe(() => scrollHideCaret(false)); this.ionBlur.subscribe(() => this._relocateInput(false)); const self = this; function scrollHideCaret(shouldHideCaret: boolean) { // if it does have focus, then do the dom write if (self.isFocus()) { self._dom.write(() => self._relocateInput(shouldHideCaret)); } } } _enableResizeAssist() { assert(this._content, 'content is undefined'); console.debug('Input: enableAutoScroll'); this.ionFocus.subscribe(() => { const scrollData = this._getScrollData(); if (Math.abs(scrollData.scrollAmount) > 100) { this._content.scrollTo(0, scrollData.scrollTo, scrollData.scrollDuration); } }); } _pointerStart(ev: UIEvent) { assert(this._content, 'content is undefined'); // 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}`); } _pointerEnd(ev: UIEvent) { assert(this._content, 'content is undefined'); // 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 var 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.isFocus()) { ev.preventDefault(); ev.stopPropagation(); // begin the input focus process this._jsSetFocus(); } } this._coord = null; } _jsSetFocus() { assert(this._content, 'content is undefined'); // begin the process of setting focus to the inner input element const content = this._content; console.debug(`input-base, initFocus(), scrollView: ${!!content}`); if (!content) { // not inside of a scroll view, just focus it this.setFocus(); } var scrollData = this._getScrollData(); 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(); return; } // 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 this._relocateInput(true); this.setFocus(); // scroll the input into place content.scrollTo(0, scrollData.scrollTo, scrollData.scrollDuration, () => { // the scroll view is in the correct position now // give the native text input focus this._relocateInput(false); // ensure this is the focused input this.setFocus(); }); } } /** * @name TextArea * @description * * `ion-textarea` is used for multi-line text inputs. Ionic still * uses an actual `