From f88bd6f741c3c4777817d4f7b5a50c6997e4697d Mon Sep 17 00:00:00 2001 From: Yury Betrozov Date: Fri, 11 Feb 2022 14:50:05 +0200 Subject: [PATCH] fix(platform): multi-combobox, fix selection in mobile mode (#7483) * fix(platform): multi-combobox, fix selection in mobile mode * fix(platform): multi-combobox, add states support * fix(platform): minor fixes of selection and unit tests * fix(platform): fix conflicts * fix(platform): fix failed e2e test * fix(platform): fix e2e Co-authored-by: Evgeny Beregovoy --- ...combobox-datasource-example.component.html | 2 +- ...lti-combobox-states-example.component.html | 68 ++++++ ...multi-combobox-states-example.component.ts | 38 ++++ ...latform-multi-combobox-docs.component.html | 14 ++ .../platform-multi-combobox-docs.component.ts | 16 ++ .../platform-multi-combobox-docs.module.ts | 10 +- .../auto-complete/auto-complete.directive.ts | 1 + .../commons/base-multi-combobox.ts | 207 +++++++++++++----- .../multi-combobox.component.html | 182 ++++++++------- .../multi-combobox.component.scss | 12 + .../multi-combobox.component.ts | 73 ++---- 11 files changed, 416 insertions(+), 207 deletions(-) create mode 100644 apps/docs/src/app/platform/component-docs/platform-multi-combobox/examples/multi-combobox-states/multi-combobox-states-example.component.html create mode 100644 apps/docs/src/app/platform/component-docs/platform-multi-combobox/examples/multi-combobox-states/multi-combobox-states-example.component.ts diff --git a/apps/docs/src/app/platform/component-docs/platform-multi-combobox/examples/multi-combobox-datasource/multi-combobox-datasource-example.component.html b/apps/docs/src/app/platform/component-docs/platform-multi-combobox/examples/multi-combobox-datasource/multi-combobox-datasource-example.component.html index 6a30576cf9e..75e1b67e48d 100644 --- a/apps/docs/src/app/platform/component-docs/platform-multi-combobox/examples/multi-combobox-datasource/multi-combobox-datasource-example.component.html +++ b/apps/docs/src/app/platform/component-docs/platform-multi-combobox/examples/multi-combobox-datasource/multi-combobox-datasource-example.component.html @@ -54,7 +54,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/src/app/platform/component-docs/platform-multi-combobox/examples/multi-combobox-states/multi-combobox-states-example.component.ts b/apps/docs/src/app/platform/component-docs/platform-multi-combobox/examples/multi-combobox-states/multi-combobox-states-example.component.ts new file mode 100644 index 00000000000..2e5dc85197a --- /dev/null +++ b/apps/docs/src/app/platform/component-docs/platform-multi-combobox/examples/multi-combobox-states/multi-combobox-states-example.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; +import { ComboboxSelectionChangeEvent } from '@fundamental-ngx/platform/form'; + +import { DATA_PROVIDERS } from '@fundamental-ngx/platform/shared'; + +@Component({ + selector: 'fdp-multi-combobox-states-example', + templateUrl: './multi-combobox-states-example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + providers: [{ provide: DATA_PROVIDERS, useValue: new Map() }], + styles: [ + ` + fdp-form-group .fd-row__form-item > .fd-col:first-child { + margin-bottom: 1rem; + } + ` + ] +}) +export class MultiComboboxStatesExampleComponent { + dataSource = [ + { name: 'Apple' }, + { name: 'Banana' }, + { name: 'Pineapple' }, + { name: 'Strawberry' }, + { name: 'Broccoli' }, + { name: 'Carrot' }, + { name: 'JalapeƱo' }, + { name: 'Spinach' } + ]; + + states = ['Default', 'Success', 'Error', 'Warning', 'Information']; + selectedState = this.states[0]; + + onSelectState(item: ComboboxSelectionChangeEvent): void { + this.selectedState = item.payload; + } +} diff --git a/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.component.html b/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.component.html index 31cb27b5e6f..e678d4d7a0c 100644 --- a/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.component.html +++ b/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.component.html @@ -61,6 +61,20 @@ + Multi Combobox states. + + The Multi Combobox component with states. +
    +
  • Set [state]property to: 'success' | 'error' | 'warning' | 'information'
  • +
+
+ + + + + + + Reactive Form The Multi-Combobox component may also be used within Angular Reactive Forms. diff --git a/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.component.ts b/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.component.ts index 87468be0a1e..c56e75f18bc 100644 --- a/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.component.ts +++ b/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.component.ts @@ -12,6 +12,8 @@ import multiComboboxGroupHtml from '!./examples/multi-combobox-group/multi-combo import multiComboboxGroupTs from '!./examples/multi-combobox-group/multi-combobox-group-example.component?raw'; import multiComboboxColumnsHtml from '!./examples/multi-combobox-columns/multi-combobox-columns-example.component.html?raw'; import multiComboboxColumnsTs from '!./examples/multi-combobox-columns/multi-combobox-columns-example.component?raw'; +import multiComboboxStatesHtml from '!./examples/multi-combobox-states/multi-combobox-states-example.component.html?raw'; +import multiComboboxStatesTs from '!./examples/multi-combobox-states/multi-combobox-states-example.component?raw'; import multiComboboxLoadingHtml from '!./examples/multi-combobox-loading/multi-combobox-loading-example.component.html?raw'; import multiComboboxLoadingTs from '!./examples/multi-combobox-loading/multi-combobox-loading-example.component?raw'; @@ -90,6 +92,20 @@ export class PlatformMultiComboboxDocsComponent { } ]; + multiComboboxStatesExample: ExampleFile[] = [ + { + language: 'html', + fileName: 'multi-combobox-states-example', + code: multiComboboxStatesHtml + }, + { + language: 'typescript', + fileName: 'multi-combobox-states-example', + code: multiComboboxStatesTs, + component: 'MultiComboboxStatesExampleComponent' + } + ]; + multiComboboxLoadingExample: ExampleFile[] = [ { language: 'html', diff --git a/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.module.ts b/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.module.ts index 656f50609af..73c15c6e2ba 100644 --- a/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.module.ts +++ b/apps/docs/src/app/platform/component-docs/platform-multi-combobox/platform-multi-combobox-docs.module.ts @@ -2,7 +2,11 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { MOBILE_MODE_CONFIG } from '@fundamental-ngx/core/mobile-mode'; -import { FdpFormGroupModule, PlatformMultiComboboxModule } from '@fundamental-ngx/platform/form'; +import { + FdpFormGroupModule, + PlatformMultiComboboxModule, + PlatformComboboxModule +} from '@fundamental-ngx/platform/form'; import { BusyIndicatorModule } from '@fundamental-ngx/core/busy-indicator'; import { ApiComponent } from '../../../documentation/core-helpers/api/api.component'; @@ -16,6 +20,7 @@ import { MultiComboboxMobileExampleComponent } from './examples/multi-combobox-m import { MultiComboboxGroupExampleComponent } from './examples/multi-combobox-group/multi-combobox-group-example.component'; import { MultiComboboxColumnsExampleComponent } from './examples/multi-combobox-columns/multi-combobox-columns-example.component'; import { MultiComboboxFormsExampleComponent } from './examples/multi-combobox-forms/multi-combobox-forms-example.component'; +import { MultiComboboxStatesExampleComponent } from './examples/multi-combobox-states/multi-combobox-states-example.component'; import { MultiComboboxLoadingExampleComponent } from './examples/multi-combobox-loading/multi-combobox-loading-example.component'; const routes: Routes = [ @@ -35,6 +40,8 @@ const routes: Routes = [ SharedDocumentationPageModule, FdpFormGroupModule, PlatformMultiComboboxModule, + PlatformComboboxModule, + PlatformMultiComboboxModule, BusyIndicatorModule ], exports: [RouterModule], @@ -47,6 +54,7 @@ const routes: Routes = [ MultiComboboxGroupExampleComponent, MultiComboboxColumnsExampleComponent, MultiComboboxFormsExampleComponent, + MultiComboboxStatesExampleComponent, MultiComboboxLoadingExampleComponent ] }) diff --git a/libs/platform/src/lib/form/auto-complete/auto-complete.directive.ts b/libs/platform/src/lib/form/auto-complete/auto-complete.directive.ts index ea59319695d..b9458c0c9a0 100644 --- a/libs/platform/src/lib/form/auto-complete/auto-complete.directive.ts +++ b/libs/platform/src/lib/form/auto-complete/auto-complete.directive.ts @@ -82,6 +82,7 @@ export class AutoCompleteDirective { this._oldValue = this.inputText; const item = this._searchByStrategy(); + if (item) { this._typeahead(item.label); } diff --git a/libs/platform/src/lib/form/multi-combobox/commons/base-multi-combobox.ts b/libs/platform/src/lib/form/multi-combobox/commons/base-multi-combobox.ts index e4ca9ecf3f8..207e20c00bb 100644 --- a/libs/platform/src/lib/form/multi-combobox/commons/base-multi-combobox.ts +++ b/libs/platform/src/lib/form/multi-combobox/commons/base-multi-combobox.ts @@ -17,6 +17,7 @@ import { ViewChild } from '@angular/core'; import { NgControl, NgForm } from '@angular/forms'; +import { SafeHtml } from '@angular/platform-browser'; import { BACKSPACE, CONTROL, @@ -30,8 +31,9 @@ import { UP_ARROW } from '@angular/cdk/keycodes'; -import { BehaviorSubject, fromEvent, isObservable, Observable, Subject, Subscription } from 'rxjs'; -import { filter, takeUntil, tap } from 'rxjs/operators'; +import { BehaviorSubject, fromEvent, isObservable, skip, Observable, Subject, Subscription, timer } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import equal from 'fast-deep-equal'; import { DialogConfig } from '@fundamental-ngx/core/dialog'; import { ContentDensity, FocusEscapeDirection, KeyUtil, TemplateDirective } from '@fundamental-ngx/core/utils'; @@ -57,6 +59,7 @@ import { ObservableMultiComboBoxDataSource, SelectableOptionItem } from '@fundamental-ngx/platform/shared'; + import { TextAlignment } from '../../combobox'; import { MultiComboboxConfig } from '../multi-combobox.config'; @@ -101,6 +104,16 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A @Input() autoComplete = true; + /** + * Content Density of element. + * Can be 'cozy', 'compact'. + */ + @Input() + set contentDensity(contentDensity: ContentDensity) { + this._contentDensity = contentDensity; + this.isCompact = contentDensity === 'compact'; + } + /** * TODO: Name of the entity for which DataProvider will be loaded. You can either pass list of * items or use this entityClass and internally we should be able to do lookup to some registry @@ -123,11 +136,11 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A /** A field name to use to group data by (support dotted notation). */ @Input() - groupKey?: string; + groupKey: string; /** The field to show data in secondary column. */ @Input() - secondaryKey?: string; + secondaryKey: string; /** Show the second column (applicable for two columns layout). */ @Input() @@ -160,7 +173,11 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A /** Sets title attribute to addon button. */ @Input() - addonIconTitle: string = null; + addonIconTitle = 'Select Options'; + + /** Sets invalid entry message. */ + @Input() + invalidEntryMessage = 'Invalid entry'; /** Event emitted when item is selected. */ @Output() @@ -190,31 +207,38 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A @ContentChildren(TemplateDirective) customTemplates: QueryList; - /** @hidden + /** + * @hidden * Custom Option item Template. - * */ + */ optionItemTemplate: TemplateRef; - /** @hidden + /** + * @hidden * Custom Group Header item Template. - * */ + */ groupItemTemplate: TemplateRef; - /** @hidden + /** + * @hidden * Custom Secondary item Template. - * */ + */ secondaryItemTemplate: TemplateRef; - /** @hidden + /** + * @hidden * Custom Selected option item Template. - * */ + */ selectedItemTemplate: TemplateRef; /** @hidden */ _contentDensity: ContentDensity = this.multiComboboxConfig.contentDensity; - /** @hidden */ - controlTemplate: TemplateRef; + /** + * @hidden + * Whether "contentDensity" is "compact". + */ + isCompact: boolean = this._contentDensity === 'compact'; /** @hidden */ listTemplate: TemplateRef; @@ -244,35 +268,41 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A /** Whether the Multi Input is opened. */ isOpen = false; - /** @hidden + /** + * @hidden * List of matched suggestions - * */ + */ _suggestions: SelectableOptionItem[]; - /** @hidden + /** + * @hidden * Grouped suggestions mapped to array. - * */ + */ _flatSuggestions: SelectableOptionItem[]; /** @hidden */ _fullFlatSuggestions: SelectableOptionItem[]; - /** @hidden + /** + * @hidden * List of selected suggestions - * */ + */ _selectedSuggestions: SelectableOptionItem[]; - /** @hidden + /** + * @hidden * Max width of list container - * */ - maxWidth?: number; + */ + maxWidth: number; - /** @hidden + /** + * @hidden * Min width of list container - * */ - minWidth?: number; + */ + minWidth: number; - /** @hidden + /** + * @hidden * Need for opening mobile version */ openChange = new Subject(); @@ -280,25 +310,25 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A /** @hidden */ selectedShown$ = new BehaviorSubject(false); - /** @hidden */ - isSearchInvalid = false; - - /** @hidden */ - isListEmpty = true; - /** @hidden */ protected _dataSource: FdpMultiComboboxDataSource; /** @hidden */ private _inputTextValue: string; + /** @hidden */ private _matchingStrategy: MatchingStrategy = this.multiComboboxConfig.matchingStrategy; + /** @hidden */ private _dsSubscription?: Subscription; + /** @hidden */ private _element: HTMLElement = this.elementRef.nativeElement; - /** @hidden - * Keys, that won't trigger the popover's open state, when dispatched on search input. */ + + /** + * @hidden + * Keys, that won't trigger the popover's open state, when dispatched on search input. + */ private readonly _nonOpeningKeys: number[] = [ BACKSPACE, ESCAPE, @@ -312,6 +342,15 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A LEFT_ARROW ]; + /** @hidden */ + private _timerSub$: Subscription; + + /** @hidden */ + private _previousState: 'success' | 'error' | 'warning' | 'default' | 'information'; + + /** @hidden */ + private _previousStateMessage: string | SafeHtml; + /** @hidden */ private _displayFn = (value: any): string => this.displayValue(value); @@ -361,19 +400,22 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A } } - /** @hidden + /** + * @hidden * Method to set input text as item label. - * */ + */ abstract setInputTextFromOptionItem(item: SelectableOptionItem): void; - /** @hidden + /** + * @hidden * Toggle item selection. - * */ + */ abstract toggleSelection(item: SelectableOptionItem): void; - /** @hidden + /** + * @hidden * Toggle item selection by input text value. - * */ + */ abstract toggleSelectionByInputText(): void; /** write value for ControlValueAccessor */ @@ -385,7 +427,7 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A } /** @hidden */ - popoverOpenChangeHandle(isOpen): void { + popoverOpenChangeHandle(isOpen: boolean): void { this.isOpen = isOpen; } @@ -427,6 +469,7 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A if (text) { this.open(); } + const map = new Map(); map.set('query', text); map.set('limit', 12); @@ -482,7 +525,7 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A event.stopPropagation(); this.showList(false); - } else if (!event.ctrlKey && !KeyUtil.isKeyCode(event, this._nonOpeningKeys)) { + } else if (!KeyUtil.isKeyCode(event, [...this._nonOpeningKeys, CONTROL])) { this.showList(true); const acceptedKeys = !KeyUtil.isKeyType(event, 'alphabetical') && !KeyUtil.isKeyType(event, 'numeric'); if (this.isEmptyValue && acceptedKeys) { @@ -561,10 +604,10 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A return; } - const activeValue: SelectableOptionItem = this._getSelectItemByInputValue(this.inputText); - const index: number = this._flatSuggestions.findIndex((value) => value === activeValue); + const activeValue = this._getSelectItemByInputValue(this.inputText); + const index = this._flatSuggestions.findIndex((value) => value === activeValue); const position = !this.inputText && offset === -1 ? this._flatSuggestions.length - 1 : index + offset; - const item: SelectableOptionItem = this._flatSuggestions[position]; + const item = this._flatSuggestions[position]; if (item) { this.setInputTextFromOptionItem(item); @@ -620,12 +663,15 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A this._dsSubscription = new Subscription(); const dsSub = initDataSource .open() - .pipe( - takeUntil(this._destroyed), - tap((data) => (this.isListEmpty = !data?.length)), - filter((data) => !!data.length) - ) + .pipe(skip(1), takeUntil(this._destroyed)) .subscribe((data) => { + console.log('data', data); + if (data.length === 0) { + this._processingEmptyData(); + + return; + } + this._suggestions = this._convertToOptionItems(data).map((optionItem: SelectableOptionItem) => { const selectedElement = this._selectedSuggestions.find( (selectedItem: SelectableOptionItem) => selectedItem.label === optionItem.label @@ -643,6 +689,18 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A isInitDataSource = false; } + const selectedSuggestionsLength = this._selectedSuggestions.length; + if (selectedSuggestionsLength > 0) { + for (let i = 0; i < selectedSuggestionsLength; i++) { + const selectedSuggestion = this._selectedSuggestions[i]; + const idx = this._suggestions.findIndex((item) => equal(item.value, selectedSuggestion.value)); + + if (idx !== -1) { + this._suggestions[idx].selected = true; + } + } + } + this.stateChanges.next('initDataSource.open().'); this._cd.markForCheck(); @@ -673,6 +731,45 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A return initDataSource; } + /** @hidden */ + private _processingEmptyData(): void { + this.inputText = this.inputText.slice(0, -1); + + this._setInvalidEntry(); + + if (this._timerSub$) { + this._timerSub$.unsubscribe(); + } + + this._timerSub$ = timer(3000).subscribe(() => this._unsetInvalidEntry()); + } + + /** @hidden */ + private _setInvalidEntry(): void { + if (this._previousState || this._previousStateMessage) { + return; + } + + this._previousState = this.state; + this.state = 'error'; + + this._previousStateMessage = this.stateMessage; + this.stateMessage = this.invalidEntryMessage; + + this._cd.markForCheck(); + } + + /** @hidden */ + private _unsetInvalidEntry(): void { + this.state = this._previousState; + this._previousState = null; + + this.stateMessage = this._previousStateMessage; + this._previousStateMessage = null; + + this._cd.markForCheck(); + } + /** @hidden */ private _toDataStream(source: FdpMultiComboboxDataSource): MultiComboBoxDataSource | undefined { if (isDataSource(source)) { @@ -787,6 +884,7 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A } else { selectItem.children = this._convertObjectsToDefaultOptionItems(currentGroup); } + return selectItem; }); } @@ -848,9 +946,10 @@ export abstract class BaseMultiCombobox extends CollectionBaseInput implements A return selectItems; } - /** @hidden + /** + * @hidden * Assign custom templates - * */ + */ private _assignCustomTemplates(): void { this.customTemplates.forEach((template) => { switch (template.getName()) { diff --git a/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.html b/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.html index 71a421f20d1..db943cbd910 100644 --- a/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.html +++ b/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.html @@ -23,83 +23,87 @@ - - + - - {{ token.label }} - + + {{ token.label }} + - - - - - - + + + + + + @@ -108,7 +112,7 @@ class="fdp-multi-combobox-input-group-custom" [state]="status === 'error' ? 'error' : state" [buttonFocusable]="false" - [compact]="contentDensity | isCompactDensity" + [compact]="isCompact" [isControl]="true" [disabled]="disabled || readonly" [isExpanded]="!mobile && isOpen && _suggestions.length > 0" @@ -135,9 +139,8 @@ tabindex="0" [id]="id" [name]="name" - [compact]="contentDensity | isCompactDensity" + [compact]="isCompact" (keydown)="onInputKeydownHandler($event)" - (keyup)="validateOnKeyup($event)" [disabled]="disabled" [(ngModel)]="inputText" (ngModelChange)="searchTermChanged()" @@ -159,14 +162,14 @@ class="fdp-multi-combobox__list fd-list--multi-input" [id]="id + '-result'" role="listbox" - [compact]="contentDensity | isCompactDensity" + [compact]="isCompact" [style.maxHeight]="!mobile && maxHeight" [style.minWidth]="!mobile && minWidth + 'px'" [style.maxWidth]="autoResize && maxWidth + 'px'" [attr.aria-labelledby]="id + '-search'" aria-multiselectable="true" > - + @@ -189,11 +192,11 @@ role="option" [tabindex]="0" (click)="!mobile && close()" - (keyDown)="onItemKeyDownHandler($event, optionItem, i)" + (keyDown)="onItemKeyDownHandler($event, i)" [selected]="optionItem.selected" > @@ -213,11 +216,11 @@ role="option" [tabindex]="0" (click)="!mobile && close()" - (keyDown)="onItemKeyDownHandler($event, optionItem, i)" + (keyDown)="onItemKeyDownHandler($event, i)" [selected]="optionItem.selected" > @@ -287,16 +290,3 @@ - - - - Invalid entry - - diff --git a/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.scss b/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.scss index cefbd690bf7..af7bdeb46e5 100644 --- a/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.scss +++ b/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.scss @@ -28,4 +28,16 @@ } } } + + fd-form-message { + display: block; + max-width: 100%; + } +} + +.fd-dialog__body { + fd-form-message { + display: block; + max-width: 100%; + } } diff --git a/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.ts b/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.ts index 1ad89656be0..846dde3f914 100644 --- a/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.ts +++ b/libs/platform/src/lib/form/multi-combobox/multi-combobox/multi-combobox.component.ts @@ -20,10 +20,10 @@ import { NgControl, NgForm } from '@angular/forms'; import { A, DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW } from '@angular/cdk/keycodes'; import { of } from 'rxjs'; import { delay, takeUntil } from 'rxjs/operators'; +import equal from 'fast-deep-equal'; import { DynamicComponentService, KeyUtil } from '@fundamental-ngx/core/utils'; import { DialogConfig } from '@fundamental-ngx/core/dialog'; -import { TokenizerComponent } from '@fundamental-ngx/core/token'; import { DATA_PROVIDERS, DataProvider, @@ -33,6 +33,7 @@ import { OptionItem, SelectableOptionItem } from '@fundamental-ngx/platform/shared'; + import { BaseMultiCombobox } from '../commons/base-multi-combobox'; import { MultiComboboxMobileComponent } from '../multi-combobox-mobile/multi-combobox/multi-combobox-mobile.component'; import { PlatformMultiComboboxMobileModule } from '../multi-combobox-mobile/multi-combobox-mobile.module'; @@ -49,10 +50,6 @@ import { AutoCompleteEvent } from '../../auto-complete/auto-complete.directive'; providers: [{ provide: FormFieldControl, useExisting: MultiComboboxComponent, multi: true }] }) export class MultiComboboxComponent extends BaseMultiCombobox implements OnInit, AfterViewInit { - /** @hidden */ - @ViewChild('controlTemplate') - controlTemplate: TemplateRef; - /** @hidden */ @ViewChild('mobileControlTemplate') mobileControlTemplate: TemplateRef; @@ -61,18 +58,12 @@ export class MultiComboboxComponent extends BaseMultiCombobox implements OnInit, @ViewChild('listTemplate') listTemplate: TemplateRef; - /** @hidden */ - @ViewChild(TokenizerComponent) - tokenizer: TokenizerComponent; - - /** @hidden + /** + * @hidden * List of selected suggestions - * */ + */ _selectedSuggestions: SelectableOptionItem[] = []; - /** @hidden */ - private _timeout: any; // NodeJS.Timeout - constructor( readonly cd: ChangeDetectorRef, readonly elementRef: ElementRef, @@ -80,10 +71,10 @@ export class MultiComboboxComponent extends BaseMultiCombobox implements OnInit, @Optional() @SkipSelf() readonly ngForm: NgForm, @Optional() readonly dialogConfig: DialogConfig, readonly _dynamicComponentService: DynamicComponentService, - @Optional() @Inject(DATA_PROVIDERS) private providers: Map>, + @Optional() @Inject(DATA_PROVIDERS) private _providers: Map>, readonly _multiComboboxConfig: MultiComboboxConfig, - readonly _viewContainerRef: ViewContainerRef, - readonly _injector: Injector, + private readonly _viewContainerRef: ViewContainerRef, + private readonly _injector: Injector, @Optional() @SkipSelf() @Host() formField: FormField, @Optional() @SkipSelf() @Host() formControl: FormFieldControl ) { @@ -94,7 +85,7 @@ export class MultiComboboxComponent extends BaseMultiCombobox implements OnInit, ngOnInit(): void { super.ngOnInit(); - const providers = this.providers?.size === 0 ? this._multiComboboxConfig.providers : this.providers; + const providers = this._providers?.size === 0 ? this._multiComboboxConfig.providers : this._providers; // if we have both prefer dataSource if (!this.dataSource && this.entityClass && providers.has(this.entityClass)) { this.dataSource = new MultiComboBoxDataSource(providers.get(this.entityClass)); @@ -159,7 +150,7 @@ export class MultiComboboxComponent extends BaseMultiCombobox implements OnInit, /** @hidden */ navigateByTokens(event: KeyboardEvent): void { if (KeyUtil.isKeyCode(event, [DOWN_ARROW, UP_ARROW]) && this.isOpen) { - this.listComponent.items?.first?.focus(); + this.listComponent.items?.first.focus(); } } @@ -189,7 +180,9 @@ export class MultiComboboxComponent extends BaseMultiCombobox implements OnInit, moreClicked(): void { this._suggestions = this.isGroup ? this._convertObjectsToGroupOptionItems(this._selectedSuggestions.map(({ value }) => value)) - : [...this._selectedSuggestions]; + : this._suggestions.filter((value) => + this._selectedSuggestions.some((item) => equal(item.value, value.value)) + ); this.showList(true); this.selectedShown$.next(true); @@ -209,37 +202,6 @@ export class MultiComboboxComponent extends BaseMultiCombobox implements OnInit, } } - /** @hidden */ - validateOnKeyup(event: KeyboardEvent): void { - const isPrintableKey = event.key?.length === 1; - if (!event.shiftKey && !isPrintableKey) { - return; - } - - if (this.inputText && this.isListEmpty) { - this.inputText = this.inputText.slice(0, -1); - - this.isSearchInvalid = true; - this.state = 'error'; - this.inputText ? this.showList(true) : this.showList(false); - - this.searchTermChanged(''); - - if (this._timeout) { - clearTimeout(this._timeout); - } - const threeSeconds = 3000; - this._timeout = setTimeout(() => { - this.isSearchInvalid = false; - this.state = 'default'; - this.cd.markForCheck(); - }, threeSeconds); - } else { - this.isSearchInvalid = false; - this.state = 'default'; - } - } - /** * @hidden * Method to set input text as item label. @@ -255,7 +217,7 @@ export class MultiComboboxComponent extends BaseMultiCombobox implements OnInit, } /** @hidden */ - onItemKeyDownHandler(event: KeyboardEvent, item: SelectableOptionItem, index = 0): void { + onItemKeyDownHandler(event: KeyboardEvent, index = 0): void { if (KeyUtil.isKeyCode(event, ESCAPE)) { this._focusToSearchField(); this.close(); @@ -266,13 +228,12 @@ export class MultiComboboxComponent extends BaseMultiCombobox implements OnInit, event.preventDefault(); this.listComponent?.setItemActive(index + 1); } else if ((event.ctrlKey || event.metaKey) && event.shiftKey && KeyUtil.isKeyCode(event, A)) { - // unselect all event.preventDefault(); this.handleSelectAllItems(false); } else if ((event.ctrlKey || event.metaKey) && KeyUtil.isKeyCode(event, A)) { event.preventDefault(); this.handleSelectAllItems(true); - } else if (!KeyUtil.isKeyCode(event, ENTER) && !KeyUtil.isKeyCode(event, SPACE)) { + } else if (!KeyUtil.isKeyCode(event, [ENTER, SPACE])) { return; } else if (KeyUtil.isKeyCode(event, ENTER) && !this.mobile) { this.close(); @@ -296,7 +257,9 @@ export class MultiComboboxComponent extends BaseMultiCombobox implements OnInit, /** @hidden */ private _getTokenIndexByLabelOrValue(item: SelectableOptionItem): number { - return this._selectedSuggestions.findIndex((token) => token.label === item.label || token.value === item.value); + return this._selectedSuggestions.findIndex( + (token) => token.label === item.label || equal(token.value, item.value) + ); } /** @hidden */