/**
* Provides extended keyboard support for the "contextmenu" event such that:
* <ul>
* <li>The browser's default context menu is suppressed regardless of how the event is triggered.</li>
* <li>On Windows the "contextmenu" event is fired consistently regardless of whether the user
* pressed the Menu key or Shift + F10.</li>
* <li>When the "contextmenu" event is fired via the keyboard, the pageX, pageY, clientX and clientY
* properties reference the center of the event target. This makes it easy for "contextmenu" event listeners
* to position an overlay in response to the event by not having to worry about special handling of the x
* and y coordinates based on the device that fired the event.</li>
* <li>For Webkit and Gecko on the Mac it enables the use of the Shift + Control + Option + M keyboard
* shortcut to fire the "contextmenu" event, which (by default) is only available when VoiceOver
* (the screen reader on the Mac) is enabled.</li>
* <li>For Opera on the Mac it ensures the "contextmenu" event is fired when the user presses
* Shift + Command + M (Opera's context menu keyboard shortcut).</li>
* </ul>
* @module event-contextmenu
* @requires event
*/
var Event = Y.Event,
DOM = Y.DOM,
UA = Y.UA,
OS = Y.UA.os,
ie = UA.ie,
gecko = UA.gecko,
webkit = UA.webkit,
opera = UA.opera,
isWin = (OS === "windows"),
isMac = (OS === "macintosh"),
eventData = {},
conf = {
on: function (node, subscription, notifier, filter) {
var handles = [];
handles.push(Event._attach(["contextmenu", function (e) {
// Any developer listening for the "contextmenu" event is likely
// going to call preventDefault() to prevent the display of
// the browser's context menu. So, you know, save them a step.
e.preventDefault();
var id = Y.stamp(node),
data = eventData[id];
if (data) {
e.clientX = data.clientX;
e.clientY = data.clientY;
e.pageX = data.pageX;
e.pageY = data.pageY;
delete eventData[id];
}
notifier.fire(e);
}, node]));
handles.push(node[filter ? "delegate" : "on"]("keydown", function (e) {
var target = this.getDOMNode(),
shiftKey = e.shiftKey,
keyCode = e.keyCode,
shiftF10 = (shiftKey && keyCode == 121),
menuKey = (isWin && keyCode == 93),
ctrlKey = e.ctrlKey,
mKey = (keyCode === 77),
macWebkitAndGeckoShortcut = (isMac && (webkit || gecko) && ctrlKey && shiftKey && e.altKey && mKey),
// Note: The context menu keyboard shortcut for Opera on the Mac is Shift + Cmd (metaKey) + M,
// but e.metaKey is false for Opera, and Opera sets e.ctrlKey to true instead.
macOperaShortcut = (isMac && opera && ctrlKey && shiftKey && mKey),
clientX = 0,
clientY = 0,
scrollX,
scrollY,
pageX,
pageY,
xy,
x,
y;
if ((isWin && (shiftF10 || menuKey)) ||
(macWebkitAndGeckoShortcut || macOperaShortcut)) {
// Need to call preventDefault() here b/c:
// 1) To prevent IE's menubar from gaining focus when the
// user presses Shift + F10
// 2) In Firefox and Opera for Win, Shift + F10 will display a
// context menu, but won't fire the "contextmenu" event. So, need
// to call preventDefault() to prevent the display of the
// browser's context menu
// 3) For Opera on the Mac the context menu keyboard shortcut
// (Shift + Cmd + M) will display a context menu, but like Firefox
// and Opera on windows, Opera doesn't fire a "contextmenu" event,
// so preventDefault() is just used to supress Opera's
// default context menu.
if (((ie || (isWin && (gecko || opera))) && shiftF10) || macOperaShortcut) {
e.preventDefault();
}
xy = DOM.getXY(target);
x = xy[0];
y = xy[1];
scrollX = DOM.docScrollX();
scrollY = DOM.docScrollY();
// Protect against instances where xy and might not be returned,
// for example if the target is the document.
if (!Y.Lang.isUndefined(x)) {
clientX = (x + (target.offsetWidth/2)) - scrollX;
clientY = (y + (target.offsetHeight/2)) - scrollY;
}
pageX = clientX + scrollX;
pageY = clientY + scrollY;
// When the "contextmenu" event is fired from the keyboard
// clientX, clientY, pageX or pageY aren't set to useful
// values. So, we follow Safari's model here of setting
// the x & x coords to the center of the event target.
if (menuKey || (isWin && webkit && shiftF10)) {
eventData[Y.stamp(node)] = {
clientX: clientX,
clientY: clientY,
pageX: pageX,
pageY: pageY
};
}
// Don't need to call notifier.fire(e) when the Menu key
// is pressed as it fires the "contextmenu" event by default.
//
// In IE the call to preventDefault() for Shift + F10
// prevents the "contextmenu" event from firing, so we need
// to call notifier.fire(e)
//
// Need to also call notifier.fire(e) for Gecko and Opera since
// neither Shift + F10 or Shift + Cmd + M fire the "contextmenu" event.
//
// Lastly, also need to call notifier.fire(e) for all Mac browsers
// since neither Shift + Ctrl + Option + M (Webkit and Gecko) or
// Shift + Command + M (Opera) fire the "contextmenu" event.
if (((ie || (isWin && (gecko || opera))) && shiftF10) || isMac) {
e.clientX = clientX;
e.clientY = clientY;
e.pageX = pageX;
e.pageY = pageY;
notifier.fire(e);
}
}
}, filter));
subscription._handles = handles;
},
detach: function (node, subscription, notifier) {
Y.each(subscription._handles, function (handle) {
handle.detach();
});
}
};
conf.delegate = conf.on;
conf.detachDelegate = conf.detach;
Event.define("contextmenu", conf, true);