root / drupal7 / misc / autocomplete.js @ 2196f227
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 |
$(this.input).trigger('autocompleteSelect', [node]); |
118 |
}; |
119 |
|
120 |
/**
|
121 |
* Highlights the next suggestion.
|
122 |
*/
|
123 |
Drupal.jsAC.prototype.selectDown = function () { |
124 |
if (this.selected && this.selected.nextSibling) { |
125 |
this.highlight(this.selected.nextSibling); |
126 |
} |
127 |
else if (this.popup) { |
128 |
var lis = $('li', this.popup); |
129 |
if (lis.length > 0) { |
130 |
this.highlight(lis.get(0)); |
131 |
} |
132 |
} |
133 |
}; |
134 |
|
135 |
/**
|
136 |
* Highlights the previous suggestion.
|
137 |
*/
|
138 |
Drupal.jsAC.prototype.selectUp = function () { |
139 |
if (this.selected && this.selected.previousSibling) { |
140 |
this.highlight(this.selected.previousSibling); |
141 |
} |
142 |
}; |
143 |
|
144 |
/**
|
145 |
* Highlights a suggestion.
|
146 |
*/
|
147 |
Drupal.jsAC.prototype.highlight = function (node) { |
148 |
if (this.selected) { |
149 |
$(this.selected).removeClass('selected'); |
150 |
} |
151 |
$(node).addClass('selected'); |
152 |
this.selected = node;
|
153 |
$(this.ariaLive).html($(this.selected).html()); |
154 |
}; |
155 |
|
156 |
/**
|
157 |
* Unhighlights a suggestion.
|
158 |
*/
|
159 |
Drupal.jsAC.prototype.unhighlight = function (node) { |
160 |
$(node).removeClass('selected'); |
161 |
this.selected = false; |
162 |
$(this.ariaLive).empty(); |
163 |
}; |
164 |
|
165 |
/**
|
166 |
* Hides the autocomplete suggestions.
|
167 |
*/
|
168 |
Drupal.jsAC.prototype.hidePopup = function (keycode) { |
169 |
// Select item if the right key or mousebutton was pressed.
|
170 |
if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) { |
171 |
this.select(this.selected); |
172 |
} |
173 |
// Hide popup.
|
174 |
var popup = this.popup; |
175 |
if (popup) {
|
176 |
this.popup = null; |
177 |
$(popup).fadeOut('fast', function () { $(popup).remove(); }); |
178 |
} |
179 |
this.selected = false; |
180 |
$(this.ariaLive).empty(); |
181 |
}; |
182 |
|
183 |
/**
|
184 |
* Positions the suggestions popup and starts a search.
|
185 |
*/
|
186 |
Drupal.jsAC.prototype.populatePopup = function () { |
187 |
var $input = $(this.input); |
188 |
var position = $input.position(); |
189 |
// Show popup.
|
190 |
if (this.popup) { |
191 |
$(this.popup).remove(); |
192 |
} |
193 |
this.selected = false; |
194 |
this.popup = $('<div id="autocomplete"></div>')[0]; |
195 |
this.popup.owner = this; |
196 |
$(this.popup).css({ |
197 |
top: parseInt(position.top + this.input.offsetHeight, 10) + 'px', |
198 |
left: parseInt(position.left, 10) + 'px', |
199 |
width: $input.innerWidth() + 'px', |
200 |
display: 'none' |
201 |
}); |
202 |
$input.before(this.popup); |
203 |
|
204 |
// Do search.
|
205 |
this.db.owner = this; |
206 |
this.db.search(this.input.value); |
207 |
}; |
208 |
|
209 |
/**
|
210 |
* Fills the suggestion popup with any matches received.
|
211 |
*/
|
212 |
Drupal.jsAC.prototype.found = function (matches) { |
213 |
// If no value in the textfield, do not show the popup.
|
214 |
if (!this.input.value.length) { |
215 |
return false; |
216 |
} |
217 |
|
218 |
// Prepare matches.
|
219 |
var ul = $('<ul></ul>'); |
220 |
var ac = this; |
221 |
for (key in matches) { |
222 |
$('<li></li>') |
223 |
.html($('<div></div>').html(matches[key])) |
224 |
.mousedown(function () { ac.hidePopup(this); }) |
225 |
.mouseover(function () { ac.highlight(this); }) |
226 |
.mouseout(function () { ac.unhighlight(this); }) |
227 |
.data('autocompleteValue', key)
|
228 |
.appendTo(ul); |
229 |
} |
230 |
|
231 |
// Show popup with matches, if any.
|
232 |
if (this.popup) { |
233 |
if (ul.children().length) {
|
234 |
$(this.popup).empty().append(ul).show(); |
235 |
$(this.ariaLive).html(Drupal.t('Autocomplete popup')); |
236 |
} |
237 |
else {
|
238 |
$(this.popup).css({ visibility: 'hidden' }); |
239 |
this.hidePopup();
|
240 |
} |
241 |
} |
242 |
}; |
243 |
|
244 |
Drupal.jsAC.prototype.setStatus = function (status) { |
245 |
switch (status) {
|
246 |
case 'begin': |
247 |
$(this.input).addClass('throbbing'); |
248 |
$(this.ariaLive).html(Drupal.t('Searching for matches...')); |
249 |
break;
|
250 |
case 'cancel': |
251 |
case 'error': |
252 |
case 'found': |
253 |
$(this.input).removeClass('throbbing'); |
254 |
break;
|
255 |
} |
256 |
}; |
257 |
|
258 |
/**
|
259 |
* An AutoComplete DataBase object.
|
260 |
*/
|
261 |
Drupal.ACDB = function (uri) { |
262 |
this.uri = uri;
|
263 |
this.delay = 300; |
264 |
this.cache = {};
|
265 |
}; |
266 |
|
267 |
/**
|
268 |
* Performs a cached and delayed search.
|
269 |
*/
|
270 |
Drupal.ACDB.prototype.search = function (searchString) { |
271 |
var db = this; |
272 |
this.searchString = searchString;
|
273 |
|
274 |
// See if this string needs to be searched for anyway. The pattern ../ is
|
275 |
// stripped since it may be misinterpreted by the browser.
|
276 |
searchString = searchString.replace(/^\s+|\.{2,}\/|\s+$/g, ''); |
277 |
// Skip empty search strings, or search strings ending with a comma, since
|
278 |
// that is the separator between search terms.
|
279 |
if (searchString.length <= 0 || |
280 |
searchString.charAt(searchString.length - 1) == ',') { |
281 |
return;
|
282 |
} |
283 |
|
284 |
// See if this key has been searched for before.
|
285 |
if (this.cache[searchString]) { |
286 |
return this.owner.found(this.cache[searchString]); |
287 |
} |
288 |
|
289 |
// Initiate delayed search.
|
290 |
if (this.timer) { |
291 |
clearTimeout(this.timer);
|
292 |
} |
293 |
this.timer = setTimeout(function () { |
294 |
db.owner.setStatus('begin');
|
295 |
|
296 |
// Ajax GET request for autocompletion. We use Drupal.encodePath instead of
|
297 |
// encodeURIComponent to allow autocomplete search terms to contain slashes.
|
298 |
$.ajax({
|
299 |
type: 'GET', |
300 |
url: db.uri + '/' + Drupal.encodePath(searchString), |
301 |
dataType: 'json', |
302 |
success: function (matches) { |
303 |
if (typeof matches.status == 'undefined' || matches.status != 0) { |
304 |
db.cache[searchString] = matches; |
305 |
// Verify if these are still the matches the user wants to see.
|
306 |
if (db.searchString == searchString) {
|
307 |
db.owner.found(matches); |
308 |
} |
309 |
db.owner.setStatus('found');
|
310 |
} |
311 |
}, |
312 |
error: function (xmlhttp) { |
313 |
Drupal.displayAjaxError(Drupal.ajaxError(xmlhttp, db.uri)); |
314 |
} |
315 |
}); |
316 |
}, this.delay);
|
317 |
}; |
318 |
|
319 |
/**
|
320 |
* Cancels the current autocomplete request.
|
321 |
*/
|
322 |
Drupal.ACDB.prototype.cancel = function () { |
323 |
if (this.owner) this.owner.setStatus('cancel'); |
324 |
if (this.timer) clearTimeout(this.timer); |
325 |
this.searchString = ''; |
326 |
}; |
327 |
|
328 |
})(jQuery); |