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