From a0f7e8d21f6d7c337551a82f6682ba46dffb8633 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 17 Aug 2021 22:03:49 +0800 Subject: [PATCH] refactor: tree --- components/_util/hooks/useConfigInject.ts | 3 + components/style/themes/default.less | 1 + components/tree/DirectoryTree copy.tsx | 251 +++++++++ components/tree/DirectoryTree.tsx | 232 +------- components/tree/Tree copy.tsx | 289 ++++++++++ components/tree/Tree.tsx | 529 ++++++++++-------- components/tree/index.tsx | 15 + components/tree/style/directory.less | 127 ++--- components/tree/style/index.less | 272 +-------- components/tree/style/{index.ts => index.tsx} | 0 components/tree/style/mixin.less | 271 ++++++++- components/tree/style/rtl.less | 72 +++ components/tree/utils/dictUtil.ts | 92 +++ components/tree/utils/dropIndicator.tsx | 33 ++ components/tree/utils/iconUtil.tsx | 52 ++ components/upload/Upload.tsx | 4 +- components/vc-tree-select/src/util.js | 2 +- components/vc-tree/MotionTreeNode.tsx | 2 +- components/vc-tree/NodeList.tsx | 7 +- components/vc-tree/Tree.tsx | 62 +- components/vc-tree/TreeNode.tsx | 7 +- components/vc-tree/contextTypes.ts | 3 +- components/vc-tree/index.ts | 4 +- components/vc-tree/interface.tsx | 6 +- components/vc-tree/props.ts | 15 +- examples/App.vue | 85 ++- 26 files changed, 1545 insertions(+), 891 deletions(-) create mode 100644 components/tree/DirectoryTree copy.tsx create mode 100644 components/tree/Tree copy.tsx rename components/tree/style/{index.ts => index.tsx} (100%) create mode 100644 components/tree/style/rtl.less create mode 100644 components/tree/utils/dictUtil.ts create mode 100644 components/tree/utils/dropIndicator.tsx create mode 100644 components/tree/utils/iconUtil.tsx diff --git a/components/_util/hooks/useConfigInject.ts b/components/_util/hooks/useConfigInject.ts index 93f18e1f8..d7b64dbbb 100644 --- a/components/_util/hooks/useConfigInject.ts +++ b/components/_util/hooks/useConfigInject.ts @@ -20,6 +20,7 @@ export default ( }>; autoInsertSpaceInButton: ComputedRef; renderEmpty?: ComputedRef<(componentName?: string) => VNodeChild | JSX.Element>; + virtual: ComputedRef; } => { const configProvider = inject>( 'configProvider', @@ -34,6 +35,7 @@ export default ( const form = computed(() => configProvider.form); const size = computed(() => props.size || configProvider.componentSize); const getTargetContainer = computed(() => props.getTargetContainer); + const virtual = computed(() => props.virtual); return { configProvider, prefixCls, @@ -45,5 +47,6 @@ export default ( form, autoInsertSpaceInButton, renderEmpty, + virtual, }; }; diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 1b9b0c301..3808946a6 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -747,6 +747,7 @@ // Tree // --- +@tree-bg: @component-background; @tree-title-height: 24px; @tree-child-padding: 18px; @tree-directory-selected-color: #fff; diff --git a/components/tree/DirectoryTree copy.tsx b/components/tree/DirectoryTree copy.tsx new file mode 100644 index 000000000..3e096b36e --- /dev/null +++ b/components/tree/DirectoryTree copy.tsx @@ -0,0 +1,251 @@ +import type { VNode } from 'vue'; +import { defineComponent, inject } from 'vue'; +import omit from 'omit.js'; +import debounce from 'lodash-es/debounce'; +import FolderOpenOutlined from '@ant-design/icons-vue/FolderOpenOutlined'; +import FolderOutlined from '@ant-design/icons-vue/FolderOutlined'; +import FileOutlined from '@ant-design/icons-vue/FileOutlined'; +import PropTypes from '../_util/vue-types'; +import classNames from '../_util/classNames'; +import { conductExpandParent, convertTreeToEntities } from '../vc-tree/src/util'; +import type { CheckEvent, ExpendEvent, SelectEvent } from './Tree'; +import Tree, { TreeProps } from './Tree'; +import { + calcRangeKeys, + getFullKeyList, + convertDirectoryKeysToNodes, + getFullKeyListByTreeData, +} from './util'; +import BaseMixin from '../_util/BaseMixin'; +import { getOptionProps, getComponent, getSlot } from '../_util/props-util'; +import initDefaultProps from '../_util/props-util/initDefaultProps'; +import { defaultConfigProvider } from '../config-provider'; + +// export type ExpandAction = false | 'click' | 'dblclick'; export interface +// DirectoryTreeProps extends TreeProps { expandAction?: ExpandAction; } +// export interface DirectoryTreeState { expandedKeys?: string[]; +// selectedKeys?: string[]; } + +export interface DirectoryTreeState { + _expandedKeys?: (string | number)[]; + _selectedKeys?: (string | number)[]; +} + +function getIcon(props: { isLeaf: boolean; expanded: boolean } & VNode) { + const { isLeaf, expanded } = props; + if (isLeaf) { + return ; + } + return expanded ? : ; +} + +export default defineComponent({ + name: 'ADirectoryTree', + mixins: [BaseMixin], + inheritAttrs: false, + props: initDefaultProps( + { + ...TreeProps(), + expandAction: PropTypes.oneOf([false, 'click', 'doubleclick', 'dblclick']), + }, + { + showIcon: true, + expandAction: 'click', + }, + ), + setup() { + return { + children: null, + onDebounceExpand: null, + tree: null, + lastSelectedKey: '', + cachedSelectedKeys: [], + configProvider: inject('configProvider', defaultConfigProvider), + }; + }, + data() { + const props = getOptionProps(this); + const { defaultExpandAll, defaultExpandParent, expandedKeys, defaultExpandedKeys } = props; + const children = getSlot(this); + const { keyEntities } = convertTreeToEntities(children); + const state: DirectoryTreeState = {}; + // Selected keys + state._selectedKeys = props.selectedKeys || props.defaultSelectedKeys || []; + + // Expanded keys + if (defaultExpandAll) { + if (props.treeData) { + state._expandedKeys = getFullKeyListByTreeData(props.treeData, props.replaceFields); + } else { + state._expandedKeys = getFullKeyList(children); + } + } else if (defaultExpandParent) { + state._expandedKeys = conductExpandParent(expandedKeys || defaultExpandedKeys, keyEntities); + } else { + state._expandedKeys = expandedKeys || defaultExpandedKeys; + } + return { + _selectedKeys: [], + _expandedKeys: [], + ...state, + }; + }, + watch: { + expandedKeys(val) { + this.setState({ _expandedKeys: val }); + }, + selectedKeys(val) { + this.setState({ _selectedKeys: val }); + }, + }, + created() { + this.onDebounceExpand = debounce(this.expandFolderNode, 200, { leading: true }); + }, + methods: { + handleExpand(expandedKeys: (string | number)[], info: ExpendEvent) { + this.setUncontrolledState({ _expandedKeys: expandedKeys }); + this.$emit('update:expandedKeys', expandedKeys); + this.$emit('expand', expandedKeys, info); + + return undefined; + }, + + handleClick(event: MouseEvent, node: VNode) { + const { expandAction } = this.$props; + + // Expand the tree + if (expandAction === 'click') { + this.onDebounceExpand(event, node); + } + this.$emit('click', event, node); + }, + + handleDoubleClick(event: MouseEvent, node: VNode) { + const { expandAction } = this.$props; + + // Expand the tree + if (expandAction === 'dblclick' || expandAction === 'doubleclick') { + this.onDebounceExpand(event, node); + } + + this.$emit('doubleclick', event, node); + this.$emit('dblclick', event, node); + }, + + hanldeSelect(keys: (string | number)[], event: SelectEvent) { + const { multiple } = this.$props; + const children = this.children || []; + const { _expandedKeys: expandedKeys = [] } = this.$data; + const { node, nativeEvent } = event; + const { eventKey = '' } = node; + + const newState: DirectoryTreeState = {}; + + // We need wrap this event since some value is not same + const newEvent = { + ...event, + selected: true, // Directory selected always true + }; + + // Windows / Mac single pick + const ctrlPick = nativeEvent.ctrlKey || nativeEvent.metaKey; + const shiftPick = nativeEvent.shiftKey; + + // Generate new selected keys + let newSelectedKeys: (string | number)[]; + if (multiple && ctrlPick) { + // Control click + newSelectedKeys = keys; + this.lastSelectedKey = eventKey; + this.cachedSelectedKeys = newSelectedKeys; + newEvent.selectedNodes = convertDirectoryKeysToNodes(children, newSelectedKeys); + } else if (multiple && shiftPick) { + // Shift click + newSelectedKeys = Array.from( + new Set([ + ...(this.cachedSelectedKeys || []), + ...calcRangeKeys(children, expandedKeys, eventKey, this.lastSelectedKey), + ]), + ); + newEvent.selectedNodes = convertDirectoryKeysToNodes(children, newSelectedKeys); + } else { + // Single click + newSelectedKeys = [eventKey]; + this.lastSelectedKey = eventKey; + this.cachedSelectedKeys = newSelectedKeys; + newEvent.selectedNodes = [event.node]; + } + newState._selectedKeys = newSelectedKeys; + + this.$emit('update:selectedKeys', newSelectedKeys); + this.$emit('select', newSelectedKeys, newEvent); + + this.setUncontrolledState(newState); + }, + setTreeRef(node: VNode) { + this.tree = node; + }, + + expandFolderNode(event: MouseEvent, node: { isLeaf: boolean } & VNode) { + const { isLeaf } = node; + + if (isLeaf || event.shiftKey || event.metaKey || event.ctrlKey) { + return; + } + + if (this.tree.tree) { + // Get internal vc-tree + const internalTree = this.tree.tree; + + // Call internal rc-tree expand function + // https://github.com/ant-design/ant-design/issues/12567 + internalTree.onNodeExpand(event, node); + } + }, + + setUncontrolledState(state: unknown) { + const newState = omit( + state, + Object.keys(getOptionProps(this)).map(p => `_${p}`), + ); + if (Object.keys(newState).length) { + this.setState(newState); + } + }, + handleCheck(checkedObj: (string | number)[], eventObj: CheckEvent) { + this.$emit('update:checkedKeys', checkedObj); + this.$emit('check', checkedObj, eventObj); + }, + }, + + render() { + this.children = getSlot(this); + const { prefixCls: customizePrefixCls, ...props } = getOptionProps(this); + const getPrefixCls = this.configProvider.getPrefixCls; + const prefixCls = getPrefixCls('tree', customizePrefixCls); + const { _expandedKeys: expandedKeys, _selectedKeys: selectedKeys } = this.$data; + const { class: className, ...restAttrs } = this.$attrs; + const connectClassName = classNames(`${prefixCls}-directory`, className); + const treeProps = { + icon: getIcon, + ...restAttrs, + ...omit(props, ['onUpdate:selectedKeys', 'onUpdate:checkedKeys', 'onUpdate:expandedKeys']), + prefixCls, + expandedKeys, + selectedKeys, + switcherIcon: getComponent(this, 'switcherIcon'), + ref: this.setTreeRef, + class: connectClassName, + onSelect: this.hanldeSelect, + onClick: this.handleClick, + onDblclick: this.handleDoubleClick, + onExpand: this.handleExpand, + onCheck: this.handleCheck, + }; + return ( + + {this.children} + + ); + }, +}); diff --git a/components/tree/DirectoryTree.tsx b/components/tree/DirectoryTree.tsx index 3e096b36e..55c958b9c 100644 --- a/components/tree/DirectoryTree.tsx +++ b/components/tree/DirectoryTree.tsx @@ -1,4 +1,4 @@ -import type { VNode } from 'vue'; +import type { ExtractPropTypes, PropType, VNode } from 'vue'; import { defineComponent, inject } from 'vue'; import omit from 'omit.js'; import debounce from 'lodash-es/debounce'; @@ -7,8 +7,7 @@ import FolderOutlined from '@ant-design/icons-vue/FolderOutlined'; import FileOutlined from '@ant-design/icons-vue/FileOutlined'; import PropTypes from '../_util/vue-types'; import classNames from '../_util/classNames'; -import { conductExpandParent, convertTreeToEntities } from '../vc-tree/src/util'; -import type { CheckEvent, ExpendEvent, SelectEvent } from './Tree'; +import { treeProps } from './Tree'; import Tree, { TreeProps } from './Tree'; import { calcRangeKeys, @@ -16,20 +15,11 @@ import { convertDirectoryKeysToNodes, getFullKeyListByTreeData, } from './util'; -import BaseMixin from '../_util/BaseMixin'; import { getOptionProps, getComponent, getSlot } from '../_util/props-util'; import initDefaultProps from '../_util/props-util/initDefaultProps'; import { defaultConfigProvider } from '../config-provider'; -// export type ExpandAction = false | 'click' | 'dblclick'; export interface -// DirectoryTreeProps extends TreeProps { expandAction?: ExpandAction; } -// export interface DirectoryTreeState { expandedKeys?: string[]; -// selectedKeys?: string[]; } - -export interface DirectoryTreeState { - _expandedKeys?: (string | number)[]; - _selectedKeys?: (string | number)[]; -} +export type ExpandAction = false | 'click' | 'doubleClick' | 'dblclick'; function getIcon(props: { isLeaf: boolean; expanded: boolean } & VNode) { const { isLeaf, expanded } = props; @@ -39,213 +29,23 @@ function getIcon(props: { isLeaf: boolean; expanded: boolean } & VNode) { return expanded ? : ; } +const directoryTreeProps = { + ...treeProps(), + expandAction: { type: [Boolean, String] as PropType }, +}; + +export type DirectoryTreeProps = Partial>; + export default defineComponent({ name: 'ADirectoryTree', - mixins: [BaseMixin], inheritAttrs: false, - props: initDefaultProps( - { - ...TreeProps(), - expandAction: PropTypes.oneOf([false, 'click', 'doubleclick', 'dblclick']), - }, - { - showIcon: true, - expandAction: 'click', - }, - ), + props: initDefaultProps(directoryTreeProps, { + showIcon: true, + expandAction: 'click', + }), setup() { - return { - children: null, - onDebounceExpand: null, - tree: null, - lastSelectedKey: '', - cachedSelectedKeys: [], - configProvider: inject('configProvider', defaultConfigProvider), + return () => { + return null; }; }, - data() { - const props = getOptionProps(this); - const { defaultExpandAll, defaultExpandParent, expandedKeys, defaultExpandedKeys } = props; - const children = getSlot(this); - const { keyEntities } = convertTreeToEntities(children); - const state: DirectoryTreeState = {}; - // Selected keys - state._selectedKeys = props.selectedKeys || props.defaultSelectedKeys || []; - - // Expanded keys - if (defaultExpandAll) { - if (props.treeData) { - state._expandedKeys = getFullKeyListByTreeData(props.treeData, props.replaceFields); - } else { - state._expandedKeys = getFullKeyList(children); - } - } else if (defaultExpandParent) { - state._expandedKeys = conductExpandParent(expandedKeys || defaultExpandedKeys, keyEntities); - } else { - state._expandedKeys = expandedKeys || defaultExpandedKeys; - } - return { - _selectedKeys: [], - _expandedKeys: [], - ...state, - }; - }, - watch: { - expandedKeys(val) { - this.setState({ _expandedKeys: val }); - }, - selectedKeys(val) { - this.setState({ _selectedKeys: val }); - }, - }, - created() { - this.onDebounceExpand = debounce(this.expandFolderNode, 200, { leading: true }); - }, - methods: { - handleExpand(expandedKeys: (string | number)[], info: ExpendEvent) { - this.setUncontrolledState({ _expandedKeys: expandedKeys }); - this.$emit('update:expandedKeys', expandedKeys); - this.$emit('expand', expandedKeys, info); - - return undefined; - }, - - handleClick(event: MouseEvent, node: VNode) { - const { expandAction } = this.$props; - - // Expand the tree - if (expandAction === 'click') { - this.onDebounceExpand(event, node); - } - this.$emit('click', event, node); - }, - - handleDoubleClick(event: MouseEvent, node: VNode) { - const { expandAction } = this.$props; - - // Expand the tree - if (expandAction === 'dblclick' || expandAction === 'doubleclick') { - this.onDebounceExpand(event, node); - } - - this.$emit('doubleclick', event, node); - this.$emit('dblclick', event, node); - }, - - hanldeSelect(keys: (string | number)[], event: SelectEvent) { - const { multiple } = this.$props; - const children = this.children || []; - const { _expandedKeys: expandedKeys = [] } = this.$data; - const { node, nativeEvent } = event; - const { eventKey = '' } = node; - - const newState: DirectoryTreeState = {}; - - // We need wrap this event since some value is not same - const newEvent = { - ...event, - selected: true, // Directory selected always true - }; - - // Windows / Mac single pick - const ctrlPick = nativeEvent.ctrlKey || nativeEvent.metaKey; - const shiftPick = nativeEvent.shiftKey; - - // Generate new selected keys - let newSelectedKeys: (string | number)[]; - if (multiple && ctrlPick) { - // Control click - newSelectedKeys = keys; - this.lastSelectedKey = eventKey; - this.cachedSelectedKeys = newSelectedKeys; - newEvent.selectedNodes = convertDirectoryKeysToNodes(children, newSelectedKeys); - } else if (multiple && shiftPick) { - // Shift click - newSelectedKeys = Array.from( - new Set([ - ...(this.cachedSelectedKeys || []), - ...calcRangeKeys(children, expandedKeys, eventKey, this.lastSelectedKey), - ]), - ); - newEvent.selectedNodes = convertDirectoryKeysToNodes(children, newSelectedKeys); - } else { - // Single click - newSelectedKeys = [eventKey]; - this.lastSelectedKey = eventKey; - this.cachedSelectedKeys = newSelectedKeys; - newEvent.selectedNodes = [event.node]; - } - newState._selectedKeys = newSelectedKeys; - - this.$emit('update:selectedKeys', newSelectedKeys); - this.$emit('select', newSelectedKeys, newEvent); - - this.setUncontrolledState(newState); - }, - setTreeRef(node: VNode) { - this.tree = node; - }, - - expandFolderNode(event: MouseEvent, node: { isLeaf: boolean } & VNode) { - const { isLeaf } = node; - - if (isLeaf || event.shiftKey || event.metaKey || event.ctrlKey) { - return; - } - - if (this.tree.tree) { - // Get internal vc-tree - const internalTree = this.tree.tree; - - // Call internal rc-tree expand function - // https://github.com/ant-design/ant-design/issues/12567 - internalTree.onNodeExpand(event, node); - } - }, - - setUncontrolledState(state: unknown) { - const newState = omit( - state, - Object.keys(getOptionProps(this)).map(p => `_${p}`), - ); - if (Object.keys(newState).length) { - this.setState(newState); - } - }, - handleCheck(checkedObj: (string | number)[], eventObj: CheckEvent) { - this.$emit('update:checkedKeys', checkedObj); - this.$emit('check', checkedObj, eventObj); - }, - }, - - render() { - this.children = getSlot(this); - const { prefixCls: customizePrefixCls, ...props } = getOptionProps(this); - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('tree', customizePrefixCls); - const { _expandedKeys: expandedKeys, _selectedKeys: selectedKeys } = this.$data; - const { class: className, ...restAttrs } = this.$attrs; - const connectClassName = classNames(`${prefixCls}-directory`, className); - const treeProps = { - icon: getIcon, - ...restAttrs, - ...omit(props, ['onUpdate:selectedKeys', 'onUpdate:checkedKeys', 'onUpdate:expandedKeys']), - prefixCls, - expandedKeys, - selectedKeys, - switcherIcon: getComponent(this, 'switcherIcon'), - ref: this.setTreeRef, - class: connectClassName, - onSelect: this.hanldeSelect, - onClick: this.handleClick, - onDblclick: this.handleDoubleClick, - onExpand: this.handleExpand, - onCheck: this.handleCheck, - }; - return ( - - {this.children} - - ); - }, }); diff --git a/components/tree/Tree copy.tsx b/components/tree/Tree copy.tsx new file mode 100644 index 000000000..dc4230ffd --- /dev/null +++ b/components/tree/Tree copy.tsx @@ -0,0 +1,289 @@ +import type { VNode, PropType, CSSProperties } from 'vue'; +import { defineComponent, inject } from 'vue'; +import classNames from '../_util/classNames'; +import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; +import FileOutlined from '@ant-design/icons-vue/FileOutlined'; +import CaretDownFilled from '@ant-design/icons-vue/CaretDownFilled'; +import MinusSquareOutlined from '@ant-design/icons-vue/MinusSquareOutlined'; +import PlusSquareOutlined from '@ant-design/icons-vue/PlusSquareOutlined'; +import VcTree, { TreeNode } from '../vc-tree'; +import animation from '../_util/openAnimation'; +import PropTypes from '../_util/vue-types'; +import { getOptionProps, getComponent, getSlot } from '../_util/props-util'; +import initDefaultProps from '../_util/props-util/initDefaultProps'; +import { cloneElement } from '../_util/vnode'; +import { defaultConfigProvider } from '../config-provider'; + +export interface TreeDataItem { + key?: string | number; + title?: string; + isLeaf?: boolean; + selectable?: boolean; + children?: TreeDataItem[]; + disableCheckbox?: boolean; + disabled?: boolean; + class?: string; + style?: CSSProperties; + checkable?: boolean; + icon?: VNode; + + slots?: Record; + switcherIcon?: VNode; + // support custom field + [key: string]: any; +} + +interface DefaultEvent { + nativeEvent: MouseEvent; + node: Record; +} + +export interface CheckEvent extends DefaultEvent { + checked: boolean; + checkedNodes: Array>; + checkedNodesPositions: { node: Record; pos: string | number }[]; + event: string; + halfCheckedKeys: (string | number)[]; +} + +export interface ExpendEvent extends DefaultEvent { + expanded: boolean; +} + +export interface SelectEvent extends DefaultEvent { + event: string; + selected: boolean; + selectedNodes: Array>; +} + +export interface TreeDragEvent { + event: DragEvent; + expandedKeys: (string | number)[]; + node: Record; +} + +export interface DropEvent { + dragNode: Record; + dragNodesKeys: (string | number)[]; + dropPosition: number; + dropToGap: boolean; + event: DragEvent; + node: Record; +} + +function TreeProps() { + return { + showLine: PropTypes.looseBool, + /** 是否支持多选 */ + multiple: PropTypes.looseBool, + /** 是否自动展开父节点 */ + autoExpandParent: PropTypes.looseBool, + /** checkable状态下节点选择完全受控(父子节点选中状态不再关联)*/ + checkStrictly: PropTypes.looseBool, + /** 是否支持选中 */ + checkable: PropTypes.looseBool, + /** 是否禁用树 */ + disabled: PropTypes.looseBool, + /** 默认展开所有树节点 */ + defaultExpandAll: PropTypes.looseBool, + /** 默认展开对应树节点 */ + defaultExpandParent: PropTypes.looseBool, + /** 默认展开指定的树节点 */ + defaultExpandedKeys: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + ), + /** (受控)展开指定的树节点 */ + expandedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + /** (受控)选中复选框的树节点 */ + checkedKeys: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + PropTypes.shape({ + checked: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + halfChecked: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + }).loose, + ]), + /** 默认选中复选框的树节点 */ + defaultCheckedKeys: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + ), + /** (受控)设置选中的树节点 */ + selectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + /** 默认选中的树节点 */ + defaultSelectedKeys: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + ), + selectable: PropTypes.looseBool, + + /** filter some AntTreeNodes as you need. it should return true */ + filterAntTreeNode: PropTypes.func, + /** 异步加载数据 */ + loadData: PropTypes.func, + loadedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + // onLoaded: (loadedKeys: string[], info: { event: 'load', node: AntTreeNode; }) => void, + /** 响应右键点击 */ + // onRightClick: (options: AntTreeNodeMouseEvent) => void, + /** 设置节点可拖拽(IE>8)*/ + draggable: PropTypes.looseBool, + // /** 开始拖拽时调用 */ + // onDragStart: (options: AntTreeNodeMouseEvent) => void, + // /** dragenter 触发时调用 */ + // onDragEnter: (options: AntTreeNodeMouseEvent) => void, + // /** dragover 触发时调用 */ + // onDragOver: (options: AntTreeNodeMouseEvent) => void, + // /** dragleave 触发时调用 */ + // onDragLeave: (options: AntTreeNodeMouseEvent) => void, + // /** drop 触发时调用 */ + // onDrop: (options: AntTreeNodeMouseEvent) => void, + showIcon: PropTypes.looseBool, + icon: PropTypes.func, + switcherIcon: PropTypes.any, + prefixCls: PropTypes.string, + filterTreeNode: PropTypes.func, + openAnimation: PropTypes.any, + treeData: { + type: Array as PropType, + }, + /** + * @default{title,key,children} + * 替换treeNode中 title,key,children字段为treeData中对应的字段 + */ + replaceFields: PropTypes.object, + blockNode: PropTypes.looseBool, + /** 展开/收起节点时触发 */ + onExpand: PropTypes.func, + /** 点击复选框触发 */ + onCheck: PropTypes.func, + /** 点击树节点触发 */ + onSelect: PropTypes.func, + /** 单击树节点触发 */ + onClick: PropTypes.func, + /** 双击树节点触发 */ + onDoubleclick: PropTypes.func, + onDblclick: PropTypes.func, + 'onUpdate:selectedKeys': PropTypes.func, + 'onUpdate:checkedKeys': PropTypes.func, + 'onUpdate:expandedKeys': PropTypes.func, + }; +} + +export { TreeProps }; + +export default defineComponent({ + name: 'ATree', + inheritAttrs: false, + props: initDefaultProps(TreeProps(), { + checkable: false, + showIcon: false, + openAnimation: { + ...animation, + appear: null, + }, + blockNode: false, + }), + setup() { + return { + tree: null, + configProvider: inject('configProvider', defaultConfigProvider), + }; + }, + TreeNode, + methods: { + renderSwitcherIcon(prefixCls: string, switcherIcon: VNode, { isLeaf, loading, expanded }) { + const { showLine } = this.$props; + if (loading) { + return ; + } + + if (isLeaf) { + return showLine ? : null; + } + const switcherCls = `${prefixCls}-switcher-icon`; + if (switcherIcon) { + return cloneElement(switcherIcon, { + class: switcherCls, + }); + } + return showLine ? ( + expanded ? ( + + ) : ( + + ) + ) : ( + + ); + }, + updateTreeData(treeData: TreeDataItem[]) { + const { $slots } = this; + const defaultFields = { children: 'children', title: 'title', key: 'key' }; + const replaceFields = { ...defaultFields, ...this.$props.replaceFields }; + return treeData.map(item => { + const key = item[replaceFields.key]; + const children = item[replaceFields.children]; + const { slots = {}, class: cls, style, ...restProps } = item; + const treeNodeProps = { + ...restProps, + icon: $slots[slots.icon] || restProps.icon, + switcherIcon: $slots[slots.switcherIcon] || restProps.switcherIcon, + title: $slots[slots.title] || $slots.title || restProps[replaceFields.title], + dataRef: item, + key, + class: cls, + style, + }; + if (children) { + return { ...treeNodeProps, children: this.updateTreeData(children) }; + } + return treeNodeProps; + }); + }, + setTreeRef(node: VNode) { + this.tree = node; + }, + handleCheck(checkedObj: (number | string)[], eventObj: CheckEvent) { + this.$emit('update:checkedKeys', checkedObj); + this.$emit('check', checkedObj, eventObj); + }, + handleExpand(expandedKeys: (number | string)[], eventObj: ExpendEvent) { + this.$emit('update:expandedKeys', expandedKeys); + this.$emit('expand', expandedKeys, eventObj); + }, + handleSelect(selectedKeys: (number | string)[], eventObj: SelectEvent) { + this.$emit('update:selectedKeys', selectedKeys); + this.$emit('select', selectedKeys, eventObj); + }, + }, + render() { + const props = getOptionProps(this); + const { prefixCls: customizePrefixCls, showIcon, treeNodes, blockNode } = props; + const getPrefixCls = this.configProvider.getPrefixCls; + const prefixCls = getPrefixCls('tree', customizePrefixCls); + const switcherIcon = getComponent(this, 'switcherIcon'); + const checkable = props.checkable; + let treeData = props.treeData || treeNodes; + if (treeData) { + treeData = this.updateTreeData(treeData); + } + const { class: className, ...restAttrs } = this.$attrs; + const vcTreeProps = { + ...props, + prefixCls, + checkable: checkable ? : checkable, + children: getSlot(this), + switcherIcon: nodeProps => this.renderSwitcherIcon(prefixCls, switcherIcon, nodeProps), + ref: this.setTreeRef, + ...restAttrs, + class: classNames(className, { + [`${prefixCls}-icon-hide`]: !showIcon, + [`${prefixCls}-block-node`]: blockNode, + }), + onCheck: this.handleCheck, + onExpand: this.handleExpand, + onSelect: this.handleSelect, + } as Record; + if (treeData) { + vcTreeProps.treeData = treeData; + } + return ; + }, +}); diff --git a/components/tree/Tree.tsx b/components/tree/Tree.tsx index b01c4fcd8..a1426969e 100644 --- a/components/tree/Tree.tsx +++ b/components/tree/Tree.tsx @@ -1,290 +1,347 @@ -import type { VNode, PropType, CSSProperties } from 'vue'; -import { defineComponent, inject } from 'vue'; +import { VNode, PropType, DefineComponent, ExtractPropTypes, ref } from 'vue'; +import { defineComponent } from 'vue'; import classNames from '../_util/classNames'; -import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; -import FileOutlined from '@ant-design/icons-vue/FileOutlined'; -import CaretDownFilled from '@ant-design/icons-vue/CaretDownFilled'; -import MinusSquareOutlined from '@ant-design/icons-vue/MinusSquareOutlined'; -import PlusSquareOutlined from '@ant-design/icons-vue/PlusSquareOutlined'; -import VcTree from '../vc-tree'; +import VcTree, { TreeNode } from '../vc-tree'; import animation from '../_util/openAnimation'; import PropTypes from '../_util/vue-types'; -import { getOptionProps, getComponent, getSlot } from '../_util/props-util'; +import { filterEmpty } from '../_util/props-util'; import initDefaultProps from '../_util/props-util/initDefaultProps'; -import { cloneElement } from '../_util/vnode'; -import { defaultConfigProvider } from '../config-provider'; +import { DataNode, FieldNames, Key } from '../vc-tree/interface'; +import { treeProps as vcTreeProps } from '../vc-tree/props'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import renderSwitcherIcon from './utils/iconUtil'; +import dropIndicatorRender from './utils/dropIndicator'; -const TreeNode = VcTree.TreeNode; - -export interface TreeDataItem { - key?: string | number; - title?: string; - isLeaf?: boolean; - selectable?: boolean; - children?: TreeDataItem[]; - disableCheckbox?: boolean; - disabled?: boolean; - class?: string; - style?: CSSProperties; - checkable?: boolean; - icon?: VNode; - slots?: Record; - switcherIcon?: VNode; - // support custom field - [key: string]: any; -} - -interface DefaultEvent { - nativeEvent: MouseEvent; - node: Record; -} - -export interface CheckEvent extends DefaultEvent { - checked: boolean; - checkedNodes: Array>; - checkedNodesPositions: { node: Record; pos: string | number }[]; - event: string; - halfCheckedKeys: (string | number)[]; -} - -export interface ExpendEvent extends DefaultEvent { +export interface AntdTreeNodeAttribute { + eventKey: string; + prefixCls: string; + className: string; expanded: boolean; -} - -export interface SelectEvent extends DefaultEvent { - event: string; selected: boolean; - selectedNodes: Array>; + checked: boolean; + halfChecked: boolean; + children: any; + title: any; + pos: string; + dragOver: boolean; + dragOverGapTop: boolean; + dragOverGapBottom: boolean; + isLeaf: boolean; + selectable: boolean; + disabled: boolean; + disableCheckbox: boolean; } -export interface TreeDragEvent { +export interface AntTreeNodeProps { + className?: string; + checkable?: boolean; + disabled?: boolean; + disableCheckbox?: boolean; + title?: string | any; + key?: Key; + eventKey?: string; + isLeaf?: boolean; + checked?: boolean; + expanded?: boolean; + loading?: boolean; + selected?: boolean; + selectable?: boolean; + icon?: ((treeNode: AntdTreeNodeAttribute) => any) | VNode; + children?: any; + [customProp: string]: any; +} + +export interface AntTreeNode extends DefineComponent {} + +export interface AntTreeNodeBaseEvent { + node: AntTreeNode; + nativeEvent: MouseEvent; +} + +export interface AntTreeNodeCheckedEvent extends AntTreeNodeBaseEvent { + event: 'check'; + checked?: boolean; + checkedNodes?: AntTreeNode[]; +} + +export interface AntTreeNodeSelectedEvent extends AntTreeNodeBaseEvent { + event: 'select'; + selected?: boolean; + selectedNodes?: DataNode[]; +} + +export interface AntTreeNodeExpandedEvent extends AntTreeNodeBaseEvent { + expanded?: boolean; +} + +export interface AntTreeNodeMouseEvent { + node: AntTreeNode; event: DragEvent; - expandedKeys: (string | number)[]; - node: Record; } -export interface DropEvent { - dragNode: Record; - dragNodesKeys: (string | number)[]; +export interface AntTreeNodeDragEnterEvent extends AntTreeNodeMouseEvent { + expandedKeys: Key[]; +} + +export interface AntTreeNodeDropEvent { + node: AntTreeNode; + dragNode: AntTreeNode; + dragNodesKeys: Key[]; dropPosition: number; - dropToGap: boolean; - event: DragEvent; - node: Record; + dropToGap?: boolean; + event: MouseEvent; } -function TreeProps() { +// [Legacy] Compatible for v2 +export type TreeDataItem = DataNode; + +export const treeProps = () => { return { - showLine: PropTypes.looseBool, + ...vcTreeProps(), + showLine: { type: Boolean, default: undefined }, /** 是否支持多选 */ - multiple: PropTypes.looseBool, + multiple: { type: Boolean, default: undefined }, /** 是否自动展开父节点 */ - autoExpandParent: PropTypes.looseBool, + autoExpandParent: { type: Boolean, default: undefined }, /** checkable状态下节点选择完全受控(父子节点选中状态不再关联)*/ - checkStrictly: PropTypes.looseBool, + checkStrictly: { type: Boolean, default: undefined }, /** 是否支持选中 */ - checkable: PropTypes.looseBool, + checkable: { type: Boolean, default: undefined }, /** 是否禁用树 */ - disabled: PropTypes.looseBool, + disabled: { type: Boolean, default: undefined }, /** 默认展开所有树节点 */ - defaultExpandAll: PropTypes.looseBool, + defaultExpandAll: { type: Boolean, default: undefined }, /** 默认展开对应树节点 */ - defaultExpandParent: PropTypes.looseBool, + defaultExpandParent: { type: Boolean, default: undefined }, /** 默认展开指定的树节点 */ - defaultExpandedKeys: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - ), + defaultExpandedKeys: { type: Array as PropType }, /** (受控)展开指定的树节点 */ - expandedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + expandedKeys: { type: Array as PropType }, /** (受控)选中复选框的树节点 */ - checkedKeys: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - PropTypes.shape({ - checked: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - halfChecked: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - }).loose, - ]), + checkedKeys: { + type: [Array, Object] as PropType, + }, /** 默认选中复选框的树节点 */ - defaultCheckedKeys: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - ), + defaultCheckedKeys: { type: Array as PropType }, /** (受控)设置选中的树节点 */ - selectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + selectedKeys: { type: Array as PropType }, /** 默认选中的树节点 */ - defaultSelectedKeys: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - ), - selectable: PropTypes.looseBool, + defaultSelectedKeys: { type: Array as PropType }, + selectable: { type: Boolean, default: undefined }, /** filter some AntTreeNodes as you need. it should return true */ - filterAntTreeNode: PropTypes.func, - /** 异步加载数据 */ - loadData: PropTypes.func, - loadedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - // onLoaded: (loadedKeys: string[], info: { event: 'load', node: AntTreeNode; }) => void, - /** 响应右键点击 */ - // onRightClick: (options: AntTreeNodeMouseEvent) => void, - /** 设置节点可拖拽(IE>8)*/ - draggable: PropTypes.looseBool, - // /** 开始拖拽时调用 */ - // onDragStart: (options: AntTreeNodeMouseEvent) => void, - // /** dragenter 触发时调用 */ - // onDragEnter: (options: AntTreeNodeMouseEvent) => void, - // /** dragover 触发时调用 */ - // onDragOver: (options: AntTreeNodeMouseEvent) => void, - // /** dragleave 触发时调用 */ - // onDragLeave: (options: AntTreeNodeMouseEvent) => void, - // /** drop 触发时调用 */ - // onDrop: (options: AntTreeNodeMouseEvent) => void, - showIcon: PropTypes.looseBool, - icon: PropTypes.func, + filterAntTreeNode: { type: Function as PropType<(node: AntTreeNode) => boolean> }, + loadedKeys: { type: Array as PropType }, + draggable: { type: Boolean, default: undefined }, + showIcon: { type: Boolean, default: undefined }, + icon: { type: Function as PropType<(nodeProps: AntdTreeNodeAttribute) => any> }, switcherIcon: PropTypes.any, prefixCls: PropTypes.string, - filterTreeNode: PropTypes.func, - openAnimation: PropTypes.any, - treeData: { - type: Array as PropType, - }, /** * @default{title,key,children} + * deprecated, please use `fieldNames` instead * 替换treeNode中 title,key,children字段为treeData中对应的字段 */ - replaceFields: PropTypes.object, - blockNode: PropTypes.looseBool, - /** 展开/收起节点时触发 */ - onExpand: PropTypes.func, - /** 点击复选框触发 */ - onCheck: PropTypes.func, - /** 点击树节点触发 */ - onSelect: PropTypes.func, - /** 单击树节点触发 */ - onClick: PropTypes.func, - /** 双击树节点触发 */ - onDoubleclick: PropTypes.func, - onDblclick: PropTypes.func, - 'onUpdate:selectedKeys': PropTypes.func, - 'onUpdate:checkedKeys': PropTypes.func, - 'onUpdate:expandedKeys': PropTypes.func, + replaceFields: { type: Object as PropType }, + blockNode: { type: Boolean, default: undefined }, }; -} +}; -export { TreeProps }; +export type TreeProps = Partial>>; export default defineComponent({ name: 'ATree', inheritAttrs: false, - props: initDefaultProps(TreeProps(), { + props: initDefaultProps(treeProps(), { checkable: false, + selectable: true, showIcon: false, openAnimation: { ...animation, - appear: null, + appear: false, }, blockNode: false, }), - setup() { - return { - tree: null, - configProvider: inject('configProvider', defaultConfigProvider), + slots: ['icon', 'title', 'switcherIcon'], + emits: [ + 'update:selectedKeys', + 'update:checkedKeys', + 'update:expandedKeys', + 'expand', + 'select', + 'check', + ], + TreeNode, + setup(props, { attrs, expose, emit, slots }) { + const { prefixCls, direction, virtual } = useConfigInject('tree', props); + const tree = ref(); + expose({ + tree, + }); + + const handleCheck: TreeProps['onCheck'] = (checkedObjOrKeys, eventObj) => { + emit('update:checkedKeys', checkedObjOrKeys); + emit('check', checkedObjOrKeys, eventObj); + }; + const handleExpand: TreeProps['onExpand'] = (expandedKeys, eventObj) => { + emit('update:expandedKeys', expandedKeys); + emit('expand', expandedKeys, eventObj); + }; + const handleSelect: TreeProps['onSelect'] = (selectedKeys, eventObj) => { + emit('update:selectedKeys', selectedKeys); + emit('select', selectedKeys, eventObj); + }; + return () => { + const { + showIcon, + showLine, + switcherIcon = slots.switcherIcon?.(), + icon = slots.icon, + blockNode, + checkable, + selectable, + fieldNames, + replaceFields, + } = props; + const newProps = { + ...attrs, + ...props, + showLine: Boolean(showLine), + dropIndicatorRender, + fieldNames: fieldNames || (replaceFields as FieldNames), + icon, + }; + + return ( + + renderSwitcherIcon(prefixCls.value, switcherIcon, showLine, nodeProps) + } + onCheck={handleCheck} + onExpand={handleExpand} + onSelect={handleSelect} + v-slots={{ + checkable: () => , + }} + children={filterEmpty(slots.default?.())} + > + ); }; }, - TreeNode, - methods: { - renderSwitcherIcon(prefixCls: string, switcherIcon: VNode, { isLeaf, loading, expanded }) { - const { showLine } = this.$props; - if (loading) { - return ; - } + // methods: { + // renderSwitcherIcon(prefixCls: string, switcherIcon: VNode, { isLeaf, loading, expanded }) { + // const { showLine } = this.$props; + // if (loading) { + // return ; + // } - if (isLeaf) { - return showLine ? : null; - } - const switcherCls = `${prefixCls}-switcher-icon`; - if (switcherIcon) { - return cloneElement(switcherIcon, { - class: switcherCls, - }); - } - return showLine ? ( - expanded ? ( - - ) : ( - - ) - ) : ( - - ); - }, - updateTreeData(treeData: TreeDataItem[]) { - const { $slots } = this; - const defaultFields = { children: 'children', title: 'title', key: 'key' }; - const replaceFields = { ...defaultFields, ...this.$props.replaceFields }; - return treeData.map(item => { - const key = item[replaceFields.key]; - const children = item[replaceFields.children]; - const { slots = {}, class: cls, style, ...restProps } = item; - const treeNodeProps = { - ...restProps, - icon: $slots[slots.icon] || restProps.icon, - switcherIcon: $slots[slots.switcherIcon] || restProps.switcherIcon, - title: $slots[slots.title] || $slots.title || restProps[replaceFields.title], - dataRef: item, - key, - class: cls, - style, - }; - if (children) { - return { ...treeNodeProps, children: this.updateTreeData(children) }; - } - return treeNodeProps; - }); - }, - setTreeRef(node: VNode) { - this.tree = node; - }, - handleCheck(checkedObj: (number | string)[], eventObj: CheckEvent) { - this.$emit('update:checkedKeys', checkedObj); - this.$emit('check', checkedObj, eventObj); - }, - handleExpand(expandedKeys: (number | string)[], eventObj: ExpendEvent) { - this.$emit('update:expandedKeys', expandedKeys); - this.$emit('expand', expandedKeys, eventObj); - }, - handleSelect(selectedKeys: (number | string)[], eventObj: SelectEvent) { - this.$emit('update:selectedKeys', selectedKeys); - this.$emit('select', selectedKeys, eventObj); - }, - }, - render() { - const props = getOptionProps(this); - const { prefixCls: customizePrefixCls, showIcon, treeNodes, blockNode } = props; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('tree', customizePrefixCls); - const switcherIcon = getComponent(this, 'switcherIcon'); - const checkable = props.checkable; - let treeData = props.treeData || treeNodes; - if (treeData) { - treeData = this.updateTreeData(treeData); - } - const { class: className, ...restAttrs } = this.$attrs; - const vcTreeProps = { - ...props, - prefixCls, - checkable: checkable ? : checkable, - children: getSlot(this), - switcherIcon: nodeProps => this.renderSwitcherIcon(prefixCls, switcherIcon, nodeProps), - ref: this.setTreeRef, - ...restAttrs, - class: classNames(className, { - [`${prefixCls}-icon-hide`]: !showIcon, - [`${prefixCls}-block-node`]: blockNode, - }), - onCheck: this.handleCheck, - onExpand: this.handleExpand, - onSelect: this.handleSelect, - } as Record; - if (treeData) { - vcTreeProps.treeData = treeData; - } - return ; - }, + // if (isLeaf) { + // return showLine ? : null; + // } + // const switcherCls = `${prefixCls}-switcher-icon`; + // if (switcherIcon) { + // return cloneElement(switcherIcon, { + // class: switcherCls, + // }); + // } + // return showLine ? ( + // expanded ? ( + // + // ) : ( + // + // ) + // ) : ( + // + // ); + // }, + // updateTreeData(treeData: TreeDataItem[]) { + // const { $slots } = this; + // const defaultFields = { children: 'children', title: 'title', key: 'key' }; + // const replaceFields = { ...defaultFields, ...this.$props.replaceFields }; + // return treeData.map(item => { + // const key = item[replaceFields.key]; + // const children = item[replaceFields.children]; + // const { slots = {}, class: cls, style, ...restProps } = item; + // const treeNodeProps = { + // ...restProps, + // icon: $slots[slots.icon] || restProps.icon, + // switcherIcon: $slots[slots.switcherIcon] || restProps.switcherIcon, + // title: $slots[slots.title] || $slots.title || restProps[replaceFields.title], + // dataRef: item, + // key, + // class: cls, + // style, + // }; + // if (children) { + // return { ...treeNodeProps, children: this.updateTreeData(children) }; + // } + // return treeNodeProps; + // }); + // }, + // setTreeRef(node: VNode) { + // this.tree = node; + // }, + // handleCheck(checkedObj: (number | string)[], eventObj: CheckEvent) { + // this.$emit('update:checkedKeys', checkedObj); + // this.$emit('check', checkedObj, eventObj); + // }, + // handleExpand(expandedKeys: (number | string)[], eventObj: ExpendEvent) { + // this.$emit('update:expandedKeys', expandedKeys); + // this.$emit('expand', expandedKeys, eventObj); + // }, + // handleSelect(selectedKeys: (number | string)[], eventObj: SelectEvent) { + // this.$emit('update:selectedKeys', selectedKeys); + // this.$emit('select', selectedKeys, eventObj); + // }, + // }, + // render() { + // const props = getOptionProps(this); + // const { prefixCls: customizePrefixCls, showIcon, treeNodes, blockNode } = props; + // const getPrefixCls = this.configProvider.getPrefixCls; + // const prefixCls = getPrefixCls('tree', customizePrefixCls); + // const switcherIcon = getComponent(this, 'switcherIcon'); + // const checkable = props.checkable; + // let treeData = props.treeData || treeNodes; + // if (treeData) { + // treeData = this.updateTreeData(treeData); + // } + // const { class: className, ...restAttrs } = this.$attrs; + // const vcTreeProps = { + // ...props, + // prefixCls, + // checkable: checkable ? : checkable, + // children: getSlot(this), + // switcherIcon: nodeProps => this.renderSwitcherIcon(prefixCls, switcherIcon, nodeProps), + // ref: this.setTreeRef, + // ...restAttrs, + // class: classNames(className, { + // [`${prefixCls}-icon-hide`]: !showIcon, + // [`${prefixCls}-block-node`]: blockNode, + // }), + // onCheck: this.handleCheck, + // onExpand: this.handleExpand, + // onSelect: this.handleSelect, + // } as Record; + // if (treeData) { + // vcTreeProps.treeData = treeData; + // } + // return ; + // }, }); diff --git a/components/tree/index.tsx b/components/tree/index.tsx index 446ceb119..54e4e9940 100644 --- a/components/tree/index.tsx +++ b/components/tree/index.tsx @@ -2,6 +2,21 @@ import type { App, Plugin } from 'vue'; import Tree from './Tree'; import DirectoryTree from './DirectoryTree'; +export { EventDataNode, DataNode } from '../vc-tree/interface'; + +export { + TreeProps, + AntTreeNode, + AntTreeNodeMouseEvent, + AntTreeNodeExpandedEvent, + AntTreeNodeCheckedEvent, + AntTreeNodeSelectedEvent, + AntdTreeNodeAttribute, + AntTreeNodeProps, +} from './Tree'; + +export { ExpandAction as DirectoryTreeExpandAction, DirectoryTreeProps } from './DirectoryTree'; + Tree.TreeNode.name = 'ATreeNode'; Tree.DirectoryTree = DirectoryTree; /* istanbul ignore next */ diff --git a/components/tree/style/directory.less b/components/tree/style/directory.less index 91caf33c0..51baf617c 100644 --- a/components/tree/style/directory.less +++ b/components/tree/style/directory.less @@ -2,93 +2,70 @@ @tree-prefix-cls: ~'@{ant-prefix}-tree'; -.@{tree-prefix-cls} { - &.@{tree-prefix-cls}-directory { +.@{tree-prefix-cls}.@{tree-prefix-cls}-directory { + // ================== TreeNode ================== + .@{tree-prefix-cls}-treenode { position: relative; - // Stretch selector width - > li, - .@{tree-prefix-cls}-child-tree > li { - span { - &.@{tree-prefix-cls}-switcher { - position: relative; - z-index: 1; + // Hover color + &::before { + position: absolute; + top: 0; + right: 0; + bottom: 4px; + left: 0; + transition: background-color 0.3s; + content: ''; + pointer-events: none; + } - &.@{tree-prefix-cls}-switcher-noop { - pointer-events: none; - } - } + &:hover { + &::before { + background: @item-hover-bg; + } + } - &.@{tree-prefix-cls}-checkbox { - position: relative; - z-index: 1; - } + // Elements + > * { + z-index: 1; + } - &.@{tree-prefix-cls}-node-content-wrapper { - border-radius: 0; - user-select: none; + // >>> Switcher + .@{tree-prefix-cls}-switcher { + transition: color 0.3s; + } - &:hover { - background: transparent; + // >>> Title + .@{tree-prefix-cls}-node-content-wrapper { + border-radius: 0; + user-select: none; - &::before { - background: @item-hover-bg; - } - } - - &.@{tree-prefix-cls}-node-selected { - color: @tree-directory-selected-color; - background: transparent; - } - - &::before { - position: absolute; - right: 0; - left: 0; - height: @tree-title-height; - transition: all 0.3s; - content: ''; - } - - > span { - position: relative; - z-index: 1; - } - } + &:hover { + background: transparent; } - &.@{tree-prefix-cls}-treenode-selected { - > span { - &.@{tree-prefix-cls}-switcher { - color: @tree-directory-selected-color; - } + &.@{tree-prefix-cls}-node-selected { + color: @tree-directory-selected-color; + background: transparent; + } + } - &.@{tree-prefix-cls}-checkbox { - .@{tree-prefix-cls}-checkbox-inner { - border-color: @primary-color; - } + // ============= Selected ============= + &-selected { + &:hover::before, + &::before { + background: @tree-directory-selected-bg; + } - &.@{tree-prefix-cls}-checkbox-checked { - &::after { - border-color: @checkbox-check-color; - } + // >>> Switcher + .@{tree-prefix-cls}-switcher { + color: @tree-directory-selected-color; + } - .@{tree-prefix-cls}-checkbox-inner { - background: @checkbox-check-color; - - &::after { - border-color: @primary-color; - } - } - } - } - - &.@{tree-prefix-cls}-node-content-wrapper { - &::before { - background: @tree-directory-selected-bg; - } - } - } + // >>> Title + .@{tree-prefix-cls}-node-content-wrapper { + color: @tree-directory-selected-color; + background: transparent; } } } diff --git a/components/tree/style/index.less b/components/tree/style/index.less index 31f57bf09..a2c0067f2 100644 --- a/components/tree/style/index.less +++ b/components/tree/style/index.less @@ -5,276 +5,12 @@ @import './directory'; @tree-prefix-cls: ~'@{ant-prefix}-tree'; -@tree-showline-icon-color: @text-color-secondary; -@tree-node-padding: 4px; +@tree-node-prefix-cls: ~'@{tree-prefix-cls}-treenode'; .antCheckboxFn(@checkbox-prefix-cls: ~'@{ant-prefix}-tree-checkbox'); .@{tree-prefix-cls} { - /* see https://github.com/ant-design/ant-design/issues/16259 */ - &-checkbox-checked::after { - position: absolute; - top: 16.67%; - left: 0; - width: 100%; - height: 66.67%; - } - - .reset-component(); - - margin: 0; - padding: 0; - - ol, - ul { - margin: 0; - padding: 0; - list-style: none; - } - - li { - margin: 0; - padding: @tree-node-padding 0; - white-space: nowrap; - list-style: none; - outline: 0; - span[draggable], - span[draggable='true'] { - line-height: @tree-title-height - 4px; - border-top: 2px transparent solid; - border-bottom: 2px transparent solid; - user-select: none; - /* Required to make elements draggable in old WebKit */ - -khtml-user-drag: element; - -webkit-user-drag: element; - } - &.drag-over { - > span[draggable] { - color: white; - background-color: @primary-color; - opacity: 0.8; - } - } - &.drag-over-gap-top { - > span[draggable] { - border-top-color: @primary-color; - } - } - &.drag-over-gap-bottom { - > span[draggable] { - border-bottom-color: @primary-color; - } - } - &.filter-node { - > span { - color: @highlight-color !important; - font-weight: 500 !important; - } - } - - // When node is loading - &.@{tree-prefix-cls}-treenode-loading { - span { - &.@{tree-prefix-cls}-switcher { - &.@{tree-prefix-cls}-switcher_open, - &.@{tree-prefix-cls}-switcher_close { - .@{tree-prefix-cls}-switcher-loading-icon { - position: absolute; - left: 0; - display: inline-block; - width: 24px; - height: @tree-title-height; - color: @primary-color; - font-size: 14px; - transform: none; - svg { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - } - } - - :root &::after { - opacity: 0; - } - } - } - } - } - - ul { - margin: 0; - padding: 0 0 0 @tree-child-padding; - } - .@{tree-prefix-cls}-node-content-wrapper { - display: inline-block; - height: @tree-title-height; - margin: 0; - padding: 0 5px; - color: @text-color; - line-height: @tree-title-height; - text-decoration: none; - vertical-align: top; - border-radius: @border-radius-sm; - cursor: pointer; - transition: all 0.3s; - &:hover { - background-color: @tree-node-hover-bg; - } - &.@{tree-prefix-cls}-node-selected { - background-color: @tree-node-selected-bg; - } - } - span { - &.@{tree-prefix-cls}-checkbox { - top: initial; - height: @tree-title-height; - margin: 0 4px 0 2px; - padding: ((@tree-title-height - 16px) / 2) 0; - } - &.@{tree-prefix-cls}-switcher, - &.@{tree-prefix-cls}-iconEle { - display: inline-block; - width: 24px; - height: @tree-title-height; - margin: 0; - line-height: @tree-title-height; - text-align: center; - vertical-align: top; - border: 0 none; - outline: none; - cursor: pointer; - } - - &.@{tree-prefix-cls}-iconEle:empty { - display: none; - } - - &.@{tree-prefix-cls}-switcher { - position: relative; - - &.@{tree-prefix-cls}-switcher-noop { - cursor: default; - } - &.@{tree-prefix-cls}-switcher_open { - .antTreeSwitcherIcon(); - } - &.@{tree-prefix-cls}-switcher_close { - .antTreeSwitcherIcon(); - .@{tree-prefix-cls}-switcher-icon { - svg { - transform: rotate(-90deg); - } - } - } - } - } - &:last-child > span { - &.@{tree-prefix-cls}-switcher, - &.@{tree-prefix-cls}-iconEle { - &::before { - display: none; - } - } - } - } - - > li { - &:first-child { - padding-top: 7px; - } - &:last-child { - padding-bottom: 7px; - } - } - &-child-tree { - // https://github.com/ant-design/ant-design/issues/14958 - > li { - // Provide additional padding between top child node and parent node - &:first-child { - padding-top: 2 * @tree-node-padding; - } - - // Hide additional padding between last child node and next parent node - &:last-child { - padding-bottom: 0; - } - } - } - li&-treenode-disabled { - > span:not(.@{tree-prefix-cls}-switcher), - > .@{tree-prefix-cls}-node-content-wrapper, - > .@{tree-prefix-cls}-node-content-wrapper span { - color: @disabled-color; - cursor: not-allowed; - } - > .@{tree-prefix-cls}-node-content-wrapper:hover { - background: transparent; - } - } - &-icon__open { - margin-right: 2px; - vertical-align: top; - } - &-icon__close { - margin-right: 2px; - vertical-align: top; - } - // Tree with line - &&-show-line { - li { - position: relative; - span { - &.@{tree-prefix-cls}-switcher { - color: @tree-showline-icon-color; - background: @component-background; - &.@{tree-prefix-cls}-switcher-noop { - .antTreeShowLineIcon('tree-doc-icon'); - } - &.@{tree-prefix-cls}-switcher_open { - .antTreeShowLineIcon('tree-showline-open-icon'); - } - &.@{tree-prefix-cls}-switcher_close { - .antTreeShowLineIcon('tree-showline-close-icon'); - } - } - } - } - li:not(:last-child)::before { - position: absolute; - left: 12px; - width: 1px; - height: 100%; - height: calc(100% - 22px); // Remove additional height if support - margin: 22px 0 0; - border-left: 1px solid @border-color-base; - content: ' '; - } - } - - &.@{tree-prefix-cls}-icon-hide { - .@{tree-prefix-cls}-treenode-loading { - .@{tree-prefix-cls}-iconEle { - display: none; - } - } - } - - &.@{tree-prefix-cls}-block-node { - li { - .@{tree-prefix-cls}-node-content-wrapper { - width: ~'calc(100% - 24px)'; - } - span { - &.@{tree-prefix-cls}-checkbox { - + .@{tree-prefix-cls}-node-content-wrapper { - width: ~'calc(100% - 46px)'; - } - } - } - } - } + .antTreeFn(@tree-prefix-cls); } + +@import './rtl'; diff --git a/components/tree/style/index.ts b/components/tree/style/index.tsx similarity index 100% rename from components/tree/style/index.ts rename to components/tree/style/index.tsx diff --git a/components/tree/style/mixin.less b/components/tree/style/mixin.less index 929cecdee..197c20662 100644 --- a/components/tree/style/mixin.less +++ b/components/tree/style/mixin.less @@ -1,29 +1,274 @@ @import '../../style/mixins/index'; @tree-prefix-cls: ~'@{ant-prefix}-tree'; -@tree-select-prefix-cls: ~'@{ant-prefix}-select'; +@tree-node-prefix-cls: ~'@{tree-prefix-cls}-treenode'; +@select-tree-prefix-cls: ~'@{ant-prefix}-select-tree'; +@tree-motion: ~'@{ant-prefix}-motion-collapse'; +@tree-node-padding: (@padding-xs / 2); +@tree-node-hightlight-color: inherit; .antTreeSwitcherIcon(@type: 'tree-default-open-icon') { .@{tree-prefix-cls}-switcher-icon, - .@{tree-select-prefix-cls}-switcher-icon { - .iconfont-size-under-12px(10px); - + .@{select-tree-prefix-cls}-switcher-icon { display: inline-block; - font-weight: bold; + font-size: 10px; + vertical-align: baseline; svg { transition: transform 0.3s; } } } -.antTreeShowLineIcon(@type) { - .@{tree-prefix-cls}-switcher-icon, - .@{tree-select-prefix-cls}-switcher-icon { - display: inline-block; - font-weight: normal; - font-size: 12px; - svg { - transition: transform 0.3s; +.drop-indicator() { + .@{tree-prefix-cls}-drop-indicator { + position: absolute; + // it should displayed over the following node + z-index: 1; + height: 2px; + background-color: @primary-color; + border-radius: 1px; + pointer-events: none; + &::after { + position: absolute; + top: -3px; + left: -6px; + width: 8px; + height: 8px; + background-color: transparent; + border: 2px solid @primary-color; + border-radius: 50%; + content: ''; + } + } +} + +.antTreeFn(@custom-tree-prefix-cls) { + @custom-tree-node-prefix-cls: ~'@{custom-tree-prefix-cls}-treenode'; + .reset-component(); + background: @tree-bg; + border-radius: @border-radius-base; + transition: background-color 0.3s; + + &-focused:not(:hover):not(&-active-focused) { + background: @primary-1; + } + + // =================== Virtual List =================== + &-list-holder-inner { + align-items: flex-start; + } + + &.@{custom-tree-prefix-cls}-block-node { + .@{custom-tree-prefix-cls}-list-holder-inner { + align-items: stretch; + + // >>> Title + .@{custom-tree-prefix-cls}-node-content-wrapper { + flex: auto; + } + } + } + + // ===================== TreeNode ===================== + .@{custom-tree-node-prefix-cls} { + display: flex; + align-items: flex-start; + padding: 0 0 @tree-node-padding 0; + outline: none; + // Disabled + &-disabled { + // >>> Title + .@{custom-tree-prefix-cls}-node-content-wrapper { + color: @disabled-color; + cursor: not-allowed; + + &:hover { + background: transparent; + } + } + } + + &-active .@{custom-tree-prefix-cls}-node-content-wrapper { + background: @tree-node-hover-bg; + } + + &:not(&-disabled).filter-node .@{custom-tree-prefix-cls}-title { + color: @tree-node-hightlight-color; + font-weight: 500; + } + } + + // >>> Indent + &-indent { + align-self: stretch; + white-space: nowrap; + user-select: none; + + &-unit { + display: inline-block; + width: @tree-title-height; + } + } + + // >>> Switcher + &-switcher { + .antTreeSwitcherIcon(); + position: relative; + flex: none; + align-self: stretch; + width: @tree-title-height; + margin: 0; + line-height: @tree-title-height; + text-align: center; + cursor: pointer; + user-select: none; + + &-noop { + cursor: default; + } + + &_close { + .@{custom-tree-prefix-cls}-switcher-icon { + svg { + transform: rotate(-90deg); + } + } + } + + &-loading-icon { + color: @primary-color; + } + + &-leaf-line { + position: relative; + z-index: 1; + display: inline-block; + width: 100%; + height: 100%; + &::before { + position: absolute; + top: 0; + bottom: -@tree-node-padding; + margin-left: -1px; + border-left: 1px solid @normal-color; + content: ' '; + } + &::after { + position: absolute; + width: @tree-title-height - 14px; + height: @tree-title-height - 10px; + margin-left: -1px; + border-bottom: 1px solid @normal-color; + content: ' '; + } + } + } + + // >>> Checkbox + &-checkbox { + top: initial; + margin: ((@tree-title-height - @checkbox-size) / 2) 8px 0 0; + } + + // >>> Title + & &-node-content-wrapper { + position: relative; + z-index: auto; + min-height: @tree-title-height; + margin: 0; + padding: 0 4px; + color: inherit; + line-height: @tree-title-height; + background: transparent; + border-radius: @border-radius-base; + cursor: pointer; + transition: all 0.3s, border 0s, line-height 0s, box-shadow 0s; + + &:hover { + background-color: @tree-node-hover-bg; + } + + &.@{custom-tree-prefix-cls}-node-selected { + background-color: @tree-node-selected-bg; + } + + // Icon + .@{custom-tree-prefix-cls}-iconEle { + display: inline-block; + width: @tree-title-height; + height: @tree-title-height; + line-height: @tree-title-height; + text-align: center; + vertical-align: top; + &:empty { + display: none; + } + } + } + + // https://github.com/ant-design/ant-design/issues/28217 + &-unselectable &-node-content-wrapper:hover { + background-color: transparent; + } + + // ==================== Draggable ===================== + &-node-content-wrapper[draggable='true'] { + line-height: @tree-title-height; + user-select: none; + + .drop-indicator(); + } + + .@{custom-tree-node-prefix-cls}.drop-container { + > [draggable] { + box-shadow: 0 0 0 2px @primary-color; + } + } + + // ==================== Show Line ===================== + &-show-line { + // ================ Indent lines ================ + .@{custom-tree-prefix-cls}-indent { + &-unit { + position: relative; + height: 100%; + + &::before { + position: absolute; + top: 0; + right: (@tree-title-height / 2); + bottom: -@tree-node-padding; + border-right: 1px solid @border-color-base; + content: ''; + } + + &-end { + &::before { + display: none; + } + } + } + } + + // ============== Cover Background ============== + .@{custom-tree-prefix-cls}-switcher { + background: @component-background; + + &-line-icon { + vertical-align: -0.225em; + } + } + } +} + +.@{tree-node-prefix-cls}-leaf-last { + .@{tree-prefix-cls}-switcher { + &-leaf-line { + &::before { + top: auto !important; + bottom: auto !important; + height: @tree-title-height - 10px !important; + } } } } diff --git a/components/tree/style/rtl.less b/components/tree/style/rtl.less new file mode 100644 index 000000000..bc985c956 --- /dev/null +++ b/components/tree/style/rtl.less @@ -0,0 +1,72 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; +@import '../../checkbox/style/mixin'; + +@tree-prefix-cls: ~'@{ant-prefix}-tree'; +@select-tree-prefix-cls: ~'@{ant-prefix}-select-tree'; +@tree-node-prefix-cls: ~'@{tree-prefix-cls}-treenode'; + +.@{tree-prefix-cls} { + &-rtl { + direction: rtl; + .@{tree-prefix-cls}-node-content-wrapper[draggable='true'] { + .@{tree-prefix-cls}-drop-indicator { + &::after { + right: -6px; + left: unset; + } + } + } + } + + // ===================== TreeNode ===================== + .@{tree-node-prefix-cls} { + &-rtl { + direction: rtl; + } + } + + // >>> Switcher + &-switcher { + &_close { + .@{tree-prefix-cls}-switcher-icon { + svg { + .@{tree-prefix-cls}-rtl & { + transform: rotate(90deg); + } + } + } + } + } + // ==================== Show Line ===================== + &-show-line { + // ================ Indent lines ================ + .@{tree-prefix-cls}-indent { + &-unit { + &::before { + .@{tree-prefix-cls}-rtl& { + right: auto; + left: -(@tree-title-height / 2) - 1px; + border-right: none; + border-left: 1px solid @border-color-base; + } + } + } + } + } + // >>> Checkbox + &-checkbox { + .@{tree-prefix-cls}-rtl& { + margin: ((@tree-title-height - @checkbox-size) / 2) 0 0 8px; + } + } +} + +.@{select-tree-prefix-cls} { + // >>> Checkbox + &-checkbox { + .@{tree-prefix-cls}-select-dropdown-rtl & { + margin: ((@tree-title-height - @checkbox-size) / 2) 0 0 8px; + } + } +} diff --git a/components/tree/utils/dictUtil.ts b/components/tree/utils/dictUtil.ts new file mode 100644 index 000000000..bd1f0a339 --- /dev/null +++ b/components/tree/utils/dictUtil.ts @@ -0,0 +1,92 @@ +import type { DataNode, Key } from 'ant-design-vue/es/vc-tree/interface'; + +enum Record { + None, + Start, + End, +} + +function traverseNodesKey( + treeData: DataNode[], + callback: (key: Key | number | null, node: DataNode) => boolean, +) { + function processNode(dataNode: DataNode) { + const { key, children } = dataNode; + if (callback(key, dataNode) !== false) { + traverseNodesKey(children || [], callback); + } + } + + treeData.forEach(processNode); +} + +/** 计算选中范围,只考虑expanded情况以优化性能 */ +export function calcRangeKeys({ + treeData, + expandedKeys, + startKey, + endKey, +}: { + treeData: DataNode[]; + expandedKeys: Key[]; + startKey?: Key; + endKey?: Key; +}): Key[] { + const keys: Key[] = []; + let record: Record = Record.None; + + if (startKey && startKey === endKey) { + return [startKey]; + } + if (!startKey || !endKey) { + return []; + } + + function matchKey(key: Key) { + return key === startKey || key === endKey; + } + + traverseNodesKey(treeData, (key: Key) => { + if (record === Record.End) { + return false; + } + + if (matchKey(key)) { + // Match test + keys.push(key); + + if (record === Record.None) { + record = Record.Start; + } else if (record === Record.Start) { + record = Record.End; + return false; + } + } else if (record === Record.Start) { + // Append selection + keys.push(key); + } + + if (expandedKeys.indexOf(key) === -1) { + return false; + } + + return true; + }); + + return keys; +} + +export function convertDirectoryKeysToNodes(treeData: DataNode[], keys: Key[]) { + const restKeys: Key[] = [...keys]; + const nodes: DataNode[] = []; + traverseNodesKey(treeData, (key: Key, node: DataNode) => { + const index = restKeys.indexOf(key); + if (index !== -1) { + nodes.push(node); + restKeys.splice(index, 1); + } + + return !!restKeys.length; + }); + return nodes; +} diff --git a/components/tree/utils/dropIndicator.tsx b/components/tree/utils/dropIndicator.tsx new file mode 100644 index 000000000..a69569908 --- /dev/null +++ b/components/tree/utils/dropIndicator.tsx @@ -0,0 +1,33 @@ +import { CSSProperties } from 'vue'; + +export const offset = 4; + +export default function dropIndicatorRender(props: { + dropPosition: -1 | 0 | 1; + dropLevelOffset: number; + indent: number; + prefixCls: string; + direction: 'ltr' | 'rtl'; +}) { + const { dropPosition, dropLevelOffset, prefixCls, indent, direction = 'ltr' } = props; + const startPosition = direction === 'ltr' ? 'left' : 'right'; + const endPosition = direction === 'ltr' ? 'right' : 'left'; + const style: CSSProperties = { + [startPosition]: `${-dropLevelOffset * indent + offset}px`, + [endPosition]: 0, + }; + switch (dropPosition) { + case -1: + style.top = `${-3}px`; + break; + case 1: + style.bottom = `${-3}px`; + break; + default: + // dropPosition === 0 + style.bottom = `${-3}px`; + style[startPosition] = `${indent + offset}px`; + break; + } + return
; +} diff --git a/components/tree/utils/iconUtil.tsx b/components/tree/utils/iconUtil.tsx new file mode 100644 index 000000000..59dc2f8cb --- /dev/null +++ b/components/tree/utils/iconUtil.tsx @@ -0,0 +1,52 @@ +import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; +import FileOutlined from '@ant-design/icons-vue/FileOutlined'; +import MinusSquareOutlined from '@ant-design/icons-vue/MinusSquareOutlined'; +import PlusSquareOutlined from '@ant-design/icons-vue/PlusSquareOutlined'; +import CaretDownFilled from '@ant-design/icons-vue/CaretDownFilled'; +import { AntTreeNodeProps } from '../Tree'; +import { isValidElement } from 'ant-design-vue/es/_util/props-util'; + +import { cloneVNode } from 'vue'; + +export default function renderSwitcherIcon( + prefixCls: string, + switcherIcon: any, + showLine: boolean | { showLeafIcon: boolean } | undefined, + { isLeaf, expanded, loading }: AntTreeNodeProps, +) { + if (loading) { + return ; + } + let showLeafIcon; + if (showLine && typeof showLine === 'object') { + showLeafIcon = showLine.showLeafIcon; + } + if (isLeaf) { + if (showLine) { + if (typeof showLine === 'object' && !showLeafIcon) { + return ; + } + return ; + } + return null; + } + const switcherCls = `${prefixCls}-switcher-icon`; + if (isValidElement(switcherIcon)) { + return cloneVNode(switcherIcon, { + class: switcherCls, + }); + } + + if (switcherIcon) { + return switcherIcon; + } + + if (showLine) { + return expanded ? ( + + ) : ( + + ); + } + return ; +} diff --git a/components/upload/Upload.tsx b/components/upload/Upload.tsx index 4e1041d73..686b01653 100644 --- a/components/upload/Upload.tsx +++ b/components/upload/Upload.tsx @@ -13,7 +13,7 @@ import UploadList from './UploadList'; import { UploadProps } from './interface'; import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from './utils'; import { defineComponent, inject } from 'vue'; -import { getDataAndAria } from '../vc-tree/src/util'; +import { getDataAndAriaProps } from '../_util/util'; export default defineComponent({ name: 'AUpload', @@ -280,7 +280,7 @@ export default defineComponent({ [`${prefixCls}-disabled`]: disabled, }); return ( - +
}, }, - slots: ['title', 'icon', 'switcherIcon'], + slots: ['title', 'icon', 'switcherIcon', 'checkable'], setup(props, { attrs, slots }) { const visible = ref(true); const context = useInjectTreeContext(); diff --git a/components/vc-tree/NodeList.tsx b/components/vc-tree/NodeList.tsx index 01c3a2cce..912c3a721 100644 --- a/components/vc-tree/NodeList.tsx +++ b/components/vc-tree/NodeList.tsx @@ -94,6 +94,7 @@ export default defineComponent({ name: 'NodeList', inheritAttrs: false, props: nodeListProps, + slots: ['checkable'], setup(props, { expose, attrs, slots }) { // =============================== Ref ================================ const listRef = ref(null); @@ -275,8 +276,7 @@ export default defineComponent({ itemHeight={itemHeight} prefixCls={`${prefixCls}-list`} ref={listRef} - > - {(treeNode: FlattenNode) => { + children={(treeNode: FlattenNode) => { const { pos, data: { ...restProps }, @@ -310,10 +310,11 @@ export default defineComponent({ onMousemove={() => { onActiveChange(null); }} + v-slots={slots} /> ); }} - + > ); }; diff --git a/components/vc-tree/Tree.tsx b/components/vc-tree/Tree.tsx index 654c59d7f..83de46dd3 100644 --- a/components/vc-tree/Tree.tsx +++ b/components/vc-tree/Tree.tsx @@ -32,6 +32,7 @@ import classNames from '../_util/classNames'; export default defineComponent({ name: 'Tree', inheritAttrs: false, + slots: ['checkable'], props: initDefaultProps(treeProps(), { prefixCls: 'vc-tree', showLine: false, @@ -52,7 +53,7 @@ export default defineComponent({ allowDrop: () => true, }), - setup(props, { attrs }) { + setup(props, { attrs, slots }) { const destroyed = ref(false); let delayedDragEnterLogic: Record = {}; @@ -91,6 +92,34 @@ export default defineComponent({ return props.treeData !== undefined ? props.treeData : convertTreeToData(props.children); }); const keyEntities = ref({}); + + const focused = ref(false); + const activeKey = ref(null); + + const listChanging = ref(false); + + const fieldNames = computed(() => fillFieldNames(props.fieldNames)); + + const listRef = ref(); + + let dragStartMousePosition = null; + + let dragNode = null; + + const treeNodeRequiredProps = computed(() => { + return { + expandedKeys: expandedKeys.value || [], + selectedKeys: selectedKeys.value || [], + loadedKeys: loadedKeys.value || [], + loadingKeys: loadingKeys.value || [], + checkedKeys: checkedKeys.value || [], + halfCheckedKeys: halfCheckedKeys.value || [], + dragOverNodeKey: dragState.dragOverNodeKey, + dropPosition: dragState.dropPosition, + keyEntities: keyEntities.value, + }; + }); + watchEffect(() => { if (treeData.value) { const entitiesMap = convertDataToEntities(treeData.value, { fieldNames: fieldNames.value }); @@ -186,32 +215,6 @@ export default defineComponent({ } }); - const focused = ref(false); - const activeKey = ref(null); - - const listChanging = ref(false); - - const fieldNames = computed(() => fillFieldNames(props.fieldNames)); - - const listRef = ref(); - - let dragStartMousePosition = null; - - let dragNode = null; - - const treeNodeRequiredProps = computed(() => { - return { - expandedKeys: expandedKeys.value || [], - selectedKeys: selectedKeys.value || [], - loadedKeys: loadedKeys.value || [], - loadingKeys: loadingKeys.value || [], - checkedKeys: checkedKeys.value || [], - halfCheckedKeys: halfCheckedKeys.value || [], - dragOverNodeKey: dragState.dragOverNodeKey, - dropPosition: dragState.dropPosition, - keyEntities: keyEntities.value, - }; - }); const scrollTo: ScrollTo = scroll => { listRef.value.scrollTo(scroll); }; @@ -983,7 +986,7 @@ export default defineComponent({ checkable, checkStrictly, disabled, - motion, + openAnimation, loadData, filterTreeNode, height, @@ -1059,7 +1062,7 @@ export default defineComponent({ disabled={disabled} selectable={selectable} checkable={!!checkable} - motion={motion} + motion={openAnimation} dragging={dragging} height={height} itemHeight={itemHeight} @@ -1078,6 +1081,7 @@ export default defineComponent({ onScroll={onScroll} {...treeNodeRequiredProps.value} {...domProps} + v-slots={slots} />
diff --git a/components/vc-tree/TreeNode.tsx b/components/vc-tree/TreeNode.tsx index da4650072..1a03081c3 100644 --- a/components/vc-tree/TreeNode.tsx +++ b/components/vc-tree/TreeNode.tsx @@ -16,7 +16,7 @@ export default defineComponent({ inheritAttrs: false, props: treeNodeProps, isTreeNode: 1, - slots: ['title', 'icon', 'switcherIcon'], + slots: ['title', 'icon', 'switcherIcon', 'checkable'], setup(props, { attrs, expose, slots }) { const dragNodeHighlight = ref(false); const context = useInjectTreeContext(); @@ -275,9 +275,6 @@ export default defineComponent({ if (!checkable) return null; - // [Legacy] Custom element should be separate with `checkable` in future - const $custom = typeof checkable !== 'boolean' ? checkable : null; - return ( - {$custom} + {slots.checkable?.()} ); }; diff --git a/components/vc-tree/contextTypes.ts b/components/vc-tree/contextTypes.ts index 36d61e174..995bf16c8 100644 --- a/components/vc-tree/contextTypes.ts +++ b/components/vc-tree/contextTypes.ts @@ -84,6 +84,7 @@ export interface TreeContextProps { const TreeContextKey: InjectionKey> = Symbol('TreeContextKey'); export const TreeContext = defineComponent({ + name: 'TreeContext', props: { value: { type: Object as PropType }, }, @@ -92,7 +93,7 @@ export const TreeContext = defineComponent({ TreeContextKey, computed(() => props.value), ); - return slots.default?.(); + return () => slots.default?.(); }, }); diff --git a/components/vc-tree/index.ts b/components/vc-tree/index.ts index 6df172093..94a53c555 100644 --- a/components/vc-tree/index.ts +++ b/components/vc-tree/index.ts @@ -1,8 +1,6 @@ +import type { TreeProps, TreeNodeProps } from './props'; import Tree from './Tree'; import TreeNode from './TreeNode'; -import type { TreeProps } from './Tree'; -import type { TreeNodeProps } from './TreeNode'; - export { TreeNode }; export type { TreeProps, TreeNodeProps }; export default Tree; diff --git a/components/vc-tree/interface.tsx b/components/vc-tree/interface.tsx index 1d277f9d0..0f3e9db93 100644 --- a/components/vc-tree/interface.tsx +++ b/components/vc-tree/interface.tsx @@ -1,4 +1,4 @@ -import { VNode } from 'vue'; +import { CSSProperties, VNode } from 'vue'; export type { ScrollTo } from '../vc-virtual-list/List'; export interface DataNode { @@ -14,8 +14,8 @@ export interface DataNode { switcherIcon?: IconType; /** Set style of TreeNode. This is not recommend if you don't have any force requirement */ - // className?: string; - // style?: CSSProperties; + class?: string; + style?: CSSProperties; } export interface EventDataNode extends DataNode { diff --git a/components/vc-tree/props.ts b/components/vc-tree/props.ts index 32513b4d0..9ac8eb0fc 100644 --- a/components/vc-tree/props.ts +++ b/components/vc-tree/props.ts @@ -5,8 +5,15 @@ import type { NodeMouseEventHandler, NodeMouseEventParams, } from './contextTypes'; -import type { DataNode, Key, FlattenNode, DataEntity, EventDataNode, Direction } from './interface'; -import { fillFieldNames } from './utils/treeUtil'; +import type { + DataNode, + Key, + FlattenNode, + DataEntity, + EventDataNode, + Direction, + FieldNames, +} from './interface'; export interface CheckInfo { event: 'check'; @@ -103,7 +110,7 @@ export const treeProps = () => ({ tabindex: Number, children: PropTypes.VNodeChild, treeData: { type: Array as PropType }, // Generate treeNode by children - fieldNames: fillFieldNames, + fieldNames: { type: Object as PropType }, showLine: { type: Boolean, default: undefined }, showIcon: { type: Boolean, default: undefined }, icon: PropTypes.any, @@ -218,7 +225,7 @@ export const treeProps = () => ({ */ onActiveChange: { type: Function as PropType<(key: Key) => void> }, filterTreeNode: { type: Function as PropType<(treeNode: EventDataNode) => boolean> }, - motion: PropTypes.any, + openAnimation: PropTypes.any, switcherIcon: PropTypes.any, // Virtual List diff --git a/examples/App.vue b/examples/App.vue index 65b1ba433..ed81c908f 100644 --- a/examples/App.vue +++ b/examples/App.vue @@ -1,39 +1,62 @@ -