activeadmin_selectize 0.1.0

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