UNPKG

white-ui-kit

Version:

Модуль компонентов UI Имя модуля: `white-ui-kit`

365 lines (318 loc) 10.5 kB
import { Component, forwardRef, Injector, EventEmitter, HostBinding, ViewChild, Input, OnInit, AfterViewInit, OnDestroy, HostListener, ElementRef, } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, NgControl, } from '@angular/forms'; import { CdkOverlayOrigin, OverlayRef, ConnectedPosition, } from '@angular/cdk/overlay'; import { Subscription, Observable, of, } from 'rxjs'; import { debounceTime, switchMap, finalize, tap, filter, } from 'rxjs/operators'; // Сенсор import { ResizeSensor } from 'css-element-queries'; // Компоненты import { HitSelectorPopupComponent } from '../hit-selector-popup/hit-selector-popup.component'; // Сервисы import { HitOverlayService } from '../../services'; // Хелпер import { MathHelper } from '../../helpers'; // Тип делегата type InvokeListLoad = (search: string) => Observable<any[]>; // Селектор @Component({ selector: 'hit-selector', templateUrl: './hit-selector.component.html', styleUrls: ['./hit-selector.component.scss'], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => HitSelectorComponent), multi: true, }], }) export class HitSelectorComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { constructor( private readonly el: ElementRef, private readonly injector: Injector, private readonly overlayService: HitOverlayService, ) { this.CONFIG.closing.subscribe(() => { this.filterControl.patchValue(this.selected && this.selected.title); }); this.DATA.select.subscribe((selected: any) => { this.selected = selected; this.focusInput(); this.close(); }); } // Флаг наличия ошибки public hasError: boolean; @HostBinding('attr.has-error') public get attrHasError(): string { return this.hasError ? 'has-error' : null; } // Ссылка на компонент @ViewChild('popupOrigin') private readonly popupOrigin: CdkOverlayOrigin; // Заглушка поля @Input() public placeholder: string; // Заглушка private readonly EMPTY_INVOKE: InvokeListLoad = () => of([]); // Делегат вызова загрузки списка private _invokeListLoad: InvokeListLoad; public get invokeListLoad(): InvokeListLoad { return this._invokeListLoad || this.EMPTY_INVOKE; } @Input('getList') public set invokeListLoad(invoke: InvokeListLoad) { this._invokeListLoad = invoke; } // Вызовем когда значение изменится private onChange: (value) => void; // Вызовем при любом дествии пользователя с контроллом private onTouched: () => void; // Флаг, отключено private _disabled: boolean = false; public get disabled(): boolean { return this._disabled; } public set disabled(value: boolean) { this._disabled = value; if (value) { this.filterControl.disable(); } else { this.filterControl.enable(); } } // Контрол private control: FormControl; // Подписка на контрол private subscription: Subscription; // Контрол фильтрации public filterControl = new FormControl(); // Подписка на контрол фильтрации private filterSubscription: Subscription; // Выбранный элемент public _selected: any; public get selected(): any { return this._selected; } public set selected(value: any) { this._selected = value; this.DATA.selected = value; this.filterControl.patchValue(value && value.title); if (this.onChange) { this.onChange(value); } } // Сенсор изменения размера private resizeSensor: any; // Конфигурация всплывающего окна private readonly CONFIG = { elementRef: null, overlayComponent: HitSelectorPopupComponent, positions: [ { overlayX: 'start', overlayY: 'top', originX: 'start', originY: 'bottom', }, { overlayX: 'start', overlayY: 'bottom', originX: 'start', originY: 'top', }, ] as ConnectedPosition[], canClose: false, closing: new EventEmitter<void>(), }; // Данные всплывающего окна private readonly DATA = { width: 0, // ширина окна isLoaded: false, // флаг, список загружен list: [], // список selected: null, // выбранный элемент из списка select: new EventEmitter<any>(), // событие выбора }; // Ссылка на всплывающее окно private overlayRef: OverlayRef; // Флаг, окно открыто private get isPopupOpened(): boolean { return !!(this.overlayRef && this.overlayRef.hasAttached()); } // Флаг, фокус в контроле private get isFocusOrigin(): boolean { return MathHelper.hasParent(this.el.nativeElement, document.activeElement); } // Флаг, фокус в окне private get isFocusOverlay(): boolean { return MathHelper.hasParent( this.overlayRef && this.overlayRef.overlayElement, document.activeElement, ); } // -------------------------------------------------------------------------- // Добавить сенсор private addSensor(): void { if (this.el && this.el.nativeElement) { this.resizeSensor = new ResizeSensor( this.el.nativeElement, this.updateWidth.bind(this), ); this.updateWidth(); } } // Удалить сенсор private removeSensor(): void { if (this.resizeSensor) { this.resizeSensor.detach(); } } // Обновить ширину private updateWidth(): void { this.DATA.width = this.el && this.el.nativeElement && this.el.nativeElement.offsetWidth; } // -------------------------------------------------------------------------- // Выбрать первый в списке public selectFirst(event: KeyboardEvent): void { if ( !this.disabled && this.isPopupOpened && this.DATA.isLoaded && this.DATA.list[0] ) { event.stopPropagation(); this.selected = this.DATA.list[0]; this.focusInput(); this.close(); } } // Очистить public clear(): void { if (!this.disabled) { this.selected = null; } } // Обработчик смены фокуса private onFocus(): void { if (this.isFocusOrigin) { this.focusInput(); this.open(); } } // Обработчик потери фокуса private onBlur(): void { if (this.isPopupOpened) { setTimeout(() => { if (!this.isFocusOverlay && !this.isFocusOrigin) { this.close(); } }); } } // Установить фокус на поле фильтрации private focusInput(): void { this.el.nativeElement.getElementsByTagName('input')[0].focus(); } // -------------------------------------------------------------------------- // Открыть окно public open(): void { if (!this.isPopupOpened && !this.disabled) { this.overlayRef = this.overlayService.openContext(this.CONFIG, this.DATA); this.filterControl.patchValue(this.filterControl.value); } } // Закрыть окно public close(): void { this.CONFIG.closing.emit(); if (this.overlayRef) { this.overlayRef.dispose(); } } // -------------------------------------------------------------------------- // Реакция на клик хост-элемента @HostListener('click') public onClick(): void { if (this.onTouched) { this.onTouched(); } } // Вызовет форма, если значение изменилось извне public writeValue(selected: any): void { this.selected = selected; } // Сохраняем обратный вызов для изменений public registerOnChange(fn: any): void { this.onChange = fn; } // Сохраняем обратный вызов для "касаний" public registerOnTouched(fn: any): void { this.onTouched = fn; } // Установка состояния disabled public setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } // -------------------------------------------------------------------------- // Инициализация public ngOnInit(): void { document.addEventListener('focus', this.onFocus.bind(this), true); document.addEventListener('blur', this.onBlur.bind(this), true); this.addSensor(); this.CONFIG.elementRef = this.popupOrigin.elementRef; this.filterSubscription = this.filterControl .valueChanges .pipe( tap(() => { this.onFocus(); this.DATA.isLoaded = false; }), debounceTime(300), filter(() => this.isPopupOpened), switchMap(search => this.invokeListLoad(search)), finalize(() => { this.DATA.isLoaded = true; }), ) .subscribe((list: any[]) => { this.DATA.list = list; this.DATA.isLoaded = true; }); } // Инициализация завершена public ngAfterViewInit(): void { const ngControl: NgControl = this.injector.get(NgControl, null); if (ngControl) { this.control = ngControl.control as FormControl; this.subscription = this.control.statusChanges .subscribe(() => { this.hasError = !this.control.pristine && !this.control.valid; }); } } // Уничтожение public ngOnDestroy(): void { if (this.subscription) { this.subscription.unsubscribe(); } if (this.filterSubscription) { this.filterSubscription.unsubscribe(); } this.removeSensor(); this.close(); document.removeEventListener('focus', this.onFocus.bind(this), true); document.removeEventListener('blur', this.onBlur.bind(this), true); } }