import { EXPANDED_DROPDOWN_POSITIONS , UpdateHostClassService } from 'ngx-tethys/core' ; import { coerceBooleanProperty } from 'ngx-tethys/util' ; import { ConnectedOverlayPositionChange , ConnectionPositionPair } from '@angular/cdk/overlay' ; import { ChangeDetectorRef , Component , ElementRef , EventEmitter , forwardRef , HostListener , Input , OnInit , Output , TemplateRef , ViewChild } from '@angular/core' ; import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ; import { CascaderOption } from './types' ; function toArray < T > ( value ) { let ret ; if ( value == null ) { ret = [ ] ; } else if ( ! Array . isArray ( value ) ) { ret = [ value ] ; } else { ret = value ; } return ret ; } function arrayEquals < T > ( array1 , array2 ) { if ( ! array1 || ! array2 || array1 . length !== array2 . length ) { return false ; } const len = array1 . length ; for ( let i = 0 ; i < len ; i ++ ) { if ( array1 [ i ] !== array2 [ i ] ) { return false ; } } return true ; } const defaultDisplayRender = ( label ) => label . join ( ' / ' ) ; export type ThyCascaderTriggerType = 'click' | 'hover' ; export type ThyCascaderExpandTrigger = 'click' | 'hover' ; @ Component ( { selector : 'thy-cascader,[thy-cascader]' , templateUrl : './cascader.component.html' , providers : [ UpdateHostClassService , { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => ThyCascaderComponent ) , multi : true } ] , styles : [ `
            .thy-cascader-menus {
                position: relative;
            }
        ` ] } ) export class ThyCascaderComponent implements OnInit , ControlValueAccessor { private changeOnSelect = false ; private showInput = true ; public prefixCls = 'thy-cascader' ; private menuClassName : string ; private columnClassName : string ; private _menuColumnCls : any ; private defaultValue : any [ ] ; public dropDownPosition = 'bottom' ; public menuVisible = false ; public isLoading = false ; public isOpening = false ; public showSearch = false ; private _thySize = 'md' ; private _arrowCls : { [ name ] : any ; } ; private _menuCls : { [ name ] : any ; } ; private _labelCls : { [ name ] : any ; } ; private _clearCls : { [ name ] : any ; } ; private _inputCls : { [ name ] : any ; } ; private labelRenderTpl : TemplateRef < any > ; public isLabelRenderTemplate = false ; public labelRenderText : string ; public labelRenderContext : any = { } ; onChange : any = Function . prototype ; onTouched : any = Function . prototype ; private cascaderPositon = [ ... EXPANDED_DROPDOWN_POSITIONS ] ; positions : ConnectionPositionPair [ ] ; @ Input ( ) set thyLabelRender ( value ) { this . labelRenderTpl = value ; this . isLabelRenderTemplate = value instanceof TemplateRef ; } get thyLabelRender ( ) : TemplateRef < any > { return this . labelRenderTpl ; } private value : any [ ] ; private selectedOptions : CascaderOption [ ] = [ ] ; private activatedOptions : CascaderOption [ ] = [ ] ; public thyColumns : CascaderOption [ ] [ ] = [ ] ; @ Input ( ) thyValueProperty = 'value' ; @ Input ( ) thyLabelProperty = 'label' ; @ Input ( ) thyPlaceHolder = '请选择' ; private _inputValue = '' ; get inputValue ( ) : string { return this . _inputValue ; } set inputValue ( inputValue ) { this . _inputValue = inputValue ; const willBeInSearch = ! ! inputValue ; } @ Input ( ) thyLoadData : ( node : CascaderOption , index ? : number ) => PromiseLike < any > ; @ Input ( ) set thyChangeOnSelect ( value ) { this . changeOnSelect = coerceBooleanProperty ( value ) ; } get thyChangeOnSelect ( ) : boolean { return this . changeOnSelect ; } @ Input ( ) set thyShowInput ( value ) { this . showInput = coerceBooleanProperty ( value ) ; } get thyShowInput ( ) : boolean { return this . showInput ; } public searchWidthStyle : string ; private oldColumnsHolder : any ; private oldActivatedOptions : any ; public inSearch = false ; @ Input ( ) thyTriggerAction : ThyCascaderTriggerType | ThyCascaderTriggerType [ ] = [ 'click' ] ; @ Input ( ) thyExpandTriggerAction : ThyCascaderExpandTrigger | ThyCascaderExpandTrigger [ ] = [ 'click' ] ; @ Input ( ) thyMenuStyle : { [ key ] : string ; } ; @ Input ( ) set thyOptions ( options ) { this . oldColumnsHolder = this . thyColumns = options && options . length ? [ options ] : [ ] ; if ( ! this . inSearch ) { if ( this . defaultValue && this . thyColumns . length ) { this . initOptions ( 0 ) ; } } } @ Input ( ) set thyMenuClassName ( value ) { this . menuClassName = value ; this . setMenuClass ( ) ; } get thyMenuClassName ( ) : string { return this . menuClassName ; } @ Input ( ) set thyColumnClassName ( value ) { this . columnClassName = value ; this . setMenuClass ( ) ; } get thyColumnClassName ( ) : string { return this . columnClassName ; } @ Input ( ) disabled = false ; @ Input ( ) set thySize ( v ) { this . _thySize = v ; } get thySize ( ) { return this . _thySize ; } @ Output ( ) thyChange = new EventEmitter < any [ ] > ( ) ; @ Output ( ) thySelectionChange = new EventEmitter < CascaderOption [ ] > ( ) ; @ Output ( ) thySelect = new EventEmitter < { option : CascaderOption ; index : number ; } > ( ) ; @ Input ( ) thyChangeOn : ( option : CascaderOption , level : number ) => boolean ; @ Output ( ) thyClear = new EventEmitter < void > ( ) ; @ ViewChild ( 'input' ) input : ElementRef ; @ ViewChild ( 'menu' ) menu : ElementRef ; ngOnInit ( ) { this . setClassMap ( ) ; this . setMenuClass ( ) ; this . setMenuColumnClass ( ) ; this . setArrowClass ( ) ; this . setLabelClass ( ) ; this . setClearClass ( ) ; this . setInputClass ( ) ; this . initPosition ( ) ; } private initPosition ( ) { this . cascaderPositon [ 0 ] . offsetY = 10 ; this . cascaderPositon [ 1 ] . offsetY = 10 ; this . cascaderPositon [ 2 ] . offsetY = - 10 ; this . cascaderPositon [ 3 ] . offsetY = - 10 ; this . positions = this . cascaderPositon ; } private initOptions ( index ) { const vs = this . defaultValue ; const load = ( ) => { this . activateOnInit ( index , vs [ index ] ) ; if ( index < vs . length - 1 ) { this . initOptions ( index + 1 ) ; } if ( index === vs . length - 1 ) { this . afterWriteValue ( ) ; } } ; if ( this . isLoaded ( index ) || ! this . thyLoadData ) { load ( ) ; } else { const node = this . activatedOptions [ index - 1 ] || { } ; this . loadChildren ( node , index - 1 , load , this . afterWriteValue . bind ( this ) ) ; } } private activateOnInit ( index , value ) { let option = this . findOption ( value , index ) ; if ( ! option ) { option = typeof value === 'object' ? value : { [ `${ this . thyValueProperty || 'value' }` ] : value , [ `${ this . thyLabelProperty || 'label' }` ] : value } ; } this . setActiveOption ( option , index , false , false ) ; } writeValue ( value ) { const vs = ( this . defaultValue = toArray ( value ) ) ; if ( vs . length ) { this . initOptions ( 0 ) ; } else { this . value = vs ; this . activatedOptions = [ ] ; this . afterWriteValue ( ) ; } } afterWriteValue ( ) { this . selectedOptions = this . activatedOptions ; this . value = this . getSubmitValue ( ) ; this . buildDisplayLabel ( ) ; } registerOnChange ( fn ) { this . onChange = fn ; } registerOnTouched ( fn ) { this . onTouched = fn ; } public onPositionChange ( position ) { const newValue = position . connectionPair . originY === 'bottom' ? 'bottom' : 'top' ; if ( this . dropDownPosition !== newValue ) { this . dropDownPosition = newValue ; this . cdr . detectChanges ( ) ; } } private isLoaded ( index ) { return this . thyColumns [ index ] && this . thyColumns [ index ] . length > 0 ; } public getOptionLabel ( option ) { return option [ this . thyLabelProperty || 'label' ] ; } public getOptionValue ( option ) { return option [ this . thyValueProperty || 'value' ] ; } private hasInput ( ) { return this . inputValue . length > 0 ; } private hasValue ( ) { return this . value && this . value . length > 0 ; } public get showPlaceholder ( ) : boolean { return ! ( this . hasInput ( ) || this . hasValue ( ) ) ; } public isActivedOption ( option , index ) { const activeOpt = this . activatedOptions [ index ] ; return activeOpt === option ; } private findOption ( option , index ) { const options = this . thyColumns [ index ] ; if ( options ) { const value = typeof option === 'object' ? this . getOptionValue ( option ) : option ; return options . find ( o => value === this . getOptionValue ( o ) ) ; } return null ; } private buildDisplayLabel ( ) { const selectedOptions = this . selectedOptions ; const labels = selectedOptions . map ( o => this . getOptionLabel ( o ) ) ; if ( this . isLabelRenderTemplate ) { this . labelRenderContext = { labels , selectedOptions } ; } else { this . labelRenderText = defaultDisplayRender . call ( this , labels , selectedOptions ) ; } } public isMenuVisible ( ) { return this . menuVisible ; } public setMenuVisible ( menuVisible ) { if ( this . menuVisible !== menuVisible ) { this . menuVisible = menuVisible ; this . setClassMap ( ) ; this . setArrowClass ( ) ; this . setMenuClass ( ) ; } } public get menuCls ( ) : any { return this . _menuCls ; } private setMenuClass ( ) { this . _menuCls = { [ `${ this . prefixCls }-menus` ] : true , [ `${ this . prefixCls }-menus-hidden` ] : ! this . menuVisible , [ `${ this . thyMenuClassName }` ] : this . thyMenuClassName } ; } public get menuColumnCls ( ) : any { return this . _menuColumnCls ; } private setMenuColumnClass ( ) { this . _menuColumnCls = { [ `${ this . prefixCls }-menu` ] : true , [ `${ this . thyColumnClassName }` ] : this . thyColumnClassName } ; } public get arrowCls ( ) : any { return this . _arrowCls ; } private setArrowClass ( ) { this . _arrowCls = { [ `${ this . prefixCls }-picker-arrow` ] : true , [ `${ this . prefixCls }-picker-arrow-expand` ] : this . menuVisible } ; } public get clearCls ( ) : any { return this . _clearCls ; } private setClearClass ( ) { this . _clearCls = { [ `${ this . prefixCls }-picker-clear` ] : true , [ `remove-link` ] : true } ; } public get labelCls ( ) : any { return this . _labelCls ; } private setLabelClass ( ) { this . _labelCls = { [ `${ this . prefixCls }-picker-label` ] : true , [ `${ this . prefixCls }-show-search` ] : false , [ `${ this . prefixCls }-focused` ] : false } ; } public get inputCls ( ) : any { return this . _inputCls ; } private setInputClass ( ) { this . _inputCls = { [ `${ this . prefixCls }-input` ] : true } ; } private setClassMap ( ) { const classMap = { [ `${ this . prefixCls }` ] : true , [ `${ this . prefixCls }-picker` ] : true , [ `${ this . prefixCls }-${ this . thySize }` ] : true , [ `${ this . prefixCls }-picker-disabled` ] : this . disabled , [ `${ this . prefixCls }-picker-open` ] : this . menuVisible } ; this . updateHostClassService . updateClassByMap ( classMap ) ; } private isClickTriggerAction ( ) { if ( typeof this . thyTriggerAction === 'string' ) { return this . thyTriggerAction === 'click' ; } return this . thyTriggerAction . indexOf ( 'click' ) !== - 1 ; } private isHoverTriggerAction ( ) { if ( typeof this . thyTriggerAction === 'string' ) { return this . thyTriggerAction === 'hover' ; } return this . thyTriggerAction . indexOf ( 'hover' ) !== - 1 ; } private isHoverExpandTriggerAction ( ) { if ( typeof this . thyExpandTriggerAction === 'string' ) { return this . thyExpandTriggerAction === 'hover' ; } return this . thyExpandTriggerAction . indexOf ( 'hover' ) !== - 1 ; } @ HostListener ( 'click' , [ '$event' ] ) public trggleClick ( $event ) { if ( this . disabled ) { return ; } this . onTouched ( ) ; if ( this . isClickTriggerAction ( ) ) { this . setMenuVisible ( ! this . menuVisible ) ; } } @ HostListener ( 'mouseover' , [ '$event' ] ) public trggleHover ( $event ) { if ( this . disabled ) { return ; } if ( this . isHoverTriggerAction ( ) ) { this . setMenuVisible ( ! this . menuVisible ) ; } } onOptionClick ( option , index , event ) { if ( event ) { event . preventDefault ( ) ; } if ( option && option . disabled ) { return ; } this . setActiveOption ( option , index , true ) ; } onOptionMouseover ( option , index , event ) { if ( event ) { event . preventDefault ( ) ; } if ( option && option . disabled ) { return ; } if ( ! this . isHoverExpandTriggerAction ( ) ) { return ; } this . setActiveOption ( option , index , false ) ; } onMenuMouseleave ( event ) { if ( event ) { event . preventDefault ( ) ; } if ( ! this . isHoverTriggerAction ( ) ) { return ; } this . setMenuVisible ( ! this . menuVisible ) ; } public closeMenu ( ) { this . setMenuVisible ( false ) ; this . setArrowClass ( ) ; } public setActiveOption ( option , index , select , loadChildren = true ) { if ( ! option || option . disabled ) { return ; } this . activatedOptions [ index ] = option ; for ( let i = index - 1 ; i >= 0 ; i -- ) { if ( ! this . activatedOptions [ i ] ) { this . activatedOptions [ i ] = this . activatedOptions [ i + 1 ] . parent ; } } if ( index < this . activatedOptions . length - 1 ) { this . activatedOptions = this . activatedOptions . slice ( 0 , index + 1 ) ; } if ( option . children && option . children . length ) { option . isLeaf = false ; option . children . forEach ( child => ( child . parent = option ) ) ; this . setColumnData ( option . children , index + 1 ) ; } else if ( ! option . isLeaf && loadChildren ) { this . loadChildren ( option , index ) ; } else { if ( index < this . thyColumns . length - 1 ) { this . thyColumns = this . thyColumns . slice ( 0 , index + 1 ) ; } } if ( select ) { this . onSelectOption ( option , index ) ; } } private onSelectOption ( option , index ) { this . thySelect . emit ( { option , index } ) ; if ( option . isLeaf || this . thyChangeOnSelect || this . shouldPerformSelection ( option , index ) ) { this . selectedOptions = this . activatedOptions ; this . buildDisplayLabel ( ) ; this . onValueChange ( ) ; } if ( option . isLeaf ) { this . setMenuVisible ( false ) ; } } private shouldPerformSelection ( option , level ) { return typeof this . thyChangeOn === 'function' ? this . thyChangeOn ( option , level ) === true : false ; } private onValueChange ( ) { const value = this . getSubmitValue ( ) ; if ( ! arrayEquals ( this . value , value ) ) { this . defaultValue = null ; this . value = value ; this . onChange ( value ) ; if ( value . length === 0 ) { this . thyClear . emit ( ) ; } this . thySelectionChange . emit ( this . selectedOptions ) ; this . thyChange . emit ( value ) ; } } public clearSelection ( $event ) { if ( $event ) { $event . stopPropagation ( ) ; $event . preventDefault ( ) ; } this . labelRenderText = '' ; this . labelRenderContext = { } ; this . selectedOptions = [ ] ; this . activatedOptions = [ ] ; this . inputValue = '' ; this . setMenuVisible ( false ) ; this . onValueChange ( ) ; } public focus ( ) { } private loadChildren ( option , index , success ? , failure ? ) { if ( this . thyLoadData ) { this . isLoading = true ; this . thyLoadData ( option , index ) . then ( ( ) => { option . loading = this . isLoading = false ; if ( option . children ) { option . children . forEach ( child => ( child . parent = index < 0 ? undefined : option ) ) ; this . setColumnData ( option . children , index + 1 ) ; } if ( success ) { success ( ) ; } } , ( ) => { option . loading = this . isLoading = false ; option . isLeaf = true ; if ( failure ) { failure ( ) ; } } ) ; } } private setColumnData ( options , index ) { if ( ! arrayEquals ( this . thyColumns [ index ] , options ) ) { this . thyColumns [ index ] = options ; if ( index < this . thyColumns . length - 1 ) { this . thyColumns = this . thyColumns . slice ( 0 , index + 1 ) ; } } } public getSubmitValue ( ) { const values = [ ] ; this . selectedOptions . forEach ( option => { values . push ( this . getOptionValue ( option ) ) ; } ) ; return values ; } constructor ( private cdr , private elementRef , private updateHostClassService ) { updateHostClassService . initializeElement ( elementRef . nativeElement ) ; } }