diff options
author | nodiscc <nodiscc@gmail.com> | 2015-03-15 14:21:59 +0100 |
---|---|---|
committer | nodiscc <nodiscc@gmail.com> | 2015-03-15 14:21:59 +0100 |
commit | 9a631bab7fb7ca0c60c9fd1c688b3c58ecec40f8 (patch) | |
tree | a821338f733f60e13ad0319761dac74bf6f8be07 /inc/awesomplete.js | |
parent | bc66d513a9aa6da9361dbfc2f0b4bf384e9597a6 (diff) | |
parent | bdd1715b249561ed919e4f03a06aec1f4c327335 (diff) | |
download | Shaarli-9a631bab7fb7ca0c60c9fd1c688b3c58ecec40f8.tar.gz Shaarli-9a631bab7fb7ca0c60c9fd1c688b3c58ecec40f8.tar.zst Shaarli-9a631bab7fb7ca0c60c9fd1c688b3c58ecec40f8.zip |
Merge remote-tracking branch 'ArthurHoaro/autocomplete' into next
Diffstat (limited to 'inc/awesomplete.js')
-rw-r--r-- | inc/awesomplete.js | 388 |
1 files changed, 388 insertions, 0 deletions
diff --git a/inc/awesomplete.js b/inc/awesomplete.js new file mode 100644 index 00000000..fae550e2 --- /dev/null +++ b/inc/awesomplete.js | |||
@@ -0,0 +1,388 @@ | |||
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 | }()); | ||