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