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
10 var _ = function (input
, o
) {
13 // Keep track of number of instances for unique IDs
14 Awesomplete
.count
= (Awesomplete
.count
|| 0) + 1;
15 this.count
= Awesomplete
.count
;
19 this.isOpened
= false;
21 this.input
= $(input
);
22 this.input
.setAttribute("autocomplete", "off");
23 this.input
.setAttribute("aria-owns", "awesomplete_list_" + this.count
);
24 this.input
.setAttribute("role", "combobox");
33 filter
: _
.FILTER_CONTAINS
,
34 sort
: o
.sort
=== false ? false : _
.SORT_BYLENGTH
,
41 // Create necessary elements
43 this.container
= $.create("div", {
44 className
: "awesomplete",
48 this.ul
= $.create("ul", {
51 id
: "awesomplete_list_" + this.count
,
52 inside
: this.container
55 this.status
= $.create("span", {
56 className
: "visually-hidden",
58 "aria-live": "assertive",
60 inside
: this.container
,
61 textContent
: this.minChars
!= 0 ? ("Type " + this.minChars
+ " or more characters for results.") : "Begin typing for results."
68 "input": this.evaluate
.bind(this),
69 "blur": this.close
.bind(this, { reason
: "blur" }),
70 "keydown": function(evt
) {
73 // If the dropdown `ul` is in view, then act on keydown for the following keys:
74 // Enter / Esc / Up / Down
76 if (c
=== 13 && me
.selected
) { // Enter
80 else if (c
=== 27) { // Esc
81 me
.close({ reason
: "esc" });
83 else if (c
=== 38 || c
=== 40) { // Down/Up arrow
85 me
[c
=== 38? "previous" : "next"]();
91 "submit": this.close
.bind(this, { reason
: "submit" })
94 "mousedown": function(evt
) {
99 while (li
&& !/li/i.test(li
.nodeName
)) {
103 if (li
&& evt
.button
=== 0) { // Only select on left click
104 evt
.preventDefault();
105 me
.select(li
, evt
.target
);
112 $.bind(this.input
, this._events
.input
);
113 $.bind(this.input
.form
, this._events
.form
);
114 $.bind(this.ul
, this._events
.ul
);
116 if (this.input
.hasAttribute("list")) {
117 this.list
= "#" + this.input
.getAttribute("list");
118 this.input
.removeAttribute("list");
121 this.list
= this.input
.getAttribute("data-list") || o
.list
|| [];
129 if (Array
.isArray(list
)) {
132 else if (typeof list
=== "string" && list
.indexOf(",") > -1) {
133 this._list
= list
.split(/\s*,\s*/);
135 else { // Element or CSS selector
138 if (list
&& list
.children
) {
140 slice
.apply(list
.children
).forEach(function (el
) {
142 var text
= el
.textContent
.trim();
143 var value
= el
.value
|| text
;
144 var label
= el
.label
|| text
;
146 items
.push({ label
: label
, value
: value
});
154 if (document
.activeElement
=== this.input
) {
160 return this.index
> -1;
164 return this.isOpened
;
167 close: function (o
) {
172 this.ul
.setAttribute("hidden", "");
173 this.isOpened
= false;
176 $.fire(this.input
, "awesomplete-close", o
|| {});
180 this.ul
.removeAttribute("hidden");
181 this.isOpened
= true;
183 if (this.autoFirst
&& this.index
=== -1) {
187 $.fire(this.input
, "awesomplete-open");
190 destroy: function() {
191 //remove events from the input and its form
192 $.unbind(this.input
, this._events
.input
);
193 $.unbind(this.input
.form
, this._events
.form
);
195 //move the input out of the awesomplete container and remove the container and its children
196 var parentNode
= this.container
.parentNode
;
198 parentNode
.insertBefore(this.input
, this.container
);
199 parentNode
.removeChild(this.container
);
201 //remove autocomplete and aria-autocomplete attributes
202 this.input
.removeAttribute("autocomplete");
203 this.input
.removeAttribute("aria-autocomplete");
205 //remove this awesomeplete instance from the global array of instances
206 var indexOfAwesomplete
= _
.all
.indexOf(this);
208 if (indexOfAwesomplete
!== -1) {
209 _
.all
.splice(indexOfAwesomplete
, 1);
214 var count
= this.ul
.children
.length
;
215 this.goto(this.index
< count
- 1 ? this.index
+ 1 : (count
? 0 : -1) );
218 previous: function () {
219 var count
= this.ul
.children
.length
;
220 var pos
= this.index
- 1;
222 this.goto(this.selected
&& pos
!== -1 ? pos
: count
- 1);
225 // Should not be used, highlights specific item without any checks!
227 var lis
= this.ul
.children
;
230 lis
[this.index
].setAttribute("aria-selected", "false");
235 if (i
> -1 && lis
.length
> 0) {
236 lis
[i
].setAttribute("aria-selected", "true");
238 this.status
.textContent
= lis
[i
].textContent
+ ", list item " + (i
+ 1) + " of " + lis
.length
;
240 this.input
.setAttribute("aria-activedescendant", this.ul
.id
+ "_item_" + this.index
);
242 // scroll to highlighted element in case parent's height is fixed
243 this.ul
.scrollTop
= lis
[i
].offsetTop
- this.ul
.clientHeight
+ lis
[i
].clientHeight
;
245 $.fire(this.input
, "awesomplete-highlight", {
246 text
: this.suggestions
[this.index
]
251 select: function (selected
, origin
) {
253 this.index
= $.siblingIndex(selected
);
255 selected
= this.ul
.children
[this.index
];
259 var suggestion
= this.suggestions
[this.index
];
261 var allowed
= $.fire(this.input
, "awesomplete-select", {
263 origin
: origin
|| selected
267 this.replace(suggestion
);
268 this.close({ reason
: "select" });
269 $.fire(this.input
, "awesomplete-selectcomplete", {
276 evaluate: function() {
278 var value
= this.input
.value
;
280 if (value
.length
>= this.minChars
&& this._list
.length
> 0) {
282 // Populate list with options that match
283 this.ul
.innerHTML
= "";
285 this.suggestions
= this._list
286 .map(function(item
) {
287 return new Suggestion(me
.data(item
, value
));
289 .filter(function(item
) {
290 return me
.filter(item
, value
);
293 if (this.sort
!== false) {
294 this.suggestions
= this.suggestions
.sort(this.sort
);
297 this.suggestions
= this.suggestions
.slice(0, this.maxItems
);
299 this.suggestions
.forEach(function(text
, index
) {
300 me
.ul
.appendChild(me
.item(text
, value
, index
));
303 if (this.ul
.children
.length
=== 0) {
305 this.status
.textContent
= "No results found";
307 this.close({ reason
: "nomatches" });
312 this.status
.textContent
= this.ul
.children
.length
+ " results found";
316 this.close({ reason
: "nomatches" });
318 this.status
.textContent
= "No results found";
323 // Static methods/properties
327 _
.FILTER_CONTAINS = function (text
, input
) {
328 return RegExp($.regExpEscape(input
.trim()), "i").test(text
);
331 _
.FILTER_STARTSWITH = function (text
, input
) {
332 return RegExp("^" + $.regExpEscape(input
.trim()), "i").test(text
);
335 _
.SORT_BYLENGTH = function (a
, b
) {
336 if (a
.length
!== b
.length
) {
337 return a
.length
- b
.length
;
340 return a
< b
? -1 : 1;
343 _
.ITEM = function (text
, input
, item_id
) {
344 var html
= input
.trim() === "" ? text
: text
.replace(RegExp($.regExpEscape(input
.trim()), "gi"), "<mark>$&</mark>");
345 return $.create("li", {
347 "aria-selected": "false",
348 "id": "awesomplete_list_" + this.count
+ "_item_" + item_id
352 _
.REPLACE = function (text
) {
353 this.input
.value
= text
.value
;
356 _
.DATA = function (item
/*, input*/) { return item
; };
360 function Suggestion(data
) {
361 var o
= Array
.isArray(data
)
362 ? { label
: data
[0], value
: data
[1] }
363 : typeof data
=== "object" && "label" in data
&& "value" in data
? data
: { label
: data
, value
: data
};
365 this.label
= o
.label
|| o
.value
;
366 this.value
= o
.value
;
368 Object
.defineProperty(Suggestion
.prototype = Object
.create(String
.prototype), "length", {
369 get: function() { return this.label
.length
; }
371 Suggestion
.prototype.toString
= Suggestion
.prototype.valueOf = function () {
372 return "" + this.label
;
375 function configure(instance
, properties
, o
) {
376 for (var i
in properties
) {
377 var initial
= properties
[i
],
378 attrValue
= instance
.input
.getAttribute("data-" + i
.toLowerCase());
380 if (typeof initial
=== "number") {
381 instance
[i
] = parseInt(attrValue
);
383 else if (initial
=== false) { // Boolean options must be false by default anyway
384 instance
[i
] = attrValue
!== null;
386 else if (initial
instanceof Function
) {
390 instance
[i
] = attrValue
;
393 if (!instance
[i
] && instance
[i
] !== 0) {
394 instance
[i
] = (i
in o
)? o
[i
] : initial
;
401 var slice
= Array
.prototype.slice
;
403 function $(expr
, con
) {
404 return typeof expr
=== "string"? (con
|| document
).querySelector(expr
) : expr
|| null;
407 function $$(expr
, con
) {
408 return slice
.call((con
|| document
).querySelectorAll(expr
));
411 $.create = function(tag
, o
) {
412 var element
= document
.createElement(tag
);
417 if (i
=== "inside") {
418 $(val
).appendChild(element
);
420 else if (i
=== "around") {
422 ref
.parentNode
.insertBefore(element
, ref
);
423 element
.appendChild(ref
);
425 else if (i
in element
) {
429 element
.setAttribute(i
, val
);
436 $.bind = function(element
, o
) {
438 for (var event
in o
) {
439 var callback
= o
[event
];
441 event
.split(/\s+/).forEach(function (event
) {
442 element
.addEventListener(event
, callback
);
448 $.unbind = function(element
, o
) {
450 for (var event
in o
) {
451 var callback
= o
[event
];
453 event
.split(/\s+/).forEach(function(event
) {
454 element
.removeEventListener(event
, callback
);
460 $.fire = function(target
, type
, properties
) {
461 var evt
= document
.createEvent("HTMLEvents");
463 evt
.initEvent(type
, true, true );
465 for (var j
in properties
) {
466 evt
[j
] = properties
[j
];
469 return target
.dispatchEvent(evt
);
472 $.regExpEscape = function (s
) {
473 return s
.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
476 $.siblingIndex = function (el
) {
477 /* eslint-disable no-cond-assign */
478 for (var i
= 0; el
= el
.previousElementSibling
; i
++);
485 $$("input.awesomplete").forEach(function (input
) {
490 // Make sure to export Awesomplete on self when in a browser
491 if (typeof self
!== "undefined") {
492 self
.Awesomplete
= _
;
495 // Are we in a browser? Check for Document constructor
496 if (typeof Document
!== "undefined") {
497 // DOM already loaded?
498 if (document
.readyState
!== "loading") {
503 document
.addEventListener("DOMContentLoaded", init
);
510 // Expose Awesomplete as a CJS module
511 if (typeof module
=== "object" && module
.exports
) {