Skip to content

Commit

Permalink
feat(tree): support virtual scroll; support tree nodes drag; and make…
Browse files Browse the repository at this point in the history
… code same as Vue2 (#3410)

* docs(tree): 添加调试协助文档

* refactor(tree): tree 组件,移植 vue2 项目的代码

* docs(tree): 完善调试文档

* refactor(tree): 解决 vue2,vue3 的 useVModel 使用差异问题

* refactor(tree): tree 组件解决 vue3 类型警告问题

* refactor(tree): 解决类型告警问题

* refactor(tree): 解决组件引用方式告警的问题

* refactor(tree): tree 组件解决 usVModel 使用错误的问题

* refactor(tree): 更新 tree 组件示例,与 vue2 同步

* refactor(tree): 解决与 vue2 示例的状态一致性问题

* refactor(tree): 解决与 vue2 的适配问题

* refactor(tree): 解决 vue3 h 方法差异问题

* refactor(tree): tree 组件,修正与 vue2 slots 获取方式存在差异的问题

* refactor(tree): 示例消除 vue2, vue3 差异

* refactor(tree): 解决与 vue2 受控代码一致性问题

* refactor(tree): tree 组件统一受控示例

* refactor(tree): 解决数据变更示例vue3下需要变更的问题

* refactor(tree): tree 组件完善示例样式

* refactor(tree): tree 组件,完善示例样式

* refactor(tree): tree 组件,示例代码与vue2 统一

* refactor(tree): tree 组件,解决图标引用问题

* refactor(tree): 改进结构,使用共享对象传递公共数据

* refactor(tree): 改进代码结构

* refactor(tree): 解决 vue3 未能触发 item render 的问题

* refactor(tree): 改进组件结构

* refactor(tree): 解决 vue3 props.disableCheck 未能正常工作的问题

* refactor(tree): 解决 empty 示例废弃 api 未生效的问题

* refactor(tree): tree 组件 解决与 vue2 的示例差异

* refactor(tree): tree 组件完善节点操作示例

* fix(tree): tree 组件,解决 setData 方法未触发节点更新的问题

* refactor(tree): tree 组件,解决 setData 方法未能触发新增属性变更的问题

* refactor(tree): tree 组件完善属性同步示例

* refactor(tree): tree 组件虚拟滚动示例,降低数量便于调试

* feat(tree): tree 组件添加虚拟滚动能力

* docs(tree): 完善调试文档

* refactor(tree): tree 组件,改进代码结构,实现tree结构的立即渲染

* test(unit): tree 组件,同步 vue2 项目下的单元测试

* test(unit): tree 组件,测试代码适配 vue2 版本

* refactor(tree): tree 组件虚拟滚动事件绑定适配 vue3

* test(unit): tree 组件统一vue2,vue3测试用例形式

* test(unit): tree 组件测试用例适配 vue2 项目

* test(unit): tree 组件,vue2,vue3 统一测试用例

* test(unit): tree 组件,vue2,vue3 统一测试用例

* fix(tree): tree 组件虚拟滚动解决滚动条样式异常问题

* refactor(tree): tree 组件文件按规范重命名

* refactor(tree): tree 组件完善 expand-all 示例

* refactor(tree): tree 组件示例样式改进

* refactor(tree): tree 组件改进示例

* refactor(tree): 完善组件示例

* refactor(tree): tree 组件完善示例,分拆不同形式虚拟滚动示例

* fix(tree): tree 组件实现正确的受控呈现

* refactor(tree): 完善虚拟滚动示例

* fix(tree): tree 组件修正受控表现

* fix(tree): tree 组件修正事件触发规则

* test(unit): tree 组件,完善单元测试

* chore(tree): tree 组件变更,更新 src/_common

* docs(tree): update documents

* chore(tree): 更新 common

* refactor(tree): src/hooks/useVModel.ts 文件还原为之前的版本

* refactor(tree): 实现对 modelValue 属性的兼容

* feat(tree): update api

* feat(tree): update api

* chore(tree): 更新 common

* refactor(tree): tree 组件,适配器文件移除冗余代码

* fix(tree): remove unused code

* fix(tree): ts error

* refactor(tree-select): 修正类型错误

* test(unit): tree, update snapshot

---------

Co-authored-by: chaishi <[email protected]>
  • Loading branch information
TabSpace and chaishi authored Oct 10, 2023
1 parent c016733 commit af27b45
Show file tree
Hide file tree
Showing 79 changed files with 22,032 additions and 11,114 deletions.
2 changes: 1 addition & 1 deletion src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export type OptionData = {
} & PlainObject;

export type TreeOptionData<T = string | number> = {
children?: Array<TreeOptionData<T>>;
children?: Array<TreeOptionData<T>> | boolean;
/** option label content */
label?: string | TNode;
/** option search text */
Expand Down
43 changes: 43 additions & 0 deletions src/hooks/useLazyLoad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ref, onMounted, computed, nextTick, Ref, UnwrapRef } from 'vue';
import observe from '../_common/js/utils/observe';
import { isServer } from '../utils/dom';

export type UseLazyLoadParams = UnwrapRef<{
type: 'lazy' | 'virtual';
rowHeight?: number;
bufferSize?: number;
}>;

export default function useLazyLoad(
containerRef: Ref<HTMLElement>,
childRef: Ref<HTMLElement>,
params: UseLazyLoadParams,
) {
const tRowHeight = computed(() => Math.max(params.rowHeight || 48, 48));
const isInit = ref(false);
const hasLazyLoadHolder = computed(() => params?.type === 'lazy' && !isInit.value);

const requestAnimationFrame = (!isServer && window.requestAnimationFrame) || ((cb) => setTimeout(cb, 16.6));

const init = () => {
if (!isInit.value) {
requestAnimationFrame(() => {
isInit.value = true;
});
}
};

onMounted(() => {
if (params?.type !== 'lazy') return;
nextTick(() => {
const bufferSize = Math.max(10, params.bufferSize || 10);
const height = tRowHeight.value * bufferSize;
observe(childRef.value, containerRef.value, init, height);
});
});

return {
hasLazyLoadHolder,
tRowHeight,
};
}
4 changes: 2 additions & 2 deletions src/tree-select/tree-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export default defineComponent({

const treeNodeChange = (
valueParam: Array<TreeNodeValue>,
context: { node: TreeNodeModel<TreeOptionData>; e: MouseEvent },
context: { node: TreeNodeModel<TreeOptionData>; e?: MouseEvent },
) => {
let current: TreeSelectValue = valueParam;
if (isObjectValue.value) {
Expand All @@ -192,7 +192,7 @@ export default defineComponent({

const treeNodeActive = (
valueParam: Array<TreeNodeValue>,
context: { node: TreeNodeModel<TreeOptionData>; e: MouseEvent },
context: { node: TreeNodeModel<TreeOptionData>; e?: MouseEvent },
) => {
if (!props.multiple) {
setInnerVisible(false, context);
Expand Down
315 changes: 315 additions & 0 deletions src/tree/__tests__/activable.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
import { mount } from '@vue/test-utils';
import Tree from '@/src/tree/index.ts';
import { delay } from './kit';

describe('Tree:activable', () => {
vi.useRealTimers();
describe('props.activable', () => {
it('设置 activable 为 true, 节点可以具有激活态', async () => {
const data = [
{
value: 't1',
children: [
{
value: 't1.1',
},
{
value: 't1.2',
},
],
},
];
const wrapper = mount({
render() {
return <Tree ref="tree" data={data} activable expand-all transition={false}></Tree>;
},
});
await delay(1);
const { tree } = wrapper.vm.$refs;
tree.setItem('t1.1', {
actived: true,
});
await delay(1);
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false);
tree.setItem('t1.1', {
actived: false,
});
await delay(1);
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false);
});

it('在 data 中可以预先设置激活态', async () => {
const data = [
{
value: 't1',
children: [
{
value: 't1.1',
actived: true,
},
{
value: 't1.2',
actived: true,
},
],
},
];
const wrapper = mount({
render() {
return <Tree ref="tree" data={data} activable expand-all transition={false}></Tree>;
},
});
await delay(1);
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(true);
});
});

describe('props.defaultActived', () => {
it('defaultActived 可以用来设置默认激活状态', async () => {
const data = [
{
value: 't1',
children: [
{
value: 't1.1',
},
{
value: 't1.2',
},
],
},
];
const wrapper = mount({
render() {
return <Tree ref="tree" data={data} activable defaultActived={['t1.1']} expand-all transition={false}></Tree>;
},
});
await delay(1);
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false);
});
});

describe('props.actived', () => {
it('设置 actived 可指定激活节点', async () => {
const data = [
{
value: 't1',
children: [
{
value: 't1.1',
},
{
value: 't1.2',
},
],
},
];
const wrapper = mount({
render() {
return <Tree ref="tree" data={data} activable actived={['t1.1']} expand-all transition={false}></Tree>;
},
});
await delay(1);
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false);
});

it('设置 actived 可变更激活节点', async () => {
const data = [
{
value: 't1',
children: [
{
value: 't1.1',
},
{
value: 't1.2',
},
],
},
];
const wrapper = mount({
data() {
return {
actived: ['t1.1'],
};
},
render() {
return <Tree ref="tree" data={data} activable actived={this.actived} expand-all transition={false}></Tree>;
},
});
await delay(1);
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false);
wrapper.setData({
actived: ['t1.2'],
});
await delay(1);
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(true);
wrapper.setData({
actived: ['t1'],
});
await delay(1);
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false);
});

it('actived 受控处理可赋值空值', async () => {
const data = [
{
value: 't1',
children: [
{
value: 't1.1',
},
{
value: 't1.2',
},
],
},
];
const wrapper = mount({
data() {
return {
actived: ['t1'],
};
},
methods: {
onActive(vals) {
const actived = vals.filter((val) => val !== 't1');
this.actived = actived;
},
},
render() {
return (
<Tree
ref="tree"
data={data}
activable
actived={this.actived}
onActive={this.onActive}
expand-all
transition={false}
></Tree>
);
},
});
await delay(1);
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(true);
await wrapper.find('[data-value="t1"] .t-tree__label').trigger('click');
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
await delay(1);
await wrapper.find('[data-value="t1"] .t-tree__label').trigger('click');
expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
});
});

describe('props.activeMultiple', () => {
it('默认激活其他节点,原激活节点要取消激活状态', async () => {
const data = [
{
value: 't1',
actived: true,
children: [
{
value: 't1.1',
},
{
value: 't1.2',
},
],
},
];
const wrapper = mount({
render() {
return <Tree ref="tree" data={data} activable expand-all transition={false}></Tree>;
},
});
await delay(1);
const { tree } = wrapper.vm.$refs;

expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false);

tree.setItem('t1.1', {
actived: true,
});

await delay(1);

expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false);

tree.setItem('t1.2', {
actived: true,
});
await delay(1);

expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(true);
});

it('设置 activeMultiple 为 true,节点激活状态互不关联', async () => {
const data = [
{
value: 't1',
actived: true,
children: [
{
value: 't1.1',
},
{
value: 't1.2',
},
],
},
];
const wrapper = mount({
render() {
return <Tree ref="tree" data={data} activable expand-all activeMultiple={true} transition={false}></Tree>;
},
});
await delay(1);
const { tree } = wrapper.vm.$refs;

expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(false);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false);

tree.setItem('t1.1', {
actived: true,
});

await delay(1);

expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false);

tree.setItem('t1.2', {
actived: true,
});
await delay(1);

expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(true);
expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(true);
});
});
});
1 change: 1 addition & 0 deletions src/tree/__tests__/adapt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { defineComponent } from 'vue';
Loading

0 comments on commit af27b45

Please sign in to comment.