autocomplete.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. // Copyright 2006 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Gmail-like AutoComplete logic.
  16. *
  17. * @see ../../demos/autocomplete-basic.html
  18. */
  19. goog.provide('goog.ui.ac.AutoComplete');
  20. goog.provide('goog.ui.ac.AutoComplete.EventType');
  21. goog.require('goog.array');
  22. goog.require('goog.asserts');
  23. goog.require('goog.events');
  24. goog.require('goog.events.EventTarget');
  25. goog.require('goog.object');
  26. goog.require('goog.ui.ac.RenderOptions');
  27. /**
  28. * This is the central manager class for an AutoComplete instance. The matcher
  29. * can specify disabled rows that should not be hilited or selected by
  30. * implementing <code>isRowDisabled(row):boolean</code> for each autocomplete
  31. * row. No row will be considered disabled if this method is not implemented.
  32. *
  33. * @param {Object} matcher A data source and row matcher, implements
  34. * <code>requestMatchingRows(token, maxMatches, matchCallback)</code>.
  35. * @param {goog.events.EventTarget} renderer An object that implements
  36. * <code>
  37. * isVisible():boolean<br>
  38. * renderRows(rows:Array, token:string, target:Element);<br>
  39. * hiliteId(row-id:number);<br>
  40. * dismiss();<br>
  41. * dispose():
  42. * </code>.
  43. * @param {Object} selectionHandler An object that implements
  44. * <code>
  45. * selectRow(row);<br>
  46. * update(opt_force);
  47. * </code>.
  48. *
  49. * @constructor
  50. * @extends {goog.events.EventTarget}
  51. * @suppress {underscore}
  52. */
  53. goog.ui.ac.AutoComplete = function(matcher, renderer, selectionHandler) {
  54. goog.events.EventTarget.call(this);
  55. /**
  56. * A data-source which provides autocomplete suggestions.
  57. *
  58. * TODO(chrishenry): Tighten the type to !goog.ui.ac.AutoComplete.Matcher.
  59. *
  60. * @type {Object}
  61. * @protected
  62. * @suppress {underscore|visibility}
  63. */
  64. this.matcher_ = matcher;
  65. /**
  66. * A handler which interacts with the input DOM element (textfield, textarea,
  67. * or richedit).
  68. *
  69. * TODO(chrishenry): Tighten the type to !Object.
  70. *
  71. * @type {Object}
  72. * @protected
  73. * @suppress {underscore|visibility}
  74. */
  75. this.selectionHandler_ = selectionHandler;
  76. /**
  77. * A renderer to render/show/highlight/hide the autocomplete menu.
  78. * @type {goog.events.EventTarget}
  79. * @protected
  80. * @suppress {underscore|visibility}
  81. */
  82. this.renderer_ = renderer;
  83. goog.events.listen(
  84. renderer,
  85. [
  86. goog.ui.ac.AutoComplete.EventType.HILITE,
  87. goog.ui.ac.AutoComplete.EventType.SELECT,
  88. goog.ui.ac.AutoComplete.EventType.CANCEL_DISMISS,
  89. goog.ui.ac.AutoComplete.EventType.DISMISS
  90. ],
  91. this.handleEvent, false, this);
  92. /**
  93. * Currently typed token which will be used for completion.
  94. * @type {?string}
  95. * @protected
  96. * @suppress {underscore|visibility}
  97. */
  98. this.token_ = null;
  99. /**
  100. * Autocomplete suggestion items.
  101. * @type {Array<?>}
  102. * @protected
  103. * @suppress {underscore|visibility}
  104. */
  105. this.rows_ = [];
  106. /**
  107. * Id of the currently highlighted row.
  108. * @type {number}
  109. * @protected
  110. * @suppress {underscore|visibility}
  111. */
  112. this.hiliteId_ = -1;
  113. /**
  114. * Id of the first row in autocomplete menu. Note that new ids are assigned
  115. * every time new suggestions are fetched.
  116. *
  117. * TODO(chrishenry): Figure out what subclass does with this value
  118. * and whether we should expose a more proper API.
  119. *
  120. * @type {number}
  121. * @protected
  122. * @suppress {underscore|visibility}
  123. */
  124. this.firstRowId_ = 0;
  125. /**
  126. * The target HTML node for displaying.
  127. * @type {Element}
  128. * @protected
  129. * @suppress {underscore|visibility}
  130. */
  131. this.target_ = null;
  132. /**
  133. * The timer id for dismissing autocomplete menu with a delay.
  134. * @type {?number}
  135. * @private
  136. */
  137. this.dismissTimer_ = null;
  138. /**
  139. * Mapping from text input element to the anchor element. If the
  140. * mapping does not exist, the input element will act as the anchor
  141. * element.
  142. * @type {Object<Element>}
  143. * @private
  144. */
  145. this.inputToAnchorMap_ = {};
  146. };
  147. goog.inherits(goog.ui.ac.AutoComplete, goog.events.EventTarget);
  148. /**
  149. * The maximum number of matches that should be returned
  150. * @type {number}
  151. * @private
  152. */
  153. goog.ui.ac.AutoComplete.prototype.maxMatches_ = 10;
  154. /**
  155. * True iff the first row should automatically be highlighted
  156. * @type {boolean}
  157. * @private
  158. */
  159. goog.ui.ac.AutoComplete.prototype.autoHilite_ = true;
  160. /**
  161. * True iff the user can unhilight all rows by pressing the up arrow.
  162. * @type {boolean}
  163. * @private
  164. */
  165. goog.ui.ac.AutoComplete.prototype.allowFreeSelect_ = false;
  166. /**
  167. * True iff item selection should wrap around from last to first. If
  168. * allowFreeSelect_ is on in conjunction, there is a step of free selection
  169. * before wrapping.
  170. * @type {boolean}
  171. * @private
  172. */
  173. goog.ui.ac.AutoComplete.prototype.wrap_ = false;
  174. /**
  175. * Whether completion from suggestion triggers fetching new suggestion.
  176. * @type {boolean}
  177. * @private
  178. */
  179. goog.ui.ac.AutoComplete.prototype.triggerSuggestionsOnUpdate_ = false;
  180. /**
  181. * Events associated with the autocomplete
  182. * @enum {string}
  183. */
  184. goog.ui.ac.AutoComplete.EventType = {
  185. /** A row has been highlighted by the renderer */
  186. ROW_HILITE: 'rowhilite',
  187. // Note: The events below are used for internal autocomplete events only and
  188. // should not be used in non-autocomplete code.
  189. /** A row has been mouseovered and should be highlighted by the renderer. */
  190. HILITE: 'hilite',
  191. /** A row has been selected by the renderer */
  192. SELECT: 'select',
  193. /** A dismiss event has occurred */
  194. DISMISS: 'dismiss',
  195. /** Event that cancels a dismiss event */
  196. CANCEL_DISMISS: 'canceldismiss',
  197. /**
  198. * Field value was updated. A row field is included and is non-null when a
  199. * row has been selected. The value of the row typically includes fields:
  200. * contactData and formattedValue as well as a toString function (though none
  201. * of these fields are guaranteed to exist). The row field may be used to
  202. * return custom-type row data.
  203. */
  204. UPDATE: 'update',
  205. /**
  206. * The list of suggestions has been updated, usually because either the list
  207. * has opened, or because the user has typed another character and the
  208. * suggestions have been updated, or the user has dismissed the autocomplete.
  209. */
  210. SUGGESTIONS_UPDATE: 'suggestionsupdate'
  211. };
  212. /**
  213. * @typedef {{
  214. * requestMatchingRows:(!Function|undefined),
  215. * isRowDisabled:(!Function|undefined)
  216. * }}
  217. */
  218. goog.ui.ac.AutoComplete.Matcher;
  219. /**
  220. * @return {!Object} The data source providing the `autocomplete
  221. * suggestions.
  222. */
  223. goog.ui.ac.AutoComplete.prototype.getMatcher = function() {
  224. return goog.asserts.assert(this.matcher_);
  225. };
  226. /**
  227. * Sets the data source providing the autocomplete suggestions.
  228. *
  229. * See constructor documentation for the interface.
  230. *
  231. * @param {!Object} matcher The matcher.
  232. * @protected
  233. */
  234. goog.ui.ac.AutoComplete.prototype.setMatcher = function(matcher) {
  235. this.matcher_ = matcher;
  236. };
  237. /**
  238. * @return {!Object} The handler used to interact with the input DOM
  239. * element (textfield, textarea, or richedit), e.g. to update the
  240. * input DOM element with selected value.
  241. * @protected
  242. */
  243. goog.ui.ac.AutoComplete.prototype.getSelectionHandler = function() {
  244. return goog.asserts.assert(this.selectionHandler_);
  245. };
  246. /**
  247. * @return {goog.events.EventTarget} The renderer that
  248. * renders/shows/highlights/hides the autocomplete menu.
  249. * See constructor documentation for the expected renderer API.
  250. */
  251. goog.ui.ac.AutoComplete.prototype.getRenderer = function() {
  252. return this.renderer_;
  253. };
  254. /**
  255. * Sets the renderer that renders/shows/highlights/hides the autocomplete
  256. * menu.
  257. *
  258. * See constructor documentation for the expected renderer API.
  259. *
  260. * @param {goog.events.EventTarget} renderer The renderer.
  261. * @protected
  262. */
  263. goog.ui.ac.AutoComplete.prototype.setRenderer = function(renderer) {
  264. this.renderer_ = renderer;
  265. };
  266. /**
  267. * @return {?string} The currently typed token used for completion.
  268. * @protected
  269. */
  270. goog.ui.ac.AutoComplete.prototype.getToken = function() {
  271. return this.token_;
  272. };
  273. /**
  274. * Sets the current token (without changing the rendered autocompletion).
  275. *
  276. * NOTE(chrishenry): This method will likely go away when we figure
  277. * out a better API.
  278. *
  279. * @param {?string} token The new token.
  280. * @protected
  281. */
  282. goog.ui.ac.AutoComplete.prototype.setTokenInternal = function(token) {
  283. this.token_ = token;
  284. };
  285. /**
  286. * @param {number} index The suggestion index, must be within the
  287. * interval [0, this.getSuggestionCount()).
  288. * @return {Object} The currently suggested item at the given index
  289. * (or null if there is none).
  290. */
  291. goog.ui.ac.AutoComplete.prototype.getSuggestion = function(index) {
  292. return this.rows_[index];
  293. };
  294. /**
  295. * @return {!Array<?>} The current autocomplete suggestion items.
  296. */
  297. goog.ui.ac.AutoComplete.prototype.getAllSuggestions = function() {
  298. return goog.asserts.assert(this.rows_);
  299. };
  300. /**
  301. * @return {number} The number of currently suggested items.
  302. */
  303. goog.ui.ac.AutoComplete.prototype.getSuggestionCount = function() {
  304. return this.rows_.length;
  305. };
  306. /**
  307. * @return {number} The id (not index!) of the currently highlighted row.
  308. */
  309. goog.ui.ac.AutoComplete.prototype.getHighlightedId = function() {
  310. return this.hiliteId_;
  311. };
  312. /**
  313. * Generic event handler that handles any events this object is listening to.
  314. * @param {goog.events.Event} e Event Object.
  315. */
  316. goog.ui.ac.AutoComplete.prototype.handleEvent = function(e) {
  317. var matcher = /** @type {?goog.ui.ac.AutoComplete.Matcher} */ (this.matcher_);
  318. if (e.target == this.renderer_) {
  319. switch (e.type) {
  320. case goog.ui.ac.AutoComplete.EventType.HILITE:
  321. this.hiliteId(/** @type {number} */ (e.row));
  322. break;
  323. case goog.ui.ac.AutoComplete.EventType.SELECT:
  324. var rowDisabled = false;
  325. // e.row can be either a valid row id or empty.
  326. if (goog.isNumber(e.row)) {
  327. var rowId = e.row;
  328. var index = this.getIndexOfId(rowId);
  329. var row = this.rows_[index];
  330. // Make sure the row selected is not a disabled row.
  331. rowDisabled =
  332. !!row && matcher.isRowDisabled && matcher.isRowDisabled(row);
  333. if (row && !rowDisabled && this.hiliteId_ != rowId) {
  334. // Event target row not currently highlighted - fix the mismatch.
  335. this.hiliteId(rowId);
  336. }
  337. }
  338. if (!rowDisabled) {
  339. // Note that rowDisabled can be false even if e.row does not
  340. // contain a valid row ID; at least one client depends on us
  341. // proceeding anyway.
  342. this.selectHilited();
  343. }
  344. break;
  345. case goog.ui.ac.AutoComplete.EventType.CANCEL_DISMISS:
  346. this.cancelDelayedDismiss();
  347. break;
  348. case goog.ui.ac.AutoComplete.EventType.DISMISS:
  349. this.dismissOnDelay();
  350. break;
  351. }
  352. }
  353. };
  354. /**
  355. * Sets the max number of matches to fetch from the Matcher.
  356. *
  357. * @param {number} max Max number of matches.
  358. */
  359. goog.ui.ac.AutoComplete.prototype.setMaxMatches = function(max) {
  360. this.maxMatches_ = max;
  361. };
  362. /**
  363. * Sets whether or not the first row should be highlighted by default.
  364. *
  365. * @param {boolean} autoHilite true iff the first row should be
  366. * highlighted by default.
  367. */
  368. goog.ui.ac.AutoComplete.prototype.setAutoHilite = function(autoHilite) {
  369. this.autoHilite_ = autoHilite;
  370. };
  371. /**
  372. * Sets whether or not the up/down arrow can unhilite all rows.
  373. *
  374. * @param {boolean} allowFreeSelect true iff the up arrow can unhilite all rows.
  375. */
  376. goog.ui.ac.AutoComplete.prototype.setAllowFreeSelect = function(
  377. allowFreeSelect) {
  378. this.allowFreeSelect_ = allowFreeSelect;
  379. };
  380. /**
  381. * Sets whether or not selections can wrap around the edges.
  382. *
  383. * @param {boolean} wrap true iff sections should wrap around the edges.
  384. */
  385. goog.ui.ac.AutoComplete.prototype.setWrap = function(wrap) {
  386. this.wrap_ = wrap;
  387. };
  388. /**
  389. * Sets whether or not to request new suggestions immediately after completion
  390. * of a suggestion.
  391. *
  392. * @param {boolean} triggerSuggestionsOnUpdate true iff completion should fetch
  393. * new suggestions.
  394. */
  395. goog.ui.ac.AutoComplete.prototype.setTriggerSuggestionsOnUpdate = function(
  396. triggerSuggestionsOnUpdate) {
  397. this.triggerSuggestionsOnUpdate_ = triggerSuggestionsOnUpdate;
  398. };
  399. /**
  400. * Sets the token to match against. This triggers calls to the Matcher to
  401. * fetch the matches (up to maxMatches), and then it triggers a call to
  402. * <code>renderer.renderRows()</code>.
  403. *
  404. * @param {string} token The string for which to search in the Matcher.
  405. * @param {string=} opt_fullString Optionally, the full string in the input
  406. * field.
  407. */
  408. goog.ui.ac.AutoComplete.prototype.setToken = function(token, opt_fullString) {
  409. if (this.token_ == token) {
  410. return;
  411. }
  412. this.token_ = token;
  413. this.matcher_.requestMatchingRows(
  414. this.token_, this.maxMatches_, goog.bind(this.matchListener_, this),
  415. opt_fullString);
  416. this.cancelDelayedDismiss();
  417. };
  418. /**
  419. * Gets the current target HTML node for displaying autocomplete UI.
  420. * @return {Element} The current target HTML node for displaying autocomplete
  421. * UI.
  422. */
  423. goog.ui.ac.AutoComplete.prototype.getTarget = function() {
  424. return this.target_;
  425. };
  426. /**
  427. * Sets the current target HTML node for displaying autocomplete UI.
  428. * Can be an implementation specific definition of how to display UI in relation
  429. * to the target node.
  430. * This target will be passed into <code>renderer.renderRows()</code>
  431. *
  432. * @param {Element} target The current target HTML node for displaying
  433. * autocomplete UI.
  434. */
  435. goog.ui.ac.AutoComplete.prototype.setTarget = function(target) {
  436. this.target_ = target;
  437. };
  438. /**
  439. * @return {boolean} Whether the autocomplete's renderer is open.
  440. */
  441. goog.ui.ac.AutoComplete.prototype.isOpen = function() {
  442. return this.renderer_.isVisible();
  443. };
  444. /**
  445. * @return {number} Number of rows in the autocomplete.
  446. * @deprecated Use this.getSuggestionCount().
  447. */
  448. goog.ui.ac.AutoComplete.prototype.getRowCount = function() {
  449. return this.getSuggestionCount();
  450. };
  451. /**
  452. * Moves the hilite to the next non-disabled row.
  453. * Calls renderer.hiliteId() when there's something to do.
  454. * @return {boolean} Returns true on a successful hilite.
  455. */
  456. goog.ui.ac.AutoComplete.prototype.hiliteNext = function() {
  457. var lastId = this.firstRowId_ + this.rows_.length - 1;
  458. var toHilite = this.hiliteId_;
  459. // Hilite the next row, skipping any disabled rows.
  460. for (var i = 0; i < this.rows_.length; i++) {
  461. // Increment to the next row.
  462. if (toHilite >= this.firstRowId_ && toHilite < lastId) {
  463. toHilite++;
  464. } else if (toHilite == -1) {
  465. toHilite = this.firstRowId_;
  466. } else if (this.allowFreeSelect_ && toHilite == lastId) {
  467. this.hiliteId(-1);
  468. return false;
  469. } else if (this.wrap_ && toHilite == lastId) {
  470. toHilite = this.firstRowId_;
  471. } else {
  472. return false;
  473. }
  474. if (this.hiliteId(toHilite)) {
  475. return true;
  476. }
  477. }
  478. return false;
  479. };
  480. /**
  481. * Moves the hilite to the previous non-disabled row. Calls
  482. * renderer.hiliteId() when there's something to do.
  483. * @return {boolean} Returns true on a successful hilite.
  484. */
  485. goog.ui.ac.AutoComplete.prototype.hilitePrev = function() {
  486. var lastId = this.firstRowId_ + this.rows_.length - 1;
  487. var toHilite = this.hiliteId_;
  488. // Hilite the previous row, skipping any disabled rows.
  489. for (var i = 0; i < this.rows_.length; i++) {
  490. // Decrement to the previous row.
  491. if (toHilite > this.firstRowId_) {
  492. toHilite--;
  493. } else if (this.allowFreeSelect_ && toHilite == this.firstRowId_) {
  494. this.hiliteId(-1);
  495. return false;
  496. } else if (this.wrap_ && (toHilite == -1 || toHilite == this.firstRowId_)) {
  497. toHilite = lastId;
  498. } else {
  499. return false;
  500. }
  501. if (this.hiliteId(toHilite)) {
  502. return true;
  503. }
  504. }
  505. return false;
  506. };
  507. /**
  508. * Hilites the id if it's valid and the row is not disabled, otherwise does
  509. * nothing.
  510. * @param {number} id A row id (not index).
  511. * @return {boolean} Whether the id was hilited. Returns false if the row is
  512. * disabled.
  513. */
  514. goog.ui.ac.AutoComplete.prototype.hiliteId = function(id) {
  515. var index = this.getIndexOfId(id);
  516. var row = this.rows_[index];
  517. var rowDisabled =
  518. !!row && this.matcher_.isRowDisabled && this.matcher_.isRowDisabled(row);
  519. if (!rowDisabled) {
  520. this.hiliteId_ = id;
  521. this.renderer_.hiliteId(id);
  522. return index != -1;
  523. }
  524. return false;
  525. };
  526. /**
  527. * Hilites the index, if it's valid and the row is not disabled, otherwise does
  528. * nothing.
  529. * @param {number} index The row's index.
  530. * @return {boolean} Whether the index was hilited.
  531. */
  532. goog.ui.ac.AutoComplete.prototype.hiliteIndex = function(index) {
  533. return this.hiliteId(this.getIdOfIndex_(index));
  534. };
  535. /**
  536. * If there are any current matches, this passes the hilited row data to
  537. * <code>selectionHandler.selectRow()</code>
  538. * @return {boolean} Whether there are any current matches.
  539. */
  540. goog.ui.ac.AutoComplete.prototype.selectHilited = function() {
  541. var index = this.getIndexOfId(this.hiliteId_);
  542. if (index != -1) {
  543. var selectedRow = this.rows_[index];
  544. var suppressUpdate = this.selectionHandler_.selectRow(selectedRow);
  545. if (this.triggerSuggestionsOnUpdate_) {
  546. this.token_ = null;
  547. this.dismissOnDelay();
  548. } else {
  549. this.dismiss();
  550. }
  551. if (!suppressUpdate) {
  552. this.dispatchEvent({
  553. type: goog.ui.ac.AutoComplete.EventType.UPDATE,
  554. row: selectedRow,
  555. index: index
  556. });
  557. if (this.triggerSuggestionsOnUpdate_) {
  558. this.selectionHandler_.update(true);
  559. }
  560. }
  561. return true;
  562. } else {
  563. this.dismiss();
  564. this.dispatchEvent({
  565. type: goog.ui.ac.AutoComplete.EventType.UPDATE,
  566. row: null,
  567. index: null
  568. });
  569. return false;
  570. }
  571. };
  572. /**
  573. * Returns whether or not the autocomplete is open and has a highlighted row.
  574. * @return {boolean} Whether an autocomplete row is highlighted.
  575. */
  576. goog.ui.ac.AutoComplete.prototype.hasHighlight = function() {
  577. return this.isOpen() && this.getIndexOfId(this.hiliteId_) != -1;
  578. };
  579. /**
  580. * Clears out the token, rows, and hilite, and calls
  581. * <code>renderer.dismiss()</code>
  582. */
  583. goog.ui.ac.AutoComplete.prototype.dismiss = function() {
  584. this.hiliteId_ = -1;
  585. this.token_ = null;
  586. this.firstRowId_ += this.rows_.length;
  587. this.rows_ = [];
  588. window.clearTimeout(this.dismissTimer_);
  589. this.dismissTimer_ = null;
  590. this.renderer_.dismiss();
  591. this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE);
  592. this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.DISMISS);
  593. };
  594. /**
  595. * Call a dismiss after a delay, if there's already a dismiss active, ignore.
  596. */
  597. goog.ui.ac.AutoComplete.prototype.dismissOnDelay = function() {
  598. if (!this.dismissTimer_) {
  599. this.dismissTimer_ = window.setTimeout(goog.bind(this.dismiss, this), 100);
  600. }
  601. };
  602. /**
  603. * Cancels any delayed dismiss events immediately.
  604. * @return {boolean} Whether a delayed dismiss was cancelled.
  605. * @private
  606. */
  607. goog.ui.ac.AutoComplete.prototype.immediatelyCancelDelayedDismiss_ =
  608. function() {
  609. if (this.dismissTimer_) {
  610. window.clearTimeout(this.dismissTimer_);
  611. this.dismissTimer_ = null;
  612. return true;
  613. }
  614. return false;
  615. };
  616. /**
  617. * Cancel the active delayed dismiss if there is one.
  618. */
  619. goog.ui.ac.AutoComplete.prototype.cancelDelayedDismiss = function() {
  620. // Under certain circumstances a cancel event occurs immediately prior to a
  621. // delayedDismiss event that it should be cancelling. To handle this situation
  622. // properly, a timer is used to stop that event.
  623. // Using only the timer creates undesirable behavior when the cancel occurs
  624. // less than 10ms before the delayed dismiss timout ends. If that happens the
  625. // clearTimeout() will occur too late and have no effect.
  626. if (!this.immediatelyCancelDelayedDismiss_()) {
  627. window.setTimeout(
  628. goog.bind(this.immediatelyCancelDelayedDismiss_, this), 10);
  629. }
  630. };
  631. /** @override */
  632. goog.ui.ac.AutoComplete.prototype.disposeInternal = function() {
  633. goog.ui.ac.AutoComplete.superClass_.disposeInternal.call(this);
  634. delete this.inputToAnchorMap_;
  635. this.renderer_.dispose();
  636. this.selectionHandler_.dispose();
  637. this.matcher_ = null;
  638. };
  639. /**
  640. * Callback passed to Matcher when requesting matches for a token.
  641. * This might be called synchronously, or asynchronously, or both, for
  642. * any implementation of a Matcher.
  643. * If the Matcher calls this back, with the same token this AutoComplete
  644. * has set currently, then this will package the matching rows in object
  645. * of the form
  646. * <pre>
  647. * {
  648. * id: an integer ID unique to this result set and AutoComplete instance,
  649. * data: the raw row data from Matcher
  650. * }
  651. * </pre>
  652. *
  653. * @param {string} matchedToken Token that corresponds with the rows.
  654. * @param {!Array<?>} rows Set of data that match the given token.
  655. * @param {(boolean|goog.ui.ac.RenderOptions)=} opt_options If true,
  656. * keeps the currently hilited (by index) element hilited. If false not.
  657. * Otherwise a RenderOptions object.
  658. * @private
  659. */
  660. goog.ui.ac.AutoComplete.prototype.matchListener_ = function(
  661. matchedToken, rows, opt_options) {
  662. if (this.token_ != matchedToken) {
  663. // Matcher's response token doesn't match current token.
  664. // This is probably an async response that came in after
  665. // the token was changed, so don't do anything.
  666. return;
  667. }
  668. this.renderRows(rows, opt_options);
  669. };
  670. /**
  671. * Renders the rows and adds highlighting.
  672. * @param {!Array<?>} rows Set of data that match the given token.
  673. * @param {(boolean|goog.ui.ac.RenderOptions)=} opt_options If true,
  674. * keeps the currently hilited (by index) element hilited. If false not.
  675. * Otherwise a RenderOptions object.
  676. */
  677. goog.ui.ac.AutoComplete.prototype.renderRows = function(rows, opt_options) {
  678. // The optional argument should be a RenderOptions object. It can be a
  679. // boolean for backwards compatibility, defaulting to false.
  680. var optionsObj = goog.typeOf(opt_options) == 'object' && opt_options;
  681. var preserveHilited =
  682. optionsObj ? optionsObj.getPreserveHilited() : opt_options;
  683. var indexToHilite = preserveHilited ? this.getIndexOfId(this.hiliteId_) : -1;
  684. // Current token matches the matcher's response token.
  685. this.firstRowId_ += this.rows_.length;
  686. this.rows_ = rows;
  687. var rendRows = [];
  688. for (var i = 0; i < rows.length; ++i) {
  689. rendRows.push({id: this.getIdOfIndex_(i), data: rows[i]});
  690. }
  691. var anchor = null;
  692. if (this.target_) {
  693. anchor = this.inputToAnchorMap_[goog.getUid(this.target_)] || this.target_;
  694. }
  695. this.renderer_.setAnchorElement(anchor);
  696. this.renderer_.renderRows(rendRows, this.token_, this.target_);
  697. var autoHilite = this.autoHilite_;
  698. if (optionsObj && optionsObj.getAutoHilite() !== undefined) {
  699. autoHilite = optionsObj.getAutoHilite();
  700. }
  701. this.hiliteId_ = -1;
  702. if ((autoHilite || indexToHilite >= 0) && rendRows.length != 0 &&
  703. this.token_) {
  704. if (indexToHilite >= 0) {
  705. this.hiliteId(this.getIdOfIndex_(indexToHilite));
  706. } else {
  707. // Hilite the first non-disabled row.
  708. this.hiliteNext();
  709. }
  710. }
  711. this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE);
  712. };
  713. /**
  714. * Gets the index corresponding to a particular id.
  715. * @param {number} id A unique id for the row.
  716. * @return {number} A valid index into rows_, or -1 if the id is invalid.
  717. * @protected
  718. */
  719. goog.ui.ac.AutoComplete.prototype.getIndexOfId = function(id) {
  720. var index = id - this.firstRowId_;
  721. if (index < 0 || index >= this.rows_.length) {
  722. return -1;
  723. }
  724. return index;
  725. };
  726. /**
  727. * Gets the id corresponding to a particular index. (Does no checking.)
  728. * @param {number} index The index of a row in the result set.
  729. * @return {number} The id that currently corresponds to that index.
  730. * @private
  731. */
  732. goog.ui.ac.AutoComplete.prototype.getIdOfIndex_ = function(index) {
  733. return this.firstRowId_ + index;
  734. };
  735. /**
  736. * Attach text areas or input boxes to the autocomplete by DOM reference. After
  737. * elements are attached to the autocomplete, when a user types they will see
  738. * the autocomplete drop down.
  739. * @param {...Element} var_args Variable args: Input or text area elements to
  740. * attach the autocomplete too.
  741. */
  742. goog.ui.ac.AutoComplete.prototype.attachInputs = function(var_args) {
  743. // Delegate to the input handler
  744. var inputHandler = /** @type {goog.ui.ac.InputHandler} */
  745. (this.selectionHandler_);
  746. inputHandler.attachInputs.apply(inputHandler, arguments);
  747. };
  748. /**
  749. * Detach text areas or input boxes to the autocomplete by DOM reference.
  750. * @param {...Element} var_args Variable args: Input or text area elements to
  751. * detach from the autocomplete.
  752. */
  753. goog.ui.ac.AutoComplete.prototype.detachInputs = function(var_args) {
  754. // Delegate to the input handler
  755. var inputHandler = /** @type {goog.ui.ac.InputHandler} */
  756. (this.selectionHandler_);
  757. inputHandler.detachInputs.apply(inputHandler, arguments);
  758. // Remove mapping from input to anchor if one exists.
  759. goog.array.forEach(arguments, function(input) {
  760. goog.object.remove(this.inputToAnchorMap_, goog.getUid(input));
  761. }, this);
  762. };
  763. /**
  764. * Attaches the autocompleter to a text area or text input element
  765. * with an anchor element. The anchor element is the element the
  766. * autocomplete box will be positioned against.
  767. * @param {Element} inputElement The input element. May be 'textarea',
  768. * text 'input' element, or any other element that exposes similar
  769. * interface.
  770. * @param {Element} anchorElement The anchor element.
  771. */
  772. goog.ui.ac.AutoComplete.prototype.attachInputWithAnchor = function(
  773. inputElement, anchorElement) {
  774. this.inputToAnchorMap_[goog.getUid(inputElement)] = anchorElement;
  775. this.attachInputs(inputElement);
  776. };
  777. /**
  778. * Forces an update of the display.
  779. * @param {boolean=} opt_force Whether to force an update.
  780. */
  781. goog.ui.ac.AutoComplete.prototype.update = function(opt_force) {
  782. var inputHandler = /** @type {goog.ui.ac.InputHandler} */
  783. (this.selectionHandler_);
  784. inputHandler.update(opt_force);
  785. };