@progress/kendo-angular-dropdowns
Version:
A wide variety of native Angular dropdown components including AutoComplete, ComboBox, DropDownList, DropDownTree, MultiColumnComboBox, MultiSelect, and MultiSelectTree
2,356 lines • 96.8 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 { ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, forwardRef, HostBinding, Injector, Input, isDevMode, NgZone, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
import { NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { anyChanged, guid, hasObservers, Keys, KendoInput, isDocumentAvailable, EventsOutsideAngularDirective, ResizeSensorComponent, TemplateContextDirective } from '@progress/kendo-angular-common';
import { AdaptiveService } from '@progress/kendo-angular-utils';
import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
import { NavigationService } from '../common/navigation/navigation.service';
import { PopupService } from '@progress/kendo-angular-popup';
import { DataBoundComponent, ExpandableComponent, TreeViewComponent } from '@progress/kendo-angular-treeview';
import { DataService } from '../common/data.service';
import { DisabledItemsService } from '../common/disabled-items/disabled-items.service';
import { SelectionService } from '../common/selection/selection.service';
import { PreventableEvent } from '../common/models/preventable-event';
import { NavigationAction } from '../common/navigation/navigation-action';
import { RemoveTagEvent } from '../common/models/remove-tag-event';
import { MultiSelectTreeMessages } from '../common/constants/error-messages';
import { animationDuration, fetchDescendentNodes, getFillModeClass, getRoundedClass, getSearchableItems, getSizeClass, hasProps, isArray, isLetter, isObject, isObjectArray, isPresent, isTruthy, isUntouched, noop, parseNumber, setActionSheetTitle, updateActionSheetAdaptiveAppearance, valueFrom } from '../common/util';
import { HeaderTemplateDirective } from '../common/templates/header-template.directive';
import { FooterTemplateDirective } from '../common/templates/footer-template.directive';
import { NodeTemplateDirective } from './templates/node-template.directive';
import { NoDataTemplateDirective } from '../common/templates/no-data-template.directive';
import { TagTemplateDirective } from '../common/templates/tag-template.directive';
import { GroupTagTemplateDirective } from '../common/templates/group-tag-template.directive';
import { merge, of, Subject, Subscription } from 'rxjs';
import { debounceTime, filter, tap } from 'rxjs/operators';
import { buildTreeItem, MultiSelectTreeLookupService, nodeIndex } from './lookup/lookup.service';
import { searchIcon, xIcon } from '@progress/kendo-svg-icons';
import { ResponsiveRendererComponent } from '../common/action-sheet.component';
import { CheckDirective } from './checked-state/check.directive';
import { CheckAllDirective } from './checked-state/check-all.directive';
import { FilterInputDirective } from '../common/filter-input.directive';
import { NgIf, NgTemplateOutlet, NgClass } from '@angular/common';
import { TagListComponent } from '../common/taglist.component';
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-popup";
import * as i2 from "../common/navigation/navigation.service";
import * as i3 from "@progress/kendo-angular-l10n";
import * as i4 from "./lookup/lookup.service";
import * as i5 from "@progress/kendo-angular-utils";
const DEFAULT_POPUP_SETTINGS = { animate: true };
const DEFAULT_CHECKABLE_SETTINGS = { checkChildren: true, checkOnClick: 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';
/**
* Represents the [Kendo UI MultiSelectTree component for Angular]({% slug overview_multiselecttree %}).
*/
export class MultiSelectTreeComponent {
injector;
wrapper;
popupService;
renderer;
navigationService;
_zone;
localization;
cdr;
lookup;
adaptiveService;
/**
* @hidden
*/
touchEnabled = touchEnabled;
/**
* @hidden
*/
animationDuration = animationDuration;
/**
* @hidden
*/
searchIcon = searchIcon;
/**
* @hidden
*/
xIcon = xIcon;
hostClasses = true;
get isDisabled() {
return this.disabled || null;
}
treeViewId = `k-${guid()}`;
get hostAriaAutocomplete() {
return this.filterable ? 'list' : null;
}
get isLoading() {
return this.loading;
}
get hostAriaInvalid() {
return this.formControl?.invalid ? true : null;
}
get isBusy() {
return this.loading ? 'true' : null;
}
get id() {
return this.focusableId;
}
direction;
get hostTabIndex() {
return this.tabindex;
}
role = 'combobox';
ariaHasPopup = 'tree';
get isReadonly() {
return this.readonly ? '' : null;
}
get ariaDescribedBy() {
return this.tagListId;
}
get ariaActiveDescendant() {
return this.focusedTagId;
}
/**
* @hidden
*/
get formControl() {
const ngControl = this.injector.get(NgControl, null);
return ngControl?.control || null;
}
/**
* @hidden
*/
onFilterChange(text) {
if (this.filterable) {
this.filterChange.emit(text);
}
}
/**
* @hidden
*/
onExpand() {
this._searchableNodes = getSearchableItems(this.treeViewId, this.actionSheet.element.nativeElement);
this.isActionSheetExpanded = true;
}
/**
* @hidden
*/
onCollapse() {
this.isActionSheetExpanded = false;
}
/**
* 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.
*/
set subtitle(_subtitle) {
this._subtitle = _subtitle;
}
get subtitle() {
return this._subtitle || this.placeholder;
}
/**
* @hidden
*/
get isAdaptiveModeEnabled() {
return this.adaptiveMode === 'auto';
}
/**
* @hidden
*/
windowSize = 'large';
/**
* @hidden
*/
isActionSheetExpanded = false;
/**
* @hidden
*/
handleKeydown(event, input) {
if (event.target === this.filterInput?.nativeElement &&
(event.keyCode === Keys.ArrowLeft || event.keyCode === Keys.ArrowRight)) {
return;
}
if (input) {
event.stopImmediatePropagation();
}
const deleteTag = this.isWrapperActive && event.keyCode === Keys.Backspace && this.tags.length > 0;
if (deleteTag) {
this.handleBackspace();
return;
}
if (this.disabled || this.readonly) {
return;
}
if (!this.isFilterActive && isLetter(event.key) && !this.actionSheetSearchBar?.onFocus) {
this.printableCharacters.next(event.key);
}
const eventData = event;
const action = this.navigationService.process({
originalEvent: eventData,
openOnSpace: !this.isOpen,
closeOnSpace: false
});
if (action === NavigationAction.Open) {
eventData.preventDefault();
}
if (this.isOpen && action === NavigationAction.Enter) {
const spaceKeyDownEvent = new KeyboardEvent('keydown', { 'key': ' ', 'code': 'Space', 'keyCode': 32, 'which': 32 });
this.treeview?.element.nativeElement.dispatchEvent(spaceKeyDownEvent);
}
}
/**
* @hidden
*/
responsiveRendererComponent;
/**
* @hidden
*/
get actionSheet() {
return this.responsiveRendererComponent?.actionSheet;
}
/**
* @hidden
*/
get actionSheetSearchBar() {
return this.responsiveRendererComponent?.actionSheetSearchBar;
}
/**
* @hidden
*/
get isAdaptive() {
return this.isAdaptiveModeEnabled && this.windowSize !== 'large';
}
headerTemplate;
footerTemplate;
nodeTemplate;
noDataTemplate;
tagTemplate;
groupTagTemplate;
popupTemplate;
container;
set treeview(treeview) {
this._treeview = treeview;
if (treeview) {
// If filtering is enabled, focus the TreeView on mobile devices instead of the filter input
if (this.isFocused && !this.filterable && !this.checkAll || 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
* The Promise is required to properly change the `animate` property when
* the popup is appended to a container and opened upon initialization.
* Otherwise, the "Expression has changed..." type error will be thrown.
*/
Promise.resolve(null).then(() => this.treeview.animate = true);
}
}
get treeview() {
return this._treeview;
}
filterInput;
checkAllInput;
/**
* Specifies the [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the component.
*/
set tabindex(value) {
const providedTabIndex = parseNumber(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('input', this.size));
if (size !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, 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.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('input', this.fillMode));
if (fillMode !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, getFillModeClass('input', newFillMode));
}
this._fillMode = newFillMode;
}
get fillMode() {
return this._fillMode;
}
/**
* Configures the popup of the MultiSelectTree.
*
* 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);
// `detectChanges` needed, otherwise upon value initialization and `appendTo` property
// an error is thrown => ExpressionChangedAfterItHasBeenCheckedError
this.cdr.detectChanges();
}
get popupSettings() {
return this._popupSettings;
}
/**
* Defines the checkable settings of the MultiSelecTree nodes.
* If no value is provided, the default [`CheckableSettings`]({% slug api_dropdowns_multiselecttreecheckablesettings %}) are applied.
*/
set checkableSettings(settings) {
this._checkableSettings = Object.assign({}, DEFAULT_CHECKABLE_SETTINGS, settings);
}
get checkableSettings() {
return this._checkableSettings;
}
/**
* Sets the data of the MultiSelectTree.
*
* > The data has to be provided in an array-like list with objects.
*/
set data(data) {
this._nodes = data;
this.setState();
if (this.isContentInit) {
// Needed for when the data is loaded later/asynchronously because it would not exist on ngContentInit
this.registerLookupItems(data);
}
}
get data() {
return this._nodes;
}
/**
* Sets the value of the MultiSelectTree.
* 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(value) {
this._value = value ? value : [];
this.setState();
}
get value() {
return this._value;
}
/**
* Keeps the current `dataItems` object in order to resolve selection.
* Needs to be provided when when programmatically setting a `value` and `valuePrimitive` is set to `true`.
*/
set dataItems(items) {
this._dataItems = (items || []).map((dataItem, index) => {
if (hasProps(dataItem, ['dataItem', 'index', 'level'])) {
return dataItem;
}
const level = this.valueDepth[index] || 0;
const key = valueFrom({ dataItem, level }, this.valueField) + '_' + (this.isHeterogeneous ? this.valueDepth[index] : 0);
return {
dataItem,
index: null,
level,
key
};
});
this.setState();
}
get dataItems() {
return this._dataItems || this.value.map((value, index) => {
const level = this.valueDepth[index] || 0;
const key = valueFrom({ dataItem: value, level }, this.valueField) + '_' + (this.isHeterogeneous ? this.valueDepth[index] : 0);
return {
dataItem: value,
index: null,
level,
key
};
});
}
/**
* The fields of the data item that provide the text content of the nodes inside the
* MultiSelectTree ([see example]({% slug databinding_multiselecttree %})). 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
* MultiSelectTree ([see example]({% slug databinding_multiselecttree %})). 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 levels in the data set where the values can be found when `valueField` is an Array.
* The field serves to correctly allocate a data item used when the MultiSelectTree is initialized with a value.
*/
valueDepth = [];
/**
* Sets and gets the loading state of the MultiSelectTree.
*/
loading;
/**
* The hint which is displayed when the component is empty.
*/
placeholder = '';
/**
* 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_multiselecttree#toc-managing-the-multiselecttree-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_multiselecttree %}#toc-primitive-values)).
* If set to `true`, the selected value has to be a primitive one.
*/
valuePrimitive = false;
/**
* Indicates whether the child nodes will be fetched on node expand or will be initially prefetched.
* @default false
*/
loadOnDemand = false;
/**
* @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()}`;
/**
* 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.
* @default true
*/
clearButton = true;
/**
* Renders the built-in input element for filtering the MultiSelectTree.
* If set to `true`, the component emits the `filterChange` event, which can be used to [filter the MultiSelectTree manually]({% slug filtering_multiselecttree %}#toc-manual-filtering).
* A built-in filtering implementation is available to use with the [`kendoMultiSelectTreeHierarchyBinding`]({% slug api_dropdowns_multiselecttreehierarchybindingdirective %}) and [`kendoMultiSelectTreeFlatBinding`]({% slug api_dropdowns_multiselecttreeflatbindingdirective %}) directives.
* @default false
*/
filterable = false;
/**
* If `checkАll` is set to `true` and the checkboxes are enabled, a tri-state checkbox appears above the embedded treeview.
* Clicking the checkbox checks or unchecks all enabled items of the treeview that are loaded.
* @default false
*/
checkAll = false;
/**
* 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;
/**
* 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;
/**
* A function that is executed for each data item and determines if a specific item is disabled.
*/
itemDisabled = itemDisabled;
/**
* A user-defined callback function which receives an array of selected data items and maps them to an array of tags.
*
* @param { Any[] } dataItems - The selected data items from the list.
* @returns { Any[] } - The tags that will be rendered by the component.
*/
tagMapper = (tags) => tags || [];
/**
* Fires each time the user focuses the MultiSelectTree.
*/
onFocus = new EventEmitter();
/**
* Fires each time the MultiSelectTree gets blurred.
*/
onBlur = new EventEmitter();
/**
* Fires each time the popup is about to open
* ([see example]({% slug openstate_multiselecttree %})).
* 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_multiselecttree %})).
* 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 value is changed
* ([see example](slug:events_multiselecttree)).
*/
valueChange = new EventEmitter();
/**
* Fires each time a tag is about to be removed([see examples]({% slug summarytagmode_multiselect %}#toc-notifying-on-removing-group-tags)).
* This event is preventable. If you cancel it, the tag will not be removed.
*/
removeTag = new EventEmitter();
/**
* Fires when the value of the built-in filter input element changes.
*/
filterChange = new EventEmitter();
/**
* @hidden
*/
get focusedTagId() {
if (!isPresent(this.focusedTagIndex) || this.isOpen) {
return null;
}
return this.tagPrefix + '-' + this.focusedTagIndex;
}
set isFocused(isFocused) {
this.renderer[isFocused ? 'addClass' : 'removeClass'](this.wrapper.nativeElement, 'k-focus');
this._isFocused = isFocused;
}
get isFocused() {
return this._isFocused;
}
/**
* Returns the current open state. Returns `true` if the popup or actionSheet is open.
*/
get isOpen() {
return isTruthy(isPresent(this.popupRef) || this.isActionSheetExpanded);
}
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';
}
get appendTo() {
const { appendTo } = this.popupSettings;
if (!appendTo || appendTo === 'root') {
return undefined;
}
return appendTo === 'component' ? this.container : appendTo;
}
/**
* @hidden
*/
get popupContainerClasses() {
const containerClasses = ['k-multiselecttree-popup'];
if (this.popupSettings.popupClass) {
containerClasses.push(this.popupSettings.popupClass);
}
return containerClasses;
}
/**
* @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 `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 `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 `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;
}
get isTagFocused() {
return !this.isOpen && this.focusedTagIndex !== undefined;
}
get isTreeViewActive() {
return this.treeview && this.treeview.isActive;
}
get isWrapperActive() {
return document.activeElement === this.wrapper.nativeElement;
}
get isFilterActive() {
return this.filterInput && document.activeElement === this.filterInput.nativeElement;
}
get isCheckAllActive() {
return this.checkAllInput && document.activeElement === this.checkAllInput.nativeElement;
}
/**
* @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
*/
filterStateChange = new EventEmitter();
/**
* @hidden
*/
filter = '';
/**
* @hidden
*/
checkedItems = [];
/**
* @hidden
* The flag is needed in order to determine how to construct the items map keys.
* If `true`, then the key consists of the item's value and level (depth),
* else the key consists of the item's value and 0 (no leveling required)
*/
isHeterogeneous;
/**
* @hidden
*/
showAfter = 0;
/**
* @hidden
*/
allNodesHidden = false;
tagListId = `k-${guid()}`;
tagPrefix = "tag-" + guid();
popupRef;
tags;
focusedTagIndex = undefined;
disabledIndices;
_subtitle;
_nodes;
_value = [];
_tabindex = 0;
_popupSettings = DEFAULT_POPUP_SETTINGS;
_checkableSettings = DEFAULT_CHECKABLE_SETTINGS;
_isFocused = false;
_treeview;
_dataItems;
_tempValue;
_initiallyCheckedItems = [];
_size = 'medium';
_rounded = 'medium';
_fillMode = 'solid';
_searchableNodes = [];
_typedValue = '';
printableCharacters = new Subject();
subs = new Subscription();
// 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;
// Used as check to avoid unnecessary 'registerLookupItems()' calls upon initialization
isContentInit;
constructor(injector, wrapper, popupService, renderer, navigationService, _zone, localization, cdr, lookup, adaptiveService) {
this.injector = injector;
this.wrapper = wrapper;
this.popupService = popupService;
this.renderer = renderer;
this.navigationService = navigationService;
this._zone = _zone;
this.localization = localization;
this.cdr = cdr;
this.lookup = lookup;
this.adaptiveService = adaptiveService;
this.direction = localization.rtl ? 'rtl' : 'ltr';
this.subscribeEvents();
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.renderer.setAttribute(this.wrapper.nativeElement, 'aria-expanded', String(this.isOpen));
this.subs.add(this.localization
.changes.subscribe(({ rtl }) => {
this.direction = rtl ? 'rtl' : 'ltr';
this.cdr.markForCheck();
}));
this.setComponentClasses();
this._initiallyCheckedItems = [...this.checkedItems];
}
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
*/
ngOnDestroy() {
this.destroyPopup();
this.unsubscribeEvents();
}
/**
* @hidden
*/
ngOnChanges(changes) {
if (anyChanged(['textField', 'valueField', 'valuePrimitive'], changes, false)) {
this.isHeterogeneous = this.valueField && isArray(this.valueField);
this.setState();
}
if (anyChanged(['valueDepth', 'value', 'dataItems'], changes, false)) {
if (changes['value'] && !changes['dataItems'] && !this.valuePrimitive) {
// Update the dataItems if the value is updated programmatically (non-primitive values only)
// In the primitive case, the client should update the dataItems as well
this.dataItems = this.value;
}
else {
// Re-map the dataItems because `valueDepth` is not yet available when the check directive parses the items
this.dataItems = this.dataItems.map((item, index) => ({
...item,
key: valueFrom({ dataItem: item.dataItem, index: null, level: this.valueDepth[index] || 0 }, this.valueField) + '_' + (this.isHeterogeneous ? this.valueDepth[index] : 0),
level: this.valueDepth[index] || 0
}));
}
}
if (anyChanged(['data', 'children', 'hasChildren', 'loadOnDemand', 'valueField'], changes, true) && !this.loadOnDemand) {
this.lookup.reset();
this.registerLookupItems(this.data);
}
}
/**
* @hidden
*/
ngAfterContentChecked() {
this.verifySettings();
if (this.data?.length > 0 && this.popupRef) {
this.allNodesHidden = this.areNodesHidden(this.data);
}
}
/**
* @hidden
*/
applyValue() {
this.value = this._tempValue;
this._initiallyCheckedItems = [...this.checkedItems];
this.emitValueChange(this.value);
this.setTags();
this.toggle(false);
}
/**
* @hidden
*/
cancelValue() {
this.checkedItems = [...this._initiallyCheckedItems];
this.togglePopup(false);
}
ngAfterContentInit() {
this.isContentInit = true;
// Still need to keep the call of 'registerLookupItems()' from ngAfterContentInit in the cases when the data is passed initially
// The call is execute here because we have to make sure it happens after all input properties are loaded (not the case in the data setter initially)
this.registerLookupItems(this.data);
}
/**
* @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.placeholder && (!isPresent(this.value) || this.value.length === 0);
}
/**
* Focuses the MultiSelectTree.
*/
focus() {
if (!this.disabled) {
this.wrapper.nativeElement.focus();
}
}
/**
* Blurs the MultiSelectTree.
*/
blur() {
if (!this.disabled) {
this.wrapper.nativeElement.blur();
}
}
/**
* Focuses a specific item of the MultiSelectTree 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);
}
}
}
/**
* Resets the value of the MultiSelectTree.
* 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 = [];
this.dataItems = [];
this.valueDepth = [];
}
/**
* Toggles the visibility of the popup or actionSheet
* ([see example]({% slug openstate_multiselecttree %})).
* If you use the `toggle` method to open or close the popup, 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
*/
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();
});
}
// Re-focus the treeview if `close` is prevented
if (this.isOpen && this.treeview) {
if (this.lastNodeOnFocus) {
this.lastNodeOnFocus.setAttribute('tabindex', '0');
}
this.treeview.focus();
}
}
}
/**
* @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);
//this is needed for Ang 18 not to throw ng0100 error when closing the popup
//the component could be refactored using kendoDropDownSharedEvents directive
//once we are able to debug against Angular 18
this.cdr.markForCheck();
if (hasObservers(this.onBlur) ||
isUntouched(this.wrapper.nativeElement) ||
this.formControl?.updateOn === 'blur') {
this._zone.run(() => {
this.onBlur.emit();
this.onTouchedCallback();
});
}
}
}
/**
* @hidden
*/
handleNodeClick(node) {
if (!this.isFocused) {
// Re-focus the MultiSelectTree when popup close is prevented and a node is clicked
// On click the focus should be on the clicked element which is why we need to update the lastNodeOnFocus
const parent = node.originalEvent.target.parentElement.parentElement;
this.lastNodeOnFocus = parent;
this.focus();
}
}
/**
* @hidden
*/
togglePopup(open) {
const isDisabled = this.disabled || this.readonly;
const sameState = this.isOpen === open;
this._zone.run(() => {
this.focusedTagIndex = undefined;
});
if (isDisabled || sameState) {
return;
}
const togglePrevented = this.triggerPopupEvents(open);
if (!togglePrevented) {
if (open) {
this.createPopup();
}
else {
this.destroyPopup();
}
}
else {
this.removeTreeViewFromTabOrder();
}
}
/**
* @hidden
*/
messageFor(key) {
return this.localization.get(key);
}
lastAction = 'check';
/**
* @hidden
*/
handleCheckedItemsChange(items) {
this.valueDepth = items.map(item => item.level);
this.lastAction = items.length > this.dataItems.length ? 'check' : 'uncheck';
this.dataItems = items.slice();
this.updateValue(this.dataItems);
}
/**
* @hidden
*/
handleRemoveTag({ tag, index }) {
if (this.disabled || this.readonly) {
return;
}
const eventArgs = new RemoveTagEvent(tag);
this.removeTag.emit(eventArgs);
if (eventArgs.isDefaultPrevented()) {
return;
}
// Remove tags based on their position index
if (tag instanceof Array) {
// Remove group tag
this.dataItems = this.dataItems.filter((_item, i) => i < this.showAfter || this.disabledIndices.has(i));
this.valueDepth = this.valueDepth.filter((_item, i) => i < this.showAfter || this.disabledIndices.has(i));
}
else if (this.loadOnDemand) {
// Remove single tag when the child items are fetched on demand
this.dataItems = this.dataItems.filter((_item, i) => i !== index || this.disabledIndices.has(i));
this.valueDepth = this.valueDepth.filter((_item, i) => i !== index || this.disabledIndices.has(i));
}
else {
// Remove single tag when the child items are pre-fetched
const dataItem = this.dataItems.find(item => item.tagPositionIndex === index);
const itemKey = dataItem.key;
const lookup = this.lookup.itemLookup(itemKey);
const pendingCheck = [lookup.item];
if (this.checkableSettings.checkChildren) {
fetchDescendentNodes(lookup)
.forEach(lookup => pendingCheck.push(lookup.item));
pendingCheck.push(...this.removeParents(lookup.parent));
}
const keysToRemove = pendingCheck.map(item => item.key);
// Holds the position indexes of the items to be removed
const valueDepthIndices = [];
this.dataItems = this.dataItems.filter((_item, i) => {
const shouldStay = !keysToRemove.includes(_item.key) || this.disabledIndices.has(i);
if (!shouldStay) {
// We need to know the index position of the data item to be able to update the valueDepth array accordignly
// as each data item's position is corresponding to the same position in valueDepth
valueDepthIndices.push(i);
}
return shouldStay;
});
this.valueDepth = this.valueDepth.filter((_item, i) => {
return !valueDepthIndices.includes(i) || this.disabledIndices.has(i);
});
}
this.updateValue(this.dataItems);
if (!this.isFocused) {
this.focus();
}
}
/**
* @hidden
*/
handleTagMapperChange(showAfter) {
this.showAfter = parseNumber(showAfter);
this.setTags();
}
/**
* @hidden
*/
clearAll(event) {
event.stopImmediatePropagation();
event.preventDefault();
this.focus();
this.value = this.value.filter((_item, index) => this.disabledIndices.has(index));
this.dataItems = this.dataItems.filter((_item, index) => this.disabledIndices.has(index));
this.valueDepth = this.valueDepth.filter((_depth, index) => this.disabledIndices.has(index));
this.emitValueChange(this.value);
}
/**
* @hidden
*/
writeValue(value) {
if (!this.valuePrimitive && isPresent(value)) {
this.dataItems = value;
}
if (!isPresent(value) && isPresent(this.value)) {
this.dataItems = null;
}
this.value = 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) {
this.filter = input.value;
this.filterChange.next(input.value);
}
/**
* @hidden
*/
get filterInputClasses() {
return `${this.size ? getSizeClass('input', this.size) : ''} ${this.fillMode ? 'k-input-' + this.fillMode : ''} ${this.rounded ? getRoundedClass(this.rounded) : ''}`;
}
/**
* @hidden
*/
get checkAllCheckboxClasses() {
return `${this.size ? getSizeClass('checkbox', this.size) : ''}`;
}
/**
* @hidden
*/
toggleCheckAll() {
this.checkAllInput.nativeElement.focus();
this.checkAllInput.nativeElement.click();
}
onTouchedCallback = noop;
onChangeCallback = noop;
verifySettings() {
if (!isDevMode()) {
return;
}
if (!isPresent(this.valueField) || !isPresent(this.textField)) {
throw new Error(MultiSelectTreeMessages.textAndValue);
}
if (!isArray(this.value)) {
throw new Error(MultiSelectTreeMessages.array);
}
if (this.value.length > 0) {
if (this.valuePrimitive && this.value.some(item => isObject(item))) {
throw new Error(MultiSelectTreeMessages.primitive);
}
const isEveryDataItemObject = this.dataItems.every(item => isObject(item.dataItem));
if (this.valuePrimitive && !isArray(this.dataItems)) {
throw new Error(MultiSelectTreeMessages.dataItems);
}
if (this.valuePrimitive && !isEveryDataItemObject) {
throw new Error(MultiSelectTreeMessages.dataItems);
}
if (this.valuePrimitive && this.dataItems.length !== this.value.length) {
throw new Error(MultiSelectTreeMessages.dataItemsLength);
}
if (!this.valuePrimitive && !isObjectArray(this.value)) {
throw new Error(MultiSelectTreeMessages.object);
}
if ((isArray(this.valueField) || isArray(this.textField)) && !isArray(this.valueDepth)) {
throw new Error(MultiSelectTreeMessages.valueDepth);
}
if ((isArray(this.valueField) || isArray(this.textField)) && this.valueDepth.length === 0) {
throw new Error(MultiSelectTreeMessages.valueDepth);
}
if ((isArray(this.valueField) || isArray(this.textField)) && this.valueDepth.length !== this.value.length) {
throw new Error(MultiSelectTreeMessages.valueDepthLength);
}
}
}
areNodesHidden(nodes) {
return nodes.every((node, index) => !this.isVisible(node, String(index)));
}
emitValueChange(value) {
this.onChangeCallback(value);
this.valueChange.emit(value);
}
triggerPopupEvents(open) {
const eventArgs = new PreventableEvent();
if (hasObservers(this.open) || hasObservers(this.close)) {
this._zone.run(() => {
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;
if (!this.appendTo) {
this.renderer.setAttribute(popupWrapper, 'role', 'region');
this.renderer.setAttribute(popupWrapper, 'aria-label', this.messageFor('popupLabel'));
}
popupWrapper.style.minWidth = min;
popupWrapper.style.width = max;
popupWrapper.style.height = this.height;
this.renderer.setAttribute(this.wrapper.nativeElement, 'aria-controls', this.treeViewId);
this.renderer.setAttribute(popupWrapper, 'dir', this.direction);
this.renderer.setAttribute(this.wrapper.nativeElement, 'aria-expanded', 'true');
this.popupRef.popupOpen.subscribe(() => {
this.cdr.detectChanges();
this.opened.emit();
this._searchableNodes = getSearchableItems(this.treeViewId, this.popupRef.popupElement);
});
this.popupRef.popupClose.subscribe(() => {
if (hasObservers(this.closed)) {
this._zone.run(() => {
this.closed.emit();
});
}
});
}
destroyPopup() {
if (this.isActionSheetExpanded) {
this.closeActionSheet();
}
if (this.popupRef) {
this.popupRef.close();
this.popupRef = null;
this.renderer.setAttribute(this.wrapper.nativeElement, 'aria-expanded', 'false');
this.renderer.removeAttribute(this.wrapper.nativeElement, 'aria-controls');
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
if ((this.popupRef && this.popupRef.popupElement.contains(e.target)) || this.isActionSheetExpanded) {
return;
}
this.togglePopup(!this.isOpen);
}
subscribeEvents() {
[
this.navigationService.open.subscribe((event) => {
event.originalEvent.preventDefault();
this.togglePopup(true);
}),
this.navigationService.enter
.pipe(tap((event) => event.originalEvent.preventDefault()))
.subscribe(() => this.togglePopup(true)),
merge(this.navigationService.close, this.navigationService.esc).subscribe((event) => {
event.originalEvent.preventDefault();
this.focus();
this.togglePopup(false);
}),
this.navigationService.tab.subscribe(this.handleTabKey.bind(this)),
this.navigationService.up.subscribe(this.handleUpKey.bind(this)),
this.navigationService.down.subscribe(this.handleDownKey.bind(this)),
this.navigationService.left
.pipe(filter(() => !this.isTreeViewActive))
.subscribe(this.direction === 'rtl' ? this.handleRightKey.bind(this) : this.handleLeftKey.bind(this)),
this.navigationService.right
.pipe(filter(() => !this.isTreeViewActive))
.subscribe(this.direction === 'rtl' ? this.handleLeftKey.bind(this) : this.handleRightKey.bind(this)),
this.navigationService.home.pipe(filter(() => !this.isOpen)).subscribe(this.handleHome.bind(this)),
this.navigationService.end.pipe(filter(() => !this.isOpen)).subscribe(this.handleEnd.bind(this)),
this.navigationService.backspace.pipe(filter(() => this.isTagFocused)).subscribe(this.handleBackspace.bind(this)),
this.navigationService.delete.pipe(filter(() => this.isTagFocused)).subscribe(this.handleDelete.bind(this))
].forEach(sub => this.subs.add(sub));
}
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);
}
}
handleDocumentBlur(event) {
if (event.target !== this.wrapper.nativeElement) {
return;
}
event.stopImmediatePropagation();
this.handleBlur(event);
}
handleTabKey() {
if (!this.isActionSheetExpanded) {
this.focus();
}
if (this.isOpen) {
this.treeview.blur();
this.removeTreeViewFromTabOrder();
}
}
handleUpKey(event) {
if (!this.treeview) {
return;
}
event.originalEvent.preventDefault();
// Prevent toggling the focus between the filterInput and the wrapper elements with `up` key
if (this.isWrapperActive) {
return;
}
const isFirstNodeActive = this.treeview['navigationService']['activeIndex'] === '0';
// Current focus is on the filter input => should focus the wrapper
if (this.filterable && this.isFilterActive) {
this.focus();
// Current focus is on the treeview first node => should focus the check all checkbox if enabled
}
else if (this.checkAll && !this.isCheckAllActive && isFirstNodeActive) {
this.checkAllInput.nativeElement.focus();
// Current focus is either on the check all checkbox or the treeview's first node
// => should focus either the filter input (if enabled) or the wrapper
}
else if (this.isCheckAllActive || isFirstNodeActive) {
if (this.filterable) {
this.isActionSheetExpanded ? this.actionSheetSearchBar.focus() : this.filterInput.nativeElement.focus();
}
else {
if (!this.isActionSheetExpanded) {
this.focus();
}
}
}
}
handleDownKey(event) {
if (!this.treeview) {
return;
}
event.originalEvent.preventDefault();
// Current focus is on the wrapper => should focus the filter input
if (this.filterable && this.isWrapperActive) {
this.filterInput.nativeElement.focus();
// Current focus is on the wrapper/filter input => should focus check all checkbox if enabled
}
else if (this.checkAll && (this.isWrapperActive || this.isFilterActive)) {
this.checkAllInput.nativeElement.focus();
// Should focus the treeview if filterable and check all are disabled
}
else if (!this.treeview.isActive) {
this.treeview.focus();
}
this.focusedTagIndex = undefined;
}
handleRightKey(event) {
event.originalEvent.preventDefault();
const last = this.tags.length - 1;
if (this.focusedTagIndex === last) {
this.focusedTagIndex = undefined;
}
else if (this.focusedTagIndex < last) {
this.focusedTagIndex++;
}
else if (!this.focusedTagIndex) {
this.focusedTagIndex = 0;
}
}
handleLeftKey(event) {
event.originalEvent.preventDefault();
if (this.focusedTagIndex === undefined || this.focusedTagIndex < 0) {
this.focusedTagIndex = this.tags.length - 1;
}
else if (this.focusedTagIndex !== 0) {
this.focusedTagIndex--;
}
}
handleEnd(event) {
event.originalEvent.preventDefault();
this.focusedTagIndex = this.tags.length - 1;
}
handleHome(event) {
event.originalEvent.preventDefault();
this.focusedTagIndex = 0;
}
handleBackspace() {
if (this.focusedTagIndex !== undefined) {
this.handleDelete();
}
else {
const tag = this.tags[this.tags.length - 1];
const index = this.tags.length - 1;
this.handleRemoveTag({ tag, index });
}
}
handleDelete() {
const tag = this.tags[this.focusedTagIndex];
const index = this.focusedTagIndex;
this.handleRemoveTag({ tag, index });
if (this.focusedTagIndex === this.tags.length) {
this.focusedTagIndex = undefined;
}
}
unsubscribeEvents() {
this.subs.unsubscribe();
this.unSubscribeFocusEvents();
}
/**
* 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');
}
});
}
setState() {
if (isPresent(this.dataItems) && isPresent(this.valueField)) {
if (!this.isActionSheetExpanded) {
this.setTags();
}
this.checkedItems = this.dataItems.slice();
}
this.cdr.markForCheck();
}
setTags() {
const source = this.dataItems.map(item => item.dataItem);
this.tags = this.tagMapper(source);
this.disabledIndices = this.disabledItemsMapper();
// Create a mapping of tags to position indices
const tagIndexMap = new Map(this.tags.map((tag, index) => [JSON.stringify(tag), index]));
// Modify dataItems by adding a property to hold the item's position as displayed in the tags
this.dataItems.forEach(item => {
const serializedDataItem = JSON.stringify(item.dataItem);
item.tagPositionIndex = tagIndexMap.has(serializedDataItem)
? tagIndexMap.get(serializedDataItem) // Use the index from tags if it exists
: null; // Assign a null value if the dataItem is not visible in the tags
});
}
updateValue(value) {
const newValue = this.valuePrimitive ?
value.map(item => valueFrom(item, this.valueField)) :
value.map(item => item.dataItem);
if (this.isActionSheetExpanded) {
this._tempValue = newValue.slice();
}
else {
this.value = newValue;
this.emitValueChange(this.value);
}
}
/**
* @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
*
* Determines which of the provided tags should be disabled and stores their position indices
*/
disabledItemsMapper() {
return new Set(this.dataItems.reduce((indices, item, index) => {
if (this.itemDisabled(item.dataItem, item.index)) {
indices.push(index);
}
return indices;
}, []));
}
setComponentClasses() {
if (this.size !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, getSizeClass('input', 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('input', this.fillMode));
}
}
removeParents(parent) {
let currentParent = parent;
const nodes = [];
while (currentParent) {
nodes.push(currentParent.item);
currentParent = currentParent.parent;
}
return nodes;
}
/**
* Creates an internal map of the available tree items to be used as a reference
* to retrieve the item's children/parent and determine the checked sate
*/
registerLookupItems(data, parentItem = null, levelIndex = 0) {
if (!isPresent(data) || data.length === 0) {
return;
}
const parentIndex = nodeIndex(parentItem);
const treeItems = data.map((node) => buildTreeItem(node, this.valueField, levelIndex));
if (isPresent(parentItem)) {
this.lookup.registerChildren(parentIndex, treeItems);
}
treeItems.forEach(item => {
this.lookup.registerItem(item, parentItem);
if (!this.loadOnDemand) {
this.registerChildLookupItems(item, levelIndex);
}
});
}
registerChildLookupItems(item, levelIndex) {
if (this.hasChildren(item.dataItem)) {
this.children(item.dataItem)
.subscribe(children => {
const index = this.isHeterogeneous ? levelIndex + 1 : 0;
this.registerLookupItems(children, item, index);
});
}
}
closeActionSheet() {
this.wrapper.nativeElement.focus();
this.actionSheet.toggle(false);
this.renderer.removeAttribute(this.wrapper.nativeElement, 'aria-controls');
this.renderer.setAttribute(this.wrapper.nativeElement, 'aria-expanded', 'false');
if (this.filterable) {
this.actionSheetSearchBar.value = '';
this.filterChange.emit('');
}
this.closed.emit();
}
openActionSheet() {
this.windowSize = this.adaptiveService.size;
this.actionSheet.toggle(true);
this.title = setActionSheetTitle(this.wrapper, this.title);
this.cdr.detectChanges();
this.renderer.setAttribute(this.wrapper.nativeElement, 'aria-controls', this.treeViewId);
this.renderer.setAttribute(this.wrapper.nativeElement, 'aria-expanded', 'true');
updateActionSheetAdaptiveAppearance(this.actionSheet, this.windowSize, this.renderer);
this.cdr.detectChanges();
this.opened.emit();
this.filterable && this.actionSheetSearchBar.focus();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MultiSelectTreeComponent, deps: [{ token: i0.Injector }, { token: i0.ElementRef }, { token: i1.PopupService }, { token: i0.Renderer2 }, { token: i2.NavigationService }, { token: i0.NgZone }, { token: i3.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i4.MultiSelectTreeLookupService }, { token: i5.AdaptiveService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: MultiSelectTreeComponent, isStandalone: true, selector: "kendo-multiselecttree", inputs: { adaptiveMode: "adaptiveMode", title: "title", subtitle: "subtitle", tabindex: "tabindex", size: "size", rounded: "rounded", fillMode: "fillMode", popupSettings: "popupSettings", checkableSettings: "checkableSettings", data: "data", value: "value", dataItems: "dataItems", textField: "textField", valueField: "valueField", valueDepth: "valueDepth", loading: "loading", placeholder: "placeholder", listHeight: "listHeight", disabled: "disabled", readonly: "readonly", valuePrimitive: "valuePrimitive", loadOnDemand: "loadOnDemand", focusableId: "focusableId", clearButton: "clearButton", filterable: "filterable", checkAll: "checkAll", hasChildren: "hasChildren", fetchChildren: "fetchChildren", isNodeExpanded: "isNodeExpanded", isNodeVisible: "isNodeVisible", itemDisabled: "itemDisabled", tagMapper: "tagMapper" }, outputs: { onFocus: "focus", onBlur: "blur", open: "open", opened: "opened", close: "close", closed: "closed", nodeExpand: "nodeExpand", nodeCollapse: "nodeCollapse", valueChange: "valueChange", removeTag: "removeTag", filterChange: "filterChange" }, host: { properties: { "class.k-multiselecttree": "this.hostClasses", "class.k-input": "this.hostClasses", "class.k-disabled": "this.isDisabled", "attr.aria-disabled": "this.isDisabled", "attr.aria-autocomplete": "this.hostAriaAutocomplete", "class.k-loading": "this.isLoading", "attr.aria-invalid": "this.hostAriaInvalid", "attr.aria-busy": "this.isBusy", "attr.id": "this.id", "attr.dir": "this.direction", "attr.tabindex": "this.hostTabIndex", "attr.role": "this.role", "attr.aria-haspopup": "this.ariaHasPopup", "attr.readonly": "this.isReadonly", "attr.aria-describedby": "this.ariaDescribedBy", "attr.aria-activedescendant": "this.ariaActiveDescendant", "class.k-readonly": "this.readonly" } }, providers: [
LocalizationService,
NavigationService,
DataService,
DisabledItemsService,
SelectionService,
MultiSelectTreeLookupService,
{
provide: L10N_PREFIX,
useValue: 'kendo.multiselecttree'
},
{
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MultiSelectTreeComponent)
},
{
provide: DataBoundComponent,
useExisting: forwardRef(() => MultiSelectTreeComponent)
},
{
provide: ExpandableComponent,
useExisting: forwardRef(() => MultiSelectTreeComponent)
},
{
provide: KendoInput,
useExisting: forwardRef(() => MultiSelectTreeComponent)
}
], queries: [{ propertyName: "headerTemplate", first: true, predicate: HeaderTemplateDirective, descendants: true }, { propertyName: "footerTemplate", first: true, predicate: FooterTemplateDirective, descendants: true }, { propertyName: "nodeTemplate", first: true, predicate: NodeTemplateDirective, descendants: true }, { propertyName: "noDataTemplate", first: true, predicate: NoDataTemplateDirective, descendants: true }, { propertyName: "tagTemplate", first: true, predicate: TagTemplateDirective, descendants: true }, { propertyName: "groupTagTemplate", first: true, predicate: GroupTagTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "responsiveRendererComponent", first: true, predicate: ResponsiveRendererComponent, descendants: true }, { propertyName: "popupTemplate", first: true, predicate: ["popupTemplate"], descendants: true, static: true }, { propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "treeview", first: true, predicate: ["treeview"], descendants: true }, { propertyName: "filterInput", first: true, predicate: ["filterInput"], descendants: true }, { propertyName: "checkAllInput", first: true, predicate: ["checkAllInput"], descendants: true }], exportAs: ["kendoMultiSelectTree"], usesOnChanges: true, ngImport: i0, template: `
<ng-container kendoMultiSelectTreeLocalizedMessages
i18n-noDataText="kendo.multiselecttree.noDataText|The text displayed in the popup when there are no items"
noDataText="NO DATA FOUND"
i18n-clearTitle="kendo.multiselecttree.clearTitle|The title of the clear button"
clearTitle="clear"
i18n-checkAllText="kendo.multiselecttree.checkAllText|The text displayed for the check-all checkbox"
checkAllText="Check all"
i18n-filterInputLabel="kendo.multiselecttree.filterInputLabel|The text set as aria-label on the filter input"
filterInputLabel="Filter"
i18n-popupLabel="kendo.multiselecttree.popupLabel|The label of the popup element that contains the list of options when its role is 'region'"
popupLabel="Options list"
i18n-applyButton="kendo.multiselecttree.applyButton|The text of the Apply button in the action sheet"
applyButton="Apply"
i18n-cancelButton="kendo.multiselecttree.cancelButton|The text of the Cancel button in the action sheet"
cancelButton="Cancel"
>
</ng-container>
<kendo-taglist
class="k-input-values"
[size]="size"
[rounded]="rounded"
[fillMode]="fillMode"
[id]="tagListId"
[tags]="tags"
[focused]="focusedTagIndex"
[textField]="textField"
[valueField]="valueField"
[valueDepth]="valueDepth"
[disabled]="disabled"
[tagPrefix]="tagPrefix"
[template]="tagTemplate"
[groupTemplate]="groupTagTemplate"
[disabledIndices]="disabledIndices"
(removeTag)="handleRemoveTag($event)"
>
</kendo-taglist>
<span class="k-input-inner">
<span
*ngIf="!tags || !tags.length"
class="k-input-value-text"
>
{{ placeholder }}
</span>
</span>
<span
*ngIf="!disabled && !loading && !readonly && clearButton && tags?.length"
class="k-clear-value"
[attr.title]="messageFor('clearTitle')"
role="button"
tabindex="-1"
(click)="clearAll($event)"
>
<kendo-icon-wrapper
class="k-icon"
name="x"
[svgIcon]="xIcon"
>
</kendo-icon-wrapper>
</span>
<span
*ngIf="loading"
class="k-icon k-i-loading k-input-loading-icon"
>
</span>
<ng-template #popupTemplate>
<ng-container *ngTemplateOutlet="sharedPopupActionSheetTemplate"></ng-container>
</ng-template>
<ng-container #container></ng-container>
<!--adaptive rendering-->
<responsive-renderer
[sharedPopupActionSheetTemplate]="sharedPopupActionSheetTemplate"
[isActionSheetExpanded]="isActionSheetExpanded"
[title]="title"
[showTextInput]="filterable"
[showActionButtons]="true"
[subtitle]="subtitle"
(closePopup)="togglePopup(false)"
(onExpand)="onExpand()"
(onCollapse)="onCollapse()"
(textInputChange)="onFilterChange($event)"
(onApply)="applyValue()"
(onCancel)="cancelValue()"
>
</responsive-renderer>
<ng-template #sharedPopupActionSheetTemplate>
<div
*ngIf="filterable && !isActionSheetExpanded"
class="k-list-filter"
>
<span
class="k-textbox k-input"
[ngClass]="filterInputClasses"
>
<span class="k-input-prefix">
<kendo-icon-wrapper
class="k-icon"
name="search"
[svgIcon]="searchIcon"
>
</kendo-icon-wrapper>
</span>
<input
#filterInput
(input)="handleFilterInputChange($event.target)"
[filterInput]="filterable && !touchEnabled"
[attr.aria-label]="messageFor('filterInputLabel')"
(keydown)="handleKeydown($event, filterInput)"
[value]="filter"
class="k-input-inner"
role="searchbox"
tabindex="0"
aria-disabled="false"
aria-readonly="false"
[kendoEventsOutsideAngular]="{
blur: handleBlur
}"
[scope]="this"
>
</span>
</div>
<!--header template-->
<ng-template
*ngIf="headerTemplate"
[templateContext]="{
templateRef: headerTemplate?.templateRef
}">
</ng-template>
<div *ngIf="checkAll" class="k-check-all">
<span class="k-checkbox-wrap">
<input
#checkAllInput
[checkAll]="!filterable && !touchEnabled"
type="checkbox"
class="k-checkbox"
[ngClass]="checkAllCheckboxClasses"
role="checkbox"
tabindex="0"
aria-disabled="false"
aria-readonly="false"
[treeview]="treeview"
[checkedItems]="checkedItems"
[valueField]="valueField"
[lastAction]="lastAction"
(checkedItemsChange)="handleCheckedItemsChange($event)"
(keydown)="handleKeydown($event)"
[kendoEventsOutsideAngular]="{
blur: handleBlur
}"
[scope]="this"
>
</span>
<span
class="k-checkbox-label"
(click)="toggleCheckAll()"
(mousedown)="$event.preventDefault()"
>
{{ messageFor('checkAllText') }}
</span>
</div>
<kendo-treeview
#treeview
[size]="size"
[attr.id]="treeViewId"
[nodes]="data"
[style.maxHeight.px]="listHeight"
[animate]="false"
kendoMultiSelectTreeCheckable
[isHeterogeneous]="isHeterogeneous"
[checkable]="checkableSettings"
[checkedItems]="checkedItems"
[valueField]="valueField"
[textField]="textField"
[children]="children"
[hasChildren]="hasChildren"
[isExpanded]="isNodeExpanded"
[isDisabled]="itemDisabled"
[nodeTemplate]="nodeTemplate"
[loadOnDemand]="loadOnDemand"
[filter]="filter"
[isVisible]="isNodeVisible"
(keydown)="handleKeydown($event)"
(nodeClick)="handleNodeClick($event)"
(expand)="nodeExpand.emit($event)"
(collapse)="nodeCollapse.emit($event)"
(checkedItemsChange)="handleCheckedItemsChange($event)"
[kendoEventsOutsideAngular]="{
focusout: handleBlur
}"
[scope]="this"
(childrenLoaded)="onChildrenLoaded()"
>
</kendo-treeview>
<!--footer template-->
<ng-template
*ngIf="footerTemplate"
[templateContext]="{
templateRef: footerTemplate?.templateRef
}">
</ng-template>
<!--no-data template-->
<div class="k-no-data" *ngIf="!data || data?.length === 0 || allNodesHidden">
<ng-template [ngIf]="noDataTemplate"
[templateContext]="{
templateRef: noDataTemplate?.templateRef
}">
</ng-template>
<ng-template [ngIf]="!noDataTemplate">
<div>{{ messageFor('noDataText') }}</div>
</ng-template>
</div>
</ng-template>
<kendo-resize-sensor *ngIf="isOpen || isAdaptiveModeEnabled" (resize)="onResize()"></kendo-resize-sensor>
`, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective, selector: "\n [kendoDropDownListLocalizedMessages],\n [kendoDropDownTreeLocalizedMessages],\n [kendoComboBoxLocalizedMessages],\n [kendoMultiColumnComboBoxLocalizedMessages],\n [kendoAutoCompleteLocalizedMessages],\n [kendoMultiSelectLocalizedMessages],\n [kendoMultiSelectTreeLocalizedMessages]\n " }, { kind: "component", type: TagListComponent, selector: "kendo-taglist", inputs: ["tags", "textField", "valueField", "valueDepth", "focused", "template", "groupTemplate", "disabled", "tagPrefix", "id", "size", "rounded", "fillMode", "disabledIndices"], outputs: ["removeTag"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: ResponsiveRendererComponent, selector: "responsive-renderer", inputs: ["title", "showActionButtons", "subtitle", "size", "showTextInput", "sharedPopupActionSheetTemplate", "isActionSheetExpanded", "text", "placeholder"], outputs: ["closePopup", "textInputChange", "navigate", "onExpand", "onCollapse", "onApply", "onCancel"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: FilterInputDirective, selector: "[filterInput]", inputs: ["filterInput"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }, { kind: "directive", type: TemplateContextDirective, selector: "[templateContext]", inputs: ["templateContext"] }, { kind: "directive", type: CheckAllDirective, selector: "[checkAll]", inputs: ["lastAction", "treeview", "checkedItems", "valueField", "checkAll"], outputs: ["checkedItemsChange"] }, { kind: "component", type: TreeViewComponent, selector: "kendo-treeview", inputs: ["filterInputPlaceholder", "expandDisabledNodes", "animate", "nodeTemplate", "loadMoreButtonTemplate", "trackBy", "nodes", "textField", "hasChildren", "isChecked", "isDisabled", "hasCheckbox", "isExpanded", "isSelected", "isVisible", "navigable", "children", "loadOnDemand", "filterable", "filter", "size", "disableParentNodesOnly"], outputs: ["childrenLoaded", "blur", "focus", "expand", "collapse", "nodeDragStart", "nodeDrag", "filterStateChange", "nodeDrop", "nodeDragEnd", "addItem", "removeItem", "checkedChange", "selectionChange", "filterChange", "nodeClick", "nodeDblClick"], exportAs: ["kendoTreeView"] }, { kind: "directive", type: CheckDirective, selector: "[kendoMultiSelectTreeCheckable]", inputs: ["checkable", "valueField", "checkedItems"], outputs: ["checkedItemsChange"] }, { kind: "component", type: ResizeSensorComponent, selector: "kendo-resize-sensor", inputs: ["rateLimit"], outputs: ["resize"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MultiSelectTreeComponent, decorators: [{
type: Component,
args: [{
exportAs: 'kendoMultiSelectTree',
providers: [
LocalizationService,
NavigationService,
DataService,
DisabledItemsService,
SelectionService,
MultiSelectTreeLookupService,
{
provide: L10N_PREFIX,
useValue: 'kendo.multiselecttree'
},
{
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MultiSelectTreeComponent)
},
{
provide: DataBoundComponent,
useExisting: forwardRef(() => MultiSelectTreeComponent)
},
{
provide: ExpandableComponent,
useExisting: forwardRef(() => MultiSelectTreeComponent)
},
{
provide: KendoInput,
useExisting: forwardRef(() => MultiSelectTreeComponent)
}
],
selector: 'kendo-multiselecttree',
template: `
<ng-container kendoMultiSelectTreeLocalizedMessages
i18n-noDataText="kendo.multiselecttree.noDataText|The text displayed in the popup when there are no items"
noDataText="NO DATA FOUND"
i18n-clearTitle="kendo.multiselecttree.clearTitle|The title of the clear button"
clearTitle="clear"
i18n-checkAllText="kendo.multiselecttree.checkAllText|The text displayed for the check-all checkbox"
checkAllText="Check all"
i18n-filterInputLabel="kendo.multiselecttree.filterInputLabel|The text set as aria-label on the filter input"
filterInputLabel="Filter"
i18n-popupLabel="kendo.multiselecttree.popupLabel|The label of the popup element that contains the list of options when its role is 'region'"
popupLabel="Options list"
i18n-applyButton="kendo.multiselecttree.applyButton|The text of the Apply button in the action sheet"
applyButton="Apply"
i18n-cancelButton="kendo.multiselecttree.cancelButton|The text of the Cancel button in the action sheet"
cancelButton="Cancel"
>
</ng-container>
<kendo-taglist
class="k-input-values"
[size]="size"
[rounded]="rounded"
[fillMode]="fillMode"
[id]="tagListId"
[tags]="tags"
[focused]="focusedTagIndex"
[textField]="textField"
[valueField]="valueField"
[valueDepth]="valueDepth"
[disabled]="disabled"
[tagPrefix]="tagPrefix"
[template]="tagTemplate"
[groupTemplate]="groupTagTemplate"
[disabledIndices]="disabledIndices"
(removeTag)="handleRemoveTag($event)"
>
</kendo-taglist>
<span class="k-input-inner">
<span
*ngIf="!tags || !tags.length"
class="k-input-value-text"
>
{{ placeholder }}
</span>
</span>
<span
*ngIf="!disabled && !loading && !readonly && clearButton && tags?.length"
class="k-clear-value"
[attr.title]="messageFor('clearTitle')"
role="button"
tabindex="-1"
(click)="clearAll($event)"
>
<kendo-icon-wrapper
class="k-icon"
name="x"
[svgIcon]="xIcon"
>
</kendo-icon-wrapper>
</span>
<span
*ngIf="loading"
class="k-icon k-i-loading k-input-loading-icon"
>
</span>
<ng-template #popupTemplate>
<ng-container *ngTemplateOutlet="sharedPopupActionSheetTemplate"></ng-container>
</ng-template>
<ng-container #container></ng-container>
<!--adaptive rendering-->
<responsive-renderer
[sharedPopupActionSheetTemplate]="sharedPopupActionSheetTemplate"
[isActionSheetExpanded]="isActionSheetExpanded"
[title]="title"
[showTextInput]="filterable"
[showActionButtons]="true"
[subtitle]="subtitle"
(closePopup)="togglePopup(false)"
(onExpand)="onExpand()"
(onCollapse)="onCollapse()"
(textInputChange)="onFilterChange($event)"
(onApply)="applyValue()"
(onCancel)="cancelValue()"
>
</responsive-renderer>
<ng-template #sharedPopupActionSheetTemplate>
<div
*ngIf="filterable && !isActionSheetExpanded"
class="k-list-filter"
>
<span
class="k-textbox k-input"
[ngClass]="filterInputClasses"
>
<span class="k-input-prefix">
<kendo-icon-wrapper
class="k-icon"
name="search"
[svgIcon]="searchIcon"
>
</kendo-icon-wrapper>
</span>
<input
#filterInput
(input)="handleFilterInputChange($event.target)"
[filterInput]="filterable && !touchEnabled"
[attr.aria-label]="messageFor('filterInputLabel')"
(keydown)="handleKeydown($event, filterInput)"
[value]="filter"
class="k-input-inner"
role="searchbox"
tabindex="0"
aria-disabled="false"
aria-readonly="false"
[kendoEventsOutsideAngular]="{
blur: handleBlur
}"
[scope]="this"
>
</span>
</div>
<!--header template-->
<ng-template
*ngIf="headerTemplate"
[templateContext]="{
templateRef: headerTemplate?.templateRef
}">
</ng-template>
<div *ngIf="checkAll" class="k-check-all">
<span class="k-checkbox-wrap">
<input
#checkAllInput
[checkAll]="!filterable && !touchEnabled"
type="checkbox"
class="k-checkbox"
[ngClass]="checkAllCheckboxClasses"
role="checkbox"
tabindex="0"
aria-disabled="false"
aria-readonly="false"
[treeview]="treeview"
[checkedItems]="checkedItems"
[valueField]="valueField"
[lastAction]="lastAction"
(checkedItemsChange)="handleCheckedItemsChange($event)"
(keydown)="handleKeydown($event)"
[kendoEventsOutsideAngular]="{
blur: handleBlur
}"
[scope]="this"
>
</span>
<span
class="k-checkbox-label"
(click)="toggleCheckAll()"
(mousedown)="$event.preventDefault()"
>
{{ messageFor('checkAllText') }}
</span>
</div>
<kendo-treeview
#treeview
[size]="size"
[attr.id]="treeViewId"
[nodes]="data"
[style.maxHeight.px]="listHeight"
[animate]="false"
kendoMultiSelectTreeCheckable
[isHeterogeneous]="isHeterogeneous"
[checkable]="checkableSettings"
[checkedItems]="checkedItems"
[valueField]="valueField"
[textField]="textField"
[children]="children"
[hasChildren]="hasChildren"
[isExpanded]="isNodeExpanded"
[isDisabled]="itemDisabled"
[nodeTemplate]="nodeTemplate"
[loadOnDemand]="loadOnDemand"
[filter]="filter"
[isVisible]="isNodeVisible"
(keydown)="handleKeydown($event)"
(nodeClick)="handleNodeClick($event)"
(expand)="nodeExpand.emit($event)"
(collapse)="nodeCollapse.emit($event)"
(checkedItemsChange)="handleCheckedItemsChange($event)"
[kendoEventsOutsideAngular]="{
focusout: handleBlur
}"
[scope]="this"
(childrenLoaded)="onChildrenLoaded()"
>
</kendo-treeview>
<!--footer template-->
<ng-template
*ngIf="footerTemplate"
[templateContext]="{
templateRef: footerTemplate?.templateRef
}">
</ng-template>
<!--no-data template-->
<div class="k-no-data" *ngIf="!data || data?.length === 0 || allNodesHidden">
<ng-template [ngIf]="noDataTemplate"
[templateContext]="{
templateRef: noDataTemplate?.templateRef
}">
</ng-template>
<ng-template [ngIf]="!noDataTemplate">
<div>{{ messageFor('noDataText') }}</div>
</ng-template>
</div>
</ng-template>
<kendo-resize-sensor *ngIf="isOpen || isAdaptiveModeEnabled" (resize)="onResize()"></kendo-resize-sensor>
`,
standalone: true,
imports: [LocalizedMessagesDirective, TagListComponent, NgIf, IconWrapperComponent, NgTemplateOutlet, ResponsiveRendererComponent, NgClass, FilterInputDirective, EventsOutsideAngularDirective, TemplateContextDirective, CheckAllDirective, TreeViewComponent, CheckDirective, ResizeSensorComponent]
}]
}], ctorParameters: function () { return [{ type: i0.Injector }, { type: i0.ElementRef }, { type: i1.PopupService }, { type: i0.Renderer2 }, { type: i2.NavigationService }, { type: i0.NgZone }, { type: i3.LocalizationService }, { type: i0.ChangeDetectorRef }, { type: i4.MultiSelectTreeLookupService }, { type: i5.AdaptiveService }]; }, propDecorators: { hostClasses: [{
type: HostBinding,
args: ['class.k-multiselecttree']
}, {
type: HostBinding,
args: ['class.k-input']
}], isDisabled: [{
type: HostBinding,
args: ['class.k-disabled']
}, {
type: HostBinding,
args: ['attr.aria-disabled']
}], hostAriaAutocomplete: [{
type: HostBinding,
args: ['attr.aria-autocomplete']
}], isLoading: [{
type: HostBinding,
args: ['class.k-loading']
}], hostAriaInvalid: [{
type: HostBinding,
args: ['attr.aria-invalid']
}], isBusy: [{
type: HostBinding,
args: ['attr.aria-busy']
}], id: [{
type: HostBinding,
args: ['attr.id']
}], direction: [{
type: HostBinding,
args: ['attr.dir']
}], hostTabIndex: [{
type: HostBinding,
args: ['attr.tabindex']
}], role: [{
type: HostBinding,
args: ['attr.role']
}], ariaHasPopup: [{
type: HostBinding,
args: ['attr.aria-haspopup']
}], isReadonly: [{
type: HostBinding,
args: ['attr.readonly']
}], ariaDescribedBy: [{
type: HostBinding,
args: ['attr.aria-describedby']
}], ariaActiveDescendant: [{
type: HostBinding,
args: ['attr.aria-activedescendant']
}], adaptiveMode: [{
type: Input
}], title: [{
type: Input
}], subtitle: [{
type: Input
}], responsiveRendererComponent: [{
type: ViewChild,
args: [ResponsiveRendererComponent]
}], headerTemplate: [{
type: ContentChild,
args: [HeaderTemplateDirective, { static: false }]
}], footerTemplate: [{
type: ContentChild,
args: [FooterTemplateDirective, { static: false }]
}], nodeTemplate: [{
type: ContentChild,
args: [NodeTemplateDirective, { static: false }]
}], noDataTemplate: [{
type: ContentChild,
args: [NoDataTemplateDirective, { static: false }]
}], tagTemplate: [{
type: ContentChild,
args: [TagTemplateDirective, { static: false }]
}], groupTagTemplate: [{
type: ContentChild,
args: [GroupTagTemplateDirective, { static: false }]
}], popupTemplate: [{
type: ViewChild,
args: ['popupTemplate', { static: true }]
}], container: [{
type: ViewChild,
args: ['container', { read: ViewContainerRef, static: true }]
}], treeview: [{
type: ViewChild,
args: ['treeview', { static: false }]
}], filterInput: [{
type: ViewChild,
args: ['filterInput', { static: false }]
}], checkAllInput: [{
type: ViewChild,
args: ['checkAllInput', { static: false }]
}], tabindex: [{
type: Input
}], size: [{
type: Input
}], rounded: [{
type: Input
}], fillMode: [{
type: Input
}], popupSettings: [{
type: Input
}], checkableSettings: [{
type: Input
}], data: [{
type: Input
}], value: [{
type: Input
}], dataItems: [{
type: Input
}], textField: [{
type: Input
}], valueField: [{
type: Input
}], valueDepth: [{
type: Input
}], loading: [{
type: Input
}], placeholder: [{
type: Input
}], listHeight: [{
type: Input
}], disabled: [{
type: Input
}], readonly: [{
type: Input
}, {
type: HostBinding,
args: ['class.k-readonly']
}], valuePrimitive: [{
type: Input
}], loadOnDemand: [{
type: Input
}], focusableId: [{
type: Input
}], clearButton: [{
type: Input
}], filterable: [{
type: Input
}], checkAll: [{
type: Input
}], hasChildren: [{
type: Input
}], fetchChildren: [{
type: Input
}], isNodeExpanded: [{
type: Input
}], isNodeVisible: [{
type: Input
}], itemDisabled: [{
type: Input
}], tagMapper: [{
type: Input
}], onFocus: [{
type: Output,
args: ['focus']
}], onBlur: [{
type: Output,
args: ['blur']
}], open: [{
type: Output
}], opened: [{
type: Output
}], close: [{
type: Output
}], closed: [{
type: Output
}], nodeExpand: [{
type: Output
}], nodeCollapse: [{
type: Output
}], valueChange: [{
type: Output
}], removeTag: [{
type: Output
}], filterChange: [{
type: Output
}] } });