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