主要是基于 bootstrap 提供的下拉菜单组件,嵌套一个子菜单,并结合复选框实现了二级多选功能。二级选择框中具体选项的相关数据都是从后端拿的,所以还会涉及到动态插入元素的处理方法,下面一步步地进行介绍。
基于 bootstrap 提供的下拉菜单组件,在下拉菜单中嵌套菜单,二级菜单的具体选项动态插入的,HTML 代码如下。
<div class="container">
<div class="row">
<label class="col-xs-2 col-xs-offset-1 control-label text-center">应用列表label>
<div class="col-xs-6 dropdown">
<input type="hidden" id="checkedList">
<div id="checkedNameList" class="form-control text-center dropdown-toggle" data-toggle="dropdown">div>
<ul class="dropdown-menu col-xs-6" role="menu">
<li data-index="01" class="dropdown-submenu">
<label>社交label>
<ul class="dropdown-menu col-xs-11">ul>
li>
<li data-index="02" class="dropdown-submenu">
<label>游戏label>
<ul class="dropdown-menu col-xs-11">ul>
li>
<li data-index="03" class="dropdown-submenu">
<label>视频label>
<ul class="dropdown-menu col-xs-11">ul>
li>
<li data-index="04" class="dropdown-submenu">
<label>购物label>
<ul class="dropdown-menu col-xs-11">ul>
li>
<li data-index="05" class="dropdown-submenu">
<label>音乐label>
<ul class="dropdown-menu col-xs-11">ul>
li>
<li data-index="06" class="dropdown-submenu">
<label>下载label>
<ul class="dropdown-menu col-xs-11">ul>
li>
<li data-index="07" class="dropdown-submenu">
<label>网址label>
<ul class="dropdown-menu col-xs-11">ul>
li>
ul>
div>
div>
div>
主要是想通过 CSS 实现以下几个效果:
/* 显示所有已选择的应用名 */
#checkedNameList {
min-height: 34px;
height: auto;
}
.dropdown-toggle::after{
display: block;
content: " ";
float: right;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 4px 4px 0 4px;
border-top-color: #555;
margin-top: 10px;
}
.dropdown-menu {
text-align: center;
left: 15px;
top: auto;
padding: 0;
}
/* 更改滚动条样式 */
.dropdown-menu::-webkit-scrollbar {
width: 4px;
border-radius: 4px;
}
/* 更改滚动条的轨道样式 */
.dropdown-menu::-webkit-scrollbar-track {
border-radius: 4px;
}
/* 更改滚动条的滑块样式 */
.dropdown-menu::-webkit-scrollbar-thumb {
background-color: #d6d6d6;
border-radius: 4px;
}
/* 菜单中的选项 */
.dropdown-menu > li > label {
width: 100%;
padding: 2px 12px;
margin: 0;
font-weight: 400;
}
/* 鼠标悬停处的选项时更改背景色 */
.dropdown-menu > li:hover {
background-color: #ebebeb;
}
/* 一级菜单中的右箭头小图标 */
.dropdown-submenu > label::after {
display: block;
content: " ";
float: right;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 4px 0 4px 4px;
border-left-color: #ccc;
margin-top: 4px;
}
.dropdown-submenu:hover > label::after {
border-left-color: #000;
}
/* 二级菜单的位置 */
.dropdown-submenu > .dropdown-menu {
top: -1px;
left: 100%;
height: 170px;
margin: 0;
border-left: none;
overflow: auto;
}
/* 二级菜单中的多选框向右浮动 */
.dropdown-submenu > .dropdown-menu input {
float: right;
}
/* 鼠标悬停在一级菜单的选项时显示对应的二级菜单 */
.dropdown-submenu:hover > .dropdown-menu {
display: block;
}
现在需要考虑如何将获取到的选项数据插入到页面中。
// 假设获取到的数据如下
var data_list = [
{appId: "01-0001", appName: "QQ"},
{appId: "01-0002", appName: "微信"},
{appId: "02-0001", appName: "王者荣耀"},
{appId: "02-0002", appName: "和平精英"},
{appId: "03-0001", appName: "腾讯视频"},
{appId: "03-0002", appName: "快手短视频"},
{appId: "04-0001", appName: "淘宝"},
{appId: "04-0002", appName: "京东"},
{appId: "05-0001", appName: "QQ音乐"},
{appId: "05-0002", appName: "网易云音乐"},
{appId: "06-0001", appName: "迅雷下载"},
{appId: "06-0002", appName: "百度网盘"},
{appId: "07-0001", appName: "百度"},
{appId: "07-0002", appName: "谷歌"},
];
最基础的办法,遍历数据集,直接将每一项数据插入到二级菜单中,代码如下。
// 动态插入二级菜单的选项
data_list.forEach(v => {
var category = v.appId.slice(0, 2);
var opt = `${v.appId} ">${v.appName}`;
$(`.dropdown-submenu[data-index=${category}]`).find(".dropdown-menu").append(opt);
});
但这个方法有一个缺陷,每一次遍历都会向 DOM 中插入一个新的 li 元素。我们知道,向DOM树中新增一个DOM节点会导致回流。这还只是数据比较少的情况下,但是数据量很大的话怎么办?也太消耗性能了,此时文档片段就起了很重要的作用,先把同一类的选项放到一个文档片段中,然后再一次性添加到对应的二级菜单中,因为文档片段存在于内存中,不在DOM树里,所以把子元素插入到碎片化文档的操作不会导致回流,这样能有效地减少回流次数,代码如下。
// 对数据进行分类,并设置每个类对应的文档片段
var app_list = data_list.reduce((now, v) => {
var c = v.appId.slice(0, 2);
if (!now.hasOwnProperty(c)) {
now[c] = document.createDocumentFragment();
}
var opt = $(`${v.appId} ">${v.appName}`);
// 往碎片化文档中添加的元素必须是DOM节点
// jQuery生成的是jQuery对象,这是一个类数组对象,它的第一个元素就是对应的DOM节点
now[c].appendChild(opt[0]);
return now;
}, {});
// 将每个类的文档片段插入到相应的菜单中
for (var [k, v] of Object.entries(app_list)) {
$(`.dropdown-submenu[data-index=${k}]`).find(".dropdown-menu").append(v);
}
现在解决了动态插入的问题,看看如何实现第一次点击选中、第二次点击取消选中并显示对应的结果。因为选项是动态插入的,为了保证 input 上的事件能被触发,我们将 input 元素上的事件委托到父元素上,点击元素时,如果当前是选中状态,则将当前选项的应用名和应用ID存入,若取消选中,则将应用名和应用ID删除,代码如下。
$(".dropdown-submenu > .dropdown-menu").on('click', 'input', function(e){
// 当前选项的应用ID
var nowId = $(this).parent().parent().attr("data-value");
// 当前选项的应用名
var nowName = $(this).parent().text();
// 所有已选中的应用ID
var list = !$("#checkedList").val() ? [] : $("#checkedList").val().split(" ");
// 所有已选中的应用名
var listName = !$("#checkedNameList").text() ? [] : $("#checkedNameList").text().split(" ");
var index = list.indexOf(nowId);
if ($(this).is(":checked")) {
// 当前选项被选中,将当前选项的应用ID和应用名存入已选中的应用ID和应用名数组中
if (index === -1) {
list.push(nowId);
listName.push(nowName);
}
} else {
// 当前选项取消选中,将当前选项的应用ID和应用名从已选中的应用ID和应用名数组中删除
if (index !== -1) {
list.splice(index, 1);
listName.splice(index, 1);
}
}
$("#checkedList").val(list.join(" "));
$("#checkedNameList").text(listName.join(" "));
});
bootstrap 提供的下拉菜单组件在点击了一个选项之后,下拉菜单会消失,但是现在需要能进行多选,所以这个情况需要避免。在点击时,实际上点击的是 li 的子元素,但是由于事件冒泡,点击事件会冒泡至 li 元素导致下拉菜单被隐藏。可以通过阻止事件冒泡解决这个问题,代码如下。
// 注意最好不要直接为label元素绑定事件,因为二级菜单的选项是动态生成的。这里没有体现出来,但实际上选项的数据是异步获取的
// 可以使用jQuery的事件代理,将事件绑定到父元素上
$(".dropdown-menu").on('click', 'label', function(e){
// 阻止事件冒泡到li元素,避免出现点击一个选项后列表消失的情况
e.stopPropagation();
});