这是解决方案的小提琴。
代码在底部
http://jsfiddle.net/R68jf/5/
感谢红色十月的 Marco Ramius 上尉让我意识到解决原始发布问题的方法。“仅 One Ping”.... 如下所述。
这个http://jsfiddle.net/R68jf/5/ fiddle 提供了一个可行的解决方案,并演示了一些有用的自定义 ui-autocomplete 小部件,这也可能有助于解决其他问题。具体来说,该解决方案满足以下要求:
1) 每当用户使用鼠标或键盘浏览自动完成菜单项时,都会触发“焦点”回调,在这种情况下会显示附加信息和链接图像。这是正常的自动完成行为,在 API 中公开并记录在案。
2) 每当用户导航离开或离开菜单时,'focus' 回调的效果应该是可逆的。这不是默认行为,目前没有提供“模糊”API 来消除“焦点”的副作用。因此,“僵尸”图像仍然存在,尽管菜单项不再是焦点。原始问题小提琴显示了这个默认值和相关的“僵尸”效果。要查看它,只需键入“垃圾邮件”,然后在菜单中上下滚动。滚动时,副作用会更新。现在将鼠标从菜单上移开......僵尸留下来!
3) 如果用户使用鼠标单击或返回键在焦点项目上进行选择,则与该项目的焦点相关联的副作用应该持续存在。这很容易,通过使用文档化 API 中公开的“选择”方法。
4) 如果在进行菜单选择后,用户返回搜索输入并更改搜索,则副作用应该是相反的。这在默认情况下不会发生,而是我们有僵尸!
为了达到这些要求,我最终采用了以下方法。对于我们当中更高级的人来说,这些可能看起来很初级,但对于我的薪酬级别的其他人来说,这可能是 ui-autocomplete 自定义的有用演示:
1) 重写自动完成的_renderItem 方法来自定义菜单展示。
2) 覆盖 _move 方法以更改使用键盘导航离开菜单时的行为。
3) 使用一些受“红色十月”启发的“仅 One Ping”回调,以反转焦点副作用,然后将系统返回到其原始就绪状态。这使用 jQuery .one() 方法。
这里的代码被大量注释,但有几点值得一提:
1) 一些事件,例如从菜单中选择一个项目,然后关闭菜单,将自动触发菜单上的模糊和鼠标移出事件,作为其自然行为的一部分。This can be a problem, because, in the current example, I wanted to have a 'blur' function to kill all the zombies, but when an item is selected, that same blur function would kill the legitimate data that I wanted to persist. GRRRRRR!!!
2)当您执行 $('#mySelector').autocomplete({blah:blah, blah:blah}) 时,会创建一个名为 'uiAutocomplete' 的 ui.autocompete() 类的对象和该对象存储为 $('#mySelector') 元素中的数据。因此,要从自动完成类访问数据、选项、方法等,您只需查看 $('#mySelector').data() 就可以了!
这非常漂亮,因为这意味着像 $('#mySelector').data('uiAutocomplete') 这样的东西会让你得到整个 enchilada,如果你想覆盖任何自动完成功能,你可以通过调用 $( '#mySelector').data('uiAutocomplete').myFavoriteFunction = function (){... 做一些漂亮的事情 ..}。
您无需担心原型、子类化或其他任何事情。玩这个的一个好方法是从 ui-autocomplete 源代码中复制确切的函数,然后将其粘贴到您的实例上,然后使用它。您不能破坏任何东西,因为您没有更改原始源(类)代码,您只是在更改实例的代码。好玩,嗯?
3)当你做 $('#mySelector').autocomplete({ foo: function (event, ui) {.. do stuff here..} 你几乎可以用这种方式发明任何你想要的函数,然后调用它这样做._trigger('foo', event)。
这意味着“关闭”、“打开”、“焦点”、“选择”等“官方”方法只是“提供建议”,如果需要,您可以编写自己的函数,而无需实现特殊魔法他们。您的特殊职能可以作为“一等公民”坐在官方职能旁边,没有人会知道其中的区别。唯一的区别是“内置”函数在源代码中被调用,您必须手动调用函数。您可以从代码中的任何位置调用它们,因为它们可以通过 $('#mySelector').data('uiAutocomplete').myFunctionName() 访问
4) 此外,像“ui-menu”和“uiAutocompetemenuItem”这样的对象作为数据存储在它们各自的对象中。通常情况下,这些对象在页面首次加载时不存在,但您可以使用 on() 将事件绑定到将来创建的对象。例如,这会将 mouseenter 事件绑定到所有未来创建的 'li.spam' 对象,并导致它们在鼠标悬停时变为红色,另外从对象的 .data() 字典中获取一个值并将其分配给 $('#mySelector ')。:
$('body').on( {
mouseenter: function() {
$(this).children('a').css("color", "red");
var currentItem = $(this).data('uiAutocompleteItem');
$('#mySelector').value(currentItem.someValue);
}
}, "li.spam");
整个 enchilada 的代码(自包含,如果评论过多,请道歉):
<!DOCTYPE html>
<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.0/jquery-ui.min.js"></script>
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1/themes/smoothness/jquery-ui.css" type="text/css" />
<title>fiddle</title>
<script>
(function ($) {
var quoteList = [
{ 'id': "1",
'label': "We're here to preserve democracy.",
'value': "We're here to preserve democracy, not practice it.",
'description': "Capt. Ramsey to Lt. Commander Hunter",
'title': "Gene Hackman as Capt. Ramsey, Denzel Washington as Lt. Cmdr. Hunter",
'stub' : "Crimson Tide",
'image': "http://2.bp.blogspot.com/_nY72TdvmuKk/S0aoHWh0LeI/AAAAAAAABjY/Yu4krTxxboM/s400/crimsontide.jpg"
},
{ 'id': "2",
'label': "Well, don't like [that cigar] too much.",
'value': "Capt. Ramsey: How do you like that cigar? Hunter: It's good, sir. Capt. Ramsey: It's your first? Hunter: [coughing] Yeah. Capt. Ramsey: Well, don't like it too much. They're more expensive than drugs. ",
'description': "Capt. Ramsey to Lt. Commander Hunter",
'title': "Gene Hackman as Capt. Ramsey, Denzel Washington as Lt. Cmdr. Hunter",
'stub' : "Crimson Tide",
'image': "http://www.vimbly.com/blog/wp-content/uploads/2012/11/26-e1352860345409.jpg"
},
{ 'id': "3",
'label': "I expect and demand your very best.",
'value': "I expect and demand your very best. Anything less, you should have joined the Air Force.",
'description': "Ramsey to the crew of the Crimson Tide",
'title': "Gene Hackman as Capt. Ramsey, Denzel Washington as Lt. Cmdr. Hunter",
'stub' : "Crimson Tide",
'image': "http://i2.cdnds.net/12/34/618x405/movies_tony_scott_films_8.jpg"
},
{ 'id': "4",
'label': "The Kirby Silver Surfer is the only true Silver Surfer.",
'value': "Everybody knows that the Kirby Silver Surfer is the only true Silver Surfer.",
'description': "Hunter to Petty Officer First Class Danny Rivetti (Danny Nucci)",
'title': "Gene Hackman as Capt. Ramsey, Denzel Washington as Lt. Cmdr. Hunter",
'stub' : "Crimson Tide",
'image': "http://www.empireonline.com/images/features/a-z/quentin-tarantino/4.jpg"
},
{ 'id': "5",
'label': "I don't trust air I can't see.",
'value': "I don't trust air I can't see.",
'description': "Ramsey to Hunter",
'title': "Gene Hackman as Capt. Ramsey, Denzel Washington as Lt. Cmdr. Hunter",
'stub' : "Crimson Tide",
'image': "http://images3.cinema.de/imedia/1853/2211853,JSg9qf_ICg9XI70_mnk+0YKnEwjDCNvEJ135zZiOOG51Em+3vDb9zfon3uv_jNxJfz3ogxTr3jHE26akqhRXcA==.jpg"
},
{ 'id': "6",
'label': "Mr Hunter. I've made a decision.",
'value': "Mr Hunter. I've made a decision. I'm Captain of this boat. NOW SHUT THE FUCK UP!",
'description': "Ramsey to Hunter",
'title': "Gene Hackman as Capt. Ramsey, Denzel Washington as Lt. Cmdr. Hunter",
'stub' : "Crimson Tide",
'image': "http://images.static-bluray.com/reviews/271_1.jpg"
},
{ 'id': "7",
'label': "Mr. Vossler, this is Captain Kirk!",
'value': "Mr. Vossler, this is Captain Kirk! I need warp speed on that communications unit!",
'description': "Hunter to Petty Officer Third Class Russell Vossler (Lillo Brancato)",
'title': "Gene Hackman as Capt. Ramsey, Denzel Washington as Lt. Cmdr. Hunter",
'stub' : "Crimson Tide",
'image': "http://www.livedash.com/thumb_v5/20091130/crimson%20tide-2009-11-30-0/2_crimson%20tide-2009-11-30-0.jpg"
},
{ 'id': "8",
'label': "I will marry a round American woman and raise rabbits.",
'value': "I will live in Montana. And I will marry a round American woman and raise rabbits, and she will cook them for me. And I will have a pickup truck... maybe even a recreational vehicle. And drive from state to state. Do they let you do that? ",
'description': "Capt. Vasili Borodin (Sam Neill) to Ramius",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "Crimson Tide",
'image': "http://1.bp.blogspot.com/-08wMMkx53XQ/TrNoK0ykLzI/AAAAAAAAkK0/p5Q_F6EOrvI/s360/Crimson-Tide.jpg"
},
{ 'id': "9",
'label': "We will lay off their largest city, and listen to their rock and roll!",
'value': "We will pass through the American patrols, past their sonar nets, and lay off their largest city, and listen to their rock and roll... while we conduct missile drills.",
'description': "Ramius to the crew of the The Hunt for Red October",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://libcom.org/files/images/library/alec_baldwin1%5B1%5D.jpg"
},
{ 'id': "10",
'label': "You're afraid. Well, you should be. Personally, I'd give us one chance in three. More tea anyone?",
'value': "You're afraid of our fleet. Well, you should be. Personally, I'd give us one chance in three. More tea anyone? ",
'description': "Ramius to the officers of the The Hunt for Red October",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://i77.photobucket.com/albums/j60/bluinkalchemist/hunt-for-red-october_480_poster.jpg"
},
{ 'id': "11",
'label': "Hey, Ryan, be careful what you shoot at. Most things in here don't react too well to bullets.",
'value': "Hey, Ryan, be careful what you shoot at. Most things in here don't react too well to bullets.",
'description': "Ramius to Ryan",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://wodumedia.com/wp-content/uploads/2012/10/Sean-Connery-and-Alec-Baldwin-in-Paramounts-The-Hunt-For-Red-October-1990-0.jpg"
},
{ 'id': "12",
'label': "When he reached the New World, Cortez burned his ships. As a result his men were well motivated.",
'value': "When he reached the New World, Cortez burned his ships. As a result his men were well motivated.",
'description': "Remius to the officers of the The Hunt for Red October",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://content.internetvideoarchive.com/content/photos/070/002974_25.jpg"
},
{ 'id': "13",
'label': "Russians don't take a dump, son, without a plan.",
'value': "'The average Ruskie, son, don't take a dump without a plan.' Wait a minute. We don't have to figure out how to get the crew off the sub. He's already done that, he would have had to. All we gotta do is figure out what he's gonna do. So how's he gonna get the crew of the sub. ",
'description': "Ryan reliving a comment by Admiral Josh Painter (Fred Thompson)",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://toddmpost.files.wordpress.com/2010/12/admiralpainter.jpg?w=640"
},
{ 'id': "14",
'label': "At that speed, they could run right over my daughter's stereo and not hear it.",
'value': "They're pinging away with their active sonar like they're looking for something, but nobody's listening... they're moving at almost forty knots. At that speed, they could run right over my daughter's stereo and not hear it.",
'description': "Capt. Davenport (Daniel Davis) to Ryan",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://libcom.org/files/images/library/alec_baldwin1%5B1%5D.jpg"
},
{ 'id': "15",
'label': "I would like to have seen Montana.",
'value': "I would like to have seen Montana.",
'description': "Capt. Vasili Borodin (Sam Neill) to Capt. Ramius",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://img.geocaching.com/track/display/ad4047d5-a1f9-4a39-86c9-8a7a0873f1ef.jpg"
},
{ 'id': "16",
'label': "Next time, Jack, write a goddamn memo.",
'value': "Next time, Jack, write a goddamn memo.",
'description': "Ryan (talking to himself)",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://www.wearysloth.com/Gallery/ActorsB/860-9042.gif"
},
{ 'id': "17",
'label': "Give me a ping, Vasili. One ping only, please.",
'value': "Re-verify our range to target... one ping only. Give me a ping, Vasili. One ping only, please.",
'description': "Ramius to Capt. Vasili Borodin (Sam Neill)",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://25.media.tumblr.com/tumblr_ma1prrCDn81qkcj94o1_1280.jpg"
},
{ 'id': "18",
'label': "Come up right behind his propellor and he'll be deaf as a post!",
'value': "Not if we stay in his baffles, Seaman Beaumont. Not if we stay in his baffles. Come up right behind his propellor and he'll be deaf as a post!",
'description': "Seaman 'Jonesy' Jones (Courtney Vance) to Seaman Beaumont (Ned Vaughn)",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://cdn3.hark.com/images/000/375/672/375672/original.jpg"
},
{ 'id': "19",
'label': "Andrei, you've lost another submarine?",
'value': "One of our submarines, an Alfa, was last reported in the area of the Grand Banks. We have not heard from her for some time..... Andrei, you've lost another submarine?",
'description': "Dr. Jeffrey Pelt (Richard Jordan) to Russian Ambassador Andrei Lysenko (Joss Ackland)",
'title': "Sean Connery as Capt. Ramius, Alec Baldwin as Jack Ryan",
'stub' : "The Hunt for Red October",
'image': "http://www.wearysloth.com/Gallery/ActorsJ/8969-9042.gif"
}
];
$(document).ready (function () {
$( "#id_quote" ).autocomplete({
minLength: 2,
delay: 2,
source: quoteList,
open: function(event, ui) {
// defines the character of the ui-menu
$(".ui-autocomplete")
.css("width", "300px")
.css("max-height", "200px")
.css("overflow-y", "scroll")
},
focus: function( event, ui ) {
// This causes ancillary fields to populate whenever a menu choice is hovered
// also works if menu choices are navigated with keyboard up and down arrows
$( "#id_stub" ).val( ui.item.stub );
$( "#id_title" ).val( ui.item.title );
$( "#id_description" ).val( ui.item.description );
$( "#id_dialog" ).val( ui.item.value );
$( '#id_image').html("<img style='width:100%; height:100%;' src='" + ui.item.image + "' />");
// Ensures that when mouse leaves the manu, that the ancillary fields are emptied
// This is a 'one ping only' event, because 'mouseout' is triggered by the closing of the
// menu when a true choice is made (either by mouse click or return key).
// Making this 'one ping only' ensures that the ancillary fields don't empty
// when the menu closes after a valid selection is made.
// Of course, if there is a new focus, the 'one ping only' rule must be re-established,
// so it is put here, in the focus callback:
$('li.one-ping-only').one ("mouseleave", function() {
$( "#id_stub" ).val("");
$( "#id_title" ).val("");
$( "#id_description" ).val("");
$( "#id_dialog" ).val("");
$( '#id_image').empty();
});
},
select: function( event, ui ) {
// Takes off the 'one ping only' mouse event binding,
// otherwise it would cause the ancillary fields to empty
// due to the 'mouseout' that happens automatically when
// the menu closes.
// In some cases, the mouseout binding has already been removed, but
// in other cases not (by a true mouseout event). This is a safe
// way to make sure before a selection event fills the fields.
$('li.one-ping-only').off("mouseleave");
$( "#id_stub" ).val( ui.item.stub );
$( "#id_title" ).val( ui.item.title );
$( "#id_description" ).val( ui.item.description );
$( "#id_dialog" ).val( ui.item.value );
$( '#id_image').html("<img style='width:100%; height:100%;' src='" + ui.item.image + "' />");
$( "#id_quote" ).val( ui.item.value )
// In general, we want to make sure that if the user goes back and types
// again or backspaces in the autocomplete input, AFTER making a selection, then the
// ancillary fields will blank, to avoid zombie data persistence.
// Unfortunately, making a selection with the return key (vs. a mouse click)
// sends (or propagates?) a return key event to the original input field, AFTER the select
// event has been called. Thus, though a valid select event populates the fields (by the
// actions immediately above), the ancillary data is them immediately erased
// by this key event that arrives a split second later.
// To counter this, we make an exception so that in the event that the first key event arriving
// after a select event happens to be a return key, (the selection was made with the keyboard)
// then that single key event is exempted from blanking the field.
.one ("keypress", function (e) {
if (e.keyCode !== 13) {
$( "#id_stub" ).val("");
$( "#id_title" ).val("");
$( "#id_description" ).val("");
$( "#id_dialog" ).val("");
$( '#id_image').empty();
}
else {
// The 'one ping only' event above is REMOVED if any key event takes place, even if it was
// as 'disqualified' return key event. So, even though nothing happened visibly,
// we need to re-establish our 'one ping only' protection from 'real editing' key event.
$( "#id_quote" ).one ("keypress", function (e) {
if (e.keyCode !== 13) {
$( "#id_stub" ).val("");
$( "#id_title" ).val("");
$( "#id_description" ).val("");
$( "#id_dialog" ).val("");
$( '#id_image').empty();
}
})
}
});
}
}).data("uiAutocomplete")._renderItem = function( ul, item ) {
// The uiAutocomplete object is stored as part data in the input field it is attached to.
// When we call .data("uiAutocomplete") here, it relates back to data attribute of the $( "#id_quote" ) that the
// autocomplete widget function was attached to. It's helpful to realize that the uiAutocomplete object is part of the
// $( "#id_quote" ).data() and can be called at any time, not just here right after autocomplete is called.
// This can be confusing, but just remember that once you do $( "#id_quote" ).autocomplete({blah blah}), then forever
// .data() attribute of the $( "#id_quote" ) will contain a 'uiAutocomplete' object, the properties of which are the
// properties defined in the jQuery-ui class autocomplete, as instantiated by you.
// Anyway, we want to override one of the autocomplete methods, specifically the _renderItem method
// that makes the items in the menu.
// Here, we specify the nature of the presentation, giving it first the traditional label, and also another line
// that in this case is item.description.
// In addition, we perform a regular expression match on the label, and highlight the letters/words
// that match the search term we have typed into our search input.
var regxpmatch = new RegExp(this.term, "ig");
// Note the 'g' in 'ig' means global to catch any place the letters match, otherwise the default is to match only the first
// instance in any item.label.
// Also note the 'i' in 'ig' makes the match case insensitive.
var formatted_quote = item.label.replace(regxpmatch,"<span style='font-weight:bold;color: red;'>" + this.term + "</span>");
return $( "<li class='one-ping-only'></li>" )
// the 'one-ping-only' class is the hook for our callbacks that erase ancillary fields when a user edits the original
// input after making a selection, and also is used for another callback that erases ancillary fields if the menu loses
// focus by moving the mouse out of the menu, or alternatively using the up or down key on the keyboard to leave
// the menu.
.append( "<a><div class='subquote'>" + formatted_quote + "</div><div class='subtitle'>" + item.description + "</div></a><hr>" )
// makes the menu item have two lines, and puts a horizontal line between each item pair.
.appendTo( ul );
};
$( "#id_quote" ).data( "uiAutocomplete" )
._move = function( direction, event ) {
// here we are using a similar logic to override the _move method, ensuring that if the user exits the menu
// using the keyboard, the ancillary fields will clear, just as they do with the 'one ping only' mouseout function
// described above.
if ( !this.menu.element.is( ":visible" ) ) {
this.search( null, event );
return;
}
if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
this.menu.isLastItem() && /^next/.test( direction ) ) {
this._value( this.term );
$( "#id_stub" ).val("");
$( "#id_title" ).val("");
$( "#id_description" ).val("");
$( "#id_dialog" ).val("");
return;
}
this.menu[ direction ]( event );
};
});
})(jQuery);
</script>
<style type="text/css">
div.subquote {font-size: 11px;}
div.subtitle {font-size: 9px; color: blue;}
#id_quote {width: 250px; height:100px;}
#id_stub {width: 250px;}
#id_title, #id_description {width: 250px;}
#id_dialog {width: 250px; min-height: 100px;}
</style>
</head>
<body>
<form action="#" method='POST'>
<div style="float:left; width: 300px;">
<h4>Search submarine movie quotes...</h4>
<h4>...or simply write in your own!</h4>
<div>
<textarea name="quote" type="text" id="id_quote" placeholder="Start typing here...." ></textarea>
</div>
</div>
<div style="float:left; margin-left: 50px;">
<div style="margin: 5px 0 5px 10px;">
<input name="stub" type="text" id="id_stub" />
</div>
<div style="margin: 5px 0 5px 10px;">
<textarea name="title" type="text" id="id_title" ></textarea>
</div>
<div style="margin: 5px 0 5px 10px;">
<textarea name="description" type="text" id="id_description"></textarea>
</div>
<div style="margin: 5px 0 5px 10px;">
<textarea name="dialog" type="text" id="id_dialog"></textarea>
</div>
<button type="submit" id="id_button">Submit your Favorite!</button>
<div name="image" id="id_image" style="width: 250px; height:250px;">
</div>
</div>
</form>
</body>