blob: 5ac0789c9b46c577e642ba194fd32b158ccbb165 [file] [log] [blame]
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('cr.ui', function() {
/**
* Handles context menus.
* @constructor
*/
function ContextMenuHandler() {}
ContextMenuHandler.prototype = {
/**
* The menu that we are currently showing.
* @type {cr.ui.Menu}
*/
menu_: null,
get menu() {
return this.menu_;
},
/**
* Shows a menu as a context menu.
* @param {!Event} e The event triggering the show (usally a contextmenu
* event).
* @param {!cr.ui.Menu} menu The menu to show.
*/
showMenu: function(e, menu) {
this.menu_ = menu;
menu.style.display = 'block';
// when the menu is shown we steal all keyboard events.
menu.ownerDocument.addEventListener('keydown', this, true);
menu.ownerDocument.addEventListener('mousedown', this, true);
menu.ownerDocument.addEventListener('blur', this, true);
menu.addEventListener('activate', this);
this.positionMenu_(e, menu);
},
/**
* Hide the currently shown menu.
*/
hideMenu: function() {
var menu = this.menu;
if (!menu)
return;
menu.style.display = 'none';
menu.ownerDocument.removeEventListener('keydown', this, true);
menu.ownerDocument.removeEventListener('mousedown', this, true);
menu.ownerDocument.removeEventListener('blur', this, true);
menu.removeEventListener('activate', this);
menu.selectedIndex = -1;
this.menu_ = null;
// On windows we might hide the menu in a right mouse button up and if
// that is the case we wait some short period before we allow the menu
// to be shown again.
this.hideTimestamp_ = Date.now();
},
/**
* Positions the menu
* @param {!Event} e The event object triggering the showing.
* @param {!cr.ui.Menu} menu The menu to position.
* @private
*/
positionMenu_: function(e, menu) {
// TODO(arv): Handle scrolled documents when needed.
var x, y;
// When the user presses the context menu key (on the keyboard) we need
// to detect this.
if (e.screenX == 0 && e.screenY == 0) {
var rect = e.currentTarget.getBoundingClientRect();
x = rect.left;
y = rect.top;
} else {
x = e.clientX;
y = e.clientY;
}
var menuRect = menu.getBoundingClientRect();
var bodyRect = menu.ownerDocument.body.getBoundingClientRect();
// Does menu fit below?
if (y + menuRect.height > bodyRect.height) {
// Does menu fit above?
if (y - menuRect.height >= 0) {
y -= menuRect.height;
} else {
// Menu did not fit above nor below.
y = 0;
// We could resize the menu here but lets not worry about that at this
// point.
}
}
// Does menu fit to the right?
if (x + menuRect.width > bodyRect.width) {
// Does menu fit to the left?
if (x - menuRect.width >= 0) {
x -= menuRect.width;
} else {
// Menu did not fit to the right nor to the left.
x = 0;
// We could resize the menu here but lets not worry about that at this
// point.
}
}
menu.style.left = x + 'px';
menu.style.top = y + 'px';
},
/**
* Handles event callbacks.
* @param {!Event} e The event object.
*/
handleEvent: function(e) {
// Context menu is handled even when we have no menu.
if (e.type != 'contextmenu' && !this.menu)
return;
switch (e.type) {
case 'mousedown':
if (!this.menu.contains(e.target))
this.hideMenu();
else
e.preventDefault();
break;
case 'keydown':
// keyIdentifier does not report 'Esc' correctly
if (e.keyCode == 27 /* Esc */) {
this.hideMenu();
// If the menu is visible we let it handle all the keyboard events.
} else if (this.menu) {
this.menu.handleKeyDown(e);
e.preventDefault();
e.stopPropagation();
}
break;
case 'activate':
case 'blur':
this.hideMenu();
break;
case 'contextmenu':
if ((!this.menu || !this.menu.contains(e.target)) &&
(!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50))
this.showMenu(e, e.currentTarget.contextMenu);
e.preventDefault();
// Don't allow elements further up in the DOM to show their menus.
e.stopPropagation();
break;
}
},
/**
* Adds a contextMenu property to an element or element class.
* @param {!Element|!Function} element The element or class to add the
* contextMenu property to.
*/
addContextMenuProperty: function(element) {
if (typeof element == 'function')
element = element.prototype;
element.__defineGetter__('contextMenu', function() {
return this.contextMenu_;
});
element.__defineSetter__('contextMenu', function(menu) {
var oldContextMenu = this.contextMenu;
if (typeof menu == 'string' && menu[0] == '#') {
menu = this.ownerDocument.getElementById(menu.slice(1));
cr.ui.decorate(menu, Menu);
}
if (menu === oldContextMenu)
return;
if (oldContextMenu && !menu)
this.removeEventListener('contextmenu', contextMenuHandler);
if (menu && !oldContextMenu)
this.addEventListener('contextmenu', contextMenuHandler);
this.contextMenu_ = menu;
if (menu && menu.id)
this.setAttribute('contextmenu', '#' + menu.id);
cr.dispatchPropertyChange(this, 'contextMenu', menu, oldContextMenu);
});
}
};
/**
* The singleton context menu handler.
* @type {!ContextMenuHandler}
*/
var contextMenuHandler = new ContextMenuHandler;
// Export
return {
contextMenuHandler: contextMenuHandler
};
});