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