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