]>
Commit | Line | Data |
---|---|---|
bdd1715b A |
1 | /** |
2 | * Simple, lightweight, usable local autocomplete library for modern browsers | |
3 | * Because there weren’t enough autocomplete scripts in the world? Because I’m completely insane and have NIH syndrome? Probably both. :P | |
4 | * @author Lea Verou http://leaverou.github.io/awesomplete | |
5 | * MIT license | |
6 | */ | |
7 | ||
8 | (function () { | |
9 | ||
10 | var _ = function (input, o) { | |
11 | var me = this; | |
12 | ||
13 | // Setup | |
14 | ||
430ff071 A |
15 | this.isOpened = false; |
16 | ||
bdd1715b | 17 | this.input = $(input); |
430ff071 | 18 | this.input.setAttribute("autocomplete", "off"); |
bdd1715b A |
19 | this.input.setAttribute("aria-autocomplete", "list"); |
20 | ||
21 | o = o || {}; | |
22 | ||
430ff071 | 23 | configure(this, { |
bdd1715b A |
24 | minChars: 2, |
25 | maxItems: 10, | |
26 | autoFirst: false, | |
430ff071 | 27 | data: _.DATA, |
bdd1715b A |
28 | filter: _.FILTER_CONTAINS, |
29 | sort: _.SORT_BYLENGTH, | |
430ff071 A |
30 | item: _.ITEM, |
31 | replace: _.REPLACE | |
bdd1715b A |
32 | }, o); |
33 | ||
34 | this.index = -1; | |
35 | ||
36 | // Create necessary elements | |
37 | ||
38 | this.container = $.create("div", { | |
39 | className: "awesomplete", | |
40 | around: input | |
41 | }); | |
42 | ||
43 | this.ul = $.create("ul", { | |
430ff071 | 44 | hidden: "hidden", |
bdd1715b A |
45 | inside: this.container |
46 | }); | |
47 | ||
48 | this.status = $.create("span", { | |
49 | className: "visually-hidden", | |
50 | role: "status", | |
51 | "aria-live": "assertive", | |
52 | "aria-relevant": "additions", | |
53 | inside: this.container | |
54 | }); | |
55 | ||
56 | // Bind events | |
57 | ||
58 | $.bind(this.input, { | |
59 | "input": this.evaluate.bind(this), | |
430ff071 | 60 | "blur": this.close.bind(this, { reason: "blur" }), |
bdd1715b A |
61 | "keydown": function(evt) { |
62 | var c = evt.keyCode; | |
63 | ||
64 | // If the dropdown `ul` is in view, then act on keydown for the following keys: | |
65 | // Enter / Esc / Up / Down | |
66 | if(me.opened) { | |
67 | if (c === 13 && me.selected) { // Enter | |
68 | evt.preventDefault(); | |
69 | me.select(); | |
70 | } | |
71 | else if (c === 27) { // Esc | |
430ff071 | 72 | me.close({ reason: "esc" }); |
bdd1715b A |
73 | } |
74 | else if (c === 38 || c === 40) { // Down/Up arrow | |
75 | evt.preventDefault(); | |
76 | me[c === 38? "previous" : "next"](); | |
77 | } | |
78 | } | |
79 | } | |
80 | }); | |
81 | ||
430ff071 | 82 | $.bind(this.input.form, {"submit": this.close.bind(this, { reason: "submit" })}); |
bdd1715b A |
83 | |
84 | $.bind(this.ul, {"mousedown": function(evt) { | |
85 | var li = evt.target; | |
86 | ||
87 | if (li !== this) { | |
88 | ||
89 | while (li && !/li/i.test(li.nodeName)) { | |
90 | li = li.parentNode; | |
91 | } | |
92 | ||
430ff071 A |
93 | if (li && evt.button === 0) { // Only select on left click |
94 | evt.preventDefault(); | |
95 | me.select(li, evt.target); | |
bdd1715b A |
96 | } |
97 | } | |
98 | }}); | |
99 | ||
100 | if (this.input.hasAttribute("list")) { | |
430ff071 A |
101 | this.list = "#" + this.input.getAttribute("list"); |
102 | this.input.removeAttribute("list"); | |
bdd1715b A |
103 | } |
104 | else { | |
105 | this.list = this.input.getAttribute("data-list") || o.list || []; | |
106 | } | |
107 | ||
108 | _.all.push(this); | |
109 | }; | |
110 | ||
111 | _.prototype = { | |
112 | set list(list) { | |
113 | if (Array.isArray(list)) { | |
114 | this._list = list; | |
115 | } | |
116 | else if (typeof list === "string" && list.indexOf(",") > -1) { | |
117 | this._list = list.split(/\s*,\s*/); | |
118 | } | |
119 | else { // Element or CSS selector | |
120 | list = $(list); | |
121 | ||
122 | if (list && list.children) { | |
430ff071 A |
123 | var items = []; |
124 | slice.apply(list.children).forEach(function (el) { | |
125 | if (!el.disabled) { | |
126 | var text = el.textContent.trim(); | |
127 | var value = el.value || text; | |
128 | var label = el.label || text; | |
129 | if (value !== "") { | |
130 | items.push({ label: label, value: value }); | |
131 | } | |
132 | } | |
bdd1715b | 133 | }); |
430ff071 | 134 | this._list = items; |
bdd1715b A |
135 | } |
136 | } | |
137 | ||
138 | if (document.activeElement === this.input) { | |
139 | this.evaluate(); | |
140 | } | |
141 | }, | |
142 | ||
143 | get selected() { | |
144 | return this.index > -1; | |
145 | }, | |
146 | ||
147 | get opened() { | |
430ff071 | 148 | return this.isOpened; |
bdd1715b A |
149 | }, |
150 | ||
430ff071 A |
151 | close: function (o) { |
152 | if (!this.opened) { | |
153 | return; | |
154 | } | |
155 | ||
bdd1715b | 156 | this.ul.setAttribute("hidden", ""); |
430ff071 | 157 | this.isOpened = false; |
bdd1715b A |
158 | this.index = -1; |
159 | ||
430ff071 | 160 | $.fire(this.input, "awesomplete-close", o || {}); |
bdd1715b A |
161 | }, |
162 | ||
163 | open: function () { | |
164 | this.ul.removeAttribute("hidden"); | |
430ff071 | 165 | this.isOpened = true; |
bdd1715b A |
166 | |
167 | if (this.autoFirst && this.index === -1) { | |
168 | this.goto(0); | |
169 | } | |
170 | ||
171 | $.fire(this.input, "awesomplete-open"); | |
172 | }, | |
173 | ||
174 | next: function () { | |
175 | var count = this.ul.children.length; | |
430ff071 | 176 | this.goto(this.index < count - 1 ? this.index + 1 : (count ? 0 : -1) ); |
bdd1715b A |
177 | }, |
178 | ||
179 | previous: function () { | |
180 | var count = this.ul.children.length; | |
430ff071 | 181 | var pos = this.index - 1; |
bdd1715b | 182 | |
430ff071 | 183 | this.goto(this.selected && pos !== -1 ? pos : count - 1); |
bdd1715b A |
184 | }, |
185 | ||
186 | // Should not be used, highlights specific item without any checks! | |
187 | goto: function (i) { | |
188 | var lis = this.ul.children; | |
189 | ||
190 | if (this.selected) { | |
191 | lis[this.index].setAttribute("aria-selected", "false"); | |
192 | } | |
193 | ||
194 | this.index = i; | |
195 | ||
196 | if (i > -1 && lis.length > 0) { | |
197 | lis[i].setAttribute("aria-selected", "true"); | |
198 | this.status.textContent = lis[i].textContent; | |
bdd1715b | 199 | |
430ff071 A |
200 | // scroll to highlighted element in case parent's height is fixed |
201 | this.ul.scrollTop = lis[i].offsetTop - this.ul.clientHeight + lis[i].clientHeight; | |
202 | ||
203 | $.fire(this.input, "awesomplete-highlight", { | |
204 | text: this.suggestions[this.index] | |
205 | }); | |
206 | } | |
bdd1715b A |
207 | }, |
208 | ||
430ff071 A |
209 | select: function (selected, origin) { |
210 | if (selected) { | |
211 | this.index = $.siblingIndex(selected); | |
212 | } else { | |
213 | selected = this.ul.children[this.index]; | |
214 | } | |
bdd1715b A |
215 | |
216 | if (selected) { | |
430ff071 | 217 | var suggestion = this.suggestions[this.index]; |
bdd1715b | 218 | |
430ff071 A |
219 | var allowed = $.fire(this.input, "awesomplete-select", { |
220 | text: suggestion, | |
221 | origin: origin || selected | |
bdd1715b A |
222 | }); |
223 | ||
430ff071 A |
224 | if (allowed) { |
225 | this.replace(suggestion); | |
226 | this.close({ reason: "select" }); | |
227 | $.fire(this.input, "awesomplete-selectcomplete", { | |
228 | text: suggestion | |
229 | }); | |
bdd1715b A |
230 | } |
231 | } | |
232 | }, | |
233 | ||
234 | evaluate: function() { | |
235 | var me = this; | |
236 | var value = this.input.value; | |
237 | ||
238 | if (value.length >= this.minChars && this._list.length > 0) { | |
239 | this.index = -1; | |
240 | // Populate list with options that match | |
241 | this.ul.innerHTML = ""; | |
242 | ||
430ff071 A |
243 | this.suggestions = this._list |
244 | .map(function(item) { | |
245 | return new Suggestion(me.data(item, value)); | |
246 | }) | |
bdd1715b A |
247 | .filter(function(item) { |
248 | return me.filter(item, value); | |
249 | }) | |
250 | .sort(this.sort) | |
430ff071 | 251 | .slice(0, this.maxItems); |
bdd1715b | 252 | |
430ff071 A |
253 | this.suggestions.forEach(function(text) { |
254 | me.ul.appendChild(me.item(text, value)); | |
255 | }); | |
bdd1715b A |
256 | |
257 | if (this.ul.children.length === 0) { | |
430ff071 | 258 | this.close({ reason: "nomatches" }); |
bdd1715b A |
259 | } else { |
260 | this.open(); | |
261 | } | |
262 | } | |
263 | else { | |
430ff071 | 264 | this.close({ reason: "nomatches" }); |
bdd1715b A |
265 | } |
266 | } | |
267 | }; | |
268 | ||
269 | // Static methods/properties | |
270 | ||
271 | _.all = []; | |
272 | ||
273 | _.FILTER_CONTAINS = function (text, input) { | |
274 | return RegExp($.regExpEscape(input.trim()), "i").test(text); | |
275 | }; | |
276 | ||
277 | _.FILTER_STARTSWITH = function (text, input) { | |
278 | return RegExp("^" + $.regExpEscape(input.trim()), "i").test(text); | |
279 | }; | |
280 | ||
281 | _.SORT_BYLENGTH = function (a, b) { | |
282 | if (a.length !== b.length) { | |
283 | return a.length - b.length; | |
284 | } | |
285 | ||
286 | return a < b? -1 : 1; | |
287 | }; | |
288 | ||
430ff071 A |
289 | _.ITEM = function (text, input) { |
290 | var html = input.trim() === '' ? text : text.replace(RegExp($.regExpEscape(input.trim()), "gi"), "<mark>$&</mark>"); | |
291 | return $.create("li", { | |
292 | innerHTML: html, | |
293 | "aria-selected": "false" | |
294 | }); | |
295 | }; | |
296 | ||
297 | _.REPLACE = function (text) { | |
298 | this.input.value = text.value; | |
299 | }; | |
300 | ||
301 | _.DATA = function (item/*, input*/) { return item; }; | |
302 | ||
bdd1715b A |
303 | // Private functions |
304 | ||
430ff071 A |
305 | function Suggestion(data) { |
306 | var o = Array.isArray(data) | |
307 | ? { label: data[0], value: data[1] } | |
308 | : typeof data === "object" && "label" in data && "value" in data ? data : { label: data, value: data }; | |
309 | ||
310 | this.label = o.label || o.value; | |
311 | this.value = o.value; | |
312 | } | |
313 | Object.defineProperty(Suggestion.prototype = Object.create(String.prototype), "length", { | |
314 | get: function() { return this.label.length; } | |
315 | }); | |
316 | Suggestion.prototype.toString = Suggestion.prototype.valueOf = function () { | |
317 | return "" + this.label; | |
318 | }; | |
319 | ||
320 | function configure(instance, properties, o) { | |
bdd1715b A |
321 | for (var i in properties) { |
322 | var initial = properties[i], | |
430ff071 | 323 | attrValue = instance.input.getAttribute("data-" + i.toLowerCase()); |
bdd1715b A |
324 | |
325 | if (typeof initial === "number") { | |
430ff071 | 326 | instance[i] = parseInt(attrValue); |
bdd1715b A |
327 | } |
328 | else if (initial === false) { // Boolean options must be false by default anyway | |
430ff071 | 329 | instance[i] = attrValue !== null; |
bdd1715b A |
330 | } |
331 | else if (initial instanceof Function) { | |
430ff071 | 332 | instance[i] = null; |
bdd1715b A |
333 | } |
334 | else { | |
430ff071 | 335 | instance[i] = attrValue; |
bdd1715b A |
336 | } |
337 | ||
430ff071 A |
338 | if (!instance[i] && instance[i] !== 0) { |
339 | instance[i] = (i in o)? o[i] : initial; | |
340 | } | |
bdd1715b A |
341 | } |
342 | } | |
343 | ||
344 | // Helpers | |
345 | ||
346 | var slice = Array.prototype.slice; | |
347 | ||
348 | function $(expr, con) { | |
349 | return typeof expr === "string"? (con || document).querySelector(expr) : expr || null; | |
350 | } | |
351 | ||
352 | function $$(expr, con) { | |
353 | return slice.call((con || document).querySelectorAll(expr)); | |
354 | } | |
355 | ||
356 | $.create = function(tag, o) { | |
357 | var element = document.createElement(tag); | |
358 | ||
359 | for (var i in o) { | |
360 | var val = o[i]; | |
361 | ||
362 | if (i === "inside") { | |
363 | $(val).appendChild(element); | |
364 | } | |
365 | else if (i === "around") { | |
366 | var ref = $(val); | |
367 | ref.parentNode.insertBefore(element, ref); | |
368 | element.appendChild(ref); | |
369 | } | |
370 | else if (i in element) { | |
371 | element[i] = val; | |
372 | } | |
373 | else { | |
374 | element.setAttribute(i, val); | |
375 | } | |
376 | } | |
377 | ||
378 | return element; | |
379 | }; | |
380 | ||
381 | $.bind = function(element, o) { | |
382 | if (element) { | |
383 | for (var event in o) { | |
384 | var callback = o[event]; | |
385 | ||
386 | event.split(/\s+/).forEach(function (event) { | |
387 | element.addEventListener(event, callback); | |
388 | }); | |
389 | } | |
390 | } | |
391 | }; | |
392 | ||
393 | $.fire = function(target, type, properties) { | |
394 | var evt = document.createEvent("HTMLEvents"); | |
395 | ||
396 | evt.initEvent(type, true, true ); | |
397 | ||
398 | for (var j in properties) { | |
399 | evt[j] = properties[j]; | |
400 | } | |
401 | ||
430ff071 | 402 | return target.dispatchEvent(evt); |
bdd1715b A |
403 | }; |
404 | ||
405 | $.regExpEscape = function (s) { | |
406 | return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); | |
430ff071 A |
407 | }; |
408 | ||
409 | $.siblingIndex = function (el) { | |
410 | /* eslint-disable no-cond-assign */ | |
411 | for (var i = 0; el = el.previousElementSibling; i++); | |
412 | return i; | |
413 | }; | |
bdd1715b A |
414 | |
415 | // Initialization | |
416 | ||
417 | function init() { | |
418 | $$("input.awesomplete").forEach(function (input) { | |
430ff071 | 419 | new _(input); |
bdd1715b A |
420 | }); |
421 | } | |
422 | ||
423 | // Are we in a browser? Check for Document constructor | |
430ff071 | 424 | if (typeof Document !== "undefined") { |
bdd1715b A |
425 | // DOM already loaded? |
426 | if (document.readyState !== "loading") { | |
427 | init(); | |
428 | } | |
429 | else { | |
430 | // Wait for it | |
431 | document.addEventListener("DOMContentLoaded", init); | |
432 | } | |
433 | } | |
434 | ||
435 | _.$ = $; | |
436 | _.$$ = $$; | |
437 | ||
438 | // Make sure to export Awesomplete on self when in a browser | |
430ff071 | 439 | if (typeof self !== "undefined") { |
bdd1715b A |
440 | self.Awesomplete = _; |
441 | } | |
442 | ||
443 | // Expose Awesomplete as a CJS module | |
430ff071 | 444 | if (typeof module === "object" && module.exports) { |
bdd1715b A |
445 | module.exports = _; |
446 | } | |
447 | ||
448 | return _; | |
449 | ||
430ff071 | 450 | }()); |