@progress/kendo-angular-treeview
Version:
Kendo UI TreeView for Angular
142 lines (141 loc) • 7.99 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 { getter, setter } from '@progress/kendo-common';
import { take } from 'rxjs/operators';
import { DropPosition } from '../models';
import { expandDropTarget, collapseEmptyParent, updateMovedItemIndex } from '../drag-and-drop-utils';
import { incrementPageSize, copyPageSize, decrementPageSize } from '../../load-more/load-more-utils';
import { buildTreeIndex, getDataItem, isPresent } from '../../utils';
/**
* @hidden
*/
export class FlatEditingService {
flatBinding;
movedItemNewIndex;
constructor(flatBinding) {
this.flatBinding = flatBinding;
}
add({ sourceItem, destinationItem, dropPosition, sourceTree, destinationTree }) {
// shallow clone the item as not to mistake it for its 'older' version when the remove handler kicks in to splice the item at its old position
const clonedSourceDataItem = Object.assign({}, getDataItem(sourceItem));
if (dropPosition === DropPosition.Over) {
// expand the item that was dropped into
expandDropTarget(destinationItem, destinationTree);
const destinationItemId = getter(this.flatBinding.idField)(getDataItem(destinationItem));
setter(this.flatBinding.parentIdField)(clonedSourceDataItem, destinationItemId);
const lastChildNodeIndex = this.getLastVisibleChildNodeIndex(destinationTree, this.flatBinding.originalData, getDataItem(destinationItem));
// insert after the last visible child
const targetIndex = lastChildNodeIndex + 1;
this.flatBinding.originalData.splice(targetIndex, 0, clonedSourceDataItem);
// rebind the treeview data before searching for the focus target index
this.rebindData();
const focusTarget = this.fetchChildNodes(getDataItem(destinationItem), destinationTree).indexOf(clonedSourceDataItem);
this.movedItemNewIndex = buildTreeIndex(destinationItem.item.index, focusTarget);
}
else {
const shiftIndex = dropPosition === DropPosition.After ? 1 : 0;
const targetIndex = this.flatBinding.originalData.indexOf(getDataItem(destinationItem)) + shiftIndex;
this.flatBinding.originalData.splice(targetIndex, 0, clonedSourceDataItem);
const destinationItemParentId = getter(this.flatBinding.parentIdField)(getDataItem(destinationItem));
setter(this.flatBinding.parentIdField)(clonedSourceDataItem, destinationItemParentId);
// rebind the treeview data before searching for the focus target index
this.rebindData();
const parentIndex = destinationItem.parent ?
destinationItem.parent.item.index :
null;
const parentContainer = destinationItem.parent ?
this.fetchChildNodes(getDataItem(destinationItem.parent), destinationTree) :
destinationTree.nodes;
const focusTarget = parentContainer.indexOf(clonedSourceDataItem);
this.movedItemNewIndex = buildTreeIndex(parentIndex, focusTarget);
}
if (sourceTree !== destinationTree) {
this.addChildNodes(clonedSourceDataItem, sourceTree);
}
// increment the parent page size => an item is moved into it
const updatedParent = dropPosition === DropPosition.Over ? getDataItem(destinationItem) : getDataItem(destinationItem.parent);
incrementPageSize(destinationTree, updatedParent);
// the page sizes are stored by data-item reference => copy the old item ref page size to the new item reference
copyPageSize(destinationTree, getDataItem(sourceItem), clonedSourceDataItem);
// the source tree nodes are reloaded on `removeItem` - reload the destination tree nodes if the soruce and the destination tree are different
if (sourceTree !== destinationTree && !destinationTree.loadOnDemand) {
destinationTree.preloadChildNodes();
}
// if the source and destination trees are the same, focusing the moved item here will not have the desired effect
// as the `remove` handler has not yet kicked-in to remove the item from its old position
if (sourceTree !== destinationTree) {
// ensure the focus target is rendered and registered
destinationTree.changeDetectorRef.detectChanges();
destinationTree.focus(this.movedItemNewIndex);
}
}
remove({ sourceItem, sourceTree, destinationTree }) {
const sourceDataItem = getDataItem(sourceItem);
const sourceItemIndex = this.flatBinding.originalData.indexOf(sourceDataItem);
this.flatBinding.originalData.splice(sourceItemIndex, 1);
if (sourceTree !== destinationTree) {
this.removeChildNodes(sourceDataItem, sourceTree);
}
this.rebindData();
// emit collapse for the parent node if its last child node was spliced
const parentChildren = sourceItem.parent ? sourceItem.parent.children : [];
collapseEmptyParent(sourceItem.parent, parentChildren, sourceTree);
// decrement source item parent page size => an item has been removed from it
decrementPageSize(sourceTree, getDataItem(sourceItem.parent));
// reload the treeview nodes
if (!sourceTree.loadOnDemand) {
sourceTree.preloadChildNodes();
}
// if the source and destination trees are different we want to focus only the moved item in the destination tree
if (sourceTree === destinationTree) {
// ensure the focus target is rendered and registered
destinationTree.changeDetectorRef.detectChanges();
// after the source item is removed from its original position, the candidate index might have to be corrected
const index = updateMovedItemIndex(this.movedItemNewIndex, sourceItem.item.index);
destinationTree.focus(index);
}
}
addChildNodes(dataItem, source) {
const itemChildren = this.fetchAllDescendantNodes(dataItem, source);
this.flatBinding.originalData.push(...itemChildren);
}
removeChildNodes(dataItem, source) {
const sourceChildren = this.fetchAllDescendantNodes(dataItem, source);
sourceChildren.forEach(item => {
const index = this.flatBinding.originalData.indexOf(item);
this.flatBinding.originalData.splice(index, 1);
});
}
fetchAllDescendantNodes(node, treeview) {
let nodes = this.fetchChildNodes(node, treeview);
nodes.forEach(node => nodes = nodes.concat(this.fetchAllDescendantNodes(node, treeview) || []));
return nodes;
}
fetchChildNodes(node, treeview) {
if (!node) {
return [];
}
let nodes = [];
treeview
.children(node)
.pipe(take(1))
.subscribe(children => nodes = nodes.concat(children || []));
return nodes;
}
getLastVisibleChildNodeIndex(treeview, data, node) {
if (!isPresent(treeview.loadMoreService) || !treeview.hasChildren(node)) {
return data.length;
}
const visibleNodesCount = treeview.loadMoreService.getGroupSize(node);
const visibleChildren = this.fetchChildNodes(node, treeview).slice(0, visibleNodesCount);
const lastNode = visibleChildren[visibleChildren.length - 1];
const lastNodeIndex = data.indexOf(lastNode);
return lastNodeIndex;
}
rebindData() {
this.flatBinding.nodes = this.flatBinding.originalData;
this.flatBinding.updateNodes(this.flatBinding.originalData);
}
}