1// Copyright 2013 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. 4 5// This module implements experimental API for <webview>. 6// See web_view.js for details. 7// 8// <webview> Experimental API is only available on canary and dev channels of 9// Chrome. 10 11var ContextMenusSchema = 12 requireNative('schema_registry').GetSchema('contextMenus'); 13var CreateEvent = require('webViewEvents').CreateEvent; 14var EventBindings = require('event_bindings'); 15var MessagingNatives = requireNative('messaging_natives'); 16var WebView = require('webView').WebView; 17var WebViewInternal = require('webView').WebViewInternal; 18var WebViewSchema = requireNative('schema_registry').GetSchema('webview'); 19var idGeneratorNatives = requireNative('id_generator'); 20var utils = require('utils'); 21 22// WEB_VIEW_EXPERIMENTAL_EVENTS is a map of experimental <webview> DOM event 23// names to their associated extension event descriptor objects. 24// An event listener will be attached to the extension event |evt| specified in 25// the descriptor. 26// |fields| specifies the public-facing fields in the DOM event that are 27// accessible to <webview> developers. 28// |customHandler| allows a handler function to be called each time an extension 29// event is caught by its event listener. The DOM event should be dispatched 30// within this handler function. With no handler function, the DOM event 31// will be dispatched by default each time the extension event is caught. 32// |cancelable| (default: false) specifies whether the event's default 33// behavior can be canceled. If the default action associated with the event 34// is prevented, then its dispatch function will return false in its event 35// handler. The event must have a custom handler for this to be meaningful. 36var WEB_VIEW_EXPERIMENTAL_EVENTS = { 37 'findupdate': { 38 evt: CreateEvent('webview.onFindReply'), 39 fields: [ 40 'searchText', 41 'numberOfMatches', 42 'activeMatchOrdinal', 43 'selectionRect', 44 'canceled', 45 'finalUpdate' 46 ] 47 }, 48 'zoomchange': { 49 evt: CreateEvent('webview.onZoomChange'), 50 fields: ['oldZoomFactor', 'newZoomFactor'] 51 } 52}; 53 54function GetUniqueSubEventName(eventName) { 55 return eventName + "/" + idGeneratorNatives.GetNextId(); 56} 57 58// This is the only "webview.onClicked" named event for this renderer. 59// 60// Since we need an event per <webview>, we define events with suffix 61// (subEventName) in each of the <webview>. Behind the scenes, this event is 62// registered as a ContextMenusEvent, with filter set to the webview's 63// |viewInstanceId|. Any time a ContextMenusEvent is dispatched, we re-dispatch 64// it to the subEvent's listeners. This way 65// <webview>.contextMenus.onClicked behave as a regular chrome Event type. 66var ContextMenusEvent = CreateEvent('webview.onClicked'); 67 68/** 69 * This event is exposed as <webview>.contextMenus.onClicked. 70 * 71 * @constructor 72 */ 73function ContextMenusOnClickedEvent(opt_eventName, 74 opt_argSchemas, 75 opt_eventOptions, 76 opt_webViewInstanceId) { 77 var subEventName = GetUniqueSubEventName(opt_eventName); 78 EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions, 79 opt_webViewInstanceId); 80 81 var self = this; 82 // TODO(lazyboy): When do we dispose this listener? 83 ContextMenusEvent.addListener(function() { 84 // Re-dispatch to subEvent's listeners. 85 $Function.apply(self.dispatch, self, $Array.slice(arguments)); 86 }, {instanceId: opt_webViewInstanceId || 0}); 87} 88 89ContextMenusOnClickedEvent.prototype = { 90 __proto__: EventBindings.Event.prototype 91}; 92 93/** 94 * An instance of this class is exposed as <webview>.contextMenus. 95 * @constructor 96 */ 97function WebViewContextMenusImpl(viewInstanceId) { 98 this.viewInstanceId_ = viewInstanceId; 99}; 100 101WebViewContextMenusImpl.prototype.create = function() { 102 var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments)); 103 return $Function.apply(WebView.contextMenusCreate, null, args); 104}; 105 106WebViewContextMenusImpl.prototype.remove = function() { 107 var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments)); 108 return $Function.apply(WebView.contextMenusRemove, null, args); 109}; 110 111WebViewContextMenusImpl.prototype.removeAll = function() { 112 var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments)); 113 return $Function.apply(WebView.contextMenusRemoveAll, null, args); 114}; 115 116WebViewContextMenusImpl.prototype.update = function() { 117 var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments)); 118 return $Function.apply(WebView.contextMenusUpdate, null, args); 119}; 120 121var WebViewContextMenus = utils.expose( 122 'WebViewContextMenus', WebViewContextMenusImpl, 123 { functions: ['create', 'remove', 'removeAll', 'update'] }); 124 125/** @private */ 126WebViewInternal.prototype.maybeHandleContextMenu = function(e, webViewEvent) { 127 var requestId = e.requestId; 128 var self = this; 129 // Construct the event.menu object. 130 var actionTaken = false; 131 var validateCall = function() { 132 var ERROR_MSG_CONTEXT_MENU_ACTION_ALREADY_TAKEN = '<webview>: ' + 133 'An action has already been taken for this "contextmenu" event.'; 134 135 if (actionTaken) { 136 throw new Error(ERROR_MSG_CONTEXT_MENU_ACTION_ALREADY_TAKEN); 137 } 138 actionTaken = true; 139 }; 140 var menu = { 141 show: function(items) { 142 validateCall(); 143 // TODO(lazyboy): WebViewShowContextFunction doesn't do anything useful 144 // with |items|, implement. 145 WebView.showContextMenu(self.instanceId, requestId, items); 146 } 147 }; 148 webViewEvent.menu = menu; 149 var webviewNode = this.webviewNode; 150 var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent); 151 if (actionTaken) { 152 return; 153 } 154 if (!defaultPrevented) { 155 actionTaken = true; 156 // The default action is equivalent to just showing the context menu as is. 157 WebView.showContextMenu(self.instanceId, requestId, undefined); 158 159 // TODO(lazyboy): Figure out a way to show warning message only when 160 // listeners are registered for this event. 161 } // else we will ignore showing the context menu completely. 162}; 163 164/** 165 * @private 166 */ 167WebViewInternal.prototype.setZoom = function(zoomFactor) { 168 if (!this.instanceId) { 169 return; 170 } 171 WebView.setZoom(this.instanceId, zoomFactor); 172}; 173 174WebViewInternal.prototype.maybeGetExperimentalEvents = function() { 175 return WEB_VIEW_EXPERIMENTAL_EVENTS; 176}; 177 178/** @private */ 179WebViewInternal.prototype.maybeGetExperimentalPermissions = function() { 180 return []; 181}; 182 183/** @private */ 184WebViewInternal.prototype.maybeSetCurrentZoomFactor = 185 function(zoomFactor) { 186 this.currentZoomFactor = zoomFactor; 187}; 188 189/** @private */ 190WebViewInternal.prototype.setZoom = function(zoomFactor, callback) { 191 if (!this.instanceId) { 192 return; 193 } 194 WebView.setZoom(this.instanceId, zoomFactor, callback); 195}; 196 197WebViewInternal.prototype.getZoom = function(callback) { 198 if (!this.instanceId) { 199 return; 200 } 201 WebView.getZoom(this.instanceId, callback); 202}; 203 204/** @private */ 205WebViewInternal.prototype.captureVisibleRegion = function(spec, callback) { 206 WebView.captureVisibleRegion(this.instanceId, spec, callback); 207}; 208 209/** @private */ 210WebViewInternal.prototype.find = function(search_text, options, callback) { 211 if (!this.instanceId) { 212 return; 213 } 214 WebView.find(this.instanceId, search_text, options, callback); 215}; 216 217/** @private */ 218WebViewInternal.prototype.stopFinding = function(action) { 219 if (!this.instanceId) { 220 return; 221 } 222 WebView.stopFinding(this.instanceId, action); 223}; 224 225WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) { 226 proto.setZoom = function(zoomFactor, callback) { 227 privates(this).internal.setZoom(zoomFactor, callback); 228 }; 229 230 proto.getZoom = function(callback) { 231 return privates(this).internal.getZoom(callback); 232 }; 233 234 proto.captureVisibleRegion = function(spec, callback) { 235 privates(this).internal.captureVisibleRegion(spec, callback); 236 }; 237 238 proto.find = function(search_text, options, callback) { 239 privates(this).internal.find(search_text, options, callback); 240 }; 241 242 proto.stopFinding = function(action) { 243 privates(this).internal.stopFinding(action); 244 }; 245}; 246 247/** @private */ 248WebViewInternal.prototype.setupExperimentalContextMenus = function() { 249 var self = this; 250 var createContextMenus = function() { 251 return function() { 252 if (self.contextMenus_) { 253 return self.contextMenus_; 254 } 255 256 self.contextMenus_ = new WebViewContextMenus(self.viewInstanceId); 257 258 // Define 'onClicked' event property on |self.contextMenus_|. 259 var getOnClickedEvent = function() { 260 return function() { 261 if (!self.contextMenusOnClickedEvent_) { 262 var eventName = 'webview.onClicked'; 263 // TODO(lazyboy): Find event by name instead of events[0]. 264 var eventSchema = WebViewSchema.events[0]; 265 var eventOptions = {supportsListeners: true}; 266 var onClickedEvent = new ContextMenusOnClickedEvent( 267 eventName, eventSchema, eventOptions, self.viewInstanceId); 268 self.contextMenusOnClickedEvent_ = onClickedEvent; 269 return onClickedEvent; 270 } 271 return self.contextMenusOnClickedEvent_; 272 } 273 }; 274 Object.defineProperty( 275 self.contextMenus_, 276 'onClicked', 277 {get: getOnClickedEvent(), enumerable: true}); 278 279 return self.contextMenus_; 280 }; 281 }; 282 283 // Expose <webview>.contextMenus object. 284 Object.defineProperty( 285 this.webviewNode, 286 'contextMenus', 287 { 288 get: createContextMenus(), 289 enumerable: true 290 }); 291}; 292