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