@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.
469 lines (468 loc) • 15.1 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 { isObservable, Subscription, BehaviorSubject } from 'rxjs';
import { take } from 'rxjs/operators';
import { EventEmitter } from '@angular/core';
/**
* @hidden
*/
export const EXPANDED_STATE = {
isExpanded: () => true,
};
/**
* @hidden
*/
export const UNSELECTED_STATE = {
isRowSelected: () => false,
};
/**
* @hidden
*/
export const NOT_EDITED_STATE = {
hasNew: () => false,
context: () => null,
};
const identity = (item) => item;
function loadView(view, subject) {
view.loadData();
if (view.loading) {
// Wait for view to load and fetch children.
view.dataLoaded.pipe(take(1)).subscribe(() => {
loadView(view, subject);
});
}
else if (!view._data) {
// Fetch children if root items are resolved immediately.
loadView(view, subject);
}
else {
// Data loaded.
subject.next(view);
}
}
const LOADING = 'loading';
class ViewRange {
skip;
pageSize;
pageable;
expandAll;
static create(options) {
const { skip, pageSize, pageable, isVirtual } = options;
let rangeType = ViewRange;
if (pageSize) {
if (pageable) {
rangeType = PagerRange;
}
else if (isVirtual) {
rangeType = VirtualRange;
}
}
return new rangeType(skip, pageSize, pageable);
}
constructor(skip, pageSize, pageable) {
this.skip = skip;
this.pageSize = pageSize;
this.pageable = pageable;
}
inRange(_index, _rowIndex) {
return true;
}
levelInRange(_parent, _items, _rowIndex) {
return true;
}
includeParents(_children) {
// eslint-disable-next-line no-empty
}
}
function isPagerSettings(pageable) {
return typeof pageable === 'object';
}
class PagerRange extends ViewRange {
expandAll = true;
constructor(skip, pageSize, pageable) {
super(skip, pageSize, pageable);
if (isPagerSettings(pageable)) {
this.expandAll = pageable.countChildren !== false;
}
}
inRange(index, _rowIndex) {
return this.skip <= index && index < this.skip + this.pageSize;
}
levelInRange(parent, items, _rowIndex) {
return (parent.level === -1 ||
(this.skip <= parent.index + items.length && parent.index + 1 < this.skip + this.pageSize));
}
includeParents(children) {
let parentLevel = children.parentLevel;
while (parentLevel && !parentLevel.inRange) {
parentLevel.inRange = true;
parentLevel = parentLevel.parentLevel;
}
}
}
class VirtualRange extends ViewRange {
inRange(_index, rowIndex) {
return this.skip <= rowIndex && rowIndex < this.skip + this.pageSize;
}
levelInRange(parent, _items, rowIndex) {
return this.inRange(parent.index, rowIndex);
}
}
/**
* @hidden
*/
export class ViewItemFactory {
expandState;
editState;
selectionState;
loaded;
fieldAccessor;
observables = [];
rowIndex = 0;
rootLevel;
fetchChildren;
hasChildren;
idGetter;
hasFooter;
viewRange;
offsetFirst;
constructor(expandState, editState, selectionState, loaded, fieldAccessor, rootItem) {
this.expandState = expandState;
this.editState = editState;
this.selectionState = selectionState;
this.loaded = loaded;
this.fieldAccessor = fieldAccessor;
const options = this.fieldAccessor();
this.fetchChildren = options.fetchChildren;
this.hasChildren = options.hasChildren;
this.idGetter = options.idGetter;
this.hasFooter = options.hasFooter;
this.viewRange = ViewRange.create(options);
if (rootItem) {
this.rootLevel = this.loadChildren(rootItem);
}
else {
this.rootLevel = this.dataLevel({
level: -1,
id: null,
expanded: true,
}, options.data);
}
}
// try to stop iteration if cached total and viewRange ends
generate() {
const result = [];
const dataLevels = [this.rootLevel];
let itemIndex = 0;
let itemCount = 0;
this.addNew(result);
while (dataLevels.length) {
while (dataLevels[0] && dataLevels[0].idx >= dataLevels[0].items.length) {
const dataLevel = dataLevels.shift();
if (this.hasFooter && dataLevel.expanded && dataLevel.items.length) {
if (dataLevel.inRange ||
this.viewRange.levelInRange(dataLevel.parent, dataLevel.items, this.rowIndex)) {
result.push({
type: 'footer',
items: dataLevel.items,
aggregates: dataLevel.aggregates,
level: dataLevel.level,
parentItem: dataLevel.parent.data,
parentIndex: dataLevel.parentIndex,
rowIndex: this.rowIndex,
});
this.viewRange.includeParents(dataLevel);
}
this.rowIndex++;
}
}
if (!dataLevels.length) {
break;
}
const currentLevel = dataLevels[0];
const dataItem = currentLevel.items[currentLevel.idx++];
const viewItem = {
type: 'data',
data: dataItem,
id: this.idGetter(dataItem),
rowIndex: this.rowIndex,
index: itemIndex,
level: currentLevel.level,
hasChildren: this.hasChildren(dataItem),
parent: currentLevel.parent,
};
if (currentLevel.expanded) {
this.rowIndex++;
if (this.viewRange.inRange(itemIndex, viewItem.rowIndex)) {
if (this.offsetFirst) {
viewItem.rowIndex++;
this.rowIndex++;
this.offsetFirst = false;
}
result.push(viewItem);
viewItem.editContext = this.editState.context(viewItem.data);
viewItem.selected = this.selectionState.isRowSelected(dataItem);
this.addNew(result, dataItem);
}
itemIndex++;
}
itemCount++;
const expanded = viewItem.hasChildren && this.expandState.isExpanded(viewItem.data);
if (viewItem.hasChildren && (expanded || this.viewRange.expandAll)) {
viewItem.expanded = expanded && currentLevel.expanded;
const children = this.loadChildren(viewItem);
if (children) {
dataLevels.unshift(children);
children.parentLevel = currentLevel;
}
}
}
return {
items: result,
observables: this.observables,
total: itemCount,
totalVisible: itemIndex,
totalRows: this.rowIndex,
};
}
loadChildren(parent) {
const parentId = parent.id;
if (this.loaded.has(parentId)) {
const children = this.loaded.get(parentId);
if (children === LOADING) {
parent.loading = true;
}
else {
return this.dataLevel(parent, children);
}
}
else {
const children = this.fetchChildren(parent.data);
if (isObservable(children)) {
this.observables.push({
observable: children,
parentId: parentId,
});
parent.loading = true;
}
else if (children) {
this.loaded.set(parentId, children);
return this.dataLevel(parent, children);
}
}
}
dataLevel(parent, children) {
children = children || {};
const data = children.data || children;
const items = data && data.length ? data : [];
return {
idx: 0,
level: parent.level + 1,
items: items,
aggregates: children.aggregates,
expanded: parent.expanded,
parentIndex: parent.index,
parent: parent,
};
}
addNew(result, parent) {
if (this.editState.hasNew(parent)) {
const inRange = this.viewRange.inRange(0, this.rowIndex);
const rowIndex = parent || inRange ? this.rowIndex : this.viewRange.skip;
result.push({
parent: parent,
isNew: true,
type: 'data',
data: this.editState.newItem.dataItem,
editContext: this.editState.newItem,
rowIndex: rowIndex,
});
if (parent || inRange) {
this.rowIndex++;
}
else {
this.offsetFirst = true;
}
}
}
}
/**
* @hidden
*/
export class ViewCollection {
fieldAccessor;
expandState;
editState;
selectionState;
childrenLoaded = new EventEmitter();
dataLoaded = new EventEmitter();
resetPage = new EventEmitter();
total = 0;
totalVisible = 0;
loaded = new Map();
loading = false;
_totalRows = 0;
loadingCount = 0;
childrenSubscription;
_data;
static loadView(options) {
const subject = new BehaviorSubject(null);
const viewFields = () => Object.assign({
idGetter: identity,
pageable: false,
isVirtual: false,
skip: 0,
}, options.fields);
const childrenView = new ViewCollection(viewFields, options.expandState || EXPANDED_STATE, options.editState || NOT_EDITED_STATE, options.selectionState || UNSELECTED_STATE);
if (options.loaded) {
childrenView.loaded = options.loaded;
}
loadView(childrenView, subject);
return subject;
}
get totalRows() {
if (!this._data) {
this.loadData();
}
return this._totalRows;
}
get data() {
if (!this._data) {
this.loadData();
}
return this._data;
}
constructor(fieldAccessor, expandState, editState, selectionState) {
this.fieldAccessor = fieldAccessor;
this.expandState = expandState;
this.editState = editState;
this.selectionState = selectionState;
}
get length() {
return this.data.length;
}
get first() {
return this.data[0];
}
get firstItem() {
return this.find((item) => item.type === 'data');
}
get last() {
return this.data[this.data.length - 1];
}
at(index) {
return this.data[index];
}
itemIndex(item) {
const idGetter = this.fieldAccessor().idGetter;
return this.data.findIndex((i) => i.id === idGetter(item));
}
map(fn) {
return this.data.map(fn);
}
filter(fn) {
return this.data.filter(fn);
}
findIndex(fn) {
return this.data.findIndex(fn);
}
reduce(fn, init) {
return this.data.reduce(fn, init);
}
forEach(fn) {
this.data.forEach(fn);
}
some(fn) {
return this.data.some(fn);
}
find(fn) {
return this.data.find(fn);
}
toString() {
return this.data.toString();
}
updateSelectedState() {
this.forEach((item) => {
if (item.type === 'data') {
item.selected = this.selectionState.isRowSelected(item.data);
}
});
}
updateEditedState() {
this.forEach((item) => {
if (item.type === 'data') {
item.editContext = this.editState.context(item.data);
}
});
}
reset() {
this.loaded.clear();
this.clear();
this.unsubscribeChildren();
}
resetItem(item, resetChildren) {
const idGetter = this.fieldAccessor().idGetter;
const toReset = [item];
while (toReset.length) {
const current = toReset.shift();
const id = idGetter(current);
if (this.loaded.has(id)) {
const children = this.loaded.get(id);
this.loaded.delete(id);
if (resetChildren) {
toReset.push(...(children.data || children));
}
}
}
this.clear();
}
clear() {
this._data = null;
}
loadData() {
const itemFactory = new ViewItemFactory(this.expandState, this.editState, this.selectionState, this.loaded, this.fieldAccessor);
let result = itemFactory.generate();
if (!result.loading && result.total && (!result.items.length || !result.items.some((i) => i.type === 'data'))) {
this.resetPage.emit();
result = new ViewItemFactory(this.expandState, this.editState, this.selectionState, this.loaded, this.fieldAccessor).generate();
}
this._data = result.items;
this.total = result.total;
this.totalVisible = result.totalVisible;
this._totalRows = result.totalRows;
if (result.observables && result.observables.length) {
this.loading = true;
this.loadingCount += result.observables.length;
if (!this.childrenSubscription) {
this.childrenSubscription = new Subscription();
}
result.observables.forEach((o) => {
this.loaded.set(o.parentId, LOADING);
this.childrenSubscription.add(o.observable.subscribe((children) => {
// handle error, might show reload icon
this.clear();
this.loaded.set(o.parentId, children);
this.childrenLoaded.emit();
this.loadingCount--;
if (this.loadingCount === 0) {
this.loading = false;
this.unsubscribeChildren();
this.dataLoaded.emit();
}
}));
});
}
else {
this.dataLoaded.emit();
}
}
unsubscribeChildren() {
if (this.childrenSubscription) {
this.childrenSubscription.unsubscribe();
this.childrenSubscription = null;
this.loadingCount = 0;
}
}
}