PluginProbe ʕ •ᴥ•ʔ
Gallery : FooGallery / 3.1.36
Gallery : FooGallery v3.1.36
3.3.0 3.1.31 3.1.32 3.1.34 3.1.36 3.2.0 3.2.6 trunk 1.10.3 2.0.24 2.1.34 2.2.44 2.3.3 2.4.32 3.0.6 3.1.11 3.1.12 3.1.13 3.1.20 3.1.25 3.1.26 3.1.26.1 3.1.26.2
foogallery / lib / selectize / selectize.js
foogallery / lib / selectize Last commit date
foogallery.selectize.js 2 months ago selectize.css 2 months ago selectize.js 2 months ago
selectize.js
3847 lines
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.4)
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 if (node.nodeType === 3) {
686 var pos = node.data.search(regex);
687 if (pos >= 0 && node.data.length > 0) {
688 var match = node.data.match(regex);
689 var spannode = document.createElement('span');
690 spannode.className = 'highlight';
691 var middlebit = node.splitText(pos);
692 var endbit = middlebit.splitText(match[0].length);
693 var middleclone = middlebit.cloneNode(true);
694 spannode.appendChild(middleclone);
695 middlebit.parentNode.replaceChild(spannode, middlebit);
696 skip = 1;
697 }
698 } else if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
699 for (var i = 0; i < node.childNodes.length; ++i) {
700 i += highlight(node.childNodes[i]);
701 }
702 }
703 return skip;
704 };
705
706 return $element.each(function() {
707 highlight(this);
708 });
709 };
710
711 /**
712 * removeHighlight fn copied from highlight v5 and
713 * edited to remove with() and pass js strict mode
714 */
715 $.fn.removeHighlight = function() {
716 return this.find("span.highlight").each(function() {
717 this.parentNode.firstChild.nodeName;
718 var parent = this.parentNode;
719 parent.replaceChild(this.firstChild, this);
720 parent.normalize();
721 }).end();
722 };
723
724
725 var MicroEvent = function() {};
726 MicroEvent.prototype = {
727 on: function(event, fct){
728 this._events = this._events || {};
729 this._events[event] = this._events[event] || [];
730 this._events[event].push(fct);
731 },
732 off: function(event, fct){
733 var n = arguments.length;
734 if (n === 0) return delete this._events;
735 if (n === 1) return delete this._events[event];
736
737 this._events = this._events || {};
738 if (event in this._events === false) return;
739 this._events[event].splice(this._events[event].indexOf(fct), 1);
740 },
741 trigger: function(event /* , args... */){
742 this._events = this._events || {};
743 if (event in this._events === false) return;
744 for (var i = 0; i < this._events[event].length; i++){
745 this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1));
746 }
747 }
748 };
749
750 /**
751 * Mixin will delegate all MicroEvent.js function in the destination object.
752 *
753 * - MicroEvent.mixin(Foobar) will make Foobar able to use MicroEvent
754 *
755 * @param {object} the object which will support MicroEvent
756 */
757 MicroEvent.mixin = function(destObject){
758 var props = ['on', 'off', 'trigger'];
759 for (var i = 0; i < props.length; i++){
760 destObject.prototype[props[i]] = MicroEvent.prototype[props[i]];
761 }
762 };
763
764 var IS_MAC = /Mac/.test(navigator.userAgent);
765
766 var KEY_A = 65;
767 var KEY_COMMA = 188;
768 var KEY_RETURN = 13;
769 var KEY_ESC = 27;
770 var KEY_LEFT = 37;
771 var KEY_UP = 38;
772 var KEY_P = 80;
773 var KEY_RIGHT = 39;
774 var KEY_DOWN = 40;
775 var KEY_N = 78;
776 var KEY_BACKSPACE = 8;
777 var KEY_DELETE = 46;
778 var KEY_SHIFT = 16;
779 var KEY_CMD = IS_MAC ? 91 : 17;
780 var KEY_CTRL = IS_MAC ? 18 : 17;
781 var KEY_TAB = 9;
782
783 var TAG_SELECT = 1;
784 var TAG_INPUT = 2;
785
786 // for now, android support in general is too spotty to support validity
787 var SUPPORTS_VALIDITY_API = !/android/i.test(window.navigator.userAgent) && !!document.createElement('input').validity;
788
789
790 var isset = function(object) {
791 return typeof object !== 'undefined';
792 };
793
794 var trim = function(str) {
795 str = (str === null || typeof str === 'undefined') ? '' : (str + '');
796 return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
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 var $test = $('<test>').css({
1027 position: 'absolute',
1028 top: -99999,
1029 left: -99999,
1030 width: 'auto',
1031 padding: 0,
1032 whiteSpace: 'pre'
1033 }).text(str).appendTo('body');
1034
1035 transferStyles($parent, $test, [
1036 'letterSpacing',
1037 'fontSize',
1038 'fontFamily',
1039 'fontWeight',
1040 'textTransform'
1041 ]);
1042
1043 var width = $test.width();
1044 $test.remove();
1045
1046 return width;
1047 };
1048
1049 /**
1050 * Sets up an input to grow horizontally as the user
1051 * types. If the value is changed manually, you can
1052 * trigger the "update" handler to resize:
1053 *
1054 * $input.trigger('update');
1055 *
1056 * @param {object} $input
1057 */
1058 var autoGrow = function($input) {
1059 var currentWidth = null;
1060
1061 var update = function(e, options) {
1062 var value, keyCode, printable, placeholder, width;
1063 var shift, character, selection;
1064 e = e || window.event || {};
1065 options = options || {};
1066
1067 if (e.metaKey || e.altKey) return;
1068 if (!options.force && $input.data('grow') === false) return;
1069
1070 value = $input.val();
1071 if (e.type && e.type.toLowerCase() === 'keydown') {
1072 keyCode = e.keyCode;
1073 printable = (
1074 (keyCode >= 97 && keyCode <= 122) || // a-z
1075 (keyCode >= 65 && keyCode <= 90) || // A-Z
1076 (keyCode >= 48 && keyCode <= 57) || // 0-9
1077 keyCode === 32 // space
1078 );
1079
1080 if (keyCode === KEY_DELETE || keyCode === KEY_BACKSPACE) {
1081 selection = getSelection($input[0]);
1082 if (selection.length) {
1083 value = value.substring(0, selection.start) + value.substring(selection.start + selection.length);
1084 } else if (keyCode === KEY_BACKSPACE && selection.start) {
1085 value = value.substring(0, selection.start - 1) + value.substring(selection.start + 1);
1086 } else if (keyCode === KEY_DELETE && typeof selection.start !== 'undefined') {
1087 value = value.substring(0, selection.start) + value.substring(selection.start + 1);
1088 }
1089 } else if (printable) {
1090 shift = e.shiftKey;
1091 character = String.fromCharCode(e.keyCode);
1092 if (shift) character = character.toUpperCase();
1093 else character = character.toLowerCase();
1094 value += character;
1095 }
1096 }
1097
1098 placeholder = $input.attr('placeholder');
1099 if (!value && placeholder) {
1100 value = placeholder;
1101 }
1102
1103 width = measureString(value, $input) + 4;
1104 if (width !== currentWidth) {
1105 currentWidth = width;
1106 $input.width(width);
1107 $input.triggerHandler('resize');
1108 }
1109 };
1110
1111 $input.on('keydown keyup update blur', update);
1112 update();
1113 };
1114
1115 var domToString = function(d) {
1116 var tmp = document.createElement('div');
1117
1118 tmp.appendChild(d.cloneNode(true));
1119
1120 return tmp.innerHTML;
1121 };
1122
1123 var logError = function(message, options){
1124 if(!options) options = {};
1125 var component = "Selectize";
1126
1127 console.error(component + ": " + message)
1128
1129 if(options.explanation){
1130 // console.group is undefined in <IE11
1131 if(console.group) console.group();
1132 console.error(options.explanation);
1133 if(console.group) console.groupEnd();
1134 }
1135 }
1136
1137
1138 var Selectize = function($input, settings) {
1139 var key, i, n, dir, input, self = this;
1140 input = $input[0];
1141 input.selectize = self;
1142
1143 // detect rtl environment
1144 var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null);
1145 dir = computedStyle ? computedStyle.getPropertyValue('direction') : input.currentStyle && input.currentStyle.direction;
1146 dir = dir || $input.parents('[dir]:first').attr('dir') || '';
1147
1148 // setup default state
1149 $.extend(self, {
1150 order : 0,
1151 settings : settings,
1152 $input : $input,
1153 tabIndex : $input.attr('tabindex') || '',
1154 tagType : input.tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT,
1155 rtl : /rtl/i.test(dir),
1156
1157 eventNS : '.selectize' + (++Selectize.count),
1158 highlightedValue : null,
1159 isOpen : false,
1160 isDisabled : false,
1161 isRequired : $input.is('[required]'),
1162 isInvalid : false,
1163 isLocked : false,
1164 isFocused : false,
1165 isInputHidden : false,
1166 isSetup : false,
1167 isShiftDown : false,
1168 isCmdDown : false,
1169 isCtrlDown : false,
1170 ignoreFocus : false,
1171 ignoreBlur : false,
1172 ignoreHover : false,
1173 hasOptions : false,
1174 currentResults : null,
1175 lastValue : '',
1176 caretPos : 0,
1177 loading : 0,
1178 loadedSearches : {},
1179
1180 $activeOption : null,
1181 $activeItems : [],
1182
1183 optgroups : {},
1184 options : {},
1185 userOptions : {},
1186 items : [],
1187 renderCache : {},
1188 onSearchChange : settings.loadThrottle === null ? self.onSearchChange : debounce(self.onSearchChange, settings.loadThrottle)
1189 });
1190
1191 // search system
1192 self.sifter = new Sifter(this.options, {diacritics: settings.diacritics});
1193
1194 // build options table
1195 if (self.settings.options) {
1196 for (i = 0, n = self.settings.options.length; i < n; i++) {
1197 self.registerOption(self.settings.options[i]);
1198 }
1199 delete self.settings.options;
1200 }
1201
1202 // build optgroup table
1203 if (self.settings.optgroups) {
1204 for (i = 0, n = self.settings.optgroups.length; i < n; i++) {
1205 self.registerOptionGroup(self.settings.optgroups[i]);
1206 }
1207 delete self.settings.optgroups;
1208 }
1209
1210 // option-dependent defaults
1211 self.settings.mode = self.settings.mode || (self.settings.maxItems === 1 ? 'single' : 'multi');
1212 if (typeof self.settings.hideSelected !== 'boolean') {
1213 self.settings.hideSelected = self.settings.mode === 'multi';
1214 }
1215
1216 self.initializePlugins(self.settings.plugins);
1217 self.setupCallbacks();
1218 self.setupTemplates();
1219 self.setup();
1220 };
1221
1222 // mixins
1223 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1224
1225 MicroEvent.mixin(Selectize);
1226
1227 if(typeof MicroPlugin !== "undefined"){
1228 MicroPlugin.mixin(Selectize);
1229 }else{
1230 logError("Dependency MicroPlugin is missing",
1231 {explanation:
1232 "Make sure you either: (1) are using the \"standalone\" "+
1233 "version of Selectize, or (2) require MicroPlugin before you "+
1234 "load Selectize."}
1235 );
1236 }
1237
1238
1239 // methods
1240 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1241
1242 $.extend(Selectize.prototype, {
1243
1244 /**
1245 * Creates all elements and sets up event bindings.
1246 */
1247 setup: function() {
1248 var self = this;
1249 var settings = self.settings;
1250 var eventNS = self.eventNS;
1251 var $window = $(window);
1252 var $document = $(document);
1253 var $input = self.$input;
1254
1255 var $wrapper;
1256 var $control;
1257 var $control_input;
1258 var $dropdown;
1259 var $dropdown_content;
1260 var $dropdown_parent;
1261 var inputMode;
1262 var timeout_blur;
1263 var timeout_focus;
1264 var classes;
1265 var classes_plugins;
1266 var inputId;
1267
1268 inputMode = self.settings.mode;
1269 classes = $input.attr('class') || '';
1270
1271 $wrapper = $('<div>').addClass(settings.wrapperClass).addClass(classes).addClass(inputMode);
1272 $control = $('<div>').addClass(settings.inputClass).addClass('items').appendTo($wrapper);
1273 $control_input = $('<input type="text" autocomplete="off" />').appendTo($control).attr('tabindex', $input.is(':disabled') ? '-1' : self.tabIndex);
1274 $dropdown_parent = $(settings.dropdownParent || $wrapper);
1275 $dropdown = $('<div>').addClass(settings.dropdownClass).addClass(inputMode).hide().appendTo($dropdown_parent);
1276 $dropdown_content = $('<div>').addClass(settings.dropdownContentClass).appendTo($dropdown);
1277
1278 if(inputId = $input.attr('id')) {
1279 $control_input.attr('id', inputId + '-selectized');
1280 $("label[for='"+inputId+"']").attr('for', inputId + '-selectized');
1281 }
1282
1283 if(self.settings.copyClassesToDropdown) {
1284 $dropdown.addClass(classes);
1285 }
1286
1287 $wrapper.css({
1288 width: $input[0].style.width
1289 });
1290
1291 if (self.plugins.names.length) {
1292 classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-');
1293 $wrapper.addClass(classes_plugins);
1294 $dropdown.addClass(classes_plugins);
1295 }
1296
1297 if ((settings.maxItems === null || settings.maxItems > 1) && self.tagType === TAG_SELECT) {
1298 $input.attr('multiple', 'multiple');
1299 }
1300
1301 if (self.settings.placeholder) {
1302 $control_input.attr('placeholder', settings.placeholder);
1303 }
1304
1305 // if splitOn was not passed in, construct it from the delimiter to allow pasting universally
1306 if (!self.settings.splitOn && self.settings.delimiter) {
1307 var delimiterEscaped = self.settings.delimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1308 self.settings.splitOn = new RegExp('\\s*' + delimiterEscaped + '+\\s*');
1309 }
1310
1311 if ($input.attr('autocorrect')) {
1312 $control_input.attr('autocorrect', $input.attr('autocorrect'));
1313 }
1314
1315 if ($input.attr('autocapitalize')) {
1316 $control_input.attr('autocapitalize', $input.attr('autocapitalize'));
1317 }
1318
1319 self.$wrapper = $wrapper;
1320 self.$control = $control;
1321 self.$control_input = $control_input;
1322 self.$dropdown = $dropdown;
1323 self.$dropdown_content = $dropdown_content;
1324
1325 $dropdown.on('mouseenter', '[data-selectable]', function() { return self.onOptionHover.apply(self, arguments); });
1326 $dropdown.on('mousedown click', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); });
1327 watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); });
1328 autoGrow($control_input);
1329
1330 $control.on({
1331 mousedown : function() { return self.onMouseDown.apply(self, arguments); },
1332 click : function() { return self.onClick.apply(self, arguments); }
1333 });
1334
1335 $control_input.on({
1336 mousedown : function(e) { e.stopPropagation(); },
1337 keydown : function() { return self.onKeyDown.apply(self, arguments); },
1338 keyup : function() { return self.onKeyUp.apply(self, arguments); },
1339 keypress : function() { return self.onKeyPress.apply(self, arguments); },
1340 resize : function() { self.positionDropdown.apply(self, []); },
1341 blur : function() { return self.onBlur.apply(self, arguments); },
1342 focus : function() { self.ignoreBlur = false; return self.onFocus.apply(self, arguments); },
1343 paste : function() { return self.onPaste.apply(self, arguments); }
1344 });
1345
1346 $document.on('keydown' + eventNS, function(e) {
1347 self.isCmdDown = e[IS_MAC ? 'metaKey' : 'ctrlKey'];
1348 self.isCtrlDown = e[IS_MAC ? 'altKey' : 'ctrlKey'];
1349 self.isShiftDown = e.shiftKey;
1350 });
1351
1352 $document.on('keyup' + eventNS, function(e) {
1353 if (e.keyCode === KEY_CTRL) self.isCtrlDown = false;
1354 if (e.keyCode === KEY_SHIFT) self.isShiftDown = false;
1355 if (e.keyCode === KEY_CMD) self.isCmdDown = false;
1356 });
1357
1358 $document.on('mousedown' + eventNS, function(e) {
1359 if (self.isFocused) {
1360 // prevent events on the dropdown scrollbar from causing the control to blur
1361 if (e.target === self.$dropdown[0] || e.target.parentNode === self.$dropdown[0]) {
1362 return false;
1363 }
1364 // blur on click outside
1365 if (!self.$control.has(e.target).length && e.target !== self.$control[0]) {
1366 self.blur(e.target);
1367 }
1368 }
1369 });
1370
1371 $window.on(['scroll' + eventNS, 'resize' + eventNS].join(' '), function() {
1372 if (self.isOpen) {
1373 self.positionDropdown.apply(self, arguments);
1374 }
1375 });
1376 $window.on('mousemove' + eventNS, function() {
1377 self.ignoreHover = false;
1378 });
1379
1380 // store original children and tab index so that they can be
1381 // restored when the destroy() method is called.
1382 this.revertSettings = {
1383 $children : $input.children().detach(),
1384 tabindex : $input.attr('tabindex')
1385 };
1386
1387 $input.attr('tabindex', -1).hide().after(self.$wrapper);
1388
1389 if (Array.isArray(settings.items)) {
1390 self.setValue(settings.items);
1391 delete settings.items;
1392 }
1393
1394 // feature detect for the validation API
1395 if (SUPPORTS_VALIDITY_API) {
1396 $input.on('invalid' + eventNS, function(e) {
1397 e.preventDefault();
1398 self.isInvalid = true;
1399 self.refreshState();
1400 });
1401 }
1402
1403 self.updateOriginalInput();
1404 self.refreshItems();
1405 self.refreshState();
1406 self.updatePlaceholder();
1407 self.isSetup = true;
1408
1409 if ($input.is(':disabled')) {
1410 self.disable();
1411 }
1412
1413 self.on('change', this.onChange);
1414
1415 $input.data('selectize', self);
1416 $input.addClass('selectized');
1417 self.trigger('initialize');
1418
1419 // preload options
1420 if (settings.preload === true) {
1421 self.onSearchChange('');
1422 }
1423
1424 },
1425
1426 /**
1427 * Sets up default rendering functions.
1428 */
1429 setupTemplates: function() {
1430 var self = this;
1431 var field_label = self.settings.labelField;
1432 var field_optgroup = self.settings.optgroupLabelField;
1433
1434 var templates = {
1435 'optgroup': function(data) {
1436 return '<div class="optgroup">' + data.html + '</div>';
1437 },
1438 'optgroup_header': function(data, escape) {
1439 return '<div class="optgroup-header">' + escape(data[field_optgroup]) + '</div>';
1440 },
1441 'option': function(data, escape) {
1442 return '<div class="option">' + escape(data[field_label]) + '</div>';
1443 },
1444 'item': function(data, escape) {
1445 return '<div class="item">' + escape(data[field_label]) + '</div>';
1446 },
1447 'option_create': function(data, escape) {
1448 return '<div class="create">Add <strong>' + escape(data.input) + '</strong>&hellip;</div>';
1449 }
1450 };
1451
1452 self.settings.render = $.extend({}, templates, self.settings.render);
1453 },
1454
1455 /**
1456 * Maps fired events to callbacks provided
1457 * in the settings used when creating the control.
1458 */
1459 setupCallbacks: function() {
1460 var key, fn, callbacks = {
1461 'initialize' : 'onInitialize',
1462 'change' : 'onChange',
1463 'item_add' : 'onItemAdd',
1464 'item_remove' : 'onItemRemove',
1465 'clear' : 'onClear',
1466 'option_add' : 'onOptionAdd',
1467 'option_remove' : 'onOptionRemove',
1468 'option_clear' : 'onOptionClear',
1469 'optgroup_add' : 'onOptionGroupAdd',
1470 'optgroup_remove' : 'onOptionGroupRemove',
1471 'optgroup_clear' : 'onOptionGroupClear',
1472 'dropdown_open' : 'onDropdownOpen',
1473 'dropdown_close' : 'onDropdownClose',
1474 'type' : 'onType',
1475 'load' : 'onLoad',
1476 'focus' : 'onFocus',
1477 'blur' : 'onBlur'
1478 };
1479
1480 for (key in callbacks) {
1481 if (callbacks.hasOwnProperty(key)) {
1482 fn = this.settings[callbacks[key]];
1483 if (fn) this.on(key, fn);
1484 }
1485 }
1486 },
1487
1488 /**
1489 * Triggered when the main control element
1490 * has a click event.
1491 *
1492 * @param {object} e
1493 * @return {boolean}
1494 */
1495 onClick: function(e) {
1496 var self = this;
1497
1498 // necessary for mobile webkit devices (manual focus triggering
1499 // is ignored unless invoked within a click event)
1500 if (!self.isFocused) {
1501 self.focus();
1502 e.preventDefault();
1503 }
1504 },
1505
1506 /**
1507 * Triggered when the main control element
1508 * has a mouse down event.
1509 *
1510 * @param {object} e
1511 * @return {boolean}
1512 */
1513 onMouseDown: function(e) {
1514 var self = this;
1515 var defaultPrevented = e.isDefaultPrevented();
1516 var $target = $(e.target);
1517
1518 if (self.isFocused) {
1519 // retain focus by preventing native handling. if the
1520 // event target is the input it should not be modified.
1521 // otherwise, text selection within the input won't work.
1522 if (e.target !== self.$control_input[0]) {
1523 if (self.settings.mode === 'single') {
1524 // toggle dropdown
1525 self.isOpen ? self.close() : self.open();
1526 } else if (!defaultPrevented) {
1527 self.setActiveItem(null);
1528 }
1529 return false;
1530 }
1531 } else {
1532 // give control focus
1533 if (!defaultPrevented) {
1534 window.setTimeout(function() {
1535 self.focus();
1536 }, 0);
1537 }
1538 }
1539 },
1540
1541 /**
1542 * Triggered when the value of the control has been changed.
1543 * This should propagate the event to the original DOM
1544 * input / select element.
1545 */
1546 onChange: function() {
1547 this.$input.trigger('change');
1548 },
1549
1550 /**
1551 * Triggered on <input> paste.
1552 *
1553 * @param {object} e
1554 * @returns {boolean}
1555 */
1556 onPaste: function(e) {
1557 var self = this;
1558
1559 if (self.isFull() || self.isInputHidden || self.isLocked) {
1560 e.preventDefault();
1561 return;
1562 }
1563
1564 // If a regex or string is included, this will split the pasted
1565 // input and create Items for each separate value
1566 if (self.settings.splitOn) {
1567
1568 // Wait for pasted text to be recognized in value
1569 setTimeout(function() {
1570 var pastedText = self.$control_input.val();
1571 if(!pastedText.match(self.settings.splitOn)){ return }
1572
1573 var splitInput = trim(pastedText).split(self.settings.splitOn);
1574 for (var i = 0, n = splitInput.length; i < n; i++) {
1575 self.createItem(splitInput[i]);
1576 }
1577 }, 0);
1578 }
1579 },
1580
1581 /**
1582 * Triggered on <input> keypress.
1583 *
1584 * @param {object} e
1585 * @returns {boolean}
1586 */
1587 onKeyPress: function(e) {
1588 if (this.isLocked) return e && e.preventDefault();
1589 var character = String.fromCharCode(e.keyCode || e.which);
1590 if (this.settings.create && this.settings.mode === 'multi' && character === this.settings.delimiter) {
1591 this.createItem();
1592 e.preventDefault();
1593 return false;
1594 }
1595 },
1596
1597 /**
1598 * Triggered on <input> keydown.
1599 *
1600 * @param {object} e
1601 * @returns {boolean}
1602 */
1603 onKeyDown: function(e) {
1604 var isInput = e.target === this.$control_input[0];
1605 var self = this;
1606
1607 if (self.isLocked) {
1608 if (e.keyCode !== KEY_TAB) {
1609 e.preventDefault();
1610 }
1611 return;
1612 }
1613
1614 switch (e.keyCode) {
1615 case KEY_A:
1616 if (self.isCmdDown) {
1617 self.selectAll();
1618 return;
1619 }
1620 break;
1621 case KEY_ESC:
1622 if (self.isOpen) {
1623 e.preventDefault();
1624 e.stopPropagation();
1625 self.close();
1626 }
1627 return;
1628 case KEY_N:
1629 if (!e.ctrlKey || e.altKey) break;
1630 case KEY_DOWN:
1631 if (!self.isOpen && self.hasOptions) {
1632 self.open();
1633 } else if (self.$activeOption) {
1634 self.ignoreHover = true;
1635 var $next = self.getAdjacentOption(self.$activeOption, 1);
1636 if ($next.length) self.setActiveOption($next, true, true);
1637 }
1638 e.preventDefault();
1639 return;
1640 case KEY_P:
1641 if (!e.ctrlKey || e.altKey) break;
1642 case KEY_UP:
1643 if (self.$activeOption) {
1644 self.ignoreHover = true;
1645 var $prev = self.getAdjacentOption(self.$activeOption, -1);
1646 if ($prev.length) self.setActiveOption($prev, true, true);
1647 }
1648 e.preventDefault();
1649 return;
1650 case KEY_RETURN:
1651 if (self.isOpen && self.$activeOption) {
1652 self.onOptionSelect({currentTarget: self.$activeOption});
1653 e.preventDefault();
1654 }
1655 return;
1656 case KEY_LEFT:
1657 self.advanceSelection(-1, e);
1658 return;
1659 case KEY_RIGHT:
1660 self.advanceSelection(1, e);
1661 return;
1662 case KEY_TAB:
1663 if (self.settings.selectOnTab && self.isOpen && self.$activeOption) {
1664 self.onOptionSelect({currentTarget: self.$activeOption});
1665
1666 // Default behaviour is to jump to the next field, we only want this
1667 // if the current field doesn't accept any more entries
1668 if (!self.isFull()) {
1669 e.preventDefault();
1670 }
1671 }
1672 if (self.settings.create && self.createItem()) {
1673 e.preventDefault();
1674 }
1675 return;
1676 case KEY_BACKSPACE:
1677 case KEY_DELETE:
1678 self.deleteSelection(e);
1679 return;
1680 }
1681
1682 if ((self.isFull() || self.isInputHidden) && !(IS_MAC ? e.metaKey : e.ctrlKey)) {
1683 e.preventDefault();
1684 return;
1685 }
1686 },
1687
1688 /**
1689 * Triggered on <input> keyup.
1690 *
1691 * @param {object} e
1692 * @returns {boolean}
1693 */
1694 onKeyUp: function(e) {
1695 var self = this;
1696
1697 if (self.isLocked) return e && e.preventDefault();
1698 var value = self.$control_input.val() || '';
1699 if (self.lastValue !== value) {
1700 self.lastValue = value;
1701 self.onSearchChange(value);
1702 self.refreshOptions();
1703 self.trigger('type', value);
1704 }
1705 },
1706
1707 /**
1708 * Invokes the user-provide option provider / loader.
1709 *
1710 * Note: this function is debounced in the Selectize
1711 * constructor (by `settings.loadThrottle` milliseconds)
1712 *
1713 * @param {string} value
1714 */
1715 onSearchChange: function(value) {
1716 var self = this;
1717 var fn = self.settings.load;
1718 if (!fn) return;
1719 if (self.loadedSearches.hasOwnProperty(value)) return;
1720 self.loadedSearches[value] = true;
1721 self.load(function(callback) {
1722 fn.apply(self, [value, callback]);
1723 });
1724 },
1725
1726 /**
1727 * Triggered on <input> focus.
1728 *
1729 * @param {object} e (optional)
1730 * @returns {boolean}
1731 */
1732 onFocus: function(e) {
1733 var self = this;
1734 var wasFocused = self.isFocused;
1735
1736 if (self.isDisabled) {
1737 self.blur();
1738 e && e.preventDefault();
1739 return false;
1740 }
1741
1742 if (self.ignoreFocus) return;
1743 self.isFocused = true;
1744 if (self.settings.preload === 'focus') self.onSearchChange('');
1745
1746 if (!wasFocused) self.trigger('focus');
1747
1748 if (!self.$activeItems.length) {
1749 self.showInput();
1750 self.setActiveItem(null);
1751 self.refreshOptions(!!self.settings.openOnFocus);
1752 }
1753
1754 self.refreshState();
1755 },
1756
1757 /**
1758 * Triggered on <input> blur.
1759 *
1760 * @param {object} e
1761 * @param {Element} dest
1762 */
1763 onBlur: function(e, dest) {
1764 var self = this;
1765 if (!self.isFocused) return;
1766 self.isFocused = false;
1767
1768 if (self.ignoreFocus) {
1769 return;
1770 } else if (!self.ignoreBlur && document.activeElement === self.$dropdown_content[0]) {
1771 // necessary to prevent IE closing the dropdown when the scrollbar is clicked
1772 self.ignoreBlur = true;
1773 self.onFocus(e);
1774 return;
1775 }
1776
1777 var deactivate = function() {
1778 self.close();
1779 self.setTextboxValue('');
1780 self.setActiveItem(null);
1781 self.setActiveOption(null);
1782 self.setCaret(self.items.length);
1783 self.refreshState();
1784
1785 // IE11 bug: element still marked as active
1786 dest && dest.focus && dest.focus();
1787
1788 self.ignoreFocus = false;
1789 self.trigger('blur');
1790 };
1791
1792 self.ignoreFocus = true;
1793 if (self.settings.create && self.settings.createOnBlur) {
1794 self.createItem(null, false, deactivate);
1795 } else {
1796 deactivate();
1797 }
1798 },
1799
1800 /**
1801 * Triggered when the user rolls over
1802 * an option in the autocomplete dropdown menu.
1803 *
1804 * @param {object} e
1805 * @returns {boolean}
1806 */
1807 onOptionHover: function(e) {
1808 if (this.ignoreHover) return;
1809 this.setActiveOption(e.currentTarget, false);
1810 },
1811
1812 /**
1813 * Triggered when the user clicks on an option
1814 * in the autocomplete dropdown menu.
1815 *
1816 * @param {object} e
1817 * @returns {boolean}
1818 */
1819 onOptionSelect: function(e) {
1820 var value, $target, $option, self = this;
1821
1822 if (e.preventDefault) {
1823 e.preventDefault();
1824 e.stopPropagation();
1825 }
1826
1827 $target = $(e.currentTarget);
1828 if ($target.hasClass('create')) {
1829 self.createItem(null, function() {
1830 if (self.settings.closeAfterSelect) {
1831 self.close();
1832 }
1833 });
1834 } else {
1835 value = $target.attr('data-value');
1836 if (typeof value !== 'undefined') {
1837 self.lastQuery = null;
1838 self.setTextboxValue('');
1839 self.addItem(value);
1840 if (self.settings.closeAfterSelect) {
1841 self.close();
1842 } else if (!self.settings.hideSelected && e.type && /mouse/.test(e.type)) {
1843 self.setActiveOption(self.getOption(value));
1844 }
1845 }
1846 }
1847 },
1848
1849 /**
1850 * Triggered when the user clicks on an item
1851 * that has been selected.
1852 *
1853 * @param {object} e
1854 * @returns {boolean}
1855 */
1856 onItemSelect: function(e) {
1857 var self = this;
1858
1859 if (self.isLocked) return;
1860 if (self.settings.mode === 'multi') {
1861 e.preventDefault();
1862 self.setActiveItem(e.currentTarget, e);
1863 }
1864 },
1865
1866 /**
1867 * Invokes the provided method that provides
1868 * results to a callback---which are then added
1869 * as options to the control.
1870 *
1871 * @param {function} fn
1872 */
1873 load: function(fn) {
1874 var self = this;
1875 var $wrapper = self.$wrapper.addClass(self.settings.loadingClass);
1876
1877 self.loading++;
1878 fn.apply(self, [function(results) {
1879 self.loading = Math.max(self.loading - 1, 0);
1880 if (results && results.length) {
1881 self.addOption(results);
1882 self.refreshOptions(self.isFocused && !self.isInputHidden);
1883 }
1884 if (!self.loading) {
1885 $wrapper.removeClass(self.settings.loadingClass);
1886 }
1887 self.trigger('load', results);
1888 }]);
1889 },
1890
1891 /**
1892 * Sets the input field of the control to the specified value.
1893 *
1894 * @param {string} value
1895 */
1896 setTextboxValue: function(value) {
1897 var $input = this.$control_input;
1898 var changed = $input.val() !== value;
1899 if (changed) {
1900 $input.val(value).triggerHandler('update');
1901 this.lastValue = value;
1902 }
1903 },
1904
1905 /**
1906 * Returns the value of the control. If multiple items
1907 * can be selected (e.g. <select multiple>), this returns
1908 * an array. If only one item can be selected, this
1909 * returns a string.
1910 *
1911 * @returns {mixed}
1912 */
1913 getValue: function() {
1914 if (this.tagType === TAG_SELECT && this.$input.attr('multiple')) {
1915 return this.items;
1916 } else {
1917 return this.items.join(this.settings.delimiter);
1918 }
1919 },
1920
1921 /**
1922 * Resets the selected items to the given value.
1923 *
1924 * @param {mixed} value
1925 */
1926 setValue: function(value, silent) {
1927 var events = silent ? [] : ['change'];
1928
1929 debounce_events(this, events, function() {
1930 this.clear(silent);
1931 this.addItems(value, silent);
1932 });
1933 },
1934
1935 /**
1936 * Sets the selected item.
1937 *
1938 * @param {object} $item
1939 * @param {object} e (optional)
1940 */
1941 setActiveItem: function($item, e) {
1942 var self = this;
1943 var eventName;
1944 var i, idx, begin, end, item, swap;
1945 var $last;
1946
1947 if (self.settings.mode === 'single') return;
1948 $item = $($item);
1949
1950 // clear the active selection
1951 if (!$item.length) {
1952 $(self.$activeItems).removeClass('active');
1953 self.$activeItems = [];
1954 if (self.isFocused) {
1955 self.showInput();
1956 }
1957 return;
1958 }
1959
1960 // modify selection
1961 eventName = e && e.type.toLowerCase();
1962
1963 if (eventName === 'mousedown' && self.isShiftDown && self.$activeItems.length) {
1964 $last = self.$control.children('.active:last');
1965 begin = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$last[0]]);
1966 end = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$item[0]]);
1967 if (begin > end) {
1968 swap = begin;
1969 begin = end;
1970 end = swap;
1971 }
1972 for (i = begin; i <= end; i++) {
1973 item = self.$control[0].childNodes[i];
1974 if (self.$activeItems.indexOf(item) === -1) {
1975 $(item).addClass('active');
1976 self.$activeItems.push(item);
1977 }
1978 }
1979 e.preventDefault();
1980 } else if ((eventName === 'mousedown' && self.isCtrlDown) || (eventName === 'keydown' && this.isShiftDown)) {
1981 if ($item.hasClass('active')) {
1982 idx = self.$activeItems.indexOf($item[0]);
1983 self.$activeItems.splice(idx, 1);
1984 $item.removeClass('active');
1985 } else {
1986 self.$activeItems.push($item.addClass('active')[0]);
1987 }
1988 } else {
1989 $(self.$activeItems).removeClass('active');
1990 self.$activeItems = [$item.addClass('active')[0]];
1991 }
1992
1993 // ensure control has focus
1994 self.hideInput();
1995 if (!this.isFocused) {
1996 self.focus();
1997 }
1998 },
1999
2000 /**
2001 * Sets the selected item in the dropdown menu
2002 * of available options.
2003 *
2004 * @param {object} $object
2005 * @param {boolean} scroll
2006 * @param {boolean} animate
2007 */
2008 setActiveOption: function($option, scroll, animate) {
2009 var height_menu, height_item, y;
2010 var scroll_top, scroll_bottom;
2011 var self = this;
2012
2013 if (self.$activeOption) self.$activeOption.removeClass('active');
2014 self.$activeOption = null;
2015
2016 $option = $($option);
2017 if (!$option.length) return;
2018
2019 self.$activeOption = $option.addClass('active');
2020
2021 if (scroll || !isset(scroll)) {
2022
2023 height_menu = self.$dropdown_content.height();
2024 height_item = self.$activeOption.outerHeight(true);
2025 scroll = self.$dropdown_content.scrollTop() || 0;
2026 y = self.$activeOption.offset().top - self.$dropdown_content.offset().top + scroll;
2027 scroll_top = y;
2028 scroll_bottom = y - height_menu + height_item;
2029
2030 if (y + height_item > height_menu + scroll) {
2031 self.$dropdown_content.stop().animate({scrollTop: scroll_bottom}, animate ? self.settings.scrollDuration : 0);
2032 } else if (y < scroll) {
2033 self.$dropdown_content.stop().animate({scrollTop: scroll_top}, animate ? self.settings.scrollDuration : 0);
2034 }
2035
2036 }
2037 },
2038
2039 /**
2040 * Selects all items (CTRL + A).
2041 */
2042 selectAll: function() {
2043 var self = this;
2044 if (self.settings.mode === 'single') return;
2045
2046 self.$activeItems = Array.prototype.slice.apply(self.$control.children(':not(input)').addClass('active'));
2047 if (self.$activeItems.length) {
2048 self.hideInput();
2049 self.close();
2050 }
2051 self.focus();
2052 },
2053
2054 /**
2055 * Hides the input element out of view, while
2056 * retaining its focus.
2057 */
2058 hideInput: function() {
2059 var self = this;
2060
2061 self.setTextboxValue('');
2062 self.$control_input.css({opacity: 0, position: 'absolute', left: self.rtl ? 10000 : -10000});
2063 self.isInputHidden = true;
2064 },
2065
2066 /**
2067 * Restores input visibility.
2068 */
2069 showInput: function() {
2070 this.$control_input.css({opacity: 1, position: 'relative', left: 0});
2071 this.isInputHidden = false;
2072 },
2073
2074 /**
2075 * Gives the control focus.
2076 */
2077 focus: function() {
2078 var self = this;
2079 if (self.isDisabled) return;
2080
2081 self.ignoreFocus = true;
2082 self.$control_input[0].focus();
2083 window.setTimeout(function() {
2084 self.ignoreFocus = false;
2085 self.onFocus();
2086 }, 0);
2087 },
2088
2089 /**
2090 * Forces the control out of focus.
2091 *
2092 * @param {Element} dest
2093 */
2094 blur: function(dest) {
2095 this.$control_input[0].blur();
2096 this.onBlur(null, dest);
2097 },
2098
2099 /**
2100 * Returns a function that scores an object
2101 * to show how good of a match it is to the
2102 * provided query.
2103 *
2104 * @param {string} query
2105 * @param {object} options
2106 * @return {function}
2107 */
2108 getScoreFunction: function(query) {
2109 return this.sifter.getScoreFunction(query, this.getSearchOptions());
2110 },
2111
2112 /**
2113 * Returns search options for sifter (the system
2114 * for scoring and sorting results).
2115 *
2116 * @see https://github.com/brianreavis/sifter.js
2117 * @return {object}
2118 */
2119 getSearchOptions: function() {
2120 var settings = this.settings;
2121 var sort = settings.sortField;
2122 if (typeof sort === 'string') {
2123 sort = [{field: sort}];
2124 }
2125
2126 return {
2127 fields : settings.searchField,
2128 conjunction : settings.searchConjunction,
2129 sort : sort
2130 };
2131 },
2132
2133 /**
2134 * Searches through available options and returns
2135 * a sorted array of matches.
2136 *
2137 * Returns an object containing:
2138 *
2139 * - query {string}
2140 * - tokens {array}
2141 * - total {int}
2142 * - items {array}
2143 *
2144 * @param {string} query
2145 * @returns {object}
2146 */
2147 search: function(query) {
2148 var i, value, score, result, calculateScore;
2149 var self = this;
2150 var settings = self.settings;
2151 var options = this.getSearchOptions();
2152
2153 // validate user-provided result scoring function
2154 if (settings.score) {
2155 calculateScore = self.settings.score.apply(this, [query]);
2156 if (typeof calculateScore !== 'function') {
2157 throw new Error('Selectize "score" setting must be a function that returns a function');
2158 }
2159 }
2160
2161 // perform search
2162 if (query !== self.lastQuery) {
2163 self.lastQuery = query;
2164 result = self.sifter.search(query, $.extend(options, {score: calculateScore}));
2165 self.currentResults = result;
2166 } else {
2167 result = $.extend(true, {}, self.currentResults);
2168 }
2169
2170 // filter out selected items
2171 if (settings.hideSelected) {
2172 for (i = result.items.length - 1; i >= 0; i--) {
2173 if (self.items.indexOf(hash_key(result.items[i].id)) !== -1) {
2174 result.items.splice(i, 1);
2175 }
2176 }
2177 }
2178
2179 return result;
2180 },
2181
2182 /**
2183 * Refreshes the list of available options shown
2184 * in the autocomplete dropdown menu.
2185 *
2186 * @param {boolean} triggerDropdown
2187 */
2188 refreshOptions: function(triggerDropdown) {
2189 var i, j, k, n, groups, groups_order, option, option_html, optgroup, optgroups, html, html_children, has_create_option;
2190 var $active, $active_before, $create;
2191
2192 if (typeof triggerDropdown === 'undefined') {
2193 triggerDropdown = true;
2194 }
2195
2196 var self = this;
2197 var query = trim(self.$control_input.val());
2198 var results = self.search(query);
2199 var $dropdown_content = self.$dropdown_content;
2200 var active_before = self.$activeOption && hash_key(self.$activeOption.attr('data-value'));
2201
2202 // build markup
2203 n = results.items.length;
2204 if (typeof self.settings.maxOptions === 'number') {
2205 n = Math.min(n, self.settings.maxOptions);
2206 }
2207
2208 // render and group available options individually
2209 groups = {};
2210 groups_order = [];
2211
2212 for (i = 0; i < n; i++) {
2213 option = self.options[results.items[i].id];
2214 option_html = self.render('option', option);
2215 optgroup = option[self.settings.optgroupField] || '';
2216 optgroups = Array.isArray(optgroup) ? optgroup : [optgroup];
2217
2218 for (j = 0, k = optgroups && optgroups.length; j < k; j++) {
2219 optgroup = optgroups[j];
2220 if (!self.optgroups.hasOwnProperty(optgroup)) {
2221 optgroup = '';
2222 }
2223 if (!groups.hasOwnProperty(optgroup)) {
2224 groups[optgroup] = document.createDocumentFragment();
2225 groups_order.push(optgroup);
2226 }
2227 groups[optgroup].appendChild(option_html);
2228 }
2229 }
2230
2231 // sort optgroups
2232 if (this.settings.lockOptgroupOrder) {
2233 groups_order.sort(function(a, b) {
2234 var a_order = self.optgroups[a].$order || 0;
2235 var b_order = self.optgroups[b].$order || 0;
2236 return a_order - b_order;
2237 });
2238 }
2239
2240 // render optgroup headers & join groups
2241 html = document.createDocumentFragment();
2242 for (i = 0, n = groups_order.length; i < n; i++) {
2243 optgroup = groups_order[i];
2244 if (self.optgroups.hasOwnProperty(optgroup) && groups[optgroup].childNodes.length) {
2245 // render the optgroup header and options within it,
2246 // then pass it to the wrapper template
2247 html_children = document.createDocumentFragment();
2248 html_children.appendChild(self.render('optgroup_header', self.optgroups[optgroup]));
2249 html_children.appendChild(groups[optgroup]);
2250
2251 html.appendChild(self.render('optgroup', $.extend({}, self.optgroups[optgroup], {
2252 html: domToString(html_children),
2253 dom: html_children
2254 })));
2255 } else {
2256 html.appendChild(groups[optgroup]);
2257 }
2258 }
2259
2260 $dropdown_content.html(html);
2261
2262 // highlight matching terms inline
2263 if (self.settings.highlight && results.query.length && results.tokens.length) {
2264 $dropdown_content.removeHighlight();
2265 for (i = 0, n = results.tokens.length; i < n; i++) {
2266 highlight($dropdown_content, results.tokens[i].regex);
2267 }
2268 }
2269
2270 // add "selected" class to selected options
2271 if (!self.settings.hideSelected) {
2272 for (i = 0, n = self.items.length; i < n; i++) {
2273 self.getOption(self.items[i]).addClass('selected');
2274 }
2275 }
2276
2277 // add create option
2278 has_create_option = self.canCreate(query);
2279 if (has_create_option) {
2280 $dropdown_content.prepend(self.render('option_create', {input: query}));
2281 $create = $($dropdown_content[0].childNodes[0]);
2282 }
2283
2284 // activate
2285 self.hasOptions = results.items.length > 0 || has_create_option;
2286 if (self.hasOptions) {
2287 if (results.items.length > 0) {
2288 $active_before = active_before && self.getOption(active_before);
2289 if ($active_before && $active_before.length) {
2290 $active = $active_before;
2291 } else if (self.settings.mode === 'single' && self.items.length) {
2292 $active = self.getOption(self.items[0]);
2293 }
2294 if (!$active || !$active.length) {
2295 if ($create && !self.settings.addPrecedence) {
2296 $active = self.getAdjacentOption($create, 1);
2297 } else {
2298 $active = $dropdown_content.find('[data-selectable]:first');
2299 }
2300 }
2301 } else {
2302 $active = $create;
2303 }
2304 self.setActiveOption($active);
2305 if (triggerDropdown && !self.isOpen) { self.open(); }
2306 } else {
2307 self.setActiveOption(null);
2308 if (triggerDropdown && self.isOpen) { self.close(); }
2309 }
2310 },
2311
2312 /**
2313 * Adds an available option. If it already exists,
2314 * nothing will happen. Note: this does not refresh
2315 * the options list dropdown (use `refreshOptions`
2316 * for that).
2317 *
2318 * Usage:
2319 *
2320 * this.addOption(data)
2321 *
2322 * @param {object|array} data
2323 */
2324 addOption: function(data) {
2325 var i, n, value, self = this;
2326
2327 if (Array.isArray(data)) {
2328 for (i = 0, n = data.length; i < n; i++) {
2329 self.addOption(data[i]);
2330 }
2331 return;
2332 }
2333
2334 if (value = self.registerOption(data)) {
2335 self.userOptions[value] = true;
2336 self.lastQuery = null;
2337 self.trigger('option_add', value, data);
2338 }
2339 },
2340
2341 /**
2342 * Registers an option to the pool of options.
2343 *
2344 * @param {object} data
2345 * @return {boolean|string}
2346 */
2347 registerOption: function(data) {
2348 var key = hash_key(data[this.settings.valueField]);
2349 if (typeof key === 'undefined' || key === null || this.options.hasOwnProperty(key)) return false;
2350 data.$order = data.$order || ++this.order;
2351 this.options[key] = data;
2352 return key;
2353 },
2354
2355 /**
2356 * Registers an option group to the pool of option groups.
2357 *
2358 * @param {object} data
2359 * @return {boolean|string}
2360 */
2361 registerOptionGroup: function(data) {
2362 var key = hash_key(data[this.settings.optgroupValueField]);
2363 if (!key) return false;
2364
2365 data.$order = data.$order || ++this.order;
2366 this.optgroups[key] = data;
2367 return key;
2368 },
2369
2370 /**
2371 * Registers a new optgroup for options
2372 * to be bucketed into.
2373 *
2374 * @param {string} id
2375 * @param {object} data
2376 */
2377 addOptionGroup: function(id, data) {
2378 data[this.settings.optgroupValueField] = id;
2379 if (id = this.registerOptionGroup(data)) {
2380 this.trigger('optgroup_add', id, data);
2381 }
2382 },
2383
2384 /**
2385 * Removes an existing option group.
2386 *
2387 * @param {string} id
2388 */
2389 removeOptionGroup: function(id) {
2390 if (this.optgroups.hasOwnProperty(id)) {
2391 delete this.optgroups[id];
2392 this.renderCache = {};
2393 this.trigger('optgroup_remove', id);
2394 }
2395 },
2396
2397 /**
2398 * Clears all existing option groups.
2399 */
2400 clearOptionGroups: function() {
2401 this.optgroups = {};
2402 this.renderCache = {};
2403 this.trigger('optgroup_clear');
2404 },
2405
2406 /**
2407 * Updates an option available for selection. If
2408 * it is visible in the selected items or options
2409 * dropdown, it will be re-rendered automatically.
2410 *
2411 * @param {string} value
2412 * @param {object} data
2413 */
2414 updateOption: function(value, data) {
2415 var self = this;
2416 var $item, $item_new;
2417 var value_new, index_item, cache_items, cache_options, order_old;
2418
2419 value = hash_key(value);
2420 value_new = hash_key(data[self.settings.valueField]);
2421
2422 // sanity checks
2423 if (value === null) return;
2424 if (!self.options.hasOwnProperty(value)) return;
2425 if (typeof value_new !== 'string') throw new Error('Value must be set in option data');
2426
2427 order_old = self.options[value].$order;
2428
2429 // update references
2430 if (value_new !== value) {
2431 delete self.options[value];
2432 index_item = self.items.indexOf(value);
2433 if (index_item !== -1) {
2434 self.items.splice(index_item, 1, value_new);
2435 }
2436 }
2437 data.$order = data.$order || order_old;
2438 self.options[value_new] = data;
2439
2440 // invalidate render cache
2441 cache_items = self.renderCache['item'];
2442 cache_options = self.renderCache['option'];
2443
2444 if (cache_items) {
2445 delete cache_items[value];
2446 delete cache_items[value_new];
2447 }
2448 if (cache_options) {
2449 delete cache_options[value];
2450 delete cache_options[value_new];
2451 }
2452
2453 // update the item if it's selected
2454 if (self.items.indexOf(value_new) !== -1) {
2455 $item = self.getItem(value);
2456 $item_new = $(self.render('item', data));
2457 if ($item.hasClass('active')) $item_new.addClass('active');
2458 $item.replaceWith($item_new);
2459 }
2460
2461 // invalidate last query because we might have updated the sortField
2462 self.lastQuery = null;
2463
2464 // update dropdown contents
2465 if (self.isOpen) {
2466 self.refreshOptions(false);
2467 }
2468 },
2469
2470 /**
2471 * Removes a single option.
2472 *
2473 * @param {string} value
2474 * @param {boolean} silent
2475 */
2476 removeOption: function(value, silent) {
2477 var self = this;
2478 value = hash_key(value);
2479
2480 var cache_items = self.renderCache['item'];
2481 var cache_options = self.renderCache['option'];
2482 if (cache_items) delete cache_items[value];
2483 if (cache_options) delete cache_options[value];
2484
2485 delete self.userOptions[value];
2486 delete self.options[value];
2487 self.lastQuery = null;
2488 self.trigger('option_remove', value);
2489 self.removeItem(value, silent);
2490 },
2491
2492 /**
2493 * Clears all options.
2494 */
2495 clearOptions: function() {
2496 var self = this;
2497
2498 self.loadedSearches = {};
2499 self.userOptions = {};
2500 self.renderCache = {};
2501 self.options = self.sifter.items = {};
2502 self.lastQuery = null;
2503 self.trigger('option_clear');
2504 self.clear();
2505 },
2506
2507 /**
2508 * Returns the jQuery element of the option
2509 * matching the given value.
2510 *
2511 * @param {string} value
2512 * @returns {object}
2513 */
2514 getOption: function(value) {
2515 return this.getElementWithValue(value, this.$dropdown_content.find('[data-selectable]'));
2516 },
2517
2518 /**
2519 * Returns the jQuery element of the next or
2520 * previous selectable option.
2521 *
2522 * @param {object} $option
2523 * @param {int} direction can be 1 for next or -1 for previous
2524 * @return {object}
2525 */
2526 getAdjacentOption: function($option, direction) {
2527 var $options = this.$dropdown.find('[data-selectable]');
2528 var index = $options.index($option) + direction;
2529
2530 return index >= 0 && index < $options.length ? $options.eq(index) : $();
2531 },
2532
2533 /**
2534 * Finds the first element with a "data-value" attribute
2535 * that matches the given value.
2536 *
2537 * @param {mixed} value
2538 * @param {object} $els
2539 * @return {object}
2540 */
2541 getElementWithValue: function(value, $els) {
2542 value = hash_key(value);
2543
2544 if (typeof value !== 'undefined' && value !== null) {
2545 for (var i = 0, n = $els.length; i < n; i++) {
2546 if ($els[i].getAttribute('data-value') === value) {
2547 return $($els[i]);
2548 }
2549 }
2550 }
2551
2552 return $();
2553 },
2554
2555 /**
2556 * Returns the jQuery element of the item
2557 * matching the given value.
2558 *
2559 * @param {string} value
2560 * @returns {object}
2561 */
2562 getItem: function(value) {
2563 return this.getElementWithValue(value, this.$control.children());
2564 },
2565
2566 /**
2567 * "Selects" multiple items at once. Adds them to the list
2568 * at the current caret position.
2569 *
2570 * @param {string} value
2571 * @param {boolean} silent
2572 */
2573 addItems: function(values, silent) {
2574 var items = Array.isArray(values) ? values : [values];
2575 for (var i = 0, n = items.length; i < n; i++) {
2576 this.isPending = (i < n - 1);
2577 this.addItem(items[i], silent);
2578 }
2579 },
2580
2581 /**
2582 * "Selects" an item. Adds it to the list
2583 * at the current caret position.
2584 *
2585 * @param {string} value
2586 * @param {boolean} silent
2587 */
2588 addItem: function(value, silent) {
2589 var events = silent ? [] : ['change'];
2590
2591 debounce_events(this, events, function() {
2592 var $item, $option, $options;
2593 var self = this;
2594 var inputMode = self.settings.mode;
2595 var i, active, value_next, wasFull;
2596 value = hash_key(value);
2597
2598 if (self.items.indexOf(value) !== -1) {
2599 if (inputMode === 'single') self.close();
2600 return;
2601 }
2602
2603 if (!self.options.hasOwnProperty(value)) return;
2604 if (inputMode === 'single') self.clear(silent);
2605 if (inputMode === 'multi' && self.isFull()) return;
2606
2607 $item = $(self.render('item', self.options[value]));
2608 wasFull = self.isFull();
2609 self.items.splice(self.caretPos, 0, value);
2610 self.insertAtCaret($item);
2611 if (!self.isPending || (!wasFull && self.isFull())) {
2612 self.refreshState();
2613 }
2614
2615 if (self.isSetup) {
2616 $options = self.$dropdown_content.find('[data-selectable]');
2617
2618 // update menu / remove the option (if this is not one item being added as part of series)
2619 if (!self.isPending) {
2620 $option = self.getOption(value);
2621 value_next = self.getAdjacentOption($option, 1).attr('data-value');
2622 self.refreshOptions(self.isFocused && inputMode !== 'single');
2623 if (value_next) {
2624 self.setActiveOption(self.getOption(value_next));
2625 }
2626 }
2627
2628 // hide the menu if the maximum number of items have been selected or no options are left
2629 if (!$options.length || self.isFull()) {
2630 self.close();
2631 } else {
2632 self.positionDropdown();
2633 }
2634
2635 self.updatePlaceholder();
2636 self.trigger('item_add', value, $item);
2637 self.updateOriginalInput({silent: silent});
2638 }
2639 });
2640 },
2641
2642 /**
2643 * Removes the selected item matching
2644 * the provided value.
2645 *
2646 * @param {string} value
2647 */
2648 removeItem: function(value, silent) {
2649 var self = this;
2650 var $item, i, idx;
2651
2652 $item = (value instanceof $) ? value : self.getItem(value);
2653 value = hash_key($item.attr('data-value'));
2654 i = self.items.indexOf(value);
2655
2656 if (i !== -1) {
2657 $item.remove();
2658 if ($item.hasClass('active')) {
2659 idx = self.$activeItems.indexOf($item[0]);
2660 self.$activeItems.splice(idx, 1);
2661 }
2662
2663 self.items.splice(i, 1);
2664 self.lastQuery = null;
2665 if (!self.settings.persist && self.userOptions.hasOwnProperty(value)) {
2666 self.removeOption(value, silent);
2667 }
2668
2669 if (i < self.caretPos) {
2670 self.setCaret(self.caretPos - 1);
2671 }
2672
2673 self.refreshState();
2674 self.updatePlaceholder();
2675 self.updateOriginalInput({silent: silent});
2676 self.positionDropdown();
2677 self.trigger('item_remove', value, $item);
2678 }
2679 },
2680
2681 /**
2682 * Invokes the `create` method provided in the
2683 * selectize options that should provide the data
2684 * for the new item, given the user input.
2685 *
2686 * Once this completes, it will be added
2687 * to the item list.
2688 *
2689 * @param {string} value
2690 * @param {boolean} [triggerDropdown]
2691 * @param {function} [callback]
2692 * @return {boolean}
2693 */
2694 createItem: function(input, triggerDropdown) {
2695 var self = this;
2696 var caret = self.caretPos;
2697 input = input || trim(self.$control_input.val() || '');
2698
2699 var callback = arguments[arguments.length - 1];
2700 if (typeof callback !== 'function') callback = function() {};
2701
2702 if (typeof triggerDropdown !== 'boolean') {
2703 triggerDropdown = true;
2704 }
2705
2706 if (!self.canCreate(input)) {
2707 callback();
2708 return false;
2709 }
2710
2711 self.lock();
2712
2713 var setup = (typeof self.settings.create === 'function') ? this.settings.create : function(input) {
2714 var data = {};
2715 data[self.settings.labelField] = input;
2716 data[self.settings.valueField] = input;
2717 return data;
2718 };
2719
2720 var create = once(function(data) {
2721 self.unlock();
2722
2723 if (!data || typeof data !== 'object') return callback();
2724 var value = hash_key(data[self.settings.valueField]);
2725 if (typeof value !== 'string') return callback();
2726
2727 self.setTextboxValue('');
2728 self.addOption(data);
2729 self.setCaret(caret);
2730 self.addItem(value);
2731 self.refreshOptions(triggerDropdown && self.settings.mode !== 'single');
2732 callback(data);
2733 });
2734
2735 var output = setup.apply(this, [input, create]);
2736 if (typeof output !== 'undefined') {
2737 create(output);
2738 }
2739
2740 return true;
2741 },
2742
2743 /**
2744 * Re-renders the selected item lists.
2745 */
2746 refreshItems: function() {
2747 this.lastQuery = null;
2748
2749 if (this.isSetup) {
2750 this.addItem(this.items);
2751 }
2752
2753 this.refreshState();
2754 this.updateOriginalInput();
2755 },
2756
2757 /**
2758 * Updates all state-dependent attributes
2759 * and CSS classes.
2760 */
2761 refreshState: function() {
2762 this.refreshValidityState();
2763 this.refreshClasses();
2764 },
2765
2766 /**
2767 * Update the `required` attribute of both input and control input.
2768 *
2769 * The `required` property needs to be activated on the control input
2770 * for the error to be displayed at the right place. `required` also
2771 * needs to be temporarily deactivated on the input since the input is
2772 * hidden and can't show errors.
2773 */
2774 refreshValidityState: function() {
2775 if (!this.isRequired) return false;
2776
2777 var invalid = !this.items.length;
2778
2779 this.isInvalid = invalid;
2780 this.$control_input.prop('required', invalid);
2781 this.$input.prop('required', !invalid);
2782 },
2783
2784 /**
2785 * Updates all state-dependent CSS classes.
2786 */
2787 refreshClasses: function() {
2788 var self = this;
2789 var isFull = self.isFull();
2790 var isLocked = self.isLocked;
2791
2792 self.$wrapper
2793 .toggleClass('rtl', self.rtl);
2794
2795 self.$control
2796 .toggleClass('focus', self.isFocused)
2797 .toggleClass('disabled', self.isDisabled)
2798 .toggleClass('required', self.isRequired)
2799 .toggleClass('invalid', self.isInvalid)
2800 .toggleClass('locked', isLocked)
2801 .toggleClass('full', isFull).toggleClass('not-full', !isFull)
2802 .toggleClass('input-active', self.isFocused && !self.isInputHidden)
2803 .toggleClass('dropdown-active', self.isOpen)
2804 .toggleClass('has-options', !$.isEmptyObject(self.options))
2805 .toggleClass('has-items', self.items.length > 0);
2806
2807 self.$control_input.data('grow', !isFull && !isLocked);
2808 },
2809
2810 /**
2811 * Determines whether or not more items can be added
2812 * to the control without exceeding the user-defined maximum.
2813 *
2814 * @returns {boolean}
2815 */
2816 isFull: function() {
2817 return this.settings.maxItems !== null && this.items.length >= this.settings.maxItems;
2818 },
2819
2820 /**
2821 * Refreshes the original <select> or <input>
2822 * element to reflect the current state.
2823 */
2824 updateOriginalInput: function(opts) {
2825 var i, n, options, label, self = this;
2826 opts = opts || {};
2827
2828 if (self.tagType === TAG_SELECT) {
2829 options = [];
2830 for (i = 0, n = self.items.length; i < n; i++) {
2831 label = self.options[self.items[i]][self.settings.labelField] || '';
2832 options.push('<option value="' + escape_html(self.items[i]) + '" selected="selected">' + escape_html(label) + '</option>');
2833 }
2834 if (!options.length && !this.$input.attr('multiple')) {
2835 options.push('<option value="" selected="selected"></option>');
2836 }
2837 self.$input.html(options.join(''));
2838 } else {
2839 self.$input.val(self.getValue());
2840 self.$input.attr('value',self.$input.val());
2841 }
2842
2843 if (self.isSetup) {
2844 if (!opts.silent) {
2845 self.trigger('change', self.$input.val());
2846 }
2847 }
2848 },
2849
2850 /**
2851 * Shows/hide the input placeholder depending
2852 * on if there items in the list already.
2853 */
2854 updatePlaceholder: function() {
2855 if (!this.settings.placeholder) return;
2856 var $input = this.$control_input;
2857
2858 if (this.items.length) {
2859 $input.removeAttr('placeholder');
2860 } else {
2861 $input.attr('placeholder', this.settings.placeholder);
2862 }
2863 $input.triggerHandler('update', {force: true});
2864 },
2865
2866 /**
2867 * Shows the autocomplete dropdown containing
2868 * the available options.
2869 */
2870 open: function() {
2871 var self = this;
2872
2873 if (self.isLocked || self.isOpen || (self.settings.mode === 'multi' && self.isFull())) return;
2874 self.focus();
2875 self.isOpen = true;
2876 self.refreshState();
2877 self.$dropdown.css({visibility: 'hidden', display: 'block'});
2878 self.positionDropdown();
2879 self.$dropdown.css({visibility: 'visible'});
2880 self.trigger('dropdown_open', self.$dropdown);
2881 },
2882
2883 /**
2884 * Closes the autocomplete dropdown menu.
2885 */
2886 close: function() {
2887 var self = this;
2888 var trigger = self.isOpen;
2889
2890 if (self.settings.mode === 'single' && self.items.length) {
2891 self.hideInput();
2892 self.$control_input.blur(); // close keyboard on iOS
2893 }
2894
2895 self.isOpen = false;
2896 self.$dropdown.hide();
2897 self.setActiveOption(null);
2898 self.refreshState();
2899
2900 if (trigger) self.trigger('dropdown_close', self.$dropdown);
2901 },
2902
2903 /**
2904 * Calculates and applies the appropriate
2905 * position of the dropdown.
2906 */
2907 positionDropdown: function() {
2908 var $control = this.$control;
2909 var offset = this.settings.dropdownParent === 'body' ? $control.offset() : $control.position();
2910 offset.top += $control.outerHeight(true);
2911
2912 this.$dropdown.css({
2913 width : $control.outerWidth(),
2914 top : offset.top,
2915 left : offset.left
2916 });
2917 },
2918
2919 /**
2920 * Resets / clears all selected items
2921 * from the control.
2922 *
2923 * @param {boolean} silent
2924 */
2925 clear: function(silent) {
2926 var self = this;
2927
2928 if (!self.items.length) return;
2929 self.$control.children(':not(input)').remove();
2930 self.items = [];
2931 self.lastQuery = null;
2932 self.setCaret(0);
2933 self.setActiveItem(null);
2934 self.updatePlaceholder();
2935 self.updateOriginalInput({silent: silent});
2936 self.refreshState();
2937 self.showInput();
2938 self.trigger('clear');
2939 },
2940
2941 /**
2942 * A helper method for inserting an element
2943 * at the current caret position.
2944 *
2945 * @param {object} $el
2946 */
2947 insertAtCaret: function($el) {
2948 var caret = Math.min(this.caretPos, this.items.length);
2949 if (caret === 0) {
2950 this.$control.prepend($el);
2951 } else {
2952 $(this.$control[0].childNodes[caret]).before($el);
2953 }
2954 this.setCaret(caret + 1);
2955 },
2956
2957 /**
2958 * Removes the current selected item(s).
2959 *
2960 * @param {object} e (optional)
2961 * @returns {boolean}
2962 */
2963 deleteSelection: function(e) {
2964 var i, n, direction, selection, values, caret, option_select, $option_select, $tail;
2965 var self = this;
2966
2967 direction = (e && e.keyCode === KEY_BACKSPACE) ? -1 : 1;
2968 selection = getSelection(self.$control_input[0]);
2969
2970 if (self.$activeOption && !self.settings.hideSelected) {
2971 option_select = self.getAdjacentOption(self.$activeOption, -1).attr('data-value');
2972 }
2973
2974 // determine items that will be removed
2975 values = [];
2976
2977 if (self.$activeItems.length) {
2978 $tail = self.$control.children('.active:' + (direction > 0 ? 'last' : 'first'));
2979 caret = self.$control.children(':not(input)').index($tail);
2980 if (direction > 0) { caret++; }
2981
2982 for (i = 0, n = self.$activeItems.length; i < n; i++) {
2983 values.push($(self.$activeItems[i]).attr('data-value'));
2984 }
2985 if (e) {
2986 e.preventDefault();
2987 e.stopPropagation();
2988 }
2989 } else if ((self.isFocused || self.settings.mode === 'single') && self.items.length) {
2990 if (direction < 0 && selection.start === 0 && selection.length === 0) {
2991 values.push(self.items[self.caretPos - 1]);
2992 } else if (direction > 0 && selection.start === self.$control_input.val().length) {
2993 values.push(self.items[self.caretPos]);
2994 }
2995 }
2996
2997 // allow the callback to abort
2998 if (!values.length || (typeof self.settings.onDelete === 'function' && self.settings.onDelete.apply(self, [values]) === false)) {
2999 return false;
3000 }
3001
3002 // perform removal
3003 if (typeof caret !== 'undefined') {
3004 self.setCaret(caret);
3005 }
3006 while (values.length) {
3007 self.removeItem(values.pop());
3008 }
3009
3010 self.showInput();
3011 self.positionDropdown();
3012 self.refreshOptions(true);
3013
3014 // select previous option
3015 if (option_select) {
3016 $option_select = self.getOption(option_select);
3017 if ($option_select.length) {
3018 self.setActiveOption($option_select);
3019 }
3020 }
3021
3022 return true;
3023 },
3024
3025 /**
3026 * Selects the previous / next item (depending
3027 * on the `direction` argument).
3028 *
3029 * > 0 - right
3030 * < 0 - left
3031 *
3032 * @param {int} direction
3033 * @param {object} e (optional)
3034 */
3035 advanceSelection: function(direction, e) {
3036 var tail, selection, idx, valueLength, cursorAtEdge, $tail;
3037 var self = this;
3038
3039 if (direction === 0) return;
3040 if (self.rtl) direction *= -1;
3041
3042 tail = direction > 0 ? 'last' : 'first';
3043 selection = getSelection(self.$control_input[0]);
3044
3045 if (self.isFocused && !self.isInputHidden) {
3046 valueLength = self.$control_input.val().length;
3047 cursorAtEdge = direction < 0
3048 ? selection.start === 0 && selection.length === 0
3049 : selection.start === valueLength;
3050
3051 if (cursorAtEdge && !valueLength) {
3052 self.advanceCaret(direction, e);
3053 }
3054 } else {
3055 $tail = self.$control.children('.active:' + tail);
3056 if ($tail.length) {
3057 idx = self.$control.children(':not(input)').index($tail);
3058 self.setActiveItem(null);
3059 self.setCaret(direction > 0 ? idx + 1 : idx);
3060 }
3061 }
3062 },
3063
3064 /**
3065 * Moves the caret left / right.
3066 *
3067 * @param {int} direction
3068 * @param {object} e (optional)
3069 */
3070 advanceCaret: function(direction, e) {
3071 var self = this, fn, $adj;
3072
3073 if (direction === 0) return;
3074
3075 fn = direction > 0 ? 'next' : 'prev';
3076 if (self.isShiftDown) {
3077 $adj = self.$control_input[fn]();
3078 if ($adj.length) {
3079 self.hideInput();
3080 self.setActiveItem($adj);
3081 e && e.preventDefault();
3082 }
3083 } else {
3084 self.setCaret(self.caretPos + direction);
3085 }
3086 },
3087
3088 /**
3089 * Moves the caret to the specified index.
3090 *
3091 * @param {int} i
3092 */
3093 setCaret: function(i) {
3094 var self = this;
3095
3096 if (self.settings.mode === 'single') {
3097 i = self.items.length;
3098 } else {
3099 i = Math.max(0, Math.min(self.items.length, i));
3100 }
3101
3102 if(!self.isPending) {
3103 // the input must be moved by leaving it in place and moving the
3104 // siblings, due to the fact that focus cannot be restored once lost
3105 // on mobile webkit devices
3106 var j, n, fn, $children, $child;
3107 $children = self.$control.children(':not(input)');
3108 for (j = 0, n = $children.length; j < n; j++) {
3109 $child = $($children[j]).detach();
3110 if (j < i) {
3111 self.$control_input.before($child);
3112 } else {
3113 self.$control.append($child);
3114 }
3115 }
3116 }
3117
3118 self.caretPos = i;
3119 },
3120
3121 /**
3122 * Disables user input on the control. Used while
3123 * items are being asynchronously created.
3124 */
3125 lock: function() {
3126 this.close();
3127 this.isLocked = true;
3128 this.refreshState();
3129 },
3130
3131 /**
3132 * Re-enables user input on the control.
3133 */
3134 unlock: function() {
3135 this.isLocked = false;
3136 this.refreshState();
3137 },
3138
3139 /**
3140 * Disables user input on the control completely.
3141 * While disabled, it cannot receive focus.
3142 */
3143 disable: function() {
3144 var self = this;
3145 self.$input.prop('disabled', true);
3146 self.$control_input.prop('disabled', true).prop('tabindex', -1);
3147 self.isDisabled = true;
3148 self.lock();
3149 },
3150
3151 /**
3152 * Enables the control so that it can respond
3153 * to focus and user input.
3154 */
3155 enable: function() {
3156 var self = this;
3157 self.$input.prop('disabled', false);
3158 self.$control_input.prop('disabled', false).prop('tabindex', self.tabIndex);
3159 self.isDisabled = false;
3160 self.unlock();
3161 },
3162
3163 /**
3164 * Completely destroys the control and
3165 * unbinds all event listeners so that it can
3166 * be garbage collected.
3167 */
3168 destroy: function() {
3169 var self = this;
3170 var eventNS = self.eventNS;
3171 var revertSettings = self.revertSettings;
3172
3173 self.trigger('destroy');
3174 self.off();
3175 self.$wrapper.remove();
3176 self.$dropdown.remove();
3177
3178 self.$input
3179 .html('')
3180 .append(revertSettings.$children)
3181 .removeAttr('tabindex')
3182 .removeClass('selectized')
3183 .attr({tabindex: revertSettings.tabindex})
3184 .show();
3185
3186 self.$control_input.removeData('grow');
3187 self.$input.removeData('selectize');
3188
3189 $(window).off(eventNS);
3190 $(document).off(eventNS);
3191 $(document.body).off(eventNS);
3192
3193 delete self.$input[0].selectize;
3194 },
3195
3196 /**
3197 * A helper method for rendering "item" and
3198 * "option" templates, given the data.
3199 *
3200 * @param {string} templateName
3201 * @param {object} data
3202 * @returns {string}
3203 */
3204 render: function(templateName, data) {
3205 var value, id, label;
3206 var html = '';
3207 var cache = false;
3208 var self = this;
3209 var regex_tag = /^[\t \r\n]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i;
3210
3211 if (templateName === 'option' || templateName === 'item') {
3212 value = hash_key(data[self.settings.valueField]);
3213 cache = !!value;
3214 }
3215
3216 // pull markup from cache if it exists
3217 if (cache) {
3218 if (!isset(self.renderCache[templateName])) {
3219 self.renderCache[templateName] = {};
3220 }
3221 if (self.renderCache[templateName].hasOwnProperty(value)) {
3222 return self.renderCache[templateName][value];
3223 }
3224 }
3225
3226 // render markup
3227 html = $(self.settings.render[templateName].apply(this, [data, escape_html]));
3228
3229 // add mandatory attributes
3230 if (templateName === 'option' || templateName === 'option_create') {
3231 html.attr('data-selectable', '');
3232 }
3233 else if (templateName === 'optgroup') {
3234 id = data[self.settings.optgroupValueField] || '';
3235 html.attr('data-group', id);
3236 }
3237 if (templateName === 'option' || templateName === 'item') {
3238 html.attr('data-value', value || '');
3239 }
3240
3241 // update cache
3242 if (cache) {
3243 self.renderCache[templateName][value] = html[0];
3244 }
3245
3246 return html[0];
3247 },
3248
3249 /**
3250 * Clears the render cache for a template. If
3251 * no template is given, clears all render
3252 * caches.
3253 *
3254 * @param {string} templateName
3255 */
3256 clearCache: function(templateName) {
3257 var self = this;
3258 if (typeof templateName === 'undefined') {
3259 self.renderCache = {};
3260 } else {
3261 delete self.renderCache[templateName];
3262 }
3263 },
3264
3265 /**
3266 * Determines whether or not to display the
3267 * create item prompt, given a user input.
3268 *
3269 * @param {string} input
3270 * @return {boolean}
3271 */
3272 canCreate: function(input) {
3273 var self = this;
3274 if (!self.settings.create) return false;
3275 var filter = self.settings.createFilter;
3276 return input.length
3277 && (typeof filter !== 'function' || filter.apply(self, [input]))
3278 && (typeof filter !== 'string' || new RegExp(filter).test(input))
3279 && (!(filter instanceof RegExp) || filter.test(input));
3280 }
3281
3282 });
3283
3284
3285 Selectize.count = 0;
3286 Selectize.defaults = {
3287 options: [],
3288 optgroups: [],
3289
3290 plugins: [],
3291 delimiter: ',',
3292 splitOn: null, // regexp or string for splitting up values from a paste command
3293 persist: true,
3294 diacritics: true,
3295 create: false,
3296 createOnBlur: false,
3297 createFilter: null,
3298 highlight: true,
3299 openOnFocus: true,
3300 maxOptions: 1000,
3301 maxItems: null,
3302 hideSelected: null,
3303 addPrecedence: false,
3304 selectOnTab: false,
3305 preload: false,
3306 allowEmptyOption: false,
3307 closeAfterSelect: false,
3308
3309 scrollDuration: 60,
3310 loadThrottle: 300,
3311 loadingClass: 'loading',
3312
3313 dataAttr: 'data-data',
3314 optgroupField: 'optgroup',
3315 valueField: 'value',
3316 labelField: 'text',
3317 optgroupLabelField: 'label',
3318 optgroupValueField: 'value',
3319 lockOptgroupOrder: false,
3320
3321 sortField: '$order',
3322 searchField: ['text'],
3323 searchConjunction: 'and',
3324
3325 mode: null,
3326 wrapperClass: 'selectize-control',
3327 inputClass: 'selectize-input',
3328 dropdownClass: 'selectize-dropdown',
3329 dropdownContentClass: 'selectize-dropdown-content',
3330
3331 dropdownParent: null,
3332
3333 copyClassesToDropdown: true,
3334
3335 /*
3336 load : null, // function(query, callback) { ... }
3337 score : null, // function(search) { ... }
3338 onInitialize : null, // function() { ... }
3339 onChange : null, // function(value) { ... }
3340 onItemAdd : null, // function(value, $item) { ... }
3341 onItemRemove : null, // function(value) { ... }
3342 onClear : null, // function() { ... }
3343 onOptionAdd : null, // function(value, data) { ... }
3344 onOptionRemove : null, // function(value) { ... }
3345 onOptionClear : null, // function() { ... }
3346 onOptionGroupAdd : null, // function(id, data) { ... }
3347 onOptionGroupRemove : null, // function(id) { ... }
3348 onOptionGroupClear : null, // function() { ... }
3349 onDropdownOpen : null, // function($dropdown) { ... }
3350 onDropdownClose : null, // function($dropdown) { ... }
3351 onType : null, // function(str) { ... }
3352 onDelete : null, // function(values) { ... }
3353 */
3354
3355 render: {
3356 /*
3357 item: null,
3358 optgroup: null,
3359 optgroup_header: null,
3360 option: null,
3361 option_create: null
3362 */
3363 }
3364 };
3365
3366
3367 $.fn.selectize = function(settings_user) {
3368 var defaults = $.fn.selectize.defaults;
3369 var settings = $.extend({}, defaults, settings_user);
3370 var attr_data = settings.dataAttr;
3371 var field_label = settings.labelField;
3372 var field_value = settings.valueField;
3373 var field_optgroup = settings.optgroupField;
3374 var field_optgroup_label = settings.optgroupLabelField;
3375 var field_optgroup_value = settings.optgroupValueField;
3376
3377 /**
3378 * Initializes selectize from a <input type="text"> element.
3379 *
3380 * @param {object} $input
3381 * @param {object} settings_element
3382 */
3383 var init_textbox = function($input, settings_element) {
3384 var i, n, values, option;
3385
3386 var data_raw = $input.attr(attr_data);
3387
3388 if (!data_raw) {
3389 var value = trim($input.val() || '');
3390 if (!settings.allowEmptyOption && !value.length) return;
3391 values = value.split(settings.delimiter);
3392 for (i = 0, n = values.length; i < n; i++) {
3393 option = {};
3394 option[field_label] = values[i];
3395 option[field_value] = values[i];
3396 settings_element.options.push(option);
3397 }
3398 settings_element.items = values;
3399 } else {
3400 settings_element.options = JSON.parse(data_raw);
3401 for (i = 0, n = settings_element.options.length; i < n; i++) {
3402 settings_element.items.push(settings_element.options[i][field_value]);
3403 }
3404 }
3405 };
3406
3407 /**
3408 * Initializes selectize from a <select> element.
3409 *
3410 * @param {object} $input
3411 * @param {object} settings_element
3412 */
3413 var init_select = function($input, settings_element) {
3414 var i, n, tagName, $children, order = 0;
3415 var options = settings_element.options;
3416 var optionsMap = {};
3417
3418 var readData = function($el) {
3419 var data = attr_data && $el.attr(attr_data);
3420 if (typeof data === 'string' && data.length) {
3421 return JSON.parse(data);
3422 }
3423 return null;
3424 };
3425
3426 var addOption = function($option, group) {
3427 $option = $($option);
3428
3429 var value = hash_key($option.val());
3430 if (!value && !settings.allowEmptyOption) return;
3431
3432 // if the option already exists, it's probably been
3433 // duplicated in another optgroup. in this case, push
3434 // the current group to the "optgroup" property on the
3435 // existing option so that it's rendered in both places.
3436 if (optionsMap.hasOwnProperty(value)) {
3437 if (group) {
3438 var arr = optionsMap[value][field_optgroup];
3439 if (!arr) {
3440 optionsMap[value][field_optgroup] = group;
3441 } else if (!Array.isArray(arr)) {
3442 optionsMap[value][field_optgroup] = [arr, group];
3443 } else {
3444 arr.push(group);
3445 }
3446 }
3447 return;
3448 }
3449
3450 var option = readData($option) || {};
3451 option[field_label] = option[field_label] || $option.text();
3452 option[field_value] = option[field_value] || value;
3453 option[field_optgroup] = option[field_optgroup] || group;
3454
3455 optionsMap[value] = option;
3456 options.push(option);
3457
3458 if ($option.is(':selected')) {
3459 settings_element.items.push(value);
3460 }
3461 };
3462
3463 var addGroup = function($optgroup) {
3464 var i, n, id, optgroup, $options;
3465
3466 $optgroup = $($optgroup);
3467 id = $optgroup.attr('label');
3468
3469 if (id) {
3470 optgroup = readData($optgroup) || {};
3471 optgroup[field_optgroup_label] = id;
3472 optgroup[field_optgroup_value] = id;
3473 settings_element.optgroups.push(optgroup);
3474 }
3475
3476 $options = $('option', $optgroup);
3477 for (i = 0, n = $options.length; i < n; i++) {
3478 addOption($options[i], id);
3479 }
3480 };
3481
3482 settings_element.maxItems = $input.attr('multiple') ? null : 1;
3483
3484 $children = $input.children();
3485 for (i = 0, n = $children.length; i < n; i++) {
3486 tagName = $children[i].tagName.toLowerCase();
3487 if (tagName === 'optgroup') {
3488 addGroup($children[i]);
3489 } else if (tagName === 'option') {
3490 addOption($children[i]);
3491 }
3492 }
3493 };
3494
3495 return this.each(function() {
3496 if (this.selectize) return;
3497
3498 var instance;
3499 var $input = $(this);
3500 var tag_name = this.tagName.toLowerCase();
3501 var placeholder = $input.attr('placeholder') || $input.attr('data-placeholder');
3502 if (!placeholder && !settings.allowEmptyOption) {
3503 placeholder = $input.children('option[value=""]').text();
3504 }
3505
3506 var settings_element = {
3507 'placeholder' : placeholder,
3508 'options' : [],
3509 'optgroups' : [],
3510 'items' : []
3511 };
3512
3513 if (tag_name === 'select') {
3514 init_select($input, settings_element);
3515 } else {
3516 init_textbox($input, settings_element);
3517 }
3518
3519 instance = new Selectize($input, $.extend(true, {}, defaults, settings_element, settings_user));
3520 });
3521 };
3522
3523 $.fn.selectize.defaults = Selectize.defaults;
3524 $.fn.selectize.support = {
3525 validity: SUPPORTS_VALIDITY_API
3526 };
3527
3528
3529 Selectize.define('drag_drop', function(options) {
3530 if (!$.fn.sortable) throw new Error('The "drag_drop" plugin requires jQuery UI "sortable".');
3531 if (this.settings.mode !== 'multi') return;
3532 var self = this;
3533
3534 self.lock = (function() {
3535 var original = self.lock;
3536 return function() {
3537 var sortable = self.$control.data('sortable');
3538 if (sortable) sortable.disable();
3539 return original.apply(self, arguments);
3540 };
3541 })();
3542
3543 self.unlock = (function() {
3544 var original = self.unlock;
3545 return function() {
3546 var sortable = self.$control.data('sortable');
3547 if (sortable) sortable.enable();
3548 return original.apply(self, arguments);
3549 };
3550 })();
3551
3552 self.setup = (function() {
3553 var original = self.setup;
3554 return function() {
3555 original.apply(this, arguments);
3556
3557 var $control = self.$control.sortable({
3558 items: '[data-value]',
3559 forcePlaceholderSize: true,
3560 disabled: self.isLocked,
3561 start: function(e, ui) {
3562 ui.placeholder.css('width', ui.helper.css('width'));
3563 $control.css({overflow: 'visible'});
3564 },
3565 stop: function() {
3566 $control.css({overflow: 'hidden'});
3567 var active = self.$activeItems ? self.$activeItems.slice() : null;
3568 var values = [];
3569 $control.children('[data-value]').each(function() {
3570 values.push($(this).attr('data-value'));
3571 });
3572 self.setValue(values);
3573 self.setActiveItem(active);
3574 }
3575 });
3576 };
3577 })();
3578
3579 });
3580
3581 Selectize.define('dropdown_header', function(options) {
3582 var self = this;
3583
3584 options = $.extend({
3585 title : 'Untitled',
3586 headerClass : 'selectize-dropdown-header',
3587 titleRowClass : 'selectize-dropdown-header-title',
3588 labelClass : 'selectize-dropdown-header-label',
3589 closeClass : 'selectize-dropdown-header-close',
3590
3591 html: function(data) {
3592 return (
3593 '<div class="' + data.headerClass + '">' +
3594 '<div class="' + data.titleRowClass + '">' +
3595 '<span class="' + data.labelClass + '">' + data.title + '</span>' +
3596 '<a href="javascript:void(0)" class="' + data.closeClass + '">&times;</a>' +
3597 '</div>' +
3598 '</div>'
3599 );
3600 }
3601 }, options);
3602
3603 self.setup = (function() {
3604 var original = self.setup;
3605 return function() {
3606 original.apply(self, arguments);
3607 self.$dropdown_header = $(options.html(options));
3608 self.$dropdown.prepend(self.$dropdown_header);
3609 };
3610 })();
3611
3612 });
3613
3614 Selectize.define('optgroup_columns', function(options) {
3615 var self = this;
3616
3617 options = $.extend({
3618 equalizeWidth : true,
3619 equalizeHeight : true
3620 }, options);
3621
3622 this.getAdjacentOption = function($option, direction) {
3623 var $options = $option.closest('[data-group]').find('[data-selectable]');
3624 var index = $options.index($option) + direction;
3625
3626 return index >= 0 && index < $options.length ? $options.eq(index) : $();
3627 };
3628
3629 this.onKeyDown = (function() {
3630 var original = self.onKeyDown;
3631 return function(e) {
3632 var index, $option, $options, $optgroup;
3633
3634 if (this.isOpen && (e.keyCode === KEY_LEFT || e.keyCode === KEY_RIGHT)) {
3635 self.ignoreHover = true;
3636 $optgroup = this.$activeOption.closest('[data-group]');
3637 index = $optgroup.find('[data-selectable]').index(this.$activeOption);
3638
3639 if(e.keyCode === KEY_LEFT) {
3640 $optgroup = $optgroup.prev('[data-group]');
3641 } else {
3642 $optgroup = $optgroup.next('[data-group]');
3643 }
3644
3645 $options = $optgroup.find('[data-selectable]');
3646 $option = $options.eq(Math.min($options.length - 1, index));
3647 if ($option.length) {
3648 this.setActiveOption($option);
3649 }
3650 return;
3651 }
3652
3653 return original.apply(this, arguments);
3654 };
3655 })();
3656
3657 var getScrollbarWidth = function() {
3658 var div;
3659 var width = getScrollbarWidth.width;
3660 var doc = document;
3661
3662 if (typeof width === 'undefined') {
3663 div = doc.createElement('div');
3664 div.innerHTML = '<div style="width:50px;height:50px;position:absolute;left:-50px;top:-50px;overflow:auto;"><div style="width:1px;height:100px;"></div></div>';
3665 div = div.firstChild;
3666 doc.body.appendChild(div);
3667 width = getScrollbarWidth.width = div.offsetWidth - div.clientWidth;
3668 doc.body.removeChild(div);
3669 }
3670 return width;
3671 };
3672
3673 var equalizeSizes = function() {
3674 var i, n, height_max, width, width_last, width_parent, $optgroups;
3675
3676 $optgroups = $('[data-group]', self.$dropdown_content);
3677 n = $optgroups.length;
3678 if (!n || !self.$dropdown_content.width()) return;
3679
3680 if (options.equalizeHeight) {
3681 height_max = 0;
3682 for (i = 0; i < n; i++) {
3683 height_max = Math.max(height_max, $optgroups.eq(i).height());
3684 }
3685 $optgroups.css({height: height_max});
3686 }
3687
3688 if (options.equalizeWidth) {
3689 width_parent = self.$dropdown_content.innerWidth() - getScrollbarWidth();
3690 width = Math.round(width_parent / n);
3691 $optgroups.css({width: width});
3692 if (n > 1) {
3693 width_last = width_parent - width * (n - 1);
3694 $optgroups.eq(n - 1).css({width: width_last});
3695 }
3696 }
3697 };
3698
3699 if (options.equalizeHeight || options.equalizeWidth) {
3700 hook.after(this, 'positionDropdown', equalizeSizes);
3701 hook.after(this, 'refreshOptions', equalizeSizes);
3702 }
3703
3704
3705 });
3706
3707 Selectize.define('remove_button', function(options) {
3708 options = $.extend({
3709 label : '&times;',
3710 title : 'Remove',
3711 className : 'remove',
3712 append : true
3713 }, options);
3714
3715 var singleClose = function(thisRef, options) {
3716
3717 options.className = 'remove-single';
3718
3719 var self = thisRef;
3720 var html = '<a href="javascript:void(0)" class="' + options.className + '" tabindex="-1" title="' + escape_html(options.title) + '">' + options.label + '</a>';
3721
3722 /**
3723 * Appends an element as a child (with raw HTML).
3724 *
3725 * @param {string} html_container
3726 * @param {string} html_element
3727 * @return {string}
3728 */
3729 var append = function(html_container, html_element) {
3730 return html_container + html_element;
3731 };
3732
3733 thisRef.setup = (function() {
3734 var original = self.setup;
3735 return function() {
3736 // override the item rendering method to add the button to each
3737 if (options.append) {
3738 var id = $(self.$input.context).attr('id');
3739 var selectizer = $('#'+id);
3740
3741 var render_item = self.settings.render.item;
3742 self.settings.render.item = function(data) {
3743 return append(render_item.apply(thisRef, arguments), html);
3744 };
3745 }
3746
3747 original.apply(thisRef, arguments);
3748
3749 // add event listener
3750 thisRef.$control.on('click', '.' + options.className, function(e) {
3751 e.preventDefault();
3752 if (self.isLocked) return;
3753
3754 self.clear();
3755 });
3756
3757 };
3758 })();
3759 };
3760
3761 var multiClose = function(thisRef, options) {
3762
3763 var self = thisRef;
3764 var html = '<a href="javascript:void(0)" class="' + options.className + '" tabindex="-1" title="' + escape_html(options.title) + '">' + options.label + '</a>';
3765
3766 /**
3767 * Appends an element as a child (with raw HTML).
3768 *
3769 * @param {string} html_container
3770 * @param {string} html_element
3771 * @return {string}
3772 */
3773 var append = function(html_container, html_element) {
3774 var pos = html_container.search(/(<\/[^>]+>\s*)$/);
3775 return html_container.substring(0, pos) + html_element + html_container.substring(pos);
3776 };
3777
3778 thisRef.setup = (function() {
3779 var original = self.setup;
3780 return function() {
3781 // override the item rendering method to add the button to each
3782 if (options.append) {
3783 var render_item = self.settings.render.item;
3784 self.settings.render.item = function(data) {
3785 return append(render_item.apply(thisRef, arguments), html);
3786 };
3787 }
3788
3789 original.apply(thisRef, arguments);
3790
3791 // add event listener
3792 thisRef.$control.on('click', '.' + options.className, function(e) {
3793 e.preventDefault();
3794 if (self.isLocked) return;
3795
3796 var $item = $(e.currentTarget).parent();
3797 self.setActiveItem($item);
3798 if (self.deleteSelection()) {
3799 self.setCaret(self.items.length);
3800 }
3801 });
3802
3803 };
3804 })();
3805 };
3806
3807 if (this.settings.mode === 'single') {
3808 singleClose(this, options);
3809 return;
3810 } else {
3811 multiClose(this, options);
3812 }
3813 });
3814
3815
3816 Selectize.define('restore_on_backspace', function(options) {
3817 var self = this;
3818
3819 options.text = options.text || function(option) {
3820 return option[this.settings.labelField];
3821 };
3822
3823 this.onKeyDown = (function() {
3824 var original = self.onKeyDown;
3825 return function(e) {
3826 var index, option;
3827 if (e.keyCode === KEY_BACKSPACE && this.$control_input.val() === '' && !this.$activeItems.length) {
3828 index = this.caretPos - 1;
3829 if (index >= 0 && index < this.items.length) {
3830 option = this.options[this.items[index]];
3831 if (this.deleteSelection(e)) {
3832 this.setTextboxValue(options.text.apply(this, [option]));
3833 this.refreshOptions(true);
3834 }
3835 e.preventDefault();
3836 return;
3837 }
3838 }
3839 return original.apply(this, arguments);
3840 };
3841 })();
3842 });
3843
3844
3845 return Selectize;
3846 }));
3847