UNPKG

@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
/**----------------------------------------------------------------------------------------- * 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`&mdash;Controls the popup animation. By default, the open and close animations are enabled. * - `width: Number | String`&mdash;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`&mdash;Sets the height of the popup container. * - `popupClass: String`&mdash;Specifies a list of CSS classes that are used to style the popup. * - `appendTo: "root" | "component" | ViewContainerRef`&mdash;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&mdash; * 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>