vendor: 31,801 bytes, lines 1-1177
1/** 2 * sifter.js 3 * Copyright (c) 2013 Brian Reavis & contributors 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 6 * file except in compliance with the License. You may obtain a copy of the License at: 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under 10 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 * ANY KIND, either express or implied. See the License for the specific language 12 * governing permissions and limitations under the License. 13 * 14 * @author Brian Reavis <[email protected]> 15 */ 16 17(function(root, factory) { 18 if (typeof define === 'function' && define.amd) { 19 define('sifter', factory); 20 } else if (typeof exports === 'object') { 21 module.exports = factory(); 22 } else { 23 root.Sifter = factory(); 24 } 25}(this, function() { 26 27 /** 28 * Textually searches arrays and hashes of objects 29 * by property (or multiple properties). Designed 30 * specifically for autocomplete. 31 * 32 * @constructor 33 * @param {array|object} items 34 * @param {object} items 35 */ 36 var Sifter = function(items, settings) { 37 this.items = items; 38 this.settings = settings || {diacritics: true}; 39 }; 40 41 /** 42 * Splits a search string into an array of individual 43 * regexps to be used to match results. 44 * 45 * @param {string} query 46 * @returns {array} 47 */ 48 Sifter.prototype.tokenize = function(query) { 49 query = trim(String(query || '').toLowerCase()); 50 if (!query || !query.length) return []; 51 52 var i, n, regex, letter; 53 var tokens = []; 54 var words = query.split(/ +/); 55 56 for (i = 0, n = words.length; i < n; i++) { 57 regex = escape_regex(words[i]); 58 if (this.settings.diacritics) { 59 for (letter in DIACRITICS) { 60 if (DIACRITICS.hasOwnProperty(letter)) { 61 regex = regex.replace(new RegExp(letter, 'g'), DIACRITICS[letter]); 62 } 63 } 64 } 65 tokens.push({ 66 string : words[i], 67 regex : new RegExp(regex, 'i') 68 }); 69 } 70 71 return tokens; 72 }; 73 74 /** 75 * Iterates over arrays and hashes. 76 * 77 * ``` 78 * this.iterator(this.items, function(item, id) { 79 * // invoked for each item 80 * }); 81 * ``` 82 * 83 * @param {array|object} object 84 */ 85 Sifter.prototype.iterator = function(object, callback) { 86 var iterator; 87 if (is_array(object)) { 88 iterator = Array.prototype.forEach || function(callback) { 89 for (var i = 0, n = this.length; i < n; i++) { 90 callback(this[i], i, this); 91 } 92 }; 93 } else { 94 iterator = function(callback) { 95 for (var key in this) { 96 if (this.hasOwnProperty(key)) { 97 callback(this[key], key, this); 98 } 99 } 100 }; 101 } 102 103 iterator.apply(object, [callback]); 104 }; 105 106 /** 107 * Returns a function to be used to score individual results. 108 * 109 * Good matches will have a higher score than poor matches. 110 * If an item is not a match, 0 will be returned by the function. 111 * 112 * @param {object|string} search 113 * @param {object} options (optional) 114 * @returns {function} 115 */ 116 Sifter.prototype.getScoreFunction = function(search, options) { 117 var self, fields, tokens, token_count; 118 119 self = this; 120 search = self.prepareSearch(search, options); 121 tokens = search.tokens; 122 fields = search.options.fields; 123 token_count = tokens.length; 124 125 /** 126 * Calculates how close of a match the 127 * given value is against a search token. 128 * 129 * @param {mixed} value 130 * @param {object} token 131 * @return {number} 132 */ 133 var scoreValue = function(value, token) { 134 var score, pos; 135 136 if (!value) return 0; 137 value = String(value || ''); 138 pos = value.search(token.regex); 139 if (pos === -1) return 0; 140 score = token.string.length / value.length; 141 if (pos === 0) score += 0.5; 142 return score; 143 }; 144 145 /** 146 * Calculates the score of an object 147 * against the search query. 148 * 149 * @param {object} token 150 * @param {object} data 151 * @return {number} 152 */ 153 var scoreObject = (function() { 154 var field_count = fields.length; 155 if (!field_count) { 156 return function() { return 0; }; 157 } 158 if (field_count === 1) { 159 return function(token, data) { 160 return scoreValue(data[fields[0]], token); 161 }; 162 } 163 return function(token, data) { 164 for (var i = 0, sum = 0; i < field_count; i++) { 165 sum += scoreValue(data[fields[i]], token); 166 } 167 return sum / field_count; 168 }; 169 })(); 170 171 if (!token_count) { 172 return function() { return 0; }; 173 } 174 if (token_count === 1) { 175 return function(data) { 176 return scoreObject(tokens[0], data); 177 }; 178 } 179 180 if (search.options.conjunction === 'and') { 181 return function(data) { 182 var score; 183 for (var i = 0, sum = 0; i < token_count; i++) { 184 score = scoreObject(tokens[i], data); 185 if (score <= 0) return 0; 186 sum += score; 187 } 188 return sum / token_count; 189 }; 190 } else { 191 return function(data) { 192 for (var i = 0, sum = 0; i < token_count; i++) { 193 sum += scoreObject(tokens[i], data); 194 } 195 return sum / token_count; 196 }; 197 } 198 }; 199 200 /** 201 * Returns a function that can be used to compare two 202 * results, for sorting purposes. If no sorting should 203 * be performed, `null` will be returned. 204 * 205 * @param {string|object} search 206 * @param {object} options 207 * @return function(a,b) 208 */ 209 Sifter.prototype.getSortFunction = function(search, options) { 210 var i, n, self, field, fields, fields_count, multiplier, multipliers, get_field, implicit_score, sort; 211 212 self = this; 213 search = self.prepareSearch(search, options); 214 sort = (!search.query && options.sort_empty) || options.sort; 215 216 /** 217 * Fetches the specified sort field value 218 * from a search result item. 219 * 220 * @param {string} name 221 * @param {object} result 222 * @return {mixed} 223 */ 224 get_field = function(name, result) { 225 if (name === '$score') return result.score; 226 return self.items[result.id][name]; 227 }; 228 229 // parse options 230 fields = []; 231 if (sort) { 232 for (i = 0, n = sort.length; i < n; i++) { 233 if (search.query || sort[i].field !== '$score') { 234 fields.push(sort[i]); 235 } 236 } 237 } 238 239 // the "$score" field is implied to be the primary 240 // sort field, unless it's manually specified 241 if (search.query) { 242 implicit_score = true; 243 for (i = 0, n = fields.length; i < n; i++) { 244 if (fields[i].field === '$score') { 245 implicit_score = false; 246 break; 247 } 248 } 249 if (implicit_score) { 250 fields.unshift({field: '$score', direction: 'desc'}); 251 } 252 } else { 253 for (i = 0, n = fields.length; i < n; i++) { 254 if (fields[i].field === '$score') { 255 fields.splice(i, 1); 256 break; 257 } 258 } 259 } 260 261 multipliers = []; 262 for (i = 0, n = fields.length; i < n; i++) { 263 multipliers.push(fields[i].direction === 'desc' ? -1 : 1); 264 } 265 266 // build function 267 fields_count = fields.length; 268 if (!fields_count) { 269 return null; 270 } else if (fields_count === 1) { 271 field = fields[0].field; 272 multiplier = multipliers[0]; 273 return function(a, b) { 274 return multiplier * cmp( 275 get_field(field, a), 276 get_field(field, b) 277 ); 278 }; 279 } else { 280 return function(a, b) { 281 var i, result, a_value, b_value, field; 282 for (i = 0; i < fields_count; i++) { 283 field = fields[i].field; 284 result = multipliers[i] * cmp( 285 get_field(field, a), 286 get_field(field, b) 287 ); 288 if (result) return result; 289 } 290 return 0; 291 }; 292 } 293 }; 294 295 /** 296 * Parses a search query and returns an object 297 * with tokens and fields ready to be populated 298 * with results. 299 * 300 * @param {string} query 301 * @param {object} options 302 * @returns {object} 303 */ 304 Sifter.prototype.prepareSearch = function(query, options) { 305 if (typeof query === 'object') return query; 306 307 options = extend({}, options); 308 309 var option_fields = options.fields; 310 var option_sort = options.sort; 311 var option_sort_empty = options.sort_empty; 312 313 if (option_fields && !is_array(option_fields)) options.fields = [option_fields]; 314 if (option_sort && !is_array(option_sort)) options.sort = [option_sort]; 315 if (option_sort_empty && !is_array(option_sort_empty)) options.sort_empty = [option_sort_empty]; 316 317 return { 318 options : options, 319 query : String(query || '').toLowerCase(), 320 tokens : this.tokenize(query), 321 total : 0, 322 items : [] 323 }; 324 }; 325 326 /** 327 * Searches through all items and returns a sorted array of matches. 328 * 329 * The `options` parameter can contain: 330 * 331 * - fields {string|array} 332 * - sort {array} 333 * - score {function} 334 * - filter {bool} 335 * - limit {integer} 336 * 337 * Returns an object containing: 338 * 339 * - options {object} 340 * - query {string} 341 * - tokens {array} 342 * - total {int} 343 * - items {array} 344 * 345 * @param {string} query 346 * @param {object} options 347 * @returns {object} 348 */ 349 Sifter.prototype.search = function(query, options) { 350 var self = this, value, score, search, calculateScore; 351 var fn_sort; 352 var fn_score; 353 354 search = this.prepareSearch(query, options); 355 options = search.options; 356 query = search.query; 357 358 // generate result scoring function 359 fn_score = options.score || self.getScoreFunction(search); 360 361 // perform search and sort 362 if (query.length) { 363 self.iterator(self.items, function(item, id) { 364 score = fn_score(item); 365 if (options.filter === false || score > 0) { 366 search.items.push({'score': score, 'id': id}); 367 } 368 }); 369 } else { 370 self.iterator(self.items, function(item, id) { 371 search.items.push({'score': 1, 'id': id}); 372 }); 373 } 374 375 fn_sort = self.getSortFunction(search, options); 376 if (fn_sort) search.items.sort(fn_sort); 377 378 // apply limits 379 search.total = search.items.length; 380 if (typeof options.limit === 'number') { 381 search.items = search.items.slice(0, options.limit); 382 } 383 384 return search; 385 }; 386 387 // utilities 388 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 389 390 var cmp = function(a, b) { 391 if (typeof a === 'number' && typeof b === 'number') { 392 return a > b ? 1 : (a < b ? -1 : 0); 393 } 394 a = asciifold(String(a || '')); 395 b = asciifold(String(b || '')); 396 if (a > b) return 1; 397 if (b > a) return -1; 398 return 0; 399 }; 400 401 var extend = function(a, b) { 402 var i, n, k, object; 403 for (i = 1, n = arguments.length; i < n; i++) { 404 object = arguments[i]; 405 if (!object) continue; 406 for (k in object) { 407 if (object.hasOwnProperty(k)) { 408 a[k] = object[k]; 409 } 410 } 411 } 412 return a; 413 }; 414 415 var trim = function(str) { 416 return (str + '').replace(/^\s+|\s+$|/g, ''); 417 }; 418 419 var escape_regex = function(str) { 420 return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); 421 }; 422 423 var is_array = Array.isArray || ($ && $.isArray) || function(object) { 424 return Object.prototype.toString.call(object) === '[object Array]'; 425 }; 426 427 var DIACRITICS = { 428 'a': '[aÃÃÃÃÃà à áâãäåÄÄÄ Ä]', 429 'c': '[cÃçÄÄÄÄ]', 430 'd': '[dÄÄÄÄ]', 431 'e': '[eÃÃÃÃèéêëÄÄÄÄÄÄ]', 432 'i': '[iÃÃÃÃìÃîïĪī]', 433 'l': '[lÅÅ]', 434 'n': '[nÃñÅÅÅÅ]', 435 'o': '[oÃÃÃÃÃÃÃòóôõöøÅÅ]', 436 'r': '[rÅÅ]', 437 's': '[sÅ Å¡ÅÅ]', 438 't': '[tťŤ]', 439 'u': '[uÃÃÃÃùúûüůŮŪū]', 440 'y': '[yŸÿýÃ]', 441 'z': '[zŽžżŻźŹ]' 442 }; 443 444 var asciifold = (function() { 445 var i, n, k, chunk; 446 var foreignletters = ''; 447 var lookup = {}; 448 for (k in DIACRITICS) { 449 if (DIACRITICS.hasOwnProperty(k)) { 450 chunk = DIACRITICS[k].substring(2, DIACRITICS[k].length - 1); 451 foreignletters += chunk; 452 for (i = 0, n = chunk.length; i < n; i++) { 453 lookup[chunk.charAt(i)] = k; 454 } 455 } 456 } 457 var regexp = new RegExp('[' + foreignletters + ']', 'g'); 458 return function(str) { 459 return str.replace(regexp, function(foreignletter) { 460 return lookup[foreignletter]; 461 }).toLowerCase(); 462 }; 463 })(); 464 465 466 // export 467 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 468 469 return Sifter; 470})); 471 472 473 474/** 475 * microplugin.js 476 * Copyright (c) 2013 Brian Reavis & contributors 477 * 478 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 479 * file except in compliance with the License. You may obtain a copy of the License at: 480 * http://www.apache.org/licenses/LICENSE-2.0 481 * 482 * Unless required by applicable law or agreed to in writing, software distributed under 483 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 484 * ANY KIND, either express or implied. See the License for the specific language 485 * governing permissions and limitations under the License. 486 * 487 * @author Brian Reavis <[email protected]> 488 */ 489 490(function(root, factory) { 491 if (typeof define === 'function' && define.amd) { 492 define('microplugin', factory); 493 } else if (typeof exports === 'object') { 494 module.exports = factory(); 495 } else { 496 root.MicroPlugin = factory(); 497 } 498}(this, function() { 499 var MicroPlugin = {}; 500 501 MicroPlugin.mixin = function(Interface) { 502 Interface.plugins = {}; 503 504 /** 505 * Initializes the listed plugins (with options). 506 * Acceptable formats: 507 * 508 * List (without options): 509 * ['a', 'b', 'c'] 510 * 511 * List (with options): 512 * [{'name': 'a', options: {}}, {'name': 'b', options: {}}] 513 * 514 * Hash (with options): 515 * {'a': { ... }, 'b': { ... }, 'c': { ... }} 516 * 517 * @param {mixed} plugins 518 */ 519 Interface.prototype.initializePlugins = function(plugins) { 520 var i, n, key; 521 var self = this; 522 var queue = []; 523 524 self.plugins = { 525 names : [], 526 settings : {}, 527 requested : {}, 528 loaded : {} 529 }; 530 531 if (utils.isArray(plugins)) { 532 for (i = 0, n = plugins.length; i < n; i++) { 533 if (typeof plugins[i] === 'string') { 534 queue.push(plugins[i]); 535 } else { 536 self.plugins.settings[plugins[i].name] = plugins[i].options; 537 queue.push(plugins[i].name); 538 } 539 } 540 } else if (plugins) { 541 for (key in plugins) { 542 if (plugins.hasOwnProperty(key)) { 543 self.plugins.settings[key] = plugins[key]; 544 queue.push(key); 545 } 546 } 547 } 548 549 while (queue.length) { 550 self.require(queue.shift()); 551 } 552 }; 553 554 Interface.prototype.loadPlugin = function(name) { 555 var self = this; 556 var plugins = self.plugins; 557 var plugin = Interface.plugins[name]; 558 559 if (!Interface.plugins.hasOwnProperty(name)) { 560 throw new Error('Unable to find "' + name + '" plugin'); 561 } 562 563 plugins.requested[name] = true; 564 plugins.loaded[name] = plugin.fn.apply(self, [self.plugins.settings[name] || {}]); 565 plugins.names.push(name); 566 }; 567 568 /** 569 * Initializes a plugin. 570 * 571 * @param {string} name 572 */ 573 Interface.prototype.require = function(name) { 574 var self = this; 575 var plugins = self.plugins; 576 577 if (!self.plugins.loaded.hasOwnProperty(name)) { 578 if (plugins.requested[name]) { 579 throw new Error('Plugin has circular dependency ("' + name + '")'); 580 } 581 self.loadPlugin(name); 582 } 583 584 return plugins.loaded[name]; 585 }; 586 587 /** 588 * Registers a plugin. 589 * 590 * @param {string} name 591 * @param {function} fn 592 */ 593 Interface.define = function(name, fn) { 594 Interface.plugins[name] = { 595 'name' : name, 596 'fn' : fn 597 }; 598 }; 599 }; 600 601 var utils = { 602 isArray: Array.isArray || function(vArg) { 603 return Object.prototype.toString.call(vArg) === '[object Array]'; 604 } 605 }; 606 607 return MicroPlugin; 608})); 609 610/** 611 * selectize.js (v0.12.1) 612 * Copyright (c) 2013â2015 Brian Reavis & contributors 613 * 614 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 615 * file except in compliance with the License. You may obtain a copy of the License at: 616 * http://www.apache.org/licenses/LICENSE-2.0 617 * 618 * Unless required by applicable law or agreed to in writing, software distributed under 619 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 620 * ANY KIND, either express or implied. See the License for the specific language 621 * governing permissions and limitations under the License. 622 * 623 * @author Brian Reavis <[email protected]> 624 */ 625 626/*jshint curly:false */ 627/*jshint browser:true */ 628 629(function(root, factory) { 630 if (typeof define === 'function' && define.amd) { 631 define('selectize', ['jquery','sifter','microplugin'], factory); 632 } else if (typeof exports === 'object') { 633 module.exports = factory(require('jquery'), require('sifter'), require('microplugin')); 634 } else { 635 root.Selectize = factory(root.jQuery, root.Sifter, root.MicroPlugin); 636 } 637}(this, function($, Sifter, MicroPlugin) { 638 'use strict'; 639 640 var highlight = function($element, pattern) { 641 if (typeof pattern === 'string' && !pattern.length) return; 642 var regex = (typeof pattern === 'string') ? new RegExp(pattern, 'i') : pattern; 643 644 var highlight = function(node) { 645 var skip = 0; 646 if (node.nodeType === 3) { 647 var pos = node.data.search(regex); 648 if (pos >= 0 && node.data.length > 0) { 649 var match = node.data.match(regex); 650 var spannode = document.createElement('span'); 651 spannode.className = 'highlight'; 652 var middlebit = node.splitText(pos); 653 var endbit = middlebit.splitText(match[0].length); 654 var middleclone = middlebit.cloneNode(true); 655 spannode.appendChild(middleclone); 656 middlebit.parentNode.replaceChild(spannode, middlebit); 657 skip = 1; 658 } 659 } else if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) { 660 for (var i = 0; i < node.childNodes.length; ++i) { 661 i += highlight(node.childNodes[i]); 662 } 663 } 664 return skip; 665 }; 666 667 return $element.each(function() { 668 highlight(this); 669 }); 670 }; 671 672 var MicroEvent = function() {}; 673 MicroEvent.prototype = { 674 on: function(event, fct){ 675 this._events = this._events || {}; 676 this._events[event] = this._events[event] || []; 677 this._events[event].push(fct); 678 }, 679 off: function(event, fct){ 680 var n = arguments.length; 681 if (n === 0) return delete this._events; 682 if (n === 1) return delete this._events[event]; 683 684 this._events = this._events || {}; 685 if (event in this._events === false) return; 686 this._events[event].splice(this._events[event].indexOf(fct), 1); 687 }, 688 trigger: function(event /* , args... */){ 689 this._events = this._events || {}; 690 if (event in this._events === false) return; 691 for (var i = 0; i < this._events[event].length; i++){ 692 this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)); 693 } 694 } 695 }; 696 697 /** 698 * Mixin will delegate all MicroEvent.js function in the destination object. 699 * 700 * - MicroEvent.mixin(Foobar) will make Foobar able to use MicroEvent 701 * 702 * @param {object} the object which will support MicroEvent 703 */ 704 MicroEvent.mixin = function(destObject){ 705 var props = ['on', 'off', 'trigger']; 706 for (var i = 0; i < props.length; i++){ 707 destObject.prototype[props[i]] = MicroEvent.prototype[props[i]]; 708 } 709 }; 710 711 var IS_MAC = /Mac/.test(navigator.userAgent); 712 713 var KEY_A = 65; 714 var KEY_COMMA = 188; 715 var KEY_RETURN = 13; 716 var KEY_ESC = 27; 717 var KEY_LEFT = 37; 718 var KEY_UP = 38; 719 var KEY_P = 80; 720 var KEY_RIGHT = 39; 721 var KEY_DOWN = 40; 722 var KEY_N = 78; 723 var KEY_BACKSPACE = 8; 724 var KEY_DELETE = 46; 725 var KEY_SHIFT = 16; 726 var KEY_CMD = IS_MAC ? 91 : 17; 727 var KEY_CTRL = IS_MAC ? 18 : 17; 728 var KEY_TAB = 9; 729 730 var TAG_SELECT = 1; 731 var TAG_INPUT = 2; 732 733 // for now, android support in general is too spotty to support validity 734 var SUPPORTS_VALIDITY_API = !/android/i.test(window.navigator.userAgent) && !!document.createElement('form').validity; 735 736 var isset = function(object) { 737 return typeof object !== 'undefined'; 738 }; 739 740 /** 741 * Converts a scalar to its best string representation 742 * for hash keys and HTML attribute values. 743 * 744 * Transformations: 745 * 'str' -> 'str' 746 * null -> '' 747 * undefined -> '' 748 * true -> '1' 749 * false -> '0' 750 * 0 -> '0' 751 * 1 -> '1' 752 * 753 * @param {string} value 754 * @returns {string|null} 755 */ 756 var hash_key = function(value) { 757 if (typeof value === 'undefined' || value === null) return null; 758 if (typeof value === 'boolean') return value ? '1' : '0'; 759 return value + ''; 760 }; 761 762 /** 763 * Escapes a string for use within HTML. 764 * 765 * @param {string} str 766 * @returns {string} 767 */ 768 var escape_html = function(str) { 769 return (str + '') 770 .replace(/&/g, '&') 771 .replace(/</g, '<') 772 .replace(/>/g, '>') 773 .replace(/"/g, '"'); 774 }; 775 776 /** 777 * Escapes "$" characters in replacement strings. 778 * 779 * @param {string} str 780 * @returns {string} 781 */ 782 var escape_replace = function(str) { 783 return (str + '').replace(/\$/g, '$$$$'); 784 }; 785 786 var hook = {}; 787 788 /** 789 * Wraps `method` on `self` so that `fn` 790 * is invoked before the original method. 791 * 792 * @param {object} self 793 * @param {string} method 794 * @param {function} fn 795 */ 796 hook.before = function(self, method, fn) { 797 var original = self[method]; 798 self[method] = function() { 799 fn.apply(self, arguments); 800 return original.apply(self, arguments); 801 }; 802 }; 803 804 /** 805 * Wraps `method` on `self` so that `fn` 806 * is invoked after the original method. 807 * 808 * @param {object} self 809 * @param {string} method 810 * @param {function} fn 811 */ 812 hook.after = function(self, method, fn) { 813 var original = self[method]; 814 self[method] = function() { 815 var result = original.apply(self, arguments); 816 fn.apply(self, arguments); 817 return result; 818 }; 819 }; 820 821 /** 822 * Wraps `fn` so that it can only be invoked once. 823 * 824 * @param {function} fn 825 * @returns {function} 826 */ 827 var once = function(fn) { 828 var called = false; 829 return function() { 830 if (called) return; 831 called = true; 832 fn.apply(this, arguments); 833 }; 834 }; 835 836 /** 837 * Wraps `fn` so that it can only be called once 838 * every `delay` milliseconds (invoked on the falling edge). 839 * 840 * @param {function} fn 841 * @param {int} delay 842 * @returns {function} 843 */ 844 var debounce = function(fn, delay) { 845 var timeout; 846 return function() { 847 var self = this; 848 var args = arguments; 849 window.clearTimeout(timeout); 850 timeout = window.setTimeout(function() { 851 fn.apply(self, args); 852 }, delay); 853 }; 854 }; 855 856 /** 857 * Debounce all fired events types listed in `types` 858 * while executing the provided `fn`. 859 * 860 * @param {object} self 861 * @param {array} types 862 * @param {function} fn 863 */ 864 var debounce_events = function(self, types, fn) { 865 var type; 866 var trigger = self.trigger; 867 var event_args = {}; 868 869 // override trigger method 870 self.trigger = function() { 871 var type = arguments[0]; 872 if (types.indexOf(type) !== -1) { 873 event_args[type] = arguments; 874 } else { 875 return trigger.apply(self, arguments); 876 } 877 }; 878 879 // invoke provided function 880 fn.apply(self, []); 881 self.trigger = trigger; 882 883 // trigger queued events 884 for (type in event_args) { 885 if (event_args.hasOwnProperty(type)) { 886 trigger.apply(self, event_args[type]); 887 } 888 } 889 }; 890 891 /** 892 * A workaround for http://bugs.jquery.com/ticket/6696 893 * 894 * @param {object} $parent - Parent element to listen on. 895 * @param {string} event - Event name. 896 * @param {string} selector - Descendant selector to filter by. 897 * @param {function} fn - Event handler. 898 */ 899 var watchChildEvent = function($parent, event, selector, fn) { 900 $parent.on(event, selector, function(e) { 901 var child = e.target; 902 while (child && child.parentNode !== $parent[0]) { 903 child = child.parentNode; 904 } 905 e.currentTarget = child; 906 return fn.apply(this, [e]); 907 }); 908 }; 909 910 /** 911 * Determines the current selection within a text input control. 912 * Returns an object containing: 913 * - start 914 * - length 915 * 916 * @param {object} input 917 * @returns {object} 918 */ 919 var getSelection = function(input) { 920 var result = {}; 921 if ('selectionStart' in input) { 922 result.start = input.selectionStart; 923 result.length = input.selectionEnd - result.start; 924 } else if (document.selection) { 925 input.focus(); 926 var sel = document.selection.createRange(); 927 var selLen = document.selection.createRange().text.length; 928 sel.moveStart('character', -input.value.length); 929 result.start = sel.text.length - selLen; 930 result.length = selLen; 931 } 932 return result; 933 }; 934 935 /** 936 * Copies CSS properties from one element to another. 937 * 938 * @param {object} $from 939 * @param {object} $to 940 * @param {array} properties 941 */ 942 var transferStyles = function($from, $to, properties) { 943 var i, n, styles = {}; 944 if (properties) { 945 for (i = 0, n = properties.length; i < n; i++) { 946 styles[properties[i]] = $from.css(properties[i]); 947 } 948 } else { 949 styles = $from.css(); 950 } 951 $to.css(styles); 952 }; 953 954 /** 955 * Measures the width of a string within a 956 * parent element (in pixels). 957 * 958 * @param {string} str 959 * @param {object} $parent 960 * @returns {int} 961 */ 962 var measureString = function(str, $parent) { 963 if (!str) { 964 return 0; 965 } 966 967 var $test = $('<test>').css({ 968 position: 'absolute', 969 top: -99999, 970 left: -99999, 971 width: 'auto', 972 padding: 0, 973 whiteSpace: 'pre' 974 }).text(str).appendTo('body'); 975 976 transferStyles($parent, $test, [ 977 'letterSpacing', 978 'fontSize', 979 'fontFamily', 980 'fontWeight', 981 'textTransform' 982 ]); 983 984 var width = $test.width(); 985 $test.remove(); 986 987 return width; 988 }; 989 990 /** 991 * Sets up an input to grow horizontally as the user 992 * types. If the value is changed manually, you can 993 * trigger the "update" handler to resize: 994 * 995 * $input.trigger('update'); 996 * 997 * @param {object} $input 998 */ 999 var autoGrow = function($input) { 1000 var currentWidth = null; 1001 1002 var update = function(e, options) { 1003 var value, keyCode, printable, placeholder, width; 1004 var shift, character, selection; 1005 e = e || window.event || {}; 1006 options = options || {}; 1007 1008 if (e.metaKey || e.altKey) return; 1009 if (!options.force && $input.data('grow') === false) return; 1010 1011 value = $input.val(); 1012 if (e.type && e.type.toLowerCase() === 'keydown') { 1013 keyCode = e.keyCode; 1014 printable = ( 1015 (keyCode >= 97 && keyCode <= 122) || // a-z 1016 (keyCode >= 65 && keyCode <= 90) || // A-Z 1017 (keyCode >= 48 && keyCode <= 57) || // 0-9 1018 keyCode === 32 // space 1019 ); 1020 1021 if (keyCode === KEY_DELETE || keyCode === KEY_BACKSPACE) { 1022 selection = getSelection($input[0]); 1023 if (selection.length) { 1024 value = value.substring(0, selection.start) + value.substring(selection.start + selection.length); 1025 } else if (keyCode === KEY_BACKSPACE && selection.start) { 1026 value = value.substring(0, selection.start - 1) + value.substring(selection.start + 1); 1027 } else if (keyCode === KEY_DELETE && typeof selection.start !== 'undefined') { 1028 value = value.substring(0, selection.start) + value.substring(selection.start + 1); 1029 } 1030 } else if (printable) { 1031 shift = e.shiftKey; 1032 character = String.fromCharCode(e.keyCode); 1033 if (shift) character = character.toUpperCase(); 1034 else character = character.toLowerCase(); 1035 value += character; 1036 } 1037 } 1038 1039 placeholder = $input.attr('placeholder'); 1040 if (!value && placeholder) { 1041 value = placeholder; 1042 } 1043 1044 width = measureString(value, $input) + 4; 1045 if (width !== currentWidth) { 1046 currentWidth = width; 1047 $input.width(width); 1048 $input.triggerHandler('resize'); 1049 } 1050 }; 1051 1052 $input.on('keydown keyup update blur', update); 1053 update(); 1054 }; 1055 1056 var Selectize = function($input, settings) { 1057 var key, i, n, dir, input, self = this; 1058 input = $input[0]; 1059 input.selectize = self; 1060 1061 // detect rtl environment 1062 var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null); 1063 dir = computedStyle ? computedStyle.getPropertyValue('direction') : input.currentStyle && input.currentStyle.direction; 1064 dir = dir || $input.parents('[dir]:first').attr('dir') || ''; 1065 1066 // setup default state 1067 $.extend(self, { 1068 order : 0, 1069 settings : settings, 1070 $input : $input, 1071 tabIndex : $input.attr('tabindex') || '', 1072 tagType : input.tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT, 1073 rtl : /rtl/i.test(dir), 1074 1075 eventNS : '.selectize' + (++Selectize.count), 1076 highlightedValue : null, 1077 isOpen : false, 1078 isDisabled : false, 1079 isRequired : $input.is('[required]'), 1080 isInvalid : false, 1081 isLocked : false, 1082 isFocused : false, 1083 isInputHidden : false, 1084 isSetup : false, 1085 isShiftDown : false, 1086 isCmdDown : false, 1087 isCtrlDown : false, 1088 ignoreFocus : false, 1089 ignoreBlur : false, 1090 ignoreHover : false, 1091 hasOptions : false, 1092 currentResults : null, 1093 lastValue : '', 1094 caretPos : 0, 1095 loading : 0, 1096 loadedSearches : {}, 1097 1098 $activeOption : null, 1099 $activeItems : [], 1100 1101 optgroups : {}, 1102 options : {}, 1103 userOptions : {}, 1104 items : [], 1105 renderCache : {}, 1106 onSearchChange : settings.loadThrottle === null ? self.onSearchChange : debounce(self.onSearchChange, settings.loadThrottle) 1107 }); 1108 1109 // search system 1110 self.sifter = new Sifter(this.options, {diacritics: settings.diacritics}); 1111 1112 // build options table 1113 if (self.settings.options) { 1114 for (i = 0, n = self.settings.options.length; i < n; i++) { 1115 self.registerOption(self.settings.options[i]); 1116 } 1117 delete self.settings.options; 1118 } 1119 1120 // build optgroup table 1121 if (self.settings.optgroups) { 1122 for (i = 0, n = self.settings.optgroups.length; i < n; i++) { 1123 self.registerOptionGroup(self.settings.optgroups[i]); 1124 } 1125 delete self.settings.optgroups; 1126 } 1127 1128 // option-dependent defaults 1129 self.settings.mode = self.settings.mode || (self.settings.maxItems === 1 ? 'single' : 'multi'); 1130 if (typeof self.settings.hideSelected !== 'boolean') { 1131 self.settings.hideSelected = self.settings.mode === 'multi'; 1132 } 1133 1134 self.initializePlugins(self.settings.plugins); 1135 self.setupCallbacks(); 1136 self.setupTemplates(); 1137 self.setup(); 1138 }; 1139 1140 // mixins 1141 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1142 1143 MicroEvent.mixin(Selectize); 1144 MicroPlugin.mixin(Selectize); 1145 1146 // methods 1147 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1148 1149 $.extend(Selectize.prototype, { 1150 1151 /** 1152 * Creates all elements and sets up event bindings. 1153 */ 1154 setup: function() { 1155 var self = this; 1156 var settings = self.settings; 1157 var eventNS = self.eventNS; 1158 var $window = $(window); 1159 var $document = $(document); 1160 var $input = self.$input; 1161 1162 var $wrapper; 1163 var $control; 1164 var $control_input; 1165 var $dropdown; 1166 var $dropdown_content; 1167 var $dropdown_parent; 1168 var inputMode; 1169 var timeout_blur; 1170 var timeout_focus; 1171 var classes; 1172 var classes_plugins; 1173 1174 inputMode = self.settings.mode; 1175 classes = $input.attr('class') || ''; 1176 1177 $wrapper = $('<div>
vendor: 23,415 bytes, lines 1177-2002
1177').addClass(settings.wrapperClass).addClass(classes).addClass(inputMode); 1178 $control = $('<div>').addClass(settings.inputClass).addClass('items').appendTo($wrapper); 1179 $control_input = $('<input type="text" autocomplete="off" />').appendTo($control).attr('tabindex', $input.is(':disabled') ? '-1' : self.tabIndex); 1180 $dropdown_parent = $(settings.dropdownParent || $wrapper); 1181 $dropdown = $('<div>').addClass(settings.dropdownClass).addClass(inputMode).hide().appendTo($dropdown_parent); 1182 $dropdown_content = $('<div>').addClass(settings.dropdownContentClass).appendTo($dropdown); 1183 1184 if(self.settings.copyClassesToDropdown) { 1185 $dropdown.addClass(classes); 1186 } 1187 1188 $wrapper.css({ 1189 width: $input[0].style.width 1190 }); 1191 1192 if (self.plugins.names.length) { 1193 classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-'); 1194 $wrapper.addClass(classes_plugins); 1195 $dropdown.addClass(classes_plugins); 1196 } 1197 1198 if ((settings.maxItems === null || settings.maxItems > 1) && self.tagType === TAG_SELECT) { 1199 $input.attr('multiple', 'multiple'); 1200 } 1201 1202 if (self.settings.placeholder) { 1203 $control_input.attr('placeholder', settings.placeholder); 1204 } 1205 1206 // if splitOn was not passed in, construct it from the delimiter to allow pasting universally 1207 if (!self.settings.splitOn && self.settings.delimiter) { 1208 var delimiterEscaped = self.settings.delimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 1209 self.settings.splitOn = new RegExp('\\s*' + delimiterEscaped + '+\\s*'); 1210 } 1211 1212 if ($input.attr('autocorrect')) { 1213 $control_input.attr('autocorrect', $input.attr('autocorrect')); 1214 } 1215 1216 if ($input.attr('autocapitalize')) { 1217 $control_input.attr('autocapitalize', $input.attr('autocapitalize')); 1218 } 1219 1220 self.$wrapper = $wrapper; 1221 self.$control = $control; 1222 self.$control_input = $control_input; 1223 self.$dropdown = $dropdown; 1224 self.$dropdown_content = $dropdown_content; 1225 1226 $dropdown.on('mouseenter', '[data-selectable]', function() { return self.onOptionHover.apply(self, arguments); }); 1227 $dropdown.on('mousedown click', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); }); 1228 watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); }); 1229 autoGrow($control_input); 1230 1231 $control.on({ 1232 mousedown : function() { return self.onMouseDown.apply(self, arguments); }, 1233 click : function() { return self.onClick.apply(self, arguments); } 1234 }); 1235 1236 $control_input.on({ 1237 mousedown : function(e) { e.stopPropagation(); }, 1238 keydown : function() { return self.onKeyDown.apply(self, arguments); }, 1239 keyup : function() { return self.onKeyUp.apply(self, arguments); }, 1240 keypress : function() { return self.onKeyPress.apply(self, arguments); }, 1241 resize : function() { self.positionDropdown.apply(self, []); }, 1242 blur : function() { return self.onBlur.apply(self, arguments); }, 1243 focus : function() { self.ignoreBlur = false; return self.onFocus.apply(self, arguments); }, 1244 paste : function() { return self.onPaste.apply(self, arguments); } 1245 }); 1246 1247 $document.on('keydown' + eventNS, function(e) { 1248 self.isCmdDown = e[IS_MAC ? 'metaKey' : 'ctrlKey']; 1249 self.isCtrlDown = e[IS_MAC ? 'altKey' : 'ctrlKey']; 1250 self.isShiftDown = e.shiftKey; 1251 }); 1252 1253 $document.on('keyup' + eventNS, function(e) { 1254 if (e.keyCode === KEY_CTRL) self.isCtrlDown = false; 1255 if (e.keyCode === KEY_SHIFT) self.isShiftDown = false; 1256 if (e.keyCode === KEY_CMD) self.isCmdDown = false; 1257 }); 1258 1259 $document.on('mousedown' + eventNS, function(e) { 1260 if (self.isFocused) { 1261 // prevent events on the dropdown scrollbar from causing the control to blur 1262 if (e.target === self.$dropdown[0] || e.target.parentNode === self.$dropdown[0]) { 1263 return false; 1264 } 1265 // blur on click outside 1266 if (!self.$control.has(e.target).length && e.target !== self.$control[0]) { 1267 self.blur(e.target); 1268 } 1269 } 1270 }); 1271 1272 $window.on(['scroll' + eventNS, 'resize' + eventNS].join(' '), function() { 1273 if (self.isOpen) { 1274 self.positionDropdown.apply(self, arguments); 1275 } 1276 }); 1277 $window.on('mousemove' + eventNS, function() { 1278 self.ignoreHover = false; 1279 }); 1280 1281 // store original children and tab index so that they can be 1282 // restored when the destroy() method is called. 1283 this.revertSettings = { 1284 $children : $input.children().detach(), 1285 tabindex : $input.attr('tabindex') 1286 }; 1287 1288 $input.attr('tabindex', -1).hide().after(self.$wrapper); 1289 1290 if ($.isArray(settings.items)) { 1291 self.setValue(settings.items); 1292 delete settings.items; 1293 } 1294 1295 // feature detect for the validation API 1296 if (SUPPORTS_VALIDITY_API) { 1297 $input.on('invalid' + eventNS, function(e) { 1298 e.preventDefault(); 1299 self.isInvalid = true; 1300 self.refreshState(); 1301 }); 1302 } 1303 1304 self.updateOriginalInput(); 1305 self.refreshItems(); 1306 self.refreshState(); 1307 self.updatePlaceholder(); 1308 self.isSetup = true; 1309 1310 if ($input.is(':disabled')) { 1311 self.disable(); 1312 } 1313 1314 self.on('change', this.onChange); 1315 1316 $input.data('selectize', self); 1317 $input.addClass('selectized'); 1318 self.trigger('initialize'); 1319 1320 // preload options 1321 if (settings.preload === true) { 1322 self.onSearchChange(''); 1323 } 1324 1325 }, 1326 1327 /** 1328 * Sets up default rendering functions. 1329 */ 1330 setupTemplates: function() { 1331 var self = this; 1332 var field_label = self.settings.labelField; 1333 var field_optgroup = self.settings.optgroupLabelField; 1334 1335 var templates = { 1336 'optgroup': function(data) { 1337 return '<div class="optgroup">' + data.html + '</div>'; 1338 }, 1339 'optgroup_header': function(data, escape) { 1340 return '<div class="optgroup-header">' + escape(data[field_optgroup]) + '</div>'; 1341 }, 1342 'option': function(data, escape) { 1343 return '<div class="option">' + escape(data[field_label]) + '</div>'; 1344 }, 1345 'item': function(data, escape) { 1346 return '<div class="item">' + escape(data[field_label]) + '</div>'; 1347 }, 1348 'option_create': function(data, escape) { 1349 return '<div class="create">Add <strong>' + escape(data.input) + '</strong>…</div>'; 1350 } 1351 }; 1352 1353 self.settings.render = $.extend({}, templates, self.settings.render); 1354 }, 1355 1356 /** 1357 * Maps fired events to callbacks provided 1358 * in the settings used when creating the control. 1359 */ 1360 setupCallbacks: function() { 1361 var key, fn, callbacks = { 1362 'initialize' : 'onInitialize', 1363 'change' : 'onChange', 1364 'item_add' : 'onItemAdd', 1365 'item_remove' : 'onItemRemove', 1366 'clear' : 'onClear', 1367 'option_add' : 'onOptionAdd', 1368 'option_remove' : 'onOptionRemove', 1369 'option_clear' : 'onOptionClear', 1370 'optgroup_add' : 'onOptionGroupAdd', 1371 'optgroup_remove' : 'onOptionGroupRemove', 1372 'optgroup_clear' : 'onOptionGroupClear', 1373 'dropdown_open' : 'onDropdownOpen', 1374 'dropdown_close' : 'onDropdownClose', 1375 'type' : 'onType', 1376 'load' : 'onLoad', 1377 'focus' : 'onFocus', 1378 'blur' : 'onBlur' 1379 }; 1380 1381 for (key in callbacks) { 1382 if (callbacks.hasOwnProperty(key)) { 1383 fn = this.settings[callbacks[key]]; 1384 if (fn) this.on(key, fn); 1385 } 1386 } 1387 }, 1388 1389 /** 1390 * Triggered when the main control element 1391 * has a click event. 1392 * 1393 * @param {object} e 1394 * @return {boolean} 1395 */ 1396 onClick: function(e) { 1397 var self = this; 1398 1399 // necessary for mobile webkit devices (manual focus triggering 1400 // is ignored unless invoked within a click event) 1401 if (!self.isFocused) { 1402 self.focus(); 1403 e.preventDefault(); 1404 } 1405 }, 1406 1407 /** 1408 * Triggered when the main control element 1409 * has a mouse down event. 1410 * 1411 * @param {object} e 1412 * @return {boolean} 1413 */ 1414 onMouseDown: function(e) { 1415 var self = this; 1416 var defaultPrevented = e.isDefaultPrevented(); 1417 var $target = $(e.target); 1418 1419 if (self.isFocused) { 1420 // retain focus by preventing native handling. if the 1421 // event target is the input it should not be modified. 1422 // otherwise, text selection within the input won't work. 1423 if (e.target !== self.$control_input[0]) { 1424 if (self.settings.mode === 'single') { 1425 // toggle dropdown 1426 self.isOpen ? self.close() : self.open(); 1427 } else if (!defaultPrevented) { 1428 self.setActiveItem(null); 1429 } 1430 return false; 1431 } 1432 } else { 1433 // give control focus 1434 if (!defaultPrevented) { 1435 window.setTimeout(function() { 1436 self.focus(); 1437 }, 0); 1438 } 1439 } 1440 }, 1441 1442 /** 1443 * Triggered when the value of the control has been changed. 1444 * This should propagate the event to the original DOM 1445 * input / select element. 1446 */ 1447 onChange: function() { 1448 this.$input.trigger('change'); 1449 }, 1450 1451 /** 1452 * Triggered on <input> paste. 1453 * 1454 * @param {object} e 1455 * @returns {boolean} 1456 */ 1457 onPaste: function(e) { 1458 var self = this; 1459 if (self.isFull() || self.isInputHidden || self.isLocked) { 1460 e.preventDefault(); 1461 } else { 1462 // If a regex or string is included, this will split the pasted 1463 // input and create Items for each separate value 1464 if (self.settings.splitOn) { 1465 setTimeout(function() { 1466 var splitInput = $.trim(self.$control_input.val() || '').split(self.settings.splitOn); 1467 for (var i = 0, n = splitInput.length; i < n; i++) { 1468 self.createItem(splitInput[i]); 1469 } 1470 }, 0); 1471 } 1472 } 1473 }, 1474 1475 /** 1476 * Triggered on <input> keypress. 1477 * 1478 * @param {object} e 1479 * @returns {boolean} 1480 */ 1481 onKeyPress: function(e) { 1482 if (this.isLocked) return e && e.preventDefault(); 1483 var character = String.fromCharCode(e.keyCode || e.which); 1484 if (this.settings.create && this.settings.mode === 'multi' && character === this.settings.delimiter) { 1485 this.createItem(); 1486 e.preventDefault(); 1487 return false; 1488 } 1489 }, 1490 1491 /** 1492 * Triggered on <input> keydown. 1493 * 1494 * @param {object} e 1495 * @returns {boolean} 1496 */ 1497 onKeyDown: function(e) { 1498 var isInput = e.target === this.$control_input[0]; 1499 var self = this; 1500 1501 if (self.isLocked) { 1502 if (e.keyCode !== KEY_TAB) { 1503 e.preventDefault(); 1504 } 1505 return; 1506 } 1507 1508 switch (e.keyCode) { 1509 case KEY_A: 1510 if (self.isCmdDown) { 1511 self.selectAll(); 1512 return; 1513 } 1514 break; 1515 case KEY_ESC: 1516 if (self.isOpen) { 1517 e.preventDefault(); 1518 e.stopPropagation(); 1519 self.close(); 1520 } 1521 return; 1522 case KEY_N: 1523 if (!e.ctrlKey || e.altKey) break; 1524 case KEY_DOWN: 1525 if (!self.isOpen && self.hasOptions) { 1526 self.open(); 1527 } else if (self.$activeOption) { 1528 self.ignoreHover = true; 1529 var $next = self.getAdjacentOption(self.$activeOption, 1); 1530 if ($next.length) self.setActiveOption($next, true, true); 1531 } 1532 e.preventDefault(); 1533 return; 1534 case KEY_P: 1535 if (!e.ctrlKey || e.altKey) break; 1536 case KEY_UP: 1537 if (self.$activeOption) { 1538 self.ignoreHover = true; 1539 var $prev = self.getAdjacentOption(self.$activeOption, -1); 1540 if ($prev.length) self.setActiveOption($prev, true, true); 1541 } 1542 e.preventDefault(); 1543 return; 1544 case KEY_RETURN: 1545 if (self.isOpen && self.$activeOption) { 1546 self.onOptionSelect({currentTarget: self.$activeOption}); 1547 e.preventDefault(); 1548 } 1549 return; 1550 case KEY_LEFT: 1551 self.advanceSelection(-1, e); 1552 return; 1553 case KEY_RIGHT: 1554 self.advanceSelection(1, e); 1555 return; 1556 case KEY_TAB: 1557 if (self.settings.selectOnTab && self.isOpen && self.$activeOption) { 1558 self.onOptionSelect({currentTarget: self.$activeOption}); 1559 1560 // Default behaviour is to jump to the next field, we only want this 1561 // if the current field doesn't accept any more entries 1562 if (!self.isFull()) { 1563 e.preventDefault(); 1564 } 1565 } 1566 if (self.settings.create && self.createItem()) { 1567 e.preventDefault(); 1568 } 1569 return; 1570 case KEY_BACKSPACE: 1571 case KEY_DELETE: 1572 self.deleteSelection(e); 1573 return; 1574 } 1575 1576 if ((self.isFull() || self.isInputHidden) && !(IS_MAC ? e.metaKey : e.ctrlKey)) { 1577 e.preventDefault(); 1578 return; 1579 } 1580 }, 1581 1582 /** 1583 * Triggered on <input> keyup. 1584 * 1585 * @param {object} e 1586 * @returns {boolean} 1587 */ 1588 onKeyUp: function(e) { 1589 var self = this; 1590 1591 if (self.isLocked) return e && e.preventDefault(); 1592 var value = self.$control_input.val() || ''; 1593 if (self.lastValue !== value) { 1594 self.lastValue = value; 1595 self.onSearchChange(value); 1596 self.refreshOptions(); 1597 self.trigger('type', value); 1598 } 1599 }, 1600 1601 /** 1602 * Invokes the user-provide option provider / loader. 1603 * 1604 * Note: this function is debounced in the Selectize 1605 * constructor (by `settings.loadDelay` milliseconds) 1606 * 1607 * @param {string} value 1608 */ 1609 onSearchChange: function(value) { 1610 var self = this; 1611 var fn = self.settings.load; 1612 if (!fn) return; 1613 if (self.loadedSearches.hasOwnProperty(value)) return; 1614 self.loadedSearches[value] = true; 1615 self.load(function(callback) { 1616 fn.apply(self, [value, callback]); 1617 }); 1618 }, 1619 1620 /** 1621 * Triggered on <input> focus. 1622 * 1623 * @param {object} e (optional) 1624 * @returns {boolean} 1625 */ 1626 onFocus: function(e) { 1627 var self = this; 1628 var wasFocused = self.isFocused; 1629 1630 if (self.isDisabled) { 1631 self.blur(); 1632 e && e.preventDefault(); 1633 return false; 1634 } 1635 1636 if (self.ignoreFocus) return; 1637 self.isFocused = true; 1638 if (self.settings.preload === 'focus') self.onSearchChange(''); 1639 1640 if (!wasFocused) self.trigger('focus'); 1641 1642 if (!self.$activeItems.length) { 1643 self.showInput(); 1644 self.setActiveItem(null); 1645 self.refreshOptions(!!self.settings.openOnFocus); 1646 } 1647 1648 self.refreshState(); 1649 }, 1650 1651 /** 1652 * Triggered on <input> blur. 1653 * 1654 * @param {object} e 1655 * @param {Element} dest 1656 */ 1657 onBlur: function(e, dest) { 1658 var self = this; 1659 if (!self.isFocused) return; 1660 self.isFocused = false; 1661 1662 if (self.ignoreFocus) { 1663 return; 1664 } else if (!self.ignoreBlur && document.activeElement === self.$dropdown_content[0]) { 1665 // necessary to prevent IE closing the dropdown when the scrollbar is clicked 1666 self.ignoreBlur = true; 1667 self.onFocus(e); 1668 return; 1669 } 1670 1671 var deactivate = function() { 1672 self.close(); 1673 self.setTextboxValue(''); 1674 self.setActiveItem(null); 1675 self.setActiveOption(null); 1676 self.setCaret(self.items.length); 1677 self.refreshState(); 1678 1679 // IE11 bug: element still marked as active 1680 (dest || document.body).focus(); 1681 1682 self.ignoreFocus = false; 1683 self.trigger('blur'); 1684 }; 1685 1686 self.ignoreFocus = true; 1687 if (self.settings.create && self.settings.createOnBlur) { 1688 self.createItem(null, false, deactivate); 1689 } else { 1690 deactivate(); 1691 } 1692 }, 1693 1694 /** 1695 * Triggered when the user rolls over 1696 * an option in the autocomplete dropdown menu. 1697 * 1698 * @param {object} e 1699 * @returns {boolean} 1700 */ 1701 onOptionHover: function(e) { 1702 if (this.ignoreHover) return; 1703 this.setActiveOption(e.currentTarget, false); 1704 }, 1705 1706 /** 1707 * Triggered when the user clicks on an option 1708 * in the autocomplete dropdown menu. 1709 * 1710 * @param {object} e 1711 * @returns {boolean} 1712 */ 1713 onOptionSelect: function(e) { 1714 var value, $target, $option, self = this; 1715 1716 if (e.preventDefault) { 1717 e.preventDefault(); 1718 e.stopPropagation(); 1719 } 1720 1721 $target = $(e.currentTarget); 1722 if ($target.hasClass('create')) { 1723 self.createItem(null, function() { 1724 if (self.settings.closeAfterSelect) { 1725 self.close(); 1726 } 1727 }); 1728 } else { 1729 value = $target.attr('data-value'); 1730 if (typeof value !== 'undefined') { 1731 self.lastQuery = null; 1732 self.setTextboxValue(''); 1733 self.addItem(value); 1734 if (self.settings.closeAfterSelect) { 1735 self.close(); 1736 } else if (!self.settings.hideSelected && e.type && /mouse/.test(e.type)) { 1737 self.setActiveOption(self.getOption(value)); 1738 } 1739 } 1740 } 1741 }, 1742 1743 /** 1744 * Triggered when the user clicks on an item 1745 * that has been selected. 1746 * 1747 * @param {object} e 1748 * @returns {boolean} 1749 */ 1750 onItemSelect: function(e) { 1751 var self = this; 1752 1753 if (self.isLocked) return; 1754 if (self.settings.mode === 'multi') { 1755 e.preventDefault(); 1756 self.setActiveItem(e.currentTarget, e); 1757 } 1758 }, 1759 1760 /** 1761 * Invokes the provided method that provides 1762 * results to a callback---which are then added 1763 * as options to the control. 1764 * 1765 * @param {function} fn 1766 */ 1767 load: function(fn) { 1768 var self = this; 1769 var $wrapper = self.$wrapper.addClass(self.settings.loadingClass); 1770 1771 self.loading++; 1772 fn.apply(self, [function(results) { 1773 self.loading = Math.max(self.loading - 1, 0); 1774 if (results && results.length) { 1775 self.addOption(results); 1776 self.refreshOptions(self.isFocused && !self.isInputHidden); 1777 } 1778 if (!self.loading) { 1779 $wrapper.removeClass(self.settings.loadingClass); 1780 } 1781 self.trigger('load', results); 1782 }]); 1783 }, 1784 1785 /** 1786 * Sets the input field of the control to the specified value. 1787 * 1788 * @param {string} value 1789 */ 1790 setTextboxValue: function(value) { 1791 var $input = this.$control_input; 1792 var changed = $input.val() !== value; 1793 if (changed) { 1794 $input.val(value).triggerHandler('update'); 1795 this.lastValue = value; 1796 } 1797 }, 1798 1799 /** 1800 * Returns the value of the control. If multiple items 1801 * can be selected (e.g. <select multiple>), this returns 1802 * an array. If only one item can be selected, this 1803 * returns a string. 1804 * 1805 * @returns {mixed} 1806 */ 1807 getValue: function() { 1808 if (this.tagType === TAG_SELECT && this.$input.attr('multiple')) { 1809 return this.items; 1810 } else { 1811 return this.items.join(this.settings.delimiter); 1812 } 1813 }, 1814 1815 /** 1816 * Resets the selected items to the given value. 1817 * 1818 * @param {mixed} value 1819 */ 1820 setValue: function(value, silent) { 1821 var events = silent ? [] : ['change']; 1822 1823 debounce_events(this, events, function() { 1824 this.clear(silent); 1825 this.addItems(value, silent); 1826 }); 1827 }, 1828 1829 /** 1830 * Sets the selected item. 1831 * 1832 * @param {object} $item 1833 * @param {object} e (optional) 1834 */ 1835 setActiveItem: function($item, e) { 1836 var self = this; 1837 var eventName; 1838 var i, idx, begin, end, item, swap; 1839 var $last; 1840 1841 if (self.settings.mode === 'single') return; 1842 $item = $($item); 1843 1844 // clear the active selection 1845 if (!$item.length) { 1846 $(self.$activeItems).removeClass('active'); 1847 self.$activeItems = []; 1848 if (self.isFocused) { 1849 self.showInput(); 1850 } 1851 return; 1852 } 1853 1854 // modify selection 1855 eventName = e && e.type.toLowerCase(); 1856 1857 if (eventName === 'mousedown' && self.isShiftDown && self.$activeItems.length) { 1858 $last = self.$control.children('.active:last'); 1859 begin = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$last[0]]); 1860 end = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$item[0]]); 1861 if (begin > end) { 1862 swap = begin; 1863 begin = end; 1864 end = swap; 1865 } 1866 for (i = begin; i <= end; i++) { 1867 item = self.$control[0].childNodes[i]; 1868 if (self.$activeItems.indexOf(item) === -1) { 1869 $(item).addClass('active'); 1870 self.$activeItems.push(item); 1871 } 1872 } 1873 e.preventDefault(); 1874 } else if ((eventName === 'mousedown' && self.isCtrlDown) || (eventName === 'keydown' && this.isShiftDown)) { 1875 if ($item.hasClass('active')) { 1876 idx = self.$activeItems.indexOf($item[0]); 1877 self.$activeItems.splice(idx, 1); 1878 $item.removeClass('active'); 1879 } else { 1880 self.$activeItems.push($item.addClass('active')[0]); 1881 } 1882 } else { 1883 $(self.$activeItems).removeClass('active'); 1884 self.$activeItems = [$item.addClass('active')[0]]; 1885 } 1886 1887 // ensure control has focus 1888 self.hideInput(); 1889 if (!this.isFocused) { 1890 self.focus(); 1891 } 1892 }, 1893 1894 /** 1895 * Sets the selected item in the dropdown menu 1896 * of available options. 1897 * 1898 * @param {object} $object 1899 * @param {boolean} scroll 1900 * @param {boolean} animate 1901 */ 1902 setActiveOption: function($option, scroll, animate) { 1903 var height_menu, height_item, y; 1904 var scroll_top, scroll_bottom; 1905 var self = this; 1906 1907 if (self.$activeOption) self.$activeOption.removeClass('active'); 1908 self.$activeOption = null; 1909 1910 $option = $($option); 1911 if (!$option.length) return; 1912 1913 self.$activeOption = $option.addClass('active'); 1914 1915 if (scroll || !isset(scroll)) { 1916 1917 height_menu = self.$dropdown_content.height(); 1918 height_item = self.$activeOption.outerHeight(true); 1919 scroll = self.$dropdown_content.scrollTop() || 0; 1920 y = self.$activeOption.offset().top - self.$dropdown_content.offset().top + scroll; 1921 scroll_top = y; 1922 scroll_bottom = y - height_menu + height_item; 1923 1924 if (y + height_item > height_menu + scroll) { 1925 self.$dropdown_content.stop().animate({scrollTop: scroll_bottom}, animate ? self.settings.scrollDuration : 0); 1926 } else if (y < scroll) { 1927 self.$dropdown_content.stop().animate({scrollTop: scroll_top}, animate ? self.settings.scrollDuration : 0); 1928 } 1929 1930 } 1931 }, 1932 1933 /** 1934 * Selects all items (CTRL + A). 1935 */ 1936 selectAll: function() { 1937 var self = this; 1938 if (self.settings.mode === 'single') return; 1939 1940 self.$activeItems = Array.prototype.slice.apply(self.$control.children(':not(input)').addClass('active')); 1941 if (self.$activeItems.length) { 1942 self.hideInput(); 1943 self.close(); 1944 } 1945 self.focus(); 1946 }, 1947 1948 /** 1949 * Hides the input element out of view, while 1950 * retaining its focus. 1951 */ 1952 hideInput: function() { 1953 var self = this; 1954 1955 self.setTextboxValue(''); 1956 self.$control_input.css({opacity: 0, position: 'absolute', left: self.rtl ? 10000 : -10000}); 1957 self.isInputHidden = true; 1958 }, 1959 1960 /** 1961 * Restores input visibility. 1962 */ 1963 showInput: function() { 1964 this.$control_input.css({opacity: 1, position: 'relative', left: 0}); 1965 this.isInputHidden = false; 1966 }, 1967 1968 /** 1969 * Gives the control focus. 1970 */ 1971 focus: function() { 1972 var self = this; 1973 if (self.isDisabled) return; 1974 1975 self.ignoreFocus = true; 1976 self.$control_input[0].focus(); 1977 window.setTimeout(function() { 1978 self.ignoreFocus = false; 1979 self.onFocus(); 1980 }, 0); 1981 }, 1982 1983 /** 1984 * Forces the control out of focus. 1985 * 1986 * @param {Element} dest 1987 */ 1988 blur: function(dest) { 1989 this.$control_input[0].blur(); 1990 this.onBlur(null, dest); 1991 }, 1992 1993 /** 1994 * Returns a function that scores an object 1995 * to show how good of a match it is to the 1996 * provided query. 1997 * 1998 * @param {string} query 1999 * @param {object} options 2000 * @return {function} 2001 */ 2002 getScoreFunction: function(query) {
2003 return this.sifter.getScoreFunction(query, this.getSearchOptions()); 2004 }, 2005 2006 /** 2007 * Returns search options for sifter (the system 2008 * for scoring and sorting results). 2009 * 2010 * @see https://github.com/brianreavis/sifter.js 2011 * @return {object} 2012 */ 2013 getSearchOptions: function() { 2014 var settings = this.settings; 2015 var sort = settings.sortField; 2016 if (typeof sort === 'string') { 2017 sort = [{field: sort}]; 2018 } 2019 2020 return { 2021 fields : settings.searchField, 2022 conjunction : settings.searchConjunction, 2023 sort : sort 2024 }; 2025 }, 2026 2027 /** 2028 * Searches through available options and returns 2029 * a sorted array of matches. 2030 * 2031 * Returns an object containing: 2032 * 2033 * - query {string} 2034 * - tokens {array} 2035 * - total {int} 2036 * - items {array} 2037 * 2038 * @param {string} query 2039 * @returns {object} 2040 */ 2041 search: function(query) { 2042 var i, value, score, result, calculateScore; 2043 var self = this; 2044 var settings = self.settings; 2045 var options = this.getSearchOptions(); 2046 2047 // validate user-provided result scoring function 2048 if (settings.score) { 2049 calculateScore = self.settings.score.apply(this, [query]); 2050 if (typeof calculateScore !== 'function') { 2051 throw new Error('Selectize "score" setting must be a function that returns a function'); 2052 } 2053 } 2054 2055 // perform search 2056 if (query !== self.lastQuery) { 2057 self.lastQuery = query; 2058 result = self.sifter.search(query, $.extend(options, {score: calculateScore})); 2059 self.currentResults = result; 2060 } else { 2061 result = $.extend(true, {}, self.currentResults); 2062 } 2063 2064 // filter out selected items 2065 if (settings.hideSelected) { 2066 for (i = result.items.length - 1; i >= 0; i--) { 2067 if (self.items.indexOf(hash_key(result.items[i].id)) !== -1) { 2068 result.items.splice(i, 1); 2069 } 2070 } 2071 } 2072 2073 return result; 2074 }, 2075 2076 /** 2077 * Refreshes the list of available options shown 2078 * in the autocomplete dropdown menu. 2079 * 2080 * @param {boolean} triggerDropdown 2081 */ 2082 refreshOptions: function(triggerDropdown) { 2083 var i, j, k, n, groups, groups_order, option, option_html, optgroup, optgroups, html, html_children, has_create_option; 2084 var $active, $active_before, $create; 2085 2086 if (typeof triggerDropdown === 'undefined') { 2087 triggerDropdown = true; 2088 } 2089 2090 var self = this; 2091 var query = $.trim(self.$control_input.val()); 2092 var results = self.search(query); 2093 var $dropdown_content = self.$dropdown_content; 2094 var active_before = self.$activeOption && hash_key(self.$activeOption.attr('data-value')); 2095 2096 // build markup 2097 n = results.items.length; 2098 if (typeof self.settings.maxOptions === 'number') { 2099 n = Math.min(n, self.settings.maxOptions); 2100 } 2101 2102 // render and group available options individually 2103 groups = {}; 2104 groups_order = []; 2105 2106 for (i = 0; i < n; i++) { 2107 option = self.options[results.items[i].id]; 2108 option_html = self.render('option', option); 2109 optgroup = option[self.settings.optgroupField] || ''; 2110 optgroups = $.isArray(optgroup) ? optgroup : [optgroup]; 2111 2112 for (j = 0, k = optgroups && optgroups.length; j < k; j++) { 2113 optgroup = optgroups[j]; 2114 if (!self.optgroups.hasOwnProperty(optgroup)) { 2115 optgroup = ''; 2116 } 2117 if (!groups.hasOwnProperty(optgroup)) { 2118 groups[optgroup] = []; 2119 groups_order.push(optgroup); 2120 } 2121 groups[optgroup].push(option_html); 2122 } 2123 } 2124 2125 // sort optgroups 2126 if (this.settings.lockOptgroupOrder) { 2127 groups_order.sort(function(a, b) { 2128 var a_order = self.optgroups[a].$order || 0; 2129 var b_order = self.optgroups[b].$order || 0; 2130 return a_order - b_order; 2131 }); 2132 } 2133 2134 // render optgroup headers & join groups 2135 html = []; 2136 for (i = 0, n = groups_order.length; i < n; i++) { 2137 optgroup = groups_order[i]; 2138 if (self.optgroups.hasOwnProperty(optgroup) && groups[optgroup].length) { 2139 // render the optgroup header and options within it, 2140 // then pass it to the wrapper template 2141 html_children = self.render('optgroup_header', self.optgroups[optgroup]) || ''; 2142 html_children += groups[optgroup].join(''); 2143 html.push(self.render('optgroup', $.extend({}, self.optgroups[optgroup], { 2144 html: html_children 2145 }
vendor: 42,639 bytes, lines 2145-3667
2145))); 2146 } else { 2147 html.push(groups[optgroup].join('')); 2148 } 2149 } 2150 2151 $dropdown_content.html(html.join('')); 2152 2153 // highlight matching terms inline 2154 if (self.settings.highlight && results.query.length && results.tokens.length) { 2155 for (i = 0, n = results.tokens.length; i < n; i++) { 2156 highlight($dropdown_content, results.tokens[i].regex); 2157 } 2158 } 2159 2160 // add "selected" class to selected options 2161 if (!self.settings.hideSelected) { 2162 for (i = 0, n = self.items.length; i < n; i++) { 2163 self.getOption(self.items[i]).addClass('selected'); 2164 } 2165 } 2166 2167 // add create option 2168 has_create_option = self.canCreate(query); 2169 if (has_create_option) { 2170 $dropdown_content.prepend(self.render('option_create', {input: query})); 2171 $create = $($dropdown_content[0].childNodes[0]); 2172 } 2173 2174 // activate 2175 self.hasOptions = results.items.length > 0 || has_create_option; 2176 if (self.hasOptions) { 2177 if (results.items.length > 0) { 2178 $active_before = active_before && self.getOption(active_before); 2179 if ($active_before && $active_before.length) { 2180 $active = $active_before; 2181 } else if (self.settings.mode === 'single' && self.items.length) { 2182 $active = self.getOption(self.items[0]); 2183 } 2184 if (!$active || !$active.length) { 2185 if ($create && !self.settings.addPrecedence) { 2186 $active = self.getAdjacentOption($create, 1); 2187 } else { 2188 $active = $dropdown_content.find('[data-selectable]:first'); 2189 } 2190 } 2191 } else { 2192 $active = $create; 2193 } 2194 self.setActiveOption($active); 2195 if (triggerDropdown && !self.isOpen) { self.open(); } 2196 } else { 2197 self.setActiveOption(null); 2198 if (triggerDropdown && self.isOpen) { self.close(); } 2199 } 2200 }, 2201 2202 /** 2203 * Adds an available option. If it already exists, 2204 * nothing will happen. Note: this does not refresh 2205 * the options list dropdown (use `refreshOptions` 2206 * for that). 2207 * 2208 * Usage: 2209 * 2210 * this.addOption(data) 2211 * 2212 * @param {object|array} data 2213 */ 2214 addOption: function(data) { 2215 var i, n, value, self = this; 2216 2217 if ($.isArray(data)) { 2218 for (i = 0, n = data.length; i < n; i++) { 2219 self.addOption(data[i]); 2220 } 2221 return; 2222 } 2223 2224 if (value = self.registerOption(data)) { 2225 self.userOptions[value] = true; 2226 self.lastQuery = null; 2227 self.trigger('option_add', value, data); 2228 } 2229 }, 2230 2231 /** 2232 * Registers an option to the pool of options. 2233 * 2234 * @param {object} data 2235 * @return {boolean|string} 2236 */ 2237 registerOption: function(data) { 2238 var key = hash_key(data[this.settings.valueField]); 2239 if (!key || this.options.hasOwnProperty(key)) return false; 2240 data.$order = data.$order || ++this.order; 2241 this.options[key] = data; 2242 return key; 2243 }, 2244 2245 /** 2246 * Registers an option group to the pool of option groups. 2247 * 2248 * @param {object} data 2249 * @return {boolean|string} 2250 */ 2251 registerOptionGroup: function(data) { 2252 var key = hash_key(data[this.settings.optgroupValueField]); 2253 if (!key) return false; 2254 2255 data.$order = data.$order || ++this.order; 2256 this.optgroups[key] = data; 2257 return key; 2258 }, 2259 2260 /** 2261 * Registers a new optgroup for options 2262 * to be bucketed into. 2263 * 2264 * @param {string} id 2265 * @param {object} data 2266 */ 2267 addOptionGroup: function(id, data) { 2268 data[this.settings.optgroupValueField] = id; 2269 if (id = this.registerOptionGroup(data)) { 2270 this.trigger('optgroup_add', id, data); 2271 } 2272 }, 2273 2274 /** 2275 * Removes an existing option group. 2276 * 2277 * @param {string} id 2278 */ 2279 removeOptionGroup: function(id) { 2280 if (this.optgroups.hasOwnProperty(id)) { 2281 delete this.optgroups[id]; 2282 this.renderCache = {}; 2283 this.trigger('optgroup_remove', id); 2284 } 2285 }, 2286 2287 /** 2288 * Clears all existing option groups. 2289 */ 2290 clearOptionGroups: function() { 2291 this.optgroups = {}; 2292 this.renderCache = {}; 2293 this.trigger('optgroup_clear'); 2294 }, 2295 2296 /** 2297 * Updates an option available for selection. If 2298 * it is visible in the selected items or options 2299 * dropdown, it will be re-rendered automatically. 2300 * 2301 * @param {string} value 2302 * @param {object} data 2303 */ 2304 updateOption: function(value, data) { 2305 var self = this; 2306 var $item, $item_new; 2307 var value_new, index_item, cache_items, cache_options, order_old; 2308 2309 value = hash_key(value); 2310 value_new = hash_key(data[self.settings.valueField]); 2311 2312 // sanity checks 2313 if (value === null) return; 2314 if (!self.options.hasOwnProperty(value)) return; 2315 if (typeof value_new !== 'string') throw new Error('Value must be set in option data'); 2316 2317 order_old = self.options[value].$order; 2318 2319 // update references 2320 if (value_new !== value) { 2321 delete self.options[value]; 2322 index_item = self.items.indexOf(value); 2323 if (index_item !== -1) { 2324 self.items.splice(index_item, 1, value_new); 2325 } 2326 } 2327 data.$order = data.$order || order_old; 2328 self.options[value_new] = data; 2329 2330 // invalidate render cache 2331 cache_items = self.renderCache['item']; 2332 cache_options = self.renderCache['option']; 2333 2334 if (cache_items) { 2335 delete cache_items[value]; 2336 delete cache_items[value_new]; 2337 } 2338 if (cache_options) { 2339 delete cache_options[value]; 2340 delete cache_options[value_new]; 2341 } 2342 2343 // update the item if it's selected 2344 if (self.items.indexOf(value_new) !== -1) { 2345 $item = self.getItem(value); 2346 $item_new = $(self.render('item', data)); 2347 if ($item.hasClass('active')) $item_new.addClass('active'); 2348 $item.replaceWith($item_new); 2349 } 2350 2351 // invalidate last query because we might have updated the sortField 2352 self.lastQuery = null; 2353 2354 // update dropdown contents 2355 if (self.isOpen) { 2356 self.refreshOptions(false); 2357 } 2358 }, 2359 2360 /** 2361 * Removes a single option. 2362 * 2363 * @param {string} value 2364 * @param {boolean} silent 2365 */ 2366 removeOption: function(value, silent) { 2367 var self = this; 2368 value = hash_key(value); 2369 2370 var cache_items = self.renderCache['item']; 2371 var cache_options = self.renderCache['option']; 2372 if (cache_items) delete cache_items[value]; 2373 if (cache_options) delete cache_options[value]; 2374 2375 delete self.userOptions[value]; 2376 delete self.options[value]; 2377 self.lastQuery = null; 2378 self.trigger('option_remove', value); 2379 self.removeItem(value, silent); 2380 }, 2381 2382 /** 2383 * Clears all options. 2384 */ 2385 clearOptions: function() { 2386 var self = this; 2387 2388 self.loadedSearches = {}; 2389 self.userOptions = {}; 2390 self.renderCache = {}; 2391 self.options = self.sifter.items = {}; 2392 self.lastQuery = null; 2393 self.trigger('option_clear'); 2394 self.clear(); 2395 }, 2396 2397 /** 2398 * Returns the jQuery element of the option 2399 * matching the given value. 2400 * 2401 * @param {string} value 2402 * @returns {object} 2403 */ 2404 getOption: function(value) { 2405 return this.getElementWithValue(value, this.$dropdown_content.find('[data-selectable]')); 2406 }, 2407 2408 /** 2409 * Returns the jQuery element of the next or 2410 * previous selectable option. 2411 * 2412 * @param {object} $option 2413 * @param {int} direction can be 1 for next or -1 for previous 2414 * @return {object} 2415 */ 2416 getAdjacentOption: function($option, direction) { 2417 var $options = this.$dropdown.find('[data-selectable]'); 2418 var index = $options.index($option) + direction; 2419 2420 return index >= 0 && index < $options.length ? $options.eq(index) : $(); 2421 }, 2422 2423 /** 2424 * Finds the first element with a "data-value" attribute 2425 * that matches the given value. 2426 * 2427 * @param {mixed} value 2428 * @param {object} $els 2429 * @return {object} 2430 */ 2431 getElementWithValue: function(value, $els) { 2432 value = hash_key(value); 2433 2434 if (typeof value !== 'undefined' && value !== null) { 2435 for (var i = 0, n = $els.length; i < n; i++) { 2436 if ($els[i].getAttribute('data-value') === value) { 2437 return $($els[i]); 2438 } 2439 } 2440 } 2441 2442 return $(); 2443 }, 2444 2445 /** 2446 * Returns the jQuery element of the item 2447 * matching the given value. 2448 * 2449 * @param {string} value 2450 * @returns {object} 2451 */ 2452 getItem: function(value) { 2453 return this.getElementWithValue(value, this.$control.children()); 2454 }, 2455 2456 /** 2457 * "Selects" multiple items at once. Adds them to the list 2458 * at the current caret position. 2459 * 2460 * @param {string} value 2461 * @param {boolean} silent 2462 */ 2463 addItems: function(values, silent) { 2464 var items = $.isArray(values) ? values : [values]; 2465 for (var i = 0, n = items.length; i < n; i++) { 2466 this.isPending = (i < n - 1); 2467 this.addItem(items[i], silent); 2468 } 2469 }, 2470 2471 /** 2472 * "Selects" an item. Adds it to the list 2473 * at the current caret position. 2474 * 2475 * @param {string} value 2476 * @param {boolean} silent 2477 */ 2478 addItem: function(value, silent) { 2479 var events = silent ? [] : ['change']; 2480 2481 debounce_events(this, events, function() { 2482 var $item, $option, $options; 2483 var self = this; 2484 var inputMode = self.settings.mode; 2485 var i, active, value_next, wasFull; 2486 value = hash_key(value); 2487 2488 if (self.items.indexOf(value) !== -1) { 2489 if (inputMode === 'single') self.close(); 2490 return; 2491 } 2492 2493 if (!self.options.hasOwnProperty(value)) return; 2494 if (inputMode === 'single') self.clear(silent); 2495 if (inputMode === 'multi' && self.isFull()) return; 2496 2497 $item = $(self.render('item', self.options[value])); 2498 wasFull = self.isFull(); 2499 self.items.splice(self.caretPos, 0, value); 2500 self.insertAtCaret($item); 2501 if (!self.isPending || (!wasFull && self.isFull())) { 2502 self.refreshState(); 2503 } 2504 2505 if (self.isSetup) { 2506 $options = self.$dropdown_content.find('[data-selectable]'); 2507 2508 // update menu / remove the option (if this is not one item being added as part of series) 2509 if (!self.isPending) { 2510 $option = self.getOption(value); 2511 value_next = self.getAdjacentOption($option, 1).attr('data-value'); 2512 self.refreshOptions(self.isFocused && inputMode !== 'single'); 2513 if (value_next) { 2514 self.setActiveOption(self.getOption(value_next)); 2515 } 2516 } 2517 2518 // hide the menu if the maximum number of items have been selected or no options are left 2519 if (!$options.length || self.isFull()) { 2520 self.close(); 2521 } else { 2522 self.positionDropdown(); 2523 } 2524 2525 self.updatePlaceholder(); 2526 self.trigger('item_add', value, $item); 2527 self.updateOriginalInput({silent: silent}); 2528 } 2529 }); 2530 }, 2531 2532 /** 2533 * Removes the selected item matching 2534 * the provided value. 2535 * 2536 * @param {string} value 2537 */ 2538 removeItem: function(value, silent) { 2539 var self = this; 2540 var $item, i, idx; 2541 2542 $item = (typeof value === 'object') ? value : self.getItem(value); 2543 value = hash_key($item.attr('data-value')); 2544 i = self.items.indexOf(value); 2545 2546 if (i !== -1) { 2547 $item.remove(); 2548 if ($item.hasClass('active')) { 2549 idx = self.$activeItems.indexOf($item[0]); 2550 self.$activeItems.splice(idx, 1); 2551 } 2552 2553 self.items.splice(i, 1); 2554 self.lastQuery = null; 2555 if (!self.settings.persist && self.userOptions.hasOwnProperty(value)) { 2556 self.removeOption(value, silent); 2557 } 2558 2559 if (i < self.caretPos) { 2560 self.setCaret(self.caretPos - 1); 2561 } 2562 2563 self.refreshState(); 2564 self.updatePlaceholder(); 2565 self.updateOriginalInput({silent: silent}); 2566 self.positionDropdown(); 2567 self.trigger('item_remove', value, $item); 2568 } 2569 }, 2570 2571 /** 2572 * Invokes the `create` method provided in the 2573 * selectize options that should provide the data 2574 * for the new item, given the user input. 2575 * 2576 * Once this completes, it will be added 2577 * to the item list. 2578 * 2579 * @param {string} value 2580 * @param {boolean} [triggerDropdown] 2581 * @param {function} [callback] 2582 * @return {boolean} 2583 */ 2584 createItem: function(input, triggerDropdown) { 2585 var self = this; 2586 var caret = self.caretPos; 2587 input = input || $.trim(self.$control_input.val() || ''); 2588 2589 var callback = arguments[arguments.length - 1]; 2590 if (typeof callback !== 'function') callback = function() {}; 2591 2592 if (typeof triggerDropdown !== 'boolean') { 2593 triggerDropdown = true; 2594 } 2595 2596 if (!self.canCreate(input)) { 2597 callback(); 2598 return false; 2599 } 2600 2601 self.lock(); 2602 2603 var setup = (typeof self.settings.create === 'function') ? this.settings.create : function(input) { 2604 var data = {}; 2605 data[self.settings.labelField] = input; 2606 data[self.settings.valueField] = input; 2607 return data; 2608 }; 2609 2610 var create = once(function(data) { 2611 self.unlock(); 2612 2613 if (!data || typeof data !== 'object') return callback(); 2614 var value = hash_key(data[self.settings.valueField]); 2615 if (typeof value !== 'string') return callback(); 2616 2617 self.setTextboxValue(''); 2618 self.addOption(data); 2619 self.setCaret(caret); 2620 self.addItem(value); 2621 self.refreshOptions(triggerDropdown && self.settings.mode !== 'single'); 2622 callback(data); 2623 }); 2624 2625 var output = setup.apply(this, [input, create]); 2626 if (typeof output !== 'undefined') { 2627 create(output); 2628 } 2629 2630 return true; 2631 }, 2632 2633 /** 2634 * Re-renders the selected item lists. 2635 */ 2636 refreshItems: function() { 2637 this.lastQuery = null; 2638 2639 if (this.isSetup) { 2640 this.addItem(this.items); 2641 } 2642 2643 this.refreshState(); 2644 this.updateOriginalInput(); 2645 }, 2646 2647 /** 2648 * Updates all state-dependent attributes 2649 * and CSS classes. 2650 */ 2651 refreshState: function() { 2652 var invalid, self = this; 2653 if (self.isRequired) { 2654 if (self.items.length) self.isInvalid = false; 2655 self.$control_input.prop('required', invalid); 2656 } 2657 self.refreshClasses(); 2658 }, 2659 2660 /** 2661 * Updates all state-dependent CSS classes. 2662 */ 2663 refreshClasses: function() { 2664 var self = this; 2665 var isFull = self.isFull(); 2666 var isLocked = self.isLocked; 2667 2668 self.$wrapper 2669 .toggleClass('rtl', self.rtl); 2670 2671 self.$control 2672 .toggleClass('focus', self.isFocused) 2673 .toggleClass('disabled', self.isDisabled) 2674 .toggleClass('required', self.isRequired) 2675 .toggleClass('invalid', self.isInvalid) 2676 .toggleClass('locked', isLocked) 2677 .toggleClass('full', isFull).toggleClass('not-full', !isFull) 2678 .toggleClass('input-active', self.isFocused && !self.isInputHidden) 2679 .toggleClass('dropdown-active', self.isOpen) 2680 .toggleClass('has-options', !$.isEmptyObject(self.options)) 2681 .toggleClass('has-items', self.items.length > 0); 2682 2683 self.$control_input.data('grow', !isFull && !isLocked); 2684 }, 2685 2686 /** 2687 * Determines whether or not more items can be added 2688 * to the control without exceeding the user-defined maximum. 2689 * 2690 * @returns {boolean} 2691 */ 2692 isFull: function() { 2693 return this.settings.maxItems !== null && this.items.length >= this.settings.maxItems; 2694 }, 2695 2696 /** 2697 * Refreshes the original <select> or <input> 2698 * element to reflect the current state. 2699 */ 2700 updateOriginalInput: function(opts) { 2701 var i, n, options, label, self = this; 2702 opts = opts || {}; 2703 2704 if (self.tagType === TAG_SELECT) { 2705 options = []; 2706 for (i = 0, n = self.items.length; i < n; i++) { 2707 label = self.options[self.items[i]][self.settings.labelField] || ''; 2708 options.push('<option value="' + escape_html(self.items[i]) + '" selected="selected">' + escape_html(label) + '</option>'); 2709 } 2710 if (!options.length && !this.$input.attr('multiple')) { 2711 options.push('<option value="" selected="selected"></option>'); 2712 } 2713 self.$input.html(options.join('')); 2714 } else { 2715 self.$input.val(self.getValue()); 2716 self.$input.attr('value',self.$input.val()); 2717 } 2718 2719 if (self.isSetup) { 2720 if (!opts.silent) { 2721 self.trigger('change', self.$input.val()); 2722 } 2723 } 2724 }, 2725 2726 /** 2727 * Shows/hide the input placeholder depending 2728 * on if there items in the list already. 2729 */ 2730 updatePlaceholder: function() { 2731 if (!this.settings.placeholder) return; 2732 var $input = this.$control_input; 2733 2734 if (this.items.length) { 2735 $input.removeAttr('placeholder'); 2736 } else { 2737 $input.attr('placeholder', this.settings.placeholder); 2738 } 2739 $input.triggerHandler('update', {force: true}); 2740 }, 2741 2742 /** 2743 * Shows the autocomplete dropdown containing 2744 * the available options. 2745 */ 2746 open: function() { 2747 var self = this; 2748 2749 if (self.isLocked || self.isOpen || (self.settings.mode === 'multi' && self.isFull())) return; 2750 self.focus(); 2751 self.isOpen = true; 2752 self.refreshState(); 2753 self.$dropdown.css({visibility: 'hidden', display: 'block'}); 2754 self.positionDropdown(); 2755 self.$dropdown.css({visibility: 'visible'}); 2756 self.trigger('dropdown_open', self.$dropdown); 2757 }, 2758 2759 /** 2760 * Closes the autocomplete dropdown menu. 2761 */ 2762 close: function() { 2763 var self = this; 2764 var trigger = self.isOpen; 2765 2766 if (self.settings.mode === 'single' && self.items.length) { 2767 self.hideInput(); 2768 } 2769 2770 self.isOpen = false; 2771 self.$dropdown.hide(); 2772 self.setActiveOption(null); 2773 self.refreshState(); 2774 2775 if (trigger) self.trigger('dropdown_close', self.$dropdown); 2776 }, 2777 2778 /** 2779 * Calculates and applies the appropriate 2780 * position of the dropdown. 2781 */ 2782 positionDropdown: function() { 2783 var $control = this.$control; 2784 var offset = this.settings.dropdownParent === 'body' ? $control.offset() : $control.position(); 2785 offset.top += $control.outerHeight(true); 2786 2787 this.$dropdown.css({ 2788 width : $control.outerWidth(), 2789 top : offset.top, 2790 left : offset.left 2791 }); 2792 }, 2793 2794 /** 2795 * Resets / clears all selected items 2796 * from the control. 2797 * 2798 * @param {boolean} silent 2799 */ 2800 clear: function(silent) { 2801 var self = this; 2802 2803 if (!self.items.length) return; 2804 self.$control.children(':not(input)').remove(); 2805 self.items = []; 2806 self.lastQuery = null; 2807 self.setCaret(0); 2808 self.setActiveItem(null); 2809 self.updatePlaceholder(); 2810 self.updateOriginalInput({silent: silent}); 2811 self.refreshState(); 2812 self.showInput(); 2813 self.trigger('clear'); 2814 }, 2815 2816 /** 2817 * A helper method for inserting an element 2818 * at the current caret position. 2819 * 2820 * @param {object} $el 2821 */ 2822 insertAtCaret: function($el) { 2823 var caret = Math.min(this.caretPos, this.items.length); 2824 if (caret === 0) { 2825 this.$control.prepend($el); 2826 } else { 2827 $(this.$control[0].childNodes[caret]).before($el); 2828 } 2829 this.setCaret(caret + 1); 2830 }, 2831 2832 /** 2833 * Removes the current selected item(s). 2834 * 2835 * @param {object} e (optional) 2836 * @returns {boolean} 2837 */ 2838 deleteSelection: function(e) { 2839 var i, n, direction, selection, values, caret, option_select, $option_select, $tail; 2840 var self = this; 2841 2842 direction = (e && e.keyCode === KEY_BACKSPACE) ? -1 : 1; 2843 selection = getSelection(self.$control_input[0]); 2844 2845 if (self.$activeOption && !self.settings.hideSelected) { 2846 option_select = self.getAdjacentOption(self.$activeOption, -1).attr('data-value'); 2847 } 2848 2849 // determine items that will be removed 2850 values = []; 2851 2852 if (self.$activeItems.length) { 2853 $tail = self.$control.children('.active:' + (direction > 0 ? 'last' : 'first')); 2854 caret = self.$control.children(':not(input)').index($tail); 2855 if (direction > 0) { caret++; } 2856 2857 for (i = 0, n = self.$activeItems.length; i < n; i++) { 2858 values.push($(self.$activeItems[i]).attr('data-value')); 2859 } 2860 if (e) { 2861 e.preventDefault(); 2862 e.stopPropagation(); 2863 } 2864 } else if ((self.isFocused || self.settings.mode === 'single') && self.items.length) { 2865 if (direction < 0 && selection.start === 0 && selection.length === 0) { 2866 values.push(self.items[self.caretPos - 1]); 2867 } else if (direction > 0 && selection.start === self.$control_input.val().length) { 2868 values.push(self.items[self.caretPos]); 2869 } 2870 } 2871 2872 // allow the callback to abort 2873 if (!values.length || (typeof self.settings.onDelete === 'function' && self.settings.onDelete.apply(self, [values]) === false)) { 2874 return false; 2875 } 2876 2877 // perform removal 2878 if (typeof caret !== 'undefined') { 2879 self.setCaret(caret); 2880 } 2881 while (values.length) { 2882 self.removeItem(values.pop()); 2883 } 2884 2885 self.showInput(); 2886 self.positionDropdown(); 2887 self.refreshOptions(true); 2888 2889 // select previous option 2890 if (option_select) { 2891 $option_select = self.getOption(option_select); 2892 if ($option_select.length) { 2893 self.setActiveOption($option_select); 2894 } 2895 } 2896 2897 return true; 2898 }, 2899 2900 /** 2901 * Selects the previous / next item (depending 2902 * on the `direction` argument). 2903 * 2904 * > 0 - right 2905 * < 0 - left 2906 * 2907 * @param {int} direction 2908 * @param {object} e (optional) 2909 */ 2910 advanceSelection: function(direction, e) { 2911 var tail, selection, idx, valueLength, cursorAtEdge, $tail; 2912 var self = this; 2913 2914 if (direction === 0) return; 2915 if (self.rtl) direction *= -1; 2916 2917 tail = direction > 0 ? 'last' : 'first'; 2918 selection = getSelection(self.$control_input[0]); 2919 2920 if (self.isFocused && !self.isInputHidden) { 2921 valueLength = self.$control_input.val().length; 2922 cursorAtEdge = direction < 0 2923 ? selection.start === 0 && selection.length === 0 2924 : selection.start === valueLength; 2925 2926 if (cursorAtEdge && !valueLength) { 2927 self.advanceCaret(direction, e); 2928 } 2929 } else { 2930 $tail = self.$control.children('.active:' + tail); 2931 if ($tail.length) { 2932 idx = self.$control.children(':not(input)').index($tail); 2933 self.setActiveItem(null); 2934 self.setCaret(direction > 0 ? idx + 1 : idx); 2935 } 2936 } 2937 }, 2938 2939 /** 2940 * Moves the caret left / right. 2941 * 2942 * @param {int} direction 2943 * @param {object} e (optional) 2944 */ 2945 advanceCaret: function(direction, e) { 2946 var self = this, fn, $adj; 2947 2948 if (direction === 0) return; 2949 2950 fn = direction > 0 ? 'next' : 'prev'; 2951 if (self.isShiftDown) { 2952 $adj = self.$control_input[fn](); 2953 if ($adj.length) { 2954 self.hideInput(); 2955 self.setActiveItem($adj); 2956 e && e.preventDefault(); 2957 } 2958 } else { 2959 self.setCaret(self.caretPos + direction); 2960 } 2961 }, 2962 2963 /** 2964 * Moves the caret to the specified index. 2965 * 2966 * @param {int} i 2967 */ 2968 setCaret: function(i) { 2969 var self = this; 2970 2971 if (self.settings.mode === 'single') { 2972 i = self.items.length; 2973 } else { 2974 i = Math.max(0, Math.min(self.items.length, i)); 2975 } 2976 2977 if(!self.isPending) { 2978 // the input must be moved by leaving it in place and moving the 2979 // siblings, due to the fact that focus cannot be restored once lost 2980 // on mobile webkit devices 2981 var j, n, fn, $children, $child; 2982 $children = self.$control.children(':not(input)'); 2983 for (j = 0, n = $children.length; j < n; j++) { 2984 $child = $($children[j]).detach(); 2985 if (j < i) { 2986 self.$control_input.before($child); 2987 } else { 2988 self.$control.append($child); 2989 } 2990 } 2991 } 2992 2993 self.caretPos = i; 2994 }, 2995 2996 /** 2997 * Disables user input on the control. Used while 2998 * items are being asynchronously created. 2999 */ 3000 lock: function() { 3001 this.close(); 3002 this.isLocked = true; 3003 this.refreshState(); 3004 }, 3005 3006 /** 3007 * Re-enables user input on the control. 3008 */ 3009 unlock: function() { 3010 this.isLocked = false; 3011 this.refreshState(); 3012 }, 3013 3014 /** 3015 * Disables user input on the control completely. 3016 * While disabled, it cannot receive focus. 3017 */ 3018 disable: function() { 3019 var self = this; 3020 self.$input.prop('disabled', true); 3021 self.$control_input.prop('disabled', true).prop('tabindex', -1); 3022 self.isDisabled = true; 3023 self.lock(); 3024 }, 3025 3026 /** 3027 * Enables the control so that it can respond 3028 * to focus and user input. 3029 */ 3030 enable: function() { 3031 var self = this; 3032 self.$input.prop('disabled', false); 3033 self.$control_input.prop('disabled', false).prop('tabindex', self.tabIndex); 3034 self.isDisabled = false; 3035 self.unlock(); 3036 }, 3037 3038 /** 3039 * Completely destroys the control and 3040 * unbinds all event listeners so that it can 3041 * be garbage collected. 3042 */ 3043 destroy: function() { 3044 var self = this; 3045 var eventNS = self.eventNS; 3046 var revertSettings = self.revertSettings; 3047 3048 self.trigger('destroy'); 3049 self.off(); 3050 self.$wrapper.remove(); 3051 self.$dropdown.remove(); 3052 3053 self.$input 3054 .html('') 3055 .append(revertSettings.$children) 3056 .removeAttr('tabindex') 3057 .removeClass('selectized') 3058 .attr({tabindex: revertSettings.tabindex}) 3059 .show(); 3060 3061 self.$control_input.removeData('grow'); 3062 self.$input.removeData('selectize'); 3063 3064 $(window).off(eventNS); 3065 $(document).off(eventNS); 3066 $(document.body).off(eventNS); 3067 3068 delete self.$input[0].selectize; 3069 }, 3070 3071 /** 3072 * A helper method for rendering "item" and 3073 * "option" templates, given the data. 3074 * 3075 * @param {string} templateName 3076 * @param {object} data 3077 * @returns {string} 3078 */ 3079 render: function(templateName, data) { 3080 var value, id, label; 3081 var html = ''; 3082 var cache = false; 3083 var self = this; 3084 var regex_tag = /^[\t \r\n]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i; 3085 3086 if (templateName === 'option' || templateName === 'item') { 3087 value = hash_key(data[self.settings.valueField]); 3088 cache = !!value; 3089 } 3090 3091 // pull markup from cache if it exists 3092 if (cache) { 3093 if (!isset(self.renderCache[templateName])) { 3094 self.renderCache[templateName] = {}; 3095 } 3096 if (self.renderCache[templateName].hasOwnProperty(value)) { 3097 return self.renderCache[templateName][value]; 3098 } 3099 } 3100 3101 // render markup 3102 html = self.settings.render[templateName].apply(this, [data, escape_html]); 3103 3104 // add mandatory attributes 3105 if (templateName === 'option' || templateName === 'option_create') { 3106 html = html.replace(regex_tag, '<$1 data-selectable'); 3107 } 3108 if (templateName === 'optgroup') { 3109 id = data[self.settings.optgroupValueField] || ''; 3110 html = html.replace(regex_tag, '<$1 data-group="' + escape_replace(escape_html(id)) + '"'); 3111 } 3112 if (templateName === 'option' || templateName === 'item') { 3113 html = html.replace(regex_tag, '<$1 data-value="' + escape_replace(escape_html(value || '')) + '"'); 3114 } 3115 3116 // update cache 3117 if (cache) { 3118 self.renderCache[templateName][value] = html; 3119 } 3120 3121 return html; 3122 }, 3123 3124 /** 3125 * Clears the render cache for a template. If 3126 * no template is given, clears all render 3127 * caches. 3128 * 3129 * @param {string} templateName 3130 */ 3131 clearCache: function(templateName) { 3132 var self = this; 3133 if (typeof templateName === 'undefined') { 3134 self.renderCache = {}; 3135 } else { 3136 delete self.renderCache[templateName]; 3137 } 3138 }, 3139 3140 /** 3141 * Determines whether or not to display the 3142 * create item prompt, given a user input. 3143 * 3144 * @param {string} input 3145 * @return {boolean} 3146 */ 3147 canCreate: function(input) { 3148 var self = this; 3149 if (!self.settings.create) return false; 3150 var filter = self.settings.createFilter; 3151 return input.length 3152 && (typeof filter !== 'function' || filter.apply(self, [input])) 3153 && (typeof filter !== 'string' || new RegExp(filter).test(input)) 3154 && (!(filter instanceof RegExp) || filter.test(input)); 3155 } 3156 3157 }); 3158 3159 3160 Selectize.count = 0; 3161 Selectize.defaults = { 3162 options: [], 3163 optgroups: [], 3164 3165 plugins: [], 3166 delimiter: ',', 3167 splitOn: null, // regexp or string for splitting up values from a paste command 3168 persist: true, 3169 diacritics: true, 3170 create: false, 3171 createOnBlur: false, 3172 createFilter: null, 3173 highlight: true, 3174 openOnFocus: true, 3175 maxOptions: 1000, 3176 maxItems: null, 3177 hideSelected: null, 3178 addPrecedence: false, 3179 selectOnTab: false, 3180 preload: false, 3181 allowEmptyOption: false, 3182 closeAfterSelect: false, 3183 3184 scrollDuration: 60, 3185 loadThrottle: 300, 3186 loadingClass: 'loading', 3187 3188 dataAttr: 'data-data', 3189 optgroupField: 'optgroup', 3190 valueField: 'value', 3191 labelField: 'text', 3192 optgroupLabelField: 'label', 3193 optgroupValueField: 'value', 3194 lockOptgroupOrder: false, 3195 3196 sortField: '$order', 3197 searchField: ['text'], 3198 searchConjunction: 'and', 3199 3200 mode: null, 3201 wrapperClass: 'selectize-control', 3202 inputClass: 'selectize-input', 3203 dropdownClass: 'selectize-dropdown', 3204 dropdownContentClass: 'selectize-dropdown-content', 3205 3206 dropdownParent: null, 3207 3208 copyClassesToDropdown: true, 3209 3210 /* 3211 load : null, // function(query, callback) { ... } 3212 score : null, // function(search) { ... } 3213 onInitialize : null, // function() { ... } 3214 onChange : null, // function(value) { ... } 3215 onItemAdd : null, // function(value, $item) { ... } 3216 onItemRemove : null, // function(value) { ... } 3217 onClear : null, // function() { ... } 3218 onOptionAdd : null, // function(value, data) { ... } 3219 onOptionRemove : null, // function(value) { ... } 3220 onOptionClear : null, // function() { ... } 3221 onOptionGroupAdd : null, // function(id, data) { ... } 3222 onOptionGroupRemove : null, // function(id) { ... } 3223 onOptionGroupClear : null, // function() { ... } 3224 onDropdownOpen : null, // function($dropdown) { ... } 3225 onDropdownClose : null, // function($dropdown) { ... } 3226 onType : null, // function(str) { ... } 3227 onDelete : null, // function(values) { ... } 3228 */ 3229 3230 render: { 3231 /* 3232 item: null, 3233 optgroup: null, 3234 optgroup_header: null, 3235 option: null, 3236 option_create: null 3237 */ 3238 } 3239 }; 3240 3241 3242 $.fn.selectize = function(settings_user) { 3243 var defaults = $.fn.selectize.defaults; 3244 var settings = $.extend({}, defaults, settings_user); 3245 var attr_data = settings.dataAttr; 3246 var field_label = settings.labelField; 3247 var field_value = settings.valueField; 3248 var field_optgroup = settings.optgroupField; 3249 var field_optgroup_label = settings.optgroupLabelField; 3250 var field_optgroup_value = settings.optgroupValueField; 3251 3252 /** 3253 * Initializes selectize from a <input type="text"> element. 3254 * 3255 * @param {object} $input 3256 * @param {object} settings_element 3257 */ 3258 var init_textbox = function($input, settings_element) { 3259 var i, n, values, option; 3260 3261 var data_raw = $input.attr(attr_data); 3262 3263 if (!data_raw) { 3264 var value = $.trim($input.val() || ''); 3265 if (!settings.allowEmptyOption && !value.length) return; 3266 values = value.split(settings.delimiter); 3267 for (i = 0, n = values.length; i < n; i++) { 3268 option = {}; 3269 option[field_label] = values[i]; 3270 option[field_value] = values[i]; 3271 settings_element.options.push(option); 3272 } 3273 settings_element.items = values; 3274 } else { 3275 settings_element.options = JSON.parse(data_raw); 3276 for (i = 0, n = settings_element.options.length; i < n; i++) { 3277 settings_element.items.push(settings_element.options[i][field_value]); 3278 } 3279 } 3280 }; 3281 3282 /** 3283 * Initializes selectize from a <select> element. 3284 * 3285 * @param {object} $input 3286 * @param {object} settings_element 3287 */ 3288 var init_select = function($input, settings_element) { 3289 var i, n, tagName, $children, order = 0; 3290 var options = settings_element.options; 3291 var optionsMap = {}; 3292 3293 var readData = function($el) { 3294 var data = attr_data && $el.attr(attr_data); 3295 if (typeof data === 'string' && data.length) { 3296 return JSON.parse(data); 3297 } 3298 return null; 3299 }; 3300 3301 var addOption = function($option, group) { 3302 $option = $($option); 3303 3304 var value = hash_key($option.attr('value')); 3305 if (!value && !settings.allowEmptyOption) return; 3306 3307 // if the option already exists, it's probably been 3308 // duplicated in another optgroup. in this case, push 3309 // the current group to the "optgroup" property on the 3310 // existing option so that it's rendered in both places. 3311 if (optionsMap.hasOwnProperty(value)) { 3312 if (group) { 3313 var arr = optionsMap[value][field_optgroup]; 3314 if (!arr) { 3315 optionsMap[value][field_optgroup] = group; 3316 } else if (!$.isArray(arr)) { 3317 optionsMap[value][field_optgroup] = [arr, group]; 3318 } else { 3319 arr.push(group); 3320 } 3321 } 3322 return; 3323 } 3324 3325 var option = readData($option) || {}; 3326 option[field_label] = option[field_label] || $option.text(); 3327 option[field_value] = option[field_value] || value; 3328 option[field_optgroup] = option[field_optgroup] || group; 3329 3330 optionsMap[value] = option; 3331 options.push(option); 3332 3333 if ($option.is(':selected')) { 3334 settings_element.items.push(value); 3335 } 3336 }; 3337 3338 var addGroup = function($optgroup) { 3339 var i, n, id, optgroup, $options; 3340 3341 $optgroup = $($optgroup); 3342 id = $optgroup.attr('label'); 3343 3344 if (id) { 3345 optgroup = readData($optgroup) || {}; 3346 optgroup[field_optgroup_label] = id; 3347 optgroup[field_optgroup_value] = id; 3348 settings_element.optgroups.push(optgroup); 3349 } 3350 3351 $options = $('option', $optgroup); 3352 for (i = 0, n = $options.length; i < n; i++) { 3353 addOption($options[i], id); 3354 } 3355 }; 3356 3357 settings_element.maxItems = $input.attr('multiple') ? null : 1; 3358 3359 $children = $input.children(); 3360 for (i = 0, n = $children.length; i < n; i++) { 3361 tagName = $children[i].tagName.toLowerCase(); 3362 if (tagName === 'optgroup') { 3363 addGroup($children[i]); 3364 } else if (tagName === 'option') { 3365 addOption($children[i]); 3366 } 3367 } 3368 }; 3369 3370 return this.each(function() { 3371 if (this.selectize) return; 3372 3373 var instance; 3374 var $input = $(this); 3375 var tag_name = this.tagName.toLowerCase(); 3376 var placeholder = $input.attr('placeholder') || $input.attr('data-placeholder'); 3377 if (!placeholder && !settings.allowEmptyOption) { 3378 placeholder = $input.children('option[value=""]').text(); 3379 } 3380 3381 var settings_element = { 3382 'placeholder' : placeholder, 3383 'options' : [], 3384 'optgroups' : [], 3385 'items' : [] 3386 }; 3387 3388 if (tag_name === 'select') { 3389 init_select($input, settings_element); 3390 } else { 3391 init_textbox($input, settings_element); 3392 } 3393 3394 instance = new Selectize($input, $.extend(true, {}, defaults, settings_element, settings_user)); 3395 }); 3396 }; 3397 3398 $.fn.selectize.defaults = Selectize.defaults; 3399 $.fn.selectize.support = { 3400 validity: SUPPORTS_VALIDITY_API 3401 }; 3402 3403 3404 Selectize.define('drag_drop', function(options) { 3405 if (!$.fn.sortable) throw new Error('The "drag_drop" plugin requires jQuery UI "sortable".'); 3406 if (this.settings.mode !== 'multi') return; 3407 var self = this; 3408 3409 self.lock = (function() { 3410 var original = self.lock; 3411 return function() { 3412 var sortable = self.$control.data('sortable'); 3413 if (sortable) sortable.disable(); 3414 return original.apply(self, arguments); 3415 }; 3416 })(); 3417 3418 self.unlock = (function() { 3419 var original = self.unlock; 3420 return function() { 3421 var sortable = self.$control.data('sortable'); 3422 if (sortable) sortable.enable(); 3423 return original.apply(self, arguments); 3424 }; 3425 })(); 3426 3427 self.setup = (function() { 3428 var original = self.setup; 3429 return function() { 3430 original.apply(this, arguments); 3431 3432 var $control = self.$control.sortable({ 3433 items: '[data-value]', 3434 forcePlaceholderSize: true, 3435 disabled: self.isLocked, 3436 start: function(e, ui) { 3437 ui.placeholder.css('width', ui.helper.css('width')); 3438 $control.css({overflow: 'visible'}); 3439 }, 3440 stop: function() { 3441 $control.css({overflow: 'hidden'}); 3442 var active = self.$activeItems ? self.$activeItems.slice() : null; 3443 var values = []; 3444 $control.children('[data-value]').each(function() { 3445 values.push($(this).attr('data-value')); 3446 }); 3447 self.setValue(values); 3448 self.setActiveItem(active); 3449 } 3450 }); 3451 }; 3452 })(); 3453 3454 }); 3455 3456 Selectize.define('dropdown_header', function(options) { 3457 var self = this; 3458 3459 options = $.extend({ 3460 title : 'Untitled', 3461 headerClass : 'selectize-dropdown-header', 3462 titleRowClass : 'selectize-dropdown-header-title', 3463 labelClass : 'selectize-dropdown-header-label', 3464 closeClass : 'selectize-dropdown-header-close', 3465 3466 html: function(data) { 3467 return ( 3468 '<div class="' + data.headerClass + '">' + 3469 '<div class="' + data.titleRowClass + '">' + 3470 '<span class="' + data.labelClass + '">' + data.title + '</span>' + 3471 '<a href="javascript:void(0)" class="' + data.closeClass + '">×</a>' + 3472 '</div>' + 3473 '</div>' 3474 ); 3475 } 3476 }, options); 3477 3478 self.setup = (function() { 3479 var original = self.setup; 3480 return function() { 3481 original.apply(self, arguments); 3482 self.$dropdown_header = $(options.html(options)); 3483 self.$dropdown.prepend(self.$dropdown_header); 3484 }; 3485 })(); 3486 3487 }); 3488 3489 Selectize.define('optgroup_columns', function(options) { 3490 var self = this; 3491 3492 options = $.extend({ 3493 equalizeWidth : true, 3494 equalizeHeight : true 3495 }, options); 3496 3497 this.getAdjacentOption = function($option, direction) { 3498 var $options = $option.closest('[data-group]').find('[data-selectable]'); 3499 var index = $options.index($option) + direction; 3500 3501 return index >= 0 && index < $options.length ? $options.eq(index) : $(); 3502 }; 3503 3504 this.onKeyDown = (function() { 3505 var original = self.onKeyDown; 3506 return function(e) { 3507 var index, $option, $options, $optgroup; 3508 3509 if (this.isOpen && (e.keyCode === KEY_LEFT || e.keyCode === KEY_RIGHT)) { 3510 self.ignoreHover = true; 3511 $optgroup = this.$activeOption.closest('[data-group]'); 3512 index = $optgroup.find('[data-selectable]').index(this.$activeOption); 3513 3514 if(e.keyCode === KEY_LEFT) { 3515 $optgroup = $optgroup.prev('[data-group]'); 3516 } else { 3517 $optgroup = $optgroup.next('[data-group]'); 3518 } 3519 3520 $options = $optgroup.find('[data-selectable]'); 3521 $option = $options.eq(Math.min($options.length - 1, index)); 3522 if ($option.length) { 3523 this.setActiveOption($option); 3524 } 3525 return; 3526 } 3527 3528 return original.apply(this, arguments); 3529 }; 3530 })(); 3531 3532 var getScrollbarWidth = function() { 3533 var div; 3534 var width = getScrollbarWidth.width; 3535 var doc = document; 3536 3537 if (typeof width === 'undefined') { 3538 div = doc.createElement('div'); 3539 div.innerHTML = '<div style="width:50px;height:50px;position:absolute;left:-50px;top:-50px;overflow:auto;"><div style="width:1px;height:100px;"></div></div>'; 3540 div = div.firstChild; 3541 doc.body.appendChild(div); 3542 width = getScrollbarWidth.width = div.offsetWidth - div.clientWidth; 3543 doc.body.removeChild(div); 3544 } 3545 return width; 3546 }; 3547 3548 var equalizeSizes = function() { 3549 var i, n, height_max, width, width_last, width_parent, $optgroups; 3550 3551 $optgroups = $('[data-group]', self.$dropdown_content); 3552 n = $optgroups.length; 3553 if (!n || !self.$dropdown_content.width()) return; 3554 3555 if (options.equalizeHeight) { 3556 height_max = 0; 3557 for (i = 0; i < n; i++) { 3558 height_max = Math.max(height_max, $optgroups.eq(i).height()); 3559 } 3560 $optgroups.css({height: height_max}); 3561 } 3562 3563 if (options.equalizeWidth) { 3564 width_parent = self.$dropdown_content.innerWidth() - getScrollbarWidth(); 3565 width = Math.round(width_parent / n); 3566 $optgroups.css({width: width}); 3567 if (n > 1) { 3568 width_last = width_parent - width * (n - 1); 3569 $optgroups.eq(n - 1).css({width: width_last}); 3570 } 3571 } 3572 }; 3573 3574 if (options.equalizeHeight || options.equalizeWidth) { 3575 hook.after(this, 'positionDropdown', equalizeSizes); 3576 hook.after(this, 'refreshOptions', equalizeSizes); 3577 } 3578 3579 3580 }); 3581 3582 Selectize.define('remove_button', function(options) { 3583 if (this.settings.mode === 'single') return; 3584 3585 options = $.extend({ 3586 label : '×', 3587 title : 'Remove', 3588 className : 'remove', 3589 append : true 3590 }, options); 3591 3592 var self = this; 3593 var html = '<a href="javascript:void(0)" class="' + options.className + '" tabindex="-1" title="' + escape_html(options.title) + '">' + options.label + '</a>'; 3594 3595 /** 3596 * Appends an element as a child (with raw HTML). 3597 * 3598 * @param {string} html_container 3599 * @param {string} html_element 3600 * @return {string} 3601 */ 3602 var append = function(html_container, html_element) { 3603 var pos = html_container.search(/(<\/[^>]+>\s*)$/); 3604 return html_container.substring(0, pos) + html_element + html_container.substring(pos); 3605 }; 3606 3607 this.setup = (function() { 3608 var original = self.setup; 3609 return function() { 3610 // override the item rendering method to add the button to each 3611 if (options.append) { 3612 var render_item = self.settings.render.item; 3613 self.settings.render.item = function(data) { 3614 return append(render_item.apply(this, arguments), html); 3615 }; 3616 } 3617 3618 original.apply(this, arguments); 3619 3620 // add event listener 3621 this.$control.on('click', '.' + options.className, function(e) { 3622 e.preventDefault(); 3623 if (self.isLocked) return; 3624 3625 var $item = $(e.currentTarget).parent(); 3626 self.setActiveItem($item); 3627 if (self.deleteSelection()) { 3628 self.setCaret(self.items.length); 3629 } 3630 }); 3631 3632 }; 3633 })(); 3634 3635 }); 3636 3637 Selectize.define('restore_on_backspace', function(options) { 3638 var self = this; 3639 3640 options.text = options.text || function(option) { 3641 return option[this.settings.labelField]; 3642 }; 3643 3644 this.onKeyDown = (function() { 3645 var original = self.onKeyDown; 3646 return function(e) { 3647 var index, option; 3648 if (e.keyCode === KEY_BACKSPACE && this.$control_input.val() === '' && !this.$activeItems.length) { 3649 index = this.caretPos - 1; 3650 if (index >= 0 && index < this.items.length) { 3651 option = this.options[this.items[index]]; 3652 if (this.deleteSelection(e)) { 3653 this.setTextboxValue(options.text.apply(this, [option])); 3654 this.refreshOptions(true); 3655 } 3656 e.preventDefault(); 3657 return; 3658 } 3659 } 3660 return original.apply(this, arguments); 3661 }; 3662 })(); 3663 }); 3664 3665 3666 return Selectize; 3667}));
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.