menu.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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 base menu class that supports key and mouse events. The menu
  16. * can be bound to an existing HTML structure or can generate its own DOM.
  17. *
  18. * To decorate, the menu should be bound to an element containing children
  19. * with the classname 'goog-menuitem'. HRs will be classed as separators.
  20. *
  21. * Decorate Example:
  22. * <div id="menu" class="goog-menu" tabIndex="0">
  23. * <div class="goog-menuitem">Google</div>
  24. * <div class="goog-menuitem">Yahoo</div>
  25. * <div class="goog-menuitem">MSN</div>
  26. * <hr>
  27. * <div class="goog-menuitem">New...</div>
  28. * </div>
  29. * <script>
  30. *
  31. * var menu = new goog.ui.Menu();
  32. * menu.decorate(goog.dom.getElement('menu'));
  33. *
  34. * TESTED=FireFox 2.0, IE6, Opera 9, Chrome.
  35. * TODO(user): Key handling is flaky in Opera and Chrome
  36. * TODO(user): Rename all references of "item" to child since menu is
  37. * essentially very generic and could, in theory, host a date or color picker.
  38. *
  39. * @see ../demos/menu.html
  40. * @see ../demos/menus.html
  41. */
  42. goog.provide('goog.ui.Menu');
  43. goog.provide('goog.ui.Menu.EventType');
  44. goog.require('goog.dom.TagName');
  45. goog.require('goog.math.Coordinate');
  46. goog.require('goog.string');
  47. goog.require('goog.style');
  48. goog.require('goog.ui.Component.EventType');
  49. goog.require('goog.ui.Component.State');
  50. goog.require('goog.ui.Container');
  51. goog.require('goog.ui.Container.Orientation');
  52. goog.require('goog.ui.MenuHeader');
  53. goog.require('goog.ui.MenuItem');
  54. goog.require('goog.ui.MenuRenderer');
  55. goog.require('goog.ui.MenuSeparator');
  56. // The dependencies MenuHeader, MenuItem, and MenuSeparator are implicit.
  57. // There are no references in the code, but we need to load these
  58. // classes before goog.ui.Menu.
  59. // TODO(robbyw): Reverse constructor argument order for consistency.
  60. /**
  61. * A basic menu class.
  62. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  63. * @param {goog.ui.MenuRenderer=} opt_renderer Renderer used to render or
  64. * decorate the container; defaults to {@link goog.ui.MenuRenderer}.
  65. * @constructor
  66. * @extends {goog.ui.Container}
  67. */
  68. goog.ui.Menu = function(opt_domHelper, opt_renderer) {
  69. goog.ui.Container.call(
  70. this, goog.ui.Container.Orientation.VERTICAL,
  71. opt_renderer || goog.ui.MenuRenderer.getInstance(), opt_domHelper);
  72. // Unlike Containers, Menus aren't keyboard-accessible by default. This line
  73. // preserves backwards compatibility with code that depends on menus not
  74. // receiving focus - e.g. {@code goog.ui.MenuButton}.
  75. this.setFocusable(false);
  76. };
  77. goog.inherits(goog.ui.Menu, goog.ui.Container);
  78. goog.tagUnsealableClass(goog.ui.Menu);
  79. // TODO(robbyw): Remove this and all references to it.
  80. // Please ensure that BEFORE_SHOW behavior is not disrupted as a result.
  81. /**
  82. * Event types dispatched by the menu.
  83. * @enum {string}
  84. * @deprecated Use goog.ui.Component.EventType.
  85. */
  86. goog.ui.Menu.EventType = {
  87. /** Dispatched before the menu becomes visible */
  88. BEFORE_SHOW: goog.ui.Component.EventType.BEFORE_SHOW,
  89. /** Dispatched when the menu is shown */
  90. SHOW: goog.ui.Component.EventType.SHOW,
  91. /** Dispatched before the menu becomes hidden */
  92. BEFORE_HIDE: goog.ui.Component.EventType.HIDE,
  93. /** Dispatched when the menu is hidden */
  94. HIDE: goog.ui.Component.EventType.HIDE
  95. };
  96. // TODO(robbyw): Remove this and all references to it.
  97. /**
  98. * CSS class for menus.
  99. * @type {string}
  100. * @deprecated Use goog.ui.MenuRenderer.CSS_CLASS.
  101. */
  102. goog.ui.Menu.CSS_CLASS = goog.ui.MenuRenderer.CSS_CLASS;
  103. /**
  104. * Coordinates of the mousedown event that caused this menu to be made visible.
  105. * Used to prevent the consequent mouseup event due to a simple click from
  106. * activating a menu item immediately. Considered protected; should only be used
  107. * within this package or by subclasses.
  108. * @type {goog.math.Coordinate|undefined}
  109. */
  110. goog.ui.Menu.prototype.openingCoords;
  111. /**
  112. * Whether the menu can move the focus to its key event target when it is
  113. * shown. Default = true
  114. * @type {boolean}
  115. * @private
  116. */
  117. goog.ui.Menu.prototype.allowAutoFocus_ = true;
  118. /**
  119. * Whether the menu should use windows style behavior and allow disabled menu
  120. * items to be highlighted (though not selectable). Defaults to false
  121. * @type {boolean}
  122. * @private
  123. */
  124. goog.ui.Menu.prototype.allowHighlightDisabled_ = false;
  125. /**
  126. * Returns the CSS class applied to menu elements, also used as the prefix for
  127. * derived styles, if any. Subclasses should override this method as needed.
  128. * Considered protected.
  129. * @return {string} The CSS class applied to menu elements.
  130. * @protected
  131. * @deprecated Use getRenderer().getCssClass().
  132. */
  133. goog.ui.Menu.prototype.getCssClass = function() {
  134. return this.getRenderer().getCssClass();
  135. };
  136. /**
  137. * Returns whether the provided element is to be considered inside the menu for
  138. * purposes such as dismissing the menu on an event. This is so submenus can
  139. * make use of elements outside their own DOM.
  140. * @param {Element} element The element to test for.
  141. * @return {boolean} Whether the provided element is to be considered inside
  142. * the menu.
  143. */
  144. goog.ui.Menu.prototype.containsElement = function(element) {
  145. if (this.getRenderer().containsElement(this, element)) {
  146. return true;
  147. }
  148. for (var i = 0, count = this.getChildCount(); i < count; i++) {
  149. var child = this.getChildAt(i);
  150. if (typeof child.containsElement == 'function' &&
  151. child.containsElement(element)) {
  152. return true;
  153. }
  154. }
  155. return false;
  156. };
  157. /**
  158. * Adds a new menu item at the end of the menu.
  159. * @param {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu
  160. * item to add to the menu.
  161. * @deprecated Use {@link #addChild} instead, with true for the second argument.
  162. */
  163. goog.ui.Menu.prototype.addItem = function(item) {
  164. this.addChild(item, true);
  165. };
  166. /**
  167. * Adds a new menu item at a specific index in the menu.
  168. * @param {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu
  169. * item to add to the menu.
  170. * @param {number} n Index at which to insert the menu item.
  171. * @deprecated Use {@link #addChildAt} instead, with true for the third
  172. * argument.
  173. */
  174. goog.ui.Menu.prototype.addItemAt = function(item, n) {
  175. this.addChildAt(item, n, true);
  176. };
  177. /**
  178. * Removes an item from the menu and disposes of it.
  179. * @param {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator} item The
  180. * menu item to remove.
  181. * @deprecated Use {@link #removeChild} instead.
  182. */
  183. goog.ui.Menu.prototype.removeItem = function(item) {
  184. var removedChild = this.removeChild(item, true);
  185. if (removedChild) {
  186. removedChild.dispose();
  187. }
  188. };
  189. /**
  190. * Removes a menu item at a given index in the menu and disposes of it.
  191. * @param {number} n Index of item.
  192. * @deprecated Use {@link #removeChildAt} instead.
  193. */
  194. goog.ui.Menu.prototype.removeItemAt = function(n) {
  195. var removedChild = this.removeChildAt(n, true);
  196. if (removedChild) {
  197. removedChild.dispose();
  198. }
  199. };
  200. /**
  201. * Returns a reference to the menu item at a given index.
  202. * @param {number} n Index of menu item.
  203. * @return {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator|null}
  204. * Reference to the menu item.
  205. * @deprecated Use {@link #getChildAt} instead.
  206. */
  207. goog.ui.Menu.prototype.getItemAt = function(n) {
  208. return /** @type {goog.ui.MenuItem?} */ (this.getChildAt(n));
  209. };
  210. /**
  211. * Returns the number of items in the menu (including separators).
  212. * @return {number} The number of items in the menu.
  213. * @deprecated Use {@link #getChildCount} instead.
  214. */
  215. goog.ui.Menu.prototype.getItemCount = function() {
  216. return this.getChildCount();
  217. };
  218. /**
  219. * Returns an array containing the menu items contained in the menu.
  220. * @return {!Array<goog.ui.MenuItem>} An array of menu items.
  221. * @deprecated Use getChildAt, forEachChild, and getChildCount.
  222. */
  223. goog.ui.Menu.prototype.getItems = function() {
  224. // TODO(user): Remove reference to getItems and instead use getChildAt,
  225. // forEachChild, and getChildCount
  226. var children = [];
  227. this.forEachChild(function(child) { children.push(child); });
  228. return children;
  229. };
  230. /**
  231. * Sets the position of the menu relative to the view port.
  232. * @param {number|goog.math.Coordinate} x Left position or coordinate obj.
  233. * @param {number=} opt_y Top position.
  234. */
  235. goog.ui.Menu.prototype.setPosition = function(x, opt_y) {
  236. // NOTE(user): It is necessary to temporarily set the display from none, so
  237. // that the position gets set correctly.
  238. var visible = this.isVisible();
  239. if (!visible) {
  240. goog.style.setElementShown(this.getElement(), true);
  241. }
  242. goog.style.setPageOffset(this.getElement(), x, opt_y);
  243. if (!visible) {
  244. goog.style.setElementShown(this.getElement(), false);
  245. }
  246. };
  247. /**
  248. * Gets the page offset of the menu, or null if the menu isn't visible
  249. * @return {goog.math.Coordinate?} Object holding the x-y coordinates of the
  250. * menu or null if the menu is not visible.
  251. */
  252. goog.ui.Menu.prototype.getPosition = function() {
  253. return this.isVisible() ? goog.style.getPageOffset(this.getElement()) : null;
  254. };
  255. /**
  256. * Sets whether the menu can automatically move focus to its key event target
  257. * when it is set to visible.
  258. * @param {boolean} allow Whether the menu can automatically move focus to its
  259. * key event target when it is set to visible.
  260. */
  261. goog.ui.Menu.prototype.setAllowAutoFocus = function(allow) {
  262. this.allowAutoFocus_ = allow;
  263. if (allow) {
  264. this.setFocusable(true);
  265. }
  266. };
  267. /**
  268. * @return {boolean} Whether the menu can automatically move focus to its key
  269. * event target when it is set to visible.
  270. */
  271. goog.ui.Menu.prototype.getAllowAutoFocus = function() {
  272. return this.allowAutoFocus_;
  273. };
  274. /**
  275. * Sets whether the menu will highlight disabled menu items or skip to the next
  276. * active item.
  277. * @param {boolean} allow Whether the menu will highlight disabled menu items or
  278. * skip to the next active item.
  279. */
  280. goog.ui.Menu.prototype.setAllowHighlightDisabled = function(allow) {
  281. this.allowHighlightDisabled_ = allow;
  282. };
  283. /**
  284. * @return {boolean} Whether the menu will highlight disabled menu items or skip
  285. * to the next active item.
  286. */
  287. goog.ui.Menu.prototype.getAllowHighlightDisabled = function() {
  288. return this.allowHighlightDisabled_;
  289. };
  290. /**
  291. * @override
  292. * @param {boolean} show Whether to show or hide the menu.
  293. * @param {boolean=} opt_force If true, doesn't check whether the menu
  294. * already has the requested visibility, and doesn't dispatch any events.
  295. * @param {goog.events.Event=} opt_e Mousedown event that caused this menu to
  296. * be made visible (ignored if show is false).
  297. */
  298. goog.ui.Menu.prototype.setVisible = function(show, opt_force, opt_e) {
  299. var visibilityChanged =
  300. goog.ui.Menu.superClass_.setVisible.call(this, show, opt_force);
  301. if (visibilityChanged && show && this.isInDocument() &&
  302. this.allowAutoFocus_) {
  303. this.getKeyEventTarget().focus();
  304. }
  305. if (show && opt_e && goog.isNumber(opt_e.clientX)) {
  306. this.openingCoords = new goog.math.Coordinate(opt_e.clientX, opt_e.clientY);
  307. } else {
  308. this.openingCoords = null;
  309. }
  310. return visibilityChanged;
  311. };
  312. /** @override */
  313. goog.ui.Menu.prototype.handleEnterItem = function(e) {
  314. if (this.allowAutoFocus_) {
  315. this.getKeyEventTarget().focus();
  316. }
  317. return goog.ui.Menu.superClass_.handleEnterItem.call(this, e);
  318. };
  319. /**
  320. * Highlights the next item that begins with the specified string. If no
  321. * (other) item begins with the given string, the selection is unchanged.
  322. * @param {string} charStr The prefix to match.
  323. * @return {boolean} Whether a matching prefix was found.
  324. */
  325. goog.ui.Menu.prototype.highlightNextPrefix = function(charStr) {
  326. var re = new RegExp('^' + goog.string.regExpEscape(charStr), 'i');
  327. return this.highlightHelper(function(index, max) {
  328. // Index is >= -1 because it is set to -1 when nothing is selected.
  329. var start = index < 0 ? 0 : index;
  330. var wrapped = false;
  331. // We always start looking from one after the current, because we
  332. // keep the current selection only as a last resort. This makes the
  333. // loop a little awkward in the case where there is no current
  334. // selection, as we need to stop somewhere but can't just stop
  335. // when index == start, which is why we need the 'wrapped' flag.
  336. do {
  337. ++index;
  338. if (index == max) {
  339. index = 0;
  340. wrapped = true;
  341. }
  342. var name = this.getChildAt(index).getCaption();
  343. if (name && name.match(re)) {
  344. return index;
  345. }
  346. } while (!wrapped || index != start);
  347. return this.getHighlightedIndex();
  348. }, this.getHighlightedIndex());
  349. };
  350. /** @override */
  351. goog.ui.Menu.prototype.canHighlightItem = function(item) {
  352. return (this.allowHighlightDisabled_ || item.isEnabled()) &&
  353. item.isVisible() && item.isSupportedState(goog.ui.Component.State.HOVER);
  354. };
  355. /** @override */
  356. goog.ui.Menu.prototype.decorateInternal = function(element) {
  357. this.decorateContent(element);
  358. goog.ui.Menu.superClass_.decorateInternal.call(this, element);
  359. };
  360. /** @override */
  361. goog.ui.Menu.prototype.handleKeyEventInternal = function(e) {
  362. var handled = goog.ui.Menu.base(this, 'handleKeyEventInternal', e);
  363. if (!handled) {
  364. // Loop through all child components, and for each menu item call its
  365. // key event handler so that keyboard mnemonics can be handled.
  366. this.forEachChild(function(menuItem) {
  367. if (!handled && menuItem.getMnemonic &&
  368. menuItem.getMnemonic() == e.keyCode) {
  369. if (this.isEnabled()) {
  370. this.setHighlighted(menuItem);
  371. }
  372. // We still delegate to handleKeyEvent, so that it can handle
  373. // enabled/disabled state.
  374. handled = menuItem.handleKeyEvent(e);
  375. }
  376. }, this);
  377. }
  378. return handled;
  379. };
  380. /** @override */
  381. goog.ui.Menu.prototype.setHighlightedIndex = function(index) {
  382. goog.ui.Menu.base(this, 'setHighlightedIndex', index);
  383. // Bring the highlighted item into view. This has no effect if the menu is not
  384. // scrollable.
  385. var child = this.getChildAt(index);
  386. if (child) {
  387. goog.style.scrollIntoContainerView(child.getElement(), this.getElement());
  388. }
  389. };
  390. /**
  391. * Decorate menu items located in any descendent node which as been explicitly
  392. * marked as a 'content' node.
  393. * @param {Element} element Element to decorate.
  394. * @protected
  395. */
  396. goog.ui.Menu.prototype.decorateContent = function(element) {
  397. var renderer = this.getRenderer();
  398. var contentElements = this.getDomHelper().getElementsByTagNameAndClass(
  399. goog.dom.TagName.DIV, goog.getCssName(renderer.getCssClass(), 'content'),
  400. element);
  401. // Some versions of IE do not like it when you access this nodeList
  402. // with invalid indices. See
  403. // http://code.google.com/p/closure-library/issues/detail?id=373
  404. var length = contentElements.length;
  405. for (var i = 0; i < length; i++) {
  406. renderer.decorateChildren(this, contentElements[i]);
  407. }
  408. };