selectize-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5f5f5589d1ad2624b7e8dd134e6da8febf3d5e34
4
+ data.tar.gz: 59832eaa0ba270ff1df87a7e4d46ac97c9943d93
5
+ SHA512:
6
+ metadata.gz: 193daee9a81ac47beb089eb6bfa3a314505c0113f5bd92c14495c80b5c038fcc3a4654c40546e1c504e12686e9f0751f2d3158fa50c39be100031edd06cd0650
7
+ data.tar.gz: 3979aa2049b5d43e9569a142db01c2c453df4fef9544598b857dc286e1a1396a14bd989458e048af25647f39a38030591baf6e3c90b457291792bb692de94673
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ _source
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in selectize-rails.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Manuel van Rijn
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # selectize-rails [![Gem Version](https://badge.fury.io/rb/selectize-rails.png)](http://badge.fury.io/rb/selectize-rails)
2
+
3
+ selectize-rails provides the [selectize.js](http://brianreavis.github.io/selectize.js/)
4
+ plugin as a Rails engine to use it within the asset pipeline.
5
+
6
+ ## Installation
7
+
8
+ Add this to your Gemfile:
9
+
10
+ ```ruby
11
+ gem "selectize-rails"
12
+ ```
13
+
14
+ and run `bundle install`.
15
+
16
+ ## Usage
17
+
18
+ In your `application.js`, include the following:
19
+
20
+ ```js
21
+ //= require selectize
22
+ ```
23
+
24
+ In your `application.css`, include the following:
25
+
26
+ ```css
27
+ *= require selectize
28
+ ```
29
+
30
+ ## Examples
31
+
32
+ See the [demo page of Brian Reavis](http://brianreavis.github.io/selectize.js/) for examples how to use the plugin
33
+
34
+ ## Changes
35
+
36
+ | Version | Notes |
37
+ |---------+---------------------------------------------------------------------------|
38
+ | 0.1.0 | Initial release with plugin version 0.1.5 |
39
+
40
+ ## License
41
+
42
+ * The [selectize.js](http://brianreavis.github.io/selectize.js/) plugin is licensed under the
43
+ [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
44
+ * The [selectize-rails](https://github.com/manuelvanrijn/selectize-rails) project is
45
+ licensed under the [MIT License](http://opensource.org/licenses/mit-license.html)
46
+
47
+ ## Contributing
48
+
49
+ 1. Fork it
50
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
51
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
52
+ 4. Push to the branch (`git push origin my-new-feature`)
53
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,12 @@
1
+ require "rails"
2
+ require "selectize-rails/version"
3
+
4
+ module Selectize
5
+ module Rails
6
+ if ::Rails.version < "3.1"
7
+ require "selectize-rails/railtie"
8
+ else
9
+ require "selectize-rails/engine"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module Selectize
2
+ module Rails
3
+ class Engine < ::Rails::Engine; end
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Selectize
2
+ module Rails
3
+ class Railtie < ::Rails::Railtie; end
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Selectize
2
+ module Rails
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'selectize-rails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "selectize-rails"
8
+ spec.version = Selectize::Rails::VERSION
9
+ spec.authors = ["Manuel van Rijn"]
10
+ spec.email = ["manuel@manuelles.nl"]
11
+ spec.description = %q{A small gem for putting selectize.js into the Rails asset pipeline}
12
+ spec.summary = %q{an asset gemification of the selectize.js plugin}
13
+ spec.homepage = "https://github.com/manuelvanrijn/selectize-rails"
14
+ spec.license = "MIT, Apache License v2.0"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ end
@@ -0,0 +1,1823 @@
1
+ /*! selectize.js | https://github.com/brianreavis/selectize.js | Apache License (v2) */
2
+
3
+ (function (factory) {
4
+ if (typeof exports === 'object') {
5
+ factory(require('jquery'));
6
+ } else if (typeof define === 'function' && define.amd) {
7
+ define(['jquery'], factory);
8
+ } else {
9
+ factory(jQuery);
10
+ }
11
+ }(function ($) {
12
+ "use strict";
13
+
14
+ // --- src/contrib/highlight.js ---
15
+
16
+ /**
17
+ * highlight v3 | MIT license | Johann Burkard <jb@eaio.com>
18
+ * Highlights arbitrary terms in a node.
19
+ *
20
+ * - Modified by Marshal <beatgates@gmail.com> 2011-6-24 (added regex)
21
+ * - Modified by Brian Reavis <brian@thirdroute.com> 2012-8-27 (cleanup)
22
+ */
23
+
24
+ var highlight = function($element, pattern) {
25
+ if (typeof pattern === 'string' && !pattern.length) return;
26
+ var regex = (typeof pattern === 'string') ? new RegExp(pattern, 'i') : pattern;
27
+
28
+ var highlight = function(node) {
29
+ var skip = 0;
30
+ if (node.nodeType === 3) {
31
+ var pos = node.data.search(regex);
32
+ if (pos >= 0 && node.data.length > 0) {
33
+ var match = node.data.match(regex);
34
+ var spannode = document.createElement('span');
35
+ spannode.className = 'highlight';
36
+ var middlebit = node.splitText(pos);
37
+ var endbit = middlebit.splitText(match[0].length);
38
+ var middleclone = middlebit.cloneNode(true);
39
+ spannode.appendChild(middleclone);
40
+ middlebit.parentNode.replaceChild(spannode, middlebit);
41
+ skip = 1;
42
+ }
43
+ } else if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
44
+ for (var i = 0; i < node.childNodes.length; ++i) {
45
+ i += highlight(node.childNodes[i]);
46
+ }
47
+ }
48
+ return skip;
49
+ };
50
+
51
+ return $element.each(function() {
52
+ highlight(this);
53
+ });
54
+ };
55
+
56
+ var unhighlight = function($element) {
57
+ return $element.find('span.highlight').each(function() {
58
+ var parent = this.parentNode;
59
+ parent.replaceChild(parent.firstChild, parent);
60
+ parent.normalize();
61
+ }).end();
62
+ };
63
+
64
+ // --- src/constants.js ---
65
+
66
+ /**
67
+ * selectize - A highly customizable select control with autocomplete.
68
+ * Copyright (c) 2013 Brian Reavis & contributors
69
+ *
70
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
71
+ * file except in compliance with the License. You may obtain a copy of the License at:
72
+ * http://www.apache.org/licenses/LICENSE-2.0
73
+ *
74
+ * Unless required by applicable law or agreed to in writing, software distributed under
75
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
76
+ * ANY KIND, either express or implied. See the License for the specific language
77
+ * governing permissions and limitations under the License.
78
+ *
79
+ * @author Brian Reavis <brian@thirdroute.com>
80
+ */
81
+
82
+ var IS_MAC = /Mac/.test(navigator.userAgent);
83
+
84
+ var KEY_COMMA = 188;
85
+ var KEY_RETURN = 13;
86
+ var KEY_ESC = 27;
87
+ var KEY_LEFT = 37;
88
+ var KEY_UP = 38;
89
+ var KEY_RIGHT = 39;
90
+ var KEY_DOWN = 40;
91
+ var KEY_BACKSPACE = 8;
92
+ var KEY_DELETE = 46;
93
+ var KEY_SHIFT = 16;
94
+ var KEY_CTRL = IS_MAC ? 18 : 17;
95
+ var KEY_TAB = 9;
96
+
97
+ var TAG_SELECT = 1;
98
+ var TAG_INPUT = 2;
99
+
100
+ var DIACRITICS = {
101
+ 'a': '[aÀÁÂÃÄÅàáâãäå]',
102
+ 'c': '[cÇç]',
103
+ 'e': '[eÈÉÊËèéêë]',
104
+ 'i': '[iÌÍÎÏìíîï]',
105
+ 'n': '[nÑñ]',
106
+ 'o': '[oÒÓÔÕÕÖØòóôõöø]',
107
+ 's': '[sŠš]',
108
+ 'u': '[uÙÚÛÜùúûü]',
109
+ 'y': '[yŸÿý]',
110
+ 'z': '[zŽž]'
111
+ };
112
+
113
+ // --- src/selectize.jquery.js ---
114
+
115
+ var defaults = {
116
+ delimiter: ',',
117
+ persist: true,
118
+ diacritics: true,
119
+ create: false,
120
+ highlight: true,
121
+ openOnFocus: true,
122
+ maxOptions: 1000,
123
+ maxItems: null,
124
+ hideSelected: null,
125
+
126
+ scrollDuration: 60,
127
+ loadThrottle: 300,
128
+
129
+ dataAttr: 'data-data',
130
+ sortField: null,
131
+ sortDirection: 'asc',
132
+ valueField: 'value',
133
+ labelField: 'text',
134
+ searchField: ['text'],
135
+
136
+ mode: null,
137
+ theme: 'default',
138
+ wrapperClass: 'selectize-control',
139
+ inputClass: 'selectize-input',
140
+ dropdownClass: 'selectize-dropdown',
141
+
142
+ load : null, // function(query, callback)
143
+ score : null, // function(search)
144
+ onChange : null, // function(value)
145
+ onItemAdd : null, // function(value, $item) { ... }
146
+ onItemRemove : null, // function(value) { ... }
147
+ onClear : null, // function() { ... }
148
+ onOptionAdd : null, // function(value, data) { ... }
149
+ onOptionRemove : null, // function(value) { ... }
150
+ onDropdownOpen : null, // function($dropdown) { ... }
151
+ onDropdownClose : null, // function($dropdown) { ... }
152
+ onType : null, // function(str) { ... }
153
+
154
+ render: {
155
+ item: null,
156
+ option: null,
157
+ option_create: null
158
+ }
159
+ };
160
+
161
+ $.fn.selectize = function (settings) {
162
+ var defaults = $.fn.selectize.defaults;
163
+ settings = settings || {};
164
+
165
+ return this.each(function() {
166
+ var instance, value, values, i, n, data, dataAttr, settings_element, tagName;
167
+ var $options, $option, $input = $(this);
168
+
169
+ tagName = $input[0].tagName.toLowerCase();
170
+
171
+ if (typeof settings === 'string') {
172
+ instance = $input.data('selectize');
173
+ instance[settings].apply(instance, Array.prototype.splice.apply(arguments, 1));
174
+ } else {
175
+ dataAttr = settings.dataAttr || defaults.dataAttr;
176
+ settings_element = {};
177
+ settings_element.placeholder = $input.attr('placeholder');
178
+ settings_element.options = {};
179
+ settings_element.items = [];
180
+
181
+ if (tagName === 'select') {
182
+ settings_element.maxItems = !!$input.attr('multiple') ? null : 1;
183
+ $options = $input.children();
184
+ for (i = 0, n = $options.length; i < n; i++) {
185
+ $option = $($options[i]);
186
+ value = $option.attr('value') || '';
187
+ if (!value.length) continue;
188
+ data = (dataAttr && $option.attr(dataAttr)) || {
189
+ 'text' : $option.html(),
190
+ 'value' : value
191
+ };
192
+
193
+ if (typeof data === 'string') data = JSON.parse(data);
194
+ settings_element.options[value] = data;
195
+ if ($option.is(':selected')) {
196
+ settings_element.items.push(value);
197
+ }
198
+ }
199
+ } else {
200
+ value = $.trim($input.val() || '');
201
+ if (value.length) {
202
+ values = value.split(settings.delimiter || defaults.delimiter);
203
+ for (i = 0, n = values.length; i < n; i++) {
204
+ settings_element.options[values[i]] = {
205
+ 'text' : values[i],
206
+ 'value' : values[i]
207
+ };
208
+ }
209
+ settings_element.items = values;
210
+ }
211
+ }
212
+
213
+ instance = new Selectize($input, $.extend(true, {}, defaults, settings_element, settings));
214
+ $input.data('selectize', instance);
215
+ $input.addClass('selectized');
216
+ }
217
+ });
218
+ };
219
+
220
+ $.fn.selectize.defaults = defaults;
221
+
222
+ // --- src/utils.js ---
223
+
224
+ var isset = function(object) {
225
+ return typeof object !== 'undefined';
226
+ };
227
+
228
+ var htmlEntities = function(str) {
229
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
230
+ };
231
+
232
+ var quoteRegExp = function(str) {
233
+ return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
234
+ };
235
+
236
+ var once = function(fn) {
237
+ var called = false;
238
+ return function() {
239
+ if (called) return;
240
+ called = true;
241
+ fn.apply(this, arguments);
242
+ };
243
+ };
244
+
245
+ var debounce = function(fn, delay) {
246
+ var timeout;
247
+ return function() {
248
+ var self = this;
249
+ var args = arguments;
250
+ window.clearTimeout(timeout);
251
+ timeout = window.setTimeout(function() {
252
+ fn.apply(self, args);
253
+ }, delay);
254
+ };
255
+ };
256
+
257
+ /**
258
+ * A workaround for http://bugs.jquery.com/ticket/6696
259
+ *
260
+ * @param {object} $parent - Parent element to listen on.
261
+ * @param {string} event - Event name.
262
+ * @param {string} selector - Descendant selector to filter by.
263
+ * @param {function} fn - Event handler.
264
+ */
265
+ var watchChildEvent = function($parent, event, selector, fn) {
266
+ $parent.on(event, selector, function(e) {
267
+ var child = e.target;
268
+ while (child && child.parentNode !== $parent[0]) {
269
+ child = child.parentNode;
270
+ }
271
+ e.currentTarget = child;
272
+ return fn.apply(this, [e]);
273
+ });
274
+ };
275
+
276
+ var getSelection = function(input) {
277
+ var result = {};
278
+ if ('selectionStart' in input) {
279
+ result.start = input.selectionStart;
280
+ result.length = input.selectionEnd - result.start;
281
+ } else if (document.selection) {
282
+ input.focus();
283
+ var sel = document.selection.createRange();
284
+ var selLen = document.selection.createRange().text.length;
285
+ sel.moveStart('character', -input.value.length);
286
+ result.start = sel.text.length - selLen;
287
+ result.length = selLen;
288
+ }
289
+ return result;
290
+ };
291
+
292
+ var transferStyles = function($from, $to, properties) {
293
+ var styles = {};
294
+ if (properties) {
295
+ for (var i = 0; i < properties.length; i++) {
296
+ styles[properties[i]] = $from.css(properties[i]);
297
+ }
298
+ } else {
299
+ styles = $from.css();
300
+ }
301
+ $to.css(styles);
302
+ return $to;
303
+ };
304
+
305
+ var measureString = function(str, $parent) {
306
+ var $test = $('<test>').css({
307
+ position: 'absolute',
308
+ top: -99999,
309
+ left: -99999,
310
+ width: 'auto',
311
+ padding: 0,
312
+ whiteSpace: 'nowrap'
313
+ }).text(str).appendTo('body');
314
+
315
+ transferStyles($parent, $test, [
316
+ 'letterSpacing',
317
+ 'fontSize',
318
+ 'fontFamily',
319
+ 'fontWeight',
320
+ 'textTransform'
321
+ ]);
322
+
323
+ var width = $test.width();
324
+ $test.remove();
325
+
326
+ return width;
327
+ };
328
+
329
+ var autoGrow = function($input) {
330
+ var update = function(e) {
331
+ var value, keyCode, printable, placeholder, width;
332
+ var shift, character;
333
+
334
+ e = e || window.event;
335
+ value = $input.val();
336
+ if (e.type && e.type.toLowerCase() === 'keydown') {
337
+ keyCode = e.keyCode;
338
+ printable = (
339
+ (keyCode >= 97 && keyCode <= 122) || // a-z
340
+ (keyCode >= 65 && keyCode <= 90) || // A-Z
341
+ (keyCode >= 48 && keyCode <= 57) || // 0-9
342
+ keyCode == 32 // space
343
+ );
344
+
345
+ if (printable) {
346
+ shift = e.shiftKey;
347
+ character = String.fromCharCode(e.keyCode);
348
+ if (shift) character = character.toUpperCase();
349
+ else character = character.toLowerCase();
350
+ value += character;
351
+ }
352
+ }
353
+
354
+ placeholder = $input.attr('placeholder') || '';
355
+ if (!value.length && placeholder.length) {
356
+ value = placeholder;
357
+ }
358
+
359
+ width = measureString(value, $input) + 4;
360
+ if (width !== $input.width()) {
361
+ $input.width(width);
362
+ $input.triggerHandler('resize');
363
+ }
364
+ };
365
+ $input.on('keydown keyup update blur', update);
366
+ update({});
367
+ };
368
+
369
+ // --- src/selectize.js ---
370
+
371
+ /**
372
+ * selectize.js
373
+ * Copyright (c) 2013 Brian Reavis & contributors
374
+ *
375
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
376
+ * file except in compliance with the License. You may obtain a copy of the License at:
377
+ * http://www.apache.org/licenses/LICENSE-2.0
378
+ *
379
+ * Unless required by applicable law or agreed to in writing, software distributed under
380
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
381
+ * ANY KIND, either express or implied. See the License for the specific language
382
+ * governing permissions and limitations under the License.
383
+ *
384
+ * @author Brian Reavis <brian@thirdroute.com>
385
+ */
386
+
387
+ var Selectize = function($input, settings) {
388
+ $input[0].selectize = this;
389
+
390
+ this.$input = $input;
391
+ this.tagType = $input[0].tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT;
392
+ this.settings = settings;
393
+
394
+ this.highlightedValue = null;
395
+ this.isOpen = false;
396
+ this.isLocked = false;
397
+ this.isFocused = false;
398
+ this.isInputFocused = false;
399
+ this.isSetup = false;
400
+ this.isShiftDown = false;
401
+ this.isCtrlDown = false;
402
+ this.ignoreFocus = false;
403
+ this.hasOptions = false;
404
+ this.currentResults = null;
405
+ this.lastValue = '';
406
+ this.caretPos = 0;
407
+ this.loading = 0;
408
+ this.loadedSearches = {};
409
+
410
+ this.$activeOption = null;
411
+ this.$activeItems = [];
412
+
413
+ this.options = {};
414
+ this.userOptions = {};
415
+ this.items = [];
416
+ this.renderCache = {};
417
+ this.onSearchChange = debounce(this.onSearchChange, this.settings.loadThrottle);
418
+
419
+ if ($.isArray(settings.options)) {
420
+ var key = settings.valueField;
421
+ for (var i = 0; i < settings.options.length; i++) {
422
+ if (settings.options[i].hasOwnProperty(key)) {
423
+ this.options[settings.options[i][key]] = settings.options[i];
424
+ }
425
+ }
426
+ } else if (typeof settings.options === 'object') {
427
+ $.extend(this.options, settings.options);
428
+ delete this.settings.options;
429
+ }
430
+
431
+ // option-dependent defaults
432
+ this.settings.mode = this.settings.mode || (this.settings.maxItems === 1 ? 'single' : 'multi');
433
+ if (typeof this.settings.hideSelected !== 'boolean') {
434
+ this.settings.hideSelected = this.settings.mode === 'multi';
435
+ }
436
+
437
+ this.setup();
438
+ };
439
+
440
+ /**
441
+ * Creates all elements and sets up event bindings.
442
+ */
443
+ Selectize.prototype.setup = function() {
444
+ var self = this;
445
+ var $wrapper;
446
+ var $control;
447
+ var $control_input;
448
+ var $dropdown;
449
+ var inputMode;
450
+ var displayMode;
451
+ var timeout_blur;
452
+ var timeout_focus;
453
+
454
+ $wrapper = $('<div>').addClass(this.settings.theme).addClass(this.settings.wrapperClass);
455
+ $control = $('<div>').addClass(this.settings.inputClass).addClass('items').toggleClass('has-options', !$.isEmptyObject(this.options)).appendTo($wrapper);
456
+ $control_input = $('<input type="text">').appendTo($control);
457
+ $dropdown = $('<div>').addClass(this.settings.dropdownClass).hide().appendTo($wrapper);
458
+
459
+ displayMode = this.$input.css('display');
460
+ $wrapper.css({
461
+ width: this.$input[0].style.width,
462
+ display: displayMode
463
+ });
464
+
465
+ inputMode = this.settings.mode;
466
+ $wrapper.toggleClass('single', inputMode === 'single');
467
+ $wrapper.toggleClass('multi', inputMode === 'multi');
468
+
469
+ if ((this.settings.maxItems === null || this.settings.maxItems > 1) && this.tagType === TAG_SELECT) {
470
+ this.$input.attr('multiple', 'multiple');
471
+ }
472
+
473
+ if (this.settings.placeholder) {
474
+ $control_input.attr('placeholder', this.settings.placeholder);
475
+ }
476
+
477
+ this.$wrapper = $wrapper;
478
+ this.$control = $control;
479
+ this.$control_input = $control_input;
480
+ this.$dropdown = $dropdown;
481
+
482
+ $control.on('mousedown', function(e) {
483
+ if (e.currentTarget === self.$control[0]) {
484
+ $control_input.trigger('focus');
485
+ } else {
486
+ self.focus(true);
487
+ }
488
+ e.preventDefault();
489
+ });
490
+
491
+ watchChildEvent($dropdown, 'mouseenter', '*', function() { return self.onOptionHover.apply(self, arguments); });
492
+ watchChildEvent($dropdown, 'mousedown', '*', function() { return self.onOptionSelect.apply(self, arguments); });
493
+ watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); });
494
+
495
+ autoGrow($control_input);
496
+
497
+ $control_input.on({
498
+ mousedown : function(e) { e.stopPropagation(); },
499
+ keydown : function() { return self.onKeyDown.apply(self, arguments); },
500
+ keyup : function() { return self.onKeyUp.apply(self, arguments); },
501
+ keypress : function() { return self.onKeyPress.apply(self, arguments); },
502
+ resize : function() { self.positionDropdown.apply(self, []); },
503
+ blur : function() { return self.onBlur.apply(self, arguments); },
504
+ focus : function() { return self.onFocus.apply(self, arguments); }
505
+ });
506
+
507
+ $(document).on({
508
+ keydown: function(e) {
509
+ self.isCtrlDown = e[IS_MAC ? 'altKey' : 'ctrlKey'];
510
+ self.isShiftDown = e.shiftKey;
511
+ if (self.isFocused && !self.isLocked) {
512
+ var tagName = (e.target.tagName || '').toLowerCase();
513
+ if (tagName === 'input' || tagName === 'textarea') return;
514
+ if ([KEY_SHIFT, KEY_BACKSPACE, KEY_DELETE, KEY_ESC, KEY_LEFT, KEY_RIGHT, KEY_TAB].indexOf(e.keyCode) !== -1) {
515
+ return self.onKeyDown.apply(self, arguments);
516
+ }
517
+ }
518
+ },
519
+ keyup: function(e) {
520
+ if (e.keyCode === KEY_CTRL) self.isCtrlDown = false;
521
+ else if (e.keyCode === KEY_SHIFT) self.isShiftDown = false;
522
+ },
523
+ mousedown: function(e) {
524
+ if (self.isFocused && !self.isLocked) {
525
+ // prevent events on the dropdown scrollbar from causing the control to blur
526
+ if (e.target === self.$dropdown[0]) {
527
+ var ignoreFocus = self.ignoreFocus;
528
+ self.ignoreFocus = true;
529
+ window.setTimeout(function() {
530
+ self.ignoreFocus = ignoreFocus;
531
+ self.focus(false);
532
+ }, 0);
533
+ return;
534
+ }
535
+ // blur on click outside
536
+ if (!self.$control.has(e.target).length && e.target !== self.$control[0]) {
537
+ self.blur();
538
+ }
539
+ }
540
+ }
541
+ });
542
+
543
+ $(window).on({
544
+ resize: function() {
545
+ if (self.isOpen) {
546
+ self.positionDropdown.apply(self, arguments);
547
+ }
548
+ }
549
+ });
550
+
551
+ this.$input.hide().after(this.$wrapper);
552
+
553
+ if ($.isArray(this.settings.items)) {
554
+ this.setValue(this.settings.items);
555
+ delete this.settings.items;
556
+ }
557
+
558
+ this.updateOriginalInput();
559
+ this.refreshItems();
560
+ this.updatePlaceholder();
561
+ this.isSetup = true;
562
+ };
563
+
564
+ /**
565
+ * Triggers a callback defined in the user-provided settings.
566
+ * Events: onItemAdd, onOptionAdd, etc
567
+ *
568
+ * @param {string} event
569
+ */
570
+ Selectize.prototype.trigger = function(event) {
571
+ var args;
572
+ if (typeof this.settings[event] === 'function') {
573
+ args = Array.prototype.slice.apply(arguments, [1]);
574
+ this.settings.event.apply(this, args);
575
+ }
576
+ };
577
+
578
+ /**
579
+ * Triggered on <input> keypress.
580
+ *
581
+ * @param {object} e
582
+ * @returns {boolean}
583
+ */
584
+ Selectize.prototype.onKeyPress = function(e) {
585
+ if (this.isLocked) return;
586
+ var character = String.fromCharCode(e.keyCode || e.which);
587
+ if (this.settings.create && character === this.settings.delimiter) {
588
+ this.createItem();
589
+ e.preventDefault();
590
+ return false;
591
+ }
592
+ };
593
+
594
+ /**
595
+ * Triggered on <input> keydown.
596
+ *
597
+ * @param {object} e
598
+ * @returns {boolean}
599
+ */
600
+ Selectize.prototype.onKeyDown = function(e) {
601
+ if (this.isLocked) return;
602
+ var isInput = e.target === this.$control_input[0];
603
+
604
+ switch (e.keyCode || e.which) {
605
+ case KEY_ESC:
606
+ this.blur();
607
+ return;
608
+ case KEY_DOWN:
609
+ if (!this.isOpen && this.hasOptions && this.isInputFocused) {
610
+ this.open();
611
+ } else if (this.$activeOption) {
612
+ var $next = this.$activeOption.next();
613
+ if ($next.length) this.setActiveOption($next, true, true);
614
+ }
615
+ e.preventDefault();
616
+ break;
617
+ case KEY_UP:
618
+ if (this.$activeOption) {
619
+ var $prev = this.$activeOption.prev();
620
+ if ($prev.length) this.setActiveOption($prev, true, true);
621
+ }
622
+ e.preventDefault();
623
+ break;
624
+ case KEY_RETURN:
625
+ if (this.$activeOption) {
626
+ this.onOptionSelect({currentTarget: this.$activeOption});
627
+ }
628
+ e.preventDefault();
629
+ break;
630
+ case KEY_LEFT:
631
+ this.advanceSelection(-1, e);
632
+ break;
633
+ case KEY_RIGHT:
634
+ this.advanceSelection(1, e);
635
+ break;
636
+ case KEY_TAB:
637
+ if (this.settings.create && $.trim(this.$control_input.val()).length) {
638
+ this.createItem();
639
+ e.preventDefault();
640
+ }
641
+ break;
642
+ case KEY_BACKSPACE:
643
+ case KEY_DELETE:
644
+ this.deleteSelection(e);
645
+ break;
646
+ default:
647
+ if (this.isFull()) {
648
+ e.preventDefault();
649
+ return;
650
+ }
651
+ }
652
+ if (!this.isFull()) {
653
+ this.focus(true);
654
+ }
655
+ };
656
+
657
+ /**
658
+ * Triggered on <input> keyup.
659
+ *
660
+ * @param {object} e
661
+ * @returns {boolean}
662
+ */
663
+ Selectize.prototype.onKeyUp = function(e) {
664
+ if (this.isLocked) return;
665
+ var value = this.$control_input.val() || '';
666
+ if (this.lastValue !== value) {
667
+ this.lastValue = value;
668
+ this.onSearchChange(value);
669
+ this.refreshOptions();
670
+ this.trigger('onType', value);
671
+ }
672
+ };
673
+
674
+ /**
675
+ * Invokes the user-provide option provider / loader.
676
+ *
677
+ * Note: this function is debounced in the Selectize
678
+ * constructor (by `settings.loadDelay` milliseconds)
679
+ *
680
+ * @param {string} value
681
+ */
682
+ Selectize.prototype.onSearchChange = function(value) {
683
+ if (!this.settings.load) return;
684
+ if (this.loadedSearches.hasOwnProperty(value)) return;
685
+ var self = this;
686
+ var $wrapper = this.$wrapper.addClass('loading');
687
+
688
+ this.loading++;
689
+ this.loadedSearches[value] = true;
690
+ this.settings.load.apply(this, [value, function(results) {
691
+ self.loading = Math.max(self.loading - 1, 0);
692
+ if (results && results.length) {
693
+ self.addOption(results);
694
+ self.refreshOptions(false);
695
+ if (self.isInputFocused) self.open();
696
+ }
697
+ if (!self.loading) {
698
+ $wrapper.removeClass('loading');
699
+ }
700
+ }]);
701
+ };
702
+
703
+ /**
704
+ * Triggered on <input> focus.
705
+ *
706
+ * @param {object} e
707
+ * @returns {boolean}
708
+ */
709
+ Selectize.prototype.onFocus = function(e) {
710
+ this.showInput();
711
+ this.isInputFocused = true;
712
+ this.isFocused = true;
713
+ if (this.ignoreFocus) return;
714
+
715
+ this.setActiveItem(null);
716
+ this.$control.addClass('focus');
717
+ this.refreshOptions(!!this.settings.openOnFocus);
718
+ };
719
+
720
+ /**
721
+ * Triggered on <input> blur.
722
+ *
723
+ * @param {object} e
724
+ * @returns {boolean}
725
+ */
726
+ Selectize.prototype.onBlur = function(e) {
727
+ this.isInputFocused = false;
728
+ if (this.ignoreFocus) return;
729
+
730
+ this.close();
731
+ this.setTextboxValue('');
732
+ this.setActiveOption(null);
733
+ this.setCaret(this.items.length, false);
734
+ if (!this.$activeItems.length) {
735
+ this.$control.removeClass('focus');
736
+ this.isFocused = false;
737
+ }
738
+ };
739
+
740
+ /**
741
+ * Triggered when the user rolls over
742
+ * an option in the autocomplete dropdown menu.
743
+ *
744
+ * @param {object} e
745
+ * @returns {boolean}
746
+ */
747
+ Selectize.prototype.onOptionHover = function(e) {
748
+ this.setActiveOption(e.currentTarget, false);
749
+ };
750
+
751
+ /**
752
+ * Triggered when the user clicks on an option
753
+ * in the autocomplete dropdown menu.
754
+ *
755
+ * @param {object} e
756
+ * @returns {boolean}
757
+ */
758
+ Selectize.prototype.onOptionSelect = function(e) {
759
+ var $target = $(e.currentTarget);
760
+ if ($target.hasClass('create')) {
761
+ this.createItem();
762
+ } else {
763
+ var value = $target.attr('data-value');
764
+ if (value) {
765
+ this.addItem(value);
766
+ this.setTextboxValue('');
767
+ }
768
+ }
769
+ };
770
+
771
+ /**
772
+ * Triggered when the user clicks on an item
773
+ * that has been selected.
774
+ *
775
+ * @param {object} e
776
+ * @returns {boolean}
777
+ */
778
+ Selectize.prototype.onItemSelect = function(e) {
779
+ if (this.settings.mode === 'multi') {
780
+ this.$control_input.trigger('blur');
781
+ this.setActiveItem(e.currentTarget, e);
782
+ e.stopPropagation();
783
+ }
784
+ };
785
+
786
+ /**
787
+ * Sets the input field of the control to the specified value.
788
+ *
789
+ * @param {string} value
790
+ */
791
+ Selectize.prototype.setTextboxValue = function(value) {
792
+ this.$control_input.val(value);
793
+ this.lastValue = value;
794
+ };
795
+
796
+ /**
797
+ * Returns the value of the control. If multiple items
798
+ * can be selected (e.g. <select multiple>), this returns
799
+ * an array. If only one item can be selected, this
800
+ * returns a string.
801
+ *
802
+ * @returns {mixed}
803
+ */
804
+ Selectize.prototype.getValue = function() {
805
+ if (this.tagType === TAG_SELECT && this.$input.attr('multiple')) {
806
+ return this.items;
807
+ } else {
808
+ return this.items.join(this.settings.delimiter);
809
+ }
810
+ };
811
+
812
+ /**
813
+ * Resets the selected items to the given value.
814
+ *
815
+ * @param {mixed} value
816
+ */
817
+ Selectize.prototype.setValue = function(value) {
818
+ this.clear();
819
+ var items = $.isArray(value) ? value : [value];
820
+ for (var i = 0, n = items.length; i < n; i++) {
821
+ this.addItem(items[i]);
822
+ }
823
+ };
824
+
825
+ /**
826
+ * Sets the selected item.
827
+ *
828
+ * @param {object} $item
829
+ * @param {object} e (optional)
830
+ */
831
+ Selectize.prototype.setActiveItem = function($item, e) {
832
+ var eventName;
833
+ var i, idx, begin, end, item, swap;
834
+ var $last;
835
+
836
+ $item = $($item);
837
+
838
+ // clear the active selection
839
+ if (!$item.length) {
840
+ $(this.$activeItems).removeClass('active');
841
+ this.$activeItems = [];
842
+ this.isFocused = this.isInputFocused;
843
+ return;
844
+ }
845
+
846
+ // modify selection
847
+ eventName = e && e.type.toLowerCase();
848
+
849
+ if (eventName === 'mousedown' && this.isShiftDown && this.$activeItems.length) {
850
+ $last = this.$control.children('.active:last');
851
+ begin = Array.prototype.indexOf.apply(this.$control[0].childNodes, [$last[0]]);
852
+ end = Array.prototype.indexOf.apply(this.$control[0].childNodes, [$item[0]]);
853
+ if (begin > end) {
854
+ swap = begin;
855
+ begin = end;
856
+ end = swap;
857
+ }
858
+ for (i = begin; i <= end; i++) {
859
+ item = this.$control[0].childNodes[i];
860
+ if (this.$activeItems.indexOf(item) === -1) {
861
+ $(item).addClass('active');
862
+ this.$activeItems.push(item);
863
+ }
864
+ }
865
+ e.preventDefault();
866
+ } else if ((eventName === 'mousedown' && this.isCtrlDown) || (eventName === 'keydown' && this.isShiftDown)) {
867
+ if ($item.hasClass('active')) {
868
+ idx = this.$activeItems.indexOf($item[0]);
869
+ this.$activeItems.splice(idx, 1);
870
+ $item.removeClass('active');
871
+ } else {
872
+ this.$activeItems.push($item.addClass('active')[0]);
873
+ }
874
+ } else {
875
+ $(this.$activeItems).removeClass('active');
876
+ this.$activeItems = [$item.addClass('active')[0]];
877
+ }
878
+
879
+ this.isFocused = !!this.$activeItems.length || this.isInputFocused;
880
+ };
881
+
882
+ /**
883
+ * Sets the selected item in the dropdown menu
884
+ * of available options.
885
+ *
886
+ * @param {object} $object
887
+ * @param {boolean} scroll
888
+ * @param {boolean} animate
889
+ */
890
+ Selectize.prototype.setActiveOption = function($option, scroll, animate) {
891
+ var height_menu, height_item, y;
892
+ var scroll_top, scroll_bottom;
893
+
894
+ if (this.$activeOption) this.$activeOption.removeClass('active');
895
+ this.$activeOption = null;
896
+
897
+ $option = $($option);
898
+ if (!$option.length) return;
899
+
900
+ this.$activeOption = $option.addClass('active');
901
+
902
+ if (scroll || !isset(scroll)) {
903
+
904
+ height_menu = this.$dropdown.height();
905
+ height_item = this.$activeOption.outerHeight(true);
906
+ scroll = this.$dropdown.scrollTop() || 0;
907
+ y = this.$activeOption.offset().top - this.$dropdown.offset().top + scroll;
908
+ scroll_top = y;
909
+ scroll_bottom = y - height_menu + height_item;
910
+
911
+ if (y + height_item > height_menu - scroll) {
912
+ this.$dropdown.stop().animate({scrollTop: scroll_bottom}, animate ? this.settings.scrollDuration : 0);
913
+ } else if (y < scroll) {
914
+ this.$dropdown.stop().animate({scrollTop: scroll_top}, animate ? this.settings.scrollDuration : 0);
915
+ }
916
+
917
+ }
918
+ };
919
+
920
+ /**
921
+ * Hides the input element out of view, while
922
+ * retaining its focus.
923
+ */
924
+ Selectize.prototype.hideInput = function() {
925
+ this.$control_input.css({opacity: 0});
926
+ this.isInputFocused = false;
927
+ };
928
+
929
+ /**
930
+ * Restores input visibility.
931
+ */
932
+ Selectize.prototype.showInput = function() {
933
+ this.$control_input.css({opacity: 1});
934
+ };
935
+
936
+ /**
937
+ * Gives the control focus. If "trigger" is falsy,
938
+ * focus handlers won't be fired--causing the focus
939
+ * to happen silently in the background.
940
+ *
941
+ * @param {boolean} trigger
942
+ */
943
+ Selectize.prototype.focus = function(trigger) {
944
+ var ignoreFocus = this.ignoreFocus;
945
+ this.ignoreFocus = !trigger;
946
+ this.$control_input[0].focus();
947
+ this.ignoreFocus = ignoreFocus;
948
+ };
949
+
950
+ /**
951
+ * Forces the control out of focus.
952
+ */
953
+ Selectize.prototype.blur = function() {
954
+ this.$control_input.trigger('blur');
955
+ this.setActiveItem(null);
956
+ };
957
+
958
+ /**
959
+ * Splits a search string into an array of
960
+ * individual regexps to be used to match results.
961
+ *
962
+ * @param {string} query
963
+ * @returns {array}
964
+ */
965
+ Selectize.prototype.parseSearchTokens = function(query) {
966
+ query = $.trim(String(query || '').toLowerCase());
967
+ if (!query || !query.length) return [];
968
+
969
+ var i, n, regex, letter;
970
+ var tokens = [];
971
+ var words = query.split(/ +/);
972
+
973
+ for (i = 0, n = words.length; i < n; i++) {
974
+ regex = quoteRegExp(words[i]);
975
+ if (this.settings.diacritics) {
976
+ for (letter in DIACRITICS) {
977
+ if (DIACRITICS.hasOwnProperty(letter)) {
978
+ regex = regex.replace(new RegExp(letter, 'g'), DIACRITICS[letter]);
979
+ }
980
+ }
981
+ }
982
+ tokens.push({
983
+ string : words[i],
984
+ regex : new RegExp(regex, 'i')
985
+ });
986
+ }
987
+
988
+ return tokens;
989
+ };
990
+
991
+ /**
992
+ * Returns a function to be used to score individual results.
993
+ * Results will be sorted by the score (descending). Scores less
994
+ * than or equal to zero (no match) will not be included in the results.
995
+ *
996
+ * @param {object} data
997
+ * @param {object} search
998
+ * @returns {function}
999
+ */
1000
+ Selectize.prototype.getScoreFunction = function(search) {
1001
+ var self = this;
1002
+ var tokens = search.tokens;
1003
+
1004
+ var calculateFieldScore = (function() {
1005
+ if (!tokens.length) {
1006
+ return function() { return 0; };
1007
+ } else if (tokens.length === 1) {
1008
+ return function(value) {
1009
+ var score, pos;
1010
+
1011
+ value = String(value || '').toLowerCase();
1012
+ pos = value.search(tokens[0].regex);
1013
+ if (pos === -1) return 0;
1014
+ score = tokens[0].string.length / value.length;
1015
+ if (pos === 0) score += 0.5;
1016
+ return score;
1017
+ };
1018
+ } else {
1019
+ return function(value) {
1020
+ var score, pos, i, j;
1021
+
1022
+ value = String(value || '').toLowerCase();
1023
+ score = 0;
1024
+ for (i = 0, j = tokens.length; i < j; i++) {
1025
+ pos = value.search(tokens[i].regex);
1026
+ if (pos === -1) return 0;
1027
+ if (pos === 0) score += 0.5;
1028
+ score += tokens[i].string.length / value.length;
1029
+ }
1030
+ return score / tokens.length;
1031
+ };
1032
+ }
1033
+ })();
1034
+
1035
+ var calculateScore = (function() {
1036
+ var fields = self.settings.searchField;
1037
+ if (typeof fields === 'string') {
1038
+ fields = [fields];
1039
+ }
1040
+ if (!fields || !fields.length) {
1041
+ return function() { return 0; };
1042
+ } else if (fields.length === 1) {
1043
+ var field = fields[0];
1044
+ return function(data) {
1045
+ if (!data.hasOwnProperty(field)) return 0;
1046
+ return calculateFieldScore(data[field]);
1047
+ };
1048
+ } else {
1049
+ return function(data) {
1050
+ var n = 0;
1051
+ var score = 0;
1052
+ for (var i = 0, j = fields.length; i < j; i++) {
1053
+ if (data.hasOwnProperty(fields[i])) {
1054
+ score += calculateFieldScore(data[fields[i]]);
1055
+ n++;
1056
+ }
1057
+ }
1058
+ return score / n;
1059
+ };
1060
+ }
1061
+ })();
1062
+
1063
+ return calculateScore;
1064
+ };
1065
+
1066
+ /**
1067
+ * Searches through available options and returns
1068
+ * a sorted array of matches. Includes options that
1069
+ * have already been selected.
1070
+ *
1071
+ * The `settings` parameter can contain:
1072
+ *
1073
+ * - searchField
1074
+ * - sortField
1075
+ * - sortDirection
1076
+ *
1077
+ * Returns an object containing:
1078
+ *
1079
+ * - query {string}
1080
+ * - tokens {array}
1081
+ * - total {int}
1082
+ * - items {array}
1083
+ *
1084
+ * @param {string} query
1085
+ * @param {object} settings
1086
+ * @returns {object}
1087
+ */
1088
+ Selectize.prototype.search = function(query, settings) {
1089
+ var self = this;
1090
+ var value, score, search, calculateScore;
1091
+
1092
+ settings = settings || {};
1093
+ query = $.trim(String(query || '').toLowerCase());
1094
+
1095
+ if (query !== this.lastQuery) {
1096
+ this.lastQuery = query;
1097
+
1098
+ search = {
1099
+ query : query,
1100
+ tokens : this.parseSearchTokens(query),
1101
+ total : 0,
1102
+ items : []
1103
+ };
1104
+
1105
+ // generate result scoring function
1106
+ if (this.settings.score) {
1107
+ calculateScore = this.settings.score.apply(this, [search]);
1108
+ if (typeof calculateScore !== 'function') {
1109
+ throw new Error('Selectize "score" setting must be a function that returns a function');
1110
+ }
1111
+ } else {
1112
+ calculateScore = this.getScoreFunction(search);
1113
+ }
1114
+
1115
+ // perform search and sort
1116
+ if (query.length) {
1117
+ for (value in this.options) {
1118
+ if (this.options.hasOwnProperty(value)) {
1119
+ score = calculateScore(this.options[value]);
1120
+ if (score > 0) {
1121
+ search.items.push({
1122
+ score: score,
1123
+ value: value
1124
+ });
1125
+ }
1126
+ }
1127
+ }
1128
+ search.items.sort(function(a, b) {
1129
+ return b.score - a.score;
1130
+ });
1131
+ } else {
1132
+ for (value in this.options) {
1133
+ if (this.options.hasOwnProperty(value)) {
1134
+ search.items.push({
1135
+ score: 1,
1136
+ value: value
1137
+ });
1138
+ }
1139
+ }
1140
+ if (this.settings.sortField) {
1141
+ search.items.sort((function() {
1142
+ var field = self.settings.sortField;
1143
+ var multiplier = self.settings.sortDirection === 'desc' ? -1 : 1;
1144
+ return function(a, b) {
1145
+ a = a && String(self.options[a.value][field] || '').toLowerCase();
1146
+ b = b && String(self.options[b.value][field] || '').toLowerCase();
1147
+ if (a > b) return 1 * multiplier;
1148
+ if (b > a) return -1 * multiplier;
1149
+ return 0;
1150
+ };
1151
+ })());
1152
+ }
1153
+ }
1154
+ this.currentResults = search;
1155
+ } else {
1156
+ search = $.extend(true, {}, this.currentResults);
1157
+ }
1158
+
1159
+ // apply limits and return
1160
+ return this.prepareResults(search, settings);
1161
+ };
1162
+
1163
+ /**
1164
+ * Filters out any items that have already been selected
1165
+ * and applies search limits.
1166
+ *
1167
+ * @param {object} results
1168
+ * @param {object} settings
1169
+ * @returns {object}
1170
+ */
1171
+ Selectize.prototype.prepareResults = function(search, settings) {
1172
+ if (this.settings.hideSelected) {
1173
+ for (var i = search.items.length - 1; i >= 0; i--) {
1174
+ if (this.items.indexOf(String(search.items[i].value)) !== -1) {
1175
+ search.items.splice(i, 1);
1176
+ }
1177
+ }
1178
+ }
1179
+
1180
+ search.total = search.items.length;
1181
+ if (typeof settings.limit === 'number') {
1182
+ search.items = search.items.slice(0, settings.limit);
1183
+ }
1184
+
1185
+ return search;
1186
+ };
1187
+
1188
+ /**
1189
+ * Refreshes the list of available options shown
1190
+ * in the autocomplete dropdown menu.
1191
+ *
1192
+ * @param {boolean} triggerDropdown
1193
+ */
1194
+ Selectize.prototype.refreshOptions = function(triggerDropdown) {
1195
+ if (typeof triggerDropdown === 'undefined') {
1196
+ triggerDropdown = true;
1197
+ }
1198
+
1199
+ var i, n;
1200
+ var hasCreateOption;
1201
+ var query = this.$control_input.val();
1202
+ var results = this.search(query, {});
1203
+ var html = [];
1204
+
1205
+ // build markup
1206
+ n = results.items.length;
1207
+ if (typeof this.settings.maxOptions === 'number') {
1208
+ n = Math.min(n, this.settings.maxOptions);
1209
+ }
1210
+ for (i = 0; i < n; i++) {
1211
+ html.push(this.render('option', this.options[results.items[i].value]));
1212
+ }
1213
+
1214
+ this.$dropdown.html(html.join(''));
1215
+
1216
+ // highlight matching terms inline
1217
+ if (this.settings.highlight && results.query.length && results.tokens.length) {
1218
+ for (i = 0, n = results.tokens.length; i < n; i++) {
1219
+ highlight(this.$dropdown, results.tokens[i].regex);
1220
+ }
1221
+ }
1222
+
1223
+ // add "selected" class to selected options
1224
+ if (!this.settings.hideSelected) {
1225
+ for (i = 0, n = this.items.length; i < n; i++) {
1226
+ this.getOption(this.items[i]).addClass('selected');
1227
+ }
1228
+ }
1229
+
1230
+ // add create option
1231
+ hasCreateOption = this.settings.create && results.query.length;
1232
+ if (hasCreateOption) {
1233
+ this.$dropdown.prepend(this.render('option_create', {input: query}));
1234
+ }
1235
+
1236
+ // activate
1237
+ this.hasOptions = results.items.length > 0 || hasCreateOption;
1238
+ if (this.hasOptions) {
1239
+ this.setActiveOption(this.$dropdown[0].childNodes[hasCreateOption && results.items.length > 0 ? 1 : 0]);
1240
+ if (triggerDropdown && !this.isOpen) { this.open(); }
1241
+ } else {
1242
+ this.setActiveOption(null);
1243
+ if (triggerDropdown && this.isOpen) { this.close(); }
1244
+ }
1245
+ };
1246
+
1247
+ /**
1248
+ * Adds an available option. If it already exists,
1249
+ * nothing will happen. Note: this does not refresh
1250
+ * the options list dropdown (use `refreshOptions`
1251
+ * for that).
1252
+ *
1253
+ * @param {string} value
1254
+ * @param {object} data
1255
+ */
1256
+ Selectize.prototype.addOption = function(value, data) {
1257
+ if ($.isArray(value)) {
1258
+ for (var i = 0, n = value.length; i < n; i++) {
1259
+ this.addOption(value[i][this.settings.valueField], value[i]);
1260
+ }
1261
+ return;
1262
+ }
1263
+
1264
+ if (this.options.hasOwnProperty(value)) return;
1265
+ value = String(value);
1266
+ this.userOptions[value] = true;
1267
+ this.options[value] = data;
1268
+ this.lastQuery = null;
1269
+ this.trigger('onOptionAdd', value, data);
1270
+ };
1271
+
1272
+ /**
1273
+ * Updates an option available for selection. If
1274
+ * it is visible in the selected items or options
1275
+ * dropdown, it will be re-rendered automatically.
1276
+ *
1277
+ * @param {string} value
1278
+ * @param {object} data
1279
+ */
1280
+ Selectize.prototype.updateOption = function(value, data) {
1281
+ value = String(value);
1282
+ this.options[value] = data;
1283
+ if (isset(this.renderCache['item'])) delete this.renderCache['item'][value];
1284
+ if (isset(this.renderCache['option'])) delete this.renderCache['option'][value];
1285
+
1286
+ if (this.items.indexOf(value) !== -1) {
1287
+ var $item = this.getItem(value);
1288
+ var $item_new = $(this.render('item', data));
1289
+ if ($item.hasClass('active')) $item_new.addClass('active');
1290
+ $item.replaceWith($item_new);
1291
+ }
1292
+
1293
+ if (this.isOpen) {
1294
+ this.refreshOptions(false);
1295
+ }
1296
+ };
1297
+
1298
+ /**
1299
+ * Removes an option.
1300
+ *
1301
+ * @param {string} value
1302
+ */
1303
+ Selectize.prototype.removeOption = function(value) {
1304
+ value = String(value);
1305
+ delete this.userOptions[value];
1306
+ delete this.options[value];
1307
+ this.lastQuery = null;
1308
+ this.trigger('onOptionRemove', value);
1309
+ };
1310
+
1311
+ /**
1312
+ * Returns the jQuery element of the option
1313
+ * matching the given value.
1314
+ *
1315
+ * @param {string} value
1316
+ * @returns {object}
1317
+ */
1318
+ Selectize.prototype.getOption = function(value) {
1319
+ return this.$dropdown.children('[data-value="' + value.replace(/(['"])/g, '\\$1') + '"]:first');
1320
+ };
1321
+
1322
+ /**
1323
+ * Returns the jQuery element of the item
1324
+ * matching the given value.
1325
+ *
1326
+ * @param {string} value
1327
+ * @returns {object}
1328
+ */
1329
+ Selectize.prototype.getItem = function(value) {
1330
+ var i = this.items.indexOf(value);
1331
+ if (i !== -1) {
1332
+ if (i >= this.caretPos) i++;
1333
+ var $el = $(this.$control[0].childNodes[i]);
1334
+ if ($el.attr('data-value') === value) {
1335
+ return $el;
1336
+ }
1337
+ }
1338
+ return $();
1339
+ };
1340
+
1341
+ /**
1342
+ * "Selects" an item. Adds it to the list
1343
+ * at the current caret position.
1344
+ *
1345
+ * @param {string} value
1346
+ */
1347
+ Selectize.prototype.addItem = function(value) {
1348
+ var $item;
1349
+ var self = this;
1350
+ var inputMode = this.settings.mode;
1351
+ var isFull = this.isFull();
1352
+ value = String(value);
1353
+
1354
+ if (inputMode === 'single') this.clear();
1355
+ if (inputMode === 'multi' && isFull) return;
1356
+ if (this.items.indexOf(value) !== -1) return;
1357
+ if (!this.options.hasOwnProperty(value)) return;
1358
+
1359
+ $item = $(this.render('item', this.options[value]));
1360
+ this.items.splice(this.caretPos, 0, value);
1361
+ this.insertAtCaret($item);
1362
+
1363
+ isFull = this.isFull();
1364
+ this.$control.toggleClass('has-items', true);
1365
+ this.$control.toggleClass('full', isFull).toggleClass('not-full', !isFull);
1366
+
1367
+ if (this.isSetup) {
1368
+ // remove the option from the menu
1369
+ var options = this.$dropdown[0].childNodes;
1370
+ for (var i = 0; i < options.length; i++) {
1371
+ var $option = $(options[i]);
1372
+ if ($option.attr('data-value') === value) {
1373
+ $option.remove();
1374
+ if ($option[0] === this.$activeOption[0]) {
1375
+ this.setActiveOption(options.length ? $(options[0]).addClass('active') : null);
1376
+ }
1377
+ break;
1378
+ }
1379
+ }
1380
+
1381
+ // hide the menu if the maximum number of items have been selected or no options are left
1382
+ if (!options.length || (this.settings.maxItems !== null && this.items.length >= this.settings.maxItems)) {
1383
+ this.close();
1384
+ } else {
1385
+ this.positionDropdown();
1386
+ }
1387
+
1388
+ // restore focus to input
1389
+ if (this.isFocused) {
1390
+ window.setTimeout(function() {
1391
+ if (inputMode === 'single') {
1392
+ self.blur();
1393
+ self.focus(false);
1394
+ self.hideInput();
1395
+ } else {
1396
+ self.focus(false);
1397
+ }
1398
+ }, 0);
1399
+ }
1400
+
1401
+ this.updatePlaceholder();
1402
+ this.updateOriginalInput();
1403
+ this.trigger('onItemAdd', value, $item);
1404
+ }
1405
+ };
1406
+
1407
+ /**
1408
+ * Removes the selected item matching
1409
+ * the provided value.
1410
+ *
1411
+ * @param {string} value
1412
+ */
1413
+ Selectize.prototype.removeItem = function(value) {
1414
+ var $item, i, idx;
1415
+
1416
+ $item = (typeof value === 'object') ? value : this.getItem(value);
1417
+ value = String($item.attr('data-value'));
1418
+ i = this.items.indexOf(value);
1419
+
1420
+ if (i !== -1) {
1421
+ $item.remove();
1422
+ if ($item.hasClass('active')) {
1423
+ idx = this.$activeItems.indexOf($item[0]);
1424
+ this.$activeItems.splice(idx, 1);
1425
+ }
1426
+
1427
+ this.items.splice(i, 1);
1428
+ this.$control.toggleClass('has-items', this.items.length > 0);
1429
+ this.$control.removeClass('full').addClass('not-full');
1430
+ this.lastQuery = null;
1431
+ if (!this.settings.persist && this.userOptions.hasOwnProperty(value)) {
1432
+ this.removeOption(value);
1433
+ }
1434
+ this.setCaret(i);
1435
+ this.positionDropdown();
1436
+ this.refreshOptions(false);
1437
+
1438
+ if (!this.hasOptions) { this.close(); }
1439
+ else if (this.isInputFocused) { this.open(); }
1440
+
1441
+ this.updatePlaceholder();
1442
+ this.updateOriginalInput();
1443
+ this.trigger('onItemRemove', value);
1444
+ }
1445
+ };
1446
+
1447
+ /**
1448
+ * Invokes the `create` method provided in the
1449
+ * selectize options that should provide the data
1450
+ * for the new item, given the user input.
1451
+ *
1452
+ * Once this completes, it will be added
1453
+ * to the item list.
1454
+ */
1455
+ Selectize.prototype.createItem = function() {
1456
+ var self = this;
1457
+ var input = $.trim(this.$control_input.val() || '');
1458
+ var caret = this.caretPos;
1459
+ if (!input.length) return;
1460
+ this.lock();
1461
+ this.$control_input[0].blur();
1462
+
1463
+ var setup = (typeof this.settings.create === 'function') ? this.settings.create : function(input) {
1464
+ var data = {};
1465
+ data[self.settings.labelField] = input;
1466
+ data[self.settings.valueField] = input;
1467
+ return data;
1468
+ };
1469
+
1470
+ var create = once(function(data) {
1471
+ self.unlock();
1472
+ self.focus(false);
1473
+
1474
+ var value = data && data[self.settings.valueField];
1475
+ if (!value) return;
1476
+
1477
+ self.addOption(value, data);
1478
+ self.setCaret(caret, false);
1479
+ self.addItem(value);
1480
+ self.refreshOptions(false);
1481
+ self.setTextboxValue('');
1482
+ });
1483
+
1484
+ var output = setup(input, create);
1485
+ if (typeof output === 'object') {
1486
+ create(output);
1487
+ }
1488
+ };
1489
+
1490
+ /**
1491
+ * Re-renders the selected item lists.
1492
+ */
1493
+ Selectize.prototype.refreshItems = function() {
1494
+ var isFull = this.isFull();
1495
+ this.lastQuery = null;
1496
+ this.$control.toggleClass('full', isFull).toggleClass('not-full', !isFull);
1497
+ this.$control.toggleClass('has-items', this.items.length > 0);
1498
+
1499
+ if (this.isSetup) {
1500
+ for (var i = 0; i < this.items.length; i++) {
1501
+ this.addItem(this.items);
1502
+ }
1503
+ }
1504
+
1505
+ this.updateOriginalInput();
1506
+ };
1507
+
1508
+ /**
1509
+ * Determines whether or not more items can be added
1510
+ * to the control without exceeding the user-defined maximum.
1511
+ *
1512
+ * @returns {boolean}
1513
+ */
1514
+ Selectize.prototype.isFull = function() {
1515
+ return this.settings.maxItems !== null && this.items.length >= this.settings.maxItems;
1516
+ };
1517
+
1518
+ /**
1519
+ * Refreshes the original <select> or <input>
1520
+ * element to reflect the current state.
1521
+ */
1522
+ Selectize.prototype.updateOriginalInput = function() {
1523
+ var i, n, options;
1524
+
1525
+ if (this.$input[0].tagName.toLowerCase() === 'select') {
1526
+ options = [];
1527
+ for (i = 0, n = this.items.length; i < n; i++) {
1528
+ options.push('<option value="' + htmlEntities(this.items[i]) + '" selected="selected"></option>');
1529
+ }
1530
+ if (!options.length && !this.$input.attr('multiple')) {
1531
+ options.push('<option value="" selected="selected"></option>');
1532
+ }
1533
+ this.$input.html(options.join(''));
1534
+ } else {
1535
+ this.$input.val(this.getValue());
1536
+ }
1537
+
1538
+ this.$input.trigger('change');
1539
+ if (this.isSetup) {
1540
+ this.trigger('onChange', this.$input.val());
1541
+ }
1542
+ };
1543
+
1544
+ /**
1545
+ * Shows/hide the input placeholder depending
1546
+ * on if there items in the list already.
1547
+ */
1548
+ Selectize.prototype.updatePlaceholder = function() {
1549
+ if (!this.settings.placeholder) return;
1550
+ var $input = this.$control_input;
1551
+
1552
+ if (this.items.length) {
1553
+ $input.removeAttr('placeholder');
1554
+ } else {
1555
+ $input.attr('placeholder', this.settings.placeholder);
1556
+ }
1557
+ $input.triggerHandler('update');
1558
+ };
1559
+
1560
+ /**
1561
+ * Shows the autocomplete dropdown containing
1562
+ * the available options.
1563
+ */
1564
+ Selectize.prototype.open = function() {
1565
+ if (this.isOpen || (this.settings.mode === 'multi' && this.isFull())) return;
1566
+ this.isOpen = true;
1567
+ this.positionDropdown();
1568
+ this.$control.addClass('dropdown-active');
1569
+ this.$dropdown.show();
1570
+ this.trigger('onDropdownOpen', this.$dropdown);
1571
+ };
1572
+
1573
+ /**
1574
+ * Closes the autocomplete dropdown menu.
1575
+ */
1576
+ Selectize.prototype.close = function() {
1577
+ if (!this.isOpen) return;
1578
+ this.$dropdown.hide();
1579
+ this.$control.removeClass('dropdown-active');
1580
+ this.isOpen = false;
1581
+ this.trigger('onDropdownClose', this.$dropdown);
1582
+ };
1583
+
1584
+ /**
1585
+ * Calculates and applies the appropriate
1586
+ * position of the dropdown.
1587
+ */
1588
+ Selectize.prototype.positionDropdown = function() {
1589
+ var $control = this.$control;
1590
+ var offset = $control.position();
1591
+ offset.top += $control.outerHeight(true);
1592
+
1593
+ this.$dropdown.css({
1594
+ width : $control.outerWidth(),
1595
+ top : offset.top,
1596
+ left : offset.left
1597
+ });
1598
+ };
1599
+
1600
+ /**
1601
+ * Resets / clears all selected items
1602
+ * from the control.
1603
+ */
1604
+ Selectize.prototype.clear = function() {
1605
+ if (!this.items.length) return;
1606
+ this.$control.removeClass('has-items');
1607
+ this.$control.children(':not(input)').remove();
1608
+ this.items = [];
1609
+ this.setCaret(0);
1610
+ this.updatePlaceholder();
1611
+ this.updateOriginalInput();
1612
+ this.trigger('onClear');
1613
+ };
1614
+
1615
+ /**
1616
+ * A helper method for inserting an element
1617
+ * at the current caret position.
1618
+ *
1619
+ * @param {object} $el
1620
+ */
1621
+ Selectize.prototype.insertAtCaret = function($el) {
1622
+ var caret = Math.min(this.caretPos, this.items.length);
1623
+ if (caret === 0) {
1624
+ this.$control.prepend($el);
1625
+ } else {
1626
+ $(this.$control[0].childNodes[caret]).before($el);
1627
+ }
1628
+ this.setCaret(caret + 1);
1629
+ };
1630
+
1631
+ /**
1632
+ * Removes the current selected item(s).
1633
+ *
1634
+ * @param {object} e (optional)
1635
+ */
1636
+ Selectize.prototype.deleteSelection = function(e) {
1637
+ var i, n, direction, selection, values, caret, $tail;
1638
+
1639
+ direction = (e.keyCode === KEY_BACKSPACE) ? -1 : 1;
1640
+ selection = getSelection(this.$control_input[0]);
1641
+ if (this.$activeItems.length) {
1642
+ $tail = this.$control.children('.active:' + (direction > 0 ? 'last' : 'first'));
1643
+ caret = Array.prototype.indexOf.apply(this.$control[0].childNodes, [$tail[0]]);
1644
+ if (this.$activeItems.length > 1 && direction > 0) { caret--; }
1645
+
1646
+ values = [];
1647
+ for (i = 0, n = this.$activeItems.length; i < n; i++) {
1648
+ values.push($(this.$activeItems[i]).attr('data-value'));
1649
+ }
1650
+ while (values.length) {
1651
+ this.removeItem(values.pop());
1652
+ }
1653
+
1654
+ this.setCaret(caret);
1655
+ e.preventDefault();
1656
+ e.stopPropagation();
1657
+ } else if ((this.isInputFocused || this.settings.mode === 'single') && this.items.length) {
1658
+ if (direction < 0 && selection.start === 0 && selection.length === 0) {
1659
+ this.removeItem(this.items[this.caretPos - 1]);
1660
+ } else if (direction > 0 && selection.start === this.$control_input.val().length) {
1661
+ this.removeItem(this.items[this.caretPos]);
1662
+ }
1663
+ }
1664
+ };
1665
+
1666
+ /**
1667
+ * Selects the previous / next item (depending
1668
+ * on the `direction` argument).
1669
+ *
1670
+ * > 0 - right
1671
+ * < 0 - left
1672
+ *
1673
+ * @param {int} direction
1674
+ * @param {object} e (optional)
1675
+ */
1676
+ Selectize.prototype.advanceSelection = function(direction, e) {
1677
+ if (direction === 0) return;
1678
+ var tail = direction > 0 ? 'last' : 'first';
1679
+ var selection = getSelection(this.$control_input[0]);
1680
+
1681
+ if (this.isInputFocused) {
1682
+ var valueLength = this.$control_input.val().length;
1683
+ var cursorAtEdge = direction < 0
1684
+ ? selection.start === 0 && selection.length === 0
1685
+ : selection.start === valueLength;
1686
+
1687
+ if (cursorAtEdge && !valueLength) {
1688
+ this.advanceCaret(direction, e);
1689
+ }
1690
+ } else {
1691
+ var $tail = this.$control.children('.active:' + tail);
1692
+ if ($tail.length) {
1693
+ var idx = Array.prototype.indexOf.apply(this.$control[0].childNodes, [$tail[0]]);
1694
+ this.setCaret(direction > 0 ? idx + 1 : idx);
1695
+ }
1696
+ }
1697
+ };
1698
+
1699
+ /**
1700
+ * Moves the caret left / right.
1701
+ *
1702
+ * @param {int} direction
1703
+ * @param {object} e (optional)
1704
+ */
1705
+ Selectize.prototype.advanceCaret = function(direction, e) {
1706
+ if (direction === 0) return;
1707
+ var fn = direction > 0 ? 'next' : 'prev';
1708
+ if (this.isShiftDown) {
1709
+ var $adj = this.$control_input[fn]();
1710
+ if ($adj.length) {
1711
+ this.blur();
1712
+ this.setActiveItem($adj);
1713
+ e && e.preventDefault();
1714
+ }
1715
+ } else {
1716
+ this.setCaret(this.caretPos + direction);
1717
+ }
1718
+ };
1719
+
1720
+ /**
1721
+ * Moves the caret to the specified index.
1722
+ *
1723
+ * @param {int} i
1724
+ * @param {boolean} focus
1725
+ */
1726
+ Selectize.prototype.setCaret = function(i, focus) {
1727
+ if (this.settings.mode === 'single' || this.isFull()) {
1728
+ i = this.items.length;
1729
+ } else {
1730
+ i = Math.max(0, Math.min(this.items.length, i));
1731
+ }
1732
+
1733
+ this.ignoreFocus = true;
1734
+ this.$control_input.detach();
1735
+ if (i === this.items.length) {
1736
+ this.$control.append(this.$control_input);
1737
+ } else {
1738
+ this.$control_input.insertBefore(this.$control.children(':not(input)')[i]);
1739
+ }
1740
+ this.ignoreFocus = false;
1741
+ if (focus && this.isSetup) {
1742
+ this.focus(true);
1743
+ }
1744
+
1745
+ this.caretPos = i;
1746
+ };
1747
+
1748
+ /**
1749
+ * Disables user input on the control. Used while
1750
+ * items are being asynchronously created.
1751
+ */
1752
+ Selectize.prototype.lock = function() {
1753
+ this.isLocked = true;
1754
+ this.$control.addClass('locked');
1755
+ };
1756
+
1757
+ /**
1758
+ * Re-enables user input on the control.
1759
+ */
1760
+ Selectize.prototype.unlock = function() {
1761
+ this.isLocked = false;
1762
+ this.$control.removeClass('locked');
1763
+ };
1764
+
1765
+ /**
1766
+ * A helper method for rendering "item" and
1767
+ * "option" templates, given the data.
1768
+ *
1769
+ * @param {string} templateName
1770
+ * @param {object} data
1771
+ * @returns {string}
1772
+ */
1773
+ Selectize.prototype.render = function(templateName, data) {
1774
+ cache = isset(cache) ? cache : true;
1775
+
1776
+ var value, label;
1777
+ var html = '';
1778
+ var cache = false;
1779
+
1780
+ if (['option', 'item'].indexOf(templateName) !== -1) {
1781
+ value = data[this.settings.valueField];
1782
+ cache = isset(value);
1783
+ }
1784
+
1785
+ if (cache) {
1786
+ if (!isset(this.renderCache[templateName])) {
1787
+ this.renderCache[templateName] = {};
1788
+ }
1789
+ if (this.renderCache[templateName].hasOwnProperty(value)) {
1790
+ return this.renderCache[templateName][value];
1791
+ }
1792
+ }
1793
+
1794
+ if (this.settings.render && typeof this.settings.render[templateName] === 'function') {
1795
+ html = this.settings.render[templateName].apply(this, [data]);
1796
+ } else {
1797
+ label = data[this.settings.labelField];
1798
+ switch (templateName) {
1799
+ case 'option':
1800
+ html = '<div class="option">' + label + '</div>';
1801
+ break;
1802
+ case 'item':
1803
+ html = '<div class="item">' + label + '</div>';
1804
+ break;
1805
+ case 'option_create':
1806
+ html = '<div class="create">Create <strong>' + htmlEntities(data.input) + '</strong>&hellip;</div>';
1807
+ break;
1808
+ }
1809
+ }
1810
+
1811
+ if (isset(value)) {
1812
+ html = html.replace(/^[\ ]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i, '<$1 data-value="' + value + '"');
1813
+ }
1814
+ if (cache) {
1815
+ this.renderCache[templateName][value] = html;
1816
+ }
1817
+
1818
+ return html;
1819
+ };
1820
+
1821
+ return Selectize;
1822
+
1823
+ }));