selectize-rails 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -41,6 +41,7 @@ See the [demo page of Brian Reavis](http://brianreavis.github.io/selectize.js/)
41
41
 
42
42
  | Version | Notes |
43
43
  | -------:| ----------------------------------------------------------- |
44
+ | 0.8.0 | Update to v0.8.0 of selectize.js |
44
45
  | 0.7.7 | Update to v0.7.7 of selectize.js |
45
46
  | 0.7.6 | Update to v0.7.6 of selectize.js |
46
47
  | 0.7.5 | Update to v0.7.5 of selectize.js |
@@ -1,5 +1,5 @@
1
1
  module Selectize
2
2
  module Rails
3
- VERSION = "0.7.7"
3
+ VERSION = "0.8.0"
4
4
  end
5
5
  end
@@ -176,12 +176,120 @@
176
176
  return scoreObject(tokens[0], data);
177
177
  };
178
178
  }
179
- return function(data) {
180
- for (var i = 0, sum = 0; i < token_count; i++) {
181
- sum += scoreObject(tokens[i], data);
182
- }
183
- return sum / token_count;
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];
184
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
+ }
185
293
  };
186
294
 
187
295
  /**
@@ -195,8 +303,19 @@
195
303
  */
196
304
  Sifter.prototype.prepareSearch = function(query, options) {
197
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
+
198
317
  return {
199
- options : extend({}, options),
318
+ options : options,
200
319
  query : String(query || '').toLowerCase(),
201
320
  tokens : this.tokenize(query),
202
321
  total : 0,
@@ -210,9 +329,9 @@
210
329
  * The `options` parameter can contain:
211
330
  *
212
331
  * - fields {string|array}
213
- * - sort {string}
214
- * - direction {string}
332
+ * - sort {array}
215
333
  * - score {function}
334
+ * - filter {bool}
216
335
  * - limit {integer}
217
336
  *
218
337
  * Returns an object containing:
@@ -229,41 +348,33 @@
229
348
  */
230
349
  Sifter.prototype.search = function(query, options) {
231
350
  var self = this, value, score, search, calculateScore;
351
+ var fn_sort;
352
+ var fn_score;
232
353
 
233
354
  search = this.prepareSearch(query, options);
234
355
  options = search.options;
235
356
  query = search.query;
236
357
 
237
358
  // generate result scoring function
238
- if (!is_array(options.fields)) options.fields = [options.fields];
239
- calculateScore = options.score || self.getScoreFunction(search);
359
+ fn_score = options.score || self.getScoreFunction(search);
240
360
 
241
361
  // perform search and sort
242
362
  if (query.length) {
243
363
  self.iterator(self.items, function(item, id) {
244
- score = calculateScore(item);
245
- if (score > 0) {
364
+ score = fn_score(item);
365
+ if (options.filter === false || score > 0) {
246
366
  search.items.push({'score': score, 'id': id});
247
367
  }
248
368
  });
249
- search.items.sort(function(a, b) {
250
- return b.score - a.score;
251
- });
252
369
  } else {
253
370
  self.iterator(self.items, function(item, id) {
254
371
  search.items.push({'score': 1, 'id': id});
255
372
  });
256
- if (options.sort) {
257
- search.items.sort((function() {
258
- var field = options.sort;
259
- var multiplier = options.direction === 'desc' ? -1 : 1;
260
- return function(a, b) {
261
- return cmp(self.items[a.id][field], self.items[b.id][field]) * multiplier;
262
- };
263
- })());
264
- }
265
373
  }
266
374
 
375
+ fn_sort = self.getSortFunction(search, options);
376
+ if (fn_sort) search.items.sort(fn_sort);
377
+
267
378
  // apply limits
268
379
  search.total = search.items.length;
269
380
  if (typeof options.limit === 'number') {
@@ -315,7 +426,8 @@
315
426
 
316
427
  var DIACRITICS = {
317
428
  'a': '[aÀÁÂÃÄÅàáâãäå]',
318
- 'c': '[cÇç]',
429
+ 'c': '[cÇçćĆčČ]',
430
+ 'd': '[dđĐ]',
319
431
  'e': '[eÈÉÊËèéêë]',
320
432
  'i': '[iÌÍÎÏìíîï]',
321
433
  'n': '[nÑñ]',
@@ -330,9 +442,10 @@
330
442
  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
331
443
 
332
444
  return Sifter;
333
-
334
445
  }));
335
446
 
447
+
448
+
336
449
  /**
337
450
  * microplugin.js
338
451
  * Copyright (c) 2013 Brian Reavis & contributors
@@ -470,7 +583,7 @@
470
583
  }));
471
584
 
472
585
  /**
473
- * selectize.js (v0.7.7)
586
+ * selectize.js (v0.8.0)
474
587
  * Copyright (c) 2013 Brian Reavis & contributors
475
588
  *
476
589
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
@@ -830,7 +943,7 @@
830
943
  left: -99999,
831
944
  width: 'auto',
832
945
  padding: 0,
833
- whiteSpace: 'nowrap'
946
+ whiteSpace: 'pre'
834
947
  }).text(str).appendTo('body');
835
948
 
836
949
  transferStyles($parent, $test, [
@@ -910,22 +1023,29 @@
910
1023
  };
911
1024
 
912
1025
  var Selectize = function($input, settings) {
913
- var key, i, n, self = this;
914
- $input[0].selectize = self;
1026
+ var key, i, n, dir, input, self = this;
1027
+ input = $input[0];
1028
+ input.selectize = self;
1029
+
1030
+ // detect rtl environment
1031
+ dir = window.getComputedStyle ? window.getComputedStyle(input, null).getPropertyValue('direction') : input.currentStyle && input.currentStyle.direction;
1032
+ dir = dir || $input.parents('[dir]:first').attr('dir') || '';
915
1033
 
916
1034
  // setup default state
917
1035
  $.extend(self, {
918
1036
  settings : settings,
919
1037
  $input : $input,
920
- tagType : $input[0].tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT,
1038
+ tagType : input.tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT,
1039
+ rtl : /rtl/i.test(dir),
921
1040
 
922
1041
  eventNS : '.selectize' + (++Selectize.count),
923
1042
  highlightedValue : null,
924
1043
  isOpen : false,
925
1044
  isDisabled : false,
1045
+ isRequired : $input.is(':required'),
1046
+ isInvalid : false,
926
1047
  isLocked : false,
927
1048
  isFocused : false,
928
- isInputFocused : false,
929
1049
  isInputHidden : false,
930
1050
  isSetup : false,
931
1051
  isShiftDown : false,
@@ -1014,7 +1134,7 @@
1014
1134
 
1015
1135
  $wrapper = $('<div>').addClass(settings.wrapperClass).addClass(classes).addClass(inputMode);
1016
1136
  $control = $('<div>').addClass(settings.inputClass).addClass('items').appendTo($wrapper);
1017
- $control_input = $('<input type="text">').appendTo($control).attr('tabindex', tab_index);
1137
+ $control_input = $('<input type="text" autocomplete="off">').appendTo($control).attr('tabindex', tab_index);
1018
1138
  $dropdown_parent = $(settings.dropdownParent || $wrapper);
1019
1139
  $dropdown = $('<div>').addClass(settings.dropdownClass).addClass(classes).addClass(inputMode).hide().appendTo($dropdown_parent);
1020
1140
  $dropdown_content = $('<div>').addClass(settings.dropdownContentClass).appendTo($dropdown);
@@ -1043,27 +1163,16 @@
1043
1163
  self.$dropdown = $dropdown;
1044
1164
  self.$dropdown_content = $dropdown_content;
1045
1165
 
1046
- $control.on('mousedown', function(e) {
1047
- if (!e.isDefaultPrevented()) {
1048
- window.setTimeout(function() {
1049
- self.focus(true);
1050
- }, 0);
1051
- }
1052
- });
1053
-
1054
- // necessary for mobile webkit devices (manual focus triggering
1055
- // is ignored unless invoked within a click event)
1056
- $control.on('click', function(e) {
1057
- if (!self.isInputFocused) {
1058
- self.focus(true);
1059
- }
1060
- });
1061
-
1062
1166
  $dropdown.on('mouseenter', '[data-selectable]', function() { return self.onOptionHover.apply(self, arguments); });
1063
1167
  $dropdown.on('mousedown', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); });
1064
1168
  watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); });
1065
1169
  autoGrow($control_input);
1066
1170
 
1171
+ $control.on({
1172
+ mousedown : function() { return self.onMouseDown.apply(self, arguments); },
1173
+ click : function() { return self.onClick.apply(self, arguments); }
1174
+ });
1175
+
1067
1176
  $control_input.on({
1068
1177
  mousedown : function(e) { e.stopPropagation(); },
1069
1178
  keydown : function() { return self.onKeyDown.apply(self, arguments); },
@@ -1090,13 +1199,7 @@
1090
1199
  if (self.isFocused) {
1091
1200
  // prevent events on the dropdown scrollbar from causing the control to blur
1092
1201
  if (e.target === self.$dropdown[0] || e.target.parentNode === self.$dropdown[0]) {
1093
- var ignoreFocus = self.ignoreFocus;
1094
- self.ignoreFocus = true;
1095
- window.setTimeout(function() {
1096
- self.ignoreFocus = ignoreFocus;
1097
- self.focus(false);
1098
- }, 0);
1099
- return;
1202
+ return false;
1100
1203
  }
1101
1204
  // blur on click outside
1102
1205
  if (!self.$control.has(e.target).length && e.target !== self.$control[0]) {
@@ -1114,16 +1217,25 @@
1114
1217
  self.ignoreHover = false;
1115
1218
  });
1116
1219
 
1117
- self.$input.attr('tabindex',-1).hide().after(self.$wrapper);
1220
+ self.$input.attr('tabindex', -1).hide().after(self.$wrapper);
1118
1221
 
1119
1222
  if ($.isArray(settings.items)) {
1120
1223
  self.setValue(settings.items);
1121
1224
  delete settings.items;
1122
1225
  }
1123
1226
 
1227
+ // feature detect for the validation API
1228
+ if (self.$input[0].validity) {
1229
+ self.$input.on('invalid' + eventNS, function(e) {
1230
+ e.preventDefault();
1231
+ self.isInvalid = true;
1232
+ self.refreshState();
1233
+ });
1234
+ }
1235
+
1124
1236
  self.updateOriginalInput();
1125
1237
  self.refreshItems();
1126
- self.refreshClasses();
1238
+ self.refreshState();
1127
1239
  self.updatePlaceholder();
1128
1240
  self.isSetup = true;
1129
1241
 
@@ -1163,7 +1275,7 @@
1163
1275
  },
1164
1276
  'option_create': function(data, escape) {
1165
1277
  return '<div class="create">Add <strong>' + escape(data.input) + '</strong>&hellip;</div>';
1166
- },
1278
+ }
1167
1279
  };
1168
1280
 
1169
1281
  self.settings.render = $.extend({}, templates, self.settings.render);
@@ -1196,6 +1308,59 @@
1196
1308
  }
1197
1309
  },
1198
1310
 
1311
+ /**
1312
+ * Triggered when the main control element
1313
+ * has a click event.
1314
+ *
1315
+ * @param {object} e
1316
+ * @return {boolean}
1317
+ */
1318
+ onClick: function(e) {
1319
+ var self = this;
1320
+
1321
+ // necessary for mobile webkit devices (manual focus triggering
1322
+ // is ignored unless invoked within a click event)
1323
+ if (!self.isFocused) {
1324
+ self.focus();
1325
+ e.preventDefault();
1326
+ }
1327
+ },
1328
+
1329
+ /**
1330
+ * Triggered when the main control element
1331
+ * has a mouse down event.
1332
+ *
1333
+ * @param {object} e
1334
+ * @return {boolean}
1335
+ */
1336
+ onMouseDown: function(e) {
1337
+ var self = this;
1338
+ var defaultPrevented = e.isDefaultPrevented();
1339
+ var $target = $(e.target);
1340
+
1341
+ if (self.isFocused) {
1342
+ // retain focus by preventing native handling. if the
1343
+ // event target is the input it should not be modified.
1344
+ // otherwise, text selection within the input won't work.
1345
+ if (e.target !== self.$control_input[0]) {
1346
+ if (self.settings.mode === 'single') {
1347
+ // toggle dropdown
1348
+ self.isOpen ? self.close() : self.open();
1349
+ } else if (!defaultPrevented) {
1350
+ self.setActiveItem(null);
1351
+ }
1352
+ return false;
1353
+ }
1354
+ } else {
1355
+ // give control focus
1356
+ if (!defaultPrevented) {
1357
+ window.setTimeout(function() {
1358
+ self.focus();
1359
+ }, 0);
1360
+ }
1361
+ }
1362
+ },
1363
+
1199
1364
  /**
1200
1365
  * Triggered when the value of the control has been changed.
1201
1366
  * This should propagate the event to the original DOM
@@ -1267,7 +1432,7 @@
1267
1432
  e.preventDefault();
1268
1433
  return;
1269
1434
  case KEY_RETURN:
1270
- if (self.$activeOption) {
1435
+ if (self.isOpen && self.$activeOption) {
1271
1436
  self.onOptionSelect({currentTarget: self.$activeOption});
1272
1437
  }
1273
1438
  e.preventDefault();
@@ -1342,7 +1507,6 @@
1342
1507
  onFocus: function(e) {
1343
1508
  var self = this;
1344
1509
 
1345
- self.isInputFocused = true;
1346
1510
  self.isFocused = true;
1347
1511
  if (self.isDisabled) {
1348
1512
  self.blur();
@@ -1353,10 +1517,13 @@
1353
1517
  if (self.ignoreFocus) return;
1354
1518
  if (self.settings.preload === 'focus') self.onSearchChange('');
1355
1519
 
1356
- self.showInput();
1357
- self.setActiveItem(null);
1358
- self.refreshOptions(!!self.settings.openOnFocus);
1359
- self.refreshClasses();
1520
+ if (!self.$activeItems.length) {
1521
+ self.showInput();
1522
+ self.setActiveItem(null);
1523
+ self.refreshOptions(!!self.settings.openOnFocus);
1524
+ }
1525
+
1526
+ self.refreshState();
1360
1527
  },
1361
1528
 
1362
1529
  /**
@@ -1367,7 +1534,7 @@
1367
1534
  */
1368
1535
  onBlur: function(e) {
1369
1536
  var self = this;
1370
- self.isInputFocused = false;
1537
+ self.isFocused = false;
1371
1538
  if (self.ignoreFocus) return;
1372
1539
 
1373
1540
  self.close();
@@ -1375,8 +1542,7 @@
1375
1542
  self.setActiveItem(null);
1376
1543
  self.setActiveOption(null);
1377
1544
  self.setCaret(self.items.length);
1378
- self.isFocused = false;
1379
- self.refreshClasses();
1545
+ self.refreshState();
1380
1546
  },
1381
1547
 
1382
1548
  /**
@@ -1401,9 +1567,10 @@
1401
1567
  onOptionSelect: function(e) {
1402
1568
  var value, $target, $option, self = this;
1403
1569
 
1404
- e.preventDefault && e.preventDefault();
1405
- e.stopPropagation && e.stopPropagation();
1406
- self.focus(false);
1570
+ if (e.preventDefault) {
1571
+ e.preventDefault();
1572
+ e.stopPropagation();
1573
+ }
1407
1574
 
1408
1575
  $target = $(e.currentTarget);
1409
1576
  if ($target.hasClass('create')) {
@@ -1430,11 +1597,10 @@
1430
1597
  onItemSelect: function(e) {
1431
1598
  var self = this;
1432
1599
 
1600
+ if (self.isLocked) return;
1433
1601
  if (self.settings.mode === 'multi') {
1434
1602
  e.preventDefault();
1435
1603
  self.setActiveItem(e.currentTarget, e);
1436
- self.focus(false);
1437
- self.hideInput();
1438
1604
  }
1439
1605
  },
1440
1606
 
@@ -1454,8 +1620,7 @@
1454
1620
  self.loading = Math.max(self.loading - 1, 0);
1455
1621
  if (results && results.length) {
1456
1622
  self.addOption(results);
1457
- self.refreshOptions(false);
1458
- if (self.isInputFocused) self.open();
1623
+ self.refreshOptions(self.isFocused && !self.isInputHidden);
1459
1624
  }
1460
1625
  if (!self.loading) {
1461
1626
  $wrapper.removeClass('loading');
@@ -1517,13 +1682,16 @@
1517
1682
  var i, idx, begin, end, item, swap;
1518
1683
  var $last;
1519
1684
 
1685
+ if (self.settings.mode === 'single') return;
1520
1686
  $item = $($item);
1521
1687
 
1522
1688
  // clear the active selection
1523
1689
  if (!$item.length) {
1524
1690
  $(self.$activeItems).removeClass('active');
1525
1691
  self.$activeItems = [];
1526
- self.isFocused = self.isInputFocused;
1692
+ if (self.isFocused) {
1693
+ self.showInput();
1694
+ }
1527
1695
  return;
1528
1696
  }
1529
1697
 
@@ -1560,7 +1728,11 @@
1560
1728
  self.$activeItems = [$item.addClass('active')[0]];
1561
1729
  }
1562
1730
 
1563
- self.isFocused = !!self.$activeItems.length || self.isInputFocused;
1731
+ // ensure control has focus
1732
+ self.hideInput();
1733
+ if (!this.isFocused) {
1734
+ self.focus();
1735
+ }
1564
1736
  },
1565
1737
 
1566
1738
  /**
@@ -1607,8 +1779,11 @@
1607
1779
  */
1608
1780
  selectAll: function() {
1609
1781
  this.$activeItems = Array.prototype.slice.apply(this.$control.children(':not(input)').addClass('active'));
1610
- this.isFocused = true;
1611
- if (this.$activeItems.length) this.hideInput();
1782
+ if (this.$activeItems.length) {
1783
+ this.hideInput();
1784
+ this.close();
1785
+ }
1786
+ this.focus();
1612
1787
  },
1613
1788
 
1614
1789
  /**
@@ -1618,9 +1793,8 @@
1618
1793
  hideInput: function() {
1619
1794
  var self = this;
1620
1795
 
1621
- self.close();
1622
1796
  self.setTextboxValue('');
1623
- self.$control_input.css({opacity: 0, position: 'absolute', left: -10000});
1797
+ self.$control_input.css({opacity: 0, position: 'absolute', left: self.rtl ? 10000 : -10000});
1624
1798
  self.isInputHidden = true;
1625
1799
  },
1626
1800
 
@@ -1639,16 +1813,15 @@
1639
1813
  *
1640
1814
  * @param {boolean} trigger
1641
1815
  */
1642
- focus: function(trigger) {
1816
+ focus: function() {
1643
1817
  var self = this;
1644
-
1645
1818
  if (self.isDisabled) return;
1819
+
1646
1820
  self.ignoreFocus = true;
1647
1821
  self.$control_input[0].focus();
1648
- self.isInputFocused = true;
1649
1822
  window.setTimeout(function() {
1650
1823
  self.ignoreFocus = false;
1651
- if (trigger) self.onFocus();
1824
+ self.onFocus();
1652
1825
  }, 0);
1653
1826
  },
1654
1827
 
@@ -1681,12 +1854,15 @@
1681
1854
  */
1682
1855
  getSearchOptions: function() {
1683
1856
  var settings = this.settings;
1684
- var fields = settings.searchField;
1857
+ var sort = settings.sortField;
1858
+ if (typeof sort === 'string') {
1859
+ sort = {field: sort};
1860
+ }
1685
1861
 
1686
1862
  return {
1687
- fields : $.isArray(fields) ? fields : [fields],
1688
- sort : settings.sortField,
1689
- direction : settings.sortDirection,
1863
+ fields : settings.searchField,
1864
+ conjunction : settings.searchConjunction,
1865
+ sort : sort
1690
1866
  };
1691
1867
  },
1692
1868
 
@@ -1746,17 +1922,18 @@
1746
1922
  * @param {boolean} triggerDropdown
1747
1923
  */
1748
1924
  refreshOptions: function(triggerDropdown) {
1925
+ var i, j, k, n, groups, groups_order, option, option_html, optgroup, optgroups, html, html_children, has_create_option;
1926
+ var $active, $active_before, $create;
1927
+
1749
1928
  if (typeof triggerDropdown === 'undefined') {
1750
1929
  triggerDropdown = true;
1751
1930
  }
1752
1931
 
1753
- var self = this;
1754
- var i, n, groups, groups_order, option, optgroup, html, html_children;
1755
- var hasCreateOption;
1756
- var query = self.$control_input.val();
1757
- var results = self.search(query);
1758
- var $active, $create;
1932
+ var self = this;
1933
+ var query = self.$control_input.val();
1934
+ var results = self.search(query);
1759
1935
  var $dropdown_content = self.$dropdown_content;
1936
+ var active_before = self.$activeOption && hash_key(self.$activeOption.attr('data-value'));
1760
1937
 
1761
1938
  // build markup
1762
1939
  n = results.items.length;
@@ -1777,16 +1954,22 @@
1777
1954
  }
1778
1955
 
1779
1956
  for (i = 0; i < n; i++) {
1780
- option = self.options[results.items[i].id];
1781
- optgroup = option[self.settings.optgroupField] || '';
1782
- if (!self.optgroups.hasOwnProperty(optgroup)) {
1783
- optgroup = '';
1784
- }
1785
- if (!groups.hasOwnProperty(optgroup)) {
1786
- groups[optgroup] = [];
1787
- groups_order.push(optgroup);
1957
+ option = self.options[results.items[i].id];
1958
+ option_html = self.render('option', option);
1959
+ optgroup = option[self.settings.optgroupField] || '';
1960
+ optgroups = $.isArray(optgroup) ? optgroup : [optgroup];
1961
+
1962
+ for (j = 0, k = optgroups && optgroups.length; j < k; j++) {
1963
+ optgroup = optgroups[j];
1964
+ if (!self.optgroups.hasOwnProperty(optgroup)) {
1965
+ optgroup = '';
1966
+ }
1967
+ if (!groups.hasOwnProperty(optgroup)) {
1968
+ groups[optgroup] = [];
1969
+ groups_order.push(optgroup);
1970
+ }
1971
+ groups[optgroup].push(option_html);
1788
1972
  }
1789
- groups[optgroup].push(self.render('option', option));
1790
1973
  }
1791
1974
 
1792
1975
  // render optgroup headers & join groups
@@ -1823,20 +2006,28 @@
1823
2006
  }
1824
2007
 
1825
2008
  // add create option
1826
- hasCreateOption = self.settings.create && results.query.length;
1827
- if (hasCreateOption) {
2009
+ has_create_option = self.settings.create && results.query.length;
2010
+ if (has_create_option) {
1828
2011
  $dropdown_content.prepend(self.render('option_create', {input: query}));
1829
2012
  $create = $($dropdown_content[0].childNodes[0]);
1830
2013
  }
1831
2014
 
1832
2015
  // activate
1833
- self.hasOptions = results.items.length > 0 || hasCreateOption;
2016
+ self.hasOptions = results.items.length > 0 || has_create_option;
1834
2017
  if (self.hasOptions) {
1835
2018
  if (results.items.length > 0) {
1836
- if ($create) {
1837
- $active = self.getAdjacentOption($create, 1);
1838
- } else {
1839
- $active = $dropdown_content.find("[data-selectable]").first();
2019
+ $active_before = active_before && self.getOption(active_before);
2020
+ if ($active_before && $active_before.length) {
2021
+ $active = $active_before;
2022
+ } else if (self.settings.mode === 'single' && self.items.length) {
2023
+ $active = self.getOption(self.items[0]);
2024
+ }
2025
+ if (!$active || !$active.length) {
2026
+ if ($create && !self.settings.addPrecedence) {
2027
+ $active = self.getAdjacentOption($create, 1);
2028
+ } else {
2029
+ $active = $dropdown_content.find('[data-selectable]:first');
2030
+ }
1840
2031
  }
1841
2032
  } else {
1842
2033
  $active = $create;
@@ -2060,7 +2251,7 @@
2060
2251
  $item = $(self.render('item', self.options[value]));
2061
2252
  self.items.splice(self.caretPos, 0, value);
2062
2253
  self.insertAtCaret($item);
2063
- self.refreshClasses();
2254
+ self.refreshState();
2064
2255
 
2065
2256
  if (self.isSetup) {
2066
2257
  options = self.$dropdown_content.find('[data-selectable]');
@@ -2080,19 +2271,6 @@
2080
2271
  self.positionDropdown();
2081
2272
  }
2082
2273
 
2083
- // restore focus to input
2084
- if (self.isFocused) {
2085
- window.setTimeout(function() {
2086
- if (inputMode === 'single') {
2087
- self.blur();
2088
- self.focus(false);
2089
- self.hideInput();
2090
- } else {
2091
- self.focus(false);
2092
- }
2093
- }, 0);
2094
- }
2095
-
2096
2274
  self.updatePlaceholder();
2097
2275
  self.trigger('item_add', value, $item);
2098
2276
  self.updateOriginalInput();
@@ -2131,7 +2309,7 @@
2131
2309
  self.setCaret(self.caretPos - 1);
2132
2310
  }
2133
2311
 
2134
- self.refreshClasses();
2312
+ self.refreshState();
2135
2313
  self.updatePlaceholder();
2136
2314
  self.updateOriginalInput();
2137
2315
  self.positionDropdown();
@@ -2163,7 +2341,6 @@
2163
2341
 
2164
2342
  var create = once(function(data) {
2165
2343
  self.unlock();
2166
- self.focus(false);
2167
2344
 
2168
2345
  if (!data || typeof data !== 'object') return;
2169
2346
  var value = hash_key(data[self.settings.valueField]);
@@ -2174,7 +2351,6 @@
2174
2351
  self.setCaret(caret);
2175
2352
  self.addItem(value);
2176
2353
  self.refreshOptions(self.settings.mode !== 'single');
2177
- self.focus(false);
2178
2354
  });
2179
2355
 
2180
2356
  var output = setup.apply(this, [input, create]);
@@ -2195,25 +2371,45 @@
2195
2371
  }
2196
2372
  }
2197
2373
 
2198
- this.refreshClasses();
2374
+ this.refreshState();
2199
2375
  this.updateOriginalInput();
2200
2376
  },
2201
2377
 
2378
+ /**
2379
+ * Updates all state-dependent attributes
2380
+ * and CSS classes.
2381
+ */
2382
+ refreshState: function() {
2383
+ var self = this;
2384
+ var invalid = self.isRequired && !self.items.length;
2385
+ if (!invalid) self.isInvalid = false;
2386
+ self.$control_input.prop('required', invalid);
2387
+ self.refreshClasses();
2388
+ },
2389
+
2202
2390
  /**
2203
2391
  * Updates all state-dependent CSS classes.
2204
2392
  */
2205
2393
  refreshClasses: function() {
2206
- var self = this;
2207
- var isFull = self.isFull();
2394
+ var self = this;
2395
+ var isFull = self.isFull();
2208
2396
  var isLocked = self.isLocked;
2397
+
2398
+ this.$wrapper
2399
+ .toggleClass('rtl', self.rtl);
2400
+
2209
2401
  this.$control
2210
2402
  .toggleClass('focus', self.isFocused)
2211
2403
  .toggleClass('disabled', self.isDisabled)
2404
+ .toggleClass('required', self.isRequired)
2405
+ .toggleClass('invalid', self.isInvalid)
2212
2406
  .toggleClass('locked', isLocked)
2213
2407
  .toggleClass('full', isFull).toggleClass('not-full', !isFull)
2408
+ .toggleClass('input-active', self.isFocused && !self.isInputHidden)
2214
2409
  .toggleClass('dropdown-active', self.isOpen)
2215
2410
  .toggleClass('has-options', !$.isEmptyObject(self.options))
2216
2411
  .toggleClass('has-items', self.items.length > 0);
2412
+
2217
2413
  this.$control_input.data('grow', !isFull && !isLocked);
2218
2414
  },
2219
2415
 
@@ -2276,9 +2472,9 @@
2276
2472
  var self = this;
2277
2473
 
2278
2474
  if (self.isLocked || self.isOpen || (self.settings.mode === 'multi' && self.isFull())) return;
2279
- self.focus(true);
2475
+ self.focus();
2280
2476
  self.isOpen = true;
2281
- self.refreshClasses();
2477
+ self.refreshState();
2282
2478
  self.$dropdown.css({visibility: 'hidden', display: 'block'});
2283
2479
  self.positionDropdown();
2284
2480
  self.$dropdown.css({visibility: 'visible'});
@@ -2290,13 +2486,18 @@
2290
2486
  */
2291
2487
  close: function() {
2292
2488
  var self = this;
2489
+ var trigger = self.isOpen;
2293
2490
 
2294
- if (!self.isOpen) return;
2491
+ if (self.settings.mode === 'single' && this.items.length) {
2492
+ self.hideInput();
2493
+ }
2494
+
2495
+ self.isOpen = false;
2295
2496
  self.$dropdown.hide();
2296
2497
  self.setActiveOption(null);
2297
- self.isOpen = false;
2298
- self.refreshClasses();
2299
- self.trigger('dropdown_close', self.$dropdown);
2498
+ self.refreshState();
2499
+
2500
+ if (trigger) self.trigger('dropdown_close', self.$dropdown);
2300
2501
  },
2301
2502
 
2302
2503
  /**
@@ -2328,7 +2529,7 @@
2328
2529
  self.setCaret(0);
2329
2530
  self.updatePlaceholder();
2330
2531
  self.updateOriginalInput();
2331
- self.refreshClasses();
2532
+ self.refreshState();
2332
2533
  self.showInput();
2333
2534
  self.trigger('clear');
2334
2535
  },
@@ -2431,11 +2632,12 @@
2431
2632
  var self = this;
2432
2633
 
2433
2634
  if (direction === 0) return;
2635
+ if (self.rtl) direction *= -1;
2434
2636
 
2435
2637
  tail = direction > 0 ? 'last' : 'first';
2436
2638
  selection = getSelection(self.$control_input[0]);
2437
2639
 
2438
- if (self.isInputFocused && !self.isInputHidden) {
2640
+ if (self.isFocused && !self.isInputHidden) {
2439
2641
  valueLength = self.$control_input.val().length;
2440
2642
  cursorAtEdge = direction < 0
2441
2643
  ? selection.start === 0 && selection.length === 0
@@ -2450,7 +2652,6 @@
2450
2652
  idx = self.$control.children(':not(input)').index($tail);
2451
2653
  self.setActiveItem(null);
2452
2654
  self.setCaret(direction > 0 ? idx + 1 : idx);
2453
- self.showInput();
2454
2655
  }
2455
2656
  }
2456
2657
  },
@@ -2462,11 +2663,13 @@
2462
2663
  * @param {object} e (optional)
2463
2664
  */
2464
2665
  advanceCaret: function(direction, e) {
2666
+ var self = this, fn, $adj;
2667
+
2465
2668
  if (direction === 0) return;
2466
- var self = this;
2467
- var fn = direction > 0 ? 'next' : 'prev';
2669
+
2670
+ fn = direction > 0 ? 'next' : 'prev';
2468
2671
  if (self.isShiftDown) {
2469
- var $adj = self.$control_input[fn]();
2672
+ $adj = self.$control_input[fn]();
2470
2673
  if ($adj.length) {
2471
2674
  self.hideInput();
2472
2675
  self.setActiveItem($adj);
@@ -2515,7 +2718,7 @@
2515
2718
  lock: function() {
2516
2719
  this.close();
2517
2720
  this.isLocked = true;
2518
- this.refreshClasses();
2721
+ this.refreshState();
2519
2722
  },
2520
2723
 
2521
2724
  /**
@@ -2523,7 +2726,7 @@
2523
2726
  */
2524
2727
  unlock: function() {
2525
2728
  this.isLocked = false;
2526
- this.refreshClasses();
2729
+ this.refreshState();
2527
2730
  },
2528
2731
 
2529
2732
  /**
@@ -2637,6 +2840,7 @@
2637
2840
  maxOptions: 1000,
2638
2841
  maxItems: null,
2639
2842
  hideSelected: null,
2843
+ addPrecedence: false,
2640
2844
  preload: false,
2641
2845
 
2642
2846
  scrollDuration: 60,
@@ -2644,14 +2848,15 @@
2644
2848
 
2645
2849
  dataAttr: 'data-data',
2646
2850
  optgroupField: 'optgroup',
2647
- sortField: '$order',
2648
- sortDirection: 'asc',
2649
2851
  valueField: 'value',
2650
2852
  labelField: 'text',
2651
2853
  optgroupLabelField: 'label',
2652
2854
  optgroupValueField: 'value',
2653
2855
  optgroupOrder: null,
2856
+
2857
+ sortField: '$order',
2654
2858
  searchField: ['text'],
2859
+ searchConjunction: 'and',
2655
2860
 
2656
2861
  mode: null,
2657
2862
  wrapperClass: 'selectize-control',
@@ -2689,28 +2894,33 @@
2689
2894
  }
2690
2895
  };
2691
2896
 
2692
- $.fn.selectize = function(settings) {
2693
- settings = settings || {};
2694
-
2695
- var defaults = $.fn.selectize.defaults;
2696
- var dataAttr = settings.dataAttr || defaults.dataAttr;
2897
+ $.fn.selectize = function(settings_user) {
2898
+ var defaults = $.fn.selectize.defaults;
2899
+ var settings = $.extend({}, defaults, settings_user);
2900
+ var attr_data = settings.dataAttr;
2901
+ var field_label = settings.labelField;
2902
+ var field_value = settings.valueField;
2903
+ var field_optgroup = settings.optgroupField;
2904
+ var field_optgroup_label = settings.optgroupLabelField;
2905
+ var field_optgroup_value = settings.optgroupValueField;
2697
2906
 
2698
2907
  /**
2699
2908
  * Initializes selectize from a <input type="text"> element.
2700
2909
  *
2701
2910
  * @param {object} $input
2702
- * @param {object} settings
2911
+ * @param {object} settings_element
2703
2912
  */
2704
2913
  var init_textbox = function($input, settings_element) {
2705
- var i, n, values, value = $.trim($input.val() || '');
2914
+ var i, n, values, option, value = $.trim($input.val() || '');
2706
2915
  if (!value.length) return;
2707
2916
 
2708
- values = value.split(settings.delimiter || defaults.delimiter);
2917
+ values = value.split(settings.delimiter);
2709
2918
  for (i = 0, n = values.length; i < n; i++) {
2710
- settings_element.options[values[i]] = {
2711
- 'text' : values[i],
2712
- 'value' : values[i]
2713
- };
2919
+ option = {};
2920
+ option[field_label] = values[i];
2921
+ option[field_value] = values[i];
2922
+
2923
+ settings_element.options[values[i]] = option;
2714
2924
  }
2715
2925
 
2716
2926
  settings_element.items = values;
@@ -2720,16 +2930,14 @@
2720
2930
  * Initializes selectize from a <select> element.
2721
2931
  *
2722
2932
  * @param {object} $input
2723
- * @param {object} settings
2933
+ * @param {object} settings_element
2724
2934
  */
2725
2935
  var init_select = function($input, settings_element) {
2726
- var i, n, tagName;
2727
- var $children;
2728
- var order = 0;
2729
- settings_element.maxItems = !!$input.attr('multiple') ? null : 1;
2936
+ var i, n, tagName, $children, order = 0;
2937
+ var options = settings_element.options;
2730
2938
 
2731
2939
  var readData = function($el) {
2732
- var data = dataAttr && $el.attr(dataAttr);
2940
+ var data = attr_data && $el.attr(attr_data);
2733
2941
  if (typeof data === 'string' && data.length) {
2734
2942
  return JSON.parse(data);
2735
2943
  }
@@ -2744,14 +2952,30 @@
2744
2952
  value = $option.attr('value') || '';
2745
2953
  if (!value.length) return;
2746
2954
 
2747
- option = readData($option) || {
2748
- 'text' : $option.text(),
2749
- 'value' : value,
2750
- 'optgroup' : group
2751
- };
2955
+ // if the option already exists, it's probably been
2956
+ // duplicated in another optgroup. in this case, push
2957
+ // the current group to the "optgroup" property on the
2958
+ // existing option so that it's rendered in both places.
2959
+ if (options.hasOwnProperty(value)) {
2960
+ if (group) {
2961
+ if (!options[value].optgroup) {
2962
+ options[value].optgroup = group;
2963
+ } else if (!$.isArray(options[value].optgroup)) {
2964
+ options[value].optgroup = [options[value].optgroup, group];
2965
+ } else {
2966
+ options[value].optgroup.push(group);
2967
+ }
2968
+ }
2969
+ return;
2970
+ }
2971
+
2972
+ option = readData($option) || {};
2973
+ option[field_label] = option[field_label] || $option.text();
2974
+ option[field_value] = option[field_value] || value;
2975
+ option[field_optgroup] = option[field_optgroup] || group;
2752
2976
 
2753
2977
  option.$order = ++order;
2754
- settings_element.options[value] = option;
2978
+ options[value] = option;
2755
2979
 
2756
2980
  if ($option.is(':selected')) {
2757
2981
  settings_element.items.push(value);
@@ -2759,21 +2983,26 @@
2759
2983
  };
2760
2984
 
2761
2985
  var addGroup = function($optgroup) {
2762
- var i, n, $options = $('option', $optgroup);
2986
+ var i, n, id, optgroup, $options;
2987
+
2763
2988
  $optgroup = $($optgroup);
2989
+ id = $optgroup.attr('label');
2764
2990
 
2765
- var id = $optgroup.attr('label');
2766
- if (id && id.length) {
2767
- settings_element.optgroups[id] = readData($optgroup) || {
2768
- 'label': id
2769
- };
2991
+ if (id) {
2992
+ optgroup = readData($optgroup) || {};
2993
+ optgroup[field_optgroup_label] = id;
2994
+ optgroup[field_optgroup_value] = id;
2995
+ settings_element.optgroups[id] = optgroup;
2770
2996
  }
2771
2997
 
2998
+ $options = $('option', $optgroup);
2772
2999
  for (i = 0, n = $options.length; i < n; i++) {
2773
3000
  addOption($options[i], id);
2774
3001
  }
2775
3002
  };
2776
3003
 
3004
+ settings_element.maxItems = $input.attr('multiple') ? null : 1;
3005
+
2777
3006
  $children = $input.children();
2778
3007
  for (i = 0, n = $children.length; i < n; i++) {
2779
3008
  tagName = $children[i].tagName.toLowerCase();
@@ -2786,9 +3015,11 @@
2786
3015
  };
2787
3016
 
2788
3017
  return this.each(function() {
3018
+ if (this.selectize) return;
3019
+
2789
3020
  var instance;
2790
3021
  var $input = $(this);
2791
- var tag_name = $input[0].tagName.toLowerCase();
3022
+ var tag_name = this.tagName.toLowerCase();
2792
3023
  var settings_element = {
2793
3024
  'placeholder' : $input.children('option[value=""]').text() || $input.attr('placeholder'),
2794
3025
  'options' : {},
@@ -2802,7 +3033,7 @@
2802
3033
  init_textbox($input, settings_element);
2803
3034
  }
2804
3035
 
2805
- instance = new Selectize($input, $.extend(true, {}, defaults, settings_element, settings));
3036
+ instance = new Selectize($input, $.extend(true, {}, defaults, settings_element, settings_user));
2806
3037
  $input.data('selectize', instance);
2807
3038
  $input.addClass('selectized');
2808
3039
  });
@@ -2815,21 +3046,40 @@
2815
3046
  if (this.settings.mode !== 'multi') return;
2816
3047
  var self = this;
2817
3048
 
2818
- this.setup = (function() {
3049
+ self.lock = (function() {
3050
+ var original = self.lock;
3051
+ return function() {
3052
+ var sortable = self.$control.data('sortable');
3053
+ if (sortable) sortable.disable();
3054
+ return original.apply(self, arguments);
3055
+ };
3056
+ })();
3057
+
3058
+ self.unlock = (function() {
3059
+ var original = self.unlock;
3060
+ return function() {
3061
+ var sortable = self.$control.data('sortable');
3062
+ if (sortable) sortable.enable();
3063
+ return original.apply(self, arguments);
3064
+ };
3065
+ })();
3066
+
3067
+ self.setup = (function() {
2819
3068
  var original = self.setup;
2820
3069
  return function() {
2821
3070
  original.apply(this, arguments);
2822
3071
 
2823
- var $control = this.$control.sortable({
3072
+ var $control = self.$control.sortable({
2824
3073
  items: '[data-value]',
2825
3074
  forcePlaceholderSize: true,
3075
+ disabled: self.isLocked,
2826
3076
  start: function(e, ui) {
2827
3077
  ui.placeholder.css('width', ui.helper.css('width'));
2828
3078
  $control.css({overflow: 'visible'});
2829
3079
  },
2830
3080
  stop: function() {
2831
3081
  $control.css({overflow: 'hidden'});
2832
- var active = this.$activeItems ? this.$activeItems.slice() : null;
3082
+ var active = self.$activeItems ? self.$activeItems.slice() : null;
2833
3083
  var values = [];
2834
3084
  $control.children('[data-value]').each(function() {
2835
3085
  values.push($(this).attr('data-value'));
@@ -2960,7 +3210,7 @@
2960
3210
  label : '&times;',
2961
3211
  title : 'Remove',
2962
3212
  className : 'remove',
2963
- append : true,
3213
+ append : true
2964
3214
  }, options);
2965
3215
 
2966
3216
  var self = this;
@@ -2994,6 +3244,8 @@
2994
3244
  // add event listener
2995
3245
  this.$control.on('click', '.' + options.className, function(e) {
2996
3246
  e.preventDefault();
3247
+ if (self.isLocked) return;
3248
+
2997
3249
  var $item = $(e.target).parent();
2998
3250
  self.setActiveItem($item);
2999
3251
  if (self.deleteSelection()) {