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

FuzzEd/static/lib/selectize/js/selectize-0.9.1.js   F

Complexity

Total Complexity 565
Complexity/F 3.38

Size

Lines of Code 2801
Function Count 167

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
dl 0
loc 2801
rs 2.4
c 0
b 0
f 0
wmc 565
nc 0
mnd 5
bc 396
fnc 167
bpm 2.3712
cpm 3.3832
noi 64

How to fix   Complexity   

Complexity

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