Home | History | Annotate | Download | only in ui
      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 {!Array.<!InspectorFrontendHostAPI.ContextMenuDescriptor>} items
     29  * @param {function(string)} itemSelectedCallback
     30  * @param {!WebInspector.SoftContextMenu=} parentMenu
     31  */
     32 WebInspector.SoftContextMenu = function(items, itemSelectedCallback, parentMenu)
     33 {
     34     this._items = items;
     35     this._itemSelectedCallback = itemSelectedCallback;
     36     this._parentMenu = parentMenu;
     37 }
     38 
     39 WebInspector.SoftContextMenu.prototype = {
     40     /**
     41      * @param {number} x
     42      * @param {number} y
     43      */
     44     show: function(x, y)
     45     {
     46         this._x = x;
     47         this._y = y;
     48         this._time = new Date().getTime();
     49 
     50         // Create context menu.
     51         this._contextMenuElement = document.createElementWithClass("div", "soft-context-menu");
     52         this._contextMenuElement.tabIndex = 0;
     53         this._contextMenuElement.style.top = y + "px";
     54         this._contextMenuElement.style.left = x + "px";
     55 
     56         this._contextMenuElement.addEventListener("mouseup", consumeEvent, false);
     57         this._contextMenuElement.addEventListener("keydown", this._menuKeyDown.bind(this), false);
     58 
     59         for (var i = 0; i < this._items.length; ++i)
     60             this._contextMenuElement.appendChild(this._createMenuItem(this._items[i]));
     61 
     62         // Install glass pane capturing events.
     63         if (!this._parentMenu) {
     64             this._glassPaneElement = document.createElementWithClass("div", "soft-context-menu-glass-pane");
     65             this._glassPaneElement.tabIndex = 0;
     66             this._glassPaneElement.addEventListener("mouseup", this._glassPaneMouseUp.bind(this), false);
     67             this._glassPaneElement.appendChild(this._contextMenuElement);
     68             document.body.appendChild(this._glassPaneElement);
     69             this._focus();
     70         } else {
     71             this._parentMenu._parentGlassPaneElement().appendChild(this._contextMenuElement);
     72         }
     73 
     74         // Re-position menu in case it does not fit.
     75         if (document.body.offsetWidth <  this._contextMenuElement.offsetLeft + this._contextMenuElement.offsetWidth)
     76             this._contextMenuElement.style.left = Math.max(0, x - this._contextMenuElement.offsetWidth) + "px";
     77         if (document.body.offsetHeight < this._contextMenuElement.offsetTop + this._contextMenuElement.offsetHeight)
     78             this._contextMenuElement.style.top = Math.max(0, document.body.offsetHeight - this._contextMenuElement.offsetHeight) + "px";
     79     },
     80 
     81     _parentGlassPaneElement: function()
     82     {
     83         if (this._glassPaneElement)
     84             return this._glassPaneElement;
     85         if (this._parentMenu)
     86             return this._parentMenu._parentGlassPaneElement();
     87         return null;
     88     },
     89 
     90     _createMenuItem: function(item)
     91     {
     92         if (item.type === "separator")
     93             return this._createSeparator();
     94 
     95         if (item.type === "subMenu")
     96             return this._createSubMenu(item);
     97 
     98         var menuItemElement = document.createElementWithClass("div", "soft-context-menu-item");
     99         var checkMarkElement = menuItemElement.createChild("span", "soft-context-menu-item-checkmark");
    100         checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
    101         if (!item.checked)
    102             checkMarkElement.style.opacity = "0";
    103 
    104         menuItemElement.createTextChild(item.label);
    105 
    106         menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
    107         menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
    108 
    109         // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
    110         menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
    111         menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);
    112 
    113         menuItemElement._actionId = item.id;
    114         return menuItemElement;
    115     },
    116 
    117     _createSubMenu: function(item)
    118     {
    119         var menuItemElement = document.createElementWithClass("div", "soft-context-menu-item");
    120         menuItemElement._subItems = item.subItems;
    121 
    122         // Occupy the same space on the left in all items.
    123         var checkMarkElement = menuItemElement.createChild("span", "soft-context-menu-item-checkmark");
    124         checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
    125         checkMarkElement.style.opacity = "0";
    126 
    127         menuItemElement.createTextChild(item.label);
    128 
    129         var subMenuArrowElement = menuItemElement.createChild("span", "soft-context-menu-item-submenu-arrow");
    130         subMenuArrowElement.textContent = "\u25B6"; // BLACK RIGHT-POINTING TRIANGLE
    131 
    132         menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
    133         menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
    134 
    135         // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
    136         menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
    137         menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);
    138 
    139         return menuItemElement;
    140     },
    141 
    142     _createSeparator: function()
    143     {
    144         var separatorElement = document.createElementWithClass("div", "soft-context-menu-separator");
    145         separatorElement._isSeparator = true;
    146         separatorElement.addEventListener("mouseover", this._hideSubMenu.bind(this), false);
    147         separatorElement.createChild("div", "separator-line");
    148         return separatorElement;
    149     },
    150 
    151     _menuItemMouseDown: function(event)
    152     {
    153         // Do not let separator's mouse down hit menu's handler - we need to receive mouse up!
    154         event.consume(true);
    155     },
    156 
    157     _menuItemMouseUp: function(event)
    158     {
    159         this._triggerAction(event.target, event);
    160         event.consume();
    161     },
    162 
    163     _focus: function()
    164     {
    165         this._contextMenuElement.focus();
    166     },
    167 
    168     _triggerAction: function(menuItemElement, event)
    169     {
    170         if (!menuItemElement._subItems) {
    171             this._discardMenu(true, event);
    172             if (typeof menuItemElement._actionId !== "undefined") {
    173                 this._itemSelectedCallback(menuItemElement._actionId);
    174                 delete menuItemElement._actionId;
    175             }
    176             return;
    177         }
    178 
    179         this._showSubMenu(menuItemElement);
    180         event.consume();
    181     },
    182 
    183     _showSubMenu: function(menuItemElement)
    184     {
    185         if (menuItemElement._subMenuTimer) {
    186             clearTimeout(menuItemElement._subMenuTimer);
    187             delete menuItemElement._subMenuTimer;
    188         }
    189         if (this._subMenu)
    190             return;
    191 
    192         this._subMenu = new WebInspector.SoftContextMenu(menuItemElement._subItems, this._itemSelectedCallback, this);
    193         this._subMenu.show(this._x + menuItemElement.offsetWidth - 3, this._y + menuItemElement.offsetTop - 1);
    194     },
    195 
    196     _hideSubMenu: function()
    197     {
    198         if (!this._subMenu)
    199             return;
    200         this._subMenu._discardSubMenus();
    201         this._focus();
    202     },
    203 
    204     _menuItemMouseOver: function(event)
    205     {
    206         this._highlightMenuItem(event.target);
    207     },
    208 
    209     _menuItemMouseOut: function(event)
    210     {
    211         if (!this._subMenu || !event.relatedTarget) {
    212             this._highlightMenuItem(null);
    213             return;
    214         }
    215 
    216         var relatedTarget = event.relatedTarget;
    217         if (this._contextMenuElement.isSelfOrAncestor(relatedTarget) || relatedTarget.classList.contains("soft-context-menu-glass-pane"))
    218             this._highlightMenuItem(null);
    219     },
    220 
    221     _highlightMenuItem: function(menuItemElement)
    222     {
    223         if (this._highlightedMenuItemElement ===  menuItemElement)
    224             return;
    225 
    226         this._hideSubMenu();
    227         if (this._highlightedMenuItemElement) {
    228             this._highlightedMenuItemElement.classList.remove("soft-context-menu-item-mouse-over");
    229             if (this._highlightedMenuItemElement._subItems && this._highlightedMenuItemElement._subMenuTimer) {
    230                 clearTimeout(this._highlightedMenuItemElement._subMenuTimer);
    231                 delete this._highlightedMenuItemElement._subMenuTimer;
    232             }
    233         }
    234         this._highlightedMenuItemElement = menuItemElement;
    235         if (this._highlightedMenuItemElement) {
    236             this._highlightedMenuItemElement.classList.add("soft-context-menu-item-mouse-over");
    237             this._contextMenuElement.focus();
    238             if (this._highlightedMenuItemElement._subItems && !this._highlightedMenuItemElement._subMenuTimer)
    239                 this._highlightedMenuItemElement._subMenuTimer = setTimeout(this._showSubMenu.bind(this, this._highlightedMenuItemElement), 150);
    240         }
    241     },
    242 
    243     _highlightPrevious: function()
    244     {
    245         var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.previousSibling : this._contextMenuElement.lastChild;
    246         while (menuItemElement && menuItemElement._isSeparator)
    247             menuItemElement = menuItemElement.previousSibling;
    248         if (menuItemElement)
    249             this._highlightMenuItem(menuItemElement);
    250     },
    251 
    252     _highlightNext: function()
    253     {
    254         var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.nextSibling : this._contextMenuElement.firstChild;
    255         while (menuItemElement && menuItemElement._isSeparator)
    256             menuItemElement = menuItemElement.nextSibling;
    257         if (menuItemElement)
    258             this._highlightMenuItem(menuItemElement);
    259     },
    260 
    261     _menuKeyDown: function(event)
    262     {
    263         switch (event.keyIdentifier) {
    264         case "Up":
    265             this._highlightPrevious(); break;
    266         case "Down":
    267             this._highlightNext(); break;
    268         case "Left":
    269             if (this._parentMenu) {
    270                 this._highlightMenuItem(null);
    271                 this._parentMenu._focus();
    272             }
    273             break;
    274         case "Right":
    275             if (!this._highlightedMenuItemElement)
    276                 break;
    277             if (this._highlightedMenuItemElement._subItems) {
    278                 this._showSubMenu(this._highlightedMenuItemElement);
    279                 this._subMenu._focus();
    280                 this._subMenu._highlightNext();
    281             }
    282             break;
    283         case "U+001B": // Escape
    284             this._discardMenu(true, event); break;
    285         case "Enter":
    286             if (!isEnterKey(event))
    287                 break;
    288             // Fall through
    289         case "U+0020": // Space
    290             if (this._highlightedMenuItemElement)
    291                 this._triggerAction(this._highlightedMenuItemElement, event);
    292             break;
    293         }
    294         event.consume(true);
    295     },
    296 
    297     _glassPaneMouseUp: function(event)
    298     {
    299         // Return if this is simple 'click', since dispatched on glass pane, can't use 'click' event.
    300         if (event.x === this._x && event.y === this._y && new Date().getTime() - this._time < 300)
    301             return;
    302         this._discardMenu(true, event);
    303         event.consume();
    304     },
    305 
    306     /**
    307      * @param {boolean} closeParentMenus
    308      * @param {!Event=} event
    309      */
    310     _discardMenu: function(closeParentMenus, event)
    311     {
    312         if (this._subMenu && !closeParentMenus)
    313             return;
    314         if (this._glassPaneElement) {
    315             var glassPane = this._glassPaneElement;
    316             delete this._glassPaneElement;
    317             // This can re-enter discardMenu due to blur.
    318             document.body.removeChild(glassPane);
    319             if (this._parentMenu) {
    320                 delete this._parentMenu._subMenu;
    321                 if (closeParentMenus)
    322                     this._parentMenu._discardMenu(closeParentMenus, event);
    323             }
    324 
    325             if (event)
    326                 event.consume(true);
    327         } else if (this._parentMenu && this._contextMenuElement.parentElement) {
    328             this._discardSubMenus();
    329             if (closeParentMenus)
    330                 this._parentMenu._discardMenu(closeParentMenus, event);
    331 
    332             if (event)
    333                 event.consume(true);
    334         }
    335     },
    336 
    337     _discardSubMenus: function()
    338     {
    339         if (this._subMenu)
    340             this._subMenu._discardSubMenus();
    341         this._contextMenuElement.remove();
    342         if (this._parentMenu)
    343             delete this._parentMenu._subMenu;
    344     }
    345 }
    346