import { BooleanInput , coerceBooleanProperty } from '@angular/cdk/coercion' ; import { getSupportedInputTypes , Platform } from '@angular/cdk/platform' ; import { AutofillMonitor } from '@angular/cdk/text-field' ; import { AfterViewInit , Directive , DoCheck , ElementRef , HostListener , Inject , Input , NgZone , OnChanges , OnDestroy , Optional , Self , } from '@angular/core' ; import { FormGroupDirective , NgControl , NgForm } from '@angular/forms' ; import { CanUpdateErrorState , mixinErrorState , SbbErrorStateMatcher } from '@sbb-esta/angular/core' ; import { SbbFormFieldControl } from '@sbb-esta/angular/form-field' ; import { getSbbInputUnsupportedTypeError } from './input-errors' ; import { SBB_INPUT_VALUE_ACCESSOR } from './input-value-accessor' ; let nextId = 0 ; const SBB_INPUT_INVALID_TYPES = [ 'button' , 'checkbox' , 'file' , 'hidden' , 'image' , 'radio' , 'range' , 'reset' , 'submit' , ] ; docs-private const _SbbInputBase = mixinErrorState ( class { constructor ( public _defaultErrorStateMatcher , public _parentForm , public _parentFormGroup , docs-private public ngControl ) { } } ) ; /** Directive that allows a native input to work inside a `SbbFormField`. */ @ Directive ( { selector : 'input[sbbInput], select[sbbInput], textarea[sbbInput]' , exportAs : 'sbbInput' , host : { class : 'sbb-input-element' , '[attr.id]' : 'id' , '[disabled]' : 'disabled' , '[required]' : 'required' , '[attr.readonly]' : 'readonly && !_isNativeSelect || null' , '[attr.aria-invalid]' : '(empty && required) ? null : errorState' , '[attr.aria-required]' : 'required' , '[attr.placeholder]' : `!readonly ? (placeholder || null) : '-'` , '[attr.tabindex]' : `empty && readonly ? -1  : null` , } , providers : [ { provide : SbbFormFieldControl , useExisting : SbbInput } ] , } ) export class SbbInput extends _SbbInputBase implements SbbFormFieldControl < any > , AfterViewInit , OnChanges , DoCheck , OnDestroy , CanUpdateErrorState { private _previousNativeValue : any ; private _inputValueAccessor : { value : any ; } ; /** Whether the component is a native html select. */ readonly _isNativeSelect : boolean ; /** Whether the component is a textarea. */ readonly _isTextarea : boolean ; docs-private focused : boolean = false ; docs-private controlType : string = 'sbb-input' ; docs-private autofilled : boolean = false ; docs-private @ Input ( ) get disabled ( ) : boolean { if ( this . ngControl && this . ngControl . disabled !== null ) { return this . ngControl . disabled ; } return this . _disabled ; } set disabled ( value ) { this . _disabled = coerceBooleanProperty ( value ) ; if ( this . focused ) { this . focused = false ; this . stateChanges . next ( ) ; } } private _disabled = false ; docs-private @ Input ( ) id : string = `sbb-native-input-${ nextId ++ }` ; docs-private @ Input ( ) placeholder : string ; docs-private @ Input ( ) get required ( ) : boolean { return this . _required ; } set required ( value ) { this . _required = coerceBooleanProperty ( value ) ; } private _required = false ; /** Input type of the element. */ @ Input ( ) get type ( ) : string { return this . _type ; } set type ( value ) { this . _type = value || 'text' ; this . _validateType ( ) ; if ( ! this . _isTextarea && getSupportedInputTypes ( ) . has ( this . _type ) ) { ( this . _elementRef . nativeElement as HTMLInputElement ) . type = this . _type ; } } private _type = 'text' ; /** An object used to control when error messages are shown. */ @ Input ( ) override errorStateMatcher : SbbErrorStateMatcher ; docs-private @ Input ( 'aria-describedby' ) userAriaDescribedBy : string ; docs-private @ Input ( ) get value ( ) : string { return this . _inputValueAccessor . value ; } set value ( value ) { if ( value !== this . value ) { this . _inputValueAccessor . value = value ; this . stateChanges . next ( ) ; } } /** Whether the element is readonly. */ @ Input ( ) get readonly ( ) : boolean { return this . _readonly ; } set readonly ( value ) { this . _readonly = coerceBooleanProperty ( value ) ; } private _readonly = false ; docs-private get empty ( ) : boolean { return ( ! this . _isNeverEmpty ( ) && ! this . _elementRef . nativeElement . value && ! this . _isBadInput ( ) && ! this . autofilled ) ; } private _neverEmptyInputTypes = [ 'date' , 'datetime' , 'datetime-local' , 'month' , 'time' , 'week' , ] . filter ( ( t ) => getSupportedInputTypes ( ) . has ( t ) ) ; constructor ( private _elementRef , private _platform , docs-private @ Optional ( ) @ Self ( ) public override ngControl , @ Optional ( ) parentForm , @ Optional ( ) parentFormGroup , defaultErrorStateMatcher , @ Optional ( ) @ Self ( ) @ Inject ( SBB_INPUT_VALUE_ACCESSOR ) inputValueAccessor , private _autofillMonitor , ngZone ) { super ( defaultErrorStateMatcher , parentForm , parentFormGroup , ngControl ) ; const element = this . _elementRef . nativeElement ; const nodeName = element . nodeName . toLowerCase ( ) ; this . _inputValueAccessor = inputValueAccessor || element ; this . _previousNativeValue = this . value ; if ( _platform . IOS ) { ngZone . runOutsideAngular ( ( ) => { _elementRef . nativeElement . addEventListener ( 'keyup' , ( event ) => { const el = event . target as HTMLInputElement ; if ( ! el . value && el . selectionStart === 0 && el . selectionEnd === 0 ) { el . setSelectionRange ( 1 , 1 ) ; el . setSelectionRange ( 0 , 0 ) ; } } ) ; } ) ; } this . _isNativeSelect = nodeName === 'select' ; this . _isTextarea = nodeName === 'textarea' ; if ( this . _isNativeSelect ) { this . controlType = ( element as HTMLSelectElement ) . multiple ? 'sbb-native-select-multiple' : 'sbb-native-select' ; } } ngAfterViewInit ( ) { if ( this . _platform . isBrowser ) { this . _autofillMonitor . monitor ( this . _elementRef . nativeElement ) . subscribe ( ( event ) => { this . autofilled = event . isAutofilled ; this . stateChanges . next ( ) ; } ) ; } } ngOnChanges ( ) { this . stateChanges . next ( ) ; } ngOnDestroy ( ) { this . stateChanges . complete ( ) ; if ( this . _platform . isBrowser ) { this . _autofillMonitor . stopMonitoring ( this . _elementRef . nativeElement ) ; } } ngDoCheck ( ) { if ( this . ngControl ) { this . updateErrorState ( ) ; } this . _dirtyCheckNativeValue ( ) ; } /** Focuses the input. */ focus ( options ? ) { this . _elementRef . nativeElement . focus ( options ) ; } /** Callback for the cases where the focused state of the input changes. */ @ HostListener ( 'focus' , [ 'true' ] ) @ HostListener ( 'blur' , [ 'false' ] ) _focusChanged ( isFocused ) { if ( isFocused !== this . focused ) { this . focused = isFocused ; this . stateChanges . next ( ) ; } } /** Does some manual dirty checking on the native input `value` property. */ private _dirtyCheckNativeValue ( ) { const newValue = this . _elementRef . nativeElement . value ; if ( this . _previousNativeValue !== newValue ) { this . _previousNativeValue = newValue ; this . stateChanges . next ( ) ; } } /** Make sure the input is a supported type. */ private _validateType ( ) { if ( SBB_INPUT_INVALID_TYPES . indexOf ( this . _type ) > - 1 && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) { throw getSbbInputUnsupportedTypeError ( this . _type ) ; } } /** Checks whether the input type is one of the types that are never empty. */ private _isNeverEmpty ( ) { return this . _neverEmptyInputTypes . indexOf ( this . _type ) > - 1 ; } /** Checks whether the input is invalid based on the native validation. */ private _isBadInput ( ) { const validity = ( this . _elementRef . nativeElement as HTMLInputElement ) . validity ; return validity && validity . badInput ; } docs-private setDescribedByIds ( ids ) { if ( ids . length ) { this . _elementRef . nativeElement . setAttribute ( 'aria-describedby' , ids . join ( ' ' ) ) ; } else { this . _elementRef . nativeElement . removeAttribute ( 'aria-describedby' ) ; } } docs-private onContainerClick ( ) { if ( ! this . focused ) { this . focus ( ) ; } } }