aerogel-admin 1.4.4

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