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