Commit | Line | Data |
---|---|---|
5e35e81b VZ |
1 | /* jQuery autocomplete Copyright Dylan Verheul <dylan@dyve.net> |
2 | * Licensed like jQuery, see http://docs.jquery.com/License | |
3 | */ | |
4 | ||
5 | $.autocomplete = function(input, options) { | |
6 | // Create a link to self | |
7 | var me = this; | |
8 | ||
9 | // Create jQuery object for input element | |
10 | var $input = $(input).attr("autocomplete", "off");; | |
11 | ||
12 | // Apply inputClass if necessary | |
13 | if (options.inputClass) $input.addClass(options.inputClass); | |
14 | ||
15 | // Create results | |
16 | var results = document.createElement("div"); | |
17 | // Create jQuery object for results | |
18 | var $results = $(results); | |
19 | // Set default values for results | |
20 | var pos = findPos(input); | |
21 | $results.hide().addClass(options.resultsClass).css({ | |
22 | position: "absolute", | |
23 | top: (pos.y + input.offsetHeight) + "px", | |
24 | left: pos.x + "px", | |
25 | minWidth: $(input).width() | |
26 | }); | |
27 | // Add to body element | |
28 | $(input).parent().append(results); | |
29 | ||
30 | input.autocompleter = me; | |
31 | input.lastSelected = $input.val(); | |
32 | ||
33 | var timeout = null; | |
34 | var prev = ""; | |
35 | var active = -1; | |
36 | var cache = {}; | |
37 | var keyb = false; | |
38 | ||
39 | $input | |
40 | .keydown(function(e) { | |
41 | switch(e.keyCode) { | |
42 | case 38: // up | |
43 | e.preventDefault(); | |
44 | moveSelect(-1); | |
45 | break; | |
46 | case 40: // down | |
47 | e.preventDefault(); | |
48 | moveSelect(1); | |
49 | break; | |
50 | case 9: // tab | |
51 | case 13: // return | |
52 | if (selectCurrent()) { | |
53 | e.preventDefault(); | |
54 | } | |
55 | break; | |
56 | default: | |
57 | active = -1; | |
58 | if (timeout) clearTimeout(timeout); | |
59 | timeout = setTimeout(onChange, options.delay); | |
60 | break; | |
61 | } | |
62 | }) | |
63 | .blur(function() { | |
64 | hideResults(); | |
65 | }); | |
66 | ||
67 | hideResultsNow(); | |
68 | ||
69 | function onChange() { | |
70 | var v = $input.val(); | |
71 | if (v == prev) return; | |
72 | prev = v; | |
73 | if (v.length >= options.minChars) { | |
74 | $input.addClass(options.loadingClass); | |
75 | requestData(v); | |
76 | } else { | |
77 | $input.removeClass(options.loadingClass); | |
78 | $results.hide(); | |
79 | } | |
80 | }; | |
81 | ||
82 | function moveSelect(step) { | |
83 | ||
84 | var lis = $("li", results); | |
85 | if (!lis) return; | |
86 | ||
87 | active += step; | |
88 | ||
89 | if (active < 0) { | |
90 | active = 0; | |
91 | } else if (active >= lis.size()) { | |
92 | active = lis.size() - 1; | |
93 | } | |
94 | ||
95 | lis.removeClass("over"); | |
96 | ||
97 | $(lis[active]).addClass("over"); | |
98 | ||
99 | // Weird behaviour in IE | |
100 | // if (lis[active] && lis[active].scrollIntoView) { | |
101 | // lis[active].scrollIntoView(false); | |
102 | // } | |
103 | ||
104 | }; | |
105 | ||
106 | function selectCurrent() { | |
107 | var li = $("li.over", results)[0]; | |
108 | if (!li) { | |
109 | var $li = $("li", results); | |
110 | if (options.selectOnly) { | |
111 | if ($li.length == 1) li = $li[0]; | |
112 | } else if (options.selectFirst) { | |
113 | li = $li[0]; | |
114 | } | |
115 | } | |
116 | if (li) { | |
117 | selectItem(li); | |
118 | return true; | |
119 | } else { | |
120 | return false; | |
121 | } | |
122 | }; | |
123 | ||
124 | function selectItem(li) { | |
125 | if (!li) { | |
126 | li = document.createElement("li"); | |
127 | li.extra = []; | |
128 | li.selectValue = ""; | |
129 | } | |
130 | var v = $.trim(li.selectValue ? li.selectValue : li.innerHTML); | |
131 | input.lastSelected = v; | |
132 | prev = v; | |
133 | $results.html(""); | |
134 | $input.val(v); | |
135 | hideResultsNow(); | |
136 | if (options.onItemSelect) setTimeout(function() { options.onItemSelect(li) }, 1); | |
137 | }; | |
138 | ||
139 | function hideResults() { | |
140 | if (timeout) clearTimeout(timeout); | |
141 | timeout = setTimeout(hideResultsNow, 200); | |
142 | }; | |
143 | ||
144 | function hideResultsNow() { | |
145 | if (timeout) clearTimeout(timeout); | |
146 | $input.removeClass(options.loadingClass); | |
147 | if ($results.is(":visible")) { | |
148 | $results.hide(); | |
149 | } | |
150 | if (options.mustMatch) { | |
151 | var v = $input.val(); | |
152 | if (v != input.lastSelected) { | |
153 | selectItem(null); | |
154 | } | |
155 | } | |
156 | }; | |
157 | ||
158 | function receiveData(q, data) { | |
159 | if (data) { | |
160 | $input.removeClass(options.loadingClass); | |
161 | results.innerHTML = ""; | |
162 | if ($.browser.msie) { | |
163 | // we put a styled iframe behind the calendar so HTML SELECT elements don't show through | |
164 | $results.append(document.createElement('iframe')); | |
165 | } | |
166 | var datasInDom = dataToDom(data); | |
167 | results.appendChild(datasInDom); | |
168 | $results.show(); | |
169 | // set size of ul smaller (for borders) but almost as big as div | |
170 | $(datasInDom).width($results.width()-2); | |
171 | } else { | |
172 | hideResultsNow(); | |
173 | } | |
174 | }; | |
175 | ||
176 | function parseData(data) { | |
177 | if (!data) return null; | |
178 | var parsed = []; | |
179 | var rows = data.split(options.lineSeparator); | |
180 | for (var i=0; i < rows.length; i++) { | |
181 | var row = $.trim(rows[i]); | |
182 | if (row) { | |
183 | parsed[parsed.length] = row.split(options.cellSeparator); | |
184 | } | |
185 | } | |
186 | return parsed; | |
187 | }; | |
188 | ||
189 | function dataToDom(data) { | |
190 | var ul = document.createElement("ul"); | |
191 | var num = data.length; | |
192 | for (var i=0; i < num; i++) { | |
193 | var row = data[i]; | |
194 | if (!row) continue; | |
195 | var li = document.createElement("li"); | |
196 | if (options.formatItem) { | |
197 | li.innerHTML = options.formatItem(row, i, num); | |
198 | li.selectValue = row[0]; | |
199 | } else { | |
200 | li.innerHTML = row[0]; | |
201 | } | |
202 | var extra = null; | |
203 | if (row.length > 1) { | |
204 | extra = []; | |
205 | for (var j=1; j < row.length; j++) { | |
206 | extra[extra.length] = row[j]; | |
207 | } | |
208 | } | |
209 | li.extra = extra; | |
210 | ul.appendChild(li); | |
211 | $(li).hover( | |
212 | function() { $("li", ul).removeClass("over"); $(this).addClass("over"); }, | |
213 | function() { $(this).removeClass("over"); } | |
214 | ).click(function(e) { e.preventDefault(); e.stopPropagation(); selectItem(this) }); | |
215 | } | |
216 | return ul; | |
217 | }; | |
218 | ||
219 | function requestData(q) { | |
220 | if (!options.matchCase) q = q.toLowerCase(); | |
221 | var data = options.cacheLength ? loadFromCache(q) : null; | |
222 | if (data) { | |
223 | receiveData(q, data); | |
224 | } else { | |
225 | $.get(makeUrl(q), function(data) { | |
226 | data = parseData(data) | |
227 | addToCache(q, data); | |
228 | receiveData(q, data); | |
229 | }); | |
230 | } | |
231 | }; | |
232 | ||
233 | function makeUrl(q) { | |
234 | var url = options.url + "?q=" + q; | |
235 | for (var i in options.extraParams) { | |
236 | url += "&" + i + "=" + options.extraParams[i]; | |
237 | } | |
238 | return url; | |
239 | }; | |
240 | ||
241 | function loadFromCache(q) { | |
242 | if (!q) return null; | |
243 | if (cache[q]) return cache[q]; | |
244 | if (options.matchSubset) { | |
245 | for (var i = q.length - 1; i >= options.minChars; i--) { | |
246 | var qs = q.substr(0, i); | |
247 | var c = cache[qs]; | |
248 | if (c) { | |
249 | var csub = []; | |
250 | for (var j = 0; j < c.length; j++) { | |
251 | var x = c[j]; | |
252 | var x0 = x[0]; | |
253 | if (matchSubset(x0, q)) { | |
254 | csub[csub.length] = x; | |
255 | } | |
256 | } | |
257 | return csub; | |
258 | } | |
259 | } | |
260 | } | |
261 | return null; | |
262 | }; | |
263 | ||
264 | function matchSubset(s, sub) { | |
265 | if (!options.matchCase) s = s.toLowerCase(); | |
266 | var i = s.indexOf(sub); | |
267 | if (i == -1) return false; | |
268 | return i == 0 || options.matchContains; | |
269 | }; | |
270 | ||
271 | this.flushCache = function() { | |
272 | cache = {}; | |
273 | }; | |
274 | ||
275 | this.setExtraParams = function(p) { | |
276 | options.extraParams = p; | |
277 | }; | |
278 | ||
279 | function addToCache(q, data) { | |
280 | if (!data || !q || !options.cacheLength) return; | |
281 | if (!cache.length || cache.length > options.cacheLength) { | |
282 | cache = {}; | |
283 | cache.length = 1; // we know we're adding something | |
284 | } else if (!cache[q]) { | |
285 | cache.length++; | |
286 | } | |
287 | cache[q] = data; | |
288 | }; | |
289 | ||
290 | function findPos(obj) { | |
291 | var curleft = obj.offsetLeft || 0; | |
292 | var curtop = obj.offsetTop || 0; | |
293 | while (obj = obj.offsetParent) { | |
294 | curleft += obj.offsetLeft | |
295 | curtop += obj.offsetTop | |
296 | } | |
297 | return {x:curleft,y:curtop}; | |
298 | } | |
299 | } | |
300 | ||
301 | $.fn.autocomplete = function(url, options) { | |
302 | // Make sure options exists | |
303 | options = options || {}; | |
304 | // Set url as option | |
305 | options.url = url; | |
306 | // Set default values for required options | |
307 | options.inputClass = options.inputClass || "ac_input"; | |
308 | options.resultsClass = options.resultsClass || "ac_results"; | |
309 | options.lineSeparator = options.lineSeparator || "\n"; | |
310 | options.cellSeparator = options.cellSeparator || "|"; | |
311 | options.minChars = options.minChars || 1; | |
312 | options.delay = options.delay || 400; | |
313 | options.matchCase = options.matchCase || 0; | |
314 | options.matchSubset = typeof(options.matchSubset) != 'undefined' ? options.matchSubset : 1; | |
315 | options.matchContains = options.matchContains || 0; | |
316 | options.cacheLength = options.cacheLength || 1; | |
317 | options.mustMatch = options.mustMatch || 0; | |
318 | options.extraParams = options.extraParams || {}; | |
319 | options.loadingClass = options.loadingClass || "ac_loading"; | |
320 | options.selectFirst = options.selectFirst || false; | |
321 | options.selectOnly = options.selectOnly || false; | |
322 | ||
323 | this.each(function() { | |
324 | var input = this; | |
325 | new $.autocomplete(input, options); | |
326 | }); | |
327 | ||
328 | // Don't break the chain | |
329 | return this; | |
330 | } |