forked from mirrors/gecko-dev
--HG-- rename : addon-sdk/source/test/fixtures/test-page-worker.html => addon-sdk/source/test/addons/e10s-content/data/test-page-worker.html rename : addon-sdk/source/test/fixtures/test-page-worker.js => addon-sdk/source/test/addons/e10s-content/data/test-page-worker.js rename : addon-sdk/source/test/addons/places/favicon-helpers.js => addon-sdk/source/test/addons/places/lib/favicon-helpers.js rename : addon-sdk/source/test/addons/places/httpd.js => addon-sdk/source/test/addons/places/lib/httpd.js rename : addon-sdk/source/test/addons/places/places-helper.js => addon-sdk/source/test/addons/places/lib/places-helper.js rename : addon-sdk/source/test/addons/places/tests/test-places-utils.js => addon-sdk/source/test/addons/places/lib/test-places-utils.js rename : addon-sdk/source/test/fixtures/test-page-worker.html => addon-sdk/source/test/fixtures/addon-sdk/data/test-page-worker.html rename : addon-sdk/source/test/fixtures/test-page-worker.js => addon-sdk/source/test/fixtures/addon-sdk/data/test-page-worker.js
384 lines
13 KiB
JavaScript
384 lines
13 KiB
JavaScript
/* 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 Contexts = require("./context");
|
|
const Readers = require("./readers");
|
|
const Component = require("../ui/component");
|
|
const { Class } = require("../core/heritage");
|
|
const { map, filter, object, reduce, keys, symbols,
|
|
pairs, values, each, some, isEvery, count } = require("../util/sequence");
|
|
const { loadModule } = require("framescript/manager");
|
|
const { Cu, Cc, Ci } = require("chrome");
|
|
const prefs = require("sdk/preferences/service");
|
|
|
|
const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
|
|
.getService(Ci.nsIMessageListenerManager);
|
|
const preferencesService = Cc["@mozilla.org/preferences-service;1"].
|
|
getService(Ci.nsIPrefService).
|
|
getBranch(null);
|
|
|
|
|
|
const readTable = Symbol("context-menu/read-table");
|
|
const nameTable = Symbol("context-menu/name-table");
|
|
const onContext = Symbol("context-menu/on-context");
|
|
const isMatching = Symbol("context-menu/matching-handler?");
|
|
|
|
exports.onContext = onContext;
|
|
exports.readTable = readTable;
|
|
exports.nameTable = nameTable;
|
|
|
|
|
|
const propagateOnContext = (item, data) =>
|
|
each(child => child[onContext](data), item.state.children);
|
|
|
|
const isContextMatch = item => !item[isMatching] || item[isMatching]();
|
|
|
|
// For whatever reason addWeakMessageListener does not seems to work as our
|
|
// instance seems to dropped even though it's alive. This is simple workaround
|
|
// to avoid dead object excetptions.
|
|
const WeakMessageListener = function(receiver, handler="receiveMessage") {
|
|
this.receiver = receiver
|
|
this.handler = handler
|
|
};
|
|
WeakMessageListener.prototype = {
|
|
constructor: WeakMessageListener,
|
|
receiveMessage(message) {
|
|
if (Cu.isDeadWrapper(this.receiver)) {
|
|
message.target.messageManager.removeMessageListener(message.name, this);
|
|
}
|
|
else {
|
|
this.receiver[this.handler](message);
|
|
}
|
|
}
|
|
};
|
|
|
|
const OVERFLOW_THRESH = "extensions.addon-sdk.context-menu.overflowThreshold";
|
|
const onMessage = Symbol("context-menu/message-listener");
|
|
const onPreferceChange = Symbol("context-menu/preference-change");
|
|
const ContextMenuExtension = Class({
|
|
extends: Component,
|
|
initialize: Component,
|
|
setup() {
|
|
const messageListener = new WeakMessageListener(this, onMessage);
|
|
loadModule(globalMessageManager, "framescript/context-menu", true, "onContentFrame");
|
|
globalMessageManager.addMessageListener("sdk/context-menu/read", messageListener);
|
|
globalMessageManager.addMessageListener("sdk/context-menu/readers?", messageListener);
|
|
|
|
preferencesService.addObserver(OVERFLOW_THRESH, this, false);
|
|
},
|
|
observe(_, __, name) {
|
|
if (name === OVERFLOW_THRESH) {
|
|
const overflowThreshold = prefs.get(OVERFLOW_THRESH, 10);
|
|
this[Component.patch]({overflowThreshold});
|
|
}
|
|
},
|
|
[onMessage]({name, data, target}) {
|
|
if (name === "sdk/context-menu/read")
|
|
this[onContext]({target, data});
|
|
if (name === "sdk/context-menu/readers?")
|
|
target.messageManager.sendAsyncMessage("sdk/context-menu/readers",
|
|
JSON.parse(JSON.stringify(this.state.readers)));
|
|
},
|
|
[Component.initial](options={}, children) {
|
|
const element = options.element || null;
|
|
const target = options.target || null;
|
|
const readers = Object.create(null);
|
|
const users = Object.create(null);
|
|
const registry = new WeakSet();
|
|
const overflowThreshold = prefs.get(OVERFLOW_THRESH, 10);
|
|
|
|
return { target, children: [], readers, users, element,
|
|
registry, overflowThreshold };
|
|
},
|
|
[Component.isUpdated](before, after) {
|
|
// Update only if target changed, since there is no point in re-rendering
|
|
// when children are. Also new items added won't be in sync with a latest
|
|
// context target so we should really just render before drawing context
|
|
// menu.
|
|
return before.target !== after.target;
|
|
},
|
|
[Component.render]({element, children, overflowThreshold}) {
|
|
if (!element) return null;
|
|
|
|
const items = children.filter(isContextMatch);
|
|
const body = items.length === 0 ? items :
|
|
items.length < overflowThreshold ? [new Separator(),
|
|
...items] :
|
|
[{tagName: "menu",
|
|
className: "sdk-context-menu-overflow-menu",
|
|
label: "Add-ons",
|
|
accesskey: "A",
|
|
children: [{tagName: "menupopup",
|
|
children: items}]}];
|
|
return {
|
|
element: element,
|
|
tagName: "menugroup",
|
|
style: "-moz-box-orient: vertical;",
|
|
className: "sdk-context-menu-extension",
|
|
children: body
|
|
}
|
|
},
|
|
// Adds / remove child to it's own list.
|
|
add(item) {
|
|
this[Component.patch]({children: this.state.children.concat(item)});
|
|
},
|
|
remove(item) {
|
|
this[Component.patch]({
|
|
children: this.state.children.filter(x => x !== item)
|
|
});
|
|
},
|
|
register(item) {
|
|
const { users, registry } = this.state;
|
|
if (registry.has(item)) return;
|
|
registry.add(item);
|
|
|
|
// Each (ContextHandler) item has a readTable that is a
|
|
// map of keys to readers extracting them from the content.
|
|
// During the registraction we update intrnal record of unique
|
|
// readers and users per reader. Most context will have a reader
|
|
// shared across all instances there for map of users per reader
|
|
// is stored separately from the reader so that removing reader
|
|
// will occur only when no users remain.
|
|
const table = item[readTable];
|
|
// Context readers store data in private symbols so we need to
|
|
// collect both table keys and private symbols.
|
|
const names = [...keys(table), ...symbols(table)];
|
|
const readers = map(name => table[name], names);
|
|
// Create delta for registered readers that will be merged into
|
|
// internal readers table.
|
|
const added = filter(x => !users[x.id], readers);
|
|
const delta = object(...map(x => [x.id, x], added));
|
|
|
|
const update = reduce((update, reader) => {
|
|
const n = update[reader.id] || 0;
|
|
update[reader.id] = n + 1;
|
|
return update;
|
|
}, Object.assign({}, users), readers);
|
|
|
|
// Patch current state with a changes that registered item caused.
|
|
this[Component.patch]({users: update,
|
|
readers: Object.assign(this.state.readers, delta)});
|
|
|
|
if (count(added)) {
|
|
globalMessageManager.broadcastAsyncMessage("sdk/context-menu/readers",
|
|
JSON.parse(JSON.stringify(delta)));
|
|
}
|
|
},
|
|
unregister(item) {
|
|
const { users, registry } = this.state;
|
|
if (!registry.has(item)) return;
|
|
registry.delete(item);
|
|
|
|
const table = item[readTable];
|
|
const names = [...keys(table), ...symbols(table)];
|
|
const readers = map(name => table[name], names);
|
|
const update = reduce((update, reader) => {
|
|
update[reader.id] = update[reader.id] - 1;
|
|
return update;
|
|
}, Object.assign({}, users), readers);
|
|
const removed = filter(id => !update[id], keys(update));
|
|
const delta = object(...map(x => [x, null], removed));
|
|
|
|
this[Component.patch]({users: update,
|
|
readers: Object.assign(this.state.readers, delta)});
|
|
|
|
if (count(removed)) {
|
|
globalMessageManager.broadcastAsyncMessage("sdk/context-menu/readers",
|
|
JSON.parse(JSON.stringify(delta)));
|
|
}
|
|
},
|
|
|
|
[onContext]({data, target}) {
|
|
propagateOnContext(this, data);
|
|
const document = target.ownerDocument;
|
|
const element = document.getElementById("contentAreaContextMenu");
|
|
|
|
this[Component.patch]({target: data, element: element});
|
|
}
|
|
});this,
|
|
exports.ContextMenuExtension = ContextMenuExtension;
|
|
|
|
// Takes an item options and
|
|
const makeReadTable = ({context, read}) => {
|
|
// Result of this function is a tuple of all readers &
|
|
// name, reader id pairs.
|
|
|
|
// Filter down to contexts that have a reader associated.
|
|
const contexts = filter(context => context.read, context);
|
|
// Merge all contexts read maps to a single hash, note that there should be
|
|
// no name collisions as context implementations expect to use private
|
|
// symbols for storing it's read data.
|
|
return Object.assign({}, ...map(({read}) => read, contexts), read);
|
|
}
|
|
|
|
const readTarget = (nameTable, data) =>
|
|
object(...map(([name, id]) => [name, data[id]], nameTable))
|
|
|
|
const ContextHandler = Class({
|
|
extends: Component,
|
|
initialize: Component,
|
|
get context() {
|
|
return this.state.options.context;
|
|
},
|
|
get read() {
|
|
return this.state.options.read;
|
|
},
|
|
[Component.initial](options) {
|
|
return {
|
|
table: makeReadTable(options),
|
|
requiredContext: filter(context => context.isRequired, options.context),
|
|
optionalContext: filter(context => !context.isRequired, options.context)
|
|
}
|
|
},
|
|
[isMatching]() {
|
|
const {target, requiredContext, optionalContext} = this.state;
|
|
return isEvery(context => context.isCurrent(target), requiredContext) &&
|
|
(count(optionalContext) === 0 ||
|
|
some(context => context.isCurrent(target), optionalContext));
|
|
},
|
|
setup() {
|
|
const table = makeReadTable(this.state.options);
|
|
this[readTable] = table;
|
|
this[nameTable] = [...map(symbol => [symbol, table[symbol].id], symbols(table)),
|
|
...map(name => [name, table[name].id], keys(table))];
|
|
|
|
|
|
contextMenu.register(this);
|
|
|
|
each(child => contextMenu.remove(child), this.state.children);
|
|
contextMenu.add(this);
|
|
},
|
|
dispose() {
|
|
contextMenu.remove(this);
|
|
|
|
each(child => contextMenu.unregister(child), this.state.children);
|
|
contextMenu.unregister(this);
|
|
},
|
|
// Internal `Symbol("onContext")` method is invoked when "contextmenu" event
|
|
// occurs in content process. Context handles with children delegate to each
|
|
// child and patch it's internal state to reflect new contextmenu target.
|
|
[onContext](data) {
|
|
propagateOnContext(this, data);
|
|
this[Component.patch]({target: readTarget(this[nameTable], data)});
|
|
}
|
|
});
|
|
const isContextHandler = item => item instanceof ContextHandler;
|
|
|
|
exports.ContextHandler = ContextHandler;
|
|
|
|
const Menu = Class({
|
|
extends: ContextHandler,
|
|
[isMatching]() {
|
|
return ContextHandler.prototype[isMatching].call(this) &&
|
|
this.state.children.filter(isContextHandler)
|
|
.some(isContextMatch);
|
|
},
|
|
[Component.render]({children, options}) {
|
|
const items = children.filter(isContextMatch);
|
|
return {tagName: "menu",
|
|
className: "sdk-context-menu menu-iconic",
|
|
label: options.label,
|
|
accesskey: options.accesskey,
|
|
image: options.icon,
|
|
children: [{tagName: "menupopup",
|
|
children: items}]};
|
|
}
|
|
});
|
|
exports.Menu = Menu;
|
|
|
|
const onCommand = Symbol("context-menu/item/onCommand");
|
|
const Item = Class({
|
|
extends: ContextHandler,
|
|
get onClick() {
|
|
return this.state.options.onClick;
|
|
},
|
|
[Component.render]({options}) {
|
|
const {label, icon, accesskey} = options;
|
|
return {tagName: "menuitem",
|
|
className: "sdk-context-menu-item menuitem-iconic",
|
|
label,
|
|
accesskey,
|
|
image: icon,
|
|
oncommand: this};
|
|
},
|
|
handleEvent(event) {
|
|
if (this.onClick)
|
|
this.onClick(this.state.target);
|
|
}
|
|
});
|
|
exports.Item = Item;
|
|
|
|
var Separator = Class({
|
|
extends: Component,
|
|
initialize: Component,
|
|
[Component.render]() {
|
|
return {tagName: "menuseparator",
|
|
className: "sdk-context-menu-separator"}
|
|
},
|
|
[onContext]() {
|
|
|
|
}
|
|
});
|
|
exports.Separator = Separator;
|
|
|
|
exports.Contexts = Contexts;
|
|
exports.Readers = Readers;
|
|
|
|
const createElement = (vnode, {document}) => {
|
|
const node = vnode.namespace ?
|
|
document.createElementNS(vnode.namespace, vnode.tagName) :
|
|
document.createElement(vnode.tagName);
|
|
|
|
node.setAttribute("data-component-path", vnode[Component.path]);
|
|
|
|
each(([key, value]) => {
|
|
if (key === "tagName") {
|
|
return;
|
|
}
|
|
if (key === "children") {
|
|
return;
|
|
}
|
|
|
|
if (key.startsWith("on")) {
|
|
node.addEventListener(key.substr(2), value)
|
|
return;
|
|
}
|
|
|
|
if (typeof(value) !== "object" &&
|
|
typeof(value) !== "function" &&
|
|
value !== void(0) &&
|
|
value !== null)
|
|
{
|
|
if (key === "className") {
|
|
node[key] = value;
|
|
}
|
|
else {
|
|
node.setAttribute(key, value);
|
|
}
|
|
return;
|
|
}
|
|
}, pairs(vnode));
|
|
|
|
each(child => node.appendChild(createElement(child, {document})), vnode.children);
|
|
return node;
|
|
};
|
|
|
|
const htmlWriter = tree => {
|
|
if (tree !== null) {
|
|
const root = tree.element;
|
|
const node = createElement(tree, {document: root.ownerDocument});
|
|
const before = root.querySelector("[data-component-path='/']");
|
|
if (before) {
|
|
root.replaceChild(node, before);
|
|
} else {
|
|
root.appendChild(node);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
const contextMenu = ContextMenuExtension();
|
|
exports.contextMenu = contextMenu;
|
|
Component.mount(contextMenu, htmlWriter);
|