source: public/assets/js/vendors/selectize.min.js

Last change on this file was 0924b6c, checked in by Özkan İliyaz <iliyaz_96@…>, 4 years ago

initial commit

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