| /* |
| * 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; |
| } |
| } |