igniteui-angular-sovn
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
462 lines (426 loc) • 16.3 kB
text/typescript
import { NgClass, NgIf, NgTemplateOutlet } from '@angular/common';
import {
AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnInit, OnDestroy,
Optional, Inject, Injector, ViewChild, Input, Output, EventEmitter, HostListener
} from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { IgxSelectionAPIService } from '../core/selection';
import { IBaseEventArgs, IBaseCancelableEventArgs, CancelableEventArgs } from '../core/utils';
import { IgxStringFilteringOperand, IgxBooleanFilteringOperand } from '../data-operations/filtering-condition';
import { FilteringLogic } from '../data-operations/filtering-expression.interface';
import { IgxForOfDirective } from '../directives/for-of/for_of.directive';
import { IgxIconService } from '../icon/icon.service';
import { IgxRippleDirective } from '../directives/ripple/ripple.directive';
import { IgxButtonDirective } from '../directives/button/button.directive';
import { IgxInputGroupComponent } from '../input-group/input-group.component';
import { IgxComboItemComponent } from './combo-item.component';
import { IgxComboDropDownComponent } from './combo-dropdown.component';
import { IgxComboFilteringPipe, IgxComboGroupingPipe } from './combo.pipes';
import { DisplayDensityToken, IDisplayDensityOptions } from '../core/density';
import { IGX_COMBO_COMPONENT, IgxComboBaseDirective } from './combo.common';
import { IgxComboAddItemComponent } from './combo-add-item.component';
import { IgxComboAPIService } from './combo.api';
import { EditorProvider } from '../core/edit-provider';
import { IgxInputGroupType, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
import { IgxDropDownItemNavigationDirective } from '../drop-down/drop-down-navigation.directive';
import { IgxIconComponent } from '../icon/icon.component';
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
import { IgxInputDirective } from '../directives/input/input.directive';
/** Event emitted when an igx-combo's selection is changing */
export interface IComboSelectionChangingEventArgs extends IBaseCancelableEventArgs {
/** An array containing the values that are currently selected */
oldSelection: any[];
/** An array containing the values that will be selected after this event */
newSelection: any[];
/** An array containing the values that will be added to the selection (if any) */
added: any[];
/** An array containing the values that will be removed from the selection (if any) */
removed: any[];
/** The text that will be displayed in the combo text box */
displayText: string;
/** The user interaction that triggered the selection change */
event?: Event;
}
/** Event emitted when the igx-combo's search input changes */
export interface IComboSearchInputEventArgs extends IBaseCancelableEventArgs {
/** The text that has been typed into the search input */
searchText: string;
}
export interface IComboItemAdditionEvent extends IBaseEventArgs, CancelableEventArgs {
oldCollection: any[];
addedItem: any;
newCollection: any[];
}
/**
* When called with sets A & B, returns A - B (as array);
*
* @hidden
*/
const diffInSets = (set1: Set<any>, set2: Set<any>): any[] => {
const results = [];
set1.forEach(entry => {
if (!set2.has(entry)) {
results.push(entry);
}
});
return results;
};
/**
* Represents a drop-down list that provides editable functionalities, allowing users to choose an option from a predefined list.
*
* @igxModule IgxComboModule
* @igxTheme igx-combo-theme
* @igxKeywords combobox, combo selection
* @igxGroup Grids & Lists
*
* @remarks
* It provides the ability to filter items as well as perform selection with the provided data.
* Additionally, it exposes keyboard navigation and custom styling capabilities.
* @example
* ```html
* <igx-combo [itemsMaxHeight]="250" [data]="locationData"
* [displayKey]="'field'" [valueKey]="'field'"
* placeholder="Location(s)" searchPlaceholder="Search...">
* </igx-combo>
* ```
*/
export class IgxComboComponent extends IgxComboBaseDirective implements AfterViewInit, ControlValueAccessor, OnInit,
OnDestroy, EditorProvider {
/**
* An @Input property that controls whether the combo's search box
* should be focused after the `opened` event is called
* When `false`, the combo's list item container will be focused instead
*/
public autoFocusSearch = true;
/**
* @deprecated in version 14.0.0. Use the IComboFilteringOptions.filterable
*
* An @Input property that enabled/disables filtering in the list. The default is `true`.
* ```html
* <igx-combo [filterable]="false">
* ```
*/
public get filterable(): boolean {
return this.filteringOptions.filterable;
}
public set filterable(value: boolean) {
this.filteringOptions = Object.assign({}, this.filteringOptions, { filterable: value });
}
/**
* Defines the placeholder value for the combo dropdown search field
*
* ```typescript
* // get
* let myComboSearchPlaceholder = this.combo.searchPlaceholder;
* ```
*
* ```html
* <!--set-->
* <igx-combo [searchPlaceholder]='newPlaceHolder'></igx-combo>
* ```
*/
public searchPlaceholder = 'Enter a Search Term';
/**
* Emitted when item selection is changing, before the selection completes
*
* ```html
* <igx-combo (selectionChanging)='handleSelection()'></igx-combo>
* ```
*/
public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();
/** @hidden @internal */
public dropdown: IgxComboDropDownComponent;
/**
* @hidden @internal
*/
public get inputEmpty(): boolean {
return !this.value && !this.placeholder;
}
/** @hidden @internal */
public get filteredData(): any[] | null {
return this.filteringOptions.filterable ? this._filteredData : this.data;
}
/** @hidden @internal */
public set filteredData(val: any[] | null) {
this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
this.checkMatch();
}
/**
* @hidden @internal
*/
public filteringLogic = FilteringLogic.Or;
protected stringFilters = IgxStringFilteringOperand;
protected booleanFilters = IgxBooleanFilteringOperand;
protected _prevInputValue = '';
constructor(
elementRef: ElementRef,
cdr: ChangeDetectorRef,
selectionService: IgxSelectionAPIService,
comboAPI: IgxComboAPIService,
_iconService: IgxIconService,
_displayDensityOptions: IDisplayDensityOptions,
_inputGroupType: IgxInputGroupType,
_injector: Injector) {
super(elementRef, cdr, selectionService, comboAPI, _iconService, _displayDensityOptions, _inputGroupType, _injector);
this.comboAPI.register(this);
}
public onArrowDown(event: Event) {
event.preventDefault();
event.stopPropagation();
this.open();
}
/** @hidden @internal */
public get displaySearchInput(): boolean {
return this.filteringOptions.filterable || this.allowCustomValues;
}
/**
* @hidden @internal
*/
public handleKeyUp(event: KeyboardEvent): void {
// TODO: use PlatformUtil for keyboard navigation
if (event.key === 'ArrowDown' || event.key === 'Down') {
this.dropdown.focusedItem = this.dropdown.items[0];
this.dropdownContainer.nativeElement.focus();
} else if (event.key === 'Escape' || event.key === 'Esc') {
this.toggle();
}
}
/**
* @hidden @internal
*/
public handleSelectAll(evt) {
if (evt.checked) {
this.selectAllItems();
} else {
this.deselectAllItems();
}
}
/**
* @hidden @internal
*/
public writeValue(value: any[]): void {
const selection = Array.isArray(value) ? value.filter(x => x !== undefined) : [];
const oldSelection = this.selection;
this.selectionService.select_items(this.id, selection, true);
this.cdr.markForCheck();
this._value = this.createDisplayText(this.selection, oldSelection);
}
/**
* @hidden
*/
public getEditElement(): HTMLElement {
return this.comboInput.nativeElement;
}
/**
* @hidden @internal
*/
public get context(): any {
return {
$implicit: this
};
}
/**
* @hidden @internal
*/
public handleClearItems(event: Event): void {
if (this.disabled) {
return;
}
this.deselectAllItems(true, event);
if (this.collapsed) {
this.getEditElement().focus();
} else {
this.focusSearchInput(true);
}
event.stopPropagation();
}
/**
* Select defined items
*
* @param newItems new items to be selected
* @param clearCurrentSelection if true clear previous selected items
* ```typescript
* this.combo.select(["New York", "New Jersey"]);
* ```
*/
public select(newItems: Array<any>, clearCurrentSelection?: boolean, event?: Event) {
if (newItems) {
const newSelection = this.selectionService.add_items(this.id, newItems, clearCurrentSelection);
this.setSelection(newSelection, event);
}
}
/**
* Deselect defined items
*
* @param items items to deselected
* ```typescript
* this.combo.deselect(["New York", "New Jersey"]);
* ```
*/
public deselect(items: Array<any>, event?: Event) {
if (items) {
const newSelection = this.selectionService.delete_items(this.id, items);
this.setSelection(newSelection, event);
}
}
/**
* Select all (filtered) items
*
* @param ignoreFilter if set to true, selects all items, otherwise selects only the filtered ones.
* ```typescript
* this.combo.selectAllItems();
* ```
*/
public selectAllItems(ignoreFilter?: boolean, event?: Event) {
const allVisible = this.selectionService.get_all_ids(ignoreFilter ? this.data : this.filteredData, this.valueKey);
const newSelection = this.selectionService.add_items(this.id, allVisible);
this.setSelection(newSelection, event);
}
/**
* Deselect all (filtered) items
*
* @param ignoreFilter if set to true, deselects all items, otherwise deselects only the filtered ones.
* ```typescript
* this.combo.deselectAllItems();
* ```
*/
public deselectAllItems(ignoreFilter?: boolean, event?: Event): void {
let newSelection = this.selectionService.get_empty();
if (this.filteredData.length !== this.data.length && !ignoreFilter) {
newSelection = this.selectionService.delete_items(this.id, this.selectionService.get_all_ids(this.filteredData, this.valueKey));
}
this.setSelection(newSelection, event);
}
/**
* Selects/Deselects a single item
*
* @param itemID the itemID of the specific item
* @param select If the item should be selected (true) or deselected (false)
*
* Without specified valueKey;
* ```typescript
* this.combo.valueKey = null;
* const items: { field: string, region: string}[] = data;
* this.combo.setSelectedItem(items[0], true);
* ```
* With specified valueKey;
* ```typescript
* this.combo.valueKey = 'field';
* const items: { field: string, region: string}[] = data;
* this.combo.setSelectedItem('Connecticut', true);
* ```
*/
public setSelectedItem(itemID: any, select = true, event?: Event): void {
if (itemID === undefined) {
return;
}
if (select) {
this.select([itemID], false, event);
} else {
this.deselect([itemID], event);
}
}
/** @hidden @internal */
public handleOpened() {
this.triggerCheck();
// Disabling focus of the search input should happen only when drop down opens.
// During keyboard navigation input should receive focus, even the autoFocusSearch is disabled.
// That is why in such cases focusing of the dropdownContainer happens outside focusSearchInput method.
if (this.autoFocusSearch) {
this.focusSearchInput(true);
} else {
this.dropdownContainer.nativeElement.focus();
}
this.opened.emit({ owner: this });
}
/** @hidden @internal */
public focusSearchInput(opening?: boolean): void {
if (this.displaySearchInput && this.searchInput) {
this.searchInput.nativeElement.focus();
} else {
if (opening) {
this.dropdownContainer.nativeElement.focus();
} else {
this.comboInput.nativeElement.focus();
this.toggle();
}
}
}
protected setSelection(newSelection: Set<any>, event?: Event): void {
const removed = diffInSets(this.selectionService.get(this.id), newSelection);
const added = diffInSets(newSelection, this.selectionService.get(this.id));
const newSelectionAsArray = Array.from(newSelection);
const oldSelectionAsArray = Array.from(this.selectionService.get(this.id) || []);
const displayText = this.createDisplayText(newSelectionAsArray, oldSelectionAsArray);
const args: IComboSelectionChangingEventArgs = {
newSelection: newSelectionAsArray,
oldSelection: oldSelectionAsArray,
added,
removed,
event,
owner: this,
displayText,
cancel: false
};
this.selectionChanging.emit(args);
if (!args.cancel) {
this.selectionService.select_items(this.id, args.newSelection, true);
if (displayText !== args.displayText) {
this._value = args.displayText;
} else {
this._value = this.createDisplayText(args.newSelection, args.oldSelection);
}
this._onChangeCallback(args.newSelection);
} else if (this.isRemote) {
this.registerRemoteEntries(args.added, false);
}
}
protected createDisplayText(newSelection: any[], oldSelection: any[]) {
return this.isRemote
? this.getRemoteSelection(newSelection, oldSelection)
: this.concatDisplayText(newSelection);
}
/** Returns a string that should be populated in the combo's text box */
private concatDisplayText(selection: any[]): string {
const value = this.displayKey !== null && this.displayKey !== undefined ?
this.convertKeysToItems(selection).map(entry => entry[this.displayKey]).join(', ') :
selection.join(', ');
return value;
}
}