找一个开源 JS 库像 jQuery, 是一个提高工作效率的好办法,它能帮你以简洁的代码快速操作或遍历 DOM 集,而且不必担心跨浏览器兼容问题;但 jQuery 不是万能钥匙,如果你要完成某些公共的功能(如组件,公共 JS 模块),需要自己写插件,你也可以偷懒,从网上找到开源的 jQuery 插件,获得对源码的修改许可后还可以根据实际使用场景进行修改或改进。但使用或修改别人的插件,这中间的 bug 风险和维护成本首先得考虑进去。那自己写一个吧。如果你写的是 jQuery 插件,那么你未来的插件必须与大小至少是 150k 的 jQuery 库(如:v1.4.2 未压缩开发版)产生依赖;大概评估一下,如果你的插件里有约 30% 的代码能通过 jQuery 帮你精简下来,并且写 JS 兼容性代码让你感到有压力,那么即使有依赖也是值得的,否则我宁愿自己写一个独立的 JavaScript 组件。
虽然暂时会辛苦一点,但从它以后给你带来的回报来看,你的辛苦是很值得的:在以后的项目中你无需再为依赖版本,代码冲突发愁,因为你的 JavaScript 组件更加迷你灵活。
从下拉列表中选单位,允许输入,同时根据输入的内容过滤下拉列表中的内容。我们也可以称之为自动完成功能 (Auto Complete)。如图 1 输入“单位”时,下拉列表中出现与单位相关的条目供输入时选择。
图 1. 下拉列表示例
为输入框(包括 Input,TextArea)提供下拉框选择,辅助输入,提高输入准确性和效率
- 对下拉列表中匹配关键字的部分亮显 (如图中“单位”)
- 鼠标滑入下拉项时改变背景色为焦点色,滑出时恢复显示先前背景色
- 单击某一项时,将该项的值设为输入框的值
- 点击下拉框和输入框之外的位置时,收起下拉框
- 点击输入框时,如果已收起,则显示;如果已显示,则收起
- 键盘方向键控制下拉项当前位置,默认没有选中项,向上时定位到最后一项,向下时定位到第一项;当按下向右,向下键时当定位到下一项;当按下向左,向上键时定位到上一项;到在第一项上按上移键时,定位到最后一项;当在最后一项上按下移键时,定位到第一项
- 按下回车时键时,将选中项的值设为输入框的值
- 允许输入在下拉框中不存在的值
- 允许同一页面出现多个下拉框实例
- 允许连续多次选择(如 email 收件人地址,输入每个收件人时都有自动提示)
-
组件组成部分
输入框(Input,TextArea),下拉选择框(Div),数据加载时的动画图标(GIF)。
输入框在构建组件时,通过构造参数传入,下拉选择框 DIV 动态生成,由事件控制隐藏和显示,我们能想到的事件如输入控件 click 事件,输入框中输入 BackSpace 导致内容为空或达到最低触发长度等。
-
输入框
提供用户输入的地方,输入包括:字符输入,方向控制和选择控制(如回车和退出),输入框中的值在用户切换并选中条目时发生变化。
-
下拉选择框
下拉框初次构建好后,以后在事件中控制隐藏和显示 下拉框出现的位置,高和宽:位置为绝对位置并与输入框对齐;宽等于输入框的宽,高根据参数确定 下拉框中的内容由事件触发装载,装载数据源类型分远程和本地,事件类型由参数指定,如:click(普通下拉), keyup(autocomplete)等;为每类事件定制特定的参数,如 keyup,为避免加载频繁,可以设定 keyup 事件触发后延迟 300ms 执行数据装载;click,为避免每次 click 事件触发数据装载,设定只装载一次(初始化时)的参数。
-
数据加载动画
当加载远程数据时,在输入框右侧显示 Ajax 加载动画(GIF 图片)。
-
-
事件设计
-
内部事件(private)
输入框:按键:Up(Left),Down(Right):控制条目聚焦,Enter 键选中,Escape 键退出,并清空选择下拉条目:在每次装载完下拉数据时设置下拉框中每个条目的事件。如:条目的焦点移动,鼠标移入,移出,点击。
-
对外事件(public)
用户可以实现的对外事件有:beforeDropdown:(可选)下拉前开始装载数据时触发,afterDropdown:(可选)下拉完数据装载完毕时触发,onSelect:(必须)选中时触发,默认实现会提示用户需要实现此事件。onEmpty:(可选)输入为空时触发。onNewInput:(可选)输入的值在下拉框条目中不存在时触发。getFilter:加载下拉内容时的过滤条件的值;默认实现:返回输入框的值,此设计满足了特殊输入的要求,如连续多输入的情况(多个 Email 收件人),这种情况下,用户需要重写 getFilter 返回最后的分隔字符串作为下拉过滤条件值,同时用户在 onSelect 的实现里需要将选中的值作为当前分隔段的值。
对外事件,以 配置项的方式提供。
-
-
配置项设计
-
主要配置项
配置项名称 配置项含义 用法 作用 className 风格类名 样式名将作用到下拉框最外层对象,样式由用户在使用组件前定义好 让你的下拉框根据样式名融入不同的网页风格 dataFields 描述数据源的元数据: 列名,数据类型,如字符串,数值等 用 JSON 对象数组格式描述: [{'name':'xx', 'type':'tpXX'},{'name':'xx', 'type':'tpXX'}] 决定用户能操作哪些数据,如作为值,标题或其它; dataFields为 数据提供者提供返回数据的规范 dataKeyField 决定 dataFields中哪一列作为值列 用 JSON 对象格式描述:{'name' : 'id', 'type' : 'int'} 由用户灵活指定值列 dropdownTrigger 指定哪些事件能触发下拉下拉列表展开并从数据源装载内容 用 JSON 对象数组格式描述:[{eventName:'keyup', cacheable:true, triggerWhenEmpty: false, triggerMinLength:1}, {eventName:'click', loadAllForOnce:true}] 能适用不同的使用场景: 既可以作为纯下拉列表也可以用作自动填充 (AutoComplete) beforeDropdown 供用户实现的事件: 下拉并装载数据前触发 用户可选择实现 如下拉前对输入内容进行检查,修改或替换 afterDropdown 供用户实现的事件: 下拉并装载数据后触发 用户可选择实现 如对选中的值进行检查,修改或替换 onSelect 供用户实现的事件: 选中(点击下拉项目,或在选中的项上回车确认) 用户必须实现 为输入框赋值 onNewInput 供用户实现的事件: 当输入的值在下拉中不存在时触发 用户可选择实现 用户不需要知道输入的值是否已经存在时不需要实现此事件 onEmpty 供用户实现的事件: 当输入的值为空时触发 用户可选择实现 如:输入为空时需要给用户提醒 loadingImgPath 远程数据加载时,提供动画的名称(含路径) 指定一个 GIF 图片名称字符串 灵活指定加载动画 height 下拉框最高值(pix) 指定高度值 规定下拉框最大高度 footHtml 指定下拉框页脚 html 标记字符串 下拉框能适用更多的场合 footHeight 页脚高度 html 标记字符串 限定页脚高度 colorOfhighlightWords 与输入框内容匹配的高亮字符串颜色 颜色值字符串,如黑色 : 'black' 动态指定高亮字串的颜色,适用不同的要求 bgColorOfItem 指定下拉列表中交错显示不同的背景颜色 JSON 数组:['#ffffff','#F3F2FF', '#6666FF'],第一位:奇数行,第二位:偶数行,第三位:焦点行 能为不同的应用场合指定配色, 此项不受 className 对应的子样式影响
-
- 主代码
- 公共调用部分
这部分代码能从主代码中抽出来作为独立的文件,如 utils.js。 dropDownList.js 中用到这里面的 ajaxGet, positionOf 等公共方法。 此处代码比较简单,只提供方法接口,不一一提供实现,仅说明主代码中要用到的常用方法。
清单 1. utils.js
/* 处理 Ajax GET: url: 包含服务和参数的 URL callBack: 异步时,需要用户实现的回调,回调参数为 data,格式根据 resultType 指定 resultType: 返回类型,如 JSON, XML, text asynch: true 异步, false 同步 progressCallback 调用过程的回调,可选,如果用户需要知道进度可以实现 */ function ajaxGet(url, callBack, resultType, asynch, progressCallback) {} /* 获取对象位置 */ function positionOf(obj){ var curleft = curtop = 0; do { curleft += obj.offsetLeft; curtop += obj.offsetTop; } while (obj = obj.offsetParent); return {'left':curleft,'top':curtop}; } /* 空格裁剪 */ function trim(src) {} String.prototype.trim = function(){ return trim(this); } /* 进度条对象 */ function LoadingBar(posConfig) {} /*javascript 对象 extend 实现 */ Object.extend = function(destination, source) { if(source) { for (var property in source) { destination[property] = source[property]; } } return destination; } /* 事件绑定的浏览器兼容实现 */ function attachEventX(target,eventName,handlerName){ if ( target.addEventListener ) target.addEventListener(eventName, handlerName, false); else if ( target.attachEvent ) target.attachEvent("on" + eventName, handlerName); else target["on" + eventName] = handlerName; } /* 编码字符串:让 javascript 赋值时不会认为是特殊字符而报错 */ String.prototype.escape = function(){} /* 恢复字符串,按原样显示 */ String.prototype.unescape = function(){}
- 主体部分
为了便于理解控件整体实现思路,主体代码只保留了代码总体结构,省去了具体实现细节,用户可以在文章结尾部分下载完整代码查看具体实现。
清单 2. 代码概览
function DropDownList(targetObj, dataProvider, config) { this.conf = Object.extend( { /* 属性和事件的默认配置 */ }, config/* 用户自定义配置 */ ); this.target = targetObj;// 组件的 DOM 组成部分 this.target.setAttribute("autocomplete", "off");// 关闭浏览器的自动提示 this.dataService = dataProvider;// 数据来源: 1, 远程 URL 链接 2, 本地 JSON 对象数组 this.targetPos = positionOf(this.target);// 初始化组件的位置 this.loadingBar = new CLoadingBar();// 初始化 AJAX-Loading 的进度子控件 this.ajaxCall = ajaxGet;//ajax 请求方法 this.currObj;// 记录当前选中条目 this.data = [];// 缓存的下拉列表数据 this.evtCache = {}; var self = this; this.hide = function(){}// 隐藏下拉列表 this.show = function(){}// 显示下拉列表 this.toggleDisplay = function(){} this.goDown = function(){}// 往下移动当前条目 this.goUp = function(){}// 往上移动当前条目 this.getItemIndex = function(item){}// 获取当前条目的次序 this.doSel = function(item){}// 选中事件 this.isOptItem = function(obj){}// 判断是否是下拉框中的条目 this.isDirectionKeyEntered = function(evt){}// 判断是否是方向键,Enter 键或是 Escape 键 this.doMsOver = function(item){}// 鼠标移到下拉条目上时 this.doMsOut = function(item){}// 鼠标移出下拉条目时 /* 根据事件类型返回对应事件的配置信息 */ this.getEventConfig = function(eventType){} /* 为输入控件绑定事件 */ for(var e = 0; e < this.conf.dropdownTrigger.length; e++) { attachEventX(this.target, this.conf.dropdownTrigger[e].eventName, function(event){}); } /* 动态创建下拉列表 DIV*/ this.createPopup = function(){} this.createPopup(); /* 为 body 绑定 click 事件:当点击 body 时,能处理下拉框的隐藏 */ attachEventX(document.body, 'click', function(event){}); /* 下拉框下拉并完成内容装载 */ this.dropdownCustomerList = function(evt, e){} /* 下拉框渲染:处理下拉数据并根据处理后的数据生成下拉条目 */ this.processResult = function(data, content) {} }
同前面我们详细分析控件需求时的结果一样, 主代码由配置项,子组件(输入框,下拉 DIV,加载动画)和事件组成。
清单 3. 配置项相关代码
function DropDownList(targetObj, dataProvider, config) { this.conf = Object.extend( { 'className' : '',// 下拉框外框的样式名 /* 数据源须提供的数据字段格式,包括名称和类型 */ 'dataFields':[{'name' : 'name', 'type' : 'string'}], /* 数据源须提供的作为值列的字段,如 ID*/ 'dataKeyField':{'name' : 'name', 'type' : 'string'}, /* 数据源须提供的作为显示列的字段,如 Name*/ 'dataDescField':{'name' : 'name', 'type' : 'string'}, /* 触发下拉的事件 */ 'dropdownTrigger': [ { eventName:'keyup', cacheable:true, triggerWhenEmpty: false, triggerMinLength:1 } , {eventName:'click', loadAllForOnce:true} ], /* 可外部扩展事件: 下拉触发前 */ 'beforeDropdown': function(){}, /* 可外部扩展事件: 下拉触发后 */ 'afterDropdown': function(){}, /* 可外部扩展事件: 选中时 */ 'onSelect': function(data){ alert('onSelect event is not implemented!');}, /* 可外部扩展事件: 输入值是新值,即不在下拉项中 */ 'onNewInput': function(){}, /* 可外部扩展事件: 输入空时 */ 'onEmpty': function(){}, /* 下拉前将过滤后的输入框内容作为下拉过滤条件 */ 'getFiltered': function(){}, /* 远程加载数据时的等待图片 */ 'loadingImgPath':'', /* 默认下拉框显示高度 */ 'height':200, /* 默认下拉框显示宽度,0: 与输入框宽度相同 */ 'width':0, /* 默认下拉页脚内容:HTML 格式 */ 'footHtml':'', /* 默认页脚高度 */ 'footHeight':0, /* 下拉项中与输入框内容匹配的部分的颜色 */ 'colorOfhighlightWords':'blue', /*0: 间隔偶数项背景色 1:间隔奇数想背景色 2:选中或焦点项背景色 */ 'bgColorOfItem':['#ffffff','#F3F2FF', '#6666FF'] }, config );
上面的代码详细展示了 配置项数据结构。用户通过在构造参数中指定 config来覆盖组件的默认属性和行为。
清单 4. 将用户配置的事件绑定到输入控件
for(var e = 0; e < this.conf.dropdownTrigger.length; e++) { attachEventX(this.target, this.conf.dropdownTrigger[e].eventName, function(event){ if(window.event) { event.cancelBubble=true; } else { event.stopPropagation(); } var param = self.getEventConfig(event.type); if(!self.isDirectionKeyEntered(event) && param && typeof(param.delay) != 'undefined' && param.delay > 0) { var to = eval('window.timeout_'+ self.target.getAttribute("id")); if(to) { clearTimeout(to); } to = setTimeout( function(){ eval('window.timeout_'+ self.target.getAttribute("id") + ' = null;'); self.dropdownCustomerList( event, param ); }, param.delay ); eval('window.timeout_'+ self.target.getAttribute("id") + ' = to;'); }else self.dropdownCustomerList( event, param ); }); }
上面的代码让控件能适用不同的应用场景, 如单击鼠标时下拉和击键时下拉 (AutoComplete),或是它们的组合。取决于用户在配置项 dropdownTrigger中的设置。
清单 5. 下拉框渲染
this.dropdownCustomerList = function(evt, e){ this.conf.beforeDropdown(); if(this.target.value == '' || this.target.value.length == 0){ this.hide(); this.conf.onEmpty(); } if(evt) kc = window.event?window.event.keyCode : evt.which; /* 按键时的处理 */ if(kc == 38 || kc == 37){ this.goUp(); //Down OR Right Arrow key }else if(kc == 40 || kc == 39){ this.goDown(); }else if(kc == 27){//Escape Key this.data.length = 0; this.target.value = ''; this.hide(); this.conf.onEmpty(); }else if(kc == 13){//Enter key if(!this.currObj)//Did not download completely return false; this.doSel(this.currObj); }else { /* 处理下拉框渲染部分的代码从略,感兴趣的朋友可以在本文结尾处下载代码 */ } } }
上面的方法由用户配置的 dropdownTrigger中的事件驱动,如 keyup 事件触发时会调用本方法。通过触发此方法,控制下拉框的隐藏或显示,内容渲染。
- 公共调用部分
到此,我们已经分析了 JavaScript 控件需求,组成和实现思路,并列举了关键代码说明了实现要点。以下我们以本地数据源为例列举两类最常用的下拉选择的应用场景。
- 下拉框选择 ( 本地数据源 )
- 代码
清单 6. 本地数据源的下拉例子
<html> <body> <input type="text" value="" id="my_input" > </body> <script src="dropDownList.js"></script> <script> var dropDownList = new DropDownList( document.getElementById("my_input"), [{name:'Beijing'},{name:'Beihai'}, {name:'Beida'},{name:'Shanghai'},{name:'wuhan'}], { dropdownTrigger: [{eventName:'click'}], onSelect:function(data){ document.getElementById(" my_input").value = data['name']; } } ); </script> </html>
- 效果图
图 2. 下拉列表示例
- 代码
- 自动填充 ( 本地数据源 )
- 代码
清单 7. 本地数据源的自动填充例子
<html> <body> <input type="text" value="" id="my_input" > </body> <script src="dropDownList.js"></script> <script> var dropDownList = new DropDownList( document.getElementById("my_input"), [{name:'Beijing'},{name:'Beihai'},{name:'Beida'}, {name:'Shanghai'},{name:'wuhan'}], { dropdownTrigger: [{eventName:'keyup'}], onSelect:function(data){ document.getElementById("my_input").value = data['name']; } } ); </script> </html>
- 效果图
图 3. 自动填充示例
- 代码
- 其他
远程数据源的两种实际应用场景,如果大家感兴趣,不妨试试,有问题可以给我 Email。另外你还可以覆盖默认 配置项中 getFiltered 方法,以让你的控件适用多输入的情况,如 Email 多个收件人的场景 , 以及 TextArea 中语法提示的场景。 关于语法提示,我能想起这样一个具体应用: web 页面上对查询结果集的筛选。如高级查询,我们可以提供一个带语法提示的 TextArea,让用户在一个编辑框中一次输入所有的过滤条件,而不用逐项挨个选择或输入。
本文以日常工作中常见的下拉列表为例,分析了自己编写 JavaScript 控件给日常开发工作带来的好处,带领读者从整理实际的应用需求着手,区分不同的应用场景,分析组件工作的细节,并用代码逐一说明了实现这一个控件的主要步骤,最后给大家列举了这一控件在常见的几类场景中的应用。
本文也通过实例说明了编写自己的 JavaScript 控件也是提高工作效率的一种方法,一次编写,多处收益,而且维护起来很方便。项目中大家经常能听到埋怨编写代码的时间太短的抱怨声,但回头想想,我们手上有没有足够好的"武器"可用呢?高质量和通用灵活的控件就是你的武器,那你平常积攒了多少呢?当你积攒了够好,够多的武器时,面对开发时间紧缩的情况,你的眉宇可能更舒展一些吧。在此我也希望读者是一个工作中的有心人,当反复碰到某一类应用场景时,留意一下现在的代码能否封装为公共代码,做一些积累。
说到效率,有人又说,“没那么多时间专门写控件啊,我用复制粘贴一样高效快速”。分时候,复制粘贴对于长单词,变量,函数名等短的内容,可以减少因键盘输入带来的低级错误,当你的 IDE 的代码检查和代码提示不能覆盖上面的情况时,复制粘贴是可取的。但对于大段代码复制粘贴的情况,我个人比较反对(这里的情况跟 License 有区分,违反 License 的情况属道德和法律约束的范围),这很可能是一种危险的信号,预示你的代码将会很臃肿,或很凌乱,很难维护。这时你可能需要重新整理思路,分析这种反复出现的场景的共性是什么,想想如何重构你的代码以适应不断变化的应用场景,使你的 JavaScript 更加灵活通用,更加简洁。