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.
5 // require: event_target.js
7 cr.define('cr.ui', function() {
8 /** @const */ var EventTarget = cr.EventTarget;
9 /** @const */ var Menu = cr.ui.Menu;
12 * Handles context menus.
14 * @extends {EventTarget}
16 function ContextMenuHandler() {
17 this.showingEvents_ = new EventTracker();
20 ContextMenuHandler.prototype = {
21 __proto__: EventTarget.prototype,
24 * The menu that we are currently showing.
33 * Shows a menu as a context menu.
34 * @param {!Event} e The event triggering the show (usually a contextmenu
36 * @param {!cr.ui.Menu} menu The menu to show.
38 showMenu: function(e, menu) {
39 menu.updateCommands(e.currentTarget);
40 if (!menu.hasVisibleItems())
44 menu.classList.remove('hide-delayed');
46 menu.contextElement = e.currentTarget;
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, 'focus', this);
54 this.showingEvents_.add(win, 'popstate', this);
55 this.showingEvents_.add(win, 'resize', this);
56 this.showingEvents_.add(win, 'blur', this);
57 this.showingEvents_.add(menu, 'contextmenu', this);
58 this.showingEvents_.add(menu, 'activate', this);
59 this.positionMenu_(e, menu);
61 var ev = new Event('show');
62 ev.element = menu.contextElement;
64 this.dispatchEvent(ev);
68 * Hide the currently shown menu.
69 * @param {HideType=} opt_hideType Type of hide.
70 * default: cr.ui.HideType.INSTANT.
72 hideMenu: function(opt_hideType) {
77 if (opt_hideType == cr.ui.HideType.DELAYED)
78 menu.classList.add('hide-delayed');
80 menu.classList.remove('hide-delayed');
82 var originalContextElement = menu.contextElement;
83 menu.contextElement = null;
84 this.showingEvents_.removeAll();
85 menu.selectedIndex = -1;
88 // On windows we might hide the menu in a right mouse button up and if
89 // that is the case we wait some short period before we allow the menu
91 this.hideTimestamp_ = cr.isWindows ? Date.now() : 0;
93 var ev = new Event('hide');
94 ev.element = menu.contextElement;
96 this.dispatchEvent(ev);
101 * @param {!Event} e The event object triggering the showing.
102 * @param {!cr.ui.Menu} menu The menu to position.
105 positionMenu_: function(e, menu) {
106 // TODO(arv): Handle scrolled documents when needed.
108 var element = e.currentTarget;
110 // When the user presses the context menu key (on the keyboard) we need
112 if (this.keyIsDown_) {
113 var rect = element.getRectForContextMenu ?
114 element.getRectForContextMenu() :
115 element.getBoundingClientRect();
116 var offset = Math.min(rect.width, rect.height) / 2;
117 x = rect.left + offset;
118 y = rect.top + offset;
124 cr.ui.positionPopupAtPoint(x, y, menu);
128 * Handles event callbacks.
129 * @param {!Event} e The event object.
131 handleEvent: function(e) {
132 // Keep track of keydown state so that we can use that to determine the
133 // reason for the contextmenu event.
136 this.keyIsDown_ = !e.ctrlKey && !e.altKey &&
137 // context menu key or Shift-F10
138 (e.keyCode == 93 && !e.shiftKey ||
139 e.keyIdentifier == 'F10' && e.shiftKey);
143 this.keyIsDown_ = false;
147 // Context menu is handled even when we have no menu.
148 if (e.type != 'contextmenu' && !this.menu)
153 if (!this.menu.contains(e.target))
159 // keyIdentifier does not report 'Esc' correctly
160 if (e.keyCode == 27 /* Esc */) {
165 // If the menu is visible we let it handle all the keyboard events.
166 } else if (this.menu) {
167 this.menu.handleKeyDown(e);
174 var hideDelayed = e.target instanceof cr.ui.MenuItem &&
176 this.hideMenu(hideDelayed ? cr.ui.HideType.DELAYED :
177 cr.ui.HideType.INSTANT);
181 if (!this.menu.contains(e.target))
195 if ((!this.menu || !this.menu.contains(e.target)) &&
196 (!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50))
197 this.showMenu(e, e.currentTarget.contextMenu);
199 // Don't allow elements further up in the DOM to show their menus.
206 * Adds a contextMenu property to an element or element class.
207 * @param {!Element|!Function} element The element or class to add the
208 * contextMenu property to.
210 addContextMenuProperty: function(element) {
211 if (typeof element == 'function')
212 element = element.prototype;
214 element.__defineGetter__('contextMenu', function() {
215 return this.contextMenu_;
217 element.__defineSetter__('contextMenu', function(menu) {
218 var oldContextMenu = this.contextMenu;
220 if (typeof menu == 'string' && menu[0] == '#') {
221 menu = this.ownerDocument.getElementById(menu.slice(1));
222 cr.ui.decorate(menu, Menu);
225 if (menu === oldContextMenu)
228 if (oldContextMenu && !menu) {
229 this.removeEventListener('contextmenu', contextMenuHandler);
230 this.removeEventListener('keydown', contextMenuHandler);
231 this.removeEventListener('keyup', contextMenuHandler);
233 if (menu && !oldContextMenu) {
234 this.addEventListener('contextmenu', contextMenuHandler);
235 this.addEventListener('keydown', contextMenuHandler);
236 this.addEventListener('keyup', contextMenuHandler);
239 this.contextMenu_ = menu;
242 this.setAttribute('contextmenu', '#' + menu.id);
244 cr.dispatchPropertyChange(this, 'contextMenu', menu, oldContextMenu);
247 if (!element.getRectForContextMenu) {
249 * @return {!ClientRect} The rect to use for positioning the context
250 * menu when the context menu is not opened using a mouse position.
252 element.getRectForContextMenu = function() {
253 return this.getBoundingClientRect();
259 * Sets the given contextMenu to the given element. A contextMenu property
260 * would be added if necessary.
261 * @param {!Element} element The element or class to set the contextMenu to.
262 * @param {!cr.ui.Menu} contextMenu The contextMenu property to be set.
264 setContextMenu: function(element, contextMenu) {
265 if (!element.contextMenu)
266 this.addContextMenuProperty(element);
267 element.contextMenu = contextMenu;
272 * The singleton context menu handler.
273 * @type {!ContextMenuHandler}
275 var contextMenuHandler = new ContextMenuHandler;
279 contextMenuHandler: contextMenuHandler,