PageRenderTime 54ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/files//0.8.2/typeahead.js

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