|
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); |