| /* |
| * Copyright (C) 2011 Google Inc. All Rights Reserved. |
| * Copyright (C) 2017 Sony Interactive Entertainment Inc. |
| * |
| * 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. |
| */ |
| |
| WI.SoftContextMenu = class SoftContextMenu |
| { |
| constructor(items, parentMenu) |
| { |
| this._items = items; |
| this._parentMenu = parentMenu; |
| } |
| |
| // Public |
| |
| show(event) |
| { |
| const isSubMenu = !!this._parentMenu; |
| |
| this._contextMenuElement = document.createElement("div"); |
| this._contextMenuElement.className = "soft-context-menu"; |
| this._contextMenuElement.style.left = event.pageX + "px"; |
| this._contextMenuElement.style.top = event.pageY + "px"; |
| this._contextMenuElement.tabIndex = 0; |
| this._contextMenuElement.addEventListener("keydown", this._menuKeyDown.bind(this), false); |
| for (let item of this._items) |
| this._contextMenuElement.appendChild(this._createMenuItem(item)); |
| |
| // Install glass pane capturing events. |
| if (!isSubMenu) { |
| this._glassPaneElement = document.createElement("div"); |
| this._glassPaneElement.className = "soft-context-menu-glass-pane"; |
| this._glassPaneElement.addEventListener("mousedown", this._glassPaneMouseDown.bind(this), false); |
| this._glassPaneElement.appendChild(this._contextMenuElement); |
| document.body.appendChild(this._glassPaneElement); |
| this._focus(); |
| this._consumeEvent(event, true); |
| } else |
| this._parentGlassPaneElement().appendChild(this._contextMenuElement); |
| |
| this._repositionMenuOnScreen(isSubMenu); |
| } |
| |
| // Private |
| |
| _consumeEvent(event, preventDefault) |
| { |
| event.stopImmediatePropagation(); |
| |
| if (preventDefault) |
| event.preventDefault(); |
| |
| event.handled = true; |
| } |
| |
| _parentGlassPaneElement() |
| { |
| if (!this._parentMenu) |
| return null; |
| |
| if (this._parentMenu._glassPaneElement) |
| return this._parentMenu._glassPaneElement; |
| |
| return this._parentMenu._parentGlassPaneElement(); |
| } |
| |
| _createMenuItem(item) |
| { |
| if (item.type === "separator") |
| return this._createSeparator(); |
| |
| const checkmark = "\u2713"; |
| const blackRightPointingTriangle = "\u25b6"; |
| |
| const menuItemElement = document.createElement("div"); |
| menuItemElement.className = "item"; |
| if (!item.enabled) |
| menuItemElement.classList.add("disabled"); |
| |
| const checkMarkElement = document.createElement("span"); |
| checkMarkElement.textContent = item.checked ? checkmark : ""; |
| checkMarkElement.className = "checkmark"; |
| menuItemElement.appendChild(checkMarkElement); |
| |
| const labelElement = document.createElement("span"); |
| labelElement.textContent = item.label; |
| labelElement.className = "label"; |
| menuItemElement.appendChild(labelElement); |
| |
| if (item.type === "subMenu") { |
| const subMenuArrowElement = document.createElement("span"); |
| subMenuArrowElement.textContent = blackRightPointingTriangle; |
| subMenuArrowElement.className = "submenu-arrow"; |
| menuItemElement.appendChild(subMenuArrowElement); |
| |
| menuItemElement._subItems = item.subItems; |
| } else |
| menuItemElement._actionId = item.id; |
| |
| menuItemElement.addEventListener("contextmenu", this._menuItemContextMenu.bind(this), false); |
| menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false); |
| menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false); |
| menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false); |
| menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false); |
| |
| return menuItemElement; |
| } |
| |
| _createSeparator() |
| { |
| const separatorElement = document.createElement("div"); |
| separatorElement.className = "separator"; |
| separatorElement._isSeparator = true; |
| separatorElement.createChild("div", "line"); |
| |
| separatorElement.addEventListener("contextmenu", this._menuItemContextMenu.bind(this), false); |
| separatorElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false); |
| separatorElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false); |
| separatorElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false); |
| |
| return separatorElement; |
| } |
| |
| _repositionMenuOnScreen(isSubMenu) |
| { |
| if (this._contextMenuElement.offsetLeft + this._contextMenuElement.offsetWidth > document.body.offsetWidth) { |
| if (isSubMenu) { |
| const parentContextMenuElement = this._parentMenu._contextMenuElement; |
| const leftOfParent = parentContextMenuElement.offsetLeft - this._contextMenuElement.offsetWidth + 2; |
| const fromParentRight = this._contextMenuElement.offsetLeft - this._contextMenuElement.offsetWidth + 2; |
| this._contextMenuElement.style.left = (leftOfParent >= 0 ? leftOfParent : fromParentRight) + "px"; |
| } else { |
| const leftOfCursor = this._contextMenuElement.offsetLeft - this._contextMenuElement.offsetWidth; |
| const fromRightEdge = document.body.offsetWidth - this._contextMenuElement.offsetWidth; |
| this._contextMenuElement.style.left = (leftOfCursor >= 0 ? leftOfCursor : fromRightEdge) + "px"; |
| } |
| } |
| |
| if (this._contextMenuElement.offsetTop + this._contextMenuElement.offsetHeight > document.body.offsetHeight) { |
| const aboveCursor = this._contextMenuElement.offsetTop - this._contextMenuElement.offsetHeight; |
| const fromBottomEdge = document.body.offsetHeight - this._contextMenuElement.offsetHeight; |
| this._contextMenuElement.style.top = (!isSubMenu && aboveCursor >= 0 ? aboveCursor : fromBottomEdge) + "px"; |
| } |
| } |
| |
| _showSubMenu(menuItemElement) |
| { |
| if (menuItemElement._subMenuTimer) { |
| clearTimeout(menuItemElement._subMenuTimer); |
| menuItemElement._subMenuTimer = 0; |
| } |
| |
| if (this._subMenu) |
| return; |
| |
| this._subMenu = new WI.SoftContextMenu(menuItemElement._subItems, this); |
| this._subMenu.show({ |
| pageX: this._contextMenuElement.offsetLeft + menuItemElement.offsetWidth, |
| pageY: this._contextMenuElement.offsetTop + menuItemElement.offsetTop - 4 |
| }); |
| } |
| |
| _hideSubMenu() |
| { |
| if (!this._subMenu) |
| return; |
| |
| this._subMenu._discardSubMenus(); |
| this._focus(); |
| } |
| |
| _menuItemContextMenu(event) |
| { |
| // Prevent our non-native context menu from getting a native context menu on right-click. |
| this._consumeEvent(event, true); |
| } |
| |
| _menuItemMouseDown(event) |
| { |
| // Prevent menu from disappearing before mouseup. |
| this._consumeEvent(event, true); |
| } |
| |
| _menuItemMouseUp(event) |
| { |
| this._triggerAction(event.target, event); |
| this._consumeEvent(event); |
| } |
| |
| _menuItemMouseOver(event) |
| { |
| this._highlightMenuItem(event.target._isSeparator ? null : event.target); |
| } |
| |
| _menuItemMouseOut(event) |
| { |
| const shouldUnhighlight = !this._subMenu || !event.relatedTarget || |
| this._contextMenuElement.contains(event.relatedTarget) || |
| event.relatedTarget.classList.contains("soft-context-menu-glass-pane"); |
| |
| if (shouldUnhighlight) |
| this._highlightMenuItem(null); |
| } |
| |
| _menuKeyDown(event) |
| { |
| switch (event.keyIdentifier) { |
| case "Up": |
| this._highlightPrevious(); |
| break; |
| case "Down": |
| this._highlightNext(); |
| break; |
| case "Left": |
| if (this._parentMenu) { |
| this._highlightMenuItem(null); |
| this._parentMenu._hideSubMenu(); |
| } |
| break; |
| case "Enter": |
| if (!isEnterKey(event)) |
| break; |
| // falls through |
| case "U+0020": // Space |
| if (this._highlightedMenuItemElement && !this._highlightedMenuItemElement._subItems) |
| this._triggerAction(this._highlightedMenuItemElement, event); |
| // falls through |
| case "Right": |
| if (this._highlightedMenuItemElement && this._highlightedMenuItemElement._subItems) { |
| this._showSubMenu(this._highlightedMenuItemElement); |
| this._subMenu._focus(); |
| this._subMenu._highlightNext(); |
| } |
| break; |
| case "U+001B": // Escape |
| this._discardMenu(true, event); |
| break; |
| } |
| this._consumeEvent(event, true); |
| } |
| |
| _glassPaneMouseDown(event) |
| { |
| this._discardMenu(true, event); |
| this._consumeEvent(event); |
| } |
| |
| _focus() |
| { |
| this._contextMenuElement.focus(); |
| } |
| |
| _triggerAction(menuItemElement, event) |
| { |
| if (!menuItemElement._subItems) { |
| this._discardMenu(true, event); |
| if (typeof menuItemElement._actionId === "number") { |
| WI.ContextMenu.contextMenuItemSelected(menuItemElement._actionId); |
| menuItemElement._actionId = null; |
| } |
| return; |
| } |
| |
| this._showSubMenu(menuItemElement); |
| this._consumeEvent(event); |
| } |
| |
| _highlightMenuItem(menuItemElement, skipSubMenuExpansion) |
| { |
| if (this._highlightedMenuItemElement === menuItemElement) |
| return; |
| |
| this._hideSubMenu(); |
| if (this._highlightedMenuItemElement) { |
| this._highlightedMenuItemElement.classList.remove("highlighted"); |
| |
| if (this._highlightedMenuItemElement._subItems && this._highlightedMenuItemElement._subMenuTimer) { |
| clearTimeout(this._highlightedMenuItemElement._subMenuTimer); |
| this._highlightedMenuItemElement._subMenuTimer = 0; |
| } |
| } |
| |
| this._highlightedMenuItemElement = menuItemElement; |
| if (this._highlightedMenuItemElement) { |
| this._highlightedMenuItemElement.classList.add("highlighted"); |
| this._contextMenuElement.focus(); |
| |
| if (!skipSubMenuExpansion && 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 = menuItemElement.previousSibling; |
| |
| if (menuItemElement) |
| this._highlightMenuItem(menuItemElement, true); |
| } |
| |
| _highlightNext() |
| { |
| let menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.nextSibling : this._contextMenuElement.firstChild; |
| |
| while (menuItemElement && menuItemElement._isSeparator) |
| menuItemElement = menuItemElement.nextSibling; |
| |
| if (menuItemElement) |
| this._highlightMenuItem(menuItemElement, true); |
| } |
| |
| _discardMenu(closeParentMenus, event) |
| { |
| if (this._subMenu && !closeParentMenus) |
| return; |
| |
| if (this._glassPaneElement) { |
| const glassPane = this._glassPaneElement; |
| this._glassPaneElement = null; |
| // This can re-enter discardMenu due to blur. |
| document.body.removeChild(glassPane); |
| |
| if (this._parentMenu) { |
| this._parentMenu._subMenu = null; |
| if (closeParentMenus) |
| this._parentMenu._discardMenu(closeParentMenus, event); |
| } |
| |
| if (event) |
| this._consumeEvent(event, true); |
| } else if (this._parentMenu && this._contextMenuElement.parentElement) { |
| this._discardSubMenus(); |
| |
| if (closeParentMenus) |
| this._parentMenu._discardMenu(closeParentMenus, event); |
| |
| if (event) |
| this._consumeEvent(event, true); |
| } |
| } |
| |
| _discardSubMenus() |
| { |
| if (this._subMenu) |
| this._subMenu._discardSubMenus(); |
| |
| if (this._contextMenuElement.parentElement) |
| this._contextMenuElement.parentElement.removeChild(this._contextMenuElement); |
| |
| if (this._parentMenu) |
| this._parentMenu._subMenu = null; |
| } |
| }; |