Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import {
- AfterViewInit,
- ChangeDetectionStrategy,
- ChangeDetectorRef,
- Component, ElementRef,
- forwardRef,
- Host,
- HostListener,
- Input,
- OnDestroy,
- OnInit,
- Optional,
- ViewChild,
- } from '@angular/core';
- import { ControlContainer, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
- import { Subscription } from 'rxjs';
- import { TranslateService } from '@ngx-translate/core';
- import {
- MultiselectDropdownListComponent,
- MultiselectDropdownMenuData,
- SELECT_ALL,
- } from '@shared/components/multiselect/multiselect-dropdawn-list/multiselect-dropdown-list.component';
- import { KeyCodes } from '@shared/enums';
- import { Viewport } from '@shared/modules/viewport';
- import { DummyItem } from '@shared/interfaces';
- import { AutoUnsubscribe } from '@shared/decorators';
- import { ScrollStrategies } from '@shared/modules/overlay/enums/scroll-strategies.enum';
- import { ControlValueAccessorClass } from '@shared/classes';
- import { PositionStrategyWithSettings } from '@shared/modules/overlay/models/overlay-config.model';
- import { ObservableHandler, SubjectHandler } from '@shared/models';
- import { AnchorPoints, Overlay, OverlayRef, PositionStrategies } from '@shared/modules/overlay';
- import { as, initFormControl, isNullOrUndefined, isNullOrUndefinedOrEmpty, safe, safeDetectChanges } from '@shared/utils';
- import {PrimaryFiltersModalData} from '@app/ads/ad-findings/primary-filters-modal/primary-filters-modal.component';
- const DEFAULT_NULL_CHOICE_LABEL = 'COMPONENTS.SELECT.DEFAULT_NULL_CHOICE';
- @AutoUnsubscribe
- @Component({
- selector: 'sm-multiselect',
- templateUrl: './multiselect.component.html',
- styleUrls: ['./multiselect.component.scss'],
- changeDetection: ChangeDetectionStrategy.OnPush,
- providers: [
- { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MultiselectComponent), multi: true },
- { provide: NG_VALIDATORS, useExisting: forwardRef(() => MultiselectComponent), multi: true },
- ],
- })
- export class MultiselectComponent extends ControlValueAccessorClass implements OnInit, AfterViewInit, OnDestroy {
- readonly isDesktop = Viewport.isDesktop;
- /**
- * Имя свойства у выбираемых значений/объектов, которое будет использоваться в качестве итогового (выбранного) значения.
- */
- @Input() itemsPropertyAsValue = 'key';
- /**
- * Имя свойства у выбираемых значений/объектов, которое будет использоваться в качестве отображаемого названия.
- */
- @Input() itemPropertyAsLabel = 'value';
- /**
- * Имя свойства у выбираемых значений/объектов, которое будет использоваться для определения приоритетных значений.
- */
- @Input() itemsPropertyAsPriority = 'isPriority';
- /**
- * Список значений, доступных для выбора. Отображается в выпадающем меню ({@link dropdawnListRef}).
- */
- @Input() set items(sourceItems: Object[]) {
- if (sourceItems instanceof Array) {
- this._items.emit(sourceItems.map((item: Object) => ({
- value: item.getProperty(this.itemsPropertyAsValue),
- label: item.getProperty(this.itemPropertyAsLabel),
- priority: item.getProperty(this.itemsPropertyAsPriority),
- })));
- }
- }
- @Input() title: string;
- @Input() placeholder: string;
- @Input() formControl: FormControl;
- @Input() formControlName: string;
- @Input() event: PrimaryFiltersModalData;
- /**
- * Кастомные классы. Применяются к div.sm-form-field.
- */
- @Input() customClass: string | string[];
- /**
- * Ключ перевода для отображаемоего названия варианта "ничего не выбрано".
- */
- @Input() nullChoiceLabel = DEFAULT_NULL_CHOICE_LABEL;
- /**
- * Название фильтра, к которому относится компонент
- */
- @Input() label = '';
- @ViewChild('input') private _inputRef: ElementRef;
- /**
- * True если есть хотя бы один выбранный элемент, иначе false.
- */
- somethingSelected = false;
- /**
- * Элемент списка, обозначающий "ничего не выбрано". Создается динамически методом _createNullChoiceItem().
- */
- nullChoiceItem: DummyItem;
- /**
- * Состояние инпута disabled. Устанавливается через метод {@link setDisabledState}.
- */
- isDisabled = false;
- /**
- * Значение инпута. Выступает как отображение выбранного из списка элемента, однако если изменять это значение непосредственно через
- * инпут оно будет использоваться для поиска.
- */
- searchFormControl = new FormControl({ value: '', disabled: this.isDisabled });
- /**
- * Контроллер оверлей-компонента выпадающего-списка.
- */
- dropdawnListRef: OverlayRef<MultiselectDropdownListComponent>;
- private _values: any[];
- private _formValue: any[];
- private _selectedItemsIndexes = new SubjectHandler<number[]>([]);
- private _items = new SubjectHandler<DummyItem[]>([]);
- private _search = new SubjectHandler<string>('');
- private _keydown = new SubjectHandler<KeyCodes>();
- private _totalSelectedLabel$$ = new ObservableHandler(
- this._translate.get('COMPONENTS.MULTISELECT.TOTAL_SELECTED'),
- );
- private _allSelectedLabel$$ = new ObservableHandler(
- this._translate.get('COMPONENTS.MULTISELECT.ALL_SELECTED'),
- );
- private _searchIsActivated = false;
- private _selectItemSubscription: Subscription;
- private _unselectItemSubscription: Subscription;
- private _openDropdownListSubscription: Subscription;
- private _closeDropdownListSubscription: Subscription;
- private _nullChoiceLabelSubscription: Subscription;
- private _statusChangesSubscription: Subscription;
- private _cancelChangesSubscription: Subscription;
- private _searchFormControlSubscription: Subscription;
- constructor(@Optional() @Host() private _controlContainer: ControlContainer,
- private _translate: TranslateService,
- public viewport: Viewport,
- private _cdr: ChangeDetectorRef) {
- super();
- }
- ngOnInit(): void {
- this.formControl = initFormControl(this.formControl, this.formControlName, MultiselectComponent.name, this._controlContainer);
- if (!isNullOrUndefined(this.formControl)) {
- this._formValue = this.formControl.value;
- this._statusChangesSubscription = this.formControl.statusChanges.subscribe(() => safeDetectChanges(this._cdr));
- }
- this._searchFormControlSubscription = this.searchFormControl.valueChanges.subscribe(value => this._search.emit(value));
- // Инициализация элемента "ничего не выбрано".
- this._createNullChoiceItem();
- }
- ngAfterViewInit(): void {
- // Инициализация оверлей-компонента выпадающего списка.
- this.dropdawnListRef = this._initDropdawnList();
- this._openDropdownListSubscription = this.dropdawnListRef.attach.subscribe(this._afterOpenDropdownList.bind(this));
- this._closeDropdownListSubscription = this.dropdawnListRef.detach.subscribe(this._afterCloseDropdownList.bind(this));
- if (event) {
- setTimeout(() => {
- this._inputRef.nativeElement.focus();
- }, 0);
- }
- }
- @HostListener('keydown', ['$event'])
- onKeyDown($event: KeyboardEvent) {
- switch ($event.keyCode) {
- case KeyCodes.KEY_ENTER:
- case KeyCodes.KEY_DOWNARROW:
- case KeyCodes.KEY_UPARROW:
- this._keydown.emit($event.keyCode);
- event.preventDefault();
- break;
- case KeyCodes.KEY_ESCAPE:
- this.dropdawnListRef.detachView();
- (this._inputRef.nativeElement as HTMLElement).blur();
- break;
- }
- }
- toggleDropdawnList($event: MouseEvent): void {
- if (this.dropdawnListRef.viewIsAttached) {
- (this._inputRef.nativeElement as HTMLElement).blur();
- } else {
- (this._inputRef.nativeElement as HTMLElement).focus();
- }
- $event.stopImmediatePropagation();
- $event.preventDefault();
- }
- selectItem(selectedItem: DummyItem): void {
- this.formControl.markAsTouched();
- switch (selectedItem.value) {
- case null:
- this.writeValue([]);
- break;
- case SELECT_ALL:
- this.writeValue(this._items.value.map((item: DummyItem) => item.value));
- break;
- default:
- this.writeValue([...this._values, selectedItem.value]);
- }
- }
- unselectItem(unselectedItem: DummyItem): void {
- switch (unselectedItem.value) {
- case null:
- case SELECT_ALL:
- this.writeValue([]);
- break;
- default:
- const index = this._values.indexOf(unselectedItem.value);
- if (index > -1) {
- const values = this._values.slice(0);
- values.splice(index, 1);
- this.writeValue(values);
- }
- }
- }
- setDisabledState(isDisabled: boolean): void {
- this.isDisabled = isDisabled;
- this.isDisabled ? this.searchFormControl.disable() : this.searchFormControl.enable();
- safeDetectChanges(this._cdr);
- }
- writeValue(values: any[]): void {
- const previousSomethingSelected = this.somethingSelected;
- if (isNullOrUndefined(values)) {
- values = [];
- }
- this._values = values instanceof Array ? values : [values];
- this.onChangeFn(this._values);
- this.somethingSelected = !isNullOrUndefined(this._values) && this._values.length > 0;
- if (this.somethingSelected) {
- this._selectedItemsIndexes.emit(
- this._findItemsIndexes(
- this._items.value,
- this._values,
- ),
- );
- } else {
- this._selectedItemsIndexes.emit([]);
- }
- this.updateInputValue();
- safeDetectChanges(this._cdr);
- if (previousSomethingSelected !== this.somethingSelected && !isNullOrUndefined(this.dropdawnListRef)) {
- this.dropdawnListRef.updatePositionOverTime(150);
- }
- }
- /**
- * Обновляет значение инпута в соответствии с текущим состоянием компонента.
- */
- updateInputValue(): void {
- let newSearchValue: string;
- if (this._searchIsActivated) {
- // Если активирован поиск в инпуте отображается текущая искомая строка или ничего.
- newSearchValue = this._search.value || '';
- } else if (this.somethingSelected) {
- newSearchValue = this._getNewSearchValueIfSomethingSelected();
- } else {
- newSearchValue = this.placeholder || this.nullChoiceItem.label;
- }
- this.searchFormControl.setValue(newSearchValue);
- }
- private _getNewSearchValueIfSomethingSelected(): string {
- let newSearchValue: string;
- if (this._selectedItemsIndexes.value.length === 1) {
- newSearchValue = safe(() => this._items.value[this._selectedItemsIndexes.value[0]].label.toString()) || '';
- } else if (this._selectedItemsIndexes.value.length === this._items.value.length) {
- newSearchValue = this._allSelectedLabel$$.latestValue;
- } else {
- newSearchValue = `${this._selectedItemsIndexes.value.length} ${this._totalSelectedLabel$$.latestValue}`;
- }
- return newSearchValue;
- }
- private _findItemsIndexes(items: DummyItem[], values: any[]): number[] {
- if (isNullOrUndefinedOrEmpty(items, values)) {
- return [];
- }
- const indexes = [];
- const valuesSet = new Set(values.map(val => val.toString()));
- for (let i = 0, len = items.length; i < len; i++) {
- const itemValue = items[i].value.toString();
- if (valuesSet.has(itemValue)) {
- indexes.push(i);
- valuesSet.delete(itemValue);
- if (valuesSet.size === 0) {
- break;
- }
- }
- }
- return indexes;
- }
- private _createNullChoiceItem(): void {
- this.nullChoiceItem = {
- value: null,
- label: '',
- };
- this._nullChoiceLabelSubscription = this._translate.get(this.nullChoiceLabel).subscribe((label) => {
- this.nullChoiceItem.label = label;
- this.updateInputValue();
- safeDetectChanges(this._cdr);
- });
- }
- /**
- * Создает контроллер оверлей-компонента выпадающего списка.
- */
- private _initDropdawnList(): OverlayRef<MultiselectDropdownListComponent> {
- let strategy: PositionStrategyWithSettings;
- if (Viewport.isDesktop) {
- strategy = {
- strategy: PositionStrategies.Connected,
- parentElement: this._inputRef.nativeElement,
- parentAnchorPoint: AnchorPoints.BottomCenter,
- childAnchorPoint: AnchorPoints.TopCenter,
- inheritParent: { width: true },
- };
- }
- return Overlay.instance.createOverlay(
- MultiselectDropdownListComponent,
- {
- positionStrategy: strategy || PositionStrategies.Center,
- scrollStrategy: Viewport.isMobile ? ScrollStrategies.BlockScroll : null,
- closeOnClickOut: false,
- mobileFullHeight: true,
- autoBlurOnAttach: Viewport.isMobile,
- inputData: as<MultiselectDropdownMenuData>({
- selectedItemIndexes$: this._selectedItemsIndexes.observable,
- items$: this._items.observable,
- search$: this._search.observable,
- keydown$: this._keydown.observable,
- nullChoiceItem: this.nullChoiceItem,
- // for mobile
- label: this.label,
- searchFormControl: this.searchFormControl,
- }),
- },
- );
- }
- /**
- * Коллбэк, вызываемый после открытия выпадающего списка ({@link dropdawnListRef}).<br>
- * 1. Подписывается на ивенты, исходящие из оверлей-компонента выпадающего списка (такой механизм взаимодействия обусловлен тем, что до
- * момента открытия оверлей-компонент фактически не существует);
- * 2. Обновляет потоки данных со списком доступных значений и индексами уже выбранных значений;
- * 3. Включает режим поиска для инпута и обновляет его.
- */
- private _afterOpenDropdownList(): void {
- const instance = this.dropdawnListRef.componentRef.instance;
- this._selectItemSubscription = instance.select.subscribe(this.selectItem.bind(this));
- this._unselectItemSubscription = instance.unselect.subscribe(this.unselectItem.bind(this));
- this._cancelChangesSubscription = instance.cancel.subscribe(this.writeValue.bind(this, this._formValue));
- this._items.reemit();
- this._selectedItemsIndexes.reemit();
- this._searchIsActivated = true;
- this._search.emit('');
- this.updateInputValue();
- safeDetectChanges(this._cdr);
- }
- /**
- * Коллбэк, вызываемый после закрытия выпадающего списка ({@link dropdawnListRef}).<br>
- * 1. Отписывается от ивентов, исходящие из оверлей-компонента выпадающего списка, а так же обновляет значение, оторбажаемое в инпуте;
- * 2. Сбрасывает поиск и обновляет инпут;
- */
- private _afterCloseDropdownList(): void {
- this._selectItemSubscription.unsubscribe();
- this._unselectItemSubscription.unsubscribe();
- this._cancelChangesSubscription.unsubscribe();
- this._formValue = this.formControl.value;
- this._searchIsActivated = false;
- this._search.emit('');
- this.updateInputValue();
- this._cdr.detectChanges();
- this.formControl.markAsTouched();
- }
- ngOnDestroy(): void {}
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement