selectize-rails 0.7.7 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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()) {