]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - inc/awesomplete.js
Merge pull request #444 from dimtion/404_template
[github/shaarli/Shaarli.git] / inc / awesomplete.js
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 }());