Advertisement
Guest User

Untitled

a guest
Dec 9th, 2019
109
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.17 KB | None | 0 0
  1. import {
  2. AfterViewInit,
  3. ChangeDetectionStrategy,
  4. ChangeDetectorRef,
  5. Component, ElementRef,
  6. forwardRef,
  7. Host,
  8. HostListener,
  9. Input,
  10. OnDestroy,
  11. OnInit,
  12. Optional,
  13. ViewChild,
  14. } from '@angular/core';
  15. import { ControlContainer, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
  16.  
  17. import { Subscription } from 'rxjs';
  18. import { TranslateService } from '@ngx-translate/core';
  19.  
  20. import {
  21. MultiselectDropdownListComponent,
  22. MultiselectDropdownMenuData,
  23. SELECT_ALL,
  24. } from '@shared/components/multiselect/multiselect-dropdawn-list/multiselect-dropdown-list.component';
  25. import { KeyCodes } from '@shared/enums';
  26. import { Viewport } from '@shared/modules/viewport';
  27. import { DummyItem } from '@shared/interfaces';
  28. import { AutoUnsubscribe } from '@shared/decorators';
  29. import { ScrollStrategies } from '@shared/modules/overlay/enums/scroll-strategies.enum';
  30. import { ControlValueAccessorClass } from '@shared/classes';
  31. import { PositionStrategyWithSettings } from '@shared/modules/overlay/models/overlay-config.model';
  32. import { ObservableHandler, SubjectHandler } from '@shared/models';
  33. import { AnchorPoints, Overlay, OverlayRef, PositionStrategies } from '@shared/modules/overlay';
  34. import { as, initFormControl, isNullOrUndefined, isNullOrUndefinedOrEmpty, safe, safeDetectChanges } from '@shared/utils';
  35. import {PrimaryFiltersModalData} from '@app/ads/ad-findings/primary-filters-modal/primary-filters-modal.component';
  36.  
  37. const DEFAULT_NULL_CHOICE_LABEL = 'COMPONENTS.SELECT.DEFAULT_NULL_CHOICE';
  38.  
  39. @AutoUnsubscribe
  40. @Component({
  41. selector: 'sm-multiselect',
  42. templateUrl: './multiselect.component.html',
  43. styleUrls: ['./multiselect.component.scss'],
  44. changeDetection: ChangeDetectionStrategy.OnPush,
  45. providers: [
  46. { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MultiselectComponent), multi: true },
  47. { provide: NG_VALIDATORS, useExisting: forwardRef(() => MultiselectComponent), multi: true },
  48. ],
  49. })
  50. export class MultiselectComponent extends ControlValueAccessorClass implements OnInit, AfterViewInit, OnDestroy {
  51. readonly isDesktop = Viewport.isDesktop;
  52.  
  53. /**
  54. * Имя свойства у выбираемых значений/объектов, которое будет использоваться в качестве итогового (выбранного) значения.
  55. */
  56. @Input() itemsPropertyAsValue = 'key';
  57.  
  58. /**
  59. * Имя свойства у выбираемых значений/объектов, которое будет использоваться в качестве отображаемого названия.
  60. */
  61. @Input() itemPropertyAsLabel = 'value';
  62.  
  63. /**
  64. * Имя свойства у выбираемых значений/объектов, которое будет использоваться для определения приоритетных значений.
  65. */
  66. @Input() itemsPropertyAsPriority = 'isPriority';
  67.  
  68. /**
  69. * Список значений, доступных для выбора. Отображается в выпадающем меню ({@link dropdawnListRef}).
  70. */
  71. @Input() set items(sourceItems: Object[]) {
  72. if (sourceItems instanceof Array) {
  73. this._items.emit(sourceItems.map((item: Object) => ({
  74. value: item.getProperty(this.itemsPropertyAsValue),
  75. label: item.getProperty(this.itemPropertyAsLabel),
  76. priority: item.getProperty(this.itemsPropertyAsPriority),
  77. })));
  78. }
  79. }
  80.  
  81. @Input() title: string;
  82. @Input() placeholder: string;
  83. @Input() formControl: FormControl;
  84. @Input() formControlName: string;
  85. @Input() event: PrimaryFiltersModalData;
  86.  
  87. /**
  88. * Кастомные классы. Применяются к div.sm-form-field.
  89. */
  90. @Input() customClass: string | string[];
  91.  
  92. /**
  93. * Ключ перевода для отображаемоего названия варианта "ничего не выбрано".
  94. */
  95. @Input() nullChoiceLabel = DEFAULT_NULL_CHOICE_LABEL;
  96.  
  97. /**
  98. * Название фильтра, к которому относится компонент
  99. */
  100. @Input() label = '';
  101.  
  102. @ViewChild('input') private _inputRef: ElementRef;
  103.  
  104.  
  105. /**
  106. * True если есть хотя бы один выбранный элемент, иначе false.
  107. */
  108. somethingSelected = false;
  109.  
  110. /**
  111. * Элемент списка, обозначающий "ничего не выбрано". Создается динамически методом _createNullChoiceItem().
  112. */
  113. nullChoiceItem: DummyItem;
  114.  
  115. /**
  116. * Состояние инпута disabled. Устанавливается через метод {@link setDisabledState}.
  117. */
  118. isDisabled = false;
  119.  
  120. /**
  121. * Значение инпута. Выступает как отображение выбранного из списка элемента, однако если изменять это значение непосредственно через
  122. * инпут оно будет использоваться для поиска.
  123. */
  124. searchFormControl = new FormControl({ value: '', disabled: this.isDisabled });
  125.  
  126. /**
  127. * Контроллер оверлей-компонента выпадающего-списка.
  128. */
  129. dropdawnListRef: OverlayRef<MultiselectDropdownListComponent>;
  130.  
  131. private _values: any[];
  132. private _formValue: any[];
  133. private _selectedItemsIndexes = new SubjectHandler<number[]>([]);
  134. private _items = new SubjectHandler<DummyItem[]>([]);
  135. private _search = new SubjectHandler<string>('');
  136. private _keydown = new SubjectHandler<KeyCodes>();
  137. private _totalSelectedLabel$$ = new ObservableHandler(
  138. this._translate.get('COMPONENTS.MULTISELECT.TOTAL_SELECTED'),
  139. );
  140. private _allSelectedLabel$$ = new ObservableHandler(
  141. this._translate.get('COMPONENTS.MULTISELECT.ALL_SELECTED'),
  142. );
  143. private _searchIsActivated = false;
  144. private _selectItemSubscription: Subscription;
  145. private _unselectItemSubscription: Subscription;
  146. private _openDropdownListSubscription: Subscription;
  147. private _closeDropdownListSubscription: Subscription;
  148. private _nullChoiceLabelSubscription: Subscription;
  149. private _statusChangesSubscription: Subscription;
  150. private _cancelChangesSubscription: Subscription;
  151. private _searchFormControlSubscription: Subscription;
  152.  
  153. constructor(@Optional() @Host() private _controlContainer: ControlContainer,
  154. private _translate: TranslateService,
  155. public viewport: Viewport,
  156. private _cdr: ChangeDetectorRef) {
  157. super();
  158. }
  159.  
  160. ngOnInit(): void {
  161. this.formControl = initFormControl(this.formControl, this.formControlName, MultiselectComponent.name, this._controlContainer);
  162. if (!isNullOrUndefined(this.formControl)) {
  163. this._formValue = this.formControl.value;
  164. this._statusChangesSubscription = this.formControl.statusChanges.subscribe(() => safeDetectChanges(this._cdr));
  165. }
  166. this._searchFormControlSubscription = this.searchFormControl.valueChanges.subscribe(value => this._search.emit(value));
  167. // Инициализация элемента "ничего не выбрано".
  168. this._createNullChoiceItem();
  169. }
  170.  
  171. ngAfterViewInit(): void {
  172. // Инициализация оверлей-компонента выпадающего списка.
  173. this.dropdawnListRef = this._initDropdawnList();
  174. this._openDropdownListSubscription = this.dropdawnListRef.attach.subscribe(this._afterOpenDropdownList.bind(this));
  175. this._closeDropdownListSubscription = this.dropdawnListRef.detach.subscribe(this._afterCloseDropdownList.bind(this));
  176. if (event) {
  177. setTimeout(() => {
  178. this._inputRef.nativeElement.focus();
  179. }, 0);
  180. }
  181. }
  182. @HostListener('keydown', ['$event'])
  183. onKeyDown($event: KeyboardEvent) {
  184. switch ($event.keyCode) {
  185. case KeyCodes.KEY_ENTER:
  186. case KeyCodes.KEY_DOWNARROW:
  187. case KeyCodes.KEY_UPARROW:
  188. this._keydown.emit($event.keyCode);
  189. event.preventDefault();
  190. break;
  191. case KeyCodes.KEY_ESCAPE:
  192. this.dropdawnListRef.detachView();
  193. (this._inputRef.nativeElement as HTMLElement).blur();
  194. break;
  195. }
  196. }
  197.  
  198. toggleDropdawnList($event: MouseEvent): void {
  199. if (this.dropdawnListRef.viewIsAttached) {
  200. (this._inputRef.nativeElement as HTMLElement).blur();
  201. } else {
  202. (this._inputRef.nativeElement as HTMLElement).focus();
  203. }
  204. $event.stopImmediatePropagation();
  205. $event.preventDefault();
  206. }
  207.  
  208. selectItem(selectedItem: DummyItem): void {
  209. this.formControl.markAsTouched();
  210. switch (selectedItem.value) {
  211. case null:
  212. this.writeValue([]);
  213. break;
  214. case SELECT_ALL:
  215. this.writeValue(this._items.value.map((item: DummyItem) => item.value));
  216. break;
  217. default:
  218. this.writeValue([...this._values, selectedItem.value]);
  219. }
  220. }
  221.  
  222. unselectItem(unselectedItem: DummyItem): void {
  223. switch (unselectedItem.value) {
  224. case null:
  225. case SELECT_ALL:
  226. this.writeValue([]);
  227. break;
  228. default:
  229. const index = this._values.indexOf(unselectedItem.value);
  230. if (index > -1) {
  231. const values = this._values.slice(0);
  232. values.splice(index, 1);
  233. this.writeValue(values);
  234. }
  235. }
  236. }
  237.  
  238. setDisabledState(isDisabled: boolean): void {
  239. this.isDisabled = isDisabled;
  240. this.isDisabled ? this.searchFormControl.disable() : this.searchFormControl.enable();
  241. safeDetectChanges(this._cdr);
  242. }
  243.  
  244. writeValue(values: any[]): void {
  245. const previousSomethingSelected = this.somethingSelected;
  246.  
  247. if (isNullOrUndefined(values)) {
  248. values = [];
  249. }
  250.  
  251. this._values = values instanceof Array ? values : [values];
  252. this.onChangeFn(this._values);
  253. this.somethingSelected = !isNullOrUndefined(this._values) && this._values.length > 0;
  254.  
  255. if (this.somethingSelected) {
  256. this._selectedItemsIndexes.emit(
  257. this._findItemsIndexes(
  258. this._items.value,
  259. this._values,
  260. ),
  261. );
  262. } else {
  263. this._selectedItemsIndexes.emit([]);
  264. }
  265.  
  266. this.updateInputValue();
  267. safeDetectChanges(this._cdr);
  268. if (previousSomethingSelected !== this.somethingSelected && !isNullOrUndefined(this.dropdawnListRef)) {
  269. this.dropdawnListRef.updatePositionOverTime(150);
  270. }
  271. }
  272.  
  273. /**
  274. * Обновляет значение инпута в соответствии с текущим состоянием компонента.
  275. */
  276. updateInputValue(): void {
  277. let newSearchValue: string;
  278.  
  279. if (this._searchIsActivated) {
  280. // Если активирован поиск в инпуте отображается текущая искомая строка или ничего.
  281. newSearchValue = this._search.value || '';
  282. } else if (this.somethingSelected) {
  283. newSearchValue = this._getNewSearchValueIfSomethingSelected();
  284. } else {
  285. newSearchValue = this.placeholder || this.nullChoiceItem.label;
  286. }
  287.  
  288. this.searchFormControl.setValue(newSearchValue);
  289. }
  290.  
  291. private _getNewSearchValueIfSomethingSelected(): string {
  292. let newSearchValue: string;
  293.  
  294. if (this._selectedItemsIndexes.value.length === 1) {
  295. newSearchValue = safe(() => this._items.value[this._selectedItemsIndexes.value[0]].label.toString()) || '';
  296.  
  297. } else if (this._selectedItemsIndexes.value.length === this._items.value.length) {
  298. newSearchValue = this._allSelectedLabel$$.latestValue;
  299.  
  300. } else {
  301. newSearchValue = `${this._selectedItemsIndexes.value.length} ${this._totalSelectedLabel$$.latestValue}`;
  302. }
  303.  
  304. return newSearchValue;
  305. }
  306.  
  307. private _findItemsIndexes(items: DummyItem[], values: any[]): number[] {
  308. if (isNullOrUndefinedOrEmpty(items, values)) {
  309. return [];
  310. }
  311.  
  312. const indexes = [];
  313. const valuesSet = new Set(values.map(val => val.toString()));
  314.  
  315. for (let i = 0, len = items.length; i < len; i++) {
  316. const itemValue = items[i].value.toString();
  317. if (valuesSet.has(itemValue)) {
  318. indexes.push(i);
  319. valuesSet.delete(itemValue);
  320. if (valuesSet.size === 0) {
  321. break;
  322. }
  323. }
  324. }
  325.  
  326. return indexes;
  327. }
  328.  
  329. private _createNullChoiceItem(): void {
  330. this.nullChoiceItem = {
  331. value: null,
  332. label: '',
  333. };
  334. this._nullChoiceLabelSubscription = this._translate.get(this.nullChoiceLabel).subscribe((label) => {
  335. this.nullChoiceItem.label = label;
  336. this.updateInputValue();
  337. safeDetectChanges(this._cdr);
  338. });
  339. }
  340.  
  341. /**
  342. * Создает контроллер оверлей-компонента выпадающего списка.
  343. */
  344. private _initDropdawnList(): OverlayRef<MultiselectDropdownListComponent> {
  345. let strategy: PositionStrategyWithSettings;
  346. if (Viewport.isDesktop) {
  347. strategy = {
  348. strategy: PositionStrategies.Connected,
  349. parentElement: this._inputRef.nativeElement,
  350. parentAnchorPoint: AnchorPoints.BottomCenter,
  351. childAnchorPoint: AnchorPoints.TopCenter,
  352. inheritParent: { width: true },
  353. };
  354. }
  355.  
  356. return Overlay.instance.createOverlay(
  357. MultiselectDropdownListComponent,
  358. {
  359. positionStrategy: strategy || PositionStrategies.Center,
  360. scrollStrategy: Viewport.isMobile ? ScrollStrategies.BlockScroll : null,
  361. closeOnClickOut: false,
  362. mobileFullHeight: true,
  363. autoBlurOnAttach: Viewport.isMobile,
  364. inputData: as<MultiselectDropdownMenuData>({
  365. selectedItemIndexes$: this._selectedItemsIndexes.observable,
  366. items$: this._items.observable,
  367. search$: this._search.observable,
  368. keydown$: this._keydown.observable,
  369. nullChoiceItem: this.nullChoiceItem,
  370. // for mobile
  371. label: this.label,
  372. searchFormControl: this.searchFormControl,
  373. }),
  374. },
  375. );
  376. }
  377.  
  378. /**
  379. * Коллбэк, вызываемый после открытия выпадающего списка ({@link dropdawnListRef}).<br>
  380. * 1. Подписывается на ивенты, исходящие из оверлей-компонента выпадающего списка (такой механизм взаимодействия обусловлен тем, что до
  381. * момента открытия оверлей-компонент фактически не существует);
  382. * 2. Обновляет потоки данных со списком доступных значений и индексами уже выбранных значений;
  383. * 3. Включает режим поиска для инпута и обновляет его.
  384. */
  385. private _afterOpenDropdownList(): void {
  386. const instance = this.dropdawnListRef.componentRef.instance;
  387. this._selectItemSubscription = instance.select.subscribe(this.selectItem.bind(this));
  388. this._unselectItemSubscription = instance.unselect.subscribe(this.unselectItem.bind(this));
  389. this._cancelChangesSubscription = instance.cancel.subscribe(this.writeValue.bind(this, this._formValue));
  390. this._items.reemit();
  391. this._selectedItemsIndexes.reemit();
  392. this._searchIsActivated = true;
  393. this._search.emit('');
  394. this.updateInputValue();
  395. safeDetectChanges(this._cdr);
  396. }
  397.  
  398. /**
  399. * Коллбэк, вызываемый после закрытия выпадающего списка ({@link dropdawnListRef}).<br>
  400. * 1. Отписывается от ивентов, исходящие из оверлей-компонента выпадающего списка, а так же обновляет значение, оторбажаемое в инпуте;
  401. * 2. Сбрасывает поиск и обновляет инпут;
  402. */
  403. private _afterCloseDropdownList(): void {
  404. this._selectItemSubscription.unsubscribe();
  405. this._unselectItemSubscription.unsubscribe();
  406. this._cancelChangesSubscription.unsubscribe();
  407. this._formValue = this.formControl.value;
  408. this._searchIsActivated = false;
  409. this._search.emit('');
  410. this.updateInputValue();
  411. this._cdr.detectChanges();
  412. this.formControl.markAsTouched();
  413. }
  414.  
  415. ngOnDestroy(): void {}
  416. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement