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> Chrome Experimental API is only available on canary and dev
9// channels of 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');
16//var WebView = require('webViewInternal').WebView;
17var ChromeWebView = require('chromeWebViewInternal').ChromeWebView;
18var WebViewInternal = require('webView').WebViewInternal;
19var ChromeWebViewSchema =
20    requireNative('schema_registry').GetSchema('chromeWebViewInternal');
21var idGeneratorNatives = requireNative('id_generator');
22var utils = require('utils');
23
24function GetUniqueSubEventName(eventName) {
25  return eventName + "/" + idGeneratorNatives.GetNextId();
26}
27
28// This is the only "webViewInternal.onClicked" named event for this renderer.
29//
30// Since we need an event per <webview>, we define events with suffix
31// (subEventName) in each of the <webview>. Behind the scenes, this event is
32// registered as a ContextMenusEvent, with filter set to the webview's
33// |viewInstanceId|. Any time a ContextMenusEvent is dispatched, we re-dispatch
34// it to the subEvent's listeners. This way
35// <webview>.contextMenus.onClicked behave as a regular chrome Event type.
36var ContextMenusEvent = CreateEvent('chromeWebViewInternal.onClicked');
37
38/**
39 * This event is exposed as <webview>.contextMenus.onClicked.
40 *
41 * @constructor
42 */
43function ContextMenusOnClickedEvent(opt_eventName,
44                                    opt_argSchemas,
45                                    opt_eventOptions,
46                                    opt_webViewInstanceId) {
47  var subEventName = GetUniqueSubEventName(opt_eventName);
48  EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions,
49      opt_webViewInstanceId);
50
51  // TODO(lazyboy): When do we dispose this listener?
52  ContextMenusEvent.addListener(function() {
53    // Re-dispatch to subEvent's listeners.
54    $Function.apply(this.dispatch, this, $Array.slice(arguments));
55  }.bind(this), {instanceId: opt_webViewInstanceId || 0});
56}
57
58ContextMenusOnClickedEvent.prototype = {
59  __proto__: EventBindings.Event.prototype
60};
61
62/**
63 * An instance of this class is exposed as <webview>.contextMenus.
64 * @constructor
65 */
66function WebViewContextMenusImpl(viewInstanceId) {
67  this.viewInstanceId_ = viewInstanceId;
68};
69
70WebViewContextMenusImpl.prototype.create = function() {
71  var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments));
72  return $Function.apply(ChromeWebView.contextMenusCreate, null, args);
73};
74
75WebViewContextMenusImpl.prototype.remove = function() {
76  var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments));
77  return $Function.apply(ChromeWebView.contextMenusRemove, null, args);
78};
79
80WebViewContextMenusImpl.prototype.removeAll = function() {
81  var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments));
82  return $Function.apply(ChromeWebView.contextMenusRemoveAll, null, args);
83};
84
85WebViewContextMenusImpl.prototype.update = function() {
86  var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments));
87  return $Function.apply(ChromeWebView.contextMenusUpdate, null, args);
88};
89
90var WebViewContextMenus = utils.expose(
91    'WebViewContextMenus', WebViewContextMenusImpl,
92    { functions: ['create', 'remove', 'removeAll', 'update'] });
93
94/** @private */
95WebViewInternal.prototype.maybeHandleContextMenu = function(e, webViewEvent) {
96  var requestId = e.requestId;
97  // Construct the event.menu object.
98  var actionTaken = false;
99  var validateCall = function() {
100    var ERROR_MSG_CONTEXT_MENU_ACTION_ALREADY_TAKEN = '<webview>: ' +
101        'An action has already been taken for this "contextmenu" event.';
102
103    if (actionTaken) {
104      throw new Error(ERROR_MSG_CONTEXT_MENU_ACTION_ALREADY_TAKEN);
105    }
106    actionTaken = true;
107  };
108  var menu = {
109    show: function(items) {
110      validateCall();
111      // TODO(lazyboy): WebViewShowContextFunction doesn't do anything useful
112      // with |items|, implement.
113      ChromeWebView.showContextMenu(this.guestInstanceId, requestId, items);
114    }.bind(this)
115  };
116  webViewEvent.menu = menu;
117  var webviewNode = this.webviewNode;
118  var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
119  if (actionTaken) {
120    return;
121  }
122  if (!defaultPrevented) {
123    actionTaken = true;
124    // The default action is equivalent to just showing the context menu as is.
125    ChromeWebView.showContextMenu(this.guestInstanceId, requestId, undefined);
126
127    // TODO(lazyboy): Figure out a way to show warning message only when
128    // listeners are registered for this event.
129  } //  else we will ignore showing the context menu completely.
130};
131
132/** @private */
133WebViewInternal.prototype.setupExperimentalContextMenus = function() {
134  var createContextMenus = function() {
135    return function() {
136      if (this.contextMenus_) {
137        return this.contextMenus_;
138      }
139
140      this.contextMenus_ = new WebViewContextMenus(this.viewInstanceId);
141
142      // Define 'onClicked' event property on |this.contextMenus_|.
143      var getOnClickedEvent = function() {
144        return function() {
145          if (!this.contextMenusOnClickedEvent_) {
146            var eventName = 'chromeWebViewInternal.onClicked';
147            // TODO(lazyboy): Find event by name instead of events[0].
148            var eventSchema = ChromeWebViewSchema.events[0];
149            var eventOptions = {supportsListeners: true};
150            var onClickedEvent = new ContextMenusOnClickedEvent(
151                eventName, eventSchema, eventOptions, this.viewInstanceId);
152            this.contextMenusOnClickedEvent_ = onClickedEvent;
153            return onClickedEvent;
154          }
155          return this.contextMenusOnClickedEvent_;
156        }.bind(this);
157      }.bind(this);
158      Object.defineProperty(
159          this.contextMenus_,
160          'onClicked',
161          {get: getOnClickedEvent(), enumerable: true});
162
163      return this.contextMenus_;
164    }.bind(this);
165  }.bind(this);
166
167  // Expose <webview>.contextMenus object.
168  Object.defineProperty(
169      this.webviewNode,
170      'contextMenus',
171      {
172        get: createContextMenus(),
173        enumerable: true
174      });
175};
176