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);
}
}