UNPKG

@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
/**----------------------------------------------------------------------------------------- * 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