• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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