本文介紹基於jquery 的 自動提示單選組件QuickSelect ,筆者對該組件進行修改支持dwr的用法。
1 QuickSelect :
<script>
$('#ExampleOne_LocalData').quickselect({
maxItemsToShow:10,
data:["Aberdeen", "Ada", "Adamsville", ["Addyston", "Addyston", 'closed'], "Adelphi", "Adena", "Adrian", "Akron"]}) // truncated data...
</script>
<script type="text/javascript">
$(function(){
$('#zip').quickselect({
ajax:'http://www.test.com/aaa.action', /* call to url with json formatted array of data */
match:'substring',
autoSelectFirst:false,
mustMatch:true,
additionalFields:[$('#city'), $('#state')],
formatItem:function(data, index, total){return data[0]+", "+data[1]+" "+data[2]}
});
});
</script>
http://www.test.com/aaa.action?q=輸入框的值
1.2 返回數據結構
下拉提示框 的數據源格式 使用 json 格式
1.2.1 描述與提交的值一致的
[a,b,c,....]
1.2.1 描述與提交的值不一致的
[{value:'1',label:'中國'},{value:'2',label:'美國'}........]
1.3 對 quickselect的擴展
1.3.1增加對javascript的支持
<script type="text/javascript">
$(function(){
$("#input").quickselect({ maxItemsToShow:10,
finderFunctionStr:'jsfunction',
jsfunction:function(q,callback){
var ognl="daoHelper.queryForList(\"SELECT HOSTNAME label,HOSTNASID VALUE FROM t_cfg_host1_info where HOSTNAME like '%"+q+"%'\")";
//var ognl="service.query("+q+")";
ctx.eval(ognl,callback);
/*
service.query(q,calback);
*/
/*
$.post("demo_ajax_gethint.asp",{suggest:q},callback);
*/
}
});
});
</script>
finderFunctionStr:查詢數據源的的方法 值可以是 data(簡單數據源),ajax( ajax數據源),jsfunction(對外部javascript支持,此接口比較靈活,可以用於ajax,dwr的擴展)
jsfunction:組件onchange事件時會自動調用的方法
參數:
q :輸入框值
callback: jsfunction的回調函數
補充:
ctx.eval(ognl,callback);
service.query(q,calback);
dwr的調用
ctx,service 是dwr中定義的服務 。
eval是ctx的方法
2 源碼解讀
2.1 組件構成
$input_element :輸入框
$result_list :下拉選擇提示
$results_mask:遮罩層 ,當輸入變化進行查詢的時候,等待時出現的層
2.2 組件查詢過程解析
當用戶輸入,監聽輸入框的keyup事件 ,並與上次的輸入的字符進行對比,如果發生變法,則確認爲一個change事件
組件監聽change事件,通過 finder找出查詢數據源的方法,並調用該方法。
方法返回後,數據結構如1.2 所描述,然後更新$result_list 對象,彈出該對象
用戶可以輸入上下鍵選擇,按enter確認選擇,或者用鼠標點擊選擇。
2.2.1 finder
QuickSelect.finders = {
data : function( q,callback){
callback(this.options.data);
},
ajax : function( q,callback){
var url = this.options.ajax + "?q=" + encodeURI(q);
for(var i in this.options.ajaxParams){
if(this.options.ajaxParams.hasOwnProperty(i)){
url += "&" + i + "=" + encodeURI(this.options.ajaxParams[i]);
}
}
$.getJSON(url, callback);
},
dwrexpression:function( ognl,callback){
ctx.eval(ognl,callback);
//alert("daoHelper.queryForList(\"SELECT HOSTNAME label, HOSTNASID value FROM t_cfg_host1_info where HOSTNAME like '%"+ognl+"%'\")");
},
jsfunction:function(q,callback){
this.options.jsfunction(q,callback);
//ctx.eval(ognl,callback);
//alert("daoHelper.queryForList(\"SELECT HOSTNAME label, HOSTNASID value FROM t_cfg_host1_info where HOSTNAME like '%"+ognl+"%'\")");
}
};
if(options.finderFunctionStr==='dwrexpression'){
options.finderFunction = QuickSelect.finders[options.finderFunctionStr];
}else if(options.finderFunctionStr==='jsfunction'){
options.finderFunction = QuickSelect.finders[options.finderFunctionStr];
}else{
options.finderFunction = options.finderFunction || QuickSelect.finders[!options.data ? 'ajax': 'data'];
// console.log(options.finderFunction);
if(options.finderFunction==='data' || options.finderFunction==='ajax' ) options.finderFunction = QuickSelect.finders[options.finderFunction];
// console.log(options.finderFunction);
// matchMethod: (quicksilver | contains | startsWith | <custom>). Defaults to 'quicksilver' if quicksilver.js is loaded / 'contains' otherwise.
}
如果有ajax 或者data屬性,
options.finderFunction = QuickSelect.finders[options.finderFunction]
如果options.finderFunctionStr==='jsfunction' 則 使用 jsfunction
finders是定義數據源查找的js方法 。
2.2.2 matcher
排序,根據輸入的值的匹配程度去排序
3附件:源碼
// Credit to Anders Retteras for adding functionality preventing onblur event to fire on text input when clicking on scrollbars in MSIE / Opera.
// Minified version created at http://jsutility.pjoneil.net/ by running Obfuscation with all options unchecked, and then Compact.
// Packed version created at http://jsutility.pjoneil.net/ by running Compress on the Minified version.
//update by [email protected] ,add the support of the method for javascript and dwr
function object(obj){
var s = function(){};
s.prototype = obj;
return new s();
}
var QuickSelect;
(function($){
// The job of the QuickSelect object is to encapsulate all the state of a select control and manipulate the DOM and interface events.
QuickSelect = function( $input_element, options){
var self = this;
$input_element = $($input_element);
$input_element.attr('autocomplete', 'off');
self.options = options;
// Save the state of the control
// AllItems: hash of "index" -> [items], where index is the query that retrieves or filters the results.
// clickedLI: just a state variable for IE scrollbars.
self.AllItems = {};
var clickedLI = false,
activeSelection = -1,
hasFocus = false,
last_keyCode,
previous_value,
timeout,
ie_stupidity = false,
$results_list,
$results_mask;
if(/MSIE (\d+\.\d+);/.test(navigator.userAgent)){ //test for MSIE x.x;
if(Number(RegExp.$1) <= 7) ie_stupidity=true;
}
// Create the list DOM
$results_list = $('<div class="'+options.resultsClass+'" style="display:block;position:absolute;z-index:9999;"></div>').hide();
// Supposedly if we position an iframe behind the results list, before we position the results list, it will hide select elements in IE.
$results_mask = $('<iframe />');
$results_mask.css({border:'none',position:'absolute'});
if(options.width>0){
$results_list.css("width", options.width);
$results_mask.css("width", options.width);
}
$('body').append($results_list);
$results_list.hide(); // in case for some reason it didn't hide before appending it?
if(ie_stupidity) $('body').append($results_mask);
// Set up all of the methods
self.getLabel = function(item){
return item['label']||item['LABEL']||item.label || (typeof(item)==='string' ? item : item[0]) || ''; // hash:item.label; string:item; array:item[0]
};
var getValues = function(item){
return item['value']||item['VALUE']||item.values || (item.value ? [item.value] : (typeof(item)==='string' ? [item] : item)) || []; // hash:item.values || item.value; string:item; array:item[1..end]
};
var moveSelect = function(step_or_li){
var lis = $('li', $results_list);
if(!lis) return;
if(typeof(step_or_li)==="number") activeSelection = activeSelection + step_or_li;
else activeSelection = lis.index(step_or_li);
if(activeSelection < 0) activeSelection = 0;
else if(activeSelection >= lis.size()) activeSelection = lis.size() - 1;
lis.removeClass(options.selectedClass);
$(lis[activeSelection]).addClass(options.selectedClass);
if(options.autoFill && self.last_keyCode != 8){ // autoFill value, if option is set and the last user key pressed wasn't backspace
// 1. Fill in the value (keep the case the user has typed)
$input_element.val(previous_value + $(lis[activeSelection]).text().substring(previous_value.length));
// 2. SELECT the portion of the value not typed by the user (so the next character will erase if they continue typing)
var sel_start = previous_value.length,
sel_end = $input_element.val().length,
field = $input_element.get(0);
if(field.createTextRange){
var selRange = field.createTextRange();
selRange.collapse(true);
selRange.moveStart("character", sel_start);
selRange.moveEnd("character", sel_end);
selRange.select();
} else if(field.setSelectionRange){
field.setSelectionRange(sel_start, sel_end);
} else if(field.selectionStart){
field.selectionStart = sel_start;
field.selectionEnd = sel_end;
}
field.focus();
}
};
var hideResultsNow = function(){
if(timeout){clearTimeout(timeout);}
$input_element.removeClass(options.loadingClass);
if($results_list.is(":visible")) $results_list.hide();
if($results_mask.is(":visible")) $results_mask.hide();
activeSelection = -1;
};
self.selectItem = function(li, from_hide_now_function){
if(!li){
li = document.createElement("li");
li.item = '';
}
var label = self.getLabel(li.item),
values = getValues(li.item);
$input_element.lastSelected = label;
$input_element.val(label); // Set the visible value
previous_value = label;
$results_list.empty(); // clear the results list
$(options.additionalFields).each(function(i,input){$(input).val(values[i+1]);}); // set the additional fields' values
if(!from_hide_now_function)hideResultsNow(); // hide the results when something is selected
if(options.onItemSelect)setTimeout(function(){ options.onItemSelect(li); }, 1); // run the user callback, if set
return true;
};
var selectCurrent = function(){
var li = $("li."+options.selectedClass, $results_list).get(0);
if(li){
return self.selectItem(li);
} else {
// No current selection - blank the fields if options.exactMatch and current value isn't valid.
if(options.exactMatch){
$input_element.val('');
$(options.additionalFields).each(function(i,input){$(input).val('');});
}
return false;
}
};
var repopulate_items = function(items){
// Clear the results to begin:
$results_list.empty();
// If the field no longer has focus or if there are no matches, forget it.
//alert(hasFocus+"--"+items);
//!hasFocus ||
if( items === null || items.length === 0) return hideResultsNow();
// alert(hasFocus);
var ul = document.createElement("ul"),
total_count = items.length,
// hover functions
hf = function(){ moveSelect(this); },
bf = function(){},
cf = function(e){ e.preventDefault(); e.stopPropagation(); self.selectItem(this); };
$results_list.append(ul);
// limited results to a max number
if(options.maxVisibleItems > 0 && options.maxVisibleItems < total_count) total_count = options.maxVisibleItems;
// Add each item:
for(var i=0; i<total_count; i++){
var item = items[i],
li = document.createElement("li");
$results_list.append(li);
$(li).text(options.formatItem ? options.formatItem(item, i, total_count) : self.getLabel(item));
// Save the extra values (if any) to the li
li.item = item;
// Set the class name, if specified
if(item.className) li.className = item.className;
ul.appendChild(li);
$(li).hover(hf, bf).click(cf);
}
// Lastly, remove the loading class.
$input_element.removeClass(options.loadingClass);
return true;
};
var itemList;
var repopulate = function(q,callback){
var ognl;
if(options.finderFunctionStr==='dwrexpression'){
ognl=options.dwrognl.replace('{value}',q);
}else{
ognl=q;
}
queryLabel=q;
options.finderFunction.apply(self,[ognl,function(data){
itemList=data;
repopulate_items(options.matchMethod.apply(self,[q,data]));
callback();
}]);
};
var repopulate2 = function(q, callback){
repopulate_items(options.matchMethod.apply(self,[q,itemList]));
callback();
};
var show_results = function(){
// pos: get the position of the input field before showing the results_list (in case the DOM is shifted)
// iWidth: either use the specified width, or autocalculate based on form element
var pos = $input_element.offset(),
iWidth = (options.width > 0 ? options.width : $input_element.width()),
$lis = $('li', $results_list);
// reposition
$results_list.css({
width: parseInt(iWidth,10) + "px",
top: pos.top + $input_element.height() + 5 + "px",
left: pos.left + "px"
});
if(ie_stupidity){$results_mask.css({
width: parseInt(iWidth,10) - 2 + "px",
top: pos.top + $input_element.height() + 6 + "px",
left: pos.left + 1 + "px",
height: $results_list.height() - 2+'px'
}).show();}
$results_list.show();
// Option autoSelectFirst, and Option selectSingleMatch (activate the first item if only item)
if(options.autoSelectFirst || (options.selectSingleMatch && $lis.length == 1)) moveSelect($lis.get(0));
};
var onChange = function(){
// ignore if non-consequence key is pressed (such as shift, ctrl, alt, escape, caps, pg up/down, home, end, arrows)
if(itemList==null){
return ;
}
// alert("change");
if(last_keyCode >= 9 && last_keyCode <= 45){return;}
// compare with previous value / store new previous value
var q = $input_element.val();
if(q == previous_value) return;
previous_value = q;
// if enough characters have been typed, load/populate the list with whatever matches and show the results list.
if(q.length >= options.minChars){
$input_element.addClass(options.loadingClass);
// Populate the list, then show the list.
repopulate2( q,show_results);
} else { // if too short, hide the list.
if(q.length === 0 && (options.onBlank ? options.onBlank() : true)) // onBlank callback
$(options.additionalFields).each(function(i,input){input.value='';});
$input_element.removeClass(options.loadingClass);
$results_list.hide();
$results_mask.hide();
}
};
var queryLabel;
var onQuery = function(){
//查詢事件
var q = $input_element.val();
previous_value=q;
$input_element.addClass(options.loadingClass);
if(itemList==null){
repopulate(q,show_results);
//alert("repopulate");
}else{
if(q.indexOf(queryLabel)>-1){
repopulate2(q,show_results);
}else{
repopulate(q,show_results);
}
//alert("repopulate2");
}
};
// Set up the interface events
// Mark that actual item was clicked if clicked item was NOT a DIV, so the focus doesn't leave the items.
$results_list.mousedown(function(e){if(e.srcElement)clickedLI=e.srcElement.tagName!='DIV';});
$input_element.keyup(function(e){
last_keyCode = e.keyCode;
// if(e.keyCode==13){
//
// }
switch(e.keyCode){
case 38: // Up arrow - select prev item in the drop-down
e.preventDefault();
moveSelect(-1);
break;
case 40: // Down arrow - select next item in the drop-down
e.preventDefault();
if(!$results_list.is(":visible")){
show_results();
moveSelect(0);!$results_list.is(":visible")
}else{moveSelect(1);}
break;
case 13: // Enter/Return - select item and stay in field
// alert($results_list.css('display'));
/**var q = $input_element.val();
if(q != previous_value) {
onQuery();
}else if($results_list.is(":visible")){
e.preventDefault();
$input_element.select();
var li = $("li."+options.selectedClass, $results_list).get(0);
self.selectItem(li);
$results_list.hide();
}
*/
e.preventDefault();
$input_element.select();
var li = $("li."+options.selectedClass, $results_list).get(0);
self.selectItem(li);
$results_list.hide();
break;
case 9: // Tab - select the currently selected, let the onblur happen
// selectCurrent();
break;
case 27: // Esc - deselect any active selection, hide the drop-down but stay in the field
// Reset the active selection IF must be exactMatch and is not an exact match.
if(activeSelection > -1 && options.exactMatch && $input_element.val()!=$($('li', $results_list).get(activeSelection)).text()){activeSelection = -1;}
$('li', $results_list).removeClass(options.selectedClass);
hideResultsNow();
e.preventDefault();
break;
default:
var q = $input_element.val();
if(q != previous_value) {
onQuery();
}else if($results_list.is(":visible")){
e.preventDefault();
$input_element.select();
var li = $("li."+options.selectedClass, $results_list).get(0);
self.selectItem(li);
$results_list.hide();
}
//alert(q);
/**if(timeout){clearTimeout(timeout);}
timeout = setTimeout(onChange, options.delay);*/
/*if(itemList!=null&& $results_list.is(":visible")){
repopulate2(q,show_results);
}*/
break;
}
}).focus(function(){
// track whether the field has focus, we shouldn't process any results if the field no longer has focus
hasFocus = true;
}).blur(function(e){
if(activeSelection>-1){selectCurrent();}
hasFocus = false;
if(timeout){clearTimeout(timeout);}
timeout = setTimeout(function(){
hideResultsNow();
// Select null element, IF options.exactMatch and there is no selection.
// !! CLEARS THE FIELD IF YOU BLUR AFTER CHOOSING THE ITEM AND RESULTS ARE ALREADY CLOSED!
if(options.exactMatch && $input_element.val() != $input_element.lastSelected){self.selectItem(null,true);}
}, 200);
});
};
QuickSelect.matchers = {
quicksilver : function(q,data){
var match_query, match_label, self=this;
match_query = (self.options.matchCase ? q : q.toLowerCase());
self.AllItems[match_query] = [];
for(var i=0;i<data.length;i++){
match_label = (self.options.matchCase ? self.getLabel(data[i]) : self.getLabel(data[i]).toLowerCase());
// Filter by match/no-match
if(match_label.score(match_query)>0){self.AllItems[match_query].push(data[i]);}
}
// Sort by match relevance
return self.AllItems[match_query].sort(function(a,b){
// Normalize a & b
a = (self.options.matchCase ? self.getLabel(a) : self.getLabel(a).toLowerCase());
b = (self.options.matchCase ? self.getLabel(b) : self.getLabel(b).toLowerCase());
// Score a & b
a = a.score(match_query);
b = b.score(match_query);
// Compare a & b by score
return(a > b ? -1 : (b > a ? 1 : 0));
});
},
contains : function(q,data){
var match_query, match_label, self=this;
match_query = (self.options.matchCase ? q : q.toLowerCase());
self.AllItems[match_query] = [];
for(var i=0;i<data.length;i++){
match_label = (self.options.matchCase ? self.getLabel(data[i]) : self.getLabel(data[i]).toLowerCase());
if(match_label.indexOf(match_query)>-1){self.AllItems[match_query].push(data[i]);}
}
return self.AllItems[match_query].sort(function(a,b){
// Normalize a & b
a = (self.options.matchCase ? self.getLabel(a) : self.getLabel(a).toLowerCase());
b = (self.options.matchCase ? self.getLabel(b) : self.getLabel(b).toLowerCase());
// Get proximities
var a_proximity = a.indexOf(match_query);
var b_proximity = b.indexOf(match_query);
// Compare a & b by match proximity to beginning of label, secondly alphabetically
return(a_proximity > b_proximity ? -1 : (a_proximity < b_proximity ? 1 : (a > b ? -1 : (b > a ? 1 : 0))));
});
},
startsWith : function(q,data){
var match_query, match_label, self=this;
match_query = (self.options.matchCase ? q : q.toLowerCase());
self.AllItems[match_query] = [];
for(var i=0;i<data.length;i++){
match_label = (self.options.matchCase ? self.getLabel(data[i]) : self.getLabel(data[i]).toLowerCase());
if(match_label.indexOf(match_query)===0){self.AllItems[match_query].push(data[i]);}
}
return self.AllItems[match_query].sort(function(a,b){
// Normalize a & b
a = (self.options.matchCase ? self.getLabel(a) : self.getLabel(a).toLowerCase());
b = (self.options.matchCase ? self.getLabel(b) : self.getLabel(b).toLowerCase());
// Compare a & b alphabetically
return(a > b ? -1 : (b > a ? 1 : 0));
});
}
};
QuickSelect.finders = {
data : function( q,callback){
callback(this.options.data);
},
ajax : function( q,callback){
var url = this.options.ajax + "?q=" + encodeURI(q);
for(var i in this.options.ajaxParams){
if(this.options.ajaxParams.hasOwnProperty(i)){
url += "&" + i + "=" + encodeURI(this.options.ajaxParams[i]);
}
}
$.getJSON(url, callback);
},
dwrexpression:function( ognl,callback){
ctx.eval(ognl,callback);
//alert("daoHelper.queryForList(\"SELECT HOSTNAME label, HOSTNASID value FROM t_cfg_host1_info where HOSTNAME like '%"+ognl+"%'\")");
},
jsfunction:function(q,callback){
this.options.jsfunction(q,callback);
//ctx.eval(ognl,callback);
//alert("daoHelper.queryForList(\"SELECT HOSTNAME label, HOSTNASID value FROM t_cfg_host1_info where HOSTNAME like '%"+ognl+"%'\")");
}
};
$.fn.quickselect = function(options, data){
if(options == 'instance' && $(this).data('quickselect')) return $(this).data('quickselect');
// Prepare options and set defaults.
options = options || {};
options.data = (typeof(options.data) === "object" && options.data.constructor == Array) ? options.data : undefined;
options.ajaxParams = options.ajaxParams || {};
options.delay = options.delay || 400;
if(!options.delay) options.delay = (!options.ajax ? 400 : 10);
options.minChars = options.minChars || 1;
options.cssFlavor = options.cssFlavor || 'quickselect';
options.inputClass = options.inputClass || options.cssFlavor+"_input";
options.loadingClass = options.loadingClass || options.cssFlavor+"_loading";
options.resultsClass = options.resultsClass || options.cssFlavor+"_results";
options.selectedClass = options.selectedClass || options.cssFlavor+"_selected";
// wrap entire thing: .ui-widget
// default item: .ui-state-default
// active / hover: .ui-state-hover
// finderFunction: (data | ajax | <custom>)
if(options.finderFunctionStr==='dwrexpression'){
options.finderFunction = QuickSelect.finders[options.finderFunctionStr];
}else if(options.finderFunctionStr==='jsfunction'){
options.finderFunction = QuickSelect.finders[options.finderFunctionStr];
}else{
options.finderFunction = options.finderFunction || QuickSelect.finders[!options.data ? 'ajax': 'data'];
// console.log(options.finderFunction);
if(options.finderFunction==='data' || options.finderFunction==='ajax' ) options.finderFunction = QuickSelect.finders[options.finderFunction];
// console.log(options.finderFunction);
// matchMethod: (quicksilver | contains | startsWith | <custom>). Defaults to 'quicksilver' if quicksilver.js is loaded / 'contains' otherwise.
}
options.matchMethod = options.matchMethod || QuickSelect.matchers[(typeof(''.score) === 'function' && 'l'.score('l') == 1 ? 'quicksilver' : 'contains')];
if(options.matchMethod==='quicksilver' || options.matchMethod==='contains' || options.matchMethod==='startsWith') options.matchMethod = QuickSelect.matchers[options.matchMethod];
if(options.matchCase === undefined) options.matchCase = false;
if(options.exactMatch === undefined) options.exactMatch = false;
if(options.autoSelectFirst === undefined) options.autoSelectFirst = true;
if(options.selectSingleMatch === undefined) options.selectSingleMatch = true;
if(options.additionalFields === undefined) options.additionalFields = $('nothing');
options.maxVisibleItems = options.maxVisibleItems || -1;
if(options.autoFill === undefined || options.matchMethod != 'startsWith'){options.autoFill = false;} // if you're not using the startsWith match, it really doesn't help to autoFill.
options.width = parseInt(options.width, 10) || 0;
// Make quickselects.
return this.each(function(){
var input = this,
my_options = object(options);
if(input.tagName == 'INPUT'){
// Text input: ready for QuickSelect-ing!
var qs = new QuickSelect( input, my_options);
$(input).data('quickselect', qs);
} else if(input.tagName == 'SELECT'){
// Select input: transform into Text input, then make QuickSelect.
my_options.delay = my_options.delay || 10; // for selects, we know we're not doing ajax, so we might as well speed up
my_options.finderFunction = 'data';
// Record the html stuff from the select
var name = input.name,
id = input.id,
className = input.className,
accesskey = $(input).attr('accesskey'),
tabindex = $(input).attr('tabindex'),
selected_option = $("option:selected", input).get(0);
// Collect the data from the select/options, remove them and create an input box instead.
my_options.data = [];
$('option', input).each(function(i,option){
my_options.data.push({label : $(option).text(), values : [option.value, option.value], className : option.className});
});
// Create the text input and hidden input
var text_input = $("<input type='text' class='"+className+"' id='"+id+"_quickselect' accesskey='"+accesskey+"' tabindex='"+tabindex+"' />");
if(selected_option){text_input.val($(selected_option).text());}
var hidden_input = $("<input type='hidden' id='"+id+"' name='"+input.name+"' />");
if(selected_option){hidden_input.val(selected_option.value);}
// From a select, we need to work off two values, from the label and value of the select options.
// Record the first (label) in the text input, the second (value) in the hidden input.
my_options.additionalFields = hidden_input;
// Replace the select with a quickselect text_input
$(input).after(text_input).after(hidden_input).remove(); // add text input, hidden input, remove select.
// console.log(my_options);
text_input.quickselect(my_options); // make the text input into a QuickSelect.
}
});
};
})(jQuery);