PluginProbe
WebberZone Top 10 — Popular Posts / 4.3.4
WebberZone Top 10 — Popular Posts v4.3.4
4.5.1 4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 1.6.1 All 117 releases
top-10 / includes / admin / settings / js / tom-select.complete.js

tom-select.complete.js in WebberZone Top 10 — Popular Posts 4.3.4, at includes/admin/settings/js/tom-select.complete.js

5,121 lines 181.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Tom Select v2.6.1
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 */
5
6 (function (global, factory) {
7 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8 typeof define === 'function' && define.amd ? define(factory) :
9 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.TomSelect = factory());
10 })(this, (function () {
11 'use strict';
12
13 /**
14 * MicroEvent - to make any js object an event emitter
15 *
16 * - pure javascript - server compatible, browser compatible
17 * - dont rely on the browser doms
18 * - super simple - you get it immediatly, no mistery, no magic involved
19 *
20 * @author Jerome Etienne (https://github.com/jeromeetienne)
21 */
22
23 /**
24 * Execute callback for each event in space separated list of event names
25 *
26 */
27 function forEvents(events, callback) {
28 events.split(/\s+/).forEach(event => {
29 callback(event);
30 });
31 }
32 class MicroEvent {
33 constructor() {
34 this._events = {};
35 }
36 on(events, fct) {
37 forEvents(events, event => {
38 const event_array = this._events[event] || [];
39 event_array.push(fct);
40 this._events[event] = event_array;
41 });
42 }
43 off(events, fct) {
44 var n = arguments.length;
45 if (n === 0) {
46 this._events = {};
47 return;
48 }
49 forEvents(events, event => {
50 if (n === 1) {
51 delete this._events[event];
52 return;
53 }
54 const event_array = this._events[event];
55 if (event_array === undefined) return;
56 event_array.splice(event_array.indexOf(fct), 1);
57 this._events[event] = event_array;
58 });
59 }
60 trigger(events, ...args) {
61 var self = this;
62 forEvents(events, event => {
63 const event_array = self._events[event];
64 if (event_array === undefined) return;
65 event_array.forEach(fct => {
66 fct.apply(self, args);
67 });
68 });
69 }
70 }
71
72 /**
73 * microplugin.js
74 * Copyright (c) 2013 Brian Reavis & contributors
75 *
76 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
77 * file except in compliance with the License. You may obtain a copy of the License at:
78 * http://www.apache.org/licenses/LICENSE-2.0
79 *
80 * Unless required by applicable law or agreed to in writing, software distributed under
81 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
82 * ANY KIND, either express or implied. See the License for the specific language
83 * governing permissions and limitations under the License.
84 *
85 * @author Brian Reavis <[email protected]>
86 */
87
88 function MicroPlugin(Interface) {
89 Interface.plugins = {};
90 return class extends Interface {
91 constructor(...args) {
92 super(...args);
93 this.plugins = {
94 names: [],
95 settings: {},
96 requested: {},
97 loaded: {}
98 };
99 }
100 /**
101 * Registers a plugin.
102 *
103 * @param {function} fn
104 */
105 static define(name, fn) {
106 Interface.plugins[name] = {
107 'name': name,
108 'fn': fn
109 };
110 }
111
112 /**
113 * Initializes the listed plugins (with options).
114 * Acceptable formats:
115 *
116 * List (without options):
117 * ['a', 'b', 'c']
118 *
119 * List (with options):
120 * [{'name': 'a', options: {}}, {'name': 'b', options: {}}]
121 *
122 * Hash (with options):
123 * {'a': { ... }, 'b': { ... }, 'c': { ... }}
124 *
125 * @param {array|object} plugins
126 */
127 initializePlugins(plugins) {
128 var key, name;
129 const self = this;
130 const queue = [];
131 if (Array.isArray(plugins)) {
132 plugins.forEach(plugin => {
133 if (typeof plugin === 'string') {
134 queue.push(plugin);
135 } else {
136 self.plugins.settings[plugin.name] = plugin.options;
137 queue.push(plugin.name);
138 }
139 });
140 } else if (plugins) {
141 for (key in plugins) {
142 if (plugins.hasOwnProperty(key)) {
143 self.plugins.settings[key] = plugins[key];
144 queue.push(key);
145 }
146 }
147 }
148 while (name = queue.shift()) {
149 self.require(name);
150 }
151 }
152 loadPlugin(name) {
153 var self = this;
154 var plugins = self.plugins;
155 var plugin = Interface.plugins[name];
156 if (!Interface.plugins.hasOwnProperty(name)) {
157 throw new Error('Unable to find "' + name + '" plugin');
158 }
159 plugins.requested[name] = true;
160 plugins.loaded[name] = plugin.fn.apply(self, [self.plugins.settings[name] || {}]);
161 plugins.names.push(name);
162 }
163
164 /**
165 * Initializes a plugin.
166 *
167 */
168 require(name) {
169 var self = this;
170 var plugins = self.plugins;
171 if (!self.plugins.loaded.hasOwnProperty(name)) {
172 if (plugins.requested[name]) {
173 throw new Error('Plugin has circular dependency ("' + name + '")');
174 }
175 self.loadPlugin(name);
176 }
177 return plugins.loaded[name];
178 }
179 };
180 }
181
182 /**
183 * Convert array of strings to a regular expression
184 * ex ['ab','a'] => (?:ab|a)
185 * ex ['a','b'] => [ab]
186 */
187 const arrayToPattern = (chars) => {
188 chars = chars.filter(Boolean);
189 if (chars.length < 2) {
190 return chars[0] || '';
191 }
192 return (maxValueLength(chars) == 1) ? '[' + chars.join('') + ']' : '(?:' + chars.join('|') + ')';
193 };
194 const sequencePattern = (array) => {
195 if (!hasDuplicates(array)) {
196 return array.join('');
197 }
198 let pattern = '';
199 let prev_char_count = 0;
200 const prev_pattern = () => {
201 if (prev_char_count > 1) {
202 pattern += '{' + prev_char_count + '}';
203 }
204 };
205 array.forEach((char, i) => {
206 if (char === array[i - 1]) {
207 prev_char_count++;
208 return;
209 }
210 prev_pattern();
211 pattern += char;
212 prev_char_count = 1;
213 });
214 prev_pattern();
215 return pattern;
216 };
217 /**
218 * Convert array of strings to a regular expression
219 * ex ['ab','a'] => (?:ab|a)
220 * ex ['a','b'] => [ab]
221 */
222 const setToPattern = (chars) => {
223 let array = Array.from(chars);
224 return arrayToPattern(array);
225 };
226 /**
227 * https://stackoverflow.com/questions/7376598/in-javascript-how-do-i-check-if-an-array-has-duplicate-values
228 */
229 const hasDuplicates = (array) => {
230 return (new Set(array)).size !== array.length;
231 };
232 /**
233 * https://stackoverflow.com/questions/63006601/why-does-u-throw-an-invalid-escape-error
234 */
235 const escape_regex = (str) => {
236 return (str + '').replace(/([\$\(\)\*\+\.\?\[\]\^\{\|\}\\])/gu, '\\$1');
237 };
238 /**
239 * Return the max length of array values
240 */
241 const maxValueLength = (array) => {
242 return array.reduce((longest, value) => Math.max(longest, unicodeLength(value)), 0);
243 };
244 const unicodeLength = (str) => {
245 return Array.from(str).length;
246 };
247
248 /**
249 * Get all possible combinations of substrings that add up to the given string
250 * https://stackoverflow.com/questions/30169587/find-all-the-combination-of-substrings-that-add-up-to-the-given-string
251 */
252 const allSubstrings = (input) => {
253 if (input.length === 1)
254 return [[input]];
255 let result = [];
256 const start = input.substring(1);
257 const suba = allSubstrings(start);
258 suba.forEach(function (subresult) {
259 let tmp = subresult.slice(0);
260 tmp[0] = input.charAt(0) + tmp[0];
261 result.push(tmp);
262 tmp = subresult.slice(0);
263 tmp.unshift(input.charAt(0));
264 result.push(tmp);
265 });
266 return result;
267 };
268
269 const code_points = [[0, 65535]];
270 const accent_pat = '[\u0300-\u036F\u{b7}\u{2be}\u{2bc}]';
271 let unicode_map;
272 let multi_char_reg;
273 const max_char_length = 3;
274 const latin_convert = {};
275 const latin_condensed = {
276 '/': '⁄∕',
277 '0': '߀',
278 "a": "ⱥɐɑ",
279 "aa": "ꜳ",
280 "ae": "æǽǣ",
281 "ao": "ꜵ",
282 "au": "ꜷ",
283 "av": "ꜹꜻ",
284 "ay": "ꜽ",
285 "b": "ƀɓƃ",
286 "c": "ꜿƈȼↄ",
287 "d": "đɗɖ�
288 ƌꮷԁɦ",
289 "e": "ɛǝᴇɇ",
290 "f": "ꝼƒ",
291 "g": "ǥɠꞡᵹꝿɢ",
292 "h": "ħⱨⱶɥ",
293 "i": "ɨı",
294 "j": "ɉȷ",
295 "k": "ƙⱪꝁꝃ�
296 ꞣ",
297 "l": "łƚɫⱡꝉꝇꞁɭ",
298 "m": "ɱɯϻ",
299 "n": "ꞥƞɲꞑᴎлԉ",
300 "o": "øǿɔɵꝋꝍᴑ",
301 "oe": "œ",
302 "oi": "ƣ",
303 "oo": "ꝏ",
304 "ou": "ȣ",
305 "p": "ƥᵽꝑꝓꝕρ",
306 "q": "ꝗꝙɋ",
307 "r": "ɍɽꝛꞧꞃ",
308 "s": "ßȿꞩ�
309 ʂ",
310 "t": "ŧƭʈⱦꞇ",
311 "th": "þ",
312 "tz": "ꜩ",
313 "u": "ʉ",
314 "v": "ʋꝟʌ",
315 "vy": "ꝡ",
316 "w": "ⱳ",
317 "y": "ƴɏỿ",
318 "z": "ƶȥɀⱬꝣ",
319 "hv": "ƕ"
320 };
321 for (let latin in latin_condensed) {
322 let unicode = latin_condensed[latin] || '';
323 for (let i = 0; i < unicode.length; i++) {
324 let char = unicode.substring(i, i + 1);
325 latin_convert[char] = latin;
326 }
327 }
328 const convert_pat = new RegExp(Object.keys(latin_convert).join('|') + '|' + accent_pat, 'gu');
329 /**
330 * Initialize the unicode_map from the give code point ranges
331 */
332 const initialize = (_code_points) => {
333 if (unicode_map !== undefined)
334 return;
335 unicode_map = generateMap(code_points);
336 };
337 /**
338 * Helper method for normalize a string
339 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
340 */
341 const normalize = (str, form = 'NFKD') => str.normalize(form);
342 /**
343 * Remove accents without reordering string
344 * calling str.normalize('NFKD') on \u{594}\u{595}\u{596} becomes \u{596}\u{594}\u{595}
345 * via https://github.com/krisk/Fuse/issues/133#issuecomment-318692703
346 */
347 const asciifold = (str) => {
348 return Array.from(str).reduce(
349 /**
350 * @param {string} result
351 * @param {string} char
352 */
353 (result, char) => {
354 return result + _asciifold(char);
355 }, '');
356 };
357 const _asciifold = (str) => {
358 str = normalize(str)
359 .toLowerCase()
360 .replace(convert_pat, (/** @type {string} */ char) => {
361 return latin_convert[char] || '';
362 });
363 //return str;
364 return normalize(str, 'NFC');
365 };
366 /**
367 * Generate a list of unicode variants from the list of code points
368 */
369 function* generator(code_points) {
370 for (const [code_point_min, code_point_max] of code_points) {
371 for (let i = code_point_min; i <= code_point_max; i++) {
372 let composed = String.fromCharCode(i);
373 let folded = asciifold(composed);
374 if (folded == composed.toLowerCase()) {
375 continue;
376 }
377 // skip when folded is a string longer than 3 characters long
378 // bc the resulting regex patterns will be long
379 // eg:
380 // folded صلى الله عليه وسل�
381 length 18 code point 65018
382 // folded جل جلاله length 8 code point 65019
383 if (folded.length > max_char_length) {
384 continue;
385 }
386 if (folded.length == 0) {
387 continue;
388 }
389 yield { folded: folded, composed: composed, code_point: i };
390 }
391 }
392 }
393 /**
394 * Generate a unicode map from the list of code points
395 */
396 const generateSets = (code_points) => {
397 const unicode_sets = {};
398 const addMatching = (folded, to_add) => {
399 /** @type {Set<string>} */
400 const folded_set = unicode_sets[folded] || new Set();
401 const patt = new RegExp('^' + setToPattern(folded_set) + '$', 'iu');
402 if (to_add.match(patt)) {
403 return;
404 }
405 folded_set.add(escape_regex(to_add));
406 unicode_sets[folded] = folded_set;
407 };
408 for (let value of generator(code_points)) {
409 addMatching(value.folded, value.folded);
410 addMatching(value.folded, value.composed);
411 }
412 return unicode_sets;
413 };
414 /**
415 * Generate a unicode map from the list of code points
416 * ae => (?:(?:ae|Æ|Ǽ|Ǣ)|(?:A|Ⓐ|A...)(?:E|ɛ|Ⓔ...))
417 */
418 const generateMap = (code_points) => {
419 const unicode_sets = generateSets(code_points);
420 const unicode_map = {};
421 let multi_char = [];
422 for (let folded in unicode_sets) {
423 let set = unicode_sets[folded];
424 if (set) {
425 unicode_map[folded] = setToPattern(set);
426 }
427 if (folded.length > 1) {
428 multi_char.push(escape_regex(folded));
429 }
430 }
431 multi_char.sort((a, b) => b.length - a.length);
432 const multi_char_patt = arrayToPattern(multi_char);
433 multi_char_reg = new RegExp('^' + multi_char_patt, 'u');
434 return unicode_map;
435 };
436 /**
437 * Map each element of an array from its folded value to all possible unicode matches
438 */
439 const mapSequence = (strings, min_replacement = 1) => {
440 let chars_replaced = 0;
441 strings = strings.map((str) => {
442 if (unicode_map[str]) {
443 chars_replaced += str.length;
444 }
445 return unicode_map[str] || str;
446 });
447 if (chars_replaced >= min_replacement) {
448 return sequencePattern(strings);
449 }
450 return '';
451 };
452 /**
453 * Convert a short string and split it into all possible patterns
454 * Keep a pattern only if min_replacement is met
455 *
456 * 'abc'
457 * => [['abc'],['ab','c'],['a','bc'],['a','b','c']]
458 * => ['abc-pattern','ab-c-pattern'...]
459 */
460 const substringsToPattern = (str, min_replacement = 1) => {
461 min_replacement = Math.max(min_replacement, str.length - 1);
462 return arrayToPattern(allSubstrings(str).map((sub_pat) => {
463 return mapSequence(sub_pat, min_replacement);
464 }));
465 };
466 /**
467 * Convert an array of sequences into a pattern
468 * [{start:0,end:3,length:3,substr:'iii'}...] => (?:iii...)
469 */
470 const sequencesToPattern = (sequences, all = true) => {
471 let min_replacement = sequences.length > 1 ? 1 : 0;
472 return arrayToPattern(sequences.map((sequence) => {
473 let seq = [];
474 const len = all ? sequence.length() : sequence.length() - 1;
475 for (let j = 0; j < len; j++) {
476 seq.push(substringsToPattern(sequence.substrs[j] || '', min_replacement));
477 }
478 return sequencePattern(seq);
479 }));
480 };
481 /**
482 * Return true if the sequence is already in the sequences
483 */
484 const inSequences = (needle_seq, sequences) => {
485 for (const seq of sequences) {
486 if (seq.start != needle_seq.start || seq.end != needle_seq.end) {
487 continue;
488 }
489 if (seq.substrs.join('') !== needle_seq.substrs.join('')) {
490 continue;
491 }
492 let needle_parts = needle_seq.parts;
493 const filter = (part) => {
494 for (const needle_part of needle_parts) {
495 if (needle_part.start === part.start && needle_part.substr === part.substr) {
496 return false;
497 }
498 if (part.length == 1 || needle_part.length == 1) {
499 continue;
500 }
501 // check for overlapping parts
502 // a = ['::=','==']
503 // b = ['::','===']
504 // a = ['r','sm']
505 // b = ['rs','m']
506 if (part.start < needle_part.start && part.end > needle_part.start) {
507 return true;
508 }
509 if (needle_part.start < part.start && needle_part.end > part.start) {
510 return true;
511 }
512 }
513 return false;
514 };
515 let filtered = seq.parts.filter(filter);
516 if (filtered.length > 0) {
517 continue;
518 }
519 return true;
520 }
521 return false;
522 };
523 class Sequence {
524 parts;
525 substrs;
526 start;
527 end;
528 constructor() {
529 this.parts = [];
530 this.substrs = [];
531 this.start = 0;
532 this.end = 0;
533 }
534 add(part) {
535 if (part) {
536 this.parts.push(part);
537 this.substrs.push(part.substr);
538 this.start = Math.min(part.start, this.start);
539 this.end = Math.max(part.end, this.end);
540 }
541 }
542 last() {
543 return this.parts[this.parts.length - 1];
544 }
545 length() {
546 return this.parts.length;
547 }
548 clone(position, last_piece) {
549 let clone = new Sequence();
550 let parts = JSON.parse(JSON.stringify(this.parts));
551 let last_part = parts.pop();
552 for (const part of parts) {
553 clone.add(part);
554 }
555 let last_substr = last_piece.substr.substring(0, position - last_part.start);
556 let clone_last_len = last_substr.length;
557 clone.add({ start: last_part.start, end: last_part.start + clone_last_len, length: clone_last_len, substr: last_substr });
558 return clone;
559 }
560 }
561 /**
562 * Expand a regular expression pattern to include unicode variants
563 * eg /a/ becomes /aⓐaẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁ�
564 ⱥɐɑAⒶAÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢ�
565 ǺǍȀȂẠẬẶḀĄȺⱯ/
566 *
567 * Issue:
568 * ﺊﺋ [ 'ﺊ = \\u{fe8a}', 'ﺋ = \\u{fe8b}' ]
569 * becomes: ئئ [ 'ي = \\u{64a}', 'ٔ = \\u{654}', 'ي = \\u{64a}', 'ٔ = \\u{654}' ]
570 *
571 * İIJ = IIJ = �
572 �J
573 *
574 * 1/2/4
575 */
576 const getPattern = (str) => {
577 initialize();
578 str = asciifold(str);
579 let pattern = '';
580 let sequences = [new Sequence()];
581 for (let i = 0; i < str.length; i++) {
582 let substr = str.substring(i);
583 let match = substr.match(multi_char_reg);
584 const char = str.substring(i, i + 1);
585 const match_str = match ? match[0] : null;
586 // loop through sequences
587 // add either the char or multi_match
588 let overlapping = [];
589 let added_types = new Set();
590 for (const sequence of sequences) {
591 const last_piece = sequence.last();
592 if (!last_piece || last_piece.length == 1 || last_piece.end <= i) {
593 // if we have a multi match
594 if (match_str) {
595 const len = match_str.length;
596 sequence.add({ start: i, end: i + len, length: len, substr: match_str });
597 added_types.add('1');
598 }
599 else {
600 sequence.add({ start: i, end: i + 1, length: 1, substr: char });
601 added_types.add('2');
602 }
603 }
604 else if (match_str) {
605 let clone = sequence.clone(i, last_piece);
606 const len = match_str.length;
607 clone.add({ start: i, end: i + len, length: len, substr: match_str });
608 overlapping.push(clone);
609 }
610 else {
611 // don't add char
612 // adding would create invalid patterns: 234 => [2,34,4]
613 added_types.add('3');
614 }
615 }
616 // if we have overlapping
617 if (overlapping.length > 0) {
618 // ['ii','iii'] before ['i','i','iii']
619 overlapping = overlapping.sort((a, b) => {
620 return a.length() - b.length();
621 });
622 for (let clone of overlapping) {
623 // don't add if we already have an equivalent sequence
624 if (inSequences(clone, sequences)) {
625 continue;
626 }
627 sequences.push(clone);
628 }
629 continue;
630 }
631 // if we haven't done anything unique
632 // clean up the patterns
633 // helps keep patterns smaller
634 // if str = 'r₨㎧aarss', pattern will be 446 instead of 655
635 if (i > 0 && added_types.size == 1 && !added_types.has('3')) {
636 pattern += sequencesToPattern(sequences, false);
637 let new_seq = new Sequence();
638 const old_seq = sequences[0];
639 if (old_seq) {
640 new_seq.add(old_seq.last());
641 }
642 sequences = [new_seq];
643 }
644 }
645 pattern += sequencesToPattern(sequences, true);
646 return pattern;
647 };
648
649 /**
650 * A property getter resolving dot-notation
651 * @param {Object} obj The root object to fetch property on
652 * @param {String} name The optionally dotted property name to fetch
653 * @return {Object} The resolved property value
654 */
655 const getAttr = (obj, name) => {
656 if (!obj)
657 return;
658 return obj[name];
659 };
660 /**
661 * A property getter resolving dot-notation
662 * @param {Object} obj The root object to fetch property on
663 * @param {String} name The optionally dotted property name to fetch
664 * @return {Object} The resolved property value
665 */
666 const getAttrNesting = (obj, name) => {
667 if (!obj)
668 return;
669 var part, names = name.split(".");
670 while ((part = names.shift()) && (obj = obj[part]))
671 ;
672 return obj;
673 };
674 /**
675 * Calculates how close of a match the
676 * given value is against a search token.
677 *
678 */
679 const scoreValue = (value, token, weight) => {
680 var score, pos;
681 if (!value)
682 return 0;
683 value = value + '';
684 if (token.regex == null)
685 return 0;
686 pos = value.search(token.regex);
687 if (pos === -1)
688 return 0;
689 score = token.string.length / value.length;
690 if (pos === 0)
691 score += 0.5;
692 return score * weight;
693 };
694 /**
695 * Cast object property to an array if it exists and has a value
696 *
697 */
698 const propToArray = (obj, key) => {
699 var value = obj[key];
700 if (typeof value == 'function')
701 return value;
702 if (value && !Array.isArray(value)) {
703 obj[key] = [value];
704 }
705 };
706 /**
707 * Iterates over arrays and hashes.
708 *
709 * ```
710 * iterate(this.items, function(item, id) {
711 * // invoked for each item
712 * });
713 * ```
714 *
715 */
716 const iterate$1 = (object, callback) => {
717 if (Array.isArray(object)) {
718 object.forEach(callback);
719 }
720 else {
721 for (var key in object) {
722 if (object.hasOwnProperty(key)) {
723 callback(object[key], key);
724 }
725 }
726 }
727 };
728 const cmp = (a, b) => {
729 if (typeof a === 'number' && typeof b === 'number') {
730 return a > b ? 1 : (a < b ? -1 : 0);
731 }
732 a = asciifold(a + '').toLowerCase();
733 b = asciifold(b + '').toLowerCase();
734 if (a > b)
735 return 1;
736 if (b > a)
737 return -1;
738 return 0;
739 };
740
741 /**
742 * sifter.js
743 * Copyright (c) 2013–2020 Brian Reavis & contributors
744 *
745 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
746 * file except in compliance with the License. You may obtain a copy of the License at:
747 * http://www.apache.org/licenses/LICENSE-2.0
748 *
749 * Unless required by applicable law or agreed to in writing, software distributed under
750 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
751 * ANY KIND, either express or implied. See the License for the specific language
752 * governing permissions and limitations under the License.
753 *
754 * @author Brian Reavis <[email protected]>
755 */
756 class Sifter {
757 items; // []|{};
758 settings;
759 /**
760 * Textually searches arrays and hashes of objects
761 * by property (or multiple properties). Designed
762 * specifically for autocomplete.
763 *
764 */
765 constructor(items, settings) {
766 this.items = items;
767 this.settings = settings || { diacritics: true };
768 }
769 ;
770 /**
771 * Splits a search string into an array of individual
772 * regexps to be used to match results.
773 *
774 */
775 tokenize(query, respect_word_boundaries, weights) {
776 if (!query || !query.length)
777 return [];
778 const tokens = [];
779 const words = query.split(/\s+/);
780 var field_regex;
781 if (weights) {
782 field_regex = new RegExp('^(' + Object.keys(weights).map(escape_regex).join('|') + ')\:(.*)$');
783 }
784 words.forEach((word) => {
785 let field_match;
786 let field = null;
787 let regex = null;
788 // look for "field:query" tokens
789 if (field_regex && (field_match = word.match(field_regex))) {
790 field = field_match[1];
791 word = field_match[2];
792 }
793 if (word.length > 0) {
794 if (this.settings.diacritics) {
795 regex = getPattern(word) || null;
796 }
797 else {
798 regex = escape_regex(word);
799 }
800 if (regex && respect_word_boundaries)
801 regex = "\\b" + regex;
802 }
803 tokens.push({
804 string: word,
805 regex: regex ? new RegExp(regex, 'iu') : null,
806 field: field,
807 });
808 });
809 return tokens;
810 }
811 ;
812 /**
813 * Returns a function to be used to score individual results.
814 *
815 * Good matches will have a higher score than poor matches.
816 * If an item is not a match, 0 will be returned by the function.
817 *
818 * @returns {T.ScoreFn}
819 */
820 getScoreFunction(query, options) {
821 var search = this.prepareSearch(query, options);
822 return this._getScoreFunction(search);
823 }
824 /**
825 * @returns {T.ScoreFn}
826 *
827 */
828 _getScoreFunction(search) {
829 const tokens = search.tokens, token_count = tokens.length;
830 if (!token_count) {
831 return function () { return 0; };
832 }
833 const fields = search.options.fields, weights = search.weights, field_count = fields.length, getAttrFn = search.getAttrFn;
834 if (!field_count) {
835 return function () { return 1; };
836 }
837 /**
838 * Calculates the score of an object
839 * against the search query.
840 *
841 */
842 const scoreObject = (function () {
843 if (field_count === 1) {
844 return function (token, data) {
845 const field = fields[0].field;
846 return scoreValue(getAttrFn(data, field), token, weights[field] || 1);
847 };
848 }
849 return function (token, data) {
850 var sum = 0;
851 // is the token specific to a field?
852 if (token.field) {
853 const value = getAttrFn(data, token.field);
854 if (!token.regex && value) {
855 sum += (1 / field_count);
856 }
857 else {
858 sum += scoreValue(value, token, 1);
859 }
860 }
861 else {
862 iterate$1(weights, (weight, field) => {
863 sum += scoreValue(getAttrFn(data, field), token, weight);
864 });
865 }
866 return sum / field_count;
867 };
868 })();
869 if (token_count === 1) {
870 return function (data) {
871 return scoreObject(tokens[0], data);
872 };
873 }
874 if (search.options.conjunction === 'and') {
875 return function (data) {
876 var score, sum = 0;
877 for (let token of tokens) {
878 score = scoreObject(token, data);
879 if (score <= 0)
880 return 0;
881 sum += score;
882 }
883 return sum / token_count;
884 };
885 }
886 else {
887 return function (data) {
888 var sum = 0;
889 iterate$1(tokens, (token) => {
890 sum += scoreObject(token, data);
891 });
892 return sum / token_count;
893 };
894 }
895 }
896 ;
897 /**
898 * Returns a function that can be used to compare two
899 * results, for sorting purposes. If no sorting should
900 * be performed, `null` will be returned.
901 *
902 * @return function(a,b)
903 */
904 getSortFunction(query, options) {
905 var search = this.prepareSearch(query, options);
906 return this._getSortFunction(search);
907 }
908 _getSortFunction(search) {
909 var implicit_score, sort_flds = [];
910 const self = this, options = search.options, sort = (!search.query && options.sort_empty) ? options.sort_empty : options.sort;
911 if (typeof sort == 'function') {
912 return sort.bind(this);
913 }
914 /**
915 * Fetches the specified sort field value
916 * from a search result item.
917 *
918 */
919 const get_field = function (name, result) {
920 if (name === '$score')
921 return result.score;
922 return search.getAttrFn(self.items[result.id], name);
923 };
924 // parse options
925 if (sort) {
926 for (let s of sort) {
927 if (search.query || s.field !== '$score') {
928 sort_flds.push(s);
929 }
930 }
931 }
932 // the "$score" field is implied to be the primary
933 // sort field, unless it's manually specified
934 if (search.query) {
935 implicit_score = true;
936 for (let fld of sort_flds) {
937 if (fld.field === '$score') {
938 implicit_score = false;
939 break;
940 }
941 }
942 if (implicit_score) {
943 sort_flds.unshift({ field: '$score', direction: 'desc' });
944 }
945 // without a search.query, all items will have the same score
946 }
947 else {
948 sort_flds = sort_flds.filter((fld) => fld.field !== '$score');
949 }
950 // build function
951 const sort_flds_count = sort_flds.length;
952 if (!sort_flds_count) {
953 return null;
954 }
955 return function (a, b) {
956 var result, field;
957 for (let sort_fld of sort_flds) {
958 field = sort_fld.field;
959 let multiplier = sort_fld.direction === 'desc' ? -1 : 1;
960 result = multiplier * cmp(get_field(field, a), get_field(field, b));
961 if (result)
962 return result;
963 }
964 return 0;
965 };
966 }
967 ;
968 /**
969 * Parses a search query and returns an object
970 * with tokens and fields ready to be populated
971 * with results.
972 *
973 */
974 prepareSearch(query, optsUser) {
975 const weights = {};
976 var options = Object.assign({}, optsUser);
977 propToArray(options, 'sort');
978 propToArray(options, 'sort_empty');
979 // convert fields to new format
980 if (options.fields) {
981 propToArray(options, 'fields');
982 const fields = [];
983 options.fields.forEach((field) => {
984 if (typeof field == 'string') {
985 field = { field: field, weight: 1 };
986 }
987 fields.push(field);
988 weights[field.field] = ('weight' in field) ? field.weight : 1;
989 });
990 options.fields = fields;
991 }
992 return {
993 options: options,
994 query: query.toLowerCase().trim(),
995 tokens: this.tokenize(query, options.respect_word_boundaries, weights),
996 total: 0,
997 items: [],
998 weights: weights,
999 getAttrFn: (options.nesting) ? getAttrNesting : getAttr,
1000 };
1001 }
1002 ;
1003 /**
1004 * Searches through all items and returns a sorted array of matches.
1005 *
1006 */
1007 search(query, options) {
1008 var self = this, score, search;
1009 search = this.prepareSearch(query, options);
1010 options = search.options;
1011 query = search.query;
1012 // generate result scoring function
1013 const fn_score = options.score || self._getScoreFunction(search);
1014 // perform search and sort
1015 if (query.length) {
1016 iterate$1(self.items, (item, id) => {
1017 score = fn_score(item);
1018 if (options.filter === false || score > 0) {
1019 search.items.push({ 'score': score, 'id': id });
1020 }
1021 });
1022 }
1023 else {
1024 iterate$1(self.items, (_, id) => {
1025 search.items.push({ 'score': 1, 'id': id });
1026 });
1027 }
1028 const fn_sort = self._getSortFunction(search);
1029 if (fn_sort)
1030 search.items.sort(fn_sort);
1031 // apply limits
1032 search.total = search.items.length;
1033 if (typeof options.limit === 'number') {
1034 search.items = search.items.slice(0, options.limit);
1035 }
1036 return search;
1037 }
1038 ;
1039 }
1040
1041 /**
1042 * Converts a scalar to its best string representation
1043 * for hash keys and HTML attribute values.
1044 *
1045 * Transformations:
1046 * 'str' -> 'str'
1047 * null -> ''
1048 * undefined -> ''
1049 * true -> '1'
1050 * false -> '0'
1051 * 0 -> '0'
1052 * 1 -> '1'
1053 *
1054 */
1055 const hash_key = value => {
1056 if (typeof value === 'undefined' || value === null) return null;
1057 return get_hash(value);
1058 };
1059 const get_hash = value => {
1060 if (typeof value === 'boolean') return value ? '1' : '0';
1061 return value + '';
1062 };
1063
1064 /**
1065 * Escapes a string for use within HTML.
1066 *
1067 */
1068 const escape_html = str => {
1069 return (str + '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1070 };
1071
1072 /**
1073 * use setTimeout if timeout > 0
1074 */
1075 const timeout = (fn, timeout) => {
1076 if (timeout > 0) {
1077 return window.setTimeout(fn, timeout);
1078 }
1079 fn.call(null);
1080 return null;
1081 };
1082
1083 /**
1084 * Debounce the user provided load function
1085 *
1086 */
1087 const loadDebounce = (fn, delay) => {
1088 var timeout;
1089 return function (value, callback) {
1090 var self = this;
1091 if (timeout) {
1092 self.loading = Math.max(self.loading - 1, 0);
1093 clearTimeout(timeout);
1094 }
1095 timeout = setTimeout(function () {
1096 timeout = null;
1097 self.loadedSearches[value] = true;
1098 fn.call(self, value, callback);
1099 }, delay);
1100 };
1101 };
1102
1103 /**
1104 * Debounce all fired events types listed in `types`
1105 * while executing the provided `fn`.
1106 *
1107 */
1108 const debounce_events = (self, types, fn) => {
1109 var type;
1110 var trigger = self.trigger;
1111 var event_args = {};
1112
1113 // override trigger method
1114 self.trigger = function () {
1115 var type = arguments[0];
1116 if (types.indexOf(type) !== -1) {
1117 event_args[type] = arguments;
1118 } else {
1119 return trigger.apply(self, arguments);
1120 }
1121 };
1122
1123 // invoke provided function
1124 fn.apply(self, []);
1125 self.trigger = trigger;
1126
1127 // trigger queued events
1128 for (type of types) {
1129 if (type in event_args) {
1130 trigger.apply(self, event_args[type]);
1131 }
1132 }
1133 };
1134
1135 /**
1136 * Determines the current selection within a text input control.
1137 * Returns an object containing:
1138 * - start
1139 * - length
1140 *
1141 * Note: "selectionStart, selectionEnd ... apply only to inputs of types text, search, URL, tel and password"
1142 * - https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange
1143 */
1144 const getSelection = input => {
1145 return {
1146 start: input.selectionStart || 0,
1147 length: (input.selectionEnd || 0) - (input.selectionStart || 0)
1148 };
1149 };
1150
1151 /**
1152 * Prevent default
1153 *
1154 */
1155 const preventDefault = (evt, stop = false) => {
1156 if (evt) {
1157 evt.preventDefault();
1158 if (stop) {
1159 evt.stopPropagation();
1160 }
1161 }
1162 };
1163
1164 /**
1165 * Add event helper
1166 *
1167 */
1168 const addEvent = (target, type, callback, options) => {
1169 target.addEventListener(type, callback, options);
1170 };
1171
1172 /**
1173 * Return true if the requested key is down
1174 * Will return false if more than one control character is pressed ( when [ctrl+shift+a] != [ctrl+a] )
1175 * The current evt may not always set ( eg calling advanceSelection() )
1176 *
1177 */
1178 const isKeyDown = (key_name, evt) => {
1179 if (!evt) {
1180 return false;
1181 }
1182 if (!evt[key_name]) {
1183 return false;
1184 }
1185 var count = (evt.altKey ? 1 : 0) + (evt.ctrlKey ? 1 : 0) + (evt.shiftKey ? 1 : 0) + (evt.metaKey ? 1 : 0);
1186 if (count === 1) {
1187 return true;
1188 }
1189 return false;
1190 };
1191
1192 /**
1193 * Get the id of an element
1194 * If the id attribute is not set, set the attribute with the given id
1195 *
1196 */
1197 const getId = (el, id) => {
1198 const existing_id = el.getAttribute('id');
1199 if (existing_id) {
1200 return existing_id;
1201 }
1202 el.setAttribute('id', id);
1203 return id;
1204 };
1205
1206 /**
1207 * Returns a string with backslashes added before characters that need to be escaped.
1208 */
1209 const addSlashes = str => {
1210 return str.replace(/[\\"']/g, '\\$&');
1211 };
1212
1213 /**
1214 *
1215 */
1216 const append = (parent, node) => {
1217 if (node) parent.append(node);
1218 };
1219
1220 /**
1221 * Iterates over arrays and hashes.
1222 *
1223 * ```
1224 * iterate(this.items, function(item, id) {
1225 * // invoked for each item
1226 * });
1227 * ```
1228 *
1229 */
1230 const iterate = (object, callback) => {
1231 if (Array.isArray(object)) {
1232 object.forEach(callback);
1233 } else {
1234 for (var key in object) {
1235 if (object.hasOwnProperty(key)) {
1236 callback(object[key], key);
1237 }
1238 }
1239 }
1240 };
1241
1242 /**
1243 * Return a dom element from either a dom query string, jQuery object, a dom element or html string
1244 * https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
1245 *
1246 * param query should be {}
1247 */
1248 const getDom = query => {
1249 if (query.jquery) {
1250 return query[0];
1251 }
1252 if (query instanceof HTMLElement) {
1253 return query;
1254 }
1255 if (isHtmlString(query)) {
1256 var tpl = document.createElement('template');
1257 tpl.innerHTML = query.trim(); // Never return a text node of whitespace as the result
1258 return tpl.content.firstChild;
1259 }
1260 return document.querySelector(query);
1261 };
1262 const isHtmlString = arg => {
1263 if (typeof arg === 'string' && arg.indexOf('<') > -1) {
1264 return true;
1265 }
1266 return false;
1267 };
1268 const escapeQuery = query => {
1269 return query.replace(/['"\\]/g, '\\$&');
1270 };
1271
1272 /**
1273 * Dispatch an event
1274 *
1275 */
1276 const triggerEvent = (dom_el, event_name) => {
1277 var event = document.createEvent('HTMLEvents');
1278 event.initEvent(event_name, true, false);
1279 dom_el.dispatchEvent(event);
1280 };
1281
1282 /**
1283 * Apply CSS rules to a dom element
1284 *
1285 */
1286 const applyCSS = (dom_el, css) => {
1287 Object.assign(dom_el.style, css);
1288 };
1289
1290 /**
1291 * Add css classes
1292 *
1293 */
1294 const addClasses = (elmts, ...classes) => {
1295 var norm_classes = classesArray(classes);
1296 elmts = castAsArray(elmts);
1297 elmts.map(el => {
1298 norm_classes.map(cls => {
1299 el.classList.add(cls);
1300 });
1301 });
1302 };
1303
1304 /**
1305 * Remove css classes
1306 *
1307 */
1308 const removeClasses = (elmts, ...classes) => {
1309 var norm_classes = classesArray(classes);
1310 elmts = castAsArray(elmts);
1311 elmts.map(el => {
1312 norm_classes.map(cls => {
1313 el.classList.remove(cls);
1314 });
1315 });
1316 };
1317
1318 /**
1319 * Return arguments
1320 *
1321 */
1322 const classesArray = args => {
1323 var classes = [];
1324 iterate(args, _classes => {
1325 if (typeof _classes === 'string') {
1326 _classes = _classes.trim().split(/[\t\n\f\r\s]/);
1327 }
1328 if (Array.isArray(_classes)) {
1329 classes = classes.concat(_classes);
1330 }
1331 });
1332 return classes.filter(Boolean);
1333 };
1334
1335 /**
1336 * Create an array from arg if it's not already an array
1337 *
1338 */
1339 const castAsArray = arg => {
1340 if (!Array.isArray(arg)) {
1341 arg = [arg];
1342 }
1343 return arg;
1344 };
1345
1346 /**
1347 * Get the closest node to the evt.target matching the selector
1348 * Stops at wrapper
1349 *
1350 */
1351 const parentMatch = (target, selector, wrapper) => {
1352 if (wrapper && !wrapper.contains(target)) {
1353 return;
1354 }
1355 while (target && target.matches) {
1356 if (target.matches(selector)) {
1357 return target;
1358 }
1359 target = target.parentNode;
1360 }
1361 };
1362
1363 /**
1364 * Get the first or last item from an array
1365 *
1366 * > 0 - right (last)
1367 * <= 0 - left (first)
1368 *
1369 */
1370 const getTail = (list, direction = 0) => {
1371 if (direction > 0) {
1372 return list[list.length - 1];
1373 }
1374 return list[0];
1375 };
1376
1377 /**
1378 * Return true if an object is empty
1379 *
1380 */
1381 const isEmptyObject = obj => {
1382 return Object.keys(obj).length === 0;
1383 };
1384
1385 /**
1386 * Get the index of an element amongst sibling nodes of the same type
1387 *
1388 */
1389 const nodeIndex = (el, amongst) => {
1390 if (!el) return -1;
1391 amongst = amongst || el.nodeName;
1392 var i = 0;
1393 while (el = el.previousElementSibling) {
1394 if (el.matches(amongst)) {
1395 i++;
1396 }
1397 }
1398 return i;
1399 };
1400
1401 /**
1402 * Set attributes of an element
1403 *
1404 */
1405 const setAttr = (el, attrs) => {
1406 iterate(attrs, (val, attr) => {
1407 if (val == null) {
1408 el.removeAttribute(attr);
1409 } else {
1410 el.setAttribute(attr, '' + val);
1411 }
1412 });
1413 };
1414
1415 /**
1416 * Replace a node
1417 */
1418 const replaceNode = (existing, replacement) => {
1419 if (existing.parentNode) existing.parentNode.replaceChild(replacement, existing);
1420 };
1421
1422 /**
1423 * highlight v3 | MIT license | Johann Burkard <[email protected]>
1424 * Highlights arbitrary terms in a node.
1425 *
1426 * - Modified by Marshal <[email protected]> 2011-6-24 (added regex)
1427 * - Modified by Brian Reavis <[email protected]> 2012-8-27 (cleanup)
1428 */
1429
1430 const highlight = (element, regex) => {
1431 if (regex === null) return;
1432
1433 // convet string to regex
1434 if (typeof regex === 'string') {
1435 if (!regex.length) return;
1436 regex = new RegExp(regex, 'i');
1437 }
1438
1439 // Wrap matching part of text node with highlighting <span>, e.g.
1440 // Soccer -> <span class="highlight">Soc</span>cer for regex = /soc/i
1441 const highlightText = node => {
1442 var match = node.data.match(regex);
1443 if (match && node.data.length > 0) {
1444 var spannode = document.createElement('span');
1445 spannode.className = 'highlight';
1446 var middlebit = node.splitText(match.index);
1447 middlebit.splitText(match[0].length);
1448 var middleclone = middlebit.cloneNode(true);
1449 spannode.appendChild(middleclone);
1450 replaceNode(middlebit, spannode);
1451 return 1;
1452 }
1453 return 0;
1454 };
1455
1456 // Recurse element node, looking for child text nodes to highlight, unless element
1457 // is childless, <script>, <style>, or already highlighted: <span class="hightlight">
1458 const highlightChildren = node => {
1459 if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName) && (node.className !== 'highlight' || node.tagName !== 'SPAN')) {
1460 Array.from(node.childNodes).forEach(element => {
1461 highlightRecursive(element);
1462 });
1463 }
1464 };
1465 const highlightRecursive = node => {
1466 if (node.nodeType === 3) {
1467 return highlightText(node);
1468 }
1469 highlightChildren(node);
1470 return 0;
1471 };
1472 highlightRecursive(element);
1473 };
1474
1475 /**
1476 * removeHighlight fn copied from highlight v5 and
1477 * edited to remove with(), pass js strict mode, and use without jquery
1478 */
1479 const removeHighlight = el => {
1480 var elements = el.querySelectorAll("span.highlight");
1481 Array.prototype.forEach.call(elements, function (el) {
1482 var parent = el.parentNode;
1483 parent.replaceChild(el.firstChild, el);
1484 parent.normalize();
1485 });
1486 };
1487
1488 const KEY_A = 65;
1489 const KEY_RETURN = 13;
1490 const KEY_ESC = 27;
1491 const KEY_LEFT = 37;
1492 const KEY_UP = 38;
1493 const KEY_RIGHT = 39;
1494 const KEY_DOWN = 40;
1495 const KEY_BACKSPACE = 8;
1496 const KEY_DELETE = 46;
1497 const KEY_TAB = 9;
1498 const IS_MAC = typeof navigator === 'undefined' ? false : /Mac/.test(navigator.userAgent);
1499 const KEY_SHORTCUT = IS_MAC ? 'metaKey' : 'ctrlKey'; // ctrl key or apple key for ma
1500
1501 var defaults = {
1502 options: [],
1503 optgroups: [],
1504 plugins: [],
1505 delimiter: ',',
1506 splitOn: null,
1507 // regexp or string for splitting up values from a paste command
1508 persist: true,
1509 diacritics: true,
1510 create: null,
1511 createOnBlur: false,
1512 createFilter: null,
1513 clearAfterSelect: false,
1514 highlight: true,
1515 openOnFocus: true,
1516 shouldOpen: null,
1517 maxOptions: 50,
1518 maxItems: null,
1519 hideSelected: null,
1520 duplicates: false,
1521 addPrecedence: false,
1522 selectOnTab: false,
1523 preload: null,
1524 allowEmptyOption: false,
1525 //closeAfterSelect: false,
1526 refreshThrottle: 300,
1527 loadThrottle: 300,
1528 loadingClass: 'loading',
1529 dataAttr: null,
1530 //'data-data',
1531 optgroupField: 'optgroup',
1532 valueField: 'value',
1533 labelField: 'text',
1534 disabledField: 'disabled',
1535 optgroupLabelField: 'label',
1536 optgroupValueField: 'value',
1537 lockOptgroupOrder: false,
1538 sortField: '$order',
1539 searchField: ['text'],
1540 searchConjunction: 'and',
1541 mode: null,
1542 wrapperClass: 'ts-wrapper',
1543 controlClass: 'ts-control',
1544 dropdownClass: 'ts-dropdown',
1545 dropdownContentClass: 'ts-dropdown-content',
1546 itemClass: 'item',
1547 optionClass: 'option',
1548 dropdownParent: null,
1549 controlInput: '<input type="text" autocomplete="off" size="1" />',
1550 copyClassesToDropdown: false,
1551 placeholder: null,
1552 hidePlaceholder: null,
1553 shouldLoad: function (query) {
1554 return query.length > 0;
1555 },
1556 /*
1557 load : null, // function(query, callback) { ... }
1558 score : null, // function(search) { ... }
1559 onInitialize : null, // function() { ... }
1560 onChange : null, // function(value) { ... }
1561 onItemAdd : null, // function(value, $item) { ... }
1562 onItemRemove : null, // function(value) { ... }
1563 onClear : null, // function() { ... }
1564 onOptionAdd : null, // function(value, data) { ... }
1565 onOptionRemove : null, // function(value) { ... }
1566 onOptionClear : null, // function() { ... }
1567 onOptionGroupAdd : null, // function(id, data) { ... }
1568 onOptionGroupRemove : null, // function(id) { ... }
1569 onOptionGroupClear : null, // function() { ... }
1570 onDropdownOpen : null, // function(dropdown) { ... }
1571 onDropdownClose : null, // function(dropdown) { ... }
1572 onType : null, // function(str) { ... }
1573 onDelete : null, // function(values) { ... }
1574 */
1575
1576 render: {
1577 /*
1578 item: null,
1579 optgroup: null,
1580 optgroup_header: null,
1581 option: null,
1582 option_create: null
1583 */
1584 }
1585 };
1586
1587 function getSettings(input, settings_user) {
1588 var settings = Object.assign({}, defaults, settings_user);
1589 var attr_data = settings.dataAttr;
1590 var field_label = settings.labelField;
1591 var field_value = settings.valueField;
1592 var field_disabled = settings.disabledField;
1593 var field_optgroup = settings.optgroupField;
1594 var field_optgroup_label = settings.optgroupLabelField;
1595 var field_optgroup_value = settings.optgroupValueField;
1596 var tag_name = input.tagName.toLowerCase();
1597 var placeholder = input.getAttribute('placeholder') || input.getAttribute('data-placeholder');
1598 if (!placeholder && !settings.allowEmptyOption) {
1599 let option = input.querySelector('option[value=""]');
1600 if (option) {
1601 placeholder = option.textContent;
1602 }
1603 }
1604 var settings_element = {
1605 placeholder: placeholder,
1606 options: [],
1607 optgroups: [],
1608 items: [],
1609 maxItems: null
1610 };
1611
1612 /**
1613 * Initialize from a <select> element.
1614 *
1615 */
1616 var init_select = () => {
1617 var tagName;
1618 var options = settings_element.options;
1619 var optionsMap = {};
1620 var group_count = 1;
1621 let $order = 0;
1622 var readData = el => {
1623 var data = Object.assign({}, el.dataset); // get plain object from DOMStringMap
1624 var json = attr_data && data[attr_data];
1625 if (typeof json === 'string' && json.length) {
1626 data = Object.assign(data, JSON.parse(json));
1627 }
1628 return data;
1629 };
1630 var addOption = (option, group) => {
1631 var value = hash_key(option.value);
1632 if (value == null) return;
1633 if (!value && !settings.allowEmptyOption) return;
1634
1635 // if the option already exists, it's probably been
1636 // duplicated in another optgroup. in this case, push
1637 // the current group to the "optgroup" property on the
1638 // existing option so that it's rendered in both places.
1639 if (optionsMap.hasOwnProperty(value)) {
1640 if (group) {
1641 var arr = optionsMap[value][field_optgroup];
1642 if (!arr) {
1643 optionsMap[value][field_optgroup] = group;
1644 } else if (!Array.isArray(arr)) {
1645 optionsMap[value][field_optgroup] = [arr, group];
1646 } else {
1647 arr.push(group);
1648 }
1649 }
1650 } else {
1651 var option_data = readData(option);
1652 option_data[field_label] = option_data[field_label] || option.textContent;
1653 option_data[field_value] = option_data[field_value] || value;
1654 option_data[field_disabled] = option_data[field_disabled] || option.disabled;
1655 option_data[field_optgroup] = option_data[field_optgroup] || group;
1656 option_data.$option = option;
1657 option_data.$order = option_data.$order || ++$order;
1658 optionsMap[value] = option_data;
1659 options.push(option_data);
1660 }
1661 if (option.selected) {
1662 settings_element.items.push(value);
1663 }
1664 };
1665 var addGroup = optgroup => {
1666 var id, optgroup_data;
1667 optgroup_data = readData(optgroup);
1668 optgroup_data[field_optgroup_label] = optgroup_data[field_optgroup_label] || optgroup.getAttribute('label') || '';
1669 optgroup_data[field_optgroup_value] = optgroup_data[field_optgroup_value] || group_count++;
1670 optgroup_data[field_disabled] = optgroup_data[field_disabled] || optgroup.disabled;
1671 optgroup_data.$order = optgroup_data.$order || ++$order;
1672 settings_element.optgroups.push(optgroup_data);
1673 id = optgroup_data[field_optgroup_value];
1674 iterate(optgroup.children, option => {
1675 addOption(option, id);
1676 });
1677 };
1678 settings_element.maxItems = input.hasAttribute('multiple') ? null : 1;
1679 iterate(input.children, child => {
1680 tagName = child.tagName.toLowerCase();
1681 if (tagName === 'optgroup') {
1682 addGroup(child);
1683 } else if (tagName === 'option') {
1684 addOption(child);
1685 }
1686 });
1687 };
1688
1689 /**
1690 * Initialize from a <input type="text"> element.
1691 *
1692 */
1693 var init_textbox = () => {
1694 const data_raw = input.getAttribute(attr_data);
1695 if (!data_raw) {
1696 var _input$value$trim, _input$value;
1697 var value = (_input$value$trim = input == null || (_input$value = input.value) == null ? void 0 : _input$value.trim()) != null ? _input$value$trim : '';
1698 if (!settings.allowEmptyOption && !value.length) return;
1699 const values = value.split(settings.delimiter);
1700 iterate(values, value => {
1701 const option = {};
1702 option[field_label] = value;
1703 option[field_value] = value;
1704 settings_element.options.push(option);
1705 });
1706 settings_element.items = values;
1707 } else {
1708 settings_element.options = JSON.parse(data_raw);
1709 iterate(settings_element.options, opt => {
1710 settings_element.items.push(opt[field_value]);
1711 });
1712 }
1713 };
1714 if (tag_name === 'select') {
1715 init_select();
1716 } else {
1717 init_textbox();
1718 }
1719 return Object.assign({}, defaults, settings_element, settings_user);
1720 }
1721
1722 var instance_i = 0;
1723 class TomSelect extends MicroPlugin(MicroEvent) {
1724 constructor(input_arg, user_settings) {
1725 super();
1726 this.order = 0;
1727 this.isOpen = false;
1728 this.isDisabled = false;
1729 this.isReadOnly = false;
1730 this.isInvalid = false;
1731 // @deprecated 1.8
1732 this.isValid = true;
1733 this.isLocked = false;
1734 this.isFocused = false;
1735 this.isInputHidden = false;
1736 this.isSetup = false;
1737 this.isDropdownContentStale = true;
1738 this.ignoreFocus = false;
1739 this.ignoreHover = false;
1740 this.hasOptions = false;
1741 this.lastValue = '';
1742 this.caretPos = 0;
1743 this.loading = 0;
1744 this.loadedSearches = {};
1745 this.activeOption = null;
1746 this.activeItems = [];
1747 this.optgroups = {};
1748 this.options = {};
1749 this.userOptions = {};
1750 this.items = [];
1751 this.refreshTimeout = null;
1752 instance_i++;
1753 var dir;
1754 var input = getDom(input_arg);
1755 if (input.tomselect) {
1756 throw new Error('Tom Select already initialized on this element');
1757 }
1758 input.tomselect = this;
1759
1760 // detect rtl environment
1761 var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null);
1762 dir = computedStyle.getPropertyValue('direction');
1763
1764 // setup default state
1765 const settings = getSettings(input, user_settings);
1766 this.settings = settings;
1767 this.input = input;
1768 this.tabIndex = input.tabIndex || 0;
1769 this.is_select_tag = input.tagName.toLowerCase() === 'select';
1770 this.rtl = /rtl/i.test(dir);
1771 this.inputId = getId(input, 'tomselect-' + instance_i);
1772 this.isRequired = input.required;
1773
1774 // search system
1775 this.sifter = new Sifter(this.options, {
1776 diacritics: settings.diacritics
1777 });
1778
1779 // option-dependent defaults
1780 settings.mode = settings.mode || (settings.maxItems === 1 ? 'single' : 'multi');
1781 if (typeof settings.hideSelected !== 'boolean') {
1782 settings.hideSelected = settings.mode === 'multi';
1783 }
1784 if (typeof settings.hidePlaceholder !== 'boolean') {
1785 settings.hidePlaceholder = settings.mode !== 'multi';
1786 }
1787
1788 // set up createFilter callback
1789 var filter = settings.createFilter;
1790 if (typeof filter !== 'function') {
1791 if (typeof filter === 'string') {
1792 filter = new RegExp(filter);
1793 }
1794 if (filter instanceof RegExp) {
1795 settings.createFilter = input => filter.test(input);
1796 } else {
1797 settings.createFilter = value => {
1798 return this.settings.duplicates || !this.options[value];
1799 };
1800 }
1801 }
1802 this.initializePlugins(settings.plugins);
1803 this.setupCallbacks();
1804 this.setupTemplates();
1805
1806 // Create all elements
1807 const wrapper = getDom('<div>');
1808 const control = getDom('<div>');
1809 const dropdown = this._render('dropdown');
1810 const dropdown_content = getDom(`<div role="listbox" tabindex="-1">`);
1811 const classes = this.input.getAttribute('class') || '';
1812 const inputMode = settings.mode;
1813 var control_input;
1814 addClasses(wrapper, settings.wrapperClass, classes, inputMode);
1815 addClasses(control, settings.controlClass);
1816 append(wrapper, control);
1817 addClasses(dropdown, settings.dropdownClass, inputMode);
1818 if (settings.copyClassesToDropdown) {
1819 addClasses(dropdown, classes);
1820 }
1821 addClasses(dropdown_content, settings.dropdownContentClass);
1822 append(dropdown, dropdown_content);
1823 getDom(settings.dropdownParent || wrapper).appendChild(dropdown);
1824
1825 // default controlInput
1826 if (isHtmlString(settings.controlInput)) {
1827 control_input = getDom(settings.controlInput);
1828
1829 // set attributes
1830 var attrs = ['autocorrect', 'autocapitalize', 'autocomplete', 'spellcheck', 'aria-label'];
1831 iterate(attrs, attr => {
1832 if (input.getAttribute(attr)) {
1833 setAttr(control_input, {
1834 [attr]: input.getAttribute(attr)
1835 });
1836 }
1837 });
1838 control_input.tabIndex = -1;
1839 control.appendChild(control_input);
1840 this.focus_node = control_input;
1841
1842 // dom element
1843 } else if (settings.controlInput) {
1844 control_input = getDom(settings.controlInput);
1845 this.focus_node = control_input;
1846 } else {
1847 control_input = getDom('<input/>');
1848 this.focus_node = control;
1849 }
1850 this.wrapper = wrapper;
1851 this.dropdown = dropdown;
1852 this.dropdown_content = dropdown_content;
1853 this.control = control;
1854 this.control_input = control_input;
1855 this.setup();
1856 }
1857
1858 /**
1859 * set up event bindings.
1860 *
1861 */
1862 setup() {
1863 const self = this;
1864 const settings = self.settings;
1865 const control_input = self.control_input;
1866 const dropdown = self.dropdown;
1867 const dropdown_content = self.dropdown_content;
1868 const wrapper = self.wrapper;
1869 const control = self.control;
1870 const input = self.input;
1871 const focus_node = self.focus_node;
1872 const passive_event = {
1873 passive: true
1874 };
1875 const listboxId = self.inputId + '-ts-dropdown';
1876 setAttr(dropdown_content, {
1877 id: listboxId
1878 });
1879 setAttr(focus_node, {
1880 role: 'combobox',
1881 'aria-haspopup': 'listbox',
1882 'aria-expanded': 'false',
1883 'aria-controls': listboxId
1884 });
1885 const control_id = getId(focus_node, self.inputId + '-ts-control');
1886 const query = "label[for='" + escapeQuery(self.inputId) + "']";
1887 const label = document.querySelector(query);
1888 const label_click = self.focus.bind(self);
1889 if (label) {
1890 addEvent(label, 'click', label_click);
1891 setAttr(label, {
1892 for: control_id
1893 });
1894 const label_id = getId(label, self.inputId + '-ts-label');
1895 setAttr(focus_node, {
1896 'aria-labelledby': label_id
1897 });
1898 setAttr(dropdown_content, {
1899 'aria-labelledby': label_id
1900 });
1901 }
1902 wrapper.style.width = input.style.width;
1903 wrapper.style.minWidth = input.style.minWidth;
1904 wrapper.style.maxWidth = input.style.maxWidth;
1905 if (self.plugins.names.length) {
1906 const classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-');
1907 addClasses([wrapper, dropdown], classes_plugins);
1908 }
1909 if ((settings.maxItems === null || settings.maxItems > 1) && self.is_select_tag) {
1910 setAttr(input, {
1911 multiple: 'multiple'
1912 });
1913 }
1914 if (settings.placeholder) {
1915 setAttr(control_input, {
1916 placeholder: settings.placeholder
1917 });
1918 }
1919
1920 // if splitOn was not passed in, construct it from the delimiter to allow pasting universally
1921 if (!settings.splitOn && settings.delimiter) {
1922 settings.splitOn = new RegExp('\\s*' + escape_regex(settings.delimiter) + '+\\s*');
1923 }
1924
1925 // debounce user defined load() if loadThrottle > 0
1926 // after initializePlugins() so plugins can create/modify user defined loaders
1927 if (settings.load && settings.loadThrottle) {
1928 settings.load = loadDebounce(settings.load, settings.loadThrottle);
1929 }
1930 addEvent(dropdown, 'mousemove', () => {
1931 self.ignoreHover = false;
1932 });
1933 addEvent(dropdown, 'mouseenter', e => {
1934 var target_match = parentMatch(e.target, '[data-selectable]', dropdown);
1935 if (target_match) self.onOptionHover(e, target_match);
1936 }, {
1937 capture: true
1938 });
1939
1940 // clicking on an option should select it
1941 addEvent(dropdown, 'click', evt => {
1942 const option = parentMatch(evt.target, '[data-selectable]');
1943 if (option) {
1944 self.onOptionSelect(evt, option);
1945 preventDefault(evt, true);
1946 }
1947 });
1948 addEvent(control, 'click', evt => {
1949 var target_match = parentMatch(evt.target, '[data-ts-item]', control);
1950 if (target_match && self.onItemSelect(evt, target_match)) {
1951 preventDefault(evt, true);
1952 return;
1953 }
1954
1955 // retain focus (see control_input mousedown)
1956 if (control_input.value != '') {
1957 return;
1958 }
1959 self.onClick();
1960 preventDefault(evt, true);
1961 });
1962
1963 // keydown on focus_node for arrow_down/arrow_up
1964 addEvent(focus_node, 'keydown', e => self.onKeyDown(e));
1965
1966 // keypress and input/keyup
1967 addEvent(control_input, 'keypress', e => self.onKeyPress(e));
1968 addEvent(control_input, 'input', e => self.onInput(e));
1969 addEvent(focus_node, 'blur', e => self.onBlur(e));
1970 addEvent(focus_node, 'focus', e => self.onFocus(e));
1971 addEvent(control_input, 'paste', e => self.onPaste(e));
1972 const doc_mousedown = evt => {
1973 // blur if target is outside of this instance
1974 // dropdown is not always inside wrapper
1975 const target = evt.composedPath()[0];
1976 if (!wrapper.contains(target) && !dropdown.contains(target)) {
1977 if (self.isFocused) {
1978 self.blur();
1979 }
1980 self.inputState();
1981 return;
1982 }
1983
1984 // retain focus by preventing native handling. if the
1985 // event target is the input it should not be modified.
1986 // otherwise, text selection within the input won't work.
1987 // Fixes bug #212 which is no covered by tests
1988 if (target == control_input && self.isOpen) {
1989 evt.stopPropagation();
1990
1991 // clicking anywhere in the control should not blur the control_input (which would close the dropdown)
1992 } else {
1993 preventDefault(evt, true);
1994 }
1995 };
1996 const win_scroll = () => {
1997 if (self.isOpen) {
1998 self.positionDropdown();
1999 }
2000 };
2001 const input_invalid = () => {
2002 if (self.isValid) {
2003 self.isValid = false;
2004 self.isInvalid = true;
2005 self.refreshState();
2006 }
2007 };
2008 addEvent(input, 'invalid', input_invalid);
2009 addEvent(document, 'mousedown', doc_mousedown);
2010 addEvent(window, 'scroll', win_scroll, passive_event);
2011 addEvent(window, 'resize', win_scroll, passive_event);
2012 this._destroy = () => {
2013 input.removeEventListener('invalid', input_invalid);
2014 document.removeEventListener('mousedown', doc_mousedown);
2015 window.removeEventListener('scroll', win_scroll);
2016 window.removeEventListener('resize', win_scroll);
2017 if (label) label.removeEventListener('click', label_click);
2018 };
2019
2020 // store original html and tab index so that they can be
2021 // restored when the destroy() method is called.
2022 this.revertSettings = {
2023 innerHTML: input.innerHTML,
2024 tabIndex: input.tabIndex
2025 };
2026 input.tabIndex = -1;
2027 input.insertAdjacentElement('afterend', self.wrapper);
2028 self.sync(false);
2029 settings.items = [];
2030 delete settings.optgroups;
2031 delete settings.options;
2032 self.refreshItems();
2033 self.close(false);
2034 self.inputState();
2035 self.isSetup = true;
2036 self.on('change', this.onChange);
2037 addClasses(input, 'tomselected', 'ts-hidden-accessible');
2038 self.trigger('initialize');
2039
2040 // preload options
2041 if (settings.preload === true) {
2042 self.preload();
2043 }
2044 }
2045
2046 /**
2047 * Register options and optgroups
2048 *
2049 */
2050 setupOptions(options = [], optgroups = []) {
2051 // build options table
2052 this.addOptions(options);
2053
2054 // build optgroup table
2055 iterate(optgroups, optgroup => {
2056 this.registerOptionGroup(optgroup);
2057 });
2058 }
2059
2060 /**
2061 * Sets up default rendering functions.
2062 */
2063 setupTemplates() {
2064 var self = this;
2065 var field_label = self.settings.labelField;
2066 var field_optgroup = self.settings.optgroupLabelField;
2067 var templates = {
2068 'optgroup': data => {
2069 let optgroup = document.createElement('div');
2070 optgroup.className = 'optgroup';
2071 optgroup.appendChild(data.options);
2072 return optgroup;
2073 },
2074 'optgroup_header': (data, escape) => {
2075 return '<div class="optgroup-header">' + escape(data[field_optgroup]) + '</div>';
2076 },
2077 'option': (data, escape) => {
2078 return '<div>' + escape(data[field_label]) + '</div>';
2079 },
2080 'item': (data, escape) => {
2081 return '<div>' + escape(data[field_label]) + '</div>';
2082 },
2083 'option_create': (data, escape) => {
2084 return '<div class="create">Add <strong>' + escape(data.input) + '</strong>&hellip;</div>';
2085 },
2086 'no_results': () => {
2087 return '<div class="no-results">No results found</div>';
2088 },
2089 'loading': () => {
2090 return '<div class="spinner"></div>';
2091 },
2092 'not_loading': () => { },
2093 'dropdown': () => {
2094 return '<div></div>';
2095 }
2096 };
2097 self.settings.render = Object.assign({}, templates, self.settings.render);
2098 }
2099
2100 /**
2101 * Maps fired events to callbacks provided
2102 * in the settings used when creating the control.
2103 */
2104 setupCallbacks() {
2105 var key, fn;
2106 var callbacks = {
2107 'initialize': 'onInitialize',
2108 'change': 'onChange',
2109 'item_add': 'onItemAdd',
2110 'item_remove': 'onItemRemove',
2111 'item_select': 'onItemSelect',
2112 'clear': 'onClear',
2113 'option_add': 'onOptionAdd',
2114 'option_remove': 'onOptionRemove',
2115 'option_clear': 'onOptionClear',
2116 'optgroup_add': 'onOptionGroupAdd',
2117 'optgroup_remove': 'onOptionGroupRemove',
2118 'optgroup_clear': 'onOptionGroupClear',
2119 'dropdown_open': 'onDropdownOpen',
2120 'dropdown_close': 'onDropdownClose',
2121 'type': 'onType',
2122 'load': 'onLoad',
2123 'focus': 'onFocus',
2124 'blur': 'onBlur'
2125 };
2126 for (key in callbacks) {
2127 fn = this.settings[callbacks[key]];
2128 if (fn) this.on(key, fn);
2129 }
2130 }
2131
2132 /**
2133 * Sync the Tom Select instance with the original input or select
2134 *
2135 */
2136 sync(get_settings = true) {
2137 const self = this;
2138 const settings = get_settings ? getSettings(self.input, {
2139 delimiter: self.settings.delimiter,
2140 allowEmptyOption: self.settings.allowEmptyOption
2141 }) : self.settings;
2142 self.setupOptions(settings.options, settings.optgroups);
2143 self.setValue(settings.items || [], true); // silent prevents recursion
2144
2145 if (self.input.disabled) {
2146 self.disable();
2147 } else if (self.input.readOnly) {
2148 self.setReadOnly(true);
2149 } else {
2150 self.enable(); //sets tabIndex
2151 }
2152 self.lastQuery = null; // so updated options will be displayed in dropdown
2153 }
2154
2155 /**
2156 * Triggered when the main control element
2157 * has a click event.
2158 *
2159 */
2160 onClick() {
2161 var self = this;
2162 if (self.activeItems.length > 0) {
2163 self.clearActiveItems();
2164 self.focus();
2165 return;
2166 }
2167 if (self.isFocused && self.isOpen) {
2168 self.blur();
2169 } else {
2170 self.focus();
2171 }
2172 }
2173
2174 /**
2175 * @deprecated v1.7
2176 *
2177 */
2178 onMouseDown() { }
2179
2180 /**
2181 * Triggered when the value of the control has been changed.
2182 * This should propagate the event to the original DOM
2183 * input / select element.
2184 */
2185 onChange() {
2186 triggerEvent(this.input, 'input');
2187 triggerEvent(this.input, 'change');
2188 }
2189
2190 /**
2191 * Triggered on <input> paste.
2192 *
2193 */
2194 onPaste(e) {
2195 var self = this;
2196 if (self.isInputHidden || self.isLocked) {
2197 preventDefault(e);
2198 return;
2199 }
2200
2201 // If a regex or string is included, this will split the pasted
2202 // input and create Items for each separate value
2203 if (!self.settings.splitOn) {
2204 return;
2205 }
2206
2207 // Wait for pasted text to be recognized in value
2208 setTimeout(() => {
2209 var pastedText = self.inputValue();
2210 if (!pastedText.match(self.settings.splitOn)) {
2211 return;
2212 }
2213 var splitInput = pastedText.trim().split(self.settings.splitOn);
2214 iterate(splitInput, piece => {
2215 const hash = hash_key(piece);
2216 if (hash) {
2217 if (this.options[piece]) {
2218 self.addItem(piece);
2219 } else {
2220 self.createItem(piece);
2221 }
2222 }
2223 });
2224 }, 0);
2225 }
2226
2227 /**
2228 * Triggered on <input> keypress.
2229 *
2230 */
2231 onKeyPress(e) {
2232 var self = this;
2233 if (self.isLocked) {
2234 preventDefault(e);
2235 return;
2236 }
2237 var character = String.fromCharCode(e.keyCode || e.which);
2238 if (self.settings.create && self.settings.mode === 'multi' && character === self.settings.delimiter) {
2239 self.createItem();
2240 preventDefault(e);
2241 return;
2242 }
2243 }
2244
2245 /**
2246 * Triggered on <input> keydown.
2247 *
2248 */
2249 onKeyDown(e) {
2250 var self = this;
2251 self.ignoreHover = true;
2252 if (self.isLocked) {
2253 if (e.keyCode !== KEY_TAB) {
2254 preventDefault(e);
2255 }
2256 return;
2257 }
2258 switch (e.keyCode) {
2259 // ctrl+A: select all
2260 case KEY_A:
2261 if (isKeyDown(KEY_SHORTCUT, e)) {
2262 if (self.control_input.value == '') {
2263 preventDefault(e);
2264 self.selectAll();
2265 return;
2266 }
2267 }
2268 break;
2269
2270 // esc: close dropdown
2271 case KEY_ESC:
2272 if (self.isOpen) {
2273 preventDefault(e, true);
2274 self.close();
2275 }
2276 self.clearActiveItems();
2277 return;
2278
2279 // down: open dropdown or move selection down
2280 case KEY_DOWN:
2281 if (!self.isOpen && self.hasOptions) {
2282 self.open();
2283 } else if (self.activeOption) {
2284 let next = self.getAdjacent(self.activeOption, 1);
2285 if (next) self.setActiveOption(next);
2286 }
2287 preventDefault(e);
2288 return;
2289
2290 // up: move selection up
2291 case KEY_UP:
2292 if (self.activeOption) {
2293 let prev = self.getAdjacent(self.activeOption, -1);
2294 if (prev) self.setActiveOption(prev);
2295 }
2296 preventDefault(e);
2297 return;
2298
2299 // return: select active option
2300 case KEY_RETURN:
2301 if (self.canSelect(self.activeOption)) {
2302 self.onOptionSelect(e, self.activeOption);
2303 preventDefault(e);
2304
2305 // if the option_create=null, the dropdown might be closed
2306 } else if (self.settings.create && self.createItem()) {
2307 preventDefault(e);
2308
2309 // don't submit form when searching for a value
2310 } else if (document.activeElement == self.control_input && self.isOpen) {
2311 preventDefault(e);
2312 }
2313 return;
2314
2315 // left: modifiy item selection to the left
2316 case KEY_LEFT:
2317 self.advanceSelection(-1, e);
2318 return;
2319
2320 // right: modifiy item selection to the right
2321 case KEY_RIGHT:
2322 self.advanceSelection(1, e);
2323 return;
2324
2325 // tab: select active option and/or create item
2326 case KEY_TAB:
2327 if (self.settings.selectOnTab) {
2328 if (self.canSelect(self.activeOption)) {
2329 self.onOptionSelect(e, self.activeOption);
2330
2331 // prevent default [tab] behaviour of jump to the next field
2332 // if select isFull, then the dropdown won't be open and [tab] will work normally
2333 preventDefault(e);
2334 } else if (self.settings.create && self.createItem()) {
2335 preventDefault(e);
2336 }
2337 }
2338 return;
2339
2340 // delete|backspace: delete items
2341 case KEY_BACKSPACE:
2342 case KEY_DELETE:
2343 self.deleteSelection(e);
2344 return;
2345 }
2346
2347 // don't enter text in the control_input when active items are selected
2348 if (self.isInputHidden && !isKeyDown(KEY_SHORTCUT, e)) {
2349 preventDefault(e);
2350 }
2351 }
2352
2353 /**
2354 * Triggered on <input> keyup.
2355 *
2356 */
2357 onInput(e) {
2358 if (this.isLocked) {
2359 return;
2360 }
2361 const value = this.inputValue();
2362 if (this.lastValue === value) return;
2363 this.lastValue = value;
2364 if (value == '') {
2365 this._onInput();
2366 return;
2367 }
2368 if (this.refreshTimeout) {
2369 window.clearTimeout(this.refreshTimeout);
2370 }
2371 this.refreshTimeout = timeout(() => {
2372 this.refreshTimeout = null;
2373 this._onInput();
2374 }, this.settings.refreshThrottle);
2375 }
2376 _onInput() {
2377 const value = this.lastValue;
2378 if (this.settings.shouldLoad.call(this, value)) {
2379 this.load(value);
2380 }
2381 this.refreshOptions();
2382 this.trigger('type', value);
2383 }
2384
2385 /**
2386 * Triggered when the user rolls over
2387 * an option in the autocomplete dropdown menu.
2388 *
2389 */
2390 onOptionHover(evt, option) {
2391 if (this.ignoreHover) return;
2392 this.setActiveOption(option, false);
2393 }
2394
2395 /**
2396 * Triggered on <input> focus.
2397 *
2398 */
2399 onFocus(e) {
2400 var self = this;
2401 var wasFocused = self.isFocused;
2402 if (self.isDisabled || self.isReadOnly) {
2403 self.blur();
2404 preventDefault(e);
2405 return;
2406 }
2407 if (self.ignoreFocus) return;
2408 self.isFocused = true;
2409 if (self.settings.preload === 'focus') self.preload();
2410 if (!wasFocused) self.trigger('focus');
2411 if (!self.activeItems.length) {
2412 self.inputState();
2413 self.refreshOptions(!!self.settings.openOnFocus);
2414 }
2415 self.refreshState();
2416 }
2417
2418 /**
2419 * Triggered on <input> blur.
2420 *
2421 */
2422 onBlur(e) {
2423 if (document.hasFocus() === false) return;
2424 var self = this;
2425 if (!self.isFocused) return;
2426 self.isFocused = false;
2427 self.ignoreFocus = false;
2428 var deactivate = () => {
2429 self.close();
2430 self.setActiveItem();
2431 self.setCaret(self.items.length);
2432 self.trigger('blur');
2433 };
2434 if (self.settings.create && self.settings.createOnBlur) {
2435 self.createItem(null, deactivate);
2436 } else {
2437 deactivate();
2438 }
2439 }
2440
2441 /**
2442 * Triggered when the user clicks on an option
2443 * in the autocomplete dropdown menu.
2444 *
2445 */
2446 onOptionSelect(evt, option) {
2447 var value,
2448 self = this;
2449
2450 // should not be possible to trigger a option under a disabled optgroup
2451 if (option.parentElement && option.parentElement.matches('[data-disabled]')) {
2452 return;
2453 }
2454 if (option.classList.contains('create')) {
2455 self.createItem(null, () => {
2456 if (self.settings.closeAfterSelect) {
2457 self.close();
2458 } else if (self.settings.clearAfterSelect) {
2459 self.setTextboxValue();
2460 }
2461 });
2462 } else {
2463 value = option.dataset.value;
2464 if (typeof value !== 'undefined') {
2465 self.isDropdownContentStale = self.settings.hideSelected;
2466 self.addItem(value);
2467 if (self.settings.closeAfterSelect) {
2468 self.close();
2469 } else if (self.settings.clearAfterSelect) {
2470 self.setTextboxValue();
2471 }
2472 if (!self.settings.hideSelected && evt.type && /click/.test(evt.type)) {
2473 self.setActiveOption(option);
2474 }
2475 }
2476 }
2477 }
2478
2479 /**
2480 * Return true if the given option can be selected
2481 *
2482 */
2483 canSelect(option) {
2484 if (this.isOpen && option && this.dropdown_content.contains(option)) {
2485 return true;
2486 }
2487 return false;
2488 }
2489
2490 /**
2491 * Triggered when the user clicks on an item
2492 * that has been selected.
2493 *
2494 */
2495 onItemSelect(evt, item) {
2496 var self = this;
2497 if (!self.isLocked && self.settings.mode === 'multi') {
2498 preventDefault(evt);
2499 self.setActiveItem(item, evt);
2500 return true;
2501 }
2502 return false;
2503 }
2504
2505 /**
2506 * Determines whether or not to invoke
2507 * the user-provided option provider / loader
2508 *
2509 * Note, there is a subtle difference between
2510 * this.canLoad() and this.settings.shouldLoad();
2511 *
2512 * - settings.shouldLoad() is a user-input validator.
2513 * When false is returned, the not_loading template
2514 * will be added to the dropdown
2515 *
2516 * - canLoad() is lower level validator that checks
2517 * the Tom Select instance. There is no inherent user
2518 * feedback when canLoad returns false
2519 *
2520 */
2521 canLoad(value) {
2522 if (!this.settings.load) return false;
2523 if (this.loadedSearches.hasOwnProperty(value)) return false;
2524 return true;
2525 }
2526
2527 /**
2528 * Invokes the user-provided option provider / loader.
2529 *
2530 */
2531 load(value) {
2532 const self = this;
2533 if (!self.canLoad(value)) return;
2534 addClasses(self.wrapper, self.settings.loadingClass);
2535 self.loading++;
2536 const callback = self.loadCallback.bind(self);
2537 self.settings.load.call(self, value, callback);
2538 }
2539
2540 /**
2541 * Invoked by the user-provided option provider
2542 *
2543 */
2544 loadCallback(options, optgroups) {
2545 const self = this;
2546 self.loading = Math.max(self.loading - 1, 0);
2547 self.isDropdownContentStale = true;
2548 self.clearActiveOption(); // when new results load, focus should be on first option
2549 self.setupOptions(options, optgroups);
2550 self.refreshOptions(self.isFocused && !self.isInputHidden);
2551 if (!self.loading) {
2552 removeClasses(self.wrapper, self.settings.loadingClass);
2553 }
2554 self.trigger('load', options, optgroups);
2555 }
2556 preload() {
2557 var classList = this.wrapper.classList;
2558 if (classList.contains('preloaded')) return;
2559 classList.add('preloaded');
2560 this.load('');
2561 }
2562
2563 /**
2564 * Sets the input field of the control to the specified value.
2565 *
2566 */
2567 setTextboxValue(value = '') {
2568 var input = this.control_input;
2569 var changed = input.value !== value;
2570 if (changed) {
2571 input.value = value;
2572 triggerEvent(input, 'update');
2573 this.lastValue = value;
2574 }
2575 }
2576
2577 /**
2578 * Returns the value of the control. If multiple items
2579 * can be selected (e.g. <select multiple>), this returns
2580 * an array. If only one item can be selected, this
2581 * returns a string.
2582 *
2583 */
2584 getValue() {
2585 if (this.is_select_tag && this.input.hasAttribute('multiple')) {
2586 return this.items;
2587 }
2588 return this.items.join(this.settings.delimiter);
2589 }
2590
2591 /**
2592 * Resets the selected items to the given value.
2593 *
2594 */
2595 setValue(value, silent) {
2596 var events = silent ? [] : ['change'];
2597 debounce_events(this, events, () => {
2598 this.clear(silent);
2599 this.addItems(value, silent);
2600 });
2601 }
2602
2603 /**
2604 * Resets the number of max items to the given value
2605 *
2606 */
2607 setMaxItems(value) {
2608 if (value === 0) value = null; //reset to unlimited items.
2609 this.settings.maxItems = value;
2610 this.refreshState();
2611 }
2612
2613 /**
2614 * Sets the selected item.
2615 *
2616 */
2617 setActiveItem(item, e) {
2618 var self = this;
2619 var eventName;
2620 var i, begin, end, swap;
2621 var last;
2622 if (self.settings.mode === 'single') return;
2623
2624 // clear the active selection
2625 if (!item) {
2626 self.clearActiveItems();
2627 if (self.isFocused) {
2628 self.inputState();
2629 }
2630 return;
2631 }
2632
2633 // modify selection
2634 eventName = e && e.type.toLowerCase();
2635 if (eventName === 'click' && isKeyDown('shiftKey', e) && self.activeItems.length) {
2636 last = self.getLastActive();
2637 begin = Array.prototype.indexOf.call(self.control.children, last);
2638 end = Array.prototype.indexOf.call(self.control.children, item);
2639 if (begin > end) {
2640 swap = begin;
2641 begin = end;
2642 end = swap;
2643 }
2644 for (i = begin; i <= end; i++) {
2645 item = self.control.children[i];
2646 if (self.activeItems.indexOf(item) === -1) {
2647 self.setActiveItemClass(item);
2648 }
2649 }
2650 preventDefault(e);
2651 } else if (eventName === 'click' && isKeyDown(KEY_SHORTCUT, e) || eventName === 'keydown' && isKeyDown('shiftKey', e)) {
2652 if (item.classList.contains('active')) {
2653 self.removeActiveItem(item);
2654 } else {
2655 self.setActiveItemClass(item);
2656 }
2657 } else {
2658 self.clearActiveItems();
2659 self.setActiveItemClass(item);
2660 }
2661
2662 // ensure control has focus
2663 self.inputState();
2664 if (!self.isFocused) {
2665 self.focus();
2666 }
2667 }
2668
2669 /**
2670 * Set the active and last-active classes
2671 *
2672 */
2673 setActiveItemClass(item) {
2674 const self = this;
2675 const last_active = self.control.querySelector('.last-active');
2676 if (last_active) removeClasses(last_active, 'last-active');
2677 addClasses(item, 'active last-active');
2678 self.trigger('item_select', item);
2679 if (self.activeItems.indexOf(item) == -1) {
2680 self.activeItems.push(item);
2681 }
2682 }
2683
2684 /**
2685 * Remove active item
2686 *
2687 */
2688 removeActiveItem(item) {
2689 var idx = this.activeItems.indexOf(item);
2690 this.activeItems.splice(idx, 1);
2691 removeClasses(item, 'active');
2692 }
2693
2694 /**
2695 * Clears all the active items
2696 *
2697 */
2698 clearActiveItems() {
2699 removeClasses(this.activeItems, 'active');
2700 this.activeItems = [];
2701 }
2702
2703 /**
2704 * Sets the selected item in the dropdown menu
2705 * of available options.
2706 *
2707 */
2708 setActiveOption(option, scroll = true) {
2709 if (option === this.activeOption) {
2710 return;
2711 }
2712 this.clearActiveOption();
2713 if (!option) return;
2714 this.activeOption = option;
2715 setAttr(this.focus_node, {
2716 'aria-activedescendant': option.getAttribute('id')
2717 });
2718 setAttr(option, {
2719 'aria-selected': 'true'
2720 });
2721 addClasses(option, 'active');
2722 if (scroll) this.scrollToOption(option);
2723 }
2724
2725 /**
2726 * Sets the dropdown_content scrollTop to display the option
2727 *
2728 */
2729 scrollToOption(option, behavior) {
2730 if (!option) return;
2731 const content = this.dropdown_content;
2732 const height_menu = content.clientHeight;
2733 const scrollTop = content.scrollTop || 0;
2734 const height_item = option.offsetHeight;
2735 const y = option.getBoundingClientRect().top - content.getBoundingClientRect().top + scrollTop;
2736 if (y + height_item > height_menu + scrollTop) {
2737 this.scroll(y - height_menu + height_item, behavior);
2738 } else if (y < scrollTop) {
2739 this.scroll(y, behavior);
2740 }
2741 }
2742
2743 /**
2744 * Scroll the dropdown to the given position
2745 *
2746 */
2747 scroll(scrollTop, behavior) {
2748 const content = this.dropdown_content;
2749 if (behavior) {
2750 content.style.scrollBehavior = behavior;
2751 }
2752 content.scrollTop = scrollTop;
2753 content.style.scrollBehavior = '';
2754 }
2755
2756 /**
2757 * Clears the active option
2758 *
2759 */
2760 clearActiveOption() {
2761 if (this.activeOption) {
2762 removeClasses(this.activeOption, 'active');
2763 setAttr(this.activeOption, {
2764 'aria-selected': null
2765 });
2766 }
2767 this.activeOption = null;
2768 setAttr(this.focus_node, {
2769 'aria-activedescendant': null
2770 });
2771 }
2772
2773 /**
2774 * Selects all items (CTRL + A).
2775 */
2776 selectAll() {
2777 const self = this;
2778 if (self.settings.mode === 'single') return;
2779 const activeItems = self.controlChildren();
2780 if (!activeItems.length) return;
2781 self.inputState();
2782 self.close();
2783 self.activeItems = activeItems;
2784 iterate(activeItems, item => {
2785 self.setActiveItemClass(item);
2786 });
2787 }
2788
2789 /**
2790 * Determines if the control_input should be in a hidden or visible state
2791 *
2792 */
2793 inputState() {
2794 var self = this;
2795 if (!self.control.contains(self.control_input)) return;
2796 setAttr(self.control_input, {
2797 placeholder: self.settings.placeholder
2798 });
2799 if (self.activeItems.length > 0 || !self.isFocused && self.settings.hidePlaceholder && self.items.length > 0) {
2800 self.setTextboxValue();
2801 self.isInputHidden = true;
2802 } else {
2803 if (self.settings.hidePlaceholder && self.items.length > 0) {
2804 setAttr(self.control_input, {
2805 placeholder: ''
2806 });
2807 }
2808 self.isInputHidden = false;
2809 }
2810 self.wrapper.classList.toggle('input-hidden', self.isInputHidden);
2811 }
2812
2813 /**
2814 * Get the input value
2815 */
2816 inputValue() {
2817 return this.control_input.value.trim();
2818 }
2819
2820 /**
2821 * Gives the control focus.
2822 */
2823 focus() {
2824 var self = this;
2825 if (self.isDisabled || self.isReadOnly) return;
2826 self.ignoreFocus = true;
2827 const focusTarget = this.control_input.offsetWidth ? this.control_input : this.focus_node;
2828 focusTarget.focus();
2829 setTimeout(() => {
2830 self.ignoreFocus = false;
2831 // Fix https://github.com/orchidjs/tom-select/issues/806
2832 // Only proceed if this instance's element is still the active element. If Edge autofill
2833 // (or anything else) has moved focus to a different element in the interim, calling
2834 // onFocus() here would steal focus back and restart the cascade loop.
2835 const root = focusTarget.getRootNode();
2836 if (root.activeElement !== focusTarget) {
2837 return;
2838 }
2839 this.onFocus();
2840 }, 0);
2841 }
2842
2843 /**
2844 * Forces the control out of focus.
2845 *
2846 */
2847 blur() {
2848 this.focus_node.blur();
2849 this.onBlur();
2850 }
2851
2852 /**
2853 * Returns a function that scores an object
2854 * to show how good of a match it is to the
2855 * provided query.
2856 *
2857 * @return {function}
2858 */
2859 getScoreFunction(query) {
2860 return this.sifter.getScoreFunction(query, this.getSearchOptions());
2861 }
2862
2863 /**
2864 * Returns search options for sifter (the system
2865 * for scoring and sorting results).
2866 *
2867 * @see https://github.com/orchidjs/sifter.js
2868 * @return {object}
2869 */
2870 getSearchOptions() {
2871 var settings = this.settings;
2872 var sort = settings.sortField;
2873 if (typeof settings.sortField === 'string') {
2874 sort = [{
2875 field: settings.sortField
2876 }];
2877 }
2878 return {
2879 fields: settings.searchField,
2880 conjunction: settings.searchConjunction,
2881 sort: sort,
2882 nesting: settings.nesting
2883 };
2884 }
2885
2886 /**
2887 * Searches through available options and returns
2888 * a sorted array of matches.
2889 *
2890 */
2891 search(query) {
2892 var result, calculateScore;
2893 var self = this;
2894 var options = this.getSearchOptions();
2895
2896 // validate user-provided result scoring function
2897 if (self.settings.score) {
2898 calculateScore = self.settings.score.call(self, query);
2899 if (typeof calculateScore !== 'function') {
2900 throw new Error('Tom Select "score" setting must be a function that returns a function');
2901 }
2902 }
2903
2904 // perform search
2905 if (self.isDropdownContentStale || query !== self.lastQuery) {
2906 self.lastQuery = query;
2907 // temp fix for https://github.com/orchidjs/tom-select/issues/987
2908 // UI crashed when more than 30 same chars in a row, prevent search and return empt result
2909 if (/(.)\1{15,}/.test(query)) {
2910 query = '';
2911 }
2912 result = self.sifter.search(query, Object.assign(options, {
2913 score: calculateScore
2914 }));
2915 self.currentResults = result;
2916 } else {
2917 result = Object.assign({}, self.currentResults);
2918 }
2919
2920 // filter out selected items
2921 if (self.settings.hideSelected) {
2922 result.items = result.items.filter(item => {
2923 let hashed = hash_key(item.id);
2924 return !(hashed !== null && self.items.indexOf(hashed) !== -1);
2925 });
2926 }
2927 return result;
2928 }
2929
2930 /**
2931 * Refreshes the list of available options shown
2932 * in the autocomplete dropdown menu.
2933 *
2934 */
2935 refreshOptions(triggerDropdown = true) {
2936 var i, j, k, n, optgroup, optgroups, html, has_create_option, active_group;
2937 var create;
2938 const groups = {};
2939 const groups_order = [];
2940 var self = this;
2941 var query = self.inputValue();
2942 const same_query = query === self.lastQuery || query == '' && self.lastQuery == null;
2943 var results = self.search(query);
2944 var active_option = null;
2945 var show_dropdown = self.settings.shouldOpen || false;
2946 var dropdown_content = self.dropdown_content;
2947 if (same_query) {
2948 active_option = self.activeOption;
2949 if (active_option) {
2950 active_group = active_option.closest('[data-group]');
2951 }
2952 }
2953
2954 // build markup
2955 n = results.items.length;
2956 if (typeof self.settings.maxOptions === 'number') {
2957 n = Math.min(n, self.settings.maxOptions);
2958 }
2959 if (n > 0) {
2960 show_dropdown = true;
2961 }
2962
2963 // get fragment for group and the position of the group in group_order
2964 const getGroupFragment = (optgroup, order) => {
2965 let group_order_i = groups[optgroup];
2966 if (group_order_i !== undefined) {
2967 let order_group = groups_order[group_order_i];
2968 if (order_group !== undefined) {
2969 return [group_order_i, order_group.fragment];
2970 }
2971 }
2972 let group_fragment = document.createDocumentFragment();
2973 group_order_i = groups_order.length;
2974 groups_order.push({
2975 fragment: group_fragment,
2976 order,
2977 optgroup
2978 });
2979 return [group_order_i, group_fragment];
2980 };
2981
2982 // render and group available options individually
2983 for (i = 0; i < n; i++) {
2984 // get option dom element
2985 let item = results.items[i];
2986 if (!item) continue;
2987 let opt_value = item.id;
2988 let option = self.options[opt_value];
2989 if (option === undefined) continue;
2990 let opt_hash = get_hash(opt_value);
2991 let option_el = self.getOption(opt_hash, true);
2992
2993 // toggle 'selected' class
2994 if (!self.settings.hideSelected) {
2995 option_el.classList.toggle('selected', self.items.includes(opt_hash));
2996 }
2997 optgroup = option[self.settings.optgroupField] || '';
2998 optgroups = Array.isArray(optgroup) ? optgroup : [optgroup];
2999 for (j = 0, k = optgroups && optgroups.length; j < k; j++) {
3000 optgroup = optgroups[j];
3001 let order = option.$order;
3002 let self_optgroup = self.optgroups[optgroup];
3003 if (self_optgroup === undefined && typeof self.settings.optionGroupRegister === 'function') {
3004 var regGroup;
3005 if (regGroup = self.settings.optionGroupRegister.apply(self, [optgroup])) {
3006 self.registerOptionGroup(regGroup);
3007 }
3008 }
3009 self_optgroup = self.optgroups[optgroup];
3010 if (self_optgroup === undefined) {
3011 optgroup = '';
3012 } else {
3013 order = self_optgroup.$order;
3014 }
3015 const [group_order_i, group_fragment] = getGroupFragment(optgroup, order);
3016
3017 // nodes can only have one parent, so if the option is in mutple groups, we need a clone
3018 if (j > 0) {
3019 option_el = option_el.cloneNode(true);
3020 setAttr(option_el, {
3021 id: option.$id + '-clone-' + j,
3022 'aria-selected': null
3023 });
3024 option_el.classList.add('ts-cloned');
3025 removeClasses(option_el, 'active');
3026
3027 // make sure we keep the activeOption in the same group
3028 if (self.activeOption && self.activeOption.dataset.value == opt_value) {
3029 if (active_group && active_group.dataset.group === optgroup.toString()) {
3030 active_option = option_el;
3031 }
3032 }
3033 }
3034 group_fragment.appendChild(option_el);
3035 if (optgroup != '') {
3036 groups[optgroup] = group_order_i;
3037 }
3038 }
3039 }
3040
3041 // sort optgroups
3042 if (self.settings.lockOptgroupOrder) {
3043 groups_order.sort((a, b) => {
3044 return a.order - b.order;
3045 });
3046 }
3047
3048 // render optgroup headers & join groups
3049 html = document.createDocumentFragment();
3050 iterate(groups_order, group_order => {
3051 let group_fragment = group_order.fragment;
3052 let optgroup = group_order.optgroup;
3053 if (!group_fragment || !group_fragment.children.length) return;
3054 let group_heading = self.optgroups[optgroup];
3055 if (group_heading !== undefined) {
3056 let group_options = document.createDocumentFragment();
3057 let header = self.render('optgroup_header', group_heading);
3058 append(group_options, header);
3059 append(group_options, group_fragment);
3060 let group_html = self.render('optgroup', {
3061 group: group_heading,
3062 options: group_options
3063 });
3064 append(html, group_html);
3065 } else {
3066 append(html, group_fragment);
3067 }
3068 });
3069 dropdown_content.innerHTML = '';
3070 append(dropdown_content, html);
3071 self.isDropdownContentStale = false;
3072
3073 // highlight matching terms inline
3074 if (self.settings.highlight) {
3075 removeHighlight(dropdown_content);
3076 if (results.query.length && results.tokens.length) {
3077 iterate(results.tokens, tok => {
3078 highlight(dropdown_content, tok.regex);
3079 });
3080 }
3081 }
3082
3083 // helper method for adding templates to dropdown
3084 var add_template = template => {
3085 let content = self.render(template, {
3086 input: query
3087 });
3088 if (content) {
3089 show_dropdown = true;
3090 dropdown_content.insertBefore(content, dropdown_content.firstChild);
3091 }
3092 return content;
3093 };
3094
3095 // add loading message
3096 if (self.loading) {
3097 add_template('loading');
3098
3099 // invalid query
3100 } else if (!self.settings.shouldLoad.call(self, query)) {
3101 add_template('not_loading');
3102
3103 // add no_results message
3104 } else if (results.items.length === 0) {
3105 add_template('no_results');
3106 }
3107
3108 // add create option
3109 has_create_option = self.canCreate(query);
3110 if (has_create_option) {
3111 create = add_template('option_create');
3112 }
3113
3114 // activate
3115 self.hasOptions = results.items.length > 0 || has_create_option;
3116 if (show_dropdown) {
3117 if (results.items.length > 0) {
3118 if (!active_option && self.settings.mode === 'single' && self.items[0] != undefined) {
3119 active_option = self.getOption(self.items[0]);
3120 }
3121 if (!dropdown_content.contains(active_option)) {
3122 let active_index = 0;
3123 if (create && !self.settings.addPrecedence) {
3124 active_index = 1;
3125 }
3126 active_option = self.selectable()[active_index];
3127 }
3128 } else if (create) {
3129 active_option = create;
3130 }
3131 if (triggerDropdown && !self.isOpen) {
3132 self.open();
3133 self.scrollToOption(active_option, 'auto');
3134 }
3135 self.setActiveOption(active_option);
3136 } else {
3137 self.clearActiveOption();
3138 if (triggerDropdown && self.isOpen) {
3139 self.close(false); // if create_option=null, we want the dropdown to close but not reset the textbox value
3140 }
3141 }
3142 }
3143
3144 /**
3145 * Return list of selectable options
3146 *
3147 */
3148 selectable() {
3149 return this.dropdown_content.querySelectorAll('[data-selectable]');
3150 }
3151
3152 /**
3153 * Adds an available option. If it already exists,
3154 * nothing will happen. Note: this does not refresh
3155 * the options list dropdown (use `refreshOptions`
3156 * for that).
3157 *
3158 * Usage:
3159 *
3160 * this.addOption(data)
3161 *
3162 */
3163 addOption(data, user_created = false) {
3164 const self = this;
3165
3166 // @deprecated 1.7.7
3167 // use addOptions( array, user_created ) for adding multiple options
3168 if (Array.isArray(data)) {
3169 self.addOptions(data, user_created);
3170 return false;
3171 }
3172 const key = hash_key(data[self.settings.valueField]);
3173 if (key === null || self.options.hasOwnProperty(key)) {
3174 self.updateOption(data[self.settings.valueField], data);
3175 return false;
3176 }
3177 data.$order = data.$order || ++self.order;
3178 data.$id = self.inputId + '-opt-' + data.$order;
3179 self.options[key] = data;
3180 self.isDropdownContentStale = true;
3181 if (user_created) {
3182 self.userOptions[key] = user_created;
3183 self.trigger('option_add', key, data);
3184 }
3185 return key;
3186 }
3187
3188 /**
3189 * Add multiple options
3190 *
3191 */
3192 addOptions(data, user_created = false) {
3193 iterate(data, dat => {
3194 this.addOption(dat, user_created);
3195 });
3196 }
3197
3198 /**
3199 * @deprecated 1.7.7
3200 */
3201 registerOption(data) {
3202 return this.addOption(data);
3203 }
3204
3205 /**
3206 * Registers an option group to the pool of option groups.
3207 *
3208 * @return {boolean|string}
3209 */
3210 registerOptionGroup(data) {
3211 var key = hash_key(data[this.settings.optgroupValueField]);
3212 if (key === null) return false;
3213 data.$order = data.$order || ++this.order;
3214 this.optgroups[key] = data;
3215 return key;
3216 }
3217
3218 /**
3219 * Registers a new optgroup for options
3220 * to be bucketed into.
3221 *
3222 */
3223 addOptionGroup(id, data) {
3224 var hashed_id;
3225 data[this.settings.optgroupValueField] = id;
3226 if (hashed_id = this.registerOptionGroup(data)) {
3227 this.trigger('optgroup_add', hashed_id, data);
3228 }
3229 }
3230
3231 /**
3232 * Removes an existing option group.
3233 *
3234 */
3235 removeOptionGroup(id) {
3236 if (this.optgroups.hasOwnProperty(id)) {
3237 delete this.optgroups[id];
3238 this.clearCache();
3239 this.trigger('optgroup_remove', id);
3240 }
3241 }
3242
3243 /**
3244 * Clears all existing option groups.
3245 */
3246 clearOptionGroups() {
3247 this.optgroups = {};
3248 this.clearCache();
3249 this.trigger('optgroup_clear');
3250 }
3251
3252 /**
3253 * Updates an option available for selection. If
3254 * it is visible in the selected items or options
3255 * dropdown, it will be re-rendered automatically.
3256 *
3257 */
3258 updateOption(value, data) {
3259 const self = this;
3260 var item_new;
3261 var index_item;
3262 const value_old = hash_key(value);
3263 const value_new = hash_key(data[self.settings.valueField]);
3264
3265 // sanity checks
3266 if (value_old === null) return;
3267 const data_old = self.options[value_old];
3268 if (data_old == undefined) return;
3269 if (typeof value_new !== 'string') throw new Error('Value must be set in option data');
3270 const option = self.getOption(value_old);
3271 const item = self.getItem(value_old);
3272 data.$order = data.$order || data_old.$order;
3273 delete self.options[value_old];
3274
3275 // invalidate render cache
3276 // don't remove existing node yet, we'll remove it after replacing it
3277 self.uncacheValue(value_new);
3278 self.options[value_new] = data;
3279
3280 // update the option if it's in the dropdown
3281 if (option) {
3282 if (self.dropdown_content.contains(option)) {
3283 const option_new = self._render('option', data);
3284 replaceNode(option, option_new);
3285 if (self.activeOption === option) {
3286 self.setActiveOption(option_new);
3287 }
3288 }
3289 option.remove();
3290 }
3291
3292 // update the item if we have one
3293 if (item) {
3294 index_item = self.items.indexOf(value_old);
3295 if (index_item !== -1) {
3296 self.items.splice(index_item, 1, value_new);
3297 }
3298 item_new = self._render('item', data);
3299 if (item.classList.contains('active')) addClasses(item_new, 'active');
3300 replaceNode(item, item_new);
3301 }
3302
3303 // we might have updated the sortField
3304 self.isDropdownContentStale = true;
3305 }
3306
3307 /**
3308 * Removes a single option.
3309 *
3310 */
3311 removeOption(value, silent) {
3312 const self = this;
3313 value = get_hash(value);
3314 self.uncacheValue(value);
3315 delete self.userOptions[value];
3316 delete self.options[value];
3317 self.isDropdownContentStale = true;
3318 self.trigger('option_remove', value);
3319 self.removeItem(value, silent);
3320 }
3321
3322 /**
3323 * Clears all options.
3324 */
3325 clearOptions(filter) {
3326 const boundFilter = (filter || this.clearFilter).bind(this);
3327 this.loadedSearches = {};
3328 this.userOptions = {};
3329 this.clearCache();
3330 const selected = {};
3331 iterate(this.options, (option, key) => {
3332 if (boundFilter(option, key)) {
3333 selected[key] = option;
3334 }
3335 });
3336 this.options = this.sifter.items = selected;
3337 this.isDropdownContentStale = true;
3338 this.trigger('option_clear');
3339 }
3340
3341 /**
3342 * Used by clearOptions() to decide whether or not an option should be removed
3343 * Return true to keep an option, false to remove
3344 *
3345 */
3346 clearFilter(option, value) {
3347 if (this.items.indexOf(value) >= 0) {
3348 return true;
3349 }
3350 return false;
3351 }
3352
3353 /**
3354 * Returns the dom element of the option
3355 * matching the given value.
3356 *
3357 */
3358 getOption(value, create = false) {
3359 const hashed = hash_key(value);
3360 if (hashed === null) return null;
3361 const option = this.options[hashed];
3362 if (option != undefined) {
3363 if (option.$div) {
3364 return option.$div;
3365 }
3366 if (create) {
3367 return this._render('option', option);
3368 }
3369 }
3370 return null;
3371 }
3372
3373 /**
3374 * Returns the dom element of the next or previous dom element of the same type
3375 * Note: adjacent options may not be adjacent DOM elements (optgroups)
3376 *
3377 */
3378 getAdjacent(option, direction, type = 'option') {
3379 var self = this,
3380 all;
3381 if (!option) {
3382 return null;
3383 }
3384 if (type == 'item') {
3385 all = self.controlChildren();
3386 } else {
3387 all = self.dropdown_content.querySelectorAll('[data-selectable]');
3388 }
3389 for (let i = 0; i < all.length; i++) {
3390 if (all[i] != option) {
3391 continue;
3392 }
3393 if (direction > 0) {
3394 return all[i + 1];
3395 }
3396 return all[i - 1];
3397 }
3398 return null;
3399 }
3400
3401 /**
3402 * Returns the dom element of the item
3403 * matching the given value.
3404 *
3405 */
3406 getItem(item) {
3407 if (typeof item == 'object') {
3408 return item;
3409 }
3410 var value = hash_key(item);
3411 return value !== null ? this.control.querySelector(`[data-value="${addSlashes(value)}"]`) : null;
3412 }
3413
3414 /**
3415 * "Selects" multiple items at once. Adds them to the list
3416 * at the current caret position.
3417 *
3418 */
3419 addItems(values, silent) {
3420 var self = this;
3421 var items = Array.isArray(values) ? values : [values];
3422 items = items.filter(x => self.items.indexOf(x) === -1);
3423 const last_item = items[items.length - 1];
3424 items.forEach(item => {
3425 self.isPending = item !== last_item;
3426 self.addItem(item, silent);
3427 });
3428 }
3429
3430 /**
3431 * "Selects" an item. Adds it to the list
3432 * at the current caret position.
3433 *
3434 */
3435 addItem(value, silent) {
3436 var events = silent ? [] : ['change', 'dropdown_close'];
3437 debounce_events(this, events, () => {
3438 var item, wasFull;
3439 const self = this;
3440 const inputMode = self.settings.mode;
3441 const hashed = hash_key(value);
3442 if (hashed && self.items.indexOf(hashed) !== -1) {
3443 if (inputMode === 'single') {
3444 self.close();
3445 }
3446 if (inputMode === 'single' || !self.settings.duplicates) {
3447 return;
3448 }
3449 }
3450 if (hashed === null || !self.options.hasOwnProperty(hashed)) return;
3451 if (inputMode === 'single') self.clear(silent);
3452 if (inputMode === 'multi' && self.isFull()) return;
3453 item = self._render('item', self.options[hashed]);
3454 if (self.control.contains(item)) {
3455 // duplicates
3456 item = item.cloneNode(true);
3457 }
3458 wasFull = self.isFull();
3459 self.items.splice(self.caretPos, 0, hashed);
3460 self.insertAtCaret(item);
3461 if (self.isSetup) {
3462 // update menu / remove the option (if this is not one item being added as part of series)
3463 if (!self.isPending && self.settings.hideSelected) {
3464 let option = self.getOption(hashed);
3465 let next = self.getAdjacent(option, 1);
3466 if (next) {
3467 self.setActiveOption(next);
3468 }
3469 }
3470
3471 //remove input value when enabled
3472 if (self.settings.clearAfterSelect) {
3473 self.setTextboxValue();
3474 }
3475
3476 // refreshOptions after setActiveOption(),
3477 // otherwise setActiveOption() will be called by refreshOptions() with the wrong value
3478 if (!self.isPending && !self.settings.closeAfterSelect) {
3479 self.refreshOptions(self.isFocused && inputMode !== 'single');
3480 }
3481
3482 // hide the menu if the maximum number of items have been selected or no options are left
3483 if (self.settings.closeAfterSelect != false && self.isFull()) {
3484 self.close();
3485 } else if (!self.isPending) {
3486 self.positionDropdown();
3487 }
3488 self.trigger('item_add', hashed, item);
3489 if (!self.isPending) {
3490 self.updateOriginalInput({
3491 silent: silent
3492 });
3493 }
3494 }
3495 if (!self.isPending || !wasFull && self.isFull()) {
3496 self.inputState();
3497 self.refreshState();
3498 }
3499 });
3500 }
3501
3502 /**
3503 * Removes the selected item matching
3504 * the provided value.
3505 *
3506 */
3507 removeItem(item = null, silent) {
3508 const self = this;
3509 item = self.getItem(item);
3510 if (!item) return;
3511 var i, idx;
3512 const value = item.dataset.value;
3513 i = nodeIndex(item);
3514 item.remove();
3515 if (item.classList.contains('active')) {
3516 idx = self.activeItems.indexOf(item);
3517 self.activeItems.splice(idx, 1);
3518 removeClasses(item, 'active');
3519 }
3520 self.items.splice(i, 1);
3521 self.isDropdownContentStale = true;
3522 if (!self.settings.persist && self.userOptions.hasOwnProperty(value)) {
3523 self.removeOption(value, silent);
3524 }
3525 if (i < self.caretPos) {
3526 self.setCaret(self.caretPos - 1);
3527 }
3528 self.updateOriginalInput({
3529 silent: silent
3530 });
3531 self.refreshState();
3532 self.positionDropdown();
3533 self.trigger('item_remove', value, item);
3534 }
3535
3536 /**
3537 * Invokes the `create` method provided in the
3538 * TomSelect options that should provide the data
3539 * for the new item, given the user input.
3540 *
3541 * Once this completes, it will be added
3542 * to the item list.
3543 *
3544 */
3545 createItem(input = null, callback = () => { }) {
3546 // triggerDropdown parameter @deprecated 2.1.1
3547 if (arguments.length === 3) {
3548 callback = arguments[2];
3549 }
3550 if (typeof callback != 'function') {
3551 callback = () => { };
3552 }
3553 var self = this;
3554 var caret = self.caretPos;
3555 var output;
3556 input = input || self.inputValue();
3557 if (!self.canCreate(input)) {
3558 const hash = hash_key(input);
3559 if (hash) {
3560 if (this.options[input]) {
3561 self.addItem(input);
3562 }
3563 }
3564 callback();
3565 return false;
3566 }
3567 self.lock();
3568 var created = false;
3569 var create = data => {
3570 self.unlock();
3571 if (!data || typeof data !== 'object') return callback();
3572 var value = hash_key(data[self.settings.valueField]);
3573 if (typeof value !== 'string') {
3574 return callback();
3575 }
3576 self.setTextboxValue();
3577 self.addOption(data, true);
3578 self.setCaret(caret);
3579 self.addItem(value);
3580 callback(data);
3581 created = true;
3582 };
3583 if (typeof self.settings.create === 'function') {
3584 output = self.settings.create.call(this, input, create);
3585 } else {
3586 output = {
3587 [self.settings.labelField]: input,
3588 [self.settings.valueField]: input
3589 };
3590 }
3591 if (!created) {
3592 create(output);
3593 }
3594 return true;
3595 }
3596
3597 /**
3598 * Re-renders the selected item lists.
3599 */
3600 refreshItems() {
3601 var self = this;
3602 self.isDropdownContentStale = true;
3603 if (self.isSetup) {
3604 self.addItems(self.items);
3605 }
3606 self.updateOriginalInput();
3607 self.refreshState();
3608 }
3609
3610 /**
3611 * Updates all state-dependent attributes
3612 * and CSS classes.
3613 */
3614 refreshState() {
3615 const self = this;
3616 self.refreshValidityState();
3617 const isFull = self.isFull();
3618 const isLocked = self.isLocked;
3619 self.wrapper.classList.toggle('rtl', self.rtl);
3620 const wrap_classList = self.wrapper.classList;
3621 wrap_classList.toggle('focus', self.isFocused);
3622 wrap_classList.toggle('disabled', self.isDisabled);
3623 wrap_classList.toggle('readonly', self.isReadOnly);
3624 wrap_classList.toggle('required', self.isRequired);
3625 wrap_classList.toggle('invalid', !self.isValid);
3626 wrap_classList.toggle('locked', isLocked);
3627 wrap_classList.toggle('full', isFull);
3628 wrap_classList.toggle('input-active', self.isFocused && !self.isInputHidden);
3629 wrap_classList.toggle('dropdown-active', self.isOpen);
3630 wrap_classList.toggle('has-options', isEmptyObject(self.options));
3631 wrap_classList.toggle('has-items', self.items.length > 0);
3632 }
3633
3634 /**
3635 * Update the `required` attribute of both input and control input.
3636 *
3637 * The `required` property needs to be activated on the control input
3638 * for the error to be displayed at the right place. `required` also
3639 * needs to be temporarily deactivated on the input since the input is
3640 * hidden and can't show errors.
3641 */
3642 refreshValidityState() {
3643 var self = this;
3644 if (!self.input.validity) {
3645 return;
3646 }
3647 self.isValid = self.input.validity.valid;
3648 self.isInvalid = !self.isValid;
3649 }
3650
3651 /**
3652 * Determines whether or not more items can be added
3653 * to the control without exceeding the user-defined maximum.
3654 *
3655 * @returns {boolean}
3656 */
3657 isFull() {
3658 return this.settings.maxItems !== null && this.items.length >= this.settings.maxItems;
3659 }
3660
3661 /**
3662 * Refreshes the original <select> or <input>
3663 * element to reflect the current state.
3664 *
3665 */
3666 updateOriginalInput(opts = {}) {
3667 const self = this;
3668 var option, label;
3669 const empty_option = self.input.querySelector('option[value=""]');
3670 if (self.is_select_tag) {
3671 const selected = [];
3672 const has_selected = self.input.querySelectorAll('option:checked').length;
3673 function AddSelected(option_el, value, label) {
3674 if (!option_el) {
3675 option_el = getDom('<option value="' + escape_html(value) + '">' + escape_html(label) + '</option>');
3676 }
3677
3678 // don't move empty option from top of list
3679 // fixes bug in firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1725293
3680 if (option_el != empty_option) {
3681 self.input.append(option_el);
3682 }
3683 selected.push(option_el);
3684
3685 // marking empty option as selected can break validation
3686 // fixes https://github.com/orchidjs/tom-select/issues/303
3687 if (option_el != empty_option || has_selected > 0) {
3688 option_el.selected = true;
3689 }
3690 return option_el;
3691 }
3692
3693 // unselect all selected options
3694 self.input.querySelectorAll('option:checked').forEach(option_el => {
3695 option_el.selected = false;
3696 });
3697
3698 // nothing selected?
3699 if (self.items.length == 0 && self.settings.mode == 'single') {
3700 AddSelected(empty_option, "", "");
3701
3702 // order selected <option> tags for values in self.items
3703 } else {
3704 self.items.forEach(value => {
3705 option = self.options[value];
3706 label = option[self.settings.labelField] || '';
3707 if (selected.includes(option.$option)) {
3708 const reuse_opt = self.input.querySelector(`option[value="${addSlashes(value)}"]:not(:checked)`);
3709 AddSelected(reuse_opt, value, label);
3710 } else {
3711 option.$option = AddSelected(option.$option, value, label);
3712 }
3713 });
3714 }
3715 } else {
3716 self.input.value = self.getValue();
3717 }
3718 if (self.isSetup) {
3719 if (!opts.silent) {
3720 self.trigger('change', self.getValue());
3721 }
3722 }
3723 }
3724
3725 /**
3726 * Shows the autocomplete dropdown containing
3727 * the available options.
3728 */
3729 open() {
3730 var self = this;
3731 if (self.isLocked || self.isOpen || self.settings.mode === 'multi' && self.isFull()) return;
3732 self.isOpen = true;
3733 setAttr(self.focus_node, {
3734 'aria-expanded': 'true'
3735 });
3736 self.refreshState();
3737 applyCSS(self.dropdown, {
3738 visibility: 'hidden',
3739 display: 'block'
3740 });
3741 self.positionDropdown();
3742 applyCSS(self.dropdown, {
3743 visibility: 'visible',
3744 display: 'block'
3745 });
3746 self.focus();
3747 self.trigger('dropdown_open', self.dropdown);
3748 }
3749
3750 /**
3751 * Closes the autocomplete dropdown menu.
3752 */
3753 close(setTextboxValue = true) {
3754 var self = this;
3755 var trigger = self.isOpen;
3756 if (setTextboxValue) {
3757 // before blur() to prevent form onchange event
3758 self.setTextboxValue();
3759 if (self.settings.mode === 'single' && self.items.length) {
3760 self.inputState();
3761 }
3762 }
3763 self.isOpen = false;
3764 setAttr(self.focus_node, {
3765 'aria-expanded': 'false'
3766 });
3767 applyCSS(self.dropdown, {
3768 display: 'none'
3769 });
3770 if (self.settings.hideSelected) {
3771 self.clearActiveOption();
3772 }
3773 self.refreshState();
3774 if (trigger) self.trigger('dropdown_close', self.dropdown);
3775 }
3776
3777 /**
3778 * Calculates and applies the appropriate
3779 * position of the dropdown if dropdownParent = 'body'.
3780 * Otherwise, position is determined by css
3781 */
3782 positionDropdown() {
3783 if (this.settings.dropdownParent !== 'body') {
3784 return;
3785 }
3786 var context = this.control;
3787 var rect = context.getBoundingClientRect();
3788 var top = context.offsetHeight + rect.top + window.scrollY;
3789 var left = rect.left + window.scrollX;
3790 applyCSS(this.dropdown, {
3791 width: rect.width + 'px',
3792 top: top + 'px',
3793 left: left + 'px'
3794 });
3795 }
3796
3797 /**
3798 * Resets / clears all selected items
3799 * from the control.
3800 *
3801 */
3802 clear(silent) {
3803 var self = this;
3804 if (!self.items.length) return;
3805 var items = self.controlChildren();
3806 iterate(items, item => {
3807 self.removeItem(item, true);
3808 });
3809 self.inputState();
3810 if (!silent) self.updateOriginalInput();
3811 self.trigger('clear');
3812 }
3813
3814 /**
3815 * A helper method for inserting an element
3816 * at the current caret position.
3817 *
3818 */
3819 insertAtCaret(el) {
3820 const self = this;
3821 const caret = self.caretPos;
3822 const target = self.control;
3823 target.insertBefore(el, target.children[caret] || null);
3824 self.setCaret(caret + 1);
3825 }
3826
3827 /**
3828 * Removes the current selected item(s).
3829 *
3830 */
3831 deleteSelection(e) {
3832 var direction, selection, caret, tail;
3833 var self = this;
3834 direction = e && e.keyCode === KEY_BACKSPACE ? -1 : 1;
3835 selection = getSelection(self.control_input);
3836
3837 // determine items that will be removed
3838 const rm_items = [];
3839 if (self.activeItems.length) {
3840 tail = getTail(self.activeItems, direction);
3841 caret = nodeIndex(tail);
3842 if (direction > 0) {
3843 caret++;
3844 }
3845 iterate(self.activeItems, item => rm_items.push(item));
3846 } else if ((self.isFocused || self.settings.mode === 'single') && self.items.length) {
3847 const items = self.controlChildren();
3848 let rm_item;
3849 if (direction < 0 && selection.start === 0 && selection.length === 0) {
3850 rm_item = items[self.caretPos - 1];
3851 } else if (direction > 0 && selection.start === self.inputValue().length) {
3852 rm_item = items[self.caretPos];
3853 }
3854 if (rm_item !== undefined) {
3855 rm_items.push(rm_item);
3856 }
3857 }
3858 if (!self.shouldDelete(rm_items, e)) {
3859 return false;
3860 }
3861 preventDefault(e, true);
3862
3863 // perform removal
3864 if (typeof caret !== 'undefined') {
3865 self.setCaret(caret);
3866 }
3867 while (rm_items.length) {
3868 self.removeItem(rm_items.pop());
3869 }
3870 self.inputState();
3871 self.positionDropdown();
3872 self.refreshOptions(false);
3873 return true;
3874 }
3875
3876 /**
3877 * Return true if the items should be deleted
3878 */
3879 shouldDelete(items, evt) {
3880 const values = items.map(item => item.dataset.value);
3881
3882 // allow the callback to abort
3883 if (!values.length || typeof this.settings.onDelete === 'function' && this.settings.onDelete.call(this, values, evt) === false) {
3884 return false;
3885 }
3886 return true;
3887 }
3888
3889 /**
3890 * Selects the previous / next item (depending on the `direction` argument).
3891 *
3892 * > 0 - right
3893 * < 0 - left
3894 *
3895 */
3896 advanceSelection(direction, e) {
3897 var last_active,
3898 adjacent,
3899 self = this;
3900 if (self.rtl) direction *= -1;
3901 if (self.inputValue().length) return;
3902
3903 // add or remove to active items
3904 if (isKeyDown(KEY_SHORTCUT, e) || isKeyDown('shiftKey', e)) {
3905 last_active = self.getLastActive(direction);
3906 if (last_active) {
3907 if (!last_active.classList.contains('active')) {
3908 adjacent = last_active;
3909 } else {
3910 adjacent = self.getAdjacent(last_active, direction, 'item');
3911 }
3912
3913 // if no active item, get items adjacent to the control input
3914 } else if (direction > 0) {
3915 adjacent = self.control_input.nextElementSibling;
3916 } else {
3917 adjacent = self.control_input.previousElementSibling;
3918 }
3919 if (adjacent) {
3920 if (adjacent.classList.contains('active')) {
3921 self.removeActiveItem(last_active);
3922 }
3923 self.setActiveItemClass(adjacent); // mark as last_active !! after removeActiveItem() on last_active
3924 }
3925
3926 // move caret to the left or right
3927 } else {
3928 self.moveCaret(direction);
3929 }
3930 }
3931 moveCaret(direction) { }
3932
3933 /**
3934 * Get the last active item
3935 *
3936 */
3937 getLastActive(direction) {
3938 let last_active = this.control.querySelector('.last-active');
3939 if (last_active) {
3940 return last_active;
3941 }
3942 var result = this.control.querySelectorAll('.active');
3943 if (result) {
3944 return getTail(result, direction);
3945 }
3946 }
3947
3948 /**
3949 * Moves the caret to the specified index.
3950 *
3951 * The input must be moved by leaving it in place and moving the
3952 * siblings, due to the fact that focus cannot be restored once lost
3953 * on mobile webkit devices
3954 *
3955 */
3956 setCaret(new_pos) {
3957 this.caretPos = this.items.length;
3958 }
3959
3960 /**
3961 * Return list of item dom elements
3962 *
3963 */
3964 controlChildren() {
3965 return Array.from(this.control.querySelectorAll('[data-ts-item]'));
3966 }
3967
3968 /**
3969 * Disables user input on the control. Used while
3970 * items are being asynchronously created.
3971 */
3972 lock() {
3973 this.setLocked(true);
3974 }
3975
3976 /**
3977 * Re-enables user input on the control.
3978 */
3979 unlock() {
3980 this.setLocked(false);
3981 }
3982
3983 /**
3984 * Disable or enable user input on the control
3985 */
3986 setLocked(lock = this.isReadOnly || this.isDisabled) {
3987 this.isLocked = lock;
3988 this.refreshState();
3989 }
3990
3991 /**
3992 * Disables user input on the control completely.
3993 * While disabled, it cannot receive focus.
3994 */
3995 disable() {
3996 this.setDisabled(true);
3997 this.close();
3998 }
3999
4000 /**
4001 * Enables the control so that it can respond
4002 * to focus and user input.
4003 */
4004 enable() {
4005 this.setDisabled(false);
4006 }
4007 setDisabled(disabled) {
4008 this.focus_node.tabIndex = disabled ? -1 : this.tabIndex;
4009 this.isDisabled = disabled;
4010 this.input.disabled = disabled;
4011 this.control_input.disabled = disabled;
4012 this.setLocked();
4013 }
4014 setReadOnly(isReadOnly) {
4015 this.isReadOnly = isReadOnly;
4016 this.input.readOnly = isReadOnly;
4017 this.control_input.readOnly = isReadOnly;
4018 this.setLocked();
4019 }
4020
4021 /**
4022 * Completely destroys the control and
4023 * unbinds all event listeners so that it can
4024 * be garbage collected.
4025 */
4026 destroy() {
4027 var self = this;
4028 var revertSettings = self.revertSettings;
4029 self.trigger('destroy');
4030 self.off();
4031 self.wrapper.remove();
4032 self.dropdown.remove();
4033 self.input.innerHTML = revertSettings.innerHTML;
4034 self.input.tabIndex = revertSettings.tabIndex;
4035 removeClasses(self.input, 'tomselected', 'ts-hidden-accessible');
4036 self._destroy();
4037 delete self.input.tomselect;
4038 }
4039
4040 /**
4041 * A helper method for rendering "item" and
4042 * "option" templates, given the data.
4043 *
4044 */
4045 render(templateName, data) {
4046 var id, html;
4047 const self = this;
4048 if (typeof this.settings.render[templateName] !== 'function') {
4049 return null;
4050 }
4051
4052 // render markup
4053 html = self.settings.render[templateName].call(this, data, escape_html);
4054 if (!html) {
4055 return null;
4056 }
4057 html = getDom(html);
4058
4059 // add mandatory attributes
4060 if (templateName === 'option' || templateName === 'option_create') {
4061 if (data[self.settings.disabledField]) {
4062 setAttr(html, {
4063 'aria-disabled': 'true'
4064 });
4065 } else {
4066 setAttr(html, {
4067 'data-selectable': ''
4068 });
4069 }
4070 } else if (templateName === 'optgroup') {
4071 id = data.group[self.settings.optgroupValueField];
4072 setAttr(html, {
4073 'data-group': id
4074 });
4075 if (data.group[self.settings.disabledField]) {
4076 setAttr(html, {
4077 'data-disabled': ''
4078 });
4079 }
4080 }
4081 if (templateName === 'option' || templateName === 'item') {
4082 const value = get_hash(data[self.settings.valueField]);
4083 setAttr(html, {
4084 'data-value': value
4085 });
4086
4087 // make sure we have some classes if a template is overwritten
4088 if (templateName === 'item') {
4089 addClasses(html, self.settings.itemClass);
4090 setAttr(html, {
4091 'data-ts-item': ''
4092 });
4093 } else {
4094 addClasses(html, self.settings.optionClass);
4095 setAttr(html, {
4096 role: 'option',
4097 id: data.$id
4098 });
4099
4100 // update cache
4101 data.$div = html;
4102 self.options[value] = data;
4103 }
4104 }
4105 return html;
4106 }
4107
4108 /**
4109 * Type guarded rendering
4110 *
4111 */
4112 _render(templateName, data) {
4113 const html = this.render(templateName, data);
4114 if (html == null) {
4115 throw 'HTMLElement expected';
4116 }
4117 return html;
4118 }
4119
4120 /**
4121 * Clears the render cache for a template. If
4122 * no template is given, clears all render
4123 * caches.
4124 *
4125 */
4126 clearCache() {
4127 iterate(this.options, option => {
4128 if (option.$div) {
4129 option.$div.remove();
4130 delete option.$div;
4131 }
4132 });
4133 }
4134
4135 /**
4136 * Removes a value from item and option caches
4137 *
4138 */
4139 uncacheValue(value) {
4140 const option_el = this.getOption(value);
4141 if (option_el) option_el.remove();
4142 }
4143
4144 /**
4145 * Determines whether or not to display the
4146 * create item prompt, given a user input.
4147 *
4148 */
4149 canCreate(input) {
4150 return this.settings.create && input.length > 0 && this.settings.createFilter.call(this, input);
4151 }
4152
4153 /**
4154 * Wraps this.`method` so that `new_fn` can be invoked 'before', 'after', or 'instead' of the original method
4155 *
4156 * this.hook('instead','onKeyDown',function( arg1, arg2 ...){
4157 *
4158 * });
4159 */
4160 hook(when, method, new_fn) {
4161 var self = this;
4162 var orig_method = self[method];
4163 self[method] = function () {
4164 var result, result_new;
4165 if (when === 'after') {
4166 result = orig_method.apply(self, arguments);
4167 }
4168 result_new = new_fn.apply(self, arguments);
4169 if (when === 'instead') {
4170 return result_new;
4171 }
4172 if (when === 'before') {
4173 result = orig_method.apply(self, arguments);
4174 }
4175 return result;
4176 };
4177 }
4178 }
4179
4180 /**
4181 * Plugin: "change_listener" (Tom Select)
4182 * Copyright (c) contributors
4183 *
4184 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4185 * file except in compliance with the License. You may obtain a copy of the License at:
4186 * http://www.apache.org/licenses/LICENSE-2.0
4187 *
4188 * Unless required by applicable law or agreed to in writing, software distributed under
4189 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4190 * ANY KIND, either express or implied. See the License for the specific language
4191 * governing permissions and limitations under the License.
4192 *
4193 */
4194
4195 function change_listener() {
4196 addEvent(this.input, 'change', () => {
4197 this.sync();
4198 });
4199 }
4200
4201 /**
4202 * Plugin: "checkbox_options" (Tom Select)
4203 * Copyright (c) contributors
4204 *
4205 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4206 * file except in compliance with the License. You may obtain a copy of the License at:
4207 * http://www.apache.org/licenses/LICENSE-2.0
4208 *
4209 * Unless required by applicable law or agreed to in writing, software distributed under
4210 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4211 * ANY KIND, either express or implied. See the License for the specific language
4212 * governing permissions and limitations under the License.
4213 *
4214 */
4215
4216 function checkbox_options(userOptions) {
4217 var self = this;
4218 var orig_onOptionSelect = self.onOptionSelect;
4219 self.settings.hideSelected = false;
4220 const cbOptions = Object.assign({
4221 // so that the user may add different ones as well
4222 className: "tomselect-checkbox",
4223 // the following default to the historic plugin's values
4224 checkedClassNames: undefined,
4225 uncheckedClassNames: undefined
4226 }, userOptions);
4227 var UpdateChecked = function UpdateChecked(checkbox, toCheck) {
4228 if (toCheck) {
4229 checkbox.checked = true;
4230 if (cbOptions.uncheckedClassNames) {
4231 checkbox.classList.remove(...cbOptions.uncheckedClassNames);
4232 }
4233 if (cbOptions.checkedClassNames) {
4234 checkbox.classList.add(...cbOptions.checkedClassNames);
4235 }
4236 } else {
4237 checkbox.checked = false;
4238 if (cbOptions.checkedClassNames) {
4239 checkbox.classList.remove(...cbOptions.checkedClassNames);
4240 }
4241 if (cbOptions.uncheckedClassNames) {
4242 checkbox.classList.add(...cbOptions.uncheckedClassNames);
4243 }
4244 }
4245 };
4246
4247 // update the checkbox for an option
4248 var UpdateCheckbox = function UpdateCheckbox(option) {
4249 setTimeout(() => {
4250 var checkbox = option.querySelector('input.' + cbOptions.className);
4251 if (checkbox instanceof HTMLInputElement) {
4252 UpdateChecked(checkbox, option.classList.contains('selected'));
4253 }
4254 }, 1);
4255 };
4256
4257 // add checkbox to option template
4258 self.hook('after', 'setupTemplates', () => {
4259 var orig_render_option = self.settings.render.option;
4260 self.settings.render.option = (data, escape_html) => {
4261 var rendered = getDom(orig_render_option.call(self, data, escape_html));
4262 var checkbox = document.createElement('input');
4263 if (cbOptions.className) {
4264 checkbox.classList.add(cbOptions.className);
4265 }
4266 checkbox.addEventListener('click', function (evt) {
4267 preventDefault(evt);
4268 });
4269 checkbox.type = 'checkbox';
4270 const hashed = hash_key(data[self.settings.valueField]);
4271 UpdateChecked(checkbox, !!(hashed && self.items.indexOf(hashed) > -1));
4272 rendered.prepend(checkbox);
4273 return rendered;
4274 };
4275 });
4276
4277 // uncheck when item removed
4278 self.on('item_remove', value => {
4279 var option = self.getOption(value);
4280 if (option) {
4281 // if dropdown hasn't been opened yet, the option won't exist
4282 option.classList.remove('selected'); // selected class won't be removed yet
4283 UpdateCheckbox(option);
4284 }
4285 });
4286
4287 // check when item added
4288 self.on('item_add', value => {
4289 var option = self.getOption(value);
4290 if (option) {
4291 // if dropdown hasn't been opened yet, the option won't exist
4292 UpdateCheckbox(option);
4293 }
4294 });
4295
4296 // remove items when selected option is clicked
4297 self.hook('instead', 'onOptionSelect', (evt, option) => {
4298 if (option.classList.contains('selected')) {
4299 option.classList.remove('selected');
4300 self.removeItem(option.dataset.value);
4301 self.refreshOptions();
4302 preventDefault(evt, true);
4303 return;
4304 }
4305 orig_onOptionSelect.call(self, evt, option);
4306 UpdateCheckbox(option);
4307 });
4308 }
4309
4310 /**
4311 * Plugin: "dropdown_header" (Tom Select)
4312 * Copyright (c) contributors
4313 *
4314 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4315 * file except in compliance with the License. You may obtain a copy of the License at:
4316 * http://www.apache.org/licenses/LICENSE-2.0
4317 *
4318 * Unless required by applicable law or agreed to in writing, software distributed under
4319 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4320 * ANY KIND, either express or implied. See the License for the specific language
4321 * governing permissions and limitations under the License.
4322 *
4323 */
4324
4325 function clear_button(userOptions) {
4326 const self = this;
4327 const options = Object.assign({
4328 className: 'clear-button',
4329 title: 'Clear All',
4330 role: 'button',
4331 tabindex: 0,
4332 html: data => {
4333 return `<div class="${data.className}" title="${data.title}" role="${data.role}" tabindex="${data.tabindex}">&times;</div>`;
4334 }
4335 }, userOptions);
4336 self.on('initialize', () => {
4337 var button = getDom(options.html(options));
4338 button.addEventListener('click', evt => {
4339 if (self.isLocked) return;
4340 self.clear();
4341 if (self.settings.mode === 'single' && self.settings.allowEmptyOption) {
4342 self.addItem('');
4343 }
4344 self.refreshOptions(false);
4345 evt.preventDefault();
4346 evt.stopPropagation();
4347 });
4348 self.control.appendChild(button);
4349 });
4350 }
4351
4352 /**
4353 * Plugin: "drag_drop" (Tom Select)
4354 * Copyright (c) contributors
4355 *
4356 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4357 * file except in compliance with the License. You may obtain a copy of the License at:
4358 * http://www.apache.org/licenses/LICENSE-2.0
4359 *
4360 * Unless required by applicable law or agreed to in writing, software distributed under
4361 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4362 * ANY KIND, either express or implied. See the License for the specific language
4363 * governing permissions and limitations under the License.
4364 *
4365 */
4366
4367 const insertAfter = (referenceNode, newNode) => {
4368 var _referenceNode$parent;
4369 (_referenceNode$parent = referenceNode.parentNode) == null || _referenceNode$parent.insertBefore(newNode, referenceNode.nextSibling);
4370 };
4371 const insertBefore = (referenceNode, newNode) => {
4372 var _referenceNode$parent2;
4373 (_referenceNode$parent2 = referenceNode.parentNode) == null || _referenceNode$parent2.insertBefore(newNode, referenceNode);
4374 };
4375 const isBefore = (referenceNode, newNode) => {
4376 do {
4377 var _newNode;
4378 newNode = (_newNode = newNode) == null ? void 0 : _newNode.previousElementSibling;
4379 if (referenceNode == newNode) {
4380 return true;
4381 }
4382 } while (newNode && newNode.previousElementSibling);
4383 return false;
4384 };
4385 function drag_drop() {
4386 var self = this;
4387 if (self.settings.mode !== 'multi') return;
4388 var orig_lock = self.lock;
4389 var orig_unlock = self.unlock;
4390 let sortable = true;
4391 let drag_item;
4392
4393 /**
4394 * Add draggable attribute to item
4395 */
4396 self.hook('after', 'setupTemplates', () => {
4397 var orig_render_item = self.settings.render.item;
4398 self.settings.render.item = (data, escape) => {
4399 const item = getDom(orig_render_item.call(self, data, escape));
4400 setAttr(item, {
4401 'draggable': 'true'
4402 });
4403
4404 // prevent doc_mousedown (see tom-select.ts)
4405 const mousedown = evt => {
4406 if (!sortable) preventDefault(evt);
4407 evt.stopPropagation();
4408 };
4409 const dragStart = evt => {
4410 drag_item = item;
4411 setTimeout(() => {
4412 item.classList.add('ts-dragging');
4413 }, 0);
4414 };
4415 const dragOver = evt => {
4416 evt.preventDefault();
4417 item.classList.add('ts-drag-over');
4418 moveitem(item, drag_item);
4419 };
4420 const dragLeave = () => {
4421 item.classList.remove('ts-drag-over');
4422 };
4423 const moveitem = (targetitem, dragitem) => {
4424 if (dragitem === undefined) return;
4425 if (isBefore(dragitem, item)) {
4426 insertAfter(targetitem, dragitem);
4427 } else {
4428 insertBefore(targetitem, dragitem);
4429 }
4430 };
4431 const dragend = () => {
4432 var _drag_item;
4433 document.querySelectorAll('.ts-drag-over').forEach(el => el.classList.remove('ts-drag-over'));
4434 (_drag_item = drag_item) == null || _drag_item.classList.remove('ts-dragging');
4435 drag_item = undefined;
4436 var values = [];
4437 self.control.querySelectorAll(`[data-value]`).forEach(el => {
4438 if (el.dataset.value) {
4439 let value = el.dataset.value;
4440 if (value) {
4441 values.push(value);
4442 }
4443 }
4444 });
4445 self.setValue(values);
4446 };
4447 addEvent(item, 'mousedown', mousedown);
4448 addEvent(item, 'dragstart', dragStart);
4449 addEvent(item, 'dragenter', dragOver);
4450 addEvent(item, 'dragover', dragOver);
4451 addEvent(item, 'dragleave', dragLeave);
4452 addEvent(item, 'dragend', dragend);
4453 return item;
4454 };
4455 });
4456 self.hook('instead', 'lock', () => {
4457 sortable = false;
4458 return orig_lock.call(self);
4459 });
4460 self.hook('instead', 'unlock', () => {
4461 sortable = true;
4462 return orig_unlock.call(self);
4463 });
4464 }
4465
4466 /**
4467 * Plugin: "dropdown_header" (Tom Select)
4468 * Copyright (c) contributors
4469 *
4470 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4471 * file except in compliance with the License. You may obtain a copy of the License at:
4472 * http://www.apache.org/licenses/LICENSE-2.0
4473 *
4474 * Unless required by applicable law or agreed to in writing, software distributed under
4475 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4476 * ANY KIND, either express or implied. See the License for the specific language
4477 * governing permissions and limitations under the License.
4478 *
4479 */
4480
4481 function dropdown_header(userOptions) {
4482 const self = this;
4483 const options = Object.assign({
4484 title: 'Untitled',
4485 headerClass: 'dropdown-header',
4486 titleRowClass: 'dropdown-header-title',
4487 labelClass: 'dropdown-header-label',
4488 closeClass: 'dropdown-header-close',
4489 html: data => {
4490 return '<div class="' + data.headerClass + '">' + '<div class="' + data.titleRowClass + '">' + '<span class="' + data.labelClass + '">' + data.title + '</span>' + '<a class="' + data.closeClass + '">&times;</a>' + '</div>' + '</div>';
4491 }
4492 }, userOptions);
4493 self.on('initialize', () => {
4494 var header = getDom(options.html(options));
4495 var close_link = header.querySelector('.' + options.closeClass);
4496 if (close_link) {
4497 close_link.addEventListener('click', evt => {
4498 preventDefault(evt, true);
4499 self.close();
4500 });
4501 }
4502 self.dropdown.insertBefore(header, self.dropdown.firstChild);
4503 });
4504 }
4505
4506 /**
4507 * Plugin: "dropdown_input" (Tom Select)
4508 * Copyright (c) contributors
4509 *
4510 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4511 * file except in compliance with the License. You may obtain a copy of the License at:
4512 * http://www.apache.org/licenses/LICENSE-2.0
4513 *
4514 * Unless required by applicable law or agreed to in writing, software distributed under
4515 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4516 * ANY KIND, either express or implied. See the License for the specific language
4517 * governing permissions and limitations under the License.
4518 *
4519 */
4520
4521 function caret_position() {
4522 var self = this;
4523
4524 /**
4525 * Moves the caret to the specified index.
4526 *
4527 * The input must be moved by leaving it in place and moving the
4528 * siblings, due to the fact that focus cannot be restored once lost
4529 * on mobile webkit devices
4530 *
4531 */
4532 self.hook('instead', 'setCaret', new_pos => {
4533 if (self.settings.mode === 'single' || !self.control.contains(self.control_input)) {
4534 new_pos = self.items.length;
4535 } else {
4536 new_pos = Math.max(0, Math.min(self.items.length, new_pos));
4537 if (new_pos != self.caretPos && !self.isPending) {
4538 self.controlChildren().forEach((child, j) => {
4539 if (j < new_pos) {
4540 self.control_input.insertAdjacentElement('beforebegin', child);
4541 } else {
4542 self.control.appendChild(child);
4543 }
4544 });
4545 }
4546 }
4547 self.caretPos = new_pos;
4548 });
4549 self.hook('instead', 'moveCaret', direction => {
4550 if (!self.isFocused) return;
4551
4552 // move caret before or after selected items
4553 const last_active = self.getLastActive(direction);
4554 if (last_active) {
4555 const idx = nodeIndex(last_active);
4556 self.setCaret(direction > 0 ? idx + 1 : idx);
4557 self.setActiveItem();
4558 removeClasses(last_active, 'last-active');
4559
4560 // move caret left or right of current position
4561 } else {
4562 self.setCaret(self.caretPos + direction);
4563 }
4564 });
4565 }
4566
4567 /**
4568 * Plugin: "dropdown_input" (Tom Select)
4569 * Copyright (c) contributors
4570 *
4571 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4572 * file except in compliance with the License. You may obtain a copy of the License at:
4573 * http://www.apache.org/licenses/LICENSE-2.0
4574 *
4575 * Unless required by applicable law or agreed to in writing, software distributed under
4576 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4577 * ANY KIND, either express or implied. See the License for the specific language
4578 * governing permissions and limitations under the License.
4579 *
4580 */
4581
4582 function dropdown_input() {
4583 const self = this;
4584 self.settings.shouldOpen = true; // make sure the input is shown even if there are no options to display in the dropdown
4585
4586 self.hook('before', 'setup', () => {
4587 var _self$input;
4588 self.focus_node = self.control;
4589 addClasses(self.control_input, 'dropdown-input');
4590 const div = getDom('<div class="dropdown-input-wrap">');
4591 div.append(self.control_input);
4592 self.dropdown.insertBefore(div, self.dropdown.firstChild);
4593
4594 // set a placeholder in the select control
4595 const placeholder = getDom('<input class="items-placeholder" tabindex="-1" />');
4596 placeholder.placeholder = self.settings.placeholder || '';
4597 self.control.append(placeholder);
4598 /**
4599 * TomSelect renders a custom control with a focusable <input class="items-placeholder">.
4600 * The source <select>'s aria-label is not automatically propagated to that input,
4601 * which triggers "Missing form label" accessibility warnings.
4602 * This helper copies the label from the <select> onto the generated input.
4603 */
4604 const label = (_self$input = self.input) == null ? void 0 : _self$input.getAttribute('aria-label');
4605 if (!label) return;
4606 placeholder.setAttribute('aria-label', label);
4607 });
4608 self.on('initialize', () => {
4609 // set tabIndex on control to -1, otherwise [shift+tab] will put focus right back on control_input
4610 self.control_input.addEventListener('keydown', evt => {
4611 //addEvent(self.control_input,'keydown' as const,(evt:KeyboardEvent) =>{
4612 switch (evt.keyCode) {
4613 case KEY_ESC:
4614 if (self.isOpen) {
4615 preventDefault(evt, true);
4616 self.close();
4617 }
4618 self.clearActiveItems();
4619 return;
4620 case KEY_TAB:
4621 self.focus_node.tabIndex = -1;
4622 break;
4623 }
4624 return self.onKeyDown.call(self, evt);
4625 });
4626 self.on('blur', () => {
4627 self.focus_node.tabIndex = self.isDisabled ? -1 : self.tabIndex;
4628 });
4629
4630 // give the control_input focus when the dropdown is open
4631 self.on('dropdown_open', () => {
4632 self.control_input.focus();
4633 });
4634
4635 // prevent onBlur from closing when focus is on the control_input
4636 const orig_onBlur = self.onBlur;
4637 self.hook('instead', 'onBlur', evt => {
4638 if (evt && evt.relatedTarget == self.control_input) return;
4639 return orig_onBlur.call(self);
4640 });
4641 addEvent(self.control_input, 'blur', () => self.onBlur());
4642
4643 // return focus to control to allow further keyboard input
4644 self.hook('before', 'close', () => {
4645 if (!self.isOpen) return;
4646 self.focus_node.focus({
4647 preventScroll: true
4648 });
4649 });
4650 });
4651 }
4652
4653 /**
4654 * Plugin: "input_autogrow" (Tom Select)
4655 *
4656 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4657 * file except in compliance with the License. You may obtain a copy of the License at:
4658 * http://www.apache.org/licenses/LICENSE-2.0
4659 *
4660 * Unless required by applicable law or agreed to in writing, software distributed under
4661 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4662 * ANY KIND, either express or implied. See the License for the specific language
4663 * governing permissions and limitations under the License.
4664 *
4665 */
4666
4667 function input_autogrow() {
4668 var self = this;
4669 self.on('initialize', () => {
4670 var test_input = document.createElement('span');
4671 var control = self.control_input;
4672 test_input.style.cssText = 'position:absolute; top:-99999px; left:-99999px; width:auto; padding:0; white-space:pre; ';
4673 self.wrapper.appendChild(test_input);
4674 var transfer_styles = ['letterSpacing', 'fontSize', 'fontFamily', 'fontWeight', 'textTransform'];
4675 for (const style_name of transfer_styles) {
4676 // @ts-ignore TS7015 https://stackoverflow.com/a/50506154/697576
4677 test_input.style[style_name] = control.style[style_name];
4678 }
4679
4680 /**
4681 * Set the control width
4682 *
4683 */
4684 var resize = () => {
4685 test_input.textContent = control.value;
4686 control.style.width = test_input.clientWidth + 'px';
4687 };
4688 resize();
4689 self.on('update item_add item_remove', resize);
4690 addEvent(control, 'input', resize);
4691 addEvent(control, 'keyup', resize);
4692 addEvent(control, 'blur', resize);
4693 addEvent(control, 'update', resize);
4694 });
4695 }
4696
4697 /**
4698 * Plugin: "input_autogrow" (Tom Select)
4699 *
4700 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4701 * file except in compliance with the License. You may obtain a copy of the License at:
4702 * http://www.apache.org/licenses/LICENSE-2.0
4703 *
4704 * Unless required by applicable law or agreed to in writing, software distributed under
4705 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4706 * ANY KIND, either express or implied. See the License for the specific language
4707 * governing permissions and limitations under the License.
4708 *
4709 */
4710
4711 function no_backspace_delete() {
4712 var self = this;
4713 var orig_deleteSelection = self.deleteSelection;
4714 this.hook('instead', 'deleteSelection', evt => {
4715 if (self.activeItems.length) {
4716 return orig_deleteSelection.call(self, evt);
4717 }
4718 return false;
4719 });
4720 }
4721
4722 /**
4723 * Plugin: "no_active_items" (Tom Select)
4724 *
4725 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4726 * file except in compliance with the License. You may obtain a copy of the License at:
4727 * http://www.apache.org/licenses/LICENSE-2.0
4728 *
4729 * Unless required by applicable law or agreed to in writing, software distributed under
4730 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4731 * ANY KIND, either express or implied. See the License for the specific language
4732 * governing permissions and limitations under the License.
4733 *
4734 */
4735
4736 function no_active_items() {
4737 this.hook('instead', 'setActiveItem', () => { });
4738 this.hook('instead', 'selectAll', () => { });
4739 }
4740
4741 /**
4742 * Plugin: "optgroup_columns" (Tom Select.js)
4743 * Copyright (c) contributors
4744 *
4745 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4746 * file except in compliance with the License. You may obtain a copy of the License at:
4747 * http://www.apache.org/licenses/LICENSE-2.0
4748 *
4749 * Unless required by applicable law or agreed to in writing, software distributed under
4750 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4751 * ANY KIND, either express or implied. See the License for the specific language
4752 * governing permissions and limitations under the License.
4753 *
4754 */
4755
4756 function optgroup_columns() {
4757 var self = this;
4758 var orig_keydown = self.onKeyDown;
4759 self.hook('instead', 'onKeyDown', evt => {
4760 var index, option, options, optgroup;
4761 if (!self.isOpen || !(evt.keyCode === KEY_LEFT || evt.keyCode === KEY_RIGHT)) {
4762 return orig_keydown.call(self, evt);
4763 }
4764 self.ignoreHover = true;
4765 optgroup = parentMatch(self.activeOption, '[data-group]');
4766 index = nodeIndex(self.activeOption, '[data-selectable]');
4767 if (!optgroup) {
4768 return;
4769 }
4770 if (evt.keyCode === KEY_LEFT) {
4771 optgroup = optgroup.previousSibling;
4772 } else {
4773 optgroup = optgroup.nextSibling;
4774 }
4775 if (!optgroup) {
4776 return;
4777 }
4778 options = optgroup.querySelectorAll('[data-selectable]');
4779 option = options[Math.min(options.length - 1, index)];
4780 if (option) {
4781 self.setActiveOption(option);
4782 }
4783 });
4784 }
4785
4786 /**
4787 * Plugin: "remove_button" (Tom Select)
4788 * Copyright (c) contributors
4789 *
4790 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4791 * file except in compliance with the License. You may obtain a copy of the License at:
4792 * http://www.apache.org/licenses/LICENSE-2.0
4793 *
4794 * Unless required by applicable law or agreed to in writing, software distributed under
4795 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4796 * ANY KIND, either express or implied. See the License for the specific language
4797 * governing permissions and limitations under the License.
4798 *
4799 */
4800
4801 function remove_button(userOptions) {
4802 const options = Object.assign({
4803 label: '&times;',
4804 title: 'Remove',
4805 className: 'remove',
4806 append: true
4807 }, userOptions);
4808
4809 //options.className = 'remove-single';
4810 var self = this;
4811
4812 // override the render method to add remove button to each item
4813 if (!options.append) {
4814 return;
4815 }
4816 var html = '<a href="javascript:void(0)" class="' + options.className + '" tabindex="-1" title="' + escape_html(options.title) + '">' + options.label + '</a>';
4817 self.hook('after', 'setupTemplates', () => {
4818 var orig_render_item = self.settings.render.item;
4819 self.settings.render.item = (data, escape) => {
4820 var item = getDom(orig_render_item.call(self, data, escape));
4821 var close_button = getDom(html);
4822 item.appendChild(close_button);
4823 addEvent(close_button, 'mousedown', evt => {
4824 preventDefault(evt, true);
4825 });
4826 addEvent(close_button, 'click', evt => {
4827 if (self.isLocked) return;
4828
4829 // propagating will trigger the dropdown to show for single mode
4830 preventDefault(evt, true);
4831 if (self.isLocked) return;
4832 if (!self.shouldDelete([item], evt)) return;
4833 self.removeItem(item);
4834 self.refreshOptions(false);
4835 self.inputState();
4836 });
4837 return item;
4838 };
4839 });
4840 }
4841
4842 /**
4843 * Plugin: "restore_on_backspace" (Tom Select)
4844 * Copyright (c) contributors
4845 *
4846 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4847 * file except in compliance with the License. You may obtain a copy of the License at:
4848 * http://www.apache.org/licenses/LICENSE-2.0
4849 *
4850 * Unless required by applicable law or agreed to in writing, software distributed under
4851 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4852 * ANY KIND, either express or implied. See the License for the specific language
4853 * governing permissions and limitations under the License.
4854 *
4855 */
4856
4857 function restore_on_backspace(userOptions) {
4858 const self = this;
4859 const options = Object.assign({
4860 text: option => {
4861 return option[self.settings.labelField];
4862 }
4863 }, userOptions);
4864 self.on('item_remove', function (value) {
4865 if (!self.isFocused) {
4866 return;
4867 }
4868 if (self.control_input.value.trim() === '') {
4869 var option = self.options[value];
4870 if (option) {
4871 self.setTextboxValue(options.text.call(self, option));
4872 }
4873 }
4874 });
4875 }
4876
4877 /**
4878 * Plugin: "virtual_scroll" (Tom Select)
4879 * Copyright (c) contributors
4880 *
4881 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
4882 * file except in compliance with the License. You may obtain a copy of the License at:
4883 * http://www.apache.org/licenses/LICENSE-2.0
4884 *
4885 * Unless required by applicable law or agreed to in writing, software distributed under
4886 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
4887 * ANY KIND, either express or implied. See the License for the specific language
4888 * governing permissions and limitations under the License.
4889 *
4890 */
4891
4892 function virtual_scroll() {
4893 const self = this;
4894 const orig_canLoad = self.canLoad;
4895 const orig_clearActiveOption = self.clearActiveOption;
4896 const orig_loadCallback = self.loadCallback;
4897 var pagination = {};
4898 var dropdown_content;
4899 var loading_more = false;
4900 var load_more_opt;
4901 var default_values = [];
4902 var default_values_loaded = false;
4903 var default_pagination;
4904 if (!self.settings.shouldLoadMore) {
4905 // return true if additional results should be loaded
4906 self.settings.shouldLoadMore = () => {
4907 const scroll_percent = dropdown_content.clientHeight / (dropdown_content.scrollHeight - dropdown_content.scrollTop);
4908 if (scroll_percent > 0.9) {
4909 return true;
4910 }
4911 if (self.activeOption) {
4912 var selectable = self.selectable();
4913 var index = Array.from(selectable).indexOf(self.activeOption);
4914 if (index >= selectable.length - 2) {
4915 return true;
4916 }
4917 }
4918 return false;
4919 };
4920 }
4921 if (!self.settings.firstUrl) {
4922 throw 'virtual_scroll plugin requires a firstUrl() method';
4923 }
4924
4925 // in order for virtual scrolling to work,
4926 // options need to be ordered the same way they're returned from the remote data source
4927 self.settings.sortField = [{
4928 field: '$order'
4929 }, {
4930 field: '$score'
4931 }];
4932
4933 // can we load more results for given query?
4934 const canLoadMore = query => {
4935 if (typeof self.settings.maxOptions === 'number' && dropdown_content.children.length >= self.settings.maxOptions) {
4936 return false;
4937 }
4938 if (query in pagination && pagination[query]) {
4939 return true;
4940 }
4941 return false;
4942 };
4943 const clearFilter = (option, value) => {
4944 if (self.items.indexOf(value) >= 0 || default_values.indexOf(value) >= 0) {
4945 return true;
4946 }
4947 return false;
4948 };
4949
4950 // set the next url that will be
4951 self.setNextUrl = (value, next_url) => {
4952 pagination[value] = next_url;
4953 };
4954
4955 // getUrl() to be used in settings.load()
4956 self.getUrl = query => {
4957 if (query in pagination) {
4958 const next_url = pagination[query];
4959 pagination[query] = false;
4960 return next_url;
4961 }
4962
4963 // if the user goes back to a previous query
4964 // we need to load the first page again
4965 self.clearPagination();
4966 return self.settings.firstUrl.call(self, query);
4967 };
4968
4969 // clear pagination
4970 self.clearPagination = () => {
4971 pagination = {};
4972 };
4973
4974 // don't clear the active option (and cause unwanted dropdown scroll)
4975 // while loading more results
4976 self.hook('instead', 'clearActiveOption', () => {
4977 if (loading_more) {
4978 return;
4979 }
4980 return orig_clearActiveOption.call(self);
4981 });
4982
4983 // override the canLoad method
4984 self.hook('instead', 'canLoad', query => {
4985 // first time the query has been seen
4986 if (!(query in pagination)) {
4987 return orig_canLoad.call(self, query);
4988 }
4989 return canLoadMore(query);
4990 });
4991
4992 // wrap the load
4993 self.hook('instead', 'loadCallback', (options, optgroups) => {
4994 if (!loading_more) {
4995 self.clearOptions(clearFilter);
4996 } else if (load_more_opt) {
4997 const first_option = options[0];
4998 if (first_option !== undefined) {
4999 load_more_opt.dataset.value = first_option[self.settings.valueField];
5000 }
5001 }
5002 orig_loadCallback.call(self, options, optgroups);
5003
5004 // After the initial preload (empty query), update default_values to include
5005 // preloaded options, not just the HTML <option> elements captured on initialize
5006 if (!loading_more && !default_values_loaded) {
5007 default_values_loaded = true;
5008 if (self.lastValue === '') {
5009 default_values = Object.keys(self.options);
5010 default_pagination = pagination[''];
5011 }
5012 }
5013 loading_more = false;
5014 });
5015
5016 // as the “loading_more” element will be removed from the dropdown,
5017 // we activate the previous option if needed
5018 // to avoid the dropdown being scrolled back to the first one
5019 self.hook('before', 'refreshOptions', () => {
5020 if (self.activeOption && "option" !== self.activeOption.getAttribute("role")) {
5021 self.setActiveOption(self.activeOption.previousElementSibling);
5022 }
5023 });
5024
5025 // add templates to dropdown
5026 // loading_more if we have another url in the queue
5027 // no_more_results if we don't have another url in the queue
5028 self.hook('after', 'refreshOptions', () => {
5029 const query = self.lastValue;
5030 var option;
5031 if (canLoadMore(query)) {
5032 option = self.render('loading_more', {
5033 query: query
5034 });
5035 if (option) {
5036 option.setAttribute('data-selectable', ''); // so that navigating dropdown with [down] keypresses can navigate to this node
5037 load_more_opt = option;
5038 }
5039 } else if (query in pagination && !dropdown_content.querySelector('.no-results')) {
5040 option = self.render('no_more_results', {
5041 query: query
5042 });
5043 }
5044 if (option) {
5045 addClasses(option, self.settings.optionClass);
5046 dropdown_content.append(option);
5047 }
5048 });
5049
5050 // Restore preloaded options and pagination when clearing search
5051 const restoreDefaults = () => {
5052 if (!default_values_loaded) {
5053 return;
5054 }
5055 self.clearOptions(clearFilter);
5056 if (default_pagination) {
5057 pagination[''] = default_pagination;
5058 }
5059 };
5060 self.on('type', query => {
5061 if (query === '') {
5062 restoreDefaults();
5063 self.refreshOptions(false);
5064 }
5065 });
5066 self.on('dropdown_close', restoreDefaults);
5067
5068 // add scroll listener and default templates
5069 self.on('initialize', () => {
5070 default_values = Object.keys(self.options);
5071 dropdown_content = self.dropdown_content;
5072
5073 // default templates
5074 self.settings.render = Object.assign({}, {
5075 loading_more: () => {
5076 return `<div class="loading-more-results">Loading more results ... </div>`;
5077 },
5078 no_more_results: () => {
5079 return `<div class="no-more-results">No more results</div>`;
5080 }
5081 }, self.settings.render);
5082
5083 // watch dropdown content scroll position
5084 dropdown_content.addEventListener('scroll', () => {
5085 if (!self.settings.shouldLoadMore.call(self)) {
5086 return;
5087 }
5088
5089 // !important: this will get checked again in load() but we still need to check here otherwise loading_more will be set to true
5090 if (!canLoadMore(self.lastValue)) {
5091 return;
5092 }
5093
5094 // don't call load() too much
5095 if (loading_more) return;
5096 loading_more = true;
5097 self.load.call(self, self.lastValue);
5098 });
5099 });
5100 }
5101
5102 TomSelect.define('change_listener', change_listener);
5103 TomSelect.define('checkbox_options', checkbox_options);
5104 TomSelect.define('clear_button', clear_button);
5105 TomSelect.define('drag_drop', drag_drop);
5106 TomSelect.define('dropdown_header', dropdown_header);
5107 TomSelect.define('caret_position', caret_position);
5108 TomSelect.define('dropdown_input', dropdown_input);
5109 TomSelect.define('input_autogrow', input_autogrow);
5110 TomSelect.define('no_backspace_delete', no_backspace_delete);
5111 TomSelect.define('no_active_items', no_active_items);
5112 TomSelect.define('optgroup_columns', optgroup_columns);
5113 TomSelect.define('remove_button', remove_button);
5114 TomSelect.define('restore_on_backspace', restore_on_backspace);
5115 TomSelect.define('virtual_scroll', virtual_scroll);
5116
5117 return TomSelect;
5118
5119 }));
5120 var tomSelect = function (el, opts) { return new TomSelect(el, opts); }
5121 //# sourceMappingURL=tom-select.complete.js.map