aerogel-admin 1.4.4

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