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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions packages/components/cascader/_example-ts/virtual-scroll.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>
<t-cascader
v-model="value"
:options="options"
:scroll="{ type: 'virtual', bufferSize: 5, threshold: 10 }"
clearable
multiple
@change="onChange"
/>
</template>

<script setup>
import { onMounted, ref } from 'vue';

const options = ref([]);
const value = ref(['20.1.20']);

const onChange = (val, context) => {
console.log(val, context);
};

function initOptions() {
const list = [];
for (let i = 1; i < 100; i++) {
const children = [];
for (let j = 1; j < 100; j++) {
const child = [];
for (let k = 1; k < 100; k++) {
child.push({
label: `子选项${i}.${j}.${k}`,
value: `${i}.${j}.${k}`,
});
}
children.push({
label: `子选项${i}.${j}`,
value: `${i}.${j}`,
children: child,
});
}

list.push({
label: `选项${i}`,
value: `${i}`,
children,
});
}
return list;
}

onMounted(() => {
const data = initOptions();
options.value = data;
});
</script>
54 changes: 54 additions & 0 deletions packages/components/cascader/_example/virtual-scroll.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>
<t-cascader
v-model="value"
:options="options"
:scroll="{ type: 'virtual', bufferSize: 5, threshold: 10 }"
clearable
multiple
@change="onChange"
/>
</template>

<script setup>
import { onMounted, ref } from 'vue';

const options = ref([]);
const value = ref(['20.1.20']);

const onChange = (val, context) => {
console.log(val, context);
};

function initOptions() {
const list = [];
for (let i = 1; i < 100; i++) {
const children = [];
for (let j = 1; j < 100; j++) {
const child = [];
for (let k = 1; k < 100; k++) {
child.push({
label: `子选项${i}.${j}.${k}`,
value: `${i}.${j}.${k}`,
});
}
children.push({
label: `子选项${i}.${j}`,
value: `${i}.${j}`,
children: child,
});
}

list.push({
label: `选项${i}`,
value: `${i}`,
children,
});
}
return list;
}

onMounted(() => {
const data = initOptions();
options.value = data;
});
</script>
1 change: 1 addition & 0 deletions packages/components/cascader/cascader-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default defineComponent({
trigger={props.trigger}
cascaderContext={cascaderContext.value}
empty={props.empty}
scroll={props.scroll}
v-slots={{ empty: slots.empty, option: slots.option, loadingText: slots.loadingText }}
/>
);
Expand Down
1 change: 1 addition & 0 deletions packages/components/cascader/cascader.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ defaultPopupVisible | Boolean | - | uncontrolled property | N
prefixIcon | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
readonly | Boolean | undefined | \- | N
reserveKeyword | Boolean | true | \- | N
scroll | \- | - | Lazy loading and virtual scrolling. To maximize the benefits of the component, when the amount of data is less than the threshold `scroll.threshold`, regardless of whether the virtual scrolling configuration exists, virtual scrolling will not be enabled within the component. `scroll.threshold` defaults to `100`。Typescript:`TScroll`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
selectInputProps | Object | - | Typescript:`SelectInputProps`,[SelectInput API Documents](./select-input?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/cascader/type.ts) | N
showAllLevels | Boolean | true | \- | N
size | String | medium | options: large/medium/small。Typescript:`SizeEnum`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
Expand Down
1 change: 1 addition & 0 deletions packages/components/cascader/cascader.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ defaultPopupVisible | Boolean | - | 是否显示下拉框。非受控属性 | N
prefixIcon | Slot / Function | - | 组件前置图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
readonly | Boolean | undefined | 只读状态,值为真会隐藏输入框,且无法打开下拉框 | N
reserveKeyword | Boolean | true | 多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词 | N
scroll | \- | - | 懒加载和虚拟滚动。为保证组件收益最大化,当数据量小于阈值 `scroll.threshold` 时,无论虚拟滚动的配置是否存在,组件内部都不会开启虚拟滚动,`scroll.threshold` 默认为 `100`。TS 类型:`TScroll`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
selectInputProps | Object | - | 透传 SelectInput 筛选器输入框组件的全部属性。TS 类型:`SelectInputProps`,[SelectInput API Documents](./select-input?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/cascader/type.ts) | N
showAllLevels | Boolean | true | 选中值使用完整路径,输入框在单选时也显示完整路径 | N
size | String | medium | 组件尺寸。可选项:large/medium/small。TS 类型:`SizeEnum`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
Expand Down
1 change: 1 addition & 0 deletions packages/components/cascader/cascader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export default defineComponent({
loading={props.loading}
loadingText={props.loadingText}
cascaderContext={cascaderContext.value}
scroll={props.scroll}
v-slots={{ option: slots.option, empty: slots.empty, loadingText: slots.loadingText }}
/>
{renderTNodeJSX('panelBottomContent')}
Expand Down
168 changes: 168 additions & 0 deletions packages/components/cascader/components/List.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { computed, defineComponent, h, nextTick, PropType, ref, watch } from 'vue';
import { expandClickEffect, valueChangeEffect } from '../utils';
import { CascaderContextType, TreeNode } from '../types';
import { usePrefixClass, useTNodeDefault } from '@tdesign/shared-hooks';
import Item from './Item';
import { getDefaultNode } from '@tdesign/shared-utils';
import CascaderProps from '../props';
import { useListVirtualScroll } from '../../list/hooks';

const props = {
treeNodes: {
type: Array as PropType<TreeNode[]>,
default: [] as PropType<TreeNode[]>,
},
isFilter: {
type: Boolean,
default: false,
},
segment: {
type: Boolean,
default: true,
},
listKey: {
type: String,
},
level: {
type: Number,
default: 0,
},
option: CascaderProps.option,
trigger: CascaderProps.trigger,
scroll: CascaderProps.scroll,
cascaderContext: {
type: Object as PropType<CascaderContextType>,
},
};

export default defineComponent({
name: 'TCascaderList',
props,
setup(props) {
const renderTNodeJSXDefault = useTNodeDefault();
const COMPONENT_NAME = usePrefixClass('cascader');
const panelWrapperRef = ref<HTMLDivElement>(null);

const treeNodes = computed(() => props.treeNodes);
const isVisible = computed(() => props.cascaderContext.visible);
const scroll = computed(() => ({
rowHeight: 28,
...props.scroll,
}));

const { virtualConfig, cursorStyle, listStyle, isVirtualScroll, onInnerVirtualScroll, scrollToElement } =
useListVirtualScroll(scroll.value, panelWrapperRef, treeNodes as any);

const handleExpand = (node: TreeNode, trigger: 'hover' | 'click') => {
const { trigger: propsTrigger, cascaderContext } = props;
expandClickEffect(propsTrigger, trigger, node, cascaderContext);
};

const renderItem = (node: TreeNode, index: number) => {
const optionChild = node.data.content
? getDefaultNode(node.data.content(h))
: renderTNodeJSXDefault('option', {
params: {
item: node.data,
index,
onExpand: () => handleExpand(node, 'click'),
onChange: () => valueChangeEffect(node, props.cascaderContext),
},
});
return (
<Item
key={node.value}
node={node}
optionChild={optionChild}
cascaderContext={props.cascaderContext}
onClick={() => {
handleExpand(node, 'click');
}}
onMouseenter={() => {
handleExpand(node, 'hover');
}}
onChange={() => {
valueChangeEffect(node, props.cascaderContext);
}}
/>
);
};

const onScrollIntoView = () => {
const { level, treeNodes, cascaderContext } = props;
const checkedNodes = cascaderContext.treeStore.getCheckedNodes();
let lastCheckedNodes = checkedNodes[checkedNodes.length - 1];
let index = -1;
if (lastCheckedNodes?.level === level) {
index = treeNodes.findLastIndex((item) => item.value === lastCheckedNodes.value);
} else {
while (lastCheckedNodes) {
if (lastCheckedNodes?.level === level) {
// eslint-disable-next-line no-loop-func
index = treeNodes.findIndex((item) => item.value === lastCheckedNodes.value);
break;
}
lastCheckedNodes = lastCheckedNodes?.parent;
}
}
if (index !== -1) {
scrollToElement({
index,
});
}
};

const handleScroll = (event: WheelEvent): void => {
if (isVirtualScroll.value) onInnerVirtualScroll(event as unknown as WheelEvent);
};

watch(
isVisible,
() => {
if (props.scroll && props.cascaderContext.visible) {
setTimeout(() => {
nextTick(() => {
onScrollIntoView();
});
}, 16);
}
},
{
immediate: true,
},
);

return () => {
const { treeNodes, isFilter, segment, listKey: key } = props;

return (
<div
ref={panelWrapperRef}
onScroll={handleScroll}
class={[
`${COMPONENT_NAME.value}__menu`,
'narrow-scrollbar',
{
[`${COMPONENT_NAME.value}__menu--segment`]: segment,
[`${COMPONENT_NAME.value}__menu--filter`]: isFilter,
},
]}
style={{
position: isVirtualScroll.value ? 'relative' : undefined,
}}
>
{isVirtualScroll.value ? (
<>
<div style={cursorStyle.value}></div>
<ul key={key} style={listStyle.value}>
{virtualConfig.visibleData.value.map((node, index) => renderItem(node, index))}
</ul>
</>
) : (
<ul key={key}>{treeNodes.map((node: TreeNode, index: number) => renderItem(node, index))}</ul>
)}
</div>
);
};
},
});
Loading
Loading