PageRenderTime 68ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/files/typehead.js/0.9.3/typeahead.js

https://gitlab.com/Mirros/jsdelivr
JavaScript | 1139 lines | 1132 code | 2 blank | 5 comment | 158 complexity | ffd57fd6ad699a48787c37fe215cac01 MD5 | raw file
  1. /*!
  2. * typeahead.js 0.9.3
  3. * https://github.com/twitter/typeahead
  4. * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT
  5. */
  6. (function($) {
  7. var VERSION = "0.9.3";
  8. var utils = {
  9. isMsie: function() {
  10. var match = /(msie) ([\w.]+)/i.exec(navigator.userAgent);
  11. return match ? parseInt(match[2], 10) : false;
  12. },
  13. isBlankString: function(str) {
  14. return !str || /^\s*$/.test(str);
  15. },
  16. escapeRegExChars: function(str) {
  17. return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
  18. },
  19. isString: function(obj) {
  20. return typeof obj === "string";
  21. },
  22. isNumber: function(obj) {
  23. return typeof obj === "number";
  24. },
  25. isArray: $.isArray,
  26. isFunction: $.isFunction,
  27. isObject: $.isPlainObject,
  28. isUndefined: function(obj) {
  29. return typeof obj === "undefined";
  30. },
  31. bind: $.proxy,
  32. bindAll: function(obj) {
  33. var val;
  34. for (var key in obj) {
  35. $.isFunction(val = obj[key]) && (obj[key] = $.proxy(val, obj));
  36. }
  37. },
  38. indexOf: function(haystack, needle) {
  39. for (var i = 0; i < haystack.length; i++) {
  40. if (haystack[i] === needle) {
  41. return i;
  42. }
  43. }
  44. return -1;
  45. },
  46. each: $.each,
  47. map: $.map,
  48. filter: $.grep,
  49. every: function(obj, test) {
  50. var result = true;
  51. if (!obj) {
  52. return result;
  53. }
  54. $.each(obj, function(key, val) {
  55. if (!(result = test.call(null, val, key, obj))) {
  56. return false;
  57. }
  58. });
  59. return !!result;
  60. },
  61. some: function(obj, test) {
  62. var result = false;
  63. if (!obj) {
  64. return result;
  65. }
  66. $.each(obj, function(key, val) {
  67. if (result = test.call(null, val, key, obj)) {
  68. return false;
  69. }
  70. });
  71. return !!result;
  72. },
  73. mixin: $.extend,
  74. getUniqueId: function() {
  75. var counter = 0;
  76. return function() {
  77. return counter++;
  78. };
  79. }(),
  80. defer: function(fn) {
  81. setTimeout(fn, 0);
  82. },
  83. debounce: function(func, wait, immediate) {
  84. var timeout, result;
  85. return function() {
  86. var context = this, args = arguments, later, callNow;
  87. later = function() {
  88. timeout = null;
  89. if (!immediate) {
  90. result = func.apply(context, args);
  91. }
  92. };
  93. callNow = immediate && !timeout;
  94. clearTimeout(timeout);
  95. timeout = setTimeout(later, wait);
  96. if (callNow) {
  97. result = func.apply(context, args);
  98. }
  99. return result;
  100. };
  101. },
  102. throttle: function(func, wait) {
  103. var context, args, timeout, result, previous, later;
  104. previous = 0;
  105. later = function() {
  106. previous = new Date();
  107. timeout = null;
  108. result = func.apply(context, args);
  109. };
  110. return function() {
  111. var now = new Date(), remaining = wait - (now - previous);
  112. context = this;
  113. args = arguments;
  114. if (remaining <= 0) {
  115. clearTimeout(timeout);
  116. timeout = null;
  117. previous = now;
  118. result = func.apply(context, args);
  119. } else if (!timeout) {
  120. timeout = setTimeout(later, remaining);
  121. }
  122. return result;
  123. };
  124. },
  125. tokenizeQuery: function(str) {
  126. return $.trim(str).toLowerCase().split(/[\s]+/);
  127. },
  128. tokenizeText: function(str) {
  129. return $.trim(str).toLowerCase().split(/[\s\-_]+/);
  130. },
  131. getProtocol: function() {
  132. return location.protocol;
  133. },
  134. noop: function() {}
  135. };
  136. var EventTarget = function() {
  137. var eventSplitter = /\s+/;
  138. return {
  139. on: function(events, callback) {
  140. var event;
  141. if (!callback) {
  142. return this;
  143. }
  144. this._callbacks = this._callbacks || {};
  145. events = events.split(eventSplitter);
  146. while (event = events.shift()) {
  147. this._callbacks[event] = this._callbacks[event] || [];
  148. this._callbacks[event].push(callback);
  149. }
  150. return this;
  151. },
  152. trigger: function(events, data) {
  153. var event, callbacks;
  154. if (!this._callbacks) {
  155. return this;
  156. }
  157. events = events.split(eventSplitter);
  158. while (event = events.shift()) {
  159. if (callbacks = this._callbacks[event]) {
  160. for (var i = 0; i < callbacks.length; i += 1) {
  161. callbacks[i].call(this, {
  162. type: event,
  163. data: data
  164. });
  165. }
  166. }
  167. }
  168. return this;
  169. }
  170. };
  171. }();
  172. var EventBus = function() {
  173. var namespace = "typeahead:";
  174. function EventBus(o) {
  175. if (!o || !o.el) {
  176. $.error("EventBus initialized without el");
  177. }
  178. this.$el = $(o.el);
  179. }
  180. utils.mixin(EventBus.prototype, {
  181. trigger: function(type) {
  182. var args = [].slice.call(arguments, 1);
  183. this.$el.trigger(namespace + type, args);
  184. }
  185. });
  186. return EventBus;
  187. }();
  188. var PersistentStorage = function() {
  189. var ls, methods;
  190. try {
  191. ls = window.localStorage;
  192. ls.setItem("~~~", "!");
  193. ls.removeItem("~~~");
  194. } catch (err) {
  195. ls = null;
  196. }
  197. function PersistentStorage(namespace) {
  198. this.prefix = [ "__", namespace, "__" ].join("");
  199. this.ttlKey = "__ttl__";
  200. this.keyMatcher = new RegExp("^" + this.prefix);
  201. }
  202. if (ls && window.JSON) {
  203. methods = {
  204. _prefix: function(key) {
  205. return this.prefix + key;
  206. },
  207. _ttlKey: function(key) {
  208. return this._prefix(key) + this.ttlKey;
  209. },
  210. get: function(key) {
  211. if (this.isExpired(key)) {
  212. this.remove(key);
  213. }
  214. return decode(ls.getItem(this._prefix(key)));
  215. },
  216. set: function(key, val, ttl) {
  217. if (utils.isNumber(ttl)) {
  218. ls.setItem(this._ttlKey(key), encode(now() + ttl));
  219. } else {
  220. ls.removeItem(this._ttlKey(key));
  221. }
  222. return ls.setItem(this._prefix(key), encode(val));
  223. },
  224. remove: function(key) {
  225. ls.removeItem(this._ttlKey(key));
  226. ls.removeItem(this._prefix(key));
  227. return this;
  228. },
  229. clear: function() {
  230. var i, key, keys = [], len = ls.length;
  231. for (i = 0; i < len; i++) {
  232. if ((key = ls.key(i)).match(this.keyMatcher)) {
  233. keys.push(key.replace(this.keyMatcher, ""));
  234. }
  235. }
  236. for (i = keys.length; i--; ) {
  237. this.remove(keys[i]);
  238. }
  239. return this;
  240. },
  241. isExpired: function(key) {
  242. var ttl = decode(ls.getItem(this._ttlKey(key)));
  243. return utils.isNumber(ttl) && now() > ttl ? true : false;
  244. }
  245. };
  246. } else {
  247. methods = {
  248. get: utils.noop,
  249. set: utils.noop,
  250. remove: utils.noop,
  251. clear: utils.noop,
  252. isExpired: utils.noop
  253. };
  254. }
  255. utils.mixin(PersistentStorage.prototype, methods);
  256. return PersistentStorage;
  257. function now() {
  258. return new Date().getTime();
  259. }
  260. function encode(val) {
  261. return JSON.stringify(utils.isUndefined(val) ? null : val);
  262. }
  263. function decode(val) {
  264. return JSON.parse(val);
  265. }
  266. }();
  267. var RequestCache = function() {
  268. function RequestCache(o) {
  269. utils.bindAll(this);
  270. o = o || {};
  271. this.sizeLimit = o.sizeLimit || 10;
  272. this.cache = {};
  273. this.cachedKeysByAge = [];
  274. }
  275. utils.mixin(RequestCache.prototype, {
  276. get: function(url) {
  277. return this.cache[url];
  278. },
  279. set: function(url, resp) {
  280. var requestToEvict;
  281. if (this.cachedKeysByAge.length === this.sizeLimit) {
  282. requestToEvict = this.cachedKeysByAge.shift();
  283. delete this.cache[requestToEvict];
  284. }
  285. this.cache[url] = resp;
  286. this.cachedKeysByAge.push(url);
  287. }
  288. });
  289. return RequestCache;
  290. }();
  291. var Transport = function() {
  292. var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests, requestCache;
  293. function Transport(o) {
  294. utils.bindAll(this);
  295. o = utils.isString(o) ? {
  296. url: o
  297. } : o;
  298. requestCache = requestCache || new RequestCache();
  299. maxPendingRequests = utils.isNumber(o.maxParallelRequests) ? o.maxParallelRequests : maxPendingRequests || 6;
  300. this.url = o.url;
  301. this.wildcard = o.wildcard || "%QUERY";
  302. this.filter = o.filter;
  303. this.replace = o.replace;
  304. this.ajaxSettings = {
  305. type: "get",
  306. cache: o.cache,
  307. timeout: o.timeout,
  308. dataType: o.dataType || "json",
  309. beforeSend: o.beforeSend
  310. };
  311. this._get = (/^throttle$/i.test(o.rateLimitFn) ? utils.throttle : utils.debounce)(this._get, o.rateLimitWait || 300);
  312. }
  313. utils.mixin(Transport.prototype, {
  314. _get: function(url, cb) {
  315. var that = this;
  316. if (belowPendingRequestsThreshold()) {
  317. this._sendRequest(url).done(done);
  318. } else {
  319. this.onDeckRequestArgs = [].slice.call(arguments, 0);
  320. }
  321. function done(resp) {
  322. var data = that.filter ? that.filter(resp) : resp;
  323. cb && cb(data);
  324. requestCache.set(url, resp);
  325. }
  326. },
  327. _sendRequest: function(url) {
  328. var that = this, jqXhr = pendingRequests[url];
  329. if (!jqXhr) {
  330. incrementPendingRequests();
  331. jqXhr = pendingRequests[url] = $.ajax(url, this.ajaxSettings).always(always);
  332. }
  333. return jqXhr;
  334. function always() {
  335. decrementPendingRequests();
  336. pendingRequests[url] = null;
  337. if (that.onDeckRequestArgs) {
  338. that._get.apply(that, that.onDeckRequestArgs);
  339. that.onDeckRequestArgs = null;
  340. }
  341. }
  342. },
  343. get: function(query, cb) {
  344. var that = this, encodedQuery = encodeURIComponent(query || ""), url, resp;
  345. cb = cb || utils.noop;
  346. url = this.replace ? this.replace(this.url, encodedQuery) : this.url.replace(this.wildcard, encodedQuery);
  347. if (resp = requestCache.get(url)) {
  348. utils.defer(function() {
  349. cb(that.filter ? that.filter(resp) : resp);
  350. });
  351. } else {
  352. this._get(url, cb);
  353. }
  354. return !!resp;
  355. }
  356. });
  357. return Transport;
  358. function incrementPendingRequests() {
  359. pendingRequestsCount++;
  360. }
  361. function decrementPendingRequests() {
  362. pendingRequestsCount--;
  363. }
  364. function belowPendingRequestsThreshold() {
  365. return pendingRequestsCount < maxPendingRequests;
  366. }
  367. }();
  368. var Dataset = function() {
  369. var keys = {
  370. thumbprint: "thumbprint",
  371. protocol: "protocol",
  372. itemHash: "itemHash",
  373. adjacencyList: "adjacencyList"
  374. };
  375. function Dataset(o) {
  376. utils.bindAll(this);
  377. if (utils.isString(o.template) && !o.engine) {
  378. $.error("no template engine specified");
  379. }
  380. if (!o.local && !o.prefetch && !o.remote) {
  381. $.error("one of local, prefetch, or remote is required");
  382. }
  383. this.name = o.name || utils.getUniqueId();
  384. this.limit = o.limit || 5;
  385. this.minLength = o.minLength || 1;
  386. this.header = o.header;
  387. this.footer = o.footer;
  388. this.valueKey = o.valueKey || "value";
  389. this.template = compileTemplate(o.template, o.engine, this.valueKey);
  390. this.local = o.local;
  391. this.prefetch = o.prefetch;
  392. this.remote = o.remote;
  393. this.itemHash = {};
  394. this.adjacencyList = {};
  395. this.storage = o.name ? new PersistentStorage(o.name) : null;
  396. }
  397. utils.mixin(Dataset.prototype, {
  398. _processLocalData: function(data) {
  399. this._mergeProcessedData(this._processData(data));
  400. },
  401. _loadPrefetchData: function(o) {
  402. var that = this, thumbprint = VERSION + (o.thumbprint || ""), storedThumbprint, storedProtocol, storedItemHash, storedAdjacencyList, isExpired, deferred;
  403. if (this.storage) {
  404. storedThumbprint = this.storage.get(keys.thumbprint);
  405. storedProtocol = this.storage.get(keys.protocol);
  406. storedItemHash = this.storage.get(keys.itemHash);
  407. storedAdjacencyList = this.storage.get(keys.adjacencyList);
  408. }
  409. isExpired = storedThumbprint !== thumbprint || storedProtocol !== utils.getProtocol();
  410. o = utils.isString(o) ? {
  411. url: o
  412. } : o;
  413. o.ttl = utils.isNumber(o.ttl) ? o.ttl : 24 * 60 * 60 * 1e3;
  414. if (storedItemHash && storedAdjacencyList && !isExpired) {
  415. this._mergeProcessedData({
  416. itemHash: storedItemHash,
  417. adjacencyList: storedAdjacencyList
  418. });
  419. deferred = $.Deferred().resolve();
  420. } else {
  421. deferred = $.getJSON(o.url).done(processPrefetchData);
  422. }
  423. return deferred;
  424. function processPrefetchData(data) {
  425. var filteredData = o.filter ? o.filter(data) : data, processedData = that._processData(filteredData), itemHash = processedData.itemHash, adjacencyList = processedData.adjacencyList;
  426. if (that.storage) {
  427. that.storage.set(keys.itemHash, itemHash, o.ttl);
  428. that.storage.set(keys.adjacencyList, adjacencyList, o.ttl);
  429. that.storage.set(keys.thumbprint, thumbprint, o.ttl);
  430. that.storage.set(keys.protocol, utils.getProtocol(), o.ttl);
  431. }
  432. that._mergeProcessedData(processedData);
  433. }
  434. },
  435. _transformDatum: function(datum) {
  436. var value = utils.isString(datum) ? datum : datum[this.valueKey], tokens = datum.tokens || utils.tokenizeText(value), item = {
  437. value: value,
  438. tokens: tokens
  439. };
  440. if (utils.isString(datum)) {
  441. item.datum = {};
  442. item.datum[this.valueKey] = datum;
  443. } else {
  444. item.datum = datum;
  445. }
  446. item.tokens = utils.filter(item.tokens, function(token) {
  447. return !utils.isBlankString(token);
  448. });
  449. item.tokens = utils.map(item.tokens, function(token) {
  450. return token.toLowerCase();
  451. });
  452. return item;
  453. },
  454. _processData: function(data) {
  455. var that = this, itemHash = {}, adjacencyList = {};
  456. utils.each(data, function(i, datum) {
  457. var item = that._transformDatum(datum), id = utils.getUniqueId(item.value);
  458. itemHash[id] = item;
  459. utils.each(item.tokens, function(i, token) {
  460. var character = token.charAt(0), adjacency = adjacencyList[character] || (adjacencyList[character] = [ id ]);
  461. !~utils.indexOf(adjacency, id) && adjacency.push(id);
  462. });
  463. });
  464. return {
  465. itemHash: itemHash,
  466. adjacencyList: adjacencyList
  467. };
  468. },
  469. _mergeProcessedData: function(processedData) {
  470. var that = this;
  471. utils.mixin(this.itemHash, processedData.itemHash);
  472. utils.each(processedData.adjacencyList, function(character, adjacency) {
  473. var masterAdjacency = that.adjacencyList[character];
  474. that.adjacencyList[character] = masterAdjacency ? masterAdjacency.concat(adjacency) : adjacency;
  475. });
  476. },
  477. _getLocalSuggestions: function(terms) {
  478. var that = this, firstChars = [], lists = [], shortestList, suggestions = [];
  479. utils.each(terms, function(i, term) {
  480. var firstChar = term.charAt(0);
  481. !~utils.indexOf(firstChars, firstChar) && firstChars.push(firstChar);
  482. });
  483. utils.each(firstChars, function(i, firstChar) {
  484. var list = that.adjacencyList[firstChar];
  485. if (!list) {
  486. return false;
  487. }
  488. lists.push(list);
  489. if (!shortestList || list.length < shortestList.length) {
  490. shortestList = list;
  491. }
  492. });
  493. if (lists.length < firstChars.length) {
  494. return [];
  495. }
  496. utils.each(shortestList, function(i, id) {
  497. var item = that.itemHash[id], isCandidate, isMatch;
  498. isCandidate = utils.every(lists, function(list) {
  499. return ~utils.indexOf(list, id);
  500. });
  501. isMatch = isCandidate && utils.every(terms, function(term) {
  502. return utils.some(item.tokens, function(token) {
  503. return token.indexOf(term) === 0;
  504. });
  505. });
  506. isMatch && suggestions.push(item);
  507. });
  508. return suggestions;
  509. },
  510. initialize: function() {
  511. var deferred;
  512. this.local && this._processLocalData(this.local);
  513. this.transport = this.remote ? new Transport(this.remote) : null;
  514. deferred = this.prefetch ? this._loadPrefetchData(this.prefetch) : $.Deferred().resolve();
  515. this.local = this.prefetch = this.remote = null;
  516. this.initialize = function() {
  517. return deferred;
  518. };
  519. return deferred;
  520. },
  521. getSuggestions: function(query, cb) {
  522. var that = this, terms, suggestions, cacheHit = false;
  523. if (query.length < this.minLength) {
  524. return;
  525. }
  526. terms = utils.tokenizeQuery(query);
  527. suggestions = this._getLocalSuggestions(terms).slice(0, this.limit);
  528. if (suggestions.length < this.limit && this.transport) {
  529. cacheHit = this.transport.get(query, processRemoteData);
  530. }
  531. !cacheHit && cb && cb(suggestions);
  532. function processRemoteData(data) {
  533. suggestions = suggestions.slice(0);
  534. utils.each(data, function(i, datum) {
  535. var item = that._transformDatum(datum), isDuplicate;
  536. isDuplicate = utils.some(suggestions, function(suggestion) {
  537. return item.value === suggestion.value;
  538. });
  539. !isDuplicate && suggestions.push(item);
  540. return suggestions.length < that.limit;
  541. });
  542. cb && cb(suggestions);
  543. }
  544. }
  545. });
  546. return Dataset;
  547. function compileTemplate(template, engine, valueKey) {
  548. var renderFn, compiledTemplate;
  549. if (utils.isFunction(template)) {
  550. renderFn = template;
  551. } else if (utils.isString(template)) {
  552. compiledTemplate = engine.compile(template);
  553. renderFn = utils.bind(compiledTemplate.render, compiledTemplate);
  554. } else {
  555. renderFn = function(context) {
  556. return "<p>" + context[valueKey] + "</p>";
  557. };
  558. }
  559. return renderFn;
  560. }
  561. }();
  562. var InputView = function() {
  563. function InputView(o) {
  564. var that = this;
  565. utils.bindAll(this);
  566. this.specialKeyCodeMap = {
  567. 9: "tab",
  568. 27: "esc",
  569. 37: "left",
  570. 39: "right",
  571. 13: "enter",
  572. 38: "up",
  573. 40: "down"
  574. };
  575. this.$hint = $(o.hint);
  576. this.$input = $(o.input).on("blur.tt", this._handleBlur).on("focus.tt", this._handleFocus).on("keydown.tt", this._handleSpecialKeyEvent);
  577. if (!utils.isMsie()) {
  578. this.$input.on("input.tt", this._compareQueryToInputValue);
  579. } else {
  580. this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
  581. if (that.specialKeyCodeMap[$e.which || $e.keyCode]) {
  582. return;
  583. }
  584. utils.defer(that._compareQueryToInputValue);
  585. });
  586. }
  587. this.query = this.$input.val();
  588. this.$overflowHelper = buildOverflowHelper(this.$input);
  589. }
  590. utils.mixin(InputView.prototype, EventTarget, {
  591. _handleFocus: function() {
  592. this.trigger("focused");
  593. },
  594. _handleBlur: function() {
  595. this.trigger("blured");
  596. },
  597. _handleSpecialKeyEvent: function($e) {
  598. var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode];
  599. keyName && this.trigger(keyName + "Keyed", $e);
  600. },
  601. _compareQueryToInputValue: function() {
  602. var inputValue = this.getInputValue(), isSameQuery = compareQueries(this.query, inputValue), isSameQueryExceptWhitespace = isSameQuery ? this.query.length !== inputValue.length : false;
  603. if (isSameQueryExceptWhitespace) {
  604. this.trigger("whitespaceChanged", {
  605. value: this.query
  606. });
  607. } else if (!isSameQuery) {
  608. this.trigger("queryChanged", {
  609. value: this.query = inputValue
  610. });
  611. }
  612. },
  613. destroy: function() {
  614. this.$hint.off(".tt");
  615. this.$input.off(".tt");
  616. this.$hint = this.$input = this.$overflowHelper = null;
  617. },
  618. focus: function() {
  619. this.$input.focus();
  620. },
  621. blur: function() {
  622. this.$input.blur();
  623. },
  624. getQuery: function() {
  625. return this.query;
  626. },
  627. setQuery: function(query) {
  628. this.query = query;
  629. },
  630. getInputValue: function() {
  631. return this.$input.val();
  632. },
  633. setInputValue: function(value, silent) {
  634. this.$input.val(value);
  635. !silent && this._compareQueryToInputValue();
  636. },
  637. getHintValue: function() {
  638. return this.$hint.val();
  639. },
  640. setHintValue: function(value) {
  641. this.$hint.val(value);
  642. },
  643. getLanguageDirection: function() {
  644. return (this.$input.css("direction") || "ltr").toLowerCase();
  645. },
  646. isOverflow: function() {
  647. this.$overflowHelper.text(this.getInputValue());
  648. return this.$overflowHelper.width() > this.$input.width();
  649. },
  650. isCursorAtEnd: function() {
  651. var valueLength = this.$input.val().length, selectionStart = this.$input[0].selectionStart, range;
  652. if (utils.isNumber(selectionStart)) {
  653. return selectionStart === valueLength;
  654. } else if (document.selection) {
  655. range = document.selection.createRange();
  656. range.moveStart("character", -valueLength);
  657. return valueLength === range.text.length;
  658. }
  659. return true;
  660. }
  661. });
  662. return InputView;
  663. function buildOverflowHelper($input) {
  664. return $("<span></span>").css({
  665. position: "absolute",
  666. left: "-9999px",
  667. visibility: "hidden",
  668. whiteSpace: "nowrap",
  669. fontFamily: $input.css("font-family"),
  670. fontSize: $input.css("font-size"),
  671. fontStyle: $input.css("font-style"),
  672. fontVariant: $input.css("font-variant"),
  673. fontWeight: $input.css("font-weight"),
  674. wordSpacing: $input.css("word-spacing"),
  675. letterSpacing: $input.css("letter-spacing"),
  676. textIndent: $input.css("text-indent"),
  677. textRendering: $input.css("text-rendering"),
  678. textTransform: $input.css("text-transform")
  679. }).insertAfter($input);
  680. }
  681. function compareQueries(a, b) {
  682. a = (a || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
  683. b = (b || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
  684. return a === b;
  685. }
  686. }();
  687. var DropdownView = function() {
  688. var html = {
  689. suggestionsList: '<span class="tt-suggestions"></span>'
  690. }, css = {
  691. suggestionsList: {
  692. display: "block"
  693. },
  694. suggestion: {
  695. whiteSpace: "nowrap",
  696. cursor: "pointer"
  697. },
  698. suggestionChild: {
  699. whiteSpace: "normal"
  700. }
  701. };
  702. function DropdownView(o) {
  703. utils.bindAll(this);
  704. this.isOpen = false;
  705. this.isEmpty = true;
  706. this.isMouseOverDropdown = false;
  707. this.$menu = $(o.menu).on("mouseenter.tt", this._handleMouseenter).on("mouseleave.tt", this._handleMouseleave).on("click.tt", ".tt-suggestion", this._handleSelection).on("mouseover.tt", ".tt-suggestion", this._handleMouseover);
  708. }
  709. utils.mixin(DropdownView.prototype, EventTarget, {
  710. _handleMouseenter: function() {
  711. this.isMouseOverDropdown = true;
  712. },
  713. _handleMouseleave: function() {
  714. this.isMouseOverDropdown = false;
  715. },
  716. _handleMouseover: function($e) {
  717. var $suggestion = $($e.currentTarget);
  718. this._getSuggestions().removeClass("tt-is-under-cursor");
  719. $suggestion.addClass("tt-is-under-cursor");
  720. },
  721. _handleSelection: function($e) {
  722. var $suggestion = $($e.currentTarget);
  723. this.trigger("suggestionSelected", extractSuggestion($suggestion));
  724. },
  725. _show: function() {
  726. this.$menu.css("display", "block");
  727. },
  728. _hide: function() {
  729. this.$menu.hide();
  730. },
  731. _moveCursor: function(increment) {
  732. var $suggestions, $cur, nextIndex, $underCursor;
  733. if (!this.isVisible()) {
  734. return;
  735. }
  736. $suggestions = this._getSuggestions();
  737. $cur = $suggestions.filter(".tt-is-under-cursor");
  738. $cur.removeClass("tt-is-under-cursor");
  739. nextIndex = $suggestions.index($cur) + increment;
  740. nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1;
  741. if (nextIndex === -1) {
  742. this.trigger("cursorRemoved");
  743. return;
  744. } else if (nextIndex < -1) {
  745. nextIndex = $suggestions.length - 1;
  746. }
  747. $underCursor = $suggestions.eq(nextIndex).addClass("tt-is-under-cursor");
  748. this._ensureVisibility($underCursor);
  749. this.trigger("cursorMoved", extractSuggestion($underCursor));
  750. },
  751. _getSuggestions: function() {
  752. return this.$menu.find(".tt-suggestions > .tt-suggestion");
  753. },
  754. _ensureVisibility: function($el) {
  755. var menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10), menuScrollTop = this.$menu.scrollTop(), elTop = $el.position().top, elBottom = elTop + $el.outerHeight(true);
  756. if (elTop < 0) {
  757. this.$menu.scrollTop(menuScrollTop + elTop);
  758. } else if (menuHeight < elBottom) {
  759. this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
  760. }
  761. },
  762. destroy: function() {
  763. this.$menu.off(".tt");
  764. this.$menu = null;
  765. },
  766. isVisible: function() {
  767. return this.isOpen && !this.isEmpty;
  768. },
  769. closeUnlessMouseIsOverDropdown: function() {
  770. if (!this.isMouseOverDropdown) {
  771. this.close();
  772. }
  773. },
  774. close: function() {
  775. if (this.isOpen) {
  776. this.isOpen = false;
  777. this.isMouseOverDropdown = false;
  778. this._hide();
  779. this.$menu.find(".tt-suggestions > .tt-suggestion").removeClass("tt-is-under-cursor");
  780. this.trigger("closed");
  781. }
  782. },
  783. open: function() {
  784. if (!this.isOpen) {
  785. this.isOpen = true;
  786. !this.isEmpty && this._show();
  787. this.trigger("opened");
  788. }
  789. },
  790. setLanguageDirection: function(dir) {
  791. var ltrCss = {
  792. left: "0",
  793. right: "auto"
  794. }, rtlCss = {
  795. left: "auto",
  796. right: " 0"
  797. };
  798. dir === "ltr" ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss);
  799. },
  800. moveCursorUp: function() {
  801. this._moveCursor(-1);
  802. },
  803. moveCursorDown: function() {
  804. this._moveCursor(+1);
  805. },
  806. getSuggestionUnderCursor: function() {
  807. var $suggestion = this._getSuggestions().filter(".tt-is-under-cursor").first();
  808. return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
  809. },
  810. getFirstSuggestion: function() {
  811. var $suggestion = this._getSuggestions().first();
  812. return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
  813. },
  814. renderSuggestions: function(dataset, suggestions) {
  815. var datasetClassName = "tt-dataset-" + dataset.name, wrapper = '<div class="tt-suggestion">%body</div>', compiledHtml, $suggestionsList, $dataset = this.$menu.find("." + datasetClassName), elBuilder, fragment, $el;
  816. if ($dataset.length === 0) {
  817. $suggestionsList = $(html.suggestionsList).css(css.suggestionsList);
  818. $dataset = $("<div></div>").addClass(datasetClassName).append(dataset.header).append($suggestionsList).append(dataset.footer).appendTo(this.$menu);
  819. }
  820. if (suggestions.length > 0) {
  821. this.isEmpty = false;
  822. this.isOpen && this._show();
  823. elBuilder = document.createElement("div");
  824. fragment = document.createDocumentFragment();
  825. utils.each(suggestions, function(i, suggestion) {
  826. suggestion.dataset = dataset.name;
  827. compiledHtml = dataset.template(suggestion.datum);
  828. elBuilder.innerHTML = wrapper.replace("%body", compiledHtml);
  829. $el = $(elBuilder.firstChild).css(css.suggestion).data("suggestion", suggestion);
  830. $el.children().each(function() {
  831. $(this).css(css.suggestionChild);
  832. });
  833. fragment.appendChild($el[0]);
  834. });
  835. $dataset.show().find(".tt-suggestions").html(fragment);
  836. } else {
  837. this.clearSuggestions(dataset.name);
  838. }
  839. this.trigger("suggestionsRendered");
  840. },
  841. clearSuggestions: function(datasetName) {
  842. var $datasets = datasetName ? this.$menu.find(".tt-dataset-" + datasetName) : this.$menu.find('[class^="tt-dataset-"]'), $suggestions = $datasets.find(".tt-suggestions");
  843. $datasets.hide();
  844. $suggestions.empty();
  845. if (this._getSuggestions().length === 0) {
  846. this.isEmpty = true;
  847. this._hide();
  848. }
  849. }
  850. });
  851. return DropdownView;
  852. function extractSuggestion($el) {
  853. return $el.data("suggestion");
  854. }
  855. }();
  856. var TypeaheadView = function() {
  857. var html = {
  858. wrapper: '<span class="twitter-typeahead"></span>',
  859. hint: '<input class="tt-hint" type="text" autocomplete="off" spellcheck="off" disabled>',
  860. dropdown: '<span class="tt-dropdown-menu"></span>'
  861. }, css = {
  862. wrapper: {
  863. position: "relative",
  864. display: "inline-block"
  865. },
  866. hint: {
  867. position: "absolute",
  868. top: "0",
  869. left: "0",
  870. borderColor: "transparent",
  871. boxShadow: "none"
  872. },
  873. query: {
  874. position: "relative",
  875. verticalAlign: "top",
  876. backgroundColor: "transparent"
  877. },
  878. dropdown: {
  879. position: "absolute",
  880. top: "100%",
  881. left: "0",
  882. zIndex: "100",
  883. display: "none"
  884. }
  885. };
  886. if (utils.isMsie()) {
  887. utils.mixin(css.query, {
  888. backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
  889. });
  890. }
  891. if (utils.isMsie() && utils.isMsie() <= 7) {
  892. utils.mixin(css.wrapper, {
  893. display: "inline",
  894. zoom: "1"
  895. });
  896. utils.mixin(css.query, {
  897. marginTop: "-1px"
  898. });
  899. }
  900. function TypeaheadView(o) {
  901. var $menu, $input, $hint;
  902. utils.bindAll(this);
  903. this.$node = buildDomStructure(o.input);
  904. this.datasets = o.datasets;
  905. this.dir = null;
  906. this.eventBus = o.eventBus;
  907. $menu = this.$node.find(".tt-dropdown-menu");
  908. $input = this.$node.find(".tt-query");
  909. $hint = this.$node.find(".tt-hint");
  910. this.dropdownView = new DropdownView({
  911. menu: $menu
  912. }).on("suggestionSelected", this._handleSelection).on("cursorMoved", this._clearHint).on("cursorMoved", this._setInputValueToSuggestionUnderCursor).on("cursorRemoved", this._setInputValueToQuery).on("cursorRemoved", this._updateHint).on("suggestionsRendered", this._updateHint).on("opened", this._updateHint).on("closed", this._clearHint).on("opened closed", this._propagateEvent);
  913. this.inputView = new InputView({
  914. input: $input,
  915. hint: $hint
  916. }).on("focused", this._openDropdown).on("blured", this._closeDropdown).on("blured", this._setInputValueToQuery).on("enterKeyed tabKeyed", this._handleSelection).on("queryChanged", this._clearHint).on("queryChanged", this._clearSuggestions).on("queryChanged", this._getSuggestions).on("whitespaceChanged", this._updateHint).on("queryChanged whitespaceChanged", this._openDropdown).on("queryChanged whitespaceChanged", this._setLanguageDirection).on("escKeyed", this._closeDropdown).on("escKeyed", this._setInputValueToQuery).on("tabKeyed upKeyed downKeyed", this._managePreventDefault).on("upKeyed downKeyed", this._moveDropdownCursor).on("upKeyed downKeyed", this._openDropdown).on("tabKeyed leftKeyed rightKeyed", this._autocomplete);
  917. }
  918. utils.mixin(TypeaheadView.prototype, EventTarget, {
  919. _managePreventDefault: function(e) {
  920. var $e = e.data, hint, inputValue, preventDefault = false;
  921. switch (e.type) {
  922. case "tabKeyed":
  923. hint = this.inputView.getHintValue();
  924. inputValue = this.inputView.getInputValue();
  925. preventDefault = hint && hint !== inputValue;
  926. break;
  927. case "upKeyed":
  928. case "downKeyed":
  929. preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey;
  930. break;
  931. }
  932. preventDefault && $e.preventDefault();
  933. },
  934. _setLanguageDirection: function() {
  935. var dir = this.inputView.getLanguageDirection();
  936. if (dir !== this.dir) {
  937. this.dir = dir;
  938. this.$node.css("direction", dir);
  939. this.dropdownView.setLanguageDirection(dir);
  940. }
  941. },
  942. _updateHint: function() {
  943. var suggestion = this.dropdownView.getFirstSuggestion(), hint = suggestion ? suggestion.value : null, dropdownIsVisible = this.dropdownView.isVisible(), inputHasOverflow = this.inputView.isOverflow(), inputValue, query, escapedQuery, beginsWithQuery, match;
  944. if (hint && dropdownIsVisible && !inputHasOverflow) {
  945. inputValue = this.inputView.getInputValue();
  946. query = inputValue.replace(/\s{2,}/g, " ").replace(/^\s+/g, "");
  947. escapedQuery = utils.escapeRegExChars(query);
  948. beginsWithQuery = new RegExp("^(?:" + escapedQuery + ")(.*$)", "i");
  949. match = beginsWithQuery.exec(hint);
  950. this.inputView.setHintValue(inputValue + (match ? match[1] : ""));
  951. }
  952. },
  953. _clearHint: function() {
  954. this.inputView.setHintValue("");
  955. },
  956. _clearSuggestions: function() {
  957. this.dropdownView.clearSuggestions();
  958. },
  959. _setInputValueToQuery: function() {
  960. this.inputView.setInputValue(this.inputView.getQuery());
  961. },
  962. _setInputValueToSuggestionUnderCursor: function(e) {
  963. var suggestion = e.data;
  964. this.inputView.setInputValue(suggestion.value, true);
  965. },
  966. _openDropdown: function() {
  967. this.dropdownView.open();
  968. },
  969. _closeDropdown: function(e) {
  970. this.dropdownView[e.type === "blured" ? "closeUnlessMouseIsOverDropdown" : "close"]();
  971. },
  972. _moveDropdownCursor: function(e) {
  973. var $e = e.data;
  974. if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) {
  975. this.dropdownView[e.type === "upKeyed" ? "moveCursorUp" : "moveCursorDown"]();
  976. }
  977. },
  978. _handleSelection: function(e) {
  979. var byClick = e.type === "suggestionSelected", suggestion = byClick ? e.data : this.dropdownView.getSuggestionUnderCursor();
  980. if (suggestion) {
  981. this.inputView.setInputValue(suggestion.value);
  982. byClick ? this.inputView.focus() : e.data.preventDefault();
  983. byClick && utils.isMsie() ? utils.defer(this.dropdownView.close) : this.dropdownView.close();
  984. this.eventBus.trigger("selected", suggestion.datum, suggestion.dataset);
  985. }
  986. },
  987. _getSuggestions: function() {
  988. var that = this, query = this.inputView.getQuery();
  989. if (utils.isBlankString(query)) {
  990. return;
  991. }
  992. utils.each(this.datasets, function(i, dataset) {
  993. dataset.getSuggestions(query, function(suggestions) {
  994. if (query === that.inputView.getQuery()) {
  995. that.dropdownView.renderSuggestions(dataset, suggestions);
  996. }
  997. });
  998. });
  999. },
  1000. _autocomplete: function(e) {
  1001. var isCursorAtEnd, ignoreEvent, query, hint, suggestion;
  1002. if (e.type === "rightKeyed" || e.type === "leftKeyed") {
  1003. isCursorAtEnd = this.inputView.isCursorAtEnd();
  1004. ignoreEvent = this.inputView.getLanguageDirection() === "ltr" ? e.type === "leftKeyed" : e.type === "rightKeyed";
  1005. if (!isCursorAtEnd || ignoreEvent) {
  1006. return;
  1007. }
  1008. }
  1009. query = this.inputView.getQuery();
  1010. hint = this.inputView.getHintValue();
  1011. if (hint !== "" && query !== hint) {
  1012. suggestion = this.dropdownView.getFirstSuggestion();
  1013. this.inputView.setInputValue(suggestion.value);
  1014. this.eventBus.trigger("autocompleted", suggestion.datum, suggestion.dataset);
  1015. }
  1016. },
  1017. _propagateEvent: function(e) {
  1018. this.eventBus.trigger(e.type);
  1019. },
  1020. destroy: function() {
  1021. this.inputView.destroy();
  1022. this.dropdownView.destroy();
  1023. destroyDomStructure(this.$node);
  1024. this.$node = null;
  1025. },
  1026. setQuery: function(query) {
  1027. this.inputView.setQuery(query);
  1028. this.inputView.setInputValue(query);
  1029. this._clearHint();
  1030. this._clearSuggestions();
  1031. this._getSuggestions();
  1032. }
  1033. });
  1034. return TypeaheadView;
  1035. function buildDomStructure(input) {
  1036. var $wrapper = $(html.wrapper), $dropdown = $(html.dropdown), $input = $(input), $hint = $(html.hint);
  1037. $wrapper = $wrapper.css(css.wrapper);
  1038. $dropdown = $dropdown.css(css.dropdown);
  1039. $hint.css(css.hint).css({
  1040. backgroundAttachment: $input.css("background-attachment"),
  1041. backgroundClip: $input.css("background-clip"),
  1042. backgroundColor: $input.css("background-color"),
  1043. backgroundImage: $input.css("background-image"),
  1044. backgroundOrigin: $input.css("background-origin"),
  1045. backgroundPosition: $input.css("background-position"),
  1046. backgroundRepeat: $input.css("background-repeat"),
  1047. backgroundSize: $input.css("background-size")
  1048. });
  1049. $input.data("ttAttrs", {
  1050. dir: $input.attr("dir"),
  1051. autocomplete: $input.attr("autocomplete"),
  1052. spellcheck: $input.attr("spellcheck"),
  1053. style: $input.attr("style")
  1054. });
  1055. $input.addClass("tt-query").attr({
  1056. autocomplete: "off",
  1057. spellcheck: false
  1058. }).css(css.query);
  1059. try {
  1060. !$input.attr("dir") && $input.attr("dir", "auto");
  1061. } catch (e) {}
  1062. return $input.wrap($wrapper).parent().prepend($hint).append($dropdown);
  1063. }
  1064. function destroyDomStructure($node) {
  1065. var $input = $node.find(".tt-query");
  1066. utils.each($input.data("ttAttrs"), function(key, val) {
  1067. utils.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
  1068. });
  1069. $input.detach().removeData("ttAttrs").removeClass("tt-query").insertAfter($node);
  1070. $node.remove();
  1071. }
  1072. }();
  1073. (function() {
  1074. var cache = {}, viewKey = "ttView", methods;
  1075. methods = {
  1076. initialize: function(datasetDefs) {
  1077. var datasets;
  1078. datasetDefs = utils.isArray(datasetDefs) ? datasetDefs : [ datasetDefs ];
  1079. if (datasetDefs.length === 0) {
  1080. $.error("no datasets provided");
  1081. }
  1082. datasets = utils.map(datasetDefs, function(o) {
  1083. var dataset = cache[o.name] ? cache[o.name] : new Dataset(o);
  1084. if (o.name) {
  1085. cache[o.name] = dataset;
  1086. }
  1087. return dataset;
  1088. });
  1089. return this.each(initialize);
  1090. function initialize() {
  1091. var $input = $(this), deferreds, eventBus = new EventBus({
  1092. el: $input
  1093. });
  1094. deferreds = utils.map(datasets, function(dataset) {
  1095. return dataset.initialize();
  1096. });
  1097. $input.data(viewKey, new TypeaheadView({
  1098. input: $input,
  1099. eventBus: eventBus = new EventBus({
  1100. el: $input
  1101. }),
  1102. datasets: datasets
  1103. }));
  1104. $.when.apply($, deferreds).always(function() {
  1105. utils.defer(function() {
  1106. eventBus.trigger("initialized");
  1107. });
  1108. });
  1109. }
  1110. },
  1111. destroy: function() {
  1112. return this.each(destroy);
  1113. function destroy() {
  1114. var $this = $(this), view = $this.data(viewKey);
  1115. if (view) {
  1116. view.destroy();
  1117. $this.removeData(viewKey);
  1118. }
  1119. }
  1120. },
  1121. setQuery: function(query) {
  1122. return this.each(setQuery);
  1123. function setQuery() {
  1124. var view = $(this).data(viewKey);
  1125. view && view.setQuery(query);
  1126. }
  1127. }
  1128. };
  1129. jQuery.fn.typeahead = function(method) {
  1130. if (methods[method]) {
  1131. return methods[method].apply(this, [].slice.call(arguments, 1));
  1132. } else {
  1133. return methods.initialize.apply(this, arguments);
  1134. }
  1135. };
  1136. })();
  1137. })(window.jQuery);