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