558 lines
17 KiB
JavaScript
558 lines
17 KiB
JavaScript
// 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.EventType');
|
|
goog.require('goog.positioning.AnchoredViewportPosition');
|
|
goog.require('goog.positioning.Corner');
|
|
goog.require('goog.positioning.MenuAnchoredPosition');
|
|
goog.require('goog.positioning.ViewportClientPosition');
|
|
goog.require('goog.structs');
|
|
goog.require('goog.structs.Map');
|
|
goog.require('goog.style');
|
|
goog.require('goog.ui.Component.EventType');
|
|
goog.require('goog.ui.Menu');
|
|
goog.require('goog.ui.PopupBase');
|
|
goog.require('goog.userAgent');
|
|
|
|
|
|
|
|
/**
|
|
* 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);
|
|
|
|
|
|
/**
|
|
* 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);
|
|
|
|
goog.structs.forEach(this.targets_, 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);
|
|
|
|
// Webkit doesn't fire a mousedown event when opening the context menu,
|
|
// but we need one to update menu visibility properly. So in Safari handle
|
|
// contextmenu mouse events like mousedown.
|
|
// {@link http://bugs.webkit.org/show_bug.cgi?id=6595}
|
|
if (goog.userAgent.WEBKIT) {
|
|
handler.listen(this.getDomHelper().getDocument(),
|
|
goog.events.EventType.CONTEXTMENU, 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);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* 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 an event listener to a target
|
|
* @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_);
|
|
};
|
|
|
|
|
|
/**
|
|
* 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 event on one of the popup targets
|
|
* @param {goog.events.BrowserEvent} e The browser event.
|
|
* @private
|
|
*/
|
|
goog.ui.PopupMenu.prototype.onTargetClick_ = 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,
|
|
/** @type {number} */ (e.clientX),
|
|
/** @type {number} */ (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 loosing 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_;
|
|
}
|
|
};
|