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