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