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