blazer 0.0.8 → 1.0.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.

Potentially problematic release.


This version of blazer might be problematic. Click here for more details.

Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +261 -45
  4. data/app/assets/javascripts/blazer/Sortable.js +1144 -0
  5. data/app/assets/javascripts/blazer/application.js +2 -1
  6. data/app/assets/javascripts/blazer/chartkick.js +935 -0
  7. data/app/assets/javascripts/blazer/selectize.js +391 -201
  8. data/app/assets/stylesheets/blazer/application.css +17 -2
  9. data/app/assets/stylesheets/blazer/selectize.default.css +3 -2
  10. data/app/controllers/blazer/base_controller.rb +48 -0
  11. data/app/controllers/blazer/checks_controller.rb +51 -0
  12. data/app/controllers/blazer/dashboards_controller.rb +94 -0
  13. data/app/controllers/blazer/queries_controller.rb +29 -101
  14. data/app/helpers/blazer/{queries_helper.rb → base_helper.rb} +1 -1
  15. data/app/mailers/blazer/check_mailer.rb +21 -0
  16. data/app/models/blazer/check.rb +28 -0
  17. data/app/models/blazer/connection.rb +0 -1
  18. data/app/models/blazer/dashboard.rb +12 -0
  19. data/app/models/blazer/dashboard_query.rb +9 -0
  20. data/app/models/blazer/query.rb +5 -0
  21. data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
  22. data/app/views/blazer/check_mailer/state_change.html.erb +6 -0
  23. data/app/views/blazer/checks/_form.html.erb +28 -0
  24. data/app/views/blazer/checks/edit.html.erb +1 -0
  25. data/app/views/blazer/checks/index.html.erb +41 -0
  26. data/app/views/blazer/checks/new.html.erb +1 -0
  27. data/app/views/blazer/checks/run.html.erb +9 -0
  28. data/app/views/blazer/dashboards/_form.html.erb +86 -0
  29. data/app/views/blazer/dashboards/edit.html.erb +1 -0
  30. data/app/views/blazer/dashboards/index.html.erb +21 -0
  31. data/app/views/blazer/dashboards/new.html.erb +1 -0
  32. data/app/views/blazer/dashboards/show.html.erb +148 -0
  33. data/app/views/blazer/queries/_form.html.erb +16 -5
  34. data/app/views/blazer/queries/_tables.html +5 -0
  35. data/app/views/blazer/queries/index.html.erb +6 -0
  36. data/app/views/blazer/queries/run.html.erb +59 -44
  37. data/app/views/blazer/queries/show.html.erb +20 -16
  38. data/config/routes.rb +5 -0
  39. data/lib/blazer.rb +46 -2
  40. data/lib/blazer/data_source.rb +70 -0
  41. data/lib/blazer/engine.rb +6 -2
  42. data/lib/blazer/tasks.rb +12 -0
  43. data/lib/blazer/version.rb +1 -1
  44. data/lib/generators/blazer/templates/config.yml +26 -6
  45. data/lib/generators/blazer/templates/install.rb +21 -0
  46. metadata +27 -3
@@ -221,7 +221,7 @@
221
221
  * @param {object} result
222
222
  * @return {mixed}
223
223
  */
224
- get_field = function(name, result) {
224
+ get_field = function(name, result) {
225
225
  if (name === '$score') return result.score;
226
226
  return self.items[result.id][name];
227
227
  };
@@ -391,8 +391,8 @@
391
391
  if (typeof a === 'number' && typeof b === 'number') {
392
392
  return a > b ? 1 : (a < b ? -1 : 0);
393
393
  }
394
- a = String(a || '').toLowerCase();
395
- b = String(b || '').toLowerCase();
394
+ a = asciifold(String(a || ''));
395
+ b = asciifold(String(b || ''));
396
396
  if (a > b) return 1;
397
397
  if (b > a) return -1;
398
398
  return 0;
@@ -425,21 +425,44 @@
425
425
  };
426
426
 
427
427
  var DIACRITICS = {
428
- 'a': '[aÀÁÂÃÄÅàáâãäå]',
428
+ 'a': '[aÀÁÂÃÄÅàáâãäåĀāąĄ]',
429
429
  'c': '[cÇçćĆčČ]',
430
430
  'd': '[dđĐďĎ]',
431
- 'e': '[eÈÉÊËèéêëěĚ]',
432
- 'i': '[iÌÍÎÏìíîï]',
433
- 'n': '[nÑñňŇ]',
434
- 'o': '[oÒÓÔÕÕÖØòóôõöø]',
431
+ 'e': '[eÈÉÊËèéêëěĚĒēęĘ]',
432
+ 'i': '[iÌÍÎÏìíîïĪī]',
433
+ 'l': '[lłŁ]',
434
+ 'n': '[nÑñňŇńŃ]',
435
+ 'o': '[oÒÓÔÕÕÖØòóôõöøŌō]',
435
436
  'r': '[rřŘ]',
436
- 's': '[sŠš]',
437
+ 's': '[sŠšśŚ]',
437
438
  't': '[tťŤ]',
438
- 'u': '[uÙÚÛÜùúûüůŮ]',
439
+ 'u': '[uÙÚÛÜùúûüůŮŪū]',
439
440
  'y': '[yŸÿýÝ]',
440
- 'z': '[zŽž]'
441
+ 'z': '[zŽžżŻźŹ]'
441
442
  };
442
443
 
444
+ var asciifold = (function() {
445
+ var i, n, k, chunk;
446
+ var foreignletters = '';
447
+ var lookup = {};
448
+ for (k in DIACRITICS) {
449
+ if (DIACRITICS.hasOwnProperty(k)) {
450
+ chunk = DIACRITICS[k].substring(2, DIACRITICS[k].length - 1);
451
+ foreignletters += chunk;
452
+ for (i = 0, n = chunk.length; i < n; i++) {
453
+ lookup[chunk.charAt(i)] = k;
454
+ }
455
+ }
456
+ }
457
+ var regexp = new RegExp('[' + foreignletters + ']', 'g');
458
+ return function(str) {
459
+ return str.replace(regexp, function(foreignletter) {
460
+ return lookup[foreignletter];
461
+ }).toLowerCase();
462
+ };
463
+ })();
464
+
465
+
443
466
  // export
444
467
  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
445
468
 
@@ -585,8 +608,8 @@
585
608
  }));
586
609
 
587
610
  /**
588
- * selectize.js (v0.10.1)
589
- * Copyright (c) 2013 Brian Reavis & contributors
611
+ * selectize.js (v0.12.1)
612
+ * Copyright (c) 2013–2015 Brian Reavis & contributors
590
613
  *
591
614
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
592
615
  * file except in compliance with the License. You may obtain a copy of the License at:
@@ -707,6 +730,8 @@
707
730
  var TAG_SELECT = 1;
708
731
  var TAG_INPUT = 2;
709
732
 
733
+ // for now, android support in general is too spotty to support validity
734
+ var SUPPORTS_VALIDITY_API = !/android/i.test(window.navigator.userAgent) && !!document.createElement('form').validity;
710
735
 
711
736
  var isset = function(object) {
712
737
  return typeof object !== 'undefined';
@@ -726,10 +751,10 @@
726
751
  * 1 -> '1'
727
752
  *
728
753
  * @param {string} value
729
- * @returns {string}
754
+ * @returns {string|null}
730
755
  */
731
756
  var hash_key = function(value) {
732
- if (typeof value === 'undefined' || value === null) return '';
757
+ if (typeof value === 'undefined' || value === null) return null;
733
758
  if (typeof value === 'boolean') return value ? '1' : '0';
734
759
  return value + '';
735
760
  };
@@ -793,25 +818,6 @@
793
818
  };
794
819
  };
795
820
 
796
- /**
797
- * Builds a hash table out of an array of
798
- * objects, using the specified `key` within
799
- * each object.
800
- *
801
- * @param {string} key
802
- * @param {mixed} objects
803
- */
804
- var build_hash_table = function(key, objects) {
805
- if (!$.isArray(objects)) return objects;
806
- var i, n, table = {};
807
- for (i = 0, n = objects.length; i < n; i++) {
808
- if (objects[i].hasOwnProperty(key)) {
809
- table[objects[i][key]] = objects[i];
810
- }
811
- }
812
- return table;
813
- };
814
-
815
821
  /**
816
822
  * Wraps `fn` so that it can only be invoked once.
817
823
  *
@@ -1053,13 +1059,16 @@
1053
1059
  input.selectize = self;
1054
1060
 
1055
1061
  // detect rtl environment
1056
- dir = window.getComputedStyle ? window.getComputedStyle(input, null).getPropertyValue('direction') : input.currentStyle && input.currentStyle.direction;
1062
+ var computedStyle = window.getComputedStyle && window.getComputedStyle(input, null);
1063
+ dir = computedStyle ? computedStyle.getPropertyValue('direction') : input.currentStyle && input.currentStyle.direction;
1057
1064
  dir = dir || $input.parents('[dir]:first').attr('dir') || '';
1058
1065
 
1059
1066
  // setup default state
1060
1067
  $.extend(self, {
1068
+ order : 0,
1061
1069
  settings : settings,
1062
1070
  $input : $input,
1071
+ tabIndex : $input.attr('tabindex') || '',
1063
1072
  tagType : input.tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT,
1064
1073
  rtl : /rtl/i.test(dir),
1065
1074
 
@@ -1101,12 +1110,20 @@
1101
1110
  self.sifter = new Sifter(this.options, {diacritics: settings.diacritics});
1102
1111
 
1103
1112
  // build options table
1104
- $.extend(self.options, build_hash_table(settings.valueField, settings.options));
1105
- delete self.settings.options;
1113
+ if (self.settings.options) {
1114
+ for (i = 0, n = self.settings.options.length; i < n; i++) {
1115
+ self.registerOption(self.settings.options[i]);
1116
+ }
1117
+ delete self.settings.options;
1118
+ }
1106
1119
 
1107
1120
  // build optgroup table
1108
- $.extend(self.optgroups, build_hash_table(settings.optgroupValueField, settings.optgroups));
1109
- delete self.settings.optgroups;
1121
+ if (self.settings.optgroups) {
1122
+ for (i = 0, n = self.settings.optgroups.length; i < n; i++) {
1123
+ self.registerOptionGroup(self.settings.optgroups[i]);
1124
+ }
1125
+ delete self.settings.optgroups;
1126
+ }
1110
1127
 
1111
1128
  // option-dependent defaults
1112
1129
  self.settings.mode = self.settings.mode || (self.settings.maxItems === 1 ? 'single' : 'multi');
@@ -1114,16 +1131,6 @@
1114
1131
  self.settings.hideSelected = self.settings.mode === 'multi';
1115
1132
  }
1116
1133
 
1117
- if (self.settings.create) {
1118
- self.canCreate = function(input) {
1119
- var filter = self.settings.createFilter;
1120
- return input.length
1121
- && (typeof filter !== 'function' || filter.apply(self, [input]))
1122
- && (typeof filter !== 'string' || new RegExp(filter).test(input))
1123
- && (!(filter instanceof RegExp) || filter.test(input));
1124
- };
1125
- }
1126
-
1127
1134
  self.initializePlugins(self.settings.plugins);
1128
1135
  self.setupCallbacks();
1129
1136
  self.setupTemplates();
@@ -1161,21 +1168,23 @@
1161
1168
  var inputMode;
1162
1169
  var timeout_blur;
1163
1170
  var timeout_focus;
1164
- var tab_index;
1165
1171
  var classes;
1166
1172
  var classes_plugins;
1167
1173
 
1168
1174
  inputMode = self.settings.mode;
1169
- tab_index = $input.attr('tabindex') || '';
1170
1175
  classes = $input.attr('class') || '';
1171
1176
 
1172
1177
  $wrapper = $('<div>').addClass(settings.wrapperClass).addClass(classes).addClass(inputMode);
1173
1178
  $control = $('<div>').addClass(settings.inputClass).addClass('items').appendTo($wrapper);
1174
- $control_input = $('<input type="text" autocomplete="off" />').appendTo($control).attr('tabindex', tab_index);
1179
+ $control_input = $('<input type="text" autocomplete="off" />').appendTo($control).attr('tabindex', $input.is(':disabled') ? '-1' : self.tabIndex);
1175
1180
  $dropdown_parent = $(settings.dropdownParent || $wrapper);
1176
- $dropdown = $('<div>').addClass(settings.dropdownClass).addClass(classes).addClass(inputMode).hide().appendTo($dropdown_parent);
1181
+ $dropdown = $('<div>').addClass(settings.dropdownClass).addClass(inputMode).hide().appendTo($dropdown_parent);
1177
1182
  $dropdown_content = $('<div>').addClass(settings.dropdownContentClass).appendTo($dropdown);
1178
1183
 
1184
+ if(self.settings.copyClassesToDropdown) {
1185
+ $dropdown.addClass(classes);
1186
+ }
1187
+
1179
1188
  $wrapper.css({
1180
1189
  width: $input[0].style.width
1181
1190
  });
@@ -1194,6 +1203,12 @@
1194
1203
  $control_input.attr('placeholder', settings.placeholder);
1195
1204
  }
1196
1205
 
1206
+ // if splitOn was not passed in, construct it from the delimiter to allow pasting universally
1207
+ if (!self.settings.splitOn && self.settings.delimiter) {
1208
+ var delimiterEscaped = self.settings.delimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1209
+ self.settings.splitOn = new RegExp('\\s*' + delimiterEscaped + '+\\s*');
1210
+ }
1211
+
1197
1212
  if ($input.attr('autocorrect')) {
1198
1213
  $control_input.attr('autocorrect', $input.attr('autocorrect'));
1199
1214
  }
@@ -1209,7 +1224,7 @@
1209
1224
  self.$dropdown_content = $dropdown_content;
1210
1225
 
1211
1226
  $dropdown.on('mouseenter', '[data-selectable]', function() { return self.onOptionHover.apply(self, arguments); });
1212
- $dropdown.on('mousedown', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); });
1227
+ $dropdown.on('mousedown click', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); });
1213
1228
  watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); });
1214
1229
  autoGrow($control_input);
1215
1230
 
@@ -1249,7 +1264,7 @@
1249
1264
  }
1250
1265
  // blur on click outside
1251
1266
  if (!self.$control.has(e.target).length && e.target !== self.$control[0]) {
1252
- self.blur();
1267
+ self.blur(e.target);
1253
1268
  }
1254
1269
  }
1255
1270
  });
@@ -1278,7 +1293,7 @@
1278
1293
  }
1279
1294
 
1280
1295
  // feature detect for the validation API
1281
- if ($input[0].validity) {
1296
+ if (SUPPORTS_VALIDITY_API) {
1282
1297
  $input.on('invalid' + eventNS, function(e) {
1283
1298
  e.preventDefault();
1284
1299
  self.isInvalid = true;
@@ -1344,17 +1359,23 @@
1344
1359
  */
1345
1360
  setupCallbacks: function() {
1346
1361
  var key, fn, callbacks = {
1347
- 'initialize' : 'onInitialize',
1348
- 'change' : 'onChange',
1349
- 'item_add' : 'onItemAdd',
1350
- 'item_remove' : 'onItemRemove',
1351
- 'clear' : 'onClear',
1352
- 'option_add' : 'onOptionAdd',
1353
- 'option_remove' : 'onOptionRemove',
1354
- 'option_clear' : 'onOptionClear',
1355
- 'dropdown_open' : 'onDropdownOpen',
1356
- 'dropdown_close' : 'onDropdownClose',
1357
- 'type' : 'onType'
1362
+ 'initialize' : 'onInitialize',
1363
+ 'change' : 'onChange',
1364
+ 'item_add' : 'onItemAdd',
1365
+ 'item_remove' : 'onItemRemove',
1366
+ 'clear' : 'onClear',
1367
+ 'option_add' : 'onOptionAdd',
1368
+ 'option_remove' : 'onOptionRemove',
1369
+ 'option_clear' : 'onOptionClear',
1370
+ 'optgroup_add' : 'onOptionGroupAdd',
1371
+ 'optgroup_remove' : 'onOptionGroupRemove',
1372
+ 'optgroup_clear' : 'onOptionGroupClear',
1373
+ 'dropdown_open' : 'onDropdownOpen',
1374
+ 'dropdown_close' : 'onDropdownClose',
1375
+ 'type' : 'onType',
1376
+ 'load' : 'onLoad',
1377
+ 'focus' : 'onFocus',
1378
+ 'blur' : 'onBlur'
1358
1379
  };
1359
1380
 
1360
1381
  for (key in callbacks) {
@@ -1427,7 +1448,6 @@
1427
1448
  this.$input.trigger('change');
1428
1449
  },
1429
1450
 
1430
-
1431
1451
  /**
1432
1452
  * Triggered on <input> paste.
1433
1453
  *
@@ -1438,6 +1458,17 @@
1438
1458
  var self = this;
1439
1459
  if (self.isFull() || self.isInputHidden || self.isLocked) {
1440
1460
  e.preventDefault();
1461
+ } else {
1462
+ // If a regex or string is included, this will split the pasted
1463
+ // input and create Items for each separate value
1464
+ if (self.settings.splitOn) {
1465
+ setTimeout(function() {
1466
+ var splitInput = $.trim(self.$control_input.val() || '').split(self.settings.splitOn);
1467
+ for (var i = 0, n = splitInput.length; i < n; i++) {
1468
+ self.createItem(splitInput[i]);
1469
+ }
1470
+ }, 0);
1471
+ }
1441
1472
  }
1442
1473
  },
1443
1474
 
@@ -1450,7 +1481,7 @@
1450
1481
  onKeyPress: function(e) {
1451
1482
  if (this.isLocked) return e && e.preventDefault();
1452
1483
  var character = String.fromCharCode(e.keyCode || e.which);
1453
- if (this.settings.create && character === this.settings.delimiter) {
1484
+ if (this.settings.create && this.settings.mode === 'multi' && character === this.settings.delimiter) {
1454
1485
  this.createItem();
1455
1486
  e.preventDefault();
1456
1487
  return false;
@@ -1482,7 +1513,11 @@
1482
1513
  }
1483
1514
  break;
1484
1515
  case KEY_ESC:
1485
- self.close();
1516
+ if (self.isOpen) {
1517
+ e.preventDefault();
1518
+ e.stopPropagation();
1519
+ self.close();
1520
+ }
1486
1521
  return;
1487
1522
  case KEY_N:
1488
1523
  if (!e.ctrlKey || e.altKey) break;
@@ -1509,8 +1544,8 @@
1509
1544
  case KEY_RETURN:
1510
1545
  if (self.isOpen && self.$activeOption) {
1511
1546
  self.onOptionSelect({currentTarget: self.$activeOption});
1547
+ e.preventDefault();
1512
1548
  }
1513
- e.preventDefault();
1514
1549
  return;
1515
1550
  case KEY_LEFT:
1516
1551
  self.advanceSelection(-1, e);
@@ -1521,7 +1556,12 @@
1521
1556
  case KEY_TAB:
1522
1557
  if (self.settings.selectOnTab && self.isOpen && self.$activeOption) {
1523
1558
  self.onOptionSelect({currentTarget: self.$activeOption});
1524
- e.preventDefault();
1559
+
1560
+ // Default behaviour is to jump to the next field, we only want this
1561
+ // if the current field doesn't accept any more entries
1562
+ if (!self.isFull()) {
1563
+ e.preventDefault();
1564
+ }
1525
1565
  }
1526
1566
  if (self.settings.create && self.createItem()) {
1527
1567
  e.preventDefault();
@@ -1585,8 +1625,8 @@
1585
1625
  */
1586
1626
  onFocus: function(e) {
1587
1627
  var self = this;
1628
+ var wasFocused = self.isFocused;
1588
1629
 
1589
- self.isFocused = true;
1590
1630
  if (self.isDisabled) {
1591
1631
  self.blur();
1592
1632
  e && e.preventDefault();
@@ -1594,8 +1634,11 @@
1594
1634
  }
1595
1635
 
1596
1636
  if (self.ignoreFocus) return;
1637
+ self.isFocused = true;
1597
1638
  if (self.settings.preload === 'focus') self.onSearchChange('');
1598
1639
 
1640
+ if (!wasFocused) self.trigger('focus');
1641
+
1599
1642
  if (!self.$activeItems.length) {
1600
1643
  self.showInput();
1601
1644
  self.setActiveItem(null);
@@ -1609,31 +1652,43 @@
1609
1652
  * Triggered on <input> blur.
1610
1653
  *
1611
1654
  * @param {object} e
1612
- * @returns {boolean}
1655
+ * @param {Element} dest
1613
1656
  */
1614
- onBlur: function(e) {
1657
+ onBlur: function(e, dest) {
1615
1658
  var self = this;
1659
+ if (!self.isFocused) return;
1616
1660
  self.isFocused = false;
1617
- if (self.ignoreFocus) return;
1618
1661
 
1619
- // necessary to prevent IE closing the dropdown when the scrollbar is clicked
1620
- if (!self.ignoreBlur && document.activeElement === self.$dropdown_content[0]) {
1662
+ if (self.ignoreFocus) {
1663
+ return;
1664
+ } else if (!self.ignoreBlur && document.activeElement === self.$dropdown_content[0]) {
1665
+ // necessary to prevent IE closing the dropdown when the scrollbar is clicked
1621
1666
  self.ignoreBlur = true;
1622
1667
  self.onFocus(e);
1623
-
1624
1668
  return;
1625
1669
  }
1626
1670
 
1671
+ var deactivate = function() {
1672
+ self.close();
1673
+ self.setTextboxValue('');
1674
+ self.setActiveItem(null);
1675
+ self.setActiveOption(null);
1676
+ self.setCaret(self.items.length);
1677
+ self.refreshState();
1678
+
1679
+ // IE11 bug: element still marked as active
1680
+ (dest || document.body).focus();
1681
+
1682
+ self.ignoreFocus = false;
1683
+ self.trigger('blur');
1684
+ };
1685
+
1686
+ self.ignoreFocus = true;
1627
1687
  if (self.settings.create && self.settings.createOnBlur) {
1628
- self.createItem(false);
1688
+ self.createItem(null, false, deactivate);
1689
+ } else {
1690
+ deactivate();
1629
1691
  }
1630
-
1631
- self.close();
1632
- self.setTextboxValue('');
1633
- self.setActiveItem(null);
1634
- self.setActiveOption(null);
1635
- self.setCaret(self.items.length);
1636
- self.refreshState();
1637
1692
  },
1638
1693
 
1639
1694
  /**
@@ -1665,14 +1720,20 @@
1665
1720
 
1666
1721
  $target = $(e.currentTarget);
1667
1722
  if ($target.hasClass('create')) {
1668
- self.createItem();
1723
+ self.createItem(null, function() {
1724
+ if (self.settings.closeAfterSelect) {
1725
+ self.close();
1726
+ }
1727
+ });
1669
1728
  } else {
1670
1729
  value = $target.attr('data-value');
1671
- if (value) {
1730
+ if (typeof value !== 'undefined') {
1672
1731
  self.lastQuery = null;
1673
1732
  self.setTextboxValue('');
1674
1733
  self.addItem(value);
1675
- if (!self.settings.hideSelected && e.type && /mouse/.test(e.type)) {
1734
+ if (self.settings.closeAfterSelect) {
1735
+ self.close();
1736
+ } else if (!self.settings.hideSelected && e.type && /mouse/.test(e.type)) {
1676
1737
  self.setActiveOption(self.getOption(value));
1677
1738
  }
1678
1739
  }
@@ -1705,7 +1766,7 @@
1705
1766
  */
1706
1767
  load: function(fn) {
1707
1768
  var self = this;
1708
- var $wrapper = self.$wrapper.addClass('loading');
1769
+ var $wrapper = self.$wrapper.addClass(self.settings.loadingClass);
1709
1770
 
1710
1771
  self.loading++;
1711
1772
  fn.apply(self, [function(results) {
@@ -1715,7 +1776,7 @@
1715
1776
  self.refreshOptions(self.isFocused && !self.isInputHidden);
1716
1777
  }
1717
1778
  if (!self.loading) {
1718
- $wrapper.removeClass('loading');
1779
+ $wrapper.removeClass(self.settings.loadingClass);
1719
1780
  }
1720
1781
  self.trigger('load', results);
1721
1782
  }]);
@@ -1756,10 +1817,12 @@
1756
1817
  *
1757
1818
  * @param {mixed} value
1758
1819
  */
1759
- setValue: function(value) {
1760
- debounce_events(this, ['change'], function() {
1761
- this.clear();
1762
- this.addItems(value);
1820
+ setValue: function(value, silent) {
1821
+ var events = silent ? [] : ['change'];
1822
+
1823
+ debounce_events(this, events, function() {
1824
+ this.clear(silent);
1825
+ this.addItems(value, silent);
1763
1826
  });
1764
1827
  },
1765
1828
 
@@ -1903,11 +1966,7 @@
1903
1966
  },
1904
1967
 
1905
1968
  /**
1906
- * Gives the control focus. If "trigger" is falsy,
1907
- * focus handlers won't be fired--causing the focus
1908
- * to happen silently in the background.
1909
- *
1910
- * @param {boolean} trigger
1969
+ * Gives the control focus.
1911
1970
  */
1912
1971
  focus: function() {
1913
1972
  var self = this;
@@ -1923,9 +1982,12 @@
1923
1982
 
1924
1983
  /**
1925
1984
  * Forces the control out of focus.
1985
+ *
1986
+ * @param {Element} dest
1926
1987
  */
1927
- blur: function() {
1928
- this.$control_input.trigger('blur');
1988
+ blur: function(dest) {
1989
+ this.$control_input[0].blur();
1990
+ this.onBlur(null, dest);
1929
1991
  },
1930
1992
 
1931
1993
  /**
@@ -1952,7 +2014,7 @@
1952
2014
  var settings = this.settings;
1953
2015
  var sort = settings.sortField;
1954
2016
  if (typeof sort === 'string') {
1955
- sort = {field: sort};
2017
+ sort = [{field: sort}];
1956
2018
  }
1957
2019
 
1958
2020
  return {
@@ -2026,7 +2088,7 @@
2026
2088
  }
2027
2089
 
2028
2090
  var self = this;
2029
- var query = self.$control_input.val();
2091
+ var query = $.trim(self.$control_input.val());
2030
2092
  var results = self.search(query);
2031
2093
  var $dropdown_content = self.$dropdown_content;
2032
2094
  var active_before = self.$activeOption && hash_key(self.$activeOption.attr('data-value'));
@@ -2039,15 +2101,7 @@
2039
2101
 
2040
2102
  // render and group available options individually
2041
2103
  groups = {};
2042
-
2043
- if (self.settings.optgroupOrder) {
2044
- groups_order = self.settings.optgroupOrder;
2045
- for (i = 0; i < groups_order.length; i++) {
2046
- groups[groups_order[i]] = [];
2047
- }
2048
- } else {
2049
- groups_order = [];
2050
- }
2104
+ groups_order = [];
2051
2105
 
2052
2106
  for (i = 0; i < n; i++) {
2053
2107
  option = self.options[results.items[i].id];
@@ -2068,6 +2122,15 @@
2068
2122
  }
2069
2123
  }
2070
2124
 
2125
+ // sort optgroups
2126
+ if (this.settings.lockOptgroupOrder) {
2127
+ groups_order.sort(function(a, b) {
2128
+ var a_order = self.optgroups[a].$order || 0;
2129
+ var b_order = self.optgroups[b].$order || 0;
2130
+ return a_order - b_order;
2131
+ });
2132
+ }
2133
+
2071
2134
  // render optgroup headers & join groups
2072
2135
  html = [];
2073
2136
  for (i = 0, n = groups_order.length; i < n; i++) {
@@ -2102,7 +2165,7 @@
2102
2165
  }
2103
2166
 
2104
2167
  // add create option
2105
- has_create_option = self.settings.create && self.canCreate(results.query);
2168
+ has_create_option = self.canCreate(query);
2106
2169
  if (has_create_option) {
2107
2170
  $dropdown_content.prepend(self.render('option_create', {input: query}));
2108
2171
  $create = $($dropdown_content[0].childNodes[0]);
@@ -2146,10 +2209,10 @@
2146
2209
  *
2147
2210
  * this.addOption(data)
2148
2211
  *
2149
- * @param {object} data
2212
+ * @param {object|array} data
2150
2213
  */
2151
2214
  addOption: function(data) {
2152
- var i, n, optgroup, value, self = this;
2215
+ var i, n, value, self = this;
2153
2216
 
2154
2217
  if ($.isArray(data)) {
2155
2218
  for (i = 0, n = data.length; i < n; i++) {
@@ -2158,13 +2221,40 @@
2158
2221
  return;
2159
2222
  }
2160
2223
 
2161
- value = hash_key(data[self.settings.valueField]);
2162
- if (!value || self.options.hasOwnProperty(value)) return;
2224
+ if (value = self.registerOption(data)) {
2225
+ self.userOptions[value] = true;
2226
+ self.lastQuery = null;
2227
+ self.trigger('option_add', value, data);
2228
+ }
2229
+ },
2230
+
2231
+ /**
2232
+ * Registers an option to the pool of options.
2233
+ *
2234
+ * @param {object} data
2235
+ * @return {boolean|string}
2236
+ */
2237
+ registerOption: function(data) {
2238
+ var key = hash_key(data[this.settings.valueField]);
2239
+ if ((!key || this.options.hasOwnProperty(key)) && !this.settings.allowEmptyOption) return false;
2240
+ data.$order = data.$order || ++this.order;
2241
+ this.options[key] = data;
2242
+ return key;
2243
+ },
2163
2244
 
2164
- self.userOptions[value] = true;
2165
- self.options[value] = data;
2166
- self.lastQuery = null;
2167
- self.trigger('option_add', value, data);
2245
+ /**
2246
+ * Registers an option group to the pool of option groups.
2247
+ *
2248
+ * @param {object} data
2249
+ * @return {boolean|string}
2250
+ */
2251
+ registerOptionGroup: function(data) {
2252
+ var key = hash_key(data[this.settings.optgroupValueField]);
2253
+ if (!key) return false;
2254
+
2255
+ data.$order = data.$order || ++this.order;
2256
+ this.optgroups[key] = data;
2257
+ return key;
2168
2258
  },
2169
2259
 
2170
2260
  /**
@@ -2175,8 +2265,32 @@
2175
2265
  * @param {object} data
2176
2266
  */
2177
2267
  addOptionGroup: function(id, data) {
2178
- this.optgroups[id] = data;
2179
- this.trigger('optgroup_add', id, data);
2268
+ data[this.settings.optgroupValueField] = id;
2269
+ if (id = this.registerOptionGroup(data)) {
2270
+ this.trigger('optgroup_add', id, data);
2271
+ }
2272
+ },
2273
+
2274
+ /**
2275
+ * Removes an existing option group.
2276
+ *
2277
+ * @param {string} id
2278
+ */
2279
+ removeOptionGroup: function(id) {
2280
+ if (this.optgroups.hasOwnProperty(id)) {
2281
+ delete this.optgroups[id];
2282
+ this.renderCache = {};
2283
+ this.trigger('optgroup_remove', id);
2284
+ }
2285
+ },
2286
+
2287
+ /**
2288
+ * Clears all existing option groups.
2289
+ */
2290
+ clearOptionGroups: function() {
2291
+ this.optgroups = {};
2292
+ this.renderCache = {};
2293
+ this.trigger('optgroup_clear');
2180
2294
  },
2181
2295
 
2182
2296
  /**
@@ -2190,14 +2304,17 @@
2190
2304
  updateOption: function(value, data) {
2191
2305
  var self = this;
2192
2306
  var $item, $item_new;
2193
- var value_new, index_item, cache_items, cache_options;
2307
+ var value_new, index_item, cache_items, cache_options, order_old;
2194
2308
 
2195
2309
  value = hash_key(value);
2196
2310
  value_new = hash_key(data[self.settings.valueField]);
2197
2311
 
2198
2312
  // sanity checks
2313
+ if (value === null) return;
2199
2314
  if (!self.options.hasOwnProperty(value)) return;
2200
- if (!value_new) throw new Error('Value must be set in option data');
2315
+ if (typeof value_new !== 'string') throw new Error('Value must be set in option data');
2316
+
2317
+ order_old = self.options[value].$order;
2201
2318
 
2202
2319
  // update references
2203
2320
  if (value_new !== value) {
@@ -2207,6 +2324,7 @@
2207
2324
  self.items.splice(index_item, 1, value_new);
2208
2325
  }
2209
2326
  }
2327
+ data.$order = data.$order || order_old;
2210
2328
  self.options[value_new] = data;
2211
2329
 
2212
2330
  // invalidate render cache
@@ -2230,6 +2348,9 @@
2230
2348
  $item.replaceWith($item_new);
2231
2349
  }
2232
2350
 
2351
+ // invalidate last query because we might have updated the sortField
2352
+ self.lastQuery = null;
2353
+
2233
2354
  // update dropdown contents
2234
2355
  if (self.isOpen) {
2235
2356
  self.refreshOptions(false);
@@ -2240,8 +2361,9 @@
2240
2361
  * Removes a single option.
2241
2362
  *
2242
2363
  * @param {string} value
2364
+ * @param {boolean} silent
2243
2365
  */
2244
- removeOption: function(value) {
2366
+ removeOption: function(value, silent) {
2245
2367
  var self = this;
2246
2368
  value = hash_key(value);
2247
2369
 
@@ -2254,7 +2376,7 @@
2254
2376
  delete self.options[value];
2255
2377
  self.lastQuery = null;
2256
2378
  self.trigger('option_remove', value);
2257
- self.removeItem(value);
2379
+ self.removeItem(value, silent);
2258
2380
  },
2259
2381
 
2260
2382
  /**
@@ -2309,7 +2431,7 @@
2309
2431
  getElementWithValue: function(value, $els) {
2310
2432
  value = hash_key(value);
2311
2433
 
2312
- if (value) {
2434
+ if (typeof value !== 'undefined' && value !== null) {
2313
2435
  for (var i = 0, n = $els.length; i < n; i++) {
2314
2436
  if ($els[i].getAttribute('data-value') === value) {
2315
2437
  return $($els[i]);
@@ -2336,12 +2458,13 @@
2336
2458
  * at the current caret position.
2337
2459
  *
2338
2460
  * @param {string} value
2461
+ * @param {boolean} silent
2339
2462
  */
2340
- addItems: function(values) {
2463
+ addItems: function(values, silent) {
2341
2464
  var items = $.isArray(values) ? values : [values];
2342
2465
  for (var i = 0, n = items.length; i < n; i++) {
2343
2466
  this.isPending = (i < n - 1);
2344
- this.addItem(items[i]);
2467
+ this.addItem(items[i], silent);
2345
2468
  }
2346
2469
  },
2347
2470
 
@@ -2350,9 +2473,12 @@
2350
2473
  * at the current caret position.
2351
2474
  *
2352
2475
  * @param {string} value
2476
+ * @param {boolean} silent
2353
2477
  */
2354
- addItem: function(value) {
2355
- debounce_events(this, ['change'], function() {
2478
+ addItem: function(value, silent) {
2479
+ var events = silent ? [] : ['change'];
2480
+
2481
+ debounce_events(this, events, function() {
2356
2482
  var $item, $option, $options;
2357
2483
  var self = this;
2358
2484
  var inputMode = self.settings.mode;
@@ -2365,7 +2491,7 @@
2365
2491
  }
2366
2492
 
2367
2493
  if (!self.options.hasOwnProperty(value)) return;
2368
- if (inputMode === 'single') self.clear();
2494
+ if (inputMode === 'single') self.clear(silent);
2369
2495
  if (inputMode === 'multi' && self.isFull()) return;
2370
2496
 
2371
2497
  $item = $(self.render('item', self.options[value]));
@@ -2398,7 +2524,7 @@
2398
2524
 
2399
2525
  self.updatePlaceholder();
2400
2526
  self.trigger('item_add', value, $item);
2401
- self.updateOriginalInput();
2527
+ self.updateOriginalInput({silent: silent});
2402
2528
  }
2403
2529
  });
2404
2530
  },
@@ -2409,7 +2535,7 @@
2409
2535
  *
2410
2536
  * @param {string} value
2411
2537
  */
2412
- removeItem: function(value) {
2538
+ removeItem: function(value, silent) {
2413
2539
  var self = this;
2414
2540
  var $item, i, idx;
2415
2541
 
@@ -2427,7 +2553,7 @@
2427
2553
  self.items.splice(i, 1);
2428
2554
  self.lastQuery = null;
2429
2555
  if (!self.settings.persist && self.userOptions.hasOwnProperty(value)) {
2430
- self.removeOption(value);
2556
+ self.removeOption(value, silent);
2431
2557
  }
2432
2558
 
2433
2559
  if (i < self.caretPos) {
@@ -2436,9 +2562,9 @@
2436
2562
 
2437
2563
  self.refreshState();
2438
2564
  self.updatePlaceholder();
2439
- self.updateOriginalInput();
2565
+ self.updateOriginalInput({silent: silent});
2440
2566
  self.positionDropdown();
2441
- self.trigger('item_remove', value);
2567
+ self.trigger('item_remove', value, $item);
2442
2568
  }
2443
2569
  },
2444
2570
 
@@ -2450,19 +2576,30 @@
2450
2576
  * Once this completes, it will be added
2451
2577
  * to the item list.
2452
2578
  *
2579
+ * @param {string} value
2580
+ * @param {boolean} [triggerDropdown]
2581
+ * @param {function} [callback]
2453
2582
  * @return {boolean}
2454
2583
  */
2455
- createItem: function(triggerDropdown) {
2584
+ createItem: function(input, triggerDropdown) {
2456
2585
  var self = this;
2457
- var input = $.trim(self.$control_input.val() || '');
2458
2586
  var caret = self.caretPos;
2459
- if (!self.canCreate(input)) return false;
2460
- self.lock();
2587
+ input = input || $.trim(self.$control_input.val() || '');
2461
2588
 
2462
- if (typeof triggerDropdown === 'undefined') {
2589
+ var callback = arguments[arguments.length - 1];
2590
+ if (typeof callback !== 'function') callback = function() {};
2591
+
2592
+ if (typeof triggerDropdown !== 'boolean') {
2463
2593
  triggerDropdown = true;
2464
2594
  }
2465
2595
 
2596
+ if (!self.canCreate(input)) {
2597
+ callback();
2598
+ return false;
2599
+ }
2600
+
2601
+ self.lock();
2602
+
2466
2603
  var setup = (typeof self.settings.create === 'function') ? this.settings.create : function(input) {
2467
2604
  var data = {};
2468
2605
  data[self.settings.labelField] = input;
@@ -2473,15 +2610,16 @@
2473
2610
  var create = once(function(data) {
2474
2611
  self.unlock();
2475
2612
 
2476
- if (!data || typeof data !== 'object') return;
2613
+ if (!data || typeof data !== 'object') return callback();
2477
2614
  var value = hash_key(data[self.settings.valueField]);
2478
- if (!value) return;
2615
+ if (typeof value !== 'string') return callback();
2479
2616
 
2480
2617
  self.setTextboxValue('');
2481
2618
  self.addOption(data);
2482
2619
  self.setCaret(caret);
2483
2620
  self.addItem(value);
2484
2621
  self.refreshOptions(triggerDropdown && self.settings.mode !== 'single');
2622
+ callback(data);
2485
2623
  });
2486
2624
 
2487
2625
  var output = setup.apply(this, [input, create]);
@@ -2499,9 +2637,7 @@
2499
2637
  this.lastQuery = null;
2500
2638
 
2501
2639
  if (this.isSetup) {
2502
- for (var i = 0; i < this.items.length; i++) {
2503
- this.addItem(this.items);
2504
- }
2640
+ this.addItem(this.items);
2505
2641
  }
2506
2642
 
2507
2643
  this.refreshState();
@@ -2561,13 +2697,15 @@
2561
2697
  * Refreshes the original <select> or <input>
2562
2698
  * element to reflect the current state.
2563
2699
  */
2564
- updateOriginalInput: function() {
2565
- var i, n, options, self = this;
2700
+ updateOriginalInput: function(opts) {
2701
+ var i, n, options, label, self = this;
2702
+ opts = opts || {};
2566
2703
 
2567
2704
  if (self.tagType === TAG_SELECT) {
2568
2705
  options = [];
2569
2706
  for (i = 0, n = self.items.length; i < n; i++) {
2570
- options.push('<option value="' + escape_html(self.items[i]) + '" selected="selected"></option>');
2707
+ label = self.options[self.items[i]][self.settings.labelField] || '';
2708
+ options.push('<option value="' + escape_html(self.items[i]) + '" selected="selected">' + escape_html(label) + '</option>');
2571
2709
  }
2572
2710
  if (!options.length && !this.$input.attr('multiple')) {
2573
2711
  options.push('<option value="" selected="selected"></option>');
@@ -2579,7 +2717,9 @@
2579
2717
  }
2580
2718
 
2581
2719
  if (self.isSetup) {
2582
- self.trigger('change', self.$input.val());
2720
+ if (!opts.silent) {
2721
+ self.trigger('change', self.$input.val());
2722
+ }
2583
2723
  }
2584
2724
  },
2585
2725
 
@@ -2654,8 +2794,10 @@
2654
2794
  /**
2655
2795
  * Resets / clears all selected items
2656
2796
  * from the control.
2797
+ *
2798
+ * @param {boolean} silent
2657
2799
  */
2658
- clear: function() {
2800
+ clear: function(silent) {
2659
2801
  var self = this;
2660
2802
 
2661
2803
  if (!self.items.length) return;
@@ -2665,7 +2807,7 @@
2665
2807
  self.setCaret(0);
2666
2808
  self.setActiveItem(null);
2667
2809
  self.updatePlaceholder();
2668
- self.updateOriginalInput();
2810
+ self.updateOriginalInput({silent: silent});
2669
2811
  self.refreshState();
2670
2812
  self.showInput();
2671
2813
  self.trigger('clear');
@@ -2876,6 +3018,7 @@
2876
3018
  disable: function() {
2877
3019
  var self = this;
2878
3020
  self.$input.prop('disabled', true);
3021
+ self.$control_input.prop('disabled', true).prop('tabindex', -1);
2879
3022
  self.isDisabled = true;
2880
3023
  self.lock();
2881
3024
  },
@@ -2887,6 +3030,7 @@
2887
3030
  enable: function() {
2888
3031
  var self = this;
2889
3032
  self.$input.prop('disabled', false);
3033
+ self.$control_input.prop('disabled', false).prop('tabindex', self.tabIndex);
2890
3034
  self.isDisabled = false;
2891
3035
  self.unlock();
2892
3036
  },
@@ -2937,7 +3081,7 @@
2937
3081
  var html = '';
2938
3082
  var cache = false;
2939
3083
  var self = this;
2940
- var regex_tag = /^[\t ]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i;
3084
+ var regex_tag = /^[\t \r\n]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i;
2941
3085
 
2942
3086
  if (templateName === 'option' || templateName === 'item') {
2943
3087
  value = hash_key(data[self.settings.valueField]);
@@ -2991,16 +3135,36 @@
2991
3135
  } else {
2992
3136
  delete self.renderCache[templateName];
2993
3137
  }
2994
- }
3138
+ },
2995
3139
 
3140
+ /**
3141
+ * Determines whether or not to display the
3142
+ * create item prompt, given a user input.
3143
+ *
3144
+ * @param {string} input
3145
+ * @return {boolean}
3146
+ */
3147
+ canCreate: function(input) {
3148
+ var self = this;
3149
+ if (!self.settings.create) return false;
3150
+ var filter = self.settings.createFilter;
3151
+ return input.length
3152
+ && (typeof filter !== 'function' || filter.apply(self, [input]))
3153
+ && (typeof filter !== 'string' || new RegExp(filter).test(input))
3154
+ && (!(filter instanceof RegExp) || filter.test(input));
3155
+ }
2996
3156
 
2997
3157
  });
2998
3158
 
2999
3159
 
3000
3160
  Selectize.count = 0;
3001
3161
  Selectize.defaults = {
3162
+ options: [],
3163
+ optgroups: [],
3164
+
3002
3165
  plugins: [],
3003
3166
  delimiter: ',',
3167
+ splitOn: null, // regexp or string for splitting up values from a paste command
3004
3168
  persist: true,
3005
3169
  diacritics: true,
3006
3170
  create: false,
@@ -3014,9 +3178,12 @@
3014
3178
  addPrecedence: false,
3015
3179
  selectOnTab: false,
3016
3180
  preload: false,
3181
+ allowEmptyOption: false,
3182
+ closeAfterSelect: false,
3017
3183
 
3018
3184
  scrollDuration: 60,
3019
3185
  loadThrottle: 300,
3186
+ loadingClass: 'loading',
3020
3187
 
3021
3188
  dataAttr: 'data-data',
3022
3189
  optgroupField: 'optgroup',
@@ -3024,7 +3191,7 @@
3024
3191
  labelField: 'text',
3025
3192
  optgroupLabelField: 'label',
3026
3193
  optgroupValueField: 'value',
3027
- optgroupOrder: null,
3194
+ lockOptgroupOrder: false,
3028
3195
 
3029
3196
  sortField: '$order',
3030
3197
  searchField: ['text'],
@@ -3038,21 +3205,26 @@
3038
3205
 
3039
3206
  dropdownParent: null,
3040
3207
 
3208
+ copyClassesToDropdown: true,
3209
+
3041
3210
  /*
3042
- load : null, // function(query, callback) { ... }
3043
- score : null, // function(search) { ... }
3044
- onInitialize : null, // function() { ... }
3045
- onChange : null, // function(value) { ... }
3046
- onItemAdd : null, // function(value, $item) { ... }
3047
- onItemRemove : null, // function(value) { ... }
3048
- onClear : null, // function() { ... }
3049
- onOptionAdd : null, // function(value, data) { ... }
3050
- onOptionRemove : null, // function(value) { ... }
3051
- onOptionClear : null, // function() { ... }
3052
- onDropdownOpen : null, // function($dropdown) { ... }
3053
- onDropdownClose : null, // function($dropdown) { ... }
3054
- onType : null, // function(str) { ... }
3055
- onDelete : null, // function(values) { ... }
3211
+ load : null, // function(query, callback) { ... }
3212
+ score : null, // function(search) { ... }
3213
+ onInitialize : null, // function() { ... }
3214
+ onChange : null, // function(value) { ... }
3215
+ onItemAdd : null, // function(value, $item) { ... }
3216
+ onItemRemove : null, // function(value) { ... }
3217
+ onClear : null, // function() { ... }
3218
+ onOptionAdd : null, // function(value, data) { ... }
3219
+ onOptionRemove : null, // function(value) { ... }
3220
+ onOptionClear : null, // function() { ... }
3221
+ onOptionGroupAdd : null, // function(id, data) { ... }
3222
+ onOptionGroupRemove : null, // function(id) { ... }
3223
+ onOptionGroupClear : null, // function() { ... }
3224
+ onDropdownOpen : null, // function($dropdown) { ... }
3225
+ onDropdownClose : null, // function($dropdown) { ... }
3226
+ onType : null, // function(str) { ... }
3227
+ onDelete : null, // function(values) { ... }
3056
3228
  */
3057
3229
 
3058
3230
  render: {
@@ -3066,6 +3238,7 @@
3066
3238
  }
3067
3239
  };
3068
3240
 
3241
+
3069
3242
  $.fn.selectize = function(settings_user) {
3070
3243
  var defaults = $.fn.selectize.defaults;
3071
3244
  var settings = $.extend({}, defaults, settings_user);
@@ -3083,19 +3256,27 @@
3083
3256
  * @param {object} settings_element
3084
3257
  */
3085
3258
  var init_textbox = function($input, settings_element) {
3086
- var i, n, values, option, value = $.trim($input.val() || '');
3087
- if (!value.length) return;
3259
+ var i, n, values, option;
3088
3260
 
3089
- values = value.split(settings.delimiter);
3090
- for (i = 0, n = values.length; i < n; i++) {
3091
- option = {};
3092
- option[field_label] = values[i];
3093
- option[field_value] = values[i];
3261
+ var data_raw = $input.attr(attr_data);
3094
3262
 
3095
- settings_element.options[values[i]] = option;
3263
+ if (!data_raw) {
3264
+ var value = $.trim($input.val() || '');
3265
+ if (!settings.allowEmptyOption && !value.length) return;
3266
+ values = value.split(settings.delimiter);
3267
+ for (i = 0, n = values.length; i < n; i++) {
3268
+ option = {};
3269
+ option[field_label] = values[i];
3270
+ option[field_value] = values[i];
3271
+ settings_element.options.push(option);
3272
+ }
3273
+ settings_element.items = values;
3274
+ } else {
3275
+ settings_element.options = JSON.parse(data_raw);
3276
+ for (i = 0, n = settings_element.options.length; i < n; i++) {
3277
+ settings_element.items.push(settings_element.options[i][field_value]);
3278
+ }
3096
3279
  }
3097
-
3098
- settings_element.items = values;
3099
3280
  };
3100
3281
 
3101
3282
  /**
@@ -3107,6 +3288,7 @@
3107
3288
  var init_select = function($input, settings_element) {
3108
3289
  var i, n, tagName, $children, order = 0;
3109
3290
  var options = settings_element.options;
3291
+ var optionsMap = {};
3110
3292
 
3111
3293
  var readData = function($el) {
3112
3294
  var data = attr_data && $el.attr(attr_data);
@@ -3117,37 +3299,36 @@
3117
3299
  };
3118
3300
 
3119
3301
  var addOption = function($option, group) {
3120
- var value, option;
3121
-
3122
3302
  $option = $($option);
3123
3303
 
3124
- value = $option.attr('value') || '';
3125
- if (!value.length) return;
3304
+ var value = hash_key($option.attr('value'));
3305
+ if (!value && !settings.allowEmptyOption) return;
3126
3306
 
3127
3307
  // if the option already exists, it's probably been
3128
3308
  // duplicated in another optgroup. in this case, push
3129
3309
  // the current group to the "optgroup" property on the
3130
3310
  // existing option so that it's rendered in both places.
3131
- if (options.hasOwnProperty(value)) {
3311
+ if (optionsMap.hasOwnProperty(value)) {
3132
3312
  if (group) {
3133
- if (!options[value].optgroup) {
3134
- options[value].optgroup = group;
3135
- } else if (!$.isArray(options[value].optgroup)) {
3136
- options[value].optgroup = [options[value].optgroup, group];
3313
+ var arr = optionsMap[value][field_optgroup];
3314
+ if (!arr) {
3315
+ optionsMap[value][field_optgroup] = group;
3316
+ } else if (!$.isArray(arr)) {
3317
+ optionsMap[value][field_optgroup] = [arr, group];
3137
3318
  } else {
3138
- options[value].optgroup.push(group);
3319
+ arr.push(group);
3139
3320
  }
3140
3321
  }
3141
3322
  return;
3142
3323
  }
3143
3324
 
3144
- option = readData($option) || {};
3325
+ var option = readData($option) || {};
3145
3326
  option[field_label] = option[field_label] || $option.text();
3146
3327
  option[field_value] = option[field_value] || value;
3147
3328
  option[field_optgroup] = option[field_optgroup] || group;
3148
3329
 
3149
- option.$order = ++order;
3150
- options[value] = option;
3330
+ optionsMap[value] = option;
3331
+ options.push(option);
3151
3332
 
3152
3333
  if ($option.is(':selected')) {
3153
3334
  settings_element.items.push(value);
@@ -3164,7 +3345,7 @@
3164
3345
  optgroup = readData($optgroup) || {};
3165
3346
  optgroup[field_optgroup_label] = id;
3166
3347
  optgroup[field_optgroup_value] = id;
3167
- settings_element.optgroups[id] = optgroup;
3348
+ settings_element.optgroups.push(optgroup);
3168
3349
  }
3169
3350
 
3170
3351
  $options = $('option', $optgroup);
@@ -3192,10 +3373,15 @@
3192
3373
  var instance;
3193
3374
  var $input = $(this);
3194
3375
  var tag_name = this.tagName.toLowerCase();
3376
+ var placeholder = $input.attr('placeholder') || $input.attr('data-placeholder');
3377
+ if (!placeholder && !settings.allowEmptyOption) {
3378
+ placeholder = $input.children('option[value=""]').text();
3379
+ }
3380
+
3195
3381
  var settings_element = {
3196
- 'placeholder' : $input.children('option[value=""]').text() || $input.attr('placeholder'),
3197
- 'options' : {},
3198
- 'optgroups' : {},
3382
+ 'placeholder' : placeholder,
3383
+ 'options' : [],
3384
+ 'optgroups' : [],
3199
3385
  'items' : []
3200
3386
  };
3201
3387
 
@@ -3210,6 +3396,9 @@
3210
3396
  };
3211
3397
 
3212
3398
  $.fn.selectize.defaults = Selectize.defaults;
3399
+ $.fn.selectize.support = {
3400
+ validity: SUPPORTS_VALIDITY_API
3401
+ };
3213
3402
 
3214
3403
 
3215
3404
  Selectize.define('drag_drop', function(options) {
@@ -3452,7 +3641,7 @@
3452
3641
  return option[this.settings.labelField];
3453
3642
  };
3454
3643
 
3455
- this.onKeyDown = (function(e) {
3644
+ this.onKeyDown = (function() {
3456
3645
  var original = self.onKeyDown;
3457
3646
  return function(e) {
3458
3647
  var index, option;
@@ -3473,5 +3662,6 @@
3473
3662
  })();
3474
3663
  });
3475
3664
 
3665
+
3476
3666
  return Selectize;
3477
3667
  }));