mirror of https://github.com/opensumi/core
1883 lines
61 KiB
TypeScript
1883 lines
61 KiB
TypeScript
/* eslint-disable @typescript-eslint/prefer-for-of */
|
||
import {
|
||
CancellationToken,
|
||
CancellationTokenSource,
|
||
DisposableCollection,
|
||
Emitter,
|
||
Event,
|
||
ThrottledDelayer,
|
||
isUndefined,
|
||
path,
|
||
} from '@opensumi/ide-utils';
|
||
|
||
import {
|
||
IAccessibilityInformation,
|
||
ICompositeTreeNode,
|
||
IMetadataChange,
|
||
ITree,
|
||
ITreeNode,
|
||
ITreeNodeOrCompositeTreeNode,
|
||
ITreeWatcher,
|
||
IWatchTerminator,
|
||
IWatcherCallback,
|
||
IWatcherEvent,
|
||
IWatcherInfo,
|
||
MetadataChangeType,
|
||
TreeNodeEvent,
|
||
TreeNodeType,
|
||
TreeViewItemCheckboxInfo,
|
||
WatchEvent,
|
||
} from '../types';
|
||
|
||
const { Path } = path;
|
||
/**
|
||
* 裁剪数组
|
||
*
|
||
* @param arr 裁剪数组
|
||
* @param start 起始位置
|
||
* @param deleteCount 删除或替换位置
|
||
* @param items 插入的数组
|
||
*/
|
||
export function spliceArray(arr: number[], start: number, deleteCount = 0, items?: number[] | null) {
|
||
const a = arr.slice(0);
|
||
a.splice(start, deleteCount, ...(items || []));
|
||
return a;
|
||
}
|
||
|
||
export enum BranchOperatorStatus {
|
||
EXPANDED = 1,
|
||
SHRINKED,
|
||
}
|
||
|
||
export type TreeNodeOrCompositeTreeNode = TreeNode | CompositeTreeNode;
|
||
|
||
export interface IGlobalTreeState {
|
||
isExpanding: boolean;
|
||
isLoadingPath: boolean;
|
||
isRefreshing: boolean;
|
||
refreshCancelToken: CancellationTokenSource;
|
||
loadPathCancelToken: CancellationTokenSource;
|
||
}
|
||
|
||
export interface IOptionalGlobalTreeState {
|
||
isExpanding?: boolean;
|
||
isLoadingPath?: boolean;
|
||
isRefreshing?: boolean;
|
||
refreshCancelToken?: CancellationTokenSource;
|
||
loadPathCancelToken?: CancellationTokenSource;
|
||
}
|
||
|
||
export class TreeNode implements ITreeNode {
|
||
public static nextId = (() => {
|
||
let id = 0;
|
||
return () => id++;
|
||
})();
|
||
|
||
public static is(node: any): node is ITreeNode {
|
||
return !!node && 'depth' in node && 'name' in node && 'path' in node && 'id' in node;
|
||
}
|
||
|
||
public static getTreeNodeById(id: number) {
|
||
return TreeNode.idToTreeNode.get(id);
|
||
}
|
||
|
||
public static getTreeNodeByPath(path: string) {
|
||
return TreeNode.pathToTreeNode.get(path);
|
||
}
|
||
|
||
public static setTreeNode(id: number, path: string, node: TreeNode) {
|
||
TreeNode.idToTreeNode.set(id, node);
|
||
TreeNode.pathToTreeNode.set(path, node);
|
||
}
|
||
|
||
public static removeTreeNode(id: number, path: string) {
|
||
TreeNode.idToTreeNode.delete(id);
|
||
TreeNode.pathToTreeNode.delete(path);
|
||
}
|
||
|
||
public static getIdByPath(path: string) {
|
||
return TreeNode.pathToId.get(path);
|
||
}
|
||
|
||
public static setIdByPath(path: string, id: number) {
|
||
return TreeNode.pathToId.set(path, id);
|
||
}
|
||
|
||
public static getGlobalTreeState(path: string) {
|
||
let state = TreeNode.pathToGlobalTreeState.get(path);
|
||
if (!state) {
|
||
state = {
|
||
isExpanding: false,
|
||
isLoadingPath: false,
|
||
isRefreshing: false,
|
||
refreshCancelToken: new CancellationTokenSource(),
|
||
loadPathCancelToken: new CancellationTokenSource(),
|
||
};
|
||
}
|
||
return state;
|
||
}
|
||
|
||
public static setGlobalTreeState(path: string, updateState: IOptionalGlobalTreeState) {
|
||
let state = TreeNode.pathToGlobalTreeState.get(path);
|
||
if (!state) {
|
||
state = {
|
||
isExpanding: false,
|
||
isLoadingPath: false,
|
||
isRefreshing: false,
|
||
refreshCancelToken: new CancellationTokenSource(),
|
||
loadPathCancelToken: new CancellationTokenSource(),
|
||
};
|
||
}
|
||
state = {
|
||
...state,
|
||
...updateState,
|
||
};
|
||
TreeNode.pathToGlobalTreeState.set(path, state);
|
||
return state;
|
||
}
|
||
|
||
public static idToTreeNode: Map<number, ITreeNodeOrCompositeTreeNode> = new Map();
|
||
public static pathToTreeNode: Map<string, ITreeNodeOrCompositeTreeNode> = new Map();
|
||
public static pathToId: Map<string, number> = new Map();
|
||
// 每颗树都只会在根节点上绑定一个可取消的对象,即同个时间点只能存在一个改变树数据结构的事情
|
||
private static pathToGlobalTreeState: Map<string, IGlobalTreeState> = new Map();
|
||
|
||
private _parent: ICompositeTreeNode | undefined;
|
||
|
||
private _metadata: {
|
||
[key: string]: any;
|
||
};
|
||
|
||
private _uid: number;
|
||
private _disposed: boolean;
|
||
|
||
protected _depth: number;
|
||
protected _watcher: ITreeWatcher;
|
||
protected _tree: ITree;
|
||
protected _visible: boolean;
|
||
protected _path: string;
|
||
|
||
protected constructor(
|
||
tree: ITree,
|
||
parent?: ICompositeTreeNode,
|
||
watcher?: ITreeWatcher,
|
||
optionalMetadata?: { [key: string]: any },
|
||
) {
|
||
this._uid = TreeNode.nextId();
|
||
this._parent = parent;
|
||
this._tree = tree;
|
||
this._disposed = false;
|
||
this._visible = true;
|
||
this._metadata = { ...(optionalMetadata || {}) };
|
||
this._depth = parent ? parent.depth + 1 : 0;
|
||
if (watcher) {
|
||
this._watcher = watcher;
|
||
} else if (parent) {
|
||
this._watcher = (parent as any).watcher;
|
||
}
|
||
}
|
||
|
||
get disposed() {
|
||
return this._disposed;
|
||
}
|
||
|
||
/**
|
||
* 获取基础信息
|
||
*/
|
||
get depth() {
|
||
return this._depth;
|
||
}
|
||
|
||
get parent() {
|
||
return this._parent;
|
||
}
|
||
|
||
set parent(node: ICompositeTreeNode | undefined) {
|
||
this._parent = node;
|
||
// 节点 `parent` 变化,更新当前节点的 `path` 属性
|
||
this._path = '';
|
||
}
|
||
|
||
get type() {
|
||
return TreeNodeType.TreeNode;
|
||
}
|
||
|
||
get id() {
|
||
return this._uid;
|
||
}
|
||
|
||
set id(id: number) {
|
||
this._uid = id;
|
||
}
|
||
|
||
get displayName() {
|
||
return this.name;
|
||
}
|
||
|
||
/**
|
||
* 由于 Tree 对于唯一路径的 path 的依赖
|
||
* 在传入 `name` 值时必须保证其在路径上的唯一性
|
||
* 一般不建议手动管理 `name`,采用默认值即可
|
||
*/
|
||
get name() {
|
||
if (!this.parent) {
|
||
return `root_${this.id}`;
|
||
}
|
||
return this.getMetadata('name') || String(this.id);
|
||
}
|
||
|
||
set name(name: string) {
|
||
this.addMetadata('name', name);
|
||
// 节点 `name` 变化,更新当前节点的 `path` 属性
|
||
this._path = '';
|
||
}
|
||
|
||
// 节点绝对路径
|
||
get path(): string {
|
||
if (!this._path) {
|
||
if (!this.parent) {
|
||
this._path = this.name.startsWith(Path.separator) ? this.name : `${Path.separator}${this.name}`;
|
||
} else {
|
||
this._path = this.name.startsWith(Path.separator)
|
||
? `${this.parent.path}${this.name}`
|
||
: `${this.parent.path}${Path.separator}${this.name}`;
|
||
}
|
||
}
|
||
return this._path;
|
||
}
|
||
|
||
get checkboxInfo(): TreeViewItemCheckboxInfo | undefined {
|
||
return {
|
||
checked: false,
|
||
tooltip: '',
|
||
accessibilityInformation: this.accessibilityInformation,
|
||
};
|
||
}
|
||
|
||
get accessibilityInformation(): IAccessibilityInformation {
|
||
return {
|
||
label: this.name,
|
||
role: 'treeitem',
|
||
};
|
||
}
|
||
|
||
public getMetadata(withKey: string): any {
|
||
if (withKey === 'name' && !this._metadata[withKey]) {
|
||
this._metadata[withKey] = String(TreeNode.nextId());
|
||
}
|
||
return this._metadata[withKey];
|
||
}
|
||
|
||
public addMetadata(withKey: string, value: any) {
|
||
if (!(withKey in this._metadata)) {
|
||
this._metadata[withKey] = value;
|
||
this._watcher.notifyDidChangeMetadata(this, {
|
||
type: MetadataChangeType.Added,
|
||
key: withKey,
|
||
prevValue: void 0,
|
||
value,
|
||
});
|
||
} else {
|
||
const prevValue = this._metadata[withKey];
|
||
this._metadata[withKey] = value;
|
||
this._watcher.notifyDidChangeMetadata(this, { type: MetadataChangeType.Updated, key: withKey, prevValue, value });
|
||
}
|
||
}
|
||
|
||
public removeMetadata(withKey: string) {
|
||
if (withKey in this._metadata) {
|
||
const prevValue = this._metadata[withKey];
|
||
delete this._metadata[withKey];
|
||
this._watcher.notifyDidChangeMetadata(this, {
|
||
type: MetadataChangeType.Removed,
|
||
key: withKey,
|
||
prevValue,
|
||
value: void 0,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 这里的move操作可能为移动,也可能为重命名
|
||
*
|
||
* @param {ICompositeTreeNode} to
|
||
* @param {string} [name=this.name]
|
||
* @returns
|
||
* @memberof TreeNode
|
||
*/
|
||
public mv(to: ICompositeTreeNode | null, name: string = this.name) {
|
||
// 一个普通节点必含有父节点,根节点不允许任何操作
|
||
const prevParent = this._parent as CompositeTreeNode;
|
||
if (to === null || !CompositeTreeNode.is(to)) {
|
||
this.parent = undefined;
|
||
this.dispose();
|
||
return;
|
||
}
|
||
const didChangeParent = prevParent !== to;
|
||
const prevPath = this.path;
|
||
|
||
this._depth = to.depth + 1;
|
||
|
||
if (didChangeParent || name !== this.name) {
|
||
this.name = name;
|
||
if (didChangeParent) {
|
||
this._watcher.notifyWillChangeParent(this, prevParent, to);
|
||
}
|
||
if (this.parent) {
|
||
(this.parent as CompositeTreeNode).unlinkItem(this, true);
|
||
this.parent = to;
|
||
(this.parent as CompositeTreeNode).insertItem(this);
|
||
}
|
||
if (didChangeParent) {
|
||
this._watcher.notifyDidChangeParent(this, prevParent, to);
|
||
}
|
||
}
|
||
|
||
if (this.path !== prevPath) {
|
||
this._watcher.notifyDidChangePath(this);
|
||
}
|
||
}
|
||
|
||
public get visible(): boolean {
|
||
return this._visible;
|
||
}
|
||
|
||
public setVisible(b: boolean): this {
|
||
this._visible = b;
|
||
return this;
|
||
}
|
||
|
||
public dispose() {
|
||
if (this._disposed) {
|
||
return;
|
||
}
|
||
|
||
TreeNode.removeTreeNode(this.id, this.path);
|
||
this._disposed = true;
|
||
this._watcher.notifyDidDispose(this);
|
||
}
|
||
}
|
||
|
||
export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
||
private static defaultSortComparator(a: ITreeNodeOrCompositeTreeNode, b: ITreeNodeOrCompositeTreeNode): number {
|
||
if (a.constructor === b.constructor) {
|
||
return a.name > b.name ? 1 : a.name < b.name ? -1 : 0;
|
||
}
|
||
return CompositeTreeNode.is(a) ? -1 : CompositeTreeNode.is(b) ? 1 : 0;
|
||
}
|
||
|
||
public static is(node: any): node is ICompositeTreeNode {
|
||
return !!node && 'children' in node;
|
||
}
|
||
|
||
public static isRoot(node: any): boolean {
|
||
return CompositeTreeNode.is(node) && !node.parent;
|
||
}
|
||
|
||
private static REFRESH_DELAY = 200;
|
||
|
||
protected _children: ITreeNodeOrCompositeTreeNode[] | null = null;
|
||
// 节点的分支数量
|
||
private _branchSize: number;
|
||
private _flattenedBranch: number[] | null;
|
||
|
||
private _lock = false;
|
||
private _root: ICompositeTreeNode | null;
|
||
|
||
private watchTerminator: (path: string) => void;
|
||
|
||
public watchEvents: Map<string, IWatcherInfo>;
|
||
public isExpanded: boolean;
|
||
|
||
private refreshThrottler: ThrottledDelayer<void> = new ThrottledDelayer(CompositeTreeNode.REFRESH_DELAY);
|
||
private toRefreshPathQueue = new Set<string>();
|
||
|
||
protected generatorWatcher() {
|
||
const emitter = new Emitter<any>();
|
||
const onEventChanges: Event<any> = emitter.event;
|
||
const disposeCollection = new DisposableCollection();
|
||
const terminateWatch = (path: string) => {
|
||
this.watchEvents.delete(path);
|
||
};
|
||
const watcher: ITreeWatcher = {
|
||
notifyWillProcessWatchEvent: (target: ICompositeTreeNode, event: IWatcherEvent) => {
|
||
emitter.fire({ type: TreeNodeEvent.WillProcessWatchEvent, args: [target, event] });
|
||
},
|
||
notifyWillChangeParent: (
|
||
target: ITreeNodeOrCompositeTreeNode,
|
||
prevParent: ICompositeTreeNode,
|
||
newParent: ICompositeTreeNode,
|
||
) => {
|
||
emitter.fire({ type: TreeNodeEvent.WillChangeParent, args: [target, prevParent, newParent] });
|
||
},
|
||
notifyDidChangeParent: (
|
||
target: ITreeNodeOrCompositeTreeNode,
|
||
prevParent: ICompositeTreeNode,
|
||
newParent: ICompositeTreeNode,
|
||
) => {
|
||
emitter.fire({ type: TreeNodeEvent.DidChangeParent, args: [target, prevParent, newParent] });
|
||
},
|
||
notifyDidDispose: (target: ITreeNodeOrCompositeTreeNode) => {
|
||
emitter.fire({ type: TreeNodeEvent.DidDispose, args: [target] });
|
||
},
|
||
notifyDidProcessWatchEvent: (target: ICompositeTreeNode, event: IWatcherEvent) => {
|
||
emitter.fire({ type: TreeNodeEvent.DidProcessWatchEvent, args: [target, event] });
|
||
},
|
||
notifyDidChangePath: (target: ITreeNodeOrCompositeTreeNode) => {
|
||
emitter.fire({ type: TreeNodeEvent.DidChangePath, args: [target] });
|
||
},
|
||
notifyWillChangeExpansionState: (target: ICompositeTreeNode, nowExpanded: boolean) => {
|
||
const isVisible = this.isItemVisibleAtSurface(target);
|
||
emitter.fire({ type: TreeNodeEvent.WillChangeExpansionState, args: [target, nowExpanded, isVisible] });
|
||
},
|
||
notifyDidChangeExpansionState: (target: ICompositeTreeNode, nowExpanded: boolean) => {
|
||
const isVisible = this.isItemVisibleAtSurface(target);
|
||
emitter.fire({ type: TreeNodeEvent.DidChangeExpansionState, args: [target, nowExpanded, isVisible] });
|
||
},
|
||
notifyDidChangeMetadata: (target: ITreeNode | ICompositeTreeNode, change: IMetadataChange) => {
|
||
emitter.fire({ type: TreeNodeEvent.DidChangeMetadata, args: [target, change] });
|
||
},
|
||
notifyDidUpdateBranch: () => {
|
||
emitter.fire({ type: TreeNodeEvent.BranchDidUpdate, args: [] });
|
||
},
|
||
notifyWillResolveChildren: (target: ICompositeTreeNode, nowExpanded: boolean) => {
|
||
emitter.fire({ type: TreeNodeEvent.WillResolveChildren, args: [target, nowExpanded] });
|
||
},
|
||
notifyDidResolveChildren: (target: ICompositeTreeNode, nowExpanded: boolean) => {
|
||
emitter.fire({ type: TreeNodeEvent.DidResolveChildren, args: [target, nowExpanded] });
|
||
},
|
||
// 监听所有事件
|
||
on: (event: TreeNodeEvent, callback: any) => {
|
||
const dispose = onEventChanges((data) => {
|
||
if (data.type === event) {
|
||
callback(...data.args);
|
||
}
|
||
});
|
||
disposeCollection.push(dispose);
|
||
return dispose;
|
||
},
|
||
// 监听Watch事件变化
|
||
onWatchEvent: (path: string, callback: IWatcherCallback): IWatchTerminator => {
|
||
const terminator: IWatchTerminator = terminateWatch;
|
||
this.watchEvents.set(path, { terminator, callback });
|
||
return terminator;
|
||
},
|
||
dispose: disposeCollection,
|
||
};
|
||
return watcher;
|
||
}
|
||
|
||
// parent 为undefined即表示该节点为根节点
|
||
constructor(
|
||
tree: ITree,
|
||
parent?: ICompositeTreeNode,
|
||
watcher?: ITreeWatcher,
|
||
optionalMetadata?: { [key: string]: any },
|
||
) {
|
||
super(tree, parent, watcher, optionalMetadata);
|
||
this.isExpanded = parent ? false : true;
|
||
this._branchSize = 0;
|
||
if (!parent) {
|
||
this.watchEvents = new Map();
|
||
// 为根节点创建监听器
|
||
this._watcher = this.generatorWatcher();
|
||
this._root = this;
|
||
TreeNode.setTreeNode(this.id, this.path, this);
|
||
} else {
|
||
this._watcher = (parent as any).watcher;
|
||
}
|
||
}
|
||
|
||
// 重载 name 的 getter/setter,路径改变时需要重新监听文件节点变化
|
||
set name(name: string) {
|
||
const prevPath = this.path;
|
||
if (!CompositeTreeNode.isRoot(this) && typeof this.watchTerminator === 'function') {
|
||
this.watchTerminator(prevPath);
|
||
this.addMetadata('name', name);
|
||
this.watchTerminator = this.watcher.onWatchEvent(this.path, this.handleWatchEvent);
|
||
} else {
|
||
this.addMetadata('name', name);
|
||
}
|
||
// 节点 `name` 变化,更新当前节点的 `path` 属性
|
||
this._path = '';
|
||
}
|
||
|
||
get name() {
|
||
// 根节点保证路径不重复
|
||
if (!this.parent) {
|
||
return `root_${this.id}`;
|
||
}
|
||
return this.getMetadata('name');
|
||
}
|
||
|
||
// 作为根节点唯一的watcher需要在生成新节点的时候传入
|
||
get watcher() {
|
||
return this._watcher;
|
||
}
|
||
|
||
get type() {
|
||
return TreeNodeType.CompositeTreeNode;
|
||
}
|
||
|
||
get children() {
|
||
return this._children;
|
||
}
|
||
|
||
get expanded() {
|
||
return this.isExpanded;
|
||
}
|
||
|
||
/**
|
||
* 当前可见的分支数量
|
||
*
|
||
* 当节点为展开状态时,其整个分支(递归展平)由上一级的分支(根(位于数据层可见)或处于折叠状态的分支)拥有
|
||
* 当节点为折叠状态时,其整个分支(递归展平)由其上一级父目录展开该节点
|
||
*
|
||
* @readonly
|
||
* @memberof CompositeTreeNode
|
||
*/
|
||
get branchSize() {
|
||
return this._branchSize;
|
||
}
|
||
|
||
/**
|
||
* 获取当前节点的分支数,一般为顶层节点,如Root上获取
|
||
*
|
||
* @readonly
|
||
* @memberof CompositeTreeNode
|
||
*/
|
||
get flattenedBranch() {
|
||
return this._flattenedBranch;
|
||
}
|
||
|
||
get lock() {
|
||
return this._lock;
|
||
}
|
||
|
||
get root() {
|
||
if (isUndefined(this._root)) {
|
||
this._root = this.getRoot() || null;
|
||
}
|
||
return this._root;
|
||
}
|
||
|
||
private getRoot() {
|
||
let root = this.parent;
|
||
while (root && root.parent) {
|
||
root = root.parent;
|
||
}
|
||
return root;
|
||
}
|
||
|
||
/**
|
||
* 确保此“目录”的子级已加载(不影响“展开”状态)
|
||
* 如果子级已经加载,则返回的Promise将立即解决
|
||
* 否则,将发出重新加载请求并返回Promise
|
||
* 一旦返回的Promise.resolve,“CompositeTreeNode#children” 便可以访问到对于节点
|
||
*/
|
||
public async ensureLoaded(token?: CancellationToken) {
|
||
if (this._children) {
|
||
return;
|
||
}
|
||
return await this.hardReloadChildren(token);
|
||
}
|
||
|
||
// 展开节点
|
||
public async setExpanded(ensureVisible = true, quiet = false, isOwner = true, token?: CancellationToken) {
|
||
if (this.disposed) {
|
||
return;
|
||
}
|
||
// 根节点不可折叠
|
||
if (CompositeTreeNode.isRoot(this)) {
|
||
return;
|
||
}
|
||
if (this.isExpanded) {
|
||
return;
|
||
}
|
||
if (isOwner) {
|
||
const state = TreeNode.getGlobalTreeState(this.path);
|
||
state.loadPathCancelToken.cancel();
|
||
state.refreshCancelToken.cancel();
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isExpanding: true,
|
||
});
|
||
}
|
||
this.isExpanded = true;
|
||
if (this._children === null) {
|
||
this._watcher.notifyWillResolveChildren(this, this.isExpanded);
|
||
await this.hardReloadChildren(token);
|
||
this._watcher.notifyDidResolveChildren(this, this.isExpanded);
|
||
// 检查其是否展开;可能同时执行了 setCollapsed 方法
|
||
if (!this.isExpanded || token?.isCancellationRequested) {
|
||
if (isOwner) {
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isExpanding: false,
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (ensureVisible && this.parent && CompositeTreeNode.is(this.parent)) {
|
||
/**
|
||
* 在传入 ensureVisible = true 时,这里传入的 token 不能取消所有副作用
|
||
* 故在使用 ensureVisible = true 时必须保证 `setExpanded` 与 `setCollapsed` 的独立性
|
||
* 如需要 `await node.setExpanded(true)` 后再执行 `node.setCollapsed()`
|
||
*/
|
||
await (this.parent as CompositeTreeNode).setExpanded(true, !quiet, false, token);
|
||
}
|
||
|
||
if (token?.isCancellationRequested) {
|
||
if (isOwner) {
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isExpanding: false,
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (this.isExpanded) {
|
||
this._watcher.notifyWillChangeExpansionState(this, true);
|
||
// 与根节点合并分支
|
||
this.expandBranch(this, quiet);
|
||
this._watcher.notifyDidChangeExpansionState(this, true);
|
||
}
|
||
if (isOwner) {
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isExpanding: false,
|
||
});
|
||
TreeNode.setTreeNode(this.id, this.path, this);
|
||
}
|
||
}
|
||
|
||
// 获取当前节点下所有展开的节点路径
|
||
private getAllExpandedNodePath() {
|
||
const paths: string[] = [];
|
||
let start = 0;
|
||
if (!CompositeTreeNode.isRoot(this)) {
|
||
// 找到节点位置下标,向下进一步查找展开目录
|
||
start = (this.root as CompositeTreeNode)?.getIndexAtTreeNodeId(this.id) + 1;
|
||
}
|
||
const end = start + this.branchSize;
|
||
for (let i = start; i < end; i++) {
|
||
const node = (this.root as CompositeTreeNode)?.getTreeNodeAtIndex(i);
|
||
if (CompositeTreeNode.is(node) && (node as CompositeTreeNode).expanded) {
|
||
paths.push(node.path);
|
||
}
|
||
}
|
||
return paths;
|
||
}
|
||
|
||
// 获取当前节点下所有折叠的节点路径
|
||
private getAllCollapsedNodePath() {
|
||
let paths: string[] = [];
|
||
if (this.children) {
|
||
for (let i = 0; i < this.children.length; i++) {
|
||
const child = this.children[i];
|
||
if (!CompositeTreeNode.is(child)) {
|
||
continue;
|
||
}
|
||
if ((child as CompositeTreeNode).isExpanded) {
|
||
paths = paths.concat((child as CompositeTreeNode).getAllCollapsedNodePath());
|
||
} else {
|
||
paths.push(child.path);
|
||
}
|
||
}
|
||
return paths;
|
||
} else {
|
||
return paths;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理节点数据,让节点重新加载子节点及初始化 flattenedBranch
|
||
* @param token CancellationToken
|
||
*/
|
||
private async resolveChildrens(token?: CancellationToken) {
|
||
let childrens = this.children;
|
||
let expandedPaths: string[] = [];
|
||
try {
|
||
childrens = (await this._tree.resolveChildren(this)) || [];
|
||
} catch (e) {
|
||
childrens = [];
|
||
}
|
||
|
||
if (token?.isCancellationRequested) {
|
||
return false;
|
||
}
|
||
|
||
const flatTree = new Array(childrens.length);
|
||
this._children = Array(childrens.length);
|
||
for (let i = 0; i < childrens.length; i++) {
|
||
const child = childrens[i];
|
||
// 如果存在上一次缓存的节点,则使用缓存节点的 ID
|
||
(child as TreeNode).id = TreeNode.getIdByPath(child.path) || (child as TreeNode).id;
|
||
this._children[i] = child;
|
||
TreeNode.setIdByPath(child.path, child.id);
|
||
}
|
||
|
||
this._children?.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);
|
||
|
||
for (let i = 0; i < childrens.length; i++) {
|
||
flatTree[i] = this._children[i].id;
|
||
}
|
||
const expandedChilds: CompositeTreeNode[] = [];
|
||
for (let i = 0, len = this.children?.length || 0; i < len; i++) {
|
||
const subChild = this.children?.[i];
|
||
if (CompositeTreeNode.is(subChild) && subChild.expanded) {
|
||
const paths = await (subChild as CompositeTreeNode).resolveChildrens(token);
|
||
if (paths) {
|
||
expandedPaths = expandedPaths.concat(paths);
|
||
}
|
||
if (token?.isCancellationRequested) {
|
||
return;
|
||
}
|
||
expandedChilds.push(subChild as CompositeTreeNode);
|
||
}
|
||
}
|
||
this._branchSize = flatTree.length;
|
||
this.setFlattenedBranch(flatTree);
|
||
for (let i = 0; i < expandedChilds.length; i++) {
|
||
const child = expandedChilds[i];
|
||
child.expandBranch(child, true);
|
||
}
|
||
return expandedPaths.concat(expandedChilds.map((child) => child.path.toString()));
|
||
}
|
||
|
||
private updateTreeNodeCache(child: CompositeTreeNode | TreeNode) {
|
||
TreeNode.setTreeNode(child.id, child.path, child);
|
||
if (CompositeTreeNode.is(child) && child.expanded && child.children?.length) {
|
||
for (let i = 0; i < child.children.length; i++) {
|
||
const subChild = child.children[i];
|
||
this.updateTreeNodeCache(subChild as TreeNode | CompositeTreeNode);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 静默刷新子节点, 即不触发分支更新事件
|
||
* @param toExpandPaths 待展开的路径
|
||
* @param token CancellationToken
|
||
* @param origin 当 this === origin 时,说明此节点为调用的源头节点
|
||
*/
|
||
private async refreshTreeNodeByPaths(
|
||
toExpandPaths: string[] = this.getAllExpandedNodePath(),
|
||
token?: CancellationToken,
|
||
origin?: CompositeTreeNode,
|
||
): Promise<string[] | void> {
|
||
if (!CompositeTreeNode.is(this)) {
|
||
return;
|
||
}
|
||
// 如果某次刷新操作被取消,则下次刷新依旧使用上一次刷新的展开目录进行刷新
|
||
let toExpandPath;
|
||
const originChildren = this.children;
|
||
let childrens = this.children || [];
|
||
if (this.expanded) {
|
||
if (this === origin) {
|
||
try {
|
||
childrens = (await this._tree.resolveChildren(this)) || [];
|
||
} catch (e) {
|
||
childrens = [];
|
||
}
|
||
|
||
if (token?.isCancellationRequested) {
|
||
return;
|
||
}
|
||
if (!this.expanded) {
|
||
// 当请求刷新节点时,如果该节点已经不应该被处理,则清理 Children
|
||
// 下次再被展开时便会自动更新 Children 最新内容
|
||
if (this.children) {
|
||
// 清理子节点,等待下次展开时更新
|
||
if (!!this.children && this.parent) {
|
||
for (let i = 0; i < this.children.length; i++) {
|
||
const child = this.children[i];
|
||
(child as CompositeTreeNode).dispose();
|
||
}
|
||
this._children = null;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
while ((toExpandPath = toExpandPaths.shift())) {
|
||
const isRelative = toExpandPath.indexOf(`${this.path}${Path.separator}`) > -1;
|
||
if (!isRelative) {
|
||
if (toExpandPath === this.path) {
|
||
toExpandPath = undefined;
|
||
}
|
||
break;
|
||
}
|
||
const child = childrens?.find((child) => child.path === toExpandPath);
|
||
// 对于压缩情况的路径需要额外处理一下
|
||
// 如果这里加载的路径是 a/b/c, 有可能目前只加载到 a/b
|
||
if (!child) {
|
||
if (!childrens || childrens.length === 0) {
|
||
break;
|
||
}
|
||
for (let i = 0; i < childrens.length; i++) {
|
||
const child = childrens[i];
|
||
const isInclude = toExpandPath.indexOf(`${child.path}${Path.separator}`) === 0; // 展开路径包含子节点路径
|
||
if (isInclude && CompositeTreeNode.is(child)) {
|
||
// 包含压缩节点的情况
|
||
if (!CompositeTreeNode.is(child)) {
|
||
// 说明此节点为非折叠节点时不处理
|
||
continue;
|
||
}
|
||
(child as CompositeTreeNode).isExpanded = true;
|
||
// 加载路径包含当前判断路径,尝试加载该节点再匹配
|
||
const extraExpandedPaths = await (child as CompositeTreeNode).resolveChildrens(token);
|
||
if (token?.isCancellationRequested) {
|
||
return;
|
||
}
|
||
if (extraExpandedPaths) {
|
||
toExpandPaths = toExpandPaths.filter((path) => !extraExpandedPaths.find((a) => a === path));
|
||
}
|
||
if (child.path !== toExpandPath && !toExpandPaths.includes(child.path)) {
|
||
toExpandPaths.unshift(toExpandPath);
|
||
}
|
||
if (toExpandPaths.length > 0) {
|
||
// 不需要重新加载压缩节点的子节点内容
|
||
toExpandPaths =
|
||
(await (child as CompositeTreeNode).refreshTreeNodeByPaths([...toExpandPaths], token, origin)) || [];
|
||
if (token?.isCancellationRequested) {
|
||
return;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
} else if (CompositeTreeNode.is(child)) {
|
||
// 如果节点默认展开,则忽略后续操作
|
||
if (!(child as CompositeTreeNode).expanded) {
|
||
(child as CompositeTreeNode).isExpanded = true;
|
||
const extraExpandedPaths = await (child as CompositeTreeNode).resolveChildrens(token);
|
||
if (token?.isCancellationRequested) {
|
||
return;
|
||
}
|
||
if (extraExpandedPaths) {
|
||
toExpandPaths = toExpandPaths.filter((path) => !extraExpandedPaths.find((a) => a.includes(path)));
|
||
}
|
||
if (toExpandPaths.length > 0 && !token?.isCancellationRequested) {
|
||
toExpandPaths =
|
||
(await (child as CompositeTreeNode).refreshTreeNodeByPaths([...toExpandPaths], token, origin)) || [];
|
||
if (token?.isCancellationRequested) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (toExpandPath) {
|
||
// 仍然存在需要进一步处理的待展开路径
|
||
toExpandPaths.unshift(toExpandPath);
|
||
if (this === origin) {
|
||
// 说明待展开的路径已经不存在,直接处理子节点
|
||
if (originChildren) {
|
||
this.shrinkBranch(this, true);
|
||
|
||
for (let i = 0; i < originChildren.length; i++) {
|
||
const child = originChildren[i];
|
||
child?.dispose();
|
||
}
|
||
}
|
||
const expandedChilds: CompositeTreeNode[] = [];
|
||
|
||
const flatTree = new Array(childrens.length);
|
||
this._children = Array(childrens.length);
|
||
for (let i = 0; i < childrens.length; i++) {
|
||
const child = childrens[i];
|
||
// 如果存在上一次缓存的节点,则使用缓存节点的 ID
|
||
(child as TreeNode).id = TreeNode.getIdByPath(child.path) || (child as TreeNode).id;
|
||
this._children[i] = child;
|
||
TreeNode.setIdByPath(child.path, child.id);
|
||
if (CompositeTreeNode.is(child) && child.expanded) {
|
||
expandedChilds.push(child as CompositeTreeNode);
|
||
}
|
||
}
|
||
this._children.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);
|
||
for (let i = 0; i < childrens.length; i++) {
|
||
flatTree[i] = this._children[i].id;
|
||
}
|
||
|
||
this._branchSize = flatTree.length;
|
||
this.setFlattenedBranch(flatTree, true);
|
||
this.watcher.notifyDidUpdateBranch();
|
||
}
|
||
if (this.parent !== origin) {
|
||
// 将所有子节点合并至第二层 Children 上,减少后续递归拼接带来额外成本
|
||
this.expandBranch(this, true);
|
||
}
|
||
return toExpandPaths;
|
||
} else if (CompositeTreeNode.isRoot(this)) {
|
||
if (this.children) {
|
||
this.shrinkBranch(this, true);
|
||
for (let i = 0; i < this.children.length; i++) {
|
||
const child = this.children[i];
|
||
child?.dispose();
|
||
}
|
||
}
|
||
const expandedChilds: CompositeTreeNode[] = [];
|
||
const otherChilds: CompositeTreeNode[] = [];
|
||
|
||
const flatTree = new Array(childrens.length);
|
||
this._children = Array(childrens.length);
|
||
for (let i = 0; i < childrens.length; i++) {
|
||
const child = childrens[i];
|
||
// 如果存在上一次缓存的节点,则使用缓存节点的 ID
|
||
(child as TreeNode).id = TreeNode.getIdByPath(child.path) || (child as TreeNode).id;
|
||
this._children[i] = child;
|
||
TreeNode.setIdByPath(child.path, child.id);
|
||
if (CompositeTreeNode.is(child) && child.expanded) {
|
||
if (!child.children) {
|
||
await (child as CompositeTreeNode).resolveChildrens(token);
|
||
if (token?.isCancellationRequested) {
|
||
return;
|
||
}
|
||
}
|
||
expandedChilds.push(child as CompositeTreeNode);
|
||
} else {
|
||
otherChilds.push(child as CompositeTreeNode);
|
||
}
|
||
}
|
||
|
||
this._children.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);
|
||
|
||
for (let i = 0; i < childrens.length; i++) {
|
||
flatTree[i] = this._children[i].id;
|
||
}
|
||
|
||
this._branchSize = flatTree.length;
|
||
this.setFlattenedBranch(flatTree, true);
|
||
for (let i = 0; i < expandedChilds.length; i++) {
|
||
const child = expandedChilds[i];
|
||
child.expandBranch(child, true);
|
||
this.updateTreeNodeCache(child);
|
||
}
|
||
for (let i = 0; i < otherChilds.length; i++) {
|
||
const child = otherChilds[i];
|
||
this.updateTreeNodeCache(child);
|
||
}
|
||
// 清理上一次监听函数
|
||
if (typeof this.watchTerminator === 'function') {
|
||
this.watchTerminator(this.path);
|
||
}
|
||
this.watchTerminator = this.watcher.onWatchEvent(this.path, this.handleWatchEvent);
|
||
this.watcher.notifyDidUpdateBranch();
|
||
} else {
|
||
// 非根节点刷新的情况
|
||
const expandedChilds: CompositeTreeNode[] = [];
|
||
|
||
if (this === origin) {
|
||
// 通知节点更新
|
||
if (this.children) {
|
||
// 重置旧的节点分支
|
||
this.shrinkBranch(this, true);
|
||
}
|
||
if (this.children) {
|
||
for (let i = 0, len = this.children.length; i < len; i++) {
|
||
const child = this.children[i];
|
||
(child as TreeNode).dispose();
|
||
}
|
||
}
|
||
const flatTree = new Array(childrens.length);
|
||
this._children = Array(childrens.length);
|
||
|
||
for (let i = 0, len = childrens.length; i < len; i++) {
|
||
const child = childrens[i];
|
||
(child as TreeNode).id = TreeNode.getIdByPath(child.path) || (child as TreeNode).id;
|
||
this._children[i] = child;
|
||
TreeNode.setIdByPath(child.path, child.id);
|
||
if (CompositeTreeNode.is(child) && (child as CompositeTreeNode).expanded) {
|
||
expandedChilds.push(child as CompositeTreeNode);
|
||
}
|
||
this.updateTreeNodeCache(child as TreeNode | CompositeTreeNode);
|
||
}
|
||
|
||
this._children.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);
|
||
|
||
for (let i = 0; i < childrens.length; i++) {
|
||
flatTree[i] = this._children[i].id;
|
||
}
|
||
|
||
this._branchSize = flatTree.length;
|
||
this.setFlattenedBranch(flatTree);
|
||
for (let i = 0; i < expandedChilds.length; i++) {
|
||
const child = expandedChilds[i];
|
||
child.expandBranch(child, true);
|
||
}
|
||
} else {
|
||
for (let i = 0; i < childrens.length; i++) {
|
||
const child = childrens[i];
|
||
if ((child as CompositeTreeNode).expanded) {
|
||
expandedChilds.push(child as CompositeTreeNode);
|
||
}
|
||
}
|
||
}
|
||
for (let i = 0; i < expandedChilds.length; i++) {
|
||
const child = expandedChilds[i];
|
||
child.expandBranch(child, true);
|
||
}
|
||
if (typeof this.watchTerminator === 'function') {
|
||
this.watchTerminator(this.path);
|
||
}
|
||
this.watchTerminator = this.watcher.onWatchEvent(this.path, this.handleWatchEvent);
|
||
if (this === origin) {
|
||
this.expandBranch(this);
|
||
}
|
||
}
|
||
} else {
|
||
// 仅需处理存在子节点的情况,否则将会影响刷新后的节点长度
|
||
if (this.children) {
|
||
// 清理子节点,等待下次展开时更新
|
||
if (!!this.children && this.parent) {
|
||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||
for (let i = 0, len = this.children.length; i < len; i++) {
|
||
const child = this.children[i];
|
||
(child as CompositeTreeNode).dispose();
|
||
}
|
||
this._children = null;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
public async expandedAll(collapsedPaths: string[] = this.getAllCollapsedNodePath()) {
|
||
// 仅根节点使用
|
||
if (!CompositeTreeNode.isRoot(this)) {
|
||
return;
|
||
}
|
||
collapsedPaths = collapsedPaths.sort((a, b) => Path.pathDepth(a) - Path.pathDepth(b));
|
||
let path;
|
||
while (collapsedPaths.length > 0) {
|
||
path = collapsedPaths.pop();
|
||
const item = TreeNode.getTreeNodeByPath(path);
|
||
if (CompositeTreeNode.is(item)) {
|
||
await (item as CompositeTreeNode).setExpanded(false, true);
|
||
}
|
||
}
|
||
// 通知分支树已更新
|
||
this.watcher.notifyDidUpdateBranch();
|
||
}
|
||
|
||
public async collapsedAll(expandedPaths: string[] = this.getAllExpandedNodePath()) {
|
||
// 仅根节点使用
|
||
if (!CompositeTreeNode.isRoot(this)) {
|
||
return;
|
||
}
|
||
expandedPaths = expandedPaths.sort((a, b) => Path.pathDepth(a) - Path.pathDepth(b));
|
||
let path;
|
||
while (expandedPaths.length > 0) {
|
||
path = expandedPaths.pop();
|
||
const item = TreeNode.getTreeNodeByPath(path);
|
||
if (CompositeTreeNode.is(item)) {
|
||
(item as CompositeTreeNode).setCollapsed(true);
|
||
}
|
||
}
|
||
// 通知分支树已更新
|
||
this.watcher.notifyDidUpdateBranch();
|
||
}
|
||
|
||
// 折叠节点
|
||
public setCollapsed(quiet = false) {
|
||
// 根节点不可折叠
|
||
if (CompositeTreeNode.isRoot(this) || this.disposed) {
|
||
return;
|
||
}
|
||
if (!this.isExpanded) {
|
||
return;
|
||
}
|
||
const state = TreeNode.getGlobalTreeState(this.path);
|
||
if (state.isExpanding) {
|
||
// 当节点处于加载子节点过程时,尽管为展开状态,但此时不应该支持折叠节点
|
||
return;
|
||
}
|
||
state.loadPathCancelToken.cancel();
|
||
state.refreshCancelToken.cancel();
|
||
this._watcher.notifyWillChangeExpansionState(this, false);
|
||
if (this._children && this.parent) {
|
||
// 从根节点裁剪分支
|
||
this.shrinkBranch(this, quiet);
|
||
}
|
||
this.isExpanded = false;
|
||
TreeNode.setTreeNode(this.id, this.path, this);
|
||
this._watcher.notifyDidChangeExpansionState(this, false);
|
||
}
|
||
|
||
public mv(to: ICompositeTreeNode, name: string = this.name) {
|
||
const prevPath = this.path;
|
||
super.mv(to, name);
|
||
if (typeof this.watchTerminator === 'function') {
|
||
this.watchTerminator(prevPath);
|
||
this.watchTerminator = this.watcher.onWatchEvent(this.path, this.handleWatchEvent);
|
||
}
|
||
// 同时移动过子节点
|
||
if (this.children) {
|
||
for (let i = 0; i < this.children.length; i++) {
|
||
const child = this.children[i];
|
||
child.mv(child.parent as ICompositeTreeNode, child.name);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在节点中插入新的节点
|
||
*
|
||
* 直接调用此方法将不会触发onWillHandleWatchEvent和onDidHandleWatchEvent事件
|
||
*/
|
||
public insertItem(item: ITreeNodeOrCompositeTreeNode) {
|
||
if (item.parent !== this) {
|
||
item.mv(this, item.name);
|
||
return;
|
||
}
|
||
if (this.children) {
|
||
for (let i = 0; i < this.children.length; i++) {
|
||
// path / id 是节点唯一标识
|
||
if (this.children[i].path === item.path) {
|
||
this.children[i] = item;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
const branchSizeIncrease = 1 + (item instanceof CompositeTreeNode && item.expanded ? item._branchSize : 0);
|
||
if (this._children) {
|
||
this._children.push(item);
|
||
this._children.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);
|
||
}
|
||
this._branchSize += branchSizeIncrease;
|
||
let master = this as CompositeTreeNode;
|
||
// 如果该节点无叶子节点,则继续往上查找合适的插入位置
|
||
while (!master._flattenedBranch) {
|
||
if (master.parent) {
|
||
master = master.parent as CompositeTreeNode;
|
||
master._branchSize += branchSizeIncrease;
|
||
}
|
||
}
|
||
if (!this._children) {
|
||
return;
|
||
}
|
||
let relativeInsertionIndex = this._children.indexOf(item);
|
||
let absInsertionIndex;
|
||
const leadingSibling = this._children[relativeInsertionIndex - 1];
|
||
if (leadingSibling) {
|
||
const siblingIdx = master._flattenedBranch.indexOf(leadingSibling.id);
|
||
relativeInsertionIndex =
|
||
siblingIdx +
|
||
(leadingSibling instanceof CompositeTreeNode && leadingSibling.expanded ? leadingSibling._branchSize : 0);
|
||
} else {
|
||
relativeInsertionIndex = master._flattenedBranch.indexOf(this.id);
|
||
}
|
||
if (relativeInsertionIndex === -1) {
|
||
if (this._branchSize === 1) {
|
||
// 在空Tree中插入节点时,相对插入位置为0
|
||
relativeInsertionIndex = 0;
|
||
}
|
||
}
|
||
// 非空Tree情况下需要+1,为了容纳自身节点位置,在插入节点下方插入新增节点
|
||
absInsertionIndex = relativeInsertionIndex + 1;
|
||
// 空 Tree 情况下需要重置为 0,避免设置 Uint32Array 时超出范围
|
||
if (master._flattenedBranch.length === 0) {
|
||
absInsertionIndex = 0;
|
||
}
|
||
let branch: number[] = [item.id];
|
||
if (item instanceof CompositeTreeNode && item.expanded && item._flattenedBranch) {
|
||
branch = branch.concat(item._flattenedBranch);
|
||
(item as CompositeTreeNode).setFlattenedBranch(null);
|
||
}
|
||
master.setFlattenedBranch(spliceArray(master._flattenedBranch, absInsertionIndex, 0, branch));
|
||
TreeNode.setTreeNode(item.id, item.path, item as TreeNode);
|
||
return item;
|
||
}
|
||
|
||
/**
|
||
* 从父节点中移除节点
|
||
*
|
||
* 直接调用此方法将不会触发onWillHandleWatchEvent和onDidHandleWatchEvent事件
|
||
*/
|
||
public unlinkItem(item: ITreeNodeOrCompositeTreeNode, reparenting?: boolean): void {
|
||
if (!this._children) {
|
||
return;
|
||
}
|
||
const idx = this._children.indexOf(item);
|
||
if (idx === -1) {
|
||
return;
|
||
}
|
||
// 当删除时父节点已不存在界面上时,跳过插入操作
|
||
if (!this.isItemVisibleAtRootSurface(this)) {
|
||
return;
|
||
}
|
||
this._children?.splice(idx, 1);
|
||
const branchSizeDecrease = 1 + (item instanceof CompositeTreeNode && item.expanded ? item._branchSize : 0);
|
||
this._branchSize -= branchSizeDecrease;
|
||
// 逐级往上查找节点的父节点,并沿途裁剪分支数
|
||
let master = this as CompositeTreeNode;
|
||
while (!master._flattenedBranch) {
|
||
if (master.parent) {
|
||
master = master.parent as CompositeTreeNode;
|
||
master._branchSize -= branchSizeDecrease;
|
||
}
|
||
}
|
||
const removalBeginIdx = master._flattenedBranch.indexOf(item.id);
|
||
if (removalBeginIdx === -1) {
|
||
return;
|
||
}
|
||
|
||
if (item instanceof CompositeTreeNode && item.expanded) {
|
||
(item as CompositeTreeNode).setFlattenedBranch(
|
||
master._flattenedBranch.slice(removalBeginIdx + 1, removalBeginIdx + branchSizeDecrease),
|
||
);
|
||
}
|
||
|
||
master.setFlattenedBranch(spliceArray(master._flattenedBranch, removalBeginIdx, branchSizeDecrease));
|
||
|
||
if (!reparenting && item.parent === this) {
|
||
item.mv(null);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 转换节点路径
|
||
*/
|
||
private transferItem(oldPath: string, newPath: string) {
|
||
const oldP = new Path(oldPath);
|
||
const from = oldP.dir.toString();
|
||
if (from !== this.path) {
|
||
return;
|
||
}
|
||
const name = oldP.base.toString();
|
||
const item = this._children?.find((c) => c.name === name);
|
||
if (!item) {
|
||
return;
|
||
}
|
||
const newP = new Path(newPath);
|
||
const to = newP.dir.toString();
|
||
const destDir = to === from ? this : TreeNode.getTreeNodeByPath(to);
|
||
if (!CompositeTreeNode.is(destDir)) {
|
||
this.unlinkItem(item);
|
||
return;
|
||
}
|
||
item.mv(destDir, newP.base.toString());
|
||
return item;
|
||
}
|
||
|
||
public dispose() {
|
||
// 如果存在对应文件路径下的监听,同样需要清理掉
|
||
if (this.watchEvents) {
|
||
const watcher = this.watchEvents.get(this.path);
|
||
if (watcher) {
|
||
watcher.terminator();
|
||
}
|
||
this.watchEvents.clear();
|
||
}
|
||
if (this._children) {
|
||
// 移除后应该折叠,因为下次初始化默认值为折叠,否则将会导致下次插入异常
|
||
this.isExpanded = false;
|
||
this._children.forEach((child) => {
|
||
(child as CompositeTreeNode).dispose();
|
||
});
|
||
this._children = null;
|
||
this._flattenedBranch = null;
|
||
}
|
||
super.dispose();
|
||
}
|
||
|
||
/**
|
||
* 设置扁平化的分支信息
|
||
*/
|
||
protected setFlattenedBranch(leaves: number[] | null, withoutNotify?: boolean) {
|
||
this._flattenedBranch = leaves;
|
||
// Root节点才通知更新
|
||
if (CompositeTreeNode.isRoot(this) && !withoutNotify) {
|
||
this.watcher.notifyDidUpdateBranch();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 展开分支节点
|
||
* @param branch 分支节点
|
||
*/
|
||
protected expandBranch(branch: CompositeTreeNode, withoutNotify?: boolean) {
|
||
if (this !== branch) {
|
||
// 但节点为展开状态时进行裁剪
|
||
if (branch._flattenedBranch) {
|
||
this._branchSize += branch._branchSize;
|
||
}
|
||
}
|
||
// 当前节点为折叠状态,更新分支信息
|
||
if (this !== branch && this._flattenedBranch) {
|
||
const injectionStartIdx = this._flattenedBranch.indexOf(branch.id) + 1;
|
||
if (injectionStartIdx === 0) {
|
||
// 中途发生了branch更新事件,此时的_flattenedBranch可能已被更新,即查找不到branch.id
|
||
// 这种情况在父节点发生了多路径目录的创建定位动作下更易复现
|
||
// 例:文件树在执行a/b/c定位操作时需要请求三次数据,而更新操作可能只需要一次
|
||
// 导致就算更新操作后置执行,也可能比定位操作先执行完,同时将_flattenedBranch更新
|
||
// 最终导致此处查询不到对应节点,下面的shrinkBranch同样可能有相同问题,如点击折叠全部功能时
|
||
return;
|
||
}
|
||
this.setFlattenedBranch(
|
||
spliceArray(this._flattenedBranch, injectionStartIdx, 0, branch._flattenedBranch),
|
||
withoutNotify,
|
||
);
|
||
// 取消展开分支对于分支的所有权,即最终只会有顶部Root拥有所有分支信息
|
||
branch.setFlattenedBranch(null, withoutNotify);
|
||
} else if (this.parent) {
|
||
(this.parent as CompositeTreeNode).expandBranch(branch, withoutNotify);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理分支节点
|
||
* @param branch 分支节点
|
||
*/
|
||
protected shrinkBranch(branch: CompositeTreeNode, withoutNotify?: boolean) {
|
||
if (this !== branch) {
|
||
// 这里的`this`实际上为父节点
|
||
// `this`的分支大小没有改变,仍然具有相同数量的叶子,但是从父级参照系(即根节点)来看,其分支缩小了
|
||
this._branchSize -= branch._branchSize;
|
||
}
|
||
if (this !== branch && this._flattenedBranch) {
|
||
const removalStartIdx = this._flattenedBranch.indexOf(branch.id) + 1;
|
||
if (removalStartIdx === 0) {
|
||
// 中途发生了branch更新事件,此时的_flattenedBranch可能已被更新,即查找不到branch.id
|
||
return;
|
||
}
|
||
// 返回分支对于分支信息所有权,即将折叠的节点信息再次存储于折叠了的节点中
|
||
branch.setFlattenedBranch(
|
||
this._flattenedBranch.slice(removalStartIdx, removalStartIdx + branch._branchSize),
|
||
withoutNotify,
|
||
);
|
||
this.setFlattenedBranch(
|
||
spliceArray(
|
||
this._flattenedBranch,
|
||
removalStartIdx,
|
||
branch._flattenedBranch ? branch._flattenedBranch.length : 0,
|
||
),
|
||
withoutNotify,
|
||
);
|
||
} else if (this.parent) {
|
||
(this.parent as CompositeTreeNode).shrinkBranch(branch, withoutNotify);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载子节点信息
|
||
* 当返回值为 true 时,正常加载完子节点并同步到数据结构中
|
||
* 返回值为 false 时,加载节点的过程被中断
|
||
*
|
||
* @memberof CompositeTreeNode
|
||
*/
|
||
public async hardReloadChildren(token?: CancellationToken) {
|
||
let rawItems;
|
||
|
||
const oldPath = this.path;
|
||
|
||
try {
|
||
// ! `this.path` maybe changed after `resolveChildren` in file tree compact mode
|
||
rawItems = (await this._tree.resolveChildren(this)) || [];
|
||
} catch (e) {
|
||
rawItems = [];
|
||
}
|
||
// 当获取到新的子节点时,如果当前节点正处于非展开状态时,忽略后续裁切逻辑
|
||
// 后续的 expandBranch 也不应该被响应
|
||
if (!this.expanded || token?.isCancellationRequested) {
|
||
return false;
|
||
}
|
||
if (this.path !== oldPath) {
|
||
// do some clean up
|
||
TreeNode.setGlobalTreeState(oldPath, {
|
||
isExpanding: false,
|
||
isLoadingPath: false,
|
||
isRefreshing: false,
|
||
});
|
||
}
|
||
|
||
const expandedChilds: CompositeTreeNode[] = [];
|
||
const flatTree = new Array(rawItems.length);
|
||
const tempChildren = new Array(rawItems.length);
|
||
for (let i = 0; i < rawItems.length; i++) {
|
||
const child = rawItems[i];
|
||
(child as TreeNode).id = TreeNode.getIdByPath(child.path) || (child as TreeNode).id;
|
||
tempChildren[i] = child;
|
||
TreeNode.setIdByPath(child.path, child.id);
|
||
if (CompositeTreeNode.is(child) && child.expanded) {
|
||
if (!(child as CompositeTreeNode).children) {
|
||
await (child as CompositeTreeNode).resolveChildrens(token);
|
||
}
|
||
if (token?.isCancellationRequested) {
|
||
return false;
|
||
}
|
||
expandedChilds.push(child as CompositeTreeNode);
|
||
}
|
||
}
|
||
|
||
tempChildren.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);
|
||
|
||
for (let i = 0; i < rawItems.length; i++) {
|
||
flatTree[i] = tempChildren[i].id;
|
||
}
|
||
|
||
if (this.children) {
|
||
this.shrinkBranch(this);
|
||
}
|
||
if (this.children) {
|
||
for (let i = 0; i < this.children.length; i++) {
|
||
const child = this.children[i];
|
||
// The Child maybe `undefined`.
|
||
child?.dispose();
|
||
}
|
||
}
|
||
|
||
for (let i = 0; i < tempChildren.length; i++) {
|
||
this.updateTreeNodeCache(tempChildren[i]);
|
||
}
|
||
|
||
this._children = tempChildren;
|
||
this._branchSize = flatTree.length;
|
||
this.setFlattenedBranch(flatTree);
|
||
|
||
for (let i = 0; i < expandedChilds.length; i++) {
|
||
const child = expandedChilds[i];
|
||
(child as CompositeTreeNode).expandBranch(child, true);
|
||
}
|
||
|
||
// 清理上一次监听函数
|
||
if (typeof this.watchTerminator === 'function') {
|
||
this.watchTerminator(this.path);
|
||
}
|
||
this.watchTerminator = this.watcher.onWatchEvent(this.path, this.handleWatchEvent);
|
||
return true;
|
||
}
|
||
|
||
public moveNode(oldPath: string, newPath: string) {
|
||
if (typeof oldPath !== 'string') {
|
||
throw new TypeError('Expected oldPath to be a string');
|
||
}
|
||
if (typeof newPath !== 'string') {
|
||
throw new TypeError('Expected newPath to be a string');
|
||
}
|
||
if (Path.isRelative(oldPath)) {
|
||
throw new TypeError('oldPath must be absolute');
|
||
}
|
||
if (Path.isRelative(newPath)) {
|
||
throw new TypeError('newPath must be absolute');
|
||
}
|
||
return this.transferItem(oldPath, newPath);
|
||
}
|
||
|
||
public addNode(node: TreeNode) {
|
||
if (!TreeNode.is(node)) {
|
||
throw new TypeError('Expected node to be a TreeNode');
|
||
}
|
||
return this.insertItem(node);
|
||
}
|
||
|
||
public removeNode(path: string) {
|
||
const pathObject = new Path(path);
|
||
const dirName = pathObject.dir.toString();
|
||
const name = pathObject.base.toString();
|
||
if (dirName === this.path && !!this.children) {
|
||
const item = this.children.find((c) => c.name === name);
|
||
if (item) {
|
||
this.unlinkItem(item);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理 Watch 事件,同时可通过外部手动调 g用节点更新函数进行节点替换,这里为通用的事件管理
|
||
* 如: transferItem,insertItem, unlinkItem等
|
||
* @private
|
||
* @memberof CompositeTreeNode
|
||
*/
|
||
private handleWatchEvent = async (event: IWatcherEvent) => {
|
||
this.watcher.notifyWillProcessWatchEvent(this, event);
|
||
|
||
switch (event.type) {
|
||
case WatchEvent.Moved: {
|
||
const { oldPath, newPath } = event;
|
||
if (typeof oldPath !== 'string') {
|
||
throw new TypeError('Expected oldPath to be a string');
|
||
}
|
||
if (typeof newPath !== 'string') {
|
||
throw new TypeError('Expected newPath to be a string');
|
||
}
|
||
if (Path.isRelative(oldPath)) {
|
||
throw new TypeError('oldPath must be absolute');
|
||
}
|
||
if (Path.isRelative(newPath)) {
|
||
throw new TypeError('newPath must be absolute');
|
||
}
|
||
this.transferItem(oldPath, newPath);
|
||
break;
|
||
}
|
||
case WatchEvent.Added: {
|
||
const { node } = event;
|
||
if (!TreeNode.is(node)) {
|
||
throw new TypeError('Expected node to be a TreeNode');
|
||
}
|
||
this.insertItem(node);
|
||
break;
|
||
}
|
||
|
||
case WatchEvent.Removed: {
|
||
const { path } = event;
|
||
const pathObject = new Path(path);
|
||
const dirName = pathObject.dir.toString();
|
||
const name = pathObject.base.toString();
|
||
if (dirName === this.path && !!this.children) {
|
||
const item = this.children.find((c) => c.name === name);
|
||
if (item) {
|
||
this.unlinkItem(item);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
// 如果当前变化的节点已在数据视图(并非滚动到不可见区域)中不可见,则将该节点折叠,待下次更新即可,
|
||
if (!this.isItemVisibleAtRootSurface(this)) {
|
||
this.isExpanded = false;
|
||
this._children = null;
|
||
} else {
|
||
await this.refresh();
|
||
}
|
||
break;
|
||
}
|
||
|
||
this.watcher.notifyDidProcessWatchEvent(this, event);
|
||
};
|
||
|
||
// 当没有传入具体路径时,使用当前展开目录作为刷新路径
|
||
public async refresh(tokenSource?: CancellationTokenSource, target?: CompositeTreeNode) {
|
||
if (!CompositeTreeNode.isRoot(this)) {
|
||
// 刷新操作只能从根节点进行,便于对重复的刷新操作进行合并
|
||
return await (this.root as CompositeTreeNode).refresh(tokenSource, this);
|
||
}
|
||
const state = TreeNode.getGlobalTreeState(this.path);
|
||
if (state.isLoadingPath || state.isExpanding) {
|
||
return;
|
||
}
|
||
let token;
|
||
if (tokenSource && !tokenSource.token.isCancellationRequested) {
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isRefreshing: true,
|
||
refreshCancelToken: tokenSource,
|
||
});
|
||
token = tokenSource.token;
|
||
} else {
|
||
if (state.refreshCancelToken.token.isCancellationRequested) {
|
||
const refreshCancelToken = new CancellationTokenSource();
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isRefreshing: true,
|
||
refreshCancelToken,
|
||
});
|
||
token = refreshCancelToken.token;
|
||
} else {
|
||
token = state.refreshCancelToken.token;
|
||
}
|
||
}
|
||
this.toRefreshPathQueue.add((target || this).path);
|
||
await this.refreshThrottler.trigger(() => this.doRefresh(token));
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isRefreshing: false,
|
||
});
|
||
}
|
||
|
||
private async doRefresh(token?: CancellationToken) {
|
||
const target = (this.getRefreshNode() as CompositeTreeNode) || this;
|
||
if (!CompositeTreeNode.is(target)) {
|
||
return;
|
||
}
|
||
const paths = target.getAllExpandedNodePath();
|
||
await target.refreshTreeNodeByPaths(paths, token, target);
|
||
}
|
||
|
||
private getRefreshNode() {
|
||
let paths = Array.from(this.toRefreshPathQueue);
|
||
this.toRefreshPathQueue.clear();
|
||
if (!paths.length) {
|
||
return this.root;
|
||
}
|
||
// 根据路径层级深度进行排序
|
||
paths = paths.sort((a, b) => {
|
||
const depthA = Path.pathDepth(a);
|
||
const depthB = Path.pathDepth(b);
|
||
return depthA - depthB;
|
||
});
|
||
if (paths.length === 1 || Path.pathDepth(paths[0]) === 1) {
|
||
// 说明刷新队列中包含根节点,直接返回根节点进行刷新
|
||
return TreeNode.getTreeNodeByPath(paths[0]);
|
||
}
|
||
const sortedPaths = paths.map((p) => new Path(p));
|
||
let rootPath = sortedPaths[0];
|
||
for (let i = 1, len = sortedPaths.length; i < len; i++) {
|
||
if (rootPath.isEqualOrParent(sortedPaths[i])) {
|
||
continue;
|
||
} else {
|
||
while (!rootPath.isRoot) {
|
||
rootPath = rootPath.dir;
|
||
if (!rootPath || rootPath.isEqualOrParent(sortedPaths[i])) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (rootPath) {
|
||
return TreeNode.getTreeNodeByPath(rootPath.toString());
|
||
}
|
||
|
||
return this.root;
|
||
}
|
||
|
||
private isItemVisibleAtRootSurface(node: TreeNode) {
|
||
let parent: ITreeNodeOrCompositeTreeNode = node;
|
||
while (parent.parent) {
|
||
parent = parent.parent;
|
||
}
|
||
return (parent as CompositeTreeNode).isItemVisibleAtSurface(node);
|
||
}
|
||
|
||
/**
|
||
* 检查节点是否可见,而不是被隐藏在节点中
|
||
*
|
||
* 这里的可见并不表示节点在当前视图中可见,而是在用户滚动到特定位置便可看见
|
||
*
|
||
* 隐藏在节点中可能的原因为其父节点中有一个以上处于折叠状态
|
||
*/
|
||
public isItemVisibleAtSurface(item: ITreeNodeOrCompositeTreeNode): boolean {
|
||
if (item === this) {
|
||
return true;
|
||
}
|
||
return !!this._flattenedBranch && this._flattenedBranch.indexOf(item.id) > -1;
|
||
}
|
||
|
||
private transformToRelativePath(path: string): string[] {
|
||
const { splitPath } = Path;
|
||
const pathFlag = splitPath(path);
|
||
pathFlag.shift();
|
||
return pathFlag;
|
||
}
|
||
|
||
/**
|
||
* 根据路径展开节点树
|
||
* @memberof CompositeTreeNode
|
||
*/
|
||
public async loadTreeNodeByPath(path: string, quiet = false): Promise<ITreeNodeOrCompositeTreeNode | undefined> {
|
||
if (!CompositeTreeNode.isRoot(this)) {
|
||
return;
|
||
}
|
||
const state = TreeNode.getGlobalTreeState(this.path);
|
||
if (state.isExpanding) {
|
||
return;
|
||
}
|
||
state.refreshCancelToken.cancel();
|
||
state.loadPathCancelToken.cancel();
|
||
const loadPathCancelToken = new CancellationTokenSource();
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isLoadingPath: true,
|
||
loadPathCancelToken,
|
||
});
|
||
const token = loadPathCancelToken.token;
|
||
|
||
const flattenedBranchChilds: CompositeTreeNode[] = [];
|
||
const { splitPath, isRelative } = Path;
|
||
const pathFlag = isRelative(path) ? splitPath(path) : this.transformToRelativePath(path);
|
||
if (pathFlag.length === 0) {
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isLoadingPath: false,
|
||
});
|
||
return this;
|
||
}
|
||
if (!this.children) {
|
||
await this.ensureLoaded(token);
|
||
}
|
||
if (token.isCancellationRequested) {
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isLoadingPath: false,
|
||
});
|
||
return;
|
||
}
|
||
let next = this._children;
|
||
let preItem: CompositeTreeNode | undefined;
|
||
let preItemPath = '';
|
||
let name;
|
||
while (next && (name = pathFlag.shift())) {
|
||
let item = next.find((c) => c.name.indexOf(name) === 0);
|
||
if (item) {
|
||
if (CompositeTreeNode.is(item)) {
|
||
(item as CompositeTreeNode)._watcher.notifyWillChangeExpansionState(item, true);
|
||
(item as CompositeTreeNode).isExpanded = true;
|
||
if (!(item as CompositeTreeNode).children) {
|
||
await (item as CompositeTreeNode).resolveChildrens(token);
|
||
if (token.isCancellationRequested) {
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isLoadingPath: false,
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
flattenedBranchChilds.push(item as CompositeTreeNode);
|
||
(item as CompositeTreeNode)._watcher.notifyDidChangeExpansionState(item, true);
|
||
}
|
||
if (pathFlag.length === 0) {
|
||
preItem = item as CompositeTreeNode;
|
||
break;
|
||
}
|
||
}
|
||
// 可能展开后路径发生了变化, 需要重新处理一下当前加载路径
|
||
if (!item && preItem) {
|
||
const compactPath = splitPath(preItem.name).slice(1);
|
||
if (compactPath[0] === name) {
|
||
compactPath.shift();
|
||
while (compactPath.length > 0) {
|
||
if (compactPath[0] === pathFlag[0]) {
|
||
compactPath.shift();
|
||
pathFlag.shift();
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
name = pathFlag.shift();
|
||
item = next.find((c) => c.name.indexOf(name) === 0);
|
||
}
|
||
}
|
||
// 最终加载到的路径节点
|
||
if (!item || (!CompositeTreeNode.is(item) && pathFlag.length > 0)) {
|
||
break;
|
||
}
|
||
if (CompositeTreeNode.is(item)) {
|
||
const isCompactName = item.name.indexOf(Path.separator) > 0;
|
||
if (isCompactName) {
|
||
const compactPath = splitPath(item.name).slice(1);
|
||
while (compactPath.length > 0) {
|
||
if (compactPath[0] === pathFlag[0]) {
|
||
compactPath.shift();
|
||
pathFlag.shift();
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!(item as CompositeTreeNode)._children) {
|
||
preItemPath = item.path;
|
||
if (CompositeTreeNode.is(item)) {
|
||
(item as CompositeTreeNode).isExpanded = true;
|
||
if (!item.children) {
|
||
await (item as CompositeTreeNode).resolveChildrens(token);
|
||
if (token.isCancellationRequested) {
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isLoadingPath: false,
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
flattenedBranchChilds.push(item as CompositeTreeNode);
|
||
}
|
||
}
|
||
if (item && pathFlag.length === 0) {
|
||
preItem = item as CompositeTreeNode;
|
||
break;
|
||
} else {
|
||
if (!!preItemPath && preItemPath !== item.path) {
|
||
// 说明此时已发生了路径压缩,如从 a -> a/b/c
|
||
// 需要根据路径变化移除对应的展开路径, 这里只需考虑短变长场景
|
||
const prePaths = splitPath(preItemPath);
|
||
const nextPaths = splitPath(item.path);
|
||
if (nextPaths.length > prePaths.length) {
|
||
pathFlag.splice(0, nextPaths.length - prePaths.length);
|
||
}
|
||
}
|
||
next = (item as CompositeTreeNode)._children;
|
||
preItem = item as CompositeTreeNode;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (preItem) {
|
||
let child;
|
||
if (preItem.disposed) {
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isLoadingPath: false,
|
||
});
|
||
return;
|
||
}
|
||
while ((child = flattenedBranchChilds.pop())) {
|
||
(child as CompositeTreeNode).expandBranch(child, true);
|
||
if (flattenedBranchChilds.length === 0) {
|
||
this.updateTreeNodeCache(child as CompositeTreeNode);
|
||
}
|
||
}
|
||
if (!quiet) {
|
||
this.watcher.notifyDidUpdateBranch();
|
||
}
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isLoadingPath: false,
|
||
});
|
||
return preItem;
|
||
}
|
||
TreeNode.setGlobalTreeState(this.path, {
|
||
isLoadingPath: false,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 根据节点获取节点ID下标位置
|
||
* @param {number} id
|
||
* @returns
|
||
* @memberof CompositeTreeNode
|
||
*/
|
||
public getIndexAtTreeNodeId(id: number) {
|
||
if (this._flattenedBranch) {
|
||
return this._flattenedBranch.indexOf(id);
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/**
|
||
* 根据节点获取节点下标位置
|
||
* @param {ITreeNodeOrCompositeTreeNode} node
|
||
* @returns
|
||
* @memberof CompositeTreeNode
|
||
*/
|
||
public getIndexAtTreeNode(node: ITreeNodeOrCompositeTreeNode) {
|
||
if (this._flattenedBranch) {
|
||
return this._flattenedBranch.indexOf(node.id);
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/**
|
||
* 根据下标位置获取节点
|
||
* @param {number} index
|
||
* @returns
|
||
* @memberof CompositeTreeNode
|
||
*/
|
||
public getTreeNodeAtIndex(index: number) {
|
||
const id = this._flattenedBranch?.[index];
|
||
if (!id) {
|
||
return undefined;
|
||
}
|
||
return TreeNode.getTreeNodeById(id);
|
||
}
|
||
|
||
/**
|
||
* 根据节点ID获取节点
|
||
* @param {number} id
|
||
* @returns
|
||
* @memberof CompositeTreeNode
|
||
*/
|
||
public getTreeNodeById(id: number) {
|
||
return TreeNode.getTreeNodeById(id);
|
||
}
|
||
|
||
/**
|
||
* 根据节点路径获取节点
|
||
* @param {string} path
|
||
* @returns
|
||
* @memberof CompositeTreeNode
|
||
*/
|
||
public getTreeNodeByPath(path: string) {
|
||
return TreeNode.getTreeNodeByPath(path);
|
||
}
|
||
}
|