最终效果图:
所需jar包列表:commons-beanutils-1.8.3.jar,commons-collections-3.2.1.jar,commons-lang-2.5.jar,commons-logging-1.1.1.jar,ezmorph-1.0.6.jar,json-lib-2.1.jar
?
jquery.ui.autocomplete.js 关键的地方已经写了注释
?
/* * jQuery UI Autocomplete 1.8.13 * * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI/Autocomplete * * Depends: * jquery.ui.core.js * jquery.ui.widget.js * jquery.ui.position.js */ (function( $, undefined ) { // used to prevent race conditions with remote data sources var requestIndex = 0; //minLength:输入多少个字符后开始自动完成 //delay:延迟 $.widget( "ui.autocomplete", { options: { appendTo: "body", autoFocus: false, delay: 300, minLength: 1, position: { my: "left top", at: "left bottom", collision: "none" }, source: null }, pending: 0, _create: function() { var self = this, doc = this.element[ 0 ].ownerDocument, suppressKeyPress; this.element .addClass( "ui-autocomplete-input" ) .attr( "autocomplete", "off" ) // TODO verify these actually work as intended .attr({ role: "textbox", "aria-autocomplete": "list", "aria-haspopup": "true" }) .bind( "keydown.autocomplete", function( event ) { if ( self.options.disabled || self.element.attr( "readonly" ) ) { return; } suppressKeyPress = false; var keyCode = $.ui.keyCode; switch( event.keyCode ) { case keyCode.PAGE_UP: self._move( "previousPage", event ); break; case keyCode.PAGE_DOWN: self._move( "nextPage", event ); break; case keyCode.UP: self._move( "previous", event ); // prevent moving cursor to beginning of text field in some browsers event.preventDefault(); break; case keyCode.DOWN: self._move( "next", event ); // prevent moving cursor to end of text field in some browsers event.preventDefault(); break; case keyCode.ENTER: case keyCode.NUMPAD_ENTER: // when menu is open and has focus if ( self.menu.active ) { // #6055 - Opera still allows the keypress to occur // which causes forms to submit suppressKeyPress = true; event.preventDefault(); } //passthrough - ENTER and TAB both select the current element case keyCode.TAB: if ( !self.menu.active ) { return; } self.menu.select( event ); break; case keyCode.ESCAPE: self.element.val( self.term ); self.close( event ); break; default: // keypress is triggered before the input value is changed clearTimeout( self.searching ); self.searching = setTimeout(function() { // only search if the value has changed if ( self.term != self.element.val() ) { self.selectedItem = null; self.search( null, event ); } }, self.options.delay ); break; } }) .bind( "keypress.autocomplete", function( event ) { if ( suppressKeyPress ) { suppressKeyPress = false; event.preventDefault(); } }) .bind( "focus.autocomplete", function() { if ( self.options.disabled ) { return; } self.selectedItem = null; self.previous = self.element.val(); }) .bind( "blur.autocomplete", function( event ) { if ( self.options.disabled ) { return; } clearTimeout( self.searching ); // clicks on the menu (or a button to trigger a search) will cause a blur event self.closing = setTimeout(function() { self.close( event ); self._change( event ); }, 150 ); }); this._initSource(); this.response = function() { return self._response.apply( self, arguments ); }; this.menu = $( "<ul></ul>" ) .addClass( "ui-autocomplete" ) .appendTo( $( this.options.appendTo || "body", doc )[0] ) // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown) .mousedown(function( event ) { // clicking on the scrollbar causes focus to shift to the body // but we can't detect a mouseup or a click immediately afterward // so we have to track the next mousedown and close the menu if // the user clicks somewhere outside of the autocomplete var menuElement = self.menu.element[ 0 ]; if ( !$( event.target ).closest( ".ui-menu-item" ).length ) { setTimeout(function() { $( document ).one( 'mousedown', function( event ) { if ( event.target !== self.element[ 0 ] && event.target !== menuElement && !$.ui.contains( menuElement, event.target ) ) { self.close(); } }); }, 1 ); } // use another timeout to make sure the blur-event-handler on the input was already triggered setTimeout(function() { clearTimeout( self.closing ); }, 13); }) .menu({ focus: function( event, ui ) { var item = ui.item.data( "item.autocomplete" ); if ( false !== self._trigger( "focus", event, { item: item } ) ) { // use value to match what will end up in the input, if it was a key event if ( /^key/.test(event.originalEvent.type) ) { self.element.val( item.value ); } } }, selected: function( event, ui ) { var item = ui.item.data( "item.autocomplete" ), previous = self.previous; // only trigger when focus was lost (click on menu) if ( self.element[0] !== doc.activeElement ) { self.element.focus(); self.previous = previous; // #6109 - IE triggers two focus events and the second // is asynchronous, so we need to reset the previous // term synchronously and asynchronously :-( setTimeout(function() { self.previous = previous; self.selectedItem = item; }, 1); } if ( false !== self._trigger( "select", event, { item: item } ) ) { //console.debug("userId:" + item.id); //下拉框中的内容被选中后 //设置hidden的值,item为json数组中的每一项 $("#uid").attr("value", item.id); //设置text的值为选中项的value值 self.element.val( item.value ); } // reset the term after the select event // this allows custom select handling to work properly self.term = self.element.val(); self.close( event ); self.selectedItem = item; }, blur: function( event, ui ) { // don't set the value of the text field if it's already correct // this prevents moving the cursor unnecessarily if ( self.menu.element.is(":visible") && ( self.element.val() !== self.term ) ) { self.element.val( self.term ); } } }) .zIndex( this.element.zIndex() + 1 ) // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781 .css({ top: 0, left: 0 }) .hide() .data( "menu" ); if ( $.fn.bgiframe ) { this.menu.element.bgiframe(); } }, destroy: function() { this.element .removeClass( "ui-autocomplete-input" ) .removeAttr( "autocomplete" ) .removeAttr( "role" ) .removeAttr( "aria-autocomplete" ) .removeAttr( "aria-haspopup" ); this.menu.element.remove(); $.Widget.prototype.destroy.call( this ); }, _setOption: function( key, value ) { $.Widget.prototype._setOption.apply( this, arguments ); if ( key === "source" ) { this._initSource(); } if ( key === "appendTo" ) { this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] ) } if ( key === "disabled" && value && this.xhr ) { this.xhr.abort(); } }, _initSource: function() { var self = this, array, url; if ( $.isArray(this.options.source) ) { array = this.options.source; this.source = function( request, response ) { response( $.ui.autocomplete.filter(array, request.term) ); }; } else if ( typeof this.options.source === "string" ) { url = this.options.source; this.source = function( request, response ) { if ( self.xhr ) { self.xhr.abort(); } self.xhr = $.ajax({ url: url, data: request, dataType: "json", autocompleteRequest: ++requestIndex, success: function( data, status ) { if ( this.autocompleteRequest === requestIndex ) { response( data ); } }, error: function() { if ( this.autocompleteRequest === requestIndex ) { response( [] ); } } }); }; } else { this.source = this.options.source; } }, search: function( value, event ) { value = value != null ? value : this.element.val(); //中文处理,在这里进行encode,在后台decode,即可解决乱码问题 value = encodeURI(value); // always save the actual value, not the one passed as an argument this.term = this.element.val(); if ( value.length < this.options.minLength ) { return this.close( event ); } clearTimeout( this.closing ); if ( this._trigger( "search", event ) === false ) { return; } return this._search( value ); }, _search: function( value ) { this.pending++; this.element.addClass( "ui-autocomplete-loading" ); this.source( { term: value }, this.response ); }, _response: function( content ) { if ( !this.options.disabled && content && content.length ) { content = this._normalize( content ); this._suggest( content ); this._trigger( "open" ); } else { this.close(); } this.pending--; if ( !this.pending ) { this.element.removeClass( "ui-autocomplete-loading" ); } }, close: function( event ) { clearTimeout( this.closing ); if ( this.menu.element.is(":visible") ) { this.menu.element.hide(); this.menu.deactivate(); this._trigger( "close", event ); } }, _change: function( event ) { if ( this.previous !== this.element.val() ) { this._trigger( "change", event, { item: this.selectedItem } ); } }, _normalize: function( items ) { // assume all items have the right format when the first item is complete if ( items.length && items[0].label && items[0].value ) { return items; } return $.map( items, function(item) { if ( typeof item === "string" ) { return { label: item, value: item }; } return $.extend({ label: item.label || item.value, value: item.value || item.label }, item ); }); }, _suggest: function( items ) { var ul = this.menu.element .empty() .zIndex( this.element.zIndex() + 1 ); this._renderMenu( ul, items ); // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate this.menu.deactivate(); this.menu.refresh(); // size and position menu ul.show(); this._resizeMenu(); ul.position( $.extend({ of: this.element }, this.options.position )); if ( this.options.autoFocus ) { this.menu.next( new $.Event("mouseover") ); } }, _resizeMenu: function() { var ul = this.menu.element; ul.outerWidth( Math.max( ul.width( "" ).outerWidth(), this.element.outerWidth() ) ); }, _renderMenu: function( ul, items ) { var self = this; $.each( items, function( index, item ) { self._renderItem( ul, item ); }); }, _renderItem: function( ul, item) { return $( "<li></li>" ) .data( "item.autocomplete", item ) //.html:解析html标签,.text:不解析html标签,根据项目需要选择 .append( $( "<a></a>" ).html( item.label ) ) .appendTo( ul ); }, _move: function( direction, event ) { if ( !this.menu.element.is(":visible") ) { this.search( null, event ); return; } if ( this.menu.first() && /^previous/.test(direction) || this.menu.last() && /^next/.test(direction) ) { this.element.val( this.term ); this.menu.deactivate(); return; } this.menu[ direction ]( event ); }, widget: function() { return this.menu.element; } }); $.extend( $.ui.autocomplete, { escapeRegex: function( value ) { return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); }, filter: function(array, term) { var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); return $.grep( array, function(value) { return matcher.test( value.label || value.value || value ); }); } }); }( jQuery )); /* * jQuery UI Menu (not officially released) * * This widget isn't yet finished and the API is subject to change. We plan to finish * it for the next release. You're welcome to give it a try anyway and give us feedback, * as long as you're okay with migrating your code later on. We can help with that, too. * * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI/Menu * * Depends: * jquery.ui.core.js * jquery.ui.widget.js */ (function($) { $.widget("ui.menu", { _create: function() { var self = this; this.element .addClass("ui-menu ui-widget ui-widget-content ui-corner-all") .attr({ role: "listbox", "aria-activedescendant": "ui-active-menuitem" }) .click(function( event ) { if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) { return; } // temporary event.preventDefault(); self.select( event ); }); this.refresh(); }, refresh: function() { var self = this; // don't refresh list items that are already adapted var items = this.element.children("li:not(.ui-menu-item):has(a)") .addClass("ui-menu-item") .attr("role", "menuitem"); items.children("a") .addClass("ui-corner-all") .attr("tabindex", -1) // mouseenter doesn't work with event delegation .mouseenter(function( event ) { self.activate( event, $(this).parent() ); }) .mouseleave(function() { self.deactivate(); }); }, activate: function( event, item ) { this.deactivate(); if (this.hasScroll()) { var offset = item.offset().top - this.element.offset().top, scroll = this.element.scrollTop(), elementHeight = this.element.height(); if (offset < 0) { this.element.scrollTop( scroll + offset); } else if (offset >= elementHeight) { this.element.scrollTop( scroll + offset - elementHeight + item.height()); } } this.active = item.eq(0) .children("a") .addClass("ui-state-hover") .attr("id", "ui-active-menuitem") .end(); this._trigger("focus", event, { item: item }); }, deactivate: function() { if (!this.active) { return; } this.active.children("a") .removeClass("ui-state-hover") .removeAttr("id"); this._trigger("blur"); this.active = null; }, next: function(event) { this.move("next", ".ui-menu-item:first", event); }, previous: function(event) { this.move("prev", ".ui-menu-item:last", event); }, first: function() { return this.active && !this.active.prevAll(".ui-menu-item").length; }, last: function() { return this.active && !this.active.nextAll(".ui-menu-item").length; }, move: function(direction, edge, event) { if (!this.active) { this.activate(event, this.element.children(edge)); return; } var next = this.active[direction + "All"](".ui-menu-item").eq(0); if (next.length) { this.activate(event, next); } else { this.activate(event, this.element.children(edge)); } }, // TODO merge with previousPage nextPage: function(event) { if (this.hasScroll()) { // TODO merge with no-scroll-else if (!this.active || this.last()) { this.activate(event, this.element.children(".ui-menu-item:first")); return; } var base = this.active.offset().top, height = this.element.height(), result = this.element.children(".ui-menu-item").filter(function() { var close = $(this).offset().top - base - height + $(this).height(); // TODO improve approximation return close < 10 && close > -10; }); // TODO try to catch this earlier when scrollTop indicates the last page anyway if (!result.length) { result = this.element.children(".ui-menu-item:last"); } this.activate(event, result); } else { this.activate(event, this.element.children(".ui-menu-item") .filter(!this.active || this.last() ? ":first" : ":last")); } }, // TODO merge with nextPage previousPage: function(event) { if (this.hasScroll()) { // TODO merge with no-scroll-else if (!this.active || this.first()) { this.activate(event, this.element.children(".ui-menu-item:last")); return; } var base = this.active.offset().top, height = this.element.height(); result = this.element.children(".ui-menu-item").filter(function() { var close = $(this).offset().top - base + height - $(this).height(); // TODO improve approximation return close < 10 && close > -10; }); // TODO try to catch this earlier when scrollTop indicates the last page anyway if (!result.length) { result = this.element.children(".ui-menu-item:first"); } this.activate(event, result); } else { this.activate(event, this.element.children(".ui-menu-item") .filter(!this.active || this.first() ? ":last" : ":first")); } }, hasScroll: function() { return this.element.height() < this.element[ $.fn.prop ? "prop" : "attr" ]("scrollHeight"); }, select: function( event ) { this._trigger("selected", event, { item: this.active }); } }); }(jQuery));
?
?JSP页面:index.jsp
?
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>test auto complete</title> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <link rel="stylesheet" type="text/css" href="js/jqueryui/jquery-ui-1.8.13.custom.css"> <script type="text/javascript" src="js/jquery1.4.4.min.js"></script> <script type="text/javascript" src="js/jqueryui/jquery.ui.core.min.js"></script> <script type="text/javascript" src="js/jqueryui/jquery.ui.widget.min.js"></script> <script type="text/javascript" src="js/jqueryui/jquery.ui.position.min.js"></script> <script type="text/javascript" src="js/jqueryui/jquery.ui.autocomplete.js"></script> <style type="text/css"> <%-- loading图片 --%> .ui-autocomplete-loading { background: white url('js/jqueryui/images/ui-anim_basic_16x16.gif') right center no-repeat; } .ui-widget .ui-corner-all{ height:30px; vertical-align: top;} .ui-autocomplete { max-height: 108px; overflow-y: auto; /* 防止出现水平滚动条 */ overflow-x: hidden; /* 内容与右侧滚动条的距离 */ padding-right: 0px; } /* IE 6 不支持 max-height,用height代替 */ * html .ui-autocomplete { height: 130px; } </style> </head> <body> <script type="text/javascript"> $(function() { var cache = {}, lastXhr; $( "#nickName" ).autocomplete({ minLength: 1, source: function( request, response ) { var term = request.term; if ( term in cache ) { response( cache[ term ] ); return; } var params = { "nickName" : term }; lastXhr = $.getJSON("SearchServlet", params, function( data, status, xhr ) { cache[ term ] = data; if ( xhr === lastXhr ) { response( data ); } }); } }); }); </script> <div class="ui-widget"> <%--为了演示方便,采用get方式,在地址栏可以看到参数 --%> <form id="form1" name="form1" method="get" action="sendMessage.action"> <input type="hidden" name="uid" id="uid"/> <label for="nickName">昵称: </label> <input id="nickName" type="text" name="nickName" style="width:250px;"/> <input type="submit" value="发送"/> </form> </div> </body> </html>
?后台代码
User.java
?
package com.gary.test.entity; import java.io.Serializable; /** * 用户信息 * @author gary * */ public class User implements Serializable{ private static final long serialVersionUID = 1L; private Integer id; private String nickName; private String password; private String gravatar; private Short gender; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getGravatar() { return gravatar; } public void setGravatar(String gravatar) { this.gravatar = gravatar; } public void setGender(Short gender) { this.gender = gender; } public Short getGender() { return gender; } public void setNickName(String nickName) { this.nickName = nickName; } public String getNickName() { return nickName; } public User(Integer id, String nickName, String password, String gravatar, Short gender) { super(); this.id = id; this.nickName = nickName; this.password = password; this.gravatar = gravatar; this.gender = gender; } }
?UserDTO.java
?
package com.gary.test.util; /** * 用户信息DTO * @author gary * */ public class UserDTO { //input hidden中的用户ID private Integer id; //下拉列表显示内容 private String label; //下拉列表选中后,input text中显示的内容 private String value; public UserDTO(Integer id, String label, String value) { this.id = id; this.label = label; this.value = value; } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public void setId(Integer id) { this.id = id; } public Integer getId() { return id; } }
?AutoCompleteUtil.java
?
package com.gary.test.util; import java.util.ArrayList; import java.util.List; import com.gary.test.entity.User; /** * 自动完成工具 * @author gary * */ public class AutoCompleteUtil { /** * 转化数据,entity to dto * @param userList * @return */ public static List<UserDTO> convert(List<User> userList){ List<UserDTO> resultList = new ArrayList<UserDTO>(); int imgWidth = 30; int imgHeight = 30; //图片标签前缀 final String imgTagPrefix = "<img src=\"images/"; //图片标签后缀 final String imgTagPostfix = "\" border=\"0\" width=\"" + imgWidth + "px;\" height=\"" + imgHeight + "px;\" />"; int namePaddingLeft = 15; //昵称 final String nameTagPrefix = "<span style=\"padding-left:" + namePaddingLeft + "px; vertical-align: top;\">"; final String nameTagPostfix = "</span>"; int genderPaddingLeft = 15; //性别 final String genderTagPrefix = "<span style=\"padding-left:" + genderPaddingLeft + "px; vertical-align: top;\">"; final String genderTagPostfix = "</span>"; for (User ui : userList) { StringBuffer label = new StringBuffer(); label.append(imgTagPrefix); label.append(ui.getGravatar()); label.append(imgTagPostfix); label.append(nameTagPrefix); label.append(ui.getNickName()); label.append(nameTagPostfix); label.append(genderTagPrefix); int gender = ui.getGender().shortValue(); if(gender == 1){ label.append("男"); }else{ label.append("女"); } label.append(genderTagPostfix); UserDTO userDTO = new UserDTO(ui.getId(), label.toString(), ui.getNickName()); resultList.add(userDTO); } return resultList; } }
?SearchServlet.java
?
package com.gary.test.servlet; import java.io.IOException; import java.io.PrintWriter; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import net.sf.json.JSONArray; import com.gary.test.entity.User; import com.gary.test.util.AutoCompleteUtil; import com.gary.test.util.UserDTO; /** * 搜索用户 * @author gary * */ public class SearchServlet extends HttpServlet { private static final long serialVersionUID = 1L; public SearchServlet() { super(); } public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); String nickName = request.getParameter("nickName"); if(!StringUtils.isBlank(nickName)){ nickName = URLDecoder.decode(nickName, "UTF-8"); } log("nickName:" + nickName); response.setContentType("text/html"); PrintWriter out = response.getWriter(); //TODO:测试数据仅供演示,需改为从DAO获取查询结果 List<User> userList = this.getTestData(); List<UserDTO> list = AutoCompleteUtil.convert(userList); if(list.size() == 0){ list.add(new UserDTO(0, "无此用户","无此用户")); } //生成JSON数组 JSONArray json = JSONArray.fromObject(list); String jsonStr = json.toString(); log(jsonStr); out.print(jsonStr); out.flush(); out.close(); } /** * 生成测试数据,模拟搜索结果 * @return */ private List<User> getTestData(){ List<User> list = new ArrayList<User>(); for (int i = 1; i < 20; i++) { User u = new User(i, "测试用户" + i,"123", "gravatar.jpg", (short) (i % 2)); list.add(u); } return list; } }