renderer.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131
  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 Class for rendering the results of an auto complete and
  16. * allow the user to select an row.
  17. *
  18. */
  19. goog.provide('goog.ui.ac.Renderer');
  20. goog.provide('goog.ui.ac.Renderer.CustomRenderer');
  21. goog.require('goog.a11y.aria');
  22. goog.require('goog.a11y.aria.Role');
  23. goog.require('goog.a11y.aria.State');
  24. goog.require('goog.array');
  25. goog.require('goog.asserts');
  26. goog.require('goog.dispose');
  27. goog.require('goog.dom');
  28. goog.require('goog.dom.NodeType');
  29. goog.require('goog.dom.TagName');
  30. goog.require('goog.dom.classlist');
  31. goog.require('goog.events');
  32. goog.require('goog.events.EventTarget');
  33. goog.require('goog.events.EventType');
  34. goog.require('goog.fx.dom.FadeInAndShow');
  35. goog.require('goog.fx.dom.FadeOutAndHide');
  36. goog.require('goog.positioning');
  37. goog.require('goog.positioning.Corner');
  38. goog.require('goog.positioning.Overflow');
  39. goog.require('goog.string');
  40. goog.require('goog.style');
  41. goog.require('goog.ui.IdGenerator');
  42. goog.require('goog.ui.ac.AutoComplete');
  43. /**
  44. * Class for rendering the results of an auto-complete in a drop down list.
  45. *
  46. * @constructor
  47. * @param {Element=} opt_parentNode optional reference to the parent element
  48. * that will hold the autocomplete elements. goog.dom.getDocument().body
  49. * will be used if this is null.
  50. * @param {?({renderRow}|{render})=} opt_customRenderer Custom full renderer to
  51. * render each row. Should be something with a renderRow or render method.
  52. * @param {boolean=} opt_rightAlign Determines if the autocomplete will always
  53. * be right aligned. False by default.
  54. * @param {boolean=} opt_useStandardHighlighting Determines if standard
  55. * highlighting should be applied to each row of data. Standard highlighting
  56. * bolds every matching substring for a given token in each row. True by
  57. * default.
  58. * @extends {goog.events.EventTarget}
  59. * @suppress {underscore}
  60. */
  61. goog.ui.ac.Renderer = function(
  62. opt_parentNode, opt_customRenderer, opt_rightAlign,
  63. opt_useStandardHighlighting) {
  64. goog.ui.ac.Renderer.base(this, 'constructor');
  65. /**
  66. * Reference to the parent element that will hold the autocomplete elements
  67. * @type {Element}
  68. * @private
  69. */
  70. this.parent_ = opt_parentNode || goog.dom.getDocument().body;
  71. /**
  72. * Dom helper for the parent element's document.
  73. * @type {goog.dom.DomHelper}
  74. * @private
  75. */
  76. this.dom_ = goog.dom.getDomHelper(this.parent_);
  77. /**
  78. * Whether to reposition the autocomplete UI below the target node
  79. * @type {boolean}
  80. * @private
  81. */
  82. this.reposition_ = !opt_parentNode;
  83. /**
  84. * Reference to the main element that controls the rendered autocomplete
  85. * @type {Element}
  86. * @private
  87. */
  88. this.element_ = null;
  89. /**
  90. * The current token that has been entered
  91. * @type {string}
  92. * @private
  93. */
  94. this.token_ = '';
  95. /**
  96. * Array used to store the current set of rows being displayed
  97. * @type {Array<!Object>}
  98. * @private
  99. */
  100. this.rows_ = [];
  101. /**
  102. * Array of the node divs that hold each result that is being displayed.
  103. * @type {Array<Element>}
  104. * @protected
  105. * @suppress {underscore|visibility}
  106. */
  107. this.rowDivs_ = [];
  108. /**
  109. * The index of the currently highlighted row
  110. * @type {number}
  111. * @protected
  112. * @suppress {underscore|visibility}
  113. */
  114. this.hilitedRow_ = -1;
  115. /**
  116. * The time that the rendering of the menu rows started
  117. * @type {number}
  118. * @protected
  119. * @suppress {underscore|visibility}
  120. */
  121. this.startRenderingRows_ = -1;
  122. /**
  123. * Store the current state for the renderer
  124. * @type {boolean}
  125. * @private
  126. */
  127. this.visible_ = false;
  128. /**
  129. * Classname for the main element. This must be a single valid class name.
  130. * @type {string}
  131. */
  132. this.className = goog.getCssName('ac-renderer');
  133. /**
  134. * Classname for row divs. This must be a single valid class name.
  135. * @type {string}
  136. */
  137. this.rowClassName = goog.getCssName('ac-row');
  138. // TODO(gboyer): Remove this as soon as we remove references and ensure that
  139. // no groups are pushing javascript using this.
  140. /**
  141. * The old class name for active row. This name is deprecated because its
  142. * name is generic enough that a typical implementation would require a
  143. * descendant selector.
  144. * Active row will have rowClassName & activeClassName &
  145. * legacyActiveClassName.
  146. * @type {string}
  147. * @private
  148. */
  149. this.legacyActiveClassName_ = goog.getCssName('active');
  150. /**
  151. * Class name for active row div. This must be a single valid class name.
  152. * Active row will have rowClassName & activeClassName &
  153. * legacyActiveClassName.
  154. * @type {string}
  155. */
  156. this.activeClassName = goog.getCssName('ac-active');
  157. /**
  158. * Class name for the bold tag highlighting the matched part of the text.
  159. * @type {string}
  160. */
  161. this.highlightedClassName = goog.getCssName('ac-highlighted');
  162. /**
  163. * Custom full renderer
  164. * @type {?({renderRow}|{render})}
  165. * @private
  166. */
  167. this.customRenderer_ = opt_customRenderer || null;
  168. /**
  169. * Flag to indicate whether standard highlighting should be applied.
  170. * this is set to true if left unspecified to retain existing
  171. * behaviour for autocomplete clients
  172. * @type {boolean}
  173. * @private
  174. */
  175. this.useStandardHighlighting_ =
  176. opt_useStandardHighlighting != null ? opt_useStandardHighlighting : true;
  177. /**
  178. * Flag to indicate whether matches should be done on whole words instead
  179. * of any string.
  180. * @type {boolean}
  181. * @private
  182. */
  183. this.matchWordBoundary_ = true;
  184. /**
  185. * Flag to set all tokens as highlighted in the autocomplete row.
  186. * @type {boolean}
  187. * @private
  188. */
  189. this.highlightAllTokens_ = false;
  190. /**
  191. * Determines if the autocomplete will always be right aligned
  192. * @type {boolean}
  193. * @private
  194. */
  195. this.rightAlign_ = !!opt_rightAlign;
  196. /**
  197. * Whether to align with top of target field
  198. * @type {boolean}
  199. * @private
  200. */
  201. this.topAlign_ = false;
  202. /**
  203. * Duration (in msec) of fade animation when menu is shown/hidden.
  204. * Setting to 0 (default) disables animation entirely.
  205. * @type {number}
  206. * @private
  207. */
  208. this.menuFadeDuration_ = 0;
  209. /**
  210. * Whether we should limit the dropdown from extending past the bottom of the
  211. * screen and instead show a scrollbar on the dropdown.
  212. * @type {boolean}
  213. * @private
  214. */
  215. this.showScrollbarsIfTooLarge_ = false;
  216. /**
  217. * Animation in progress, if any.
  218. * @type {goog.fx.Animation|undefined}
  219. */
  220. this.animation_;
  221. };
  222. goog.inherits(goog.ui.ac.Renderer, goog.events.EventTarget);
  223. /**
  224. * The anchor element to position the rendered autocompleter against.
  225. * @type {Element}
  226. * @private
  227. */
  228. goog.ui.ac.Renderer.prototype.anchorElement_;
  229. /**
  230. * The anchor element to position the rendered autocompleter against.
  231. * @protected {Element|undefined}
  232. */
  233. goog.ui.ac.Renderer.prototype.target_;
  234. /**
  235. * The element on which to base the width of the autocomplete.
  236. * @protected {Node}
  237. */
  238. goog.ui.ac.Renderer.prototype.widthProvider_;
  239. /**
  240. * The border width of the autocomplete dropdown, only used in calculating the
  241. * dropdown width.
  242. * @private {number}
  243. */
  244. goog.ui.ac.Renderer.prototype.borderWidth_ = 0;
  245. /**
  246. * A flag used to make sure we highlight only one match in the rendered row.
  247. * @private {boolean}
  248. */
  249. goog.ui.ac.Renderer.prototype.wasHighlightedAtLeastOnce_;
  250. /**
  251. * The delay before mouseover events are registered, in milliseconds
  252. * @type {number}
  253. * @const
  254. */
  255. goog.ui.ac.Renderer.DELAY_BEFORE_MOUSEOVER = 300;
  256. /**
  257. * Gets the renderer's element.
  258. * @return {Element} The main element that controls the rendered autocomplete.
  259. */
  260. goog.ui.ac.Renderer.prototype.getElement = function() {
  261. return this.element_;
  262. };
  263. /**
  264. * Sets the width provider element. The provider is only used on redraw and as
  265. * such will not automatically update on resize.
  266. * @param {Node} widthProvider The element whose width should be mirrored.
  267. * @param {number=} opt_borderWidth The with of the border of the autocomplete,
  268. * which will be subtracted from the width of the autocomplete dropdown.
  269. */
  270. goog.ui.ac.Renderer.prototype.setWidthProvider = function(
  271. widthProvider, opt_borderWidth) {
  272. this.widthProvider_ = widthProvider;
  273. if (opt_borderWidth) {
  274. this.borderWidth_ = opt_borderWidth;
  275. }
  276. };
  277. /**
  278. * Set whether to align autocomplete to top of target element
  279. * @param {boolean} align If true, align to top.
  280. */
  281. goog.ui.ac.Renderer.prototype.setTopAlign = function(align) {
  282. this.topAlign_ = align;
  283. };
  284. /**
  285. * @return {boolean} Whether we should be aligning to the top of
  286. * the target element.
  287. */
  288. goog.ui.ac.Renderer.prototype.getTopAlign = function() {
  289. return this.topAlign_;
  290. };
  291. /**
  292. * Set whether to align autocomplete to the right of the target element.
  293. * @param {boolean} align If true, align to right.
  294. */
  295. goog.ui.ac.Renderer.prototype.setRightAlign = function(align) {
  296. this.rightAlign_ = align;
  297. };
  298. /**
  299. * @return {boolean} Whether the autocomplete menu should be right aligned.
  300. */
  301. goog.ui.ac.Renderer.prototype.getRightAlign = function() {
  302. return this.rightAlign_;
  303. };
  304. /**
  305. * @param {boolean} show Whether we should limit the dropdown from extending
  306. * past the bottom of the screen and instead show a scrollbar on the
  307. * dropdown.
  308. */
  309. goog.ui.ac.Renderer.prototype.setShowScrollbarsIfTooLarge = function(show) {
  310. this.showScrollbarsIfTooLarge_ = show;
  311. };
  312. /**
  313. * Set whether or not standard highlighting should be used when rendering rows.
  314. * @param {boolean} useStandardHighlighting true if standard highlighting used.
  315. */
  316. goog.ui.ac.Renderer.prototype.setUseStandardHighlighting = function(
  317. useStandardHighlighting) {
  318. this.useStandardHighlighting_ = useStandardHighlighting;
  319. };
  320. /**
  321. * @param {boolean} matchWordBoundary Determines whether matches should be
  322. * higlighted only when the token matches text at a whole-word boundary.
  323. * True by default.
  324. */
  325. goog.ui.ac.Renderer.prototype.setMatchWordBoundary = function(
  326. matchWordBoundary) {
  327. this.matchWordBoundary_ = matchWordBoundary;
  328. };
  329. /**
  330. * Set whether or not to highlight all matching tokens rather than just the
  331. * first.
  332. * @param {boolean} highlightAllTokens Whether to highlight all matching tokens
  333. * rather than just the first.
  334. */
  335. goog.ui.ac.Renderer.prototype.setHighlightAllTokens = function(
  336. highlightAllTokens) {
  337. this.highlightAllTokens_ = highlightAllTokens;
  338. };
  339. /**
  340. * Sets the duration (in msec) of the fade animation when menu is shown/hidden.
  341. * Setting to 0 (default) disables animation entirely.
  342. * @param {number} duration Duration (in msec) of the fade animation (or 0 for
  343. * no animation).
  344. */
  345. goog.ui.ac.Renderer.prototype.setMenuFadeDuration = function(duration) {
  346. this.menuFadeDuration_ = duration;
  347. };
  348. /**
  349. * Sets the anchor element for the subsequent call to renderRows.
  350. * @param {Element} anchor The anchor element.
  351. */
  352. goog.ui.ac.Renderer.prototype.setAnchorElement = function(anchor) {
  353. this.anchorElement_ = anchor;
  354. };
  355. /**
  356. * @return {Element} The anchor element.
  357. * @protected
  358. */
  359. goog.ui.ac.Renderer.prototype.getAnchorElement = function() {
  360. return this.anchorElement_;
  361. };
  362. /**
  363. * Render the autocomplete UI
  364. *
  365. * @param {Array<!Object>} rows Matching UI rows.
  366. * @param {string} token Token we are currently matching against.
  367. * @param {Element=} opt_target Current HTML node, will position popup beneath
  368. * this node.
  369. */
  370. goog.ui.ac.Renderer.prototype.renderRows = function(rows, token, opt_target) {
  371. this.token_ = token;
  372. this.rows_ = rows;
  373. this.hilitedRow_ = -1;
  374. this.startRenderingRows_ = goog.now();
  375. this.target_ = opt_target;
  376. this.rowDivs_ = [];
  377. this.redraw();
  378. };
  379. /**
  380. * Hide the object.
  381. */
  382. goog.ui.ac.Renderer.prototype.dismiss = function() {
  383. if (this.visible_) {
  384. this.visible_ = false;
  385. this.toggleAriaMarkup_(false /* isShown */);
  386. if (this.menuFadeDuration_ > 0) {
  387. goog.dispose(this.animation_);
  388. this.animation_ =
  389. new goog.fx.dom.FadeOutAndHide(this.element_, this.menuFadeDuration_);
  390. this.animation_.play();
  391. } else {
  392. goog.style.setElementShown(this.element_, false);
  393. }
  394. }
  395. };
  396. /**
  397. * Show the object.
  398. */
  399. goog.ui.ac.Renderer.prototype.show = function() {
  400. if (!this.visible_) {
  401. this.visible_ = true;
  402. this.toggleAriaMarkup_(true /* isShown */);
  403. if (this.menuFadeDuration_ > 0) {
  404. goog.dispose(this.animation_);
  405. this.animation_ =
  406. new goog.fx.dom.FadeInAndShow(this.element_, this.menuFadeDuration_);
  407. this.animation_.play();
  408. } else {
  409. goog.style.setElementShown(this.element_, true);
  410. }
  411. }
  412. };
  413. /**
  414. * Toggle the ARIA markup to add popup semantics when the target is shown and
  415. * to remove them when it is hidden.
  416. * @param {boolean} isShown Whether the menu is being shown.
  417. * @private
  418. */
  419. goog.ui.ac.Renderer.prototype.toggleAriaMarkup_ = function(isShown) {
  420. if (!this.target_) {
  421. return;
  422. }
  423. goog.a11y.aria.setState(this.target_, goog.a11y.aria.State.HASPOPUP, isShown);
  424. goog.a11y.aria.setState(
  425. goog.asserts.assert(this.element_), goog.a11y.aria.State.EXPANDED,
  426. isShown);
  427. goog.a11y.aria.setState(this.target_, goog.a11y.aria.State.EXPANDED, isShown);
  428. if (isShown) {
  429. goog.a11y.aria.setState(
  430. this.target_, goog.a11y.aria.State.OWNS, this.element_.id);
  431. } else {
  432. goog.a11y.aria.removeState(this.target_, goog.a11y.aria.State.OWNS);
  433. goog.a11y.aria.setActiveDescendant(this.target_, null);
  434. }
  435. };
  436. /**
  437. * @return {boolean} True if the object is visible.
  438. */
  439. goog.ui.ac.Renderer.prototype.isVisible = function() {
  440. return this.visible_;
  441. };
  442. /**
  443. * Sets the 'active' class of the nth item.
  444. * @param {number} index Index of the item to highlight.
  445. */
  446. goog.ui.ac.Renderer.prototype.hiliteRow = function(index) {
  447. var row =
  448. index >= 0 && index < this.rows_.length ? this.rows_[index] : undefined;
  449. var rowDiv = index >= 0 && index < this.rowDivs_.length ?
  450. this.rowDivs_[index] :
  451. undefined;
  452. var evtObj = /** @lends {goog.events.Event.prototype} */ ({
  453. type: goog.ui.ac.AutoComplete.EventType.ROW_HILITE,
  454. rowNode: rowDiv,
  455. row: row ? row.data : null
  456. });
  457. if (this.dispatchEvent(evtObj)) {
  458. this.hiliteNone();
  459. this.hilitedRow_ = index;
  460. if (rowDiv) {
  461. goog.dom.classlist.addAll(
  462. rowDiv, [this.activeClassName, this.legacyActiveClassName_]);
  463. if (this.target_) {
  464. goog.a11y.aria.setActiveDescendant(this.target_, rowDiv);
  465. }
  466. goog.style.scrollIntoContainerView(rowDiv, this.element_);
  467. }
  468. }
  469. };
  470. /**
  471. * Removes the 'active' class from the currently selected row.
  472. */
  473. goog.ui.ac.Renderer.prototype.hiliteNone = function() {
  474. if (this.hilitedRow_ >= 0) {
  475. goog.dom.classlist.removeAll(
  476. goog.asserts.assert(this.rowDivs_[this.hilitedRow_]),
  477. [this.activeClassName, this.legacyActiveClassName_]);
  478. }
  479. };
  480. /**
  481. * Sets the 'active' class of the item with a given id.
  482. * @param {number} id Id of the row to hilight. If id is -1 then no rows get
  483. * hilited.
  484. */
  485. goog.ui.ac.Renderer.prototype.hiliteId = function(id) {
  486. if (id == -1) {
  487. this.hiliteRow(-1);
  488. } else {
  489. for (var i = 0; i < this.rows_.length; i++) {
  490. if (this.rows_[i].id == id) {
  491. this.hiliteRow(i);
  492. return;
  493. }
  494. }
  495. }
  496. };
  497. /**
  498. * Sets CSS classes on autocomplete conatainer element.
  499. *
  500. * @param {Element} elem The container element.
  501. * @private
  502. */
  503. goog.ui.ac.Renderer.prototype.setMenuClasses_ = function(elem) {
  504. goog.asserts.assert(elem);
  505. // Legacy clients may set the renderer's className to a space-separated list
  506. // or even have a trailing space.
  507. goog.dom.classlist.addAll(elem, goog.string.trim(this.className).split(' '));
  508. };
  509. /**
  510. * If the main HTML element hasn't been made yet, creates it and appends it
  511. * to the parent.
  512. * @private
  513. */
  514. goog.ui.ac.Renderer.prototype.maybeCreateElement_ = function() {
  515. if (!this.element_) {
  516. // Make element and add it to the parent
  517. var el = this.dom_.createDom(goog.dom.TagName.DIV, {style: 'display:none'});
  518. if (this.showScrollbarsIfTooLarge_) {
  519. // Make sure that the dropdown will get scrollbars if it isn't large
  520. // enough to show all rows.
  521. el.style.overflowY = 'auto';
  522. }
  523. this.element_ = el;
  524. this.setMenuClasses_(el);
  525. goog.a11y.aria.setRole(el, goog.a11y.aria.Role.LISTBOX);
  526. el.id = goog.ui.IdGenerator.getInstance().getNextUniqueId();
  527. this.dom_.appendChild(this.parent_, el);
  528. // Add this object as an event handler
  529. goog.events.listen(
  530. el, goog.events.EventType.CLICK, this.handleClick_, false, this);
  531. goog.events.listen(
  532. el, goog.events.EventType.MOUSEDOWN, this.handleMouseDown_, false,
  533. this);
  534. goog.events.listen(
  535. el, goog.events.EventType.MOUSEOVER, this.handleMouseOver_, false,
  536. this);
  537. }
  538. };
  539. /**
  540. * Redraw (or draw if this is the first call) the rendered auto-complete drop
  541. * down.
  542. */
  543. goog.ui.ac.Renderer.prototype.redraw = function() {
  544. // Create the element if it doesn't yet exist
  545. this.maybeCreateElement_();
  546. // For top aligned with target (= bottom aligned element),
  547. // we need to hide and then add elements while hidden to prevent
  548. // visible repositioning
  549. if (this.topAlign_) {
  550. this.element_.style.visibility = 'hidden';
  551. }
  552. if (this.widthProvider_) {
  553. var width = this.widthProvider_.clientWidth - this.borderWidth_ + 'px';
  554. this.element_.style.minWidth = width;
  555. }
  556. // Remove the current child nodes
  557. this.rowDivs_.length = 0;
  558. this.dom_.removeChildren(this.element_);
  559. // Generate the new rows (use forEach so we can change rows_ from an
  560. // array to a different datastructure if required)
  561. if (this.customRenderer_ && this.customRenderer_.render) {
  562. this.customRenderer_.render(this, this.element_, this.rows_, this.token_);
  563. } else {
  564. var curRow = null;
  565. goog.array.forEach(this.rows_, function(row) {
  566. row = this.renderRowHtml(row, this.token_);
  567. if (this.topAlign_) {
  568. // Aligned with top of target = best match at bottom
  569. this.element_.insertBefore(row, curRow);
  570. } else {
  571. this.dom_.appendChild(this.element_, row);
  572. }
  573. curRow = row;
  574. }, this);
  575. }
  576. // Don't show empty result sets
  577. if (this.rows_.length == 0) {
  578. this.dismiss();
  579. return;
  580. } else {
  581. this.show();
  582. }
  583. this.reposition();
  584. // Make the autocompleter unselectable, so that it
  585. // doesn't steal focus from the input field when clicked.
  586. goog.style.setUnselectable(this.element_, true);
  587. };
  588. /**
  589. * @return {goog.positioning.Corner} The anchor corner to position the popup at.
  590. * @protected
  591. */
  592. goog.ui.ac.Renderer.prototype.getAnchorCorner = function() {
  593. var anchorCorner = this.rightAlign_ ? goog.positioning.Corner.BOTTOM_RIGHT :
  594. goog.positioning.Corner.BOTTOM_LEFT;
  595. if (this.topAlign_) {
  596. anchorCorner = goog.positioning.flipCornerVertical(anchorCorner);
  597. }
  598. return anchorCorner;
  599. };
  600. /**
  601. * Repositions the auto complete popup relative to the location node, if it
  602. * exists and the auto position has been set.
  603. */
  604. goog.ui.ac.Renderer.prototype.reposition = function() {
  605. if (this.target_ && this.reposition_) {
  606. var anchorElement = this.anchorElement_ || this.target_;
  607. var anchorCorner = this.getAnchorCorner();
  608. var overflowMode = goog.positioning.Overflow.ADJUST_X_EXCEPT_OFFSCREEN;
  609. if (this.showScrollbarsIfTooLarge_) {
  610. // positionAtAnchor will set the height of this.element_ when it runs
  611. // (because of RESIZE_HEIGHT), and it will never increase it relative to
  612. // its current value when it runs again. But if the user scrolls their
  613. // page, then we might actually want a bigger height when the dropdown is
  614. // displayed next time. So we clear the height before calling
  615. // positionAtAnchor, so it is free to set the height as large as it
  616. // chooses.
  617. this.element_.style.height = '';
  618. overflowMode |= goog.positioning.Overflow.RESIZE_HEIGHT;
  619. }
  620. goog.positioning.positionAtAnchor(
  621. anchorElement, anchorCorner, this.element_,
  622. goog.positioning.flipCornerVertical(anchorCorner), null, null,
  623. overflowMode);
  624. if (this.topAlign_) {
  625. // This flickers, but is better than the alternative of positioning
  626. // in the wrong place and then moving.
  627. this.element_.style.visibility = 'visible';
  628. }
  629. }
  630. };
  631. /**
  632. * Sets whether the renderer should try to determine where to position the
  633. * drop down.
  634. * @param {boolean} auto Whether to autoposition the drop down.
  635. */
  636. goog.ui.ac.Renderer.prototype.setAutoPosition = function(auto) {
  637. this.reposition_ = auto;
  638. };
  639. /**
  640. * @return {boolean} Whether the drop down will be autopositioned.
  641. * @protected
  642. */
  643. goog.ui.ac.Renderer.prototype.getAutoPosition = function() {
  644. return this.reposition_;
  645. };
  646. /**
  647. * @return {Element} The target element.
  648. * @protected
  649. */
  650. goog.ui.ac.Renderer.prototype.getTarget = function() {
  651. return this.target_ || null;
  652. };
  653. /**
  654. * Disposes of the renderer and its associated HTML.
  655. * @override
  656. * @protected
  657. */
  658. goog.ui.ac.Renderer.prototype.disposeInternal = function() {
  659. if (this.element_) {
  660. goog.events.unlisten(
  661. this.element_, goog.events.EventType.CLICK, this.handleClick_, false,
  662. this);
  663. goog.events.unlisten(
  664. this.element_, goog.events.EventType.MOUSEDOWN, this.handleMouseDown_,
  665. false, this);
  666. goog.events.unlisten(
  667. this.element_, goog.events.EventType.MOUSEOVER, this.handleMouseOver_,
  668. false, this);
  669. this.dom_.removeNode(this.element_);
  670. this.element_ = null;
  671. this.visible_ = false;
  672. }
  673. goog.dispose(this.animation_);
  674. this.parent_ = null;
  675. goog.ui.ac.Renderer.base(this, 'disposeInternal');
  676. };
  677. /**
  678. * Generic function that takes a row and renders a DOM structure for that row.
  679. *
  680. * Normally this will only be matching a maximum of 20 or so items. Even with
  681. * 40 rows, DOM this building is fine.
  682. *
  683. * @param {Object} row Object representing row.
  684. * @param {string} token Token to highlight.
  685. * @param {Node} node The node to render into.
  686. * @private
  687. */
  688. goog.ui.ac.Renderer.prototype.renderRowContents_ = function(row, token, node) {
  689. goog.dom.setTextContent(node, row.data.toString());
  690. };
  691. /**
  692. * Goes through a node and all of its child nodes, replacing HTML text that
  693. * matches a token with <b>token</b>.
  694. * The replacement will happen on the first match or all matches depending on
  695. * this.highlightAllTokens_ value.
  696. *
  697. * @param {Node} node Node to match.
  698. * @param {string|Array<string>} tokenOrArray Token to match or array of tokens
  699. * to match. By default, only the first match will be highlighted. If
  700. * highlightAllTokens is set, then all tokens appearing at the start of a
  701. * word, in whatever order and however many times, will be highlighted.
  702. * @private
  703. */
  704. goog.ui.ac.Renderer.prototype.startHiliteMatchingText_ = function(
  705. node, tokenOrArray) {
  706. this.wasHighlightedAtLeastOnce_ = false;
  707. this.hiliteMatchingText_(node, tokenOrArray);
  708. };
  709. /**
  710. * @param {Node} node Node to match.
  711. * @param {string|Array<string>} tokenOrArray Token to match or array of tokens
  712. * to match.
  713. * @private
  714. */
  715. goog.ui.ac.Renderer.prototype.hiliteMatchingText_ = function(
  716. node, tokenOrArray) {
  717. if (!this.highlightAllTokens_ && this.wasHighlightedAtLeastOnce_) {
  718. return;
  719. }
  720. if (node.nodeType == goog.dom.NodeType.TEXT) {
  721. var rest = null;
  722. if (goog.isArray(tokenOrArray) && tokenOrArray.length > 1 &&
  723. !this.highlightAllTokens_) {
  724. rest = goog.array.slice(tokenOrArray, 1);
  725. }
  726. var token = this.getTokenRegExp_(tokenOrArray);
  727. if (token.length == 0) return;
  728. var text = node.nodeValue;
  729. // Create a regular expression to match a token at the beginning of a line
  730. // or preceded by non-alpha-numeric characters. Note: token could have |
  731. // operators in it, so we need to parenthesise it before adding \b to it.
  732. // or preceded by non-alpha-numeric characters
  733. //
  734. // NOTE(user): When using word matches, this used to have
  735. // a (^|\\W+) clause where it now has \\b but it caused various
  736. // browsers to hang on really long strings. The (^|\\W+) matcher was also
  737. // unnecessary, because \b already checks that the character before the
  738. // is a non-word character, and ^ matches the start of the line or following
  739. // a line terminator character, which is also \W. The regexp also used to
  740. // have a capturing match before the \\b, which would capture the
  741. // non-highlighted content, but that caused the regexp matching to run much
  742. // slower than the current version.
  743. var re = this.matchWordBoundary_ ?
  744. new RegExp('\\b(?:' + token + ')', 'gi') :
  745. new RegExp(token, 'gi');
  746. var textNodes = [];
  747. var lastIndex = 0;
  748. // Find all matches
  749. // Note: text.split(re) has inconsistencies between IE and FF, so
  750. // manually recreated the logic
  751. var match = re.exec(text);
  752. var numMatches = 0;
  753. while (match) {
  754. numMatches++;
  755. textNodes.push(text.substring(lastIndex, match.index));
  756. textNodes.push(text.substring(match.index, re.lastIndex));
  757. lastIndex = re.lastIndex;
  758. match = re.exec(text);
  759. }
  760. textNodes.push(text.substring(lastIndex));
  761. // Replace the tokens with bolded text. Each pair of textNodes
  762. // (starting at index idx) includes a node of text before the bolded
  763. // token, and a node (at idx + 1) consisting of what should be
  764. // enclosed in bold tags.
  765. if (textNodes.length > 1) {
  766. var maxNumToBold = !this.highlightAllTokens_ ? 1 : numMatches;
  767. for (var i = 0; i < maxNumToBold; i++) {
  768. var idx = 2 * i;
  769. node.nodeValue = textNodes[idx];
  770. var boldTag = this.dom_.createElement(goog.dom.TagName.B);
  771. boldTag.className = this.highlightedClassName;
  772. this.dom_.appendChild(
  773. boldTag, this.dom_.createTextNode(textNodes[idx + 1]));
  774. boldTag = node.parentNode.insertBefore(boldTag, node.nextSibling);
  775. node.parentNode.insertBefore(
  776. this.dom_.createTextNode(''), boldTag.nextSibling);
  777. node = boldTag.nextSibling;
  778. }
  779. // Append the remaining text nodes to the end.
  780. var remainingTextNodes = goog.array.slice(textNodes, maxNumToBold * 2);
  781. node.nodeValue = remainingTextNodes.join('');
  782. this.wasHighlightedAtLeastOnce_ = true;
  783. } else if (rest) {
  784. this.hiliteMatchingText_(node, rest);
  785. }
  786. } else {
  787. var child = node.firstChild;
  788. while (child) {
  789. var nextChild = child.nextSibling;
  790. this.hiliteMatchingText_(child, tokenOrArray);
  791. child = nextChild;
  792. }
  793. }
  794. };
  795. /**
  796. * Transforms a token into a string ready to be put into the regular expression
  797. * in hiliteMatchingText_.
  798. * @param {string|Array<string>} tokenOrArray The token or array to get the
  799. * regex string from.
  800. * @return {string} The regex-ready token.
  801. * @private
  802. */
  803. goog.ui.ac.Renderer.prototype.getTokenRegExp_ = function(tokenOrArray) {
  804. var token = '';
  805. if (!tokenOrArray) {
  806. return token;
  807. }
  808. if (goog.isArray(tokenOrArray)) {
  809. // Remove invalid tokens from the array, which may leave us with nothing.
  810. tokenOrArray = goog.array.filter(tokenOrArray, function(str) {
  811. return !goog.string.isEmptyOrWhitespace(goog.string.makeSafe(str));
  812. });
  813. }
  814. // If highlighting all tokens, join them with '|' so the regular expression
  815. // will match on any of them.
  816. if (this.highlightAllTokens_) {
  817. if (goog.isArray(tokenOrArray)) {
  818. var tokenArray = goog.array.map(tokenOrArray, goog.string.regExpEscape);
  819. token = tokenArray.join('|');
  820. } else {
  821. // Remove excess whitespace from the string so bars will separate valid
  822. // tokens in the regular expression.
  823. token = goog.string.collapseWhitespace(tokenOrArray);
  824. token = goog.string.regExpEscape(token);
  825. token = token.replace(/ /g, '|');
  826. }
  827. } else {
  828. // Not highlighting all matching tokens. If tokenOrArray is a string, use
  829. // that as the token. If it is an array, use the first element in the
  830. // array.
  831. // TODO(user): why is this this way?. We should match against all
  832. // tokens in the array, but only accept the first match.
  833. if (goog.isArray(tokenOrArray)) {
  834. token = tokenOrArray.length > 0 ?
  835. goog.string.regExpEscape(tokenOrArray[0]) :
  836. '';
  837. } else {
  838. // For the single-match string token, we refuse to match anything if
  839. // the string begins with a non-word character, as matches by definition
  840. // can only occur at the start of a word. (This also handles the
  841. // goog.string.isEmptyOrWhitespace(goog.string.makeSafe(tokenOrArray))
  842. // case.)
  843. if (!/^\W/.test(tokenOrArray)) {
  844. token = goog.string.regExpEscape(tokenOrArray);
  845. }
  846. }
  847. }
  848. return token;
  849. };
  850. /**
  851. * Render a row by creating a div and then calling row rendering callback or
  852. * default row handler
  853. *
  854. * @param {Object} row Object representing row.
  855. * @param {string} token Token to highlight.
  856. * @return {!Element} An element with the rendered HTML.
  857. */
  858. goog.ui.ac.Renderer.prototype.renderRowHtml = function(row, token) {
  859. // Create and return the element.
  860. var elem = this.dom_.createDom(goog.dom.TagName.DIV, {
  861. className: this.rowClassName,
  862. id: goog.ui.IdGenerator.getInstance().getNextUniqueId()
  863. });
  864. goog.a11y.aria.setRole(elem, goog.a11y.aria.Role.OPTION);
  865. if (this.customRenderer_ && this.customRenderer_.renderRow) {
  866. this.customRenderer_.renderRow(row, token, elem);
  867. } else {
  868. this.renderRowContents_(row, token, elem);
  869. }
  870. if (token && this.useStandardHighlighting_) {
  871. this.startHiliteMatchingText_(elem, token);
  872. }
  873. goog.dom.classlist.add(elem, this.rowClassName);
  874. this.rowDivs_.push(elem);
  875. return elem;
  876. };
  877. /**
  878. * Given an event target looks up through the parents till it finds a div. Once
  879. * found it will then look to see if that is one of the childnodes, if it is
  880. * then the index is returned, otherwise -1 is returned.
  881. * @param {Element} et HtmlElement.
  882. * @return {number} Index corresponding to event target.
  883. * @private
  884. */
  885. goog.ui.ac.Renderer.prototype.getRowFromEventTarget_ = function(et) {
  886. while (et && et != this.element_ &&
  887. !goog.dom.classlist.contains(et, this.rowClassName)) {
  888. et = /** @type {Element} */ (et.parentNode);
  889. }
  890. return et ? goog.array.indexOf(this.rowDivs_, et) : -1;
  891. };
  892. /**
  893. * Handle the click events. These are redirected to the AutoComplete object
  894. * which then makes a callback to select the correct row.
  895. * @param {goog.events.Event} e Browser event object.
  896. * @private
  897. */
  898. goog.ui.ac.Renderer.prototype.handleClick_ = function(e) {
  899. var index = this.getRowFromEventTarget_(/** @type {Element} */ (e.target));
  900. if (index >= 0) {
  901. this.dispatchEvent(/** @lends {goog.events.Event.prototype} */ ({
  902. type: goog.ui.ac.AutoComplete.EventType.SELECT,
  903. row: this.rows_[index].id
  904. }));
  905. }
  906. e.stopPropagation();
  907. };
  908. /**
  909. * Handle the mousedown event and prevent the AC from losing focus.
  910. * @param {goog.events.Event} e Browser event object.
  911. * @private
  912. */
  913. goog.ui.ac.Renderer.prototype.handleMouseDown_ = function(e) {
  914. e.stopPropagation();
  915. e.preventDefault();
  916. };
  917. /**
  918. * Handle the mousing events. These are redirected to the AutoComplete object
  919. * which then makes a callback to set the correctly highlighted row. This is
  920. * because the AutoComplete can move the focus as well, and there is no sense
  921. * duplicating the code
  922. * @param {goog.events.Event} e Browser event object.
  923. * @private
  924. */
  925. goog.ui.ac.Renderer.prototype.handleMouseOver_ = function(e) {
  926. var index = this.getRowFromEventTarget_(/** @type {Element} */ (e.target));
  927. if (index >= 0) {
  928. if ((goog.now() - this.startRenderingRows_) <
  929. goog.ui.ac.Renderer.DELAY_BEFORE_MOUSEOVER) {
  930. return;
  931. }
  932. this.dispatchEvent({
  933. type: goog.ui.ac.AutoComplete.EventType.HILITE,
  934. row: this.rows_[index].id
  935. });
  936. }
  937. };
  938. /**
  939. * Class allowing different implementations to custom render the autocomplete.
  940. * Extending classes should override the render function.
  941. * @constructor
  942. */
  943. goog.ui.ac.Renderer.CustomRenderer = function() {};
  944. /**
  945. * Renders the autocomplete box. May be set to null.
  946. *
  947. * Because of the type, this function cannot be documented with param JSDoc.
  948. *
  949. * The function expects the following parameters:
  950. *
  951. * renderer, goog.ui.ac.Renderer: The autocomplete renderer.
  952. * element, Element: The main element that controls the rendered autocomplete.
  953. * rows, Array: The current set of rows being displayed.
  954. * token, string: The current token that has been entered. *
  955. *
  956. * @type {function(goog.ui.ac.Renderer, Element, Array, string)|
  957. * null|undefined}
  958. */
  959. goog.ui.ac.Renderer.CustomRenderer.prototype.render = function(
  960. renderer, element, rows, token) {};
  961. /**
  962. * Generic function that takes a row and renders a DOM structure for that row.
  963. * @param {Object} row Object representing row.
  964. * @param {string} token Token to highlight.
  965. * @param {Node} node The node to render into.
  966. */
  967. goog.ui.ac.Renderer.CustomRenderer.prototype.renderRow = function(
  968. row, token, node) {};