Valid HTML test
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / misc / autocomplete.js
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);