@progress/kendo-angular-dropdowns
Version:
A wide variety of native Angular dropdown components including AutoComplete, ComboBox, DropDownList, DropDownTree, MultiColumnComboBox, MultiSelect, and MultiSelectTree
1,354 lines (1,353 loc) • 76.7 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 { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostBinding, Input, Output, NgZone, Renderer2, TemplateRef, ViewChild, ChangeDetectorRef, ContentChild, forwardRef, ViewContainerRef, isDevMode, Injector } from '@angular/core';
import { anyChanged, EventsOutsideAngularDirective, guid, hasObservers, isChanged, isDocumentAvailable, KendoInput, Keys, ResizeSensorComponent, TemplateContextDirective } from '@progress/kendo-angular-common';
import { AdaptiveService } from '@progress/kendo-angular-utils';
import { PopupService } from '@progress/kendo-angular-popup';
import { TreeViewComponent, DataBoundComponent, ExpandableComponent, SelectDirective } from '@progress/kendo-angular-treeview';
import { getter, touchEnabled } from '@progress/kendo-common';
import { NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DataService } from '../common/data.service';
import { DisabledItemsService } from '../common/disabled-items/disabled-items.service';
import { NavigationService } from '../common/navigation/navigation.service';
import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
import { SelectionService } from '../common/selection/selection.service';
import { PreventableEvent } from '../common/models/preventable-event';
import { animationDuration, getFillModeClass, getRoundedClass, getSearchableItems, getSizeClass, inDropDown, isArray, isLetter, isPresent, isTruthy, isUntouched, noop, setActionSheetTitle, updateActionSheetAdaptiveAppearance } from '../common/util';
import { NoDataTemplateDirective } from '../common/templates/no-data-template.directive';
import { of, Subject, Subscription } from 'rxjs';
import { debounceTime, take, tap } from 'rxjs/operators';
import { HeaderTemplateDirective } from '../common/templates/header-template.directive';
import { FooterTemplateDirective } from '../common/templates/footer-template.directive';
import { NodeTemplateDirective } from './templates/node-template.directive';
import { DropDownTreeMessages } from '../common/constants/error-messages';
import { ValueTemplateDirective } from '../common/templates/value-template.directive';
import { caretAltDownIcon, searchIcon, xIcon } from '@progress/kendo-svg-icons';
import { ResponsiveRendererComponent } from '../common/action-sheet.component';
import { FilterInputDirective } from '../common/filter-input.directive';
import { NgIf, NgClass, NgTemplateOutlet } from '@angular/common';
import { LocalizedMessagesDirective } from '../common/localization/localized-messages.directive';
import { IconWrapperComponent } from '@progress/kendo-angular-icons';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-popup";
import * as i2 from "../common/navigation/navigation.service";
import * as i3 from "@progress/kendo-angular-l10n";
import * as i4 from "@progress/kendo-angular-utils";
const DEFAULT_POPUP_SETTINGS = { animate: true };
const hasChildren = () => false;
const fetchChildren = () => of([]);
const itemDisabled = () => false;
const isNodeVisible = () => true;
const DEFAULT_SIZE = 'medium';
const DEFAULT_ROUNDED = 'medium';
const DEFAULT_FILL_MODE = 'solid';
const stopPropagation = (event) => event.stopImmediatePropagation();
/**
* Represents the [Kendo UI DropDownTree component for Angular]({% slug overview_ddt %}).
*/
export class DropDownTreeComponent {
injector;
wrapper;
popupService;
navigationService;
renderer;
_zone;
cdr;
localization;
adaptiveService;
/**
* @hidden
*/
icon;
/**
* @hidden
*/
svgIcon;
/**
* @hidden
*/
touchEnabled = touchEnabled;
/**
* @hidden
*/
animationDuration = animationDuration;
/**
* @hidden
*/
searchIcon = searchIcon;
/**
* @hidden
*/
caretAltDownIcon = caretAltDownIcon;
/**
* @hidden
*/
xIcon = xIcon;
/**
* @hidden
*/
responsiveRendererComponent;
/**
* @hidden
*/
get actionSheet() {
return this.responsiveRendererComponent?.actionSheet;
}
/**
* @hidden
*/
get actionSheetSearchBar() {
return this.responsiveRendererComponent?.actionSheetSearchBar;
}
hostClasses = true;
get isReadonly() {
return this.readonly ? '' : null;
}
get hostAriaInvalid() {
return this.formControl?.invalid ? true : null;
}
get isDisabled() {
return this.disabled || null;
}
get isLoading() {
return this.loading;
}
get isBusy() {
return this.loading ? 'true' : null;
}
get hostAriaControls() {
return this.isOpen ? this.treeViewId : undefined;
}
get id() {
return this.focusableId;
}
direction;
get hostTabIndex() {
return this.tabindex;
}
role = 'combobox';
ariaHasPopup = 'tree';
get isAriaExpanded() {
return this.isOpen;
}
get hostAriaAutocomplete() {
return this.filterable ? 'list' : null;
}
noDataTemplate;
headerTemplate;
footerTemplate;
nodeTemplate;
valueTemplate;
popupTemplate;
container;
set treeview(treeview) {
if (treeview) {
if (this.isFocused && !this.filterable || this.touchEnabled) {
treeview.focus();
}
// the treeview animations are initially disabled (we don't want expand animations during popup opening)
// re-enables the animations for user interaction
treeview.animate = true;
this._treeview = treeview;
}
}
get treeview() {
return this._treeview;
}
filterInput;
/**
* Fires each time the popup is about to open
* ([see example]({% slug openstate_ddt %})).
* 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
* ([see example]({% slug openstate_ddt %})).
* 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 when the user expands a node in the popup TreeView.
*/
nodeExpand = new EventEmitter();
/**
* Fires when the user collapses a node in the popup TreeView.
*/
nodeCollapse = new EventEmitter();
/**
* Fires each time the user focuses the DropDownTree.
*/
onFocus = new EventEmitter();
/**
* Fires each time the DropDownTree gets blurred.
*/
onBlur = new EventEmitter();
/**
* Fires each time the value is changed
* ([see example](slug:events_ddt)).
*/
valueChange = new EventEmitter();
/**
* Fires when the value of the built-in filter input element changes.
*/
filterChange = new EventEmitter();
/**
* Sets and gets the loading state of the DropDownTree.
*/
loading;
/**
* 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;
/**
* Sets the data of the DropDownTree.
*
* > The data has to be provided in an array-like list with objects.
*/
set data(data) {
this._nodes = data;
this.setState();
}
get data() {
return this._nodes;
}
/**
* Sets the value of the DropDownTree.
* It can either be of the primitive (string, numbers) or of the complex (objects) type.
* To define the type, use the `valuePrimitive` option.
*
*/
set value(newValue) {
this._value = newValue;
this.setState();
}
get value() {
return this._value;
}
/**
* The fields of the data item that provide the text content of the nodes inside the
* DropDownTree ([see example]({% slug databinding_ddt %})). If the `textField`
* input is set to an array, each hierarchical level uses the field that corresponds
* to the same index in the array, or the last item in the array.
*
* > The `textField` property can be set to point to a nested property value - e.g. `category.name`.
*/
textField;
/**
* The fields of the data item that provide the value of the nodes inside the
* DropDownTree ([see example]({% slug databinding_ddt %})). If the `valueField`
* input is set to an array, each hierarchical level uses the field that corresponds
* to the same index in the array, or the last item in the array.
*
* > The `valueField` property can be set to point to a nested property value - e.g. `category.id`.
*/
valueField;
/**
* Sets the level in the data set where the value can be found when `valueField` is an Array.
* The field serves to correctly allocate a data item used when the DropDownTree is initialized with a value.
*/
valueDepth;
/**
* A function which determines if a specific node has child nodes.
*/
hasChildren = hasChildren;
/**
* A function which provides the child nodes for a given parent node.
*/
fetchChildren = fetchChildren;
/**
* The hint which is displayed when the component is empty.
*/
placeholder = "";
/**
* Configures the popup of the DropDownTree.
*
* 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.
* - `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({}, DEFAULT_POPUP_SETTINGS, settings);
}
get popupSettings() {
return this._popupSettings;
}
/**
* Keeps the current `dataItem` object in order to resolve selection.
* Needs to be provided when `value` is bound in and `valuePrimitive` is set to true.
*/
set dataItem(item) {
this._dataItem = item;
this.setState();
}
get dataItem() {
return this._dataItem ? this._dataItem : this.value;
}
/**
* 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 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_ddt#toc-managing-the-dropdowntree-disabled-state-in-reactive-forms).
*/
disabled = false;
/**
* Sets the read-only state of the component.
*
* @default false
*/
readonly = false;
/**
* Specifies the type of the selected value
* ([more information and example]({% slug valuebinding_ddt %}#toc-primitive-values)).
* If set to `true`, the selected value has to be of a primitive value.
*/
valuePrimitive = false;
/**
* Specifies the [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the component.
*/
set tabindex(value) {
const providedTabIndex = Number(value);
const defaultTabIndex = 0;
this._tabindex = !isNaN(providedTabIndex) ? providedTabIndex : defaultTabIndex;
}
get tabindex() {
return this.disabled ? -1 : this._tabindex;
}
/**
* 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.nativeElement, getSizeClass('picker', this.size));
if (size !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, getSizeClass('picker', 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.nativeElement, getRoundedClass(this.rounded));
if (rounded !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, 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.nativeElement, getFillModeClass('picker', this.fillMode));
if (fillMode !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, getFillModeClass('picker', newFillMode));
}
this._fillMode = newFillMode;
}
get fillMode() {
return this._fillMode;
}
/**
* A function that is executed for each data item and determines if a specific item is disabled.
*/
itemDisabled = itemDisabled;
/**
* A function that is executed for each data item and determines if a specific node is expanded.
*/
isNodeExpanded;
/**
* A callback which determines whether a tree node should be rendered as hidden. The utility .k-hidden class is used to hide the nodes.
* Useful for custom filtering implementations.
*/
isNodeVisible = isNodeVisible;
/**
* Indicates whether the child nodes will be fetched on node expand or will be initially prefetched.
* @default true
*/
loadOnDemand = true;
/**
* Renders the built-in input element for filtering the DropDownTree.
* If set to `true`, the component emits the `filterChange` event, which can be used to [filter the DropDownTree manually]({% slug filtering_ddt %}#toc-manual-filtering).
* A built-in filtering implementation is available to use with the [`kendoDropDownTreeHierarchyBinding`]({% slug api_dropdowns_dropdowntreehierarchybindingdirective %}) and [`kendoDropDownTreeFlatBinding`]({% slug api_dropdowns_dropdowntreeflatbindingdirective %}) directives.
*/
filterable = false;
/**
* @hidden
*/
filter = '';
/**
* @hidden
*
* Used by the kendo-label and kendo-floatinglabel to access and associate the focusable element with the provided label via aria-labelledby.
*/
focusableId = `k-${guid()}`;
set isFocused(isFocused) {
this.renderer[isFocused ? 'addClass' : 'removeClass'](this.wrapper.nativeElement, 'k-focus');
this._isFocused = isFocused;
}
get isFocused() {
return this._isFocused;
}
get width() {
const wrapperWidth = this.wrapper.nativeElement.offsetWidth;
const width = this.popupSettings.width || wrapperWidth;
const minWidth = isNaN(wrapperWidth) ? wrapperWidth : `${wrapperWidth}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';
}
/**
* Returns the current open state. Returns `true` if the popup or actionSheet is open.
*/
get isOpen() {
return isTruthy(isPresent(this.popupRef) || this.isActionSheetExpanded);
}
get clearButtonVisiblity() {
if (this.touchEnabled) {
return 'visible';
}
}
get isFilterActive() {
return this.filterInput && document.activeElement === this.filterInput.nativeElement;
}
popupRef;
/**
* @hidden
*/
selectedKeys = [];
/**
* @hidden
*/
selectBy;
/**
* @hidden
*/
text;
/**
* @hidden
*/
onFilterChange(text) {
if (this.filterable) {
this.filterChange.emit(text);
}
}
/**
* 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.
*/
title = '';
/**
* Sets the subtitle of the ActionSheet that is rendered instead of the Popup when using small screen devices.
* By default the ActionSheet subtitle uses the text provided for the `placeholder` of the AutoComplete.
*/
subtitle = '';
/**
* @hidden
*/
get isAdaptiveModeEnabled() {
return this.adaptiveMode === 'auto';
}
/**
* @hidden
*/
windowSize = 'large';
/**
* @hidden
*/
isActionSheetExpanded = false;
/**
* @hidden
*/
get isAdaptive() {
return this.isAdaptiveModeEnabled && this.windowSize !== 'large';
}
/**
* @hidden
*
* Alias for `isNodeExpanded`. Used for compatibility with the `ExpandableComponent` interface.
* Required for the expand-directive.
*/
set isExpanded(callback) {
this.isNodeExpanded = callback;
}
get isExpanded() {
return this.isNodeExpanded;
}
/**
* @hidden
*
* Alias for `nodeExpand`. Used for compatibility with the `ExpandableComponent` interface.
* Required for the expand-directive.
*/
get expand() {
return this.nodeExpand;
}
/**
* @hidden
*
* Alias for `nodeCollapse`. Used for compatibility with the `ExpandableComponent` interface.
* Required for the expand-directive.
*/
get collapse() {
return this.nodeCollapse;
}
/**
* @hidden
*
* Alias for `data`. Used for compatibility with the `DataBoundComponent` interface.
* Required for the data-binding directives.
*/
set nodes(nodes) {
this.data = nodes;
}
get nodes() {
return this.data;
}
/**
* @hidden
*
* Alias for `fetchChildren`. Used for compatibility with the `DataBoundComponent` interface.
* Required for the data-binding directives
*/
set children(callback) {
this.fetchChildren = callback;
}
get children() {
return this.fetchChildren;
}
/**
* @hidden
*
* Alias for `isNodeVisible`. Used for compatibility with the `DataBoundComponent` interface.
* The `DataBoundComponent` interface is used in the data-binding directives.
*/
set isVisible(callback) {
this.isNodeVisible = callback;
}
get isVisible() {
return this.isNodeVisible;
}
/**
* @hidden
*/
filterStateChange = new EventEmitter();
/**
* @hidden
*/
allNodesHidden = false;
/**
* @hidden
*
* Used to associate the value label with the wrapper via aria-describedby.
*/
valueLabelId = `k-${guid()}`;
/**
* @hidden
*/
get formControl() {
const ngControl = this.injector.get(NgControl, null);
return ngControl?.control || null;
}
treeViewId = `k-${guid()}`;
_nodes;
_value;
_popupSettings = DEFAULT_POPUP_SETTINGS;
_tabindex = 0;
_isFocused = false;
_dataItem;
_treeview;
_size = 'medium';
_rounded = 'medium';
_fillMode = 'solid';
_searchableNodes = [];
_typedValue = '';
printableCharacters = new Subject();
subs = new Subscription();
touchstartDisposeHandler;
// Keep an instance of the last focused node for when the popup close is prevented
// in order to be able to properly restore the focus
lastNodeOnFocus;
constructor(injector, wrapper, popupService, navigationService, renderer, _zone, cdr, localization, adaptiveService) {
this.injector = injector;
this.wrapper = wrapper;
this.popupService = popupService;
this.navigationService = navigationService;
this.renderer = renderer;
this._zone = _zone;
this.cdr = cdr;
this.localization = localization;
this.adaptiveService = adaptiveService;
this.direction = localization.rtl ? 'rtl' : 'ltr';
this.subscribeEvents();
this.subscribeTouchEvents();
this.subscribeFocusEvents();
}
ngOnInit() {
this.subs.add(this.printableCharacters.pipe(tap((char) => {
this._typedValue += char;
const itemToFocus = this._searchableNodes.find((node) => {
return node.text.toLowerCase().indexOf(this._typedValue) === 0;
});
this.treeview.focus(itemToFocus?.index);
}), debounceTime(1000)).subscribe(() => {
this._typedValue = '';
}));
this.renderer.removeAttribute(this.wrapper.nativeElement, 'tabindex');
this.assignAriaDescribedBy();
this.subs.add(this.localization
.changes.subscribe(({ rtl }) => {
this.direction = rtl ? 'rtl' : 'ltr';
this.cdr.markForCheck();
}));
this.setComponentClasses();
}
/**
* @hidden
*/
ngOnDestroy() {
this.destroyPopup();
this.unsubscribeEvents();
}
/**
* @hidden
*/
ngOnChanges(changes) {
if (anyChanged(['textField', 'valueField', 'valuePrimitive'], changes, false)) {
this.setState();
}
if (isChanged('value', changes, false)) {
if (changes['value'] && !changes['dataItem'] && !this.valuePrimitive) {
// Update the dataItem if the value is updated programmatically (non-primitive values only)
this.dataItem = this.value;
}
}
}
/**
* @hidden
*/
ngAfterContentChecked() {
this.verifySettings();
if (this.data?.length > 0 && this.popupRef) {
this.allNodesHidden = this.areNodesHidden(this.data);
}
}
ngAfterViewInit() {
this.windowSize = this.adaptiveService.size;
this.subs.add(this.renderer.listen(this.wrapper.nativeElement, 'click', this.handleClick.bind(this)));
this.subs.add(this.renderer.listen(this.wrapper.nativeElement, 'keydown', this.handleKeydown.bind(this)));
if (this.actionSheet && isDocumentAvailable()) {
// The following syntax is used as it does not violate CSP compliance
this.actionSheet.element.nativeElement.style.setProperty('--kendo-actionsheet-height', '60vh');
this.actionSheet.element.nativeElement.style.setProperty('--kendo-actionsheet-max-height', 'none');
}
}
/**
* @hidden
*/
onResize() {
const currentWindowSize = this.adaptiveService.size;
if (this.isAdaptiveModeEnabled && this.windowSize !== currentWindowSize) {
if (this.isOpen) {
this.togglePopup(false);
}
this.windowSize = currentWindowSize;
}
if (this.isOpen && !this.isActionSheetExpanded) {
const popupWrapper = this.popupRef.popupElement;
const { min, max } = this.width;
popupWrapper.style.minWidth = min;
popupWrapper.style.width = max;
}
}
/**
* @hidden
*
* Used by the kendo-floatinglabel component to determine if the floating label
* should be rendered inside the input when the component is not focused.
*/
isEmpty() {
return !this.text && !this.placeholder;
}
/**
* @hidden
*/
togglePopup(open) {
const isDisabled = this.disabled || this.readonly;
const sameState = this.isOpen === open;
if (isDisabled || sameState) {
return;
}
const togglePrevented = this.triggerPopupEvents(open);
if (!togglePrevented) {
if (open) {
this.createPopup();
}
else {
this.destroyPopup();
}
}
}
/**
* @hidden
*/
handleFocus(event) {
if (event.target !== this.wrapper.nativeElement) {
return;
}
event.stopImmediatePropagation();
if (!this.isFocused) {
this.isFocused = true;
if (hasObservers(this.onFocus)) {
this._zone.run(() => {
this.onFocus.emit();
});
}
}
}
/**
* @hidden
*/
handleBlur(e) {
if (!this.isActionSheetExpanded) {
const relatedTarget = e && e.relatedTarget;
if (this.wrapper.nativeElement.contains(relatedTarget) ||
(this.isOpen && this.popupRef.popupElement.contains(relatedTarget))) {
return;
}
this.isFocused = false;
this.togglePopup(false);
if (hasObservers(this.onBlur) ||
isUntouched(this.wrapper.nativeElement) ||
this.formControl?.updateOn === 'blur') {
this._zone.run(() => {
this.onBlur.emit();
this.onTouchedCallback();
});
}
}
}
/**
* @hidden
*/
handleKeydown(event, input) {
if (this.disabled || this.readonly) {
return;
}
if (event.keyCode === Keys.Tab && this.isActionSheetExpanded) {
this.togglePopup(false);
return;
}
if (!this.isFilterActive && isLetter(event.key) && !this.actionSheetSearchBar?.onFocus) {
this.printableCharacters.next(event.key);
}
const eventData = event;
this.navigationService.process({
originalEvent: eventData,
openOnSpace: !this.isOpen,
closeOnSpace: this.isOpen && !input && !(event.target instanceof HTMLInputElement)
});
}
/**
* Focuses a specific item of the DropDownTree based on a provided index in the format of `1_1`.
* The targeted item should be expanded in order for it to be focused.
* If null or invalid index is provided the focus will be set on the first item.
*/
focusItemAt(index) {
if (this.treeview) {
const lookup = this.treeview.itemLookup(index);
const isItemDisabled = !isPresent(lookup) || this.treeview.isDisabled(lookup.item.dataItem, lookup.item.index);
if (!isItemDisabled) {
this.treeview.focus(index);
}
}
}
/**
* Focuses the DropDownTree.
*/
focus() {
if (!this.disabled) {
this.wrapper.nativeElement.focus();
}
}
/**
* Blurs the DropDownTree.
*/
blur() {
if (!this.disabled) {
this.wrapper.nativeElement.blur();
}
}
/**
* Resets the value of the DropDownTree.
* 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 = undefined;
this.dataItem = undefined;
}
/**
* Toggles the visibility of the popup or actionSheet.
* ([see example]({% slug openstate_ddt %})).
* 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) {
// The Promise is required to open the popup on load.
// Otherwise, the "ViewContainerRef not found..." error will be thrown.
Promise.resolve(null).then(() => {
const shouldOpen = isPresent(open) ? open : !isPresent(this.popupRef);
this.destroyPopup();
if (shouldOpen) {
this.createPopup();
}
});
}
/**
* @hidden
*/
get popupContainerClasses() {
const containerClasses = ['k-dropdowntree-popup'];
if (this.popupSettings.popupClass) {
containerClasses.push(this.popupSettings.popupClass);
}
return containerClasses;
}
/**
* @hidden
*/
onSelectionChange({ dataItem, index }) {
this.valueDepth = index.split('_').length - 1;
const valueField = this.getField(this.valueField, dataItem);
const newValue = this.valuePrimitive ?
getter(valueField)(dataItem) :
dataItem;
const shouldUpdateValue = newValue !== this.value;
if (shouldUpdateValue) {
this.value = newValue;
this.dataItem = dataItem;
this.emitValueChange(this.value);
}
this.togglePopup(false);
this.focus();
}
/**
* @hidden
*/
messageFor(key) {
return this.localization.get(key);
}
/**
* @hidden
*/
clearValue(event) {
event.stopImmediatePropagation();
this.focus();
this.value = undefined;
this.dataItem = undefined;
this.clearState();
this.valueChange.emit(undefined);
this.emitValueChange();
}
get appendTo() {
const { appendTo } = this.popupSettings;
if (!appendTo || appendTo === 'root') {
return undefined;
}
return appendTo === 'component' ? this.container : appendTo;
}
/**
* @hidden
*/
preventEventDefault(event) {
event.preventDefault();
}
/**
* @hidden
*/
writeValue(value) {
// If the user resets the value by providing null/undefined we need to reset the `dataItem`
// Because upon initialization of the component the `writeValue` is being called twice -
// first time with `null` value regardless of sync/async value - an extra check is added to
// distinguish between client reset and initial phantom 'null' value
if (!isPresent(value) && isPresent(this.value)) {
this.dataItem = null;
}
this.value = value === null ? undefined : value;
// Update the dataItem if the value is updated programmatically via a form control (non-primitive values only)
if (isPresent(this.value) && !this.valuePrimitive) {
this.dataItem = this.value;
}
}
/**
* @hidden
*/
registerOnChange(fn) {
this.onChangeCallback = fn;
}
/**
* @hidden
*/
registerOnTouched(fn) {
this.onTouchedCallback = fn;
}
/**
* @hidden
*/
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this.cdr.markForCheck();
}
/**
* @hidden
*/
handleFilterInputChange(input) {
const value = typeof input === 'string' ? input : input.value;
this.filter = value;
this.filterChange.next(value);
this.allNodesHidden = this.areNodesHidden(this.nodes);
this._zone.onStable.pipe(take(1)).subscribe(() => {
if (this.data.length === 0 || this.allNodesHidden || this.filter === '') {
this.filterInput?.nativeElement.focus();
}
});
}
/**
* @hidden
*/
get selectButtonClasses() {
return `${this.size ? getSizeClass('button', this.size) : ''} ${this.fillMode ? 'k-button-' + this.fillMode : ''} ${this.fillMode ? 'k-button-' + this.fillMode + '-base' : ''}`;
}
/**
* @hidden
*/
get filterInputClasses() {
return `${this.size ? getSizeClass('input', this.size) : ''} ${this.fillMode ? 'k-input-' + this.fillMode : ''} ${this.rounded ? getRoundedClass(this.rounded) : ''}`;
}
onTouchedCallback = noop;
onChangeCallback = noop;
verifySettings() {
if (!isDevMode()) {
return;
}
if (this.valuePrimitive === true && isPresent(this.value) && typeof this.value === "object") {
throw new Error(DropDownTreeMessages.primitive);
}
if (this.valuePrimitive === true && isPresent(this.value) && typeof this.dataItem !== "object") {
throw new Error(DropDownTreeMessages.dataItem);
}
if (this.valuePrimitive === false && isPresent(this.value) && typeof this.value !== "object") {
throw new Error(DropDownTreeMessages.object);
}
if (!isPresent(this.valueField) || !isPresent(this.textField)) {
throw new Error(DropDownTreeMessages.textAndValue);
}
if ((isArray(this.valueField) || isArray(this.textField)) && isPresent(this.value) && !isPresent(this.valueDepth)) {
throw new Error(DropDownTreeMessages.valueDepth);
}
}
emitValueChange(value) {
this.onChangeCallback(value);
this.valueChange.emit(value);
}
getText(textField, dataItem) {
if (isPresent(dataItem) && isPresent(textField)) {
const field = this.getField(textField, dataItem);
return getter(field)(dataItem);
}
return null;
}
/**
* @hidden
*/
onChildrenLoaded() {
setTimeout(() => {
if (this.popupRef) {
this._searchableNodes = getSearchableItems(this.treeViewId, this.popupRef.popupElement);
}
if (this.isActionSheetExpanded) {
this._searchableNodes = getSearchableItems(this.treeViewId, this.actionSheet.element.nativeElement);
}
});
}
/**
* @hidden
*/
onExpand() {
this._searchableNodes = getSearchableItems(this.treeViewId, this.actionSheet.element.nativeElement);
this.isActionSheetExpanded = true;
}
/**
* @hidden
*/
onCollapse() {
this.isActionSheetExpanded = false;
}
/**
* @hidden
*
* Determines the `valueField` and `textField` for a specific level in the data set
* @param field - the field value (string | string[])
* @param value - current value
*/
getField(field, value) {
const fieldsCount = field.length - 1;
if (typeof field === 'string') {
// If the `valueField` | `textField` is the same for all levels
return field;
}
else if (isPresent(this.valueDepth)) {
// When `valueDepth` can be defined from the index on selectionChange or provided by the user
return fieldsCount < this.valueDepth ? field[fieldsCount] : field[this.valueDepth];
}
else if (value && typeof value === 'object') {
// Fallback: Look to find a match of each field in the current data item
// Side effect may occur if all of the listed fields are present in the data item
return field.find(item => item in value);
}
}
areNodesHidden(nodes) {
return nodes.every((node, index) => !this.isVisible(node, String(index)));
}
triggerPopupEvents(open) {
const eventArgs = new PreventableEvent();
if (open) {
this.open.emit(eventArgs);
}
else {
this.close.emit(eventArgs);
}
return eventArgs.isDefaultPrevented();
}
createPopup() {
this.windowSize = this.adaptiveService.size;
if (this.isAdaptive) {
this.openActionSheet();
this.cdr.detectChanges();
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,
appendTo: this.appendTo,
anchorAlign: anchorPosition,
animate: this.popupSettings.animate,
content: this.popupTemplate,
popupAlign: popupPosition,
positionMode: appendToComponent ? 'fixed' : 'absolute',
popupClass: this.popupContainerClasses
});
const popupWrapper = this.popupRef.popupElement;
const { min, max } = this.width;
this.renderer.setStyle(popupWrapper, 'minWidth', min);
this.renderer.setStyle(popupWrapper, 'width', max);
this.renderer.setStyle(popupWrapper, 'height', this.height);
this.renderer.setAttribute(popupWrapper, 'dir', this.direction);
if (!this.appendTo) {
this.renderer.setAttribute(popupWrapper, 'role', 'region');
this.renderer.setAttribute(popupWrapper, 'aria-label', this.messageFor('popupLabel'));
}
this.popupRef.popupOpen.subscribe(() => {
this.allNodesHidden = this.areNodesHidden(this.data);
this.popupRef.popupElement.addEventListener('click', (event) => stopPropagation(event));
this.cdr.detectChanges();
this.opened.emit();
this._searchableNodes = getSearchableItems(this.treeViewId, this.popupRef.popupElement);
});
this.popupRef.popupClose.subscribe(() => {
this.closed.emit();
});
}
destroyPopup() {
if (this.isActionSheetExpanded) {
this.closeActionSheet();
}
if (this.popupRef) {
this.popupRef.popupElement.removeEventListener('click', (event) => stopPropagation(event));
this.popupRef.close();
this.popupRef = null;
if (this.filter !== "") {
this.filter = "";
this.allNodesHidden = false;
if (hasObservers(this.filterChange)) {
this._zone.run(() => {
this.filterChange.emit("");
});
}
}
}
}
handleClick(e) {
// The check is needed otherwise when appended to the component, the popup reopens on click
// https://github.com/telerik/kendo-angular/issues/3738
this.windowSize = this.adaptiveService.size;
if (!this.isActionSheetExpanded) {
if ((this.popupRef && !this.popupRef.popupElement.contains(e.target))
|| (!this.popupRef && !e.target.className.includes('k-treeview-leaf'))) {
this.togglePopup(!this.isOpen);
}
}
}
handleEscape() {
this.togglePopup(false);
this.focus();
}
setState() {
if (isPresent(this.value) && isPresent(this.dataItem) && isPresent(this.valueField)) {
this.text = this.getText(this.textField, this.dataItem);
const valueField = this.getField(this.valueField, this.dataItem);
this.selectBy = valueField;
this.selectedKeys = [getter(valueField)(this.dataItem)];
}
else {
this.clearState();
}
this.cdr.markForCheck();
}
clearState() {
this.text = undefined;
this.selectedKeys = [];
}
subscribeEvents() {
[
this.navigationService.open.subscribe((event) => {
event.originalEvent.preventDefault();
this.togglePopup(true);
}),
this.navigationService.close.subscribe((event) => {
event.originalEvent.preventDefault();
this.togglePopup(false);
this.focus();
}),
this.navigationService.enter
.pipe(tap((event) => event.originalEvent.preventDefault()))
.subscribe((e) => {
// The check is needed otherwise when appended to the component, the popup reopens on click
// https://github.com/telerik/kendo-angular/issues/3738
if (e.originalEvent.target === this.wrapper.nativeElement) {
this.togglePopup(true);
}
if (!this.isOpen) {
this.focus();
}
}),
this.navigationService.esc
.subscribe(() => this.handleEscape()),
this.navigationService.tab.subscribe(() => {
this.focus();
if (this.isOpen) {
this.treeview.blur();
this.removeTreeViewFromTabOrder();
}
}),
this.navigationService.down.subscribe((event) => {
if (!this.treeview) {
return;
}
event.originalEvent.preventDefault();
if (!this.treeview.isActive) {
this.treeview.focus();
}
}),
this.navigationService.up.subscribe((event) => {
if (!this.treeview) {
return;
}
event.originalEvent.preventDefault();
if (this.filterable && this.treeview['navigationService']['activeIndex'] === '0') {
if (this.isActionSheetExpanded) {
this.actionSheetSearchBar.focus();
}
else {
this.filterInput.nativeElement.focus();
}
}
})
].forEach(sub => this.subs.add(sub));
}
subscribeTouchEvents() {
if (!isDocumentAvailable() || !this.touchEnabled) {
return;
}
this._zone.runOutsideAngular(() =>
// Roll up DropDownTree on iOS when tapped outside
this.touchstartDisposeHandler = this.renderer.listen(document, 'touchstart', (e) => {
const target = e.target;
if (this.isFocused && !inDropDown(this.wrapper, target, this.popupRef)) {
this._zone.run(() => {
if (this.isOpen) {
this.treeview.blur();
}
this.blur();
});
}
}));
}
subscribeFocusEvents() {
if (isDocumentAvailable()) {
this.handleFocus = this.handleFocus.bind(this);
this.handleDocumentBlur = this.handleDocumentBlur.bind(this);
this._zone.runOutsideAngular(() => {
const useCapture = true;
document.addEventListener('focus', this.handleFocus, useCapture);
document.addEventListener('blur', this.handleDocumentBlur, useCapture);
});
}
}
unSubscribeFocusEvents() {
if (isDocumentAvailable()) {
const useCapture = true;
document.removeEventListener('focus', this.handleFocus, useCapture);
document.removeEventListener('blur', this.handleDocumentBlur, useCapture);
}
}
unsubscribeEvents() {
this.subs.unsubscribe();
this.unSubscribeFocusEvents();
if (this.touchstartDisposeHandler) {
this.touchstartDisposeHandler();
}
}
handleDocumentBlur(event) {
if (event.target !== this.wrapper.nativeElement) {
return;
}
event.stopImmediatePropagation();
this.handleBlur(event);
}
assignAriaDescribedBy() {
const currentValue = this.wrapper.nativeElement.getAttribute('aria-describedby') || '';
// add to the current value - don't replace it (the aria-describedby is used by the FormField component as well)
const newValue = `${this.valueLabelId} ${currentValue.trim()}`.trim();
this.renderer.setAttribute(this.wrapper.nativeElement, 'aria-describedby', newValue);
}
setComponentClasses() {
if (this.size !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, getSizeClass('picker', this.size));
}
if (this.rounded !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, getRoundedClass(this.rounded));
}
if (this.fillMode !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, getFillModeClass('picker', this.fillMode));
}
}
/**
* Remove the `TreeView` from the tab order, otherwise a focus loop between the page elements will occur
* and the user will not be able to tab to the rest of the browser elements
*/
removeTreeViewFromTabOrder() {
const nodes = this.treeview.element.nativeElement.querySelectorAll('li');
nodes.forEach(item => {
if (item.getAttribute('tabindex') === '0') {
this.lastNodeOnFocus = item;
this.lastNodeOnFocus.setAttribute('tabindex', '-1');
}
});
}
closeActionSheet() {
this.actionSheet.toggle(false);
if (this.filterable) {
this.actionSheetSearchBar.value = '';
this.filterChange.emit('');
}
this.wrapper.nativeElement.focus();
this.closed.emit();
}
openActionSheet() {
this.windowSize = this.adaptiveService.size;
this.isActionSheetExpanded = true;
this.actionSheet.toggle(true);
this.title = setActionSheetTitle(this.wrapper, this.title);
this.cdr.detectChanges();
updateActionSheetAdaptiveAppearance(this.actionSheet, this.windowSize, this.renderer);
this.cdr.detectChanges();
this.opened.emit();
this.removeTreeViewFromTabOrder();
this.filterable && this.actionSheetSearchBar.focus();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DropDownTreeComponent, deps: [{ token: i0.Injector }, { token: i0.ElementRef }, { token: i1.PopupService }, { token: i2.NavigationService }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: i3.LocalizationService }, { token: i4.AdaptiveService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: DropDownTreeComponent, isStandalone: true, selector: "kendo-dropdowntree", inputs: { icon: "icon", svgIcon: "svgIcon", loading: "loading", clearButton: "clearButton", data: "data", value: "value", textField: "textField", valueField: "valueField", valueDepth: "valueDepth", hasChildren: "hasChildren", fetchChildren: "fetchChildren", placeholder: "placeholder", popupSettings: "popupSettings", dataItem: "dataItem", listHeight: "listHeight", disabled: "disabled", readonly: "readonly", valuePrimitive: "valuePrimitive", tabindex: "tabindex", size: "size", rounded: "rounded", fillMode: "fillMode", itemDisabled: "itemDisabled", isNodeExpanded: "isNodeExpanded", isNodeVisible: "isNodeVisible", loadOnDemand: "loadOnDemand", filterable: "filterable", filter: "filter", focusableId: "focusableId", adaptiveMode: "adaptiveMode", title: "title", subtitle: "subtitle" }, outputs: { open: "open", opened: "opened", close: "close", closed: "closed", nodeExpand: "nodeExpand", nodeCollapse: "nodeCollapse", onFocus: "focus", onBlur: "blur", valueChange: "valueChange", filterChange: "filterChange" }, host: { properties: { "class.k-dropdowntree": "this.hostClasses", "class.k-picker": "this.hostClasses", "attr.readonly": "this.isReadonly", "attr.aria-invalid": "this.hostAriaInvalid", "class.k-disabled": "this.isDisabled", "attr.aria-disabled": "this.isDisabled", "class.k-loading": "this.isLoading", "attr.aria-busy": "this.isBusy", "attr.aria-controls": "this.hostAriaControls", "attr.id": "this.id", "attr.dir": "this.direction", "attr.tabindex": "this.hostTabIndex", "attr.role": "this.role", "attr.aria-haspopup": "this.ariaHasPopup", "attr.aria-expanded": "this.isAriaExpanded", "attr.aria-autocomplete": "this.hostAriaAutocomplete", "class.k-readonly": "this.readonly" } }, providers: [
DataService,
SelectionService,
NavigationService,
DisabledItemsService,
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.dropdowntree'
},
{
multi: true,
provide: NG_VALUE_ACCESSOR,