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