Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2011 Google Inc. All Rights Reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions
      6  * are met:
      7  * 1. Redistributions of source code must retain the above copyright
      8  *    notice, this list of conditions and the following disclaimer.
      9  * 2. Redistributions in binary form must reproduce the above copyright
     10  *    notice, this list of conditions and the following disclaimer in the
     11  *    documentation and/or other materials provided with the distribution.
     12  *
     13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
     14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
     17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
     21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     24  */
     25 
     26 /**
     27  * @constructor
     28  * @param {!WebInspector.SoftContextMenu=} parentMenu
     29  */
     30 WebInspector.SoftContextMenu = function(items, parentMenu)
     31 {
     32     this._items = items;
     33     this._parentMenu = parentMenu;
     34 }
     35 
     36 WebInspector.SoftContextMenu.prototype = {
     37     /**
     38      * @param {!Event} event
     39      */
     40     show: function(event)
     41     {
     42         this._x = event.x;
     43         this._y = event.y;
     44         this._time = new Date().getTime();
     45 
     46         // Absolutely position menu for iframes.
     47         var absoluteX = event.pageX;
     48         var absoluteY = event.pageY;
     49         var targetElement = event.target;
     50         while (targetElement && window !== targetElement.ownerDocument.defaultView) {
     51             var frameElement = targetElement.ownerDocument.defaultView.frameElement;
     52             absoluteY += frameElement.totalOffsetTop();
     53             absoluteX += frameElement.totalOffsetLeft();
     54             targetElement = frameElement;
     55         }
     56 
     57         // Create context menu.
     58         var targetRect;
     59         this._contextMenuElement = document.createElement("div");
     60         this._contextMenuElement.className = "soft-context-menu";
     61         this._contextMenuElement.tabIndex = 0;
     62         this._contextMenuElement.style.top = absoluteY + "px";
     63         this._contextMenuElement.style.left = absoluteX + "px";
     64 
     65         this._contextMenuElement.addEventListener("mouseup", consumeEvent, false);
     66         this._contextMenuElement.addEventListener("keydown", this._menuKeyDown.bind(this), false);
     67 
     68         for (var i = 0; i < this._items.length; ++i)
     69             this._contextMenuElement.appendChild(this._createMenuItem(this._items[i]));
     70 
     71         // Install glass pane capturing events.
     72         if (!this._parentMenu) {
     73             this._glassPaneElement = document.createElement("div");
     74             this._glassPaneElement.className = "soft-context-menu-glass-pane";
     75             this._glassPaneElement.tabIndex = 0;
     76             this._glassPaneElement.addEventListener("mouseup", this._glassPaneMouseUp.bind(this), false);
     77             this._glassPaneElement.appendChild(this._contextMenuElement);
     78             document.body.appendChild(this._glassPaneElement);
     79             this._focus();
     80         } else
     81             this._parentMenu._parentGlassPaneElement().appendChild(this._contextMenuElement);
     82 
     83         // Re-position menu in case it does not fit.
     84         if (document.body.offsetWidth <  this._contextMenuElement.offsetLeft + this._contextMenuElement.offsetWidth)
     85             this._contextMenuElement.style.left = (absoluteX - this._contextMenuElement.offsetWidth) + "px";
     86         if (document.body.offsetHeight < this._contextMenuElement.offsetTop + this._contextMenuElement.offsetHeight)
     87             this._contextMenuElement.style.top = (document.body.offsetHeight - this._contextMenuElement.offsetHeight) + "px";
     88 
     89         event.consume(true);
     90     },
     91 
     92     _parentGlassPaneElement: function()
     93     {
     94         if (this._glassPaneElement)
     95             return this._glassPaneElement;
     96         if (this._parentMenu)
     97             return this._parentMenu._parentGlassPaneElement();
     98         return null;
     99     },
    100 
    101     _createMenuItem: function(item)
    102     {
    103         if (item.type === "separator")
    104             return this._createSeparator();
    105 
    106         if (item.type === "subMenu")
    107             return this._createSubMenu(item);
    108 
    109         var menuItemElement = document.createElement("div");
    110         menuItemElement.className = "soft-context-menu-item";
    111 
    112         var checkMarkElement = document.createElement("span");
    113         checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
    114         checkMarkElement.className = "soft-context-menu-item-checkmark";
    115         if (!item.checked)
    116             checkMarkElement.style.opacity = "0";
    117 
    118         menuItemElement.appendChild(checkMarkElement);
    119         menuItemElement.appendChild(document.createTextNode(item.label));
    120 
    121         menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
    122         menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
    123 
    124         // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
    125         menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
    126         menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);
    127 
    128         menuItemElement._actionId = item.id;
    129         return menuItemElement;
    130     },
    131 
    132     _createSubMenu: function(item)
    133     {
    134         var menuItemElement = document.createElement("div");
    135         menuItemElement.className = "soft-context-menu-item";
    136         menuItemElement._subItems = item.subItems;
    137 
    138         // Occupy the same space on the left in all items.
    139         var checkMarkElement = document.createElement("span");
    140         checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
    141         checkMarkElement.className = "soft-context-menu-item-checkmark";
    142         checkMarkElement.style.opacity = "0";
    143         menuItemElement.appendChild(checkMarkElement);
    144 
    145         var subMenuArrowElement = document.createElement("span");
    146         subMenuArrowElement.textContent = "\u25B6"; // BLACK RIGHT-POINTING TRIANGLE
    147         subMenuArrowElement.className = "soft-context-menu-item-submenu-arrow";
    148 
    149         menuItemElement.appendChild(document.createTextNode(item.label));
    150         menuItemElement.appendChild(subMenuArrowElement);
    151 
    152         menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
    153         menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
    154 
    155         // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
    156         menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
    157         menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);
    158 
    159         return menuItemElement;
    160     },
    161 
    162     _createSeparator: function()
    163     {
    164         var separatorElement = document.createElement("div");
    165         separatorElement.className = "soft-context-menu-separator";
    166         separatorElement._isSeparator = true;
    167         separatorElement.addEventListener("mouseover", this._hideSubMenu.bind(this), false);
    168         separatorElement.createChild("div", "separator-line");
    169         return separatorElement;
    170     },
    171 
    172     _menuItemMouseDown: function(event)
    173     {
    174         // Do not let separator's mouse down hit menu's handler - we need to receive mouse up!
    175         event.consume(true);
    176     },
    177 
    178     _menuItemMouseUp: function(event)
    179     {
    180         this._triggerAction(event.target, event);
    181         event.consume();
    182     },
    183 
    184     _focus: function()
    185     {
    186         this._contextMenuElement.focus();
    187     },
    188 
    189     _triggerAction: function(menuItemElement, event)
    190     {
    191         if (!menuItemElement._subItems) {
    192             this._discardMenu(true, event);
    193             if (typeof menuItemElement._actionId !== "undefined") {
    194                 WebInspector.contextMenuItemSelected(menuItemElement._actionId);
    195                 delete menuItemElement._actionId;
    196             }
    197             return;
    198         }
    199 
    200         this._showSubMenu(menuItemElement, event);
    201         event.consume();
    202     },
    203 
    204     _showSubMenu: function(menuItemElement, event)
    205     {
    206         if (menuItemElement._subMenuTimer) {
    207             clearTimeout(menuItemElement._subMenuTimer);
    208             delete menuItemElement._subMenuTimer;
    209         }
    210         if (this._subMenu)
    211             return;
    212 
    213         this._subMenu = new WebInspector.SoftContextMenu(menuItemElement._subItems, this);
    214         this._subMenu.show(this._buildMouseEventForSubMenu(menuItemElement));
    215     },
    216 
    217     _buildMouseEventForSubMenu: function(subMenuItemElement)
    218     {
    219         var subMenuOffset = { x: subMenuItemElement.offsetWidth - 3, y: subMenuItemElement.offsetTop - 1 };
    220         var targetX = this._x + subMenuOffset.x;
    221         var targetY = this._y + subMenuOffset.y;
    222         var targetPageX = parseInt(this._contextMenuElement.style.left, 10) + subMenuOffset.x;
    223         var targetPageY = parseInt(this._contextMenuElement.style.top, 10) + subMenuOffset.y;
    224         return { x: targetX, y: targetY, pageX: targetPageX, pageY: targetPageY, consume: function() {} };
    225     },
    226 
    227     _hideSubMenu: function()
    228     {
    229         if (!this._subMenu)
    230             return;
    231         this._subMenu._discardSubMenus();
    232         this._focus();
    233     },
    234 
    235     _menuItemMouseOver: function(event)
    236     {
    237         this._highlightMenuItem(event.target);
    238     },
    239 
    240     _menuItemMouseOut: function(event)
    241     {
    242         if (!this._subMenu || !event.relatedTarget) {
    243             this._highlightMenuItem(null);
    244             return;
    245         }
    246 
    247         var relatedTarget = event.relatedTarget;
    248         if (this._contextMenuElement.isSelfOrAncestor(relatedTarget) || relatedTarget.classList.contains("soft-context-menu-glass-pane"))
    249             this._highlightMenuItem(null);
    250     },
    251 
    252     _highlightMenuItem: function(menuItemElement)
    253     {
    254         if (this._highlightedMenuItemElement ===  menuItemElement)
    255             return;
    256 
    257         this._hideSubMenu();
    258         if (this._highlightedMenuItemElement) {
    259             this._highlightedMenuItemElement.classList.remove("soft-context-menu-item-mouse-over");
    260             if (this._highlightedMenuItemElement._subItems && this._highlightedMenuItemElement._subMenuTimer) {
    261                 clearTimeout(this._highlightedMenuItemElement._subMenuTimer);
    262                 delete this._highlightedMenuItemElement._subMenuTimer;
    263             }
    264         }
    265         this._highlightedMenuItemElement = menuItemElement;
    266         if (this._highlightedMenuItemElement) {
    267             this._highlightedMenuItemElement.classList.add("soft-context-menu-item-mouse-over");
    268             this._contextMenuElement.focus();
    269             if (this._highlightedMenuItemElement._subItems && !this._highlightedMenuItemElement._subMenuTimer)
    270                 this._highlightedMenuItemElement._subMenuTimer = setTimeout(this._showSubMenu.bind(this, this._highlightedMenuItemElement, this._buildMouseEventForSubMenu(this._highlightedMenuItemElement)), 150);
    271         }
    272     },
    273 
    274     _highlightPrevious: function()
    275     {
    276         var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.previousSibling : this._contextMenuElement.lastChild;
    277         while (menuItemElement && menuItemElement._isSeparator)
    278             menuItemElement = menuItemElement.previousSibling;
    279         if (menuItemElement)
    280             this._highlightMenuItem(menuItemElement);
    281     },
    282 
    283     _highlightNext: function()
    284     {
    285         var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.nextSibling : this._contextMenuElement.firstChild;
    286         while (menuItemElement && menuItemElement._isSeparator)
    287             menuItemElement = menuItemElement.nextSibling;
    288         if (menuItemElement)
    289             this._highlightMenuItem(menuItemElement);
    290     },
    291 
    292     _menuKeyDown: function(event)
    293     {
    294         switch (event.keyIdentifier) {
    295         case "Up":
    296             this._highlightPrevious(); break;
    297         case "Down":
    298             this._highlightNext(); break;
    299         case "Left":
    300             if (this._parentMenu) {
    301                 this._highlightMenuItem(null);
    302                 this._parentMenu._focus();
    303             }
    304             break;
    305         case "Right":
    306             if (!this._highlightedMenuItemElement)
    307                 break;
    308             if (this._highlightedMenuItemElement._subItems) {
    309                 this._showSubMenu(this._highlightedMenuItemElement, this._buildMouseEventForSubMenu(this._highlightedMenuItemElement));
    310                 this._subMenu._focus();
    311                 this._subMenu._highlightNext();
    312             }
    313             break;
    314         case "U+001B": // Escape
    315             this._discardMenu(true, event); break;
    316         case "Enter":
    317             if (!isEnterKey(event))
    318                 break;
    319             // Fall through
    320         case "U+0020": // Space
    321             if (this._highlightedMenuItemElement)
    322                 this._triggerAction(this._highlightedMenuItemElement, event);
    323             break;
    324         }
    325         event.consume(true);
    326     },
    327 
    328     _glassPaneMouseUp: function(event)
    329     {
    330         // Return if this is simple 'click', since dispatched on glass pane, can't use 'click' event.
    331         if (event.x === this._x && event.y === this._y && new Date().getTime() - this._time < 300)
    332             return;
    333         this._discardMenu(true, event);
    334         event.consume();
    335     },
    336 
    337     /**
    338      * @param {boolean} closeParentMenus
    339      * @param {!Event=} event
    340      */
    341     _discardMenu: function(closeParentMenus, event)
    342     {
    343         if (this._subMenu && !closeParentMenus)
    344             return;
    345         if (this._glassPaneElement) {
    346             var glassPane = this._glassPaneElement;
    347             delete this._glassPaneElement;
    348             // This can re-enter discardMenu due to blur.
    349             document.body.removeChild(glassPane);
    350             if (this._parentMenu) {
    351                 delete this._parentMenu._subMenu;
    352                 if (closeParentMenus)
    353                     this._parentMenu._discardMenu(closeParentMenus, event);
    354             }
    355 
    356             if (event)
    357                 event.consume(true);
    358         } else if (this._parentMenu && this._contextMenuElement.parentElement) {
    359             this._discardSubMenus();
    360             if (closeParentMenus)
    361                 this._parentMenu._discardMenu(closeParentMenus, event);
    362 
    363             if (event)
    364                 event.consume(true);
    365         }
    366     },
    367 
    368     _discardSubMenus: function()
    369     {
    370         if (this._subMenu)
    371             this._subMenu._discardSubMenus();
    372         this._contextMenuElement.remove();
    373         if (this._parentMenu)
    374             delete this._parentMenu._subMenu;
    375     }
    376 }
    377 
    378 if (!InspectorFrontendHost.showContextMenu) {
    379 
    380 InspectorFrontendHost.showContextMenu = function(event, items)
    381 {
    382     new WebInspector.SoftContextMenu(items).show(event);
    383 }
    384 
    385 }
    386