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