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