blob: 4a3ce84a0470b2732bf44f6d385b380afa81c229 [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @constructor
* @param {!WebInspector.SoftContextMenu=} parentMenu
*/
WebInspector.SoftContextMenu = function(items, parentMenu)
{
this._items = items;
this._parentMenu = parentMenu;
}
WebInspector.SoftContextMenu.prototype = {
/**
* @param {!Event} event
*/
show: function(event)
{
this._x = event.x;
this._y = event.y;
this._time = new Date().getTime();
// Absolutely position menu for iframes.
var absoluteX = event.pageX;
var absoluteY = event.pageY;
var targetElement = event.target;
while (targetElement && window !== targetElement.ownerDocument.defaultView) {
var frameElement = targetElement.ownerDocument.defaultView.frameElement;
absoluteY += frameElement.totalOffsetTop();
absoluteX += frameElement.totalOffsetLeft();
targetElement = frameElement;
}
// Create context menu.
var targetRect;
this._contextMenuElement = document.createElement("div");
this._contextMenuElement.className = "soft-context-menu";
this._contextMenuElement.tabIndex = 0;
this._contextMenuElement.style.top = absoluteY + "px";
this._contextMenuElement.style.left = absoluteX + "px";
this._contextMenuElement.addEventListener("mouseup", consumeEvent, false);
this._contextMenuElement.addEventListener("keydown", this._menuKeyDown.bind(this), false);
for (var i = 0; i < this._items.length; ++i)
this._contextMenuElement.appendChild(this._createMenuItem(this._items[i]));
// Install glass pane capturing events.
if (!this._parentMenu) {
this._glassPaneElement = document.createElement("div");
this._glassPaneElement.className = "soft-context-menu-glass-pane";
this._glassPaneElement.tabIndex = 0;
this._glassPaneElement.addEventListener("mouseup", this._glassPaneMouseUp.bind(this), false);
this._glassPaneElement.appendChild(this._contextMenuElement);
document.body.appendChild(this._glassPaneElement);
this._focus();
} else
this._parentMenu._parentGlassPaneElement().appendChild(this._contextMenuElement);
// Re-position menu in case it does not fit.
if (document.body.offsetWidth < this._contextMenuElement.offsetLeft + this._contextMenuElement.offsetWidth)
this._contextMenuElement.style.left = (absoluteX - this._contextMenuElement.offsetWidth) + "px";
if (document.body.offsetHeight < this._contextMenuElement.offsetTop + this._contextMenuElement.offsetHeight)
this._contextMenuElement.style.top = (document.body.offsetHeight - this._contextMenuElement.offsetHeight) + "px";
event.consume(true);
},
_parentGlassPaneElement: function()
{
if (this._glassPaneElement)
return this._glassPaneElement;
if (this._parentMenu)
return this._parentMenu._parentGlassPaneElement();
return null;
},
_createMenuItem: function(item)
{
if (item.type === "separator")
return this._createSeparator();
if (item.type === "subMenu")
return this._createSubMenu(item);
var menuItemElement = document.createElement("div");
menuItemElement.className = "soft-context-menu-item";
var checkMarkElement = document.createElement("span");
checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
checkMarkElement.className = "soft-context-menu-item-checkmark";
if (!item.checked)
checkMarkElement.style.opacity = "0";
menuItemElement.appendChild(checkMarkElement);
menuItemElement.appendChild(document.createTextNode(item.label));
menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
// Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);
menuItemElement._actionId = item.id;
return menuItemElement;
},
_createSubMenu: function(item)
{
var menuItemElement = document.createElement("div");
menuItemElement.className = "soft-context-menu-item";
menuItemElement._subItems = item.subItems;
// Occupy the same space on the left in all items.
var checkMarkElement = document.createElement("span");
checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
checkMarkElement.className = "soft-context-menu-item-checkmark";
checkMarkElement.style.opacity = "0";
menuItemElement.appendChild(checkMarkElement);
var subMenuArrowElement = document.createElement("span");
subMenuArrowElement.textContent = "\u25B6"; // BLACK RIGHT-POINTING TRIANGLE
subMenuArrowElement.className = "soft-context-menu-item-submenu-arrow";
menuItemElement.appendChild(document.createTextNode(item.label));
menuItemElement.appendChild(subMenuArrowElement);
menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
// Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);
return menuItemElement;
},
_createSeparator: function()
{
var separatorElement = document.createElement("div");
separatorElement.className = "soft-context-menu-separator";
separatorElement._isSeparator = true;
separatorElement.addEventListener("mouseover", this._hideSubMenu.bind(this), false);
separatorElement.createChild("div", "separator-line");
return separatorElement;
},
_menuItemMouseDown: function(event)
{
// Do not let separator's mouse down hit menu's handler - we need to receive mouse up!
event.consume(true);
},
_menuItemMouseUp: function(event)
{
this._triggerAction(event.target, event);
event.consume();
},
_focus: function()
{
this._contextMenuElement.focus();
},
_triggerAction: function(menuItemElement, event)
{
if (!menuItemElement._subItems) {
this._discardMenu(true, event);
if (typeof menuItemElement._actionId !== "undefined") {
WebInspector.contextMenuItemSelected(menuItemElement._actionId);
delete menuItemElement._actionId;
}
return;
}
this._showSubMenu(menuItemElement, event);
event.consume();
},
_showSubMenu: function(menuItemElement, event)
{
if (menuItemElement._subMenuTimer) {
clearTimeout(menuItemElement._subMenuTimer);
delete menuItemElement._subMenuTimer;
}
if (this._subMenu)
return;
this._subMenu = new WebInspector.SoftContextMenu(menuItemElement._subItems, this);
this._subMenu.show(this._buildMouseEventForSubMenu(menuItemElement));
},
_buildMouseEventForSubMenu: function(subMenuItemElement)
{
var subMenuOffset = { x: subMenuItemElement.offsetWidth - 3, y: subMenuItemElement.offsetTop - 1 };
var targetX = this._x + subMenuOffset.x;
var targetY = this._y + subMenuOffset.y;
var targetPageX = parseInt(this._contextMenuElement.style.left, 10) + subMenuOffset.x;
var targetPageY = parseInt(this._contextMenuElement.style.top, 10) + subMenuOffset.y;
return { x: targetX, y: targetY, pageX: targetPageX, pageY: targetPageY, consume: function() {} };
},
_hideSubMenu: function()
{
if (!this._subMenu)
return;
this._subMenu._discardSubMenus();
this._focus();
},
_menuItemMouseOver: function(event)
{
this._highlightMenuItem(event.target);
},
_menuItemMouseOut: function(event)
{
if (!this._subMenu || !event.relatedTarget) {
this._highlightMenuItem(null);
return;
}
var relatedTarget = event.relatedTarget;
if (this._contextMenuElement.isSelfOrAncestor(relatedTarget) || relatedTarget.classList.contains("soft-context-menu-glass-pane"))
this._highlightMenuItem(null);
},
_highlightMenuItem: function(menuItemElement)
{
if (this._highlightedMenuItemElement === menuItemElement)
return;
this._hideSubMenu();
if (this._highlightedMenuItemElement) {
this._highlightedMenuItemElement.classList.remove("soft-context-menu-item-mouse-over");
if (this._highlightedMenuItemElement._subItems && this._highlightedMenuItemElement._subMenuTimer) {
clearTimeout(this._highlightedMenuItemElement._subMenuTimer);
delete this._highlightedMenuItemElement._subMenuTimer;
}
}
this._highlightedMenuItemElement = menuItemElement;
if (this._highlightedMenuItemElement) {
this._highlightedMenuItemElement.classList.add("soft-context-menu-item-mouse-over");
this._contextMenuElement.focus();
if (this._highlightedMenuItemElement._subItems && !this._highlightedMenuItemElement._subMenuTimer)
this._highlightedMenuItemElement._subMenuTimer = setTimeout(this._showSubMenu.bind(this, this._highlightedMenuItemElement, this._buildMouseEventForSubMenu(this._highlightedMenuItemElement)), 150);
}
},
_highlightPrevious: function()
{
var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.previousSibling : this._contextMenuElement.lastChild;
while (menuItemElement && menuItemElement._isSeparator)
menuItemElement = menuItemElement.previousSibling;
if (menuItemElement)
this._highlightMenuItem(menuItemElement);
},
_highlightNext: function()
{
var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.nextSibling : this._contextMenuElement.firstChild;
while (menuItemElement && menuItemElement._isSeparator)
menuItemElement = menuItemElement.nextSibling;
if (menuItemElement)
this._highlightMenuItem(menuItemElement);
},
_menuKeyDown: function(event)
{
switch (event.keyIdentifier) {
case "Up":
this._highlightPrevious(); break;
case "Down":
this._highlightNext(); break;
case "Left":
if (this._parentMenu) {
this._highlightMenuItem(null);
this._parentMenu._focus();
}
break;
case "Right":
if (!this._highlightedMenuItemElement)
break;
if (this._highlightedMenuItemElement._subItems) {
this._showSubMenu(this._highlightedMenuItemElement, this._buildMouseEventForSubMenu(this._highlightedMenuItemElement));
this._subMenu._focus();
this._subMenu._highlightNext();
}
break;
case "U+001B": // Escape
this._discardMenu(true, event); break;
case "Enter":
if (!isEnterKey(event))
break;
// Fall through
case "U+0020": // Space
if (this._highlightedMenuItemElement)
this._triggerAction(this._highlightedMenuItemElement, event);
break;
}
event.consume(true);
},
_glassPaneMouseUp: function(event)
{
// Return if this is simple 'click', since dispatched on glass pane, can't use 'click' event.
if (event.x === this._x && event.y === this._y && new Date().getTime() - this._time < 300)
return;
this._discardMenu(true, event);
event.consume();
},
/**
* @param {boolean} closeParentMenus
* @param {!Event=} event
*/
_discardMenu: function(closeParentMenus, event)
{
if (this._subMenu && !closeParentMenus)
return;
if (this._glassPaneElement) {
var glassPane = this._glassPaneElement;
delete this._glassPaneElement;
// This can re-enter discardMenu due to blur.
document.body.removeChild(glassPane);
if (this._parentMenu) {
delete this._parentMenu._subMenu;
if (closeParentMenus)
this._parentMenu._discardMenu(closeParentMenus, event);
}
if (event)
event.consume(true);
} else if (this._parentMenu && this._contextMenuElement.parentElement) {
this._discardSubMenus();
if (closeParentMenus)
this._parentMenu._discardMenu(closeParentMenus, event);
if (event)
event.consume(true);
}
},
_discardSubMenus: function()
{
if (this._subMenu)
this._subMenu._discardSubMenus();
this._contextMenuElement.remove();
if (this._parentMenu)
delete this._parentMenu._subMenu;
}
}
if (!InspectorFrontendHost.showContextMenu) {
InspectorFrontendHost.showContextMenu = function(event, items)
{
new WebInspector.SoftContextMenu(items).show(event);
}
}