1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5cr.define('cr.ui', function() { 6 7 /** @const */ var Menu = cr.ui.Menu; 8 9 /** 10 * Handles context menus. 11 * @constructor 12 */ 13 function ContextMenuHandler() {} 14 15 ContextMenuHandler.prototype = { 16 17 /** 18 * The menu that we are currently showing. 19 * @type {cr.ui.Menu} 20 */ 21 menu_: null, 22 get menu() { 23 return this.menu_; 24 }, 25 26 /** 27 * Shows a menu as a context menu. 28 * @param {!Event} e The event triggering the show (usually a contextmenu 29 * event). 30 * @param {!cr.ui.Menu} menu The menu to show. 31 */ 32 showMenu: function(e, menu) { 33 this.menu_ = menu; 34 35 menu.style.display = 'block'; 36 // when the menu is shown we steal all keyboard events. 37 var doc = menu.ownerDocument; 38 doc.addEventListener('keydown', this, true); 39 doc.addEventListener('mousedown', this, true); 40 doc.addEventListener('blur', this, true); 41 doc.defaultView.addEventListener('resize', this); 42 menu.addEventListener('contextmenu', this); 43 menu.addEventListener('activate', this); 44 this.positionMenu_(e, menu); 45 }, 46 47 /** 48 * Hide the currently shown menu. 49 */ 50 hideMenu: function() { 51 var menu = this.menu; 52 if (!menu) 53 return; 54 55 menu.style.display = 'none'; 56 var doc = menu.ownerDocument; 57 doc.removeEventListener('keydown', this, true); 58 doc.removeEventListener('mousedown', this, true); 59 doc.removeEventListener('blur', this, true); 60 doc.defaultView.removeEventListener('resize', this); 61 menu.removeEventListener('contextmenu', this); 62 menu.removeEventListener('activate', this); 63 menu.selectedIndex = -1; 64 this.menu_ = null; 65 66 // On windows we might hide the menu in a right mouse button up and if 67 // that is the case we wait some short period before we allow the menu 68 // to be shown again. 69 this.hideTimestamp_ = cr.isWindows ? Date.now() : 0; 70 }, 71 72 /** 73 * Positions the menu 74 * @param {!Event} e The event object triggering the showing. 75 * @param {!cr.ui.Menu} menu The menu to position. 76 * @private 77 */ 78 positionMenu_: function(e, menu) { 79 // TODO(arv): Handle scrolled documents when needed. 80 81 var element = e.currentTarget; 82 var x, y; 83 // When the user presses the context menu key (on the keyboard) we need 84 // to detect this. 85 if (this.keyIsDown_) { 86 var rect = element.getRectForContextMenu ? 87 element.getRectForContextMenu() : 88 element.getBoundingClientRect(); 89 var offset = Math.min(rect.width, rect.height) / 2; 90 x = rect.left + offset; 91 y = rect.top + offset; 92 } else { 93 x = e.clientX; 94 y = e.clientY; 95 } 96 97 cr.ui.positionPopupAtPoint(x, y, menu); 98 }, 99 100 /** 101 * Handles event callbacks. 102 * @param {!Event} e The event object. 103 */ 104 handleEvent: function(e) { 105 // Keep track of keydown state so that we can use that to determine the 106 // reason for the contextmenu event. 107 switch (e.type) { 108 case 'keydown': 109 this.keyIsDown_ = !e.ctrlKey && !e.altKey && 110 // context menu key or Shift-F10 111 (e.keyCode == 93 && !e.shiftKey || 112 e.keyIdentifier == 'F10' && e.shiftKey); 113 break; 114 115 case 'keyup': 116 this.keyIsDown_ = false; 117 break; 118 } 119 120 // Context menu is handled even when we have no menu. 121 if (e.type != 'contextmenu' && !this.menu) 122 return; 123 124 switch (e.type) { 125 case 'mousedown': 126 if (!this.menu.contains(e.target)) 127 this.hideMenu(); 128 else 129 e.preventDefault(); 130 break; 131 case 'keydown': 132 // keyIdentifier does not report 'Esc' correctly 133 if (e.keyCode == 27 /* Esc */) { 134 this.hideMenu(); 135 e.stopPropagation(); 136 e.preventDefault(); 137 138 // If the menu is visible we let it handle all the keyboard events. 139 } else if (this.menu) { 140 this.menu.handleKeyDown(e); 141 e.preventDefault(); 142 e.stopPropagation(); 143 } 144 break; 145 146 case 'activate': 147 case 'blur': 148 case 'resize': 149 this.hideMenu(); 150 break; 151 152 case 'contextmenu': 153 if ((!this.menu || !this.menu.contains(e.target)) && 154 (!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50)) 155 this.showMenu(e, e.currentTarget.contextMenu); 156 e.preventDefault(); 157 // Don't allow elements further up in the DOM to show their menus. 158 e.stopPropagation(); 159 break; 160 } 161 }, 162 163 /** 164 * Adds a contextMenu property to an element or element class. 165 * @param {!Element|!Function} element The element or class to add the 166 * contextMenu property to. 167 */ 168 addContextMenuProperty: function(element) { 169 if (typeof element == 'function') 170 element = element.prototype; 171 172 element.__defineGetter__('contextMenu', function() { 173 return this.contextMenu_; 174 }); 175 element.__defineSetter__('contextMenu', function(menu) { 176 var oldContextMenu = this.contextMenu; 177 178 if (typeof menu == 'string' && menu[0] == '#') { 179 menu = this.ownerDocument.getElementById(menu.slice(1)); 180 cr.ui.decorate(menu, Menu); 181 } 182 183 if (menu === oldContextMenu) 184 return; 185 186 if (oldContextMenu && !menu) { 187 this.removeEventListener('contextmenu', contextMenuHandler); 188 this.removeEventListener('keydown', contextMenuHandler); 189 this.removeEventListener('keyup', contextMenuHandler); 190 } 191 if (menu && !oldContextMenu) { 192 this.addEventListener('contextmenu', contextMenuHandler); 193 this.addEventListener('keydown', contextMenuHandler); 194 this.addEventListener('keyup', contextMenuHandler); 195 } 196 197 this.contextMenu_ = menu; 198 199 if (menu && menu.id) 200 this.setAttribute('contextmenu', '#' + menu.id); 201 202 cr.dispatchPropertyChange(this, 'contextMenu', menu, oldContextMenu); 203 }); 204 205 if (!element.getRectForContextMenu) { 206 /** 207 * @return {!ClientRect} The rect to use for positioning the context 208 * menu when the context menu is not opened using a mouse position. 209 */ 210 element.getRectForContextMenu = function() { 211 return this.getBoundingClientRect(); 212 }; 213 } 214 }, 215 216 /** 217 * Sets the given contextMenu to the given element. A contextMenu property 218 * would be added if necessary. 219 * @param {!Element} element The element or class to set the contextMenu to. 220 * @param {!cr.ui.Menu} contextMenu The contextMenu property to be set. 221 */ 222 setContextMenu: function(element, contextMenu) { 223 if (!element.contextMenu) 224 this.addContextMenuProperty(element); 225 element.contextMenu = contextMenu; 226 } 227 }; 228 229 /** 230 * The singleton context menu handler. 231 * @type {!ContextMenuHandler} 232 */ 233 var contextMenuHandler = new ContextMenuHandler; 234 235 // Export 236 return { 237 contextMenuHandler: contextMenuHandler 238 }; 239}); 240