diff options
Diffstat (limited to 'inc/awesomplete.js')
-rw-r--r-- | inc/awesomplete.js | 184 |
1 files changed, 123 insertions, 61 deletions
diff --git a/inc/awesomplete.js b/inc/awesomplete.js index fae550e2..32f49e5b 100644 --- a/inc/awesomplete.js +++ b/inc/awesomplete.js | |||
@@ -12,26 +12,23 @@ | |||
12 | 12 | ||
13 | // Setup | 13 | // Setup |
14 | 14 | ||
15 | this.isOpened = false; | ||
16 | |||
15 | this.input = $(input); | 17 | this.input = $(input); |
18 | this.input.setAttribute("autocomplete", "off"); | ||
16 | this.input.setAttribute("aria-autocomplete", "list"); | 19 | this.input.setAttribute("aria-autocomplete", "list"); |
17 | 20 | ||
18 | o = o || {}; | 21 | o = o || {}; |
19 | 22 | ||
20 | configure.call(this, { | 23 | configure(this, { |
21 | minChars: 2, | 24 | minChars: 2, |
22 | maxItems: 10, | 25 | maxItems: 10, |
23 | autoFirst: false, | 26 | autoFirst: false, |
27 | data: _.DATA, | ||
24 | filter: _.FILTER_CONTAINS, | 28 | filter: _.FILTER_CONTAINS, |
25 | sort: _.SORT_BYLENGTH, | 29 | sort: _.SORT_BYLENGTH, |
26 | item: function (text, input) { | 30 | item: _.ITEM, |
27 | return $.create("li", { | 31 | replace: _.REPLACE |
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); | 32 | }, o); |
36 | 33 | ||
37 | this.index = -1; | 34 | this.index = -1; |
@@ -44,7 +41,7 @@ | |||
44 | }); | 41 | }); |
45 | 42 | ||
46 | this.ul = $.create("ul", { | 43 | this.ul = $.create("ul", { |
47 | hidden: "", | 44 | hidden: "hidden", |
48 | inside: this.container | 45 | inside: this.container |
49 | }); | 46 | }); |
50 | 47 | ||
@@ -60,7 +57,7 @@ | |||
60 | 57 | ||
61 | $.bind(this.input, { | 58 | $.bind(this.input, { |
62 | "input": this.evaluate.bind(this), | 59 | "input": this.evaluate.bind(this), |
63 | "blur": this.close.bind(this), | 60 | "blur": this.close.bind(this, { reason: "blur" }), |
64 | "keydown": function(evt) { | 61 | "keydown": function(evt) { |
65 | var c = evt.keyCode; | 62 | var c = evt.keyCode; |
66 | 63 | ||
@@ -72,7 +69,7 @@ | |||
72 | me.select(); | 69 | me.select(); |
73 | } | 70 | } |
74 | else if (c === 27) { // Esc | 71 | else if (c === 27) { // Esc |
75 | me.close(); | 72 | me.close({ reason: "esc" }); |
76 | } | 73 | } |
77 | else if (c === 38 || c === 40) { // Down/Up arrow | 74 | else if (c === 38 || c === 40) { // Down/Up arrow |
78 | evt.preventDefault(); | 75 | evt.preventDefault(); |
@@ -82,7 +79,7 @@ | |||
82 | } | 79 | } |
83 | }); | 80 | }); |
84 | 81 | ||
85 | $.bind(this.input.form, {"submit": this.close.bind(this)}); | 82 | $.bind(this.input.form, {"submit": this.close.bind(this, { reason: "submit" })}); |
86 | 83 | ||
87 | $.bind(this.ul, {"mousedown": function(evt) { | 84 | $.bind(this.ul, {"mousedown": function(evt) { |
88 | var li = evt.target; | 85 | var li = evt.target; |
@@ -93,15 +90,16 @@ | |||
93 | li = li.parentNode; | 90 | li = li.parentNode; |
94 | } | 91 | } |
95 | 92 | ||
96 | if (li) { | 93 | if (li && evt.button === 0) { // Only select on left click |
97 | me.select(li); | 94 | evt.preventDefault(); |
95 | me.select(li, evt.target); | ||
98 | } | 96 | } |
99 | } | 97 | } |
100 | }}); | 98 | }}); |
101 | 99 | ||
102 | if (this.input.hasAttribute("list")) { | 100 | if (this.input.hasAttribute("list")) { |
103 | this.list = "#" + input.getAttribute("list"); | 101 | this.list = "#" + this.input.getAttribute("list"); |
104 | input.removeAttribute("list"); | 102 | this.input.removeAttribute("list"); |
105 | } | 103 | } |
106 | else { | 104 | else { |
107 | this.list = this.input.getAttribute("data-list") || o.list || []; | 105 | this.list = this.input.getAttribute("data-list") || o.list || []; |
@@ -122,9 +120,18 @@ | |||
122 | list = $(list); | 120 | list = $(list); |
123 | 121 | ||
124 | if (list && list.children) { | 122 | if (list && list.children) { |
125 | this._list = slice.apply(list.children).map(function (el) { | 123 | var items = []; |
126 | return el.innerHTML.trim(); | 124 | slice.apply(list.children).forEach(function (el) { |
125 | if (!el.disabled) { | ||
126 | var text = el.textContent.trim(); | ||
127 | var value = el.value || text; | ||
128 | var label = el.label || text; | ||
129 | if (value !== "") { | ||
130 | items.push({ label: label, value: value }); | ||
131 | } | ||
132 | } | ||
127 | }); | 133 | }); |
134 | this._list = items; | ||
128 | } | 135 | } |
129 | } | 136 | } |
130 | 137 | ||
@@ -138,18 +145,24 @@ | |||
138 | }, | 145 | }, |
139 | 146 | ||
140 | get opened() { | 147 | get opened() { |
141 | return this.ul && this.ul.getAttribute("hidden") == null; | 148 | return this.isOpened; |
142 | }, | 149 | }, |
143 | 150 | ||
144 | close: function () { | 151 | close: function (o) { |
152 | if (!this.opened) { | ||
153 | return; | ||
154 | } | ||
155 | |||
145 | this.ul.setAttribute("hidden", ""); | 156 | this.ul.setAttribute("hidden", ""); |
157 | this.isOpened = false; | ||
146 | this.index = -1; | 158 | this.index = -1; |
147 | 159 | ||
148 | $.fire(this.input, "awesomplete-close"); | 160 | $.fire(this.input, "awesomplete-close", o || {}); |
149 | }, | 161 | }, |
150 | 162 | ||
151 | open: function () { | 163 | open: function () { |
152 | this.ul.removeAttribute("hidden"); | 164 | this.ul.removeAttribute("hidden"); |
165 | this.isOpened = true; | ||
153 | 166 | ||
154 | if (this.autoFirst && this.index === -1) { | 167 | if (this.autoFirst && this.index === -1) { |
155 | this.goto(0); | 168 | this.goto(0); |
@@ -160,14 +173,14 @@ | |||
160 | 173 | ||
161 | next: function () { | 174 | next: function () { |
162 | var count = this.ul.children.length; | 175 | var count = this.ul.children.length; |
163 | 176 | this.goto(this.index < count - 1 ? this.index + 1 : (count ? 0 : -1) ); | |
164 | this.goto(this.index < count - 1? this.index + 1 : -1); | ||
165 | }, | 177 | }, |
166 | 178 | ||
167 | previous: function () { | 179 | previous: function () { |
168 | var count = this.ul.children.length; | 180 | var count = this.ul.children.length; |
181 | var pos = this.index - 1; | ||
169 | 182 | ||
170 | this.goto(this.selected? this.index - 1 : count - 1); | 183 | this.goto(this.selected && pos !== -1 ? pos : count - 1); |
171 | }, | 184 | }, |
172 | 185 | ||
173 | // Should not be used, highlights specific item without any checks! | 186 | // Should not be used, highlights specific item without any checks! |
@@ -183,28 +196,37 @@ | |||
183 | if (i > -1 && lis.length > 0) { | 196 | if (i > -1 && lis.length > 0) { |
184 | lis[i].setAttribute("aria-selected", "true"); | 197 | lis[i].setAttribute("aria-selected", "true"); |
185 | this.status.textContent = lis[i].textContent; | 198 | this.status.textContent = lis[i].textContent; |
186 | } | ||
187 | 199 | ||
188 | $.fire(this.input, "awesomplete-highlight"); | 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; | ||
202 | |||
203 | $.fire(this.input, "awesomplete-highlight", { | ||
204 | text: this.suggestions[this.index] | ||
205 | }); | ||
206 | } | ||
189 | }, | 207 | }, |
190 | 208 | ||
191 | select: function (selected) { | 209 | select: function (selected, origin) { |
192 | selected = selected || this.ul.children[this.index]; | 210 | if (selected) { |
211 | this.index = $.siblingIndex(selected); | ||
212 | } else { | ||
213 | selected = this.ul.children[this.index]; | ||
214 | } | ||
193 | 215 | ||
194 | if (selected) { | 216 | if (selected) { |
195 | var prevented; | 217 | var suggestion = this.suggestions[this.index]; |
196 | 218 | ||
197 | $.fire(this.input, "awesomplete-select", { | 219 | var allowed = $.fire(this.input, "awesomplete-select", { |
198 | text: selected.textContent, | 220 | text: suggestion, |
199 | preventDefault: function () { | 221 | origin: origin || selected |
200 | prevented = true; | ||
201 | } | ||
202 | }); | 222 | }); |
203 | 223 | ||
204 | if (!prevented) { | 224 | if (allowed) { |
205 | this.replace(selected.textContent); | 225 | this.replace(suggestion); |
206 | this.close(); | 226 | this.close({ reason: "select" }); |
207 | $.fire(this.input, "awesomplete-selectcomplete"); | 227 | $.fire(this.input, "awesomplete-selectcomplete", { |
228 | text: suggestion | ||
229 | }); | ||
208 | } | 230 | } |
209 | } | 231 | } |
210 | }, | 232 | }, |
@@ -218,25 +240,28 @@ | |||
218 | // Populate list with options that match | 240 | // Populate list with options that match |
219 | this.ul.innerHTML = ""; | 241 | this.ul.innerHTML = ""; |
220 | 242 | ||
221 | this._list | 243 | this.suggestions = this._list |
244 | .map(function(item) { | ||
245 | return new Suggestion(me.data(item, value)); | ||
246 | }) | ||
222 | .filter(function(item) { | 247 | .filter(function(item) { |
223 | return me.filter(item, value); | 248 | return me.filter(item, value); |
224 | }) | 249 | }) |
225 | .sort(this.sort) | 250 | .sort(this.sort) |
226 | .every(function(text, i) { | 251 | .slice(0, this.maxItems); |
227 | me.ul.appendChild(me.item(text, value)); | ||
228 | 252 | ||
229 | return i < me.maxItems - 1; | 253 | this.suggestions.forEach(function(text) { |
230 | }); | 254 | me.ul.appendChild(me.item(text, value)); |
255 | }); | ||
231 | 256 | ||
232 | if (this.ul.children.length === 0) { | 257 | if (this.ul.children.length === 0) { |
233 | this.close(); | 258 | this.close({ reason: "nomatches" }); |
234 | } else { | 259 | } else { |
235 | this.open(); | 260 | this.open(); |
236 | } | 261 | } |
237 | } | 262 | } |
238 | else { | 263 | else { |
239 | this.close(); | 264 | this.close({ reason: "nomatches" }); |
240 | } | 265 | } |
241 | } | 266 | } |
242 | }; | 267 | }; |
@@ -261,27 +286,58 @@ | |||
261 | return a < b? -1 : 1; | 286 | return a < b? -1 : 1; |
262 | }; | 287 | }; |
263 | 288 | ||
289 | _.ITEM = function (text, input) { | ||
290 | var html = input.trim() === '' ? text : text.replace(RegExp($.regExpEscape(input.trim()), "gi"), "<mark>$&</mark>"); | ||
291 | return $.create("li", { | ||
292 | innerHTML: html, | ||
293 | "aria-selected": "false" | ||
294 | }); | ||
295 | }; | ||
296 | |||
297 | _.REPLACE = function (text) { | ||
298 | this.input.value = text.value; | ||
299 | }; | ||
300 | |||
301 | _.DATA = function (item/*, input*/) { return item; }; | ||
302 | |||
264 | // Private functions | 303 | // Private functions |
265 | 304 | ||
266 | function configure(properties, o) { | 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 }; | ||
309 | |||
310 | this.label = o.label || o.value; | ||
311 | this.value = o.value; | ||
312 | } | ||
313 | Object.defineProperty(Suggestion.prototype = Object.create(String.prototype), "length", { | ||
314 | get: function() { return this.label.length; } | ||
315 | }); | ||
316 | Suggestion.prototype.toString = Suggestion.prototype.valueOf = function () { | ||
317 | return "" + this.label; | ||
318 | }; | ||
319 | |||
320 | function configure(instance, properties, o) { | ||
267 | for (var i in properties) { | 321 | for (var i in properties) { |
268 | var initial = properties[i], | 322 | var initial = properties[i], |
269 | attrValue = this.input.getAttribute("data-" + i.toLowerCase()); | 323 | attrValue = instance.input.getAttribute("data-" + i.toLowerCase()); |
270 | 324 | ||
271 | if (typeof initial === "number") { | 325 | if (typeof initial === "number") { |
272 | this[i] = +attrValue; | 326 | instance[i] = parseInt(attrValue); |
273 | } | 327 | } |
274 | else if (initial === false) { // Boolean options must be false by default anyway | 328 | else if (initial === false) { // Boolean options must be false by default anyway |
275 | this[i] = attrValue !== null; | 329 | instance[i] = attrValue !== null; |
276 | } | 330 | } |
277 | else if (initial instanceof Function) { | 331 | else if (initial instanceof Function) { |
278 | this[i] = null; | 332 | instance[i] = null; |
279 | } | 333 | } |
280 | else { | 334 | else { |
281 | this[i] = attrValue; | 335 | instance[i] = attrValue; |
282 | } | 336 | } |
283 | 337 | ||
284 | this[i] = this[i] || o[i] || initial; | 338 | if (!instance[i] && instance[i] !== 0) { |
339 | instance[i] = (i in o)? o[i] : initial; | ||
340 | } | ||
285 | } | 341 | } |
286 | } | 342 | } |
287 | 343 | ||
@@ -343,23 +399,29 @@ | |||
343 | evt[j] = properties[j]; | 399 | evt[j] = properties[j]; |
344 | } | 400 | } |
345 | 401 | ||
346 | target.dispatchEvent(evt); | 402 | return target.dispatchEvent(evt); |
347 | }; | 403 | }; |
348 | 404 | ||
349 | $.regExpEscape = function (s) { | 405 | $.regExpEscape = function (s) { |
350 | return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); | 406 | return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); |
351 | } | 407 | }; |
408 | |||
409 | $.siblingIndex = function (el) { | ||
410 | /* eslint-disable no-cond-assign */ | ||
411 | for (var i = 0; el = el.previousElementSibling; i++); | ||
412 | return i; | ||
413 | }; | ||
352 | 414 | ||
353 | // Initialization | 415 | // Initialization |
354 | 416 | ||
355 | function init() { | 417 | function init() { |
356 | $$("input.awesomplete").forEach(function (input) { | 418 | $$("input.awesomplete").forEach(function (input) { |
357 | new Awesomplete(input); | 419 | new _(input); |
358 | }); | 420 | }); |
359 | } | 421 | } |
360 | 422 | ||
361 | // Are we in a browser? Check for Document constructor | 423 | // Are we in a browser? Check for Document constructor |
362 | if (typeof Document !== 'undefined') { | 424 | if (typeof Document !== "undefined") { |
363 | // DOM already loaded? | 425 | // DOM already loaded? |
364 | if (document.readyState !== "loading") { | 426 | if (document.readyState !== "loading") { |
365 | init(); | 427 | init(); |
@@ -374,15 +436,15 @@ | |||
374 | _.$$ = $$; | 436 | _.$$ = $$; |
375 | 437 | ||
376 | // Make sure to export Awesomplete on self when in a browser | 438 | // Make sure to export Awesomplete on self when in a browser |
377 | if (typeof self !== 'undefined') { | 439 | if (typeof self !== "undefined") { |
378 | self.Awesomplete = _; | 440 | self.Awesomplete = _; |
379 | } | 441 | } |
380 | 442 | ||
381 | // Expose Awesomplete as a CJS module | 443 | // Expose Awesomplete as a CJS module |
382 | if (typeof exports === 'object') { | 444 | if (typeof module === "object" && module.exports) { |
383 | module.exports = _; | 445 | module.exports = _; |
384 | } | 446 | } |
385 | 447 | ||
386 | return _; | 448 | return _; |
387 | 449 | ||
388 | }()); | 450 | }()); \ No newline at end of file |