combobox.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998
  1. // Copyright 2007 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 A combo box control that allows user input with
  16. * auto-suggestion from a limited set of options.
  17. *
  18. * @see ../demos/combobox.html
  19. */
  20. goog.provide('goog.ui.ComboBox');
  21. goog.provide('goog.ui.ComboBoxItem');
  22. goog.require('goog.Timer');
  23. goog.require('goog.asserts');
  24. goog.require('goog.dom');
  25. goog.require('goog.dom.InputType');
  26. goog.require('goog.dom.TagName');
  27. goog.require('goog.dom.classlist');
  28. goog.require('goog.events.EventType');
  29. goog.require('goog.events.InputHandler');
  30. goog.require('goog.events.KeyCodes');
  31. goog.require('goog.events.KeyHandler');
  32. goog.require('goog.log');
  33. goog.require('goog.positioning.Corner');
  34. goog.require('goog.positioning.MenuAnchoredPosition');
  35. goog.require('goog.string');
  36. goog.require('goog.style');
  37. goog.require('goog.ui.Component');
  38. goog.require('goog.ui.ItemEvent');
  39. goog.require('goog.ui.LabelInput');
  40. goog.require('goog.ui.Menu');
  41. goog.require('goog.ui.MenuItem');
  42. goog.require('goog.ui.MenuSeparator');
  43. goog.require('goog.ui.registry');
  44. goog.require('goog.userAgent');
  45. /**
  46. * A ComboBox control.
  47. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  48. * @param {goog.ui.Menu=} opt_menu Optional menu component.
  49. * This menu is disposed of by this control.
  50. * @param {goog.ui.LabelInput=} opt_labelInput Optional label input.
  51. * This label input is disposed of by this control.
  52. * @extends {goog.ui.Component}
  53. * @constructor
  54. */
  55. goog.ui.ComboBox = function(opt_domHelper, opt_menu, opt_labelInput) {
  56. goog.ui.Component.call(this, opt_domHelper);
  57. this.labelInput_ = opt_labelInput || new goog.ui.LabelInput();
  58. this.enabled_ = true;
  59. // TODO(user): Allow lazy creation of menus/menu items
  60. this.menu_ = opt_menu || new goog.ui.Menu(this.getDomHelper());
  61. this.setupMenu_();
  62. };
  63. goog.inherits(goog.ui.ComboBox, goog.ui.Component);
  64. goog.tagUnsealableClass(goog.ui.ComboBox);
  65. /**
  66. * Number of milliseconds to wait before dismissing combobox after blur.
  67. * @type {number}
  68. */
  69. goog.ui.ComboBox.BLUR_DISMISS_TIMER_MS = 250;
  70. /**
  71. * A logger to help debugging of combo box behavior.
  72. * @type {goog.log.Logger}
  73. * @private
  74. */
  75. goog.ui.ComboBox.prototype.logger_ = goog.log.getLogger('goog.ui.ComboBox');
  76. /**
  77. * Whether the combo box is enabled.
  78. * @type {boolean}
  79. * @private
  80. */
  81. goog.ui.ComboBox.prototype.enabled_;
  82. /**
  83. * Keyboard event handler to manage key events dispatched by the input element.
  84. * @type {goog.events.KeyHandler}
  85. * @private
  86. */
  87. goog.ui.ComboBox.prototype.keyHandler_;
  88. /**
  89. * Input handler to take care of firing events when the user inputs text in
  90. * the input.
  91. * @type {goog.events.InputHandler?}
  92. * @private
  93. */
  94. goog.ui.ComboBox.prototype.inputHandler_ = null;
  95. /**
  96. * The last input token.
  97. * @type {?string}
  98. * @private
  99. */
  100. goog.ui.ComboBox.prototype.lastToken_ = null;
  101. /**
  102. * A LabelInput control that manages the focus/blur state of the input box.
  103. * @type {goog.ui.LabelInput?}
  104. * @private
  105. */
  106. goog.ui.ComboBox.prototype.labelInput_ = null;
  107. /**
  108. * Drop down menu for the combo box. Will be created at construction time.
  109. * @type {goog.ui.Menu?}
  110. * @private
  111. */
  112. goog.ui.ComboBox.prototype.menu_ = null;
  113. /**
  114. * The cached visible count.
  115. * @type {number}
  116. * @private
  117. */
  118. goog.ui.ComboBox.prototype.visibleCount_ = -1;
  119. /**
  120. * The input element.
  121. * @type {Element}
  122. * @private
  123. */
  124. goog.ui.ComboBox.prototype.input_ = null;
  125. /**
  126. * The match function. The first argument for the match function will be
  127. * a MenuItem's caption and the second will be the token to evaluate.
  128. * @type {Function}
  129. * @private
  130. */
  131. goog.ui.ComboBox.prototype.matchFunction_ = goog.string.startsWith;
  132. /**
  133. * Element used as the combo boxes button.
  134. * @type {Element}
  135. * @private
  136. */
  137. goog.ui.ComboBox.prototype.button_ = null;
  138. /**
  139. * Default text content for the input box when it is unchanged and unfocussed.
  140. * @type {string}
  141. * @private
  142. */
  143. goog.ui.ComboBox.prototype.defaultText_ = '';
  144. /**
  145. * Name for the input box created
  146. * @type {string}
  147. * @private
  148. */
  149. goog.ui.ComboBox.prototype.fieldName_ = '';
  150. /**
  151. * Timer identifier for delaying the dismissal of the combo menu.
  152. * @type {?number}
  153. * @private
  154. */
  155. goog.ui.ComboBox.prototype.dismissTimer_ = null;
  156. /**
  157. * True if the unicode inverted triangle should be displayed in the dropdown
  158. * button. Defaults to false.
  159. * @type {boolean} useDropdownArrow
  160. * @private
  161. */
  162. goog.ui.ComboBox.prototype.useDropdownArrow_ = false;
  163. /**
  164. * Create the DOM objects needed for the combo box. A span and text input.
  165. * @override
  166. */
  167. goog.ui.ComboBox.prototype.createDom = function() {
  168. this.input_ = this.getDomHelper().createDom(goog.dom.TagName.INPUT, {
  169. name: this.fieldName_,
  170. type: goog.dom.InputType.TEXT,
  171. autocomplete: 'off'
  172. });
  173. this.button_ = this.getDomHelper().createDom(
  174. goog.dom.TagName.SPAN, goog.getCssName('goog-combobox-button'));
  175. this.setElementInternal(
  176. this.getDomHelper().createDom(
  177. goog.dom.TagName.SPAN, goog.getCssName('goog-combobox'), this.input_,
  178. this.button_));
  179. if (this.useDropdownArrow_) {
  180. goog.dom.setTextContent(this.button_, '\u25BC');
  181. goog.style.setUnselectable(this.button_, true /* unselectable */);
  182. }
  183. this.input_.setAttribute('label', this.defaultText_);
  184. this.labelInput_.decorate(this.input_);
  185. this.menu_.setFocusable(false);
  186. if (!this.menu_.isInDocument()) {
  187. this.addChild(this.menu_, true);
  188. }
  189. };
  190. /**
  191. * Enables/Disables the combo box.
  192. * @param {boolean} enabled Whether to enable (true) or disable (false) the
  193. * combo box.
  194. */
  195. goog.ui.ComboBox.prototype.setEnabled = function(enabled) {
  196. this.enabled_ = enabled;
  197. this.labelInput_.setEnabled(enabled);
  198. goog.dom.classlist.enable(
  199. goog.asserts.assert(this.getElement()),
  200. goog.getCssName('goog-combobox-disabled'), !enabled);
  201. };
  202. /**
  203. * @return {boolean} Whether the menu item is enabled.
  204. */
  205. goog.ui.ComboBox.prototype.isEnabled = function() {
  206. return this.enabled_;
  207. };
  208. /** @override */
  209. goog.ui.ComboBox.prototype.enterDocument = function() {
  210. goog.ui.ComboBox.superClass_.enterDocument.call(this);
  211. var handler = this.getHandler();
  212. handler.listen(
  213. this.getElement(), goog.events.EventType.MOUSEDOWN,
  214. this.onComboMouseDown_);
  215. handler.listen(
  216. this.getDomHelper().getDocument(), goog.events.EventType.MOUSEDOWN,
  217. this.onDocClicked_);
  218. handler.listen(this.input_, goog.events.EventType.BLUR, this.onInputBlur_);
  219. this.keyHandler_ = new goog.events.KeyHandler(this.input_);
  220. handler.listen(
  221. this.keyHandler_, goog.events.KeyHandler.EventType.KEY,
  222. this.handleKeyEvent);
  223. this.inputHandler_ = new goog.events.InputHandler(this.input_);
  224. handler.listen(
  225. this.inputHandler_, goog.events.InputHandler.EventType.INPUT,
  226. this.onInputEvent_);
  227. handler.listen(
  228. this.menu_, goog.ui.Component.EventType.ACTION, this.onMenuSelected_);
  229. };
  230. /** @override */
  231. goog.ui.ComboBox.prototype.exitDocument = function() {
  232. this.keyHandler_.dispose();
  233. delete this.keyHandler_;
  234. this.inputHandler_.dispose();
  235. this.inputHandler_ = null;
  236. goog.ui.ComboBox.superClass_.exitDocument.call(this);
  237. };
  238. /**
  239. * Combo box currently can't decorate elements.
  240. * @return {boolean} The value false.
  241. * @override
  242. */
  243. goog.ui.ComboBox.prototype.canDecorate = function() {
  244. return false;
  245. };
  246. /** @override */
  247. goog.ui.ComboBox.prototype.disposeInternal = function() {
  248. goog.ui.ComboBox.superClass_.disposeInternal.call(this);
  249. this.clearDismissTimer_();
  250. this.labelInput_.dispose();
  251. this.menu_.dispose();
  252. this.labelInput_ = null;
  253. this.menu_ = null;
  254. this.input_ = null;
  255. this.button_ = null;
  256. };
  257. /**
  258. * Dismisses the menu and resets the value of the edit field.
  259. */
  260. goog.ui.ComboBox.prototype.dismiss = function() {
  261. this.clearDismissTimer_();
  262. this.hideMenu_();
  263. this.menu_.setHighlightedIndex(-1);
  264. };
  265. /**
  266. * Adds a new menu item at the end of the menu.
  267. * @param {goog.ui.MenuItem} item Menu item to add to the menu.
  268. */
  269. goog.ui.ComboBox.prototype.addItem = function(item) {
  270. this.menu_.addChild(item, true);
  271. this.visibleCount_ = -1;
  272. };
  273. /**
  274. * Adds a new menu item at a specific index in the menu.
  275. * @param {goog.ui.MenuItem} item Menu item to add to the menu.
  276. * @param {number} n Index at which to insert the menu item.
  277. */
  278. goog.ui.ComboBox.prototype.addItemAt = function(item, n) {
  279. this.menu_.addChildAt(item, n, true);
  280. this.visibleCount_ = -1;
  281. };
  282. /**
  283. * Removes an item from the menu and disposes it.
  284. * @param {goog.ui.MenuItem} item The menu item to remove.
  285. */
  286. goog.ui.ComboBox.prototype.removeItem = function(item) {
  287. var child = this.menu_.removeChild(item, true);
  288. if (child) {
  289. child.dispose();
  290. this.visibleCount_ = -1;
  291. }
  292. };
  293. /**
  294. * Remove all of the items from the ComboBox menu
  295. */
  296. goog.ui.ComboBox.prototype.removeAllItems = function() {
  297. for (var i = this.getItemCount() - 1; i >= 0; --i) {
  298. this.removeItem(this.getItemAt(i));
  299. }
  300. };
  301. /**
  302. * Removes a menu item at a given index in the menu.
  303. * @param {number} n Index of item.
  304. */
  305. goog.ui.ComboBox.prototype.removeItemAt = function(n) {
  306. var child = this.menu_.removeChildAt(n, true);
  307. if (child) {
  308. child.dispose();
  309. this.visibleCount_ = -1;
  310. }
  311. };
  312. /**
  313. * Returns a reference to the menu item at a given index.
  314. * @param {number} n Index of menu item.
  315. * @return {goog.ui.MenuItem?} Reference to the menu item.
  316. */
  317. goog.ui.ComboBox.prototype.getItemAt = function(n) {
  318. return /** @type {goog.ui.MenuItem?} */ (this.menu_.getChildAt(n));
  319. };
  320. /**
  321. * Returns the number of items in the list, including non-visible items,
  322. * such as separators.
  323. * @return {number} Number of items in the menu for this combobox.
  324. */
  325. goog.ui.ComboBox.prototype.getItemCount = function() {
  326. return this.menu_.getChildCount();
  327. };
  328. /**
  329. * @return {goog.ui.Menu} The menu that pops up.
  330. */
  331. goog.ui.ComboBox.prototype.getMenu = function() {
  332. return this.menu_;
  333. };
  334. /**
  335. * @return {Element} The input element.
  336. */
  337. goog.ui.ComboBox.prototype.getInputElement = function() {
  338. return this.input_;
  339. };
  340. /**
  341. * @return {goog.ui.LabelInput} A LabelInput control that manages the
  342. * focus/blur state of the input box.
  343. */
  344. goog.ui.ComboBox.prototype.getLabelInput = function() {
  345. return this.labelInput_;
  346. };
  347. /**
  348. * @return {number} The number of visible items in the menu.
  349. * @private
  350. */
  351. goog.ui.ComboBox.prototype.getNumberOfVisibleItems_ = function() {
  352. if (this.visibleCount_ == -1) {
  353. var count = 0;
  354. for (var i = 0, n = this.menu_.getChildCount(); i < n; i++) {
  355. var item = this.menu_.getChildAt(i);
  356. if (!(item instanceof goog.ui.MenuSeparator) && item.isVisible()) {
  357. count++;
  358. }
  359. }
  360. this.visibleCount_ = count;
  361. }
  362. goog.log.info(
  363. this.logger_, 'getNumberOfVisibleItems() - ' + this.visibleCount_);
  364. return this.visibleCount_;
  365. };
  366. /**
  367. * Sets the match function to be used when filtering the combo box menu.
  368. * @param {Function} matchFunction The match function to be used when filtering
  369. * the combo box menu.
  370. */
  371. goog.ui.ComboBox.prototype.setMatchFunction = function(matchFunction) {
  372. this.matchFunction_ = matchFunction;
  373. };
  374. /**
  375. * @return {Function} The match function for the combox box.
  376. */
  377. goog.ui.ComboBox.prototype.getMatchFunction = function() {
  378. return this.matchFunction_;
  379. };
  380. /**
  381. * Sets the default text for the combo box.
  382. * @param {string} text The default text for the combo box.
  383. */
  384. goog.ui.ComboBox.prototype.setDefaultText = function(text) {
  385. this.defaultText_ = text;
  386. if (this.labelInput_) {
  387. this.labelInput_.setLabel(this.defaultText_);
  388. }
  389. };
  390. /**
  391. * @return {string} text The default text for the combox box.
  392. */
  393. goog.ui.ComboBox.prototype.getDefaultText = function() {
  394. return this.defaultText_;
  395. };
  396. /**
  397. * Sets the field name for the combo box.
  398. * @param {string} fieldName The field name for the combo box.
  399. */
  400. goog.ui.ComboBox.prototype.setFieldName = function(fieldName) {
  401. this.fieldName_ = fieldName;
  402. };
  403. /**
  404. * @return {string} The field name for the combo box.
  405. */
  406. goog.ui.ComboBox.prototype.getFieldName = function() {
  407. return this.fieldName_;
  408. };
  409. /**
  410. * Set to true if a unicode inverted triangle should be displayed in the
  411. * dropdown button.
  412. * This option defaults to false for backwards compatibility.
  413. * @param {boolean} useDropdownArrow True to use the dropdown arrow.
  414. */
  415. goog.ui.ComboBox.prototype.setUseDropdownArrow = function(useDropdownArrow) {
  416. this.useDropdownArrow_ = !!useDropdownArrow;
  417. };
  418. /**
  419. * Sets the current value of the combo box.
  420. * @param {string} value The new value.
  421. */
  422. goog.ui.ComboBox.prototype.setValue = function(value) {
  423. goog.log.info(this.logger_, 'setValue() - ' + value);
  424. if (this.labelInput_.getValue() != value) {
  425. this.labelInput_.setValue(value);
  426. this.handleInputChange_();
  427. }
  428. };
  429. /**
  430. * @return {string} The current value of the combo box.
  431. */
  432. goog.ui.ComboBox.prototype.getValue = function() {
  433. return this.labelInput_.getValue();
  434. };
  435. /**
  436. * @return {string} HTML escaped token.
  437. */
  438. goog.ui.ComboBox.prototype.getToken = function() {
  439. // TODO(user): Remove HTML escaping and fix the existing calls.
  440. return goog.string.htmlEscape(this.getTokenText_());
  441. };
  442. /**
  443. * @return {string} The token for the current cursor position in the
  444. * input box, when multi-input is disabled it will be the full input value.
  445. * @private
  446. */
  447. goog.ui.ComboBox.prototype.getTokenText_ = function() {
  448. // TODO(user): Implement multi-input such that getToken returns a substring
  449. // of the whole input delimited by commas.
  450. return goog.string.trim(this.labelInput_.getValue().toLowerCase());
  451. };
  452. /**
  453. * @private
  454. */
  455. goog.ui.ComboBox.prototype.setupMenu_ = function() {
  456. var sm = this.menu_;
  457. sm.setVisible(false);
  458. sm.setAllowAutoFocus(false);
  459. sm.setAllowHighlightDisabled(true);
  460. };
  461. /**
  462. * Shows the menu if it isn't already showing. Also positions the menu
  463. * correctly, resets the menu item visibilities and highlights the relevant
  464. * item.
  465. * @param {boolean} showAll Whether to show all items, with the first matching
  466. * item highlighted.
  467. * @private
  468. */
  469. goog.ui.ComboBox.prototype.maybeShowMenu_ = function(showAll) {
  470. var isVisible = this.menu_.isVisible();
  471. var numVisibleItems = this.getNumberOfVisibleItems_();
  472. if (isVisible && numVisibleItems == 0) {
  473. goog.log.fine(this.logger_, 'no matching items, hiding');
  474. this.hideMenu_();
  475. } else if (!isVisible && numVisibleItems > 0) {
  476. if (showAll) {
  477. goog.log.fine(this.logger_, 'showing menu');
  478. this.setItemVisibilityFromToken_('');
  479. this.setItemHighlightFromToken_(this.getTokenText_());
  480. }
  481. // In Safari 2.0, when clicking on the combox box, the blur event is
  482. // received after the click event that invokes this function. Since we want
  483. // to cancel the dismissal after the blur event is processed, we have to
  484. // wait for all event processing to happen.
  485. goog.Timer.callOnce(this.clearDismissTimer_, 1, this);
  486. this.showMenu_();
  487. }
  488. this.positionMenu();
  489. };
  490. /**
  491. * Positions the menu.
  492. * @protected
  493. */
  494. goog.ui.ComboBox.prototype.positionMenu = function() {
  495. if (this.menu_ && this.menu_.isVisible()) {
  496. var position = new goog.positioning.MenuAnchoredPosition(
  497. this.getElement(), goog.positioning.Corner.BOTTOM_START, true);
  498. position.reposition(
  499. this.menu_.getElement(), goog.positioning.Corner.TOP_START);
  500. }
  501. };
  502. /**
  503. * Show the menu and add an active class to the combo box's element.
  504. * @private
  505. */
  506. goog.ui.ComboBox.prototype.showMenu_ = function() {
  507. this.menu_.setVisible(true);
  508. goog.dom.classlist.add(
  509. goog.asserts.assert(this.getElement()),
  510. goog.getCssName('goog-combobox-active'));
  511. };
  512. /**
  513. * Hide the menu and remove the active class from the combo box's element.
  514. * @private
  515. */
  516. goog.ui.ComboBox.prototype.hideMenu_ = function() {
  517. this.menu_.setVisible(false);
  518. goog.dom.classlist.remove(
  519. goog.asserts.assert(this.getElement()),
  520. goog.getCssName('goog-combobox-active'));
  521. };
  522. /**
  523. * Clears the dismiss timer if it's active.
  524. * @private
  525. */
  526. goog.ui.ComboBox.prototype.clearDismissTimer_ = function() {
  527. if (this.dismissTimer_) {
  528. goog.Timer.clear(this.dismissTimer_);
  529. this.dismissTimer_ = null;
  530. }
  531. };
  532. /**
  533. * Event handler for when the combo box area has been clicked.
  534. * @param {goog.events.BrowserEvent} e The browser event.
  535. * @private
  536. */
  537. goog.ui.ComboBox.prototype.onComboMouseDown_ = function(e) {
  538. // We only want this event on the element itself or the input or the button.
  539. if (this.enabled_ &&
  540. (e.target == this.getElement() || e.target == this.input_ ||
  541. goog.dom.contains(this.button_, /** @type {Node} */ (e.target)))) {
  542. if (this.menu_.isVisible()) {
  543. goog.log.fine(this.logger_, 'Menu is visible, dismissing');
  544. this.dismiss();
  545. } else {
  546. goog.log.fine(this.logger_, 'Opening dropdown');
  547. this.maybeShowMenu_(true);
  548. if (goog.userAgent.OPERA) {
  549. // select() doesn't focus <input> elements in Opera.
  550. this.input_.focus();
  551. }
  552. this.input_.select();
  553. this.menu_.setMouseButtonPressed(true);
  554. // Stop the click event from stealing focus
  555. e.preventDefault();
  556. }
  557. }
  558. // Stop the event from propagating outside of the combo box
  559. e.stopPropagation();
  560. };
  561. /**
  562. * Event handler for when the document is clicked.
  563. * @param {goog.events.BrowserEvent} e The browser event.
  564. * @private
  565. */
  566. goog.ui.ComboBox.prototype.onDocClicked_ = function(e) {
  567. if (!goog.dom.contains(
  568. this.menu_.getElement(), /** @type {Node} */ (e.target))) {
  569. this.dismiss();
  570. }
  571. };
  572. /**
  573. * Handle the menu's select event.
  574. * @param {goog.events.Event} e The event.
  575. * @private
  576. */
  577. goog.ui.ComboBox.prototype.onMenuSelected_ = function(e) {
  578. goog.log.info(this.logger_, 'onMenuSelected_()');
  579. var item = /** @type {!goog.ui.MenuItem} */ (e.target);
  580. // Stop propagation of the original event and redispatch to allow the menu
  581. // select to be cancelled at this level. i.e. if a menu item should cause
  582. // some behavior such as a user prompt instead of assigning the caption as
  583. // the value.
  584. if (this.dispatchEvent(
  585. new goog.ui.ItemEvent(
  586. goog.ui.Component.EventType.ACTION, this, item))) {
  587. var caption = item.getCaption();
  588. goog.log.fine(
  589. this.logger_, 'Menu selection: ' + caption + '. Dismissing menu');
  590. if (this.labelInput_.getValue() != caption) {
  591. this.labelInput_.setValue(caption);
  592. this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
  593. }
  594. this.dismiss();
  595. }
  596. e.stopPropagation();
  597. };
  598. /**
  599. * Event handler for when the input box looses focus -- hide the menu
  600. * @param {goog.events.BrowserEvent} e The browser event.
  601. * @private
  602. */
  603. goog.ui.ComboBox.prototype.onInputBlur_ = function(e) {
  604. goog.log.info(this.logger_, 'onInputBlur_() - delayed dismiss');
  605. this.clearDismissTimer_();
  606. this.dismissTimer_ = goog.Timer.callOnce(
  607. this.dismiss, goog.ui.ComboBox.BLUR_DISMISS_TIMER_MS, this);
  608. };
  609. /**
  610. * Handles keyboard events from the input box. Returns true if the combo box
  611. * was able to handle the event, false otherwise.
  612. * @param {goog.events.KeyEvent} e Key event to handle.
  613. * @return {boolean} Whether the event was handled by the combo box.
  614. * @protected
  615. * @suppress {visibility} performActionInternal
  616. */
  617. goog.ui.ComboBox.prototype.handleKeyEvent = function(e) {
  618. var isMenuVisible = this.menu_.isVisible();
  619. // Give the menu a chance to handle the event.
  620. if (isMenuVisible && this.menu_.handleKeyEvent(e)) {
  621. return true;
  622. }
  623. // The menu is either hidden or didn't handle the event.
  624. var handled = false;
  625. switch (e.keyCode) {
  626. case goog.events.KeyCodes.ESC:
  627. // If the menu is visible and the user hit Esc, dismiss the menu.
  628. if (isMenuVisible) {
  629. goog.log.fine(
  630. this.logger_, 'Dismiss on Esc: ' + this.labelInput_.getValue());
  631. this.dismiss();
  632. handled = true;
  633. }
  634. break;
  635. case goog.events.KeyCodes.TAB:
  636. // If the menu is open and an option is highlighted, activate it.
  637. if (isMenuVisible) {
  638. var highlighted = this.menu_.getHighlighted();
  639. if (highlighted) {
  640. goog.log.fine(
  641. this.logger_, 'Select on Tab: ' + this.labelInput_.getValue());
  642. highlighted.performActionInternal(e);
  643. handled = true;
  644. }
  645. }
  646. break;
  647. case goog.events.KeyCodes.UP:
  648. case goog.events.KeyCodes.DOWN:
  649. // If the menu is hidden and the user hit the up/down arrow, show it.
  650. if (!isMenuVisible) {
  651. goog.log.fine(this.logger_, 'Up/Down - maybe show menu');
  652. this.maybeShowMenu_(true);
  653. handled = true;
  654. }
  655. break;
  656. }
  657. if (handled) {
  658. e.preventDefault();
  659. }
  660. return handled;
  661. };
  662. /**
  663. * Handles the content of the input box changing.
  664. * @param {goog.events.Event} e The INPUT event to handle.
  665. * @private
  666. */
  667. goog.ui.ComboBox.prototype.onInputEvent_ = function(e) {
  668. // If the key event is text-modifying, update the menu.
  669. goog.log.fine(
  670. this.logger_, 'Key is modifying: ' + this.labelInput_.getValue());
  671. this.handleInputChange_();
  672. };
  673. /**
  674. * Handles the content of the input box changing, either because of user
  675. * interaction or programmatic changes.
  676. * @private
  677. */
  678. goog.ui.ComboBox.prototype.handleInputChange_ = function() {
  679. var token = this.getTokenText_();
  680. this.setItemVisibilityFromToken_(token);
  681. if (goog.dom.getActiveElement(this.getDomHelper().getDocument()) ==
  682. this.input_) {
  683. // Do not alter menu visibility unless the user focus is currently on the
  684. // combobox (otherwise programmatic changes may cause the menu to become
  685. // visible).
  686. this.maybeShowMenu_(false);
  687. }
  688. var highlighted = this.menu_.getHighlighted();
  689. if (token == '' || !highlighted || !highlighted.isVisible()) {
  690. this.setItemHighlightFromToken_(token);
  691. }
  692. this.lastToken_ = token;
  693. this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
  694. };
  695. /**
  696. * Loops through all menu items setting their visibility according to a token.
  697. * @param {string} token The token.
  698. * @private
  699. */
  700. goog.ui.ComboBox.prototype.setItemVisibilityFromToken_ = function(token) {
  701. goog.log.info(this.logger_, 'setItemVisibilityFromToken_() - ' + token);
  702. var isVisibleItem = false;
  703. var count = 0;
  704. var recheckHidden = !this.matchFunction_(token, this.lastToken_);
  705. for (var i = 0, n = this.menu_.getChildCount(); i < n; i++) {
  706. var item = this.menu_.getChildAt(i);
  707. if (item instanceof goog.ui.MenuSeparator) {
  708. // Ensure that separators are only shown if there is at least one visible
  709. // item before them.
  710. item.setVisible(isVisibleItem);
  711. isVisibleItem = false;
  712. } else if (item instanceof goog.ui.MenuItem) {
  713. if (!item.isVisible() && !recheckHidden) continue;
  714. var caption = item.getCaption();
  715. var visible = this.isItemSticky_(item) ||
  716. caption && this.matchFunction_(caption.toLowerCase(), token);
  717. if (typeof item.setFormatFromToken == 'function') {
  718. item.setFormatFromToken(token);
  719. }
  720. item.setVisible(!!visible);
  721. isVisibleItem = visible || isVisibleItem;
  722. } else {
  723. // Assume all other items are correctly using their visibility.
  724. isVisibleItem = item.isVisible() || isVisibleItem;
  725. }
  726. if (!(item instanceof goog.ui.MenuSeparator) && item.isVisible()) {
  727. count++;
  728. }
  729. }
  730. this.visibleCount_ = count;
  731. };
  732. /**
  733. * Highlights the first token that matches the given token.
  734. * @param {string} token The token.
  735. * @private
  736. */
  737. goog.ui.ComboBox.prototype.setItemHighlightFromToken_ = function(token) {
  738. goog.log.info(this.logger_, 'setItemHighlightFromToken_() - ' + token);
  739. if (token == '') {
  740. this.menu_.setHighlightedIndex(-1);
  741. return;
  742. }
  743. for (var i = 0, n = this.menu_.getChildCount(); i < n; i++) {
  744. var item = this.menu_.getChildAt(i);
  745. var caption = item.getCaption();
  746. if (caption && this.matchFunction_(caption.toLowerCase(), token)) {
  747. this.menu_.setHighlightedIndex(i);
  748. if (item.setFormatFromToken) {
  749. item.setFormatFromToken(token);
  750. }
  751. return;
  752. }
  753. }
  754. this.menu_.setHighlightedIndex(-1);
  755. };
  756. /**
  757. * Returns true if the item has an isSticky method and the method returns true.
  758. * @param {goog.ui.MenuItem} item The item.
  759. * @return {boolean} Whether the item has an isSticky method and the method
  760. * returns true.
  761. * @private
  762. */
  763. goog.ui.ComboBox.prototype.isItemSticky_ = function(item) {
  764. return typeof item.isSticky == 'function' && item.isSticky();
  765. };
  766. /**
  767. * Class for combo box items.
  768. * @param {goog.ui.ControlContent} content Text caption or DOM structure to
  769. * display as the content of the item (use to add icons or styling to
  770. * menus).
  771. * @param {*=} opt_data Identifying data for the menu item.
  772. * @param {goog.dom.DomHelper=} opt_domHelper Optional dom helper used for dom
  773. * interactions.
  774. * @param {goog.ui.MenuItemRenderer=} opt_renderer Optional renderer.
  775. * @constructor
  776. * @extends {goog.ui.MenuItem}
  777. */
  778. goog.ui.ComboBoxItem = function(
  779. content, opt_data, opt_domHelper, opt_renderer) {
  780. goog.ui.ComboBoxItem.base(
  781. this, 'constructor', content, opt_data, opt_domHelper, opt_renderer);
  782. };
  783. goog.inherits(goog.ui.ComboBoxItem, goog.ui.MenuItem);
  784. goog.tagUnsealableClass(goog.ui.ComboBoxItem);
  785. // Register a decorator factory function for goog.ui.ComboBoxItems.
  786. goog.ui.registry.setDecoratorByClassName(
  787. goog.getCssName('goog-combobox-item'), function() {
  788. // ComboBoxItem defaults to using MenuItemRenderer.
  789. return new goog.ui.ComboBoxItem(null);
  790. });
  791. /**
  792. * Whether the menu item is sticky, non-sticky items will be hidden as the
  793. * user types.
  794. * @type {boolean}
  795. * @private
  796. */
  797. goog.ui.ComboBoxItem.prototype.isSticky_ = false;
  798. /**
  799. * Sets the menu item to be sticky or not sticky.
  800. * @param {boolean} sticky Whether the menu item should be sticky.
  801. */
  802. goog.ui.ComboBoxItem.prototype.setSticky = function(sticky) {
  803. this.isSticky_ = sticky;
  804. };
  805. /**
  806. * @return {boolean} Whether the menu item is sticky.
  807. */
  808. goog.ui.ComboBoxItem.prototype.isSticky = function() {
  809. return this.isSticky_;
  810. };
  811. /**
  812. * Sets the format for a menu item based on a token, bolding the token.
  813. * @param {string} token The token.
  814. */
  815. goog.ui.ComboBoxItem.prototype.setFormatFromToken = function(token) {
  816. if (this.isEnabled()) {
  817. var caption = this.getCaption();
  818. var index = caption.toLowerCase().indexOf(token);
  819. if (index >= 0) {
  820. var domHelper = this.getDomHelper();
  821. this.setContent([
  822. domHelper.createTextNode(caption.substr(0, index)),
  823. domHelper.createDom(
  824. goog.dom.TagName.B, null, caption.substr(index, token.length)),
  825. domHelper.createTextNode(caption.substr(index + token.length))
  826. ]);
  827. }
  828. }
  829. };