@progress/kendo-angular-treelist
Version:
Kendo UI TreeList for Angular - Display hierarchical data in an Angular tree grid view that supports sorting, filtering, paging, and much more.
1,330 lines • 140 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Component, ContentChildren, ElementRef, EventEmitter, HostBinding, Input, Output, Renderer2, QueryList, ViewChild, isDevMode, NgZone, ViewChildren, ChangeDetectorRef, ViewEncapsulation, ChangeDetectionStrategy, forwardRef } from '@angular/core';
import { NgIf, NgTemplateOutlet } from '@angular/common';
import { FormControl, FormGroup } from '@angular/forms';
import { Subscription, merge, isObservable } from 'rxjs';
import { map, tap, take, filter, switchMap, takeUntil } from 'rxjs/operators';
import { validatePackage } from '@progress/kendo-licensing';
import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
import { guid, DraggableDirective } from '@progress/kendo-angular-common';
import { DragTargetContainerDirective, DropTargetContainerDirective } from '@progress/kendo-angular-utils';
import { IconWrapperComponent } from '@progress/kendo-angular-icons';
import { packageMetadata } from './package-metadata';
import { ColumnComponent, isColumnComponent } from './columns/column.component';
import { isSpanColumnComponent } from './columns/span-column.component';
import { isColumnGroupComponent, ColumnGroupComponent } from './columns/column-group.component';
import { isArray, anyChanged, isChanged, isPresent, isUniversal, observe, isTruthy, createPromise, hasObservers } from './utils';
import { BrowserSupportService } from './layout/browser-support.service';
import { ViewCollection } from './data/data.collection';
import { EditService } from './editing/edit.service';
import { ColumnsContainer } from './columns/columns-container';
import { ChangeNotificationService } from './data/change-notification.service';
import { NoRecordsTemplateDirective } from './rendering/no-records-template.directive';
import { ColumnBase } from './columns/column-base';
import { syncRowsHeight } from './layout/row-sync';
import { FilterService } from './filtering/filter.service';
import { PDFService } from './pdf/pdf.service';
import { PDFExportEvent } from './pdf/pdf-export-event';
import { SuspendService } from './scrolling/suspend.service';
import { ResponsiveService } from "./layout/responsive.service";
import { ExcelService } from './excel/excel.service';
import { ColumnList } from './columns/column-list';
import { ToolbarTemplateDirective } from "./rendering/toolbar/toolbar-template.directive";
import { expandColumns, expandColumnsWithSpan, isValidFieldName } from "./columns/column-common";
import { ScrollSyncService } from "./scrolling/scroll-sync.service";
import { ResizeService } from "./layout/resize.service";
import { closest, matchesClasses, matchesNodeName } from './rendering/common/dom-queries';
import { DomEventsService } from './common/dom-events.service';
import { ColumnResizingService } from "./column-resizing/column-resizing.service";
import { hasFilterRow } from './filtering/filterable';
import { SinglePopupService } from './common/single-popup.service';
import { DragAndDropService } from './dragdrop/drag-and-drop.service';
import { DragHintService } from './dragdrop/drag-hint.service';
import { DropCueService } from './dragdrop/drop-cue.service';
import { ColumnReorderService } from './dragdrop/column-reorder.service';
import { ColumnReorderEvent } from './dragdrop/column-reorder-event';
import { FocusRoot } from './navigation/focus-root';
import { NavigationService } from './navigation/navigation.service';
import { NavigationMetadata } from './navigation/navigation-metadata';
import { IdService } from './common/id.service';
import { ColumnInfoService } from "./common/column-info.service";
import { ScrollRequestService } from './scrolling/scroll-request.service';
import { SortService } from './common/sort.service';
import { ColumnMenuTemplateDirective } from './column-menu/column-menu-template.directive';
import { ColumnVisibilityChangeEvent } from './column-menu/column-visibility-change-event';
import { ColumnLockedChangeEvent } from './column-menu/column-locked-change-event';
import { sortColumns } from './columns/column-common';
import { defaultTrackBy } from './common/default-track-by';
import { ExpandStateService, defaultExpanded } from './expand-state/expand-state.service';
import { getter } from '@progress/kendo-common';
import { LocalEditService } from './editing-directives/local-edit.service';
import { OptionChangesService } from "./common/option-changes.service";
import { SelectionService } from './selection/selection.service';
import { DataBoundTreeComponent } from './binding-directives/data-bound-tree-component';
import { ExpandableTreeComponent } from './expand-state/expandable-tree-component';
import { ContextService } from './common/provider.service';
import { RowReorderService } from './row-reordering/row-reorder.service';
import { LoadingComponent } from './rendering/common/loading.component';
import { TableBodyComponent } from './rendering/table-body.component';
import { MarqueeDirective } from './selection/marquee.directive';
import { ListComponent } from './rendering/list.component';
import { ResizableContainerDirective } from './layout/resizable.directive';
import { HeaderComponent } from './rendering/header/header.component';
import { ColGroupComponent } from './rendering/common/col-group.component';
import { TableDirective } from './column-resizing/table.directive';
import { ToolbarComponent } from './rendering/toolbar/toolbar.component';
import { LocalizedMessagesDirective } from './localization/localized-messages.directive';
import { KENDO_PAGER, PagerContextService, PagerNavigationService, PagerTemplateDirective } from '@progress/kendo-angular-pager';
import { normalize } from './common/pager-settings';
import * as i0 from "@angular/core";
import * as i1 from "./layout/browser-support.service";
import * as i2 from "./data/change-notification.service";
import * as i3 from "./editing/edit.service";
import * as i4 from "./filtering/filter.service";
import * as i5 from "./pdf/pdf.service";
import * as i6 from "./layout/responsive.service";
import * as i7 from "./excel/excel.service";
import * as i8 from "./scrolling/scroll-sync.service";
import * as i9 from "./common/dom-events.service";
import * as i10 from "./column-resizing/column-resizing.service";
import * as i11 from "./dragdrop/column-reorder.service";
import * as i12 from "./common/column-info.service";
import * as i13 from "./navigation/navigation.service";
import * as i14 from "./common/sort.service";
import * as i15 from "./scrolling/scroll-request.service";
import * as i16 from "./expand-state/expand-state.service";
import * as i17 from "./common/option-changes.service";
import * as i18 from "./selection/selection.service";
import * as i19 from "@progress/kendo-angular-l10n";
import * as i20 from "./common/provider.service";
import * as i21 from "./row-reordering/row-reorder.service";
import * as i22 from "@progress/kendo-angular-pager";
const createControl = (source) => (acc, key) => {
acc[key] = new FormControl(source[key]);
return acc;
};
const validateColumnsField = (columns) => expandColumns(columns.toArray())
.filter(isColumnComponent)
.filter(({ field }) => !isValidFieldName(field))
.forEach(({ field }) => console.warn(`
TreeList column field name '${field}' does not look like a valid JavaScript identifier.
Identifiers can contain only alphanumeric characters (including "$" or "_"), and may not start with a digit.
Please use only valid identifier names to ensure error-free operation.
`));
const isInEditedCell = (element, treelistElement) => closest(element, matchesClasses('k-grid-edit-cell')) &&
closest(element, matchesNodeName('kendo-treelist')) === treelistElement;
/**
* Represents the Kendo UI TreeList component for Angular.
* Use this component to display and manage hierarchical data in a tabular format.
*
* @example
* ```html
* <kendo-treelist
* [kendoTreeListFlatBinding]="data"
* [pageSize]="10"
* [pageable]="true">
* </kendo-treelist>
* ```
*
* @remarks
* Supported children components are:
* {@link CheckboxColumnComponent},
* {@link ColumnChooserComponent},
* {@link ColumnComponent},
* {@link ColumnGroupComponent},
* {@link ColumnMenuAutoSizeAllColumnsComponent},
* {@link ColumnMenuAutoSizeColumnComponent},
* {@link ColumnMenuChooserComponent},
* {@link ColumnMenuComponent},
* {@link ColumnMenuFilterComponent},
* {@link ColumnMenuItemComponent},
* {@link ColumnMenuLockComponent},
* {@link ColumnMenuSortComponent},
* {@link CommandColumnComponent},
* {@link CustomMessagesComponent},
* {@link ExcelComponent},
* {@link TreeListSpacerComponent},
* {@link PDFComponent},
* {@link RowReorderColumnComponent},
* {@link SpanColumnComponent},
* {@link ToolBarComponent}.
*/
export class TreeListComponent {
supportService;
wrapper;
changeNotification;
editService;
filterService;
pdfService;
responsiveService;
renderer;
excelService;
ngZone;
scrollSyncService;
domEvents;
columnResizingService;
changeDetectorRef;
columnReorderService;
columnInfoService;
navigationService;
sortService;
scrollRequestService;
expandStateService;
optionChanges;
selectionService;
localization;
ctx;
rowReorderService;
/**
* Provides an accessible description of the component.
*/
ariaLabel;
/**
* Sets the data for the TreeList. When you provide an array, the TreeList gets the total count automatically
* ([more information and example]({% slug databinding_treelist %})).
*/
set data(value) {
this.view.reset();
this._data = value;
this.loadedData = null;
this.unsubscribeDataLoaded();
if (isObservable(value)) {
this.dataLoadedSubscription = value.subscribe(this.dataLoaded); // handle error
}
else {
this.dataLoaded(value);
}
}
get data() {
return this.loadedData;
}
/**
* Sets the page size for the TreeList when [paging]({% slug paging_treelist %}) is enabled.
*
* @default 10
*/
pageSize = 10;
/**
* Sets the height in pixels for the TreeList when you set the `scrollable` option.
* You can also use `style.height` to set the height. The `style.height`
* option supports units such as `px`, `%`, `em`, `rem`, and others.
*/
height;
/**
* Sets the actual height of each TreeList row (`tr`) element in the DOM.
* The [virtual scrolling functionality]({% slug scrollmmodes_treelist %}) requires this setting.
* Set the `rowHeight` option to match the exact pixel height of the `tr` element in the DOM.
*/
rowHeight;
/**
* Sets the number of records that the pager skips.
* The [paging]({% slug paging_treelist %}) functionality requires this setting.
*/
get skip() {
return this._skip;
}
set skip(value) {
if (typeof value === 'number' && value >= 0) {
this._skip = value;
this.view.clear();
}
}
/**
* Sets the scroll mode for the TreeList.
*
* @default 'scrollable'
*/
scrollable = 'scrollable';
/**
* Sets the descriptors for sorting the data ([see example]({% slug sorting_treelist %})).
*/
set sort(value) {
if (isArray(value)) {
this._sort = value;
}
}
get sort() {
return this._sort;
}
/**
* Sets a function that defines how to track changes for the data rows.
*
* By default, the TreeList tracks changes by the index of the data item.
* The TreeList tracks edited rows by reference.
*/
trackBy = defaultTrackBy;
/**
* Sets the descriptor for filtering the data ([see examples]({% slug filtering_treelist %})).
*/
filter;
/**
* When set to `true`, the TreeList renders only the columns in the current viewport.
*
* @default false
*/
virtualColumns = false;
/**
* @hidden
*/
get showTopToolbar() {
return this.toolbarTemplate && ['top', 'both'].indexOf(this.toolbarTemplate.position) > -1;
}
/**
* @hidden
*/
get showBottomToolbar() {
return this.toolbarTemplate && ['bottom', 'both'].indexOf(this.toolbarTemplate.position) > -1;
}
/**
* @hidden
*/
get isLocked() {
return this.lockedLeafColumns.length > 0;
}
/**
* @hidden
*/
get showPager() {
return !this.isVirtual && this.pageable !== false;
}
get showPagerInput() {
return this._showPagerInput;
}
set showPagerInput(value) {
if (this._showPagerInput === value) {
return;
}
this._showPagerInput = value;
}
get showPagerPageText() {
return this._showPagerPageText;
}
set showPagerPageText(value) {
if (!this.normalizedPageableSettings?.responsive) {
this._showPagerPageText = true;
}
if (this._showPagerPageText === value) {
return;
}
this._showPagerPageText = value;
}
get showPagerItemsText() {
return this._showPagerItemsText;
}
set showPagerItemsText(value) {
if (!this.normalizedPageableSettings?.responsive) {
this._showPagerItemsText = true;
}
if (this._showPagerItemsText === value) {
return;
}
this._showPagerItemsText = value;
}
get marqueeSelection() {
return this.selectionService.enableMarquee;
}
/**
* Enables the [filtering]({% slug filtering_treelist %}) of TreeList columns that have their `field` option set.
*
* @default false
*/
filterable = false;
/**
* Enables the [sorting]({% slug sorting_treelist %}) of TreeList columns that have their `field` option set.
*
* @default false
*/
sortable = false;
/**
* Configures the pager for the TreeList ([see example]({% slug paging_treelist %})).
*
* @default false
*/
pageable = false;
get normalizedPageableSettings() {
return normalize(this.pageable);
}
/**
* When keyboard navigation is enabled, you can use dedicated shortcuts to interact with the TreeList.
* By default, navigation is enabled. To disable it and include the TreeList content in the normal tab sequence, set this property to `false`.
*
* @default true
*/
navigable = true;
/**
* Determines whether TreeList columns resize during initialization to fit their headers and row content.
* Columns with `autoSize` set to `false` are excluded.
* To dynamically update the column width to match new content,
* refer to [this example]({% slug resizing_columns_treelist %}).
*
* @default false
*/
autoSize = false;
/**
* A function executed for every data row in the component. It should return a string that will be used as a CSS class for the row.
*/
set rowClass(fn) {
if (typeof fn !== 'function') {
throw new Error(`rowClass must be a function, but received ${JSON.stringify(fn)}.`);
}
this._rowClass = fn;
}
get rowClass() {
return this._rowClass;
}
/**
* Returns the currently focused cell (if any).
*/
get activeCell() {
return this.navigationService.activeCell;
}
/**
* Gets the currently focused row (if any).
*/
get activeRow() {
return this.navigationService.activeRow;
}
/**
* When set to `true`, you can resize columns by dragging the edges (resize handles) of their header cells
* ([see example]({% slug resizing_columns_treelist %})).
*
* @default false
*/
resizable = false;
/**
* When set to `true`, you can reorder columns by dragging their header cells
* ([see example]({% slug reordering_columns_treelist %})).
*
* @default false
*/
reorderable = false;
/**
* Determines whether the TreeList displays the loading indicator ([see example]({% slug databinding_treelist %})).
*
* @default false
*/
loading = false;
/**
* Determines whether the column menu of the columns displays ([see example]({% slug columnmenu_treelist %})).
*
* @default false
*/
columnMenu = false;
/**
* Determines whether the TreeList hides the header. The header is visible by default.
*
* The header includes column headers and the [filter row](slug:filter_row_treelist).
*
* @default false
*/
hideHeader = false;
/**
* Sets the name of the field that contains the unique identifier of the node.
*
* @default "id"
*/
set idField(value) {
if (typeof value === "function") {
this.idGetter = value;
}
else {
this.idGetter = getter(value);
}
this.editService.idGetter = this.idGetter;
}
/**
* Sets the TreeList selection settings.
*/
set selectable(value) {
this.selectionService.settings = value;
}
/**
* Sets a callback that determines if the given row or cell is selected.
*/
set isSelected(value) {
if (typeof value !== 'function' && isDevMode()) {
throw new Error(`isSelected must be a function, but received "${JSON.stringify(value)}".`);
}
this.selectionService.isSelected = value;
}
/**
* Enables the [row reordering]({% slug treelist_row_reordering %}) of the TreeList.
*
* @default false
*/
set rowReorderable(value) {
this._rowReorderable = value;
if (value) {
this.rowReorderSubscription = this.rowReorderService.rowReorder.subscribe(args => {
hasObservers(this.rowReorder) && this.ngZone.run(() => {
this.rowReorder.emit(args);
});
});
this.subscriptions.add(this.rowReorderSubscription);
}
else {
this.rowReorderSubscription?.unsubscribe();
}
}
get rowReorderable() {
return this._rowReorderable;
}
/**
* Fires when the TreeList selection changes.
*/
selectionChange = new EventEmitter();
/**
* Fires when you modify the TreeList filter through the UI.
* You have to handle the event and filter the data.
*/
filterChange = new EventEmitter();
/**
* Fires when the page of the TreeList changes ([see example]({% slug paging_treelist %})).
* You have to handle the event and page the data.
*/
pageChange = new EventEmitter();
/**
* Fires when the sorting of the TreeList changes ([see example]({% slug sorting_treelist %})).
* You have to handle the event and sort the data.
*/
sortChange = new EventEmitter();
/**
* Fires when the data state of the TreeList changes.
*/
dataStateChange = new EventEmitter();
/**
* Fires when you click the **Edit** command button to edit a row
* ([see example]({% slug editing_template_forms_treelist %}#toc-editing-records)).
*/
edit = new EventEmitter();
/**
* Fires when you click the **Cancel** command button to close a row
* ([see example]({% slug editing_template_forms_treelist %}#toc-cancelling-editing)).
*/
cancel = new EventEmitter();
/**
* Fires when you click the **Save** command button to save changes in a row
* ([see example]({% slug editing_template_forms_treelist %}#toc-saving-records)).
*/
save = new EventEmitter();
/**
* Fires when you click the **Remove** command button to remove a row
* ([see example]({% slug editing_template_forms_treelist %}#toc-removing-records)).
*/
remove = new EventEmitter();
/**
* Fires when you click the **Add** command button to add a new row
* ([see example]({% slug editing_template_forms_treelist %}#toc-adding-records)).
*/
add = new EventEmitter();
/**
* Fires when you leave an edited cell ([see example]({% slug editing_incell_treelist %}#toc-basic-concepts)).
*/
cellClose = new EventEmitter();
/**
* Fires when you click a cell ([see example]({% slug editing_incell_treelist %}#toc-basic-concepts)).
*/
cellClick = new EventEmitter();
/**
* Fires when you click the **Export to PDF** command button.
*/
pdfExport = new EventEmitter();
/**
* Fires when you click the **Export to Excel** command button.
*/
excelExport = new EventEmitter();
/**
* Fires when you complete the resizing of the column.
*/
columnResize = new EventEmitter();
/**
* Fires when you complete the reordering of the column.
*/
columnReorder = new EventEmitter();
/**
* Fires when you change the visibility of the columns from the column menu or column chooser.
*/
columnVisibilityChange = new EventEmitter();
/**
* Fires when you change the locked state of the columns from the column menu or by reordering the columns.
*/
columnLockedChange = new EventEmitter();
/**
* Fires when you scroll to the last record on the page and enables endless scrolling
* ([see example]({% slug scrollmmodes_treelist %}#toc-endless-scrolling)).
* You have to handle the event and page the data.
*/
scrollBottom = new EventEmitter();
/**
* Fires when the TreeList content scrolls.
* For performance reasons, the event triggers outside the Angular zone. Enter the Angular zone if you make any changes that require change detection.
*/
contentScroll = new EventEmitter();
/**
* Fires when an item expands.
*/
expandEvent = new EventEmitter();
/**
* Fires when an item collapses.
*/
collapseEvent = new EventEmitter();
/**
* @hidden
*
* Emits when the expand or collapse events are fired.
* Used by the expand directive and the Gantt component.
*/
expandStateChange = new EventEmitter();
/**
* Fires when you drop the dragged row and reordering occurs.
* Emits the [RowReorderEvent]({% slug api_treelist_rowreorderevent %}).
*/
rowReorder = new EventEmitter();
/**
* @hidden
*/
columnOrderChange = new EventEmitter();
/**
* @hidden
*/
set columnsRef(columns) {
this._columnsRef = columns;
// load the new columns list only if a valid query list is provided
if (isPresent(columns)) {
this.loadColumns(columns);
}
}
get columnsRef() {
return this._columnsRef;
}
/**
* A query list of all declared columns.
*/
columns = new QueryList();
get dir() {
return this.direction;
}
hostClasses = true;
get lockedClasses() {
return this.lockedLeafColumns.length > 0;
}
get virtualClasses() {
return this.isVirtual;
}
get noScrollbarClass() {
return this.scrollbarWidth === 0;
}
noRecordsTemplateChildren;
get noRecordsTemplate() {
if (this._customNoRecordsTemplate) {
return this._customNoRecordsTemplate;
}
return this.noRecordsTemplateChildren ? this.noRecordsTemplateChildren.first : undefined;
}
set noRecordsTemplate(customNoRecordsTemplate) {
this._customNoRecordsTemplate = customNoRecordsTemplate;
}
pagerTemplateChildren;
get pagerTemplate() {
if (this._customPagerTemplate) {
return this._customPagerTemplate;
}
return this.pagerTemplateChildren ? this.pagerTemplateChildren.first : undefined;
}
set pagerTemplate(customPagerTemplate) {
this._customPagerTemplate = customPagerTemplate;
}
toolbarTemplateChildren;
get toolbarTemplate() {
if (this._customToolbarTemplate) {
return this._customToolbarTemplate;
}
return this.toolbarTemplateChildren ? this.toolbarTemplateChildren.first : undefined;
}
set toolbarTemplate(customToolbarTemplate) {
this._customToolbarTemplate = customToolbarTemplate;
}
columnMenuTemplates;
lockedHeader;
header;
footer = new QueryList();
ariaRoot;
dragTargetContainer;
dropTargetContainer;
listComponent;
get scrollbarWidth() {
return this.supportService.scrollbarWidth;
}
get headerPadding() {
if (isUniversal()) {
return '';
}
const padding = Math.max(0, this.scrollbarWidth) + 'px';
const right = this.rtl ? 0 : padding;
const left = this.rtl ? padding : 0;
return `0 ${right} 0 ${left}`;
}
columnMenuOptions;
columnList;
columnsContainer = new ColumnsContainer(() => this.columnList.filterHierarchy(column => {
if (!isUniversal()) {
column.matchesMedia = this.matchesMedia(column);
}
return column.isVisible;
}));
get showLoading() {
return this.loading || (isObservable(this._data) && !this.loadedData) || this.excelService.loading;
}
get showFooter() {
return this.columnsContainer.hasFooter;
}
get ariaRowCount() {
return this.totalColumnLevels + 1 + this.totalCount;
}
get ariaColCount() {
return this.columnsContainer.leafColumnsToRender.length;
}
get ariaMultiselectable() {
if (this.selectionService.enabled) {
return this.selectionService.enableMultiple;
}
}
get navigation() {
return this.navigationService;
}
get isVirtual() {
return this.scrollable === 'virtual';
}
get isScrollable() {
return this.scrollable !== 'none';
}
get visibleColumns() {
return this.columnsContainer.allColumns;
}
get lockedColumns() {
return this.columnsContainer.lockedColumns;
}
get nonLockedColumns() {
return this.columnsContainer.nonLockedColumns;
}
get lockedLeafColumns() {
return this.columnsContainer.lockedLeafColumns;
}
get nonLockedLeafColumns() {
return this.columnsContainer.nonLockedLeafColumns;
}
get leafColumns() {
return this.columnsContainer.leafColumns;
}
get totalColumnLevels() {
return this.columnsContainer.totalLevels;
}
get headerColumns() {
if (this.virtualColumns && !this.pdfService.exporting) {
return this.viewportColumns;
}
return this.nonLockedColumns;
}
get headerLeafColumns() {
if (this.virtualColumns && !this.pdfService.exporting) {
return this.leafViewportColumns;
}
return this.nonLockedLeafColumns;
}
get lockedWidth() {
return expandColumns(this.lockedLeafColumns.toArray()).reduce((prev, curr) => prev + (curr.width || 0), 0);
}
get nonLockedWidth() {
if ((!this.rtl && this.lockedLeafColumns.length) || this.virtualColumns) {
return !this.virtualColumns ? this.columnsContainer.unlockedWidth :
this.leafViewportColumns.reduce((acc, column) => acc + (column.width || 0), 0);
}
return undefined;
}
get columnMenuTemplate() {
const template = this.columnMenuTemplates.first;
return template ? template.templateRef : null;
}
get totalCount() {
return this.view.totalRows;
}
/**
* Sets or gets the callback function that retrieves the child nodes for a particular node.
*/
set fetchChildren(value) {
this._fetchChildren = value;
}
get fetchChildren() {
return this._fetchChildren;
}
/**
* Sets or gets the callback function that determines if a particular node has child nodes.
*/
set hasChildren(value) {
this._hasChildren = value;
}
get hasChildren() {
return this._hasChildren;
}
/**
* Sets the callback function that determines if a particular item is expanded.
*/
set isExpanded(value) {
this.expandStateService.isExpanded = value || defaultExpanded;
this.expandIcons = Boolean(value);
}
idGetter = getter(undefined);
localEditService = new LocalEditService();
view;
expandIcons;
ariaRootId = `k-${guid()}`;
dataChanged = false;
loadedData;
_fetchChildren;
_hasChildren = (() => false);
subscriptions = new Subscription();
dataLoadedSubscription;
focusElementSubscription;
rowReorderSubscription;
detachElementEventHandlers;
rtl = false;
shouldGenerateColumns = true;
direction;
_data = [];
_sort = new Array();
_skip = 0;
_columnsRef;
cachedWindowWidth = 0;
_customNoRecordsTemplate;
_customPagerTemplate;
_customToolbarTemplate;
leafViewportColumns;
viewportColumns;
pageChangeTimeout;
_rowReorderable = false;
_showPagerInput = true;
_showPagerPageText = true;
_showPagerItemsText = true;
constructor(supportService, wrapper, changeNotification, editService, filterService, pdfService, responsiveService, renderer, excelService, ngZone, scrollSyncService, domEvents, columnResizingService, changeDetectorRef, columnReorderService, columnInfoService, navigationService, sortService, scrollRequestService, expandStateService, optionChanges, selectionService, localization, ctx, rowReorderService) {
this.supportService = supportService;
this.wrapper = wrapper;
this.changeNotification = changeNotification;
this.editService = editService;
this.filterService = filterService;
this.pdfService = pdfService;
this.responsiveService = responsiveService;
this.renderer = renderer;
this.excelService = excelService;
this.ngZone = ngZone;
this.scrollSyncService = scrollSyncService;
this.domEvents = domEvents;
this.columnResizingService = columnResizingService;
this.changeDetectorRef = changeDetectorRef;
this.columnReorderService = columnReorderService;
this.columnInfoService = columnInfoService;
this.navigationService = navigationService;
this.sortService = sortService;
this.scrollRequestService = scrollRequestService;
this.expandStateService = expandStateService;
this.optionChanges = optionChanges;
this.selectionService = selectionService;
this.localization = localization;
this.ctx = ctx;
this.rowReorderService = rowReorderService;
validatePackage(packageMetadata);
this.subscriptions.add(localization.changes.subscribe(({ rtl }) => {
this.rtl = rtl;
this.direction = this.rtl ? 'rtl' : 'ltr';
}));
this.view = new ViewCollection(this.viewFieldAccessor.bind(this), this.expandStateService, this.editService, this.selectionService);
this.selectionService.init(this);
this.ctx.treelist = this;
this.subscriptions.add(this.selectionService.changes.subscribe((args) => {
if (hasObservers(this.selectionChange)) {
this.ngZone.run(() => {
args.sender = this;
this.selectionChange.emit(args);
this.selectionService.updateSelectedState();
this.changeDetectorRef.markForCheck();
});
}
}));
this.columnInfoService.init(this.columnsContainer, () => this.columnList);
this.subscriptions.add(this.columnInfoService.visibilityChange.subscribe((changed) => {
this.columnVisibilityChange.emit(new ColumnVisibilityChangeEvent(changed));
this.changeDetectorRef.markForCheck();
}));
this.subscriptions.add(this.columnInfoService.lockedChange.subscribe((changed) => {
this.columnLockedChange.emit(new ColumnLockedChangeEvent(changed));
this.changeDetectorRef.markForCheck();
}));
this.subscriptions.add(merge(this.optionChanges.columns, this.optionChanges.options).subscribe(() => {
this.changeDetectorRef.markForCheck();
}));
this.subscriptions.add(this.filterService.changes.subscribe(x => {
this.filterChange.emit(x);
}));
this.subscriptions.add(this.sortService.changes.subscribe(x => {
this.sortChange.emit(x);
}));
this.attachStateChangesEmitter();
this.attachEditHandlers();
this.attachDomEventHandlers();
this.subscriptions.add(this.pdfService.exportClick.subscribe(this.emitPDFExportEvent.bind(this)));
this.subscriptions.add(this.excelService.exportClick.subscribe(this.saveAsExcel.bind(this)));
this.subscriptions.add(this.excelService.loadingChange.subscribe(() => {
this.changeDetectorRef.detectChanges();
}));
this.columnsContainerChange();
this.handleColumnResize();
this.columnList = new ColumnList(this.columns);
this.subscriptions.add(this.columnReorderService
.changes.subscribe(this.reorder.bind(this)));
this.subscriptions.add(this.columnInfoService.columnRangeChange.subscribe(this.onColumnRangeChange.bind(this)));
this.subscriptions.add(this.expandStateService.changes.subscribe((args) => {
if (args.expand) {
this.expandEvent.emit(args);
}
else {
this.collapseEvent.emit(args);
}
if (!args.isDefaultPrevented()) {
this.changeDetectorRef.markForCheck();
this.view.clear();
this.expandStateChange.emit(args);
}
if (this.rowReorderable) {
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
this.notifyReorderContainers();
});
}
}));
this.subscriptions.add(this.view.childrenLoaded.subscribe(() => {
this.changeDetectorRef.detectChanges();
}));
this.subscriptions.add(this.view.resetPage.subscribe(() => {
if (this.skip > 0 && hasObservers(this.pageChange)) {
// don't think there is a way to avoid this
// every callback in which the view can be computed is already passed the change detection
// computing the current page in advance also does not seem feasible for such a rare case
this.pageChangeTimeout = setTimeout(() => {
this.pageChange.emit({ skip: 0, take: this.pageSize });
}, 0);
}
this.skip = 0;
}));
this.dataLoaded = this.dataLoaded.bind(this);
this.editService.idGetter = this.idGetter;
}
/**
* @hidden
*/
viewFieldAccessor() {
return {
fetchChildren: this.fetchChildren,
hasChildren: this.hasChildren,
idGetter: this.idGetter,
skip: this.skip,
pageSize: this.pageSize,
pageable: this.pageable,
isVirtual: this.isVirtual,
data: this.loadedData,
hasFooter: this.columnsContainer.hasFooter
};
}
/**
* @hidden
*/
onDataChange() {
this.autoGenerateColumns();
this.changeNotification.notify();
this.pdfService.dataChanged.emit();
this.updateNavigationMetadata();
}
ngOnChanges(changes) {
if (this.lockedLeafColumns.length && anyChanged(["pageSize", "skip", "sort"], changes)) {
this.changeNotification.notify();
}
if (anyChanged(["pageSize", "scrollable", 'virtualColumns'], changes)) {
this.updateNavigationMetadata();
}
if (isChanged("virtualColumns", changes)) {
this.viewportColumns = this.leafViewportColumns = null;
}
if (isChanged("height", changes, false)) {
this.renderer.setStyle(this.wrapper.nativeElement, 'height', `${this.height}px`);
}
if (isChanged("filterable", changes) && this.lockedColumns.length) {
this.syncHeaderHeight(this.ngZone.onStable.asObservable().pipe(take(1)));
}
if (anyChanged(["columnMenu", "sortable", "filterable"], changes, false)) {
this.columnMenuOptions = this.columnMenu && Object.assign({
filter: Boolean(this.filterable),
sort: Boolean(this.sortable)
}, this.columnMenu);
}
if (isChanged("scrollable", changes) && this.isScrollable) {
this.ngZone.onStable.pipe(take(1)).subscribe(() => this.attachScrollSync());
}
}
ngAfterViewInit() {
this.attachScrollSync();
this.attachElementEventHandlers();
this.updateNavigationMetadata();
this.applyAutoSize();
this.subscriptions.add(this.pagerTemplateChildren.changes.subscribe(() => this.changeDetectorRef.markForCheck()));
const toolbarComponentWrapper = this.wrapper?.nativeElement?.querySelector('kendo-toolbar');
if (toolbarComponentWrapper) {
this.renderer.addClass(toolbarComponentWrapper, 'k-grid-toolbar');
}
}
ngAfterContentChecked() {
if (this.dataChanged) {
this.dataChanged = false;
this.onDataChange();
}
this.columnsContainer.refresh();
this.verifySettings();
}
ngAfterContentInit() {
// initially passed columns though the input prop are overwritten by ContentChildren-queried ones
if (isPresent(this.columnsRef)) {
this.loadColumns(this.columnsRef);
}
this.shouldGenerateColumns = !this.columns.length;
this.autoGenerateColumns();
this.columnList = new ColumnList(this.columns);
// is this needed? after content checked already does this
this.subscriptions.add(this.columns.changes.subscribe(() => {
this.verifySettings();
this.optionChanges.columnChanged();
}));
}
ngOnInit() {
if (this.navigable) {
this.navigationService.init(this.navigationMetadata());
}
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
if (this.detachElementEventHandlers) {
this.detachElementEventHandlers();
}
if (this.focusElementSubscription) {
this.focusElementSubscription.unsubscribe();
}
this.unsubscribeDataLoaded();
this.ngZone = null;
clearTimeout(this.pageChangeTimeout);
}
/**
* @hidden
*/
handleReorderEvents(ev, evType) {
this.rowReorderService[evType](ev);
}
/**
* @hidden
*/
getDefaultSelectors(type) {
return this.rowReorderService.defaultSelectors[type];
}
/**
* @hidden
*/
treeListData = () => { return this.view; };
/**
* @hidden
*/
getHintSettings(setting) {
return this.rowReorderService[setting];
}
/**
* @hidden
*/
get hintText() {
return this.rowReorderService.getDefaultHintText(this.columnList, this.view);
}
/**
* @hidden
*/
attachScrollSync() {
if (isUniversal()) {
return;
}
if (this.header) {
this.scrollSyncService.registerEmitter(this.header.nativeElement, "header");
}
if (this.footer) {
this.subscriptions.add(observe(this.footer)
.subscribe(footers => footers
.map(footer => footer.nativeElement)
.filter(isPresent)
.forEach(element => this.scrollSyncService.registerEmitter(element, "footer"))));
}
}
/**
* Switches the specified table row to edit mode ([see example]({% slug editing_template_forms_treelist %}#toc-editing-records)).
*
* @param dataItem The data item that you will edit.
* @param group - The [`FormGroup`](link:site.data.urls.angular['formgroupapi'])
* that describes the edit form.
* @param options Additional options. Use `skipFocus` to determine if the edit element of the row should receive focus.
*
* @default false
*/
editRow(dataItem, group, options) {
this.editService.editRow(dataItem, group);
this.view.updateEditedState();
this.changeDetectorRef.markForCheck();
if (options && options['skipFocus']) {
return;
}
this.focusEditElement(() => {
return `tr[data-treelist-view-index="${this.view.itemIndex(dataItem)}"]`;
});
}
/**
* Closes the editor for a given row ([see example]({% slug editing_template_forms_treelist %}#toc-cancelling-editing)).
*
* @param dataItem The data item that you will switch out of edit mode.
* @param isNew Determines whether the data item is new.
*/
closeRow(dataItem, isNew) {
this.editService.close(dataItem, isNew);
this.changeDetectorRef.markForCheck();
if (isNew) {
this.view.clear();
}
else {
this.view.updateEditedState();
}
}
/**
* Creates a new row editor ([see example]({% slug editing_template_forms_treelist %}#toc-adding-records)).
*
* @param group The [`FormGroup`](link:site.data.urls.angular['formgroupapi']) that describes
* the edit form. If called with a data item, it builds the `FormGroup` from the data item fields.
*/
addRow(group, parent) {
const isFormGroup = group instanceof FormGroup;
if (!isFormGroup) {
const fields = Object.keys(group).reduce(createControl(group), {}); // FormBuilder?
group = new FormGroup(fields);
}
if (this.isVirtual && !parent && this.skip) {
const firstVisible = this.navigationService.viewport.firstItemIndex;
if (firstVisible !== this.skip) {
this.skip = firstVisible;
this.pageChange.emit({
skip: this.skip,
take: this.pageSize
});
}
}
this.editService.addRow(parent, group);
this.changeDetectorRef.markForCheck();
this.view.clear();
this.focusEditElement(() => {
return parent ? `tr[data-treelist-view-index="${this.view.itemIndex(parent) + 1}"]` : '.k-grid-add-row';
});
}
/**
* Puts the cell that you specify by the table row and column in edit mode.
*
* @param dataItem The data item that you will edit.
* @param column The leaf column index, or the field name or the column instance that should be edited.
* @param group The [`FormGroup`](link:site.data.urls.angular['formgroupapi'])
* that describes the edit form.
*/
editCell(dataItem, column, group) {
const instance = this.columnInstance(column);
this.editService.editCell(dataItem, instance, group);
this.changeDetectorRef.markForCheck();
this.view.updateEditedState();
this.focusEditElement(() => '.k-grid-edit-cell');
}
/**
* Closes the current cell in edit mode and fires
* the [`cellClose`]({% slug api_treelist_treelistcomponent %}#toc-cellclose) event.
*
* @return {boolean} A Boolean value that indicates whether the edited cell closed.
* A `false` value indicates that the [`cellClose`]({% slug api_treelist_treelistcomponent %}#toc-cellclose) event was prevented.
*/
closeCell() {
return !this.editService.closeCell();
}
/**
* Closes the current cell in edit mode.
*/
cancelCell() {
this.editService.cancelCell();
this.view.updateEditedState();
}
/**
* Gets a flag that indicates if a row or a cell is currently edited.
*
* @return {boolean} A Boolean flag that indicates if a row or a cell is currently edited.
*/
isEditing() {
return this.editService.isEditing();
}
/**
* Gets a flag that indicates if a cell is currently edited.
*
* @return {boolean} A Boolean flag that indicates if a cell is currently being edited.
*/
isEditingCell() {
return this.editService.isEditing() && this.editService.isEditingCell();
}
/**
* Starts the PDF export ([see example]({% slug pdfexport_treelist %})).
*/
saveAsPDF() {
this.pdfService.save(this);
}
/**
* Exports the TreeList element to a Drawing [`Group`]({% slug api_kendo-drawing_group %}) by using the `kendo-treelist-pdf` component options.
* ([see example]({% slug pdfexport_treelist %}#toc-exporting-multiple-treelists-to-the-same-pdf)).
*
* @return {Promise} A promise that resolves with the Drawing `Group`.
*/
drawPDF() {
const promise = createPromise();
this.pdfService.draw(this, promise);
return promise;
}
/**
* Starts the Excel export ([see example]({% slug excelexport_treelist %})).
*/
saveAsExcel() {
this.excelService.save(this);
}
/**
* Applies the minimum possible width for the specified column,
* so that the whole text fits without wrapping. This method expects the TreeList
* to be resizable (set `resizable` to `true`).
* Execute this method only
* after the TreeList is already populated with data. [See example](slug:resizing_columns_treelist#toc-auto-fitting-the-content).
*/
autoFitColumn(column) {
this.columnResizingService.autoFit(column);
}
/**
* Adjusts the width of the specified columns to fit the entire content, including headers, without wrapping.
* If you do not specify columns, `autoFitColumns` applies to all columns.
*
* This method requires the TreeList to be resizable (set `resizable` to `true`).
* [See example](slug:resizing_columns_treelist#toc-auto-fitting-the-content).
*/
autoFitColumns(columns = this.columns) {
let cols;
if (columns instanceof QueryList) {
cols = columns.toArray();
}
else {
cols = columns;
}
this.columnResizingService.autoFit(...cols);
}
/**
* @hidden
*/
notifyPageChange(source, event) {
if (source === "list" && !this.isVirtual) {
return;
}
this.skip = event.skip;
this.pageSize = event.take;
this.closeCell();
this.cancelCell();
this.changeDetectorRef.markForCheck();
this.pageChange.emit(event);
}
/**
* @hidden
*/
handlePagerVisibilityChange(prop, ev) {
this[prop] = ev;
}
/**
* @hidden
*/
messageFor(token) {
return this.localization.get(token);
}
/**
* @hidden
*/
notifyScrollBottom() {
if (this.scrollable === 'none') {
return;
}
if (hasObservers(this.scrollBottom)) {
this.ngZone.run(() => this.scrollBottom.emit({ sender: this }));
}
}
/**
* @hidden
*/
focusEditElement(containerSelector) {
if (this.focusElementSubscription) {
this.focusElementSubscription.unsubscribe();
}
this.ngZone.runOutsideAngular(() => {
this.focusElementSubscription = this.ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
const wrapper = this.wrapper.nativeElement;
const selector = containerSelector();
if (!this.setEditFocus(wrapper.querySelector(selector)) && this.isLocked) {
this.setEditFocus(wrapper.querySelector(`.k-grid-content ${selector}`));
}
this.focusElementSubscription = null;
});
});
}
/**
* Focuses the last active or the first cell of the TreeList.
*
* @returns {NavigationCell} The focused cell.
*/
focus() {
this.assertNavigable();
return this.navigationService.focusCell();
}
/**
* Focuses the cell with the specified row and column index.
*
* The row index is based on the logical structure of the TreeList and does not correspond to the data item index.
* The row indexing is absolute and does not change with paging.
* Header rows are included, starting at index 0.
*
* If the TreeList is configured for scrolling, including virtual scrolling, the scroll position updates.
* If the row is not present on the current page, the method has no effect.
*
* @param rowIndex - The logical row index to focus. The top header row has an index 0.
* @param colIndex - The column index to focus.
* @returns {NavigationCell} The focused cell.
*
*/
focusCell(rowIndex, colIndex) {
this.assertNavigable();
return this.navigationService.focusCell(rowIndex, colIndex);
}
/**
* Focuses the next cell, optionally wrapping to the next row.
*
* @param wrap A Boolean value that indicates if the focus moves to the next row.
* @return {NavigationCell} The focused cell. If the focus is already on the last cell, returns `null`.
*
* @default true
*/
focusNextCell(wrap = true) {
this.assertNavigable();
return this.navigationService.focusNextCell(wrap);
}
/**
* Focuses