]>
Commit | Line | Data |
---|---|---|
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 | ||
15 | this.input = $(input); | |
16 | this.input.setAttribute("aria-autocomplete", "list"); | |
17 | ||
18 | o = o || {}; | |
19 | ||
20 | configure.call(this, { | |
21 | minChars: 2, | |
22 | maxItems: 10, | |
23 | autoFirst: false, | |
24 | filter: _.FILTER_CONTAINS, | |
25 | sort: _.SORT_BYLENGTH, | |
26 | item: function (text, input) { | |
27 | return $.create("li", { | |
28 | innerHTML: text.replace(RegExp($.regExpEscape(input.trim()), "gi"), "<mark>$&</mark>"), | |
29 | "aria-selected": "false" | |
30 | }); | |
31 | }, | |
32 | replace: function (text) { | |
33 | this.input.value = text; | |
34 | } | |
35 | }, o); | |
36 | ||
37 | this.index = -1; | |
38 | ||
39 | // Create necessary elements | |
40 | ||
41 | this.container = $.create("div", { | |
42 | className: "awesomplete", | |
43 | around: input | |
44 | }); | |
45 | ||
46 | this.ul = $.create("ul", { | |
47 | hidden: "", | |
48 | inside: this.container | |
49 | }); | |
50 | ||
51 | this.status = $.create("span", { | |
52 | className: "visually-hidden", | |
53 | role: "status", | |
54 | "aria-live": "assertive", | |
55 | "aria-relevant": "additions", | |
56 | inside: this.container | |
57 | }); | |
58 | ||
59 | // Bind events | |
60 | ||
61 | $.bind(this.input, { | |
62 | "input": this.evaluate.bind(this), | |
63 | "blur": this.close.bind(this), | |
64 | "keydown": function(evt) { | |
65 | var c = evt.keyCode; | |
66 | ||
67 | // If the dropdown `ul` is in view, then act on keydown for the following keys: | |
68 | // Enter / Esc / Up / Down | |
69 | if(me.opened) { | |
70 | if (c === 13 && me.selected) { // Enter | |
71 | evt.preventDefault(); | |
72 | me.select(); | |
73 | } | |
74 | else if (c === 27) { // Esc | |
75 | me.close(); | |
76 | } | |
77 | else if (c === 38 || c === 40) { // Down/Up arrow | |
78 | evt.preventDefault(); | |
79 | me[c === 38? "previous" : "next"](); | |
80 | } | |
81 | } | |
82 | } | |
83 | }); | |
84 | ||
85 | $.bind(this.input.form, {"submit": this.close.bind(this)}); | |
86 | ||
87 | $.bind(this.ul, {"mousedown": function(evt) { | |
88 | var li = evt.target; | |
89 | ||
90 | if (li !== this) { | |
91 | ||
92 | while (li && !/li/i.test(li.nodeName)) { | |
93 | li = li.parentNode; | |
94 | } | |
95 | ||
96 | if (li) { | |
97 | me.select(li); | |
98 | } | |
99 | } | |
100 | }}); | |
101 | ||
102 | if (this.input.hasAttribute("list")) { | |
103 | this.list = "#" + input.getAttribute("list"); | |
104 | input.removeAttribute("list"); | |
105 | } | |
106 | else { | |
107 | this.list = this.input.getAttribute("data-list") || o.list || []; | |
108 | } | |
109 | ||
110 | _.all.push(this); | |
111 | }; | |
112 | ||
113 | _.prototype = { | |
114 | set list(list) { | |
115 | if (Array.isArray(list)) { | |
116 | this._list = list; | |
117 | } | |
118 | else if (typeof list === "string" && list.indexOf(",") > -1) { | |
119 | this._list = list.split(/\s*,\s*/); | |
120 | } | |
121 | else { // Element or CSS selector | |
122 | list = $(list); | |
123 | ||
124 | if (list && list.children) { | |
125 | this._list = slice.apply(list.children).map(function (el) { | |
126 | return el.innerHTML.trim(); | |
127 | }); | |
128 | } | |
129 | } | |
130 | ||
131 | if (document.activeElement === this.input) { | |
132 | this.evaluate(); | |
133 | } | |
134 | }, | |
135 | ||
136 | get selected() { | |
137 | return this.index > -1; | |
138 | }, | |
139 | ||
140 | get opened() { | |
141 | return this.ul && this.ul.getAttribute("hidden") == null; | |
142 | }, | |
143 | ||
144 | close: function () { | |
145 | this.ul.setAttribute("hidden", ""); | |
146 | this.index = -1; | |
147 | ||
148 | $.fire(this.input, "awesomplete-close"); | |
149 | }, | |
150 | ||
151 | open: function () { | |
152 | this.ul.removeAttribute("hidden"); | |
153 | ||
154 | if (this.autoFirst && this.index === -1) { | |
155 | this.goto(0); | |
156 | } | |
157 | ||
158 | $.fire(this.input, "awesomplete-open"); | |
159 | }, | |
160 | ||
161 | next: function () { | |
162 | var count = this.ul.children.length; | |
163 | ||
164 | this.goto(this.index < count - 1? this.index + 1 : -1); | |
165 | }, | |
166 | ||
167 | previous: function () { | |
168 | var count = this.ul.children.length; | |
169 | ||
170 | this.goto(this.selected? this.index - 1 : count - 1); | |
171 | }, | |
172 | ||
173 | // Should not be used, highlights specific item without any checks! | |
174 | goto: function (i) { | |
175 | var lis = this.ul.children; | |
176 | ||
177 | if (this.selected) { | |
178 | lis[this.index].setAttribute("aria-selected", "false"); | |
179 | } | |
180 | ||
181 | this.index = i; | |
182 | ||
183 | if (i > -1 && lis.length > 0) { | |
184 | lis[i].setAttribute("aria-selected", "true"); | |
185 | this.status.textContent = lis[i].textContent; | |
186 | } | |
187 | ||
188 | $.fire(this.input, "awesomplete-highlight"); | |
189 | }, | |
190 | ||
191 | select: function (selected) { | |
192 | selected = selected || this.ul.children[this.index]; | |
193 | ||
194 | if (selected) { | |
195 | var prevented; | |
196 | ||
197 | $.fire(this.input, "awesomplete-select", { | |
198 | text: selected.textContent, | |
199 | preventDefault: function () { | |
200 | prevented = true; | |
201 | } | |
202 | }); | |
203 | ||
204 | if (!prevented) { | |
205 | this.replace(selected.textContent); | |
206 | this.close(); | |
207 | $.fire(this.input, "awesomplete-selectcomplete"); | |
208 | } | |
209 | } | |
210 | }, | |
211 | ||
212 | evaluate: function() { | |
213 | var me = this; | |
214 | var value = this.input.value; | |
215 | ||
216 | if (value.length >= this.minChars && this._list.length > 0) { | |
217 | this.index = -1; | |
218 | // Populate list with options that match | |
219 | this.ul.innerHTML = ""; | |
220 | ||
221 | this._list | |
222 | .filter(function(item) { | |
223 | return me.filter(item, value); | |
224 | }) | |
225 | .sort(this.sort) | |
226 | .every(function(text, i) { | |
227 | me.ul.appendChild(me.item(text, value)); | |
228 | ||
229 | return i < me.maxItems - 1; | |
230 | }); | |
231 | ||
232 | if (this.ul.children.length === 0) { | |
233 | this.close(); | |
234 | } else { | |
235 | this.open(); | |
236 | } | |
237 | } | |
238 | else { | |
239 | this.close(); | |
240 | } | |
241 | } | |
242 | }; | |
243 | ||
244 | // Static methods/properties | |
245 | ||
246 | _.all = []; | |
247 | ||
248 | _.FILTER_CONTAINS = function (text, input) { | |
249 | return RegExp($.regExpEscape(input.trim()), "i").test(text); | |
250 | }; | |
251 | ||
252 | _.FILTER_STARTSWITH = function (text, input) { | |
253 | return RegExp("^" + $.regExpEscape(input.trim()), "i").test(text); | |
254 | }; | |
255 | ||
256 | _.SORT_BYLENGTH = function (a, b) { | |
257 | if (a.length !== b.length) { | |
258 | return a.length - b.length; | |
259 | } | |
260 | ||
261 | return a < b? -1 : 1; | |
262 | }; | |
263 | ||
264 | // Private functions | |
265 | ||
266 | function configure(properties, o) { | |
267 | for (var i in properties) { | |
268 | var initial = properties[i], | |
269 | attrValue = this.input.getAttribute("data-" + i.toLowerCase()); | |
270 | ||
271 | if (typeof initial === "number") { | |
272 | this[i] = +attrValue; | |
273 | } | |
274 | else if (initial === false) { // Boolean options must be false by default anyway | |
275 | this[i] = attrValue !== null; | |
276 | } | |
277 | else if (initial instanceof Function) { | |
278 | this[i] = null; | |
279 | } | |
280 | else { | |
281 | this[i] = attrValue; | |
282 | } | |
283 | ||
284 | this[i] = this[i] || o[i] || initial; | |
285 | } | |
286 | } | |
287 | ||
288 | // Helpers | |
289 | ||
290 | var slice = Array.prototype.slice; | |
291 | ||
292 | function $(expr, con) { | |
293 | return typeof expr === "string"? (con || document).querySelector(expr) : expr || null; | |
294 | } | |
295 | ||
296 | function $$(expr, con) { | |
297 | return slice.call((con || document).querySelectorAll(expr)); | |
298 | } | |
299 | ||
300 | $.create = function(tag, o) { | |
301 | var element = document.createElement(tag); | |
302 | ||
303 | for (var i in o) { | |
304 | var val = o[i]; | |
305 | ||
306 | if (i === "inside") { | |
307 | $(val).appendChild(element); | |
308 | } | |
309 | else if (i === "around") { | |
310 | var ref = $(val); | |
311 | ref.parentNode.insertBefore(element, ref); | |
312 | element.appendChild(ref); | |
313 | } | |
314 | else if (i in element) { | |
315 | element[i] = val; | |
316 | } | |
317 | else { | |
318 | element.setAttribute(i, val); | |
319 | } | |
320 | } | |
321 | ||
322 | return element; | |
323 | }; | |
324 | ||
325 | $.bind = function(element, o) { | |
326 | if (element) { | |
327 | for (var event in o) { | |
328 | var callback = o[event]; | |
329 | ||
330 | event.split(/\s+/).forEach(function (event) { | |
331 | element.addEventListener(event, callback); | |
332 | }); | |
333 | } | |
334 | } | |
335 | }; | |
336 | ||
337 | $.fire = function(target, type, properties) { | |
338 | var evt = document.createEvent("HTMLEvents"); | |
339 | ||
340 | evt.initEvent(type, true, true ); | |
341 | ||
342 | for (var j in properties) { | |
343 | evt[j] = properties[j]; | |
344 | } | |
345 | ||
346 | target.dispatchEvent(evt); | |
347 | }; | |
348 | ||
349 | $.regExpEscape = function (s) { | |
350 | return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); | |
351 | } | |
352 | ||
353 | // Initialization | |
354 | ||
355 | function init() { | |
356 | $$("input.awesomplete").forEach(function (input) { | |
357 | new Awesomplete(input); | |
358 | }); | |
359 | } | |
360 | ||
361 | // Are we in a browser? Check for Document constructor | |
362 | if (typeof Document !== 'undefined') { | |
363 | // DOM already loaded? | |
364 | if (document.readyState !== "loading") { | |
365 | init(); | |
366 | } | |
367 | else { | |
368 | // Wait for it | |
369 | document.addEventListener("DOMContentLoaded", init); | |
370 | } | |
371 | } | |
372 | ||
373 | _.$ = $; | |
374 | _.$$ = $$; | |
375 | ||
376 | // Make sure to export Awesomplete on self when in a browser | |
377 | if (typeof self !== 'undefined') { | |
378 | self.Awesomplete = _; | |
379 | } | |
380 | ||
381 | // Expose Awesomplete as a CJS module | |
382 | if (typeof exports === 'object') { | |
383 | module.exports = _; | |
384 | } | |
385 | ||
386 | return _; | |
387 | ||
388 | }()); |