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

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

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