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 {boolean=} alignToCurrentTarget
     39      */
     40     show: function(event, alignToCurrentTarget)
     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         if (alignToCurrentTarget) {
     63             targetRect = event.currentTarget.getBoundingClientRect();
     64             // Align with bottom left of currentTarget by default.
     65             absoluteX = targetRect.left;
     66             absoluteY = targetRect.bottom;
     67         }
     68         this._contextMenuElement.style.top = absoluteY + "px";
     69         this._contextMenuElement.style.left = absoluteX + "px";
     70 
     71         this._contextMenuElement.addEventListener("mouseup", consumeEvent, false);
     72         this._contextMenuElement.addEventListener("keydown", this._menuKeyDown.bind(this), false);
     73 
     74         for (var i = 0; i < this._items.length; ++i)
     75             this._contextMenuElement.appendChild(this._createMenuItem(this._items[i]));
     76 
     77         // Install glass pane capturing events.
     78         if (!this._parentMenu) {
     79             this._glassPaneElement = document.createElement("div");
     80             this._glassPaneElement.className = "soft-context-menu-glass-pane";
     81             this._glassPaneElement.tabIndex = 0;
     82             this._glassPaneElement.addEventListener("mouseup", this._glassPaneMouseUp.bind(this), false);
     83             this._glassPaneElement.appendChild(this._contextMenuElement);
     84             document.body.appendChild(this._glassPaneElement);
     85             this._focus();
     86         } else
     87             this._parentMenu._parentGlassPaneElement().appendChild(this._contextMenuElement);
     88 
     89         // Re-position menu in case it does not fit.
     90         if (document.body.offsetWidth <  this._contextMenuElement.offsetLeft + this._contextMenuElement.offsetWidth) {
     91             if (alignToCurrentTarget)
     92                 this._contextMenuElement.style.left = Math.max(0, targetRect.right - this._contextMenuElement.offsetWidth) + "px";
     93             else
     94                 this._contextMenuElement.style.left = (absoluteX - this._contextMenuElement.offsetWidth) + "px";
     95         }
     96         if (document.body.offsetHeight < this._contextMenuElement.offsetTop + this._contextMenuElement.offsetHeight) {
     97             if (alignToCurrentTarget)
     98                 this._contextMenuElement.style.top = Math.max(0, targetRect.top - this._contextMenuElement.offsetHeight) + "px";
     99             else
    100                 this._contextMenuElement.style.top = (document.body.offsetHeight - this._contextMenuElement.offsetHeight) + "px";
    101         }
    102 
    103         event.consume(true);
    104     },
    105 
    106     _parentGlassPaneElement: function()
    107     {
    108         if (this._glassPaneElement)
    109             return this._glassPaneElement;
    110         if (this._parentMenu)
    111             return this._parentMenu._parentGlassPaneElement();
    112         return null;
    113     },
    114 
    115     _createMenuItem: function(item)
    116     {
    117         if (item.type === "separator")
    118             return this._createSeparator();
    119 
    120         if (item.type === "subMenu")
    121             return this._createSubMenu(item);
    122 
    123         var menuItemElement = document.createElement("div");
    124         menuItemElement.className = "soft-context-menu-item";
    125 
    126         var checkMarkElement = document.createElement("span");
    127         checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
    128         checkMarkElement.className = "soft-context-menu-item-checkmark";
    129         if (!item.checked)
    130             checkMarkElement.style.opacity = "0";
    131 
    132         menuItemElement.appendChild(checkMarkElement);
    133         menuItemElement.appendChild(document.createTextNode(item.label));
    134 
    135         menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
    136         menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
    137 
    138         // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
    139         menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
    140         menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);
    141 
    142         menuItemElement._actionId = item.id;
    143         return menuItemElement;
    144     },
    145 
    146     _createSubMenu: function(item)
    147     {
    148         var menuItemElement = document.createElement("div");
    149         menuItemElement.className = "soft-context-menu-item";
    150         menuItemElement._subItems = item.subItems;
    151 
    152         // Occupy the same space on the left in all items.
    153         var checkMarkElement = document.createElement("span");
    154         checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
    155         checkMarkElement.className = "soft-context-menu-item-checkmark";
    156         checkMarkElement.style.opacity = "0";
    157         menuItemElement.appendChild(checkMarkElement);
    158 
    159         var subMenuArrowElement = document.createElement("span");
    160         subMenuArrowElement.textContent = "\u25B6"; // BLACK RIGHT-POINTING TRIANGLE
    161         subMenuArrowElement.className = "soft-context-menu-item-submenu-arrow";
    162 
    163         menuItemElement.appendChild(document.createTextNode(item.label));
    164         menuItemElement.appendChild(subMenuArrowElement);
    165 
    166         menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
    167         menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
    168 
    169         // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
    170         menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
    171         menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);
    172 
    173         return menuItemElement;
    174     },
    175 
    176     _createSeparator: function()
    177     {
    178         var separatorElement = document.createElement("div");
    179         separatorElement.className = "soft-context-menu-separator";
    180         separatorElement._isSeparator = true;
    181         separatorElement.addEventListener("mouseover", this._hideSubMenu.bind(this), false);
    182         separatorElement.createChild("div", "separator-line");
    183         return separatorElement;
    184     },
    185 
    186     _menuItemMouseDown: function(event)
    187     {
    188         // Do not let separator's mouse down hit menu's handler - we need to receive mouse up!
    189         event.consume(true);
    190     },
    191 
    192     _menuItemMouseUp: function(event)
    193     {
    194         this._triggerAction(event.target, event);
    195         event.consume();
    196     },
    197 
    198     _focus: function()
    199     {
    200         this._contextMenuElement.focus();
    201     },
    202 
    203     _triggerAction: function(menuItemElement, event)
    204     {
    205         if (!menuItemElement._subItems) {
    206             this._discardMenu(true, event);
    207             if (typeof menuItemElement._actionId !== "undefined") {
    208                 WebInspector.contextMenuItemSelected(menuItemElement._actionId);
    209                 delete menuItemElement._actionId;
    210             }
    211             return;
    212         }
    213 
    214         this._showSubMenu(menuItemElement, event);
    215         event.consume();
    216     },
    217 
    218     _showSubMenu: function(menuItemElement, event)
    219     {
    220         if (menuItemElement._subMenuTimer) {
    221             clearTimeout(menuItemElement._subMenuTimer);
    222             delete menuItemElement._subMenuTimer;
    223         }
    224         if (this._subMenu)
    225             return;
    226 
    227         this._subMenu = new WebInspector.SoftContextMenu(menuItemElement._subItems, this);
    228         this._subMenu.show(this._buildMouseEventForSubMenu(menuItemElement));
    229     },
    230 
    231     _buildMouseEventForSubMenu: function(subMenuItemElement)
    232     {
    233         var subMenuOffset = { x: subMenuItemElement.offsetWidth - 3, y: subMenuItemElement.offsetTop - 1 };
    234         var targetX = this._x + subMenuOffset.x;
    235         var targetY = this._y + subMenuOffset.y;
    236         var targetPageX = parseInt(this._contextMenuElement.style.left, 10) + subMenuOffset.x;
    237         var targetPageY = parseInt(this._contextMenuElement.style.top, 10) + subMenuOffset.y;
    238         return { x: targetX, y: targetY, pageX: targetPageX, pageY: targetPageY, consume: function() {} };
    239     },
    240 
    241     _hideSubMenu: function()
    242     {
    243         if (!this._subMenu)
    244             return;
    245         this._subMenu._discardSubMenus();
    246         this._focus();
    247     },
    248 
    249     _menuItemMouseOver: function(event)
    250     {
    251         this._highlightMenuItem(event.target);
    252     },
    253 
    254     _menuItemMouseOut: function(event)
    255     {
    256         if (!this._subMenu || !event.relatedTarget) {
    257             this._highlightMenuItem(null);
    258             return;
    259         }
    260 
    261         var relatedTarget = event.relatedTarget;
    262         if (this._contextMenuElement.isSelfOrAncestor(relatedTarget) || relatedTarget.hasStyleClass("soft-context-menu-glass-pane"))
    263             this._highlightMenuItem(null);
    264     },
    265 
    266     _highlightMenuItem: function(menuItemElement)
    267     {
    268         if (this._highlightedMenuItemElement ===  menuItemElement)
    269             return;
    270 
    271         this._hideSubMenu();
    272         if (this._highlightedMenuItemElement) {
    273             this._highlightedMenuItemElement.removeStyleClass("soft-context-menu-item-mouse-over");
    274             if (this._highlightedMenuItemElement._subItems && this._highlightedMenuItemElement._subMenuTimer) {
    275                 clearTimeout(this._highlightedMenuItemElement._subMenuTimer);
    276                 delete this._highlightedMenuItemElement._subMenuTimer;
    277             }
    278         }
    279         this._highlightedMenuItemElement = menuItemElement;
    280         if (this._highlightedMenuItemElement) {
    281             this._highlightedMenuItemElement.addStyleClass("soft-context-menu-item-mouse-over");
    282             this._contextMenuElement.focus();
    283             if (this._highlightedMenuItemElement._subItems && !this._highlightedMenuItemElement._subMenuTimer)
    284                 this._highlightedMenuItemElement._subMenuTimer = setTimeout(this._showSubMenu.bind(this, this._highlightedMenuItemElement, this._buildMouseEventForSubMenu(this._highlightedMenuItemElement)), 150);
    285         }
    286     },
    287 
    288     _highlightPrevious: function()
    289     {
    290         var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.previousSibling : this._contextMenuElement.lastChild;
    291         while (menuItemElement && menuItemElement._isSeparator)
    292             menuItemElement = menuItemElement.previousSibling;
    293         if (menuItemElement)
    294             this._highlightMenuItem(menuItemElement);
    295     },
    296 
    297     _highlightNext: function()
    298     {
    299         var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.nextSibling : this._contextMenuElement.firstChild;
    300         while (menuItemElement && menuItemElement._isSeparator)
    301             menuItemElement = menuItemElement.nextSibling;
    302         if (menuItemElement)
    303             this._highlightMenuItem(menuItemElement);
    304     },
    305 
    306     _menuKeyDown: function(event)
    307     {
    308         switch (event.keyIdentifier) {
    309         case "Up":
    310             this._highlightPrevious(); break;
    311         case "Down":
    312             this._highlightNext(); break;
    313         case "Left":
    314             if (this._parentMenu) {
    315                 this._highlightMenuItem(null);
    316                 this._parentMenu._focus();
    317             }
    318             break;
    319         case "Right":
    320             if (!this._highlightedMenuItemElement)
    321                 break;
    322             if (this._highlightedMenuItemElement._subItems) {
    323                 this._showSubMenu(this._highlightedMenuItemElement, this._buildMouseEventForSubMenu(this._highlightedMenuItemElement));
    324                 this._subMenu._focus();
    325                 this._subMenu._highlightNext();
    326             }
    327             break;
    328         case "U+001B": // Escape
    329             this._discardMenu(true, event); break;
    330         case "Enter":
    331             if (!isEnterKey(event))
    332                 break;
    333             // Fall through
    334         case "U+0020": // Space
    335             if (this._highlightedMenuItemElement)
    336                 this._triggerAction(this._highlightedMenuItemElement, event);
    337             break;
    338         }
    339         event.consume(true);
    340     },
    341 
    342     _glassPaneMouseUp: function(event)
    343     {
    344         // Return if this is simple 'click', since dispatched on glass pane, can't use 'click' event.
    345         if (event.x === this._x && event.y === this._y && new Date().getTime() - this._time < 300)
    346             return;
    347         this._discardMenu(true, event);
    348         event.consume();
    349     },
    350 
    351     /**
    352      * @param {boolean} closeParentMenus
    353      * @param {Event=} event
    354      */
    355     _discardMenu: function(closeParentMenus, event)
    356     {
    357         if (this._subMenu && !closeParentMenus)
    358             return;
    359         if (this._glassPaneElement) {
    360             var glassPane = this._glassPaneElement;
    361             delete this._glassPaneElement;
    362             // This can re-enter discardMenu due to blur.
    363             document.body.removeChild(glassPane);
    364             if (this._parentMenu) {
    365                 delete this._parentMenu._subMenu;
    366                 if (closeParentMenus)
    367                     this._parentMenu._discardMenu(closeParentMenus, event);
    368             }
    369 
    370             if (event)
    371                 event.consume(true);
    372         } else if (this._parentMenu && this._contextMenuElement.parentElement) {
    373             this._discardSubMenus();
    374             if (closeParentMenus)
    375                 this._parentMenu._discardMenu(closeParentMenus, event);
    376 
    377             if (event)
    378                 event.consume(true);
    379         }
    380     },
    381 
    382     _discardSubMenus: function()
    383     {
    384         if (this._subMenu)
    385             this._subMenu._discardSubMenus();
    386         this._contextMenuElement.remove();
    387         if (this._parentMenu)
    388             delete this._parentMenu._subMenu;
    389     }
    390 }
    391 
    392 if (!InspectorFrontendHost.showContextMenu) {
    393 
    394 InspectorFrontendHost.showContextMenu = function(event, items)
    395 {
    396     new WebInspector.SoftContextMenu(items).show(event);
    397 }
    398 
    399 }
    400