]>
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.isOpened
= false;
17 this.input
= $(input
);
18 this.input
.setAttribute("autocomplete", "off");
19 this.input
.setAttribute("aria-autocomplete", "list");
28 filter: _
.FILTER_CONTAINS
,
29 sort: _
.SORT_BYLENGTH
,
36 // Create necessary elements
38 this.container
= $.create("div", {
39 className: "awesomplete",
43 this.ul
= $.create("ul", {
45 inside: this.container
48 this.status
= $.create("span", {
49 className: "visually-hidden",
51 "aria-live": "assertive",
52 "aria-relevant": "additions",
53 inside: this.container
59 "input": this.evaluate
.bind(this),
60 "blur": this.close
.bind(this, { reason: "blur" }),
61 "keydown": function(evt
) {
64 // If the dropdown `ul` is in view, then act on keydown for the following keys:
65 // Enter / Esc / Up / Down
67 if (c
=== 13 && me
.selected
) { // Enter
71 else if (c
=== 27) { // Esc
72 me
.close({ reason: "esc" });
74 else if (c
=== 38 || c
=== 40) { // Down/Up arrow
76 me
[c
=== 38? "previous" : "next"]();
82 $.bind(this.input
.form
, {"submit": this.close
.bind(this, { reason: "submit" })});
84 $.bind(this.ul
, {"mousedown": function(evt
) {
89 while (li
&& !/li/i.test(li
.nodeName
)) {
93 if (li
&& evt
.button
=== 0) { // Only select on left click
95 me
.select(li
, evt
.target
);
100 if (this.input
.hasAttribute("list")) {
101 this.list
= "#" + this.input
.getAttribute("list");
102 this.input
.removeAttribute("list");
105 this.list
= this.input
.getAttribute("data-list") || o
.list
|| [];
113 if (Array
.isArray(list
)) {
116 else if (typeof list
=== "string" && list
.indexOf(",") > -1) {
117 this._list
= list
.split(/\s*,\s*/);
119 else { // Element or CSS selector
122 if (list
&& list
.children
) {
124 slice
.apply(list
.children
).forEach(function (el
) {
126 var text
= el
.textContent
.trim();
127 var value
= el
.value
|| text
;
128 var label
= el
.label
|| text
;
130 items
.push({ label: label
, value: value
});
138 if (document
.activeElement
=== this.input
) {
144 return this.index
> -1;
148 return this.isOpened
;
151 close: function (o
) {
156 this.ul
.setAttribute("hidden", "");
157 this.isOpened
= false;
160 $.fire(this.input
, "awesomplete-close", o
|| {});
164 this.ul
.removeAttribute("hidden");
165 this.isOpened
= true;
167 if (this.autoFirst
&& this.index
=== -1) {
171 $.fire(this.input
, "awesomplete-open");
175 var count
= this.ul
.children
.length
;
176 this.goto(this.index
< count
- 1 ? this.index
+ 1 : (count
? 0 : -1) );
179 previous: function () {
180 var count
= this.ul
.children
.length
;
181 var pos
= this.index
- 1;
183 this.goto(this.selected
&& pos
!== -1 ? pos : count
- 1);
186 // Should not be used, highlights specific item without any checks!
188 var lis
= this.ul
.children
;
191 lis
[this.index
].setAttribute("aria-selected", "false");
196 if (i
> -1 && lis
.length
> 0) {
197 lis
[i
].setAttribute("aria-selected", "true");
198 this.status
.textContent
= lis
[i
].textContent
;
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
;
203 $.fire(this.input
, "awesomplete-highlight", {
204 text: this.suggestions
[this.index
]
209 select: function (selected
, origin
) {
211 this.index
= $.siblingIndex(selected
);
213 selected
= this.ul
.children
[this.index
];
217 var suggestion
= this.suggestions
[this.index
];
219 var allowed
= $.fire(this.input
, "awesomplete-select", {
221 origin: origin
|| selected
225 this.replace(suggestion
);
226 this.close({ reason: "select" });
227 $.fire(this.input
, "awesomplete-selectcomplete", {
234 evaluate: function() {
236 var value
= this.input
.value
;
238 if (value
.length
>= this.minChars
&& this._list
.length
> 0) {
240 // Populate list with options that match
241 this.ul
.innerHTML
= "";
243 this.suggestions
= this._list
244 .map(function(item
) {
245 return new Suggestion(me
.data(item
, value
));
247 .filter(function(item
) {
248 return me
.filter(item
, value
);
251 .slice(0, this.maxItems
);
253 this.suggestions
.forEach(function(text
) {
254 me
.ul
.appendChild(me
.item(text
, value
));
257 if (this.ul
.children
.length
=== 0) {
258 this.close({ reason: "nomatches" });
264 this.close({ reason: "nomatches" });
269 // Static methods/properties
273 _
.FILTER_CONTAINS = function (text
, input
) {
274 return RegExp($.regExpEscape(input
.trim()), "i").test(text
);
277 _
.FILTER_STARTSWITH = function (text
, input
) {
278 return RegExp("^" + $.regExpEscape(input
.trim()), "i").test(text
);
281 _
.SORT_BYLENGTH = function (a
, b
) {
282 if (a
.length
!== b
.length
) {
283 return a
.length
- b
.length
;
286 return a
< b
? -1 : 1;
289 _
.ITEM = function (text
, input
) {
290 var html
= input
.trim() === '' ? text : text
.replace(RegExp($.regExpEscape(input
.trim()), "gi"), "<mark>$&</mark>");
291 return $.create("li", {
293 "aria-selected": "false"
297 _
.REPLACE = function (text
) {
298 this.input
.value
= text
.value
;
301 _
.DATA = function (item
/*, input*/) { return item
; };
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
};
310 this.label
= o
.label
|| o
.value
;
311 this.value
= o
.value
;
313 Object
.defineProperty(Suggestion
.prototype = Object
.create(String
.prototype), "length", {
314 get: function() { return this.label
.length
; }
316 Suggestion
.prototype.toString
= Suggestion
.prototype.valueOf = function () {
317 return "" + this.label
;
320 function configure(instance
, properties
, o
) {
321 for (var i
in properties
) {
322 var initial
= properties
[i
],
323 attrValue
= instance
.input
.getAttribute("data-" + i
.toLowerCase());
325 if (typeof initial
=== "number") {
326 instance
[i
] = parseInt(attrValue
);
328 else if (initial
=== false) { // Boolean options must be false by default anyway
329 instance
[i
] = attrValue
!== null;
331 else if (initial
instanceof Function
) {
335 instance
[i
] = attrValue
;
338 if (!instance
[i
] && instance
[i
] !== 0) {
339 instance
[i
] = (i
in o
)? o
[i
] : initial
;
346 var slice
= Array
.prototype.slice
;
348 function $(expr
, con
) {
349 return typeof expr
=== "string"? (con
|| document
).querySelector(expr
) : expr
|| null;
352 function $$(expr
, con
) {
353 return slice
.call((con
|| document
).querySelectorAll(expr
));
356 $.create = function(tag
, o
) {
357 var element
= document
.createElement(tag
);
362 if (i
=== "inside") {
363 $(val
).appendChild(element
);
365 else if (i
=== "around") {
367 ref
.parentNode
.insertBefore(element
, ref
);
368 element
.appendChild(ref
);
370 else if (i
in element
) {
374 element
.setAttribute(i
, val
);
381 $.bind = function(element
, o
) {
383 for (var event
in o
) {
384 var callback
= o
[event
];
386 event
.split(/\s+/).forEach(function (event
) {
387 element
.addEventListener(event
, callback
);
393 $.fire = function(target
, type
, properties
) {
394 var evt
= document
.createEvent("HTMLEvents");
396 evt
.initEvent(type
, true, true );
398 for (var j
in properties
) {
399 evt
[j
] = properties
[j
];
402 return target
.dispatchEvent(evt
);
405 $.regExpEscape = function (s
) {
406 return s
.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
409 $.siblingIndex = function (el
) {
410 /* eslint-disable no-cond-assign */
411 for (var i
= 0; el
= el
.previousElementSibling
; i
++);
418 $$("input.awesomplete").forEach(function (input
) {
423 // Are we in a browser? Check for Document constructor
424 if (typeof Document
!== "undefined") {
425 // DOM already loaded?
426 if (document
.readyState
!== "loading") {
431 document
.addEventListener("DOMContentLoaded", init
);
438 // Make sure to export Awesomplete on self when in a browser
439 if (typeof self
!== "undefined") {
440 self
.Awesomplete
= _
;
443 // Expose Awesomplete as a CJS module
444 if (typeof module
=== "object" && module
.exports
) {