root / drupal7 / misc / autocomplete.js @ f4462ddf
1 |
(function ($) { |
---|---|
2 |
|
3 |
/**
|
4 |
* Attaches the autocomplete behavior to all required fields.
|
5 |
*/
|
6 |
Drupal.behaviors.autocomplete = { |
7 |
attach: function (context, settings) { |
8 |
var acdb = [];
|
9 |
$('input.autocomplete', context).once('autocomplete', function () { |
10 |
var uri = this.value; |
11 |
if (!acdb[uri]) {
|
12 |
acdb[uri] = new Drupal.ACDB(uri);
|
13 |
} |
14 |
var $input = $('#' + this.id.substr(0, this.id.length - 13)) |
15 |
.attr('autocomplete', 'OFF') |
16 |
.attr('aria-autocomplete', 'list'); |
17 |
$($input[0].form).submit(Drupal.autocompleteSubmit); |
18 |
$input.parent()
|
19 |
.attr('role', 'application') |
20 |
.append($('<span class="element-invisible" aria-live="assertive"></span>') |
21 |
.attr('id', $input.attr('id') + '-autocomplete-aria-live') |
22 |
); |
23 |
new Drupal.jsAC($input, acdb[uri]); |
24 |
}); |
25 |
} |
26 |
}; |
27 |
|
28 |
/**
|
29 |
* Prevents the form from submitting if the suggestions popup is open
|
30 |
* and closes the suggestions popup when doing so.
|
31 |
*/
|
32 |
Drupal.autocompleteSubmit = function () { |
33 |
return $('#autocomplete').each(function () { |
34 |
this.owner.hidePopup();
|
35 |
}).length == 0;
|
36 |
}; |
37 |
|
38 |
/**
|
39 |
* An AutoComplete object.
|
40 |
*/
|
41 |
Drupal.jsAC = function ($input, db) { |
42 |
var ac = this; |
43 |
this.input = $input[0]; |
44 |
this.ariaLive = $('#' + this.input.id + '-autocomplete-aria-live'); |
45 |
this.db = db;
|
46 |
|
47 |
$input
|
48 |
.keydown(function (event) { return ac.onkeydown(this, event); }) |
49 |
.keyup(function (event) { ac.onkeyup(this, event); }) |
50 |
.blur(function () { ac.hidePopup(); ac.db.cancel(); });
|
51 |
|
52 |
}; |
53 |
|
54 |
/**
|
55 |
* Handler for the "keydown" event.
|
56 |
*/
|
57 |
Drupal.jsAC.prototype.onkeydown = function (input, e) { |
58 |
if (!e) {
|
59 |
e = window.event; |
60 |
} |
61 |
switch (e.keyCode) {
|
62 |
case 40: // down arrow. |
63 |
this.selectDown();
|
64 |
return false; |
65 |
case 38: // up arrow. |
66 |
this.selectUp();
|
67 |
return false; |
68 |
default: // All other keys. |
69 |
return true; |
70 |
} |
71 |
}; |
72 |
|
73 |
/**
|
74 |
* Handler for the "keyup" event.
|
75 |
*/
|
76 |
Drupal.jsAC.prototype.onkeyup = function (input, e) { |
77 |
if (!e) {
|
78 |
e = window.event; |
79 |
} |
80 |
switch (e.keyCode) {
|
81 |
case 16: // Shift. |
82 |
case 17: // Ctrl. |
83 |
case 18: // Alt. |
84 |
case 20: // Caps lock. |
85 |
case 33: // Page up. |
86 |
case 34: // Page down. |
87 |
case 35: // End. |
88 |
case 36: // Home. |
89 |
case 37: // Left arrow. |
90 |
case 38: // Up arrow. |
91 |
case 39: // Right arrow. |
92 |
case 40: // Down arrow. |
93 |
return true; |
94 |
|
95 |
case 9: // Tab. |
96 |
case 13: // Enter. |
97 |
case 27: // Esc. |
98 |
this.hidePopup(e.keyCode);
|
99 |
return true; |
100 |
|
101 |
default: // All other keys. |
102 |
if (input.value.length > 0 && !input.readOnly) { |
103 |
this.populatePopup();
|
104 |
} |
105 |
else {
|
106 |
this.hidePopup(e.keyCode);
|
107 |
} |
108 |
return true; |
109 |
} |
110 |
}; |
111 |
|
112 |
/**
|
113 |
* Puts the currently highlighted suggestion into the autocomplete field.
|
114 |
*/
|
115 |
Drupal.jsAC.prototype.select = function (node) { |
116 |
this.input.value = $(node).data('autocompleteValue'); |
117 |
}; |
118 |
|
119 |
/**
|
120 |
* Highlights the next suggestion.
|
121 |
*/
|
122 |
Drupal.jsAC.prototype.selectDown = function () { |
123 |
if (this.selected && this.selected.nextSibling) { |
124 |
this.highlight(this.selected.nextSibling); |
125 |
} |
126 |
else if (this.popup) { |
127 |
var lis = $('li', this.popup); |
128 |
if (lis.length > 0) { |
129 |
this.highlight(lis.get(0)); |
130 |
} |
131 |
} |
132 |
}; |
133 |
|
134 |
/**
|
135 |
* Highlights the previous suggestion.
|
136 |
*/
|
137 |
Drupal.jsAC.prototype.selectUp = function () { |
138 |
if (this.selected && this.selected.previousSibling) { |
139 |
this.highlight(this.selected.previousSibling); |
140 |
} |
141 |
}; |
142 |
|
143 |
/**
|
144 |
* Highlights a suggestion.
|
145 |
*/
|
146 |
Drupal.jsAC.prototype.highlight = function (node) { |
147 |
if (this.selected) { |
148 |
$(this.selected).removeClass('selected'); |
149 |
} |
150 |
$(node).addClass('selected'); |
151 |
this.selected = node;
|
152 |
$(this.ariaLive).html($(this.selected).html()); |
153 |
}; |
154 |
|
155 |
/**
|
156 |
* Unhighlights a suggestion.
|
157 |
*/
|
158 |
Drupal.jsAC.prototype.unhighlight = function (node) { |
159 |
$(node).removeClass('selected'); |
160 |
this.selected = false; |
161 |
$(this.ariaLive).empty(); |
162 |
}; |
163 |
|
164 |
/**
|
165 |
* Hides the autocomplete suggestions.
|
166 |
*/
|
167 |
Drupal.jsAC.prototype.hidePopup = function (keycode) { |
168 |
// Select item if the right key or mousebutton was pressed.
|
169 |
if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) { |
170 |
this.input.value = $(this.selected).data('autocompleteValue'); |
171 |
} |
172 |
// Hide popup.
|
173 |
var popup = this.popup; |
174 |
if (popup) {
|
175 |
this.popup = null; |
176 |
$(popup).fadeOut('fast', function () { $(popup).remove(); }); |
177 |
} |
178 |
this.selected = false; |
179 |
$(this.ariaLive).empty(); |
180 |
}; |
181 |
|
182 |
/**
|
183 |
* Positions the suggestions popup and starts a search.
|
184 |
*/
|
185 |
Drupal.jsAC.prototype.populatePopup = function () { |
186 |
var $input = $(this.input); |
187 |
var position = $input.position(); |
188 |
// Show popup.
|
189 |
if (this.popup) { |
190 |
$(this.popup).remove(); |
191 |
} |
192 |
this.selected = false; |
193 |
this.popup = $('<div id="autocomplete"></div>')[0]; |
194 |
this.popup.owner = this; |
195 |
$(this.popup).css({ |
196 |
top: parseInt(position.top + this.input.offsetHeight, 10) + 'px', |
197 |
left: parseInt(position.left, 10) + 'px', |
198 |
width: $input.innerWidth() + 'px', |
199 |
display: 'none' |
200 |
}); |
201 |
$input.before(this.popup); |
202 |
|
203 |
// Do search.
|
204 |
this.db.owner = this; |
205 |
this.db.search(this.input.value); |
206 |
}; |
207 |
|
208 |
/**
|
209 |
* Fills the suggestion popup with any matches received.
|
210 |
*/
|
211 |
Drupal.jsAC.prototype.found = function (matches) { |
212 |
// If no value in the textfield, do not show the popup.
|
213 |
if (!this.input.value.length) { |
214 |
return false; |
215 |
} |
216 |
|
217 |
// Prepare matches.
|
218 |
var ul = $('<ul></ul>'); |
219 |
var ac = this; |
220 |
for (key in matches) { |
221 |
$('<li></li>') |
222 |
.html($('<div></div>').html(matches[key])) |
223 |
.mousedown(function () { ac.select(this); }) |
224 |
.mouseover(function () { ac.highlight(this); }) |
225 |
.mouseout(function () { ac.unhighlight(this); }) |
226 |
.data('autocompleteValue', key)
|
227 |
.appendTo(ul); |
228 |
} |
229 |
|
230 |
// Show popup with matches, if any.
|
231 |
if (this.popup) { |
232 |
if (ul.children().length) {
|
233 |
$(this.popup).empty().append(ul).show(); |
234 |
$(this.ariaLive).html(Drupal.t('Autocomplete popup')); |
235 |
} |
236 |
else {
|
237 |
$(this.popup).css({ visibility: 'hidden' }); |
238 |
this.hidePopup();
|
239 |
} |
240 |
} |
241 |
}; |
242 |
|
243 |
Drupal.jsAC.prototype.setStatus = function (status) { |
244 |
switch (status) {
|
245 |
case 'begin': |
246 |
$(this.input).addClass('throbbing'); |
247 |
$(this.ariaLive).html(Drupal.t('Searching for matches...')); |
248 |
break;
|
249 |
case 'cancel': |
250 |
case 'error': |
251 |
case 'found': |
252 |
$(this.input).removeClass('throbbing'); |
253 |
break;
|
254 |
} |
255 |
}; |
256 |
|
257 |
/**
|
258 |
* An AutoComplete DataBase object.
|
259 |
*/
|
260 |
Drupal.ACDB = function (uri) { |
261 |
this.uri = uri;
|
262 |
this.delay = 300; |
263 |
this.cache = {};
|
264 |
}; |
265 |
|
266 |
/**
|
267 |
* Performs a cached and delayed search.
|
268 |
*/
|
269 |
Drupal.ACDB.prototype.search = function (searchString) { |
270 |
var db = this; |
271 |
this.searchString = searchString;
|
272 |
|
273 |
// See if this string needs to be searched for anyway.
|
274 |
searchString = searchString.replace(/^\s+|\s+$/, ''); |
275 |
if (searchString.length <= 0 || |
276 |
searchString.charAt(searchString.length - 1) == ',') { |
277 |
return;
|
278 |
} |
279 |
|
280 |
// See if this key has been searched for before.
|
281 |
if (this.cache[searchString]) { |
282 |
return this.owner.found(this.cache[searchString]); |
283 |
} |
284 |
|
285 |
// Initiate delayed search.
|
286 |
if (this.timer) { |
287 |
clearTimeout(this.timer);
|
288 |
} |
289 |
this.timer = setTimeout(function () { |
290 |
db.owner.setStatus('begin');
|
291 |
|
292 |
// Ajax GET request for autocompletion. We use Drupal.encodePath instead of
|
293 |
// encodeURIComponent to allow autocomplete search terms to contain slashes.
|
294 |
$.ajax({
|
295 |
type: 'GET', |
296 |
url: db.uri + '/' + Drupal.encodePath(searchString), |
297 |
dataType: 'json', |
298 |
success: function (matches) { |
299 |
if (typeof matches.status == 'undefined' || matches.status != 0) { |
300 |
db.cache[searchString] = matches; |
301 |
// Verify if these are still the matches the user wants to see.
|
302 |
if (db.searchString == searchString) {
|
303 |
db.owner.found(matches); |
304 |
} |
305 |
db.owner.setStatus('found');
|
306 |
} |
307 |
}, |
308 |
error: function (xmlhttp) { |
309 |
alert(Drupal.ajaxError(xmlhttp, db.uri)); |
310 |
} |
311 |
}); |
312 |
}, this.delay);
|
313 |
}; |
314 |
|
315 |
/**
|
316 |
* Cancels the current autocomplete request.
|
317 |
*/
|
318 |
Drupal.ACDB.prototype.cancel = function () { |
319 |
if (this.owner) this.owner.setStatus('cancel'); |
320 |
if (this.timer) clearTimeout(this.timer); |
321 |
this.searchString = ''; |
322 |
}; |
323 |
|
324 |
})(jQuery); |