// 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 . // See web_view.js for details. // // 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('webView').WebView; var WebViewInternal = require('webView').WebViewInternal; var WebViewSchema = requireNative('schema_registry').GetSchema('webview'); var idGeneratorNatives = requireNative('id_generator'); var utils = require('utils'); // WEB_VIEW_EXPERIMENTAL_EVENTS is a map of experimental 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 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('webview.onFindReply'), fields: [ 'searchText', 'numberOfMatches', 'activeMatchOrdinal', 'selectionRect', 'canceled', 'finalUpdate' ] }, 'zoomchange': { evt: CreateEvent('webview.onZoomChange'), fields: ['oldZoomFactor', 'newZoomFactor'] } }; function GetUniqueSubEventName(eventName) { return eventName + "/" + idGeneratorNatives.GetNextId(); } // This is the only "webview.onClicked" named event for this renderer. // // Since we need an event per , we define events with suffix // (subEventName) in each of the . 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 // .contextMenus.onClicked behave as a regular chrome Event type. var ContextMenusEvent = CreateEvent('webview.onClicked'); /** * This event is exposed as .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 .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 = ': ' + '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 = 'webview.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 .contextMenus object. Object.defineProperty( this.webviewNode, 'contextMenus', { get: createContextMenus(), enumerable: true }); };