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