]>
git.immae.eu Git - github/shaarli/Shaarli.git/blob - inc/awesomplete.js
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
) {
15 this.input
= $(input
);
16 this.input
.setAttribute("autocomplete", "off");
17 this.input
.setAttribute("aria-autocomplete", "list");
26 filter: _
.FILTER_CONTAINS
,
27 sort: _
.SORT_BYLENGTH
,
34 // Create necessary elements
36 this.container
= $.create("div", {
37 className: "awesomplete",
41 this.ul
= $.create("ul", {
43 inside: this.container
46 this.status
= $.create("span", {
47 className: "visually-hidden",
49 "aria-live": "assertive",
50 "aria-relevant": "additions",
51 inside: this.container
57 "input": this.evaluate
.bind(this),
58 "blur": this.close
.bind(this),
59 "keydown": function(evt
) {
62 // If the dropdown `ul` is in view, then act on keydown for the following keys:
63 // Enter / Esc / Up / Down
65 if (c
=== 13 && me
.selected
) { // Enter
69 else if (c
=== 27) { // Esc
72 else if (c
=== 38 || c
=== 40) { // Down/Up arrow
74 me
[c
=== 38? "previous" : "next"]();
80 $.bind(this.input
.form
, {"submit": this.close
.bind(this)});
82 $.bind(this.ul
, {"mousedown": function(evt
) {
87 while (li
&& !/li/i.test(li
.nodeName
)) {
91 if (li
&& evt
.button
=== 0) { // Only select on left click
93 me
.select(li
, evt
.target
);
98 if (this.input
.hasAttribute("list")) {
99 this.list
= "#" + this.input
.getAttribute("list");
100 this.input
.removeAttribute("list");
103 this.list
= this.input
.getAttribute("data-list") || o
.list
|| [];
111 if (Array
.isArray(list
)) {
114 else if (typeof list
=== "string" && list
.indexOf(",") > -1) {
115 this._list
= list
.split(/\s*,\s*/);
117 else { // Element or CSS selector
120 if (list
&& list
.children
) {
122 slice
.apply(list
.children
).forEach(function (el
) {
124 var text
= el
.textContent
.trim();
125 var value
= el
.value
|| text
;
126 var label
= el
.label
|| text
;
128 items
.push({ label: label
, value: value
});
136 if (document
.activeElement
=== this.input
) {
142 return this.index
> -1;
146 return !this.ul
.hasAttribute("hidden");
150 this.ul
.setAttribute("hidden", "");
153 $.fire(this.input
, "awesomplete-close");
157 this.ul
.removeAttribute("hidden");
159 if (this.autoFirst
&& this.index
=== -1) {
163 $.fire(this.input
, "awesomplete-open");
167 var count
= this.ul
.children
.length
;
169 this.goto(this.index
< count
- 1? this.index
+ 1 : -1);
172 previous: function () {
173 var count
= this.ul
.children
.length
;
175 this.goto(this.selected
? this.index
- 1 : count
- 1);
178 // Should not be used, highlights specific item without any checks!
180 var lis
= this.ul
.children
;
183 lis
[this.index
].setAttribute("aria-selected", "false");
188 if (i
> -1 && lis
.length
> 0) {
189 lis
[i
].setAttribute("aria-selected", "true");
190 this.status
.textContent
= lis
[i
].textContent
;
192 $.fire(this.input
, "awesomplete-highlight", {
193 text: this.suggestions
[this.index
]
198 select: function (selected
, origin
) {
200 this.index
= $.siblingIndex(selected
);
202 selected
= this.ul
.children
[this.index
];
206 var suggestion
= this.suggestions
[this.index
];
208 var allowed
= $.fire(this.input
, "awesomplete-select", {
210 origin: origin
|| selected
214 this.replace(suggestion
);
216 $.fire(this.input
, "awesomplete-selectcomplete", {
223 evaluate: function() {
225 var value
= this.input
.value
;
227 if (value
.length
>= this.minChars
&& this._list
.length
> 0) {
229 // Populate list with options that match
230 this.ul
.innerHTML
= "";
232 this.suggestions
= this._list
233 .map(function(item
) {
234 return new Suggestion(me
.data(item
, value
));
236 .filter(function(item
) {
237 return me
.filter(item
, value
);
240 .slice(0, this.maxItems
);
242 this.suggestions
.forEach(function(text
) {
243 me
.ul
.appendChild(me
.item(text
, value
));
246 if (this.ul
.children
.length
=== 0) {
258 // Static methods/properties
262 _
.FILTER_CONTAINS = function (text
, input
) {
263 return RegExp($.regExpEscape(input
.trim()), "i").test(text
);
266 _
.FILTER_STARTSWITH = function (text
, input
) {
267 return RegExp("^" + $.regExpEscape(input
.trim()), "i").test(text
);
270 _
.SORT_BYLENGTH = function (a
, b
) {
271 if (a
.length
!== b
.length
) {
272 return a
.length
- b
.length
;
275 return a
< b
? -1 : 1;
278 _
.ITEM = function (text
, input
) {
279 var html
= input
=== '' ? text : text
.replace(RegExp($.regExpEscape(input
.trim()), "gi"), "<mark>$&</mark>");
280 return $.create("li", {
282 "aria-selected": "false"
286 _
.REPLACE = function (text
) {
287 this.input
.value
= text
.value
;
290 _
.DATA = function (item
/*, input*/) { return item
; };
294 function Suggestion(data
) {
295 var o
= Array
.isArray(data
)
296 ? { label: data
[0], value: data
[1] }
297 : typeof data
=== "object" && "label" in data
&& "value" in data
? data : { label: data
, value: data
};
299 this.label
= o
.label
|| o
.value
;
300 this.value
= o
.value
;
302 Object
.defineProperty(Suggestion
.prototype = Object
.create(String
.prototype), "length", {
303 get: function() { return this.label
.length
; }
305 Suggestion
.prototype.toString
= Suggestion
.prototype.valueOf = function () {
306 return "" + this.label
;
309 function configure(instance
, properties
, o
) {
310 for (var i
in properties
) {
311 var initial
= properties
[i
],
312 attrValue
= instance
.input
.getAttribute("data-" + i
.toLowerCase());
314 if (typeof initial
=== "number") {
315 instance
[i
] = parseInt(attrValue
);
317 else if (initial
=== false) { // Boolean options must be false by default anyway
318 instance
[i
] = attrValue
!== null;
320 else if (initial
instanceof Function
) {
324 instance
[i
] = attrValue
;
327 if (!instance
[i
] && instance
[i
] !== 0) {
328 instance
[i
] = (i
in o
)? o
[i
] : initial
;
335 var slice
= Array
.prototype.slice
;
337 function $(expr
, con
) {
338 return typeof expr
=== "string"? (con
|| document
).querySelector(expr
) : expr
|| null;
341 function $$(expr
, con
) {
342 return slice
.call((con
|| document
).querySelectorAll(expr
));
345 $.create = function(tag
, o
) {
346 var element
= document
.createElement(tag
);
351 if (i
=== "inside") {
352 $(val
).appendChild(element
);
354 else if (i
=== "around") {
356 ref
.parentNode
.insertBefore(element
, ref
);
357 element
.appendChild(ref
);
359 else if (i
in element
) {
363 element
.setAttribute(i
, val
);
370 $.bind = function(element
, o
) {
372 for (var event
in o
) {
373 var callback
= o
[event
];
375 event
.split(/\s+/).forEach(function (event
) {
376 element
.addEventListener(event
, callback
);
382 $.fire = function(target
, type
, properties
) {
383 var evt
= document
.createEvent("HTMLEvents");
385 evt
.initEvent(type
, true, true );
387 for (var j
in properties
) {
388 evt
[j
] = properties
[j
];
391 return target
.dispatchEvent(evt
);
394 $.regExpEscape = function (s
) {
395 return s
.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
398 $.siblingIndex = function (el
) {
399 /* eslint-disable no-cond-assign */
400 for (var i
= 0; el
= el
.previousElementSibling
; i
++);
407 $$("input.awesomplete").forEach(function (input
) {
412 // Are we in a browser? Check for Document constructor
413 if (typeof Document
!== "undefined") {
414 // DOM already loaded?
415 if (document
.readyState
!== "loading") {
420 document
.addEventListener("DOMContentLoaded", init
);
427 // Make sure to export Awesomplete on self when in a browser
428 if (typeof self
!== "undefined") {
429 self
.Awesomplete
= _
;
432 // Expose Awesomplete as a CJS module
433 if (typeof module
=== "object" && module
.exports
) {