123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637 |
- // Copyright 2007 The Closure Library Authors. All Rights Reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS-IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- /**
- * @fileoverview A menu class for showing popups. A single popup can be
- * attached to multiple anchor points. The menu will try to reposition itself
- * if it goes outside the viewport.
- *
- * Decoration is the same as goog.ui.Menu except that the outer DIV can have a
- * 'for' property, which is the ID of the element which triggers the popup.
- *
- * Decorate Example:
- * <button id="dButton">Decorated Popup</button>
- * <div id="dMenu" for="dButton" class="goog-menu">
- * <div class="goog-menuitem">A a</div>
- * <div class="goog-menuitem">B b</div>
- * <div class="goog-menuitem">C c</div>
- * <div class="goog-menuitem">D d</div>
- * <div class="goog-menuitem">E e</div>
- * <div class="goog-menuitem">F f</div>
- * </div>
- *
- * TESTED=FireFox 2.0, IE6, Opera 9, Chrome.
- * TODO(user): Key handling is flakey in Opera and Chrome
- *
- * @see ../demos/popupmenu.html
- */
- goog.provide('goog.ui.PopupMenu');
- goog.require('goog.events');
- goog.require('goog.events.BrowserEvent');
- goog.require('goog.events.EventType');
- goog.require('goog.events.KeyCodes');
- goog.require('goog.positioning.AnchoredViewportPosition');
- goog.require('goog.positioning.Corner');
- goog.require('goog.positioning.MenuAnchoredPosition');
- goog.require('goog.positioning.Overflow');
- goog.require('goog.positioning.ViewportClientPosition');
- goog.require('goog.structs.Map');
- goog.require('goog.style');
- goog.require('goog.ui.Component');
- goog.require('goog.ui.Menu');
- goog.require('goog.ui.PopupBase');
- /**
- * A basic menu class.
- * @param {?goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
- * @param {?goog.ui.MenuRenderer=} opt_renderer Renderer used to render or
- * decorate the container; defaults to {@link goog.ui.MenuRenderer}.
- * @extends {goog.ui.Menu}
- * @constructor
- */
- goog.ui.PopupMenu = function(opt_domHelper, opt_renderer) {
- goog.ui.Menu.call(this, opt_domHelper, opt_renderer);
- this.setAllowAutoFocus(true);
- // Popup menus are hidden by default.
- this.setVisible(false, true);
- /**
- * Map of attachment points for the menu. Key -> Object
- * @type {!goog.structs.Map}
- * @private
- */
- this.targets_ = new goog.structs.Map();
- };
- goog.inherits(goog.ui.PopupMenu, goog.ui.Menu);
- goog.tagUnsealableClass(goog.ui.PopupMenu);
- /**
- * If true, then if the menu will toggle off if it is already visible.
- * @type {boolean}
- * @private
- */
- goog.ui.PopupMenu.prototype.toggleMode_ = false;
- /**
- * Time that the menu was last shown.
- * @type {number}
- * @private
- */
- goog.ui.PopupMenu.prototype.lastHide_ = 0;
- /**
- * Current element where the popup menu is anchored.
- * @type {?Element}
- * @private
- */
- goog.ui.PopupMenu.prototype.currentAnchor_ = null;
- /**
- * Decorate an existing HTML structure with the menu. Menu items will be
- * constructed from elements with classname 'goog-menuitem', separators will be
- * made from HR elements.
- * @param {?Element} element Element to decorate.
- * @override
- */
- goog.ui.PopupMenu.prototype.decorateInternal = function(element) {
- goog.ui.PopupMenu.superClass_.decorateInternal.call(this, element);
- // 'for' is a custom attribute for attaching the menu to a click target
- var htmlFor = element.getAttribute('for') || element.htmlFor;
- if (htmlFor) {
- this.attach(
- this.getDomHelper().getElement(htmlFor),
- goog.positioning.Corner.BOTTOM_LEFT);
- }
- };
- /** @override */
- goog.ui.PopupMenu.prototype.enterDocument = function() {
- goog.ui.PopupMenu.superClass_.enterDocument.call(this);
- this.targets_.forEach(this.attachEvent_, this);
- var handler = this.getHandler();
- handler.listen(this, goog.ui.Component.EventType.ACTION, this.onAction_);
- handler.listen(
- this.getDomHelper().getDocument(), goog.events.EventType.MOUSEDOWN,
- this.onDocClick, true);
- };
- /**
- * Attaches the menu to a new popup position and anchor element. A menu can
- * only be attached to an element once, since attaching the same menu for
- * multiple positions doesn't make sense.
- *
- * @param {?Element} element Element whose click event should trigger the menu.
- * @param {?goog.positioning.Corner=} opt_targetCorner Corner of the target that
- * the menu should be anchored to.
- * @param {goog.positioning.Corner=} opt_menuCorner Corner of the menu that
- * should be anchored.
- * @param {boolean=} opt_contextMenu Whether the menu should show on
- * {@link goog.events.EventType.CONTEXTMENU} events, false if it should
- * show on {@link goog.events.EventType.MOUSEDOWN} events. Default is
- * MOUSEDOWN.
- * @param {?goog.math.Box=} opt_margin Margin for the popup used in positioning
- * algorithms.
- */
- goog.ui.PopupMenu.prototype.attach = function(
- element, opt_targetCorner, opt_menuCorner, opt_contextMenu, opt_margin) {
- if (this.isAttachTarget(element)) {
- // Already in the popup, so just return.
- return;
- }
- var target = this.createAttachTarget(
- element, opt_targetCorner, opt_menuCorner, opt_contextMenu, opt_margin);
- if (this.isInDocument()) {
- this.attachEvent_(target);
- }
- // Add a listener for keyboard actions on the menu.
- var handler = goog.partial(this.onMenuKeyboardAction_, element);
- if (this.getElement()) {
- this.getHandler().listen(
- this.getElement(), goog.events.EventType.KEYDOWN, handler);
- }
- };
- /**
- * Handles keyboard actions on the PopupMenu, according to
- * http://www.w3.org/WAI/PF/aria-practices/#menubutton.
- *
- * <p>If the ESC key is pressed, the menu is hidden (which is handled by
- * this.onAction_), and the focus is returned to the element whose click event
- * triggered opening of the menu.
- *
- * <p>If the SPACE or ENTER keys are pressed, the highlighted menu item's
- * listeners are fired.
- *
- * @param {Element} element Element whose click event triggered the menu.
- * @param {!goog.events.BrowserEvent} e The key down event.
- * @private
- */
- goog.ui.PopupMenu.prototype.onMenuKeyboardAction_ = function(element, e) {
- if (e.keyCode == goog.events.KeyCodes.ESC) {
- element.focus();
- return;
- }
- var highlightedItem = this.getChildAt(this.getHighlightedIndex());
- if (!highlightedItem) {
- return;
- }
- var targetElement = highlightedItem.getElement();
- // Create an event to pass to the menu item's listener.
- var event = new goog.events.BrowserEvent(e.getBrowserEvent(), targetElement);
- event.target = targetElement;
- // If an item is highlighted and the user presses the SPACE/ENTER key, the
- // event target is the menu rather than the menu item, so we manually fire
- // the listener of the correct menu item.
- if (e.keyCode == goog.events.KeyCodes.SPACE ||
- e.keyCode == goog.events.KeyCodes.ENTER) {
- goog.events.fireListeners(
- targetElement, goog.events.EventType.KEYDOWN, false, event);
- }
- // After activating a menu item the PopupMenu should be hidden (already
- // implemented in this.onAction_ for ENTER/MOUSEDOWN).
- if (e.keyCode == goog.events.KeyCodes.SPACE) {
- this.hide();
- }
- };
- /**
- * Creates an object describing how the popup menu should be attached to the
- * anchoring element based on the given parameters. The created object is
- * stored, keyed by {@code element} and is retrievable later by invoking
- * {@link #getAttachTarget(element)} at a later point.
- *
- * Subclass may add more properties to the returned object, as needed.
- *
- * @param {?Element} element Element whose click event should trigger the menu.
- * @param {?goog.positioning.Corner=} opt_targetCorner Corner of the target that
- * the menu should be anchored to.
- * @param {?goog.positioning.Corner=} opt_menuCorner Corner of the menu that
- * should be anchored.
- * @param {boolean=} opt_contextMenu Whether the menu should show on
- * {@link goog.events.EventType.CONTEXTMENU} events, false if it should
- * show on {@link goog.events.EventType.MOUSEDOWN} events. Default is
- * MOUSEDOWN.
- * @param {?goog.math.Box=} opt_margin Margin for the popup used in positioning
- * algorithms.
- *
- * @return {?Object} An object that describes how the popup menu should be
- * attached to the anchoring element.
- *
- * @protected
- */
- goog.ui.PopupMenu.prototype.createAttachTarget = function(
- element, opt_targetCorner, opt_menuCorner, opt_contextMenu, opt_margin) {
- if (!element) {
- return null;
- }
- var target = {
- element_: element,
- targetCorner_: opt_targetCorner,
- menuCorner_: opt_menuCorner,
- eventType_: opt_contextMenu ? goog.events.EventType.CONTEXTMENU :
- goog.events.EventType.MOUSEDOWN,
- margin_: opt_margin
- };
- this.targets_.set(goog.getUid(element), target);
- return target;
- };
- /**
- * Returns the object describing how the popup menu should be attach to given
- * element or {@code null}. The object is created and the association is formed
- * when {@link #attach} is invoked.
- *
- * @param {?Element} element DOM element.
- * @return {?Object} The object created when {@link attach} is invoked on
- * {@code element}. Returns {@code null} if the element does not trigger
- * the menu (i.e. {@link attach} has never been invoked on
- * {@code element}).
- * @protected
- */
- goog.ui.PopupMenu.prototype.getAttachTarget = function(element) {
- return element ?
- /** @type {?Object} */ (this.targets_.get(goog.getUid(element))) :
- null;
- };
- /**
- * @param {?Element} element Any DOM element.
- * @return {boolean} Whether clicking on the given element will trigger the
- * menu.
- *
- * @protected
- */
- goog.ui.PopupMenu.prototype.isAttachTarget = function(element) {
- return element ? this.targets_.containsKey(goog.getUid(element)) : false;
- };
- /**
- * @return {?Element} The current element where the popup is anchored, if it's
- * visible.
- */
- goog.ui.PopupMenu.prototype.getAttachedElement = function() {
- return this.currentAnchor_;
- };
- /**
- * Attaches two event listeners to a target. One with corresponding event type,
- * and one with the KEYDOWN event type for accessibility purposes.
- * @param {?Object} target The target to attach an event to.
- * @private
- */
- goog.ui.PopupMenu.prototype.attachEvent_ = function(target) {
- this.getHandler().listen(
- target.element_, target.eventType_, this.onTargetClick_);
- if (target.eventType_ != goog.events.EventType.CONTEXTMENU) {
- this.getHandler().listen(
- target.element_, goog.events.EventType.KEYDOWN,
- this.onTargetKeyboardAction_);
- }
- };
- /**
- * Detaches all listeners
- */
- goog.ui.PopupMenu.prototype.detachAll = function() {
- if (this.isInDocument()) {
- var keys = this.targets_.getKeys();
- for (var i = 0; i < keys.length; i++) {
- this.detachEvent_(/** @type {!Object} */ (this.targets_.get(keys[i])));
- }
- }
- this.targets_.clear();
- };
- /**
- * Detaches a menu from a given element.
- * @param {?Element} element Element whose click event should trigger the menu.
- */
- goog.ui.PopupMenu.prototype.detach = function(element) {
- if (!this.isAttachTarget(element)) {
- throw Error('Menu not attached to provided element, unable to detach.');
- }
- var key = goog.getUid(element);
- if (this.isInDocument()) {
- this.detachEvent_(/** @type {!Object} */ (this.targets_.get(key)));
- }
- this.targets_.remove(key);
- };
- /**
- * Detaches an event listener to a target
- * @param {!Object} target The target to detach events from.
- * @private
- */
- goog.ui.PopupMenu.prototype.detachEvent_ = function(target) {
- this.getHandler().unlisten(
- target.element_, target.eventType_, this.onTargetClick_);
- };
- /**
- * Sets whether the menu should toggle if it is already open. For context
- * menus this should be false, for toolbar menus it makes more sense to be true.
- * @param {boolean} toggle The new toggle mode.
- */
- goog.ui.PopupMenu.prototype.setToggleMode = function(toggle) {
- this.toggleMode_ = toggle;
- };
- /**
- * Gets whether the menu is in toggle mode
- * @return {boolean} toggle.
- */
- goog.ui.PopupMenu.prototype.getToggleMode = function() {
- return this.toggleMode_;
- };
- /**
- * Show the menu using given positioning object.
- * @param {?goog.positioning.AbstractPosition} position The positioning
- * instance.
- * @param {goog.positioning.Corner=} opt_menuCorner The corner of the menu to be
- * positioned.
- * @param {?goog.math.Box=} opt_margin A margin specified in pixels.
- * @param {?Element=} opt_anchor The element which acts as visual anchor for
- * this menu.
- */
- goog.ui.PopupMenu.prototype.showWithPosition = function(
- position, opt_menuCorner, opt_margin, opt_anchor) {
- var isVisible = this.isVisible();
- if (this.isOrWasRecentlyVisible() && this.toggleMode_) {
- this.hide();
- return;
- }
- // Set current anchor before dispatching BEFORE_SHOW. This is typically useful
- // when we would need to make modifications based on the current anchor to the
- // menu just before displaying it.
- this.currentAnchor_ = opt_anchor || null;
- // Notify event handlers that the menu is about to be shown.
- if (!this.dispatchEvent(goog.ui.Component.EventType.BEFORE_SHOW)) {
- return;
- }
- var menuCorner = typeof opt_menuCorner != 'undefined' ?
- opt_menuCorner :
- goog.positioning.Corner.TOP_START;
- // This is a little hacky so that we can position the menu with minimal
- // flicker.
- if (!isVisible) {
- // On IE, setting visibility = 'hidden' on a visible menu
- // will cause a blur, forcing the menu to close immediately.
- this.getElement().style.visibility = 'hidden';
- }
- goog.style.setElementShown(this.getElement(), true);
- position.reposition(this.getElement(), menuCorner, opt_margin);
- if (!isVisible) {
- this.getElement().style.visibility = 'visible';
- }
- this.setHighlightedIndex(-1);
- // setVisible dispatches a goog.ui.Component.EventType.SHOW event, which may
- // be canceled to prevent the menu from being shown.
- this.setVisible(true);
- };
- /**
- * Show the menu at a given attached target.
- * @param {!Object} target Popup target.
- * @param {number} x The client-X associated with the show event.
- * @param {number} y The client-Y associated with the show event.
- * @protected
- */
- goog.ui.PopupMenu.prototype.showMenu = function(target, x, y) {
- var position = goog.isDef(target.targetCorner_) ?
- new goog.positioning.AnchoredViewportPosition(
- target.element_, target.targetCorner_, true) :
- new goog.positioning.ViewportClientPosition(x, y);
- if (position.setLastResortOverflow) {
- // This is a ViewportClientPosition, so we can set the overflow policy.
- // Allow the menu to slide from the corner rather than clipping if it is
- // completely impossible to fit it otherwise.
- position.setLastResortOverflow(
- goog.positioning.Overflow.ADJUST_X |
- goog.positioning.Overflow.ADJUST_Y);
- }
- this.showWithPosition(
- position, target.menuCorner_, target.margin_, target.element_);
- };
- /**
- * Shows the menu immediately at the given client coordinates.
- * @param {number} x The client-X associated with the show event.
- * @param {number} y The client-Y associated with the show event.
- * @param {goog.positioning.Corner=} opt_menuCorner Corner of the menu that
- * should be anchored.
- */
- goog.ui.PopupMenu.prototype.showAt = function(x, y, opt_menuCorner) {
- this.showWithPosition(
- new goog.positioning.ViewportClientPosition(x, y), opt_menuCorner);
- };
- /**
- * Shows the menu immediately attached to the given element
- * @param {?Element} element The element to show at.
- * @param {goog.positioning.Corner} targetCorner The corner of the target to
- * anchor to.
- * @param {goog.positioning.Corner=} opt_menuCorner Corner of the menu that
- * should be anchored.
- */
- goog.ui.PopupMenu.prototype.showAtElement = function(
- element, targetCorner, opt_menuCorner) {
- this.showWithPosition(
- new goog.positioning.MenuAnchoredPosition(element, targetCorner, true),
- opt_menuCorner, null, element);
- };
- /**
- * Hides the menu.
- */
- goog.ui.PopupMenu.prototype.hide = function() {
- if (!this.isVisible()) {
- return;
- }
- // setVisible dispatches a goog.ui.Component.EventType.HIDE event, which may
- // be canceled to prevent the menu from being hidden.
- this.setVisible(false);
- if (!this.isVisible()) {
- // HIDE event wasn't canceled; the menu is now hidden.
- this.lastHide_ = goog.now();
- this.currentAnchor_ = null;
- }
- };
- /**
- * Returns whether the menu is currently visible or was visible within about
- * 150 ms ago. This stops the menu toggling back on if the toggleMode == false.
- * @return {boolean} Whether the popup is currently visible or was visible
- * within about 150 ms ago.
- */
- goog.ui.PopupMenu.prototype.isOrWasRecentlyVisible = function() {
- return this.isVisible() || this.wasRecentlyHidden();
- };
- /**
- * Used to stop the menu toggling back on if the toggleMode == false.
- * @return {boolean} Whether the menu was recently hidden.
- * @protected
- */
- goog.ui.PopupMenu.prototype.wasRecentlyHidden = function() {
- return goog.now() - this.lastHide_ < goog.ui.PopupBase.DEBOUNCE_DELAY_MS;
- };
- /**
- * Dismiss the popup menu when an action fires.
- * @param {?goog.events.Event=} opt_e The optional event.
- * @private
- */
- goog.ui.PopupMenu.prototype.onAction_ = function(opt_e) {
- this.hide();
- };
- /**
- * Handles a browser click event on one of the popup targets.
- * @param {?goog.events.BrowserEvent} e The browser event.
- * @private
- */
- goog.ui.PopupMenu.prototype.onTargetClick_ = function(e) {
- this.onTargetActivation_(e);
- };
- /**
- * Handles a KEYDOWN browser event on one of the popup targets.
- * @param {!goog.events.BrowserEvent} e The browser event.
- * @private
- */
- goog.ui.PopupMenu.prototype.onTargetKeyboardAction_ = function(e) {
- if (e.keyCode == goog.events.KeyCodes.SPACE ||
- e.keyCode == goog.events.KeyCodes.ENTER ||
- e.keyCode == goog.events.KeyCodes.DOWN) {
- this.onTargetActivation_(e);
- }
- // If the popupmenu is opened using the DOWN key, the focus should be on the
- // first menu item.
- if (e.keyCode == goog.events.KeyCodes.DOWN) {
- this.highlightFirst();
- }
- };
- /**
- * Handles a browser event on one of the popup targets.
- * @param {?goog.events.BrowserEvent} e The browser event.
- * @private
- */
- goog.ui.PopupMenu.prototype.onTargetActivation_ = function(e) {
- var keys = this.targets_.getKeys();
- for (var i = 0; i < keys.length; i++) {
- var target = /** @type {!Object} */ (this.targets_.get(keys[i]));
- if (target.element_ == e.currentTarget) {
- this.showMenu(target, (e.clientX), (e.clientY));
- e.preventDefault();
- e.stopPropagation();
- return;
- }
- }
- };
- /**
- * Handles click events that propagate to the document.
- * @param {!goog.events.BrowserEvent} e The browser event.
- * @protected
- */
- goog.ui.PopupMenu.prototype.onDocClick = function(e) {
- if (this.isVisible() &&
- !this.containsElement(/** @type {!Element} */ (e.target))) {
- this.hide();
- }
- };
- /**
- * Handles the key event target losing focus.
- * @param {?goog.events.BrowserEvent} e The browser event.
- * @protected
- * @override
- */
- goog.ui.PopupMenu.prototype.handleBlur = function(e) {
- goog.ui.PopupMenu.superClass_.handleBlur.call(this, e);
- this.hide();
- };
- /** @override */
- goog.ui.PopupMenu.prototype.disposeInternal = function() {
- // Always call the superclass' disposeInternal() first (Bug 715885).
- goog.ui.PopupMenu.superClass_.disposeInternal.call(this);
- // Disposes of the attachment target map.
- if (this.targets_) {
- this.targets_.clear();
- delete this.targets_;
- }
- };
|