import { ChangeDetectionStrategy , Component , ElementRef , EventEmitter , Input , Optional , 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 { copyInputAttributes , hasPointerMoved , pointerCoord } from '../../util/dom' ; import { DomController } from '../../platform/dom-controller' ; import { Form , IonicFormInput } from '../../util/form' ; import { BaseInput } from '../../util/base-input' ; import { assert , isTrueProperty } from '../../util/util' ; import { Item } from '../item/item' ; import { Platform } from '../../platform/platform' ; name description usage demo @ Component ( { selector : 'ion-input,ion-textarea' , template : '<input #textInput *ngIf="!_isTextarea" class="text-input" ' + '[ngClass]="\'text-input-\' + _mode"' + '(input)="onInput($event)" ' + '(blur)="onBlur($event)" ' + '(focus)="onFocus($event)" ' + '(keydown)="onKeydown($event)" ' + '[type]="_type" ' + 'dir="auto" ' + '[attr.aria-labelledby]="_labelId" ' + '[attr.min]="min" ' + '[attr.max]="max" ' + '[attr.step]="step" ' + '[attr.autocomplete]="autocomplete" ' + '[attr.autocorrect]="autocorrect" ' + '[placeholder]="placeholder" ' + '[disabled]="_disabled" ' + '[readonly]="_readonly">' + '<textarea #textInput *ngIf="_isTextarea" class="text-input" ' + '[ngClass]="\'text-input-\' + _mode"' + '(input)="onInput($event)" ' + '(blur)="onBlur($event)" ' + '(focus)="onFocus($event)" ' + '(keydown)="onKeydown($event)" ' + '[attr.aria-labelledby]="_labelId" ' + '[attr.autocomplete]="autocomplete" ' + '[attr.autocorrect]="autocorrect" ' + '[placeholder]="placeholder" ' + '[disabled]="_disabled" ' + '[readonly]="_readonly"></textarea>' + '<button ion-button *ngIf="_clearInput" clear class="text-input-clear-icon" ' + 'type="button" ' + '(click)="clearTextInput($event)" ' + '(mousedown)="clearTextInput($event)" ' + 'tabindex="-1"></button>' + '<div class="input-cover" *ngIf="_useAssist" ' + '(touchstart)="_pointerStart($event)" ' + '(touchend)="_pointerEnd($event)" ' + '(mousedown)="_pointerStart($event)" ' + '(mouseup)="_pointerEnd($event)"></div>' , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , inputs : [ 'value' ] } ) export class TextInput extends BaseInput < string > 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 < void > ( ) ; _coord : any ; _isTouch : boolean ; _useAssist = false ; _relocated : boolean = false ; input @ Input ( ) get clearInput ( ) { return this . _clearInput ; } set clearInput ( val ) { this . _clearInput = ( ! this . _isTextarea && isTrueProperty ( val ) ) ; } input @ Input ( ) get type ( ) { return ( this . _isTextarea ) ? 'text' : this . _type ; } set type ( val ) { this . _type = val ; } input @ Input ( ) get readonly ( ) { return this . _readonly ; } set readonly ( val ) { this . _readonly = isTrueProperty ( val ) ; } input @ Input ( ) get clearOnEdit ( ) { return this . _clearOnEdit ; } set clearOnEdit ( val ) { this . _clearOnEdit = isTrueProperty ( val ) ; } hidden @ ViewChild ( 'textInput' , { read : ElementRef } ) _native : ElementRef ; input @ Input ( ) autocomplete : string = '' ; input @ Input ( ) autocorrect : string = '' ; input @ Input ( ) placeholder : string = '' ; input @ Input ( ) min : number | string = null ; input @ Input ( ) max : number | string = null ; input @ Input ( ) step : number | string = null ; hidden @ Output ( ) input = new EventEmitter < UIEvent > ( ) ; hidden @ Output ( ) blur = new EventEmitter < UIEvent > ( ) ; hidden @ Output ( ) focus = new EventEmitter < UIEvent > ( ) ; constructor ( config , private _plt , _form , private _app , elementRef , renderer , @ Optional ( ) private _content , @ Optional ( ) _item , @ Optional ( ) public ngControl , private _dom ) { super ( config , elementRef , renderer , '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 ( this . _isTextarea && _item ) { _item . setElementClass ( 'item-textarea' , true ) ; } if ( ! _content ) { return ; } const hideCaretOnScroll = config . getBoolean ( 'hideCaretOnScroll' , false ) ; if ( hideCaretOnScroll ) { this . _enableHideCaretOnScroll ( ) ; } const win = _plt . win ( ) as any ; const keyboardPlugin = win . Ionic && win . Ionic . keyboardPlugin ; if ( keyboardPlugin ) { const keyboardResizes = config . getBoolean ( 'keyboardResizes' , false ) ; if ( keyboardResizes ) { this . _keyboardHeight = config . getNumber ( 'keyboardSafeArea' , 60 ) ; this . _enableScrollMove ( ) ; } else { this . _enableScrollPadding ( ) ; this . _enableScrollMove ( ) ; } } 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' ) ; if ( this . clearOnEdit !== false && this . type === 'password' ) { this . clearOnEdit = true ; } const ionInputEle = this . _elementRef . nativeElement ; const nativeInputEle = this . _native . nativeElement ; copyInputAttributes ( ionInputEle , nativeInputEle ) ; if ( ionInputEle . hasAttribute ( 'tabIndex' ) ) { ionInputEle . removeAttribute ( 'tabIndex' ) ; } if ( ionInputEle . hasAttribute ( 'autofocus' ) ) { ionInputEle . removeAttribute ( 'autofocus' ) ; switch ( this . _autoFocusAssist ) { case 'immediate' : nativeInputEle . focus ( ) ; break ; case 'delay' : this . _plt . timeout ( ( ) => nativeInputEle . focus ( ) , 800 ) ; break ; } } 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 ( ) { if ( ! this . isFocus ( ) ) { this . _native . nativeElement . focus ( ) ; } } hidden setBlur ( ) { if ( this . isFocus ( ) ) { this . _native . nativeElement . blur ( ) ; } } hidden onInput ( ev ) { this . value = ev . target . value ; this . input . emit ( ev ) ; } hidden onBlur ( ev ) { this . _fireBlur ( ) ; this . blur . emit ( ev ) ; this . _scrollData = null ; if ( this . _clearOnEdit && this . hasValue ( ) ) { this . _didBlurAfterEdit = true ; } } hidden onFocus ( ev ) { this . _fireFocus ( ) ; this . focus . emit ( ev ) ; } hidden onKeydown ( ev ) { 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 = '' ; } hidden checkClearOnEdit ( _ ) { if ( ! this . _clearOnEdit ) { return ; } if ( this . _didBlurAfterEdit && this . hasValue ( ) ) { this . clearTextInput ( ) ; } this . _didBlurAfterEdit = false ; } _getScrollData ( ) { if ( ! this . _content ) { return newScrollData ( ) ; } if ( this . _scrollData ) { return this . _scrollData ; } let ele = this . _elementRef . nativeElement ; ele = < HTMLElement > 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 ) { 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 ) { cloneInputComponent ( platform , componentEle , focusedInputEle ) ; const inputRelativeY = this . _getScrollData ( ) . inputSafeY ; const tx = this . _plt . isRTL ? 9999 : - 9999 ; focusedInputEle . style [ platform . Css . transform ] = `translate3d(${ tx }px,${ 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 ; const scrollPadding = this . _getScrollData ( ) . scrollPadding ; content . addScrollPadding ( 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 ) { if ( self . isFocus ( ) ) { self . _dom . write ( ( ) => self . _relocateInput ( shouldHideCaret ) ) ; } } } _enableScrollMove ( ) { assert ( this . _content , 'content is undefined' ) ; console . debug ( 'Input: enableAutoScroll' ) ; this . ionFocus . subscribe ( ( ) => { const scrollData = this . _getScrollData ( ) ; if ( Math . abs ( scrollData . scrollAmount ) > 4 ) { this . _content . scrollTo ( 0 , scrollData . scrollTo , scrollData . scrollDuration ) ; } } ) ; } _pointerStart ( ev ) { assert ( this . _content , 'content is undefined' ) ; if ( ev . type === 'touchstart' ) { this . _isTouch = true ; } if ( ( this . _isTouch || ( ! this . _isTouch && ev . type === 'mousedown' ) ) && this . _app . isEnabled ( ) ) { this . _coord = pointerCoord ( ev ) ; } console . debug ( `input-base, pointerStart, type: ${ ev . type }` ) ; } _pointerEnd ( ev ) { assert ( this . _content , 'content is undefined' ) ; console . debug ( `input-base, pointerEnd, type: ${ ev . type }` ) ; if ( ( this . _isTouch && ev . type === 'mouseup' ) || ! this . _app . isEnabled ( ) ) { ev . preventDefault ( ) ; ev . stopPropagation ( ) ; } else if ( this . _coord ) { var endCoord = pointerCoord ( ev ) ; if ( ! hasPointerMoved ( 8 , this . _coord , endCoord ) && ! this . isFocus ( ) ) { ev . preventDefault ( ) ; ev . stopPropagation ( ) ; this . _jsSetFocus ( ) ; } } this . _coord = null ; } _jsSetFocus ( ) { assert ( this . _content , 'content is undefined' ) ; const content = this . _content ; console . debug ( `input-base, initFocus(), scrollView: ${ ! ! content }` ) ; if ( ! content ) { this . setFocus ( ) ; } var scrollData = this . _getScrollData ( ) ; if ( Math . abs ( scrollData . scrollAmount ) < 4 ) { this . setFocus ( ) ; return ; } this . _relocateInput ( true ) ; this . setFocus ( ) ; content . scrollTo ( 0 , scrollData . scrollTo , scrollData . scrollDuration , ( ) => { this . _relocateInput ( false ) ; this . setFocus ( ) ; } ) ; } } name description usage demo const SCROLL_ASSIST_SPEED = 0.3 ; function newScrollData ( ) { return { scrollAmount : 0 , scrollTo : 0 , scrollPadding : 0 , scrollDuration : 0 , inputSafeY : 0 } ; } hidden export function getScrollData ( inputOffsetTop , inputOffsetHeight , scrollViewDimensions , keyboardHeight , plaformHeight ) { const inputTop = ( inputOffsetTop + scrollViewDimensions . contentTop - scrollViewDimensions . scrollTop ) ; const inputBottom = ( inputTop + inputOffsetHeight ) ; const safeAreaTop = scrollViewDimensions . contentTop ; const safeAreaHeight = ( plaformHeight - keyboardHeight - safeAreaTop ) / 2 ; const safeAreaBottom = safeAreaTop + safeAreaHeight ; const inputTopWithinSafeArea = ( inputTop >= safeAreaTop && inputTop <= safeAreaBottom ) ; const inputTopAboveSafeArea = ( inputTop < safeAreaTop ) ; const inputTopBelowSafeArea = ( inputTop > safeAreaBottom ) ; const inputBottomWithinSafeArea = ( inputBottom >= safeAreaTop && inputBottom <= safeAreaBottom ) ; const inputBottomBelowSafeArea = ( inputBottom > safeAreaBottom ) ; const scrollData = newScrollData ( ) ; scrollData . scrollPadding = keyboardHeight ; if ( inputTopWithinSafeArea && inputBottomWithinSafeArea ) { return scrollData ; } if ( inputTopBelowSafeArea || inputBottomBelowSafeArea || inputTopAboveSafeArea ) { if ( safeAreaHeight > inputOffsetHeight ) { scrollData . scrollAmount = Math . round ( safeAreaBottom - inputBottom ) ; } else { scrollData . scrollAmount = Math . round ( safeAreaTop - inputTop ) ; } scrollData . inputSafeY = - ( inputTop - safeAreaTop ) + 4 ; if ( inputTopAboveSafeArea && scrollData . scrollAmount > inputOffsetHeight ) { scrollData . scrollAmount = inputOffsetHeight ; } } scrollData . scrollTo = ( scrollViewDimensions . scrollTop - scrollData . scrollAmount ) ; const distance = Math . abs ( scrollData . scrollAmount ) ; const duration = distance / SCROLL_ASSIST_SPEED ; scrollData . scrollDuration = Math . min ( 400 , Math . max ( 150 , duration ) ) ; return scrollData ; } function cloneInputComponent ( plt , srcComponentEle , srcNativeInputEle ) { assert ( srcComponentEle . parentElement . querySelector ( '.cloned-input' ) === null , 'leaked cloned input' ) ; if ( srcComponentEle ) { var srcTop = srcComponentEle . offsetTop ; var srcLeft = srcComponentEle . offsetLeft ; var srcWidth = srcComponentEle . offsetWidth ; var srcHeight = srcComponentEle . offsetHeight ; var clonedComponentEle = < HTMLElement > srcComponentEle . cloneNode ( false ) ; var clonedStyle = clonedComponentEle . style ; clonedComponentEle . classList . add ( 'cloned-input' ) ; clonedComponentEle . setAttribute ( 'aria-hidden' , 'true' ) ; clonedStyle . pointerEvents = 'none' ; clonedStyle . position = 'absolute' ; clonedStyle . top = srcTop + 'px' ; clonedStyle . left = srcLeft + 'px' ; clonedStyle . width = srcWidth + 'px' ; clonedStyle . height = srcHeight + 'px' ; var clonedNativeInputEle = < HTMLInputElement > srcNativeInputEle . cloneNode ( false ) ; clonedNativeInputEle . value = srcNativeInputEle . value ; clonedNativeInputEle . tabIndex = - 1 ; clonedComponentEle . appendChild ( clonedNativeInputEle ) ; srcComponentEle . parentNode . appendChild ( clonedComponentEle ) ; srcComponentEle . style . pointerEvents = 'none' ; } ( < any > srcNativeInputEle . style ) [ plt . Css . transform ] = 'scale(0)' ; } function removeClone ( plt , srcComponentEle , srcNativeInputEle ) { if ( srcComponentEle && srcComponentEle . parentElement ) { var clonedInputEles = srcComponentEle . parentElement . querySelectorAll ( '.cloned-input' ) ; for ( var i = 0 ; i < clonedInputEles . length ; i ++ ) { clonedInputEles [ i ] . parentNode . removeChild ( clonedInputEles [ i ] ) ; } srcComponentEle . style . pointerEvents = '' ; } ( < any > srcNativeInputEle . style ) [ plt . Css . transform ] = '' ; srcNativeInputEle . style . opacity = '' ; } export interface ScrollData { scrollAmount : number ; scrollTo : number ; scrollPadding : number ; scrollDuration : number ; inputSafeY : number ; }