1/** 2* Tom Select v2.4.3
vendor: 50,361 bytes, lines 2-1687
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 highlight: true, 1506 openOnFocus: true, 1507 shouldOpen: null, 1508 maxOptions: 50, 1509 maxItems: null, 1510 hideSelected: null, 1511 duplicates: false, 1512 addPrecedence: false, 1513 selectOnTab: false, 1514 preload: null, 1515 allowEmptyOption: false, 1516 //closeAfterSelect: false, 1517 refreshThrottle: 300, 1518 loadThrottle: 300, 1519 loadingClass: 'loading', 1520 dataAttr: null, 1521 //'data-data', 1522 optgroupField: 'optgroup', 1523 valueField: 'value', 1524 labelField: 'text', 1525 disabledField: 'disabled', 1526 optgroupLabelField: 'label', 1527 optgroupValueField: 'value', 1528 lockOptgroupOrder: false, 1529 sortField: '$order', 1530 searchField: ['text'], 1531 searchConjunction: 'and', 1532 mode: null, 1533 wrapperClass: 'ts-wrapper', 1534 controlClass: 'ts-control', 1535 dropdownClass: 'ts-dropdown', 1536 dropdownContentClass: 'ts-dropdown-content', 1537 itemClass: 'item', 1538 optionClass: 'option', 1539 dropdownParent: null, 1540 controlInput: '<input type="text" autocomplete="off" size="1" />', 1541 copyClassesToDropdown: false, 1542 placeholder: null, 1543 hidePlaceholder: null, 1544 shouldLoad: function (query) { 1545 return query.length > 0; 1546 }, 1547 /* 1548 load : null, // function(query, callback) { ... } 1549 score : null, // function(search) { ... } 1550 onInitialize : null, // function() { ... } 1551 onChange : null, // function(value) { ... } 1552 onItemAdd : null, // function(value, $item) { ... } 1553 onItemRemove : null, // function(value) { ... } 1554 onClear : null, // function() { ... } 1555 onOptionAdd : null, // function(value, data) { ... } 1556 onOptionRemove : null, // function(value) { ... } 1557 onOptionClear : null, // function() { ... } 1558 onOptionGroupAdd : null, // function(id, data) { ... } 1559 onOptionGroupRemove : null, // function(id) { ... } 1560 onOptionGroupClear : null, // function() { ... } 1561 onDropdownOpen : null, // function(dropdown) { ... } 1562 onDropdownClose : null, // function(dropdown) { ... } 1563 onType : null, // function(str) { ... } 1564 onDelete : null, // function(values) { ... } 1565 */ 1566 1567 render: { 1568 /* 1569 item: null, 1570 optgroup: null, 1571 optgroup_header: null, 1572 option: null, 1573 option_create: null 1574 */ 1575 } 1576 }; 1577 1578 function getSettings(input, settings_user) { 1579 var settings = Object.assign({}, defaults, settings_user); 1580 var attr_data = settings.dataAttr; 1581 var field_label = settings.labelField; 1582 var field_value = settings.valueField; 1583 var field_disabled = settings.disabledField; 1584 var field_optgroup = settings.optgroupField; 1585 var field_optgroup_label = settings.optgroupLabelField; 1586 var field_optgroup_value = settings.optgroupValueField; 1587 var tag_name = input.tagName.toLowerCase(); 1588 var placeholder = input.getAttribute('placeholder') || input.getAttribute('data-placeholder'); 1589 if (!placeholder && !settings.allowEmptyOption) { 1590 let option = input.querySelector('option[value=""]'); 1591 if (option) { 1592 placeholder = option.textContent; 1593 } 1594 } 1595 var settings_element = { 1596 placeholder: placeholder, 1597 options: [], 1598 optgroups: [], 1599 items: [], 1600 maxItems: null 1601 }; 1602 1603 /** 1604 * Initialize from a <select> element. 1605 * 1606 */ 1607 var init_select = () => { 1608 var tagName; 1609 var options = settings_element.options; 1610 var optionsMap = {}; 1611 var group_count = 1; 1612 let $order = 0; 1613 var readData = el => { 1614 var data = Object.assign({}, el.dataset); // get plain object from DOMStringMap 1615 var json = attr_data && data[attr_data]; 1616 if (typeof json === 'string' && json.length) { 1617 data = Object.assign(data, JSON.parse(json)); 1618 } 1619 return data; 1620 }; 1621 var addOption = (option, group) => { 1622 var value = hash_key(option.value); 1623 if (value == null) return; 1624 if (!value && !settings.allowEmptyOption) return; 1625 1626 // if the option already exists, it's probably been 1627 // duplicated in another optgroup. in this case, push 1628 // the current group to the "optgroup" property on the 1629 // existing option so that it's rendered in both places. 1630 if (optionsMap.hasOwnProperty(value)) { 1631 if (group) { 1632 var arr = optionsMap[value][field_optgroup]; 1633 if (!arr) { 1634 optionsMap[value][field_optgroup] = group; 1635 } else if (!Array.isArray(arr)) { 1636 optionsMap[value][field_optgroup] = [arr, group]; 1637 } else { 1638 arr.push(group); 1639 } 1640 } 1641 } else { 1642 var option_data = readData(option); 1643 option_data[field_label] = option_data[field_label] || option.textContent; 1644 option_data[field_value] = option_data[field_value] || value; 1645 option_data[field_disabled] = option_data[field_disabled] || option.disabled; 1646 option_data[field_optgroup] = option_data[field_optgroup] || group; 1647 option_data.$option = option; 1648 option_data.$order = option_data.$order || ++$order; 1649 optionsMap[value] = option_data; 1650 options.push(option_data); 1651 } 1652 if (option.selected) { 1653 settings_element.items.push(value); 1654 } 1655 }; 1656 var addGroup = optgroup => { 1657 var id, optgroup_data; 1658 optgroup_data = readData(optgroup); 1659 optgroup_data[field_optgroup_label] = optgroup_data[field_optgroup_label] || optgroup.getAttribute('label') || ''; 1660 optgroup_data[field_optgroup_value] = optgroup_data[field_optgroup_value] || group_count++; 1661 optgroup_data[field_disabled] = optgroup_data[field_disabled] || optgroup.disabled; 1662 optgroup_data.$order = optgroup_data.$order || ++$order; 1663 settings_element.optgroups.push(optgroup_data); 1664 id = optgroup_data[field_optgroup_value]; 1665 iterate(optgroup.children, option => { 1666 addOption(option, id); 1667 }); 1668 }; 1669 settings_element.maxItems = input.hasAttribute('multiple') ? null : 1; 1670 iterate(input.children, child => { 1671 tagName = child.tagName.toLowerCase(); 1672 if (tagName === 'optgroup') { 1673 addGroup(child); 1674 } else if (tagName === 'option') { 1675 addOption(child); 1676 } 1677 }); 1678 }; 1679 1680 /** 1681 * Initialize from a <input type="text"> element. 1682 * 1683 */ 1684 var init_textbox = () => { 1685 const data_raw = input.getAttribute(attr_data); 1686 if (!data_raw) { 1687 var
vendor: 7,693 bytes, lines 1687-1918
1687value = input.value.trim() || ''; 1688 if (!settings.allowEmptyOption && !value.length) return; 1689 const values = value.split(settings.delimiter); 1690 iterate(values, value => { 1691 const option = {}; 1692 option[field_label] = value; 1693 option[field_value] = value; 1694 settings_element.options.push(option); 1695 }); 1696 settings_element.items = values; 1697 } else { 1698 settings_element.options = JSON.parse(data_raw); 1699 iterate(settings_element.options, opt => { 1700 settings_element.items.push(opt[field_value]); 1701 }); 1702 } 1703 }; 1704 if (tag_name === 'select') { 1705 init_select(); 1706 } else { 1707 init_textbox(); 1708 } 1709 return Object.assign({}, defaults, settings_element, settings_user); 1710 } 1711 1712 var instance_i = 0; 1713 class TomSelect extends MicroPlugin(MicroEvent) { 1714 constructor(input_arg, user_settings) { 1715 super(); 1716 this.order = 0; 1717 this.isOpen = false; 1718 this.isDisabled = false; 1719 this.isReadOnly = false; 1720 this.isInvalid = false; 1721 // @deprecated 1.8 1722 this.isValid = true; 1723 this.isLocked = false; 1724 this.isFocused = false; 1725 this.isInputHidden = false; 1726 this.isSetup = false; 1727 this.ignoreFocus = false; 1728 this.ignoreHover = false; 1729 this.hasOptions = false; 1730 this.lastValue = ''; 1731 this.caretPos = 0; 1732 this.loading = 0; 1733 this.loadedSearches = {}; 1734 this.activeOption = null; 1735 this.activeItems = []; 1736 this.optgroups = {}; 1737 this.options = {}; 1738 this.userOptions = {}; 1739 this.items = []; 1740 this.refreshTimeout = null; 1741 instance_i++; 1742 var dir; 1743 var input = getDom(input_arg); 1744 if (input.tomselect) { 1745 throw new Error('Tom Select already initialized on this element'); 1746 } 1747 input.tomselect = this; 1748 1749 // detect rtl environment 1750 var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null); 1751 dir = computedStyle.getPropertyValue('direction'); 1752 1753 // setup default state 1754 const settings = getSettings(input, user_settings); 1755 this.settings = settings; 1756 this.input = input; 1757 this.tabIndex = input.tabIndex || 0; 1758 this.is_select_tag = input.tagName.toLowerCase() === 'select'; 1759 this.rtl = /rtl/i.test(dir); 1760 this.inputId = getId(input, 'tomselect-' + instance_i); 1761 this.isRequired = input.required; 1762 1763 // search system 1764 this.sifter = new Sifter(this.options, { 1765 diacritics: settings.diacritics 1766 }); 1767 1768 // option-dependent defaults 1769 settings.mode = settings.mode || (settings.maxItems === 1 ? 'single' : 'multi'); 1770 if (typeof settings.hideSelected !== 'boolean') { 1771 settings.hideSelected = settings.mode === 'multi'; 1772 } 1773 if (typeof settings.hidePlaceholder !== 'boolean') { 1774 settings.hidePlaceholder = settings.mode !== 'multi'; 1775 } 1776 1777 // set up createFilter callback 1778 var filter = settings.createFilter; 1779 if (typeof filter !== 'function') { 1780 if (typeof filter === 'string') { 1781 filter = new RegExp(filter); 1782 } 1783 if (filter instanceof RegExp) { 1784 settings.createFilter = input => filter.test(input); 1785 } else { 1786 settings.createFilter = value => { 1787 return this.settings.duplicates || !this.options[value]; 1788 }; 1789 } 1790 } 1791 this.initializePlugins(settings.plugins); 1792 this.setupCallbacks(); 1793 this.setupTemplates(); 1794 1795 // Create all elements 1796 const wrapper = getDom('<div>'); 1797 const control = getDom('<div>'); 1798 const dropdown = this._render('dropdown'); 1799 const dropdown_content = getDom(`<div role="listbox" tabindex="-1">`); 1800 const classes = this.input.getAttribute('class') || ''; 1801 const inputMode = settings.mode; 1802 var control_input; 1803 addClasses(wrapper, settings.wrapperClass, classes, inputMode); 1804 addClasses(control, settings.controlClass); 1805 append(wrapper, control); 1806 addClasses(dropdown, settings.dropdownClass, inputMode); 1807 if (settings.copyClassesToDropdown) { 1808 addClasses(dropdown, classes); 1809 } 1810 addClasses(dropdown_content, settings.dropdownContentClass); 1811 append(dropdown, dropdown_content); 1812 getDom(settings.dropdownParent || wrapper).appendChild(dropdown); 1813 1814 // default controlInput 1815 if (isHtmlString(settings.controlInput)) { 1816 control_input = getDom(settings.controlInput); 1817 1818 // set attributes 1819 var attrs = ['autocorrect', 'autocapitalize', 'autocomplete', 'spellcheck']; 1820 iterate(attrs, attr => { 1821 if (input.getAttribute(attr)) { 1822 setAttr(control_input, { 1823 [attr]: input.getAttribute(attr) 1824 }); 1825 } 1826 }); 1827 control_input.tabIndex = -1; 1828 control.appendChild(control_input); 1829 this.focus_node = control_input; 1830 1831 // dom element 1832 } else if (settings.controlInput) { 1833 control_input = getDom(settings.controlInput); 1834 this.focus_node = control_input; 1835 } else { 1836 control_input = getDom('<input/>'); 1837 this.focus_node = control; 1838 } 1839 this.wrapper = wrapper; 1840 this.dropdown = dropdown; 1841 this.dropdown_content = dropdown_content; 1842 this.control = control; 1843 this.control_input = control_input; 1844 this.setup(); 1845 } 1846 1847 /** 1848 * set up event bindings. 1849 * 1850 */ 1851 setup() { 1852 const self = this; 1853 const settings = self.settings; 1854 const control_input = self.control_input; 1855 const dropdown = self.dropdown; 1856 const dropdown_content = self.dropdown_content; 1857 const wrapper = self.wrapper; 1858 const control = self.control; 1859 const input = self.input; 1860 const focus_node = self.focus_node; 1861 const passive_event = { 1862 passive: true 1863 }; 1864 const listboxId = self.inputId + '-ts-dropdown'; 1865 setAttr(dropdown_content, { 1866 id: listboxId 1867 }); 1868 setAttr(focus_node, { 1869 role: 'combobox', 1870 'aria-haspopup': 'listbox', 1871 'aria-expanded': 'false', 1872 'aria-controls': listboxId 1873 }); 1874 const control_id = getId(focus_node, self.inputId + '-ts-control'); 1875 const query = "label[for='" + escapeQuery(self.inputId) + "']"; 1876 const label = document.querySelector(query); 1877 const label_click = self.focus.bind(self); 1878 if (label) { 1879 addEvent(label, 'click', label_click); 1880 setAttr(label, { 1881 for: control_id 1882 }); 1883 const label_id = getId(label, self.inputId + '-ts-label'); 1884 setAttr(focus_node, { 1885 'aria-labelledby': label_id 1886 }); 1887 setAttr(dropdown_content, { 1888 'aria-labelledby': label_id 1889 }); 1890 } 1891 wrapper.style.width = input.style.width; 1892 if (self.plugins.names.length) { 1893 const classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-'); 1894 addClasses([wrapper, dropdown], classes_plugins); 1895 } 1896 if ((settings.maxItems === null || settings.maxItems > 1) && self.is_select_tag) { 1897 setAttr(input, { 1898 multiple: 'multiple' 1899 }); 1900 } 1901 if (settings.placeholder) { 1902 setAttr(control_input, { 1903 placeholder: settings.placeholder 1904 }); 1905 } 1906 1907 // if splitOn was not passed in, construct it from the delimiter to allow pasting universally 1908 if (!settings.splitOn && settings.delimiter) { 1909 settings.splitOn = new RegExp('\\s*' + escape_regex(settings.delimiter) + '+\\s*'); 1910 } 1911 1912 // debounce user defined load() if loadThrottle > 0 1913 // after initializePlugins() so plugins can create/modify user defined loaders 1914 if (settings.load && settings.loadThrottle) { 1915 settings.load = loadDebounce(settings.load, settings.loadThrottle); 1916 } 1917 addEvent(dropdown, 'mousemove', () => { 1918 self.ignoreHover = false;
vendor: 3,750 bytes, lines 1919-2028
1919 }); 1920 addEvent(dropdown, 'mouseenter', e => { 1921 var target_match = parentMatch(e.target, '[data-selectable]', dropdown); 1922 if (target_match) self.onOptionHover(e, target_match); 1923 }, { 1924 capture: true 1925 }); 1926 1927 // clicking on an option should select it 1928 addEvent(dropdown, 'click', evt => { 1929 const option = parentMatch(evt.target, '[data-selectable]'); 1930 if (option) { 1931 self.onOptionSelect(evt, option); 1932 preventDefault(evt, true); 1933 } 1934 }); 1935 addEvent(control, 'click', evt => { 1936 var target_match = parentMatch(evt.target, '[data-ts-item]', control); 1937 if (target_match && self.onItemSelect(evt, target_match)) { 1938 preventDefault(evt, true); 1939 return; 1940 } 1941 1942 // retain focus (see control_input mousedown) 1943 if (control_input.value != '') { 1944 return; 1945 } 1946 self.onClick(); 1947 preventDefault(evt, true); 1948 }); 1949 1950 // keydown on focus_node for arrow_down/arrow_up 1951 addEvent(focus_node, 'keydown', e => self.onKeyDown(e)); 1952 1953 // keypress and input/keyup 1954 addEvent(control_input, 'keypress', e => self.onKeyPress(e)); 1955 addEvent(control_input, 'input', e => self.onInput(e)); 1956 addEvent(focus_node, 'blur', e => self.onBlur(e)); 1957 addEvent(focus_node, 'focus', e => self.onFocus(e)); 1958 addEvent(control_input, 'paste', e => self.onPaste(e)); 1959 const doc_mousedown = evt => { 1960 // blur if target is outside of this instance 1961 // dropdown is not always inside wrapper 1962 const target = evt.composedPath()[0]; 1963 if (!wrapper.contains(target) && !dropdown.contains(target)) { 1964 if (self.isFocused) { 1965 self.blur(); 1966 } 1967 self.inputState(); 1968 return; 1969 } 1970 1971 // retain focus by preventing native handling. if the 1972 // event target is the input it should not be modified. 1973 // otherwise, text selection within the input won't work. 1974 // Fixes bug #212 which is no covered by tests 1975 if (target == control_input && self.isOpen) { 1976 evt.stopPropagation(); 1977 1978 // clicking anywhere in the control should not blur the control_input (which would close the dropdown) 1979 } else { 1980 preventDefault(evt, true); 1981 } 1982 }; 1983 const win_scroll = () => { 1984 if (self.isOpen) { 1985 self.positionDropdown(); 1986 } 1987 }; 1988 addEvent(document, 'mousedown', doc_mousedown); 1989 addEvent(window, 'scroll', win_scroll, passive_event); 1990 addEvent(window, 'resize', win_scroll, passive_event); 1991 this._destroy = () => { 1992 document.removeEventListener('mousedown', doc_mousedown); 1993 window.removeEventListener('scroll', win_scroll); 1994 window.removeEventListener('resize', win_scroll); 1995 if (label) label.removeEventListener('click', label_click); 1996 }; 1997 1998 // store original html and tab index so that they can be 1999 // restored when the destroy() method is called. 2000 this.revertSettings = { 2001 innerHTML: input.innerHTML, 2002 tabIndex: input.tabIndex 2003 }; 2004 input.tabIndex = -1; 2005 input.insertAdjacentElement('afterend', self.wrapper); 2006 self.sync(false); 2007 settings.items = []; 2008 delete settings.optgroups; 2009 delete settings.options; 2010 addEvent(input, 'invalid', () => { 2011 if (self.isValid) { 2012 self.isValid = false; 2013 self.isInvalid = true; 2014 self.refreshState(); 2015 } 2016 }); 2017 self.updateOriginalInput(); 2018 self.refreshItems(); 2019 self.close(false); 2020 self.inputState(); 2021 self.isSetup = true; 2022 if (input.disabled) { 2023 self.disable(); 2024 } else if (input.readOnly) { 2025 self.setReadOnly(true); 2026 } else { 2027 self.enable(); //sets tabIndex 2028 }
vendor: 3,065 bytes, lines 2028-2132
2028 2029 self.on('change', this.onChange); 2030 addClasses(input, 'tomselected', 'ts-hidden-accessible'); 2031 self.trigger('initialize'); 2032 2033 // preload options 2034 if (settings.preload === true) { 2035 self.preload(); 2036 } 2037 } 2038 2039 /** 2040 * Register options and optgroups 2041 * 2042 */ 2043 setupOptions(options = [], optgroups = []) { 2044 // build options table 2045 this.addOptions(options); 2046 2047 // build optgroup table 2048 iterate(optgroups, optgroup => { 2049 this.registerOptionGroup(optgroup); 2050 }); 2051 } 2052 2053 /** 2054 * Sets up default rendering functions. 2055 */ 2056 setupTemplates() { 2057 var self = this; 2058 var field_label = self.settings.labelField; 2059 var field_optgroup = self.settings.optgroupLabelField; 2060 var templates = { 2061 'optgroup': data => { 2062 let optgroup = document.createElement('div'); 2063 optgroup.className = 'optgroup'; 2064 optgroup.appendChild(data.options); 2065 return optgroup; 2066 }, 2067 'optgroup_header': (data, escape) => { 2068 return '<div class="optgroup-header">' + escape(data[field_optgroup]) + '</div>'; 2069 }, 2070 'option': (data, escape) => { 2071 return '<div>' + escape(data[field_label]) + '</div>'; 2072 }, 2073 'item': (data, escape) => { 2074 return '<div>' + escape(data[field_label]) + '</div>'; 2075 }, 2076 'option_create': (data, escape) => { 2077 return '<div class="create">Add <strong>' + escape(data.input) + '</strong>…</div>'; 2078 }, 2079 'no_results': () => { 2080 return '<div class="no-results">No results found</div>'; 2081 }, 2082 'loading': () => { 2083 return '<div class="spinner"></div>'; 2084 }, 2085 'not_loading': () => {}, 2086 'dropdown': () => { 2087 return '<div></div>'; 2088 } 2089 }; 2090 self.settings.render = Object.assign({}, templates, self.settings.render); 2091 } 2092 2093 /** 2094 * Maps fired events to callbacks provided 2095 * in the settings used when creating the control. 2096 */ 2097 setupCallbacks() { 2098 var key, fn; 2099 var callbacks = { 2100 'initialize': 'onInitialize', 2101 'change': 'onChange', 2102 'item_add': 'onItemAdd', 2103 'item_remove': 'onItemRemove', 2104 'item_select': 'onItemSelect', 2105 'clear': 'onClear', 2106 'option_add': 'onOptionAdd', 2107 'option_remove': 'onOptionRemove', 2108 'option_clear': 'onOptionClear', 2109 'optgroup_add': 'onOptionGroupAdd', 2110 'optgroup_remove': 'onOptionGroupRemove', 2111 'optgroup_clear': 'onOptionGroupClear', 2112 'dropdown_open': 'onDropdownOpen', 2113 'dropdown_close': 'onDropdownClose', 2114 'type': 'onType', 2115 'load': 'onLoad', 2116 'focus': 'onFocus', 2117 'blur': 'onBlur' 2118 }; 2119 for (key in callbacks) { 2120 fn = this.settings[callbacks[key]]; 2121 if (fn) this.on(key, fn); 2122 } 2123 } 2124 2125 /** 2126 * Sync the Tom Select instance with the original input or select 2127 * 2128 */ 2129 sync(get_settings = true) { 2130 const self = this; 2131 const settings = get_settings ? getSettings(self.input, { 2132 delimiter: self.settings.delimiter
vendor: 7,110 bytes, lines 2132-2412
2132 2133 }) : self.settings; 2134 self.setupOptions(settings.options, settings.optgroups); 2135 self.setValue(settings.items || [], true); // silent prevents recursion 2136 2137 self.lastQuery = null; // so updated options will be displayed in dropdown 2138 } 2139 2140 /** 2141 * Triggered when the main control element 2142 * has a click event. 2143 * 2144 */ 2145 onClick() { 2146 var self = this; 2147 if (self.activeItems.length > 0) { 2148 self.clearActiveItems(); 2149 self.focus(); 2150 return; 2151 } 2152 if (self.isFocused && self.isOpen) { 2153 self.blur(); 2154 } else { 2155 self.focus(); 2156 } 2157 } 2158 2159 /** 2160 * @deprecated v1.7 2161 * 2162 */ 2163 onMouseDown() {} 2164 2165 /** 2166 * Triggered when the value of the control has been changed. 2167 * This should propagate the event to the original DOM 2168 * input / select element. 2169 */ 2170 onChange() { 2171 triggerEvent(this.input, 'input'); 2172 triggerEvent(this.input, 'change'); 2173 } 2174 2175 /** 2176 * Triggered on <input> paste. 2177 * 2178 */ 2179 onPaste(e) { 2180 var self = this; 2181 if (self.isInputHidden || self.isLocked) { 2182 preventDefault(e); 2183 return; 2184 } 2185 2186 // If a regex or string is included, this will split the pasted 2187 // input and create Items for each separate value 2188 if (!self.settings.splitOn) { 2189 return; 2190 } 2191 2192 // Wait for pasted text to be recognized in value 2193 setTimeout(() => { 2194 var pastedText = self.inputValue(); 2195 if (!pastedText.match(self.settings.splitOn)) { 2196 return; 2197 } 2198 var splitInput = pastedText.trim().split(self.settings.splitOn); 2199 iterate(splitInput, piece => { 2200 const hash = hash_key(piece); 2201 if (hash) { 2202 if (this.options[piece]) { 2203 self.addItem(piece); 2204 } else { 2205 self.createItem(piece); 2206 } 2207 } 2208 }); 2209 }, 0); 2210 } 2211 2212 /** 2213 * Triggered on <input> keypress. 2214 * 2215 */ 2216 onKeyPress(e) { 2217 var self = this; 2218 if (self.isLocked) { 2219 preventDefault(e); 2220 return; 2221 } 2222 var character = String.fromCharCode(e.keyCode || e.which); 2223 if (self.settings.create && self.settings.mode === 'multi' && character === self.settings.delimiter) { 2224 self.createItem(); 2225 preventDefault(e); 2226 return; 2227 } 2228 } 2229 2230 /** 2231 * Triggered on <input> keydown. 2232 * 2233 */ 2234 onKeyDown(e) { 2235 var self = this; 2236 self.ignoreHover = true; 2237 if (self.isLocked) { 2238 if (e.keyCode !== KEY_TAB) { 2239 preventDefault(e); 2240 } 2241 return; 2242 } 2243 switch (e.keyCode) { 2244 // ctrl+A: select all 2245 case KEY_A: 2246 if (isKeyDown(KEY_SHORTCUT, e)) { 2247 if (self.control_input.value == '') { 2248 preventDefault(e); 2249 self.selectAll(); 2250 return; 2251 } 2252 } 2253 break; 2254 2255 // esc: close dropdown 2256 case KEY_ESC: 2257 if (self.isOpen) { 2258 preventDefault(e, true); 2259 self.close(); 2260 } 2261 self.clearActiveItems(); 2262 return; 2263 2264 // down: open dropdown or move selection down 2265 case KEY_DOWN: 2266 if (!self.isOpen && self.hasOptions) { 2267 self.open(); 2268 } else if (self.activeOption) { 2269 let next = self.getAdjacent(self.activeOption, 1); 2270 if (next) self.setActiveOption(next); 2271 } 2272 preventDefault(e); 2273 return; 2274 2275 // up: move selection up 2276 case KEY_UP: 2277 if (self.activeOption) { 2278 let prev = self.getAdjacent(self.activeOption, -1); 2279 if (prev) self.setActiveOption(prev); 2280 } 2281 preventDefault(e); 2282 return; 2283 2284 // return: select active option 2285 case KEY_RETURN: 2286 if (self.canSelect(self.activeOption)) { 2287 self.onOptionSelect(e, self.activeOption); 2288 preventDefault(e); 2289 2290 // if the option_create=null, the dropdown might be closed 2291 } else if (self.settings.create && self.createItem()) { 2292 preventDefault(e); 2293 2294 // don't submit form when searching for a value 2295 } else if (document.activeElement == self.control_input && self.isOpen) { 2296 preventDefault(e); 2297 } 2298 return; 2299 2300 // left: modifiy item selection to the left 2301 case KEY_LEFT: 2302 self.advanceSelection(-1, e); 2303 return; 2304 2305 // right: modifiy item selection to the right 2306 case KEY_RIGHT: 2307 self.advanceSelection(1, e); 2308 return; 2309 2310 // tab: select active option and/or create item 2311 case KEY_TAB: 2312 if (self.settings.selectOnTab) { 2313 if (self.canSelect(self.activeOption)) { 2314 self.onOptionSelect(e, self.activeOption); 2315 2316 // prevent default [tab] behaviour of jump to the next field 2317 // if select isFull, then the dropdown won't be open and [tab] will work normally 2318 preventDefault(e); 2319 } 2320 if (self.settings.create && self.createItem()) { 2321 preventDefault(e); 2322 } 2323 } 2324 return; 2325 2326 // delete|backspace: delete items 2327 case KEY_BACKSPACE: 2328 case KEY_DELETE: 2329 self.deleteSelection(e); 2330 return; 2331 } 2332 2333 // don't enter text in the control_input when active items are selected 2334 if (self.isInputHidden && !isKeyDown(KEY_SHORTCUT, e)) { 2335 preventDefault(e); 2336 } 2337 } 2338 2339 /** 2340 * Triggered on <input> keyup. 2341 * 2342 */ 2343 onInput(e) { 2344 if (this.isLocked) { 2345 return; 2346 } 2347 const value = this.inputValue(); 2348 if (this.lastValue === value) return; 2349 this.lastValue = value; 2350 if (value == '') { 2351 this._onInput(); 2352 return; 2353 } 2354 if (this.refreshTimeout) { 2355 window.clearTimeout(this.refreshTimeout); 2356 } 2357 this.refreshTimeout = timeout(() => { 2358 this.refreshTimeout = null; 2359 this._onInput(); 2360 }, this.settings.refreshThrottle); 2361 } 2362 _onInput() { 2363 const value = this.lastValue; 2364 if (this.settings.shouldLoad.call(this, value)) { 2365 this.load(value); 2366 } 2367 this.refreshOptions(); 2368 this.trigger('type', value); 2369 } 2370 2371 /** 2372 * Triggered when the user rolls over 2373 * an option in the autocomplete dropdown menu. 2374 * 2375 */ 2376 onOptionHover(evt, option) { 2377 if (this.ignoreHover) return; 2378 this.setActiveOption(option, false); 2379 } 2380 2381 /** 2382 * Triggered on <input> focus. 2383 * 2384 */ 2385 onFocus(e) { 2386 var self = this; 2387 var wasFocused = self.isFocused; 2388 if (self.isDisabled || self.isReadOnly) { 2389 self.blur(); 2390 preventDefault(e); 2391 return; 2392 } 2393 if (self.ignoreFocus) return; 2394 self.isFocused = true; 2395 if (self.settings.preload === 'focus') self.preload(); 2396 if (!wasFocused) self.trigger('focus'); 2397 if (!self.activeItems.length) { 2398 self.inputState(); 2399 self.refreshOptions(!!self.settings.openOnFocus); 2400 } 2401 self.refreshState(); 2402 } 2403 2404 /** 2405 * Triggered on <input> blur. 2406 * 2407 */ 2408 onBlur(e) { 2409 if (document.hasFocus() === false) return; 2410 var self = this; 2411 if (!self.isFocused) return; 2412 self.isFocused = false;
vendor: 3,080 bytes, lines 2413-2529
2413 self.ignoreFocus = false; 2414 var deactivate = () => { 2415 self.close(); 2416 self.setActiveItem(); 2417 self.setCaret(self.items.length); 2418 self.trigger('blur'); 2419 }; 2420 if (self.settings.create && self.settings.createOnBlur) { 2421 self.createItem(null, deactivate); 2422 } else { 2423 deactivate(); 2424 } 2425 } 2426 2427 /** 2428 * Triggered when the user clicks on an option 2429 * in the autocomplete dropdown menu. 2430 * 2431 */ 2432 onOptionSelect(evt, option) { 2433 var value, 2434 self = this; 2435 2436 // should not be possible to trigger a option under a disabled optgroup 2437 if (option.parentElement && option.parentElement.matches('[data-disabled]')) { 2438 return; 2439 } 2440 if (option.classList.contains('create')) { 2441 self.createItem(null, () => { 2442 if (self.settings.closeAfterSelect) { 2443 self.close(); 2444 } 2445 }); 2446 } else { 2447 value = option.dataset.value; 2448 if (typeof value !== 'undefined') { 2449 self.lastQuery = null; 2450 self.addItem(value); 2451 if (self.settings.closeAfterSelect) { 2452 self.close(); 2453 } 2454 if (!self.settings.hideSelected && evt.type && /click/.test(evt.type)) { 2455 self.setActiveOption(option); 2456 } 2457 } 2458 } 2459 } 2460 2461 /** 2462 * Return true if the given option can be selected 2463 * 2464 */ 2465 canSelect(option) { 2466 if (this.isOpen && option && this.dropdown_content.contains(option)) { 2467 return true; 2468 } 2469 return false; 2470 } 2471 2472 /** 2473 * Triggered when the user clicks on an item 2474 * that has been selected. 2475 * 2476 */ 2477 onItemSelect(evt, item) { 2478 var self = this; 2479 if (!self.isLocked && self.settings.mode === 'multi') { 2480 preventDefault(evt); 2481 self.setActiveItem(item, evt); 2482 return true; 2483 } 2484 return false; 2485 } 2486 2487 /** 2488 * Determines whether or not to invoke 2489 * the user-provided option provider / loader 2490 * 2491 * Note, there is a subtle difference between 2492 * this.canLoad() and this.settings.shouldLoad(); 2493 * 2494 * - settings.shouldLoad() is a user-input validator. 2495 * When false is returned, the not_loading template 2496 * will be added to the dropdown 2497 * 2498 * - canLoad() is lower level validator that checks 2499 * the Tom Select instance. There is no inherent user 2500 * feedback when canLoad returns false 2501 * 2502 */ 2503 canLoad(value) { 2504 if (!this.settings.load) return false; 2505 if (this.loadedSearches.hasOwnProperty(value)) return false; 2506 return true; 2507 } 2508 2509 /** 2510 * Invokes the user-provided option provider / loader. 2511 * 2512 */ 2513 load(value) { 2514 const self = this; 2515 if (!self.canLoad(value)) return; 2516 addClasses(self.wrapper, self.settings.loadingClass); 2517 self.loading++; 2518 const callback = self.loadCallback.bind(self); 2519 self.settings.load.call(self, value, callback); 2520 } 2521 2522 /** 2523 * Invoked by the user-provided option provider 2524 * 2525 */ 2526 loadCallback(options, optgroups) { 2527 const self = this; 2528 self.loading = Math.max(self.loading - 1, 0); 2529 self.lastQuery = null
vendor: 7,348 bytes, lines 2529-2809
2529; 2530 self.clearActiveOption(); // when new results load, focus should be on first option 2531 self.setupOptions(options, optgroups); 2532 self.refreshOptions(self.isFocused && !self.isInputHidden); 2533 if (!self.loading) { 2534 removeClasses(self.wrapper, self.settings.loadingClass); 2535 } 2536 self.trigger('load', options, optgroups); 2537 } 2538 preload() { 2539 var classList = this.wrapper.classList; 2540 if (classList.contains('preloaded')) return; 2541 classList.add('preloaded'); 2542 this.load(''); 2543 } 2544 2545 /** 2546 * Sets the input field of the control to the specified value. 2547 * 2548 */ 2549 setTextboxValue(value = '') { 2550 var input = this.control_input; 2551 var changed = input.value !== value; 2552 if (changed) { 2553 input.value = value; 2554 triggerEvent(input, 'update'); 2555 this.lastValue = value; 2556 } 2557 } 2558 2559 /** 2560 * Returns the value of the control. If multiple items 2561 * can be selected (e.g. <select multiple>), this returns 2562 * an array. If only one item can be selected, this 2563 * returns a string. 2564 * 2565 */ 2566 getValue() { 2567 if (this.is_select_tag && this.input.hasAttribute('multiple')) { 2568 return this.items; 2569 } 2570 return this.items.join(this.settings.delimiter); 2571 } 2572 2573 /** 2574 * Resets the selected items to the given value. 2575 * 2576 */ 2577 setValue(value, silent) { 2578 var events = silent ? [] : ['change']; 2579 debounce_events(this, events, () => { 2580 this.clear(silent); 2581 this.addItems(value, silent); 2582 }); 2583 } 2584 2585 /** 2586 * Resets the number of max items to the given value 2587 * 2588 */ 2589 setMaxItems(value) { 2590 if (value === 0) value = null; //reset to unlimited items. 2591 this.settings.maxItems = value; 2592 this.refreshState(); 2593 } 2594 2595 /** 2596 * Sets the selected item. 2597 * 2598 */ 2599 setActiveItem(item, e) { 2600 var self = this; 2601 var eventName; 2602 var i, begin, end, swap; 2603 var last; 2604 if (self.settings.mode === 'single') return; 2605 2606 // clear the active selection 2607 if (!item) { 2608 self.clearActiveItems(); 2609 if (self.isFocused) { 2610 self.inputState(); 2611 } 2612 return; 2613 } 2614 2615 // modify selection 2616 eventName = e && e.type.toLowerCase(); 2617 if (eventName === 'click' && isKeyDown('shiftKey', e) && self.activeItems.length) { 2618 last = self.getLastActive(); 2619 begin = Array.prototype.indexOf.call(self.control.children, last); 2620 end = Array.prototype.indexOf.call(self.control.children, item); 2621 if (begin > end) { 2622 swap = begin; 2623 begin = end; 2624 end = swap; 2625 } 2626 for (i = begin; i <= end; i++) { 2627 item = self.control.children[i]; 2628 if (self.activeItems.indexOf(item) === -1) { 2629 self.setActiveItemClass(item); 2630 } 2631 } 2632 preventDefault(e); 2633 } else if (eventName === 'click' && isKeyDown(KEY_SHORTCUT, e) || eventName === 'keydown' && isKeyDown('shiftKey', e)) { 2634 if (item.classList.contains('active')) { 2635 self.removeActiveItem(item); 2636 } else { 2637 self.setActiveItemClass(item); 2638 } 2639 } else { 2640 self.clearActiveItems(); 2641 self.setActiveItemClass(item); 2642 } 2643 2644 // ensure control has focus 2645 self.inputState(); 2646 if (!self.isFocused) { 2647 self.focus(); 2648 } 2649 } 2650 2651 /** 2652 * Set the active and last-active classes 2653 * 2654 */ 2655 setActiveItemClass(item) { 2656 const self = this; 2657 const last_active = self.control.querySelector('.last-active'); 2658 if (last_active) removeClasses(last_active, 'last-active'); 2659 addClasses(item, 'active last-active'); 2660 self.trigger('item_select', item); 2661 if (self.activeItems.indexOf(item) == -1) { 2662 self.activeItems.push(item); 2663 } 2664 } 2665 2666 /** 2667 * Remove active item 2668 * 2669 */ 2670 removeActiveItem(item) { 2671 var idx = this.activeItems.indexOf(item); 2672 this.activeItems.splice(idx, 1); 2673 removeClasses(item, 'active'); 2674 } 2675 2676 /** 2677 * Clears all the active items 2678 * 2679 */ 2680 clearActiveItems() { 2681 removeClasses(this.activeItems, 'active'); 2682 this.activeItems = []; 2683 } 2684 2685 /** 2686 * Sets the selected item in the dropdown menu 2687 * of available options. 2688 * 2689 */ 2690 setActiveOption(option, scroll = true) { 2691 if (option === this.activeOption) { 2692 return; 2693 } 2694 this.clearActiveOption(); 2695 if (!option) return; 2696 this.activeOption = option; 2697 setAttr(this.focus_node, { 2698 'aria-activedescendant': option.getAttribute('id') 2699 }); 2700 setAttr(option, { 2701 'aria-selected': 'true' 2702 }); 2703 addClasses(option, 'active'); 2704 if (scroll) this.scrollToOption(option); 2705 } 2706 2707 /** 2708 * Sets the dropdown_content scrollTop to display the option 2709 * 2710 */ 2711 scrollToOption(option, behavior) { 2712 if (!option) return; 2713 const content = this.dropdown_content; 2714 const height_menu = content.clientHeight; 2715 const scrollTop = content.scrollTop || 0; 2716 const height_item = option.offsetHeight; 2717 const y = option.getBoundingClientRect().top - content.getBoundingClientRect().top + scrollTop; 2718 if (y + height_item > height_menu + scrollTop) { 2719 this.scroll(y - height_menu + height_item, behavior); 2720 } else if (y < scrollTop) { 2721 this.scroll(y, behavior); 2722 } 2723 } 2724 2725 /** 2726 * Scroll the dropdown to the given position 2727 * 2728 */ 2729 scroll(scrollTop, behavior) { 2730 const content = this.dropdown_content; 2731 if (behavior) { 2732 content.style.scrollBehavior = behavior; 2733 } 2734 content.scrollTop = scrollTop; 2735 content.style.scrollBehavior = ''; 2736 } 2737 2738 /** 2739 * Clears the active option 2740 * 2741 */ 2742 clearActiveOption() { 2743 if (this.activeOption) { 2744 removeClasses(this.activeOption, 'active'); 2745 setAttr(this.activeOption, { 2746 'aria-selected': null 2747 }); 2748 } 2749 this.activeOption = null; 2750 setAttr(this.focus_node, { 2751 'aria-activedescendant': null 2752 }); 2753 } 2754 2755 /** 2756 * Selects all items (CTRL + A). 2757 */ 2758 selectAll() { 2759 const self = this; 2760 if (self.settings.mode === 'single') return; 2761 const activeItems = self.controlChildren(); 2762 if (!activeItems.length) return; 2763 self.inputState(); 2764 self.close(); 2765 self.activeItems = activeItems; 2766 iterate(activeItems, item => { 2767 self.setActiveItemClass(item); 2768 }); 2769 } 2770 2771 /** 2772 * Determines if the control_input should be in a hidden or visible state 2773 * 2774 */ 2775 inputState() { 2776 var self = this; 2777 if (!self.control.contains(self.control_input)) return; 2778 setAttr(self.control_input, { 2779 placeholder: self.settings.placeholder 2780 }); 2781 if (self.activeItems.length > 0 || !self.isFocused && self.settings.hidePlaceholder && self.items.length > 0) { 2782 self.setTextboxValue(); 2783 self.isInputHidden = true; 2784 } else { 2785 if (self.settings.hidePlaceholder && self.items.length > 0) { 2786 setAttr(self.control_input, { 2787 placeholder: '' 2788 }); 2789 } 2790 self.isInputHidden = false; 2791 } 2792 self.wrapper.classList.toggle('input-hidden', self.isInputHidden); 2793 } 2794 2795 /** 2796 * Get the input value 2797 */ 2798 inputValue() { 2799 return this.control_input.value.trim(); 2800 } 2801 2802 /** 2803 * Gives the control focus. 2804 */ 2805 focus() { 2806 var self = this; 2807 if (self.isDisabled || self.isReadOnly) return; 2808 self.ignoreFocus = true; 2809
vendor: 9,948 bytes, lines 2809-3134
2809if (self.control_input.offsetWidth) { 2810 self.control_input.focus(); 2811 } else { 2812 self.focus_node.focus(); 2813 } 2814 setTimeout(() => { 2815 self.ignoreFocus = false; 2816 self.onFocus(); 2817 }, 0); 2818 } 2819 2820 /** 2821 * Forces the control out of focus. 2822 * 2823 */ 2824 blur() { 2825 this.focus_node.blur(); 2826 this.onBlur(); 2827 } 2828 2829 /** 2830 * Returns a function that scores an object 2831 * to show how good of a match it is to the 2832 * provided query. 2833 * 2834 * @return {function} 2835 */ 2836 getScoreFunction(query) { 2837 return this.sifter.getScoreFunction(query, this.getSearchOptions()); 2838 } 2839 2840 /** 2841 * Returns search options for sifter (the system 2842 * for scoring and sorting results). 2843 * 2844 * @see https://github.com/orchidjs/sifter.js 2845 * @return {object} 2846 */ 2847 getSearchOptions() { 2848 var settings = this.settings; 2849 var sort = settings.sortField; 2850 if (typeof settings.sortField === 'string') { 2851 sort = [{ 2852 field: settings.sortField 2853 }]; 2854 } 2855 return { 2856 fields: settings.searchField, 2857 conjunction: settings.searchConjunction, 2858 sort: sort, 2859 nesting: settings.nesting 2860 }; 2861 } 2862 2863 /** 2864 * Searches through available options and returns 2865 * a sorted array of matches. 2866 * 2867 */ 2868 search(query) { 2869 var result, calculateScore; 2870 var self = this; 2871 var options = this.getSearchOptions(); 2872 2873 // validate user-provided result scoring function 2874 if (self.settings.score) { 2875 calculateScore = self.settings.score.call(self, query); 2876 if (typeof calculateScore !== 'function') { 2877 throw new Error('Tom Select "score" setting must be a function that returns a function'); 2878 } 2879 } 2880 2881 // perform search 2882 if (query !== self.lastQuery) { 2883 self.lastQuery = query; 2884 result = self.sifter.search(query, Object.assign(options, { 2885 score: calculateScore 2886 })); 2887 self.currentResults = result; 2888 } else { 2889 result = Object.assign({}, self.currentResults); 2890 } 2891 2892 // filter out selected items 2893 if (self.settings.hideSelected) { 2894 result.items = result.items.filter(item => { 2895 let hashed = hash_key(item.id); 2896 return !(hashed && self.items.indexOf(hashed) !== -1); 2897 }); 2898 } 2899 return result; 2900 } 2901 2902 /** 2903 * Refreshes the list of available options shown 2904 * in the autocomplete dropdown menu. 2905 * 2906 */ 2907 refreshOptions(triggerDropdown = true) { 2908 var i, j, k, n, optgroup, optgroups, html, has_create_option, active_group; 2909 var create; 2910 const groups = {}; 2911 const groups_order = []; 2912 var self = this; 2913 var query = self.inputValue(); 2914 const same_query = query === self.lastQuery || query == '' && self.lastQuery == null; 2915 var results = self.search(query); 2916 var active_option = null; 2917 var show_dropdown = self.settings.shouldOpen || false; 2918 var dropdown_content = self.dropdown_content; 2919 if (same_query) { 2920 active_option = self.activeOption; 2921 if (active_option) { 2922 active_group = active_option.closest('[data-group]'); 2923 } 2924 } 2925 2926 // build markup 2927 n = results.items.length; 2928 if (typeof self.settings.maxOptions === 'number') { 2929 n = Math.min(n, self.settings.maxOptions); 2930 } 2931 if (n > 0) { 2932 show_dropdown = true; 2933 } 2934 2935 // get fragment for group and the position of the group in group_order 2936 const getGroupFragment = (optgroup, order) => { 2937 let group_order_i = groups[optgroup]; 2938 if (group_order_i !== undefined) { 2939 let order_group = groups_order[group_order_i]; 2940 if (order_group !== undefined) { 2941 return [group_order_i, order_group.fragment]; 2942 } 2943 } 2944 let group_fragment = document.createDocumentFragment(); 2945 group_order_i = groups_order.length; 2946 groups_order.push({ 2947 fragment: group_fragment, 2948 order, 2949 optgroup 2950 }); 2951 return [group_order_i, group_fragment]; 2952 }; 2953 2954 // render and group available options individually 2955 for (i = 0; i < n; i++) { 2956 // get option dom element 2957 let item = results.items[i]; 2958 if (!item) continue; 2959 let opt_value = item.id; 2960 let option = self.options[opt_value]; 2961 if (option === undefined) continue; 2962 let opt_hash = get_hash(opt_value); 2963 let option_el = self.getOption(opt_hash, true); 2964 2965 // toggle 'selected' class 2966 if (!self.settings.hideSelected) { 2967 option_el.classList.toggle('selected', self.items.includes(opt_hash)); 2968 } 2969 optgroup = option[self.settings.optgroupField] || ''; 2970 optgroups = Array.isArray(optgroup) ? optgroup : [optgroup]; 2971 for (j = 0, k = optgroups && optgroups.length; j < k; j++) { 2972 optgroup = optgroups[j]; 2973 let order = option.$order; 2974 let self_optgroup = self.optgroups[optgroup]; 2975 if (self_optgroup === undefined) { 2976 optgroup = ''; 2977 } else { 2978 order = self_optgroup.$order; 2979 } 2980 const [group_order_i, group_fragment] = getGroupFragment(optgroup, order); 2981 2982 // nodes can only have one parent, so if the option is in mutple groups, we need a clone 2983 if (j > 0) { 2984 option_el = option_el.cloneNode(true); 2985 setAttr(option_el, { 2986 id: option.$id + '-clone-' + j, 2987 'aria-selected': null 2988 }); 2989 option_el.classList.add('ts-cloned'); 2990 removeClasses(option_el, 'active'); 2991 2992 // make sure we keep the activeOption in the same group 2993 if (self.activeOption && self.activeOption.dataset.value == opt_value) { 2994 if (active_group && active_group.dataset.group === optgroup.toString()) { 2995 active_option = option_el; 2996 } 2997 } 2998 } 2999 group_fragment.appendChild(option_el); 3000 if (optgroup != '') { 3001 groups[optgroup] = group_order_i; 3002 } 3003 } 3004 } 3005 3006 // sort optgroups 3007 if (self.settings.lockOptgroupOrder) { 3008 groups_order.sort((a, b) => { 3009 return a.order - b.order; 3010 }); 3011 } 3012 3013 // render optgroup headers & join groups 3014 html = document.createDocumentFragment(); 3015 iterate(groups_order, group_order => { 3016 let group_fragment = group_order.fragment; 3017 let optgroup = group_order.optgroup; 3018 if (!group_fragment || !group_fragment.children.length) return; 3019 let group_heading = self.optgroups[optgroup]; 3020 if (group_heading !== undefined) { 3021 let group_options = document.createDocumentFragment(); 3022 let header = self.render('optgroup_header', group_heading); 3023 append(group_options, header); 3024 append(group_options, group_fragment); 3025 let group_html = self.render('optgroup', { 3026 group: group_heading, 3027 options: group_options 3028 }); 3029 append(html, group_html); 3030 } else { 3031 append(html, group_fragment); 3032 } 3033 }); 3034 dropdown_content.innerHTML = ''; 3035 append(dropdown_content, html); 3036 3037 // highlight matching terms inline 3038 if (self.settings.highlight) { 3039 removeHighlight(dropdown_content); 3040 if (results.query.length && results.tokens.length) { 3041 iterate(results.tokens, tok => { 3042 highlight(dropdown_content, tok.regex); 3043 }); 3044 } 3045 } 3046 3047 // helper method for adding templates to dropdown 3048 var add_template = template => { 3049 let content = self.render(template, { 3050 input: query 3051 }); 3052 if (content) { 3053 show_dropdown = true; 3054 dropdown_content.insertBefore(content, dropdown_content.firstChild); 3055 } 3056 return content; 3057 }; 3058 3059 // add loading message 3060 if (self.loading) { 3061 add_template('loading'); 3062 3063 // invalid query 3064 } else if (!self.settings.shouldLoad.call(self, query)) { 3065 add_template('not_loading'); 3066 3067 // add no_results message 3068 } else if (results.items.length === 0) { 3069 add_template('no_results'); 3070 } 3071 3072 // add create option 3073 has_create_option = self.canCreate(query); 3074 if (has_create_option) { 3075 create = add_template('option_create'); 3076 } 3077 3078 // activate 3079 self.hasOptions = results.items.length > 0 || has_create_option; 3080 if (show_dropdown) { 3081 if (results.items.length > 0) { 3082 if (!active_option && self.settings.mode === 'single' && self.items[0] != undefined) { 3083 active_option = self.getOption(self.items[0]); 3084 } 3085 if (!dropdown_content.contains(active_option)) { 3086 let active_index = 0; 3087 if (create && !self.settings.addPrecedence) { 3088 active_index = 1; 3089 } 3090 active_option = self.selectable()[active_index]; 3091 } 3092 } else if (create) { 3093 active_option = create; 3094 } 3095 if (triggerDropdown && !self.isOpen) { 3096 self.open(); 3097 self.scrollToOption(active_option, 'auto'); 3098 } 3099 self.setActiveOption(active_option); 3100 } else { 3101 self.clearActiveOption(); 3102 if (triggerDropdown && self.isOpen) { 3103 self.close(false); // if create_option=null, we want the dropdown to close but not reset the textbox value 3104 } 3105 } 3106 } 3107 3108 /** 3109 * Return list of selectable options 3110 * 3111 */ 3112 selectable() { 3113 return this.dropdown_content.querySelectorAll('[data-selectable]'); 3114 } 3115 3116 /** 3117 * Adds an available option. If it already exists, 3118 * nothing will happen. Note: this does not refresh 3119 * the options list dropdown (use `refreshOptions` 3120 * for that). 3121 * 3122 * Usage: 3123 * 3124 * this.addOption(data) 3125 * 3126 */ 3127 addOption(data, user_created = false) { 3128 const self = this; 3129 3130 // @deprecated 1.7.7 3131 // use addOptions( array, user_created ) for adding multiple options 3132 if (Array.isArray(data)) { 3133 self.addOptions(data, user_created); 3134 return false;
vendor: 4,253 bytes, lines 3135-3294
3135 } 3136 const key = hash_key(data[self.settings.valueField]); 3137 if (key === null || self.options.hasOwnProperty(key)) { 3138 return false; 3139 } 3140 data.$order = data.$order || ++self.order; 3141 data.$id = self.inputId + '-opt-' + data.$order; 3142 self.options[key] = data; 3143 self.lastQuery = null; 3144 if (user_created) { 3145 self.userOptions[key] = user_created; 3146 self.trigger('option_add', key, data); 3147 } 3148 return key; 3149 } 3150 3151 /** 3152 * Add multiple options 3153 * 3154 */ 3155 addOptions(data, user_created = false) { 3156 iterate(data, dat => { 3157 this.addOption(dat, user_created); 3158 }); 3159 } 3160 3161 /** 3162 * @deprecated 1.7.7 3163 */ 3164 registerOption(data) { 3165 return this.addOption(data); 3166 } 3167 3168 /** 3169 * Registers an option group to the pool of option groups. 3170 * 3171 * @return {boolean|string} 3172 */ 3173 registerOptionGroup(data) { 3174 var key = hash_key(data[this.settings.optgroupValueField]); 3175 if (key === null) return false; 3176 data.$order = data.$order || ++this.order; 3177 this.optgroups[key] = data; 3178 return key; 3179 } 3180 3181 /** 3182 * Registers a new optgroup for options 3183 * to be bucketed into. 3184 * 3185 */ 3186 addOptionGroup(id, data) { 3187 var hashed_id; 3188 data[this.settings.optgroupValueField] = id; 3189 if (hashed_id = this.registerOptionGroup(data)) { 3190 this.trigger('optgroup_add', hashed_id, data); 3191 } 3192 } 3193 3194 /** 3195 * Removes an existing option group. 3196 * 3197 */ 3198 removeOptionGroup(id) { 3199 if (this.optgroups.hasOwnProperty(id)) { 3200 delete this.optgroups[id]; 3201 this.clearCache(); 3202 this.trigger('optgroup_remove', id); 3203 } 3204 } 3205 3206 /** 3207 * Clears all existing option groups. 3208 */ 3209 clearOptionGroups() { 3210 this.optgroups = {}; 3211 this.clearCache(); 3212 this.trigger('optgroup_clear'); 3213 } 3214 3215 /** 3216 * Updates an option available for selection. If 3217 * it is visible in the selected items or options 3218 * dropdown, it will be re-rendered automatically. 3219 * 3220 */ 3221 updateOption(value, data) { 3222 const self = this; 3223 var item_new; 3224 var index_item; 3225 const value_old = hash_key(value); 3226 const value_new = hash_key(data[self.settings.valueField]); 3227 3228 // sanity checks 3229 if (value_old === null) return; 3230 const data_old = self.options[value_old]; 3231 if (data_old == undefined) return; 3232 if (typeof value_new !== 'string') throw new Error('Value must be set in option data'); 3233 const option = self.getOption(value_old); 3234 const item = self.getItem(value_old); 3235 data.$order = data.$order || data_old.$order; 3236 delete self.options[value_old]; 3237 3238 // invalidate render cache 3239 // don't remove existing node yet, we'll remove it after replacing it 3240 self.uncacheValue(value_new); 3241 self.options[value_new] = data; 3242 3243 // update the option if it's in the dropdown 3244 if (option) { 3245 if (self.dropdown_content.contains(option)) { 3246 const option_new = self._render('option', data); 3247 replaceNode(option, option_new); 3248 if (self.activeOption === option) { 3249 self.setActiveOption(option_new); 3250 } 3251 } 3252 option.remove(); 3253 } 3254 3255 // update the item if we have one 3256 if (item) { 3257 index_item = self.items.indexOf(value_old); 3258 if (index_item !== -1) { 3259 self.items.splice(index_item, 1, value_new); 3260 } 3261 item_new = self._render('item', data); 3262 if (item.classList.contains('active')) addClasses(item_new, 'active'); 3263 replaceNode(item, item_new); 3264 } 3265 3266 // invalidate last query because we might have updated the sortField 3267 self.lastQuery = null; 3268 } 3269 3270 /** 3271 * Removes a single option. 3272 * 3273 */ 3274 removeOption(value, silent) { 3275 const self = this; 3276 value = get_hash(value); 3277 self.uncacheValue(value); 3278 delete self.userOptions[value]; 3279 delete self.options[value]; 3280 self.lastQuery = null; 3281 self.trigger('option_remove', value); 3282 self.removeItem(value, silent); 3283 } 3284 3285 /** 3286 * Clears all options. 3287 */ 3288 clearOptions(filter) { 3289 const boundFilter = (filter || this.clearFilter).bind(this); 3290 this.loadedSearches = {}; 3291 this.userOptions = {}; 3292 this.clearCache(); 3293 const selected = {}; 3294 iterate
vendor: 7,975 bytes, lines 3294-3575
3294(this.options, (option, key) => { 3295 if (boundFilter(option, key)) { 3296 selected[key] = option; 3297 } 3298 }); 3299 this.options = this.sifter.items = selected; 3300 this.lastQuery = null; 3301 this.trigger('option_clear'); 3302 } 3303 3304 /** 3305 * Used by clearOptions() to decide whether or not an option should be removed 3306 * Return true to keep an option, false to remove 3307 * 3308 */ 3309 clearFilter(option, value) { 3310 if (this.items.indexOf(value) >= 0) { 3311 return true; 3312 } 3313 return false; 3314 } 3315 3316 /** 3317 * Returns the dom element of the option 3318 * matching the given value. 3319 * 3320 */ 3321 getOption(value, create = false) { 3322 const hashed = hash_key(value); 3323 if (hashed === null) return null; 3324 const option = this.options[hashed]; 3325 if (option != undefined) { 3326 if (option.$div) { 3327 return option.$div; 3328 } 3329 if (create) { 3330 return this._render('option', option); 3331 } 3332 } 3333 return null; 3334 } 3335 3336 /** 3337 * Returns the dom element of the next or previous dom element of the same type 3338 * Note: adjacent options may not be adjacent DOM elements (optgroups) 3339 * 3340 */ 3341 getAdjacent(option, direction, type = 'option') { 3342 var self = this, 3343 all; 3344 if (!option) { 3345 return null; 3346 } 3347 if (type == 'item') { 3348 all = self.controlChildren(); 3349 } else { 3350 all = self.dropdown_content.querySelectorAll('[data-selectable]'); 3351 } 3352 for (let i = 0; i < all.length; i++) { 3353 if (all[i] != option) { 3354 continue; 3355 } 3356 if (direction > 0) { 3357 return all[i + 1]; 3358 } 3359 return all[i - 1]; 3360 } 3361 return null; 3362 } 3363 3364 /** 3365 * Returns the dom element of the item 3366 * matching the given value. 3367 * 3368 */ 3369 getItem(item) { 3370 if (typeof item == 'object') { 3371 return item; 3372 } 3373 var value = hash_key(item); 3374 return value !== null ? this.control.querySelector(`[data-value="${addSlashes(value)}"]`) : null; 3375 } 3376 3377 /** 3378 * "Selects" multiple items at once. Adds them to the list 3379 * at the current caret position. 3380 * 3381 */ 3382 addItems(values, silent) { 3383 var self = this; 3384 var items = Array.isArray(values) ? values : [values]; 3385 items = items.filter(x => self.items.indexOf(x) === -1); 3386 const last_item = items[items.length - 1]; 3387 items.forEach(item => { 3388 self.isPending = item !== last_item; 3389 self.addItem(item, silent); 3390 }); 3391 } 3392 3393 /** 3394 * "Selects" an item. Adds it to the list 3395 * at the current caret position. 3396 * 3397 */ 3398 addItem(value, silent) { 3399 var events = silent ? [] : ['change', 'dropdown_close']; 3400 debounce_events(this, events, () => { 3401 var item, wasFull; 3402 const self = this; 3403 const inputMode = self.settings.mode; 3404 const hashed = hash_key(value); 3405 if (hashed && self.items.indexOf(hashed) !== -1) { 3406 if (inputMode === 'single') { 3407 self.close(); 3408 } 3409 if (inputMode === 'single' || !self.settings.duplicates) { 3410 return; 3411 } 3412 } 3413 if (hashed === null || !self.options.hasOwnProperty(hashed)) return; 3414 if (inputMode === 'single') self.clear(silent); 3415 if (inputMode === 'multi' && self.isFull()) return; 3416 item = self._render('item', self.options[hashed]); 3417 if (self.control.contains(item)) { 3418 // duplicates 3419 item = item.cloneNode(true); 3420 } 3421 wasFull = self.isFull(); 3422 self.items.splice(self.caretPos, 0, hashed); 3423 self.insertAtCaret(item); 3424 if (self.isSetup) { 3425 // update menu / remove the option (if this is not one item being added as part of series) 3426 if (!self.isPending && self.settings.hideSelected) { 3427 let option = self.getOption(hashed); 3428 let next = self.getAdjacent(option, 1); 3429 if (next) { 3430 self.setActiveOption(next); 3431 } 3432 } 3433 3434 // refreshOptions after setActiveOption(), 3435 // otherwise setActiveOption() will be called by refreshOptions() with the wrong value 3436 if (!self.isPending && !self.settings.closeAfterSelect) { 3437 self.refreshOptions(self.isFocused && inputMode !== 'single'); 3438 } 3439 3440 // hide the menu if the maximum number of items have been selected or no options are left 3441 if (self.settings.closeAfterSelect != false && self.isFull()) { 3442 self.close(); 3443 } else if (!self.isPending) { 3444 self.positionDropdown(); 3445 } 3446 self.trigger('item_add', hashed, item); 3447 if (!self.isPending) { 3448 self.updateOriginalInput({ 3449 silent: silent 3450 }); 3451 } 3452 } 3453 if (!self.isPending || !wasFull && self.isFull()) { 3454 self.inputState(); 3455 self.refreshState(); 3456 } 3457 }); 3458 } 3459 3460 /** 3461 * Removes the selected item matching 3462 * the provided value. 3463 * 3464 */ 3465 removeItem(item = null, silent) { 3466 const self = this; 3467 item = self.getItem(item); 3468 if (!item) return; 3469 var i, idx; 3470 const value = item.dataset.value; 3471 i = nodeIndex(item); 3472 item.remove(); 3473 if (item.classList.contains('active')) { 3474 idx = self.activeItems.indexOf(item); 3475 self.activeItems.splice(idx, 1); 3476 removeClasses(item, 'active'); 3477 } 3478 self.items.splice(i, 1); 3479 self.lastQuery = null; 3480 if (!self.settings.persist && self.userOptions.hasOwnProperty(value)) { 3481 self.removeOption(value, silent); 3482 } 3483 if (i < self.caretPos) { 3484 self.setCaret(self.caretPos - 1); 3485 } 3486 self.updateOriginalInput({ 3487 silent: silent 3488 }); 3489 self.refreshState(); 3490 self.positionDropdown(); 3491 self.trigger('item_remove', value, item); 3492 } 3493 3494 /** 3495 * Invokes the `create` method provided in the 3496 * TomSelect options that should provide the data 3497 * for the new item, given the user input. 3498 * 3499 * Once this completes, it will be added 3500 * to the item list. 3501 * 3502 */ 3503 createItem(input = null, callback = () => {}) { 3504 // triggerDropdown parameter @deprecated 2.1.1 3505 if (arguments.length === 3) { 3506 callback = arguments[2]; 3507 } 3508 if (typeof callback != 'function') { 3509 callback = () => {}; 3510 } 3511 var self = this; 3512 var caret = self.caretPos; 3513 var output; 3514 input = input || self.inputValue(); 3515 if (!self.canCreate(input)) { 3516 callback(); 3517 return false; 3518 } 3519 self.lock(); 3520 var created = false; 3521 var create = data => { 3522 self.unlock(); 3523 if (!data || typeof data !== 'object') return callback(); 3524 var value = hash_key(data[self.settings.valueField]); 3525 if (typeof value !== 'string') { 3526 return callback(); 3527 } 3528 self.setTextboxValue(); 3529 self.addOption(data, true); 3530 self.setCaret(caret); 3531 self.addItem(value); 3532 callback(data); 3533 created = true; 3534 }; 3535 if (typeof self.settings.create === 'function') { 3536 output = self.settings.create.call(this, input, create); 3537 } else { 3538 output = { 3539 [self.settings.labelField]: input, 3540 [self.settings.valueField]: input 3541 }; 3542 } 3543 if (!created) { 3544 create(output); 3545 } 3546 return true; 3547 } 3548 3549 /** 3550 * Re-renders the selected item lists. 3551 */ 3552 refreshItems() { 3553 var self = this; 3554 self.lastQuery = null; 3555 if (self.isSetup) { 3556 self.addItems(self.items); 3557 } 3558 self.updateOriginalInput(); 3559 self.refreshState(); 3560 } 3561 3562 /** 3563 * Updates all state-dependent attributes 3564 * and CSS classes. 3565 */ 3566 refreshState() { 3567 const self = this; 3568 self.refreshValidityState(); 3569 const isFull = self.isFull(); 3570 const isLocked = self.isLocked; 3571 self.wrapper.classList.toggle('rtl', self.rtl); 3572 const wrap_classList = self.wrapper.classList; 3573 wrap_classList.toggle('focus', self.isFocused); 3574 wrap_classList.toggle('disabled', self.isDisabled); 3575 wrap_classList.toggle('re
vendor: 2,412 bytes, lines 3575-3639
3575adonly', self.isReadOnly); 3576 wrap_classList.toggle('required', self.isRequired); 3577 wrap_classList.toggle('invalid', !self.isValid); 3578 wrap_classList.toggle('locked', isLocked); 3579 wrap_classList.toggle('full', isFull); 3580 wrap_classList.toggle('input-active', self.isFocused && !self.isInputHidden); 3581 wrap_classList.toggle('dropdown-active', self.isOpen); 3582 wrap_classList.toggle('has-options', isEmptyObject(self.options)); 3583 wrap_classList.toggle('has-items', self.items.length > 0); 3584 } 3585 3586 /** 3587 * Update the `required` attribute of both input and control input. 3588 * 3589 * The `required` property needs to be activated on the control input 3590 * for the error to be displayed at the right place. `required` also 3591 * needs to be temporarily deactivated on the input since the input is 3592 * hidden and can't show errors. 3593 */ 3594 refreshValidityState() { 3595 var self = this; 3596 if (!self.input.validity) { 3597 return; 3598 } 3599 self.isValid = self.input.validity.valid; 3600 self.isInvalid = !self.isValid; 3601 } 3602 3603 /** 3604 * Determines whether or not more items can be added 3605 * to the control without exceeding the user-defined maximum. 3606 * 3607 * @returns {boolean} 3608 */ 3609 isFull() { 3610 return this.settings.maxItems !== null && this.items.length >= this.settings.maxItems; 3611 } 3612 3613 /** 3614 * Refreshes the original <select> or <input> 3615 * element to reflect the current state. 3616 * 3617 */ 3618 updateOriginalInput(opts = {}) { 3619 const self = this; 3620 var option, label; 3621 const empty_option = self.input.querySelector('option[value=""]'); 3622 if (self.is_select_tag) { 3623 const selected = []; 3624 const has_selected = self.input.querySelectorAll('option:checked').length; 3625 function AddSelected(option_el, value, label) { 3626 if (!option_el) { 3627 option_el = getDom('<option value="' + escape_html(value) + '">' + escape_html(label) + '</option>'); 3628 } 3629 3630 // don't move empty option from top of list 3631 // fixes bug in firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1725293 3632 if (option_el != empty_option) { 3633 self.input.append(option_el); 3634 } 3635 selected.push(option_el); 3636 3637 // marking empty option as selected can break validation 3638 // fixes https://github.com/orchidjs/tom-select/issues/303 3639 if (option_el != empty_option || has_selected > 0
vendor: 5,537 bytes, lines 3639-3835
3639) { 3640 option_el.selected = true; 3641 } 3642 return option_el; 3643 } 3644 3645 // unselect all selected options 3646 self.input.querySelectorAll('option:checked').forEach(option_el => { 3647 option_el.selected = false; 3648 }); 3649 3650 // nothing selected? 3651 if (self.items.length == 0 && self.settings.mode == 'single') { 3652 AddSelected(empty_option, "", ""); 3653 3654 // order selected <option> tags for values in self.items 3655 } else { 3656 self.items.forEach(value => { 3657 option = self.options[value]; 3658 label = option[self.settings.labelField] || ''; 3659 if (selected.includes(option.$option)) { 3660 const reuse_opt = self.input.querySelector(`option[value="${addSlashes(value)}"]:not(:checked)`); 3661 AddSelected(reuse_opt, value, label); 3662 } else { 3663 option.$option = AddSelected(option.$option, value, label); 3664 } 3665 }); 3666 } 3667 } else { 3668 self.input.value = self.getValue(); 3669 } 3670 if (self.isSetup) { 3671 if (!opts.silent) { 3672 self.trigger('change', self.getValue()); 3673 } 3674 } 3675 } 3676 3677 /** 3678 * Shows the autocomplete dropdown containing 3679 * the available options. 3680 */ 3681 open() { 3682 var self = this; 3683 if (self.isLocked || self.isOpen || self.settings.mode === 'multi' && self.isFull()) return; 3684 self.isOpen = true; 3685 setAttr(self.focus_node, { 3686 'aria-expanded': 'true' 3687 }); 3688 self.refreshState(); 3689 applyCSS(self.dropdown, { 3690 visibility: 'hidden', 3691 display: 'block' 3692 }); 3693 self.positionDropdown(); 3694 applyCSS(self.dropdown, { 3695 visibility: 'visible', 3696 display: 'block' 3697 }); 3698 self.focus(); 3699 self.trigger('dropdown_open', self.dropdown); 3700 } 3701 3702 /** 3703 * Closes the autocomplete dropdown menu. 3704 */ 3705 close(setTextboxValue = true) { 3706 var self = this; 3707 var trigger = self.isOpen; 3708 if (setTextboxValue) { 3709 // before blur() to prevent form onchange event 3710 self.setTextboxValue(); 3711 if (self.settings.mode === 'single' && self.items.length) { 3712 self.inputState(); 3713 } 3714 } 3715 self.isOpen = false; 3716 setAttr(self.focus_node, { 3717 'aria-expanded': 'false' 3718 }); 3719 applyCSS(self.dropdown, { 3720 display: 'none' 3721 }); 3722 if (self.settings.hideSelected) { 3723 self.clearActiveOption(); 3724 } 3725 self.refreshState(); 3726 if (trigger) self.trigger('dropdown_close', self.dropdown); 3727 } 3728 3729 /** 3730 * Calculates and applies the appropriate 3731 * position of the dropdown if dropdownParent = 'body'. 3732 * Otherwise, position is determined by css 3733 */ 3734 positionDropdown() { 3735 if (this.settings.dropdownParent !== 'body') { 3736 return; 3737 } 3738 var context = this.control; 3739 var rect = context.getBoundingClientRect(); 3740 var top = context.offsetHeight + rect.top + window.scrollY; 3741 var left = rect.left + window.scrollX; 3742 applyCSS(this.dropdown, { 3743 width: rect.width + 'px', 3744 top: top + 'px', 3745 left: left + 'px' 3746 }); 3747 } 3748 3749 /** 3750 * Resets / clears all selected items 3751 * from the control. 3752 * 3753 */ 3754 clear(silent) { 3755 var self = this; 3756 if (!self.items.length) return; 3757 var items = self.controlChildren(); 3758 iterate(items, item => { 3759 self.removeItem(item, true); 3760 }); 3761 self.inputState(); 3762 if (!silent) self.updateOriginalInput(); 3763 self.trigger('clear'); 3764 } 3765 3766 /** 3767 * A helper method for inserting an element 3768 * at the current caret position. 3769 * 3770 */ 3771 insertAtCaret(el) { 3772 const self = this; 3773 const caret = self.caretPos; 3774 const target = self.control; 3775 target.insertBefore(el, target.children[caret] || null); 3776 self.setCaret(caret + 1); 3777 } 3778 3779 /** 3780 * Removes the current selected item(s). 3781 * 3782 */ 3783 deleteSelection(e) { 3784 var direction, selection, caret, tail; 3785 var self = this; 3786 direction = e && e.keyCode === KEY_BACKSPACE ? -1 : 1; 3787 selection = getSelection(self.control_input); 3788 3789 // determine items that will be removed 3790 const rm_items = []; 3791 if (self.activeItems.length) { 3792 tail = getTail(self.activeItems, direction); 3793 caret = nodeIndex(tail); 3794 if (direction > 0) { 3795 caret++; 3796 } 3797 iterate(self.activeItems, item => rm_items.push(item)); 3798 } else if ((self.isFocused || self.settings.mode === 'single') && self.items.length) { 3799 const items = self.controlChildren(); 3800 let rm_item; 3801 if (direction < 0 && selection.start === 0 && selection.length === 0) { 3802 rm_item = items[self.caretPos - 1]; 3803 } else if (direction > 0 && selection.start === self.inputValue().length) { 3804 rm_item = items[self.caretPos]; 3805 } 3806 if (rm_item !== undefined) { 3807 rm_items.push(rm_item); 3808 } 3809 } 3810 if (!self.shouldDelete(rm_items, e)) { 3811 return false; 3812 } 3813 preventDefault(e, true); 3814 3815 // perform removal 3816 if (typeof caret !== 'undefined') { 3817 self.setCaret(caret); 3818 } 3819 while (rm_items.length) { 3820 self.removeItem(rm_items.pop()); 3821 } 3822 self.inputState(); 3823 self.positionDropdown(); 3824 self.refreshOptions(false); 3825 return true; 3826 } 3827 3828 /** 3829 * Return true if the items should be deleted 3830 */ 3831 shouldDelete(items, evt) { 3832 const values = items.map(item => item.dataset.value); 3833 3834 // allow the callback to abort 3835 if (!values.length || typeof this.settings.onDelete === 'function' && this.settings.onDelete
3835(
vendor: 12,846 bytes, lines 3835-4282
3835values, evt) === false) { 3836 return false; 3837 } 3838 return true; 3839 } 3840 3841 /** 3842 * Selects the previous / next item (depending on the `direction` argument). 3843 * 3844 * > 0 - right 3845 * < 0 - left 3846 * 3847 */ 3848 advanceSelection(direction, e) { 3849 var last_active, 3850 adjacent, 3851 self = this; 3852 if (self.rtl) direction *= -1; 3853 if (self.inputValue().length) return; 3854 3855 // add or remove to active items 3856 if (isKeyDown(KEY_SHORTCUT, e) || isKeyDown('shiftKey', e)) { 3857 last_active = self.getLastActive(direction); 3858 if (last_active) { 3859 if (!last_active.classList.contains('active')) { 3860 adjacent = last_active; 3861 } else { 3862 adjacent = self.getAdjacent(last_active, direction, 'item'); 3863 } 3864 3865 // if no active item, get items adjacent to the control input 3866 } else if (direction > 0) { 3867 adjacent = self.control_input.nextElementSibling; 3868 } else { 3869 adjacent = self.control_input.previousElementSibling; 3870 } 3871 if (adjacent) { 3872 if (adjacent.classList.contains('active')) { 3873 self.removeActiveItem(last_active); 3874 } 3875 self.setActiveItemClass(adjacent); // mark as last_active !! after removeActiveItem() on last_active 3876 } 3877 3878 // move caret to the left or right 3879 } else { 3880 self.moveCaret(direction); 3881 } 3882 } 3883 moveCaret(direction) {} 3884 3885 /** 3886 * Get the last active item 3887 * 3888 */ 3889 getLastActive(direction) { 3890 let last_active = this.control.querySelector('.last-active'); 3891 if (last_active) { 3892 return last_active; 3893 } 3894 var result = this.control.querySelectorAll('.active'); 3895 if (result) { 3896 return getTail(result, direction); 3897 } 3898 } 3899 3900 /** 3901 * Moves the caret to the specified index. 3902 * 3903 * The input must be moved by leaving it in place and moving the 3904 * siblings, due to the fact that focus cannot be restored once lost 3905 * on mobile webkit devices 3906 * 3907 */ 3908 setCaret(new_pos) { 3909 this.caretPos = this.items.length; 3910 } 3911 3912 /** 3913 * Return list of item dom elements 3914 * 3915 */ 3916 controlChildren() { 3917 return Array.from(this.control.querySelectorAll('[data-ts-item]')); 3918 } 3919 3920 /** 3921 * Disables user input on the control. Used while 3922 * items are being asynchronously created. 3923 */ 3924 lock() { 3925 this.setLocked(true); 3926 } 3927 3928 /** 3929 * Re-enables user input on the control. 3930 */ 3931 unlock() { 3932 this.setLocked(false); 3933 } 3934 3935 /** 3936 * Disable or enable user input on the control 3937 */ 3938 setLocked(lock = this.isReadOnly || this.isDisabled) { 3939 this.isLocked = lock; 3940 this.refreshState(); 3941 } 3942 3943 /** 3944 * Disables user input on the control completely. 3945 * While disabled, it cannot receive focus. 3946 */ 3947 disable() { 3948 this.setDisabled(true); 3949 this.close(); 3950 } 3951 3952 /** 3953 * Enables the control so that it can respond 3954 * to focus and user input. 3955 */ 3956 enable() { 3957 this.setDisabled(false); 3958 } 3959 setDisabled(disabled) { 3960 this.focus_node.tabIndex = disabled ? -1 : this.tabIndex; 3961 this.isDisabled = disabled; 3962 this.input.disabled = disabled; 3963 this.control_input.disabled = disabled; 3964 this.setLocked(); 3965 } 3966 setReadOnly(isReadOnly) { 3967 this.isReadOnly = isReadOnly; 3968 this.input.readOnly = isReadOnly; 3969 this.control_input.readOnly = isReadOnly; 3970 this.setLocked(); 3971 } 3972 3973 /** 3974 * Completely destroys the control and 3975 * unbinds all event listeners so that it can 3976 * be garbage collected. 3977 */ 3978 destroy() { 3979 var self = this; 3980 var revertSettings = self.revertSettings; 3981 self.trigger('destroy'); 3982 self.off(); 3983 self.wrapper.remove(); 3984 self.dropdown.remove(); 3985 self.input.innerHTML = revertSettings.innerHTML; 3986 self.input.tabIndex = revertSettings.tabIndex; 3987 removeClasses(self.input, 'tomselected', 'ts-hidden-accessible'); 3988 self._destroy(); 3989 delete self.input.tomselect; 3990 } 3991 3992 /** 3993 * A helper method for rendering "item" and 3994 * "option" templates, given the data. 3995 * 3996 */ 3997 render(templateName, data) { 3998 var id, html; 3999 const self = this; 4000 if (typeof this.settings.render[templateName] !== 'function') { 4001 return null; 4002 } 4003 4004 // render markup 4005 html = self.settings.render[templateName].call(this, data, escape_html); 4006 if (!html) { 4007 return null; 4008 } 4009 html = getDom(html); 4010 4011 // add mandatory attributes 4012 if (templateName === 'option' || templateName === 'option_create') { 4013 if (data[self.settings.disabledField]) { 4014 setAttr(html, { 4015 'aria-disabled': 'true' 4016 }); 4017 } else { 4018 setAttr(html, { 4019 'data-selectable': '' 4020 }); 4021 } 4022 } else if (templateName === 'optgroup') { 4023 id = data.group[self.settings.optgroupValueField]; 4024 setAttr(html, { 4025 'data-group': id 4026 }); 4027 if (data.group[self.settings.disabledField]) { 4028 setAttr(html, { 4029 'data-disabled': '' 4030 }); 4031 } 4032 } 4033 if (templateName === 'option' || templateName === 'item') { 4034 const value = get_hash(data[self.settings.valueField]); 4035 setAttr(html, { 4036 'data-value': value 4037 }); 4038 4039 // make sure we have some classes if a template is overwritten 4040 if (templateName === 'item') { 4041 addClasses(html, self.settings.itemClass); 4042 setAttr(html, { 4043 'data-ts-item': '' 4044 }); 4045 } else { 4046 addClasses(html, self.settings.optionClass); 4047 setAttr(html, { 4048 role: 'option', 4049 id: data.$id 4050 }); 4051 4052 // update cache 4053 data.$div = html; 4054 self.options[value] = data; 4055 } 4056 } 4057 return html; 4058 } 4059 4060 /** 4061 * Type guarded rendering 4062 * 4063 */ 4064 _render(templateName, data) { 4065 const html = this.render(templateName, data); 4066 if (html == null) { 4067 throw 'HTMLElement expected'; 4068 } 4069 return html; 4070 } 4071 4072 /** 4073 * Clears the render cache for a template. If 4074 * no template is given, clears all render 4075 * caches. 4076 * 4077 */ 4078 clearCache() { 4079 iterate(this.options, option => { 4080 if (option.$div) { 4081 option.$div.remove(); 4082 delete option.$div; 4083 } 4084 }); 4085 } 4086 4087 /** 4088 * Removes a value from item and option caches 4089 * 4090 */ 4091 uncacheValue(value) { 4092 const option_el = this.getOption(value); 4093 if (option_el) option_el.remove(); 4094 } 4095 4096 /** 4097 * Determines whether or not to display the 4098 * create item prompt, given a user input. 4099 * 4100 */ 4101 canCreate(input) { 4102 return this.settings.create && input.length > 0 && this.settings.createFilter.call(this, input); 4103 } 4104 4105 /** 4106 * Wraps this.`method` so that `new_fn` can be invoked 'before', 'after', or 'instead' of the original method 4107 * 4108 * this.hook('instead','onKeyDown',function( arg1, arg2 ...){ 4109 * 4110 * }); 4111 */ 4112 hook(when, method, new_fn) { 4113 var self = this; 4114 var orig_method = self[method]; 4115 self[method] = function () { 4116 var result, result_new; 4117 if (when === 'after') { 4118 result = orig_method.apply(self, arguments); 4119 } 4120 result_new = new_fn.apply(self, arguments); 4121 if (when === 'instead') { 4122 return result_new; 4123 } 4124 if (when === 'before') { 4125 result = orig_method.apply(self, arguments); 4126 } 4127 return result; 4128 }; 4129 } 4130 } 4131 4132 /** 4133 * Plugin: "change_listener" (Tom Select) 4134 * Copyright (c) contributors 4135 * 4136 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4137 * file except in compliance with the License. You may obtain a copy of the License at: 4138 * http://www.apache.org/licenses/LICENSE-2.0 4139 * 4140 * Unless required by applicable law or agreed to in writing, software distributed under 4141 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4142 * ANY KIND, either express or implied. See the License for the specific language 4143 * governing permissions and limitations under the License. 4144 * 4145 */ 4146 4147 function change_listener () { 4148 addEvent(this.input, 'change', () => { 4149 this.sync(); 4150 }); 4151 } 4152 4153 /** 4154 * Plugin: "checkbox_options" (Tom Select) 4155 * Copyright (c) contributors 4156 * 4157 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4158 * file except in compliance with the License. You may obtain a copy of the License at: 4159 * http://www.apache.org/licenses/LICENSE-2.0 4160 * 4161 * Unless required by applicable law or agreed to in writing, software distributed under 4162 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4163 * ANY KIND, either express or implied. See the License for the specific language 4164 * governing permissions and limitations under the License. 4165 * 4166 */ 4167 4168 function checkbox_options (userOptions) { 4169 var self = this; 4170 var orig_onOptionSelect = self.onOptionSelect; 4171 self.settings.hideSelected = false; 4172 const cbOptions = Object.assign({ 4173 // so that the user may add different ones as well 4174 className: "tomselect-checkbox", 4175 // the following default to the historic plugin's values 4176 checkedClassNames: undefined, 4177 uncheckedClassNames: undefined 4178 }, userOptions); 4179 var UpdateChecked = function UpdateChecked(checkbox, toCheck) { 4180 if (toCheck) { 4181 checkbox.checked = true; 4182 if (cbOptions.uncheckedClassNames) { 4183 checkbox.classList.remove(...cbOptions.uncheckedClassNames); 4184 } 4185 if (cbOptions.checkedClassNames) { 4186 checkbox.classList.add(...cbOptions.checkedClassNames); 4187 } 4188 } else { 4189 checkbox.checked = false; 4190 if (cbOptions.checkedClassNames) { 4191 checkbox.classList.remove(...cbOptions.checkedClassNames); 4192 } 4193 if (cbOptions.uncheckedClassNames) { 4194 checkbox.classList.add(...cbOptions.uncheckedClassNames); 4195 } 4196 } 4197 }; 4198 4199 // update the checkbox for an option 4200 var UpdateCheckbox = function UpdateCheckbox(option) { 4201 setTimeout(() => { 4202 var checkbox = option.querySelector('input.' + cbOptions.className); 4203 if (checkbox instanceof HTMLInputElement) { 4204 UpdateChecked(checkbox, option.classList.contains('selected')); 4205 } 4206 }, 1); 4207 }; 4208 4209 // add checkbox to option template 4210 self.hook('after', 'setupTemplates', () => { 4211 var orig_render_option = self.settings.render.option; 4212 self.settings.render.option = (data, escape_html) => { 4213 var rendered = getDom(orig_render_option.call(self, data, escape_html)); 4214 var checkbox = document.createElement('input'); 4215 if (cbOptions.className) { 4216 checkbox.classList.add(cbOptions.className); 4217 } 4218 checkbox.addEventListener('click', function (evt) { 4219 preventDefault(evt); 4220 }); 4221 checkbox.type = 'checkbox'; 4222 const hashed = hash_key(data[self.settings.valueField]); 4223 UpdateChecked(checkbox, !!(hashed && self.items.indexOf(hashed) > -1)); 4224 rendered.prepend(checkbox); 4225 return rendered; 4226 }; 4227 }); 4228 4229 // uncheck when item removed 4230 self.on('item_remove', value => { 4231 var option = self.getOption(value); 4232 if (option) { 4233 // if dropdown hasn't been opened yet, the option won't exist 4234 option.classList.remove('selected'); // selected class won't be removed yet 4235 UpdateCheckbox(option); 4236 } 4237 }); 4238 4239 // check when item added 4240 self.on('item_add', value => { 4241 var option = self.getOption(value); 4242 if (option) { 4243 // if dropdown hasn't been opened yet, the option won't exist 4244 UpdateCheckbox(option); 4245 } 4246 }); 4247 4248 // remove items when selected option is clicked 4249 self.hook('instead', 'onOptionSelect', (evt, option) => { 4250 if (option.classList.contains('selected')) { 4251 option.classList.remove('selected'); 4252 self.removeItem(option.dataset.value); 4253 self.refreshOptions(); 4254 preventDefault(evt, true); 4255 return; 4256 } 4257 orig_onOptionSelect.call(self, evt, option); 4258 UpdateCheckbox(option); 4259 }); 4260 } 4261 4262 /** 4263 * Plugin: "dropdown_header" (Tom Select) 4264 * Copyright (c) contributors 4265 * 4266 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4267 * file except in compliance with the License. You may obtain a copy of the License at: 4268 * http://www.apache.org/licenses/LICENSE-2.0 4269 * 4270 * Unless required by applicable law or agreed to in writing, software distributed under 4271 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4272 * ANY KIND, either express or implied. See the License for the specific language 4273 * governing permissions and limitations under the License. 4274 * 4275 */ 4276 4277 function clear_button (userOptions) { 4278 const self = this; 4279 const options = Object.assign({ 4280 className: 'clear-button', 4281 title: 'Clear All', 4282
4282html: data => { 4283 return `<div class="${data.className}" title="${data.title}">⨯</div>`; 4284 } 4285 }, userOptions); 4286 self.on('initialize', () => { 4287 var button = getDom(options.html(options)); 4288 button.addEventListener('click', evt => { 4289 if (self.isLocked) return; 4290 self.clear(); 4291 if (self.settings.mode === 'single' && self.settings.allowEmptyOption) { 4292 self.addItem(''); 4293 }
vendor: 8,574 bytes, lines 4293-4536
4293 4294 evt.preventDefault(); 4295 evt.stopPropagation(); 4296 }); 4297 self.control.appendChild(button); 4298 }); 4299 } 4300 4301 /** 4302 * Plugin: "drag_drop" (Tom Select) 4303 * Copyright (c) contributors 4304 * 4305 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4306 * file except in compliance with the License. You may obtain a copy of the License at: 4307 * http://www.apache.org/licenses/LICENSE-2.0 4308 * 4309 * Unless required by applicable law or agreed to in writing, software distributed under 4310 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4311 * ANY KIND, either express or implied. See the License for the specific language 4312 * governing permissions and limitations under the License. 4313 * 4314 */ 4315 4316 const insertAfter = (referenceNode, newNode) => { 4317 var _referenceNode$parent; 4318 (_referenceNode$parent = referenceNode.parentNode) == null || _referenceNode$parent.insertBefore(newNode, referenceNode.nextSibling); 4319 }; 4320 const insertBefore = (referenceNode, newNode) => { 4321 var _referenceNode$parent2; 4322 (_referenceNode$parent2 = referenceNode.parentNode) == null || _referenceNode$parent2.insertBefore(newNode, referenceNode); 4323 }; 4324 const isBefore = (referenceNode, newNode) => { 4325 do { 4326 var _newNode; 4327 newNode = (_newNode = newNode) == null ? void 0 : _newNode.previousElementSibling; 4328 if (referenceNode == newNode) { 4329 return true; 4330 } 4331 } while (newNode && newNode.previousElementSibling); 4332 return false; 4333 }; 4334 function drag_drop () { 4335 var self = this; 4336 if (self.settings.mode !== 'multi') return; 4337 var orig_lock = self.lock; 4338 var orig_unlock = self.unlock; 4339 let sortable = true; 4340 let drag_item; 4341 4342 /** 4343 * Add draggable attribute to item 4344 */ 4345 self.hook('after', 'setupTemplates', () => { 4346 var orig_render_item = self.settings.render.item; 4347 self.settings.render.item = (data, escape) => { 4348 const item = getDom(orig_render_item.call(self, data, escape)); 4349 setAttr(item, { 4350 'draggable': 'true' 4351 }); 4352 4353 // prevent doc_mousedown (see tom-select.ts) 4354 const mousedown = evt => { 4355 if (!sortable) preventDefault(evt); 4356 evt.stopPropagation(); 4357 }; 4358 const dragStart = evt => { 4359 drag_item = item; 4360 setTimeout(() => { 4361 item.classList.add('ts-dragging'); 4362 }, 0); 4363 }; 4364 const dragOver = evt => { 4365 evt.preventDefault(); 4366 item.classList.add('ts-drag-over'); 4367 moveitem(item, drag_item); 4368 }; 4369 const dragLeave = () => { 4370 item.classList.remove('ts-drag-over'); 4371 }; 4372 const moveitem = (targetitem, dragitem) => { 4373 if (dragitem === undefined) return; 4374 if (isBefore(dragitem, item)) { 4375 insertAfter(targetitem, dragitem); 4376 } else { 4377 insertBefore(targetitem, dragitem); 4378 } 4379 }; 4380 const dragend = () => { 4381 var _drag_item; 4382 document.querySelectorAll('.ts-drag-over').forEach(el => el.classList.remove('ts-drag-over')); 4383 (_drag_item = drag_item) == null || _drag_item.classList.remove('ts-dragging'); 4384 drag_item = undefined; 4385 var values = []; 4386 self.control.querySelectorAll(`[data-value]`).forEach(el => { 4387 if (el.dataset.value) { 4388 let value = el.dataset.value; 4389 if (value) { 4390 values.push(value); 4391 } 4392 } 4393 }); 4394 self.setValue(values); 4395 }; 4396 addEvent(item, 'mousedown', mousedown); 4397 addEvent(item, 'dragstart', dragStart); 4398 addEvent(item, 'dragenter', dragOver); 4399 addEvent(item, 'dragover', dragOver); 4400 addEvent(item, 'dragleave', dragLeave); 4401 addEvent(item, 'dragend', dragend); 4402 return item; 4403 }; 4404 }); 4405 self.hook('instead', 'lock', () => { 4406 sortable = false; 4407 return orig_lock.call(self); 4408 }); 4409 self.hook('instead', 'unlock', () => { 4410 sortable = true; 4411 return orig_unlock.call(self); 4412 }); 4413 } 4414 4415 /** 4416 * Plugin: "dropdown_header" (Tom Select) 4417 * Copyright (c) contributors 4418 * 4419 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4420 * file except in compliance with the License. You may obtain a copy of the License at: 4421 * http://www.apache.org/licenses/LICENSE-2.0 4422 * 4423 * Unless required by applicable law or agreed to in writing, software distributed under 4424 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4425 * ANY KIND, either express or implied. See the License for the specific language 4426 * governing permissions and limitations under the License. 4427 * 4428 */ 4429 4430 function dropdown_header (userOptions) { 4431 const self = this; 4432 const options = Object.assign({ 4433 title: 'Untitled', 4434 headerClass: 'dropdown-header', 4435 titleRowClass: 'dropdown-header-title', 4436 labelClass: 'dropdown-header-label', 4437 closeClass: 'dropdown-header-close', 4438 html: data => { 4439 return '<div class="' + data.headerClass + '">' + '<div class="' + data.titleRowClass + '">' + '<span class="' + data.labelClass + '">' + data.title + '</span>' + '<a class="' + data.closeClass + '">×</a>' + '</div>' + '</div>'; 4440 } 4441 }, userOptions); 4442 self.on('initialize', () => { 4443 var header = getDom(options.html(options)); 4444 var close_link = header.querySelector('.' + options.closeClass); 4445 if (close_link) { 4446 close_link.addEventListener('click', evt => { 4447 preventDefault(evt, true); 4448 self.close(); 4449 }); 4450 } 4451 self.dropdown.insertBefore(header, self.dropdown.firstChild); 4452 }); 4453 } 4454 4455 /** 4456 * Plugin: "dropdown_input" (Tom Select) 4457 * Copyright (c) contributors 4458 * 4459 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4460 * file except in compliance with the License. You may obtain a copy of the License at: 4461 * http://www.apache.org/licenses/LICENSE-2.0 4462 * 4463 * Unless required by applicable law or agreed to in writing, software distributed under 4464 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4465 * ANY KIND, either express or implied. See the License for the specific language 4466 * governing permissions and limitations under the License. 4467 * 4468 */ 4469 4470 function caret_position () { 4471 var self = this; 4472 4473 /** 4474 * Moves the caret to the specified index. 4475 * 4476 * The input must be moved by leaving it in place and moving the 4477 * siblings, due to the fact that focus cannot be restored once lost 4478 * on mobile webkit devices 4479 * 4480 */ 4481 self.hook('instead', 'setCaret', new_pos => { 4482 if (self.settings.mode === 'single' || !self.control.contains(self.control_input)) { 4483 new_pos = self.items.length; 4484 } else { 4485 new_pos = Math.max(0, Math.min(self.items.length, new_pos)); 4486 if (new_pos != self.caretPos && !self.isPending) { 4487 self.controlChildren().forEach((child, j) => { 4488 if (j < new_pos) { 4489 self.control_input.insertAdjacentElement('beforebegin', child); 4490 } else { 4491 self.control.appendChild(child); 4492 } 4493 }); 4494 } 4495 } 4496 self.caretPos = new_pos; 4497 }); 4498 self.hook('instead', 'moveCaret', direction => { 4499 if (!self.isFocused) return; 4500 4501 // move caret before or after selected items 4502 const last_active = self.getLastActive(direction); 4503 if (last_active) { 4504 const idx = nodeIndex(last_active); 4505 self.setCaret(direction > 0 ? idx + 1 : idx); 4506 self.setActiveItem(); 4507 removeClasses(last_active, 'last-active'); 4508 4509 // move caret left or right of current position 4510 } else { 4511 self.setCaret(self.caretPos + direction); 4512 } 4513 }); 4514 } 4515 4516 /** 4517 * Plugin: "dropdown_input" (Tom Select) 4518 * Copyright (c) contributors 4519 * 4520 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4521 * file except in compliance with the License. You may obtain a copy of the License at: 4522 * http://www.apache.org/licenses/LICENSE-2.0 4523 * 4524 * Unless required by applicable law or agreed to in writing, software distributed under 4525 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4526 * ANY KIND, either express or implied. See the License for the specific language 4527 * governing permissions and limitations under the License. 4528 * 4529 */ 4530 4531 function dropdown_input () { 4532 const self = this; 4533 self.settings.shouldOpen = true; // make sure the input is shown even if there are no options to display in the dropdown 4534 4535 self.hook('before', 'setup', () => { 4536
vendor: 3,768 bytes, lines 4536-4639
4536self.focus_node = self.control; 4537 addClasses(self.control_input, 'dropdown-input'); 4538 const div = getDom('<div class="dropdown-input-wrap">'); 4539 div.append(self.control_input); 4540 self.dropdown.insertBefore(div, self.dropdown.firstChild); 4541 4542 // set a placeholder in the select control 4543 const placeholder = getDom('<input class="items-placeholder" tabindex="-1" />'); 4544 placeholder.placeholder = self.settings.placeholder || ''; 4545 self.control.append(placeholder); 4546 }); 4547 self.on('initialize', () => { 4548 // set tabIndex on control to -1, otherwise [shift+tab] will put focus right back on control_input 4549 self.control_input.addEventListener('keydown', evt => { 4550 //addEvent(self.control_input,'keydown' as const,(evt:KeyboardEvent) =>{ 4551 switch (evt.keyCode) { 4552 case KEY_ESC: 4553 if (self.isOpen) { 4554 preventDefault(evt, true); 4555 self.close(); 4556 } 4557 self.clearActiveItems(); 4558 return; 4559 case KEY_TAB: 4560 self.focus_node.tabIndex = -1; 4561 break; 4562 } 4563 return self.onKeyDown.call(self, evt); 4564 }); 4565 self.on('blur', () => { 4566 self.focus_node.tabIndex = self.isDisabled ? -1 : self.tabIndex; 4567 }); 4568 4569 // give the control_input focus when the dropdown is open 4570 self.on('dropdown_open', () => { 4571 self.control_input.focus(); 4572 }); 4573 4574 // prevent onBlur from closing when focus is on the control_input 4575 const orig_onBlur = self.onBlur; 4576 self.hook('instead', 'onBlur', evt => { 4577 if (evt && evt.relatedTarget == self.control_input) return; 4578 return orig_onBlur.call(self); 4579 }); 4580 addEvent(self.control_input, 'blur', () => self.onBlur()); 4581 4582 // return focus to control to allow further keyboard input 4583 self.hook('before', 'close', () => { 4584 if (!self.isOpen) return; 4585 self.focus_node.focus({ 4586 preventScroll: true 4587 }); 4588 }); 4589 }); 4590 } 4591 4592 /** 4593 * Plugin: "input_autogrow" (Tom Select) 4594 * 4595 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4596 * file except in compliance with the License. You may obtain a copy of the License at: 4597 * http://www.apache.org/licenses/LICENSE-2.0 4598 * 4599 * Unless required by applicable law or agreed to in writing, software distributed under 4600 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4601 * ANY KIND, either express or implied. See the License for the specific language 4602 * governing permissions and limitations under the License. 4603 * 4604 */ 4605 4606 function input_autogrow () { 4607 var self = this; 4608 self.on('initialize', () => { 4609 var test_input = document.createElement('span'); 4610 var control = self.control_input; 4611 test_input.style.cssText = 'position:absolute; top:-99999px; left:-99999px; width:auto; padding:0; white-space:pre; '; 4612 self.wrapper.appendChild(test_input); 4613 var transfer_styles = ['letterSpacing', 'fontSize', 'fontFamily', 'fontWeight', 'textTransform']; 4614 for (const style_name of transfer_styles) { 4615 // @ts-ignore TS7015 https://stackoverflow.com/a/50506154/697576 4616 test_input.style[style_name] = control.style[style_name]; 4617 } 4618 4619 /** 4620 * Set the control width 4621 * 4622 */ 4623 var resize = () => { 4624 test_input.textContent = control.value; 4625 control.style.width = test_input.clientWidth + 'px'; 4626 }; 4627 resize(); 4628 self.on('update item_add item_remove', resize); 4629 addEvent(control, 'input', resize); 4630 addEvent(control, 'keyup', resize); 4631 addEvent(control, 'blur', resize); 4632 addEvent(control, 'update', resize); 4633 }); 4634 } 4635 4636 /** 4637 * Plugin: "input_autogrow" (Tom Select) 4638 * 4639 * Licensed under the Apache License, Version 2.0 (the "License");
vendor: 5,228 bytes, lines 4639-4785
4639 you may not use this 4640 * file except in compliance with the License. You may obtain a copy of the License at: 4641 * http://www.apache.org/licenses/LICENSE-2.0 4642 * 4643 * Unless required by applicable law or agreed to in writing, software distributed under 4644 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4645 * ANY KIND, either express or implied. See the License for the specific language 4646 * governing permissions and limitations under the License. 4647 * 4648 */ 4649 4650 function no_backspace_delete () { 4651 var self = this; 4652 var orig_deleteSelection = self.deleteSelection; 4653 this.hook('instead', 'deleteSelection', evt => { 4654 if (self.activeItems.length) { 4655 return orig_deleteSelection.call(self, evt); 4656 } 4657 return false; 4658 }); 4659 } 4660 4661 /** 4662 * Plugin: "no_active_items" (Tom Select) 4663 * 4664 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4665 * file except in compliance with the License. You may obtain a copy of the License at: 4666 * http://www.apache.org/licenses/LICENSE-2.0 4667 * 4668 * Unless required by applicable law or agreed to in writing, software distributed under 4669 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4670 * ANY KIND, either express or implied. See the License for the specific language 4671 * governing permissions and limitations under the License. 4672 * 4673 */ 4674 4675 function no_active_items () { 4676 this.hook('instead', 'setActiveItem', () => {}); 4677 this.hook('instead', 'selectAll', () => {}); 4678 } 4679 4680 /** 4681 * Plugin: "optgroup_columns" (Tom Select.js) 4682 * Copyright (c) contributors 4683 * 4684 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4685 * file except in compliance with the License. You may obtain a copy of the License at: 4686 * http://www.apache.org/licenses/LICENSE-2.0 4687 * 4688 * Unless required by applicable law or agreed to in writing, software distributed under 4689 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4690 * ANY KIND, either express or implied. See the License for the specific language 4691 * governing permissions and limitations under the License. 4692 * 4693 */ 4694 4695 function optgroup_columns () { 4696 var self = this; 4697 var orig_keydown = self.onKeyDown; 4698 self.hook('instead', 'onKeyDown', evt => { 4699 var index, option, options, optgroup; 4700 if (!self.isOpen || !(evt.keyCode === KEY_LEFT || evt.keyCode === KEY_RIGHT)) { 4701 return orig_keydown.call(self, evt); 4702 } 4703 self.ignoreHover = true; 4704 optgroup = parentMatch(self.activeOption, '[data-group]'); 4705 index = nodeIndex(self.activeOption, '[data-selectable]'); 4706 if (!optgroup) { 4707 return; 4708 } 4709 if (evt.keyCode === KEY_LEFT) { 4710 optgroup = optgroup.previousSibling; 4711 } else { 4712 optgroup = optgroup.nextSibling; 4713 } 4714 if (!optgroup) { 4715 return; 4716 } 4717 options = optgroup.querySelectorAll('[data-selectable]'); 4718 option = options[Math.min(options.length - 1, index)]; 4719 if (option) { 4720 self.setActiveOption(option); 4721 } 4722 }); 4723 } 4724 4725 /** 4726 * Plugin: "remove_button" (Tom Select) 4727 * Copyright (c) contributors 4728 * 4729 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4730 * file except in compliance with the License. You may obtain a copy of the License at: 4731 * http://www.apache.org/licenses/LICENSE-2.0 4732 * 4733 * Unless required by applicable law or agreed to in writing, software distributed under 4734 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4735 * ANY KIND, either express or implied. See the License for the specific language 4736 * governing permissions and limitations under the License. 4737 * 4738 */ 4739 4740 function remove_button (userOptions) { 4741 const options = Object.assign({ 4742 label: '×', 4743 title: 'Remove', 4744 className: 'remove', 4745 append: true 4746 }, userOptions); 4747 4748 //options.className = 'remove-single'; 4749 var self = this; 4750 4751 // override the render method to add remove button to each item 4752 if (!options.append) { 4753 return; 4754 } 4755 var html = '<a href="javascript:void(0)" class="' + options.className + '" tabindex="-1" title="' + escape_html(options.title) + '">' + options.label + '</a>'; 4756 self.hook('after', 'setupTemplates', () => { 4757 var orig_render_item = self.settings.render.item; 4758 self.settings.render.item = (data, escape) => { 4759 var item = getDom(orig_render_item.call(self, data, escape)); 4760 var close_button = getDom(html); 4761 item.appendChild(close_button); 4762 addEvent(close_button, 'mousedown', evt => { 4763 preventDefault(evt, true); 4764 }); 4765 addEvent(close_button, 'click', evt => { 4766 if (self.isLocked) return; 4767 4768 // propagating will trigger the dropdown to show for single mode 4769 preventDefault(evt, true); 4770 if (self.isLocked) return; 4771 if (!self.shouldDelete([item], evt)) return; 4772 self.removeItem(item); 4773 self.refreshOptions(false); 4774 self.inputState(); 4775 }); 4776 return item; 4777 }; 4778 }); 4779 } 4780 4781 /** 4782 * Plugin: "restore_on_backspace" (Tom Select) 4783 * Copyright (c) contributors 4784 * 4785 * Licensed under the Apache License, Version 2.0 (the "License");
vendor: 4,019 bytes, lines 4785-4908
4785 you may not use this 4786 * file except in compliance with the License. You may obtain a copy of the License at: 4787 * http://www.apache.org/licenses/LICENSE-2.0 4788 * 4789 * Unless required by applicable law or agreed to in writing, software distributed under 4790 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4791 * ANY KIND, either express or implied. See the License for the specific language 4792 * governing permissions and limitations under the License. 4793 * 4794 */ 4795 4796 function restore_on_backspace (userOptions) { 4797 const self = this; 4798 const options = Object.assign({ 4799 text: option => { 4800 return option[self.settings.labelField]; 4801 } 4802 }, userOptions); 4803 self.on('item_remove', function (value) { 4804 if (!self.isFocused) { 4805 return; 4806 } 4807 if (self.control_input.value.trim() === '') { 4808 var option = self.options[value]; 4809 if (option) { 4810 self.setTextboxValue(options.text.call(self, option)); 4811 } 4812 } 4813 }); 4814 } 4815 4816 /** 4817 * Plugin: "restore_on_backspace" (Tom Select) 4818 * Copyright (c) contributors 4819 * 4820 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 4821 * file except in compliance with the License. You may obtain a copy of the License at: 4822 * http://www.apache.org/licenses/LICENSE-2.0 4823 * 4824 * Unless required by applicable law or agreed to in writing, software distributed under 4825 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 4826 * ANY KIND, either express or implied. See the License for the specific language 4827 * governing permissions and limitations under the License. 4828 * 4829 */ 4830 4831 function virtual_scroll () { 4832 const self = this; 4833 const orig_canLoad = self.canLoad; 4834 const orig_clearActiveOption = self.clearActiveOption; 4835 const orig_loadCallback = self.loadCallback; 4836 var pagination = {}; 4837 var dropdown_content; 4838 var loading_more = false; 4839 var load_more_opt; 4840 var default_values = []; 4841 if (!self.settings.shouldLoadMore) { 4842 // return true if additional results should be loaded 4843 self.settings.shouldLoadMore = () => { 4844 const scroll_percent = dropdown_content.clientHeight / (dropdown_content.scrollHeight - dropdown_content.scrollTop); 4845 if (scroll_percent > 0.9) { 4846 return true; 4847 } 4848 if (self.activeOption) { 4849 var selectable = self.selectable(); 4850 var index = Array.from(selectable).indexOf(self.activeOption); 4851 if (index >= selectable.length - 2) { 4852 return true; 4853 } 4854 } 4855 return false; 4856 }; 4857 } 4858 if (!self.settings.firstUrl) { 4859 throw 'virtual_scroll plugin requires a firstUrl() method'; 4860 } 4861 4862 // in order for virtual scrolling to work, 4863 // options need to be ordered the same way they're returned from the remote data source 4864 self.settings.sortField = [{ 4865 field: '$order' 4866 }, { 4867 field: '$score' 4868 }]; 4869 4870 // can we load more results for given query? 4871 const canLoadMore = query => { 4872 if (typeof self.settings.maxOptions === 'number' && dropdown_content.children.length >= self.settings.maxOptions) { 4873 return false; 4874 } 4875 if (query in pagination && pagination[query]) { 4876 return true; 4877 } 4878 return false; 4879 }; 4880 const clearFilter = (option, value) => { 4881 if (self.items.indexOf(value) >= 0 || default_values.indexOf(value) >= 0) { 4882 return true; 4883 } 4884 return false; 4885 }; 4886 4887 // set the next url that will be 4888 self.setNextUrl = (value, next_url) => { 4889 pagination[value] = next_url; 4890 }; 4891 4892 // getUrl() to be used in settings.load() 4893 self.getUrl = query => { 4894 if (query in pagination) { 4895 const next_url = pagination[query]; 4896 pagination[query] = false; 4897 return next_url; 4898 } 4899 4900 // if the user goes back to a previous query 4901 // we need to load the first page again 4902 self.clearPagination(); 4903 return self.settings.firstUrl.call(self, query); 4904 }; 4905 4906 // clear pagination 4907 self.clearPagination = () => { 4908 pagination = {}
vendor: 3,808 bytes, lines 4908-5021
4908; 4909 }; 4910 4911 // don't clear the active option (and cause unwanted dropdown scroll) 4912 // while loading more results 4913 self.hook('instead', 'clearActiveOption', () => { 4914 if (loading_more) { 4915 return; 4916 } 4917 return orig_clearActiveOption.call(self); 4918 }); 4919 4920 // override the canLoad method 4921 self.hook('instead', 'canLoad', query => { 4922 // first time the query has been seen 4923 if (!(query in pagination)) { 4924 return orig_canLoad.call(self, query); 4925 } 4926 return canLoadMore(query); 4927 }); 4928 4929 // wrap the load 4930 self.hook('instead', 'loadCallback', (options, optgroups) => { 4931 if (!loading_more) { 4932 self.clearOptions(clearFilter); 4933 } else if (load_more_opt) { 4934 const first_option = options[0]; 4935 if (first_option !== undefined) { 4936 load_more_opt.dataset.value = first_option[self.settings.valueField]; 4937 } 4938 } 4939 orig_loadCallback.call(self, options, optgroups); 4940 loading_more = false; 4941 }); 4942 4943 // add templates to dropdown 4944 // loading_more if we have another url in the queue 4945 // no_more_results if we don't have another url in the queue 4946 self.hook('after', 'refreshOptions', () => { 4947 const query = self.lastValue; 4948 var option; 4949 if (canLoadMore(query)) { 4950 option = self.render('loading_more', { 4951 query: query 4952 }); 4953 if (option) { 4954 option.setAttribute('data-selectable', ''); // so that navigating dropdown with [down] keypresses can navigate to this node 4955 load_more_opt = option; 4956 } 4957 } else if (query in pagination && !dropdown_content.querySelector('.no-results')) { 4958 option = self.render('no_more_results', { 4959 query: query 4960 }); 4961 } 4962 if (option) { 4963 addClasses(option, self.settings.optionClass); 4964 dropdown_content.append(option); 4965 } 4966 }); 4967 4968 // add scroll listener and default templates 4969 self.on('initialize', () => { 4970 default_values = Object.keys(self.options); 4971 dropdown_content = self.dropdown_content; 4972 4973 // default templates 4974 self.settings.render = Object.assign({}, { 4975 loading_more: () => { 4976 return `<div class="loading-more-results">Loading more results ... </div>`; 4977 }, 4978 no_more_results: () => { 4979 return `<div class="no-more-results">No more results</div>`; 4980 } 4981 }, self.settings.render); 4982 4983 // watch dropdown content scroll position 4984 dropdown_content.addEventListener('scroll', () => { 4985 if (!self.settings.shouldLoadMore.call(self)) { 4986 return; 4987 } 4988 4989 // !important: this will get checked again in load() but we still need to check here otherwise loading_more will be set to true 4990 if (!canLoadMore(self.lastValue)) { 4991 return; 4992 } 4993 4994 // don't call load() too much 4995 if (loading_more) return; 4996 loading_more = true; 4997 self.load.call(self, self.lastValue); 4998 }); 4999 }); 5000 } 5001 5002 TomSelect.define('change_listener', change_listener); 5003 TomSelect.define('checkbox_options', checkbox_options); 5004 TomSelect.define('clear_button', clear_button); 5005 TomSelect.define('drag_drop', drag_drop); 5006 TomSelect.define('dropdown_header', dropdown_header); 5007 TomSelect.define('caret_position', caret_position); 5008 TomSelect.define('dropdown_input', dropdown_input); 5009 TomSelect.define('input_autogrow', input_autogrow); 5010 TomSelect.define('no_backspace_delete', no_backspace_delete); 5011 TomSelect.define('no_active_items', no_active_items); 5012 TomSelect.define('optgroup_columns', optgroup_columns); 5013 TomSelect.define('remove_button', remove_button); 5014 TomSelect.define('restore_on_backspace', restore_on_backspace); 5015 TomSelect.define('virtual_scroll', virtual_scroll); 5016 5017 return TomSelect; 5018 5019})); 5020var tomSelect=function(el,opts){return new TomSelect(el,opts);} 5021//# 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.