Passed
Push — master ( 1713a6...bcb549 )
by Peter
02:05
created

FuzzEd/static/lib/selectize/js/standalone/selectize.js   F

Complexity

Total Complexity 680
Complexity/F 3.32

Size

Lines of Code 3390
Function Count 205

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
dl 0
loc 3390
rs 2.4
c 0
b 0
f 0
wmc 680
nc 0
mnd 5
bc 492
fnc 205
bpm 2.4
cpm 3.317
noi 74

How to fix   Complexity   

Complexity

Complex classes like FuzzEd/static/lib/selectize/js/standalone/selectize.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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