Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions js/tree-v1/tree-node-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function getExposedProps(node: TreeNode): TreeNodeModelProps {
'actived',
'expanded',
'checked',
'disabled',
'indeterminate',
'loading',
]) as TreeNodeModelProps;
Expand Down
184 changes: 129 additions & 55 deletions js/tree-v1/tree-node.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { isNull, isFunction, isNumber, uniqueId, isBoolean, isNil, get } from 'lodash-es';
import { TreeStore } from './tree-store';
import {
TreeNodeValue,
get,
isBoolean,
isFunction,
isNil,
isNull,
isNumber,
uniqueId,
} from 'lodash-es';
import log from '../log';
import { createNodeModel, updateNodeModel } from './tree-node-model';
import { TreeStore } from './tree-store';
import type {
TreeNodeState,
TreeNodeValue,
TypeIdMap,
TypeTreeItem,
TypeSettingOptions,
TypeTreeNodeModel,
TypeTreeItem,
TypeTreeNodeData,
TypeTreeNodeModel,
} from './types';
import { createNodeModel, updateNodeModel } from './tree-node-model';
import log from '../log';

const { hasOwnProperty } = Object.prototype;

Expand Down Expand Up @@ -108,13 +116,15 @@ export class TreeNode {
// 节点在视图上实际的选中态
public checked: boolean;

public isIndeterminateManual: boolean;

// 节点实际是否为半选状态
public indeterminate: boolean;

public isIndeterminateManual: boolean;

// 节点是否已禁用
public disabled: null | boolean;
public disabled: boolean;

private disableManually: null | boolean;

// 节点是否可拖动
public draggable: null | boolean;
Expand Down Expand Up @@ -142,7 +152,6 @@ export class TreeNode {
const propChildren = keys.children || 'children';
const propLabel = keys.label || 'label';
const propValue = keys.value || 'value';
const propsDisabled = keys.disabled || 'disabled';

// 节点自身初始化数据
this.model = null;
Expand Down Expand Up @@ -173,6 +182,7 @@ export class TreeNode {
this.checkable = null;
this.expandMutex = null;
this.draggable = null;
this.disableManually = null;

// 为节点设置唯一 id
// tree 数据替换时,value 相同有可能导致节点状态渲染冲突
Expand All @@ -191,10 +201,7 @@ export class TreeNode {
nodeMap.set(this.value, this);
privateMap.set(this[privateKey], this);

// 设置标签
this.label = get(data, propLabel) || '';
// 设置是否禁用
this.disabled = get(data, propsDisabled);

// 设置子节点
const children = data[propChildren];
Expand All @@ -221,7 +228,6 @@ export class TreeNode {
}
});

// 初始化节点状态
this.initActived();
this.initExpanded();
this.initChecked();
Expand Down Expand Up @@ -562,24 +568,19 @@ export class TreeNode {

/**
* 设置节点状态
* - 为节点设置独立于配置的 disabled 状态: set({ disabled: true })
* - 清除独立于配置的 disabled 状态: set({ disabled: null })
* @param {object} item 节点状态对象
* @return void
*/
public set(item: TreeNodeState): void {
const { tree } = this;
const keys = Object.keys(item);
keys.forEach((key) => {
if (
hasOwnProperty.call(settableStatus, key)
|| key === 'label'
|| key === 'disabled'
) {
if (hasOwnProperty.call(settableStatus, key) || key === 'label') {
// @ts-ignore
// TODO: 待移除
this[key] = item[key];
}
if (key === 'disabled') {
this.setDisabled(item[key]);
}
});
tree.updated(this);
}
Expand Down Expand Up @@ -754,30 +755,55 @@ export class TreeNode {
return visible;
}

/**
* 判断节点为逻辑禁用状态,不包含过滤锁定状态
* - 优先级:Tree 配置 > checkStrictly > 手动 > 节点 data > disableCheck
*/
public isDisabledState(): boolean {
const { tree, parent } = this;
const { config } = tree;
const { checkStrictly, disabled, disableCheck, keys = {} } = config;

if (disabled) return true;

if (!checkStrictly && parent?.isDisabled()) return true;

if (typeof this.disableManually === 'boolean') return this.disableManually;
Copy link

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition should check for strict boolean types to ensure type safety. Consider using this.disableManually !== null instead to be more explicit about the intention.

Copilot uses AI. Check for mistakes.

const propDisabled = keys.disabled || 'disabled';
const state = get(this.data, propDisabled);
if (typeof state === 'boolean') return state;

if (disableCheck === true) return true;
if (typeof disableCheck === 'function') {
const stateCheck = disableCheck(this.getModel());
if (typeof stateCheck === 'boolean') {
return stateCheck;
}
}

return false;
}

/**
* 判断节点是否被禁用
* @return boolean 是否被禁用
*/
public isDisabled(): boolean {
const { tree } = this;
const { hasFilter, config } = tree;
const { disabled, allowFoldNodeOnFilter, checkStrictly } = config;
const { allowFoldNodeOnFilter } = config;
if (
hasFilter
&& !allowFoldNodeOnFilter
&& this.vmIsLocked
&& !this.vmIsRest
) {
// 当前树存在过滤条件,允许节点过滤后被折叠,当前节点为锁定节点,并且不是筛选后剩下的节点
// 则该节点应当呈现禁用状态
return true;
}
let state = disabled || false;
if (typeof this.disabled === 'boolean') {
state = this.disabled;
}
if (!checkStrictly && this.parent?.isDisabled()) {
state = true;
}
return state;
return this.isDisabledState();
}

/**
Expand Down Expand Up @@ -872,13 +898,12 @@ export class TreeNode {
}
let checked = false;
// 在 checkedMap 中,则根据 valueMode 的值进行判断
if (checkedMap.get(value)
&& (
// 如果 valueMode 为 all、parentFirst,则视为选中
valueMode !== 'onlyLeaf'
if (
checkedMap.get(value)
// 如果 valueMode 为 all、parentFirst,则视为选中
&& (valueMode !== 'onlyLeaf'
// 如果 valueMode 为 onlyLeaf 并且当前节点是叶子节点,则视为选中
|| this.isLeaf()
)
|| this.isLeaf())
) {
return true;
}
Expand Down Expand Up @@ -1121,57 +1146,90 @@ export class TreeNode {
return tree.getActived(map);
}

/**
* 是否存在未选中的未禁用子节点
* @return boolean 未选中的未禁用子节点存在与否
*/
public hasEnableUnCheckedChild(): boolean {
const { children } = this;
if (!Array.isArray(children) || children.length <= 0) {
// 没有子节点
return false;
}
let state = false;
children.some((child) => {
// 不理会禁用节点
if (child.isDisabledState()) return false;
// 不理会选中节点
if (child.isChecked()) return false;
if (child.isIndeterminate()) {
// 为半选节点则进行递归检查
if (child.hasEnableUnCheckedChild()) {
state = true;
return true;
}
// 都尽可能选中了,则检查之后的节点
return false;
}
// 子节点为未选中状态,且非半选状态
// 则直接返回 true
state = true;
return true;
});
return state;
}

/**
* 切换节点选中状态
* - 用于受控逻辑处理
* - 仅返回预期状态值数组,不直接操作状态
* @return string[] 当前树选中的节点值数组
*/
public toggleChecked(): TreeNodeValue[] {
if (this.isIndeterminate()) {
// 当前节点为半选情况下需要判断子节点是否尽可能全部选中
// 存在可操作的未选中的子节点,则应当尽可能选中子节点
// 不存在可操作的未选中的子节点,则应取消选中子节点
const expectState = this.hasEnableUnCheckedChild();
return this.setChecked(expectState);
}
return this.setChecked(!this.isChecked());
}

/**
* 设置节点选中状态
* - 节点 UI 操作时调用这个方法
* - 节点设置自身状态时调用这个方法
* @param {boolean} checked 节点选中状态
* @param {object} [opts] 操作选项
* @param {boolean} [opts.isAction=true] 是否为 UI 动作
* @param {boolean} [opts.directly=false] 是否直接操作节点状态
* @return string[] 当前树选中的节点值数组
*/
public setChecked(
checked: boolean,
opts?: TypeSettingOptions
): TreeNodeValue[] {
const { tree } = this;
const config = tree.config || {};
const options: TypeSettingOptions = {
// 为 true, 为 UI 操作,状态扩散受 disabled 影响
// 为 false, 为值操作, 状态扩散不受 disabled 影响
// 为 true, 为 UI 操作,状态变更受 disabled 影响
// 为 false, 为值操作, 状态变更不受 disabled 影响
isAction: true,
// 为 true, 直接操作节点状态
// 为 false, 返回预期状态
directly: false,
...opts,
};
let map = tree.checkedMap;

if (!options.directly) {
map = new Map(tree.checkedMap);
}
if (!this.isCheckable()) {
// 当前节点非可选节点,则不可设置选中态
return tree.getChecked(map);
}
if (options.isAction && this.isDisabled()) {
if (options.isAction && this.isDisabledState()) {
// 对于 UI 动作,禁用时不可切换选中态
return tree.getChecked(map);
}

if (checked === this.isChecked()) {
// 值没有变更,则选中态无变化
return tree.getChecked(map);
const shouldSet = this.isIndeterminate() && !this.hasEnableUnCheckedChild();
if (!shouldSet) {
// 值没有变更, 则选中态无变化
return tree.getChecked(map);
}
}

if (checked) {
Expand Down Expand Up @@ -1202,6 +1260,7 @@ export class TreeNode {
});
}
}

this.isIndeterminateManual = false;

return tree.getChecked(map);
Expand Down Expand Up @@ -1285,6 +1344,9 @@ export class TreeNode {

const { children } = this;
if (!Array.isArray(children)) return;
if (children.length <= 0) return;
// 有子节点,则选中态由子节点选中态集合来决定
map.delete(this.value);
children.forEach((node) => {
// 对于 UI 动作,向下扩散时,禁用状态会阻止状态切换
if (options.isAction && node.isDisabled()) return;
Expand All @@ -1297,6 +1359,17 @@ export class TreeNode {
});
}

/**
* 设置节点禁用状态
*/
public setDisabled(disabled: null | boolean) {
if (!this.tree.config.checkStrictly && this.parent?.isDisabled()) return;
// 当 disabled 为 null 时,恢复为默认的禁用逻辑,而非通过设置强制指定
this.disableManually = disabled;
this.update();
this.updateChildren();
Comment on lines +1365 to +1370
Copy link

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method silently returns when the parent is disabled and checkStrictly is false. This could be confusing for API consumers. Consider throwing an error or returning a boolean to indicate success/failure.

Suggested change
public setDisabled(disabled: null | boolean) {
if (!this.tree.config.checkStrictly && this.parent?.isDisabled()) return;
// 当 disabled 为 null 时,恢复为默认的禁用逻辑,而非通过设置强制指定
this.disableManually = disabled;
this.update();
this.updateChildren();
public setDisabled(disabled: null | boolean): boolean {
if (!this.tree.config.checkStrictly && this.parent?.isDisabled()) return false;
// 当 disabled 为 null 时,恢复为默认的禁用逻辑,而非通过设置强制指定
this.disableManually = disabled;
this.update();
this.updateChildren();
return true;

Copilot uses AI. Check for mistakes.
}

/* ------ 节点状态更新 ------ */

/**
Expand All @@ -1313,6 +1386,7 @@ export class TreeNode {
this.actived = this.isActived();
this.expanded = this.isExpanded();
this.visible = this.isVisible();
this.disabled = this.isDisabled();
this.tree.updated(this);
}

Expand Down
2 changes: 2 additions & 0 deletions js/tree-v1/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ export interface TypeTreeStoreOptions {
checkStrictly?: boolean;
// 禁用整个树
disabled?: boolean;
// 指定节点禁用条件
disableCheck?: boolean | TypeTreeFilter;
// 节点是否可拖动
draggable?: boolean,
// 节点加载函数
Expand Down
Loading