summaryrefslogtreecommitdiffstats
path: root/addon-sdk/source/lib/sdk/content/context-menu.js
diff options
context:
space:
mode:
Diffstat (limited to 'addon-sdk/source/lib/sdk/content/context-menu.js')
-rw-r--r--addon-sdk/source/lib/sdk/content/context-menu.js408
1 files changed, 0 insertions, 408 deletions
diff --git a/addon-sdk/source/lib/sdk/content/context-menu.js b/addon-sdk/source/lib/sdk/content/context-menu.js
deleted file mode 100644
index 2955e2f09..000000000
--- a/addon-sdk/source/lib/sdk/content/context-menu.js
+++ /dev/null
@@ -1,408 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-"use strict";
-
-const { Class } = require("../core/heritage");
-const self = require("../self");
-const { WorkerChild } = require("./worker-child");
-const { getInnerId } = require("../window/utils");
-const { Ci } = require("chrome");
-const { Services } = require("resource://gre/modules/Services.jsm");
-const system = require('../system/events');
-const { process } = require('../remote/child');
-
-// These functions are roughly copied from sdk/selection which doesn't work
-// in the content process
-function getElementWithSelection(window) {
- let element = Services.focus.getFocusedElementForWindow(window, false, {});
- if (!element)
- return null;
-
- try {
- // Accessing selectionStart and selectionEnd on e.g. a button
- // results in an exception thrown as per the HTML5 spec. See
- // http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#textFieldSelection
-
- let { value, selectionStart, selectionEnd } = element;
-
- let hasSelection = typeof value === "string" &&
- !isNaN(selectionStart) &&
- !isNaN(selectionEnd) &&
- selectionStart !== selectionEnd;
-
- return hasSelection ? element : null;
- }
- catch (err) {
- console.exception(err);
- return null;
- }
-}
-
-function safeGetRange(selection, rangeNumber) {
- try {
- let { rangeCount } = selection;
- let range = null;
-
- for (let rangeNumber = 0; rangeNumber < rangeCount; rangeNumber++ ) {
- range = selection.getRangeAt(rangeNumber);
-
- if (range && range.toString())
- break;
-
- range = null;
- }
-
- return range;
- }
- catch (e) {
- return null;
- }
-}
-
-function getSelection(window) {
- let selection = window.getSelection();
- let range = safeGetRange(selection);
- if (range)
- return range.toString();
-
- let node = getElementWithSelection(window);
- if (!node)
- return null;
-
- return node.value.substring(node.selectionStart, node.selectionEnd);
-}
-
-//These are used by PageContext.isCurrent below. If the popupNode or any of
-//its ancestors is one of these, Firefox uses a tailored context menu, and so
-//the page context doesn't apply.
-const NON_PAGE_CONTEXT_ELTS = [
- Ci.nsIDOMHTMLAnchorElement,
- Ci.nsIDOMHTMLAppletElement,
- Ci.nsIDOMHTMLAreaElement,
- Ci.nsIDOMHTMLButtonElement,
- Ci.nsIDOMHTMLCanvasElement,
- Ci.nsIDOMHTMLEmbedElement,
- Ci.nsIDOMHTMLImageElement,
- Ci.nsIDOMHTMLInputElement,
- Ci.nsIDOMHTMLMapElement,
- Ci.nsIDOMHTMLMediaElement,
- Ci.nsIDOMHTMLMenuElement,
- Ci.nsIDOMHTMLObjectElement,
- Ci.nsIDOMHTMLOptionElement,
- Ci.nsIDOMHTMLSelectElement,
- Ci.nsIDOMHTMLTextAreaElement,
-];
-
-// List all editable types of inputs. Or is it better to have a list
-// of non-editable inputs?
-var editableInputs = {
- email: true,
- number: true,
- password: true,
- search: true,
- tel: true,
- text: true,
- textarea: true,
- url: true
-};
-
-var CONTEXTS = {};
-
-var Context = Class({
- initialize: function(id) {
- this.id = id;
- },
-
- adjustPopupNode: function adjustPopupNode(popupNode) {
- return popupNode;
- },
-
- // Gets state to pass through to the parent process for the node the user
- // clicked on
- getState: function(popupNode) {
- return false;
- }
-});
-
-// Matches when the context-clicked node doesn't have any of
-// NON_PAGE_CONTEXT_ELTS in its ancestors
-CONTEXTS.PageContext = Class({
- extends: Context,
-
- getState: function(popupNode) {
- // If there is a selection in the window then this context does not match
- if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
- return false;
-
- // If the clicked node or any of its ancestors is one of the blocked
- // NON_PAGE_CONTEXT_ELTS then this context does not match
- while (!(popupNode instanceof Ci.nsIDOMDocument)) {
- if (NON_PAGE_CONTEXT_ELTS.some(type => popupNode instanceof type))
- return false;
-
- popupNode = popupNode.parentNode;
- }
-
- return true;
- }
-});
-
-// Matches when there is an active selection in the window
-CONTEXTS.SelectionContext = Class({
- extends: Context,
-
- getState: function(popupNode) {
- if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
- return true;
-
- try {
- // The node may be a text box which has selectionStart and selectionEnd
- // properties. If not this will throw.
- let { selectionStart, selectionEnd } = popupNode;
- return !isNaN(selectionStart) && !isNaN(selectionEnd) &&
- selectionStart !== selectionEnd;
- }
- catch (e) {
- return false;
- }
- }
-});
-
-// Matches when the context-clicked node or any of its ancestors matches the
-// selector given
-CONTEXTS.SelectorContext = Class({
- extends: Context,
-
- initialize: function initialize(id, selector) {
- Context.prototype.initialize.call(this, id);
- this.selector = selector;
- },
-
- adjustPopupNode: function adjustPopupNode(popupNode) {
- let selector = this.selector;
-
- while (!(popupNode instanceof Ci.nsIDOMDocument)) {
- if (popupNode.matches(selector))
- return popupNode;
-
- popupNode = popupNode.parentNode;
- }
-
- return null;
- },
-
- getState: function(popupNode) {
- return !!this.adjustPopupNode(popupNode);
- }
-});
-
-// Matches when the page url matches any of the patterns given
-CONTEXTS.URLContext = Class({
- extends: Context,
-
- getState: function(popupNode) {
- return popupNode.ownerDocument.URL;
- }
-});
-
-// Matches when the user-supplied predicate returns true
-CONTEXTS.PredicateContext = Class({
- extends: Context,
-
- getState: function(node) {
- let window = node.ownerDocument.defaultView;
- let data = {};
-
- data.documentType = node.ownerDocument.contentType;
-
- data.documentURL = node.ownerDocument.location.href;
- data.targetName = node.nodeName.toLowerCase();
- data.targetID = node.id || null ;
-
- if ((data.targetName === 'input' && editableInputs[node.type]) ||
- data.targetName === 'textarea') {
- data.isEditable = !node.readOnly && !node.disabled;
- }
- else {
- data.isEditable = node.isContentEditable;
- }
-
- data.selectionText = getSelection(window, "TEXT");
-
- data.srcURL = node.src || null;
- data.value = node.value || null;
-
- while (!data.linkURL && node) {
- data.linkURL = node.href || null;
- node = node.parentNode;
- }
-
- return data;
- },
-});
-
-function instantiateContext({ id, type, args }) {
- if (!(type in CONTEXTS)) {
- console.error("Attempt to use unknown context " + type);
- return;
- }
- return new CONTEXTS[type](id, ...args);
-}
-
-var ContextWorker = Class({
- implements: [ WorkerChild ],
-
- // Calls the context workers context listeners and returns the first result
- // that is either a string or a value that evaluates to true. If all of the
- // listeners returned false then returns false. If there are no listeners,
- // returns true (show the menu item by default).
- getMatchedContext: function getCurrentContexts(popupNode) {
- let results = this.sandbox.emitSync("context", popupNode);
- if (!results.length)
- return true;
- return results.reduce((val, result) => val || result);
- },
-
- // Emits a click event in the worker's port. popupNode is the node that was
- // context-clicked, and clickedItemData is the data of the item that was
- // clicked.
- fireClick: function fireClick(popupNode, clickedItemData) {
- this.sandbox.emitSync("click", popupNode, clickedItemData);
- }
-});
-
-// Gets the item's content script worker for a window, creating one if necessary
-// Once created it will be automatically destroyed when the window unloads.
-// If there is not content scripts for the item then null will be returned.
-function getItemWorkerForWindow(item, window) {
- if (!item.contentScript && !item.contentScriptFile)
- return null;
-
- let id = getInnerId(window);
- let worker = item.workerMap.get(id);
-
- if (worker)
- return worker;
-
- worker = ContextWorker({
- id: item.id,
- window,
- manager: item.manager,
- contentScript: item.contentScript,
- contentScriptFile: item.contentScriptFile,
- onDetach: function() {
- item.workerMap.delete(id);
- }
- });
-
- item.workerMap.set(id, worker);
-
- return worker;
-}
-
-// A very simple remote proxy for every item. It's job is to provide data for
-// the main process to use to determine visibility state and to call into
-// content scripts when clicked.
-var RemoteItem = Class({
- initialize: function(options, manager) {
- this.id = options.id;
- this.contexts = options.contexts.map(instantiateContext);
- this.contentScript = options.contentScript;
- this.contentScriptFile = options.contentScriptFile;
-
- this.manager = manager;
-
- this.workerMap = new Map();
- keepAlive.set(this.id, this);
- },
-
- destroy: function() {
- for (let worker of this.workerMap.values()) {
- worker.destroy();
- }
- keepAlive.delete(this.id);
- },
-
- activate: function(popupNode, data) {
- let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
- if (!worker)
- return;
-
- for (let context of this.contexts)
- popupNode = context.adjustPopupNode(popupNode);
-
- worker.fireClick(popupNode, data);
- },
-
- // Fills addonInfo with state data to send through to the main process
- getContextState: function(popupNode, addonInfo) {
- if (!(self.id in addonInfo)) {
- addonInfo[self.id] = {
- processID: process.id,
- items: {}
- };
- }
-
- let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
- let contextStates = {};
- for (let context of this.contexts)
- contextStates[context.id] = context.getState(popupNode);
-
- addonInfo[self.id].items[this.id] = {
- // It isn't ideal to create a PageContext for every item but there isn't
- // a good shared place to do it.
- pageContext: (new CONTEXTS.PageContext()).getState(popupNode),
- contextStates,
- hasWorker: !!worker,
- workerContext: worker ? worker.getMatchedContext(popupNode) : true
- }
- }
-});
-exports.RemoteItem = RemoteItem;
-
-// Holds remote items for this frame.
-var keepAlive = new Map();
-
-// Called to create remote proxies for items. If they already exist we destroy
-// and recreate. This can happen if the item changes in some way or in odd
-// timing cases where the frame script is create around the same time as the
-// item is created in the main process
-process.port.on('sdk/contextmenu/createitems', (process, items) => {
- for (let itemoptions of items) {
- let oldItem = keepAlive.get(itemoptions.id);
- if (oldItem) {
- oldItem.destroy();
- }
-
- let item = new RemoteItem(itemoptions, this);
- }
-});
-
-process.port.on('sdk/contextmenu/destroyitems', (process, items) => {
- for (let id of items) {
- let item = keepAlive.get(id);
- item.destroy();
- }
-});
-
-var lastPopupNode = null;
-
-system.on('content-contextmenu', ({ subject }) => {
- let { event: { target: popupNode }, addonInfo } = subject.wrappedJSObject;
- lastPopupNode = popupNode;
-
- for (let item of keepAlive.values()) {
- item.getContextState(popupNode, addonInfo);
- }
-}, true);
-
-process.port.on('sdk/contextmenu/activateitems', (process, items, data) => {
- for (let id of items) {
- let item = keepAlive.get(id);
- if (!item)
- continue;
-
- item.activate(lastPopupNode, data);
- }
-});