selectize-rails 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }));