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']
74 customHandler: function(handler, event, webViewEvent) {
75 handler.handleLoadAbortEvent(event, webViewEvent);
77 evt: CreateEvent('webViewInternal.onLoadAbort'),
78 fields: ['url', 'isTopLevel', 'reason']
81 customHandler: function(handler, event, webViewEvent) {
82 handler.handleLoadCommitEvent(event, webViewEvent);
84 evt: CreateEvent('webViewInternal.onLoadCommit'),
85 fields: ['url', 'isTopLevel']
88 evt: CreateEvent('webViewInternal.onLoadProgress'),
89 fields: ['url', 'progress']
92 evt: CreateEvent('webViewInternal.onLoadRedirect'),
93 fields: ['isTopLevel', 'oldUrl', 'newUrl']
96 evt: CreateEvent('webViewInternal.onLoadStart'),
97 fields: ['url', 'isTopLevel']
100 evt: CreateEvent('webViewInternal.onLoadStop'),
105 customHandler: function(handler, event, webViewEvent) {
106 handler.handleNewWindowEvent(event, webViewEvent);
108 evt: CreateEvent('webViewInternal.onNewWindow'),
113 'windowOpenDisposition',
117 'permissionrequest': {
119 customHandler: function(handler, event, webViewEvent) {
120 handler.handlePermissionEvent(event, webViewEvent);
122 evt: CreateEvent('webViewInternal.onPermissionRequest'),
125 'lastUnlockedBySelf',
134 evt: CreateEvent('webViewInternal.onResponsive'),
135 fields: ['processId']
138 evt: CreateEvent('webViewInternal.onSizeChanged'),
139 customHandler: function(handler, event, webViewEvent) {
140 handler.handleSizeChangedEvent(event, webViewEvent);
142 fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
145 evt: CreateEvent('webViewInternal.onUnresponsive'),
146 fields: ['processId']
150 function DeclarativeWebRequestEvent(opt_eventName,
153 opt_webViewInstanceId) {
154 var subEventName = opt_eventName + '/' + IdGenerator.GetNextId();
155 EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions,
156 opt_webViewInstanceId);
159 // TODO(lazyboy): When do we dispose this listener?
160 WebRequestMessageEvent.addListener(function() {
161 // Re-dispatch to subEvent's listeners.
162 $Function.apply(self.dispatch, self, $Array.slice(arguments));
163 }, {instanceId: opt_webViewInstanceId || 0});
166 DeclarativeWebRequestEvent.prototype = {
167 __proto__: EventBindings.Event.prototype
171 function WebViewEvents(webViewInternal, viewInstanceId) {
172 this.webViewInternal = webViewInternal;
173 this.viewInstanceId = viewInstanceId;
178 WebViewEvents.prototype.setup = function() {
179 this.setupFrameNameChangedEvent();
180 this.setupWebRequestEvents();
181 this.webViewInternal.setupExperimentalContextMenus();
183 var events = this.getEvents();
184 for (var eventName in events) {
185 this.setupEvent(eventName, events[eventName]);
189 WebViewEvents.prototype.setupFrameNameChangedEvent = function() {
191 FrameNameChangedEvent.addListener(function(e) {
192 self.webViewInternal.onFrameNameChanged(e.name);
193 }, {instanceId: self.viewInstanceId});
196 WebViewEvents.prototype.setupWebRequestEvents = function() {
199 var createWebRequestEvent = function(webRequestEvent) {
201 if (!self[webRequestEvent.name]) {
202 self[webRequestEvent.name] =
204 'webViewInternal.' + webRequestEvent.name,
205 webRequestEvent.parameters,
206 webRequestEvent.extraParameters, webRequestEvent.options,
207 self.viewInstanceId);
209 return self[webRequestEvent.name];
213 var createDeclarativeWebRequestEvent = function(webRequestEvent) {
215 if (!self[webRequestEvent.name]) {
216 // The onMessage event gets a special event type because we want
217 // the listener to fire only for messages targeted for this particular
219 var EventClass = webRequestEvent.name === 'onMessage' ?
220 DeclarativeWebRequestEvent : EventBindings.Event;
221 self[webRequestEvent.name] =
223 'webViewInternal.' + webRequestEvent.name,
224 webRequestEvent.parameters,
225 webRequestEvent.options,
226 self.viewInstanceId);
228 return self[webRequestEvent.name];
232 for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) {
233 var eventSchema = DeclarativeWebRequestSchema.events[i];
234 var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema);
235 Object.defineProperty(
239 get: webRequestEvent,
245 // Populate the WebRequest events from the API definition.
246 for (var i = 0; i < WebRequestSchema.events.length; ++i) {
247 var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
248 Object.defineProperty(
250 WebRequestSchema.events[i].name,
252 get: webRequestEvent,
258 this.webViewInternal.setRequestPropertyOnWebViewNode(request);
261 WebViewEvents.prototype.getEvents = function() {
262 var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents();
263 for (var eventName in experimentalEvents) {
264 WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
266 return WEB_VIEW_EVENTS;
269 WebViewEvents.prototype.setupEvent = function(name, info) {
271 info.evt.addListener(function(e) {
272 var details = {bubbles:true};
274 details.cancelable = true;
275 var webViewEvent = new Event(name, details);
276 $Array.forEach(info.fields, function(field) {
277 if (e[field] !== undefined) {
278 webViewEvent[field] = e[field];
281 if (info.customHandler) {
282 info.customHandler(self, e, webViewEvent);
285 self.webViewInternal.dispatchEvent(webViewEvent);
286 }, {instanceId: self.viewInstanceId});
288 this.webViewInternal.setupEventProperty(name);
293 WebViewEvents.prototype.handleContextMenu = function(e, webViewEvent) {
294 this.webViewInternal.maybeHandleContextMenu(e, webViewEvent);
297 WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) {
298 var showWarningMessage = function(dialogType) {
299 var VOWELS = ['a', 'e', 'i', 'o', 'u'];
300 var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.';
301 var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A';
302 var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article);
303 output = output.replace('%2', dialogType);
304 window.console.warn(output);
308 var requestId = event.requestId;
309 var actionTaken = false;
311 var validateCall = function() {
312 var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' +
313 'An action has already been taken for this "dialog" event.';
316 throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN);
321 var getInstanceId = function() {
322 return self.webViewInternal.getInstanceId();
326 ok: function(user_input) {
328 user_input = user_input || '';
329 WebView.setPermission(getInstanceId(), requestId, 'allow', user_input);
333 WebView.setPermission(getInstanceId(), requestId, 'deny');
336 webViewEvent.dialog = dialog;
338 var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
343 if (defaultPrevented) {
344 // Tell the JavaScript garbage collector to track lifetime of |dialog| and
345 // call back when the dialog object has been collected.
346 MessagingNatives.BindToGC(dialog, function() {
347 // Avoid showing a warning message if the decision has already been made.
351 WebView.setPermission(
352 getInstanceId(), requestId, 'default', '', function(allowed) {
356 showWarningMessage(event.messageType);
361 // The default action is equivalent to canceling the dialog.
362 WebView.setPermission(
363 getInstanceId(), requestId, 'default', '', function(allowed) {
367 showWarningMessage(event.messageType);
372 WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) {
373 var showWarningMessage = function(reason) {
374 var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
375 'The load has aborted with reason "%1".';
376 window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
378 if (this.webViewInternal.dispatchEvent(webViewEvent)) {
379 showWarningMessage(event.reason);
383 WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) {
384 this.webViewInternal.onLoadCommit(event.currentEntryIndex, event.entryCount,
385 event.processId, event.url,
387 this.webViewInternal.dispatchEvent(webViewEvent);
390 WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) {
391 var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
392 'An action has already been taken for this "newwindow" event.';
394 var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
395 'Unable to attach the new window to the provided webViewInternal.';
397 var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
399 var showWarningMessage = function() {
400 var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
401 window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
404 var requestId = event.requestId;
405 var actionTaken = false;
407 var getInstanceId = function() {
408 return self.webViewInternal.getInstanceId();
411 var validateCall = function () {
413 throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
419 attach: function(webview) {
421 if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW')
422 throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
423 // Attach happens asynchronously to give the tagWatcher an opportunity
424 // to pick up the new webview before attach operates on it, if it hasn't
425 // been attached to the DOM already.
426 // Note: Any subsequent errors cannot be exceptions because they happen
428 setTimeout(function() {
429 var webViewInternal = privates(webview).internal;
430 if (event.storagePartitionId) {
431 webViewInternal.onAttach(event.storagePartitionId);
435 webViewInternal.attachWindowAndSetUpEvents(
436 event.windowId, undefined, event.storagePartitionId);
439 window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
441 // If the object being passed into attach is not a valid <webview>
442 // then we will fail and it will be treated as if the new window
443 // was rejected. The permission API plumbing is used here to clean
444 // up the state created for the new window if attaching fails.
445 WebView.setPermission(
446 getInstanceId(), requestId, attached ? 'allow' : 'deny');
449 discard: function() {
451 WebView.setPermission(getInstanceId(), requestId, 'deny');
454 webViewEvent.window = windowObj;
456 var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
461 if (defaultPrevented) {
462 // Make browser plugin track lifetime of |windowObj|.
463 MessagingNatives.BindToGC(windowObj, function() {
464 // Avoid showing a warning message if the decision has already been made.
468 WebView.setPermission(
469 getInstanceId(), requestId, 'default', '', function(allowed) {
473 showWarningMessage();
478 // The default action is to discard the window.
479 WebView.setPermission(
480 getInstanceId(), requestId, 'default', '', function(allowed) {
484 showWarningMessage();
489 WebViewEvents.prototype.getPermissionTypes = function() {
497 return permissions.concat(
498 this.webViewInternal.maybeGetExperimentalPermissions());
501 WebViewEvents.prototype.handlePermissionEvent =
502 function(event, webViewEvent) {
503 var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
504 'Permission has already been decided for this "permissionrequest" event.';
506 var showWarningMessage = function(permission) {
507 var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
508 'The permission request for "%1" has been denied.';
510 WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
513 var requestId = event.requestId;
515 var getInstanceId = function() {
516 return self.webViewInternal.getInstanceId();
519 if (this.getPermissionTypes().indexOf(event.permission) < 0) {
520 // The permission type is not allowed. Trigger the default response.
521 WebView.setPermission(
522 getInstanceId(), requestId, 'default', '', function(allowed) {
526 showWarningMessage(event.permission);
531 var decisionMade = false;
532 var validateCall = function() {
534 throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
539 // Construct the event.request object.
543 WebView.setPermission(getInstanceId(), requestId, 'allow');
547 WebView.setPermission(getInstanceId(), requestId, 'deny');
550 webViewEvent.request = request;
552 var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
557 if (defaultPrevented) {
558 // Make browser plugin track lifetime of |request|.
559 MessagingNatives.BindToGC(request, function() {
560 // Avoid showing a warning message if the decision has already been made.
564 WebView.setPermission(
565 getInstanceId(), requestId, 'default', '', function(allowed) {
569 showWarningMessage(event.permission);
574 WebView.setPermission(
575 getInstanceId(), requestId, 'default', '', function(allowed) {
579 showWarningMessage(event.permission);
584 WebViewEvents.prototype.handleSizeChangedEvent = function(
585 event, webViewEvent) {
586 this.webViewInternal.onSizeChanged(webViewEvent.newWidth,
587 webViewEvent.newHeight);
588 this.webViewInternal.dispatchEvent(webViewEvent);
591 exports.WebViewEvents = WebViewEvents;
592 exports.CreateEvent = CreateEvent;