diff --git a/js/tree/tree-node-model.ts b/js/tree/tree-node-model.ts index 1804ae29d8..27bd974370 100644 --- a/js/tree/tree-node-model.ts +++ b/js/tree/tree-node-model.ts @@ -1,5 +1,6 @@ import isUndefined from 'lodash/isUndefined'; import isBoolean from 'lodash/isBoolean'; +import pick from 'lodash/pick'; import omit from 'lodash/omit'; import get from 'lodash/get'; import { TreeNode } from './tree-node'; @@ -9,267 +10,232 @@ import { TypeTreeNodeModel, TypeTreeNodeData, TypeTreeItem, + TreeNodeModelProps, } from './types'; import log from '../log/log'; -export const nodeKey = '__tdesign_tree-node__'; - -export class TreeNodeModel { - private [nodeKey]: TreeNode; - - constructor(node: TreeNode) { - this[nodeKey] = node; - } - - public get value() { - const node = this[nodeKey]; - return node.value; - } - - public get label() { - const node = this[nodeKey]; - return node.label; - } - - public get data() { - const node = this[nodeKey]; - return node.data; - } - - public get actived() { - const node = this[nodeKey]; - return node.actived; - } - - public get expanded() { - const node = this[nodeKey]; - return node.expanded; - } - - public get checked() { - const node = this[nodeKey]; - return node.checked; - } - - public get indeterminate() { - const node = this[nodeKey]; - return node.indeterminate; - } - - public get loading() { - const node = this[nodeKey]; - return node.loading; - } - - /** - * 获取节点所处层级 - * @return number 节点层级序号 - */ - public getLevel() { - const node = this[nodeKey]; - return node.getLevel(); - } - - /** - * 获取节点在父节点的子节点列表中的位置 - * - 如果没有父节点,则获取节点在根节点列表的位置 - * @return number 节点位置序号 - */ - public getIndex() { - const node = this[nodeKey]; - return node.getIndex(); - } - - /** - * 是否为兄弟节点中的第一个节点 - * @return boolean 是否为第一个节点 - */ - public isFirst() { - const node = this[nodeKey]; - return node.isFirst(); - } - - /** - * 是否为兄弟节点中的最后一个节点 - * @return boolean 是否为最后一个节点 - */ - public isLast() { - const node = this[nodeKey]; - return node.isLast(); - } - - /** - * 是否为叶子节点,叶子节点没有子节点 - * @return boolean 是否为叶子节点 - */ - public isLeaf() { - const node = this[nodeKey]; - return node.isLeaf(); - } - - /** - * 在当前节点之前插入节点 - * @param {object} newData 要插入的节点或者数据 - * @return void - */ - public insertBefore(newData: TypeTreeItem) { - const node = this[nodeKey]; - return node.insertBefore(newData); - } - - /** - * 在当前节点之后插入节点 - * @param {object} newData 要插入的节点或者数据 - * @return void - */ - public insertAfter(newData: TypeTreeItem) { - const node = this[nodeKey]; - return node.insertAfter(newData); - } - - /** - * 追加节点数据 - * @param {object | object[]} data 节点数据 - * @return void - */ - public appendData(data: TypeTreeNodeData | TypeTreeNodeData[]) { - const node = this[nodeKey]; - return node.append(data); - } - - /** - * 返回路径节点 - * - 路径节点包含自己在内 - * - 节点顺序与父级节点顺序相反,从根到当前 - * @return TreeNodeModel[] 路径节点数组 - */ - public getPath(): TypeTreeNodeModel[] { - const node = this[nodeKey]; - const nodes = node.getPath(); - return nodes.map((item: TreeNode) => item.getModel()); - } - - /** - * 获取本节点的父节点 - * @return TreeNodeModel 父节点 - */ - public getParent(): TypeTreeNodeModel { - const node = this[nodeKey]; - return node.parent?.getModel(); - } - - /** - * 获取所有父级节点 - * - 顺序为从当前到根 - * @return TreeNodeModel[] 父级节点数组 - */ - public getParents(): TypeTreeNodeModel[] { - const node = this[nodeKey]; - const nodes = node.getParents(); - return nodes.map((item: TreeNode) => item.getModel()); - } - - /** - * 获取本节点的根节点 - * @return TreeNodeModel 根节点 - */ - public getRoot(): TypeTreeNodeModel { - const node = this[nodeKey]; - const root = node.getRoot(); - return root?.getModel(); - } - - /** - * 获取所有兄弟节点,包含自己在内 - * @return TreeNodeModel[] 兄弟节点数组 - */ - public getSiblings(): TypeTreeNodeModel[] { - const node = this[nodeKey]; - const nodes = node.getSiblings(); - return nodes.map((item: TreeNode) => item.getModel()); - } +// 获取节点需要暴露的属性 +function getExposedProps(node: TreeNode): TreeNodeModelProps { + const props = pick(node, [ + 'value', + 'label', + 'data', + 'actived', + 'expanded', + 'checked', + 'indeterminate', + 'loading', + ]) as TreeNodeModelProps; + return props; +} - /** - * 获取当前节点的子节点 - * @param {boolean} deep 是否获取所有深层子节点 - * @return TreeNodeModel[] 子节点数组 - */ - public getChildren(deep?: boolean): boolean | TypeTreeNodeModel[] { - const node = this[nodeKey]; - let childrenModel: boolean | TypeTreeNodeModel[] = false; - const { children } = node; - if (Array.isArray(children)) { - if (children.length > 0) { - if (deep) { - const nodes = node.walk(); - nodes.shift(); - childrenModel = nodes.map((item) => item.getModel()); +// 封装对外暴露的对象 +export function createNodeModel(node: TreeNode): TypeTreeNodeModel { + const props = getExposedProps(node); + + const model: TypeTreeNodeModel = { + ...props, + + /** + * 获取节点所处层级 + * @return number 节点层级序号 + */ + getLevel() { + return node.getLevel(); + }, + + /** + * 获取节点在父节点的子节点列表中的位置 + * - 如果没有父节点,则获取节点在根节点列表的位置 + * @return number 节点位置序号 + */ + getIndex() { + return node.getIndex(); + }, + + /** + * 是否为兄弟节点中的第一个节点 + * @return boolean 是否为第一个节点 + */ + isFirst() { + return node.isFirst(); + }, + + /** + * 是否为兄弟节点中的最后一个节点 + * @return boolean 是否为最后一个节点 + */ + isLast() { + return node.isLast(); + }, + + /** + * 是否为叶子节点,叶子节点没有子节点 + * @return boolean 是否为叶子节点 + */ + isLeaf() { + return node.isLeaf(); + }, + + /** + * 在当前节点之前插入节点 + * @param {object} newData 要插入的节点或者数据 + * @return void + */ + insertBefore(newData: TypeTreeItem) { + return node.insertBefore(newData); + }, + + /** + * 在当前节点之后插入节点 + * @param {object} newData 要插入的节点或者数据 + * @return void + */ + insertAfter(newData: TypeTreeItem) { + return node.insertAfter(newData); + }, + + /** + * 追加节点数据 + * @param {object | object[]} data 节点数据 + * @return void + */ + appendData(data: TypeTreeNodeData | TypeTreeNodeData[]) { + return node.append(data); + }, + + /** + * 返回路径节点 + * - 路径节点包含自己在内 + * - 节点顺序与父级节点顺序相反,从根到当前 + * @return TreeNodeModel[] 路径节点数组 + */ + getPath(): TypeTreeNodeModel[] { + const nodes = node.getPath(); + return nodes.map((item: TreeNode) => item.getModel()); + }, + + /** + * 获取本节点的父节点 + * @return TreeNodeModel 父节点 + */ + getParent(): TypeTreeNodeModel { + return node.parent?.getModel(); + }, + + /** + * 获取所有父级节点 + * - 顺序为从当前到根 + * @return TreeNodeModel[] 父级节点数组 + */ + getParents(): TypeTreeNodeModel[] { + const nodes = node.getParents(); + return nodes.map((item: TreeNode) => item.getModel()); + }, + + /** + * 获取本节点的根节点 + * @return TreeNodeModel 根节点 + */ + getRoot(): TypeTreeNodeModel { + const root = node.getRoot(); + return root?.getModel(); + }, + + /** + * 获取所有兄弟节点,包含自己在内 + * @return TreeNodeModel[] 兄弟节点数组 + */ + getSiblings(): TypeTreeNodeModel[] { + const nodes = node.getSiblings(); + return nodes.map((item: TreeNode) => item.getModel()); + }, + + /** + * 获取当前节点的子节点 + * @param {boolean} deep 是否获取所有深层子节点 + * @return TreeNodeModel[] 子节点数组 + */ + getChildren(deep?: boolean): boolean | TypeTreeNodeModel[] { + let childrenModel: boolean | TypeTreeNodeModel[] = false; + const { children } = node; + if (Array.isArray(children)) { + if (children.length > 0) { + if (deep) { + const nodes = node.walk(); + nodes.shift(); + childrenModel = nodes.map((item) => item.getModel()); + } else { + childrenModel = children.map((item) => item.getModel()); + } } else { - childrenModel = children.map((item) => item.getModel()); + childrenModel = false; } - } else { - childrenModel = false; + } else if (isBoolean(children)) { + childrenModel = children; + } + return childrenModel; + }, + + /** + * 移除节点 + * - 提供 value 参数,移除本节点子节点中的节点 + * - 不提供 value 参数,移除自己 + * @param {string} value 目标节点值 + * @return void + */ + remove(value?: TreeNodeValue) { + if (!value) { + node.remove(); + return; } - } else if (isBoolean(children)) { - childrenModel = children; - } - return childrenModel; - } - - /** - * 移除节点 - * - 提供 value 参数,移除本节点子节点中的节点 - * - 不提供 value 参数,移除自己 - * @param {string} value 目标节点值 - * @return void - */ - public remove(value?: TreeNodeValue) { - const node = this[nodeKey]; - if (!value) { - node.remove(); - return; - } - - const { tree } = node; - const targetNode = tree.getNode(value); - if (!targetNode) { - log.warnOnce('Tree', `\`${value}\` is not exist`); - return; - } - const parents = targetNode.getParents(); - const parentValues = parents.map((pnode) => (pnode.value)); - if (parentValues.indexOf(node.value) < 0) { - log.warnOnce('Tree', `\`${value}\` is not a childNode of current node`); - return; - } - targetNode.remove(); - } + const { tree } = node; + const targetNode = tree.getNode(value); + if (!targetNode) { + log.warnOnce('Tree', `\`${value}\` is not exist`); + return; + } - /** - * 设置本节点携带的元数据 - * @param {object} data 节点数据 - * @return void - */ - public setData(data: OptionData) { - const node = this[nodeKey]; - // 详细细节可见 https://github.com/Tencent/tdesign-common/issues/655 - const _data = omit(data, ['children', 'value', 'label', 'disabled']); - const { keys } = node.tree.config; - const dataValue = get(data, keys?.value || 'value'); - const dataLabel = get(data, keys?.label || 'label'); - const dataDisabled = get(data, keys?.disabled || 'disabled'); - if (!isUndefined(dataValue)) _data.value = dataValue; - if (!isUndefined(dataLabel)) _data.label = dataLabel; - if (!isUndefined(dataDisabled)) _data.disable = dataDisabled; - Object.assign(node.data, _data); - Object.assign(node, _data); - node.update(); - } + const parents = targetNode.getParents(); + const parentValues = parents.map((pnode) => (pnode.value)); + if (parentValues.indexOf(node.value) < 0) { + log.warnOnce('Tree', `\`${value}\` is not a childNode of current node`); + return; + } + targetNode.remove(); + }, + + /** + * 设置本节点携带的元数据 + * @param {object} data 节点数据 + * @return void + */ + setData(data: OptionData) { + // 详细细节可见 https://github.com/Tencent/tdesign-common/issues/655 + const _data = omit(data, ['children', 'value', 'label']); + const { keys } = node.tree.config; + const dataValue = data[keys?.value || 'value']; + const dataLabel = data[keys?.label || 'label']; + if (!isUndefined(dataValue)) _data.value = dataValue; + if (!isUndefined(dataLabel)) _data.label = dataLabel; + + Object.assign(node.data, _data); + Object.assign(node, _data); + }, + }; + + return model; } -// 封装对外暴露的对象 -export function createNodeModel(node: TreeNode): TypeTreeNodeModel { - const model = new TreeNodeModel(node); - return model as TypeTreeNodeModel; +/** + * 同步节点属性到封装对象 + * @param {TreeNodeModel} 节点封装对象 + * @param {object} data 节点数据 + * @return void + */ +export function updateNodeModel(model: TypeTreeNodeModel, node: TreeNode) { + // 同步节点属性 + const props = getExposedProps(node); + Object.assign(model, props); } diff --git a/js/tree/tree-node.ts b/js/tree/tree-node.ts index d2f9983347..d1d4df68ac 100644 --- a/js/tree/tree-node.ts +++ b/js/tree/tree-node.ts @@ -17,6 +17,7 @@ import { } from './types'; import { createNodeModel, + updateNodeModel, } from './tree-node-model'; import log from '../log'; @@ -54,9 +55,6 @@ export class TreeNode { // 节点隶属的树实例 public tree: TreeStore; - // 节点私有 id,不接受外部传入,确保唯一性 - public [privateKey]: string; - // 节点 id ,唯一标志 public value: string; @@ -1329,6 +1327,7 @@ export class TreeNode { model = createNodeModel(this); this.model = model; } + updateNodeModel(model, this); return model; } } diff --git a/js/tree/tree-store.ts b/js/tree/tree-store.ts index 672a684ce1..72311a281f 100644 --- a/js/tree/tree-store.ts +++ b/js/tree/tree-store.ts @@ -7,10 +7,11 @@ import camelCase from 'lodash/camelCase'; import isPlainObject from 'lodash/isPlainObject'; import mitt from 'mitt'; -import { TreeNode, privateKey } from './tree-node'; +import { TreeNode } from './tree-node'; import { TreeNodeValue, TypeIdMap, + TypeTimer, TypeTargetNode, TypeTreeNodeData, TypeTreeItem, @@ -19,15 +20,8 @@ import { TypeTreeFilterOptions, TypeRelatedNodesOptions, TypeTreeEventState, - TypeUpdatedMap, } from './types'; -function nextTick(fn: () => void): Promise { - const pm = Promise.resolve(); - pm.then(fn); - return pm; -} - // 构建一个树的数据模型 // 基本设计思想:写入时更新,减少读取消耗,以减少未来实现虚拟滚动所需的计算量 // 任何一次数据写入,会触发相应节点的状态更新 @@ -73,7 +67,7 @@ export class TreeStore { public nodeMap: Map; // 节点 私有 ID 映射 - public privateMap: Map; + public privateMap: Map; // 配置选项 public config: TypeTreeStoreOptions; @@ -82,7 +76,7 @@ export class TreeStore { public activedMap: TypeIdMap; // 数据被更新的节点集合 - public updatedMap: TypeUpdatedMap; + public updatedMap: TypeIdMap; // 选中节点集合 public checkedMap: TypeIdMap; @@ -93,20 +87,20 @@ export class TreeStore { // 符合过滤条件的节点的集合 public filterMap: TypeIdMap; - // 存在过滤器标志 - public hasFilter: boolean; - - // 事件派发器 - public emitter: ReturnType; - // 数据更新计时器 - private updateTick: Promise; + public updateTimer: TypeTimer; // 识别是否需要重排 - private shouldReflow: boolean; + public shouldReflow: boolean; + + // 存在过滤器标志 + public hasFilter: boolean; // 树节点过滤器 - private prevFilter: TypeTreeFilter; + public prevFilter: TypeTreeFilter; + + // 事件派发器 + public emitter: ReturnType; public constructor(options: TypeTreeStoreOptions) { const config: TypeTreeStoreOptions = { @@ -147,7 +141,7 @@ export class TreeStore { this.filterMap = new Map(); this.prevFilter = null; // 这个计时器确保频繁的 update 事件被归纳为1次完整数据更新后的触发 - this.updateTick = null; + this.updateTimer = null; // 在子节点增删改查时,将此属性设置为 true,来触发视图更新 this.shouldReflow = false; // 这个标志会被大量用到 @@ -343,6 +337,10 @@ export class TreeStore { * @return void */ public reload(list: TypeTreeNodeData[]): void { + this.expandedMap.clear(); + this.checkedMap.clear(); + this.activedMap.clear(); + this.filterMap.clear(); this.removeAll(); this.append(list); } @@ -468,6 +466,20 @@ export class TreeStore { }); } + /** + * 更新所有树节点状态 + * @return void + */ + public refreshState(): void { + const { nodeMap } = this; + // 树在初始化未回流时,nodes 数组为空 + // 所以遍历 nodeMap 确保初始化阶段 refreshState 方法也可以触发全部节点的更新 + nodeMap.forEach((node) => { + node.update(); + node.updateChecked(); + }); + } + /** * 标记节点重排 * - 应该仅在树节点增删改查时调用 @@ -489,20 +501,13 @@ export class TreeStore { * @return void */ public updated(node?: TreeNode): void { - const { updatedMap } = this; - if (node) { - // 传入节点,则为指定节点的更新 - updatedMap.set(node[privateKey], 'changed'); - } else { - // reflow 流程不传入节点,需要更新所有节点 - this.getNodes().forEach((itemNode) => { - updatedMap.set(itemNode[privateKey], 'changed'); - }); + if (node?.value) { + this.updatedMap.set(node.value, true); } - - if (this.updateTick) return; - this.updateTick = nextTick(() => { - this.updateTick = null; + if (this.updateTimer) return; + this.updateTimer = setTimeout(() => { + clearTimeout(this.updateTimer); + this.updateTimer = null; // 检查节点是否需要回流,重排数组 if (this.shouldReflow) { @@ -515,23 +520,28 @@ export class TreeStore { // 以便于优化锁定检查算法 this.lockFilterPathNodes(); - // stateId 用于单个节点状态监控 - const stateId = `t${new Date().getTime()}`; - const updatedList = Array.from(updatedMap.keys()); - const updatedNodes = updatedList.map((nodePrivateKey) => { - updatedMap.set(nodePrivateKey, stateId); - return this.privateMap.get(nodePrivateKey); - }); - - // 统计需要更新状态的节点,派发更新事件 - this.emit('update', { - nodes: updatedNodes, - map: updatedMap, - }); + const updatedList = Array.from(this.updatedMap.keys()); + if (updatedList.length > 0) { + // 统计需要更新状态的节点,派发更新事件 + const updatedNodes = updatedList.map((value) => this.getNode(value)); + this.emit('update', { + nodes: updatedNodes, + map: this.updatedMap, + }); + } else if (this.shouldReflow) { + // 单纯的回流不需要更新节点状态 + // 但需要触发更新事件 + // 实际业务中,这个逻辑几乎无法触发,节点操作必然引发 update + // 这里代码仅仅用于边界兜底 + this.emit('update', { + nodes: [], + map: this.updatedMap, + }); + } // 每次回流检查完毕,还原检查状态 this.shouldReflow = false; - updatedMap.clear(); + this.updatedMap.clear(); }); } @@ -779,27 +789,12 @@ export class TreeStore { }); } - /** - * 更新所有树节点状态,但不更新选中态 - * 用于不影响选中态时候的更新,减少递归循环造成的时间消耗 - * @return void - */ - public refreshState(): void { - const { nodeMap } = this; - // 树在初始化未回流时,nodes 数组为空 - // 所以遍历 nodeMap 确保初始化阶段 refreshState 方法也可以触发全部节点的更新 - nodeMap.forEach((node) => { - node.update(); - }); - } - /** * 更新全部节点状态 * @return void */ public updateAll(): void { - const { nodeMap } = this; - nodeMap.forEach((node) => { + this.nodeMap.forEach((node) => { node.update(); node.updateChecked(); }); @@ -822,16 +817,10 @@ export class TreeStore { * @return void */ public removeAll(): void { - this.expandedMap.clear(); - this.checkedMap.clear(); - this.activedMap.clear(); - this.filterMap.clear(); - this.nodeMap.clear(); - this.privateMap.clear(); - this.updatedMap.clear(); - this.nodes = []; - this.children = []; - this.reflow(); + const nodes = this.getNodes(); + nodes.forEach((node) => { + node.remove(); + }); } /** diff --git a/js/tree/types.ts b/js/tree/types.ts index 2bb6d93e1a..fa843a0713 100644 --- a/js/tree/types.ts +++ b/js/tree/types.ts @@ -177,9 +177,9 @@ export interface TreeNodeModel< * 移除当前节点或当前节点的子节点,值为空则移除当前节点,值存在则移除当前节点的子节点 */ remove: (value?: TreeNodeValue) => void; - /** - * 设置当前节点数据,数据变化可自动刷新页面,泛型 `T` 表示树节点 TS 类型 - */ + /** + * 设置当前节点数据,数据变化可自动刷新页面,泛型 `T` 表示树节点 TS 类型 + */ setData: (data: T) => void; } @@ -210,7 +210,7 @@ export interface TypeTreeFilterOptions { } export interface TypeTreeNodeData extends TreeNodeState { - children?: TypeTreeNodeData[] | boolean; + children?: TypeTreeNodeData[]; [key: string]: any; } @@ -220,12 +220,10 @@ export type TypeTreeNodeModel = TreeNodeModel export type TypeTreeFilter = (node: TreeNodeModel) => boolean; -export type TypeUpdatedMap = Map; - export interface TypeTreeEventState { node?: TreeNode; nodes?: TreeNode[]; - map?: TypeUpdatedMap; + map?: TypeIdMap; data?: TypeTreeNodeData[]; } diff --git a/js/upload/utils.ts b/js/upload/utils.ts index 8681d1dd8a..6ffdf460c8 100644 --- a/js/upload/utils.ts +++ b/js/upload/utils.ts @@ -10,11 +10,34 @@ export const FILE_PPT_REGEXP = /(.ppt|.pptx|.key)/i; export const VIDEO_REGEXP = /(.avi|.mp4|.wmv|.mpg|.mpeg|.mov|.rm|.ram|.swf|.flv|.rmvb|.flash|.mid|.3gp)/i; export const AUDIO_REGEXP = /(.mp2|.mp3|.mp4|.ogg|.3gpp|.ac3|.au)/i; +/** + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + */ const INPUT_FILE_MAP = { 'audio/*': AUDIO_REGEXP, 'video/*': VIDEO_REGEXP, 'image/*': IMAGE_ALL_REGEXP, - '.doc': /(.doc|.msword)/, + '.ico': /image\/vnd.microsoft.icon/i, + '.doc': /application\/msword/i, + '.docx': /application\/vnd.openxmlformats-officedocument.wordprocessingml.document/i, + '.xls': /application\/vnd.ms-excel/i, + '.xlsx': /application\/vnd.openxmlformats-officedocument.spreadsheetml.sheet/i, + '.ppt': /application\/vnd.ms-powerpoint/i, + '.pptx': /application\/vnd.openxmlformats-officedocument.presentationml.presentation/i, + '.vsd': /application\/vnd.visio/i, + '.txt': /text\/plain/i, + '.abw': /application\/x-abiword/i, + '.avi': /video\/x-msvideo/i, + '.azw': /application\/vnd.amazon.ebook/i, + '.bin': /application\/octet-stream/i, + '.cda': /application\/x-cdf/i, + '.mpkg': /application\/vnd.apple.installer+xml/i, + '.odp': /application\/vnd.oasis.opendocument.presentation/i, + '.ods': /application\/vnd.oasis.opendocument.spreadsheet/i, + '.odt': /application\/vnd.oasis.opendocument.text/i, + '.oga': /audio\/ogg/i, + '.ogv':/video\/ogg/i, + '.ogx':/application\/ogg/i, }; /**