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