PluginProbe
Social Media Auto Poster – Schedule & Publish to Buffer / trunk
Social Media Auto Poster – Schedule & Publish to Buffer vtrunk
6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.2 6.1.1 6.1.0 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 All 125 releases
wp-to-buffer / lib / shared / js / selectize.js

selectize.js in Social Media Auto Poster – Schedule & Publish to Buffer trunk, at lib/shared/js/selectize.js

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