blob: 79304c3b102e6097436452ba84f69deda550b931 [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.
*/
/**
* @unrestricted
*/
UI.SoftContextMenu = class {
/**
* @param {!Array.<!InspectorFrontendHostAPI.ContextMenuDescriptor>} items
* @param {function(string)} itemSelectedCallback
* @param {!UI.SoftContextMenu=} parentMenu
*/
constructor(items, itemSelectedCallback, parentMenu) {
this._items = items;
this._itemSelectedCallback = itemSelectedCallback;
this._parentMenu = parentMenu;
}
/**
* @param {!Document} document
* @param {!AnchorBox} anchorBox
*/
show(document, anchorBox) {
if (!this._items.length)
return;
this._document = document;
this._glassPane = new UI.GlassPane();
this._glassPane.setPointerEventsBehavior(
this._parentMenu ? UI.GlassPane.PointerEventsBehavior.PierceGlassPane :
UI.GlassPane.PointerEventsBehavior.BlockedByGlassPane);
this._glassPane.registerRequiredCSS('ui/softContextMenu.css');
this._glassPane.setContentAnchorBox(anchorBox);
this._glassPane.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
this._glassPane.setMarginBehavior(UI.GlassPane.MarginBehavior.NoMargin);
this._glassPane.setAnchorBehavior(
this._parentMenu ? UI.GlassPane.AnchorBehavior.PreferRight : UI.GlassPane.AnchorBehavior.PreferBottom);
this._contextMenuElement = this._glassPane.contentElement.createChild('div', 'soft-context-menu');
this._contextMenuElement.tabIndex = 0;
this._contextMenuElement.addEventListener('mouseup', e => e.consume(), false);
this._contextMenuElement.addEventListener('keydown', this._menuKeyDown.bind(this), false);
for (let i = 0; i < this._items.length; ++i)
this._contextMenuElement.appendChild(this._createMenuItem(this._items[i]));
this._glassPane.show(document);
this._focusRestorer = new UI.ElementFocusRestorer(this._contextMenuElement);
if (!this._parentMenu) {
this._hideOnUserGesture = event => {
this.discard();
event.consume(true);
};
this._document.body.addEventListener('mousedown', this._hideOnUserGesture, false);
this._document.defaultView.addEventListener('resize', this._hideOnUserGesture, false);
}
}
discard() {
if (this._subMenu)
this._subMenu.discard();
if (this._focusRestorer)
this._focusRestorer.restore();
if (this._glassPane) {
this._glassPane.hide();
delete this._glassPane;
if (this._hideOnUserGesture) {
this._document.body.removeEventListener('mousedown', this._hideOnUserGesture, false);
this._document.defaultView.removeEventListener('resize', this._hideOnUserGesture, false);
delete this._hideOnUserGesture;
}
}
if (this._parentMenu)
delete this._parentMenu._subMenu;
}
_createMenuItem(item) {
if (item.type === 'separator')
return this._createSeparator();
if (item.type === 'subMenu')
return this._createSubMenu(item);
const menuItemElement = createElementWithClass('div', 'soft-context-menu-item');
const checkMarkElement = UI.Icon.create('smallicon-checkmark', 'checkmark');
menuItemElement.appendChild(checkMarkElement);
if (!item.checked)
checkMarkElement.style.opacity = '0';
if (item.element) {
const 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(item) {
const menuItemElement = createElementWithClass('div', 'soft-context-menu-item');
menuItemElement._subItems = item.subItems;
// Occupy the same space on the left in all items.
const checkMarkElement = UI.Icon.create('smallicon-checkmark', 'soft-context-menu-item-checkmark');
checkMarkElement.classList.add('checkmark');
menuItemElement.appendChild(checkMarkElement);
checkMarkElement.style.opacity = '0';
menuItemElement.createTextChild(item.label);
const 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() {
const separatorElement = createElementWithClass('div', 'soft-context-menu-separator');
separatorElement._isSeparator = true;
separatorElement.createChild('div', 'separator-line');
return separatorElement;
}
_menuItemMouseDown(event) {
// Do not let separator's mouse down hit menu's handler - we need to receive mouse up!
event.consume(true);
}
_menuItemMouseUp(event) {
this._triggerAction(event.target, event);
event.consume();
}
/**
* @return {!UI.SoftContextMenu}
*/
_root() {
let root = this;
while (root._parentMenu)
root = root._parentMenu;
return root;
}
_triggerAction(menuItemElement, event) {
if (!menuItemElement._subItems) {
this._root().discard();
event.consume(true);
if (typeof menuItemElement._actionId !== 'undefined') {
this._itemSelectedCallback(menuItemElement._actionId);
delete menuItemElement._actionId;
}
return;
}
this._showSubMenu(menuItemElement);
event.consume();
}
_showSubMenu(menuItemElement) {
if (menuItemElement._subMenuTimer) {
clearTimeout(menuItemElement._subMenuTimer);
delete menuItemElement._subMenuTimer;
}
if (this._subMenu)
return;
this._subMenu = new UI.SoftContextMenu(menuItemElement._subItems, this._itemSelectedCallback, this);
const anchorBox = menuItemElement.boxInWindow();
// Adjust for padding.
anchorBox.y -= 5;
anchorBox.x += 3;
anchorBox.width -= 6;
anchorBox.height += 10;
this._subMenu.show(this._document, anchorBox);
}
_menuItemMouseOver(event) {
this._highlightMenuItem(event.target, true);
}
_menuItemMouseLeave(event) {
if (!this._subMenu || !event.relatedTarget) {
this._highlightMenuItem(null, true);
return;
}
const relatedTarget = event.relatedTarget;
if (relatedTarget === this._contextMenuElement)
this._highlightMenuItem(null, true);
}
/**
* @param {?Element} menuItemElement
* @param {boolean} scheduleSubMenu
*/
_highlightMenuItem(menuItemElement, scheduleSubMenu) {
if (this._highlightedMenuItemElement === menuItemElement)
return;
if (this._subMenu)
this._subMenu.discard();
if (this._highlightedMenuItemElement) {
this._highlightedMenuItemElement.classList.remove('force-white-icons');
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('force-white-icons');
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() {
let 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() {
let 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(event) {
switch (event.key) {
case 'ArrowUp':
this._highlightPrevious();
break;
case 'ArrowDown':
this._highlightNext();
break;
case 'ArrowLeft':
if (this._parentMenu) {
this._highlightMenuItem(null, false);
this.discard();
}
break;
case 'ArrowRight':
if (!this._highlightedMenuItemElement)
break;
if (this._highlightedMenuItemElement._subItems) {
this._showSubMenu(this._highlightedMenuItemElement);
this._subMenu._highlightNext();
}
break;
case 'Escape':
this.discard();
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._highlightNext();
break;
}
event.consume(true);
}
};