blob: 97ac10947eddf204891f39821df4a71c1f50a924 [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
*/
export default class SoftContextMenu {
/**
* @param {!Array.<!InspectorFrontendHostAPI.ContextMenuDescriptor>} items
* @param {function(string)} itemSelectedCallback
* @param {!SoftContextMenu=} parentMenu
*/
constructor(items, itemSelectedCallback, parentMenu) {
this._items = items;
this._itemSelectedCallback = itemSelectedCallback;
this._parentMenu = parentMenu;
/** @type {?Element} */
this._highlightedMenuItemElement = null;
}
/**
* @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 = -1;
UI.ARIAUtils.markAsMenu(this._contextMenuElement);
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 => {
// If a user clicks on any submenu, prevent the menu system from closing.
let subMenu = this._subMenu;
while (subMenu) {
if (subMenu._contextMenuElement === event.path[0]) {
return;
}
subMenu = subMenu._subMenu;
}
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');
menuItemElement.tabIndex = -1;
UI.ARIAUtils.markAsMenuItem(menuItemElement);
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._customElement = item.element;
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;
let accessibleName = item.label;
if (item.type === 'checkbox') {
const checkedState = item.checked ? ls`checked` : ls`unchecked`;
if (item.shortcut) {
accessibleName = ls`${item.label}, ${item.shortcut}, ${checkedState}`;
} else {
accessibleName = ls`${item.label}, ${checkedState}`;
}
} else if (item.shortcut) {
accessibleName = ls`${item.label}, ${item.shortcut}`;
}
UI.ARIAUtils.setAccessibleName(menuItemElement, accessibleName);
return menuItemElement;
}
_createSubMenu(item) {
const menuItemElement = createElementWithClass('div', 'soft-context-menu-item');
menuItemElement._subItems = item.subItems;
menuItemElement.tabIndex = -1;
UI.ARIAUtils.markAsMenuItemSubMenu(menuItemElement);
// 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);
if (Host.isMac() && !UI.themeSupport.hasTheme()) {
const subMenuArrowElement = menuItemElement.createChild('span', 'soft-context-menu-item-submenu-arrow');
subMenuArrowElement.textContent = '\u25B6'; // BLACK RIGHT-POINTING TRIANGLE
} else {
const subMenuArrowElement = UI.Icon.create('smallicon-triangle-right', 'soft-context-menu-item-submenu-arrow');
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('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 {!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 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) {
if (UI.themeSupport.hasTheme() || Host.isMac()) {
this._highlightedMenuItemElement.classList.add('force-white-icons');
}
this._highlightedMenuItemElement.classList.add('soft-context-menu-item-mouse-over');
if (this._highlightedMenuItemElement._customElement) {
this._highlightedMenuItemElement._customElement.focus();
} else {
this._highlightedMenuItemElement.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.classList.contains('soft-context-menu-disabled'))) {
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.classList.contains('soft-context-menu-disabled'))) {
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)) {
return;
}
// Fall through
case ' ': // Space
if (!this._highlightedMenuItemElement || this._highlightedMenuItemElement._customElement) {
return;
}
this._triggerAction(this._highlightedMenuItemElement, event);
if (this._highlightedMenuItemElement._subItems) {
this._subMenu._highlightNext();
}
break;
}
event.consume(true);
}
}
/* Legacy exported object*/
self.UI = self.UI || {};
/* Legacy exported object*/
UI = UI || {};
/** @constructor */
UI.SoftContextMenu = SoftContextMenu;