最终效果图:

所需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;
}
}
本文介绍如何使用 jQuery UI 的自动完成插件实现一个简单的用户搜索功能,包括前端界面设计、后端数据处理及 JSON 数据交互过程。
253

被折叠的 条评论
为什么被折叠?



