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.

469 lines (468 loc) 15.1 kB
/**----------------------------------------------------------------------------------------- * 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; } } }