blob: c20310caae8bbb395b72e5c4eab0af05f2267999 [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 {!Array.<!InspectorFrontendHostAPI.ContextMenuDescriptor>} items
* @param {function(string)} itemSelectedCallback
* @param {!WebInspector.SoftContextMenu=} parentMenu
*/
WebInspector.SoftContextMenu = function(items, itemSelectedCallback, parentMenu)
{
this._items = items;
this._itemSelectedCallback = itemSelectedCallback;
this._parentMenu = parentMenu;
}
WebInspector.SoftContextMenu.prototype = {
/**
* @param {!Document} document
* @param {number} x
* @param {number} y
*/
show: function(document, x, y)
{
if (!this._items.length)
return;
this._document = document;
this._x = x;
this._y = y;
this._time = new Date().getTime();
// Create context menu.
this.element = createElementWithClass("div", "soft-context-menu");
var root = WebInspector.createShadowRootWithCoreStyles(this.element, "ui/softContextMenu.css");
this._contextMenuElement = root.createChild("div");
this.element.style.top = y + "px";
var subMenuOverlap = 3;
this.element.style.left = (this._parentMenu ? x - subMenuOverlap : x) + "px";
this._contextMenuElement.tabIndex = 0;
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 = createElementWithClass("div", "soft-context-menu-glass-pane fill");
this._glassPaneElement.tabIndex = 0;
this._glassPaneElement.addEventListener("mouseup", this._glassPaneMouseUp.bind(this), false);
this._glassPaneElement.appendChild(this.element);
document.body.appendChild(this._glassPaneElement);
this._discardMenuOnResizeListener = this._discardMenu.bind(this, true);
document.defaultView.addEventListener("resize", this._discardMenuOnResizeListener, false);
} else {
this._parentMenu._parentGlassPaneElement().appendChild(this.element);
}
// Re-position menu in case it does not fit.
if (document.body.offsetWidth < this.element.offsetLeft + this.element.offsetWidth) {
this.element.style.left = Math.max(WebInspector.Dialog.modalHostView().element.totalOffsetLeft(), this._parentMenu
? this._parentMenu.element.offsetLeft - this.element.offsetWidth + subMenuOverlap
: document.body.offsetWidth - this.element.offsetWidth) + "px";
}
// Move submenus upwards if it does not fit.
if (this._parentMenu && document.body.offsetHeight < this.element.offsetTop + this.element.offsetHeight) {
y = Math.max(WebInspector.Dialog.modalHostView().element.totalOffsetTop(), document.body.offsetHeight - this.element.offsetHeight);
this.element.style.top = y + "px";
}
var maxHeight = WebInspector.Dialog.modalHostView().element.offsetHeight;
maxHeight -= y - WebInspector.Dialog.modalHostView().element.totalOffsetTop();
this.element.style.maxHeight = maxHeight + "px";
this._focus();
},
discard: function()
{
this._discardMenu(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 = createElementWithClass("div", "soft-context-menu-item");
var checkMarkElement = menuItemElement.createChild("div", "checkmark");
if (!item.checked)
checkMarkElement.style.opacity = "0";
if (item.element) {
var wrapper = menuItemElement.createChild("div", "soft-context-menu-custom-item");
wrapper.appendChild(item.element);
menuItemElement._isCustom = true;
return menuItemElement;
}
if (!item.enabled)
menuItemElement.classList.add("soft-context-menu-disabled");
menuItemElement.createTextChild(item.label);
menuItemElement.createChild("span", "soft-context-menu-shortcut").textContent = item.shortcut;
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("mouseleave", this._menuItemMouseLeave.bind(this), false);
menuItemElement._actionId = item.id;
return menuItemElement;
},
_createSubMenu: function(item)
{
var menuItemElement = createElementWithClass("div", "soft-context-menu-item");
menuItemElement._subItems = item.subItems;
// Occupy the same space on the left in all items.
var checkMarkElement = menuItemElement.createChild("span", "soft-context-menu-item-checkmark checkmark");
checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
checkMarkElement.style.opacity = "0";
menuItemElement.createTextChild(item.label);
var subMenuArrowElement = menuItemElement.createChild("span", "soft-context-menu-item-submenu-arrow");
subMenuArrowElement.textContent = "\u25B6"; // BLACK RIGHT-POINTING TRIANGLE
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("mouseleave", this._menuItemMouseLeave.bind(this), false);
return menuItemElement;
},
_createSeparator: function()
{
var separatorElement = createElementWithClass("div", "soft-context-menu-separator");
separatorElement._isSeparator = true;
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") {
this._itemSelectedCallback(menuItemElement._actionId);
delete menuItemElement._actionId;
}
return;
}
this._showSubMenu(menuItemElement);
event.consume();
},
_showSubMenu: function(menuItemElement)
{
if (menuItemElement._subMenuTimer) {
clearTimeout(menuItemElement._subMenuTimer);
delete menuItemElement._subMenuTimer;
}
if (this._subMenu)
return;
this._subMenu = new WebInspector.SoftContextMenu(menuItemElement._subItems, this._itemSelectedCallback, this);
var topPadding = 4;
this._subMenu.show(this._document, menuItemElement.totalOffsetLeft() + menuItemElement.offsetWidth, menuItemElement.totalOffsetTop() - 1 - topPadding);
},
_hideSubMenu: function()
{
if (!this._subMenu)
return;
this._subMenu._discardSubMenus();
this._focus();
},
_menuItemMouseOver: function(event)
{
this._highlightMenuItem(event.target, true);
},
_menuItemMouseLeave: function(event)
{
if (!this._subMenu || !event.relatedTarget) {
this._highlightMenuItem(null, true);
return;
}
var relatedTarget = event.relatedTarget;
if (relatedTarget.classList.contains("soft-context-menu-glass-pane"))
this._highlightMenuItem(null, true);
},
/**
* @param {?Element} menuItemElement
* @param {boolean} scheduleSubMenu
*/
_highlightMenuItem: function(menuItemElement, scheduleSubMenu)
{
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 (scheduleSubMenu && this._highlightedMenuItemElement._subItems && !this._highlightedMenuItemElement._subMenuTimer)
this._highlightedMenuItemElement._subMenuTimer = setTimeout(this._showSubMenu.bind(this, this._highlightedMenuItemElement), 150);
}
},
_highlightPrevious: function()
{
var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.previousSibling : this._contextMenuElement.lastChild;
while (menuItemElement && (menuItemElement._isSeparator || menuItemElement._isCustom))
menuItemElement = menuItemElement.previousSibling;
if (menuItemElement)
this._highlightMenuItem(menuItemElement, false);
},
_highlightNext: function()
{
var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.nextSibling : this._contextMenuElement.firstChild;
while (menuItemElement && (menuItemElement._isSeparator || menuItemElement._isCustom))
menuItemElement = menuItemElement.nextSibling;
if (menuItemElement)
this._highlightMenuItem(menuItemElement, false);
},
_menuKeyDown: function(event)
{
switch (event.key) {
case "ArrowUp":
this._highlightPrevious(); break;
case "ArrowDown":
this._highlightNext(); break;
case "ArrowLeft":
if (this._parentMenu) {
this._highlightMenuItem(null, false);
this._parentMenu._hideSubMenu();
}
break;
case "ArrowRight":
if (!this._highlightedMenuItemElement)
break;
if (this._highlightedMenuItemElement._subItems) {
this._showSubMenu(this._highlightedMenuItemElement);
this._subMenu._focus();
this._subMenu._highlightNext();
}
break;
case "Escape":
this._discardMenu(false, event); break;
case "Enter":
if (!isEnterKey(event))
break;
// Fall through
case " ": // Space
if (this._highlightedMenuItemElement)
this._triggerAction(this._highlightedMenuItemElement, event);
if (this._highlightedMenuItemElement._subItems) {
this._subMenu._focus();
this._subMenu._highlightNext();
}
break;
}
event.consume(true);
},
_glassPaneMouseUp: function(event)
{
// Return if this is simple 'click', since dispatched on glass pane, can't use 'click' event.
if (new Date().getTime() - this._time < 300)
return;
if (event.target === this.element)
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.
this._document.body.removeChild(glassPane);
if (this._parentMenu) {
delete this._parentMenu._subMenu;
if (closeParentMenus)
this._parentMenu._discardMenu(closeParentMenus, event);
else
this._parentMenu._focus();
}
if (event)
event.consume(true);
} else if (this._parentMenu && this._contextMenuElement.parentElementOrShadowHost()) {
this._discardSubMenus();
if (closeParentMenus)
this._parentMenu._discardMenu(closeParentMenus, event);
else
this._parentMenu._focus();
if (event)
event.consume(true);
}
if (this._discardMenuOnResizeListener) {
this._document.defaultView.removeEventListener("resize", this._discardMenuOnResizeListener, false);
delete this._discardMenuOnResizeListener;
}
},
_discardSubMenus: function()
{
if (this._subMenu)
this._subMenu._discardSubMenus();
this.element.remove();
if (this._parentMenu)
delete this._parentMenu._subMenu;
}
}