PageRenderTime 57ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/files/selectize/0.9.0/js/standalone/selectize.js

https://gitlab.com/Mirros/jsdelivr
JavaScript | 1911 lines | 1619 code | 77 blank | 215 comment | 129 complexity | fd0e0663e4e023df1dead39726d6cc3d MD5 | raw file
  1. /**
  2. * sifter.js
  3. * Copyright (c) 2013 Brian Reavis & contributors
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
  6. * file except in compliance with the License. You may obtain a copy of the License at:
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software distributed under
  10. * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
  11. * ANY KIND, either express or implied. See the License for the specific language
  12. * governing permissions and limitations under the License.
  13. *
  14. * @author Brian Reavis <brian@thirdroute.com>
  15. */
  16. (function(root, factory) {
  17. if (typeof define === 'function' && define.amd) {
  18. define('sifter', factory);
  19. } else if (typeof exports === 'object') {
  20. module.exports = factory();
  21. } else {
  22. root.Sifter = factory();
  23. }
  24. }(this, function() {
  25. /**
  26. * Textually searches arrays and hashes of objects
  27. * by property (or multiple properties). Designed
  28. * specifically for autocomplete.
  29. *
  30. * @constructor
  31. * @param {array|object} items
  32. * @param {object} items
  33. */
  34. var Sifter = function(items, settings) {
  35. this.items = items;
  36. this.settings = settings || {diacritics: true};
  37. };
  38. /**
  39. * Splits a search string into an array of individual
  40. * regexps to be used to match results.
  41. *
  42. * @param {string} query
  43. * @returns {array}
  44. */
  45. Sifter.prototype.tokenize = function(query) {
  46. query = trim(String(query || '').toLowerCase());
  47. if (!query || !query.length) return [];
  48. var i, n, regex, letter;
  49. var tokens = [];
  50. var words = query.split(/ +/);
  51. for (i = 0, n = words.length; i < n; i++) {
  52. regex = escape_regex(words[i]);
  53. if (this.settings.diacritics) {
  54. for (letter in DIACRITICS) {
  55. if (DIACRITICS.hasOwnProperty(letter)) {
  56. regex = regex.replace(new RegExp(letter, 'g'), DIACRITICS[letter]);
  57. }
  58. }
  59. }
  60. tokens.push({
  61. string : words[i],
  62. regex : new RegExp(regex, 'i')
  63. });
  64. }
  65. return tokens;
  66. };
  67. /**
  68. * Iterates over arrays and hashes.
  69. *
  70. * ```
  71. * this.iterator(this.items, function(item, id) {
  72. * // invoked for each item
  73. * });
  74. * ```
  75. *
  76. * @param {array|object} object
  77. */
  78. Sifter.prototype.iterator = function(object, callback) {
  79. var iterator;
  80. if (is_array(object)) {
  81. iterator = Array.prototype.forEach || function(callback) {
  82. for (var i = 0, n = this.length; i < n; i++) {
  83. callback(this[i], i, this);
  84. }
  85. };
  86. } else {
  87. iterator = function(callback) {
  88. for (var key in this) {
  89. if (this.hasOwnProperty(key)) {
  90. callback(this[key], key, this);
  91. }
  92. }
  93. };
  94. }
  95. iterator.apply(object, [callback]);
  96. };
  97. /**
  98. * Returns a function to be used to score individual results.
  99. *
  100. * Good matches will have a higher score than poor matches.
  101. * If an item is not a match, 0 will be returned by the function.
  102. *
  103. * @param {object|string} search
  104. * @param {object} options (optional)
  105. * @returns {function}
  106. */
  107. Sifter.prototype.getScoreFunction = function(search, options) {
  108. var self, fields, tokens, token_count;
  109. self = this;
  110. search = self.prepareSearch(search, options);
  111. tokens = search.tokens;
  112. fields = search.options.fields;
  113. token_count = tokens.length;
  114. /**
  115. * Calculates how close of a match the
  116. * given value is against a search token.
  117. *
  118. * @param {mixed} value
  119. * @param {object} token
  120. * @return {number}
  121. */
  122. var scoreValue = function(value, token) {
  123. var score, pos;
  124. if (!value) return 0;
  125. value = String(value || '');
  126. pos = value.search(token.regex);
  127. if (pos === -1) return 0;
  128. score = token.string.length / value.length;
  129. if (pos === 0) score += 0.5;
  130. return score;
  131. };
  132. /**
  133. * Calculates the score of an object
  134. * against the search query.
  135. *
  136. * @param {object} token
  137. * @param {object} data
  138. * @return {number}
  139. */
  140. var scoreObject = (function() {
  141. var field_count = fields.length;
  142. if (!field_count) {
  143. return function() { return 0; };
  144. }
  145. if (field_count === 1) {
  146. return function(token, data) {
  147. return scoreValue(data[fields[0]], token);
  148. };
  149. }
  150. return function(token, data) {
  151. for (var i = 0, sum = 0; i < field_count; i++) {
  152. sum += scoreValue(data[fields[i]], token);
  153. }
  154. return sum / field_count;
  155. };
  156. })();
  157. if (!token_count) {
  158. return function() { return 0; };
  159. }
  160. if (token_count === 1) {
  161. return function(data) {
  162. return scoreObject(tokens[0], data);
  163. };
  164. }
  165. if (search.options.conjunction === 'and') {
  166. return function(data) {
  167. var score;
  168. for (var i = 0, sum = 0; i < token_count; i++) {
  169. score = scoreObject(tokens[i], data);
  170. if (score <= 0) return 0;
  171. sum += score;
  172. }
  173. return sum / token_count;
  174. };
  175. } else {
  176. return function(data) {
  177. for (var i = 0, sum = 0; i < token_count; i++) {
  178. sum += scoreObject(tokens[i], data);
  179. }
  180. return sum / token_count;
  181. };
  182. }
  183. };
  184. /**
  185. * Returns a function that can be used to compare two
  186. * results, for sorting purposes. If no sorting should
  187. * be performed, `null` will be returned.
  188. *
  189. * @param {string|object} search
  190. * @param {object} options
  191. * @return function(a,b)
  192. */
  193. Sifter.prototype.getSortFunction = function(search, options) {
  194. var i, n, self, field, fields, fields_count, multiplier, multipliers, get_field, implicit_score, sort;
  195. self = this;
  196. search = self.prepareSearch(search, options);
  197. sort = (!search.query && options.sort_empty) || options.sort;
  198. /**
  199. * Fetches the specified sort field value
  200. * from a search result item.
  201. *
  202. * @param {string} name
  203. * @param {object} result
  204. * @return {mixed}
  205. */
  206. get_field = function(name, result) {
  207. if (name === '$score') return result.score;
  208. return self.items[result.id][name];
  209. };
  210. // parse options
  211. fields = [];
  212. if (sort) {
  213. for (i = 0, n = sort.length; i < n; i++) {
  214. if (search.query || sort[i].field !== '$score') {
  215. fields.push(sort[i]);
  216. }
  217. }
  218. }
  219. // the "$score" field is implied to be the primary
  220. // sort field, unless it's manually specified
  221. if (search.query) {
  222. implicit_score = true;
  223. for (i = 0, n = fields.length; i < n; i++) {
  224. if (fields[i].field === '$score') {
  225. implicit_score = false;
  226. break;
  227. }
  228. }
  229. if (implicit_score) {
  230. fields.unshift({field: '$score', direction: 'desc'});
  231. }
  232. } else {
  233. for (i = 0, n = fields.length; i < n; i++) {
  234. if (fields[i].field === '$score') {
  235. fields.splice(i, 1);
  236. break;
  237. }
  238. }
  239. }
  240. multipliers = [];
  241. for (i = 0, n = fields.length; i < n; i++) {
  242. multipliers.push(fields[i].direction === 'desc' ? -1 : 1);
  243. }
  244. // build function
  245. fields_count = fields.length;
  246. if (!fields_count) {
  247. return null;
  248. } else if (fields_count === 1) {
  249. field = fields[0].field;
  250. multiplier = multipliers[0];
  251. return function(a, b) {
  252. return multiplier * cmp(
  253. get_field(field, a),
  254. get_field(field, b)
  255. );
  256. };
  257. } else {
  258. return function(a, b) {
  259. var i, result, a_value, b_value, field;
  260. for (i = 0; i < fields_count; i++) {
  261. field = fields[i].field;
  262. result = multipliers[i] * cmp(
  263. get_field(field, a),
  264. get_field(field, b)
  265. );
  266. if (result) return result;
  267. }
  268. return 0;
  269. };
  270. }
  271. };
  272. /**
  273. * Parses a search query and returns an object
  274. * with tokens and fields ready to be populated
  275. * with results.
  276. *
  277. * @param {string} query
  278. * @param {object} options
  279. * @returns {object}
  280. */
  281. Sifter.prototype.prepareSearch = function(query, options) {
  282. if (typeof query === 'object') return query;
  283. options = extend({}, options);
  284. var option_fields = options.fields;
  285. var option_sort = options.sort;
  286. var option_sort_empty = options.sort_empty;
  287. if (option_fields && !is_array(option_fields)) options.fields = [option_fields];
  288. if (option_sort && !is_array(option_sort)) options.sort = [option_sort];
  289. if (option_sort_empty && !is_array(option_sort_empty)) options.sort_empty = [option_sort_empty];
  290. return {
  291. options : options,
  292. query : String(query || '').toLowerCase(),
  293. tokens : this.tokenize(query),
  294. total : 0,
  295. items : []
  296. };
  297. };
  298. /**
  299. * Searches through all items and returns a sorted array of matches.
  300. *
  301. * The `options` parameter can contain:
  302. *
  303. * - fields {string|array}
  304. * - sort {array}
  305. * - score {function}
  306. * - filter {bool}
  307. * - limit {integer}
  308. *
  309. * Returns an object containing:
  310. *
  311. * - options {object}
  312. * - query {string}
  313. * - tokens {array}
  314. * - total {int}
  315. * - items {array}
  316. *
  317. * @param {string} query
  318. * @param {object} options
  319. * @returns {object}
  320. */
  321. Sifter.prototype.search = function(query, options) {
  322. var self = this, value, score, search, calculateScore;
  323. var fn_sort;
  324. var fn_score;
  325. search = this.prepareSearch(query, options);
  326. options = search.options;
  327. query = search.query;
  328. // generate result scoring function
  329. fn_score = options.score || self.getScoreFunction(search);
  330. // perform search and sort
  331. if (query.length) {
  332. self.iterator(self.items, function(item, id) {
  333. score = fn_score(item);
  334. if (options.filter === false || score > 0) {
  335. search.items.push({'score': score, 'id': id});
  336. }
  337. });
  338. } else {
  339. self.iterator(self.items, function(item, id) {
  340. search.items.push({'score': 1, 'id': id});
  341. });
  342. }
  343. fn_sort = self.getSortFunction(search, options);
  344. if (fn_sort) search.items.sort(fn_sort);
  345. // apply limits
  346. search.total = search.items.length;
  347. if (typeof options.limit === 'number') {
  348. search.items = search.items.slice(0, options.limit);
  349. }
  350. return search;
  351. };
  352. // utilities
  353. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  354. var cmp = function(a, b) {
  355. if (typeof a === 'number' && typeof b === 'number') {
  356. return a > b ? 1 : (a < b ? -1 : 0);
  357. }
  358. a = String(a || '').toLowerCase();
  359. b = String(b || '').toLowerCase();
  360. if (a > b) return 1;
  361. if (b > a) return -1;
  362. return 0;
  363. };
  364. var extend = function(a, b) {
  365. var i, n, k, object;
  366. for (i = 1, n = arguments.length; i < n; i++) {
  367. object = arguments[i];
  368. if (!object) continue;
  369. for (k in object) {
  370. if (object.hasOwnProperty(k)) {
  371. a[k] = object[k];
  372. }
  373. }
  374. }
  375. return a;
  376. };
  377. var trim = function(str) {
  378. return (str + '').replace(/^\s+|\s+$|/g, '');
  379. };
  380. var escape_regex = function(str) {
  381. return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
  382. };
  383. var is_array = Array.isArray || ($ && $.isArray) || function(object) {
  384. return Object.prototype.toString.call(object) === '[object Array]';
  385. };
  386. var DIACRITICS = {
  387. 'a': '[aÀÁÂÃÄÅàáâãäå]',
  388. 'c': '[cÇçćĆčČ]',
  389. 'd': '[dđĐďĎ]',
  390. 'e': '[eÈÉÊËèéêëěĚ]',
  391. 'i': '[iÌÍÎÏìíîï]',
  392. 'n': '[nÑñňŇ]',
  393. 'o': '[oÒÓÔÕÕÖØòóôõöø]',
  394. 'r': '[rřŘ]',
  395. 's': '[sŠš]',
  396. 't': '[tťŤ]',
  397. 'u': '[uÙÚÛÜùúûüůŮ]',
  398. 'y': '[yŸÿýÝ]',
  399. 'z': '[zŽž]'
  400. };
  401. // export
  402. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  403. return Sifter;
  404. }));
  405. /**
  406. * microplugin.js
  407. * Copyright (c) 2013 Brian Reavis & contributors
  408. *
  409. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
  410. * file except in compliance with the License. You may obtain a copy of the License at:
  411. * http://www.apache.org/licenses/LICENSE-2.0
  412. *
  413. * Unless required by applicable law or agreed to in writing, software distributed under
  414. * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
  415. * ANY KIND, either express or implied. See the License for the specific language
  416. * governing permissions and limitations under the License.
  417. *
  418. * @author Brian Reavis <brian@thirdroute.com>
  419. */
  420. (function(root, factory) {
  421. if (typeof define === 'function' && define.amd) {
  422. define('microplugin', factory);
  423. } else if (typeof exports === 'object') {
  424. module.exports = factory();
  425. } else {
  426. root.MicroPlugin = factory();
  427. }
  428. }(this, function() {
  429. var MicroPlugin = {};
  430. MicroPlugin.mixin = function(Interface) {
  431. Interface.plugins = {};
  432. /**
  433. * Initializes the listed plugins (with options).
  434. * Acceptable formats:
  435. *
  436. * List (without options):
  437. * ['a', 'b', 'c']
  438. *
  439. * List (with options):
  440. * [{'name': 'a', options: {}}, {'name': 'b', options: {}}]
  441. *
  442. * Hash (with options):
  443. * {'a': { ... }, 'b': { ... }, 'c': { ... }}
  444. *
  445. * @param {mixed} plugins
  446. */
  447. Interface.prototype.initializePlugins = function(plugins) {
  448. var i, n, key;
  449. var self = this;
  450. var queue = [];
  451. self.plugins = {
  452. names : [],
  453. settings : {},
  454. requested : {},
  455. loaded : {}
  456. };
  457. if (utils.isArray(plugins)) {
  458. for (i = 0, n = plugins.length; i < n; i++) {
  459. if (typeof plugins[i] === 'string') {
  460. queue.push(plugins[i]);
  461. } else {
  462. self.plugins.settings[plugins[i].name] = plugins[i].options;
  463. queue.push(plugins[i].name);
  464. }
  465. }
  466. } else if (plugins) {
  467. for (key in plugins) {
  468. if (plugins.hasOwnProperty(key)) {
  469. self.plugins.settings[key] = plugins[key];
  470. queue.push(key);
  471. }
  472. }
  473. }
  474. while (queue.length) {
  475. self.require(queue.shift());
  476. }
  477. };
  478. Interface.prototype.loadPlugin = function(name) {
  479. var self = this;
  480. var plugins = self.plugins;
  481. var plugin = Interface.plugins[name];
  482. if (!Interface.plugins.hasOwnProperty(name)) {
  483. throw new Error('Unable to find "' + name + '" plugin');
  484. }
  485. plugins.requested[name] = true;
  486. plugins.loaded[name] = plugin.fn.apply(self, [self.plugins.settings[name] || {}]);
  487. plugins.names.push(name);
  488. };
  489. /**
  490. * Initializes a plugin.
  491. *
  492. * @param {string} name
  493. */
  494. Interface.prototype.require = function(name) {
  495. var self = this;
  496. var plugins = self.plugins;
  497. if (!self.plugins.loaded.hasOwnProperty(name)) {
  498. if (plugins.requested[name]) {
  499. throw new Error('Plugin has circular dependency ("' + name + '")');
  500. }
  501. self.loadPlugin(name);
  502. }
  503. return plugins.loaded[name];
  504. };
  505. /**
  506. * Registers a plugin.
  507. *
  508. * @param {string} name
  509. * @param {function} fn
  510. */
  511. Interface.define = function(name, fn) {
  512. Interface.plugins[name] = {
  513. 'name' : name,
  514. 'fn' : fn
  515. };
  516. };
  517. };
  518. var utils = {
  519. isArray: Array.isArray || function(vArg) {
  520. return Object.prototype.toString.call(vArg) === '[object Array]';
  521. }
  522. };
  523. return MicroPlugin;
  524. }));
  525. /**
  526. * selectize.js (v0.9.0)
  527. * Copyright (c) 2013 Brian Reavis & contributors
  528. *
  529. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
  530. * file except in compliance with the License. You may obtain a copy of the License at:
  531. * http://www.apache.org/licenses/LICENSE-2.0
  532. *
  533. * Unless required by applicable law or agreed to in writing, software distributed under
  534. * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
  535. * ANY KIND, either express or implied. See the License for the specific language
  536. * governing permissions and limitations under the License.
  537. *
  538. * @author Brian Reavis <brian@thirdroute.com>
  539. */
  540. /*jshint curly:false */
  541. /*jshint browser:true */
  542. (function(root, factory) {
  543. if (typeof define === 'function' && define.amd) {
  544. define('selectize', ['jquery','sifter','microplugin'], factory);
  545. } else if (typeof exports === 'object') {
  546. module.exports = factory(require('jquery'), require('sifter'), require('microplugin'));
  547. } else {
  548. root.Selectize = factory(root.jQuery, root.Sifter, root.MicroPlugin);
  549. }
  550. }(this, function($, Sifter, MicroPlugin) {
  551. 'use strict';
  552. var highlight = function($element, pattern) {
  553. if (typeof pattern === 'string' && !pattern.length) return;
  554. var regex = (typeof pattern === 'string') ? new RegExp(pattern, 'i') : pattern;
  555. var highlight = function(node) {
  556. var skip = 0;
  557. if (node.nodeType === 3) {
  558. var pos = node.data.search(regex);
  559. if (pos >= 0 && node.data.length > 0) {
  560. var match = node.data.match(regex);
  561. var spannode = document.createElement('span');
  562. spannode.className = 'highlight';
  563. var middlebit = node.splitText(pos);
  564. var endbit = middlebit.splitText(match[0].length);
  565. var middleclone = middlebit.cloneNode(true);
  566. spannode.appendChild(middleclone);
  567. middlebit.parentNode.replaceChild(spannode, middlebit);
  568. skip = 1;
  569. }
  570. } else if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
  571. for (var i = 0; i < node.childNodes.length; ++i) {
  572. i += highlight(node.childNodes[i]);
  573. }
  574. }
  575. return skip;
  576. };
  577. return $element.each(function() {
  578. highlight(this);
  579. });
  580. };
  581. var MicroEvent = function() {};
  582. MicroEvent.prototype = {
  583. on: function(event, fct){
  584. this._events = this._events || {};
  585. this._events[event] = this._events[event] || [];
  586. this._events[event].push(fct);
  587. },
  588. off: function(event, fct){
  589. var n = arguments.length;
  590. if (n === 0) return delete this._events;
  591. if (n === 1) return delete this._events[event];
  592. this._events = this._events || {};
  593. if (event in this._events === false) return;
  594. this._events[event].splice(this._events[event].indexOf(fct), 1);
  595. },
  596. trigger: function(event /* , args... */){
  597. this._events = this._events || {};
  598. if (event in this._events === false) return;
  599. for (var i = 0; i < this._events[event].length; i++){
  600. this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1));
  601. }
  602. }
  603. };
  604. /**
  605. * Mixin will delegate all MicroEvent.js function in the destination object.
  606. *
  607. * - MicroEvent.mixin(Foobar) will make Foobar able to use MicroEvent
  608. *
  609. * @param {object} the object which will support MicroEvent
  610. */
  611. MicroEvent.mixin = function(destObject){
  612. var props = ['on', 'off', 'trigger'];
  613. for (var i = 0; i < props.length; i++){
  614. destObject.prototype[props[i]] = MicroEvent.prototype[props[i]];
  615. }
  616. };
  617. var IS_MAC = /Mac/.test(navigator.userAgent);
  618. var KEY_A = 65;
  619. var KEY_COMMA = 188;
  620. var KEY_RETURN = 13;
  621. var KEY_ESC = 27;
  622. var KEY_LEFT = 37;
  623. var KEY_UP = 38;
  624. var KEY_P = 80;
  625. var KEY_RIGHT = 39;
  626. var KEY_DOWN = 40;
  627. var KEY_N = 78;
  628. var KEY_BACKSPACE = 8;
  629. var KEY_DELETE = 46;
  630. var KEY_SHIFT = 16;
  631. var KEY_CMD = IS_MAC ? 91 : 17;
  632. var KEY_CTRL = IS_MAC ? 18 : 17;
  633. var KEY_TAB = 9;
  634. var TAG_SELECT = 1;
  635. var TAG_INPUT = 2;
  636. var isset = function(object) {
  637. return typeof object !== 'undefined';
  638. };
  639. /**
  640. * Converts a scalar to its best string representation
  641. * for hash keys and HTML attribute values.
  642. *
  643. * Transformations:
  644. * 'str' -> 'str'
  645. * null -> ''
  646. * undefined -> ''
  647. * true -> '1'
  648. * false -> '0'
  649. * 0 -> '0'
  650. * 1 -> '1'
  651. *
  652. * @param {string} value
  653. * @returns {string}
  654. */
  655. var hash_key = function(value) {
  656. if (typeof value === 'undefined' || value === null) return '';
  657. if (typeof value === 'boolean') return value ? '1' : '0';
  658. return value + '';
  659. };
  660. /**
  661. * Escapes a string for use within HTML.
  662. *
  663. * @param {string} str
  664. * @returns {string}
  665. */
  666. var escape_html = function(str) {
  667. return (str + '')
  668. .replace(/&/g, '&amp;')
  669. .replace(/</g, '&lt;')
  670. .replace(/>/g, '&gt;')
  671. .replace(/"/g, '&quot;');
  672. };
  673. /**
  674. * Escapes "$" characters in replacement strings.
  675. *
  676. * @param {string} str
  677. * @returns {string}
  678. */
  679. var escape_replace = function(str) {
  680. return (str + '').replace(/\$/g, '$$$$');
  681. };
  682. var hook = {};
  683. /**
  684. * Wraps `method` on `self` so that `fn`
  685. * is invoked before the original method.
  686. *
  687. * @param {object} self
  688. * @param {string} method
  689. * @param {function} fn
  690. */
  691. hook.before = function(self, method, fn) {
  692. var original = self[method];
  693. self[method] = function() {
  694. fn.apply(self, arguments);
  695. return original.apply(self, arguments);
  696. };
  697. };
  698. /**
  699. * Wraps `method` on `self` so that `fn`
  700. * is invoked after the original method.
  701. *
  702. * @param {object} self
  703. * @param {string} method
  704. * @param {function} fn
  705. */
  706. hook.after = function(self, method, fn) {
  707. var original = self[method];
  708. self[method] = function() {
  709. var result = original.apply(self, arguments);
  710. fn.apply(self, arguments);
  711. return result;
  712. };
  713. };
  714. /**
  715. * Builds a hash table out of an array of
  716. * objects, using the specified `key` within
  717. * each object.
  718. *
  719. * @param {string} key
  720. * @param {mixed} objects
  721. */
  722. var build_hash_table = function(key, objects) {
  723. if (!$.isArray(objects)) return objects;
  724. var i, n, table = {};
  725. for (i = 0, n = objects.length; i < n; i++) {
  726. if (objects[i].hasOwnProperty(key)) {
  727. table[objects[i][key]] = objects[i];
  728. }
  729. }
  730. return table;
  731. };
  732. /**
  733. * Wraps `fn` so that it can only be invoked once.
  734. *
  735. * @param {function} fn
  736. * @returns {function}
  737. */
  738. var once = function(fn) {
  739. var called = false;
  740. return function() {
  741. if (called) return;
  742. called = true;
  743. fn.apply(this, arguments);
  744. };
  745. };
  746. /**
  747. * Wraps `fn` so that it can only be called once
  748. * every `delay` milliseconds (invoked on the falling edge).
  749. *
  750. * @param {function} fn
  751. * @param {int} delay
  752. * @returns {function}
  753. */
  754. var debounce = function(fn, delay) {
  755. var timeout;
  756. return function() {
  757. var self = this;
  758. var args = arguments;
  759. window.clearTimeout(timeout);
  760. timeout = window.setTimeout(function() {
  761. fn.apply(self, args);
  762. }, delay);
  763. };
  764. };
  765. /**
  766. * Debounce all fired events types listed in `types`
  767. * while executing the provided `fn`.
  768. *
  769. * @param {object} self
  770. * @param {array} types
  771. * @param {function} fn
  772. */
  773. var debounce_events = function(self, types, fn) {
  774. var type;
  775. var trigger = self.trigger;
  776. var event_args = {};
  777. // override trigger method
  778. self.trigger = function() {
  779. var type = arguments[0];
  780. if (types.indexOf(type) !== -1) {
  781. event_args[type] = arguments;
  782. } else {
  783. return trigger.apply(self, arguments);
  784. }
  785. };
  786. // invoke provided function
  787. fn.apply(self, []);
  788. self.trigger = trigger;
  789. // trigger queued events
  790. for (type in event_args) {
  791. if (event_args.hasOwnProperty(type)) {
  792. trigger.apply(self, event_args[type]);
  793. }
  794. }
  795. };
  796. /**
  797. * A workaround for http://bugs.jquery.com/ticket/6696
  798. *
  799. * @param {object} $parent - Parent element to listen on.
  800. * @param {string} event - Event name.
  801. * @param {string} selector - Descendant selector to filter by.
  802. * @param {function} fn - Event handler.
  803. */
  804. var watchChildEvent = function($parent, event, selector, fn) {
  805. $parent.on(event, selector, function(e) {
  806. var child = e.target;
  807. while (child && child.parentNode !== $parent[0]) {
  808. child = child.parentNode;
  809. }
  810. e.currentTarget = child;
  811. return fn.apply(this, [e]);
  812. });
  813. };
  814. /**
  815. * Determines the current selection within a text input control.
  816. * Returns an object containing:
  817. * - start
  818. * - length
  819. *
  820. * @param {object} input
  821. * @returns {object}
  822. */
  823. var getSelection = function(input) {
  824. var result = {};
  825. if ('selectionStart' in input) {
  826. result.start = input.selectionStart;
  827. result.length = input.selectionEnd - result.start;
  828. } else if (document.selection) {
  829. input.focus();
  830. var sel = document.selection.createRange();
  831. var selLen = document.selection.createRange().text.length;
  832. sel.moveStart('character', -input.value.length);
  833. result.start = sel.text.length - selLen;
  834. result.length = selLen;
  835. }
  836. return result;
  837. };
  838. /**
  839. * Copies CSS properties from one element to another.
  840. *
  841. * @param {object} $from
  842. * @param {object} $to
  843. * @param {array} properties
  844. */
  845. var transferStyles = function($from, $to, properties) {
  846. var i, n, styles = {};
  847. if (properties) {
  848. for (i = 0, n = properties.length; i < n; i++) {
  849. styles[properties[i]] = $from.css(properties[i]);
  850. }
  851. } else {
  852. styles = $from.css();
  853. }
  854. $to.css(styles);
  855. };
  856. /**
  857. * Measures the width of a string within a
  858. * parent element (in pixels).
  859. *
  860. * @param {string} str
  861. * @param {object} $parent
  862. * @returns {int}
  863. */
  864. var measureString = function(str, $parent) {
  865. if (!str) {
  866. return 0;
  867. }
  868. var $test = $('<test>').css({
  869. position: 'absolute',
  870. top: -99999,
  871. left: -99999,
  872. width: 'auto',
  873. padding: 0,
  874. whiteSpace: 'pre'
  875. }).text(str).appendTo('body');
  876. transferStyles($parent, $test, [
  877. 'letterSpacing',
  878. 'fontSize',
  879. 'fontFamily',
  880. 'fontWeight',
  881. 'textTransform'
  882. ]);
  883. var width = $test.width();
  884. $test.remove();
  885. return width;
  886. };
  887. /**
  888. * Sets up an input to grow horizontally as the user
  889. * types. If the value is changed manually, you can
  890. * trigger the "update" handler to resize:
  891. *
  892. * $input.trigger('update');
  893. *
  894. * @param {object} $input
  895. */
  896. var autoGrow = function($input) {
  897. var currentWidth = null;
  898. var update = function(e) {
  899. var value, keyCode, printable, placeholder, width;
  900. var shift, character, selection;
  901. e = e || window.event || {};
  902. if (e.metaKey || e.altKey) return;
  903. if ($input.data('grow') === false) return;
  904. value = $input.val();
  905. if (e.type && e.type.toLowerCase() === 'keydown') {
  906. keyCode = e.keyCode;
  907. printable = (
  908. (keyCode >= 97 && keyCode <= 122) || // a-z
  909. (keyCode >= 65 && keyCode <= 90) || // A-Z
  910. (keyCode >= 48 && keyCode <= 57) || // 0-9
  911. keyCode === 32 // space
  912. );
  913. if (keyCode === KEY_DELETE || keyCode === KEY_BACKSPACE) {
  914. selection = getSelection($input[0]);
  915. if (selection.length) {
  916. value = value.substring(0, selection.start) + value.substring(selection.start + selection.length);
  917. } else if (keyCode === KEY_BACKSPACE && selection.start) {
  918. value = value.substring(0, selection.start - 1) + value.substring(selection.start + 1);
  919. } else if (keyCode === KEY_DELETE && typeof selection.start !== 'undefined') {
  920. value = value.substring(0, selection.start) + value.substring(selection.start + 1);
  921. }
  922. } else if (printable) {
  923. shift = e.shiftKey;
  924. character = String.fromCharCode(e.keyCode);
  925. if (shift) character = character.toUpperCase();
  926. else character = character.toLowerCase();
  927. value += character;
  928. }
  929. }
  930. placeholder = $input.attr('placeholder') || '';
  931. if (!value.length && placeholder.length) {
  932. value = placeholder;
  933. }
  934. width = measureString(value, $input) + 4;
  935. if (width !== currentWidth) {
  936. currentWidth = width;
  937. $input.width(width);
  938. $input.triggerHandler('resize');
  939. }
  940. };
  941. $input.on('keydown keyup update blur', update);
  942. update();
  943. };
  944. var Selectize = function($input, settings) {
  945. var key, i, n, dir, input, self = this;
  946. input = $input[0];
  947. input.selectize = self;
  948. // detect rtl environment
  949. dir = window.getComputedStyle ? window.getComputedStyle(input, null).getPropertyValue('direction') : input.currentStyle && input.currentStyle.direction;
  950. dir = dir || $input.parents('[dir]:first').attr('dir') || '';
  951. // setup default state
  952. $.extend(self, {
  953. settings : settings,
  954. $input : $input,
  955. tagType : input.tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT,
  956. rtl : /rtl/i.test(dir),
  957. eventNS : '.selectize' + (++Selectize.count),
  958. highlightedValue : null,
  959. isOpen : false,
  960. isDisabled : false,
  961. isRequired : $input.is('[required]'),
  962. isInvalid : false,
  963. isLocked : false,
  964. isFocused : false,
  965. isInputHidden : false,
  966. isSetup : false,
  967. isShiftDown : false,
  968. isCmdDown : false,
  969. isCtrlDown : false,
  970. ignoreFocus : false,
  971. ignoreHover : false,
  972. hasOptions : false,
  973. currentResults : null,
  974. lastValue : '',
  975. caretPos : 0,
  976. loading : 0,
  977. loadedSearches : {},
  978. $activeOption : null,
  979. $activeItems : [],
  980. optgroups : {},
  981. options : {},
  982. userOptions : {},
  983. items : [],
  984. renderCache : {},
  985. onSearchChange : debounce(self.onSearchChange, settings.loadThrottle)
  986. });
  987. // search system
  988. self.sifter = new Sifter(this.options, {diacritics: settings.diacritics});
  989. // build options table
  990. $.extend(self.options, build_hash_table(settings.valueField, settings.options));
  991. delete self.settings.options;
  992. // build optgroup table
  993. $.extend(self.optgroups, build_hash_table(settings.optgroupValueField, settings.optgroups));
  994. delete self.settings.optgroups;
  995. // option-dependent defaults
  996. self.settings.mode = self.settings.mode || (self.settings.maxItems === 1 ? 'single' : 'multi');
  997. if (typeof self.settings.hideSelected !== 'boolean') {
  998. self.settings.hideSelected = self.settings.mode === 'multi';
  999. }
  1000. self.initializePlugins(self.settings.plugins);
  1001. self.setupCallbacks();
  1002. self.setupTemplates();
  1003. self.setup();
  1004. };
  1005. // mixins
  1006. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1007. MicroEvent.mixin(Selectize);
  1008. MicroPlugin.mixin(Selectize);
  1009. // methods
  1010. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1011. $.extend(Selectize.prototype, {
  1012. /**
  1013. * Creates all elements and sets up event bindings.
  1014. */
  1015. setup: function() {
  1016. var self = this;
  1017. var settings = self.settings;
  1018. var eventNS = self.eventNS;
  1019. var $window = $(window);
  1020. var $document = $(document);
  1021. var $wrapper;
  1022. var $control;
  1023. var $control_input;
  1024. var $dropdown;
  1025. var $dropdown_content;
  1026. var $dropdown_parent;
  1027. var inputMode;
  1028. var timeout_blur;
  1029. var timeout_focus;
  1030. var tab_index;
  1031. var classes;
  1032. var classes_plugins;
  1033. inputMode = self.settings.mode;
  1034. tab_index = self.$input.attr('tabindex') || '';
  1035. classes = self.$input.attr('class') || '';
  1036. $wrapper = $('<div>').addClass(settings.wrapperClass).addClass(classes).addClass(inputMode);
  1037. $control = $('<div>').addClass(settings.inputClass).addClass('items').appendTo($wrapper);
  1038. $control_input = $('<input type="text" autocomplete="off" />').appendTo($control).attr('tabindex', tab_index);
  1039. $dropdown_parent = $(settings.dropdownParent || $wrapper);
  1040. $dropdown = $('<div>').addClass(settings.dropdownClass).addClass(classes).addClass(inputMode).hide().appendTo($dropdown_parent);
  1041. $dropdown_content = $('<div>').addClass(settings.dropdownContentClass).appendTo($dropdown);
  1042. $wrapper.css({
  1043. width: self.$input[0].style.width
  1044. });
  1045. if (self.plugins.names.length) {
  1046. classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-');
  1047. $wrapper.addClass(classes_plugins);
  1048. $dropdown.addClass(classes_plugins);
  1049. }
  1050. if ((settings.maxItems === null || settings.maxItems > 1) && self.tagType === TAG_SELECT) {
  1051. self.$input.attr('multiple', 'multiple');
  1052. }
  1053. if (self.settings.placeholder) {
  1054. $control_input.attr('placeholder', settings.placeholder);
  1055. }
  1056. self.$wrapper = $wrapper;
  1057. self.$control = $control;
  1058. self.$control_input = $control_input;
  1059. self.$dropdown = $dropdown;
  1060. self.$dropdown_content = $dropdown_content;
  1061. $dropdown.on('mouseenter', '[data-selectable]', function() { return self.onOptionHover.apply(self, arguments); });
  1062. $dropdown.on('mousedown', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); });
  1063. watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); });
  1064. autoGrow($control_input);
  1065. $control.on({
  1066. mousedown : function() { return self.onMouseDown.apply(self, arguments); },
  1067. click : function() { return self.onClick.apply(self, arguments); }
  1068. });
  1069. $control_input.on({
  1070. mousedown : function(e) { e.stopPropagation(); },
  1071. keydown : function() { return self.onKeyDown.apply(self, arguments); },
  1072. keyup : function() { return self.onKeyUp.apply(self, arguments); },
  1073. keypress : function() { return self.onKeyPress.apply(self, arguments); },
  1074. resize : function() { self.positionDropdown.apply(self, []); },
  1075. blur : function() { return self.onBlur.apply(self, arguments); },
  1076. focus : function() { return self.onFocus.apply(self, arguments); }
  1077. });
  1078. $document.on('keydown' + eventNS, function(e) {
  1079. self.isCmdDown = e[IS_MAC ? 'metaKey' : 'ctrlKey'];
  1080. self.isCtrlDown = e[IS_MAC ? 'altKey' : 'ctrlKey'];
  1081. self.isShiftDown = e.shiftKey;
  1082. });
  1083. $document.on('keyup' + eventNS, function(e) {
  1084. if (e.keyCode === KEY_CTRL) self.isCtrlDown = false;
  1085. if (e.keyCode === KEY_SHIFT) self.isShiftDown = false;
  1086. if (e.keyCode === KEY_CMD) self.isCmdDown = false;
  1087. });
  1088. $document.on('mousedown' + eventNS, function(e) {
  1089. if (self.isFocused) {
  1090. // prevent events on the dropdown scrollbar from causing the control to blur
  1091. if (e.target === self.$dropdown[0] || e.target.parentNode === self.$dropdown[0]) {
  1092. return false;
  1093. }
  1094. // blur on click outside
  1095. if (!self.$control.has(e.target).length && e.target !== self.$control[0]) {
  1096. self.blur();
  1097. }
  1098. }
  1099. });
  1100. $window.on(['scroll' + eventNS, 'resize' + eventNS].join(' '), function() {
  1101. if (self.isOpen) {
  1102. self.positionDropdown.apply(self, arguments);
  1103. }
  1104. });
  1105. $window.on('mousemove' + eventNS, function() {
  1106. self.ignoreHover = false;
  1107. });
  1108. // store original children and tab index so that they can be
  1109. // restored when the destroy() method is called.
  1110. this.revertSettings = {
  1111. $children : self.$input.children().detach(),
  1112. tabindex : self.$input.attr('tabindex')
  1113. };
  1114. self.$input.attr('tabindex', -1).hide().after(self.$wrapper);
  1115. if ($.isArray(settings.items)) {
  1116. self.setValue(settings.items);
  1117. delete settings.items;
  1118. }
  1119. // feature detect for the validation API
  1120. if (self.$input[0].validity) {
  1121. self.$input.on('invalid' + eventNS, function(e) {
  1122. e.preventDefault();
  1123. self.isInvalid = true;
  1124. self.refreshState();
  1125. });
  1126. }
  1127. self.updateOriginalInput();
  1128. self.refreshItems();
  1129. self.refreshState();
  1130. self.updatePlaceholder();
  1131. self.isSetup = true;
  1132. if (self.$input.is(':disabled')) {
  1133. self.disable();
  1134. }
  1135. self.on('change', this.onChange);
  1136. self.trigger('initialize');
  1137. // preload options
  1138. if (settings.preload === true) {
  1139. self.onSearchChange('');
  1140. }
  1141. },
  1142. /**
  1143. * Sets up default rendering functions.
  1144. */
  1145. setupTemplates: function() {
  1146. var self = this;
  1147. var field_label = self.settings.labelField;
  1148. var field_optgroup = self.settings.optgroupLabelField;
  1149. var templates = {
  1150. 'optgroup': function(data) {
  1151. return '<div class="optgroup">' + data.html + '</div>';
  1152. },
  1153. 'optgroup_header': function(data, escape) {
  1154. return '<div class="optgroup-header">' + escape(data[field_optgroup]) + '</div>';
  1155. },
  1156. 'option': function(data, escape) {
  1157. return '<div class="option">' + escape(data[field_label]) + '</div>';
  1158. },
  1159. 'item': function(data, escape) {
  1160. return '<div class="item">' + escape(data[field_label]) + '</div>';
  1161. },
  1162. 'option_create': function(data, escape) {
  1163. return '<div class="create">Add <strong>' + escape(data.input) + '</strong>&hellip;</div>';
  1164. }
  1165. };
  1166. self.settings.render = $.extend({}, templates, self.settings.render);
  1167. },
  1168. /**
  1169. * Maps fired events to callbacks provided
  1170. * in the settings used when creating the control.
  1171. */
  1172. setupCallbacks: function() {
  1173. var key, fn, callbacks = {
  1174. 'initialize' : 'onInitialize',
  1175. 'change' : 'onChange',
  1176. 'item_add' : 'onItemAdd',
  1177. 'item_remove' : 'onItemRemove',
  1178. 'clear' : 'onClear',
  1179. 'option_add' : 'onOptionAdd',
  1180. 'option_remove' : 'onOptionRemove',
  1181. 'option_clear' : 'onOptionClear',
  1182. 'dropdown_open' : 'onDropdownOpen',
  1183. 'dropdown_close' : 'onDropdownClose',
  1184. 'type' : 'onType'
  1185. };
  1186. for (key in callbacks) {
  1187. if (callbacks.hasOwnProperty(key)) {
  1188. fn = this.settings[callbacks[key]];
  1189. if (fn) this.on(key, fn);
  1190. }
  1191. }
  1192. },
  1193. /**
  1194. * Triggered when the main control element
  1195. * has a click event.
  1196. *
  1197. * @param {object} e
  1198. * @return {boolean}
  1199. */
  1200. onClick: function(e) {
  1201. var self = this;
  1202. // necessary for mobile webkit devices (manual focus triggering
  1203. // is ignored unless invoked within a click event)
  1204. if (!self.isFocused) {
  1205. self.focus();
  1206. e.preventDefault();
  1207. }
  1208. },
  1209. /**
  1210. * Triggered when the main control element
  1211. * has a mouse down event.
  1212. *
  1213. * @param {object} e
  1214. * @return {boolean}
  1215. */
  1216. onMouseDown: function(e) {
  1217. var self = this;
  1218. var defaultPrevented = e.isDefaultPrevented();
  1219. var $target = $(e.target);
  1220. if (self.isFocused) {
  1221. // retain focus by preventing native handling. if the
  1222. // event target is the input it should not be modified.
  1223. // otherwise, text selection within the input won't work.
  1224. if (e.target !== self.$control_input[0]) {
  1225. if (self.settings.mode === 'single') {
  1226. // toggle dropdown
  1227. self.isOpen ? self.close() : self.open();
  1228. } else if (!defaultPrevented) {
  1229. self.setActiveItem(null);
  1230. }
  1231. return false;
  1232. }
  1233. } else {
  1234. // give control focus
  1235. if (!defaultPrevented) {
  1236. window.setTimeout(function() {
  1237. self.focus();
  1238. }, 0);
  1239. }
  1240. }
  1241. },
  1242. /**
  1243. * Triggered when the value of the control has been changed.
  1244. * This should propagate the event to the original DOM
  1245. * input / select element.
  1246. */
  1247. onChange: function() {
  1248. this.$input.trigger('change');
  1249. },
  1250. /**
  1251. * Triggered on <input> keypress.
  1252. *
  1253. * @param {object} e
  1254. * @returns {boolean}
  1255. */
  1256. onKeyPress: function(e) {
  1257. if (this.isLocked) return e && e.preventDefault();
  1258. var character = String.fromCharCode(e.keyCode || e.which);
  1259. if (this.settings.create && character === this.settings.delimiter) {
  1260. this.createItem();
  1261. e.preventDefault();
  1262. return false;
  1263. }
  1264. },
  1265. /**
  1266. * Triggered on <input> keydown.
  1267. *
  1268. * @param {object} e
  1269. * @returns {boolean}
  1270. */
  1271. onKeyDown: function(e) {
  1272. var isInput = e.target === this.$control_input[0];
  1273. var self = this;
  1274. if (self.isLocked) {
  1275. if (e.keyCode !== KEY_TAB) {
  1276. e.preventDefault();
  1277. }
  1278. return;
  1279. }
  1280. switch (e.keyCode) {
  1281. case KEY_A:
  1282. if (self.isCmdDown) {
  1283. self.selectAll();
  1284. return;
  1285. }
  1286. break;
  1287. case KEY_ESC:
  1288. self.close();
  1289. return;
  1290. case KEY_N:
  1291. if (!e.ctrlKey) break;
  1292. case KEY_DOWN:
  1293. if (!self.isOpen && self.hasOptions) {
  1294. self.open();
  1295. } else if (self.$activeOption) {
  1296. self.ignoreHover = true;
  1297. var $next = self.getAdjacentOption(self.$activeOption, 1);
  1298. if ($next.length) self.setActiveOption($next, true, true);
  1299. }
  1300. e.preventDefault();
  1301. return;
  1302. case KEY_P:
  1303. if (!e.ctrlKey) break;
  1304. case KEY_UP:
  1305. if (self.$activeOption) {
  1306. self.ignoreHover = true;
  1307. var $prev = self.getAdjacentOption(self.$activeOption, -1);
  1308. if ($prev.length) self.setActiveOption($prev, true, true);
  1309. }
  1310. e.preventDefault();
  1311. return;
  1312. case KEY_RETURN:
  1313. if (self.isOpen && self.$activeOption) {
  1314. self.onOptionSelect({currentTarget: self.$activeOption});
  1315. }
  1316. e.preventDefault();
  1317. return;
  1318. case KEY_LEFT:
  1319. self.advanceSelection(-1, e);
  1320. return;
  1321. case KEY_RIGHT:
  1322. self.advanceSelection(1, e);
  1323. return;
  1324. case KEY_TAB:
  1325. if (self.isOpen && self.$activeOption) {
  1326. self.onOptionSelect({currentTarget: self.$activeOption});
  1327. }
  1328. if (self.settings.create && self.createItem()) {
  1329. e.preventDefault();
  1330. }
  1331. return;
  1332. case KEY_BACKSPACE:
  1333. case KEY_DELETE:
  1334. self.deleteSelection(e);
  1335. return;
  1336. }
  1337. if (self.isFull() || self.isInputHidden) {
  1338. e.preventDefault();
  1339. return;
  1340. }
  1341. },
  1342. /**
  1343. * Triggered on <input> keyup.
  1344. *
  1345. * @param {object} e
  1346. * @returns {boolean}
  1347. */
  1348. onKeyUp: function(e) {
  1349. var self = this;
  1350. if (self.isLocked) return e && e.preventDefault();
  1351. var value = self.$control_input.val() || '';
  1352. if (self.lastValue !== value) {
  1353. self.lastValue = value;
  1354. self.onSearchChange(value);
  1355. self.refreshOptions();
  1356. self.trigger('type', value);
  1357. }
  1358. },
  1359. /**
  1360. * Invokes the user-provide option provider / loader.
  1361. *
  1362. * Note: this function is debounced in the Selectize
  1363. * constructor (by `settings.loadDelay` milliseconds)
  1364. *
  1365. * @param {string} value
  1366. */
  1367. onSearchChange: function(value) {
  1368. var self = this;
  1369. var fn = self.settings.load;
  1370. if (!fn) return;
  1371. if (self.loadedSearches.hasOwnProperty(value)) return;
  1372. self.loadedSearches[value] = true;
  1373. self.load(function(callback) {
  1374. fn.apply(self, [value, callback]);
  1375. });
  1376. },
  1377. /**
  1378. * Triggered on <input> focus.
  1379. *
  1380. * @param {object} e (optional)
  1381. * @returns {boolean}
  1382. */
  1383. onFocus: function(e) {
  1384. var self = this;
  1385. self.isFocused = true;
  1386. if (self.isDisabled) {
  1387. self.blur();
  1388. e && e.preventDefault();
  1389. return false;
  1390. }
  1391. if (self.ignoreFocus) return;
  1392. if (self.settings.preload === 'focus') self.onSearchChange('');
  1393. if (!self.$activeItems.length) {
  1394. self.showInput();
  1395. self.setActiveItem(null);
  1396. self.refreshOptions(!!self.settings.openOnFocus);
  1397. }
  1398. self.refreshState();
  1399. },
  1400. /**
  1401. * Triggered on <input> blur.
  1402. *
  1403. * @param {object} e
  1404. * @returns {boolean}
  1405. */
  1406. onBlur: function(e) {
  1407. var self = this;
  1408. self.isFocused = false;
  1409. if (self.ignoreFocus) return;
  1410. if (self.settings.create && self.settings.createOnBlur) {
  1411. self.createItem(false);
  1412. }
  1413. self.close();
  1414. self.setTextboxValue('');
  1415. self.setActiveItem(null);
  1416. self.setActiveOption(null);
  1417. self.setCaret(self.items.length);
  1418. self.refreshState();
  1419. },
  1420. /**
  1421. * Triggered when the user rolls over
  1422. * an option in the autocomplete dropdown menu.
  1423. *
  1424. * @param {object} e
  1425. * @returns {boolean}
  1426. */
  1427. onOptionHover: function(e) {
  1428. if (this.ignoreHover) return;
  1429. this.setActiveOption(e.currentTarget, false);
  1430. },
  1431. /**
  1432. * Triggered when the user clicks on an option
  1433. * in the autocomplete dropdown menu.
  1434. *
  1435. * @param {object} e
  1436. * @returns {boolean}
  1437. */
  1438. onOptionSelect: function(e) {
  1439. var value, $target, $option, self = this;
  1440. if (e.preventDefault) {
  1441. e.preventDefault();
  1442. e.stopPropagation();
  1443. }
  1444. $target = $(e.currentTarget);
  1445. if ($target.hasClass('create')) {
  1446. self.createItem();
  1447. } else {
  1448. value = $target.attr('data-value');
  1449. if (value) {
  1450. self.lastQuery = null;
  1451. self.setTextboxValue('');
  1452. self.addItem(value);
  1453. if (!self.settings.hideSelected && e.type && /mouse/.test(e.type)) {
  1454. self.setActiveOption(self.getOption(value));
  1455. }
  1456. }
  1457. }
  1458. },
  1459. /**
  1460. * Triggered when the user clicks on an item
  1461. * that has been selected.
  1462. *
  1463. * @param {object} e
  1464. * @returns {boolean}
  1465. */
  1466. onItemSelect: function(e) {
  1467. var self = this;
  1468. if (self.isLocked) return;
  1469. if (self.settings.mode === 'multi') {
  1470. e.preventDefault();
  1471. self.setActiveItem(e.currentTarget, e);
  1472. }
  1473. },
  1474. /**
  1475. * Invokes the provided method that provides
  1476. * results to a callback---which are then added
  1477. * as options to the control.
  1478. *
  1479. * @param {function} fn
  1480. */
  1481. load: function(fn) {
  1482. var self = this;
  1483. var $wrapper = self.$wrapper.addClass('loading');
  1484. self.loading++;
  1485. fn.apply(self, [function(results) {
  1486. self.loading = Math.max(self.loading - 1, 0);
  1487. if (results && results.length) {
  1488. self.addOption(results);
  1489. self.refreshOptions(self.isFocused && !self.isInputHidden);
  1490. }
  1491. if (!self.loading) {
  1492. $wrapper.removeClass('loading');
  1493. }
  1494. self.trigger('load', results);
  1495. }]);
  1496. },
  1497. /**
  1498. * Sets the input field of the control to the specified value.
  1499. *
  1500. * @param {string} value
  1501. */
  1502. setTextboxValue: function(value) {
  1503. this.$control_input.val(value).triggerHandler('update');
  1504. this.lastValue = value;
  1505. },
  1506. /**
  1507. * Returns the value of the control. If multiple items
  1508. * can be selected (e.g. <select multiple>), this returns
  1509. * an array. If only one item can be selected, this
  1510. * returns a string.
  1511. *
  1512. * @returns {mixed}
  1513. */
  1514. getValue: function() {
  1515. if (this.tagType === TAG_SELECT && this.$input.attr('multiple')) {
  1516. return this.items;
  1517. } else {
  1518. return this.items.join(this.settings.delimiter);
  1519. }
  1520. },
  1521. /**
  1522. * Resets the selected items to the given value.
  1523. *
  1524. * @param {mixed} value
  1525. */
  1526. setValue: function(value) {
  1527. debounce_events(this, ['change'], function() {
  1528. this.clear();
  1529. this.addItems(value);
  1530. });
  1531. },
  1532. /**
  1533. * Sets the selected item.
  1534. *
  1535. * @param {object} $item
  1536. * @param {object} e (optional)
  1537. */
  1538. setActiveItem: function($item, e) {
  1539. var self = this;
  1540. var eventName;
  1541. var i, idx, begin, end, item, swap;
  1542. var $last;
  1543. if (self.settings.mode === 'single') return;
  1544. $item = $($item);
  1545. // clear the active selection
  1546. if (!$item.length) {
  1547. $(self.$activeItems).removeClass('active');
  1548. self.$activeItems = [];
  1549. if (self.isFocused) {
  1550. self.showInput();
  1551. }
  1552. return;
  1553. }
  1554. // modify selection
  1555. eventName = e && e.type.toLowerCase();
  1556. if (eventName === 'mousedown' && self.isShiftDown && self.$activeItems.length) {
  1557. $last = self.$control.children('.active:last');
  1558. begin = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$last[0]]);
  1559. end = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$item[0]]);
  1560. if (begin > end) {
  1561. swap = begin;
  1562. begin = end;
  1563. end = swap;
  1564. }
  1565. for (i = begin; i <= end; i++) {
  1566. item = self.$control[0].childNodes[i];
  1567. if (self.$activeItems.indexOf(item) === -1) {
  1568. $(item).addClass('active');
  1569. self.$activeItems.push(item);
  1570. }
  1571. }
  1572. e.preventDefault();
  1573. } else if ((eventName === 'mousedown' && self.isCtrlDown) || (eventName === 'keydown' && this.isShiftDown)) {
  1574. if ($item.hasClass('active')) {
  1575. idx = self.$activeItems.indexOf($item[0]);
  1576. self.$activeItems.splice(idx, 1);
  1577. $item.removeClass('active');
  1578. } else {
  1579. self.$activeItems.push($item.addClass('active')[0]);
  1580. }
  1581. } else {
  1582. $(self.$activeItems).removeClass('active');
  1583. self.$activeItems = [$item.addClass('active')[0]];
  1584. }
  1585. // ensure control has focus
  1586. self.hideInput();
  1587. if (!this.isFocused) {
  1588. self.focus();
  1589. }
  1590. },
  1591. /**
  1592. * Sets the selected item in the dropdown menu
  1593. * of available options.
  1594. *
  1595. * @param {object} $object
  1596. * @param {boolean} scroll
  1597. * @param {boolean} animate
  1598. */
  1599. setActiveOption: function($option, scroll, animate) {
  1600. var height_menu, height_item, y;
  1601. var scroll_top, scroll_bottom;
  1602. var self = this;
  1603. if (self.$activeOption) self.$activeOption.removeClass('active');
  1604. self.$activeOption = null;
  1605. $option = $($option);
  1606. if (!$option.length) return;
  1607. self.$activeOption = $option.addClass('active');
  1608. if (scroll || !isset(scroll)) {
  1609. height_menu = self.$dropdown_content.height();
  1610. height_item = self.$activeOption.outerHeight(true);
  1611. scroll = self.$dropdown_content.scrollTop() || 0;
  1612. y = self.$activeOption.offset().top - self.$dropdown_content.offset().top + scroll;
  1613. scroll_top = y;
  1614. scroll_bottom = y - height_menu + height_item;
  1615. if (y + height_item > height_menu + scroll) {
  1616. self.$dropdown_content.stop().animate({scrollTop: scroll_bottom}, animate ? self.settings.scrollDuration : 0);
  1617. } else if (y < scroll) {
  1618. self.$dropdown_content.stop().animate({scrollTop: scroll_top}, animate ? self.settings.scrollDuration : 0);
  1619. }
  1620. }
  1621. },
  1622. /**
  1623. * Selects all items (CTRL + A).
  1624. */
  1625. selectAll: function() {
  1626. var self = this;
  1627. if (self.settings.mode === 'single') return;
  1628. self.$activeItems = Array.prototype.slice.apply(self.$control.children(':not(input)').addClass('active'));
  1629. if (self.$activeItems.length) {
  1630. self.hideInput();
  1631. self.close();
  1632. }
  1633. self.focus();
  1634. },
  1635. /**
  1636. * Hides the input element out of view, while
  1637. * retaining its focus.
  1638. */
  1639. hideInput: function() {
  1640. var self = this;
  1641. self.setTextboxValue('');
  1642. self.$control_input.css({opacity: 0, position: 'absolute', left: self.rtl ? 10000 : -10000});
  1643. self.isInputHidden = true;
  1644. },
  1645. /**
  1646. * Restores input visibility.
  1647. */
  1648. showInput: function() {
  1649. this.$control_input.css({opacity: 1, position: 'relative', left: 0});
  1650. this.isInputHidden = false;
  1651. },
  1652. /**
  1653. * Gives the control focus. If "trigger" is falsy,
  1654. * focus handlers won't be fired--causing the focus
  1655. * to happen silently in the background.
  1656. *
  1657. * @param {boolean} trigger
  1658. */
  1659. focus: function() {
  1660. var self = this;
  1661. if (self.isDisabled) return;
  1662. self.ignoreFocus = true;
  1663. self.$control_input[0].focus();
  1664. window.setTimeout(function() {
  1665. self.ignoreFocus = false;
  1666. self.onFocus();
  1667. }, 0);
  1668. },
  1669. /**
  1670. * Forces the control out of focus.
  1671. */
  1672. blur: function() {
  1673. this.$control_input.trigger('blur');
  1674. },
  1675. /**
  1676. * Returns a function that scores an object
  1677. * to show how good of a match it is to the
  1678. * provided query.
  1679. *
  1680. * @param {string} query
  1681. * @param {object} options
  1682. * @return {function}
  1683. */
  1684. getScoreFunction: function(query) {
  1685. return this.sifter.getScoreFunction(query, this.getSearchOptions());
  1686. },
  1687. /**
  1688. * Returns search options for sifter (the system
  1689. * for scoring and sorting results).
  1690. *
  1691. * @see https://github.com/brianreavis/sifter.js
  1692. * @return {object}
  1693. */
  1694. getSearchOptions: function() {
  1695. var settings = this.settings;
  1696. var sort = settings.sortField;
  1697. if (typeof sort === 'string') {
  1698. sort = {field: sort};
  1699. }
  1700. return {
  1701. fields : settings.searchField,
  1702. conjunction : settings.searchConjunction,
  1703. sort : sort
  1704. };
  1705. },
  1706. /