@progress/kendo-angular-dropdowns
Version:
A wide variety of native Angular dropdown components including AutoComplete, ComboBox, DropDownList, DropDownTree, MultiColumnComboBox, MultiSelect, and MultiSelectTree
1,314 lines (1,306 loc) • 65.3 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { Component, Renderer2, forwardRef, ElementRef, Input, Output, EventEmitter, ContentChild, ViewChild, ViewContainerRef, TemplateRef, HostBinding, isDevMode, ChangeDetectorRef, NgZone, Injector } from '@angular/core';
import { isDocumentAvailable, KendoInput, hasObservers, SuffixTemplateDirective, PrefixTemplateDirective, isControlRequired, SeparatorComponent, ResizeSensorComponent, TemplateContextDirective } from '@progress/kendo-angular-common';
import { AdaptiveService } from '@progress/kendo-angular-utils';
import { NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { SearchBarComponent } from '../common/searchbar.component';
import { ItemTemplateDirective } from '../common/templates/item-template.directive';
import { HeaderTemplateDirective } from '../common/templates/header-template.directive';
import { FooterTemplateDirective } from '../common/templates/footer-template.directive';
import { GroupTemplateDirective } from '../common/templates/group-template.directive';
import { FixedGroupTemplateDirective } from '../common/templates/fixed-group-template.directive';
import { SelectionService } from '../common/selection/selection.service';
import { NavigationService } from '../common/navigation/navigation.service';
import { DisabledItemsService } from '../common/disabled-items/disabled-items.service';
import { Subject, Subscription, merge } from 'rxjs';
import { isPresent, guid, getter, isUntouched, noop, inDropDown, getSizeClass, getRoundedClass, getFillModeClass, isTruthy, setListBoxAriaLabelledBy, setActionSheetTitle, animationDuration } from '../common/util';
import { NavigationAction } from '../common/navigation/navigation-action';
import { NoDataTemplateDirective } from '../common/templates/no-data-template.directive';
import { PreventableEvent } from '../common/models/preventable-event';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { PopupService } from '@progress/kendo-angular-popup';
import { FilterableComponent } from '../common/filtering/filterable-component';
import { DataService } from '../common/data.service';
import { ListComponent } from '../common/list.component';
import { normalizeVirtualizationSettings } from '../common/models/virtualization-settings';
import { xIcon } from '@progress/kendo-svg-icons';
import { AdaptiveRendererComponent } from '../common/adaptive-renderer.component';
import { NgIf, NgTemplateOutlet } from '@angular/common';
import { SharedDropDownEventsDirective } from '../common/shared-events.directive';
import { LocalizedMessagesDirective } from '../common/localization/localized-messages.directive';
import { IconWrapperComponent } from '@progress/kendo-angular-icons';
import { touchEnabled } from '@progress/kendo-common';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
import * as i2 from "../common/data.service";
import * as i3 from "@progress/kendo-angular-popup";
import * as i4 from "../common/selection/selection.service";
import * as i5 from "../common/navigation/navigation.service";
import * as i6 from "../common/disabled-items/disabled-items.service";
import * as i7 from "@progress/kendo-angular-utils";
const NO_VALUE = "";
const DEFAULT_SIZE = 'medium';
const DEFAULT_ROUNDED = 'medium';
const DEFAULT_FILL_MODE = 'solid';
/**
* @hidden
*/
export const AUTOCOMPLETE_VALUE_ACCESSOR = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AutoCompleteComponent)
};
/**
* Represents the [Kendo UI AutoComplete component for Angular]({% slug overview_autocomplete %}).
*
* @example
* ```ts
* _@Component({
* selector: 'my-app',
* template: `
* <kendo-autocomplete
* [data]="listItems"
* [placeholder]="placeholder"
* >
* `
* })
* class AppComponent {
* public placeholder: string = 'Type "it" for suggestions';
* public listItems: Array<string> = ["Item 1", "Item 2", "Item 3", "Item 4"];
* }
* ```
*/
export class AutoCompleteComponent {
localization;
dataService;
popupService;
selectionService;
navigationService;
disabledItemsService;
_zone;
cdr;
renderer;
hostElement;
injector;
adaptiveService;
/**
* @hidden
*/
animationDuration = animationDuration;
/**
* @hidden
*/
xIcon = xIcon;
/**
* @hidden
*/
adaptiveRendererComponent;
/**
* @hidden
*/
get actionSheet() {
return this.adaptiveRendererComponent?.actionSheet;
}
/**
* @hidden
*/
get actionSheetSearchBar() {
return this.adaptiveRendererComponent?.actionSheetSearchBar;
}
get width() {
let wrapperOffsetWidth = 0;
if (isDocumentAvailable()) {
wrapperOffsetWidth = this.wrapper.offsetWidth;
}
const width = this.popupSettings.width || wrapperOffsetWidth;
const minWidth = isNaN(wrapperOffsetWidth) ? wrapperOffsetWidth : `${wrapperOffsetWidth}px`;
const maxWidth = isNaN(width) ? width : `${width}px`;
return { min: minWidth, max: maxWidth };
}
get height() {
const popupHeight = this.popupSettings.height;
return isPresent(popupHeight) ? `${popupHeight}px` : 'auto';
}
get listContainerClasses() {
const containerClasses = ['k-list-container', 'k-autocomplete-popup'];
if (this.popupSettings.popupClass) {
containerClasses.push(this.popupSettings.popupClass);
}
return containerClasses;
}
get suggestion() {
if (!this.text || !this.suggestedText) {
this.suggestedText = undefined;
return;
}
const hasMatch = this.suggestedText.toLowerCase().startsWith(this.text.toLowerCase());
const shouldSuggest = this.suggest && !this.backspacePressed;
if (shouldSuggest && hasMatch) {
return this.suggestedText;
}
}
get appendTo() {
const { appendTo } = this.popupSettings;
if (!appendTo || appendTo === 'root') {
return undefined;
}
return appendTo === 'component' ? this.container : appendTo;
}
get clearButtonVisiblity() {
if (touchEnabled) {
return 'visible';
}
}
get ariaControls() {
return this.isOpen ? this.listBoxId : undefined;
}
/**
* @hidden
*/
get isControlRequired() {
return isControlRequired(this.formControl);
}
dataItem;
/**
* Toggles the visibility of the popup or actionSheet.
* If you use the `toggle` method to open or close the popup or actionSheet, the `open` and `close` events will not be fired.
*
* @param open - The state of the popup.
*/
toggle(open) {
Promise.resolve(null).then(() => {
const shouldOpen = isPresent(open) ? open : !this._open;
this._toggle(shouldOpen);
});
}
/**
* Returns the current open state. Returns `true` if the popup or actionSheet is open.
*/
get isOpen() {
return isTruthy(this._open || this.isActionSheetExpanded);
}
/**
* @hidden
*/
handleClick() {
this.windowSize = this.adaptiveService.size;
if (this.isAdaptive) {
this.togglePopup(true);
}
}
/**
* @hidden
*/
togglePopup(open) {
const isDisabled = this.disabled || this.readonly;
const sameState = this.isOpen === open;
if (isDisabled || sameState) {
return;
}
const isDefaultPrevented = this.triggerPopupEvents(open);
if (!isDefaultPrevented) {
this._toggle(open);
}
}
get activeDescendant() {
if (!this.isOpen || !isPresent(this.selectionService.focused) || this.selectionService.focused === -1) {
return null;
}
return this.optionPrefix + "-" + this.selectionService.focused;
}
/**
* Defines whether the first match from the suggestions list will be automatically focused.
* By default, `highlightFirst` is set to `true`.
*/
highlightFirst = true;
/**
* Shows or hides the current group sticky header when using grouped data.
* By default the sticky header is displayed ([see example]({% slug grouping_autocomplete %}#toc-sticky-header)).
*/
showStickyHeader = true;
/**
* @hidden
*/
focusableId = `k-${guid()}`;
/**
* Sets the data of the AutoComplete.
*
* > The data has to be provided in an array-like list.
*/
set data(data) {
this.dataService.data = data || [];
if (this.virtual) {
this.virtual.skip = 0;
}
if (this.filterable) {
this.selectionService.focused = this.isOpen && this.data.length && this.highlightFirst ? this.firstFocusableIndex(0) : -1;
}
if (this.suggest && this.dataService.itemsCount > 0) {
this.suggestedText = getter(this.dataService.itemAt(0), this.valueField);
}
}
get data() {
const virtual = this.virtual;
if (virtual) {
const start = virtual.skip || 0;
const end = start + virtual.pageSize;
// Use length instead of itemsCount because of the grouping.
virtual.total = this.dataService.data.length;
return this.dataService.data.slice(start, end);
}
return this.dataService.data;
}
/**
* Sets the value of the AutoComplete.
*/
set value(newValue) {
this.verifySettings(newValue);
this._value = newValue || NO_VALUE;
this.text = this.value;
this.cdr.markForCheck();
}
get value() {
return this._value || NO_VALUE;
}
/**
* Specifies the `string` property of the data item that represents the item value.
* If the data contains only primitive values, do not define it.
*
* > The `valueField` property can be set to point to a nested property value - e.g. `category.name`.
*/
valueField;
/**
* The hint which is displayed when the component is empty.
*/
placeholder = "";
/**
* Enables or disables the adaptive mode. By default the adaptive rendering is disabled.
*/
adaptiveMode = 'none';
/**
* Sets the title of the ActionSheet that is rendered instead of the Popup when using small screen devices.
* By default the ActionSheet title uses the text provided for the label of the AutoComplete.
*/
adaptiveTitle = '';
/**
* Sets the subtitle of the ActionSheet that is rendered instead of the Popup when using small screen devices.
* By default the ActionSheet does not render a subtitle.
*/
adaptiveSubtitle;
/**
* @hidden
*/
get isAdaptiveModeEnabled() {
return this.adaptiveMode === 'auto';
}
/**
* Configures the popup of the AutoComplete.
*
* The available options are:
* - `animate: Boolean`—Controls the popup animation. By default, the open and close animations are enabled.
* - `width: Number | String`—Sets the width of the popup container. By default, the width of the host element is used. If set to `auto`, the component automatically adjusts the width of the popup and no item labels are wrapped. The `auto` mode is not supported when virtual scrolling is enabled.
* - `height: Number`—Sets the height of the popup container.
* - `popupClass: String`—Specifies a list of CSS classes that are used to style the popup.
* - `appendTo: "root" | "component" | ViewContainerRef`—Specifies the component to which the popup will be appended.
*/
set popupSettings(settings) {
this._popupSettings = Object.assign({ animate: true }, settings);
}
get popupSettings() {
return this._popupSettings;
}
/**
* Sets the height of the options list in the popup. By default, `listHeight` is 200px.
*
* > The `listHeight` property affects only the list of options and not the whole popup container.
* > To set the height of the popup container, use `popupSettings.height`.
*
* > When using `adaptiveMode` and the screen size is `small` or `medium` the `listHeight` property is set to null.
*/
set listHeight(_listHeight) {
this._listHeight = _listHeight;
}
get listHeight() {
if (this.isAdaptive) {
return;
}
return this._listHeight;
}
_listHeight = 200;
/**
* Sets and gets the loading state of the AutoComplete.
*/
loading;
/**
* @hidden
*
* If set to `true`, renders a button on hovering over the component.
* Clicking this button resets the value of the component to `undefined` and triggers the `change` event.
*/
clearButton = true;
/**
* Enables the auto-completion of the text based on the first data item.
*/
suggest;
/**
* Sets the disabled state of the component. To learn how to disable the component in reactive forms, refer to the article on [Forms Support](slug:formssupport_autocomplete#toc-managing-the-autocomplete-disabled-state-in-reactive-forms).
*/
disabled = false;
/**
* Defines a Boolean function that is executed for each data item in the component
* ([see examples]({% slug disableditems_autocomplete %})).
* Determines whether the item will be disabled.
*/
set itemDisabled(fn) {
if (typeof fn !== 'function') {
throw new Error(`itemDisabled must be a function, but received ${JSON.stringify(fn)}.`);
}
this.disabledItemsService.itemDisabled = fn;
}
/**
* Sets the read-only state of the component.
*
* @default false
*/
readonly = false;
/**
* Specifies the [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the component.
*/
tabindex = 0;
/**
* @hidden
*/
set tabIndex(tabIndex) {
this.tabindex = tabIndex;
}
get tabIndex() {
return this.tabindex;
}
/**
* Enables the [filtering]({% slug filtering_autocomplete %}) functionality.
* If set to `true`, the component emits the `filterChange` event.
*/
filterable = false;
/**
* Enables the [virtualization]({% slug virtualization_autocomplete %}) functionality.
*/
set virtual(settings) {
this._virtualSettings = normalizeVirtualizationSettings(settings);
}
get virtual() {
return this._virtualSettings;
}
/**
* Sets the size of the component.
*
* The possible values are:
* * `small`
* * `medium` (default)
* * `large`
* * `none`
*
*/
set size(size) {
const newSize = size ? size : DEFAULT_SIZE;
this.renderer.removeClass(this.wrapper, getSizeClass('input', this.size));
if (size !== 'none') {
this.renderer.addClass(this.wrapper, getSizeClass('input', newSize));
}
this._size = newSize;
}
get size() {
return this._size;
}
/**
* Sets the border radius of the component.
*
* The possible values are:
* * `small`
* * `medium` (default)
* * `large`
* * `full`
* * `none`
*
*/
set rounded(rounded) {
const newRounded = rounded ? rounded : DEFAULT_ROUNDED;
this.renderer.removeClass(this.wrapper, getRoundedClass(this.rounded));
if (rounded !== 'none') {
this.renderer.addClass(this.wrapper, getRoundedClass(newRounded));
}
this._rounded = newRounded;
}
get rounded() {
return this._rounded;
}
/**
* Sets the fillMode of the component.
*
* The possible values are:
* * `flat`
* * `solid` (default)
* * `outline`
* * `none`
*
*/
set fillMode(fillMode) {
const newFillMode = fillMode ? fillMode : DEFAULT_FILL_MODE;
this.renderer.removeClass(this.wrapper, getFillModeClass('input', this.fillMode));
if (fillMode !== 'none') {
this.renderer.addClass(this.wrapper, getFillModeClass('input', newFillMode));
}
this._fillMode = newFillMode;
}
get fillMode() {
return this._fillMode;
}
/**
* Sets the HTML attributes of the inner focusable input element. Attributes which are essential for certain component functionalities cannot be changed.
*/
inputAttributes;
/**
* Fires each time the value is changed—
* when the component is blurred or the value is cleared through the **Clear** button
* ([see example](slug:events_autocomplete)).
* When the value of the component is programmatically changed to `ngModel` or `formControl`
* through its API or form binding, the `valueChange` event is not triggered because it
* might cause a mix-up with the built-in `valueChange` mechanisms of the `ngModel` or `formControl` bindings.
*/
valueChange = new EventEmitter();
/**
* Fires each time the user types in the input field.
* You can filter the source based on the passed filtration value
* ([see example](slug:events_autocomplete)).
*/
filterChange = new EventEmitter();
/**
* Fires each time the popup is about to open.
* This event is preventable. If you cancel it, the popup will remain closed.
*/
open = new EventEmitter();
/**
* Fires after the popup has been opened.
*/
opened = new EventEmitter();
/**
* Fires each time the popup is about to close.
* This event is preventable. If you cancel it, the popup will remain open.
*/
close = new EventEmitter();
/**
* Fires after the popup has been closed.
*/
closed = new EventEmitter();
/**
* Fires each time the user focuses the AutoComplete.
*/
onFocus = new EventEmitter();
/**
* Fires each time the AutoComplete gets blurred.
*/
onBlur = new EventEmitter();
/**
* Fires each time the user focuses the `input` element.
*/
inputFocus = new EventEmitter();
/**
* Fires each time the `input` element gets blurred.
*/
inputBlur = new EventEmitter();
template;
headerTemplate;
footerTemplate;
noDataTemplate;
groupTemplate;
fixedGroupTemplate;
/**
* @hidden
*/
suffixTemplate;
/**
* @hidden
*/
prefixTemplate;
container;
popupTemplate;
searchbar;
optionsList;
widgetClasses = true;
get isFocused() {
return this._isFocused;
}
set isFocused(isFocused) {
this.renderer[isFocused ? 'addClass' : 'removeClass'](this.wrapper, "k-focus");
this._isFocused = isFocused;
}
get isDisabled() {
return this.disabled;
}
get isLoading() {
return this.loading;
}
get dir() {
return this.direction;
}
text;
listBoxId = `k-${guid()}`;
optionPrefix = `k-${guid()}`;
popupRef;
/**
* @hidden
*/
windowSize = 'large';
/**
* @hidden
*/
get isActionSheetExpanded() {
return this.actionSheet?.expanded;
}
/**
* @hidden
*/
get isAdaptive() {
return this.isAdaptiveModeEnabled && this.windowSize !== 'large';
}
/**
* @hidden
*/
get formControl() {
const ngControl = this.injector.get(NgControl, null);
return ngControl?.control || null;
}
onChangeCallback = noop;
onTouchedCallback = noop;
constructor(localization, dataService, popupService, selectionService, navigationService, disabledItemsService, _zone, cdr, renderer, hostElement, injector, adaptiveService) {
this.localization = localization;
this.dataService = dataService;
this.popupService = popupService;
this.selectionService = selectionService;
this.navigationService = navigationService;
this.disabledItemsService = disabledItemsService;
this._zone = _zone;
this.cdr = cdr;
this.renderer = renderer;
this.hostElement = hostElement;
this.injector = injector;
this.adaptiveService = adaptiveService;
validatePackage(packageMetadata);
this.direction = localization.rtl ? 'rtl' : 'ltr';
this.wrapper = this.hostElement.nativeElement;
this.data = [];
this.subscribeEvents();
this.subscribeTouchEvents();
this.selectionService.resetSelection([-1]);
}
ngOnInit() {
this.renderer.removeAttribute(this.wrapper, "tabindex");
this.subs.add(this.localization
.changes
.subscribe(({ rtl }) => {
this.direction = rtl ? 'rtl' : 'ltr';
this.cdr.detectChanges();
}));
this.setComponentClasses();
}
ngAfterViewInit() {
this.windowSize = this.adaptiveService.size;
this.cdr.detectChanges();
}
ngOnDestroy() {
this.destroyPopup();
this.subs.unsubscribe();
if (this.touchstartDisposeHandler) {
this.touchstartDisposeHandler();
}
}
ngOnChanges(changes) {
const virtual = this.virtual;
const requestInitialData = virtual && changes['data'] && changes['data'].isFirstChange();
if (requestInitialData) {
this.pageChange({ skip: 0, take: virtual.pageSize });
}
}
/**
* Resets the value of the AutoComplete.
* If you use the `reset` method to clear the value of the component,
* the model will not update automatically and the `valueChange` event will not be fired.
*/
reset() {
this.value = NO_VALUE;
}
/**
* @hidden
*/
messageFor(key) {
return this.localization.get(key);
}
/**
* @hidden
*/
clearValue(event) {
event.stopImmediatePropagation();
this.focus();
this.change(NO_VALUE);
if (this.filterable) {
this.filterChange.emit('');
}
this.selectionService.resetSelection([]);
}
/**
* @hidden
*/
writeValue(value) {
this.value = value;
}
/**
* @hidden
*/
registerOnChange(fn) {
this.onChangeCallback = fn;
}
/**
* @hidden
*/
registerOnTouched(fn) {
this.onTouchedCallback = fn;
}
/**
* @hidden
*/
setDisabledState(isDisabled) {
this.cdr.markForCheck();
this.disabled = isDisabled;
}
/**
* Focuses a specific item of the AutoComplete based on a provided index.
* If null or invalid index is provided the focus will be removed.
*/
focusItemAt(index) {
const isInRange = index >= 0 && index < this.data.length;
if (isPresent(index) && isInRange && !this.disabledItemsService.isIndexDisabled(index)) {
this.selectionService.focus(index);
}
else {
this.selectionService.focus(-1);
}
}
/**
* Focuses the AutoComplete.
*/
focus() {
if (!this.disabled) {
this.searchbar.focus();
}
}
/**
* Blurs the AutoComplete.
*/
blur() {
if (!this.disabled) {
this.searchbar.blur();
}
}
/**
* @hidden
*/
onResize() {
const currentWindowSize = this.adaptiveService.size;
if (this.isAdaptiveModeEnabled && this.windowSize !== currentWindowSize) {
if (this.isOpen) {
this.togglePopup(false);
}
this.windowSize = currentWindowSize;
this.cdr.detectChanges();
}
if (this._open && !this.isActionSheetExpanded) {
const popupWrapper = this.popupRef.popupElement;
const { min, max } = this.width;
popupWrapper.style.minWidth = min;
popupWrapper.style.width = max;
}
}
emitChange(value) {
this.onChangeCallback(value);
this.valueChange.emit(value);
}
verifySettings(newValue) {
if (!isDevMode()) {
return;
}
if (isPresent(newValue) && typeof newValue !== "string") {
throw new Error("Expected value of type string. See https://www.telerik.com/kendo-angular-ui/components/dropdowns/autocomplete/value-binding/");
}
}
search(text, startFrom = 0) {
const index = this.findIndex(text, startFrom);
if (this.disabledItemsService.isIndexDisabled(index)) {
if (index + 1 < this.dataService.itemsCount) {
this.search(text, index + 1);
}
else {
this.selectionService.focus(-1);
}
}
else {
this.selectionService.focus(index);
if (this.suggest) {
this.suggestedText = getter(this.dataService.itemAt(index), this.valueField);
}
}
}
navigate(index) {
if (!this.isOpen) {
return;
}
this.selectionService.focus(index);
}
/**
* @hidden
*/
handleNavigate(event) {
const focused = isNaN(this.selectionService.focused) ? this.firstFocusableIndex(0) : this.selectionService.focused;
if (this.disabled || this.readonly || isNaN(focused)) {
return;
}
const action = this.navigationService.process({
current: focused,
max: this.dataService.itemsCount - 1,
min: 0,
originalEvent: event
});
if (action !== NavigationAction.Undefined &&
action !== NavigationAction.Backspace &&
action !== NavigationAction.Delete &&
action !== NavigationAction.Home &&
action !== NavigationAction.End &&
action !== NavigationAction.Left &&
action !== NavigationAction.Right &&
action !== NavigationAction.PageDown &&
action !== NavigationAction.PageUp &&
((action === NavigationAction.Enter && this.isOpen) || action !== NavigationAction.Enter)) {
event.preventDefault();
}
if (action === NavigationAction.Tab && this.isActionSheetExpanded) {
event.stopImmediatePropagation();
this.togglePopup(false);
}
}
handleEnter(event) {
const focused = this.selectionService.focused;
let value;
if (this.isOpen) {
event.originalEvent.preventDefault();
}
if (focused >= 0) {
value = getter(this.dataService.itemAt(focused), this.valueField);
}
else {
const match = this.suggest && this.suggestedText && this.data.length &&
getter(this.dataService.itemAt(0), this.valueField).toLowerCase() === this.searchbar.value.toLowerCase();
if (this.isOpen && match) {
value = this.suggestedText;
}
else {
value = this.searchbar.value;
}
}
if (this.isActionSheetExpanded && focused >= 0) {
this.togglePopup(false);
}
this.change(value);
}
handleEscape() {
if (this.isOpen) {
this.togglePopup(false);
}
else {
this.value = '';
}
this.selectionService.focused = -1;
this.suggestedText = null;
}
/**
* @hidden
*/
searchBarChange(text) {
const currentTextLength = isPresent(this.text) ? this.text.length : 0;
this.backspacePressed = Boolean(text.length < currentTextLength);
this.text = text;
this.togglePopup(text.length > 0);
if (!this.highlightFirst) {
this.selectionService.focused = -1;
}
if (this.filterable) {
this.filterChange.emit(text);
}
else if (this.highlightFirst) {
this.search(text);
}
}
/**
* @hidden
*/
handleInputFocus() {
this.handleFocus();
if (hasObservers(this.inputFocus)) {
this._zone.run(() => {
this.inputFocus.emit();
});
}
}
/**
* @hidden
*/
handleFocus() {
this._zone.run(() => {
if (!this.isFocused && hasObservers(this.onFocus)) {
this.onFocus.emit();
}
this.isFocused = true;
});
}
/**
* @hidden
*/
handleBlur() {
if (!this.isActionSheetExpanded) {
this.blurComponent();
}
}
/**
* @hidden
*/
handleInputBlur() {
if (!this.isActionSheetExpanded) {
const focused = this.filterable ? this.selectionService.focused : -1;
this.searchbar.input.nativeElement.scrollLeft = 0; // Firefox doesn't auto-scroll to the left on blur like other browsers
let dataItem;
let text;
if (focused !== -1) {
dataItem = this.dataService.itemAt(focused);
text = getter(dataItem, this.valueField) || "";
}
else {
text = this.searchbar.value;
}
const exactMatch = text === this.searchbar.value;
const insensitiveMatch = text.toLowerCase() === this.searchbar.value.toLowerCase();
if (!exactMatch && insensitiveMatch) {
this.selectionService.resetSelection([]);
}
const valueHasChanged = this.value !== this.text;
const runInZone = hasObservers(this.inputBlur) ||
hasObservers(this.close) ||
isUntouched(this.wrapper) ||
valueHasChanged ||
this.formControl?.updateOn === 'blur';
if (runInZone) {
this._zone.run(() => {
if (valueHasChanged) {
this.change(this.searchbar.value);
}
this.inputBlur.emit();
this.onTouchedCallback();
this.togglePopup(false);
});
}
else {
this.togglePopup(false);
}
}
}
/**
* @hidden
*/
pageChange(event) {
const virtual = this.virtual;
virtual.skip = event.skip;
}
/**
* @hidden
*/
closeActionSheet() {
this.blurComponent();
this.closed.emit();
}
change(value) {
this.togglePopup(false);
this.valueChangeSubject.next(value);
}
popupMouseDownHandler = (event) => event.preventDefault();
_popupSettings = { animate: true };
_virtualSettings;
_open = false;
_value = "";
suggestedText;
backspacePressed;
subs = new Subscription();
valueChangeSubject = new Subject();
touchstartDisposeHandler;
wrapper;
_isFocused = false;
direction;
_size = 'medium';
_rounded = 'medium';
_fillMode = 'solid';
subscribeEvents() {
if (!isDocumentAvailable()) {
return;
}
this.subs.add(this.valueChangeSubject
.subscribe(value => {
const hasChange = this.value !== value;
const index = this.findIndex(value);
this.selectionService.focused = index;
this.value = value;
this.text = value;
// emit change after assigning `this.value` => allows the user to modify the component value on `valueChange`
if (hasChange) {
this.emitChange(value);
}
}));
this.subs.add(this.selectionService.onChange.subscribe(this.handleItemChange.bind(this)));
this.subs.add(this.selectionService.onFocus.subscribe(this.handleItemFocus.bind(this)));
this.subs.add(merge(this.navigationService.up, this.navigationService.down).subscribe((event) => this.navigate(event.index)));
this.subs.add(this.navigationService.close.subscribe(() => this.togglePopup(false)));
this.subs.add(this.navigationService.open.subscribe(() => this.togglePopup(true)));
this.subs.add(this.navigationService.enter.subscribe(this.handleEnter.bind(this)));
this.subs.add(this.navigationService.esc.subscribe(this.handleEscape.bind(this)));
this.subs.add(merge(this.navigationService.pagedown, this.navigationService.pageup).subscribe((event) => {
if (this.isOpen) {
event.originalEvent.preventDefault();
this.optionsList.scrollWithOnePage(NavigationAction[event.originalEvent.code]);
}
}));
}
findIndex(value, startFrom = 0) {
let index;
if (value && value.length && this.dataService.itemsCount) {
index = this.dataService.findIndex(this.findIndexPredicate(value), startFrom);
}
else {
index = -1;
}
return index;
}
subscribeTouchEvents() {
if (!isDocumentAvailable() || !touchEnabled) {
return;
}
this._zone.runOutsideAngular(() =>
// Roll up AutoComplete on iOS when tapped outside
this.touchstartDisposeHandler = this.renderer.listen(document, 'touchstart', (e) => {
const target = e.target;
if (this.isFocused && !inDropDown(this.hostElement, target, this.popupRef)) {
this._zone.run(() => this.blur());
}
}));
}
handleItemChange(event) {
const index = event.indices.length ? event.indices[0] : undefined;
this.selectionService.resetSelection([-1]);
if (!isPresent(index)) {
return;
}
const text = getter(this.dataService.itemAt(index), this.valueField);
this.change(text);
}
handleItemFocus(_event) {
const focused = this.selectionService.focused;
const shouldSuggest = Boolean(this.suggest && this.data && this.data.length && focused >= 0);
if (shouldSuggest) {
this.suggestedText = getter(this.dataService.itemAt(focused), this.valueField);
}
}
createPopup() {
if (this.virtual) {
this.virtual.skip = 0;
}
this.windowSize = this.adaptiveService.size;
if (this.isAdaptive) {
this.openActionSheet();
return;
}
const horizontalAlign = this.direction === "rtl" ? "right" : "left";
const anchorPosition = { horizontal: horizontalAlign, vertical: "bottom" };
const popupPosition = { horizontal: horizontalAlign, vertical: "top" };
const appendToComponent = typeof this.popupSettings.appendTo === 'string' && this.popupSettings.appendTo === 'component';
this.popupRef = this.popupService.open({
anchor: this.wrapper,
animate: this.popupSettings.animate,
appendTo: this.appendTo,
content: this.popupTemplate,
popupClass: this.listContainerClasses,
positionMode: appendToComponent ? 'fixed' : 'absolute',
popupAlign: popupPosition,
anchorAlign: anchorPosition
});
const popupWrapper = this.popupRef.popupElement;
const { min, max } = this.width;
popupWrapper.addEventListener('mousedown', this.popupMouseDownHandler);
popupWrapper.style.minWidth = min;
popupWrapper.style.width = max;
popupWrapper.style.height = this.height;
popupWrapper.setAttribute('dir', this.direction);
if (!this.appendTo) {
this.renderer.setAttribute(popupWrapper, 'role', 'region');
this.renderer.setAttribute(popupWrapper, 'aria-label', this.messageFor('popupLabel'));
}
this.subs.add(this.popupRef.popupOpen.subscribe(() => {
this.cdr.detectChanges();
setListBoxAriaLabelledBy(this.optionsList, this.searchbar.input, this.renderer);
this.optionsList.scrollToItem(this.selectionService.focused);
this.selectionService.focus(this.selectionService.focused);
this.opened.emit();
}));
this.subs.add(this.popupRef.popupClose.subscribe(() => {
this.closed.emit();
}));
this.subs.add(this.popupRef.popupAnchorViewportLeave.subscribe(() => this.togglePopup(false)));
}
destroyPopup() {
if (this.popupRef) {
this.popupRef.popupElement
.removeEventListener('mousedown', this.popupMouseDownHandler);
this.popupRef.close();
this.popupRef = null;
}
}
_toggle(open) {
this._open = open;
this.destroyPopup();
if (this.isActionSheetExpanded) {
this.actionSheet.toggle(false);
this.focus();
}
if (this._open) {
this.createPopup();
}
}
triggerPopupEvents(open) {
const eventArgs = new PreventableEvent();
if (open) {
this.open.emit(eventArgs);
}
else {
this.close.emit(eventArgs);
}
return eventArgs.isDefaultPrevented();
}
firstFocusableIndex(index) {
const maxIndex = this.data.length - 1;
if (this.disabledItemsService.isIndexDisabled(index)) {
return (index < maxIndex) ? this.firstFocusableIndex(index + 1) : undefined;
}
else {
return index;
}
}
findIndexPredicate(text) {
if (this.dataService.grouped) {
return (item) => {
let itemText = getter(item.value, this.valueField);
itemText = !isPresent(itemText) ? "" : itemText.toString().toLowerCase();
return itemText.startsWith(text.toLowerCase());
};
}
else {
return (item) => {
let itemText = getter(item, this.valueField);
itemText = !isPresent(itemText) ? "" : itemText.toString().toLowerCase();
return itemText.startsWith(text.toLowerCase());
};
}
}
setComponentClasses() {
if (this.size !== 'none') {
this.renderer.addClass(this.wrapper, getSizeClass('input', this.size));
}
if (this.rounded !== 'none') {
this.renderer.addClass(this.wrapper, getRoundedClass(this.rounded));
}
if (this.fillMode !== 'none') {
this.renderer.addClass(this.wrapper, getFillModeClass('input', this.fillMode));
}
}
openActionSheet() {
this.actionSheet.toggle(true);
this.cdr.detectChanges();
setListBoxAriaLabelledBy(this.optionsList, this.searchbar.input, this.renderer);
this.adaptiveTitle = setActionSheetTitle(this.searchbar.input, this.adaptiveTitle);
this.cdr.detectChanges();
this.opened.emit();
this.optionsList.scrollToItem(this.selectionService.focused);
this.selectionService.focus(this.selectionService.focused);
this.actionSheetSearchBar.focus();
}
blurComponent() {
this.isFocused = false;
const valueHasChanged = this.value !== this.text;
const runInZone = hasObservers(this.onBlur) ||
hasObservers(this.close) ||
isUntouched(this.wrapper) ||
valueHasChanged;
if (runInZone) {
this._zone.run(() => {
if (valueHasChanged) {
this.change(this.searchbar.value);
}
this.onBlur.emit();
this.onTouchedCallback();
this.togglePopup(false);
});
}
else {
this.togglePopup(false);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AutoCompleteComponent, deps: [{ token: i1.LocalizationService }, { token: i2.DataService }, { token: i3.PopupService }, { token: i4.SelectionService }, { token: i5.NavigationService }, { token: i6.DisabledItemsService }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i0.Injector }, { token: i7.AdaptiveService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: AutoCompleteComponent, isStandalone: true, selector: "kendo-autocomplete", inputs: { highlightFirst: "highlightFirst", showStickyHeader: "showStickyHeader", focusableId: "focusableId", data: "data", value: "value", valueField: "valueField", placeholder: "placeholder", adaptiveMode: "adaptiveMode", adaptiveTitle: "adaptiveTitle", adaptiveSubtitle: "adaptiveSubtitle", popupSettings: "popupSettings", listHeight: "listHeight", loading: "loading", clearButton: "clearButton", suggest: "suggest", disabled: "disabled", itemDisabled: "itemDisabled", readonly: "readonly", tabindex: "tabindex", tabIndex: "tabIndex", filterable: "filterable", virtual: "virtual", size: "size", rounded: "rounded", fillMode: "fillMode", inputAttributes: "inputAttributes" }, outputs: { valueChange: "valueChange", filterChange: "filterChange", open: "open", opened: "opened", close: "close", closed: "closed", onFocus: "focus", onBlur: "blur", inputFocus: "inputFocus", inputBlur: "inputBlur" }, host: { properties: { "class.k-readonly": "this.readonly", "class.k-autocomplete": "this.widgetClasses", "class.k-input": "this.widgetClasses", "class.k-disabled": "this.isDisabled", "class.k-loading": "this.isLoading", "attr.dir": "this.dir" } }, providers: [
AUTOCOMPLETE_VALUE_ACCESSOR,
DataService,
SelectionService,
NavigationService,
DisabledItemsService,
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.autocomplete'
},
{
provide: FilterableComponent,
useExisting: forwardRef(() => AutoCompleteComponent)
},
{
provide: KendoInput,
useExisting: forwardRef(() => AutoCompleteComponent)
}
], queries: [{ propertyName: "template", first: true, predicate: ItemTemplateDirective, descendants: true }, { propertyName: "headerTemplate", first: true, predicate: HeaderTemplateDirective, descendants: true }, { propertyName: "footerTemplate", first: true, predicate: FooterTemplateDirective, descendants: true }, { propertyName: "noDataTemplate", first: true, predicate: NoDataTemplateDirective, descendants: true }, { propertyName: "groupTemplate", first: true, predicate: GroupTemplateDirective, descendants: true }, { propertyName: "fixedGroupTemplate", first: true, predicate: FixedGroupTemplateDirective, descendants: true }, { propertyName: "suffixTemplate", first: true, predicate: SuffixTemplateDirective, descendants: true }, { propertyName: "prefixTemplate", first: true, predicate: PrefixTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "adaptiveRendererComponent", first: true, predicate: AdaptiveRendererComponent, descendants: true }, { propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "popupTemplate", first: true, predicate: ["popupTemplate"], descendants: true, static: true }, { propertyName: "searchbar", first: true, predicate: SearchBarComponent, descendants: true, static: true }, { propertyName: "optionsList", first: true, predicate: ["optionsList"], descendants: true }], exportAs: ["kendoAutoComplete"], usesOnChanges: true, ngImport: i0, template: `
<ng-container kendoAutoCompleteLocalizedMessages
i18n-noDataText="kendo.autocomplete.noDataText|The text displayed in the popup when there are no items"
noDataText="NO DATA FOUND"
i18n-clearTitle="kendo.autocomplete.clearTitle|The title of the clear button"
clearTitle="clear"
i18n-popupLabel="kendo.autocomplete.popupLabel|The label of the popup element that contains the list of options when its role is 'region'"
popupLabel="Options list"
i18n-adaptiveCloseButtonTitle="kendo.autocomplete.adaptiveCloseButtonTitle|The title of the Close button of the ActionSheet that is rendered instead of the Popup when using small screen devices in adaptive mode"
adaptiveCloseButtonTitle="Close"
>
</ng-container>
<ng-container
kendoDropDownSharedEvents
[hostElement]="hostElement"
[(isFocused)]="isFocused"
(handleBlur)="handleBlur()"
(onFocus)="handleFocus()"
>
<span *ngIf="prefixTemplate" class="k-input-prefix k-input-prefix-horizontal">
<ng-template [ngTemplateOutlet]="prefixTemplate?.templateRef">
</ng-template>
</span>
<kendo-separator *ngIf="prefixTemplate && prefixTemplate.showSeparator"></kendo-separator>
<input
kendoSearchbar
[ariaExpanded]="isOpen"
[isSuggestable]="suggest"
[isFilterable]="filterable"
[isLoading]="isLoading"
[ariaControls]="ariaControls"
[id]="focusableId"
[activeDescendant]="activeDescendant"
[userInput]="text"
[suggestedText]="suggestion"
[disabled]="disabled"
[readonly]="readonly || this.isAdaptive"
[tabIndex]="tabIndex"
[isRequired]="isControlRequired"
[placeholder]="placeholder"
[inputAttributes]="inputAttributes"
(onNavigate)="handleNavigate($event)"
(valueChange)="searchBarChange($event)"
(onBlur)="handleInputBlur()"
(onFocus)="handleInputFocus()"
(click)="handleClick()"
/>
<span
*ngIf="!loading && !readonly && (clearButton && text?.length)"
class="k-clear-value"
[style.visibility]="clearButtonVisiblity"
[attr.title]="messageFor('clearTitle')"
role="button"
tabindex="-1"
(click)="clearValue($event)"
(mousedown)="$event.preventDefault()"
>
<kendo-icon-wrapper
name="x"
[svgIcon]="xIcon"
>
</kendo-icon-wrapper>
</span>
<span *ngIf="loading" class="k-icon k-i-loading k-input-loading-icon"></span>
<kendo-separator *ngIf="suffixTemplate && suffixTemplate.showSeparator"></kendo-separator>
<span *ngIf="suffixTemplate" class="k-input-suffix k-input-suffix-horizontal">
<ng-template [ngTemplateOutlet]="suffixTemplate?.templateRef">
</ng-template>
</span>
</ng-container>
<ng-template #popupTemplate>
<ng-container *ngTemplateOutlet="sharedPopupActionSheetTemplate"></ng-container>
</ng-template>
<ng-container #container></ng-container>
<kendo-resize-sensor *ngIf="isOpen || isAdaptiveModeEnabled" (resize)="onResize()"></kendo-resize-sensor>
<kendo-adaptive-renderer
[sharedPopupActionSheetTemplate]="sharedPopupActionSheetTemplate"
[title]="adaptiveTitle"
[showTextInput]="true"
[subtitle]="adaptiveSubtitle"
(closePopup)="closeActionSheet()"
(textInputChange)="searchBarChange($event)"
(navigate)="handleNavigate($event)"
[placeholder]="placeholder"
[searchBarValue]="text">
</kendo-adaptive-renderer>
<ng-template #sharedPopupActionSheetTemplate>
<!--header template-->
<ng-template *ngIf="headerTemplate"
[templateContext]="{
templateRef: headerTemplate.templateRef
}">
</ng-template>
<!--list-->
<kendo-list
#optionsList
[size]="isAdaptive ? 'large' : size"
[rounded]="rounded"
[id]="listBoxId"
[optionPrefix]="optionPrefix"
[data]="data"
[textField]="valueField"
[valueField]="valueField"
[template]="template"
[groupTemplate]="groupTemplate"
[fixedGroupTemplate]="fixedGroupTemplate"
[height]="listHeight"
[show]="isOpen"
[virtual]="virtual"
[showStickyHeader]="showStickyHeader"
(pageChange)="pageChange($event)"
>
</kendo-list>
<!--no-data template-->
<div class="k-no-data" *ngIf="data.length === 0">
<ng-template [ngIf]="noDataTemplate"
[templateContext]="{
templateRef: noDataTemplate?.templateRef
}">
</ng-template>
<ng-template [ngIf]="!noDataTemplate">
<div>{{ messageFor('noDataText') }}</div>
</ng-template>
</div>