PluginProbe
weForms – Easy Drag & Drop Contact Form Builder For WordPress / 1.3.3
weForms – Easy Drag & Drop Contact Form Builder For WordPress v1.3.3
1.6.7 1.6.8 1.6.9 1.6.12 1.6.13 1.6.14 1.6.15 1.6.16 1.6.17 1.6.18 1.6.19 1.6.2 1.6.20 1.6.21 1.6.22 1.6.23 1.6.24 1.6.25 1.6.26 1.6.27 1.6.28 1.6.3 1.6.4 1.6.5 1.6.6 All 74 releases
weforms / assets / wpuf / vendor / selectize / js / standalone / selectize.js

selectize.js in weForms – Easy Drag & Drop Contact Form Builder For WordPress 1.3.3, at assets/wpuf/vendor/selectize/js/standalone/selectize.js

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