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