对页面多级列表遍历 实时更新搜索结果的搜索框
- 背景
由于公司管理平台的menu的管理功能过多,于是要做一个搜索框,最终实现的搜索框会显示列表之间的层级关系,不用点击按钮便出现结果,实时响应更新搜索结果,呈现结果如下:


2. 思路
项目是php的MVC框架,功能列表的展示不是通过调用接口拿到数据然后渲染的,接收从Controller传回来的一个data,直接 echo $menuList,$menuList是一段拼接好的html字符串。可以有两种方法拿到我们需要的数组(包含各个menu标题和href),一是在controller里面定义一个searchList,在Modal里面查找返回,但是此举需要访问数据库,为了减轻服务器的负担,不进行考虑;二是在前端页面进行DOM节点的遍历,组装好我们需要的menuList,然后进行遍历,得到搜索结果。
- 先写好样式:这里涉及到一个怎么将搜索图标放进searchBox里面,将搜索图标的position设置为absolute,然后input框的border设置为none,外层包裹的div searchBox的position设置为relative即可。toast-wrap在找不到信息的时候显示提示信息,因为我们的提示信息只有一行,较为简单就不用类似tooltips的插件,其他较复杂的信息可以用bootstrap的toasts插件(https://getbootstrap.com/docs/4.2/components/toasts/)来进行展示。
<div id="searchBox">
<input id="searchTitle" type="text" autocomplete="off" placeholder="请输入想搜索的菜单..."><i id="search-icon" class="fa fa-search fa-fw"></i>
<div class="toast-wrap">
</div>
</div>
<ul class="beforeSearch" id="searchResult"></ul>
#searchBox{
display: block;
height: 30px;
background: #666666;
list-style: none;
position: relative;
}
#searchBox:focus{
border-top: 1px solid blue!important;
background: #373737;
color: #DDDDDD;
}
#searchTitle{
padding-left: 20px;
background: #666666;
width: 90%;
height: 30px;
border: none;
display: inline-block;
color: #C4C4C4;
font-size: 13px;
}
#searchBox input:focus{
border: none!important;
outline: none!important;
color: #ffffff;
}
input::-webkit-input-placeholder {
/* WebKit browsers */
color: #bbb;
}
#column-left.active #searchBox input:-moz-placeholder {
/* Mozilla Firefox 4 to 18 */
color: #bbb;
}
input::-moz-placeholder {
/* Mozilla Firefox 19+ */
color: #bbb;
}
input:-ms-input-placeholder {
/* Internet Explorer 10+ */
color: #bbb;
}
#search-icon{
display: inline-block;
z-index: 2;
color: #ffffff;
position: absolute;
line-height: 30px;
}
.toast-wrap{
opacity: 0;
position: fixed;
top: 14%;
left: 110px;
color: #fff;
}
.toast-msg{
background-color: rgba(0,0,0,0.7);
padding: 2px 5px;
border-radius: 5px;
}
.toastAnimate{
animation: toastKF 2s;
}
@keyframes toastKF{
0% {opacity: 0;}
50% {opacity: 1; z-index: 9999}
100% {opacity: 0; z-index: 0}
}
.hidesearchBox{
display: none!important;
}
.beforeSearch{
display: none;
}
.afterSearch{
list-style:none;
width: 235px;
display: inline;
position: absolute;
z-index: 2;
background: #373737;
padding: 0!important;
}
.afterSearch li a{
color: #C4C4C4!important;
text-decoration:none!important;
padding: 0 0 0 20px!important;
}
.tooltip{
position: absolute;
display: none;
border-style: solid;
white-space: nowrap;
z-index:99;
background-color:#ffffff;
border-width: 0px;
border-color: rgb(51,51,51);
font-size: 13px;
padding: 3px;
pointer-events: none;
}
- 分析列表结构
需要搜索的列表是ul>li,li>a+ul(只有最后一层列表的a标签才带有href,只有非最后一层列表才有ul),每个ul是一层列表。这里生成的menuList是树形的,后续进行搜索匹配的时候会逐层进行遍历,确保不会漏掉,还有另外一种实现方式,即生成扁平的menuList,最后遍历只需要遍历一遍然后再对遍历结果进行筛选,即可。
//方法一
$('#menu>li').each(function () { var title1 = $(this).children('a').eq(0).text().trim(); var href1 = $(this).children('a').eq(0).attr('href'); if(title1 && href1){ menuList.push({'title':title1,'href':href1}); } else { var child2Array = []; $(this).children('ul').children('li').each(function () { var title2 = $(this).children('a').eq(0).text().trim(); var href2 = $(this).children('a').eq(0).attr('href'); if(title2&&href2){ child2Array.push({'title':title2,'href':href2}); } else { var child3Array = []; $(this).find('ul>li').each(function() { var title3 = $(this).children('a').eq(0).text().trim(); var href3 = $(this).children('a').eq(0).attr('href'); child3Array.push({'title':title3,'href':href3}); }) child2Array.push({'title':title2,'childs':child3Array}); } }) menuList.push({'title':title1,'childs':child2Array}); } }) }
//方法二
$('#menu>li').each(function () {
var title1 = $(this).children('a').eq(0).text().trim();
var href1 = $(this).children('a').eq(0).attr('href');
if(title1 && href1){
menuList.push({'title':title1,'href':href1}); //一级
} else {
$(this).children('ul').children('li').each(function () {
var title2 = $(this).children('a').eq(0).text().trim();
var href2 = $(this).children('a').eq(0).attr('href');
if(title2&&href2){ //二级
menuList.push({'title':title1+'>'+title2,'href':href2});
} else { //三级
$(this).find('ul>li').each(function() {
var title3 = $(this).children('a').eq(0).text().trim();
var href3 = $(this).children('a').eq(0).attr('href');
menuList.push({'title':title1+'>'+title2+'>'+title3,'href':href3});
})
}
})
}
})
- 遍历数组,得到结果列表。对于树形的menuList,遍历的时候需要每一层都进行遍历,先遍历一层,若找到了则判断当前搜索到的是否还有子元素,因为子元素的a标签没有href,需要判断是否有子元素,将第一个子元素的href赋值给当前搜索到的结果,便于后续点击跳转。
var searchKey = ''; var menuSearch = function(){var searchResult = []; var showStr = ''; searchKey = $("#searchTitle").val().trim(); if($('#searchResult').attr("class")=='afterSearch'){ $("#searchResult>li").remove(); $("#searchResult").removeClass("afterSearch").addClass("beforeSearch"); } for (let i = 0; i<menuList.length;i++) { if((menuList[i].title.indexOf(searchKey)) !== -1 ){ if(menuList[i].href){ //一级 searchResult.push({'title':menuList[i].title,'href':menuList[i].href,'level':1}); } else if(menuList[i].childs[0].href) { //二级 searchResult.push({'title':menuList[i].title,'href':menuList[i].childs[0].href,'level':1}); } else{ // 三级 searchResult.push({'title':menuList[i].title,'href':menuList[i].childs[0].childs[0].href,'level':1}); } } if(menuList[i].childs){ var child2 = menuList[i].childs; first:for(let j = 0;j<child2.length;j++){ if((child2[j].title.indexOf(searchKey)) !== -1 ){ if(child2[j].childs){ searchResult.push({'title':menuList[i].title+'>'+child2[j].title,'href':child2[j].childs[0].href,'Level':2}); } else { searchResult.push({'title':menuList[i].title+'>'+child2[j].title,'href':child2[j].href,'Level':2}); } } if(child2[j].childs){ var child3 = child2[j].childs; second:for(let k =0;k<child3.length;k++){ if((child3[k].title.indexOf(searchKey)) !== -1) { //在三级查找到了 searchResult.push({'title':menuList[i].title+'>'+child2[j].title+'>'+child3[k].title,'href':child3[k].href,'level':3}); } } } } } else {} } // console.log(searchResult); boolFirst = true; //展示 if((searchResult.length>0) && (searchKey.length>0)){ for (let i = 0; i<searchResult.length;i++) { showStr += '<li class="searchresultList" style=\"list-style: none; height: 35px; padding-right:18px; border-bottom: 1px solid #515151; line-height: 35px; font-size: 13px; color: #C4C4C4; vertical-align: middle; \"> <a data-toggle="tooltip" data-placement="top" title=\"'+searchResult[i].title+'\" style=\"border-bottom: none; white-space:nowrap; overflow:hidden; text-overflow: ellipsis;-o-text-overflow:ellipsis; cursor:pointer;\" href=\"' +searchResult[i].href+'\">'+searchResult[i].title+'</a></li>'; } $("#searchResult").removeClass("beforeSearch").addClass("afterSearch") $('#searchResult').append(showStr) //跳转 $('#searchResult>li').on('click',function (event) { var link = $(this).children('a').eq(0).attr('href') location.href=link; event.stopPropagation(); }); } }
- 触发搜索功能
我们希望在输入完关键字就进行结果的展示,不必点击触发,可以使用bind给选定的元素进行事件绑定,bind的详细用法请移步https://www.runoob.com/jquery/event-bind.html,
//触发搜索功能
$('#searchTitle').bind("input propertychange focus",function (event) { event.stopPropagation(); if (timer) { clearTimeout(timer); } menuSearch(); if(searchKey.length>0){ timer = setTimeout(function () { toast('找不到相关信息!'); },1000); } }); $('#searchTitle').on('click',function (event) { event.stopPropagation(); }); $('#search-icon').on('click',function (event) { event.stopPropagation(); menuSearch(); if(searchKey.length==0){ toast('找不到相关信息!'); } });
propertychange 是IE专属用于动态监听监听输入框的值变化,input是标准的浏览器事件,一般应用于input输入框,当input的值发生变化就会发生,无论是键盘输入还是鼠标黏贴的改变都能及时监听到变化,两者一起用是为了兼容IE9以下input不兼容的问题。
搜索结果列表的打开和闭合,当点击除了结果列表和searchBox以外的界面的时候,关闭搜索结果列表。代码如下:
$('#searchTitle').bind("input propertychange focus",function (event) {
event.stopPropagation();
menuSearch();
});
$('#searchTitle').on('click',function (event) {
event.stopPropagation();
});
$('#search-icon').on('click',function (event) {
event.stopPropagation();
menuSearch();
if(searchKey.length==0){
toast('找不到相关信息!');
}
});
$(document).on('click',':not(#searchResult)',function(e){
if($('#searchResult').attr("class")=='afterSearch'){
$("#searchResult>li").remove();
$("#searchResult").removeClass("afterSearch").addClass("beforeSearch");
}
});
需要注意到的是,这里用到了event.stopPropagation(); 原因是在用:not选择器进行操作时,只能选取一项内容进行筛选(网上查询说:not选择器需要进行多项条件筛选用英文半角符号分割,试过没用...),那么就要对子元素和其他元素的click时间添加防止事件穿透的操作event.stopPropagation() 。这里的menuSearch()是我将上面的查询匹配过程封装成了一个function。
3. 优化
- 一开始的menuList是页面渲染的时候就有的,若是没有进行搜索操作的时候也会进行数组的组装,优化一把menuList的生成放在了第一次触发搜索功能的时候,先在外层设置一个坑boolFirst为false,第一次触发之后设置为true,只有boolFirst为false的时候才会进行menuList的组装。
- 使用时间戳进行节流。在做toast的时候,判断当一秒无操作之后再进行判断是否符合条件,符合条件则toast;
var timer = null; //外层初始化timer //下面的放置在menuSearch()函数内 if (timer) { clearTimeout(timer); } if(searchKey.length>0){ timer = setTimeout(function () { toast('找不到相关信息!'); },1000); }
- 添加tooltip:因为我们的menu是在左侧的,宽度是固定的长度,如果将searchResult设置为自适应则容易遮盖右边的内容,但是若是搜索结果过长则会出现显示不全的问题(我们的结果展示列表为固定高度,结果太长会自动换行,会有重影)。解决此问题,首先要禁止换行,将多余的部分用省略号代表,然后利用bootstrap的tooltip,鼠标悬停在搜索结果上面的时候出现小tip显示全部信息。注意:tooltip需要在脚本里面进行初始化:$('[data-toggle="tooltip"]').tooltip()才能生效。
禁止换行:white-space:nowrap;
将超出宽度的内容隐藏起来:overflow:hidden;
将超出宽度的内容用省略号表示:text-overflow: ellipsis;-o-text-overflow:ellipsis;
需要特别注意的一个点是,需要设置 i 标签的的display属性为block,i 标签是行内标签,在其上设置宽高都不起作用,所以上述有关宽度的限制和设置,overflow:hidden; text-overflow: ellipsis;-o-text-overflow:ellipsis;都会失效,
showStr += '<li class="searchresultList" style=\"list-style: none; height: 35px; padding-right:18px; border-bottom: 1px solid #515151; line-height: 35px; font-size: 13px; color: #C4C4C4; vertical-align: middle; \"> <a data-toggle="tooltip" data-placement="top" title=\"'+searchResult[i].title+'\" style=\"display: block; border-bottom: none; white-space:nowrap; overflow:hidden; text-overflow: ellipsis;-o-text-overflow:ellipsis; cursor:pointer;\" href=\"'
+searchResult[i].href+'\">'+searchResult[i].title+'</a></li>';
4. 总结
需求很小,涉及到的知识点都较为基础,一点一滴慢慢积累吧。下面是全部代码:
<div id="searchBox">
<input id="searchTitle" type="text" autocomplete="off" placeholder="请输入想搜索的菜单..."><i id="search-icon" class="fa fa-search fa-fw"></i>
<div class="toast-wrap">
</div>
</div>
<ul class="beforeSearch" id="searchResult"></ul>
<ul id="menu" style="position:relative;">
...这里是menu的列表
</ul>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip()
var boolFirst = false;
var menuList = [];
var searchResult = null;
var timer = null;
var searchKey = '';
var menuSearch = function(){
searchResult = [];
if (timer) {
clearTimeout(timer);
}
if(!boolFirst){
$('#menu>li').each(function () {
var title1 = $(this).children('a').eq(0).text().trim();
var href1 = $(this).children('a').eq(0).attr('href');
if(title1 && href1){
menuList.push({'title':title1,'href':href1});
} else {
var child2Array = [];
$(this).children('ul').children('li').each(function () {
var title2 = $(this).children('a').eq(0).text().trim();
var href2 = $(this).children('a').eq(0).attr('href');
if(title2&&href2){
child2Array.push({'title':title2,'href':href2});
} else {
var child3Array = [];
$(this).find('ul>li').each(function() {
var title3 = $(this).children('a').eq(0).text().trim();
var href3 = $(this).children('a').eq(0).attr('href');
child3Array.push({'title':title3,'href':href3});
})
child2Array.push({'title':title2,'childs':child3Array});
}
})
menuList.push({'title':title1,'childs':child2Array});
}
})
}
var showStr = '';
searchKey = $("#searchTitle").val().trim();
if($('#searchResult').attr("class")=='afterSearch'){
$("#searchResult>li").remove();
$("#searchResult").removeClass("afterSearch").addClass("beforeSearch");
}
for (let i = 0; i<menuList.length;i++) {
if((menuList[i].title.indexOf(searchKey)) !== -1 ){
if(menuList[i].href){ //一级
searchResult.push({'title':menuList[i].title,'href':menuList[i].href,'level':1});
} else if(menuList[i].childs[0].href) { //二级
searchResult.push({'title':menuList[i].title,'href':menuList[i].childs[0].href,'level':1});
} else{ // 三级
searchResult.push({'title':menuList[i].title,'href':menuList[i].childs[0].childs[0].href,'level':1});
}
}
if(menuList[i].childs){
var child2 = menuList[i].childs;
first:for(let j = 0;j<child2.length;j++){
if((child2[j].title.indexOf(searchKey)) !== -1 ){
if(child2[j].childs){
searchResult.push({'title':menuList[i].title+'>'+child2[j].title,'href':child2[j].childs[0].href,'Level':2});
} else {
searchResult.push({'title':menuList[i].title+'>'+child2[j].title,'href':child2[j].href,'Level':2});
}
}
if(child2[j].childs){
var child3 = child2[j].childs;
second:for(let k =0;k<child3.length;k++){
if((child3[k].title.indexOf(searchKey)) !== -1) { //在三级查找到了
searchResult.push({'title':menuList[i].title+'>'+child2[j].title+'>'+child3[k].title,'href':child3[k].href,'level':3});
}
}
}
}
}
else {}
}
// console.log(searchResult);
boolFirst = true;
//展示
if((searchResult.length>0) && (searchKey.length>0)){
for (let i = 0; i<searchResult.length;i++) {
showStr += '<li class="searchresultList" style=\"list-style: none; height: 35px; padding-right:18px; border-bottom: 1px solid #515151; line-height: 35px; font-size: 13px; color: #C4C4C4; vertical-align: middle; \"> <a data-toggle="tooltip" data-placement="top" title=\"'+searchResult[i].title+'\" style=\"display: block; border-bottom: none; white-space:nowrap; overflow:hidden; text-overflow: ellipsis;-o-text-overflow:ellipsis; cursor:pointer;\" href=\"'
+searchResult[i].href+'\">'+searchResult[i].title+'</a></li>';
}
$("#searchResult").removeClass("beforeSearch").addClass("afterSearch")
$('#searchResult').append(showStr)
//跳转
$('#searchResult>li').on('click',function (event) {
var link = $(this).children('a').eq(0).attr('href')
location.href=link;
event.stopPropagation();
});
} else {
if(searchKey.length>0){
timer = setTimeout(function () {
toast('找不到相关信息!');
},1000);
}
}
}
$('#searchTitle').bind("input propertychange focus",function (event) {
event.stopPropagation();
menuSearch();
});
$('#searchTitle').on('click',function (event) {
event.stopPropagation();
});
$('#search-icon').on('click',function (event) {
event.stopPropagation();
menuSearch();
if(searchKey.length==0){
toast('找不到相关信息!');
}
});
$(document).on('click',':not(#searchResult)',function(e){
if($('#searchResult').attr("class")=='afterSearch'){
$("#searchResult>li").remove();
$("#searchResult").removeClass("afterSearch").addClass("beforeSearch");
}
});
})
function toast(msg){
$('.toast-msg').remove();
setTimeout(function(){
var str = '<span class="toast-msg">'+msg+'</span>'
$('.toast-wrap').append(str);
$('.toast-wrap').removeClass("toastAnimate");
setTimeout(function(){
$('.toast-wrap').addClass("toastAnimate");
}, 100);
},100);
}
</script>
<style>
#searchBox{
display: block;
height: 30px;
background: #666666;
list-style: none;
position: relative;
}
#searchBox:focus{
border-top: 1px solid blue!important;
background: #373737;
color: #DDDDDD;
}
#searchTitle{
padding-left: 20px;
background: #666666;
width: 207px;
height: 30px;
border: none;
display: inline-block;
color: #C4C4C4;
font-size: 13px;
}
input:focus{
border: none!important;
outline: none!important;
color: #ffffff;
}
input::-webkit-input-placeholder {
/* WebKit browsers */
color: #bbb;
}
input:-moz-placeholder {
/* Mozilla Firefox 4 to 18 */
color: #bbb;
}
input::-moz-placeholder {
/* Mozilla Firefox 19+ */
color: #bbb;
}
input:-ms-input-placeholder {
/* Internet Explorer 10+ */
color: #bbb;
}
#search-icon{
display: inline-block;
z-index: 2;
color: #ffffff;
position: absolute;
line-height: 30px;
}
.toast-wrap{
opacity: 0;
position: fixed;
top: 14%;
left: 110px;
color: #fff;
}
.toast-msg{
background-color: rgba(0,0,0,0.7);
padding: 2px 5px;
border-radius: 5px;
}
.toastAnimate{
animation: toastKF 2s;
}
@keyframes toastKF{
0% {opacity: 0;}
50% {opacity: 1; z-index: 9999}
100% {opacity: 0; z-index: 0}
}
.hidesearchBox{
display: none!important;
}
.beforeSearch{
display: none;
}
.afterSearch{
list-style:none;
width: 235px;
display: inline;
position: absolute;
z-index: 2;
background: #373737;
padding: 0!important;
}
.afterSearch li a{
color: #C4C4C4!important;
text-decoration:none!important;
padding: 0 0 0 20px!important;
}</style>

浙公网安备 33010602011771号