Commit 51428658 authored by lazyboy's avatar lazyboy Committed by Commit bot

<webview>: make context menus cancellable using JS API.

Expose <webview>.contextMenus.onShow
chrome EventTarget like object so one can "addListener" that can
preventDefault() to cancel bringing up <webview> context menu.

BUG=465733
Test=Open a chrome app with <webview>. E.g.
Load a <webview> in a chrome app, e.g.
the browser sample app:
https://github.com/GoogleChrome/chrome-app-samples/tree/master/samples/webview-samples/browser

Right click on the <webview>, context menu is expected to be shown.

Now open the app's inspector: from chrome://inspect, switch to "Apps" then select to inspect "Browser sample".

Register a listener to disable context menu:

document.querySelector('webview').contextMenus.onShow.addListener(function(e) {
  e.preventDefault();
});

Right click on the <webview> again, context menu should not show up.

Review URL: https://codereview.chromium.org/1026383003

Cr-Commit-Position: refs/heads/master@{#322477}
parent ad8553a8
......@@ -130,6 +130,39 @@ class WebContentsHiddenObserver : public content::WebContentsObserver {
DISALLOW_COPY_AND_ASSIGN(WebContentsHiddenObserver);
};
// Watches for context menu to be shown, records count of how many times
// context menu was shown.
class ContextMenuCallCountObserver {
public:
ContextMenuCallCountObserver ()
: num_times_shown_(0),
menu_observer_(chrome::NOTIFICATION_RENDER_VIEW_CONTEXT_MENU_SHOWN,
base::Bind(&ContextMenuCallCountObserver::OnMenuShown,
base::Unretained(this))) {
}
~ContextMenuCallCountObserver() {}
bool OnMenuShown(const content::NotificationSource& source,
const content::NotificationDetails& details) {
++num_times_shown_;
auto context_menu = content::Source<RenderViewContextMenu>(source).ptr();
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(&RenderViewContextMenuBase::Cancel,
base::Unretained(context_menu)));
return true;
}
void Wait() { menu_observer_.Wait(); }
int num_times_shown() { return num_times_shown_; }
private:
int num_times_shown_;
content::WindowedNotificationObserver menu_observer_;
DISALLOW_COPY_AND_ASSIGN(ContextMenuCallCountObserver);
};
class EmbedderWebContentsObserver : public content::WebContentsObserver {
public:
explicit EmbedderWebContentsObserver(content::WebContents* web_contents)
......@@ -699,6 +732,17 @@ class WebViewTest : public extensions::PlatformAppBrowserTest {
}
}
void OpenContextMenu(content::WebContents* web_contents) {
blink::WebMouseEvent mouse_event;
mouse_event.type = blink::WebInputEvent::MouseDown;
mouse_event.button = blink::WebMouseEvent::ButtonRight;
mouse_event.x = 1;
mouse_event.y = 1;
web_contents->GetRenderViewHost()->ForwardMouseEvent(mouse_event);
mouse_event.type = blink::WebInputEvent::MouseUp;
web_contents->GetRenderViewHost()->ForwardMouseEvent(mouse_event);
}
content::WebContents* GetGuestWebContents() {
return guest_web_contents_;
}
......@@ -1973,6 +2017,36 @@ static bool ContextMenuNotificationCallback(
return true;
}
IN_PROC_BROWSER_TEST_F(WebViewTest, ContextMenusAPI_PreventDefault) {
LoadAppWithGuest("web_view/context_menus/basic");
content::WebContents* guest_web_contents = GetGuestWebContents();
content::WebContents* embedder = GetEmbedderWebContents();
ASSERT_TRUE(embedder);
// Add a preventDefault() call on context menu event so context menu
// does not show up.
ExtensionTestMessageListener prevent_default_listener(
"WebViewTest.CONTEXT_MENU_DEFAULT_PREVENTED", false);
EXPECT_TRUE(content::ExecuteScript(embedder, "registerPreventDefault()"));
ContextMenuCallCountObserver context_menu_shown_observer;
OpenContextMenu(guest_web_contents);
EXPECT_TRUE(prevent_default_listener.WaitUntilSatisfied());
// Expect the menu to not show up.
EXPECT_EQ(0, context_menu_shown_observer.num_times_shown());
// Now remove the preventDefault() and expect context menu to be shown.
ExecuteScriptWaitForTitle(
embedder, "removePreventDefault()", "PREVENT_DEFAULT_LISTENER_REMOVED");
OpenContextMenu(guest_web_contents);
// We expect to see a context menu for the second call to |OpenContextMenu|.
context_menu_shown_observer.Wait();
EXPECT_EQ(1, context_menu_shown_observer.num_times_shown());
}
// Tests that a context menu is created when right-clicking in the webview. This
// also tests that the 'contextmenu' event is handled correctly.
IN_PROC_BROWSER_TEST_F(WebViewTest, TestContextMenu) {
......@@ -1984,15 +2058,7 @@ IN_PROC_BROWSER_TEST_F(WebViewTest, TestContextMenu) {
chrome::NOTIFICATION_RENDER_VIEW_CONTEXT_MENU_SHOWN,
base::Bind(ContextMenuNotificationCallback));
// Open a context menu.
blink::WebMouseEvent mouse_event;
mouse_event.type = blink::WebInputEvent::MouseDown;
mouse_event.button = blink::WebMouseEvent::ButtonRight;
mouse_event.x = 1;
mouse_event.y = 1;
guest_web_contents->GetRenderViewHost()->ForwardMouseEvent(mouse_event);
mouse_event.type = blink::WebInputEvent::MouseUp;
guest_web_contents->GetRenderViewHost()->ForwardMouseEvent(mouse_event);
OpenContextMenu(guest_web_contents);
// Wait for the context menu to be visible.
menu_observer.Wait();
......
......@@ -54,7 +54,7 @@ bool ChromeWebViewGuestDelegate::HandleContextMenu(
args->Set(webview::kContextMenuItems, items.release());
args->SetInteger(webview::kRequestId, request_id);
web_view_guest()->DispatchEventToView(
new GuestViewBase::Event(webview::kEventContextMenu, args.Pass()));
new GuestViewBase::Event(webview::kEventContextMenuShow, args.Pass()));
return true;
}
......
......@@ -288,6 +288,25 @@
"type": "function",
"nodoc": true,
"$ref": "contextMenusInternal.onClicked"
},
{
"name": "onShow",
"type": "function",
"description": "Fired when context menu is about to be shown. Provides the ability to cancel the context menu by calling <code>event.preventDefault()</code> from this handler.",
"nodoc": true,
"parameters": [
{
"name": "event",
"type": "object",
"properties": {
"preventDefault": {
"type": "function",
"parameters": [
]
}
}
}
]
}
]
}
......
......@@ -12,18 +12,8 @@ var CreateEvent = require('guestViewEvents').CreateEvent;
var EventBindings = require('event_bindings');
var idGeneratorNatives = requireNative('id_generator');
var Utils = require('utils');
var WebViewEvents = require('webViewEvents').WebViewEvents;
var WebViewImpl = require('webView').WebViewImpl;
var CHROME_WEB_VIEW_EVENTS = {
'contextmenushown': {
evt: CreateEvent('chromeWebViewInternal.contextmenu'),
cancelable: true,
fields: ['items'],
handler: 'handleContextMenu'
}
};
// This is the only "webViewInternal.onClicked" named event for this renderer.
//
// Since we need an event per <webview>, we define events with suffix
......@@ -33,6 +23,9 @@ var CHROME_WEB_VIEW_EVENTS = {
// it to the subEvent's listeners. This way
// <webview>.contextMenus.onClicked behave as a regular chrome Event type.
var ContextMenusEvent = CreateEvent('chromeWebViewInternal.onClicked');
// See comment above.
var ContextMenusHandlerEvent =
CreateEvent('chromeWebViewInternal.onContextMenuShow');
// -----------------------------------------------------------------------------
// ContextMenusOnClickedEvent object.
......@@ -58,6 +51,41 @@ function ContextMenusOnClickedEvent(opt_eventName,
ContextMenusOnClickedEvent.prototype.__proto__ = EventBindings.Event.prototype;
function ContextMenusOnContextMenuEvent(webViewImpl,
opt_eventName,
opt_argSchemas,
opt_eventOptions,
opt_webViewInstanceId) {
var subEventName = GetUniqueSubEventName(opt_eventName);
EventBindings.Event.call(this,
subEventName,
opt_argSchemas,
opt_eventOptions,
opt_webViewInstanceId);
var defaultPrevented = false;
ContextMenusHandlerEvent.addListener(function(e) {
var defaultPrevented = false;
var event = {
'preventDefault': function() { defaultPrevented = true; }
};
// Re-dispatch to subEvent's listeners.
$Function.apply(this.dispatch, this, [event]);
if (!defaultPrevented) {
// TODO(lazyboy): Remove |items| parameter completely from
// ChromeWebView.showContextMenu as we don't do anything useful with it
// currently.
var items = [];
ChromeWebView.showContextMenu(
webViewImpl.guest.getId(), e.requestId, items);
}
}.bind(this), {instanceId: opt_webViewInstanceId || 0});
}
ContextMenusOnContextMenuEvent.prototype.__proto__ =
EventBindings.Event.prototype;
// -----------------------------------------------------------------------------
// WebViewContextMenusImpl object.
......@@ -93,6 +121,15 @@ var WebViewContextMenus = Utils.expose(
// -----------------------------------------------------------------------------
WebViewImpl.prototype.maybeSetupContextMenus = function() {
if (!this.contextMenusOnContextMenuEvent_) {
var eventName = 'chromeWebViewInternal.onContextMenuShow';
// TODO(lazyboy): Find event by name instead of events[1].
var eventSchema = ChromeWebViewSchema.events[1];
var eventOptions = {supportsListeners: true};
this.contextMenusOnContextMenuEvent_ = new ContextMenusOnContextMenuEvent(
this, eventName, eventSchema, eventOptions, this.viewInstanceId);
}
var createContextMenus = function() {
return function() {
if (this.contextMenus_) {
......@@ -117,17 +154,27 @@ WebViewImpl.prototype.maybeSetupContextMenus = function() {
return this.contextMenusOnClickedEvent_;
}.bind(this);
}.bind(this);
Object.defineProperty(
$Object.defineProperty(
this.contextMenus_,
'onClicked',
{get: getOnClickedEvent(), enumerable: true});
$Object.defineProperty(
this.contextMenus_,
'onShow',
{
get: function() {
return this.contextMenusOnContextMenuEvent_;
}.bind(this),
enumerable: true
});
return this.contextMenus_;
}.bind(this);
}.bind(this);
// Expose <webview>.contextMenus object.
Object.defineProperty(
// TODO(lazyboy): Add documentation for contextMenus:
// http://crbug.com/470979.
$Object.defineProperty(
this.element,
'contextMenus',
{
......@@ -136,52 +183,6 @@ WebViewImpl.prototype.maybeSetupContextMenus = function() {
});
};
WebViewEvents.prototype.handleContextMenu = function(event, eventName) {
var webViewEvent = this.makeDomEvent(event, eventName);
var requestId = event.requestId;
// Construct the event.menu object.
var actionTaken = false;
var validateCall = function() {
var ERROR_MSG_CONTEXT_MENU_ACTION_ALREADY_TAKEN = '<webview>: ' +
'An action has already been taken for this "contextmenu" event.';
if (actionTaken) {
throw new Error(ERROR_MSG_CONTEXT_MENU_ACTION_ALREADY_TAKEN);
}
actionTaken = true;
};
var menu = {
show: function(items) {
validateCall();
// TODO(lazyboy): WebViewShowContextFunction doesn't do anything useful
// with |items|, implement.
ChromeWebView.showContextMenu(this.view.guest.getId(), requestId, items);
}.bind(this)
};
webViewEvent.menu = menu;
var element = this.view.element;
var defaultPrevented = !element.dispatchEvent(webViewEvent);
if (actionTaken) {
return;
}
if (!defaultPrevented) {
actionTaken = true;
// The default action is equivalent to just showing the context menu as is.
ChromeWebView.showContextMenu(
this.view.guest.getId(), requestId, undefined);
// TODO(lazyboy): Figure out a way to show warning message only when
// listeners are registered for this event.
} // else we will ignore showing the context menu completely.
};
function GetUniqueSubEventName(eventName) {
return eventName + '/' + idGeneratorNatives.GetNextId();
}
// Exposes |CHROME_WEB_VIEW_EVENTS| when the ChromeWebView API is available.
(function() {
for (var eventName in CHROME_WEB_VIEW_EVENTS) {
WebViewEvents.EVENTS[eventName] = CHROME_WEB_VIEW_EVENTS[eventName];
}
})();
......@@ -105,6 +105,26 @@ ContextMenuTester.prototype.testRemoveAllItems = function() {
});
};
ContextMenuTester.prototype.registerPreventDefault = function() {
this.preventDefaultLister_ = function(e) {
e.preventDefault();
this.proceedTest_('DEFAULT_PREVENTED');
}.bind(this);
this.webview_.contextMenus.onShow.addListener(this.preventDefaultLister_);
};
ContextMenuTester.prototype.removePreventDefault = function() {
if (!this.preventDefaultLister_) {
LOG('Error: A listener is expected to setup prior to calling ' +
'contextMenus.onShow');
return;
}
this.webview_.contextMenus.onShow.removeListener(this.preventDefaultLister_);
this.proceedTest_('PREVENT_DEFAULT_LISTENER_REMOVED');
};
ContextMenuTester.prototype.onClick_ = function(type) {
if (type == 'global') {
this.globalClickCalled_ = true;
......@@ -146,6 +166,12 @@ ContextMenuTester.prototype.proceedTest_ = function(step) {
case 'ITEM_ALL_REMOVED':
document.title = 'ITEM_ALL_REMOVED';
break;
case 'DEFAULT_PREVENTED':
chrome.test.sendMessage('WebViewTest.CONTEXT_MENU_DEFAULT_PREVENTED');
break;
case 'PREVENT_DEFAULT_LISTENER_REMOVED':
document.title = 'PREVENT_DEFAULT_LISTENER_REMOVED';
break;
default:
break;
}
......@@ -187,9 +213,14 @@ window.createThreeMenuItems = function() {
tester.createThreeMenuItems();
};
window.removeAllItems = function() {
LOG('window.testRemoveAllItems');
tester.testRemoveAllItems();
};
window.registerPreventDefault = function() {
tester.registerPreventDefault();
};
window.removePreventDefault = function() {
tester.removePreventDefault();
};
// window.* exported functions end.
function setUpTest(messageCallback) {
......
......@@ -24,7 +24,7 @@ const char kAPILoadDataInvalidVirtualURL[] = "Invalid virtual URL \"%s\".";
const char kEventClose[] = "webViewInternal.onClose";
const char kEventConsoleMessage[] = "webViewInternal.onConsoleMessage";
const char kEventContentLoad[] = "webViewInternal.onContentLoad";
const char kEventContextMenu[] = "chromeWebViewInternal.contextmenu";
const char kEventContextMenuShow[] = "chromeWebViewInternal.onContextMenuShow";
const char kEventDialog[] = "webViewInternal.onDialog";
const char kEventDropLink[] = "webViewInternal.onDropLink";
const char kEventExit[] = "webViewInternal.onExit";
......
......@@ -28,7 +28,7 @@ extern const char kAPILoadDataInvalidVirtualURL[];
extern const char kEventClose[];
extern const char kEventConsoleMessage[];
extern const char kEventContentLoad[];
extern const char kEventContextMenu[];
extern const char kEventContextMenuShow[];
extern const char kEventDialog[];
extern const char kEventDropLink[];
extern const char kEventExit[];
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment