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 EventBindings = require('event_bindings');
8 var MessagingNatives = requireNative('messaging_natives');
9 var WebView = require('webViewInternal').WebView;
11 var CreateEvent = function(name) {
12 var eventOpts = {supportsListeners: true, supportsFilters: true};
13 return new EventBindings.Event(name, undefined, eventOpts);
16 var FrameNameChangedEvent = CreateEvent('webViewInternal.onFrameNameChanged');
17 var PluginDestroyedEvent = CreateEvent('webViewInternal.onPluginDestroyed');
19 // WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their
20 // associated extension event descriptor objects.
21 // An event listener will be attached to the extension event |evt| specified in
23 // |fields| specifies the public-facing fields in the DOM event that are
24 // accessible to <webview> developers.
25 // |customHandler| allows a handler function to be called each time an extension
26 // event is caught by its event listener. The DOM event should be dispatched
27 // within this handler function. With no handler function, the DOM event
28 // will be dispatched by default each time the extension event is caught.
29 // |cancelable| (default: false) specifies whether the event's default
30 // behavior can be canceled. If the default action associated with the event
31 // is prevented, then its dispatch function will return false in its event
32 // handler. The event must have a custom handler for this to be meaningful.
33 var WEB_VIEW_EVENTS = {
35 evt: CreateEvent('webViewInternal.onClose'),
39 evt: CreateEvent('webViewInternal.onConsoleMessage'),
40 fields: ['level', 'message', 'line', 'sourceId']
43 evt: CreateEvent('webViewInternal.onContentLoad'),
48 customHandler: function(handler, event, webViewEvent) {
49 handler.handleDialogEvent(event, webViewEvent);
51 evt: CreateEvent('webViewInternal.onDialog'),
52 fields: ['defaultPromptText', 'messageText', 'messageType', 'url']
55 evt: CreateEvent('webViewInternal.onExit'),
56 fields: ['processId', 'reason']
59 evt: CreateEvent('webViewInternal.onFindReply'),
71 customHandler: function(handler, event, webViewEvent) {
72 handler.handleLoadAbortEvent(event, webViewEvent);
74 evt: CreateEvent('webViewInternal.onLoadAbort'),
75 fields: ['url', 'isTopLevel', 'reason']
78 customHandler: function(handler, event, webViewEvent) {
79 handler.handleLoadCommitEvent(event, webViewEvent);
81 evt: CreateEvent('webViewInternal.onLoadCommit'),
82 fields: ['url', 'isTopLevel']
85 evt: CreateEvent('webViewInternal.onLoadProgress'),
86 fields: ['url', 'progress']
89 evt: CreateEvent('webViewInternal.onLoadRedirect'),
90 fields: ['isTopLevel', 'oldUrl', 'newUrl']
93 evt: CreateEvent('webViewInternal.onLoadStart'),
94 fields: ['url', 'isTopLevel']
97 evt: CreateEvent('webViewInternal.onLoadStop'),
102 customHandler: function(handler, event, webViewEvent) {
103 handler.handleNewWindowEvent(event, webViewEvent);
105 evt: CreateEvent('webViewInternal.onNewWindow'),
110 'windowOpenDisposition',
114 'permissionrequest': {
116 customHandler: function(handler, event, webViewEvent) {
117 handler.handlePermissionEvent(event, webViewEvent);
119 evt: CreateEvent('webViewInternal.onPermissionRequest'),
122 'lastUnlockedBySelf',
131 evt: CreateEvent('webViewInternal.onResponsive'),
132 fields: ['processId']
135 evt: CreateEvent('webViewInternal.onSizeChanged'),
136 customHandler: function(handler, event, webViewEvent) {
137 handler.handleSizeChangedEvent(event, webViewEvent);
139 fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
142 evt: CreateEvent('webViewInternal.onUnresponsive'),
143 fields: ['processId']
146 evt: CreateEvent('webViewInternal.onZoomChange'),
147 fields: ['oldZoomFactor', 'newZoomFactor']
152 function WebViewEvents(webViewInternal, viewInstanceId) {
153 this.webViewInternal = webViewInternal;
154 this.viewInstanceId = viewInstanceId;
159 WebViewEvents.prototype.setup = function() {
160 this.setupFrameNameChangedEvent();
161 this.setupPluginDestroyedEvent();
162 this.webViewInternal.maybeSetupChromeWebViewEvents();
163 this.webViewInternal.setupExperimentalContextMenus();
165 var events = this.getEvents();
166 for (var eventName in events) {
167 this.setupEvent(eventName, events[eventName]);
171 WebViewEvents.prototype.setupFrameNameChangedEvent = function() {
172 FrameNameChangedEvent.addListener(function(e) {
173 this.webViewInternal.onFrameNameChanged(e.name);
174 }.bind(this), {instanceId: this.viewInstanceId});
177 WebViewEvents.prototype.setupPluginDestroyedEvent = function() {
178 PluginDestroyedEvent.addListener(function(e) {
179 this.webViewInternal.onPluginDestroyed();
180 }.bind(this), {instanceId: this.viewInstanceId});
183 WebViewEvents.prototype.getEvents = function() {
184 var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents();
185 for (var eventName in experimentalEvents) {
186 WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
188 var chromeEvents = this.webViewInternal.maybeGetChromeWebViewEvents();
189 for (var eventName in chromeEvents) {
190 WEB_VIEW_EVENTS[eventName] = chromeEvents[eventName];
192 return WEB_VIEW_EVENTS;
195 WebViewEvents.prototype.setupEvent = function(name, info) {
196 info.evt.addListener(function(e) {
197 var details = {bubbles:true};
198 if (info.cancelable) {
199 details.cancelable = true;
201 var webViewEvent = new Event(name, details);
202 $Array.forEach(info.fields, function(field) {
203 if (e[field] !== undefined) {
204 webViewEvent[field] = e[field];
207 if (info.customHandler) {
208 info.customHandler(this, e, webViewEvent);
211 this.webViewInternal.dispatchEvent(webViewEvent);
212 }.bind(this), {instanceId: this.viewInstanceId});
214 this.webViewInternal.setupEventProperty(name);
218 WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) {
219 var showWarningMessage = function(dialogType) {
220 var VOWELS = ['a', 'e', 'i', 'o', 'u'];
221 var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.';
222 var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A';
223 var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article);
224 output = output.replace('%2', dialogType);
225 window.console.warn(output);
228 var requestId = event.requestId;
229 var actionTaken = false;
231 var validateCall = function() {
232 var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' +
233 'An action has already been taken for this "dialog" event.';
236 throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN);
241 var getGuestInstanceId = function() {
242 return this.webViewInternal.getGuestInstanceId();
246 ok: function(user_input) {
248 user_input = user_input || '';
249 WebView.setPermission(getGuestInstanceId(), requestId, 'allow',
254 WebView.setPermission(getGuestInstanceId(), requestId, 'deny');
257 webViewEvent.dialog = dialog;
259 var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
264 if (defaultPrevented) {
265 // Tell the JavaScript garbage collector to track lifetime of |dialog| and
266 // call back when the dialog object has been collected.
267 MessagingNatives.BindToGC(dialog, function() {
268 // Avoid showing a warning message if the decision has already been made.
272 WebView.setPermission(
273 getGuestInstanceId(), requestId, 'default', '', function(allowed) {
277 showWarningMessage(event.messageType);
282 // The default action is equivalent to canceling the dialog.
283 WebView.setPermission(
284 getGuestInstanceId(), requestId, 'default', '', function(allowed) {
288 showWarningMessage(event.messageType);
293 WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) {
294 var showWarningMessage = function(reason) {
295 var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
296 'The load has aborted with reason "%1".';
297 window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
299 if (this.webViewInternal.dispatchEvent(webViewEvent)) {
300 showWarningMessage(event.reason);
304 WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) {
305 this.webViewInternal.onLoadCommit(event.baseUrlForDataUrl,
306 event.currentEntryIndex, event.entryCount,
307 event.processId, event.url,
309 this.webViewInternal.dispatchEvent(webViewEvent);
312 WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) {
313 var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
314 'An action has already been taken for this "newwindow" event.';
316 var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
317 'Unable to attach the new window to the provided webViewInternal.';
319 var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
321 var showWarningMessage = function() {
322 var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
323 window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
326 var requestId = event.requestId;
327 var actionTaken = false;
328 var getGuestInstanceId = function() {
329 return this.webViewInternal.getGuestInstanceId();
332 var validateCall = function () {
334 throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
340 attach: function(webview) {
342 if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW')
343 throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
344 // Attach happens asynchronously to give the tagWatcher an opportunity
345 // to pick up the new webview before attach operates on it, if it hasn't
346 // been attached to the DOM already.
347 // Note: Any subsequent errors cannot be exceptions because they happen
349 setTimeout(function() {
350 var webViewInternal = privates(webview).internal;
351 // Update the partition.
352 if (event.storagePartitionId) {
353 webViewInternal.onAttach(event.storagePartitionId);
356 var attached = webViewInternal.attachWindow(event.windowId, true);
359 window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
362 var guestInstanceId = getGuestInstanceId();
363 if (!guestInstanceId) {
364 // If the opener is already gone, then we won't have its
369 // If the object being passed into attach is not a valid <webview>
370 // then we will fail and it will be treated as if the new window
371 // was rejected. The permission API plumbing is used here to clean
372 // up the state created for the new window if attaching fails.
373 WebView.setPermission(
374 guestInstanceId, requestId, attached ? 'allow' : 'deny');
377 discard: function() {
379 var guestInstanceId = getGuestInstanceId();
380 if (!guestInstanceId) {
381 // If the opener is already gone, then we won't have its
385 WebView.setPermission(guestInstanceId, requestId, 'deny');
388 webViewEvent.window = windowObj;
390 var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
395 if (defaultPrevented) {
396 // Make browser plugin track lifetime of |windowObj|.
397 MessagingNatives.BindToGC(windowObj, function() {
398 // Avoid showing a warning message if the decision has already been made.
403 var guestInstanceId = getGuestInstanceId();
404 if (!guestInstanceId) {
405 // If the opener is already gone, then we won't have its
410 WebView.setPermission(
411 guestInstanceId, requestId, 'default', '', function(allowed) {
415 showWarningMessage();
420 // The default action is to discard the window.
421 WebView.setPermission(
422 getGuestInstanceId(), requestId, 'default', '', function(allowed) {
426 showWarningMessage();
431 WebViewEvents.prototype.getPermissionTypes = function() {
439 return permissions.concat(
440 this.webViewInternal.maybeGetExperimentalPermissions());
443 WebViewEvents.prototype.handlePermissionEvent =
444 function(event, webViewEvent) {
445 var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
446 'Permission has already been decided for this "permissionrequest" event.';
448 var showWarningMessage = function(permission) {
449 var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
450 'The permission request for "%1" has been denied.';
452 WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
455 var requestId = event.requestId;
456 var getGuestInstanceId = function() {
457 return this.webViewInternal.getGuestInstanceId();
460 if (this.getPermissionTypes().indexOf(event.permission) < 0) {
461 // The permission type is not allowed. Trigger the default response.
462 WebView.setPermission(
463 getGuestInstanceId(), requestId, 'default', '', function(allowed) {
467 showWarningMessage(event.permission);
472 var decisionMade = false;
473 var validateCall = function() {
475 throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
480 // Construct the event.request object.
484 WebView.setPermission(getGuestInstanceId(), requestId, 'allow');
488 WebView.setPermission(getGuestInstanceId(), requestId, 'deny');
491 webViewEvent.request = request;
493 var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
498 if (defaultPrevented) {
499 // Make browser plugin track lifetime of |request|.
500 MessagingNatives.BindToGC(request, function() {
501 // Avoid showing a warning message if the decision has already been made.
505 WebView.setPermission(
506 getGuestInstanceId(), requestId, 'default', '', function(allowed) {
510 showWarningMessage(event.permission);
515 WebView.setPermission(
516 getGuestInstanceId(), requestId, 'default', '',
521 showWarningMessage(event.permission);
526 WebViewEvents.prototype.handleSizeChangedEvent = function(
527 event, webViewEvent) {
528 this.webViewInternal.onSizeChanged(webViewEvent);
531 exports.WebViewEvents = WebViewEvents;
532 exports.CreateEvent = CreateEvent;