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