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