| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // This module implements experimental API for <webview>. |
| // See web_view.js for details. |
| // |
| // <webview> Experimental API is only available on canary and dev channels of |
| // Chrome. |
| |
| var ContextMenusSchema = |
| requireNative('schema_registry').GetSchema('contextMenus'); |
| var CreateEvent = require('webViewEvents').CreateEvent; |
| var EventBindings = require('event_bindings'); |
| var MessagingNatives = requireNative('messaging_natives'); |
| var WebView = require('webViewInternal').WebView; |
| var WebViewInternal = require('webView').WebViewInternal; |
| var WebViewSchema = |
| requireNative('schema_registry').GetSchema('webViewInternal'); |
| var idGeneratorNatives = requireNative('id_generator'); |
| var utils = require('utils'); |
| |
| // WEB_VIEW_EXPERIMENTAL_EVENTS is a map of experimental <webview> DOM event |
| // names to their associated extension event descriptor objects. |
| // An event listener will be attached to the extension event |evt| specified in |
| // the descriptor. |
| // |fields| specifies the public-facing fields in the DOM event that are |
| // accessible to <webview> developers. |
| // |customHandler| allows a handler function to be called each time an extension |
| // event is caught by its event listener. The DOM event should be dispatched |
| // within this handler function. With no handler function, the DOM event |
| // will be dispatched by default each time the extension event is caught. |
| // |cancelable| (default: false) specifies whether the event's default |
| // behavior can be canceled. If the default action associated with the event |
| // is prevented, then its dispatch function will return false in its event |
| // handler. The event must have a custom handler for this to be meaningful. |
| var WEB_VIEW_EXPERIMENTAL_EVENTS = { |
| 'findupdate': { |
| evt: CreateEvent('webViewInternal.onFindReply'), |
| fields: [ |
| 'searchText', |
| 'numberOfMatches', |
| 'activeMatchOrdinal', |
| 'selectionRect', |
| 'canceled', |
| 'finalUpdate' |
| ] |
| }, |
| 'zoomchange': { |
| evt: CreateEvent('webViewInternal.onZoomChange'), |
| fields: ['oldZoomFactor', 'newZoomFactor'] |
| } |
| }; |
| |
| function GetUniqueSubEventName(eventName) { |
| return eventName + "/" + idGeneratorNatives.GetNextId(); |
| } |
| |
| // This is the only "webViewInternal.onClicked" named event for this renderer. |
| // |
| // Since we need an event per <webview>, we define events with suffix |
| // (subEventName) in each of the <webview>. Behind the scenes, this event is |
| // registered as a ContextMenusEvent, with filter set to the webview's |
| // |viewInstanceId|. Any time a ContextMenusEvent is dispatched, we re-dispatch |
| // it to the subEvent's listeners. This way |
| // <webview>.contextMenus.onClicked behave as a regular chrome Event type. |
| var ContextMenusEvent = CreateEvent('webViewInternal.onClicked'); |
| |
| /** |
| * This event is exposed as <webview>.contextMenus.onClicked. |
| * |
| * @constructor |
| */ |
| function ContextMenusOnClickedEvent(opt_eventName, |
| opt_argSchemas, |
| opt_eventOptions, |
| opt_webViewInstanceId) { |
| var subEventName = GetUniqueSubEventName(opt_eventName); |
| EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions, |
| opt_webViewInstanceId); |
| |
| var self = this; |
| // TODO(lazyboy): When do we dispose this listener? |
| ContextMenusEvent.addListener(function() { |
| // Re-dispatch to subEvent's listeners. |
| $Function.apply(self.dispatch, self, $Array.slice(arguments)); |
| }, {instanceId: opt_webViewInstanceId || 0}); |
| } |
| |
| ContextMenusOnClickedEvent.prototype = { |
| __proto__: EventBindings.Event.prototype |
| }; |
| |
| /** |
| * An instance of this class is exposed as <webview>.contextMenus. |
| * @constructor |
| */ |
| function WebViewContextMenusImpl(viewInstanceId) { |
| this.viewInstanceId_ = viewInstanceId; |
| }; |
| |
| WebViewContextMenusImpl.prototype.create = function() { |
| var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments)); |
| return $Function.apply(WebView.contextMenusCreate, null, args); |
| }; |
| |
| WebViewContextMenusImpl.prototype.remove = function() { |
| var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments)); |
| return $Function.apply(WebView.contextMenusRemove, null, args); |
| }; |
| |
| WebViewContextMenusImpl.prototype.removeAll = function() { |
| var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments)); |
| return $Function.apply(WebView.contextMenusRemoveAll, null, args); |
| }; |
| |
| WebViewContextMenusImpl.prototype.update = function() { |
| var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments)); |
| return $Function.apply(WebView.contextMenusUpdate, null, args); |
| }; |
| |
| var WebViewContextMenus = utils.expose( |
| 'WebViewContextMenus', WebViewContextMenusImpl, |
| { functions: ['create', 'remove', 'removeAll', 'update'] }); |
| |
| /** @private */ |
| WebViewInternal.prototype.maybeHandleContextMenu = function(e, webViewEvent) { |
| var requestId = e.requestId; |
| var self = this; |
| // Construct the event.menu object. |
| var actionTaken = false; |
| var validateCall = function() { |
| var ERROR_MSG_CONTEXT_MENU_ACTION_ALREADY_TAKEN = '<webview>: ' + |
| 'An action has already been taken for this "contextmenu" event.'; |
| |
| if (actionTaken) { |
| throw new Error(ERROR_MSG_CONTEXT_MENU_ACTION_ALREADY_TAKEN); |
| } |
| actionTaken = true; |
| }; |
| var menu = { |
| show: function(items) { |
| validateCall(); |
| // TODO(lazyboy): WebViewShowContextFunction doesn't do anything useful |
| // with |items|, implement. |
| WebView.showContextMenu(self.instanceId, requestId, items); |
| } |
| }; |
| webViewEvent.menu = menu; |
| var webviewNode = this.webviewNode; |
| var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent); |
| if (actionTaken) { |
| return; |
| } |
| if (!defaultPrevented) { |
| actionTaken = true; |
| // The default action is equivalent to just showing the context menu as is. |
| WebView.showContextMenu(self.instanceId, requestId, undefined); |
| |
| // TODO(lazyboy): Figure out a way to show warning message only when |
| // listeners are registered for this event. |
| } // else we will ignore showing the context menu completely. |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.setZoom = function(zoomFactor) { |
| if (!this.instanceId) { |
| return; |
| } |
| WebView.setZoom(this.instanceId, zoomFactor); |
| }; |
| |
| WebViewInternal.prototype.maybeGetExperimentalEvents = function() { |
| return WEB_VIEW_EXPERIMENTAL_EVENTS; |
| }; |
| |
| /** @private */ |
| WebViewInternal.prototype.maybeGetExperimentalPermissions = function() { |
| return []; |
| }; |
| |
| /** @private */ |
| WebViewInternal.prototype.maybeSetCurrentZoomFactor = |
| function(zoomFactor) { |
| this.currentZoomFactor = zoomFactor; |
| }; |
| |
| /** @private */ |
| WebViewInternal.prototype.setZoom = function(zoomFactor, callback) { |
| if (!this.instanceId) { |
| return; |
| } |
| WebView.setZoom(this.instanceId, zoomFactor, callback); |
| }; |
| |
| WebViewInternal.prototype.getZoom = function(callback) { |
| if (!this.instanceId) { |
| return; |
| } |
| WebView.getZoom(this.instanceId, callback); |
| }; |
| |
| /** @private */ |
| WebViewInternal.prototype.captureVisibleRegion = function(spec, callback) { |
| WebView.captureVisibleRegion(this.instanceId, spec, callback); |
| }; |
| |
| /** @private */ |
| WebViewInternal.prototype.find = function(search_text, options, callback) { |
| if (!this.instanceId) { |
| return; |
| } |
| WebView.find(this.instanceId, search_text, options, callback); |
| }; |
| |
| /** @private */ |
| WebViewInternal.prototype.stopFinding = function(action) { |
| if (!this.instanceId) { |
| return; |
| } |
| WebView.stopFinding(this.instanceId, action); |
| }; |
| |
| WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) { |
| proto.setZoom = function(zoomFactor, callback) { |
| privates(this).internal.setZoom(zoomFactor, callback); |
| }; |
| |
| proto.getZoom = function(callback) { |
| return privates(this).internal.getZoom(callback); |
| }; |
| |
| proto.captureVisibleRegion = function(spec, callback) { |
| privates(this).internal.captureVisibleRegion(spec, callback); |
| }; |
| |
| proto.find = function(search_text, options, callback) { |
| privates(this).internal.find(search_text, options, callback); |
| }; |
| |
| proto.stopFinding = function(action) { |
| privates(this).internal.stopFinding(action); |
| }; |
| }; |
| |
| /** @private */ |
| WebViewInternal.prototype.setupExperimentalContextMenus = function() { |
| var self = this; |
| var createContextMenus = function() { |
| return function() { |
| if (self.contextMenus_) { |
| return self.contextMenus_; |
| } |
| |
| self.contextMenus_ = new WebViewContextMenus(self.viewInstanceId); |
| |
| // Define 'onClicked' event property on |self.contextMenus_|. |
| var getOnClickedEvent = function() { |
| return function() { |
| if (!self.contextMenusOnClickedEvent_) { |
| var eventName = 'webViewInternal.onClicked'; |
| // TODO(lazyboy): Find event by name instead of events[0]. |
| var eventSchema = WebViewSchema.events[0]; |
| var eventOptions = {supportsListeners: true}; |
| var onClickedEvent = new ContextMenusOnClickedEvent( |
| eventName, eventSchema, eventOptions, self.viewInstanceId); |
| self.contextMenusOnClickedEvent_ = onClickedEvent; |
| return onClickedEvent; |
| } |
| return self.contextMenusOnClickedEvent_; |
| } |
| }; |
| Object.defineProperty( |
| self.contextMenus_, |
| 'onClicked', |
| {get: getOnClickedEvent(), enumerable: true}); |
| |
| return self.contextMenus_; |
| }; |
| }; |
| |
| // Expose <webview>.contextMenus object. |
| Object.defineProperty( |
| this.webviewNode, |
| 'contextMenus', |
| { |
| get: createContextMenus(), |
| enumerable: true |
| }); |
| }; |