对页面多级列表遍历 实时更新搜索结果的搜索框

  1. 背景

    由于公司管理平台的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>

 

posted @ 2020-11-25 09:40  SpongGirl  阅读(379)  评论(0)    收藏  举报