selectize.js 72 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748
  1. /**
  2. * selectize.js (v0.8.5)
  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. /*jshint curly:false */
  17. /*jshint browser:true */
  18. (function(root, factory) {
  19. if (typeof define === 'function' && define.amd) {
  20. define(['jquery','sifter','microplugin'], factory);
  21. } else {
  22. root.Selectize = factory(root.jQuery, root.Sifter, root.MicroPlugin);
  23. }
  24. }(this, function($, Sifter, MicroPlugin) {
  25. 'use strict';
  26. var highlight = function($element, pattern) {
  27. if (typeof pattern === 'string' && !pattern.length) return;
  28. var regex = (typeof pattern === 'string') ? new RegExp(pattern, 'i') : pattern;
  29. var highlight = function(node) {
  30. var skip = 0;
  31. if (node.nodeType === 3) {
  32. var pos = node.data.search(regex);
  33. if (pos >= 0 && node.data.length > 0) {
  34. var match = node.data.match(regex);
  35. var spannode = document.createElement('span');
  36. spannode.className = 'highlight';
  37. var middlebit = node.splitText(pos);
  38. var endbit = middlebit.splitText(match[0].length);
  39. var middleclone = middlebit.cloneNode(true);
  40. spannode.appendChild(middleclone);
  41. middlebit.parentNode.replaceChild(spannode, middlebit);
  42. skip = 1;
  43. }
  44. } else if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
  45. for (var i = 0; i < node.childNodes.length; ++i) {
  46. i += highlight(node.childNodes[i]);
  47. }
  48. }
  49. return skip;
  50. };
  51. return $element.each(function() {
  52. highlight(this);
  53. });
  54. };
  55. var MicroEvent = function() {};
  56. MicroEvent.prototype = {
  57. on: function(event, fct){
  58. this._events = this._events || {};
  59. this._events[event] = this._events[event] || [];
  60. this._events[event].push(fct);
  61. },
  62. off: function(event, fct){
  63. var n = arguments.length;
  64. if (n === 0) return delete this._events;
  65. if (n === 1) return delete this._events[event];
  66. this._events = this._events || {};
  67. if (event in this._events === false) return;
  68. this._events[event].splice(this._events[event].indexOf(fct), 1);
  69. },
  70. trigger: function(event /* , args... */){
  71. this._events = this._events || {};
  72. if (event in this._events === false) return;
  73. for (var i = 0; i < this._events[event].length; i++){
  74. this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1));
  75. }
  76. }
  77. };
  78. /**
  79. * Mixin will delegate all MicroEvent.js function in the destination object.
  80. *
  81. * - MicroEvent.mixin(Foobar) will make Foobar able to use MicroEvent
  82. *
  83. * @param {object} the object which will support MicroEvent
  84. */
  85. MicroEvent.mixin = function(destObject){
  86. var props = ['on', 'off', 'trigger'];
  87. for (var i = 0; i < props.length; i++){
  88. destObject.prototype[props[i]] = MicroEvent.prototype[props[i]];
  89. }
  90. };
  91. var IS_MAC = /Mac/.test(navigator.userAgent);
  92. var KEY_A = 65;
  93. var KEY_COMMA = 188;
  94. var KEY_RETURN = 13;
  95. var KEY_ESC = 27;
  96. var KEY_LEFT = 37;
  97. var KEY_UP = 38;
  98. var KEY_RIGHT = 39;
  99. var KEY_DOWN = 40;
  100. var KEY_BACKSPACE = 8;
  101. var KEY_DELETE = 46;
  102. var KEY_SHIFT = 16;
  103. var KEY_CMD = IS_MAC ? 91 : 17;
  104. var KEY_CTRL = IS_MAC ? 18 : 17;
  105. var KEY_TAB = 9;
  106. var TAG_SELECT = 1;
  107. var TAG_INPUT = 2;
  108. var isset = function(object) {
  109. return typeof object !== 'undefined';
  110. };
  111. /**
  112. * Converts a scalar to its best string representation
  113. * for hash keys and HTML attribute values.
  114. *
  115. * Transformations:
  116. * 'str' -> 'str'
  117. * null -> ''
  118. * undefined -> ''
  119. * true -> '1'
  120. * false -> '0'
  121. * 0 -> '0'
  122. * 1 -> '1'
  123. *
  124. * @param {string} value
  125. * @returns {string}
  126. */
  127. var hash_key = function(value) {
  128. if (typeof value === 'undefined' || value === null) return '';
  129. if (typeof value === 'boolean') return value ? '1' : '0';
  130. return value + '';
  131. };
  132. /**
  133. * Escapes a string for use within HTML.
  134. *
  135. * @param {string} str
  136. * @returns {string}
  137. */
  138. var escape_html = function(str) {
  139. return (str + '')
  140. .replace(/&/g, '&amp;')
  141. .replace(/</g, '&lt;')
  142. .replace(/>/g, '&gt;')
  143. .replace(/"/g, '&quot;');
  144. };
  145. /**
  146. * Escapes "$" characters in replacement strings.
  147. *
  148. * @param {string} str
  149. * @returns {string}
  150. */
  151. var escape_replace = function(str) {
  152. return (str + '').replace(/\$/g, '$$$$');
  153. };
  154. var hook = {};
  155. /**
  156. * Wraps `method` on `self` so that `fn`
  157. * is invoked before the original method.
  158. *
  159. * @param {object} self
  160. * @param {string} method
  161. * @param {function} fn
  162. */
  163. hook.before = function(self, method, fn) {
  164. var original = self[method];
  165. self[method] = function() {
  166. fn.apply(self, arguments);
  167. return original.apply(self, arguments);
  168. };
  169. };
  170. /**
  171. * Wraps `method` on `self` so that `fn`
  172. * is invoked after the original method.
  173. *
  174. * @param {object} self
  175. * @param {string} method
  176. * @param {function} fn
  177. */
  178. hook.after = function(self, method, fn) {
  179. var original = self[method];
  180. self[method] = function() {
  181. var result = original.apply(self, arguments);
  182. fn.apply(self, arguments);
  183. return result;
  184. };
  185. };
  186. /**
  187. * Builds a hash table out of an array of
  188. * objects, using the specified `key` within
  189. * each object.
  190. *
  191. * @param {string} key
  192. * @param {mixed} objects
  193. */
  194. var build_hash_table = function(key, objects) {
  195. if (!$.isArray(objects)) return objects;
  196. var i, n, table = {};
  197. for (i = 0, n = objects.length; i < n; i++) {
  198. if (objects[i].hasOwnProperty(key)) {
  199. table[objects[i][key]] = objects[i];
  200. }
  201. }
  202. return table;
  203. };
  204. /**
  205. * Wraps `fn` so that it can only be invoked once.
  206. *
  207. * @param {function} fn
  208. * @returns {function}
  209. */
  210. var once = function(fn) {
  211. var called = false;
  212. return function() {
  213. if (called) return;
  214. called = true;
  215. fn.apply(this, arguments);
  216. };
  217. };
  218. /**
  219. * Wraps `fn` so that it can only be called once
  220. * every `delay` milliseconds (invoked on the falling edge).
  221. *
  222. * @param {function} fn
  223. * @param {int} delay
  224. * @returns {function}
  225. */
  226. var debounce = function(fn, delay) {
  227. var timeout;
  228. return function() {
  229. var self = this;
  230. var args = arguments;
  231. window.clearTimeout(timeout);
  232. timeout = window.setTimeout(function() {
  233. fn.apply(self, args);
  234. }, delay);
  235. };
  236. };
  237. /**
  238. * Debounce all fired events types listed in `types`
  239. * while executing the provided `fn`.
  240. *
  241. * @param {object} self
  242. * @param {array} types
  243. * @param {function} fn
  244. */
  245. var debounce_events = function(self, types, fn) {
  246. var type;
  247. var trigger = self.trigger;
  248. var event_args = {};
  249. // override trigger method
  250. self.trigger = function() {
  251. var type = arguments[0];
  252. if (types.indexOf(type) !== -1) {
  253. event_args[type] = arguments;
  254. } else {
  255. return trigger.apply(self, arguments);
  256. }
  257. };
  258. // invoke provided function
  259. fn.apply(self, []);
  260. self.trigger = trigger;
  261. // trigger queued events
  262. for (type in event_args) {
  263. if (event_args.hasOwnProperty(type)) {
  264. trigger.apply(self, event_args[type]);
  265. }
  266. }
  267. };
  268. /**
  269. * A workaround for http://bugs.jquery.com/ticket/6696
  270. *
  271. * @param {object} $parent - Parent element to listen on.
  272. * @param {string} event - Event name.
  273. * @param {string} selector - Descendant selector to filter by.
  274. * @param {function} fn - Event handler.
  275. */
  276. var watchChildEvent = function($parent, event, selector, fn) {
  277. $parent.on(event, selector, function(e) {
  278. var child = e.target;
  279. while (child && child.parentNode !== $parent[0]) {
  280. child = child.parentNode;
  281. }
  282. e.currentTarget = child;
  283. return fn.apply(this, [e]);
  284. });
  285. };
  286. /**
  287. * Determines the current selection within a text input control.
  288. * Returns an object containing:
  289. * - start
  290. * - length
  291. *
  292. * @param {object} input
  293. * @returns {object}
  294. */
  295. var getSelection = function(input) {
  296. var result = {};
  297. if ('selectionStart' in input) {
  298. result.start = input.selectionStart;
  299. result.length = input.selectionEnd - result.start;
  300. } else if (document.selection) {
  301. input.focus();
  302. var sel = document.selection.createRange();
  303. var selLen = document.selection.createRange().text.length;
  304. sel.moveStart('character', -input.value.length);
  305. result.start = sel.text.length - selLen;
  306. result.length = selLen;
  307. }
  308. return result;
  309. };
  310. /**
  311. * Copies CSS properties from one element to another.
  312. *
  313. * @param {object} $from
  314. * @param {object} $to
  315. * @param {array} properties
  316. */
  317. var transferStyles = function($from, $to, properties) {
  318. var i, n, styles = {};
  319. if (properties) {
  320. for (i = 0, n = properties.length; i < n; i++) {
  321. styles[properties[i]] = $from.css(properties[i]);
  322. }
  323. } else {
  324. styles = $from.css();
  325. }
  326. $to.css(styles);
  327. };
  328. /**
  329. * Measures the width of a string within a
  330. * parent element (in pixels).
  331. *
  332. * @param {string} str
  333. * @param {object} $parent
  334. * @returns {int}
  335. */
  336. var measureString = function(str, $parent) {
  337. var $test = $('<test>').css({
  338. position: 'absolute',
  339. top: -99999,
  340. left: -99999,
  341. width: 'auto',
  342. padding: 0,
  343. whiteSpace: 'pre'
  344. }).text(str).appendTo('body');
  345. transferStyles($parent, $test, [
  346. 'letterSpacing',
  347. 'fontSize',
  348. 'fontFamily',
  349. 'fontWeight',
  350. 'textTransform'
  351. ]);
  352. var width = $test.width();
  353. $test.remove();
  354. return width;
  355. };
  356. /**
  357. * Sets up an input to grow horizontally as the user
  358. * types. If the value is changed manually, you can
  359. * trigger the "update" handler to resize:
  360. *
  361. * $input.trigger('update');
  362. *
  363. * @param {object} $input
  364. */
  365. var autoGrow = function($input) {
  366. var update = function(e) {
  367. var value, keyCode, printable, placeholder, width;
  368. var shift, character, selection;
  369. e = e || window.event || {};
  370. if (e.metaKey || e.altKey) return;
  371. if ($input.data('grow') === false) return;
  372. value = $input.val();
  373. if (e.type && e.type.toLowerCase() === 'keydown') {
  374. keyCode = e.keyCode;
  375. printable = (
  376. (keyCode >= 97 && keyCode <= 122) || // a-z
  377. (keyCode >= 65 && keyCode <= 90) || // A-Z
  378. (keyCode >= 48 && keyCode <= 57) || // 0-9
  379. keyCode === 32 // space
  380. );
  381. if (keyCode === KEY_DELETE || keyCode === KEY_BACKSPACE) {
  382. selection = getSelection($input[0]);
  383. if (selection.length) {
  384. value = value.substring(0, selection.start) + value.substring(selection.start + selection.length);
  385. } else if (keyCode === KEY_BACKSPACE && selection.start) {
  386. value = value.substring(0, selection.start - 1) + value.substring(selection.start + 1);
  387. } else if (keyCode === KEY_DELETE && typeof selection.start !== 'undefined') {
  388. value = value.substring(0, selection.start) + value.substring(selection.start + 1);
  389. }
  390. } else if (printable) {
  391. shift = e.shiftKey;
  392. character = String.fromCharCode(e.keyCode);
  393. if (shift) character = character.toUpperCase();
  394. else character = character.toLowerCase();
  395. value += character;
  396. }
  397. }
  398. placeholder = $input.attr('placeholder') || '';
  399. if (!value.length && placeholder.length) {
  400. value = placeholder;
  401. }
  402. width = measureString(value, $input) + 4;
  403. if (width !== $input.width()) {
  404. $input.width(width);
  405. $input.triggerHandler('resize');
  406. }
  407. };
  408. $input.on('keydown keyup update blur', update);
  409. update();
  410. };
  411. var Selectize = function($input, settings) {
  412. var key, i, n, dir, input, self = this;
  413. input = $input[0];
  414. input.selectize = self;
  415. // detect rtl environment
  416. dir = window.getComputedStyle ? window.getComputedStyle(input, null).getPropertyValue('direction') : input.currentStyle && input.currentStyle.direction;
  417. dir = dir || $input.parents('[dir]:first').attr('dir') || '';
  418. // setup default state
  419. $.extend(self, {
  420. settings : settings,
  421. $input : $input,
  422. tagType : input.tagName.toLowerCase() === 'select' ? TAG_SELECT : TAG_INPUT,
  423. rtl : /rtl/i.test(dir),
  424. eventNS : '.selectize' + (++Selectize.count),
  425. highlightedValue : null,
  426. isOpen : false,
  427. isDisabled : false,
  428. isRequired : $input.is('[required]'),
  429. isInvalid : false,
  430. isLocked : false,
  431. isFocused : false,
  432. isInputHidden : false,
  433. isSetup : false,
  434. isShiftDown : false,
  435. isCmdDown : false,
  436. isCtrlDown : false,
  437. ignoreFocus : false,
  438. ignoreHover : false,
  439. hasOptions : false,
  440. currentResults : null,
  441. lastValue : '',
  442. caretPos : 0,
  443. loading : 0,
  444. loadedSearches : {},
  445. $activeOption : null,
  446. $activeItems : [],
  447. optgroups : {},
  448. options : {},
  449. userOptions : {},
  450. items : [],
  451. renderCache : {},
  452. onSearchChange : debounce(self.onSearchChange, settings.loadThrottle)
  453. });
  454. // search system
  455. self.sifter = new Sifter(this.options, {diacritics: settings.diacritics});
  456. // build options table
  457. $.extend(self.options, build_hash_table(settings.valueField, settings.options));
  458. delete self.settings.options;
  459. // build optgroup table
  460. $.extend(self.optgroups, build_hash_table(settings.optgroupValueField, settings.optgroups));
  461. delete self.settings.optgroups;
  462. // option-dependent defaults
  463. self.settings.mode = self.settings.mode || (self.settings.maxItems === 1 ? 'single' : 'multi');
  464. if (typeof self.settings.hideSelected !== 'boolean') {
  465. self.settings.hideSelected = self.settings.mode === 'multi';
  466. }
  467. self.initializePlugins(self.settings.plugins);
  468. self.setupCallbacks();
  469. self.setupTemplates();
  470. self.setup();
  471. };
  472. // mixins
  473. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  474. MicroEvent.mixin(Selectize);
  475. MicroPlugin.mixin(Selectize);
  476. // methods
  477. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  478. $.extend(Selectize.prototype, {
  479. /**
  480. * Creates all elements and sets up event bindings.
  481. */
  482. setup: function() {
  483. var self = this;
  484. var settings = self.settings;
  485. var eventNS = self.eventNS;
  486. var $window = $(window);
  487. var $document = $(document);
  488. var $wrapper;
  489. var $control;
  490. var $control_input;
  491. var $dropdown;
  492. var $dropdown_content;
  493. var $dropdown_parent;
  494. var inputMode;
  495. var timeout_blur;
  496. var timeout_focus;
  497. var tab_index;
  498. var classes;
  499. var classes_plugins;
  500. inputMode = self.settings.mode;
  501. tab_index = self.$input.attr('tabindex') || '';
  502. classes = self.$input.attr('class') || '';
  503. $wrapper = $('<div>').addClass(settings.wrapperClass).addClass(classes).addClass(inputMode);
  504. $control = $('<div>').addClass(settings.inputClass).addClass('items').appendTo($wrapper);
  505. $control_input = $('<input type="text" autocomplete="off">').appendTo($control).attr('tabindex', tab_index);
  506. $dropdown_parent = $(settings.dropdownParent || $wrapper);
  507. $dropdown = $('<div>').addClass(settings.dropdownClass).addClass(classes).addClass(inputMode).hide().appendTo($dropdown_parent);
  508. $dropdown_content = $('<div>').addClass(settings.dropdownContentClass).appendTo($dropdown);
  509. $wrapper.css({
  510. width: self.$input[0].style.width
  511. });
  512. if (self.plugins.names.length) {
  513. classes_plugins = 'plugin-' + self.plugins.names.join(' plugin-');
  514. $wrapper.addClass(classes_plugins);
  515. $dropdown.addClass(classes_plugins);
  516. }
  517. if ((settings.maxItems === null || settings.maxItems > 1) && self.tagType === TAG_SELECT) {
  518. self.$input.attr('multiple', 'multiple');
  519. }
  520. if (self.settings.placeholder) {
  521. $control_input.attr('placeholder', settings.placeholder);
  522. }
  523. self.$wrapper = $wrapper;
  524. self.$control = $control;
  525. self.$control_input = $control_input;
  526. self.$dropdown = $dropdown;
  527. self.$dropdown_content = $dropdown_content;
  528. $dropdown.on('mouseenter', '[data-selectable]', function() { return self.onOptionHover.apply(self, arguments); });
  529. $dropdown.on('mousedown', '[data-selectable]', function() { return self.onOptionSelect.apply(self, arguments); });
  530. watchChildEvent($control, 'mousedown', '*:not(input)', function() { return self.onItemSelect.apply(self, arguments); });
  531. autoGrow($control_input);
  532. $control.on({
  533. mousedown : function() { return self.onMouseDown.apply(self, arguments); },
  534. click : function() { return self.onClick.apply(self, arguments); }
  535. });
  536. $control_input.on({
  537. mousedown : function(e) { e.stopPropagation(); },
  538. keydown : function() { return self.onKeyDown.apply(self, arguments); },
  539. keyup : function() { return self.onKeyUp.apply(self, arguments); },
  540. keypress : function() { return self.onKeyPress.apply(self, arguments); },
  541. resize : function() { self.positionDropdown.apply(self, []); },
  542. blur : function() { return self.onBlur.apply(self, arguments); },
  543. focus : function() { return self.onFocus.apply(self, arguments); }
  544. });
  545. $document.on('keydown' + eventNS, function(e) {
  546. self.isCmdDown = e[IS_MAC ? 'metaKey' : 'ctrlKey'];
  547. self.isCtrlDown = e[IS_MAC ? 'altKey' : 'ctrlKey'];
  548. self.isShiftDown = e.shiftKey;
  549. });
  550. $document.on('keyup' + eventNS, function(e) {
  551. if (e.keyCode === KEY_CTRL) self.isCtrlDown = false;
  552. if (e.keyCode === KEY_SHIFT) self.isShiftDown = false;
  553. if (e.keyCode === KEY_CMD) self.isCmdDown = false;
  554. });
  555. $document.on('mousedown' + eventNS, function(e) {
  556. if (self.isFocused) {
  557. // prevent events on the dropdown scrollbar from causing the control to blur
  558. if (e.target === self.$dropdown[0] || e.target.parentNode === self.$dropdown[0]) {
  559. return false;
  560. }
  561. // blur on click outside
  562. if (!self.$control.has(e.target).length && e.target !== self.$control[0]) {
  563. self.blur();
  564. }
  565. }
  566. });
  567. $window.on(['scroll' + eventNS, 'resize' + eventNS].join(' '), function() {
  568. if (self.isOpen) {
  569. self.positionDropdown.apply(self, arguments);
  570. }
  571. });
  572. $window.on('mousemove' + eventNS, function() {
  573. self.ignoreHover = false;
  574. });
  575. // store original children and tab index so that they can be
  576. // restored when the destroy() method is called.
  577. this.revertSettings = {
  578. $children : self.$input.children().detach(),
  579. tabindex : self.$input.attr('tabindex')
  580. };
  581. self.$input.attr('tabindex', -1).hide().after(self.$wrapper);
  582. if ($.isArray(settings.items)) {
  583. self.setValue(settings.items);
  584. delete settings.items;
  585. }
  586. // feature detect for the validation API
  587. if (self.$input[0].validity) {
  588. self.$input.on('invalid' + eventNS, function(e) {
  589. e.preventDefault();
  590. self.isInvalid = true;
  591. self.refreshState();
  592. });
  593. }
  594. self.updateOriginalInput();
  595. self.refreshItems();
  596. self.refreshState();
  597. self.updatePlaceholder();
  598. self.isSetup = true;
  599. if (self.$input.is(':disabled')) {
  600. self.disable();
  601. }
  602. self.on('change', this.onChange);
  603. self.trigger('initialize');
  604. // preload options
  605. if (settings.preload) {
  606. self.onSearchChange('');
  607. }
  608. },
  609. /**
  610. * Sets up default rendering functions.
  611. */
  612. setupTemplates: function() {
  613. var self = this;
  614. var field_label = self.settings.labelField;
  615. var field_optgroup = self.settings.optgroupLabelField;
  616. var templates = {
  617. 'optgroup': function(data) {
  618. return '<div class="optgroup">' + data.html + '</div>';
  619. },
  620. 'optgroup_header': function(data, escape) {
  621. return '<div class="optgroup-header">' + escape(data[field_optgroup]) + '</div>';
  622. },
  623. 'option': function(data, escape) {
  624. return '<div class="option">' + escape(data[field_label]) + '</div>';
  625. },
  626. 'item': function(data, escape) {
  627. return '<div class="item">' + escape(data[field_label]) + '</div>';
  628. },
  629. 'option_create': function(data, escape) {
  630. return '<div class="create">Add <strong>' + escape(data.input) + '</strong>&hellip;</div>';
  631. }
  632. };
  633. self.settings.render = $.extend({}, templates, self.settings.render);
  634. },
  635. /**
  636. * Maps fired events to callbacks provided
  637. * in the settings used when creating the control.
  638. */
  639. setupCallbacks: function() {
  640. var key, fn, callbacks = {
  641. 'initialize' : 'onInitialize',
  642. 'change' : 'onChange',
  643. 'item_add' : 'onItemAdd',
  644. 'item_remove' : 'onItemRemove',
  645. 'clear' : 'onClear',
  646. 'option_add' : 'onOptionAdd',
  647. 'option_remove' : 'onOptionRemove',
  648. 'option_clear' : 'onOptionClear',
  649. 'dropdown_open' : 'onDropdownOpen',
  650. 'dropdown_close' : 'onDropdownClose',
  651. 'type' : 'onType'
  652. };
  653. for (key in callbacks) {
  654. if (callbacks.hasOwnProperty(key)) {
  655. fn = this.settings[callbacks[key]];
  656. if (fn) this.on(key, fn);
  657. }
  658. }
  659. },
  660. /**
  661. * Triggered when the main control element
  662. * has a click event.
  663. *
  664. * @param {object} e
  665. * @return {boolean}
  666. */
  667. onClick: function(e) {
  668. var self = this;
  669. // necessary for mobile webkit devices (manual focus triggering
  670. // is ignored unless invoked within a click event)
  671. if (!self.isFocused) {
  672. self.focus();
  673. e.preventDefault();
  674. }
  675. },
  676. /**
  677. * Triggered when the main control element
  678. * has a mouse down event.
  679. *
  680. * @param {object} e
  681. * @return {boolean}
  682. */
  683. onMouseDown: function(e) {
  684. var self = this;
  685. var defaultPrevented = e.isDefaultPrevented();
  686. var $target = $(e.target);
  687. if (self.isFocused) {
  688. // retain focus by preventing native handling. if the
  689. // event target is the input it should not be modified.
  690. // otherwise, text selection within the input won't work.
  691. if (e.target !== self.$control_input[0]) {
  692. if (self.settings.mode === 'single') {
  693. // toggle dropdown
  694. self.isOpen ? self.close() : self.open();
  695. } else if (!defaultPrevented) {
  696. self.setActiveItem(null);
  697. }
  698. return false;
  699. }
  700. } else {
  701. // give control focus
  702. if (!defaultPrevented) {
  703. window.setTimeout(function() {
  704. self.focus();
  705. }, 0);
  706. }
  707. }
  708. },
  709. /**
  710. * Triggered when the value of the control has been changed.
  711. * This should propagate the event to the original DOM
  712. * input / select element.
  713. */
  714. onChange: function() {
  715. this.$input.trigger('change');
  716. },
  717. /**
  718. * Triggered on <input> keypress.
  719. *
  720. * @param {object} e
  721. * @returns {boolean}
  722. */
  723. onKeyPress: function(e) {
  724. if (this.isLocked) return e && e.preventDefault();
  725. var character = String.fromCharCode(e.keyCode || e.which);
  726. if (this.settings.create && character === this.settings.delimiter) {
  727. this.createItem();
  728. e.preventDefault();
  729. return false;
  730. }
  731. },
  732. /**
  733. * Triggered on <input> keydown.
  734. *
  735. * @param {object} e
  736. * @returns {boolean}
  737. */
  738. onKeyDown: function(e) {
  739. var isInput = e.target === this.$control_input[0];
  740. var self = this;
  741. if (self.isLocked) {
  742. if (e.keyCode !== KEY_TAB) {
  743. e.preventDefault();
  744. }
  745. return;
  746. }
  747. switch (e.keyCode) {
  748. case KEY_A:
  749. if (self.isCmdDown) {
  750. self.selectAll();
  751. return;
  752. }
  753. break;
  754. case KEY_ESC:
  755. self.close();
  756. return;
  757. case KEY_DOWN:
  758. if (!self.isOpen && self.hasOptions) {
  759. self.open();
  760. } else if (self.$activeOption) {
  761. self.ignoreHover = true;
  762. var $next = self.getAdjacentOption(self.$activeOption, 1);
  763. if ($next.length) self.setActiveOption($next, true, true);
  764. }
  765. e.preventDefault();
  766. return;
  767. case KEY_UP:
  768. if (self.$activeOption) {
  769. self.ignoreHover = true;
  770. var $prev = self.getAdjacentOption(self.$activeOption, -1);
  771. if ($prev.length) self.setActiveOption($prev, true, true);
  772. }
  773. e.preventDefault();
  774. return;
  775. case KEY_RETURN:
  776. if (self.isOpen && self.$activeOption) {
  777. self.onOptionSelect({currentTarget: self.$activeOption});
  778. }
  779. e.preventDefault();
  780. return;
  781. case KEY_LEFT:
  782. self.advanceSelection(-1, e);
  783. return;
  784. case KEY_RIGHT:
  785. self.advanceSelection(1, e);
  786. return;
  787. case KEY_TAB:
  788. if (self.settings.create && self.createItem()) {
  789. e.preventDefault();
  790. }
  791. return;
  792. case KEY_BACKSPACE:
  793. case KEY_DELETE:
  794. self.deleteSelection(e);
  795. return;
  796. }
  797. if (self.isFull() || self.isInputHidden) {
  798. e.preventDefault();
  799. return;
  800. }
  801. },
  802. /**
  803. * Triggered on <input> keyup.
  804. *
  805. * @param {object} e
  806. * @returns {boolean}
  807. */
  808. onKeyUp: function(e) {
  809. var self = this;
  810. if (self.isLocked) return e && e.preventDefault();
  811. var value = self.$control_input.val() || '';
  812. if (self.lastValue !== value) {
  813. self.lastValue = value;
  814. self.onSearchChange(value);
  815. self.refreshOptions();
  816. self.trigger('type', value);
  817. }
  818. },
  819. /**
  820. * Invokes the user-provide option provider / loader.
  821. *
  822. * Note: this function is debounced in the Selectize
  823. * constructor (by `settings.loadDelay` milliseconds)
  824. *
  825. * @param {string} value
  826. */
  827. onSearchChange: function(value) {
  828. var self = this;
  829. var fn = self.settings.load;
  830. if (!fn) return;
  831. if (self.loadedSearches.hasOwnProperty(value)) return;
  832. self.loadedSearches[value] = true;
  833. self.load(function(callback) {
  834. fn.apply(self, [value, callback]);
  835. });
  836. },
  837. /**
  838. * Triggered on <input> focus.
  839. *
  840. * @param {object} e (optional)
  841. * @returns {boolean}
  842. */
  843. onFocus: function(e) {
  844. var self = this;
  845. self.isFocused = true;
  846. if (self.isDisabled) {
  847. self.blur();
  848. e && e.preventDefault();
  849. return false;
  850. }
  851. if (self.ignoreFocus) return;
  852. if (self.settings.preload === 'focus') self.onSearchChange('');
  853. if (!self.$activeItems.length) {
  854. self.showInput();
  855. self.setActiveItem(null);
  856. self.refreshOptions(!!self.settings.openOnFocus);
  857. }
  858. self.refreshState();
  859. },
  860. /**
  861. * Triggered on <input> blur.
  862. *
  863. * @param {object} e
  864. * @returns {boolean}
  865. */
  866. onBlur: function(e) {
  867. var self = this;
  868. self.isFocused = false;
  869. if (self.ignoreFocus) return;
  870. if (self.settings.create && self.settings.createOnBlur) {
  871. self.createItem();
  872. }
  873. self.close();
  874. self.setTextboxValue('');
  875. self.setActiveItem(null);
  876. self.setActiveOption(null);
  877. self.setCaret(self.items.length);
  878. self.refreshState();
  879. },
  880. /**
  881. * Triggered when the user rolls over
  882. * an option in the autocomplete dropdown menu.
  883. *
  884. * @param {object} e
  885. * @returns {boolean}
  886. */
  887. onOptionHover: function(e) {
  888. if (this.ignoreHover) return;
  889. this.setActiveOption(e.currentTarget, false);
  890. },
  891. /**
  892. * Triggered when the user clicks on an option
  893. * in the autocomplete dropdown menu.
  894. *
  895. * @param {object} e
  896. * @returns {boolean}
  897. */
  898. onOptionSelect: function(e) {
  899. var value, $target, $option, self = this;
  900. if (e.preventDefault) {
  901. e.preventDefault();
  902. e.stopPropagation();
  903. }
  904. $target = $(e.currentTarget);
  905. if ($target.hasClass('create')) {
  906. self.createItem();
  907. } else {
  908. value = $target.attr('data-value');
  909. if (value) {
  910. self.lastQuery = null;
  911. self.setTextboxValue('');
  912. self.addItem(value);
  913. if (!self.settings.hideSelected && e.type && /mouse/.test(e.type)) {
  914. self.setActiveOption(self.getOption(value));
  915. }
  916. }
  917. }
  918. },
  919. /**
  920. * Triggered when the user clicks on an item
  921. * that has been selected.
  922. *
  923. * @param {object} e
  924. * @returns {boolean}
  925. */
  926. onItemSelect: function(e) {
  927. var self = this;
  928. if (self.isLocked) return;
  929. if (self.settings.mode === 'multi') {
  930. e.preventDefault();
  931. self.setActiveItem(e.currentTarget, e);
  932. }
  933. },
  934. /**
  935. * Invokes the provided method that provides
  936. * results to a callback---which are then added
  937. * as options to the control.
  938. *
  939. * @param {function} fn
  940. */
  941. load: function(fn) {
  942. var self = this;
  943. var $wrapper = self.$wrapper.addClass('loading');
  944. self.loading++;
  945. fn.apply(self, [function(results) {
  946. self.loading = Math.max(self.loading - 1, 0);
  947. if (results && results.length) {
  948. self.addOption(results);
  949. self.refreshOptions(self.isFocused && !self.isInputHidden);
  950. }
  951. if (!self.loading) {
  952. $wrapper.removeClass('loading');
  953. }
  954. self.trigger('load', results);
  955. }]);
  956. },
  957. /**
  958. * Sets the input field of the control to the specified value.
  959. *
  960. * @param {string} value
  961. */
  962. setTextboxValue: function(value) {
  963. this.$control_input.val(value).triggerHandler('update');
  964. this.lastValue = value;
  965. },
  966. /**
  967. * Returns the value of the control. If multiple items
  968. * can be selected (e.g. <select multiple>), this returns
  969. * an array. If only one item can be selected, this
  970. * returns a string.
  971. *
  972. * @returns {mixed}
  973. */
  974. getValue: function() {
  975. if (this.tagType === TAG_SELECT && this.$input.attr('multiple')) {
  976. return this.items;
  977. } else {
  978. return this.items.join(this.settings.delimiter);
  979. }
  980. },
  981. /**
  982. * Resets the selected items to the given value.
  983. *
  984. * @param {mixed} value
  985. */
  986. setValue: function(value) {
  987. debounce_events(this, ['change'], function() {
  988. this.clear();
  989. var items = $.isArray(value) ? value : [value];
  990. for (var i = 0, n = items.length; i < n; i++) {
  991. this.addItem(items[i]);
  992. }
  993. });
  994. },
  995. /**
  996. * Sets the selected item.
  997. *
  998. * @param {object} $item
  999. * @param {object} e (optional)
  1000. */
  1001. setActiveItem: function($item, e) {
  1002. var self = this;
  1003. var eventName;
  1004. var i, idx, begin, end, item, swap;
  1005. var $last;
  1006. if (self.settings.mode === 'single') return;
  1007. $item = $($item);
  1008. // clear the active selection
  1009. if (!$item.length) {
  1010. $(self.$activeItems).removeClass('active');
  1011. self.$activeItems = [];
  1012. if (self.isFocused) {
  1013. self.showInput();
  1014. }
  1015. return;
  1016. }
  1017. // modify selection
  1018. eventName = e && e.type.toLowerCase();
  1019. if (eventName === 'mousedown' && self.isShiftDown && self.$activeItems.length) {
  1020. $last = self.$control.children('.active:last');
  1021. begin = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$last[0]]);
  1022. end = Array.prototype.indexOf.apply(self.$control[0].childNodes, [$item[0]]);
  1023. if (begin > end) {
  1024. swap = begin;
  1025. begin = end;
  1026. end = swap;
  1027. }
  1028. for (i = begin; i <= end; i++) {
  1029. item = self.$control[0].childNodes[i];
  1030. if (self.$activeItems.indexOf(item) === -1) {
  1031. $(item).addClass('active');
  1032. self.$activeItems.push(item);
  1033. }
  1034. }
  1035. e.preventDefault();
  1036. } else if ((eventName === 'mousedown' && self.isCtrlDown) || (eventName === 'keydown' && this.isShiftDown)) {
  1037. if ($item.hasClass('active')) {
  1038. idx = self.$activeItems.indexOf($item[0]);
  1039. self.$activeItems.splice(idx, 1);
  1040. $item.removeClass('active');
  1041. } else {
  1042. self.$activeItems.push($item.addClass('active')[0]);
  1043. }
  1044. } else {
  1045. $(self.$activeItems).removeClass('active');
  1046. self.$activeItems = [$item.addClass('active')[0]];
  1047. }
  1048. // ensure control has focus
  1049. self.hideInput();
  1050. if (!this.isFocused) {
  1051. self.focus();
  1052. }
  1053. },
  1054. /**
  1055. * Sets the selected item in the dropdown menu
  1056. * of available options.
  1057. *
  1058. * @param {object} $object
  1059. * @param {boolean} scroll
  1060. * @param {boolean} animate
  1061. */
  1062. setActiveOption: function($option, scroll, animate) {
  1063. var height_menu, height_item, y;
  1064. var scroll_top, scroll_bottom;
  1065. var self = this;
  1066. if (self.$activeOption) self.$activeOption.removeClass('active');
  1067. self.$activeOption = null;
  1068. $option = $($option);
  1069. if (!$option.length) return;
  1070. self.$activeOption = $option.addClass('active');
  1071. if (scroll || !isset(scroll)) {
  1072. height_menu = self.$dropdown_content.height();
  1073. height_item = self.$activeOption.outerHeight(true);
  1074. scroll = self.$dropdown_content.scrollTop() || 0;
  1075. y = self.$activeOption.offset().top - self.$dropdown_content.offset().top + scroll;
  1076. scroll_top = y;
  1077. scroll_bottom = y - height_menu + height_item;
  1078. if (y + height_item > height_menu + scroll) {
  1079. self.$dropdown_content.stop().animate({scrollTop: scroll_bottom}, animate ? self.settings.scrollDuration : 0);
  1080. } else if (y < scroll) {
  1081. self.$dropdown_content.stop().animate({scrollTop: scroll_top}, animate ? self.settings.scrollDuration : 0);
  1082. }
  1083. }
  1084. },
  1085. /**
  1086. * Selects all items (CTRL + A).
  1087. */
  1088. selectAll: function() {
  1089. var self = this;
  1090. if (self.settings.mode === 'single') return;
  1091. self.$activeItems = Array.prototype.slice.apply(self.$control.children(':not(input)').addClass('active'));
  1092. if (self.$activeItems.length) {
  1093. self.hideInput();
  1094. self.close();
  1095. }
  1096. self.focus();
  1097. },
  1098. /**
  1099. * Hides the input element out of view, while
  1100. * retaining its focus.
  1101. */
  1102. hideInput: function() {
  1103. var self = this;
  1104. self.setTextboxValue('');
  1105. self.$control_input.css({opacity: 0, position: 'absolute', left: self.rtl ? 10000 : -10000});
  1106. self.isInputHidden = true;
  1107. },
  1108. /**
  1109. * Restores input visibility.
  1110. */
  1111. showInput: function() {
  1112. this.$control_input.css({opacity: 1, position: 'relative', left: 0});
  1113. this.isInputHidden = false;
  1114. },
  1115. /**
  1116. * Gives the control focus. If "trigger" is falsy,
  1117. * focus handlers won't be fired--causing the focus
  1118. * to happen silently in the background.
  1119. *
  1120. * @param {boolean} trigger
  1121. */
  1122. focus: function() {
  1123. var self = this;
  1124. if (self.isDisabled) return;
  1125. self.ignoreFocus = true;
  1126. self.$control_input[0].focus();
  1127. window.setTimeout(function() {
  1128. self.ignoreFocus = false;
  1129. self.onFocus();
  1130. }, 0);
  1131. },
  1132. /**
  1133. * Forces the control out of focus.
  1134. */
  1135. blur: function() {
  1136. this.$control_input.trigger('blur');
  1137. },
  1138. /**
  1139. * Returns a function that scores an object
  1140. * to show how good of a match it is to the
  1141. * provided query.
  1142. *
  1143. * @param {string} query
  1144. * @param {object} options
  1145. * @return {function}
  1146. */
  1147. getScoreFunction: function(query) {
  1148. return this.sifter.getScoreFunction(query, this.getSearchOptions());
  1149. },
  1150. /**
  1151. * Returns search options for sifter (the system
  1152. * for scoring and sorting results).
  1153. *
  1154. * @see https://github.com/brianreavis/sifter.js
  1155. * @return {object}
  1156. */
  1157. getSearchOptions: function() {
  1158. var settings = this.settings;
  1159. var sort = settings.sortField;
  1160. if (typeof sort === 'string') {
  1161. sort = {field: sort};
  1162. }
  1163. return {
  1164. fields : settings.searchField,
  1165. conjunction : settings.searchConjunction,
  1166. sort : sort
  1167. };
  1168. },
  1169. /**
  1170. * Searches through available options and returns
  1171. * a sorted array of matches.
  1172. *
  1173. * Returns an object containing:
  1174. *
  1175. * - query {string}
  1176. * - tokens {array}
  1177. * - total {int}
  1178. * - items {array}
  1179. *
  1180. * @param {string} query
  1181. * @returns {object}
  1182. */
  1183. search: function(query) {
  1184. var i, value, score, result, calculateScore;
  1185. var self = this;
  1186. var settings = self.settings;
  1187. var options = this.getSearchOptions();
  1188. // validate user-provided result scoring function
  1189. if (settings.score) {
  1190. calculateScore = self.settings.score.apply(this, [query]);
  1191. if (typeof calculateScore !== 'function') {
  1192. throw new Error('Selectize "score" setting must be a function that returns a function');
  1193. }
  1194. }
  1195. // perform search
  1196. if (query !== self.lastQuery) {
  1197. self.lastQuery = query;
  1198. result = self.sifter.search(query, $.extend(options, {score: calculateScore}));
  1199. self.currentResults = result;
  1200. } else {
  1201. result = $.extend(true, {}, self.currentResults);
  1202. }
  1203. // filter out selected items
  1204. if (settings.hideSelected) {
  1205. for (i = result.items.length - 1; i >= 0; i--) {
  1206. if (self.items.indexOf(hash_key(result.items[i].id)) !== -1) {
  1207. result.items.splice(i, 1);
  1208. }
  1209. }
  1210. }
  1211. return result;
  1212. },
  1213. /**
  1214. * Refreshes the list of available options shown
  1215. * in the autocomplete dropdown menu.
  1216. *
  1217. * @param {boolean} triggerDropdown
  1218. */
  1219. refreshOptions: function(triggerDropdown) {
  1220. var i, j, k, n, groups, groups_order, option, option_html, optgroup, optgroups, html, html_children, has_create_option;
  1221. var $active, $active_before, $create;
  1222. if (typeof triggerDropdown === 'undefined') {
  1223. triggerDropdown = true;
  1224. }
  1225. var self = this;
  1226. var query = self.$control_input.val();
  1227. var results = self.search(query);
  1228. var $dropdown_content = self.$dropdown_content;
  1229. var active_before = self.$activeOption && hash_key(self.$activeOption.attr('data-value'));
  1230. // build markup
  1231. n = results.items.length;
  1232. if (typeof self.settings.maxOptions === 'number') {
  1233. n = Math.min(n, self.settings.maxOptions);
  1234. }
  1235. // render and group available options individually
  1236. groups = {};
  1237. if (self.settings.optgroupOrder) {
  1238. groups_order = self.settings.optgroupOrder;
  1239. for (i = 0; i < groups_order.length; i++) {
  1240. groups[groups_order[i]] = [];
  1241. }
  1242. } else {
  1243. groups_order = [];
  1244. }
  1245. for (i = 0; i < n; i++) {
  1246. option = self.options[results.items[i].id];
  1247. option_html = self.render('option', option);
  1248. optgroup = option[self.settings.optgroupField] || '';
  1249. optgroups = $.isArray(optgroup) ? optgroup : [optgroup];
  1250. for (j = 0, k = optgroups && optgroups.length; j < k; j++) {
  1251. optgroup = optgroups[j];
  1252. if (!self.optgroups.hasOwnProperty(optgroup)) {
  1253. optgroup = '';
  1254. }
  1255. if (!groups.hasOwnProperty(optgroup)) {
  1256. groups[optgroup] = [];
  1257. groups_order.push(optgroup);
  1258. }
  1259. groups[optgroup].push(option_html);
  1260. }
  1261. }
  1262. // render optgroup headers & join groups
  1263. html = [];
  1264. for (i = 0, n = groups_order.length; i < n; i++) {
  1265. optgroup = groups_order[i];
  1266. if (self.optgroups.hasOwnProperty(optgroup) && groups[optgroup].length) {
  1267. // render the optgroup header and options within it,
  1268. // then pass it to the wrapper template
  1269. html_children = self.render('optgroup_header', self.optgroups[optgroup]) || '';
  1270. html_children += groups[optgroup].join('');
  1271. html.push(self.render('optgroup', $.extend({}, self.optgroups[optgroup], {
  1272. html: html_children
  1273. })));
  1274. } else {
  1275. html.push(groups[optgroup].join(''));
  1276. }
  1277. }
  1278. $dropdown_content.html(html.join(''));
  1279. // highlight matching terms inline
  1280. if (self.settings.highlight && results.query.length && results.tokens.length) {
  1281. for (i = 0, n = results.tokens.length; i < n; i++) {
  1282. highlight($dropdown_content, results.tokens[i].regex);
  1283. }
  1284. }
  1285. // add "selected" class to selected options
  1286. if (!self.settings.hideSelected) {
  1287. for (i = 0, n = self.items.length; i < n; i++) {
  1288. self.getOption(self.items[i]).addClass('selected');
  1289. }
  1290. }
  1291. // add create option
  1292. has_create_option = self.settings.create && results.query.length;
  1293. if (has_create_option) {
  1294. $dropdown_content.prepend(self.render('option_create', {input: query}));
  1295. $create = $($dropdown_content[0].childNodes[0]);
  1296. }
  1297. // activate
  1298. self.hasOptions = results.items.length > 0 || has_create_option;
  1299. if (self.hasOptions) {
  1300. if (results.items.length > 0) {
  1301. $active_before = active_before && self.getOption(active_before);
  1302. if ($active_before && $active_before.length) {
  1303. $active = $active_before;
  1304. } else if (self.settings.mode === 'single' && self.items.length) {
  1305. $active = self.getOption(self.items[0]);
  1306. }
  1307. if (!$active || !$active.length) {
  1308. if ($create && !self.settings.addPrecedence) {
  1309. $active = self.getAdjacentOption($create, 1);
  1310. } else {
  1311. $active = $dropdown_content.find('[data-selectable]:first');
  1312. }
  1313. }
  1314. } else {
  1315. $active = $create;
  1316. }
  1317. self.setActiveOption($active);
  1318. if (triggerDropdown && !self.isOpen) { self.open(); }
  1319. } else {
  1320. self.setActiveOption(null);
  1321. if (triggerDropdown && self.isOpen) { self.close(); }
  1322. }
  1323. },
  1324. /**
  1325. * Adds an available option. If it already exists,
  1326. * nothing will happen. Note: this does not refresh
  1327. * the options list dropdown (use `refreshOptions`
  1328. * for that).
  1329. *
  1330. * Usage:
  1331. *
  1332. * this.addOption(data)
  1333. *
  1334. * @param {object} data
  1335. */
  1336. addOption: function(data) {
  1337. var i, n, optgroup, value, self = this;
  1338. if ($.isArray(data)) {
  1339. for (i = 0, n = data.length; i < n; i++) {
  1340. self.addOption(data[i]);
  1341. }
  1342. return;
  1343. }
  1344. value = hash_key(data[self.settings.valueField]);
  1345. if (!value || self.options.hasOwnProperty(value)) return;
  1346. self.userOptions[value] = true;
  1347. self.options[value] = data;
  1348. self.lastQuery = null;
  1349. self.trigger('option_add', value, data);
  1350. },
  1351. /**
  1352. * Registers a new optgroup for options
  1353. * to be bucketed into.
  1354. *
  1355. * @param {string} id
  1356. * @param {object} data
  1357. */
  1358. addOptionGroup: function(id, data) {
  1359. this.optgroups[id] = data;
  1360. this.trigger('optgroup_add', id, data);
  1361. },
  1362. /**
  1363. * Updates an option available for selection. If
  1364. * it is visible in the selected items or options
  1365. * dropdown, it will be re-rendered automatically.
  1366. *
  1367. * @param {string} value
  1368. * @param {object} data
  1369. */
  1370. updateOption: function(value, data) {
  1371. var self = this;
  1372. var $item, $item_new;
  1373. var value_new, index_item, cache_items, cache_options;
  1374. value = hash_key(value);
  1375. value_new = hash_key(data[self.settings.valueField]);
  1376. // sanity checks
  1377. if (!self.options.hasOwnProperty(value)) return;
  1378. if (!value_new) throw new Error('Value must be set in option data');
  1379. // update references
  1380. if (value_new !== value) {
  1381. delete self.options[value];
  1382. index_item = self.items.indexOf(value);
  1383. if (index_item !== -1) {
  1384. self.items.splice(index_item, 1, value_new);
  1385. }
  1386. }
  1387. self.options[value_new] = data;
  1388. // invalidate render cache
  1389. cache_items = self.renderCache['item'];
  1390. cache_options = self.renderCache['option'];
  1391. if (isset(cache_items)) {
  1392. delete cache_items[value];
  1393. delete cache_items[value_new];
  1394. }
  1395. if (isset(cache_options)) {
  1396. delete cache_options[value];
  1397. delete cache_options[value_new];
  1398. }
  1399. // update the item if it's selected
  1400. if (self.items.indexOf(value_new) !== -1) {
  1401. $item = self.getItem(value);
  1402. $item_new = $(self.render('item', data));
  1403. if ($item.hasClass('active')) $item_new.addClass('active');
  1404. $item.replaceWith($item_new);
  1405. }
  1406. // update dropdown contents
  1407. if (self.isOpen) {
  1408. self.refreshOptions(false);
  1409. }
  1410. },
  1411. /**
  1412. * Removes a single option.
  1413. *
  1414. * @param {string} value
  1415. */
  1416. removeOption: function(value) {
  1417. var self = this;
  1418. value = hash_key(value);
  1419. delete self.userOptions[value];
  1420. delete self.options[value];
  1421. self.lastQuery = null;
  1422. self.trigger('option_remove', value);
  1423. self.removeItem(value);
  1424. },
  1425. /**
  1426. * Clears all options.
  1427. */
  1428. clearOptions: function() {
  1429. var self = this;
  1430. self.loadedSearches = {};
  1431. self.userOptions = {};
  1432. self.options = self.sifter.items = {};
  1433. self.lastQuery = null;
  1434. self.trigger('option_clear');
  1435. self.clear();
  1436. },
  1437. /**
  1438. * Returns the jQuery element of the option
  1439. * matching the given value.
  1440. *
  1441. * @param {string} value
  1442. * @returns {object}
  1443. */
  1444. getOption: function(value) {
  1445. return this.getElementWithValue(value, this.$dropdown_content.find('[data-selectable]'));
  1446. },
  1447. /**
  1448. * Returns the jQuery element of the next or
  1449. * previous selectable option.
  1450. *
  1451. * @param {object} $option
  1452. * @param {int} direction can be 1 for next or -1 for previous
  1453. * @return {object}
  1454. */
  1455. getAdjacentOption: function($option, direction) {
  1456. var $options = this.$dropdown.find('[data-selectable]');
  1457. var index = $options.index($option) + direction;
  1458. return index >= 0 && index < $options.length ? $options.eq(index) : $();
  1459. },
  1460. /**
  1461. * Finds the first element with a "data-value" attribute
  1462. * that matches the given value.
  1463. *
  1464. * @param {mixed} value
  1465. * @param {object} $els
  1466. * @return {object}
  1467. */
  1468. getElementWithValue: function(value, $els) {
  1469. value = hash_key(value);
  1470. if (value) {
  1471. for (var i = 0, n = $els.length; i < n; i++) {
  1472. if ($els[i].getAttribute('data-value') === value) {
  1473. return $($els[i]);
  1474. }
  1475. }
  1476. }
  1477. return $();
  1478. },
  1479. /**
  1480. * Returns the jQuery element of the item
  1481. * matching the given value.
  1482. *
  1483. * @param {string} value
  1484. * @returns {object}
  1485. */
  1486. getItem: function(value) {
  1487. return this.getElementWithValue(value, this.$control.children());
  1488. },
  1489. /**
  1490. * "Selects" an item. Adds it to the list
  1491. * at the current caret position.
  1492. *
  1493. * @param {string} value
  1494. */
  1495. addItem: function(value) {
  1496. debounce_events(this, ['change'], function() {
  1497. var $item, $option;
  1498. var self = this;
  1499. var inputMode = self.settings.mode;
  1500. var i, active, options, value_next;
  1501. value = hash_key(value);
  1502. if (self.items.indexOf(value) !== -1) {
  1503. if (inputMode === 'single') self.close();
  1504. return;
  1505. }
  1506. if (!self.options.hasOwnProperty(value)) return;
  1507. if (inputMode === 'single') self.clear();
  1508. if (inputMode === 'multi' && self.isFull()) return;
  1509. $item = $(self.render('item', self.options[value]));
  1510. self.items.splice(self.caretPos, 0, value);
  1511. self.insertAtCaret($item);
  1512. self.refreshState();
  1513. if (self.isSetup) {
  1514. options = self.$dropdown_content.find('[data-selectable]');
  1515. // update menu / remove the option
  1516. $option = self.getOption(value);
  1517. value_next = self.getAdjacentOption($option, 1).attr('data-value');
  1518. self.refreshOptions(self.isFocused && inputMode !== 'single');
  1519. if (value_next) {
  1520. self.setActiveOption(self.getOption(value_next));
  1521. }
  1522. // hide the menu if the maximum number of items have been selected or no options are left
  1523. if (!options.length || (self.settings.maxItems !== null && self.items.length >= self.settings.maxItems)) {
  1524. self.close();
  1525. } else {
  1526. self.positionDropdown();
  1527. }
  1528. self.updatePlaceholder();
  1529. self.trigger('item_add', value, $item);
  1530. self.updateOriginalInput();
  1531. }
  1532. });
  1533. },
  1534. /**
  1535. * Removes the selected item matching
  1536. * the provided value.
  1537. *
  1538. * @param {string} value
  1539. */
  1540. removeItem: function(value) {
  1541. var self = this;
  1542. var $item, i, idx;
  1543. $item = (typeof value === 'object') ? value : self.getItem(value);
  1544. value = hash_key($item.attr('data-value'));
  1545. i = self.items.indexOf(value);
  1546. if (i !== -1) {
  1547. $item.remove();
  1548. if ($item.hasClass('active')) {
  1549. idx = self.$activeItems.indexOf($item[0]);
  1550. self.$activeItems.splice(idx, 1);
  1551. }
  1552. self.items.splice(i, 1);
  1553. self.lastQuery = null;
  1554. if (!self.settings.persist && self.userOptions.hasOwnProperty(value)) {
  1555. self.removeOption(value);
  1556. }
  1557. if (i < self.caretPos) {
  1558. self.setCaret(self.caretPos - 1);
  1559. }
  1560. self.refreshState();
  1561. self.updatePlaceholder();
  1562. self.updateOriginalInput();
  1563. self.positionDropdown();
  1564. self.trigger('item_remove', value);
  1565. }
  1566. },
  1567. /**
  1568. * Invokes the `create` method provided in the
  1569. * selectize options that should provide the data
  1570. * for the new item, given the user input.
  1571. *
  1572. * Once this completes, it will be added
  1573. * to the item list.
  1574. *
  1575. * @return {boolean}
  1576. */
  1577. createItem: function() {
  1578. var self = this;
  1579. var input = $.trim(self.$control_input.val() || '');
  1580. var caret = self.caretPos;
  1581. if (!input.length) return false;
  1582. self.lock();
  1583. var setup = (typeof self.settings.create === 'function') ? this.settings.create : function(input) {
  1584. var data = {};
  1585. data[self.settings.labelField] = input;
  1586. data[self.settings.valueField] = input;
  1587. return data;
  1588. };
  1589. var create = once(function(data) {
  1590. self.unlock();
  1591. if (!data || typeof data !== 'object') return;
  1592. var value = hash_key(data[self.settings.valueField]);
  1593. if (!value) return;
  1594. self.setTextboxValue('');
  1595. self.addOption(data);
  1596. self.setCaret(caret);
  1597. self.addItem(value);
  1598. self.refreshOptions(self.settings.mode !== 'single');
  1599. });
  1600. var output = setup.apply(this, [input, create]);
  1601. if (typeof output !== 'undefined') {
  1602. create(output);
  1603. }
  1604. return true;
  1605. },
  1606. /**
  1607. * Re-renders the selected item lists.
  1608. */
  1609. refreshItems: function() {
  1610. this.lastQuery = null;
  1611. if (this.isSetup) {
  1612. for (var i = 0; i < this.items.length; i++) {
  1613. this.addItem(this.items);
  1614. }
  1615. }
  1616. this.refreshState();
  1617. this.updateOriginalInput();
  1618. },
  1619. /**
  1620. * Updates all state-dependent attributes
  1621. * and CSS classes.
  1622. */
  1623. refreshState: function() {
  1624. var self = this;
  1625. var invalid = self.isRequired && !self.items.length;
  1626. if (!invalid) self.isInvalid = false;
  1627. self.$control_input.prop('required', invalid);
  1628. self.refreshClasses();
  1629. },
  1630. /**
  1631. * Updates all state-dependent CSS classes.
  1632. */
  1633. refreshClasses: function() {
  1634. var self = this;
  1635. var isFull = self.isFull();
  1636. var isLocked = self.isLocked;
  1637. self.$wrapper
  1638. .toggleClass('rtl', self.rtl);
  1639. self.$control
  1640. .toggleClass('focus', self.isFocused)
  1641. .toggleClass('disabled', self.isDisabled)
  1642. .toggleClass('required', self.isRequired)
  1643. .toggleClass('invalid', self.isInvalid)
  1644. .toggleClass('locked', isLocked)
  1645. .toggleClass('full', isFull).toggleClass('not-full', !isFull)
  1646. .toggleClass('input-active', self.isFocused && !self.isInputHidden)
  1647. .toggleClass('dropdown-active', self.isOpen)
  1648. .toggleClass('has-options', !$.isEmptyObject(self.options))
  1649. .toggleClass('has-items', self.items.length > 0);
  1650. self.$control_input.data('grow', !isFull && !isLocked);
  1651. },
  1652. /**
  1653. * Determines whether or not more items can be added
  1654. * to the control without exceeding the user-defined maximum.
  1655. *
  1656. * @returns {boolean}
  1657. */
  1658. isFull: function() {
  1659. return this.settings.maxItems !== null && this.items.length >= this.settings.maxItems;
  1660. },
  1661. /**
  1662. * Refreshes the original <select> or <input>
  1663. * element to reflect the current state.
  1664. */
  1665. updateOriginalInput: function() {
  1666. var i, n, options, self = this;
  1667. if (self.$input[0].tagName.toLowerCase() === 'select') {
  1668. options = [];
  1669. for (i = 0, n = self.items.length; i < n; i++) {
  1670. options.push('<option value="' + escape_html(self.items[i]) + '" selected="selected"></option>');
  1671. }
  1672. if (!options.length && !this.$input.attr('multiple')) {
  1673. options.push('<option value="" selected="selected"></option>');
  1674. }
  1675. self.$input.html(options.join(''));
  1676. } else {
  1677. self.$input.val(self.getValue());
  1678. }
  1679. if (self.isSetup) {
  1680. self.trigger('change', self.$input.val());
  1681. }
  1682. },
  1683. /**
  1684. * Shows/hide the input placeholder depending
  1685. * on if there items in the list already.
  1686. */
  1687. updatePlaceholder: function() {
  1688. if (!this.settings.placeholder) return;
  1689. var $input = this.$control_input;
  1690. if (this.items.length) {
  1691. $input.removeAttr('placeholder');
  1692. } else {
  1693. $input.attr('placeholder', this.settings.placeholder);
  1694. }
  1695. $input.triggerHandler('update');
  1696. },
  1697. /**
  1698. * Shows the autocomplete dropdown containing
  1699. * the available options.
  1700. */
  1701. open: function() {
  1702. var self = this;
  1703. if (self.isLocked || self.isOpen || (self.settings.mode === 'multi' && self.isFull())) return;
  1704. self.focus();
  1705. self.isOpen = true;
  1706. self.refreshState();
  1707. self.$dropdown.css({visibility: 'hidden', display: 'block'});
  1708. self.positionDropdown();
  1709. self.$dropdown.css({visibility: 'visible'});
  1710. self.trigger('dropdown_open', self.$dropdown);
  1711. },
  1712. /**
  1713. * Closes the autocomplete dropdown menu.
  1714. */
  1715. close: function() {
  1716. var self = this;
  1717. var trigger = self.isOpen;
  1718. if (self.settings.mode === 'single' && self.items.length) {
  1719. self.hideInput();
  1720. }
  1721. self.isOpen = false;
  1722. self.$dropdown.hide();
  1723. self.setActiveOption(null);
  1724. self.refreshState();
  1725. if (trigger) self.trigger('dropdown_close', self.$dropdown);
  1726. },
  1727. /**
  1728. * Calculates and applies the appropriate
  1729. * position of the dropdown.
  1730. */
  1731. positionDropdown: function() {
  1732. var $control = this.$control;
  1733. var offset = this.settings.dropdownParent === 'body' ? $control.offset() : $control.position();
  1734. offset.top += $control.outerHeight(true);
  1735. this.$dropdown.css({
  1736. width : $control.outerWidth(),
  1737. top : offset.top,
  1738. left : offset.left
  1739. });
  1740. },
  1741. /**
  1742. * Resets / clears all selected items
  1743. * from the control.
  1744. */
  1745. clear: function() {
  1746. var self = this;
  1747. if (!self.items.length) return;
  1748. self.$control.children(':not(input)').remove();
  1749. self.items = [];
  1750. self.setCaret(0);
  1751. self.updatePlaceholder();
  1752. self.updateOriginalInput();
  1753. self.refreshState();
  1754. self.showInput();
  1755. self.trigger('clear');
  1756. },
  1757. /**
  1758. * A helper method for inserting an element
  1759. * at the current caret position.
  1760. *
  1761. * @param {object} $el
  1762. */
  1763. insertAtCaret: function($el) {
  1764. var caret = Math.min(this.caretPos, this.items.length);
  1765. if (caret === 0) {
  1766. this.$control.prepend($el);
  1767. } else {
  1768. $(this.$control[0].childNodes[caret]).before($el);
  1769. }
  1770. this.setCaret(caret + 1);
  1771. },
  1772. /**
  1773. * Removes the current selected item(s).
  1774. *
  1775. * @param {object} e (optional)
  1776. * @returns {boolean}
  1777. */
  1778. deleteSelection: function(e) {
  1779. var i, n, direction, selection, values, caret, option_select, $option_select, $tail;
  1780. var self = this;
  1781. direction = (e && e.keyCode === KEY_BACKSPACE) ? -1 : 1;
  1782. selection = getSelection(self.$control_input[0]);
  1783. if (self.$activeOption && !self.settings.hideSelected) {
  1784. option_select = self.getAdjacentOption(self.$activeOption, -1).attr('data-value');
  1785. }
  1786. // determine items that will be removed
  1787. values = [];
  1788. if (self.$activeItems.length) {
  1789. $tail = self.$control.children('.active:' + (direction > 0 ? 'last' : 'first'));
  1790. caret = self.$control.children(':not(input)').index($tail);
  1791. if (direction > 0) { caret++; }
  1792. for (i = 0, n = self.$activeItems.length; i < n; i++) {
  1793. values.push($(self.$activeItems[i]).attr('data-value'));
  1794. }
  1795. if (e) {
  1796. e.preventDefault();
  1797. e.stopPropagation();
  1798. }
  1799. } else if ((self.isFocused || self.settings.mode === 'single') && self.items.length) {
  1800. if (direction < 0 && selection.start === 0 && selection.length === 0) {
  1801. values.push(self.items[self.caretPos - 1]);
  1802. } else if (direction > 0 && selection.start === self.$control_input.val().length) {
  1803. values.push(self.items[self.caretPos]);
  1804. }
  1805. }
  1806. // allow the callback to abort
  1807. if (!values.length || (typeof self.settings.onDelete === 'function' && self.settings.onDelete.apply(self, [values]) === false)) {
  1808. return false;
  1809. }
  1810. // perform removal
  1811. if (typeof caret !== 'undefined') {
  1812. self.setCaret(caret);
  1813. }
  1814. while (values.length) {
  1815. self.removeItem(values.pop());
  1816. }
  1817. self.showInput();
  1818. self.positionDropdown();
  1819. self.refreshOptions(true);
  1820. // select previous option
  1821. if (option_select) {
  1822. $option_select = self.getOption(option_select);
  1823. if ($option_select.length) {
  1824. self.setActiveOption($option_select);
  1825. }
  1826. }
  1827. return true;
  1828. },
  1829. /**
  1830. * Selects the previous / next item (depending
  1831. * on the `direction` argument).
  1832. *
  1833. * > 0 - right
  1834. * < 0 - left
  1835. *
  1836. * @param {int} direction
  1837. * @param {object} e (optional)
  1838. */
  1839. advanceSelection: function(direction, e) {
  1840. var tail, selection, idx, valueLength, cursorAtEdge, $tail;
  1841. var self = this;
  1842. if (direction === 0) return;
  1843. if (self.rtl) direction *= -1;
  1844. tail = direction > 0 ? 'last' : 'first';
  1845. selection = getSelection(self.$control_input[0]);
  1846. if (self.isFocused && !self.isInputHidden) {
  1847. valueLength = self.$control_input.val().length;
  1848. cursorAtEdge = direction < 0
  1849. ? selection.start === 0 && selection.length === 0
  1850. : selection.start === valueLength;
  1851. if (cursorAtEdge && !valueLength) {
  1852. self.advanceCaret(direction, e);
  1853. }
  1854. } else {
  1855. $tail = self.$control.children('.active:' + tail);
  1856. if ($tail.length) {
  1857. idx = self.$control.children(':not(input)').index($tail);
  1858. self.setActiveItem(null);
  1859. self.setCaret(direction > 0 ? idx + 1 : idx);
  1860. }
  1861. }
  1862. },
  1863. /**
  1864. * Moves the caret left / right.
  1865. *
  1866. * @param {int} direction
  1867. * @param {object} e (optional)
  1868. */
  1869. advanceCaret: function(direction, e) {
  1870. var self = this, fn, $adj;
  1871. if (direction === 0) return;
  1872. fn = direction > 0 ? 'next' : 'prev';
  1873. if (self.isShiftDown) {
  1874. $adj = self.$control_input[fn]();
  1875. if ($adj.length) {
  1876. self.hideInput();
  1877. self.setActiveItem($adj);
  1878. e && e.preventDefault();
  1879. }
  1880. } else {
  1881. self.setCaret(self.caretPos + direction);
  1882. }
  1883. },
  1884. /**
  1885. * Moves the caret to the specified index.
  1886. *
  1887. * @param {int} i
  1888. */
  1889. setCaret: function(i) {
  1890. var self = this;
  1891. if (self.settings.mode === 'single') {
  1892. i = self.items.length;
  1893. } else {
  1894. i = Math.max(0, Math.min(self.items.length, i));
  1895. }
  1896. // the input must be moved by leaving it in place and moving the
  1897. // siblings, due to the fact that focus cannot be restored once lost
  1898. // on mobile webkit devices
  1899. var j, n, fn, $children, $child;
  1900. $children = self.$control.children(':not(input)');
  1901. for (j = 0, n = $children.length; j < n; j++) {
  1902. $child = $($children[j]).detach();
  1903. if (j < i) {
  1904. self.$control_input.before($child);
  1905. } else {
  1906. self.$control.append($child);
  1907. }
  1908. }
  1909. self.caretPos = i;
  1910. },
  1911. /**
  1912. * Disables user input on the control. Used while
  1913. * items are being asynchronously created.
  1914. */
  1915. lock: function() {
  1916. this.close();
  1917. this.isLocked = true;
  1918. this.refreshState();
  1919. },
  1920. /**
  1921. * Re-enables user input on the control.
  1922. */
  1923. unlock: function() {
  1924. this.isLocked = false;
  1925. this.refreshState();
  1926. },
  1927. /**
  1928. * Disables user input on the control completely.
  1929. * While disabled, it cannot receive focus.
  1930. */
  1931. disable: function() {
  1932. var self = this;
  1933. self.$input.prop('disabled', true);
  1934. self.isDisabled = true;
  1935. self.lock();
  1936. },
  1937. /**
  1938. * Enables the control so that it can respond
  1939. * to focus and user input.
  1940. */
  1941. enable: function() {
  1942. var self = this;
  1943. self.$input.prop('disabled', false);
  1944. self.isDisabled = false;
  1945. self.unlock();
  1946. },
  1947. /**
  1948. * Completely destroys the control and
  1949. * unbinds all event listeners so that it can
  1950. * be garbage collected.
  1951. */
  1952. destroy: function() {
  1953. var self = this;
  1954. var eventNS = self.eventNS;
  1955. var revertSettings = self.revertSettings;
  1956. self.trigger('destroy');
  1957. self.off();
  1958. self.$wrapper.remove();
  1959. self.$dropdown.remove();
  1960. self.$input
  1961. .html('')
  1962. .append(revertSettings.$children)
  1963. .removeAttr('tabindex')
  1964. .attr({tabindex: revertSettings.tabindex})
  1965. .show();
  1966. $(window).off(eventNS);
  1967. $(document).off(eventNS);
  1968. $(document.body).off(eventNS);
  1969. delete self.$input[0].selectize;
  1970. },
  1971. /**
  1972. * A helper method for rendering "item" and
  1973. * "option" templates, given the data.
  1974. *
  1975. * @param {string} templateName
  1976. * @param {object} data
  1977. * @returns {string}
  1978. */
  1979. render: function(templateName, data) {
  1980. var value, id, label;
  1981. var html = '';
  1982. var cache = false;
  1983. var self = this;
  1984. var regex_tag = /^[\t ]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i;
  1985. if (templateName === 'option' || templateName === 'item') {
  1986. value = hash_key(data[self.settings.valueField]);
  1987. cache = !!value;
  1988. }
  1989. // pull markup from cache if it exists
  1990. if (cache) {
  1991. if (!isset(self.renderCache[templateName])) {
  1992. self.renderCache[templateName] = {};
  1993. }
  1994. if (self.renderCache[templateName].hasOwnProperty(value)) {
  1995. return self.renderCache[templateName][value];
  1996. }
  1997. }
  1998. // render markup
  1999. html = self.settings.render[templateName].apply(this, [data, escape_html]);
  2000. // add mandatory attributes
  2001. if (templateName === 'option' || templateName === 'option_create') {
  2002. html = html.replace(regex_tag, '<$1 data-selectable');
  2003. }
  2004. if (templateName === 'optgroup') {
  2005. id = data[self.settings.optgroupValueField] || '';
  2006. html = html.replace(regex_tag, '<$1 data-group="' + escape_replace(escape_html(id)) + '"');
  2007. }
  2008. if (templateName === 'option' || templateName === 'item') {
  2009. html = html.replace(regex_tag, '<$1 data-value="' + escape_replace(escape_html(value || '')) + '"');
  2010. }
  2011. // update cache
  2012. if (cache) {
  2013. self.renderCache[templateName][value] = html;
  2014. }
  2015. return html;
  2016. }
  2017. });
  2018. Selectize.count = 0;
  2019. Selectize.defaults = {
  2020. plugins: [],
  2021. delimiter: ',',
  2022. persist: true,
  2023. diacritics: true,
  2024. create: false,
  2025. createOnBlur: false,
  2026. highlight: true,
  2027. openOnFocus: true,
  2028. maxOptions: 1000,
  2029. maxItems: null,
  2030. hideSelected: null,
  2031. addPrecedence: false,
  2032. preload: false,
  2033. scrollDuration: 60,
  2034. loadThrottle: 300,
  2035. dataAttr: 'data-data',
  2036. optgroupField: 'optgroup',
  2037. valueField: 'value',
  2038. labelField: 'text',
  2039. optgroupLabelField: 'label',
  2040. optgroupValueField: 'value',
  2041. optgroupOrder: null,
  2042. sortField: '$order',
  2043. searchField: ['text'],
  2044. searchConjunction: 'and',
  2045. mode: null,
  2046. wrapperClass: 'selectize-control',
  2047. inputClass: 'selectize-input',
  2048. dropdownClass: 'selectize-dropdown',
  2049. dropdownContentClass: 'selectize-dropdown-content',
  2050. dropdownParent: null,
  2051. /*
  2052. load : null, // function(query, callback) { ... }
  2053. score : null, // function(search) { ... }
  2054. onInitialize : null, // function() { ... }
  2055. onChange : null, // function(value) { ... }
  2056. onItemAdd : null, // function(value, $item) { ... }
  2057. onItemRemove : null, // function(value) { ... }
  2058. onClear : null, // function() { ... }
  2059. onOptionAdd : null, // function(value, data) { ... }
  2060. onOptionRemove : null, // function(value) { ... }
  2061. onOptionClear : null, // function() { ... }
  2062. onDropdownOpen : null, // function($dropdown) { ... }
  2063. onDropdownClose : null, // function($dropdown) { ... }
  2064. onType : null, // function(str) { ... }
  2065. onDelete : null, // function(values) { ... }
  2066. */
  2067. render: {
  2068. /*
  2069. item: null,
  2070. optgroup: null,
  2071. optgroup_header: null,
  2072. option: null,
  2073. option_create: null
  2074. */
  2075. }
  2076. };
  2077. $.fn.selectize = function(settings_user) {
  2078. var defaults = $.fn.selectize.defaults;
  2079. var settings = $.extend({}, defaults, settings_user);
  2080. var attr_data = settings.dataAttr;
  2081. var field_label = settings.labelField;
  2082. var field_value = settings.valueField;
  2083. var field_optgroup = settings.optgroupField;
  2084. var field_optgroup_label = settings.optgroupLabelField;
  2085. var field_optgroup_value = settings.optgroupValueField;
  2086. /**
  2087. * Initializes selectize from a <input type="text"> element.
  2088. *
  2089. * @param {object} $input
  2090. * @param {object} settings_element
  2091. */
  2092. var init_textbox = function($input, settings_element) {
  2093. var i, n, values, option, value = $.trim($input.val() || '');
  2094. if (!value.length) return;
  2095. values = value.split(settings.delimiter);
  2096. for (i = 0, n = values.length; i < n; i++) {
  2097. option = {};
  2098. option[field_label] = values[i];
  2099. option[field_value] = values[i];
  2100. settings_element.options[values[i]] = option;
  2101. }
  2102. settings_element.items = values;
  2103. };
  2104. /**
  2105. * Initializes selectize from a <select> element.
  2106. *
  2107. * @param {object} $input
  2108. * @param {object} settings_element
  2109. */
  2110. var init_select = function($input, settings_element) {
  2111. var i, n, tagName, $children, order = 0;
  2112. var options = settings_element.options;
  2113. var readData = function($el) {
  2114. var data = attr_data && $el.attr(attr_data);
  2115. if (typeof data === 'string' && data.length) {
  2116. return JSON.parse(data);
  2117. }
  2118. return null;
  2119. };
  2120. var addOption = function($option, group) {
  2121. var value, option;
  2122. $option = $($option);
  2123. value = $option.attr('value') || '';
  2124. if (!value.length) return;
  2125. // if the option already exists, it's probably been
  2126. // duplicated in another optgroup. in this case, push
  2127. // the current group to the "optgroup" property on the
  2128. // existing option so that it's rendered in both places.
  2129. if (options.hasOwnProperty(value)) {
  2130. if (group) {
  2131. if (!options[value].optgroup) {
  2132. options[value].optgroup = group;
  2133. } else if (!$.isArray(options[value].optgroup)) {
  2134. options[value].optgroup = [options[value].optgroup, group];
  2135. } else {
  2136. options[value].optgroup.push(group);
  2137. }
  2138. }
  2139. return;
  2140. }
  2141. option = readData($option) || {};
  2142. option[field_label] = option[field_label] || $option.text();
  2143. option[field_value] = option[field_value] || value;
  2144. option[field_optgroup] = option[field_optgroup] || group;
  2145. option.$order = ++order;
  2146. options[value] = option;
  2147. if ($option.is(':selected')) {
  2148. settings_element.items.push(value);
  2149. }
  2150. };
  2151. var addGroup = function($optgroup) {
  2152. var i, n, id, optgroup, $options;
  2153. $optgroup = $($optgroup);
  2154. id = $optgroup.attr('label');
  2155. if (id) {
  2156. optgroup = readData($optgroup) || {};
  2157. optgroup[field_optgroup_label] = id;
  2158. optgroup[field_optgroup_value] = id;
  2159. settings_element.optgroups[id] = optgroup;
  2160. }
  2161. $options = $('option', $optgroup);
  2162. for (i = 0, n = $options.length; i < n; i++) {
  2163. addOption($options[i], id);
  2164. }
  2165. };
  2166. settings_element.maxItems = $input.attr('multiple') ? null : 1;
  2167. $children = $input.children();
  2168. for (i = 0, n = $children.length; i < n; i++) {
  2169. tagName = $children[i].tagName.toLowerCase();
  2170. if (tagName === 'optgroup') {
  2171. addGroup($children[i]);
  2172. } else if (tagName === 'option') {
  2173. addOption($children[i]);
  2174. }
  2175. }
  2176. };
  2177. return this.each(function() {
  2178. if (this.selectize) return;
  2179. var instance;
  2180. var $input = $(this);
  2181. var tag_name = this.tagName.toLowerCase();
  2182. var settings_element = {
  2183. 'placeholder' : $input.children('option[value=""]').text() || $input.attr('placeholder'),
  2184. 'options' : {},
  2185. 'optgroups' : {},
  2186. 'items' : []
  2187. };
  2188. if (tag_name === 'select') {
  2189. init_select($input, settings_element);
  2190. } else {
  2191. init_textbox($input, settings_element);
  2192. }
  2193. instance = new Selectize($input, $.extend(true, {}, defaults, settings_element, settings_user));
  2194. $input.data('selectize', instance);
  2195. $input.addClass('selectized');
  2196. });
  2197. };
  2198. $.fn.selectize.defaults = Selectize.defaults;
  2199. Selectize.define('drag_drop', function(options) {
  2200. if (!$.fn.sortable) throw new Error('The "drag_drop" plugin requires jQuery UI "sortable".');
  2201. if (this.settings.mode !== 'multi') return;
  2202. var self = this;
  2203. self.lock = (function() {
  2204. var original = self.lock;
  2205. return function() {
  2206. var sortable = self.$control.data('sortable');
  2207. if (sortable) sortable.disable();
  2208. return original.apply(self, arguments);
  2209. };
  2210. })();
  2211. self.unlock = (function() {
  2212. var original = self.unlock;
  2213. return function() {
  2214. var sortable = self.$control.data('sortable');
  2215. if (sortable) sortable.enable();
  2216. return original.apply(self, arguments);
  2217. };
  2218. })();
  2219. self.setup = (function() {
  2220. var original = self.setup;
  2221. return function() {
  2222. original.apply(this, arguments);
  2223. var $control = self.$control.sortable({
  2224. items: '[data-value]',
  2225. forcePlaceholderSize: true,
  2226. disabled: self.isLocked,
  2227. start: function(e, ui) {
  2228. ui.placeholder.css('width', ui.helper.css('width'));
  2229. $control.css({overflow: 'visible'});
  2230. },
  2231. stop: function() {
  2232. $control.css({overflow: 'hidden'});
  2233. var active = self.$activeItems ? self.$activeItems.slice() : null;
  2234. var values = [];
  2235. $control.children('[data-value]').each(function() {
  2236. values.push($(this).attr('data-value'));
  2237. });
  2238. self.setValue(values);
  2239. self.setActiveItem(active);
  2240. }
  2241. });
  2242. };
  2243. })();
  2244. });
  2245. Selectize.define('dropdown_header', function(options) {
  2246. var self = this;
  2247. options = $.extend({
  2248. title : 'Untitled',
  2249. headerClass : 'selectize-dropdown-header',
  2250. titleRowClass : 'selectize-dropdown-header-title',
  2251. labelClass : 'selectize-dropdown-header-label',
  2252. closeClass : 'selectize-dropdown-header-close',
  2253. html: function(data) {
  2254. return (
  2255. '<div class="' + data.headerClass + '">' +
  2256. '<div class="' + data.titleRowClass + '">' +
  2257. '<span class="' + data.labelClass + '">' + data.title + '</span>' +
  2258. '<a href="javascript:void(0)" class="' + data.closeClass + '">&times;</a>' +
  2259. '</div>' +
  2260. '</div>'
  2261. );
  2262. }
  2263. }, options);
  2264. self.setup = (function() {
  2265. var original = self.setup;
  2266. return function() {
  2267. original.apply(self, arguments);
  2268. self.$dropdown_header = $(options.html(options));
  2269. self.$dropdown.prepend(self.$dropdown_header);
  2270. };
  2271. })();
  2272. });
  2273. Selectize.define('optgroup_columns', function(options) {
  2274. var self = this;
  2275. options = $.extend({
  2276. equalizeWidth : true,
  2277. equalizeHeight : true
  2278. }, options);
  2279. this.getAdjacentOption = function($option, direction) {
  2280. var $options = $option.closest('[data-group]').find('[data-selectable]');
  2281. var index = $options.index($option) + direction;
  2282. return index >= 0 && index < $options.length ? $options.eq(index) : $();
  2283. };
  2284. this.onKeyDown = (function() {
  2285. var original = self.onKeyDown;
  2286. return function(e) {
  2287. var index, $option, $options, $optgroup;
  2288. if (this.isOpen && (e.keyCode === KEY_LEFT || e.keyCode === KEY_RIGHT)) {
  2289. self.ignoreHover = true;
  2290. $optgroup = this.$activeOption.closest('[data-group]');
  2291. index = $optgroup.find('[data-selectable]').index(this.$activeOption);
  2292. if(e.keyCode === KEY_LEFT) {
  2293. $optgroup = $optgroup.prev('[data-group]');
  2294. } else {
  2295. $optgroup = $optgroup.next('[data-group]');
  2296. }
  2297. $options = $optgroup.find('[data-selectable]');
  2298. $option = $options.eq(Math.min($options.length - 1, index));
  2299. if ($option.length) {
  2300. this.setActiveOption($option);
  2301. }
  2302. return;
  2303. }
  2304. return original.apply(this, arguments);
  2305. };
  2306. })();
  2307. var equalizeSizes = function() {
  2308. var i, n, height_max, width, width_last, width_parent, $optgroups;
  2309. $optgroups = $('[data-group]', self.$dropdown_content);
  2310. n = $optgroups.length;
  2311. if (!n || !self.$dropdown_content.width()) return;
  2312. if (options.equalizeHeight) {
  2313. height_max = 0;
  2314. for (i = 0; i < n; i++) {
  2315. height_max = Math.max(height_max, $optgroups.eq(i).height());
  2316. }
  2317. $optgroups.css({height: height_max});
  2318. }
  2319. if (options.equalizeWidth) {
  2320. width_parent = self.$dropdown_content.innerWidth();
  2321. width = Math.round(width_parent / n);
  2322. $optgroups.css({width: width});
  2323. if (n > 1) {
  2324. width_last = width_parent - width * (n - 1);
  2325. $optgroups.eq(n - 1).css({width: width_last});
  2326. }
  2327. }
  2328. };
  2329. if (options.equalizeHeight || options.equalizeWidth) {
  2330. hook.after(this, 'positionDropdown', equalizeSizes);
  2331. hook.after(this, 'refreshOptions', equalizeSizes);
  2332. }
  2333. });
  2334. Selectize.define('remove_button', function(options) {
  2335. if (this.settings.mode === 'single') return;
  2336. options = $.extend({
  2337. label : '&times;',
  2338. title : 'Remove',
  2339. className : 'remove',
  2340. append : true
  2341. }, options);
  2342. var self = this;
  2343. var html = '<a href="javascript:void(0)" class="' + options.className + '" tabindex="-1" title="' + escape_html(options.title) + '">' + options.label + '</a>';
  2344. /**
  2345. * Appends an element as a child (with raw HTML).
  2346. *
  2347. * @param {string} html_container
  2348. * @param {string} html_element
  2349. * @return {string}
  2350. */
  2351. var append = function(html_container, html_element) {
  2352. var pos = html_container.search(/(<\/[^>]+>\s*)$/);
  2353. return html_container.substring(0, pos) + html_element + html_container.substring(pos);
  2354. };
  2355. this.setup = (function() {
  2356. var original = self.setup;
  2357. return function() {
  2358. // override the item rendering method to add the button to each
  2359. if (options.append) {
  2360. var render_item = self.settings.render.item;
  2361. self.settings.render.item = function(data) {
  2362. return append(render_item.apply(this, arguments), html);
  2363. };
  2364. }
  2365. original.apply(this, arguments);
  2366. // add event listener
  2367. this.$control.on('click', '.' + options.className, function(e) {
  2368. e.preventDefault();
  2369. if (self.isLocked) return;
  2370. var $item = $(e.target).parent();
  2371. self.setActiveItem($item);
  2372. if (self.deleteSelection()) {
  2373. self.setCaret(self.items.length);
  2374. }
  2375. });
  2376. };
  2377. })();
  2378. });
  2379. Selectize.define('restore_on_backspace', function(options) {
  2380. var self = this;
  2381. options.text = options.text || function(option) {
  2382. return option[this.settings.labelField];
  2383. };
  2384. this.onKeyDown = (function(e) {
  2385. var original = self.onKeyDown;
  2386. return function(e) {
  2387. var index, option;
  2388. if (e.keyCode === KEY_BACKSPACE && this.$control_input.val() === '' && !this.$activeItems.length) {
  2389. index = this.caretPos - 1;
  2390. if (index >= 0 && index < this.items.length) {
  2391. option = this.options[this.items[index]];
  2392. if (this.deleteSelection(e)) {
  2393. this.setTextboxValue(options.text.apply(this, [option]));
  2394. this.refreshOptions(true);
  2395. }
  2396. e.preventDefault();
  2397. return;
  2398. }
  2399. }
  2400. return original.apply(this, arguments);
  2401. };
  2402. })();
  2403. });
  2404. return Selectize;
  2405. }));