import { Subscription, Observable, of } from 'rxjs'; import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; import { Component, OnInit, Input, ViewChild, AfterViewInit, ElementRef, NgZone, Renderer2, HostBinding, OnDestroy, Output, EventEmitter, ChangeDetectorRef, ChangeDetectionStrategy, forwardRef, ViewEncapsulation, HostListener, InjectionToken, Inject } from '@angular/core'; import { InputGroupComponent } from '@farris/ui-input-group'; import { ComboService } from './combo.service'; import { ComboPanelAlign, ComboPanelState, KeyCode, DEFAULT_COMBOSTATE, defaultComboState, COMBO_FACADE_SERVICE } from './core/combo-types'; import { ComboFacadeService } from './core/combo-facade.service'; import { ComboPanelComponent } from './combo-panel.component'; export const INPUT_COMBO_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ComboComponent), multi: true }; @Component({ selector: 'farris-combo', template: ` `, providers: [ INPUT_COMBO_VALUE_ACCESSOR, {provide: DEFAULT_COMBOSTATE, useValue: defaultComboState}, {provide: COMBO_FACADE_SERVICE, useClass: ComboFacadeService, deps: [ DEFAULT_COMBOSTATE ] } ], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush }) export class ComboComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { groupIcon = ''; @HostBinding('style.position') hostStyle = 'relative'; @Input() panelWidth = 300; @Input() panelHeight = 300; @Input() data: any; @Input() disabled = false; @Input() editable = true; @Input() readonly = false; @Input() textField: string; @Input() valueField: string; @Input() idField: string; @Input() selectedValues: string; @Input() displayText = ''; /** panel 对齐方式 * - left: 与输入框左对齐 * - right: 右对齐 * - both: 两端对齐,与输入框宽度相同 */ @Input() panelAlign: ComboPanelAlign = 'auto'; @Input() beforeOpen: (ins) => Observable; @Input() beforeHide: (ins) => Observable; @Output() showPanel = new EventEmitter(); @Output() hidePanel = new EventEmitter(); @Output() clear = new EventEmitter(); @Output() valueChange = new EventEmitter(); @ViewChild('input') input: InputGroupComponent; _cmbPanel: ComboPanelComponent = null; @ViewChild(ComboPanelComponent) set cmbPanel(cmp: ComboPanelComponent) { this._cmbPanel = cmp; this.cmbState.comboPanelCreated.emit(cmp); } panel$: Observable = this.cmbFacade.panel$; disable$: Observable = this.cmbFacade.isDisabled; readonly$: Observable = this.cmbFacade.isReadonly; editable$: Observable = this.cmbFacade.isEditable; private documentClickEvent: () => void; isOpen = false; private _subscriptions: Subscription[] = []; onModelChange: (val?: any) => {}; onModelTouched: (val?: any) => {}; constructor( public el: ElementRef, public zone: NgZone, public render: Renderer2, public cd: ChangeDetectorRef, @Inject(COMBO_FACADE_SERVICE) public cmbFacade: ComboFacadeService, public cmbState: ComboService ) { this._subscriptions.push( this.cmbFacade.isOpenChange.subscribe( (value: boolean) => { this.isOpen = value; if (value) { this.documentClickEvent = this.render.listen('document', 'click', (event: any) => { if (!this.el.nativeElement.contains(event.target) && (this._cmbPanel && !this._cmbPanel.contains(event))) { this.cmbState.toggleClick.emit(false); } }); } else { this.removeDocumentListener(); } }) ); if (!this.beforeOpen) { this.beforeOpen = () => of(true); } if (!this.beforeHide) { this.beforeHide = () => of(true); } } ngOnInit(): void { if (!this.valueField && this.idField) { this.valueField = this.idField; } } ngAfterViewInit() { this.initComboState(); } ngOnDestroy() { this._subscriptions.forEach( sub => sub.unsubscribe() ); } @HostListener('keydown', ['$event']) handleKeyDown($event: KeyboardEvent) { if (KeyCode[$event.which]) { switch ($event.which) { case KeyCode.Tab: this._handleTab($event); break; case KeyCode.Esc: this.close(); $event.preventDefault(); $event.stopPropagation(); break; } } } // ------------------------ Priveate Methods ------------------------------------ protected _handleTab($event: KeyboardEvent) { if (!this.isOpen) { return; } else { this.close(); } } private initComboState() { const { textField, idField, valueField, disabled, readonly, editable } = {...this}; this.cmbFacade.initControlState({ textField, idField, valueField, disabled, readonly, editable}); this.cd.detectChanges(); } private removeDocumentListener() { if (this.documentClickEvent) { this.documentClickEvent(); this.documentClickEvent = null; } } private setPanelSize(rect: { top, left, width, height }) { // 与input 同等宽度 let _left = 0; if (this.panelAlign === 'auto') { this.panelWidth = rect.width; } else { if (this.panelAlign === 'right') { _left = rect.width - this.panelWidth; } else { _left = rect.left; } } const newPanelOpts = { left: _left, top: rect.top + rect.height, height: this.panelHeight, width: this.panelWidth }; this.cmbFacade.updatePanelState(newPanelOpts); } // -------------------- Events ------------------------// onResize(event: any) { const {left, top, width, height} = event.bindintClientRect; this.setPanelSize({left, top, width: width.toFixed(2), height: Math.floor(height)}); } onBlur(event) { if (this.isOpen) { this.close(); } } onClick() { if (!this.isOpen) { this.cmbState.toggleClick.emit(false); if (!this.displayText) { this.cmbFacade.clearSelections(); } this.beforeOpen(this).subscribe( () => { this.cmbFacade.showPanel(); this.showPanel.emit(); this.focus(); }); } else { this.beforeHide(this).subscribe( (val: any) => { this.close(); }); } } onClear() { this.onModelChange(''); this.clear.emit(); } // -------------------- Methods ---------------------------- // close() { this.cmbFacade.hidePanel(); this.hidePanel.emit(); } setDisable(val: boolean = true) { if (val) { this.cmbFacade.disable(); } else { this.cmbFacade.enable(); } } setReadonly(val: boolean = true) { this.cmbFacade.readonly(val); } setEditable(val: boolean = true) { this.cmbFacade.editable(val); } focus() { this.input.focus(); } // ------------------ Update Model ---------------------------// writeValue(obj: any): void { this.displayText = obj; this.cd.detectChanges(); } registerOnChange(fn: any): void { this.onModelChange = fn; } registerOnTouched(fn: any): void { this.onModelTouched = fn; } setDisabledState?(isDisabled: boolean): void { this.setDisable(isDisabled); } }