// eslint-disable-next-line @typescript-eslint/triple-slash-reference
///
import { EventHandler, Property, Event, EmitType, addClass, Browser, KeyboardEventArgs, removeClass, detach } from '@syncfusion/ej2-base';
import { isNullOrUndefined, NotifyPropertyChanges, getValue, setValue } from '@syncfusion/ej2-base';
import { DropDownList, dropDownListClasses } from '../drop-down-list/drop-down-list';
import { FilteringEventArgs } from '../drop-down-base/drop-down-base';
import { FieldSettingsModel } from '../drop-down-base/drop-down-base-model';
import { ComboBoxModel } from '../combo-box/combo-box-model';
import { Search } from '../common/incremental-search';
import { createSpinner, showSpinner, hideSpinner } from '@syncfusion/ej2-popups';
import { Input, InputObject, FloatLabelType } from '@syncfusion/ej2-inputs';
import { DataManager, DataOptions, Predicate, Query } from '@syncfusion/ej2-data';
const SPINNER_CLASS: string = 'e-atc-spinner-icon';
dropDownListClasses.root = 'e-combobox';
const inputObject: InputObject = {
container: null,
buttons: []
};
/**
* The ComboBox component allows the user to type a value or choose an option from the list of predefined options.
* ```html
*
* ```
* ```typescript
* let games:ComboBox = new ComboBox();
* games.appendTo("#list");
* ```
*/
@NotifyPropertyChanges
export class ComboBox extends DropDownList {
/**
* Specifies whether suggest a first matched item in input when searching. No action happens when no matches found.
*
* @default false
*/
@Property(false)
public autofill: boolean;
/**
* Specifies whether the component allows user defined value which does not exist in data source.
*
* @default true
*/
@Property(true)
public allowCustom: boolean;
/**
* Allows additional HTML attributes such as title, name, etc., and
* accepts n number of attributes in a key-value pair format.
*
* {% codeBlock src='combobox/htmlAttributes/index.md' %}{% endcodeBlock %}
*
* @default {}
* @deprecated
*/
@Property({})
public htmlAttributes: { [key: string]: string };
/**
* When allowFiltering is set to true, show the filter bar (search box) of the component.
* The filter action retrieves matched items through the `filtering` event based on
* the characters typed in the search TextBox.
* If no match is found, the value of the `noRecordsTemplate` property will be displayed.
*
* {% codeBlock src="combobox/allow-filtering-api/index.ts" %}{% endcodeBlock %}
*
* {% codeBlock src="combobox/allow-filtering-api/index.html" %}{% endcodeBlock %}
*
* @default false
* @deprecated
*/
@Property(false)
public allowFiltering: boolean;
/**
* Defines whether the popup opens in fullscreen mode on mobile devices when filtering is enabled. When set to false, the popup will display similarly on both mobile and desktop devices.
*
* @default true
*/
@Property(true)
public isDeviceFullScreen: boolean;
/**
* Accepts the external `Query`
* that execute along with [`data processing`](../../combo-box/data-binding).
*
* {% codeBlock src='combobox/query/index.md' %}{% endcodeBlock %}
*
* @default null
* @deprecated
*/
@Property(null)
public query: Query;
/**
* Gets or sets the index of the selected item in the component.
*
* {% codeBlock src="combobox/index-api/index.ts" %}{% endcodeBlock %}
*
* {% codeBlock src="combobox/index-api/index.html" %}{% endcodeBlock %}
*
* @default null
* @aspType double
* @deprecated
*/
@Property(null)
public index: number | null;
/**
* Specifies whether to show or hide the clear button.
* When the clear button is clicked, `value`, `text`, and `index` properties are reset to null.
*
* @default true
*/
@Property(true)
public showClearButton: boolean;
/**
* Enable or disable rendering component in right to left direction.
*
* @default false
* @deprecated
*/
@Property(false)
public enableRtl: boolean;
/**
* Triggers on set a
* [`custom value`](../../combo-box/getting-started#custom-values) to this component.
*
* @event customValueSpecifier
*/
@Event()
public customValueSpecifier: EmitType;
/**
* Triggers on typing a character in the component.
* > For more details about the filtering refer to [`Filtering`](../../combo-box/filtering) documentation.
*
* @event filtering
*/
@Event()
public filtering: EmitType;
/**
* Not applicable to this component.
*
* @default null
* @aspType string
* @private
*/
@Property(null)
public valueTemplate: string | Function;
/**
* Specifies whether to display the floating label above the input element.
* Possible values are:
* * Never: The label will never float in the input when the placeholder is available.
* * Always: The floating label will always float above the input.
* * Auto: The floating label will float above the input after focusing or entering a value in the input.
*
* {% codeBlock src="combobox/float-label-type-api/index.ts" %}{% endcodeBlock %}
*
* {% codeBlock src="combobox/float-label-type-api/index.html" %}{% endcodeBlock %}
*
* @default Syncfusion.EJ2.Inputs.FloatLabelType.Never
* @aspType Syncfusion.EJ2.Inputs.FloatLabelType
* @isEnumeration true
* @deprecated
*/
@Property('Never')
public floatLabelType: FloatLabelType;
/**
* Not applicable to this component.
*
* @default null
* @private
* @deprecated
*/
@Property(null)
public filterBarPlaceholder: string;
/**
* Sets CSS classes to the root element of the component that allows customization of appearance.
*
* @default null
* @deprecated
*/
@Property(null)
public cssClass: string;
/**
* Accepts the template design and assigns it to the header container of the popup list.
* > For more details about the available template options refer to [`Template`](../../drop-down-list/templates) documentation.
*
* @default null
* @aspType string
* @deprecated
*/
@Property(null)
public headerTemplate: string | Function;
/**
* Accepts the template design and assigns it to the footer container of the popup list.
* > For more details about the available template options refer to [`Template`](../../drop-down-list/templates) documentation.
*
* @default null
* @aspType string
* @deprecated
*/
@Property(null)
public footerTemplate: string | Function;
/**
* Specifies a short hint that describes the expected value of the DropDownList component.
*
* @default null
* @deprecated
*/
@Property(null)
public placeholder: string;
/**
* Specifies the width of the component. By default, the component width sets based on the width of
* its parent container. You can also set the width in pixel values.
*
* @default '100%'
* @aspType string
* @deprecated
*/
@Property('100%')
public width: string | number;
/**
* Specifies the height of the popup list.
* > For more details about the popup configuration refer to
* [`Popup Configuration`](../../drop-down-list/getting-started#configure-the-popup-list) documentation.
*
* @default '300px'
* @aspType string
* @deprecated
*/
@Property('300px')
public popupHeight: string | number;
/**
* Specifies the width of the popup list. By default, the popup width sets based on the width of
* the component.
* > For more details about the popup configuration refer to
* [`Popup Configuration`](../../drop-down-list/getting-started#configure-the-popup-list) documentation.
*
* @default '100%'
* @aspType string
* @deprecated
*/
@Property('100%')
public popupWidth: string | number;
/**
* When set to true, the user interactions on the component are disabled.
*
* @default false
* @deprecated
*/
@Property(false)
public readonly: boolean;
/**
* Gets or sets the display text of the selected item in the component.
*
* @default null
* @aspType string
* @deprecated
*/
@Property(null)
public text: string | null;
/**
* Gets or sets the value of the selected item in the component.
*
* @default null
* @isGenericType true
* @deprecated
*/
@Property(null)
public value: number | string | boolean | object | null;
/**
* Defines whether the object binding is allowed or not in the component.
*
* @default false
*/
@Property(false)
public allowObjectBinding: boolean;
/**
* *Constructor for creating the component
*
* @param {ComboBoxModel} options - Specifies the ComboBox model.
* @param {string | HTMLElement} element - Specifies the element to render as component.
* @private
*/
public constructor(options?: ComboBoxModel, element?: string | HTMLElement) {
super(options, element);
}
/**
* Initialize the event handler
*
* @private
* @returns {void}
*/
protected preRender(): void {
super.preRender();
}
protected getLocaleName(): string {
return 'combo-box';
}
protected wireEvent(): void {
if (this.getModuleName() === 'combobox') {
EventHandler.add(this.inputWrapper.buttons[0], 'mousedown', this.preventBlur, this);
EventHandler.add(this.inputWrapper.container, 'blur', this.onBlurHandler, this);
}
if (!isNullOrUndefined(this.inputWrapper.buttons[0])) {
EventHandler.add(this.inputWrapper.buttons[0], 'mousedown', this.dropDownClick, this);
}
EventHandler.add(this.inputElement, 'focus', this.targetFocus, this);
if (!this.readonly) {
EventHandler.add(this.inputElement, 'input', this.onInput, this);
EventHandler.add(this.inputElement, 'keyup', this.onFilterUp, this);
EventHandler.add(this.inputElement, 'keydown', this.onFilterDown, this);
EventHandler.add(this.inputElement, 'paste', this.pasteHandler, this);
EventHandler.add(window, 'resize', this.windowResize, this);
}
this.bindCommonEvent();
}
private preventBlur(e: MouseEvent): void {
if ((!this.allowFiltering && document.activeElement !== this.inputElement &&
!document.activeElement.classList.contains(dropDownListClasses.input) && Browser.isDevice || !Browser.isDevice)) {
e.preventDefault();
}
}
protected onBlurHandler(e: MouseEvent): void {
const inputValue: string = this.inputElement && this.inputElement.value === '' ?
null : this.inputElement && this.inputElement.value;
const text: string = !isNullOrUndefined(this.text) ? this.text.replace(/\r\n|\n|\r/g, '') : this.text;
if (!isNullOrUndefined(this.listData) && !isNullOrUndefined(inputValue) && inputValue !== text) {
this.customValue(e);
}
super.onBlurHandler(e);
}
protected targetElement(): HTMLElement | HTMLInputElement {
return this.inputElement;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected setOldText(text: string): void {
Input.setValue(this.text, this.inputElement, this.floatLabelType, this.showClearButton);
this.customValue();
this.removeSelection();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected setOldValue(value: string | number | object): void {
if (this.allowCustom) {
this.selectedLI = this.getElementByValue(this.value) as HTMLElement;
this.valueMuteChange(this.value);
} else {
this.valueMuteChange(null);
}
this.removeSelection();
this.setHiddenValue();
}
private valueMuteChange(value: string | number | boolean | object): void {
value = this.allowObjectBinding && !isNullOrUndefined(value) ? getValue((this.fields.value) ? this.fields.value : '', value) : value;
const inputValue: string = isNullOrUndefined(value) ? null : value.toString();
Input.setValue(inputValue, this.inputElement, this.floatLabelType, this.showClearButton);
let changeData: { [key: string]: Object } = {};
if (this.allowObjectBinding) {
value = this.getDataByValue(value as string | number | boolean);
if (isNullOrUndefined(value)) {
const fields: FieldSettingsModel = this.fields;
let isvalidTextField: boolean = false;
let isValidValue: boolean = false;
if (this.allowObjectBinding) {
const keys: string[] = Object.keys(this.value);
keys.forEach((key: string) => {
if (key === fields.value) {
isValidValue = true;
return;
}
});
keys.forEach((key: string) => {
if (key === fields.text) {
isvalidTextField = true;
return;
}
});
}
changeData = {
text: isValidValue ? isvalidTextField ? getValue(fields.text, this.value) : getValue(fields.value, this.value) : null,
value: isValidValue ? this.value : null,
index: null
};
}
}
if (this.allowObjectBinding) {
this.setProperties(changeData, true);
}
else {
this.setProperties({ value: value, text: value ? value.toString() : value, index: null }, true);
}
this.activeIndex = this.index;
const fields: FieldSettingsModel = this.fields;
const dataItem: { [key: string]: string | Object } = {};
dataItem[fields.text] = isNullOrUndefined(value) ? null : value.toString();
dataItem[fields.value] = isNullOrUndefined(value) ? null : value.toString();
this.itemData = <{ [key: string]: Object }>dataItem;
this.item = null;
if ((!this.allowObjectBinding && (this.previousValue !== this.value)) ||
(this.allowObjectBinding && this.previousValue && this.value &&
!this.isObjectInArray(this.previousValue, [this.value]))) {
this.detachChangeEvent(null);
}
}
protected updateValues(): void {
if (this.fields.disabled) {
if (this.value != null) {
this.value = !this.isDisableItemValue(this.value) ? this.value : null;
}
if (this.text != null) {
this.text = !this.isDisabledItemByIndex(this.getIndexByValue(this.getValueByText(this.text))) ? this.text : null;
}
if (this.index != null) {
this.index = !this.isDisabledItemByIndex(this.index) ? this.index : null;
this.activeIndex = this.index;
}
}
if (!isNullOrUndefined(this.value)) {
const currentValue: string | number | boolean = this.allowObjectBinding && !isNullOrUndefined(this.value) ? getValue((this.fields.value) ? this.fields.value : '', this.value) : this.value;
const li: Element = this.getElementByValue(currentValue);
let doesItemExist: boolean = !isNullOrUndefined(li) ? true : false;
if (this.enableVirtualization && this.value) {
const fields: string = (this.fields.value) ? this.fields.value : '';
const currentValue: string | number | boolean = this.allowObjectBinding && !isNullOrUndefined(this.value) ? getValue((this.fields.value) ? this.fields.value : '', this.value) : this.value;
if (this.dataSource instanceof DataManager) {
const getItem: any = <{ [key: string]: Object }[] | string[] | number[] | boolean[]>new DataManager(
this.virtualGroupDataSource as DataOptions | JSON[]).executeLocal(new Query().where(new Predicate(fields, 'equal', currentValue)));
if (getItem && getItem.length > 0) {
this.itemData = getItem[0];
doesItemExist = true;
const dataItem: { [key: string]: string } = this.getItemData();
const value: string | number | boolean | Object = this.allowObjectBinding
? this.getDataByValue(dataItem.value)
: dataItem.value;
if ((this.value === dataItem.value && this.text !== dataItem.text)
|| (this.value !== dataItem.value && this.text === dataItem.text)) {
this.setProperties({ 'text': dataItem.text ? dataItem.text.toString() : dataItem.text, 'value': value });
}
}
}
else{
const getItem: any = <{ [key: string]: Object }[] | string[] | number[] | boolean[]>new DataManager(
this.dataSource as DataOptions | JSON[]).executeLocal(new Query().where(new Predicate(fields, 'equal', currentValue)));
if (getItem && getItem.length > 0) {
this.itemData = getItem[0];
doesItemExist = true;
const dataItem: { [key: string]: string } = this.getItemData();
const value: string | number | boolean | Object = this.allowObjectBinding
? this.getDataByValue(dataItem.value)
: dataItem.value;
if ((this.value === dataItem.value && this.text !== dataItem.text)
|| (this.value !== dataItem.value && this.text === dataItem.text)) {
this.setProperties({ 'text': dataItem.text ? dataItem.text.toString() : dataItem.text, 'value': value });
if (isNullOrUndefined(li)) {
this.previousValue = this.value;
}
}
}
}
}
if (li) {
this.setSelection(li, null);
} else if (
(!this.enableVirtualization && this.allowCustom) ||
(this.allowCustom && this.enableVirtualization && !doesItemExist)
) {
this.valueMuteChange(this.value);
} else if (!this.enableVirtualization || (this.enableVirtualization && !doesItemExist)) {
this.valueMuteChange(null);
}
} else if (this.text && isNullOrUndefined(this.value)) {
const li: Element = this.getElementByText(this.text);
if (li) {
this.setSelection(li, null);
} else {
Input.setValue(this.text, this.inputElement, this.floatLabelType, this.showClearButton);
this.customValue();
}
} else {
this.setSelection(this.liCollections[this.activeIndex], null);
}
this.setHiddenValue();
Input.setValue(this.text, this.inputElement, this.floatLabelType, this.showClearButton);
}
protected updateIconState(): void {
if (this.showClearButton) {
if (this.inputElement && this.inputElement.value !== '' && !this.readonly) {
removeClass([this.inputWrapper.clearButton], dropDownListClasses.clearIconHide);
} else {
addClass([this.inputWrapper.clearButton], dropDownListClasses.clearIconHide);
}
}
}
protected getAriaAttributes(): { [key: string]: string } {
const ariaAttributes: { [key: string]: string } = {
'role': 'combobox',
'aria-autocomplete': 'both',
'aria-labelledby': this.hiddenElement.id,
'aria-expanded': 'false',
'aria-readonly': this.readonly ? this.readonly.toString() : 'false',
'autocomplete': 'off',
'autocapitalize': 'off',
'spellcheck': 'false'
};
return ariaAttributes;
}
protected searchLists(e: KeyboardEventArgs | MouseEvent): void {
this.isTyped = true;
if (this.isFiltering()) {
super.searchLists(e);
if (this.ulElement && this.filterInput.value.trim() === '') {
this.setHoverList(this.ulElement.querySelector('.' + dropDownListClasses.li));
}
} else {
if (this.ulElement && this.inputElement.value === '' && this.preventAutoFill) {
this.setHoverList(this.ulElement.querySelector('.' + dropDownListClasses.li));
}
this.incrementalSearch(e as KeyboardEventArgs);
}
}
protected getNgDirective(): string {
return 'EJS-COMBOBOX';
}
protected setSearchBox(): InputObject {
this.filterInput = this.inputElement;
const searchBoxContainer: InputObject = (this.isFiltering() || ((this as any).isReact && this.getModuleName() === 'combobox')) ? this.inputWrapper : inputObject;
return searchBoxContainer;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected onActionComplete(ulElement: HTMLElement, list: { [key: string]: Object }[], e?: Object, isUpdated?: boolean): void {
super.onActionComplete(ulElement, list, e);
if (this.isSelectCustom) {
this.removeSelection();
}
if (!this.preventAutoFill && this.getModuleName() === 'combobox' && this.isTyped && !this.enableVirtualization) {
setTimeout(() => {
this.inlineSearch();
});
}
}
protected getFocusElement(): Element {
const dataItem: { [key: string]: string } = this.isSelectCustom ? { text: '' } : this.getItemData();
const selected: HTMLElement = !isNullOrUndefined(this.list) ? this.list.querySelector('.' + dropDownListClasses.selected) : this.list;
const isSelected: boolean = dataItem.text && dataItem.text.toString() === this.inputElement.value && !isNullOrUndefined(selected);
if (isSelected) {
return selected;
}
if ((Browser.isDevice && !this.isDropDownClick || !Browser.isDevice) &&
!isNullOrUndefined(this.liCollections) && this.liCollections.length > 0) {
const inputValue: string = this.inputElement.value;
const dataSource: { [key: string]: Object }[] = this.sortedData as { [key: string]: Object }[];
const type: string = this.typeOfData(dataSource).typeof as string;
let activeItem: { [key: string]: Element | number } =
Search(inputValue, this.liCollections, this.filterType, true, dataSource, this.fields, type, this.ignoreAccent);
if (this.enableVirtualization && inputValue !== '' && this.getModuleName() !== 'autocomplete' && this.isTyped && !this.allowFiltering) {
let updatingincrementalindex: boolean = false;
const isEndIndexValid: boolean =
this.viewPortInfo.endIndex >= this.incrementalEndIndex &&
this.incrementalEndIndex <= this.totalItemCount;
const isIncrementalEndIndexZero: boolean = this.incrementalEndIndex === 0;
if (isEndIndexValid || isIncrementalEndIndexZero) {
updatingincrementalindex = true;
this.incrementalStartIndex = this.incrementalEndIndex;
if (isIncrementalEndIndexZero) {
this.incrementalEndIndex = Math.min(100, this.totalItemCount);
} else {
this.incrementalEndIndex = Math.min(this.incrementalEndIndex + 100, this.totalItemCount);
}
this.updateIncrementalInfo(this.incrementalStartIndex, this.incrementalEndIndex);
updatingincrementalindex = true;
}
if (this.viewPortInfo.startIndex !== 0 || updatingincrementalindex) {
this.updateIncrementalView(0, this.itemCount);
}
activeItem = Search(inputValue, this.incrementalLiCollections, this.filterType, true, dataSource, this.fields, type);
while (isNullOrUndefined(activeItem.item) && this.incrementalEndIndex < this.totalItemCount) {
this.incrementalStartIndex = this.incrementalEndIndex;
this.incrementalEndIndex = this.incrementalEndIndex + 100 > this.totalItemCount
? this.totalItemCount
: this.incrementalEndIndex + 100;
this.updateIncrementalInfo(this.incrementalStartIndex, this.incrementalEndIndex);
updatingincrementalindex = true;
if (this.viewPortInfo.startIndex !== 0 || updatingincrementalindex) {
this.updateIncrementalView(0, this.itemCount);
}
activeItem = Search(inputValue, this.incrementalLiCollections, this.filterType, true, dataSource, this.fields, type);
if (!isNullOrUndefined(activeItem)) {
activeItem.index = (activeItem.index as number) + this.incrementalStartIndex;
break;
}
if (isNullOrUndefined(activeItem) && this.incrementalEndIndex >= this.totalItemCount) {
this.incrementalStartIndex = 0;
this.incrementalEndIndex = 100 > this.totalItemCount ? this.totalItemCount : 100;
break;
}
}
const startIndex: number =
(activeItem.index as number) - ((this.itemCount / 2) - 2) > 0
? (activeItem.index as number) - ((this.itemCount / 2) - 2)
: 0;
const endIndex: number =
this.viewPortInfo.startIndex + this.itemCount > this.totalItemCount
? this.totalItemCount
: this.viewPortInfo.startIndex + this.itemCount;
if (startIndex !== this.viewPortInfo.startIndex) {
this.updateIncrementalView(startIndex, endIndex);
}
if (!isNullOrUndefined(activeItem.item)) {
const startIndex: number = this.viewPortInfo.startIndex + ((this.itemCount / 2) - 2) < this.totalItemCount
? this.viewPortInfo.startIndex + ((this.itemCount / 2) - 2)
: this.totalItemCount;
const endIndex: number = this.viewPortInfo.startIndex + this.itemCount > this.totalItemCount
? this.totalItemCount
: this.viewPortInfo.startIndex + this.itemCount;
this.updateIncrementalView(startIndex, endIndex);
activeItem.item = this.getElementByValue((activeItem.item as Element).getAttribute('data-value'));
} else {
this.updateIncrementalView(0, this.itemCount);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.list.getElementsByClassName('e-virtual-ddl-content')[0] as any).style = this.getTransformValues();
this.list.scrollTop = 0;
}
if (activeItem && activeItem.item){
activeItem.item = this.getElementByValue((activeItem.item as Element).getAttribute('data-value'));
}
}
const activeElement: Element = activeItem.item as Element;
if (!isNullOrUndefined(activeElement)) {
const count: number = this.getIndexByValue(activeElement.getAttribute('data-value')) - 1;
const height: number = parseInt(getComputedStyle(this.liCollections[0], null).getPropertyValue('height'), 10);
if (!isNaN(height) && this.getModuleName() !== 'autocomplete') {
this.removeFocus();
const fixedHead: number = this.fields.groupBy ? this.liCollections[0].offsetHeight : 0;
if (!this.enableVirtualization) {
this.list.scrollTop = count * height + fixedHead;
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const virtualContent: any = this.list.getElementsByClassName('e-virtual-ddl-content')[0];
virtualContent.style = this.getTransformValues();
if (this.enableVirtualization && !this.fields.groupBy) {
const selectedLiOffsetTop: number = (activeElement as HTMLElement).offsetTop;
const virtualListInfoStartIndex: number = this.virtualListInfo && this.virtualListInfo.startIndex
? this.virtualListInfo.startIndex
: 0;
const virtualListHeight: number = (activeElement as HTMLElement).offsetHeight;
const selectedLiOffsetTopWithStartIndex: number = selectedLiOffsetTop +
(virtualListInfoStartIndex * virtualListHeight);
const virtualListLength: number = this.list.querySelectorAll('.e-virtual-list').length;
const scrollTopOffset: number = virtualListLength * virtualListHeight;
this.list.scrollTop = selectedLiOffsetTopWithStartIndex - scrollTopOffset;
}
}
addClass([activeElement], dropDownListClasses.focus);
}
} else {
if (this.isSelectCustom && this.inputElement.value.trim() !== '') {
this.removeFocus();
if (!this.enableVirtualization) {
this.list.scrollTop = 0;
}
}
}
return activeElement;
} else {
return null;
}
}
protected setValue(e?: KeyboardEventArgs): boolean {
if ((e && e.type === 'keydown' && e.action === 'enter') || (e && e.type === 'click')) {
this.removeFillSelection();
}
if (this.autofill && this.getModuleName() === 'combobox' && e && e.type === 'keydown' && e.action !== 'enter') {
this.preventAutoFill = false;
this.inlineSearch(e);
return false;
} else {
return super.setValue(e);
}
}
protected checkCustomValue(): void {
const value: string | number | boolean = this.allowObjectBinding && !isNullOrUndefined(this.value) ? getValue((this.fields.value) ? this.fields.value : '', this.value) : this.value;
this.itemData = this.getDataByValue(value);
const dataItem: { [key: string]: string } = this.getItemData();
const setValue: string | number | boolean | Object = this.allowObjectBinding ? this.itemData : dataItem.value;
if (!(this.allowCustom && isNullOrUndefined(dataItem.value) && isNullOrUndefined(dataItem.text))) {
this.setProperties({ 'value': setValue }, !this.allowCustom);
}
}
/**
* Shows the spinner loader.
*
* @returns {void}
* @deprecated
*/
public showSpinner(): void {
if (isNullOrUndefined(this.spinnerElement)) {
this.spinnerElement = (this.getModuleName() === 'autocomplete') ? (this.inputWrapper.buttons[0] ||
this.inputWrapper.clearButton ||
Input.appendSpan('e-input-group-icon ' + SPINNER_CLASS, this.inputWrapper.container, this.createElement)) :
(this.inputWrapper.buttons[0] || this.inputWrapper.clearButton);
addClass([this.spinnerElement], dropDownListClasses.disableIcon);
createSpinner(
{
target: this.spinnerElement,
width: Browser.isDevice ? '16px' : '14px'
},
this.createElement);
showSpinner(this.spinnerElement);
}
}
/**
* Hides the spinner loader.
*
* @returns {void}
* @deprecated
*/
public hideSpinner(): void {
if (!isNullOrUndefined(this.spinnerElement)) {
hideSpinner(this.spinnerElement);
removeClass([this.spinnerElement], dropDownListClasses.disableIcon);
if (this.spinnerElement.classList.contains(SPINNER_CLASS)) {
detach(this.spinnerElement);
} else {
this.spinnerElement.innerHTML = '';
}
this.spinnerElement = null;
}
}
protected setAutoFill(activeElement: Element, isHover?: boolean): void {
if (!isHover) {
this.setHoverList(activeElement);
}
if (this.autofill && !this.preventAutoFill) {
const currentValue: string = this.getTextByValue(activeElement.getAttribute('data-value')).toString();
const currentFillValue: string | number | boolean = this.getFormattedValue(activeElement.getAttribute('data-value'));
if (this.getModuleName() === 'combobox') {
if (!this.isSelected && ((!this.allowObjectBinding && this.previousValue !== currentFillValue)) ||
(this.allowObjectBinding && this.previousValue && currentFillValue &&
!this.isObjectInArray(this.previousValue, [this.getDataByValue(currentFillValue)]))) {
this.updateSelectedItem(activeElement, null);
this.isSelected = true;
this.previousValue = this.allowObjectBinding ?
this.getDataByValue(this.getFormattedValue(activeElement.getAttribute('data-value'))) :
this.getFormattedValue(activeElement.getAttribute('data-value'));
} else {
this.updateSelectedItem(activeElement, null, true);
}
}
if (!this.isAndroidAutoFill(currentValue)) {
this.setAutoFillSelection(currentValue, isHover);
}
}
}
private isAndroidAutoFill(value: string): boolean {
if (Browser.isAndroid) {
const currentPoints: { [key: string]: number } = this.getSelectionPoints();
const prevEnd: number = this.prevSelectPoints.end;
const curEnd: number = currentPoints.end;
const prevStart: number = this.prevSelectPoints.start;
const curStart: number = currentPoints.start;
if (prevEnd !== 0 && ((prevEnd === value.length && prevStart === value.length) ||
(prevStart > curStart && prevEnd > curEnd) || (prevEnd === curEnd && prevStart === curStart))) {
return true;
} else {
return false;
}
} else {
return false;
}
}
protected clearAll(e?: MouseEvent | KeyboardEventArgs, property?: ComboBoxModel): void {
if (isNullOrUndefined(property) || (!isNullOrUndefined(property) && isNullOrUndefined(property.dataSource))) {
super.clearAll(e);
}
if (this.isFiltering() && !isNullOrUndefined(e) && e.target === this.inputWrapper.clearButton) {
this.typedString = this.filterInput.value;
this.searchLists(e);
}
}
protected isSelectFocusItem(element: Element): boolean {
return !isNullOrUndefined(element);
}
private inlineSearch(e?: KeyboardEventArgs): void {
const isKeyNavigate: boolean = (e && (e.action === 'down' || e.action === 'up' ||
e.action === 'home' || e.action === 'end' || e.action === 'pageUp' || e.action === 'pageDown'));
const activeElement: Element = isKeyNavigate ? this.liCollections[this.activeIndex] : this.getFocusElement();
if (!isNullOrUndefined(activeElement)) {
if (!isKeyNavigate) {
const value: string | number | boolean = this.getFormattedValue(activeElement.getAttribute('data-value'));
this.activeIndex = this.getIndexByValue(value);
this.activeIndex = !isNullOrUndefined(this.activeIndex) ? this.activeIndex : null;
}
this.preventAutoFill = this.inputElement.value === '' ? false : this.preventAutoFill;
this.setAutoFill(activeElement, isKeyNavigate);
} else if (!isNullOrUndefined(this.inputElement) && this.inputElement.value === '') {
this.activeIndex = null;
if (!isNullOrUndefined(this.list)) {
if (!this.enableVirtualization) {
this.list.scrollTop = 0;
}
const focusItem: Element = this.list.querySelector('.' + dropDownListClasses.li);
this.setHoverList(focusItem);
}
} else {
this.activeIndex = null;
this.removeSelection();
if (this.liCollections && this.liCollections.length > 0 && !this.isCustomFilter) {
this.removeFocus();
}
}
}
protected incrementalSearch(e: KeyboardEventArgs): void {
this.showPopup(e);
if (!isNullOrUndefined(this.listData)) {
this.inlineSearch(e);
e.preventDefault();
}
}
private setAutoFillSelection(currentValue: string, isKeyNavigate: boolean = false): void {
const selection: { [key: string]: number } = this.getSelectionPoints();
const value: string = this.inputElement.value.substr(0, selection.start);
if (value && (value.toLowerCase() === currentValue.substr(0, selection.start).toLowerCase())) {
const inputValue: string = value + currentValue.substr(value.length, currentValue.length);
Input.setValue(inputValue, this.inputElement, this.floatLabelType, this.showClearButton);
this.inputElement.setSelectionRange(selection.start, this.inputElement.value.length);
} else if (isKeyNavigate) {
Input.setValue(currentValue, this.inputElement, this.floatLabelType, this.showClearButton);
this.inputElement.setSelectionRange(0, this.inputElement.value.length);
}
}
protected getValueByText(text: string): string | number | boolean {
return super.getValueByText(text, true, this.ignoreAccent);
}
protected unWireEvent(): void {
if (this.getModuleName() === 'combobox') {
EventHandler.remove(this.inputWrapper.buttons[0], 'mousedown', this.preventBlur);
EventHandler.remove(this.inputWrapper.container, 'blur', this.onBlurHandler);
}
if (!isNullOrUndefined(this.inputWrapper.buttons[0])) {
EventHandler.remove(this.inputWrapper.buttons[0], 'mousedown', this.dropDownClick);
}
if (this.inputElement) {
EventHandler.remove(this.inputElement, 'focus', this.targetFocus);
if (!this.readonly) {
EventHandler.remove(this.inputElement, 'input', this.onInput);
EventHandler.remove(this.inputElement, 'keyup', this.onFilterUp);
EventHandler.remove(this.inputElement, 'keydown', this.onFilterDown);
EventHandler.remove(this.inputElement, 'paste', this.pasteHandler);
EventHandler.remove(window, 'resize', this.windowResize);
}
}
this.unBindCommonEvent();
}
protected setSelection(li: Element, e: MouseEvent | KeyboardEventArgs | TouchEvent): void {
super.setSelection(li, e);
if (!isNullOrUndefined(li) && !this.autofill && !this.isDropDownClick) {
this.removeFocus();
}
}
protected selectCurrentItem(e: KeyboardEventArgs): void {
let li: Element;
if (this.isPopupOpen) {
if (this.isSelected) {
li = this.list.querySelector('.' + dropDownListClasses.selected);
} else {
li = this.list.querySelector('.' + dropDownListClasses.focus);
}
if (this.isDisabledElement(li as HTMLElement)) {
return;
}
if (li) {
this.setSelection(li, e);
this.isTyped = false;
}
if (this.isSelected) {
this.isSelectCustom = false;
this.onChangeEvent(e);
}
}
if (e.action === 'enter' && this.inputElement.value === '') {
this.clearAll(e);
} else if (this.isTyped && !this.isSelected && isNullOrUndefined(li)) {
this.customValue(e);
}
this.hidePopup(e);
}
protected setHoverList(li: Element): void {
this.removeSelection();
if (this.isValidLI(li) && !li.classList.contains(dropDownListClasses.selected)) {
this.removeFocus();
li.classList.add(dropDownListClasses.focus);
}
}
private targetFocus(e: MouseEvent): void {
if (Browser.isDevice && !this.allowFiltering) {
this.preventFocus = false;
}
this.onFocus(e);
Input.calculateWidth(this.inputElement, this.inputWrapper.container);
}
protected dropDownClick(e: MouseEvent): void {
e.preventDefault();
if (Browser.isDevice && !this.isFiltering()) {
this.preventFocus = true;
}
super.dropDownClick(e);
}
private customValue(e?: MouseEvent | KeyboardEventArgs | TouchEvent): void {
let value: string | number | boolean | object = this.getValueByText(this.inputElement.value);
if (!this.allowCustom && this.inputElement.value !== '') {
const previousValue: string | number | boolean | object = this.previousValue;
const currentValue: string | number | boolean | object = this.value;
value = this.allowObjectBinding ? this.getDataByValue(value) : value;
this.setProperties({ value: value });
if (isNullOrUndefined(this.value)) {
Input.setValue('', this.inputElement, this.floatLabelType, this.showClearButton);
}
if (this.autofill && (
(!this.allowObjectBinding && previousValue === this.value) ||
(this.allowObjectBinding && previousValue && this.isObjectInArray(previousValue, [this.value]))
) && (
(!this.allowObjectBinding && currentValue !== this.value) ||
(this.allowObjectBinding && currentValue && !this.isObjectInArray(currentValue, [this.value]))
)) {
this.onChangeEvent(null);
}
} else if (this.inputElement.value !== '') {
const previousValue: string | number | boolean | object = this.value;
if (isNullOrUndefined(value)) {
const value: string | Object = this.inputElement.value === '' ? null : this.inputElement.value;
// eslint-disable-next-line max-len
const eventArgs: { [key: string]: Object | string | number } = <{ [key: string]: Object | string | number }>{ text: value, item: {} };
this.isObjectCustomValue = true;
if (!this.initial) {
this.trigger('customValueSpecifier', eventArgs, (eventArgs: { [key: string]: Object | string | number }) => {
this.updateCustomValueCallback(value, eventArgs, previousValue, e);
});
} else {
this.updateCustomValueCallback(value, eventArgs, previousValue);
}
} else {
this.isSelectCustom = false;
value = this.allowObjectBinding ? this.getDataByValue(value) : value;
this.setProperties({ value: value });
if ((!this.allowObjectBinding && previousValue !== this.value) ||
(this.allowObjectBinding && previousValue && this.value && !this.isObjectInArray(previousValue, [this.value]))
) {
this.onChangeEvent(e);
}
}
} else if (this.allowCustom && this.isFocused) {
this.isSelectCustom = true;
}
}
private updateCustomValueCallback(
value: string | Object,
eventArgs: { [key: string]: Object | string | number },
previousValue: string | number | boolean | object,
e?: MouseEvent | KeyboardEventArgs | TouchEvent): void {
const fields: FieldSettingsModel = this.fields;
const item: { [key: string]: string | Object } = <{ [key: string]: string | Object }>eventArgs.item;
let dataItem: { [key: string]: string | Object } = {};
if (item && getValue(fields.text, item) && getValue(fields.value, item)) {
dataItem = item;
} else {
setValue(fields.text, value, dataItem);
setValue(fields.value, value, dataItem);
}
this.itemData = <{ [key: string]: Object }>dataItem;
const emptyObject: { [key: string]: any } = {};
if (this.allowObjectBinding) {
let keys: string[] = this.listData && this.listData.length > 0 ? Object.keys(this.listData[0]) : Object.keys(this.itemData);
if ((!(this.listData && this.listData.length > 0)) && (this.getModuleName() === 'autocomplete' || (this.getModuleName() === 'combobox' && this.allowFiltering))){
keys = this.firstItem ? Object.keys(this.firstItem) : Object.keys(this.itemData);
}
// Create an empty object with predefined keys
keys.forEach((key: string) => {
emptyObject[key as any] = ((key === fields.value) || (key === fields.text)) ? getValue(fields.value, this.itemData) : null;
});
}
const changeData: { [key: string]: Object } = {
text: getValue(fields.text, this.itemData),
value: this.allowObjectBinding ? emptyObject : getValue(fields.value, this.itemData),
index: null
};
this.setProperties(changeData, true);
this.setSelection(null, null);
this.isSelectCustom = true;
this.isObjectCustomValue = false;
if ((!this.allowObjectBinding && (previousValue !== this.value)) ||
(this.allowObjectBinding && ((previousValue == null && this.value !== null) ||
(previousValue && !this.isObjectInArray(previousValue, [this.value]))))) {
this.onChangeEvent(e, true);
}
}
/**
* Dynamically change the value of properties.
*
* @param {ComboBoxModel} newProp - Returns the dynamic property value of the component.
* @param {ComboBoxModel} oldProp - Returns the previous property value of the component.
* @private
* @returns {void}
*/
public onPropertyChanged(newProp: ComboBoxModel, oldProp: ComboBoxModel): void {
if (this.getModuleName() === 'combobox') {
this.checkData(newProp);
this.setUpdateInitial(['fields', 'query', 'dataSource'], newProp as { [key: string]: string }, oldProp as { [key: string]: string });
}
for (const prop of Object.keys(newProp)) {
switch (prop) {
case 'readonly':
Input.setReadonly(this.readonly, this.inputElement as HTMLInputElement);
if (this.readonly) {
EventHandler.remove(this.inputElement, 'input', this.onInput);
EventHandler.remove(this.inputElement, 'keyup', this.onFilterUp);
EventHandler.remove(this.inputElement, 'keydown', this.onFilterDown);
} else {
EventHandler.add(this.inputElement, 'input', this.onInput, this);
EventHandler.add(this.inputElement, 'keyup', this.onFilterUp, this);
EventHandler.add(this.inputElement, 'keydown', this.onFilterDown, this);
}
this.setReadOnly();
break;
case 'allowFiltering':
this.setSearchBox();
if (this.isFiltering() && this.getModuleName() === 'combobox' && isNullOrUndefined(this.list)) {
super.renderList();
}
break;
case 'allowCustom':
break;
default: {
// eslint-disable-next-line max-len
const comboProps: { [key: string]: Object } = this.getPropObject(prop, <{ [key: string]: string }>newProp, <{ [key: string]: string }>oldProp);
super.onPropertyChanged(comboProps.newProperty, comboProps.oldProperty);
if (this.isFiltering() && prop === 'dataSource' && isNullOrUndefined(this.list) && this.itemTemplate &&
this.getModuleName() === 'combobox') {
super.renderList();
}
break;
}
}
}
}
/**
* To initialize the control rendering.
*
* @private
* @returns {void}
*/
public render(): void {
super.render();
this.setSearchBox();
this.renderComplete();
this.autoFill = this.autofill;
}
/**
* Return the module name of this component.
*
* @private
* @returns {string} Return the module name of this component.
*/
public getModuleName(): string {
return 'combobox';
}
/**
* Adds a new item to the combobox popup list. By default, new item appends to the list as the last item,
* but you can insert based on the index parameter.
*
* @param { Object[] } items - Specifies an array of JSON data or a JSON data.
* @param { number } itemIndex - Specifies the index to place the newly added item in the popup list.
* @returns {void}
* @deprecated
*/
public addItem(
items: { [key: string]: Object }[] | { [key: string]: Object } | string | boolean | number | string[] | boolean[] | number[],
itemIndex?: number): void {
super.addItem(items, itemIndex);
}
/**
* To filter the data from given data source by using query
*
* @param {Object[] | DataManager } dataSource - Set the data source to filter.
* @param {Query} query - Specify the query to filter the data.
* @param {FieldSettingsModel} fields - Specify the fields to map the column in the data table.
* @returns {void}
* @deprecated
*/
public filter(
dataSource: { [key: string]: Object }[] | DataManager | string[] | number[] | boolean[],
query?: Query, fields?: FieldSettingsModel): void {
super.filter(dataSource, query, fields);
}
/* eslint-disable valid-jsdoc, jsdoc/require-param */
/**
* Opens the popup that displays the list of items.
*
* @returns {void}
* @deprecated
*/
public showPopup(e?: MouseEvent | KeyboardEventArgs | TouchEvent): void {
/* eslint-enable valid-jsdoc, jsdoc/require-param */
super.showPopup(e);
}
/* eslint-disable valid-jsdoc, jsdoc/require-param */
/**
* Hides the popup if it is in open state.
*
* @returns {void}
* @deprecated
*/
public hidePopup(e?: MouseEvent | KeyboardEventArgs | TouchEvent): void {
/* eslint-enable valid-jsdoc, jsdoc/require-param */
const inputValue: string | Object = this.inputElement && this.inputElement.value === '' ? null
: this.inputElement && this.inputElement.value;
if (!isNullOrUndefined(this.listData)) {
const isEscape: boolean = this.isEscapeKey;
if (this.isEscapeKey) {
Input.setValue(this.typedString, this.inputElement, this.floatLabelType, this.showClearButton);
this.isEscapeKey = false;
}
if (this.autofill) {
this.removeFillSelection();
}
const dataItem: { [key: string]: string } = this.isSelectCustom ? { text: '' } : this.getItemData();
const text: string = !isNullOrUndefined(dataItem.text) ? dataItem.text.replace(/\r\n|\n|\r/g, '') : dataItem.text;
const selected: HTMLElement = !isNullOrUndefined(this.list) ? this.list.querySelector('.' + dropDownListClasses.selected) : null;
if (this.inputElement && text === this.inputElement.value && !isNullOrUndefined(selected)) {
if (this.isSelected) {
this.onChangeEvent(e);
this.isSelectCustom = false;
}
super.hidePopup(e);
return;
}
if (this.getModuleName() === 'combobox' && this.inputElement.value.trim() !== '') {
const dataSource: { [key: string]: Object }[] = this.sortedData as { [key: string]: Object }[];
const type: string = this.typeOfData(dataSource).typeof as string;
const searchItem: { [key: string]: number | Element } = Search(this.inputElement.value, this.liCollections, 'Equal', true, dataSource, this.fields, type);
this.selectedLI = searchItem.item as HTMLElement;
if (isNullOrUndefined(searchItem.index)) {
searchItem.index = Search(this.inputElement.value, this.liCollections, 'StartsWith', true, dataSource, this.fields, type).index as number;
}
this.activeIndex = searchItem.index as number;
if (!isNullOrUndefined(this.selectedLI)) {
this.updateSelectedItem(this.selectedLI, null, true);
} else if (isEscape) {
this.isSelectCustom = true;
this.removeSelection();
}
}
if (!this.isEscapeKey && this.isTyped && !this.isInteracted) {
this.customValue(e);
}
}
const value: string | number | boolean = this.allowObjectBinding && !isNullOrUndefined(this.value) ? getValue((this.fields.value) ? this.fields.value : '', this.value) : this.value;
if (isNullOrUndefined(this.listData) && this.allowCustom && !isNullOrUndefined(inputValue) && inputValue !== value) {
this.customValue();
}
super.hidePopup(e);
}
/**
* Sets the focus to the component for interaction.
*
* @returns {void}
*/
public focusIn(): void {
if (!this.enabled) {
return;
}
if (Browser.isDevice && !this.isFiltering()) {
this.preventFocus = true;
}
super.focusIn();
}
/**
* Allows you to clear the selected values from the component.
*
* @returns {void}
* @deprecated
*/
public clear(): void {
this.value = null;
}
/* eslint-disable valid-jsdoc, jsdoc/require-param */
/**
* Moves the focus from the component if the component is already focused.
*
* @returns {void}
* @deprecated
*/
public focusOut(e?: MouseEvent | KeyboardEventArgs): void {
/* eslint-enable valid-jsdoc, jsdoc/require-param */
super.focusOut(e);
}
/* eslint-disable valid-jsdoc, jsdoc/require-returns-description */
/**
* Gets all the list items bound on this component.
*
* @returns {Element[]}
* @deprecated
*/
public getItems(): Element[] {
return super.getItems();
}
/**
* Gets the data Object that matches the given value.
*
* @param { string | number } value - Specifies the value of the list item.
* @returns {Object}
* @deprecated
*/
public getDataByValue(value: string | number | boolean)
: { [key: string]: Object } | string | number | boolean {
return super.getDataByValue(value);
}
/* eslint-enable valid-jsdoc, jsdoc/require-returns-description */
protected renderHightSearch(): void {
// update high light search
}
}
export interface CustomValueSpecifierEventArgs {
/**
* Gets the typed custom text to make a own text format and assign it to `item` argument.
*/
text: string
/**
* Sets the text custom format data for set a `value` and `text`.
*
*/
item: { [key: string]: string | Object }
}