这几天,帮同学解决一个问题,虽然他的问题还没有完全解决,但在解决问题的过程中我做了这个“自动补全”的功能。虽然这个补全的功能在网上也有很多代码,但是在我写代码和学习的过程中,好的较少,清晰的较少,多是转载或是浮躁的写一写,我觉得这样很容易使有问题的人产生入门易,理解难的问题。所以我下面会把自己学习和写这个的过程尽量写的详细一些。
需求:
这个其实你登录到百度首页,我的需求主要是参考这个搜索框的“自动补全”功能,自己随便输入个文字,比如:“我”,看看结果理解下吧~
实现:
1.可以根据文本框的“输入”自动完成与“输入”相关的数据的匹配和前台展示。
2.可以通过键盘方向键实现浏览。
3.可以通过鼠标的点击和键盘的回车选中需要的数据项并更新显示在文本框中。
实现备注:
1.在实现的过程中,后台数据的模拟,是通过数组在js部分暂时实现,真正用到项目上我们可以通过ajax来实现,这是后话,暂不详说。
2.至于兼容性来说,我测试过ie8、chrome、firefox、遨游、360等浏览器。其它的浏览器,尤其是ie系列,如果大家愿意帮这个忙,可以帮忙测试一下。因为我后边还想把这个封装成jsp的自定义标签,所以这个还是很需要大家的帮助的,先谢谢大家了~~
综上,废话不多说,现在开始正是来分析下如何来实现这个功能,开整:
==========================================
分析html的结构:
1.这个通过我自己想,我想应该包括一个文本框,那下面用来显示搜索数据的列表应该用什么呢,我想过select、ul和table,后来还是打开百度首页,输入了个“我”,再F12看了下它的结构,我决定就用他那个吧,详细如下:
2.我的具体实现如下:
<body onload="initialize()">
<div id="bodyDiv" name="bodyDiv" style="position: relative; border-top:0px; margin-top:0; width:160px" >
<!--<span style="color:red;font-size:5px;">*补全功能默认开启</span>-->
<input type="text" id="inputUser" name="inputUser" value="" onfocus="inputFocus()" onkeydown="inputKeydown(event)" onblur="inputBlur()" />
<div id="showUser" name="showUser">
<!-- 此处添加搜索数据的结果,使用的是table标签 -->
</div>
</div>
</body>
大家可以看到我的html结构,我打算用table来做具体的列表组件,这里提一句后话:我的table的id为selector,在js中通过getElementById获得的该table并赋值给一个select变量。大家在此先不用我这么做的意义,只要先知道有这么回事儿就好。
大家应该还可以看到,我在html中绑定了几个事件:
a.initialize(),这个函数主要用于在页面加载完毕后立即初始化一些变量。
b.inputFocus(),这个函数主要是当焦点移动到文本框上时调用
c.inputKeydown(),这个函数主要是当焦点在文本框上时,用户触发了键盘事件,调用此函数处理
d.inputBlur(),这个事件主要是当焦点离开了文本框时,触发该事件。
开始分析js部分:
1.initialize()函数:
我们首先要明白一件事情,当页面加载完毕后,我们需要看到什么?一个文本框!我们不需要看到什么?那个搜索数据的列表!顺便提一句,什么时候需要看到那个列表呢?那就是当文本框中输入了数据(此处我定义的数据都是数字,所以输入的时候若想有结果也暂时输入数字),如果有匹配该输入的数据,则要实时的更新并显示列表。
我们的思路暂时理到这里,其它的留待后话再说,先看代码:
//加载完后,将"提示"列表隐藏
function initialize(){
source=['0123','023',123,1234,212,214,'033333','0352342',1987,17563,20932];
elemCSS={ focus:{'color':'black','background':'#ccc'}, blur:{'color':'black','background':'transparent'} };
inputUse=document.getElementById("inputUser");
showUse=document.getElementById("showUser");
var bodyDiv=document.getElementById("bodyDiv");
showUse.style.display="none";
inputUse.style.width=bodyDiv.style.width;
showUse.style.width=bodyDiv.style.width;
inputValue=inputUse.defaultValue;
}
这里着重要说的是函数中后四行的代码。对于涉及“width”的部分,首先我们先设置用来包含table的div为隐藏状态,这样,table也将是隐藏的。同时将文本框的宽度和列表的宽度也进行设置,此处我用到的是最外层div的宽度。
对于最后一行的代码,我们取出了文本框的默认值(空值)赋值给一个全局变量,这个变量之后的主要作用就是用来记录文本框之前的值,以便用这个值与文本框的当前值相比较,如果值改变了,则需要去后台查询匹配的数据项。
2.inputFocus()函数:
大家想想初始化完之后,我们眼前出现的应该是一个文本框,而“提示”列表此时应该是隐藏的。当我们把鼠标在文本框点击一下,页面的焦点就会切换到文本框上,此时也就出发文本框的onfocus事件,调用inputFocus()函数,代码如下:
//在文本框上触发onfocus()事件
function inputFocus(){
//调用setInterval()函数每200ms刷新一次
this.timer=setInterval(function(){
if(inputUse.value!=''){
//检查文本框的当前值与以前的值是否有变化
if(inputUse.value!=inputValue){
//如果文本框当前值与之前的值不同,记录当前值,已被下次调用时使用
inputValue=inputUse.value;
//清除上次调用显示的结果
showUse.innerHTML='';
if(inputValue!=''){
//定义JS的RegExp对象,查询以inputValue开头的数据
quickExpr=RegExp('^'+inputValue);
//如果数据源不为空,则调用match函数开始匹配数据
//此处如果通过ajax取数据,则适当修改数据源即可
if(source){
match(quickExpr,inputValue,source);
}
}
}
}
else{
inputValue=inputUse.value;
showUse.innerHTML='';
showUse.style.display="none";
}
},200)
}
从代码,我们可以看出,我们的主体内容定义在了setInterval()这个函数中。为什么要这样定义呢?因为一旦焦点在文本框中,我们就需要时刻知道文本框的内容是否发生了变化,以便匹配数据,所以我们利用setInterval函数每200m对文本框执行一次判断。
那么该如何判断呢,之前提到过inputValue,主要是利用这个值与文本框的当前值作比较(从代码中你可以体会到我的意思),并且要更新inputValue的值,以备下次调用使用。如果,文本框的值与200ms之前比发生了变化,我们就要利用js的RegExp对象来访问match()这个函数,这个函数的代码如下:
//该函数用来查询匹配数据
function match(quickExpr,value,source){
var table=null;
var tr=null;
var td=null;
//创建table标签
table=document.createElement('table');
table.id='selector';
table.style.width='100%';
//开始遍历数组
//如果用ajax从后台取数据,我们也可组织成数组的形式返回
for(var i in source){
//再次检验数据是否为空并且用正则取数据
if(value.length>0 && quickExpr.exec(source[i])!=null){
//创建tr标签
tr=document.createElement('tr');
//创建td标签
td=document.createElement('td');
//在td中插入<a href="javascript:void(null);"><span>数据项</span></a>
td.innerHTML = '<a href="javascript:void(null);">'+source[i]+'</a>';
//appendChild()在指定元素的最后一个子节点后添加节点
tr.appendChild(td);
table.appendChild(tr);
showUse.appendChild(table);
}
}
//检验table下面的a标签的数量,以此确定是否将“提示”列表显示
if(showUse.getElementsByTagName('a').length){
showUse.style.display="";
}else{
showUse.style.display="none";
}
}
针对match函数的代码,我在代码中添加的注释相信已经对你的理解有很大的帮助,但我还要在强调的是我选择在此创建包含“提示”列表的table,并根据查询到的符合条件的数据量创建对应的tr,td。这个最终将形成的结构你可以参考上面我对于百度首页的搜索框的截图,以便更好的理解。
3.inputKeydown()函数:
当我们通过上述两步,我们页面上的“提示”列表已经显示。我们必然会通过方向键等键盘按钮访问列表中的数据。这一个过程中,主要的变化是焦点由文本框内变化到了“提示”列表中,代码如下:
function inputKeydown(event){
//兼容IE
event = event || window.event;
//如果按了down键
if(event.keyCode==40){
//如果“提示”列表已经显示,则把焦点切换到列表中的第一个数据项上
if(showUse.style.display==""){
showUse.getElementsByTagName('a')[0].focus();
}else{ //如果“提示”列表未显示,则把焦点依旧留在文本框中
inputUse.focus();
}
}
//如果按了up键
else if(event.keyCode==38){
//如果“提示”列表已经显示,则把焦点切换到列表中的最后一个数据项上
if(showUse.style.display==""){
showUse.getElementsByTagName('a')[showUse.getElementsByTagName('a').length-1].focus();
}else{ //如果“提示”列表未显示,则把焦点依旧留在文本框中
inputUse.focus();
}
}
//如果按了tab键,此时的情况与“百度首页”的处理情况一样
else if(event.keyCode==9){
showUse.innerHTML='';
showUse.style.display="none";
}
}
这里主要想说的是,当"提示"列表已经展开的情况下,如果按了up或者down键(根据事件对象的键码值判断),则将数据项的最后一项或者第一项设置为高亮,并且将焦点真正切换到相应的高亮数据项上。
但是这里还需要着重注意的是按了tab键该如何处理。这里的处理方式与百度相同,就是关闭"提示"列表。
4.inputBlur函数:
这个函数的定义我们要理解下,这个事件定义的是文本框的onblur事件,该事件的含义是当焦点离开元素对象(此处就是指文本框)时调用。那么具体的内容,我们先来看看代码:
//当焦点离开文本框时,触发该事件
function inputBlur(){
//由于焦点已经离开了文本框,则取消setInterval
clearInterval(this.timer);
//记住当前有焦点的选项
var current=0;
//当前table下面的a标签的个数
var aArray=showUse.getElementsByTagName('a');
var len=aArray.length-1;
var select=document.getElementById("selector");
//定义“选项”的onclick事件
var aClick = function(){
//由于“选项”上触发了click事件,this就是指a标签,则把a标签包含的数据赋值给文本框
inputUse.value=this.childNodes[0].data;
//将文本框的当前值更新到记录以前值的变量中
inputValue=inputUse.value;
//由于上面已经选出合适的数据项,则清空table下的内容,并关闭“提示”列表
showUse.innerHTML='';
showUse.style.display='none';
//将焦点移回文本框
inputUse.focus();
};
//定义“选项”的onfocus事件
var aFocus = function(){
for(var i=len; i>=0; i--){
//this是a,this.parentNode是td,select.children[i].children[0]是table.tr.td
if(this.parentNode===select.childNodes[i].childNodes[0]){
//如果是同一个td,则将current的值置为焦点所在位置的值
current = i;
break;
}
}
//添加有焦点的效果
for(var k in elemCSS.focus){
this.style[k] = elemCSS.focus[k];
}
};
//定义“选项”的onblur事件
var aBlur= function(){
//添加无焦点的效果
for(var k in elemCSS.blur)
this.style[k] = elemCSS.blur[k];
};
//定义“选项”的onKeydown是事件
var aKeydown = function(event){
//兼容IE
event = event || window.event;
//如果在选择数据项时按了tab键,此时的情况与“百度首页”的处理情况一样
if(event.keyCode===9){
showUse.innerHTML='';
showUse.style.display = 'none';
inputUse.focus();
}
//如果按了down键
else if(event.keyCode==40){
//向下移动,准备移动焦点
current++;
//如果当前焦点在最后一个数据项上,用户用按了down键,则循环向上,回到文本框上
if(current>len){
current=-1;
inputUse.focus();
}else{
select.getElementsByTagName('a')[current].focus();
}
}
//如果按了up键
else if(event.keyCode==38){
//向上移动,准备移动焦点
current--;
//如果当前焦点在文本框上,用户用按了up键,则循环向下,回到最后一个数据项上
if(current<0){
inputUse.focus();
}else{
select.getElementsByTagName('a')[current].focus();
}
}
};
//将“选项”的事件与相应的处理函数绑定
for(var i=0; i<aArray.length; i++){
aArray[i].onclick = aClick;
aArray[i].onfocus = aFocus;
aArray[i].onblur = aBlur;
aArray[i].onkeydown = aKeydown;
}
}
上面代码的内容很长,但是大家要首先抓住主要的来看。我的建议是先看最后那个for循环。我在这里把定义的四个事件绑定到了一个数组的对象上,而这个数组中的对象就是"提示"列表中每一个数据项。到此,我想大家应该应该还记得上面的inputKeydown()函数了吧,通过该函数将焦点由文本框切换到“提示”列表上,更确切的说是数据项上,此处对应的是<a>标签。那么在数据项上的移动以及选中就要依靠上面定义的四个事件。四个事件的具体分析请大家详细的看注释,有些不懂的地方不妨用浏览器的调试来看看就明白了。
自己的收获和感想:
我先说说技术上的吧,我最初利用table调用innerHTML并将其值设置为空,来实现"提示"列表的清除,使下次调用时仅仅显示当次调用的正确结果。我最初代码的测试是面向chrome的,但后来再调试兼容性时,发现在ie下,table是无法对innerTHML属性进行设置值的,但是可读。再纠结了一下之后,发现其实兼容性的主要问题还就是集中在这个上面,于是果断重新写,最终处理了这个问题。
还有一个就是z-index的使用,这里不详述,但感兴趣的同学可以翻看《程序员》杂志2013.2月版的文章:不为人知的z-index,写的相当不错。
其实最大的体会就是,自己动手写。虽然网上也有很多人写过,但我保证很多是浅尝辄止的。就像我曾经在一个群里问了个关于焦点切换的问题,很多人都说这个东西都写烂了,当时给我的感觉就是我这个问题问的很小白阿。但我坚持说向自己写写,后来其中的一个说简单的那位看了代码也说不会。这就说明一个问题,很多事情别人轻视了,用插件来解决了,如果你在有精力的情况下,最好还是自己动手写写。
我觉的比较好的方法就是,结合我这篇博客来说,我觉得大家可以先点开百度首页自己理清思路,能理到哪儿算哪儿。然后再看看我这篇博客,加深对实现的了解。之后最好就是自己先写写了,其实按照这个的难度,一般人都可以写到焦点的切换。最后,如果实在没想法的就看看我附件只中的源码吧。加上注释,我相信一定可以使你较为轻松的明白问题。
最后,希望大家共同进步~~