1 // Copyright 2014 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 // Event management for WebViewInternal.
7 var DeclarativeWebRequestSchema =
8 requireNative('schema_registry').GetSchema('declarativeWebRequest');
9 var EventBindings = require('event_bindings');
10 var IdGenerator = requireNative('id_generator');
11 var MessagingNatives = requireNative('messaging_natives');
12 var WebRequestEvent = require('webRequestInternal').WebRequestEvent;
13 var WebRequestSchema =
14 requireNative('schema_registry').GetSchema('webRequest');
15 var WebView = require('webViewInternal').WebView;
17 var CreateEvent = function(name) {
18 var eventOpts = {supportsListeners: true, supportsFilters: true};
19 return new EventBindings.Event(name, undefined, eventOpts);
22 var FrameNameChangedEvent = CreateEvent('webViewInternal.onFrameNameChanged');
23 var WebRequestMessageEvent = CreateEvent('webViewInternal.onMessage');
25 // WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their
26 // associated extension event descriptor objects.
27 // An event listener will be attached to the extension event |evt| specified in
29 // |fields| specifies the public-facing fields in the DOM event that are
30 // accessible to <webview> developers.
31 // |customHandler| allows a handler function to be called each time an extension
32 // event is caught by its event listener. The DOM event should be dispatched
33 // within this handler function. With no handler function, the DOM event
34 // will be dispatched by default each time the extension event is caught.
35 // |cancelable| (default: false) specifies whether the event's default
36 // behavior can be canceled. If the default action associated with the event
37 // is prevented, then its dispatch function will return false in its event
38 // handler. The event must have a custom handler for this to be meaningful.
39 var WEB_VIEW_EVENTS = {
41 evt: CreateEvent('webViewInternal.onClose'),
45 evt: CreateEvent('webViewInternal.onConsoleMessage'),
46 fields: ['level', 'message', 'line', 'sourceId']
49 evt: CreateEvent('webViewInternal.onContentLoad'),
53 evt: CreateEvent('webViewInternal.contextmenu'),
55 customHandler: function(handler, event, webViewEvent) {
56 handler.handleContextMenu(event, webViewEvent);
62 customHandler: function(handler, event, webViewEvent) {
63 handler.handleDialogEvent(event, webViewEvent);
65 evt: CreateEvent('webViewInternal.onDialog'),
66 fields: ['defaultPromptText', 'messageText', 'messageType', 'url']
69 evt: CreateEvent('webViewInternal.onExit'),
70 fields: ['processId', 'reason']
73 evt: CreateEvent('webViewInternal.onFindReply'),
85 customHandler: function(handler, event, webViewEvent) {
86 handler.handleLoadAbortEvent(event, webViewEvent);
88 evt: CreateEvent('webViewInternal.onLoadAbort'),
89 fields: ['url', 'isTopLevel', 'reason']
92 customHandler: function(handler, event, webViewEvent) {
93 handler.handleLoadCommitEvent(event, webViewEvent);
95 evt: CreateEvent('webViewInternal.onLoadCommit'),
96 fields: ['url', 'isTopLevel']
99 evt: CreateEvent('webViewInternal.onLoadProgress'),
100 fields: ['url', 'progress']
103 evt: CreateEvent('webViewInternal.onLoadRedirect'),
104 fields: ['isTopLevel', 'oldUrl', 'newUrl']
107 evt: CreateEvent('webViewInternal.onLoadStart'),
108 fields: ['url', 'isTopLevel']
111 evt: CreateEvent('webViewInternal.onLoadStop'),
116 customHandler: function(handler, event, webViewEvent) {
117 handler.handleNewWindowEvent(event, webViewEvent);
119 evt: CreateEvent('webViewInternal.onNewWindow'),
124 'windowOpenDisposition',
128 'permissionrequest': {
130 customHandler: function(handler, event, webViewEvent) {
131 handler.handlePermissionEvent(event, webViewEvent);
133 evt: CreateEvent('webViewInternal.onPermissionRequest'),
136 'lastUnlockedBySelf',
145 evt: CreateEvent('webViewInternal.onResponsive'),
146 fields: ['processId']
149 evt: CreateEvent('webViewInternal.onSizeChanged'),
150 customHandler: function(handler, event, webViewEvent) {
151 handler.handleSizeChangedEvent(event, webViewEvent);
153 fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
156 evt: CreateEvent('webViewInternal.onUnresponsive'),
157 fields: ['processId']
160 evt: CreateEvent('webViewInternal.onZoomChange'),
161 fields: ['oldZoomFactor', 'newZoomFactor']
165 function DeclarativeWebRequestEvent(opt_eventName,
168 opt_webViewInstanceId) {
169 var subEventName = opt_eventName + '/' + IdGenerator.GetNextId();
170 EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions,
171 opt_webViewInstanceId);
174 // TODO(lazyboy): When do we dispose this listener?
175 WebRequestMessageEvent.addListener(function() {
176 // Re-dispatch to subEvent's listeners.
177 $Function.apply(self.dispatch, self, $Array.slice(arguments));
178 }, {instanceId: opt_webViewInstanceId || 0});
181 DeclarativeWebRequestEvent.prototype = {
182 __proto__: EventBindings.Event.prototype
186 function WebViewEvents(webViewInternal, viewInstanceId) {
187 this.webViewInternal = webViewInternal;
188 this.viewInstanceId = viewInstanceId;
193 WebViewEvents.prototype.setup = function() {
194 this.setupFrameNameChangedEvent();
195 this.setupWebRequestEvents();
196 this.webViewInternal.setupExperimentalContextMenus();
198 var events = this.getEvents();
199 for (var eventName in events) {
200 this.setupEvent(eventName, events[eventName]);
204 WebViewEvents.prototype.setupFrameNameChangedEvent = function() {
206 FrameNameChangedEvent.addListener(function(e) {
207 self.webViewInternal.onFrameNameChanged(e.name);
208 }, {instanceId: self.viewInstanceId});
211 WebViewEvents.prototype.setupWebRequestEvents = function() {
214 var createWebRequestEvent = function(webRequestEvent) {
216 if (!self[webRequestEvent.name]) {
217 self[webRequestEvent.name] =
219 'webViewInternal.' + webRequestEvent.name,
220 webRequestEvent.parameters,
221 webRequestEvent.extraParameters, webRequestEvent.options,
222 self.viewInstanceId);
224 return self[webRequestEvent.name];
228 var createDeclarativeWebRequestEvent = function(webRequestEvent) {
230 if (!self[webRequestEvent.name]) {
231 // The onMessage event gets a special event type because we want
232 // the listener to fire only for messages targeted for this particular
234 var EventClass = webRequestEvent.name === 'onMessage' ?
235 DeclarativeWebRequestEvent : EventBindings.Event;
236 self[webRequestEvent.name] =
238 'webViewInternal.' + webRequestEvent.name,
239 webRequestEvent.parameters,
240 webRequestEvent.options,
241 self.viewInstanceId);
243 return self[webRequestEvent.name];
247 for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) {
248 var eventSchema = DeclarativeWebRequestSchema.events[i];
249 var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema);
250 Object.defineProperty(
254 get: webRequestEvent,
260 // Populate the WebRequest events from the API definition.
261 for (var i = 0; i < WebRequestSchema.events.length; ++i) {
262 var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
263 Object.defineProperty(
265 WebRequestSchema.events[i].name,
267 get: webRequestEvent,
273 this.webViewInternal.setRequestPropertyOnWebViewNode(request);
276 WebViewEvents.prototype.getEvents = function() {
277 var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents();
278 for (var eventName in experimentalEvents) {
279 WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
281 return WEB_VIEW_EVENTS;
284 WebViewEvents.prototype.setupEvent = function(name, info) {
286 info.evt.addListener(function(e) {
287 var details = {bubbles:true};
289 details.cancelable = true;
290 var webViewEvent = new Event(name, details);
291 $Array.forEach(info.fields, function(field) {
292 if (e[field] !== undefined) {
293 webViewEvent[field] = e[field];
296 if (info.customHandler) {
297 info.customHandler(self, e, webViewEvent);
300 self.webViewInternal.dispatchEvent(webViewEvent);
301 }, {instanceId: self.viewInstanceId});
303 this.webViewInternal.setupEventProperty(name);
308 WebViewEvents.prototype.handleContextMenu = function(e, webViewEvent) {
309 this.webViewInternal.maybeHandleContextMenu(e, webViewEvent);
312 WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) {
313 var showWarningMessage = function(dialogType) {
314 var VOWELS = ['a', 'e', 'i', 'o', 'u'];
315 var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.';
316 var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A';
317 var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article);
318 output = output.replace('%2', dialogType);
319 window.console.warn(output);
323 var requestId = event.requestId;
324 var actionTaken = false;
326 var validateCall = function() {
327 var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' +
328 'An action has already been taken for this "dialog" event.';
331 throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN);
336 var getInstanceId = function() {
337 return self.webViewInternal.getInstanceId();
341 ok: function(user_input) {
343 user_input = user_input || '';
344 WebView.setPermission(getInstanceId(), requestId, 'allow', user_input);
348 WebView.setPermission(getInstanceId(), requestId, 'deny');
351 webViewEvent.dialog = dialog;
353 var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
358 if (defaultPrevented) {
359 // Tell the JavaScript garbage collector to track lifetime of |dialog| and
360 // call back when the dialog object has been collected.
361 MessagingNatives.BindToGC(dialog, function() {
362 // Avoid showing a warning message if the decision has already been made.
366 WebView.setPermission(
367 getInstanceId(), requestId, 'default', '', function(allowed) {
371 showWarningMessage(event.messageType);
376 // The default action is equivalent to canceling the dialog.
377 WebView.setPermission(
378 getInstanceId(), requestId, 'default', '', function(allowed) {
382 showWarningMessage(event.messageType);
387 WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) {
388 var showWarningMessage = function(reason) {
389 var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
390 'The load has aborted with reason "%1".';
391 window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
393 if (this.webViewInternal.dispatchEvent(webViewEvent)) {
394 showWarningMessage(event.reason);
398 WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) {
399 this.webViewInternal.onLoadCommit(event.currentEntryIndex, event.entryCount,
400 event.processId, event.url,
402 this.webViewInternal.dispatchEvent(webViewEvent);
405 WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) {
406 var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
407 'An action has already been taken for this "newwindow" event.';
409 var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
410 'Unable to attach the new window to the provided webViewInternal.';
412 var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
414 var showWarningMessage = function() {
415 var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
416 window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
419 var requestId = event.requestId;
420 var actionTaken = false;
422 var getInstanceId = function() {
423 return self.webViewInternal.getInstanceId();
426 var validateCall = function () {
428 throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
434 attach: function(webview) {
436 if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW')
437 throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
438 // Attach happens asynchronously to give the tagWatcher an opportunity
439 // to pick up the new webview before attach operates on it, if it hasn't
440 // been attached to the DOM already.
441 // Note: Any subsequent errors cannot be exceptions because they happen
443 setTimeout(function() {
444 var webViewInternal = privates(webview).internal;
445 // Update the partition.
446 if (event.storagePartitionId) {
447 webViewInternal.onAttach(event.storagePartitionId);
450 var attached = webViewInternal.attachWindow(event.windowId, true);
453 window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
455 // If the object being passed into attach is not a valid <webview>
456 // then we will fail and it will be treated as if the new window
457 // was rejected. The permission API plumbing is used here to clean
458 // up the state created for the new window if attaching fails.
459 WebView.setPermission(
460 getInstanceId(), requestId, attached ? 'allow' : 'deny');
463 discard: function() {
465 WebView.setPermission(getInstanceId(), requestId, 'deny');
468 webViewEvent.window = windowObj;
470 var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
475 if (defaultPrevented) {
476 // Make browser plugin track lifetime of |windowObj|.
477 MessagingNatives.BindToGC(windowObj, function() {
478 // Avoid showing a warning message if the decision has already been made.
482 WebView.setPermission(
483 getInstanceId(), requestId, 'default', '', function(allowed) {
487 showWarningMessage();
492 // The default action is to discard the window.
493 WebView.setPermission(
494 getInstanceId(), requestId, 'default', '', function(allowed) {
498 showWarningMessage();
503 WebViewEvents.prototype.getPermissionTypes = function() {
511 return permissions.concat(
512 this.webViewInternal.maybeGetExperimentalPermissions());
515 WebViewEvents.prototype.handlePermissionEvent =
516 function(event, webViewEvent) {
517 var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
518 'Permission has already been decided for this "permissionrequest" event.';
520 var showWarningMessage = function(permission) {
521 var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
522 'The permission request for "%1" has been denied.';
524 WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
527 var requestId = event.requestId;
529 var getInstanceId = function() {
530 return self.webViewInternal.getInstanceId();
533 if (this.getPermissionTypes().indexOf(event.permission) < 0) {
534 // The permission type is not allowed. Trigger the default response.
535 WebView.setPermission(
536 getInstanceId(), requestId, 'default', '', function(allowed) {
540 showWarningMessage(event.permission);
545 var decisionMade = false;
546 var validateCall = function() {
548 throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
553 // Construct the event.request object.
557 WebView.setPermission(getInstanceId(), requestId, 'allow');
561 WebView.setPermission(getInstanceId(), requestId, 'deny');
564 webViewEvent.request = request;
566 var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
571 if (defaultPrevented) {
572 // Make browser plugin track lifetime of |request|.
573 MessagingNatives.BindToGC(request, function() {
574 // Avoid showing a warning message if the decision has already been made.
578 WebView.setPermission(
579 getInstanceId(), requestId, 'default', '', function(allowed) {
583 showWarningMessage(event.permission);
588 WebView.setPermission(
589 getInstanceId(), requestId, 'default', '', function(allowed) {
593 showWarningMessage(event.permission);
598 WebViewEvents.prototype.handleSizeChangedEvent = function(
599 event, webViewEvent) {
600 this.webViewInternal.onSizeChanged(webViewEvent);
603 exports.WebViewEvents = WebViewEvents;
604 exports.CreateEvent = CreateEvent;