效果图:
github项目地址,地址。
可以点击链接,直接体验。
首先,想了解cascader组件的整体结构的,可以先看这篇文章。
由于使用了vue-virtual-scroll-list这个第三方组件,所以建议各位看官先去了解一个这个组件,地址。
实现步骤:
- 在cascader-menu中添加vue-virtual-scroll-list。
- 编写一个vue-virtual-scroll-list需要的date-component组件。
- 在cascader-panel中改写自动滚动的方法。
由于组件上的列表即cascader-node集合是在cascader-menu中渲染的,所以我们在cascader-menu中添加vue-virtual-scroll-list组件。
cascader-menu中渲染cascader-node集合的原来代码:
renderNodeList(h) {
const { menuId } = this;
const { isHoverMenu } = this.panel;
const events = { on: {} };
if (isHoverMenu) {
events.on.expand = this.handleExpand;
}
const nodes = this.nodes.map((node, index) => {
const { hasChildren } = node;
return (
<cascader-node
key={ node.uid }
node={ node }
node-id={ `${menuId}-${index}` }
aria-haspopup={ hasChildren }
aria-owns = { hasChildren ? menuId : null }
{ ...events }></cascader-node>
);
});
return [
...nodes,
isHoverMenu ? <svg ref='hoverZone' class='el-cascader-menu__hover-zone'></svg> : null
];
}
我们通过配置config.virtualScroll: true来决定是否使用虚拟滚动,添加完vue-virtual-scroll-list之后的代码是:
renderNodeList(h) {
const { menuId, nodes } = this;
const { isHoverMenu, config } = this.panel;
const events = { on: {} };
if (isHoverMenu) {
events.on.expand = this.handleExpand;
}
this.virtualListProps.menuId = menuId;
const nodeItems = nodes.map((node, index) => {
const { hasChildren } = node;
return (
<cascader-node
key={ node.uid }
node={ node }
node-id={ `${menuId}-${index}` }
aria-haspopup={ hasChildren }
aria-owns = { hasChildren ? menuId : null }
{ ...events }></cascader-node>
);
});
return [
config.virtualScroll ? <virtual-list ref="virtualList" class="el-cascader-menu__virtual-list" data-key="uid" data-sources={nodes} extra-props={this.virtualListProps} data-component={virtualListItem}>
</virtual-list> : [...nodeItems],
isHoverMenu ? <svg ref='hoverZone' class='el-cascader-menu__hover-zone'></svg> : null
];
}
我们了解一个vue-virtual-scroll-list组件(即上图代码中的virtual-list组件)的使用方式:
- data-sources:列表的数据源,比如
[ { label: 1, value: 1 }, { label: 2, value: 2} ]
- data-key:列表li的每个key值,可以默认为'uid',这个是组件自动添加的。
- data-component:vue-virtual-scroll-list对于列表的每个item都用这个属性传入的组件来渲染,即传入列表的每个item所属的组件。
- extra-props:额外可传入的参数。
由于vue-virtual-scroll-list需要一个item组件,所以我们就创建一个virtualListItem组件,代码如下:
<script>
import CascaderNode from './cascader-node.vue';
export default {
name: 'ElCascaderVirtualScrollItem',
components: {CascaderNode},
props: {
index: { // index of current item
type: Number
},
source: { // here is: {uid: 'unique_1', text: 'abc'}
type: Object,
default() {
return {};
}
},
menuId: {
type: String,
default() {
return '';
}
}
},
render(h) {
const { source, menuId, index } = this;
return (
<cascader-node
key={ source.id }
node={ source }
node-id={ `${menuId}-${index}` }
aria-haspopup={source.hasChildren }
aria-owns={source.hasChildren ? menuId : null }
></cascader-node>
);
}
};</script>
其实上图代码就是简单的用个组件套了cascader-node。
然后在cascader-menu的渲染函数做如下修改:
return (
config.virtualScroll ? <div class="el-cascader-menu">
{ isEmpty ? this.renderEmptyText() : this.renderNodeList(h) }
</div> :
<el-scrollbar
tag="ul"
role="menu"
id={ menuId }
class="el-cascader-menu"
wrap-class="el-cascader-menu__wrap"
view-class={{
'el-cascader-menu__list': true,
'is-empty': isEmpty
}}
{ ...events }>
{ isEmpty ? this.renderEmptyText(h) : this.renderNodeList(h) }
</el-scrollbar>
);
对于cascader-node组件,我们先给它添加一个inActivePath属性,以及在合适的地方更新inActivePath。
我来解释一下inActivePath代表什么,请看下图:
当我们点击选择了任何一个cascader-node组件时,那么activePath就会被更新,比如此刻activePath = 东南-江苏-南京。
而inActivePath就是用来记录该cascader-node是否是在activePath中,比如东南的inActivePath = true,而西北的inActivePath = false。
而更新cascader-node的inActivePath的时刻就在下图代码:
computed: {
inActivePath() {
let inActivePath = this.isInPath(this.panel.activePath);
// 此处更新
this.node.inActivePath = inActivePath;
return inActivePath;
}
}
我们先看一段cascader-panel的代码:
// 该方法是:当点击cascader后,出现下拉框的时候触发,用于自动将下拉框自动滚动到合适的地方。
scrollIntoView() {
if (this.$isServer) return;
const menus = this.$refs.menu || [];
// 遍历所有的cascader-menu
menus.forEach(menu => {
const menuElement = menu.$el;
if (menuElement) {
// 查询cascader-menu中处于is-active或者is-active-path的节点
// 即处于activePath中或者(activePath为空时,第一个被选中的)cascader-node
const container = menuElement.querySelector('.el-scrollbar__wrap');
const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||
menuElement.querySelector('.el-cascader-node.in-active-path');
// 将cascader-menu自动滚动到上述代码所找到的cascader-node节点。
scrollIntoView(container, activeNode);
}
});
},
由于我们用了vue-virtual-scroll-list,所有这方法的逻辑我们得自己写一个。
scrollIntoView() {
if (this.$isServer) return;
const menus = this.$refs.menu || [];
// 遍历所有的menu
menus.forEach(menu => {
// 当使用了虚拟滚动
if (this.config.virtualScroll) {
let currentNodeIndex = -1;
// 先找到当前cascader-menu中的第一个inActivePath:true的cascader-node
menu.nodes.find((item, index) => {
let flag = item.inActivePath;
flag && (currentNodeIndex = index);
return flag;
});
if (currentNodeIndex !== -1) {
// 如果确实有inActivePath的节点,则用vue-virtual-scroll-list的方法,直接滚动到改节点。
menu.$refs.virtualList && menu.$refs.virtualList.scrollToIndex(currentNodeIndex);
} else {
// 如果没找到inActivePath的节点,则找到第一个checked:true的节点。然后滚动到改节点
menu.nodes.find((item, index) => {
let flag = item.checked || item.indeterminate;
flag && (currentNodeIndex = index);
return flag;
});
menu.$refs.virtualList && currentNodeIndex === -1 ? menu.$refs.virtualList.reset() : menu.$refs.virtualList.scrollToIndex(currentNodeIndex);
}
} else {
// 如果不使用虚拟
const menuElement = menu.$el;
if (menuElement) {
const container = menuElement.querySelector('.el-scrollbar__wrap');
const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||
menuElement.querySelector('.el-cascader-node.in-active-path');
scrollIntoView(container, activeNode);
}
}
});
},