зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1350646: Part 6 - Remove SDK UI modules. r=Mossop
MozReview-Commit-ID: Joln7vw9Y9r --HG-- extra : source : 35c4d4cd77c7d33aa1ba0fd93f0e369d3a452232
This commit is contained in:
Родитель
c6d9379091
Коммит
fed32bf06a
|
@ -21,18 +21,8 @@ EXTRA_JS_MODULES.sdk.system += [
|
|||
]
|
||||
|
||||
modules = [
|
||||
'diffpatcher/diff.js',
|
||||
'diffpatcher/index.js',
|
||||
'diffpatcher/patch.js',
|
||||
'diffpatcher/rebase.js',
|
||||
'diffpatcher/test/common.js',
|
||||
'diffpatcher/test/diff.js',
|
||||
'diffpatcher/test/index.js',
|
||||
'diffpatcher/test/patch.js',
|
||||
'diffpatcher/test/tap.js',
|
||||
'framescript/FrameScriptManager.jsm',
|
||||
'framescript/content.jsm',
|
||||
'framescript/context-menu.js',
|
||||
'framescript/manager.js',
|
||||
'framescript/util.js',
|
||||
'index.js',
|
||||
|
@ -52,7 +42,6 @@ modules = [
|
|||
'sdk/console/traceback.js',
|
||||
'sdk/content/content-worker.js',
|
||||
'sdk/content/content.js',
|
||||
'sdk/content/context-menu.js',
|
||||
'sdk/content/events.js',
|
||||
'sdk/content/l10n-html.js',
|
||||
'sdk/content/loader.js',
|
||||
|
@ -65,11 +54,6 @@ modules = [
|
|||
'sdk/content/utils.js',
|
||||
'sdk/content/worker-child.js',
|
||||
'sdk/content/worker.js',
|
||||
'sdk/context-menu.js',
|
||||
'sdk/context-menu/context.js',
|
||||
'sdk/context-menu/core.js',
|
||||
'sdk/context-menu/readers.js',
|
||||
'sdk/context-menu@2.js',
|
||||
'sdk/core/disposable.js',
|
||||
'sdk/core/heritage.js',
|
||||
'sdk/core/namespace.js',
|
||||
|
@ -94,10 +78,6 @@ modules = [
|
|||
'sdk/fs/path.js',
|
||||
'sdk/hotkeys.js',
|
||||
'sdk/indexed-db.js',
|
||||
'sdk/input/browser.js',
|
||||
'sdk/input/customizable-ui.js',
|
||||
'sdk/input/frame.js',
|
||||
'sdk/input/system.js',
|
||||
'sdk/io/buffer.js',
|
||||
'sdk/io/byte-streams.js',
|
||||
'sdk/io/file.js',
|
||||
|
@ -131,9 +111,6 @@ modules = [
|
|||
'sdk/output/system.js',
|
||||
'sdk/page-mod.js',
|
||||
'sdk/page-mod/match-pattern.js',
|
||||
'sdk/panel.js',
|
||||
'sdk/panel/events.js',
|
||||
'sdk/panel/utils.js',
|
||||
'sdk/passwords.js',
|
||||
'sdk/passwords/utils.js',
|
||||
'sdk/platform/xpcom.js',
|
||||
|
@ -188,29 +165,6 @@ modules = [
|
|||
'sdk/test/runner.js',
|
||||
'sdk/test/utils.js',
|
||||
'sdk/timers.js',
|
||||
'sdk/ui.js',
|
||||
'sdk/ui/button/action.js',
|
||||
'sdk/ui/button/contract.js',
|
||||
'sdk/ui/button/toggle.js',
|
||||
'sdk/ui/button/view.js',
|
||||
'sdk/ui/button/view/events.js',
|
||||
'sdk/ui/component.js',
|
||||
'sdk/ui/frame.js',
|
||||
'sdk/ui/frame/model.js',
|
||||
'sdk/ui/frame/view.html',
|
||||
'sdk/ui/frame/view.js',
|
||||
'sdk/ui/id.js',
|
||||
'sdk/ui/sidebar.js',
|
||||
'sdk/ui/sidebar/actions.js',
|
||||
'sdk/ui/sidebar/contract.js',
|
||||
'sdk/ui/sidebar/namespace.js',
|
||||
'sdk/ui/sidebar/utils.js',
|
||||
'sdk/ui/sidebar/view.js',
|
||||
'sdk/ui/state.js',
|
||||
'sdk/ui/state/events.js',
|
||||
'sdk/ui/toolbar.js',
|
||||
'sdk/ui/toolbar/model.js',
|
||||
'sdk/ui/toolbar/view.js',
|
||||
'sdk/uri/resource.js',
|
||||
'sdk/url.js',
|
||||
'sdk/url/utils.js',
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
var method = require("../method/core")
|
||||
|
||||
// Method is designed to work with data structures representing application
|
||||
// state. Calling it with a state should return object representing `delta`
|
||||
// that has being applied to a previous state to get to a current state.
|
||||
//
|
||||
// Example
|
||||
//
|
||||
// diff(state) // => { "item-id-1": { title: "some title" } "item-id-2": null }
|
||||
var diff = method("diff@diffpatcher")
|
||||
|
||||
// diff between `null` / `undefined` to any hash is a hash itself.
|
||||
diff.define(null, function(from, to) { return to })
|
||||
diff.define(undefined, function(from, to) { return to })
|
||||
diff.define(Object, function(from, to) {
|
||||
return calculate(from, to || {}) || {}
|
||||
})
|
||||
|
||||
function calculate(from, to) {
|
||||
var diff = {}
|
||||
var changes = 0
|
||||
Object.keys(from).forEach(function(key) {
|
||||
changes = changes + 1
|
||||
if (!(key in to) && from[key] != null) diff[key] = null
|
||||
else changes = changes - 1
|
||||
})
|
||||
Object.keys(to).forEach(function(key) {
|
||||
changes = changes + 1
|
||||
var previous = from[key]
|
||||
var current = to[key]
|
||||
if (previous === current) return (changes = changes - 1)
|
||||
if (typeof(current) !== "object") return diff[key] = current
|
||||
if (typeof(previous) !== "object") return diff[key] = current
|
||||
var delta = calculate(previous, current)
|
||||
if (delta) diff[key] = delta
|
||||
else changes = changes - 1
|
||||
})
|
||||
return changes ? diff : null
|
||||
}
|
||||
|
||||
diff.calculate = calculate
|
||||
|
||||
module.exports = diff
|
|
@ -1,5 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
exports.diff = require("./diff")
|
||||
exports.patch = require("./patch")
|
||||
exports.rebase = require("./rebase")
|
|
@ -1,21 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
var method = require("../method/core")
|
||||
var rebase = require("./rebase")
|
||||
|
||||
// Method is designed to work with data structures representing application
|
||||
// state. Calling it with a state and delta should return object representing
|
||||
// new state, with changes in `delta` being applied to previous.
|
||||
//
|
||||
// ## Example
|
||||
//
|
||||
// patch(state, {
|
||||
// "item-id-1": { completed: false }, // update
|
||||
// "item-id-2": null // delete
|
||||
// })
|
||||
var patch = method("patch@diffpatcher")
|
||||
patch.define(Object, function patch(hash, delta) {
|
||||
return rebase({}, hash, delta)
|
||||
})
|
||||
|
||||
module.exports = patch
|
|
@ -1,36 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
var nil = {}
|
||||
var owns = ({}).hasOwnProperty
|
||||
|
||||
function rebase(result, parent, delta) {
|
||||
var key, current, previous, update
|
||||
for (key in parent) {
|
||||
if (owns.call(parent, key)) {
|
||||
previous = parent[key]
|
||||
update = owns.call(delta, key) ? delta[key] : nil
|
||||
if (previous === null) continue
|
||||
else if (previous === void(0)) continue
|
||||
else if (update === null) continue
|
||||
else if (update === void(0)) continue
|
||||
else result[key] = previous
|
||||
}
|
||||
}
|
||||
for (key in delta) {
|
||||
if (owns.call(delta, key)) {
|
||||
update = delta[key]
|
||||
current = owns.call(result, key) ? result[key] : nil
|
||||
if (current === update) continue
|
||||
else if (update === null) continue
|
||||
else if (update === void(0)) continue
|
||||
else if (current === nil) result[key] = update
|
||||
else if (typeof(update) !== "object") result[key] = update
|
||||
else if (typeof(current) !== "object") result[key] = update
|
||||
else result[key]= rebase({}, current, update)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = rebase
|
|
@ -1,3 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
require("test").run(require("./index"))
|
|
@ -1,59 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
var diff = require("../diff")
|
||||
|
||||
exports["test diff from null"] = function(assert) {
|
||||
var to = { a: 1, b: 2 }
|
||||
assert.equal(diff(null, to), to, "diff null to x returns x")
|
||||
assert.equal(diff(void(0), to), to, "diff undefined to x returns x")
|
||||
|
||||
}
|
||||
|
||||
exports["test diff to null"] = function(assert) {
|
||||
var from = { a: 1, b: 2 }
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, null),
|
||||
{ a: null, b: null },
|
||||
"diff x null returns x with all properties nullified")
|
||||
}
|
||||
|
||||
exports["test diff identical"] = function(assert) {
|
||||
assert.deepEqual(diff({}, {}), {}, "diff on empty objects is {}")
|
||||
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, { a: 1, b: 2 }), {},
|
||||
"if properties match diff is {}")
|
||||
|
||||
assert.deepEqual(diff({ a: 1, b: { c: { d: 3, e: 4 } } },
|
||||
{ a: 1, b: { c: { d: 3, e: 4 } } }), {},
|
||||
"diff between identical nested hashes is {}")
|
||||
|
||||
}
|
||||
|
||||
exports["test diff delete"] = function(assert) {
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, { b: 2 }), { a: null },
|
||||
"missing property is deleted")
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, { a: 2 }), { a: 2, b: null },
|
||||
"missing property is deleted another updated")
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, {}), { a: null, b: null },
|
||||
"missing propertes are deleted")
|
||||
assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, {}),
|
||||
{ a: null, b: null },
|
||||
"missing deep propertes are deleted")
|
||||
assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, { b: { c: {} } }),
|
||||
{ a: null, b: { c: { d: null } } },
|
||||
"missing nested propertes are deleted")
|
||||
}
|
||||
|
||||
exports["test add update"] = function(assert) {
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, { b: 2, c: 3 }), { a: null, c: 3 },
|
||||
"delete and add")
|
||||
assert.deepEqual(diff({ a: 1, b: 2 }, { a: 2, c: 3 }), { a: 2, b: null, c: 3 },
|
||||
"delete and adds")
|
||||
assert.deepEqual(diff({}, { a: 1, b: 2 }), { a: 1, b: 2 },
|
||||
"diff on empty objcet returns equivalen of to")
|
||||
assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, { d: 3 }),
|
||||
{ a: null, b: null, d: 3 },
|
||||
"missing deep propertes are deleted")
|
||||
assert.deepEqual(diff({ b: { c: {} }, d: null }, { a: 1, b: { c: { d: 2 } } }),
|
||||
{ a: 1, b: { c: { d: 2 } } },
|
||||
"missing nested propertes are deleted")
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
var diff = require("../diff")
|
||||
var patch = require("../patch")
|
||||
|
||||
exports["test diff"] = require("./diff")
|
||||
exports["test patch"] = require("./patch")
|
||||
|
||||
exports["test patch(a, diff(a, b)) => b"] = function(assert) {
|
||||
var a = { a: { b: 1 }, c: { d: 2 } }
|
||||
var b = { a: { e: 3 }, c: { d: 4 } }
|
||||
|
||||
assert.deepEqual(patch(a, diff(a, b)), b, "patch(a, diff(a, b)) => b")
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
var patch = require("../patch")
|
||||
|
||||
exports["test patch delete"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, { a: null }), { b: 2 }, "null removes property")
|
||||
}
|
||||
|
||||
exports["test patch delete with void"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, { a: void(0) }), { b: 2 },
|
||||
"void(0) removes property")
|
||||
}
|
||||
|
||||
exports["test patch delete missing"] = function(assert) {
|
||||
assert.deepEqual(patch({ a: 1, b: 2 }, { c: null }),
|
||||
{ a: 1, b: 2 },
|
||||
"null removes property if exists");
|
||||
|
||||
assert.deepEqual(patch({ a: 1, b: 2 }, { c: void(0) }),
|
||||
{ a: 1, b: 2 },
|
||||
"void removes property if exists");
|
||||
}
|
||||
|
||||
exports["test delete deleted"] = function(assert) {
|
||||
assert.deepEqual(patch({ a: null, b: 2, c: 3, d: void(0)},
|
||||
{ a: void(0), b: null, d: null }),
|
||||
{c: 3},
|
||||
"removed all existing and non existing");
|
||||
}
|
||||
|
||||
exports["test update deleted"] = function(assert) {
|
||||
assert.deepEqual(patch({ a: null, b: void(0), c: 3},
|
||||
{ a: { b: 2 } }),
|
||||
{ a: { b: 2 }, c: 3 },
|
||||
"replace deleted");
|
||||
}
|
||||
|
||||
exports["test patch delete with void"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, { a: void(0) }), { b: 2 },
|
||||
"void(0) removes property")
|
||||
}
|
||||
|
||||
|
||||
exports["test patch addition"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, { c: 3 }), { a: 1, b: 2, c: 3 },
|
||||
"new properties are added")
|
||||
}
|
||||
|
||||
exports["test patch addition"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, { c: 3 }), { a: 1, b: 2, c: 3 },
|
||||
"new properties are added")
|
||||
}
|
||||
|
||||
exports["test hash on itself"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, hash), hash,
|
||||
"applying hash to itself returns hash itself")
|
||||
}
|
||||
|
||||
exports["test patch with empty delta"] = function(assert) {
|
||||
var hash = { a: 1, b: 2 }
|
||||
|
||||
assert.deepEqual(patch(hash, {}), hash,
|
||||
"applying empty delta results in no changes")
|
||||
}
|
||||
|
||||
exports["test patch nested data"] = function(assert) {
|
||||
assert.deepEqual(patch({ a: { b: 1 }, c: { d: 2 } },
|
||||
{ a: { b: null, e: 3 }, c: { d: 4 } }),
|
||||
{ a: { e: 3 }, c: { d: 4 } },
|
||||
"nested structures can also be patched")
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
require("retape")(require("./index"))
|
|
@ -1,215 +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 { query, constant, cache } = require("sdk/lang/functional");
|
||||
const { pairs, each, map, object } = require("sdk/util/sequence");
|
||||
const { nodeToMessageManager } = require("./util");
|
||||
|
||||
// Decorator function that takes `f` function and returns one that attempts
|
||||
// to run `f` with given arguments. In case of exception error is logged
|
||||
// and `fallback` is returned instead.
|
||||
const Try = (fn, fallback=null) => (...args) => {
|
||||
try {
|
||||
return fn(...args);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
// Decorator funciton that takes `f` function and returns one that returns
|
||||
// JSON cloned result of whatever `f` returns for given arguments.
|
||||
const JSONReturn = f => (...args) => JSON.parse(JSON.stringify(f(...args)));
|
||||
|
||||
const Null = constant(null);
|
||||
|
||||
// Table of readers mapped to field names they're going to be reading.
|
||||
const readers = Object.create(null);
|
||||
// Read function takes "contextmenu" event target `node` and returns table of
|
||||
// read field names mapped to appropriate values. Read uses above defined read
|
||||
// table to read data for all registered readers.
|
||||
const read = node =>
|
||||
object(...map(([id, read]) => [id, read(node, id)], pairs(readers)));
|
||||
|
||||
// Table of built-in readers, each takes a descriptor and returns a reader:
|
||||
// descriptor -> node -> JSON
|
||||
const parsers = Object.create(null)
|
||||
// Function takes a descriptor of the remotely defined reader and parsese it
|
||||
// to construct a local reader that's going to read out data from context menu
|
||||
// target.
|
||||
const parse = descriptor => {
|
||||
const parser = parsers[descriptor.category];
|
||||
if (!parser) {
|
||||
console.error("Unknown reader descriptor was received", descriptor, `"${descriptor.category}"`);
|
||||
return Null
|
||||
}
|
||||
return Try(parser(descriptor));
|
||||
}
|
||||
|
||||
// TODO: Test how chrome's mediaType behaves to try and match it's behavior.
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
// Firefox always creates a HTMLVideoElement when loading an ogg file
|
||||
// directly. If the media is actually audio, be smarter and provide a
|
||||
// context menu with audio operations.
|
||||
// Source: https://github.com/mozilla/gecko-dev/blob/28c2fca3753c5371643843fc2f2f205146b083b7/browser/base/content/nsContextMenu.js#L632-L637
|
||||
const isVideoLoadingAudio = node =>
|
||||
node.readyState >= node.HAVE_METADATA &&
|
||||
(node.videoWidth == 0 || node.videoHeight == 0)
|
||||
|
||||
const isVideo = node =>
|
||||
node instanceof node.ownerGlobal.HTMLVideoElement &&
|
||||
!isVideoLoadingAudio(node);
|
||||
|
||||
const isAudio = node => {
|
||||
const {HTMLVideoElement, HTMLAudioElement} = node.ownerGlobal;
|
||||
return node instanceof HTMLAudioElement ? true :
|
||||
node instanceof HTMLVideoElement ? isVideoLoadingAudio(node) :
|
||||
false;
|
||||
};
|
||||
|
||||
const isImage = ({namespaceURI, localName}) =>
|
||||
namespaceURI === HTML_NS && localName === "img" ? true :
|
||||
namespaceURI === XUL_NS && localName === "image" ? true :
|
||||
namespaceURI === SVG_NS && localName === "image" ? true :
|
||||
false;
|
||||
|
||||
parsers["reader/MediaType()"] = constant(node =>
|
||||
isImage(node) ? "image" :
|
||||
isAudio(node) ? "audio" :
|
||||
isVideo(node) ? "video" :
|
||||
null);
|
||||
|
||||
|
||||
const readLink = node =>
|
||||
node.namespaceURI === HTML_NS && node.localName === "a" ? node.href :
|
||||
readLink(node.parentNode);
|
||||
|
||||
parsers["reader/LinkURL()"] = constant(node =>
|
||||
node.matches("a, a *") ? readLink(node) : null);
|
||||
|
||||
// Reader that reads out `true` if "contextmenu" `event.target` matches
|
||||
// `descriptor.selector` and `false` if it does not.
|
||||
parsers["reader/SelectorMatch()"] = ({selector}) =>
|
||||
node => node.matches(selector);
|
||||
|
||||
// Accessing `selectionStart` and `selectionEnd` properties on non
|
||||
// editable input nodes throw exceptions, there for we need this util
|
||||
// function to guard us against them.
|
||||
const getInputSelection = node => {
|
||||
try {
|
||||
if ("selectionStart" in node && "selectionEnd" in node) {
|
||||
const {selectionStart, selectionEnd} = node;
|
||||
return {selectionStart, selectionEnd}
|
||||
}
|
||||
}
|
||||
catch(_) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Selection reader does not really cares about descriptor so it is
|
||||
// a constant function returning selection reader. Selection reader
|
||||
// returns string of the selected text or `null` if there is no selection.
|
||||
parsers["reader/Selection()"] = constant(node => {
|
||||
const selection = node.ownerDocument.getSelection();
|
||||
if (!selection.isCollapsed) {
|
||||
return selection.toString();
|
||||
}
|
||||
// If target node is editable (text, input, textarea, etc..) document does
|
||||
// not really handles selections there. There for we fallback to checking
|
||||
// `selectionStart` `selectionEnd` properties and if they are present we
|
||||
// extract selections manually from the `node.value`.
|
||||
else {
|
||||
const selection = getInputSelection(node);
|
||||
const isSelected = selection &&
|
||||
Number.isInteger(selection.selectionStart) &&
|
||||
Number.isInteger(selection.selectionEnd) &&
|
||||
selection.selectionStart !== selection.selectionEnd;
|
||||
return isSelected ? node.value.substring(selection.selectionStart,
|
||||
selection.selectionEnd) :
|
||||
null;
|
||||
}
|
||||
});
|
||||
|
||||
// Query reader just reads out properties from the node, so we just use `query`
|
||||
// utility function.
|
||||
parsers["reader/Query()"] = ({path}) => JSONReturn(query(path));
|
||||
// Attribute reader just reads attribute of the event target node.
|
||||
parsers["reader/Attribute()"] = ({name}) => node => node.getAttribute(name);
|
||||
|
||||
// Extractor reader defines generates a reader out of serialized function, who's
|
||||
// return value is JSON cloned. Note: We do know source will evaluate to function
|
||||
// as that's what we serialized on the other end, it's also ok if generated function
|
||||
// is going to throw as registered readers are wrapped in try catch to avoid breakting
|
||||
// unrelated readers.
|
||||
parsers["reader/Extractor()"] = ({source}) =>
|
||||
JSONReturn(new Function("return (" + source + ")")());
|
||||
|
||||
// If the context-menu target node or any of its ancestors is one of these,
|
||||
// Firefox uses a tailored context menu, and so the page context doesn't apply.
|
||||
// There for `reader/isPage()` will read `false` in that case otherwise it's going
|
||||
// to read `true`.
|
||||
const nonPageElements = ["a", "applet", "area", "button", "canvas", "object",
|
||||
"embed", "img", "input", "map", "video", "audio", "menu",
|
||||
"option", "select", "textarea", "[contenteditable=true]"];
|
||||
const nonPageSelector = nonPageElements.
|
||||
concat(nonPageElements.map(tag => `${tag} *`)).
|
||||
join(", ");
|
||||
|
||||
// Note: isPageContext implementation could have actually used SelectorMatch reader,
|
||||
// but old implementation was also checked for collapsed selection there for to keep
|
||||
// the behavior same we end up implementing a new reader.
|
||||
parsers["reader/isPage()"] = constant(node =>
|
||||
node.ownerGlobal.getSelection().isCollapsed &&
|
||||
!node.matches(nonPageSelector));
|
||||
|
||||
// Reads `true` if node is in an iframe otherwise returns true.
|
||||
parsers["reader/isFrame()"] = constant(node =>
|
||||
!!node.ownerGlobal.frameElement);
|
||||
|
||||
parsers["reader/isEditable()"] = constant(node => {
|
||||
const selection = getInputSelection(node);
|
||||
return selection ? !node.readOnly && !node.disabled : node.isContentEditable;
|
||||
});
|
||||
|
||||
|
||||
// TODO: Add some reader to read out tab id.
|
||||
|
||||
const onReadersUpdate = message => {
|
||||
each(([id, descriptor]) => {
|
||||
if (descriptor) {
|
||||
readers[id] = parse(descriptor);
|
||||
}
|
||||
else {
|
||||
delete readers[id];
|
||||
}
|
||||
}, pairs(message.data));
|
||||
};
|
||||
exports.onReadersUpdate = onReadersUpdate;
|
||||
|
||||
|
||||
const onContextMenu = event => {
|
||||
if (!event.defaultPrevented) {
|
||||
const manager = nodeToMessageManager(event.target);
|
||||
manager.sendSyncMessage("sdk/context-menu/read", read(event.target), readers);
|
||||
}
|
||||
};
|
||||
exports.onContextMenu = onContextMenu;
|
||||
|
||||
|
||||
const onContentFrame = (frame) => {
|
||||
// Listen for contextmenu events in on this frame.
|
||||
frame.addEventListener("contextmenu", onContextMenu);
|
||||
// Listen to registered reader changes and update registry.
|
||||
frame.addMessageListener("sdk/context-menu/readers", onReadersUpdate);
|
||||
|
||||
// Request table of readers (if this is loaded in a new process some table
|
||||
// changes may be missed, this is way to sync up).
|
||||
frame.sendAsyncMessage("sdk/context-menu/readers?");
|
||||
};
|
||||
exports.onContentFrame = onContentFrame;
|
|
@ -1,407 +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.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.ownerGlobal.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.ownerGlobal.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.ownerGlobal;
|
||||
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.ownerGlobal);
|
||||
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.ownerGlobal);
|
||||
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);
|
||||
}
|
||||
});
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,146 +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/. */
|
||||
|
||||
const { Class } = require("../core/heritage");
|
||||
lazyRequire(this, "../util/match-pattern", "MatchPattern");
|
||||
const readers = require("./readers");
|
||||
|
||||
// Context class is required to implement a single `isCurrent(target)` method
|
||||
// that must return boolean value indicating weather given target matches a
|
||||
// context or not. Most context implementations below will have an associated
|
||||
// reader that way context implementation can setup a reader to extract necessary
|
||||
// information to make decision if target is matching a context.
|
||||
const Context = Class({
|
||||
isRequired: false,
|
||||
isCurrent(target) {
|
||||
throw Error("Context class must implement isCurrent(target) method");
|
||||
},
|
||||
get required() {
|
||||
Object.defineProperty(this, "required", {
|
||||
value: Object.assign(Object.create(Object.getPrototypeOf(this)),
|
||||
this,
|
||||
{isRequired: true})
|
||||
});
|
||||
return this.required;
|
||||
}
|
||||
});
|
||||
Context.required = function(...params) {
|
||||
return Object.assign(new this(...params), {isRequired: true});
|
||||
};
|
||||
exports.Context = Context;
|
||||
|
||||
|
||||
// Next few context implementations use an associated reader to extract info
|
||||
// from the context target and story it to a private symbol associtaed with
|
||||
// a context implementation. That way name collisions are avoided while required
|
||||
// information is still carried along.
|
||||
const isPage = Symbol("context/page?")
|
||||
const PageContext = Class({
|
||||
extends: Context,
|
||||
read: {[isPage]: new readers.isPage()},
|
||||
isCurrent: target => target[isPage]
|
||||
});
|
||||
exports.Page = PageContext;
|
||||
|
||||
const isFrame = Symbol("context/frame?");
|
||||
const FrameContext = Class({
|
||||
extends: Context,
|
||||
read: {[isFrame]: new readers.isFrame()},
|
||||
isCurrent: target => target[isFrame]
|
||||
});
|
||||
exports.Frame = FrameContext;
|
||||
|
||||
const selection = Symbol("context/selection")
|
||||
const SelectionContext = Class({
|
||||
read: {[selection]: new readers.Selection()},
|
||||
isCurrent: target => !!target[selection]
|
||||
});
|
||||
exports.Selection = SelectionContext;
|
||||
|
||||
const link = Symbol("context/link");
|
||||
const LinkContext = Class({
|
||||
extends: Context,
|
||||
read: {[link]: new readers.LinkURL()},
|
||||
isCurrent: target => !!target[link]
|
||||
});
|
||||
exports.Link = LinkContext;
|
||||
|
||||
const isEditable = Symbol("context/editable?")
|
||||
const EditableContext = Class({
|
||||
extends: Context,
|
||||
read: {[isEditable]: new readers.isEditable()},
|
||||
isCurrent: target => target[isEditable]
|
||||
});
|
||||
exports.Editable = EditableContext;
|
||||
|
||||
|
||||
const mediaType = Symbol("context/mediaType")
|
||||
|
||||
const ImageContext = Class({
|
||||
extends: Context,
|
||||
read: {[mediaType]: new readers.MediaType()},
|
||||
isCurrent: target => target[mediaType] === "image"
|
||||
});
|
||||
exports.Image = ImageContext;
|
||||
|
||||
|
||||
const VideoContext = Class({
|
||||
extends: Context,
|
||||
read: {[mediaType]: new readers.MediaType()},
|
||||
isCurrent: target => target[mediaType] === "video"
|
||||
});
|
||||
exports.Video = VideoContext;
|
||||
|
||||
|
||||
const AudioContext = Class({
|
||||
extends: Context,
|
||||
read: {[mediaType]: new readers.MediaType()},
|
||||
isCurrent: target => target[mediaType] === "audio"
|
||||
});
|
||||
exports.Audio = AudioContext;
|
||||
|
||||
const isSelectorMatch = Symbol("context/selector/mathches?")
|
||||
const SelectorContext = Class({
|
||||
extends: Context,
|
||||
initialize(selector) {
|
||||
this.selector = selector;
|
||||
// Each instance of selector context will need to store read
|
||||
// data into different field, so that case with multilpe selector
|
||||
// contexts won't cause a conflicts.
|
||||
this[isSelectorMatch] = Symbol(selector);
|
||||
this.read = {[this[isSelectorMatch]]: new readers.SelectorMatch(selector)};
|
||||
},
|
||||
isCurrent(target) {
|
||||
return target[this[isSelectorMatch]];
|
||||
}
|
||||
});
|
||||
exports.Selector = SelectorContext;
|
||||
|
||||
const url = Symbol("context/url");
|
||||
const URLContext = Class({
|
||||
extends: Context,
|
||||
initialize(pattern) {
|
||||
this.pattern = new MatchPattern(pattern);
|
||||
},
|
||||
read: {[url]: new readers.PageURL()},
|
||||
isCurrent(target) {
|
||||
return this.pattern.test(target[url]);
|
||||
}
|
||||
});
|
||||
exports.URL = URLContext;
|
||||
|
||||
var PredicateContext = Class({
|
||||
extends: Context,
|
||||
initialize(isMatch) {
|
||||
if (typeof(isMatch) !== "function") {
|
||||
throw TypeError("Predicate context mus be passed a function");
|
||||
}
|
||||
|
||||
this.isMatch = isMatch
|
||||
},
|
||||
isCurrent(target) {
|
||||
return this.isMatch(target);
|
||||
}
|
||||
});
|
||||
exports.Predicate = PredicateContext;
|
|
@ -1,384 +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 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);
|
||||
},
|
||||
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);
|
|
@ -1,112 +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/. */
|
||||
const { Class } = require("../core/heritage");
|
||||
const { extend } = require("../util/object");
|
||||
const { memoize, method, identity } = require("../lang/functional");
|
||||
|
||||
const serializeCategory = ({type}) => ({ category: `reader/${type}()` });
|
||||
|
||||
const Reader = Class({
|
||||
initialize() {
|
||||
this.id = `reader/${this.type}()`
|
||||
},
|
||||
toJSON() {
|
||||
return serializeCategory(this);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const MediaTypeReader = Class({ extends: Reader, type: "MediaType" });
|
||||
exports.MediaType = MediaTypeReader;
|
||||
|
||||
const LinkURLReader = Class({ extends: Reader, type: "LinkURL" });
|
||||
exports.LinkURL = LinkURLReader;
|
||||
|
||||
const SelectionReader = Class({ extends: Reader, type: "Selection" });
|
||||
exports.Selection = SelectionReader;
|
||||
|
||||
const isPageReader = Class({ extends: Reader, type: "isPage" });
|
||||
exports.isPage = isPageReader;
|
||||
|
||||
const isFrameReader = Class({ extends: Reader, type: "isFrame" });
|
||||
exports.isFrame = isFrameReader;
|
||||
|
||||
const isEditable = Class({ extends: Reader, type: "isEditable"});
|
||||
exports.isEditable = isEditable;
|
||||
|
||||
|
||||
|
||||
const ParameterizedReader = Class({
|
||||
extends: Reader,
|
||||
readParameter: function(value) {
|
||||
return value;
|
||||
},
|
||||
toJSON: function() {
|
||||
var json = serializeCategory(this);
|
||||
json[this.parameter] = this[this.parameter];
|
||||
return json;
|
||||
},
|
||||
initialize(...params) {
|
||||
if (params.length) {
|
||||
this[this.parameter] = this.readParameter(...params);
|
||||
}
|
||||
this.id = `reader/${this.type}(${JSON.stringify(this[this.parameter])})`;
|
||||
}
|
||||
});
|
||||
exports.ParameterizedReader = ParameterizedReader;
|
||||
|
||||
|
||||
const QueryReader = Class({
|
||||
extends: ParameterizedReader,
|
||||
type: "Query",
|
||||
parameter: "path"
|
||||
});
|
||||
exports.Query = QueryReader;
|
||||
|
||||
|
||||
const AttributeReader = Class({
|
||||
extends: ParameterizedReader,
|
||||
type: "Attribute",
|
||||
parameter: "name"
|
||||
});
|
||||
exports.Attribute = AttributeReader;
|
||||
|
||||
const SrcURLReader = Class({
|
||||
extends: AttributeReader,
|
||||
name: "src",
|
||||
});
|
||||
exports.SrcURL = SrcURLReader;
|
||||
|
||||
const PageURLReader = Class({
|
||||
extends: QueryReader,
|
||||
path: "ownerDocument.URL",
|
||||
});
|
||||
exports.PageURL = PageURLReader;
|
||||
|
||||
const SelectorMatchReader = Class({
|
||||
extends: ParameterizedReader,
|
||||
type: "SelectorMatch",
|
||||
parameter: "selector"
|
||||
});
|
||||
exports.SelectorMatch = SelectorMatchReader;
|
||||
|
||||
const extractors = new WeakMap();
|
||||
extractors.id = 0;
|
||||
|
||||
|
||||
var Extractor = Class({
|
||||
extends: ParameterizedReader,
|
||||
type: "Extractor",
|
||||
parameter: "source",
|
||||
initialize: function(f) {
|
||||
this[this.parameter] = String(f);
|
||||
if (!extractors.has(f)) {
|
||||
extractors.id = extractors.id + 1;
|
||||
extractors.set(f, extractors.id);
|
||||
}
|
||||
|
||||
this.id = `reader/${this.type}.for(${extractors.get(f)})`
|
||||
}
|
||||
});
|
||||
exports.Extractor = Extractor;
|
|
@ -1,32 +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 shared = require("toolkit/require");
|
||||
const { Item, Separator, Menu, Contexts, Readers } = shared.require("sdk/context-menu/core");
|
||||
const { setupDisposable, disposeDisposable, Disposable } = require("sdk/core/disposable")
|
||||
const { Class } = require("sdk/core/heritage")
|
||||
|
||||
const makeDisposable = Type => Class({
|
||||
extends: Type,
|
||||
implements: [Disposable],
|
||||
initialize: Type.prototype.initialize,
|
||||
setup(...params) {
|
||||
Type.prototype.setup.call(this, ...params);
|
||||
setupDisposable(this);
|
||||
},
|
||||
dispose(...params) {
|
||||
disposeDisposable(this);
|
||||
Type.prototype.dispose.call(this, ...params);
|
||||
}
|
||||
});
|
||||
|
||||
exports.Separator = Separator;
|
||||
exports.Contexts = Contexts;
|
||||
exports.Readers = Readers;
|
||||
|
||||
// Subclass Item & Menu shared classes so their items
|
||||
// will be unloaded when add-on is unloaded.
|
||||
exports.Item = makeDisposable(Item);
|
||||
exports.Menu = makeDisposable(Menu);
|
|
@ -1,73 +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 { windows, isBrowser, isInteractive, isDocumentLoaded,
|
||||
getOuterId } = require("../window/utils");
|
||||
const { InputPort } = require("./system");
|
||||
const { lift, merges, foldp, keepIf, start, Input } = require("../event/utils");
|
||||
const { patch } = require("diffpatcher/index");
|
||||
const { Sequence, seq, filter, object, pairs } = require("../util/sequence");
|
||||
|
||||
|
||||
// Create lazy iterators from the regular arrays, although
|
||||
// once https://github.com/mozilla/addon-sdk/pull/1314 lands
|
||||
// `windows` will be transforme to lazy iterators.
|
||||
// When iterated over belowe sequences items will represent
|
||||
// state of windows at the time of iteration.
|
||||
const opened = seq(function*() {
|
||||
const items = windows("navigator:browser", {includePrivate: true});
|
||||
for (let item of items) {
|
||||
yield [getOuterId(item), item];
|
||||
}
|
||||
});
|
||||
const interactive = filter(([_, window]) => isInteractive(window), opened);
|
||||
const loaded = filter(([_, window]) => isDocumentLoaded(window), opened);
|
||||
|
||||
// Helper function that converts given argument to a delta.
|
||||
const Update = window => window && object([getOuterId(window), window]);
|
||||
const Delete = window => window && object([getOuterId(window), null]);
|
||||
|
||||
|
||||
// Signal represents delta for last top level window close.
|
||||
const LastClosed = lift(Delete,
|
||||
keepIf(isBrowser, null,
|
||||
new InputPort({topic: "domwindowclosed"})));
|
||||
exports.LastClosed = LastClosed;
|
||||
|
||||
const windowFor = document => document && document.defaultView;
|
||||
|
||||
// Signal represent delta for last top level window document becoming interactive.
|
||||
const InteractiveDoc = new InputPort({topic: "chrome-document-interactive"});
|
||||
const InteractiveWin = lift(windowFor, InteractiveDoc);
|
||||
const LastInteractive = lift(Update, keepIf(isBrowser, null, InteractiveWin));
|
||||
exports.LastInteractive = LastInteractive;
|
||||
|
||||
// Signal represent delta for last top level window loaded.
|
||||
const LoadedDoc = new InputPort({topic: "chrome-document-loaded"});
|
||||
const LoadedWin = lift(windowFor, LoadedDoc);
|
||||
const LastLoaded = lift(Update, keepIf(isBrowser, null, LoadedWin));
|
||||
exports.LastLoaded = LastLoaded;
|
||||
|
||||
|
||||
const initialize = input => {
|
||||
if (!input.initialized) {
|
||||
input.value = object(...input.value);
|
||||
Input.start(input);
|
||||
input.initialized = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Signal represents set of top level interactive windows, updated any
|
||||
// time new window becomes interactive or one get's closed.
|
||||
const Interactive = foldp(patch, interactive, merges([LastInteractive,
|
||||
LastClosed]));
|
||||
Interactive[start] = initialize;
|
||||
exports.Interactive = Interactive;
|
||||
|
||||
// Signal represents set of top level loaded window, updated any time
|
||||
// new window becomes interactive or one get's closed.
|
||||
const Loaded = foldp(patch, loaded, merges([LastLoaded, LastClosed]));
|
||||
Loaded[start] = initialize;
|
||||
exports.Loaded = Loaded;
|
|
@ -1,28 +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 { Cu } = require("chrome");
|
||||
const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
|
||||
const { receive } = require("../event/utils");
|
||||
const { InputPort } = require("./system");
|
||||
const { object} = require("../util/sequence");
|
||||
const { getOuterId } = require("../window/utils");
|
||||
|
||||
const Input = function() {};
|
||||
Input.prototype = Object.create(InputPort.prototype);
|
||||
|
||||
Input.prototype.onCustomizeStart = function (window) {
|
||||
receive(this, object([getOuterId(window), true]));
|
||||
}
|
||||
|
||||
Input.prototype.onCustomizeEnd = function (window) {
|
||||
receive(this, object([getOuterId(window), null]));
|
||||
}
|
||||
|
||||
Input.prototype.addListener = input => CustomizableUI.addListener(input);
|
||||
|
||||
Input.prototype.removeListener = input => CustomizableUI.removeListener(input);
|
||||
|
||||
exports.CustomizationInput = Input;
|
|
@ -1,85 +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 { Ci } = require("chrome");
|
||||
const { InputPort } = require("./system");
|
||||
const { getFrameElement, getOuterId,
|
||||
getOwnerBrowserWindow } = require("../window/utils");
|
||||
const { isnt } = require("../lang/functional");
|
||||
const { foldp, lift, merges, keepIf } = require("../event/utils");
|
||||
const { object } = require("../util/sequence");
|
||||
const { compose } = require("../lang/functional");
|
||||
const { LastClosed } = require("./browser");
|
||||
const { patch } = require("diffpatcher/index");
|
||||
|
||||
const Document = Ci.nsIDOMDocument;
|
||||
|
||||
const isntNull = isnt(null);
|
||||
|
||||
const frameID = frame => frame.id;
|
||||
const browserID = compose(getOuterId, getOwnerBrowserWindow);
|
||||
|
||||
const isInnerFrame = frame =>
|
||||
frame && frame.hasAttribute("data-is-sdk-inner-frame");
|
||||
|
||||
// Utility function that given content window loaded in our frame views returns
|
||||
// an actual frame. This basically takes care of fact that actual frame document
|
||||
// is loaded in the nested iframe. If content window is not loaded in the nested
|
||||
// frame of the frame view it returs null.
|
||||
const getFrame = document =>
|
||||
document && document.defaultView && getFrameElement(document.defaultView);
|
||||
|
||||
const FrameInput = function(options) {
|
||||
const input = keepIf(isInnerFrame, null,
|
||||
lift(getFrame, new InputPort(options)));
|
||||
return lift(frame => {
|
||||
if (!frame) return frame;
|
||||
const [id, owner] = [frameID(frame), browserID(frame)];
|
||||
return object([id, {owners: object([owner, options.update])}]);
|
||||
}, input);
|
||||
};
|
||||
|
||||
const LastLoading = new FrameInput({topic: "document-element-inserted",
|
||||
update: {readyState: "loading"}});
|
||||
exports.LastLoading = LastLoading;
|
||||
|
||||
const LastInteractive = new FrameInput({topic: "content-document-interactive",
|
||||
update: {readyState: "interactive"}});
|
||||
exports.LastInteractive = LastInteractive;
|
||||
|
||||
const LastLoaded = new FrameInput({topic: "content-document-loaded",
|
||||
update: {readyState: "complete"}});
|
||||
exports.LastLoaded = LastLoaded;
|
||||
|
||||
const LastUnloaded = new FrameInput({topic: "content-page-hidden",
|
||||
update: null});
|
||||
exports.LastUnloaded = LastUnloaded;
|
||||
|
||||
// Represents state of SDK frames in form of data structure:
|
||||
// {"frame#1": {"id": "frame#1",
|
||||
// "inbox": {"data": "ping",
|
||||
// "target": {"id": "frame#1", "owner": "outerWindowID#2"},
|
||||
// "source": {"id": "frame#1"}}
|
||||
// "url": "resource://addon-1/data/index.html",
|
||||
// "owners": {"outerWindowID#1": {"readyState": "loading"},
|
||||
// "outerWindowID#2": {"readyState": "complete"}}
|
||||
//
|
||||
//
|
||||
// frame#2: {"id": "frame#2",
|
||||
// "url": "resource://addon-1/data/main.html",
|
||||
// "outbox": {"data": "pong",
|
||||
// "source": {"id": "frame#2", "owner": "outerWindowID#1"}
|
||||
// "target": {"id": "frame#2"}}
|
||||
// "owners": {outerWindowID#1: {readyState: "interacitve"}}}}
|
||||
const Frames = foldp(patch, {}, merges([
|
||||
LastLoading,
|
||||
LastInteractive,
|
||||
LastLoaded,
|
||||
LastUnloaded,
|
||||
new InputPort({ id: "frame-mailbox" }),
|
||||
new InputPort({ id: "frame-change" }),
|
||||
new InputPort({ id: "frame-changed" })
|
||||
]));
|
||||
exports.Frames = Frames;
|
|
@ -1,113 +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 { Cc, Ci, Cr, Cu } = require("chrome");
|
||||
const { Input, start, stop, end, receive, outputs } = require("../event/utils");
|
||||
const { once, off } = require("../event/core");
|
||||
const { id: addonID } = require("../self");
|
||||
|
||||
const unloadMessage = require("@loader/unload");
|
||||
const observerService = Cc['@mozilla.org/observer-service;1'].
|
||||
getService(Ci.nsIObserverService);
|
||||
const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
|
||||
const addObserver = ShimWaiver.getProperty(observerService, "addObserver");
|
||||
const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver");
|
||||
|
||||
|
||||
const addonUnloadTopic = "sdk:loader:destroy";
|
||||
|
||||
const isXrayWrapper = Cu.isXrayWrapper;
|
||||
// In the past SDK used to double-wrap notifications dispatched, which
|
||||
// made them awkward to use outside of SDK. At present they no longer
|
||||
// do that, although we still supported for legacy reasons.
|
||||
const isLegacyWrapper = x =>
|
||||
x && x.wrappedJSObject &&
|
||||
"observersModuleSubjectWrapper" in x.wrappedJSObject;
|
||||
|
||||
const unwrapLegacy = x => x.wrappedJSObject.object;
|
||||
|
||||
// `InputPort` provides a way to create a signal out of the observer
|
||||
// notification subject's for the given `topic`. If `options.initial`
|
||||
// is provided it is used as initial value otherwise `null` is used.
|
||||
// Constructor can be given `options.id` that will be used to create
|
||||
// a `topic` which is namespaced to an add-on (this avoids conflicts
|
||||
// when multiple add-on are used, although in a future host probably
|
||||
// should just be shared across add-ons). It is also possible to
|
||||
// specify a specific `topic` via `options.topic` which is used as
|
||||
// without namespacing. Created signal ends whenever add-on is
|
||||
// unloaded.
|
||||
const InputPort = function InputPort({id, topic, initial}) {
|
||||
this.id = id || topic;
|
||||
this.topic = topic || "sdk:" + addonID + ":" + id;
|
||||
this.value = initial === void(0) ? null : initial;
|
||||
this.observing = false;
|
||||
this[outputs] = [];
|
||||
};
|
||||
|
||||
// InputPort type implements `Input` signal interface.
|
||||
InputPort.prototype = new Input();
|
||||
InputPort.prototype.constructor = InputPort;
|
||||
|
||||
// When port is started (which is when it's subgraph get's
|
||||
// first subscriber) actual observer is registered.
|
||||
InputPort.start = input => {
|
||||
input.addListener(input);
|
||||
// Also register add-on unload observer to end this signal
|
||||
// when that happens.
|
||||
addObserver(input, addonUnloadTopic, false);
|
||||
};
|
||||
InputPort.prototype[start] = InputPort.start;
|
||||
|
||||
InputPort.addListener = input => addObserver(input, input.topic, false);
|
||||
InputPort.prototype.addListener = InputPort.addListener;
|
||||
|
||||
// When port is stopped (which is when it's subgraph has no
|
||||
// no subcribers left) an actual observer unregistered.
|
||||
// Note that port stopped once it ends as well (which is when
|
||||
// add-on is unloaded).
|
||||
InputPort.stop = input => {
|
||||
input.removeListener(input);
|
||||
removeObserver(input, addonUnloadTopic);
|
||||
};
|
||||
InputPort.prototype[stop] = InputPort.stop;
|
||||
|
||||
InputPort.removeListener = input => removeObserver(input, input.topic);
|
||||
InputPort.prototype.removeListener = InputPort.removeListener;
|
||||
|
||||
// `InputPort` also implements `nsIObserver` interface and
|
||||
// `nsISupportsWeakReference` interfaces as it's going to be used as such.
|
||||
InputPort.prototype.QueryInterface = function(iid) {
|
||||
if (!iid.equals(Ci.nsIObserver) && !iid.equals(Ci.nsISupportsWeakReference))
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
// `InputPort` instances implement `observe` method, which is invoked when
|
||||
// observer notifications are dispatched. The `subject` of that notification
|
||||
// are received on this signal.
|
||||
InputPort.prototype.observe = function(subject, topic, data) {
|
||||
// Unwrap message from the subject. SDK used to have it's own version of
|
||||
// wrappedJSObjects which take precedence, if subject has `wrappedJSObject`
|
||||
// and it's not an XrayWrapper use it as message. Otherwise use subject as
|
||||
// is.
|
||||
const message = subject === null ? null :
|
||||
isLegacyWrapper(subject) ? unwrapLegacy(subject) :
|
||||
isXrayWrapper(subject) ? subject :
|
||||
subject.wrappedJSObject ? subject.wrappedJSObject :
|
||||
subject;
|
||||
|
||||
// If observer topic matches topic of the input port receive a message.
|
||||
if (topic === this.topic) {
|
||||
receive(this, message);
|
||||
}
|
||||
|
||||
// If observe topic is add-on unload topic we create an end message.
|
||||
if (topic === addonUnloadTopic && message === unloadMessage) {
|
||||
end(this);
|
||||
}
|
||||
};
|
||||
|
||||
exports.InputPort = InputPort;
|
|
@ -1,436 +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";
|
||||
|
||||
// The panel module currently supports only Firefox and SeaMonkey.
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps
|
||||
module.metadata = {
|
||||
"stability": "stable",
|
||||
"engines": {
|
||||
"Firefox": "*",
|
||||
"SeaMonkey": "*"
|
||||
}
|
||||
};
|
||||
|
||||
const { Cu, Ci } = require("chrome");
|
||||
lazyRequire(this, './timers', "setTimeout");
|
||||
const { Class } = require("./core/heritage");
|
||||
const { DefaultWeakMap, merge } = require("./util/object");
|
||||
const { WorkerHost } = require("./content/utils");
|
||||
lazyRequire(this, "./deprecated/sync-worker", "Worker");
|
||||
const { Disposable } = require("./core/disposable");
|
||||
const { WeakReference } = require('./core/reference');
|
||||
const { contract: loaderContract } = require("./content/loader");
|
||||
const { contract } = require("./util/contract");
|
||||
lazyRequire(this, "./event/core", "on", "off", "emit", "setListeners");
|
||||
const { EventTarget } = require("./event/target");
|
||||
lazyRequireModule(this, "./panel/utils", "domPanel");
|
||||
lazyRequire(this, './frame/utils', "getDocShell");
|
||||
const { events } = require("./panel/events");
|
||||
const { filter, pipe, stripListeners } = require("./event/utils");
|
||||
lazyRequire(this, "./view/core", "getNodeView", "getActiveView");
|
||||
lazyRequire(this, "./lang/type", "isNil", "isObject", "isNumber");
|
||||
lazyRequire(this, "./content/utils", "getAttachEventType");
|
||||
const { number, boolean, object } = require('./deprecated/api-utils');
|
||||
lazyRequire(this, "./stylesheet/style", "Style");
|
||||
lazyRequire(this, "./content/mod", "attach", "detach");
|
||||
|
||||
var isRect = ({top, right, bottom, left}) => [top, right, bottom, left].
|
||||
some(value => isNumber(value) && !isNaN(value));
|
||||
|
||||
var isSDKObj = obj => obj instanceof Class;
|
||||
|
||||
var rectContract = contract({
|
||||
top: number,
|
||||
right: number,
|
||||
bottom: number,
|
||||
left: number
|
||||
});
|
||||
|
||||
var position = {
|
||||
is: object,
|
||||
map: v => (isNil(v) || isSDKObj(v) || !isObject(v)) ? v : rectContract(v),
|
||||
ok: v => isNil(v) || isSDKObj(v) || (isObject(v) && isRect(v)),
|
||||
msg: 'The option "position" must be a SDK object registered as anchor; ' +
|
||||
'or an object with one or more of the following keys set to numeric ' +
|
||||
'values: top, right, bottom, left.'
|
||||
}
|
||||
|
||||
var displayContract = contract({
|
||||
width: number,
|
||||
height: number,
|
||||
focus: boolean,
|
||||
position: position
|
||||
});
|
||||
|
||||
var panelContract = contract(merge({
|
||||
// contentStyle* / contentScript* are sharing the same validation constraints,
|
||||
// so they can be mostly reused, except for the messages.
|
||||
contentStyle: merge(Object.create(loaderContract.rules.contentScript), {
|
||||
msg: 'The `contentStyle` option must be a string or an array of strings.'
|
||||
}),
|
||||
contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), {
|
||||
msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
|
||||
}),
|
||||
contextMenu: boolean,
|
||||
allow: {
|
||||
is: ['object', 'undefined', 'null'],
|
||||
map: function (allow) { return { script: !allow || allow.script !== false }}
|
||||
},
|
||||
}, displayContract.rules, loaderContract.rules));
|
||||
|
||||
function Allow(panel) {
|
||||
return {
|
||||
get script() { return getDocShell(viewFor(panel).backgroundFrame).allowJavascript; },
|
||||
set script(value) { return setScriptState(panel, value); },
|
||||
};
|
||||
}
|
||||
|
||||
function setScriptState(panel, value) {
|
||||
let view = viewFor(panel);
|
||||
getDocShell(view.backgroundFrame).allowJavascript = value;
|
||||
getDocShell(view.viewFrame).allowJavascript = value;
|
||||
view.setAttribute("sdkscriptenabled", "" + value);
|
||||
}
|
||||
|
||||
function isDisposed(panel) {
|
||||
return !views.has(panel);
|
||||
}
|
||||
|
||||
var optionsMap = new WeakMap();
|
||||
var panels = new WeakMap();
|
||||
var models = new WeakMap();
|
||||
var views = new DefaultWeakMap(panel => {
|
||||
let model = models.get(panel);
|
||||
|
||||
// Setup view
|
||||
let viewOptions = {allowJavascript: !model.allow || (model.allow.script !== false)};
|
||||
let view = domPanel.make(null, viewOptions);
|
||||
panels.set(view, panel);
|
||||
|
||||
// Load panel content.
|
||||
domPanel.setURL(view, model.contentURL);
|
||||
|
||||
// Allow context menu
|
||||
domPanel.allowContextMenu(view, model.contextMenu);
|
||||
|
||||
return view;
|
||||
});
|
||||
var workers = new DefaultWeakMap(panel => {
|
||||
let options = optionsMap.get(panel);
|
||||
|
||||
let worker = new Worker(stripListeners(options));
|
||||
workers.set(panel, worker);
|
||||
|
||||
// pipe events from worker to a panel.
|
||||
pipe(worker, panel);
|
||||
|
||||
return worker;
|
||||
});
|
||||
var styles = new WeakMap();
|
||||
|
||||
const viewFor = (panel) => views.get(panel);
|
||||
const modelFor = (panel) => models.get(panel);
|
||||
const panelFor = (view) => panels.get(view);
|
||||
const workerFor = (panel) => workers.get(panel);
|
||||
const styleFor = (panel) => styles.get(panel);
|
||||
|
||||
function getPanelFromWeakRef(weakRef) {
|
||||
if (!weakRef) {
|
||||
return null;
|
||||
}
|
||||
let panel = weakRef.get();
|
||||
if (!panel) {
|
||||
return null;
|
||||
}
|
||||
if (isDisposed(panel)) {
|
||||
return null;
|
||||
}
|
||||
return panel;
|
||||
}
|
||||
|
||||
var SinglePanelManager = {
|
||||
visiblePanel: null,
|
||||
enqueuedPanel: null,
|
||||
enqueuedPanelCallback: null,
|
||||
// Calls |callback| with no arguments when the panel may be shown.
|
||||
requestOpen: function(panelToOpen, callback) {
|
||||
let currentPanel = getPanelFromWeakRef(SinglePanelManager.visiblePanel);
|
||||
if (currentPanel || SinglePanelManager.enqueuedPanel) {
|
||||
SinglePanelManager.enqueuedPanel = Cu.getWeakReference(panelToOpen);
|
||||
SinglePanelManager.enqueuedPanelCallback = callback;
|
||||
if (currentPanel && currentPanel.isShowing) {
|
||||
currentPanel.hide();
|
||||
}
|
||||
} else {
|
||||
SinglePanelManager.notifyPanelCanOpen(panelToOpen, callback);
|
||||
}
|
||||
},
|
||||
notifyPanelCanOpen: function(panel, callback) {
|
||||
let view = viewFor(panel);
|
||||
// Can't pass an arrow function as the event handler because we need to be
|
||||
// able to call |removeEventListener| later.
|
||||
view.addEventListener("popuphidden", SinglePanelManager.onVisiblePanelHidden, true);
|
||||
view.addEventListener("popupshown", SinglePanelManager.onVisiblePanelShown);
|
||||
SinglePanelManager.enqueuedPanel = null;
|
||||
SinglePanelManager.enqueuedPanelCallback = null;
|
||||
SinglePanelManager.visiblePanel = Cu.getWeakReference(panel);
|
||||
callback();
|
||||
},
|
||||
onVisiblePanelShown: function(event) {
|
||||
let panel = panelFor(event.target);
|
||||
if (SinglePanelManager.enqueuedPanel) {
|
||||
// Another panel started waiting for |panel| to close before |panel| was
|
||||
// even done opening.
|
||||
panel.hide();
|
||||
}
|
||||
},
|
||||
onVisiblePanelHidden: function(event) {
|
||||
let view = event.target;
|
||||
let panel = panelFor(view);
|
||||
let currentPanel = getPanelFromWeakRef(SinglePanelManager.visiblePanel);
|
||||
if (currentPanel && currentPanel != panel) {
|
||||
return;
|
||||
}
|
||||
SinglePanelManager.visiblePanel = null;
|
||||
view.removeEventListener("popuphidden", SinglePanelManager.onVisiblePanelHidden, true);
|
||||
view.removeEventListener("popupshown", SinglePanelManager.onVisiblePanelShown);
|
||||
let nextPanel = getPanelFromWeakRef(SinglePanelManager.enqueuedPanel);
|
||||
let nextPanelCallback = SinglePanelManager.enqueuedPanelCallback;
|
||||
if (nextPanel) {
|
||||
SinglePanelManager.notifyPanelCanOpen(nextPanel, nextPanelCallback);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Panel = Class({
|
||||
implements: [
|
||||
// Generate accessors for the validated properties that update model on
|
||||
// set and return values from model on get.
|
||||
panelContract.properties(modelFor),
|
||||
EventTarget,
|
||||
Disposable,
|
||||
WeakReference
|
||||
],
|
||||
extends: WorkerHost(workerFor),
|
||||
setup: function setup(options) {
|
||||
let model = merge({
|
||||
defaultWidth: 320,
|
||||
defaultHeight: 240,
|
||||
focus: true,
|
||||
position: Object.freeze({}),
|
||||
contextMenu: false
|
||||
}, panelContract(options));
|
||||
model.ready = false;
|
||||
models.set(this, model);
|
||||
|
||||
if (model.contentStyle || model.contentStyleFile) {
|
||||
styles.set(this, Style({
|
||||
uri: model.contentStyleFile,
|
||||
source: model.contentStyle
|
||||
}));
|
||||
}
|
||||
|
||||
optionsMap.set(this, options);
|
||||
|
||||
// Setup listeners.
|
||||
setListeners(this, options);
|
||||
},
|
||||
dispose: function dispose() {
|
||||
if (views.has(this))
|
||||
this.hide();
|
||||
off(this);
|
||||
|
||||
workerFor(this).destroy();
|
||||
detach(styleFor(this));
|
||||
|
||||
if (views.has(this))
|
||||
domPanel.dispose(viewFor(this));
|
||||
|
||||
views.delete(this);
|
||||
},
|
||||
/* Public API: Panel.width */
|
||||
get width() {
|
||||
return modelFor(this).width;
|
||||
},
|
||||
set width(value) {
|
||||
this.resize(value, this.height);
|
||||
},
|
||||
/* Public API: Panel.height */
|
||||
get height() {
|
||||
return modelFor(this).height;
|
||||
},
|
||||
set height(value) {
|
||||
this.resize(this.width, value);
|
||||
},
|
||||
|
||||
/* Public API: Panel.focus */
|
||||
get focus() {
|
||||
return modelFor(this).focus;
|
||||
},
|
||||
|
||||
/* Public API: Panel.position */
|
||||
get position() {
|
||||
return modelFor(this).position;
|
||||
},
|
||||
|
||||
/* Public API: Panel.contextMenu */
|
||||
get contextMenu() {
|
||||
return modelFor(this).contextMenu;
|
||||
},
|
||||
set contextMenu(allow) {
|
||||
let model = modelFor(this);
|
||||
model.contextMenu = panelContract({ contextMenu: allow }).contextMenu;
|
||||
domPanel.allowContextMenu(viewFor(this), model.contextMenu);
|
||||
},
|
||||
|
||||
get contentURL() {
|
||||
return modelFor(this).contentURL;
|
||||
},
|
||||
set contentURL(value) {
|
||||
let model = modelFor(this);
|
||||
model.contentURL = panelContract({ contentURL: value }).contentURL;
|
||||
domPanel.setURL(viewFor(this), model.contentURL);
|
||||
// Detach worker so that messages send will be queued until it's
|
||||
// reatached once panel content is ready.
|
||||
workerFor(this).detach();
|
||||
},
|
||||
|
||||
get allow() { return Allow(this); },
|
||||
set allow(value) {
|
||||
let allowJavascript = panelContract({ allow: value }).allow.script;
|
||||
return setScriptState(this, value);
|
||||
},
|
||||
|
||||
/* Public API: Panel.isShowing */
|
||||
get isShowing() {
|
||||
return !isDisposed(this) && domPanel.isOpen(viewFor(this));
|
||||
},
|
||||
|
||||
/* Public API: Panel.show */
|
||||
show: function show(options={}, anchor) {
|
||||
let view = viewFor(this);
|
||||
SinglePanelManager.requestOpen(this, () => {
|
||||
if (options instanceof Ci.nsIDOMElement) {
|
||||
[anchor, options] = [options, null];
|
||||
}
|
||||
|
||||
if (anchor instanceof Ci.nsIDOMElement) {
|
||||
console.warn(
|
||||
"Passing a DOM node to Panel.show() method is an unsupported " +
|
||||
"feature that will be soon replaced. " +
|
||||
"See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877"
|
||||
);
|
||||
}
|
||||
|
||||
let model = modelFor(this);
|
||||
let anchorView = getNodeView(anchor || options.position || model.position);
|
||||
|
||||
options = merge({
|
||||
position: model.position,
|
||||
width: model.width,
|
||||
height: model.height,
|
||||
defaultWidth: model.defaultWidth,
|
||||
defaultHeight: model.defaultHeight,
|
||||
focus: model.focus,
|
||||
contextMenu: model.contextMenu
|
||||
}, displayContract(options));
|
||||
|
||||
if (!isDisposed(this)) {
|
||||
domPanel.show(view, options, anchorView);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
/* Public API: Panel.hide */
|
||||
hide: function hide() {
|
||||
// Quit immediately if panel is disposed or there is no state change.
|
||||
domPanel.close(viewFor(this));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/* Public API: Panel.resize */
|
||||
resize: function resize(width, height) {
|
||||
let model = modelFor(this);
|
||||
let view = viewFor(this);
|
||||
let change = panelContract({
|
||||
width: width || model.width || model.defaultWidth,
|
||||
height: height || model.height || model.defaultHeight
|
||||
});
|
||||
|
||||
model.width = change.width
|
||||
model.height = change.height
|
||||
|
||||
domPanel.resize(view, model.width, model.height);
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
exports.Panel = Panel;
|
||||
|
||||
// Note must be defined only after value to `Panel` is assigned.
|
||||
getActiveView.define(Panel, viewFor);
|
||||
|
||||
// Filter panel events to only panels that are create by this module.
|
||||
var panelEvents = filter(events, ({target}) => panelFor(target));
|
||||
|
||||
// Panel events emitted after panel has being shown.
|
||||
var shows = filter(panelEvents, ({type}) => type === "popupshown");
|
||||
|
||||
// Panel events emitted after panel became hidden.
|
||||
var hides = filter(panelEvents, ({type}) => type === "popuphidden");
|
||||
|
||||
// Panel events emitted after content inside panel is ready. For different
|
||||
// panels ready may mean different state based on `contentScriptWhen` attribute.
|
||||
// Weather given event represents readyness is detected by `getAttachEventType`
|
||||
// helper function.
|
||||
var ready = filter(panelEvents, ({type, target}) =>
|
||||
getAttachEventType(modelFor(panelFor(target))) === type);
|
||||
|
||||
// Panel event emitted when the contents of the panel has been loaded.
|
||||
var readyToShow = filter(panelEvents, ({type}) => type === "DOMContentLoaded");
|
||||
|
||||
// Styles should be always added as soon as possible, and doesn't makes them
|
||||
// depends on `contentScriptWhen`
|
||||
var start = filter(panelEvents, ({type}) => type === "document-element-inserted");
|
||||
|
||||
// Forward panel show / hide events to panel's own event listeners.
|
||||
on(shows, "data", ({target}) => {
|
||||
let panel = panelFor(target);
|
||||
if (modelFor(panel).ready)
|
||||
emit(panel, "show");
|
||||
});
|
||||
|
||||
on(hides, "data", ({target}) => {
|
||||
let panel = panelFor(target);
|
||||
if (modelFor(panel).ready)
|
||||
emit(panel, "hide");
|
||||
});
|
||||
|
||||
on(ready, "data", ({target}) => {
|
||||
let panel = panelFor(target);
|
||||
let window = domPanel.getContentDocument(target).defaultView;
|
||||
|
||||
workerFor(panel).attach(window);
|
||||
});
|
||||
|
||||
on(readyToShow, "data", ({target}) => {
|
||||
let panel = panelFor(target);
|
||||
|
||||
if (!modelFor(panel).ready) {
|
||||
modelFor(panel).ready = true;
|
||||
|
||||
if (viewFor(panel).state == "open")
|
||||
emit(panel, "show");
|
||||
}
|
||||
});
|
||||
|
||||
on(start, "data", ({target}) => {
|
||||
let panel = panelFor(target);
|
||||
let window = domPanel.getContentDocument(target).defaultView;
|
||||
|
||||
attach(styleFor(panel), window);
|
||||
});
|
|
@ -1,27 +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";
|
||||
|
||||
// This module basically translates system/events to a SDK standard events
|
||||
// so that `map`, `filter` and other utilities could be used with them.
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const events = require("../system/events");
|
||||
lazyRequire(this, "../event/core", "emit");
|
||||
|
||||
var channel = {};
|
||||
|
||||
function forward({ subject, type, data }) {
|
||||
return emit(channel, "data", { target: subject, type: type, data: data });
|
||||
}
|
||||
|
||||
["popupshowing", "popuphiding", "popupshown", "popuphidden",
|
||||
"document-element-inserted", "DOMContentLoaded", "load"
|
||||
].forEach(type => events.on(type, forward));
|
||||
|
||||
exports.events = channel;
|
|
@ -1,451 +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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const { Services } = require("resource://gre/modules/Services.jsm");
|
||||
lazyRequire(this, "../timers", "setTimeout");
|
||||
lazyRequire(this, "../system", "platform");
|
||||
lazyRequire(this, "../window/utils", "getMostRecentBrowserWindow", "getOwnerBrowserWindow",
|
||||
"getScreenPixelsPerCSSPixel");
|
||||
|
||||
lazyRequire(this, "../frame/utils", { "create": "createFrame" }, "swapFrameLoaders", "getDocShell");
|
||||
lazyRequire(this, "../addon/window", { "window": "addonWindow" });
|
||||
lazyRequire(this, "../lang/type", "isNil");
|
||||
lazyRequire(this, '../self', "data");
|
||||
|
||||
lazyRequireModule(this, "../system/events", "events");
|
||||
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
|
||||
function calculateRegion({ position, width, height, defaultWidth, defaultHeight }, rect) {
|
||||
position = position || {};
|
||||
|
||||
let x, y;
|
||||
|
||||
let hasTop = !isNil(position.top);
|
||||
let hasRight = !isNil(position.right);
|
||||
let hasBottom = !isNil(position.bottom);
|
||||
let hasLeft = !isNil(position.left);
|
||||
let hasWidth = !isNil(width);
|
||||
let hasHeight = !isNil(height);
|
||||
|
||||
// if width is not specified by constructor or show's options, then get
|
||||
// the default width
|
||||
if (!hasWidth)
|
||||
width = defaultWidth;
|
||||
|
||||
// if height is not specified by constructor or show's options, then get
|
||||
// the default height
|
||||
if (!hasHeight)
|
||||
height = defaultHeight;
|
||||
|
||||
// default position is centered
|
||||
x = (rect.right - width) / 2;
|
||||
y = (rect.top + rect.bottom - height) / 2;
|
||||
|
||||
if (hasTop) {
|
||||
y = rect.top + position.top;
|
||||
|
||||
if (hasBottom && !hasHeight)
|
||||
height = rect.bottom - position.bottom - y;
|
||||
}
|
||||
else if (hasBottom) {
|
||||
y = rect.bottom - position.bottom - height;
|
||||
}
|
||||
|
||||
if (hasLeft) {
|
||||
x = position.left;
|
||||
|
||||
if (hasRight && !hasWidth)
|
||||
width = rect.right - position.right - x;
|
||||
}
|
||||
else if (hasRight) {
|
||||
x = rect.right - width - position.right;
|
||||
}
|
||||
|
||||
return {x: x, y: y, width: width, height: height};
|
||||
}
|
||||
|
||||
function open(panel, options, anchor) {
|
||||
// Wait for the XBL binding to be constructed
|
||||
if (!panel.openPopup) setTimeout(open, 50, panel, options, anchor);
|
||||
else display(panel, options, anchor);
|
||||
}
|
||||
exports.open = open;
|
||||
|
||||
function isOpen(panel) {
|
||||
return panel.state === "open"
|
||||
}
|
||||
exports.isOpen = isOpen;
|
||||
|
||||
function isOpening(panel) {
|
||||
return panel.state === "showing"
|
||||
}
|
||||
exports.isOpening = isOpening
|
||||
|
||||
function close(panel) {
|
||||
// Sometimes "TypeError: panel.hidePopup is not a function" is thrown
|
||||
// when quitting the host application while a panel is visible. To suppress
|
||||
// these errors, check for "hidePopup" in panel before calling it.
|
||||
// It's not clear if there's an issue or it's expected behavior.
|
||||
// See Bug 1151796.
|
||||
|
||||
return panel.hidePopup && panel.hidePopup();
|
||||
}
|
||||
exports.close = close
|
||||
|
||||
|
||||
function resize(panel, width, height) {
|
||||
// Resize the iframe instead of using panel.sizeTo
|
||||
// because sizeTo doesn't work with arrow panels
|
||||
if (panel.firstChild) {
|
||||
panel.firstChild.style.width = width + "px";
|
||||
panel.firstChild.style.height = height + "px";
|
||||
}
|
||||
}
|
||||
exports.resize = resize
|
||||
|
||||
function display(panel, options, anchor) {
|
||||
let document = panel.ownerDocument;
|
||||
|
||||
let x, y;
|
||||
let { width, height, defaultWidth, defaultHeight } = options;
|
||||
|
||||
let popupPosition = null;
|
||||
|
||||
// Panel XBL has some SDK incompatible styling decisions. We shim panel
|
||||
// instances until proper fix for Bug 859504 is shipped.
|
||||
shimDefaultStyle(panel);
|
||||
|
||||
if (!anchor) {
|
||||
// The XUL Panel doesn't have an arrow, so the margin needs to be reset
|
||||
// in order to, be positioned properly
|
||||
panel.style.margin = "0";
|
||||
|
||||
let viewportRect = document.defaultView.gBrowser.getBoundingClientRect();
|
||||
|
||||
({x, y, width, height} = calculateRegion(options, viewportRect));
|
||||
}
|
||||
else {
|
||||
// The XUL Panel has an arrow, so the margin needs to be reset
|
||||
// to the default value.
|
||||
panel.style.margin = "";
|
||||
let { CustomizableUI, window } = anchor.ownerGlobal;
|
||||
|
||||
// In Australis, widgets may be positioned in an overflow panel or the
|
||||
// menu panel.
|
||||
// In such cases clicking this widget will hide the overflow/menu panel,
|
||||
// and the widget's panel will show instead.
|
||||
// If `CustomizableUI` is not available, it means the anchor is not in a
|
||||
// chrome browser window, and therefore there is no need for this check.
|
||||
if (CustomizableUI) {
|
||||
let node = anchor;
|
||||
({anchor} = CustomizableUI.getWidget(anchor.id).forWindow(window));
|
||||
|
||||
// if `node` is not the `anchor` itself, it means the widget is
|
||||
// positioned in a panel, therefore we have to hide it before show
|
||||
// the widget's panel in the same anchor
|
||||
if (node !== anchor)
|
||||
CustomizableUI.hidePanelForNode(anchor);
|
||||
}
|
||||
|
||||
width = width || defaultWidth;
|
||||
height = height || defaultHeight;
|
||||
|
||||
// Open the popup by the anchor.
|
||||
let rect = anchor.getBoundingClientRect();
|
||||
|
||||
let zoom = getScreenPixelsPerCSSPixel(window);
|
||||
let screenX = rect.left + window.mozInnerScreenX * zoom;
|
||||
let screenY = rect.top + window.mozInnerScreenY * zoom;
|
||||
|
||||
// Set up the vertical position of the popup relative to the anchor
|
||||
// (always display the arrow on anchor center)
|
||||
let horizontal, vertical;
|
||||
if (screenY > window.screen.availHeight / 2 + height)
|
||||
vertical = "top";
|
||||
else
|
||||
vertical = "bottom";
|
||||
|
||||
if (screenY > window.screen.availWidth / 2 + width)
|
||||
horizontal = "left";
|
||||
else
|
||||
horizontal = "right";
|
||||
|
||||
let verticalInverse = vertical == "top" ? "bottom" : "top";
|
||||
popupPosition = vertical + "center " + verticalInverse + horizontal;
|
||||
|
||||
// Allow panel to flip itself if the panel can't be displayed at the
|
||||
// specified position (useful if we compute a bad position or if the
|
||||
// user moves the window and panel remains visible)
|
||||
panel.setAttribute("flip", "both");
|
||||
}
|
||||
|
||||
if (!panel.viewFrame) {
|
||||
panel.viewFrame = document.importNode(panel.backgroundFrame, false);
|
||||
panel.appendChild(panel.viewFrame);
|
||||
|
||||
let {privateBrowsingId} = getDocShell(panel.viewFrame).getOriginAttributes();
|
||||
let principal = Services.scriptSecurityManager.createNullPrincipal({privateBrowsingId});
|
||||
getDocShell(panel.viewFrame).createAboutBlankContentViewer(principal);
|
||||
}
|
||||
|
||||
// Resize the iframe instead of using panel.sizeTo
|
||||
// because sizeTo doesn't work with arrow panels
|
||||
panel.firstChild.style.width = width + "px";
|
||||
panel.firstChild.style.height = height + "px";
|
||||
|
||||
panel.openPopup(anchor, popupPosition, x, y);
|
||||
}
|
||||
exports.display = display;
|
||||
|
||||
// This utility function is just a workaround until Bug 859504 has shipped.
|
||||
function shimDefaultStyle(panel) {
|
||||
let document = panel.ownerDocument;
|
||||
// Please note that `panel` needs to be part of document in order to reach
|
||||
// it's anonymous nodes. One of the anonymous node has a big padding which
|
||||
// doesn't work well since panel frame needs to fill all of the panel.
|
||||
// XBL binding is a not the best option as it's applied asynchronously, and
|
||||
// makes injected frames behave in strange way. Also this feels a lot
|
||||
// cheaper to do.
|
||||
["panel-inner-arrowcontent", "panel-arrowcontent"].forEach(function(value) {
|
||||
let node = document.getAnonymousElementByAttribute(panel, "class", value);
|
||||
if (node) node.style.padding = 0;
|
||||
});
|
||||
}
|
||||
|
||||
function show(panel, options, anchor) {
|
||||
// Prevent the panel from getting focus when showing up
|
||||
// if focus is set to false
|
||||
panel.setAttribute("noautofocus", !options.focus);
|
||||
|
||||
let window = anchor && getOwnerBrowserWindow(anchor);
|
||||
let { document } = window ? window : getMostRecentBrowserWindow();
|
||||
attach(panel, document);
|
||||
|
||||
open(panel, options, anchor);
|
||||
}
|
||||
exports.show = show
|
||||
|
||||
function onPanelClick(event) {
|
||||
let { target, metaKey, ctrlKey, shiftKey, button } = event;
|
||||
let accel = platform === "darwin" ? metaKey : ctrlKey;
|
||||
let isLeftClick = button === 0;
|
||||
let isMiddleClick = button === 1;
|
||||
|
||||
if ((isLeftClick && (accel || shiftKey)) || isMiddleClick) {
|
||||
let link = target.closest('a');
|
||||
|
||||
if (link && link.href)
|
||||
getMostRecentBrowserWindow().openUILink(link.href, event)
|
||||
}
|
||||
}
|
||||
|
||||
function setupPanelFrame(frame) {
|
||||
frame.setAttribute("flex", 1);
|
||||
frame.setAttribute("transparent", "transparent");
|
||||
frame.setAttribute("autocompleteenabled", true);
|
||||
frame.setAttribute("tooltip", "aHTMLTooltip");
|
||||
if (platform === "darwin") {
|
||||
frame.style.borderRadius = "var(--arrowpanel-border-radius, 3.5px)";
|
||||
frame.style.padding = "1px";
|
||||
}
|
||||
}
|
||||
|
||||
function make(document, options) {
|
||||
document = document || getMostRecentBrowserWindow().document;
|
||||
let panel = document.createElementNS(XUL_NS, "panel");
|
||||
panel.setAttribute("type", "arrow");
|
||||
panel.setAttribute("sdkscriptenabled", options.allowJavascript);
|
||||
|
||||
// The panel needs to be attached to a browser window in order for us
|
||||
// to copy browser styles to the content document when it loads.
|
||||
attach(panel, document);
|
||||
|
||||
let frameOptions = {
|
||||
allowJavascript: options.allowJavascript,
|
||||
allowPlugins: true,
|
||||
allowAuth: true,
|
||||
allowWindowControl: false,
|
||||
// Need to override `nodeName` to use `iframe` as `browsers` save session
|
||||
// history and in consequence do not dispatch "inner-window-destroyed"
|
||||
// notifications.
|
||||
browser: false,
|
||||
};
|
||||
|
||||
let backgroundFrame = createFrame(addonWindow, frameOptions);
|
||||
setupPanelFrame(backgroundFrame);
|
||||
|
||||
getDocShell(backgroundFrame).inheritPrivateBrowsingId = false;
|
||||
|
||||
function onPopupShowing({type, target}) {
|
||||
if (target === this) {
|
||||
let attrs = getDocShell(backgroundFrame).getOriginAttributes();
|
||||
getDocShell(panel.viewFrame).setOriginAttributes(attrs);
|
||||
|
||||
swapFrameLoaders(backgroundFrame, panel.viewFrame);
|
||||
}
|
||||
}
|
||||
|
||||
function onPopupHiding({type, target}) {
|
||||
if (target === this) {
|
||||
swapFrameLoaders(backgroundFrame, panel.viewFrame);
|
||||
|
||||
panel.viewFrame.remove();
|
||||
panel.viewFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onContentReady({target, type}) {
|
||||
if (target === getContentDocument(panel)) {
|
||||
style(panel);
|
||||
events.emit(type, { subject: panel });
|
||||
}
|
||||
}
|
||||
|
||||
function onContentLoad({target, type}) {
|
||||
if (target === getContentDocument(panel))
|
||||
events.emit(type, { subject: panel });
|
||||
}
|
||||
|
||||
function onContentChange({subject: document, type}) {
|
||||
if (document === getContentDocument(panel) && document.defaultView)
|
||||
events.emit(type, { subject: panel });
|
||||
}
|
||||
|
||||
function onPanelStateChange({target, type}) {
|
||||
if (target === this)
|
||||
events.emit(type, { subject: panel })
|
||||
}
|
||||
|
||||
panel.addEventListener("popupshowing", onPopupShowing);
|
||||
panel.addEventListener("popuphiding", onPopupHiding);
|
||||
for (let event of ["popupshowing", "popuphiding", "popupshown", "popuphidden"])
|
||||
panel.addEventListener(event, onPanelStateChange);
|
||||
|
||||
panel.addEventListener("click", onPanelClick);
|
||||
|
||||
// Panel content document can be either in panel `viewFrame` or in
|
||||
// a `backgroundFrame` depending on panel state. Listeners are set
|
||||
// on both to avoid setting and removing listeners on panel state changes.
|
||||
|
||||
panel.addEventListener("DOMContentLoaded", onContentReady, true);
|
||||
backgroundFrame.addEventListener("DOMContentLoaded", onContentReady, true);
|
||||
|
||||
panel.addEventListener("load", onContentLoad, true);
|
||||
backgroundFrame.addEventListener("load", onContentLoad, true);
|
||||
|
||||
events.on("document-element-inserted", onContentChange);
|
||||
|
||||
panel.backgroundFrame = backgroundFrame;
|
||||
panel.viewFrame = null;
|
||||
|
||||
// Store event listener on the panel instance so that it won't be GC-ed
|
||||
// while panel is alive.
|
||||
panel.onContentChange = onContentChange;
|
||||
|
||||
return panel;
|
||||
}
|
||||
exports.make = make;
|
||||
|
||||
function attach(panel, document) {
|
||||
document = document || getMostRecentBrowserWindow().document;
|
||||
let container = document.getElementById("mainPopupSet");
|
||||
if (container !== panel.parentNode) {
|
||||
detach(panel);
|
||||
document.getElementById("mainPopupSet").appendChild(panel);
|
||||
}
|
||||
}
|
||||
exports.attach = attach;
|
||||
|
||||
function detach(panel) {
|
||||
if (panel.parentNode) panel.remove();
|
||||
}
|
||||
exports.detach = detach;
|
||||
|
||||
function dispose(panel) {
|
||||
panel.backgroundFrame.remove();
|
||||
panel.backgroundFrame = null;
|
||||
events.off("document-element-inserted", panel.onContentChange);
|
||||
panel.onContentChange = null;
|
||||
detach(panel);
|
||||
}
|
||||
exports.dispose = dispose;
|
||||
|
||||
function style(panel) {
|
||||
/**
|
||||
Injects default OS specific panel styles into content document that is loaded
|
||||
into given panel. Optionally `document` of the browser window can be
|
||||
given to inherit styles from it, by default it will use either panel owner
|
||||
document or an active browser's document. It should not matter though unless
|
||||
Firefox decides to style windows differently base on profile or mode like
|
||||
chrome for example.
|
||||
**/
|
||||
|
||||
try {
|
||||
let document = panel.ownerDocument;
|
||||
let contentDocument = getContentDocument(panel);
|
||||
let window = document.defaultView;
|
||||
let node = document.getAnonymousElementByAttribute(panel, "class",
|
||||
"panel-arrowcontent");
|
||||
|
||||
let { color, fontFamily, fontSize, fontWeight } = window.getComputedStyle(node);
|
||||
|
||||
let style = contentDocument.createElement("style");
|
||||
style.id = "sdk-panel-style";
|
||||
style.textContent = "body { " +
|
||||
"color: " + color + ";" +
|
||||
"font-family: " + fontFamily + ";" +
|
||||
"font-weight: " + fontWeight + ";" +
|
||||
"font-size: " + fontSize + ";" +
|
||||
"}";
|
||||
|
||||
let container = contentDocument.head ? contentDocument.head :
|
||||
contentDocument.documentElement;
|
||||
|
||||
if (container.firstChild)
|
||||
container.insertBefore(style, container.firstChild);
|
||||
else
|
||||
container.appendChild(style);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Unable to apply panel style");
|
||||
console.exception(error);
|
||||
}
|
||||
}
|
||||
exports.style = style;
|
||||
|
||||
var getContentFrame = panel => panel.viewFrame || panel.backgroundFrame;
|
||||
exports.getContentFrame = getContentFrame;
|
||||
|
||||
function getContentDocument(panel) {
|
||||
return getContentFrame(panel).contentDocument;
|
||||
}
|
||||
exports.getContentDocument = getContentDocument;
|
||||
|
||||
function setURL(panel, url) {
|
||||
let frame = getContentFrame(panel);
|
||||
let webNav = getDocShell(frame).QueryInterface(Ci.nsIWebNavigation);
|
||||
|
||||
webNav.loadURI(url ? data.url(url) : "about:blank", 0, null, null, null);
|
||||
}
|
||||
|
||||
exports.setURL = setURL;
|
||||
|
||||
function allowContextMenu(panel, allow) {
|
||||
if (allow) {
|
||||
panel.setAttribute("context", "contentAreaContextMenu");
|
||||
}
|
||||
else {
|
||||
panel.removeAttribute("context");
|
||||
}
|
||||
}
|
||||
exports.allowContextMenu = allowContextMenu;
|
|
@ -1,25 +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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'experimental',
|
||||
'engines': {
|
||||
'Firefox': '> 28'
|
||||
}
|
||||
};
|
||||
|
||||
lazyRequire(this, './ui/button/action', 'ActionButton');
|
||||
lazyRequire(this, './ui/button/toggle', 'ToggleButton');
|
||||
lazyRequire(this, './ui/sidebar', 'Sidebar');
|
||||
lazyRequire(this, './ui/frame', 'Frame');
|
||||
lazyRequire(this, './ui/toolbar', 'Toolbar');
|
||||
|
||||
module.exports = Object.freeze({
|
||||
get ActionButton() { return ActionButton; },
|
||||
get ToggleButton() { return ToggleButton; },
|
||||
get Sidebar() { return Sidebar; },
|
||||
get Frame() { return Frame; },
|
||||
get Toolbar() { return Toolbar; },
|
||||
});
|
|
@ -1,114 +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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'experimental',
|
||||
'engines': {
|
||||
'Firefox': '> 28'
|
||||
}
|
||||
};
|
||||
|
||||
const { Class } = require('../../core/heritage');
|
||||
const { merge } = require('../../util/object');
|
||||
const { Disposable } = require('../../core/disposable');
|
||||
lazyRequire(this, '../../event/core', "on", "off", "emit", "setListeners");
|
||||
const { EventTarget } = require('../../event/target');
|
||||
lazyRequire(this, '../../view/core', "getNodeView");
|
||||
|
||||
lazyRequireModule(this, './view', "view");
|
||||
const { buttonContract, stateContract } = require('./contract');
|
||||
lazyRequire(this, '../state', "properties", "render", "state", "register",
|
||||
"unregister", "getDerivedStateFor");
|
||||
lazyRequire(this, '../state/events', { "events": "stateEvents" });
|
||||
lazyRequire(this, './view/events', { "events": "viewEvents" });
|
||||
lazyRequireModule(this, '../../event/utils', "events");
|
||||
|
||||
lazyRequire(this, '../../tabs/utils', "getActiveTab");
|
||||
|
||||
lazyRequire(this, '../../self', { "id": "addonID" });
|
||||
lazyRequire(this, '../id', "identify");
|
||||
|
||||
const buttons = new Map();
|
||||
|
||||
const toWidgetId = id =>
|
||||
('action-button--' + addonID.toLowerCase()+ '-' + id).
|
||||
replace(/[^a-z0-9_-]/g, '');
|
||||
|
||||
const ActionButton = Class({
|
||||
extends: EventTarget,
|
||||
implements: [
|
||||
properties(stateContract),
|
||||
state(stateContract),
|
||||
Disposable
|
||||
],
|
||||
setup: function setup(options) {
|
||||
let state = merge({
|
||||
disabled: false
|
||||
}, buttonContract(options));
|
||||
|
||||
let id = toWidgetId(options.id);
|
||||
|
||||
register(this, state);
|
||||
|
||||
// Setup listeners.
|
||||
setListeners(this, options);
|
||||
|
||||
buttons.set(id, this);
|
||||
|
||||
view.create(merge({}, state, { id: id }));
|
||||
},
|
||||
|
||||
dispose: function dispose() {
|
||||
let id = toWidgetId(this.id);
|
||||
buttons.delete(id);
|
||||
|
||||
off(this);
|
||||
|
||||
view.dispose(id);
|
||||
|
||||
unregister(this);
|
||||
},
|
||||
|
||||
get id() {
|
||||
return this.state().id;
|
||||
},
|
||||
|
||||
click: function click() { view.click(toWidgetId(this.id)) }
|
||||
});
|
||||
exports.ActionButton = ActionButton;
|
||||
|
||||
identify.define(ActionButton, ({id}) => toWidgetId(id));
|
||||
|
||||
getNodeView.define(ActionButton, button =>
|
||||
view.nodeFor(toWidgetId(button.id))
|
||||
);
|
||||
|
||||
var actionButtonStateEvents = events.filter(stateEvents,
|
||||
e => e.target instanceof ActionButton);
|
||||
|
||||
var actionButtonViewEvents = events.filter(viewEvents,
|
||||
e => buttons.has(e.target));
|
||||
|
||||
var clickEvents = events.filter(actionButtonViewEvents, e => e.type === 'click');
|
||||
var updateEvents = events.filter(actionButtonViewEvents, e => e.type === 'update');
|
||||
|
||||
on(clickEvents, 'data', ({target: id, window}) => {
|
||||
let button = buttons.get(id);
|
||||
let state = getDerivedStateFor(button, getActiveTab(window));
|
||||
|
||||
emit(button, 'click', state);
|
||||
});
|
||||
|
||||
on(updateEvents, 'data', ({target: id, window}) => {
|
||||
render(buttons.get(id), window);
|
||||
});
|
||||
|
||||
on(actionButtonStateEvents, 'data', ({target, window, state}) => {
|
||||
let id = toWidgetId(target.id);
|
||||
view.setIcon(id, window, state.icon);
|
||||
view.setLabel(id, window, state.label);
|
||||
view.setDisabled(id, window, state.disabled);
|
||||
view.setBadge(id, window, state.badge, state.badgeColor);
|
||||
});
|
|
@ -1,73 +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 { contract } = require('../../util/contract');
|
||||
lazyRequire(this, '../../url', "isLocalURL");
|
||||
lazyRequire(this, '../../lang/type', "isNil", "isObject", "isString");
|
||||
const { required, either, string, boolean, object, number } = require('../../deprecated/api-utils');
|
||||
const { merge } = require('../../util/object');
|
||||
const { freeze } = Object;
|
||||
|
||||
const isIconSet = (icons) =>
|
||||
Object.keys(icons).
|
||||
every(size => String(size >>> 0) === size && isLocalURL(icons[size]));
|
||||
|
||||
var iconSet = {
|
||||
is: either(object, string),
|
||||
map: v => isObject(v) ? freeze(merge({}, v)) : v,
|
||||
ok: v => (isString(v) && isLocalURL(v)) || (isObject(v) && isIconSet(v)),
|
||||
msg: 'The option "icon" must be a local URL or an object with ' +
|
||||
'numeric keys / local URL values pair.'
|
||||
}
|
||||
|
||||
var id = {
|
||||
is: string,
|
||||
ok: v => /^[a-z-_][a-z0-9-_]*$/i.test(v),
|
||||
msg: 'The option "id" must be a valid alphanumeric id (hyphens and ' +
|
||||
'underscores are allowed).'
|
||||
};
|
||||
|
||||
var label = {
|
||||
is: string,
|
||||
ok: v => isNil(v) || v.trim().length > 0,
|
||||
msg: 'The option "label" must be a non empty string'
|
||||
}
|
||||
|
||||
var badge = {
|
||||
is: either(string, number),
|
||||
msg: 'The option "badge" must be a string or a number'
|
||||
}
|
||||
|
||||
var badgeColor = {
|
||||
is: string,
|
||||
msg: 'The option "badgeColor" must be a string'
|
||||
}
|
||||
|
||||
var stateContract = contract({
|
||||
label: label,
|
||||
icon: iconSet,
|
||||
disabled: boolean,
|
||||
badge: badge,
|
||||
badgeColor: badgeColor
|
||||
});
|
||||
|
||||
exports.stateContract = stateContract;
|
||||
|
||||
var buttonContract = contract(merge({}, stateContract.rules, {
|
||||
id: required(id),
|
||||
label: required(label),
|
||||
icon: required(iconSet)
|
||||
}));
|
||||
|
||||
exports.buttonContract = buttonContract;
|
||||
|
||||
exports.toggleStateContract = contract(merge({
|
||||
checked: boolean
|
||||
}, stateContract.rules));
|
||||
|
||||
exports.toggleButtonContract = contract(merge({
|
||||
checked: boolean
|
||||
}, buttonContract.rules));
|
||||
|
|
@ -1,127 +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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'experimental',
|
||||
'engines': {
|
||||
'Firefox': '> 28'
|
||||
}
|
||||
};
|
||||
|
||||
const { Class } = require('../../core/heritage');
|
||||
lazyRequire(this, '../../util/object', "merge");
|
||||
const { Disposable } = require('../../core/disposable');
|
||||
lazyRequire(this, '../../event/core', "on", "off", "emit", "setListeners");
|
||||
const { EventTarget } = require('../../event/target');
|
||||
lazyRequire(this, '../../view/core', "getNodeView");
|
||||
|
||||
lazyRequireModule(this, "./view", "view");
|
||||
const { toggleButtonContract, toggleStateContract } = require('./contract');
|
||||
lazyRequire(this, '../state', "properties", "render", "state", "register", "unregister",
|
||||
"setStateFor", "getStateFor", "getDerivedStateFor");
|
||||
lazyRequire(this, '../state/events', { "events": "stateEvents" });
|
||||
lazyRequire(this, './view/events', { "events": "viewEvents" });
|
||||
lazyRequireModule(this, '../../event/utils', "events");
|
||||
|
||||
lazyRequire(this, '../../tabs/utils', "getActiveTab");
|
||||
|
||||
lazyRequire(this, '../../self', { "id": "addonID" });
|
||||
lazyRequire(this, '../id', "identify");
|
||||
|
||||
const buttons = new Map();
|
||||
|
||||
const toWidgetId = id =>
|
||||
('toggle-button--' + addonID.toLowerCase()+ '-' + id).
|
||||
replace(/[^a-z0-9_-]/g, '');
|
||||
|
||||
const ToggleButton = Class({
|
||||
extends: EventTarget,
|
||||
implements: [
|
||||
properties(toggleStateContract),
|
||||
state(toggleStateContract),
|
||||
Disposable
|
||||
],
|
||||
setup: function setup(options) {
|
||||
let state = merge({
|
||||
disabled: false,
|
||||
checked: false
|
||||
}, toggleButtonContract(options));
|
||||
|
||||
let id = toWidgetId(options.id);
|
||||
|
||||
register(this, state);
|
||||
|
||||
// Setup listeners.
|
||||
setListeners(this, options);
|
||||
|
||||
buttons.set(id, this);
|
||||
|
||||
view.create(merge({ type: 'checkbox' }, state, { id: id }));
|
||||
},
|
||||
|
||||
dispose: function dispose() {
|
||||
let id = toWidgetId(this.id);
|
||||
buttons.delete(id);
|
||||
|
||||
off(this);
|
||||
|
||||
view.dispose(id);
|
||||
|
||||
unregister(this);
|
||||
},
|
||||
|
||||
get id() {
|
||||
return this.state().id;
|
||||
},
|
||||
|
||||
click: function click() {
|
||||
return view.click(toWidgetId(this.id));
|
||||
}
|
||||
});
|
||||
exports.ToggleButton = ToggleButton;
|
||||
|
||||
identify.define(ToggleButton, ({id}) => toWidgetId(id));
|
||||
|
||||
getNodeView.define(ToggleButton, button =>
|
||||
view.nodeFor(toWidgetId(button.id))
|
||||
);
|
||||
|
||||
var toggleButtonStateEvents = events.filter(stateEvents,
|
||||
e => e.target instanceof ToggleButton);
|
||||
|
||||
var toggleButtonViewEvents = events.filter(viewEvents,
|
||||
e => buttons.has(e.target));
|
||||
|
||||
var clickEvents = events.filter(toggleButtonViewEvents, e => e.type === 'click');
|
||||
var updateEvents = events.filter(toggleButtonViewEvents, e => e.type === 'update');
|
||||
|
||||
on(toggleButtonStateEvents, 'data', ({target, window, state}) => {
|
||||
let id = toWidgetId(target.id);
|
||||
|
||||
view.setIcon(id, window, state.icon);
|
||||
view.setLabel(id, window, state.label);
|
||||
view.setDisabled(id, window, state.disabled);
|
||||
view.setChecked(id, window, state.checked);
|
||||
view.setBadge(id, window, state.badge, state.badgeColor);
|
||||
});
|
||||
|
||||
on(clickEvents, 'data', ({target: id, window, checked }) => {
|
||||
let button = buttons.get(id);
|
||||
let windowState = getStateFor(button, window);
|
||||
|
||||
let newWindowState = merge({}, windowState, { checked: checked });
|
||||
|
||||
setStateFor(button, window, newWindowState);
|
||||
|
||||
let state = getDerivedStateFor(button, getActiveTab(window));
|
||||
|
||||
emit(button, 'click', state);
|
||||
|
||||
emit(button, 'change', state);
|
||||
});
|
||||
|
||||
on(updateEvents, 'data', ({target: id, window}) => {
|
||||
render(buttons.get(id), window);
|
||||
});
|
|
@ -1,251 +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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'experimental',
|
||||
'engines': {
|
||||
'Firefox': '> 28'
|
||||
}
|
||||
};
|
||||
|
||||
const { Cu } = require('chrome');
|
||||
lazyRequire(this, '../../event/core', "on", "off", "emit");
|
||||
|
||||
lazyRequire(this, 'sdk/self', "data");
|
||||
|
||||
lazyRequire(this, '../../lang/type', "isObject", "isNil");
|
||||
|
||||
lazyRequire(this, '../../window/utils', "getMostRecentBrowserWindow");
|
||||
lazyRequire(this, '../../private-browsing/utils', "ignoreWindow");
|
||||
const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
|
||||
const { AREA_PANEL, AREA_NAVBAR } = CustomizableUI;
|
||||
|
||||
lazyRequire(this, './view/events', { "events": "viewEvents" });
|
||||
|
||||
const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
|
||||
|
||||
const views = new Map();
|
||||
const customizedWindows = new WeakMap();
|
||||
|
||||
const buttonListener = {
|
||||
onCustomizeStart: window => {
|
||||
for (let [id, view] of views) {
|
||||
setIcon(id, window, view.icon);
|
||||
setLabel(id, window, view.label);
|
||||
}
|
||||
|
||||
customizedWindows.set(window, true);
|
||||
},
|
||||
onCustomizeEnd: window => {
|
||||
customizedWindows.delete(window);
|
||||
|
||||
for (let [id, ] of views) {
|
||||
let placement = CustomizableUI.getPlacementOfWidget(id);
|
||||
|
||||
if (placement)
|
||||
emit(viewEvents, 'data', { type: 'update', target: id, window: window });
|
||||
}
|
||||
},
|
||||
onWidgetAfterDOMChange: (node, nextNode, container) => {
|
||||
let { id } = node;
|
||||
let view = views.get(id);
|
||||
let window = node.ownerGlobal;
|
||||
|
||||
if (view) {
|
||||
emit(viewEvents, 'data', { type: 'update', target: id, window: window });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CustomizableUI.addListener(buttonListener);
|
||||
|
||||
require('../../system/unload').when( _ =>
|
||||
CustomizableUI.removeListener(buttonListener)
|
||||
);
|
||||
|
||||
function getNode(id, window) {
|
||||
let view = views.get(id);
|
||||
return view && view.nodes.get(window);
|
||||
};
|
||||
|
||||
function isInToolbar(id) {
|
||||
let placement = CustomizableUI.getPlacementOfWidget(id);
|
||||
|
||||
return placement && CustomizableUI.getAreaType(placement.area) === 'toolbar';
|
||||
}
|
||||
|
||||
|
||||
function getImage(icon, isInToolbar, pixelRatio) {
|
||||
let targetSize = (isInToolbar ? 18 : 32) * pixelRatio;
|
||||
let bestSize = 0;
|
||||
let image = icon;
|
||||
|
||||
if (isObject(icon)) {
|
||||
for (let size of Object.keys(icon)) {
|
||||
size = +size;
|
||||
let offset = targetSize - size;
|
||||
|
||||
if (offset === 0) {
|
||||
bestSize = size;
|
||||
break;
|
||||
}
|
||||
|
||||
let delta = Math.abs(offset) - Math.abs(targetSize - bestSize);
|
||||
|
||||
if (delta < 0)
|
||||
bestSize = size;
|
||||
}
|
||||
|
||||
image = icon[bestSize];
|
||||
}
|
||||
|
||||
if (image.indexOf('./') === 0)
|
||||
return data.url(image.substr(2));
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
function nodeFor(id, window=getMostRecentBrowserWindow()) {
|
||||
return customizedWindows.has(window) ? null : getNode(id, window);
|
||||
};
|
||||
exports.nodeFor = nodeFor;
|
||||
|
||||
function create(options) {
|
||||
let { id, label, icon, type, badge } = options;
|
||||
|
||||
if (views.has(id))
|
||||
throw new Error('The ID "' + id + '" seems already used.');
|
||||
|
||||
CustomizableUI.createWidget({
|
||||
id: id,
|
||||
type: 'custom',
|
||||
removable: true,
|
||||
defaultArea: AREA_NAVBAR,
|
||||
allowedAreas: [ AREA_PANEL, AREA_NAVBAR ],
|
||||
|
||||
onBuild: function(document) {
|
||||
let window = document.defaultView;
|
||||
|
||||
let node = document.createElementNS(XUL_NS, 'toolbarbutton');
|
||||
|
||||
let image = getImage(icon, true, window.devicePixelRatio);
|
||||
|
||||
node.setAttribute('id', this.id);
|
||||
node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional badged-button');
|
||||
node.setAttribute('type', type);
|
||||
node.setAttribute('label', label);
|
||||
node.setAttribute('tooltiptext', label);
|
||||
node.setAttribute('image', image);
|
||||
node.setAttribute('constrain-size', 'true');
|
||||
|
||||
if (!views.get(id)) {
|
||||
views.set(id, {
|
||||
nodes: new WeakMap(),
|
||||
});
|
||||
}
|
||||
|
||||
let view = views.get(id);
|
||||
Object.assign(view, {
|
||||
area: this.currentArea,
|
||||
icon: icon,
|
||||
label: label
|
||||
});
|
||||
|
||||
if (ignoreWindow(window))
|
||||
node.style.display = 'none';
|
||||
else
|
||||
view.nodes.set(window, node);
|
||||
|
||||
node.addEventListener('command', function(event) {
|
||||
if (views.has(id)) {
|
||||
emit(viewEvents, 'data', {
|
||||
type: 'click',
|
||||
target: id,
|
||||
window: event.view,
|
||||
checked: node.checked
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
});
|
||||
};
|
||||
exports.create = create;
|
||||
|
||||
function dispose(id) {
|
||||
if (!views.has(id)) return;
|
||||
|
||||
views.delete(id);
|
||||
CustomizableUI.destroyWidget(id);
|
||||
}
|
||||
exports.dispose = dispose;
|
||||
|
||||
function setIcon(id, window, icon) {
|
||||
let node = getNode(id, window);
|
||||
|
||||
if (node) {
|
||||
icon = customizedWindows.has(window) ? views.get(id).icon : icon;
|
||||
let image = getImage(icon, isInToolbar(id), window.devicePixelRatio);
|
||||
|
||||
node.setAttribute('image', image);
|
||||
}
|
||||
}
|
||||
exports.setIcon = setIcon;
|
||||
|
||||
function setLabel(id, window, label) {
|
||||
let node = nodeFor(id, window);
|
||||
|
||||
if (node) {
|
||||
node.setAttribute('label', label);
|
||||
node.setAttribute('tooltiptext', label);
|
||||
}
|
||||
}
|
||||
exports.setLabel = setLabel;
|
||||
|
||||
function setDisabled(id, window, disabled) {
|
||||
let node = nodeFor(id, window);
|
||||
|
||||
if (node)
|
||||
node.disabled = disabled;
|
||||
}
|
||||
exports.setDisabled = setDisabled;
|
||||
|
||||
function setChecked(id, window, checked) {
|
||||
let node = nodeFor(id, window);
|
||||
|
||||
if (node)
|
||||
node.checked = checked;
|
||||
}
|
||||
exports.setChecked = setChecked;
|
||||
|
||||
function setBadge(id, window, badge, color) {
|
||||
let node = nodeFor(id, window);
|
||||
|
||||
if (node) {
|
||||
// `Array.from` is needed to handle unicode symbol properly:
|
||||
// '𝐀𝐁'.length is 4 where Array.from('𝐀𝐁').length is 2
|
||||
let text = badge == null
|
||||
? ''
|
||||
: Array.from(String(badge)).slice(0, 4).join('');
|
||||
|
||||
node.setAttribute('badge', text);
|
||||
|
||||
let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
|
||||
'class', 'toolbarbutton-badge');
|
||||
|
||||
if (badgeNode)
|
||||
badgeNode.style.backgroundColor = color == null ? '' : color;
|
||||
}
|
||||
}
|
||||
exports.setBadge = setBadge;
|
||||
|
||||
function click(id) {
|
||||
let node = nodeFor(id);
|
||||
|
||||
if (node)
|
||||
node.click();
|
||||
}
|
||||
exports.click = click;
|
|
@ -1,18 +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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'experimental',
|
||||
'engines': {
|
||||
'Firefox': '*',
|
||||
'SeaMonkey': '*',
|
||||
'Thunderbird': '*'
|
||||
}
|
||||
};
|
||||
|
||||
var channel = {};
|
||||
|
||||
exports.events = channel;
|
|
@ -1,182 +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";
|
||||
|
||||
// Internal properties not exposed to the public.
|
||||
const cache = Symbol("component/cache");
|
||||
const writer = Symbol("component/writer");
|
||||
const isFirstWrite = Symbol("component/writer/first-write?");
|
||||
const currentState = Symbol("component/state/current");
|
||||
const pendingState = Symbol("component/state/pending");
|
||||
const isWriting = Symbol("component/writing?");
|
||||
|
||||
const isntNull = x => x !== null;
|
||||
|
||||
const Component = function(options, children) {
|
||||
this[currentState] = null;
|
||||
this[pendingState] = null;
|
||||
this[writer] = null;
|
||||
this[cache] = null;
|
||||
this[isFirstWrite] = true;
|
||||
|
||||
this[Component.construct](options, children);
|
||||
}
|
||||
Component.Component = Component;
|
||||
// Constructs component.
|
||||
Component.construct = Symbol("component/construct");
|
||||
// Called with `options` and `children` and must return
|
||||
// initial state back.
|
||||
Component.initial = Symbol("component/initial");
|
||||
|
||||
// Function patches current `state` with a given update.
|
||||
Component.patch = Symbol("component/patch");
|
||||
// Function that replaces current `state` with a passed state.
|
||||
Component.reset = Symbol("component/reset");
|
||||
|
||||
// Function that must return render tree from passed state.
|
||||
Component.render = Symbol("component/render");
|
||||
|
||||
// Path of the component with in the mount point.
|
||||
Component.path = Symbol("component/path");
|
||||
|
||||
Component.isMounted = component => !!component[writer];
|
||||
Component.isWriting = component => !!component[isWriting];
|
||||
|
||||
// Internal method that mounts component to a writer.
|
||||
// Mounts component to a writer.
|
||||
Component.mount = (component, write) => {
|
||||
if (Component.isMounted(component)) {
|
||||
throw Error("Can not mount already mounted component");
|
||||
}
|
||||
|
||||
component[writer] = write;
|
||||
Component.write(component);
|
||||
|
||||
if (component[Component.mounted]) {
|
||||
component[Component.mounted]();
|
||||
}
|
||||
}
|
||||
|
||||
// Unmounts component from a writer.
|
||||
Component.unmount = (component) => {
|
||||
if (Component.isMounted(component)) {
|
||||
component[writer] = null;
|
||||
if (component[Component.unmounted]) {
|
||||
component[Component.unmounted]();
|
||||
}
|
||||
} else {
|
||||
console.warn("Unmounting component that is not mounted is redundant");
|
||||
}
|
||||
};
|
||||
// Method invoked once after inital write occurs.
|
||||
Component.mounted = Symbol("component/mounted");
|
||||
// Internal method that unmounts component from the writer.
|
||||
Component.unmounted = Symbol("component/unmounted");
|
||||
// Function that must return true if component is changed
|
||||
Component.isUpdated = Symbol("component/updated?");
|
||||
Component.update = Symbol("component/update");
|
||||
Component.updated = Symbol("component/updated");
|
||||
|
||||
const writeChild = base => (child, index) => Component.write(child, base, index)
|
||||
Component.write = (component, base, index) => {
|
||||
if (component === null) {
|
||||
return component;
|
||||
}
|
||||
|
||||
if (!(component instanceof Component)) {
|
||||
const path = base ? `${base}${component.key || index}/` : `/`;
|
||||
return Object.assign({}, component, {
|
||||
[Component.path]: path,
|
||||
children: component.children && component.children.
|
||||
map(writeChild(path)).
|
||||
filter(isntNull)
|
||||
});
|
||||
}
|
||||
|
||||
component[isWriting] = true;
|
||||
|
||||
try {
|
||||
|
||||
const current = component[currentState];
|
||||
const pending = component[pendingState] || current;
|
||||
const isUpdated = component[Component.isUpdated];
|
||||
const isInitial = component[isFirstWrite];
|
||||
|
||||
if (isUpdated(current, pending) || isInitial) {
|
||||
if (!isInitial && component[Component.update]) {
|
||||
component[Component.update](pending, current)
|
||||
}
|
||||
|
||||
// Note: [Component.update] could have caused more updates so can't use
|
||||
// `pending` as `component[pendingState]` may have changed.
|
||||
component[currentState] = component[pendingState] || current;
|
||||
component[pendingState] = null;
|
||||
|
||||
const tree = component[Component.render](component[currentState]);
|
||||
component[cache] = Component.write(tree, base, index);
|
||||
if (component[writer]) {
|
||||
component[writer].call(null, component[cache]);
|
||||
}
|
||||
|
||||
if (!isInitial && component[Component.updated]) {
|
||||
component[Component.updated](current, pending);
|
||||
}
|
||||
}
|
||||
|
||||
component[isFirstWrite] = false;
|
||||
|
||||
return component[cache];
|
||||
} finally {
|
||||
component[isWriting] = false;
|
||||
}
|
||||
};
|
||||
|
||||
Component.prototype = Object.freeze({
|
||||
constructor: Component,
|
||||
|
||||
[Component.mounted]: null,
|
||||
[Component.unmounted]: null,
|
||||
[Component.update]: null,
|
||||
[Component.updated]: null,
|
||||
|
||||
get state() {
|
||||
return this[pendingState] || this[currentState];
|
||||
},
|
||||
|
||||
|
||||
[Component.construct](settings, items) {
|
||||
const initial = this[Component.initial];
|
||||
const base = initial(settings, items);
|
||||
const options = Object.assign(Object.create(null), base.options, settings);
|
||||
const children = base.children || items || null;
|
||||
const state = Object.assign(Object.create(null), base, {options, children});
|
||||
this[currentState] = state;
|
||||
|
||||
if (this.setup) {
|
||||
this.setup(state);
|
||||
}
|
||||
},
|
||||
[Component.initial](options, children) {
|
||||
return Object.create(null);
|
||||
},
|
||||
[Component.patch](update) {
|
||||
this[Component.reset](Object.assign({}, this.state, update));
|
||||
},
|
||||
[Component.reset](state) {
|
||||
this[pendingState] = state;
|
||||
if (Component.isMounted(this) && !Component.isWriting(this)) {
|
||||
Component.write(this);
|
||||
}
|
||||
},
|
||||
|
||||
[Component.isUpdated](before, after) {
|
||||
return before != after
|
||||
},
|
||||
|
||||
[Component.render](state) {
|
||||
throw Error("Component must implement [Component.render] member");
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Component;
|
|
@ -1,15 +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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental",
|
||||
"engines": {
|
||||
"Firefox": "> 28"
|
||||
}
|
||||
};
|
||||
|
||||
const { Frame } = require("./frame/model");
|
||||
|
||||
exports.Frame = Frame;
|
|
@ -1,154 +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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental",
|
||||
"engines": {
|
||||
"Firefox": "> 28"
|
||||
}
|
||||
};
|
||||
|
||||
const { Class } = require("../../core/heritage");
|
||||
const { EventTarget } = require("../../event/target");
|
||||
lazyRequire(this, "../../event/core", "emit", "off", "setListeners");
|
||||
const { Reactor, foldp, send, merges } = require("../../event/utils");
|
||||
const { Disposable } = require("../../core/disposable");
|
||||
const { OutputPort } = require("../../output/system");
|
||||
lazyRequire(this, "../id", "identify");
|
||||
const { pairs, object, each } = require("../../util/sequence");
|
||||
lazyRequire(this, "diffpatcher/index", "patch", "diff");
|
||||
lazyRequire(this, "../../url", "isLocalURL");
|
||||
const { compose } = require("../../lang/functional");
|
||||
const { contract } = require("../../util/contract");
|
||||
const { id: addonID, data: { url: resolve }} = require("../../self");
|
||||
const { Frames } = require("../../input/frame");
|
||||
require("./view");
|
||||
|
||||
|
||||
const output = new OutputPort({ id: "frame-change" });
|
||||
const mailbox = new OutputPort({ id: "frame-mailbox" });
|
||||
const input = Frames;
|
||||
|
||||
|
||||
const makeID = url =>
|
||||
("frame-" + addonID + "-" + url).
|
||||
split("/").join("-").
|
||||
split(".").join("-").
|
||||
replace(/[^A-Za-z0-9_\-]/g, "");
|
||||
|
||||
const validate = contract({
|
||||
name: {
|
||||
is: ["string", "undefined"],
|
||||
ok: x => /^[a-z][a-z0-9-_]+$/i.test(x),
|
||||
msg: "The `option.name` must be a valid alphanumeric string (hyphens and " +
|
||||
"underscores are allowed) starting with letter."
|
||||
},
|
||||
url: {
|
||||
map: x => x.toString(),
|
||||
is: ["string"],
|
||||
ok: x => isLocalURL(x),
|
||||
msg: "The `options.url` must be a valid local URI."
|
||||
}
|
||||
});
|
||||
|
||||
const Source = function({id, ownerID}) {
|
||||
this.id = id;
|
||||
this.ownerID = ownerID;
|
||||
};
|
||||
Source.postMessage = ({id, ownerID}, data, origin) => {
|
||||
send(mailbox, object([id, {
|
||||
inbox: {
|
||||
target: {id: id, ownerID: ownerID},
|
||||
timeStamp: Date.now(),
|
||||
data: data,
|
||||
origin: origin
|
||||
}
|
||||
}]));
|
||||
};
|
||||
Source.prototype.postMessage = function(data, origin) {
|
||||
Source.postMessage(this, data, origin);
|
||||
};
|
||||
|
||||
const Message = function({type, data, source, origin, timeStamp}) {
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
this.origin = origin;
|
||||
this.timeStamp = timeStamp;
|
||||
this.source = new Source(source);
|
||||
};
|
||||
|
||||
|
||||
const frames = new Map();
|
||||
const sources = new Map();
|
||||
|
||||
const Frame = Class({
|
||||
extends: EventTarget,
|
||||
implements: [Disposable, Source],
|
||||
initialize: function(params={}) {
|
||||
const options = validate(params);
|
||||
const id = makeID(options.name || options.url);
|
||||
|
||||
if (frames.has(id))
|
||||
throw Error("Frame with this id already exists: " + id);
|
||||
|
||||
const initial = { id: id, url: resolve(options.url) };
|
||||
this.id = id;
|
||||
|
||||
setListeners(this, params);
|
||||
|
||||
frames.set(this.id, this);
|
||||
|
||||
send(output, object([id, initial]));
|
||||
},
|
||||
get url() {
|
||||
const state = reactor.value[this.id];
|
||||
return state && state.url;
|
||||
},
|
||||
destroy: function() {
|
||||
send(output, object([this.id, null]));
|
||||
frames.delete(this.id);
|
||||
off(this);
|
||||
},
|
||||
// `JSON.stringify` serializes objects based of the return
|
||||
// value of this method. For convinienc we provide this method
|
||||
// to serialize actual state data.
|
||||
toJSON: function() {
|
||||
return { id: this.id, url: this.url };
|
||||
}
|
||||
});
|
||||
identify.define(Frame, frame => frame.id);
|
||||
|
||||
exports.Frame = Frame;
|
||||
|
||||
const reactor = new Reactor({
|
||||
onStep: (present, past) => {
|
||||
const delta = diff(past, present);
|
||||
|
||||
each(([id, update]) => {
|
||||
const frame = frames.get(id);
|
||||
if (update) {
|
||||
if (!past[id])
|
||||
emit(frame, "register");
|
||||
|
||||
if (update.outbox)
|
||||
emit(frame, "message", new Message(present[id].outbox));
|
||||
|
||||
each(([ownerID, state]) => {
|
||||
const readyState = state ? state.readyState : "detach";
|
||||
const type = readyState === "loading" ? "attach" :
|
||||
readyState === "interactive" ? "ready" :
|
||||
readyState === "complete" ? "load" :
|
||||
readyState;
|
||||
|
||||
// TODO: Cache `Source` instances somewhere to preserve
|
||||
// identity.
|
||||
emit(frame, type, {type: type,
|
||||
source: new Source({id: id, ownerID: ownerID})});
|
||||
}, pairs(update.owners));
|
||||
}
|
||||
}, pairs(delta));
|
||||
}
|
||||
});
|
||||
reactor.run(input);
|
|
@ -1,18 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
// HACK: This is not an ideal way to deliver chrome messages
|
||||
// to an inner frame content but seems only way that would
|
||||
// make `event.source` this (outer frame) window.
|
||||
window.onmessage = function(event) {
|
||||
var frame = document.querySelector("iframe");
|
||||
var content = frame.contentWindow;
|
||||
// If message is posted from chrome it has no `event.source`.
|
||||
if (event.source === null)
|
||||
content.postMessage(event.data, "*");
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body style="overflow: hidden"></body>
|
||||
</html>
|
|
@ -1,145 +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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental",
|
||||
"engines": {
|
||||
"Firefox": "> 28"
|
||||
}
|
||||
};
|
||||
|
||||
const { Cu, Ci } = require("chrome");
|
||||
const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
|
||||
const { send, Reactor } = require("../../event/utils");
|
||||
const { OutputPort } = require("../../output/system");
|
||||
lazyRequire(this, "../../util/sequence", "pairs", "keys", "object", "each");
|
||||
const { curry, compose } = require("../../lang/functional");
|
||||
lazyRequire(this, "../../window/utils", "getFrameElement", "getOuterId", "getByOuterId", "getOwnerBrowserWindow");
|
||||
lazyRequire(this, "diffpatcher/index", "patch", "diff");
|
||||
lazyRequire(this, "../../base64", "encode");
|
||||
const { Frames } = require("../../input/frame");
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const OUTER_FRAME_URI = module.uri.replace(/\.js$/, ".html");
|
||||
|
||||
const mailbox = new OutputPort({ id: "frame-mailbox" });
|
||||
|
||||
const frameID = frame => frame.id.replace("outer-", "");
|
||||
const windowID = compose(getOuterId, getOwnerBrowserWindow);
|
||||
|
||||
const getOuterFrame = (windowID, frameID) =>
|
||||
getByOuterId(windowID).document.getElementById("outer-" + frameID);
|
||||
|
||||
const listener = ({target, source, data, origin, timeStamp}) => {
|
||||
// And sent received message to outbox so that frame API model
|
||||
// will deal with it.
|
||||
if (source && source !== target) {
|
||||
const frame = getFrameElement(target);
|
||||
const id = frameID(frame);
|
||||
send(mailbox, object([id, {
|
||||
outbox: {type: "message",
|
||||
source: {id: id, ownerID: windowID(frame)},
|
||||
data: data,
|
||||
origin: origin,
|
||||
timeStamp: timeStamp}}]));
|
||||
}
|
||||
};
|
||||
|
||||
// Utility function used to create frame with a given `state` and
|
||||
// inject it into given `window`.
|
||||
const registerFrame = ({id, url}) => {
|
||||
CustomizableUI.createWidget({
|
||||
id: id,
|
||||
type: "custom",
|
||||
removable: true,
|
||||
onBuild: document => {
|
||||
let view = document.createElementNS(XUL_NS, "toolbaritem");
|
||||
view.setAttribute("id", id);
|
||||
view.setAttribute("flex", 2);
|
||||
|
||||
let outerFrame = document.createElementNS(XUL_NS, "iframe");
|
||||
outerFrame.setAttribute("src", OUTER_FRAME_URI);
|
||||
outerFrame.setAttribute("id", "outer-" + id);
|
||||
outerFrame.setAttribute("data-is-sdk-outer-frame", true);
|
||||
outerFrame.setAttribute("type", "content");
|
||||
outerFrame.setAttribute("transparent", true);
|
||||
outerFrame.setAttribute("flex", 2);
|
||||
outerFrame.setAttribute("style", "overflow: hidden;");
|
||||
outerFrame.setAttribute("scrolling", "no");
|
||||
outerFrame.setAttribute("disablehistory", true);
|
||||
outerFrame.setAttribute("seamless", "seamless");
|
||||
outerFrame.addEventListener("load", function() {
|
||||
let doc = outerFrame.contentDocument;
|
||||
|
||||
let innerFrame = doc.createElementNS(HTML_NS, "iframe");
|
||||
innerFrame.setAttribute("id", id);
|
||||
innerFrame.setAttribute("src", url);
|
||||
innerFrame.setAttribute("seamless", "seamless");
|
||||
innerFrame.setAttribute("sandbox", "allow-scripts");
|
||||
innerFrame.setAttribute("scrolling", "no");
|
||||
innerFrame.setAttribute("data-is-sdk-inner-frame", true);
|
||||
innerFrame.setAttribute("style", [ "border:none",
|
||||
"position:absolute", "width:100%", "top: 0",
|
||||
"left: 0", "overflow: hidden"].join(";"));
|
||||
|
||||
doc.body.appendChild(innerFrame);
|
||||
}, {capture: true, once: true});
|
||||
|
||||
view.appendChild(outerFrame);
|
||||
|
||||
return view;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterFrame = CustomizableUI.destroyWidget;
|
||||
|
||||
const deliverMessage = curry((frameID, data, windowID) => {
|
||||
const frame = getOuterFrame(windowID, frameID);
|
||||
const content = frame && frame.contentWindow;
|
||||
|
||||
if (content)
|
||||
content.postMessage(data, content.location.origin);
|
||||
});
|
||||
|
||||
const updateFrame = (id, {inbox, owners}, present) => {
|
||||
if (inbox) {
|
||||
const { data, target:{ownerID}, source } = present[id].inbox;
|
||||
if (ownerID)
|
||||
deliverMessage(id, data, ownerID);
|
||||
else
|
||||
each(deliverMessage(id, data), keys(present[id].owners));
|
||||
}
|
||||
|
||||
each(setupView(id), pairs(owners));
|
||||
};
|
||||
|
||||
const setupView = curry((frameID, [windowID, state]) => {
|
||||
if (state && state.readyState === "loading") {
|
||||
const frame = getOuterFrame(windowID, frameID);
|
||||
// Setup a message listener on contentWindow.
|
||||
frame.contentWindow.addEventListener("message", listener);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const reactor = new Reactor({
|
||||
onStep: (present, past) => {
|
||||
const delta = diff(past, present);
|
||||
|
||||
// Apply frame changes
|
||||
each(([id, update]) => {
|
||||
if (update === null)
|
||||
unregisterFrame(id);
|
||||
else if (past[id])
|
||||
updateFrame(id, update, present);
|
||||
else
|
||||
registerFrame(update);
|
||||
}, pairs(delta));
|
||||
},
|
||||
onEnd: state => each(unregisterFrame, keys(state))
|
||||
});
|
||||
reactor.run(Frames);
|
|
@ -1,27 +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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'experimental'
|
||||
};
|
||||
|
||||
const method = require('../../method/core');
|
||||
lazyRequire(this, '../util/uuid', "uuid");
|
||||
|
||||
// NOTE: use lang/functional memoize when it is updated to use WeakMap
|
||||
function memoize(f) {
|
||||
const memo = new WeakMap();
|
||||
|
||||
return function memoizer(o) {
|
||||
let key = o;
|
||||
if (!memo.has(key))
|
||||
memo.set(key, f.apply(this, arguments));
|
||||
return memo.get(key);
|
||||
};
|
||||
}
|
||||
|
||||
var identify = method('identify');
|
||||
identify.define(Object, memoize(function() { return uuid(); }));
|
||||
exports.identify = identify;
|
|
@ -1,304 +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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'experimental',
|
||||
'engines': {
|
||||
'Firefox': '*'
|
||||
}
|
||||
};
|
||||
|
||||
const { Class } = require('../core/heritage');
|
||||
const { merge } = require('../util/object');
|
||||
const { Disposable } = require('../core/disposable');
|
||||
lazyRequire(this, '../event/core', "off", "emit", "setListeners");
|
||||
const { EventTarget } = require('../event/target');
|
||||
lazyRequire(this, '../url', "URL");
|
||||
lazyRequire(this, '../self', { "id": "addonID" }, "data");
|
||||
lazyRequire(this, '../deprecated/window-utils', 'WindowTracker');
|
||||
lazyRequire(this, './sidebar/utils', "isShowing");
|
||||
lazyRequire(this, '../window/utils', "isBrowser", "getMostRecentBrowserWindow", "windows", "isWindowPrivate");
|
||||
const { ns } = require('../core/namespace');
|
||||
lazyRequire(this, '../util/array', { "remove": "removeFromArray" });
|
||||
lazyRequire(this, './sidebar/actions', "show", "hide", "toggle");
|
||||
lazyRequire(this, '../deprecated/sync-worker', "Worker");
|
||||
const { contract: sidebarContract } = require('./sidebar/contract');
|
||||
lazyRequire(this, './sidebar/view', "create", "dispose", "updateTitle", "updateURL", "isSidebarShowing", "showSidebar", "hideSidebar");
|
||||
lazyRequire(this, '../core/promise', "defer");
|
||||
lazyRequire(this, './sidebar/namespace', "models", "views", "viewsFor", "modelFor");
|
||||
lazyRequire(this, '../url', "isLocalURL");
|
||||
const { ensure } = require('../system/unload');
|
||||
lazyRequire(this, './id', "identify");
|
||||
lazyRequire(this, '../util/uuid', "uuid");
|
||||
lazyRequire(this, '../view/core', "viewFor");
|
||||
|
||||
const resolveURL = (url) => url ? data.url(url) : url;
|
||||
|
||||
const sidebarNS = ns();
|
||||
|
||||
const WEB_PANEL_BROWSER_ID = 'web-panels-browser';
|
||||
|
||||
const Sidebar = Class({
|
||||
implements: [ Disposable ],
|
||||
extends: EventTarget,
|
||||
setup: function(options) {
|
||||
// inital validation for the model information
|
||||
let model = sidebarContract(options);
|
||||
|
||||
// save the model information
|
||||
models.set(this, model);
|
||||
|
||||
// generate an id if one was not provided
|
||||
model.id = model.id || addonID + '-' + uuid();
|
||||
|
||||
// further validation for the title and url
|
||||
validateTitleAndURLCombo({}, this.title, this.url);
|
||||
|
||||
const self = this;
|
||||
const internals = sidebarNS(self);
|
||||
const windowNS = internals.windowNS = ns();
|
||||
|
||||
// see bug https://bugzilla.mozilla.org/show_bug.cgi?id=886148
|
||||
ensure(this, 'destroy');
|
||||
|
||||
setListeners(this, options);
|
||||
|
||||
let bars = [];
|
||||
internals.tracker = WindowTracker({
|
||||
onTrack: function(window) {
|
||||
if (!isBrowser(window))
|
||||
return;
|
||||
|
||||
let sidebar = window.document.getElementById('sidebar');
|
||||
let sidebarBox = window.document.getElementById('sidebar-box');
|
||||
|
||||
let bar = create(window, {
|
||||
id: self.id,
|
||||
title: self.title,
|
||||
sidebarurl: self.url
|
||||
});
|
||||
bars.push(bar);
|
||||
windowNS(window).bar = bar;
|
||||
|
||||
bar.addEventListener('command', function() {
|
||||
if (isSidebarShowing(window, self)) {
|
||||
hideSidebar(window, self).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
showSidebar(window, self);
|
||||
});
|
||||
|
||||
function onSidebarLoad() {
|
||||
// check if the sidebar is ready
|
||||
let isReady = sidebar.docShell && sidebar.contentDocument;
|
||||
if (!isReady)
|
||||
return;
|
||||
|
||||
// check if it is a web panel
|
||||
let panelBrowser = sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
|
||||
if (!panelBrowser) {
|
||||
bar.removeAttribute('checked');
|
||||
return;
|
||||
}
|
||||
|
||||
let sbTitle = window.document.getElementById('sidebar-title');
|
||||
function onWebPanelSidebarCreated() {
|
||||
if (panelBrowser.contentWindow.location != resolveURL(model.url) ||
|
||||
sbTitle.value != model.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
let worker = windowNS(window).worker = Worker({
|
||||
window: panelBrowser.contentWindow,
|
||||
injectInDocument: true
|
||||
});
|
||||
|
||||
function onWebPanelSidebarUnload() {
|
||||
windowNS(window).onWebPanelSidebarUnload = null;
|
||||
|
||||
// uncheck the associated menuitem
|
||||
bar.setAttribute('checked', 'false');
|
||||
|
||||
emit(self, 'hide', {});
|
||||
emit(self, 'detach', worker);
|
||||
windowNS(window).worker = null;
|
||||
}
|
||||
windowNS(window).onWebPanelSidebarUnload = onWebPanelSidebarUnload;
|
||||
panelBrowser.contentWindow.addEventListener('unload', onWebPanelSidebarUnload, true);
|
||||
|
||||
// check the associated menuitem
|
||||
bar.setAttribute('checked', 'true');
|
||||
|
||||
function onWebPanelSidebarReady() {
|
||||
panelBrowser.contentWindow.removeEventListener('DOMContentLoaded', onWebPanelSidebarReady);
|
||||
windowNS(window).onWebPanelSidebarReady = null;
|
||||
|
||||
emit(self, 'ready', worker);
|
||||
}
|
||||
windowNS(window).onWebPanelSidebarReady = onWebPanelSidebarReady;
|
||||
panelBrowser.contentWindow.addEventListener('DOMContentLoaded', onWebPanelSidebarReady);
|
||||
|
||||
function onWebPanelSidebarLoad() {
|
||||
panelBrowser.contentWindow.removeEventListener('load', onWebPanelSidebarLoad, true);
|
||||
windowNS(window).onWebPanelSidebarLoad = null;
|
||||
|
||||
// TODO: decide if returning worker is acceptable..
|
||||
//emit(self, 'show', { worker: worker });
|
||||
emit(self, 'show', {});
|
||||
}
|
||||
windowNS(window).onWebPanelSidebarLoad = onWebPanelSidebarLoad;
|
||||
panelBrowser.contentWindow.addEventListener('load', onWebPanelSidebarLoad, true);
|
||||
|
||||
emit(self, 'attach', worker);
|
||||
}
|
||||
windowNS(window).onWebPanelSidebarCreated = onWebPanelSidebarCreated;
|
||||
panelBrowser.addEventListener('DOMWindowCreated', onWebPanelSidebarCreated, true);
|
||||
}
|
||||
windowNS(window).onSidebarLoad = onSidebarLoad;
|
||||
sidebar.addEventListener('load', onSidebarLoad, true); // removed properly
|
||||
},
|
||||
onUntrack: function(window) {
|
||||
if (!isBrowser(window))
|
||||
return;
|
||||
|
||||
// hide the sidebar if it is showing
|
||||
hideSidebar(window, self).catch(() => {});
|
||||
|
||||
// kill the menu item
|
||||
let { bar } = windowNS(window);
|
||||
if (bar) {
|
||||
removeFromArray(viewsFor(self), bar);
|
||||
dispose(bar);
|
||||
}
|
||||
|
||||
// kill listeners
|
||||
let sidebar = window.document.getElementById('sidebar');
|
||||
|
||||
if (windowNS(window).onSidebarLoad) {
|
||||
sidebar && sidebar.removeEventListener('load', windowNS(window).onSidebarLoad, true)
|
||||
windowNS(window).onSidebarLoad = null;
|
||||
}
|
||||
|
||||
let panelBrowser = sidebar && sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
|
||||
if (windowNS(window).onWebPanelSidebarCreated) {
|
||||
panelBrowser && panelBrowser.removeEventListener('DOMWindowCreated', windowNS(window).onWebPanelSidebarCreated, true);
|
||||
windowNS(window).onWebPanelSidebarCreated = null;
|
||||
}
|
||||
|
||||
if (windowNS(window).onWebPanelSidebarReady) {
|
||||
panelBrowser && panelBrowser.contentWindow.removeEventListener('DOMContentLoaded', windowNS(window).onWebPanelSidebarReady);
|
||||
windowNS(window).onWebPanelSidebarReady = null;
|
||||
}
|
||||
|
||||
if (windowNS(window).onWebPanelSidebarLoad) {
|
||||
panelBrowser && panelBrowser.contentWindow.removeEventListener('load', windowNS(window).onWebPanelSidebarLoad, true);
|
||||
windowNS(window).onWebPanelSidebarLoad = null;
|
||||
}
|
||||
|
||||
if (windowNS(window).onWebPanelSidebarUnload) {
|
||||
panelBrowser && panelBrowser.contentWindow.removeEventListener('unload', windowNS(window).onWebPanelSidebarUnload, true);
|
||||
windowNS(window).onWebPanelSidebarUnload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
views.set(this, bars);
|
||||
},
|
||||
get id() {
|
||||
return (modelFor(this) || {}).id;
|
||||
},
|
||||
get title() {
|
||||
return (modelFor(this) || {}).title;
|
||||
},
|
||||
set title(v) {
|
||||
// destroyed?
|
||||
if (!modelFor(this))
|
||||
return;
|
||||
// validation
|
||||
if (typeof v != 'string')
|
||||
throw Error('title must be a string');
|
||||
validateTitleAndURLCombo(this, v, this.url);
|
||||
// do update
|
||||
updateTitle(this, v);
|
||||
return modelFor(this).title = v;
|
||||
},
|
||||
get url() {
|
||||
return (modelFor(this) || {}).url;
|
||||
},
|
||||
set url(v) {
|
||||
// destroyed?
|
||||
if (!modelFor(this))
|
||||
return;
|
||||
|
||||
// validation
|
||||
if (!isLocalURL(v))
|
||||
throw Error('the url must be a valid local url');
|
||||
|
||||
validateTitleAndURLCombo(this, this.title, v);
|
||||
|
||||
// do update
|
||||
updateURL(this, v);
|
||||
modelFor(this).url = v;
|
||||
},
|
||||
show: function(window) {
|
||||
return showSidebar(viewFor(window), this);
|
||||
},
|
||||
hide: function(window) {
|
||||
return hideSidebar(viewFor(window), this);
|
||||
},
|
||||
dispose: function() {
|
||||
const internals = sidebarNS(this);
|
||||
|
||||
off(this);
|
||||
|
||||
// stop tracking windows
|
||||
if (internals.tracker) {
|
||||
internals.tracker.unload();
|
||||
}
|
||||
|
||||
internals.tracker = null;
|
||||
internals.windowNS = null;
|
||||
|
||||
views.delete(this);
|
||||
models.delete(this);
|
||||
}
|
||||
});
|
||||
exports.Sidebar = Sidebar;
|
||||
|
||||
function validateTitleAndURLCombo(sidebar, title, url) {
|
||||
url = resolveURL(url);
|
||||
|
||||
if (sidebar.title == title && sidebar.url == url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let window of windows(null, { includePrivate: true })) {
|
||||
let sidebar = window.document.querySelector('menuitem[sidebarurl="' + url + '"][label="' + title + '"]');
|
||||
if (sidebar) {
|
||||
throw Error('The provided title and url combination is invalid (already used).');
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isShowing.define(Sidebar, isSidebarShowing.bind(null, null));
|
||||
show.define(Sidebar, showSidebar.bind(null, null));
|
||||
hide.define(Sidebar, hideSidebar.bind(null, null));
|
||||
|
||||
identify.define(Sidebar, function(sidebar) {
|
||||
return sidebar.id;
|
||||
});
|
||||
|
||||
function toggleSidebar(window, sidebar) {
|
||||
// TODO: make sure this is not private
|
||||
window = window || getMostRecentBrowserWindow();
|
||||
if (isSidebarShowing(window, sidebar)) {
|
||||
return hideSidebar(window, sidebar);
|
||||
}
|
||||
return showSidebar(window, sidebar);
|
||||
}
|
||||
toggle.define(Sidebar, toggleSidebar.bind(null, null));
|
|
@ -1,10 +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 method = require('../../../method/core');
|
||||
|
||||
exports.show = method('show');
|
||||
exports.hide = method('hide');
|
||||
exports.toggle = method('toggle');
|
|
@ -1,27 +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 { contract } = require('../../util/contract');
|
||||
const { isValidURI, URL, isLocalURL } = require('../../url');
|
||||
const { isNil, isObject, isString } = require('../../lang/type');
|
||||
|
||||
exports.contract = contract({
|
||||
id: {
|
||||
is: [ 'string', 'undefined' ],
|
||||
ok: v => /^[a-z0-9-_]+$/i.test(v),
|
||||
msg: 'The option "id" must be a valid alphanumeric id (hyphens and ' +
|
||||
'underscores are allowed).'
|
||||
},
|
||||
title: {
|
||||
is: [ 'string' ],
|
||||
ok: v => v.length
|
||||
},
|
||||
url: {
|
||||
is: [ 'string' ],
|
||||
ok: v => isLocalURL(v),
|
||||
map: v => v.toString(),
|
||||
msg: 'The option "url" must be a valid local URI.'
|
||||
}
|
||||
});
|
|
@ -1,15 +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 models = exports.models = new WeakMap();
|
||||
const views = exports.views = new WeakMap();
|
||||
exports.buttons = new WeakMap();
|
||||
|
||||
exports.viewsFor = function viewsFor(sidebar) {
|
||||
return views.get(sidebar);
|
||||
};
|
||||
exports.modelFor = function modelFor(sidebar) {
|
||||
return models.get(sidebar);
|
||||
};
|
|
@ -1,8 +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 method = require('../../../method/core');
|
||||
|
||||
exports.isShowing = method('isShowing');
|
|
@ -1,214 +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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable',
|
||||
'engines': {
|
||||
'Firefox': '*'
|
||||
}
|
||||
};
|
||||
|
||||
lazyRequire(this, './namespace', "models", "buttons", "views", "viewsFor", "modelFor");
|
||||
lazyRequire(this, '../../window/utils', "isBrowser", "getMostRecentBrowserWindow", "windows", "isWindowPrivate");
|
||||
lazyRequire(this, '../state', "setStateFor");
|
||||
lazyRequire(this, '../../core/promise', "defer");
|
||||
lazyRequire(this, '../../self', "isPrivateBrowsingSupported", "data");
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const WEB_PANEL_BROWSER_ID = 'web-panels-browser';
|
||||
|
||||
const resolveURL = (url) => url ? data.url(url) : url;
|
||||
|
||||
function create(window, details) {
|
||||
let id = makeID(details.id);
|
||||
let { document } = window;
|
||||
|
||||
if (document.getElementById(id))
|
||||
throw new Error('The ID "' + details.id + '" seems already used.');
|
||||
|
||||
let menuitem = document.createElementNS(XUL_NS, 'menuitem');
|
||||
menuitem.setAttribute('id', id);
|
||||
menuitem.setAttribute('label', details.title);
|
||||
menuitem.setAttribute('sidebarurl', resolveURL(details.sidebarurl));
|
||||
menuitem.setAttribute('checked', 'false');
|
||||
menuitem.setAttribute('type', 'checkbox');
|
||||
menuitem.setAttribute('group', 'sidebar');
|
||||
menuitem.setAttribute('autoCheck', 'false');
|
||||
|
||||
document.getElementById('viewSidebarMenu').appendChild(menuitem);
|
||||
|
||||
return menuitem;
|
||||
}
|
||||
exports.create = create;
|
||||
|
||||
function dispose(menuitem) {
|
||||
menuitem.remove();
|
||||
}
|
||||
exports.dispose = dispose;
|
||||
|
||||
function updateTitle(sidebar, title) {
|
||||
let button = buttons.get(sidebar);
|
||||
|
||||
for (let window of windows(null, { includePrivate: true })) {
|
||||
let { document } = window;
|
||||
|
||||
// update the button
|
||||
if (button) {
|
||||
setStateFor(button, window, { label: title });
|
||||
}
|
||||
|
||||
// update the menuitem
|
||||
let mi = document.getElementById(makeID(sidebar.id));
|
||||
if (mi) {
|
||||
mi.setAttribute('label', title)
|
||||
}
|
||||
|
||||
// update sidebar, if showing
|
||||
if (isSidebarShowing(window, sidebar)) {
|
||||
document.getElementById('sidebar-title').setAttribute('value', title);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.updateTitle = updateTitle;
|
||||
|
||||
function updateURL(sidebar, url) {
|
||||
let eleID = makeID(sidebar.id);
|
||||
|
||||
url = resolveURL(url);
|
||||
|
||||
for (let window of windows(null, { includePrivate: true })) {
|
||||
// update the menuitem
|
||||
let mi = window.document.getElementById(eleID);
|
||||
if (mi) {
|
||||
mi.setAttribute('sidebarurl', url)
|
||||
}
|
||||
|
||||
// update sidebar, if showing
|
||||
if (isSidebarShowing(window, sidebar)) {
|
||||
showSidebar(window, sidebar, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.updateURL = updateURL;
|
||||
|
||||
function isSidebarShowing(window, sidebar) {
|
||||
let win = window || getMostRecentBrowserWindow();
|
||||
|
||||
// make sure there is a window
|
||||
if (!win) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// make sure there is a sidebar for the window
|
||||
let sb = win.document.getElementById('sidebar');
|
||||
let sidebarTitle = win.document.getElementById('sidebar-title');
|
||||
if (!(sb && sidebarTitle)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// checks if the sidebar box is hidden
|
||||
let sbb = win.document.getElementById('sidebar-box');
|
||||
if (!sbb || sbb.hidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sidebarTitle.value == modelFor(sidebar).title) {
|
||||
let url = resolveURL(modelFor(sidebar).url);
|
||||
|
||||
// checks if the sidebar is loading
|
||||
if (win.gWebPanelURI == url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// checks if the sidebar loaded already
|
||||
let ele = sb.contentDocument && sb.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
|
||||
if (!ele) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ele.getAttribute('cachedurl') == url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ele && ele.contentWindow && ele.contentWindow.location == url) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// default
|
||||
return false;
|
||||
}
|
||||
exports.isSidebarShowing = isSidebarShowing;
|
||||
|
||||
function showSidebar(window, sidebar, newURL) {
|
||||
window = window || getMostRecentBrowserWindow();
|
||||
|
||||
let { promise, resolve, reject } = defer();
|
||||
let model = modelFor(sidebar);
|
||||
|
||||
if (!newURL && isSidebarShowing(window, sidebar)) {
|
||||
resolve({});
|
||||
}
|
||||
else if (!isPrivateBrowsingSupported && isWindowPrivate(window)) {
|
||||
reject(Error('You cannot show a sidebar on private windows'));
|
||||
}
|
||||
else {
|
||||
sidebar.once('show', resolve);
|
||||
|
||||
let menuitem = window.document.getElementById(makeID(model.id));
|
||||
menuitem.setAttribute('checked', true);
|
||||
|
||||
window.openWebPanel(model.title, resolveURL(newURL || model.url));
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
exports.showSidebar = showSidebar;
|
||||
|
||||
|
||||
function hideSidebar(window, sidebar) {
|
||||
window = window || getMostRecentBrowserWindow();
|
||||
|
||||
let { promise, resolve, reject } = defer();
|
||||
|
||||
if (!isSidebarShowing(window, sidebar)) {
|
||||
reject(Error('The sidebar is already hidden'));
|
||||
}
|
||||
else {
|
||||
sidebar.once('hide', resolve);
|
||||
|
||||
// Below was taken from http://mxr.mozilla.org/mozilla-central/source/browser/base/content/browser.js#4775
|
||||
// the code for window.todggleSideBar()..
|
||||
let { document } = window;
|
||||
let sidebarEle = document.getElementById('sidebar');
|
||||
let sidebarTitle = document.getElementById('sidebar-title');
|
||||
let sidebarBox = document.getElementById('sidebar-box');
|
||||
let sidebarSplitter = document.getElementById('sidebar-splitter');
|
||||
let commandID = sidebarBox.getAttribute('sidebarcommand');
|
||||
let sidebarBroadcaster = document.getElementById(commandID);
|
||||
|
||||
sidebarBox.hidden = true;
|
||||
sidebarSplitter.hidden = true;
|
||||
|
||||
sidebarEle.setAttribute('src', 'about:blank');
|
||||
//sidebarEle.docShell.createAboutBlankContentViewer(null);
|
||||
|
||||
sidebarBroadcaster.removeAttribute('checked');
|
||||
sidebarBox.setAttribute('sidebarcommand', '');
|
||||
sidebarTitle.value = '';
|
||||
sidebarBox.hidden = true;
|
||||
sidebarSplitter.hidden = true;
|
||||
|
||||
// TODO: perhaps this isn't necessary if the window is not most recent?
|
||||
window.gBrowser.selectedBrowser.focus();
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
exports.hideSidebar = hideSidebar;
|
||||
|
||||
function makeID(id) {
|
||||
return 'jetpack-sidebar-' + id;
|
||||
}
|
|
@ -1,239 +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';
|
||||
|
||||
// The Button module currently supports only Firefox.
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps
|
||||
module.metadata = {
|
||||
'stability': 'experimental',
|
||||
'engines': {
|
||||
'Firefox': '*',
|
||||
'SeaMonkey': '*',
|
||||
'Thunderbird': '*'
|
||||
}
|
||||
};
|
||||
|
||||
const { Ci } = require('chrome');
|
||||
|
||||
const events = require('../event/utils');
|
||||
const { events: browserEvents } = require('../browser/events');
|
||||
const { events: tabEvents } = require('../tab/events');
|
||||
const { events: stateEvents } = require('./state/events');
|
||||
|
||||
lazyRequire(this, '../window/utils', "windows", "isInteractive", "getFocusedBrowser");
|
||||
lazyRequire(this, '../tabs/utils', "getActiveTab", "getOwnerWindow");
|
||||
|
||||
lazyRequire(this, '../private-browsing/utils', "ignoreWindow");
|
||||
|
||||
const { freeze } = Object;
|
||||
const { merge } = require('../util/object');
|
||||
lazyRequire(this, '../event/core', "on", "off", "emit");
|
||||
|
||||
lazyRequire(this, '../lang/weak-set', "add", "remove", "has", "clear", "iterator");
|
||||
lazyRequire(this, '../lang/type', "isNil");
|
||||
|
||||
lazyRequire(this, '../view/core', "viewFor");
|
||||
|
||||
const components = new WeakMap();
|
||||
|
||||
const ERR_UNREGISTERED = 'The state cannot be set or get. ' +
|
||||
'The object may be not be registered, or may already have been unloaded.';
|
||||
|
||||
const ERR_INVALID_TARGET = 'The state cannot be set or get for this target.' +
|
||||
'Only window, tab and registered component are valid targets.';
|
||||
|
||||
const isWindow = thing => thing instanceof Ci.nsIDOMWindow;
|
||||
const isTab = thing => thing.tagName && thing.tagName.toLowerCase() === 'tab';
|
||||
const isActiveTab = thing => isTab(thing) && thing === getActiveTab(getOwnerWindow(thing));
|
||||
const isEnumerable = window => !ignoreWindow(window);
|
||||
const browsers = _ =>
|
||||
windows('navigator:browser', { includePrivate: true }).filter(isInteractive);
|
||||
const getMostRecentTab = _ => getActiveTab(getFocusedBrowser());
|
||||
|
||||
function getStateFor(component, target) {
|
||||
if (!isRegistered(component))
|
||||
throw new Error(ERR_UNREGISTERED);
|
||||
|
||||
if (!components.has(component))
|
||||
return null;
|
||||
|
||||
let states = components.get(component);
|
||||
|
||||
if (target) {
|
||||
if (isTab(target) || isWindow(target) || target === component)
|
||||
return states.get(target) || null;
|
||||
else
|
||||
throw new Error(ERR_INVALID_TARGET);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
exports.getStateFor = getStateFor;
|
||||
|
||||
function getDerivedStateFor(component, target) {
|
||||
if (!isRegistered(component))
|
||||
throw new Error(ERR_UNREGISTERED);
|
||||
|
||||
if (!components.has(component))
|
||||
return null;
|
||||
|
||||
let states = components.get(component);
|
||||
|
||||
let componentState = states.get(component);
|
||||
let windowState = null;
|
||||
let tabState = null;
|
||||
|
||||
if (target) {
|
||||
// has a target
|
||||
if (isTab(target)) {
|
||||
windowState = states.get(getOwnerWindow(target), null);
|
||||
|
||||
if (states.has(target)) {
|
||||
// we have a tab state
|
||||
tabState = states.get(target);
|
||||
}
|
||||
}
|
||||
else if (isWindow(target) && states.has(target)) {
|
||||
// we have a window state
|
||||
windowState = states.get(target);
|
||||
}
|
||||
}
|
||||
|
||||
return freeze(merge({}, componentState, windowState, tabState));
|
||||
}
|
||||
exports.getDerivedStateFor = getDerivedStateFor;
|
||||
|
||||
function setStateFor(component, target, state) {
|
||||
if (!isRegistered(component))
|
||||
throw new Error(ERR_UNREGISTERED);
|
||||
|
||||
let isComponentState = target === component;
|
||||
let targetWindows = isWindow(target) ? [target] :
|
||||
isActiveTab(target) ? [getOwnerWindow(target)] :
|
||||
isComponentState ? browsers() :
|
||||
isTab(target) ? [] :
|
||||
null;
|
||||
|
||||
if (!targetWindows)
|
||||
throw new Error(ERR_INVALID_TARGET);
|
||||
|
||||
// initialize the state's map
|
||||
if (!components.has(component))
|
||||
components.set(component, new WeakMap());
|
||||
|
||||
let states = components.get(component);
|
||||
|
||||
if (state === null && !isComponentState) // component state can't be deleted
|
||||
states.delete(target);
|
||||
else {
|
||||
let base = isComponentState ? states.get(target) : null;
|
||||
states.set(target, freeze(merge({}, base, state)));
|
||||
}
|
||||
|
||||
render(component, targetWindows);
|
||||
}
|
||||
exports.setStateFor = setStateFor;
|
||||
|
||||
function render(component, targetWindows) {
|
||||
targetWindows = targetWindows ? [].concat(targetWindows) : browsers();
|
||||
|
||||
for (let window of targetWindows.filter(isEnumerable)) {
|
||||
let tabState = getDerivedStateFor(component, getActiveTab(window));
|
||||
|
||||
emit(stateEvents, 'data', {
|
||||
type: 'render',
|
||||
target: component,
|
||||
window: window,
|
||||
state: tabState
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
exports.render = render;
|
||||
|
||||
function properties(contract) {
|
||||
let { rules } = contract;
|
||||
let descriptor = Object.keys(rules).reduce(function(descriptor, name) {
|
||||
descriptor[name] = {
|
||||
get: function() { return getDerivedStateFor(this)[name] },
|
||||
set: function(value) {
|
||||
let changed = {};
|
||||
changed[name] = value;
|
||||
|
||||
setStateFor(this, this, contract(changed));
|
||||
}
|
||||
}
|
||||
return descriptor;
|
||||
}, {});
|
||||
|
||||
return Object.create(Object.prototype, descriptor);
|
||||
}
|
||||
exports.properties = properties;
|
||||
|
||||
function state(contract) {
|
||||
return {
|
||||
state: function state(target, state) {
|
||||
let nativeTarget = target === 'window' ? getFocusedBrowser()
|
||||
: target === 'tab' ? getMostRecentTab()
|
||||
: target === this ? null
|
||||
: viewFor(target);
|
||||
|
||||
if (!nativeTarget && target !== this && !isNil(target))
|
||||
throw new Error(ERR_INVALID_TARGET);
|
||||
|
||||
target = nativeTarget || target;
|
||||
|
||||
// jquery style
|
||||
return arguments.length < 2
|
||||
? getDerivedStateFor(this, target)
|
||||
: setStateFor(this, target, contract(state))
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.state = state;
|
||||
|
||||
const register = (component, state) => {
|
||||
add(components, component);
|
||||
setStateFor(component, component, state);
|
||||
}
|
||||
exports.register = register;
|
||||
|
||||
const unregister = component => {
|
||||
remove(components, component);
|
||||
}
|
||||
exports.unregister = unregister;
|
||||
|
||||
const isRegistered = component => has(components, component);
|
||||
exports.isRegistered = isRegistered;
|
||||
|
||||
var tabSelect = events.filter(tabEvents, e => e.type === 'TabSelect');
|
||||
var tabClose = events.filter(tabEvents, e => e.type === 'TabClose');
|
||||
var windowOpen = events.filter(browserEvents, e => e.type === 'load');
|
||||
var windowClose = events.filter(browserEvents, e => e.type === 'close');
|
||||
|
||||
var close = events.merge([tabClose, windowClose]);
|
||||
var activate = events.merge([windowOpen, tabSelect]);
|
||||
|
||||
on(activate, 'data', ({target}) => {
|
||||
let [window, tab] = isWindow(target)
|
||||
? [target, getActiveTab(target)]
|
||||
: [getOwnerWindow(target), target];
|
||||
|
||||
if (ignoreWindow(window)) return;
|
||||
|
||||
for (let component of iterator(components)) {
|
||||
emit(stateEvents, 'data', {
|
||||
type: 'render',
|
||||
target: component,
|
||||
window: window,
|
||||
state: getDerivedStateFor(component, tab)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
on(close, 'data', function({target}) {
|
||||
for (let component of iterator(components)) {
|
||||
components.get(component).delete(target);
|
||||
}
|
||||
});
|
|
@ -1,18 +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';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'experimental',
|
||||
'engines': {
|
||||
'Firefox': '*',
|
||||
'SeaMonkey': '*',
|
||||
'Thunderbird': '*'
|
||||
}
|
||||
};
|
||||
|
||||
var channel = {};
|
||||
|
||||
exports.events = channel;
|
|
@ -1,16 +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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental",
|
||||
"engines": {
|
||||
"Firefox": "> 28"
|
||||
}
|
||||
};
|
||||
|
||||
const { Toolbar } = require("./toolbar/model");
|
||||
require("./toolbar/view");
|
||||
|
||||
exports.Toolbar = Toolbar;
|
|
@ -1,151 +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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental",
|
||||
"engines": {
|
||||
"Firefox": "> 28"
|
||||
}
|
||||
};
|
||||
|
||||
const { Class } = require("../../core/heritage");
|
||||
const { EventTarget } = require("../../event/target");
|
||||
const { off, setListeners, emit } = require("../../event/core");
|
||||
const { Reactor, foldp, merges, send } = require("../../event/utils");
|
||||
const { Disposable } = require("../../core/disposable");
|
||||
const { InputPort } = require("../../input/system");
|
||||
const { OutputPort } = require("../../output/system");
|
||||
const { identify } = require("../id");
|
||||
const { pairs, object, map, each } = require("../../util/sequence");
|
||||
const { patch, diff } = require("diffpatcher/index");
|
||||
const { contract } = require("../../util/contract");
|
||||
const { id: addonID } = require("../../self");
|
||||
|
||||
// Input state is accumulated from the input received form the toolbar
|
||||
// view code & local output. Merging local output reflects local state
|
||||
// changes without complete roundloop.
|
||||
const input = foldp(patch, {}, new InputPort({ id: "toolbar-changed" }));
|
||||
const output = new OutputPort({ id: "toolbar-change" });
|
||||
|
||||
// Takes toolbar title and normalizes is to an
|
||||
// identifier, also prefixes with add-on id.
|
||||
const titleToId = title =>
|
||||
("toolbar-" + addonID + "-" + title).
|
||||
toLowerCase().
|
||||
replace(/\s/g, "-").
|
||||
replace(/[^A-Za-z0-9_\-]/g, "");
|
||||
|
||||
const validate = contract({
|
||||
title: {
|
||||
is: ["string"],
|
||||
ok: x => x.length > 0,
|
||||
msg: "The `option.title` string must be provided"
|
||||
},
|
||||
items: {
|
||||
is:["undefined", "object", "array"],
|
||||
msg: "The `options.items` must be iterable sequence of items"
|
||||
},
|
||||
hidden: {
|
||||
is: ["boolean", "undefined"],
|
||||
msg: "The `options.hidden` must be boolean"
|
||||
}
|
||||
});
|
||||
|
||||
// Toolbars is a mapping between `toolbar.id` & `toolbar` instances,
|
||||
// which is used to find intstance for dispatching events.
|
||||
var toolbars = new Map();
|
||||
|
||||
const Toolbar = Class({
|
||||
extends: EventTarget,
|
||||
implements: [Disposable],
|
||||
initialize: function(params={}) {
|
||||
const options = validate(params);
|
||||
const id = titleToId(options.title);
|
||||
|
||||
if (toolbars.has(id))
|
||||
throw Error("Toolbar with this id already exists: " + id);
|
||||
|
||||
// Set of the items in the toolbar isn't mutable, as a matter of fact
|
||||
// it just defines desired set of items, actual set is under users
|
||||
// control. Conver test to an array and freeze to make sure users won't
|
||||
// try mess with it.
|
||||
const items = Object.freeze(options.items ? [...options.items] : []);
|
||||
|
||||
const initial = {
|
||||
id: id,
|
||||
title: options.title,
|
||||
// By default toolbars are visible when add-on is installed, unless
|
||||
// add-on authors decides it should be hidden. From that point on
|
||||
// user is in control.
|
||||
collapsed: !!options.hidden,
|
||||
// In terms of state only identifiers of items matter.
|
||||
items: items.map(identify)
|
||||
};
|
||||
|
||||
this.id = id;
|
||||
this.items = items;
|
||||
|
||||
toolbars.set(id, this);
|
||||
setListeners(this, params);
|
||||
|
||||
// Send initial state to the host so it can reflect it
|
||||
// into a user interface.
|
||||
send(output, object([id, initial]));
|
||||
},
|
||||
|
||||
get title() {
|
||||
const state = reactor.value[this.id];
|
||||
return state && state.title;
|
||||
},
|
||||
get hidden() {
|
||||
const state = reactor.value[this.id];
|
||||
return state && state.collapsed;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
send(output, object([this.id, null]));
|
||||
},
|
||||
// `JSON.stringify` serializes objects based of the return
|
||||
// value of this method. For convinienc we provide this method
|
||||
// to serialize actual state data. Note: items will also be
|
||||
// serialized so they should probably implement `toJSON`.
|
||||
toJSON: function() {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
hidden: this.hidden,
|
||||
items: this.items
|
||||
};
|
||||
}
|
||||
});
|
||||
exports.Toolbar = Toolbar;
|
||||
identify.define(Toolbar, toolbar => toolbar.id);
|
||||
|
||||
const dispose = toolbar => {
|
||||
toolbars.delete(toolbar.id);
|
||||
emit(toolbar, "detach");
|
||||
off(toolbar);
|
||||
};
|
||||
|
||||
const reactor = new Reactor({
|
||||
onStep: (present, past) => {
|
||||
const delta = diff(past, present);
|
||||
|
||||
each(([id, update]) => {
|
||||
const toolbar = toolbars.get(id);
|
||||
|
||||
// Remove
|
||||
if (!update)
|
||||
dispose(toolbar);
|
||||
// Add
|
||||
else if (!past[id])
|
||||
emit(toolbar, "attach");
|
||||
// Update
|
||||
else
|
||||
emit(toolbar, update.collapsed ? "hide" : "show", toolbar);
|
||||
}, pairs(delta));
|
||||
}
|
||||
});
|
||||
reactor.run(input);
|
|
@ -1,248 +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";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental",
|
||||
"engines": {
|
||||
"Firefox": "> 28"
|
||||
}
|
||||
};
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
|
||||
const { subscribe, send, Reactor, foldp, lift, merges } = require("../../event/utils");
|
||||
const { InputPort } = require("../../input/system");
|
||||
const { OutputPort } = require("../../output/system");
|
||||
const { Interactive } = require("../../input/browser");
|
||||
const { CustomizationInput } = require("../../input/customizable-ui");
|
||||
const { pairs, map, isEmpty, object,
|
||||
each, keys, values } = require("../../util/sequence");
|
||||
const { curry, flip } = require("../../lang/functional");
|
||||
lazyRequire(this, "diffpatcher/index", "patch", "diff");
|
||||
const prefs = require("../../preferences/service");
|
||||
lazyRequire(this, "../../window/utils", "getByOuterId");
|
||||
lazyRequire(this, '../../private-browsing/utils', "ignoreWindow");
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const PREF_ROOT = "extensions.sdk-toolbar-collapsed.";
|
||||
|
||||
|
||||
// There are two output ports one for publishing changes that occured
|
||||
// and the other for change requests. Later is synchronous and is only
|
||||
// consumed here. Note: it needs to be synchronous to avoid race conditions
|
||||
// when `collapsed` attribute changes are caused by user interaction and
|
||||
// toolbar is destroyed between the ticks.
|
||||
const output = new OutputPort({ id: "toolbar-changed" });
|
||||
const syncoutput = new OutputPort({ id: "toolbar-change", sync: true });
|
||||
|
||||
// Merge disptached changes and recevied changes from models to keep state up to
|
||||
// date.
|
||||
const Toolbars = foldp(patch, {}, merges([new InputPort({ id: "toolbar-changed" }),
|
||||
new InputPort({ id: "toolbar-change" })]));
|
||||
const State = lift((toolbars, windows, customizable) =>
|
||||
({windows: windows, toolbars: toolbars, customizable: customizable}),
|
||||
Toolbars, Interactive, new CustomizationInput());
|
||||
|
||||
// Shared event handler that makes `event.target.parent` collapsed.
|
||||
// Used as toolbar's close buttons click handler.
|
||||
const collapseToolbar = event => {
|
||||
const toolbar = event.target.parentNode;
|
||||
toolbar.collapsed = true;
|
||||
};
|
||||
|
||||
const parseAttribute = x =>
|
||||
x === "true" ? true :
|
||||
x === "false" ? false :
|
||||
x === "" ? null :
|
||||
x;
|
||||
|
||||
// Shared mutation observer that is used to observe `toolbar` node's
|
||||
// attribute mutations. Mutations are aggregated in the `delta` hash
|
||||
// and send to `ToolbarStateChanged` channel to let model know state
|
||||
// has changed.
|
||||
const attributesChanged = mutations => {
|
||||
const delta = mutations.reduce((changes, {attributeName, target}) => {
|
||||
const id = target.id;
|
||||
const field = attributeName === "toolbarname" ? "title" : attributeName;
|
||||
let change = changes[id] || (changes[id] = {});
|
||||
change[field] = parseAttribute(target.getAttribute(attributeName));
|
||||
return changes;
|
||||
}, {});
|
||||
|
||||
// Calculate what are the updates from the current state and if there are
|
||||
// any send them.
|
||||
const updates = diff(reactor.value, patch(reactor.value, delta));
|
||||
|
||||
if (!isEmpty(pairs(updates))) {
|
||||
// TODO: Consider sending sync to make sure that there won't be a new
|
||||
// update doing a delete in the meantime.
|
||||
send(syncoutput, updates);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Utility function creates `toolbar` with a "close" button and returns
|
||||
// it back. In addition it set's up a listener and observer to communicate
|
||||
// state changes.
|
||||
const addView = curry((options, {document, window}) => {
|
||||
if (ignoreWindow(window))
|
||||
return;
|
||||
|
||||
let view = document.createElementNS(XUL_NS, "toolbar");
|
||||
view.setAttribute("id", options.id);
|
||||
view.setAttribute("collapsed", options.collapsed);
|
||||
view.setAttribute("toolbarname", options.title);
|
||||
view.setAttribute("pack", "end");
|
||||
view.setAttribute("customizable", "false");
|
||||
view.setAttribute("style", "padding: 2px 0; max-height: 40px;");
|
||||
view.setAttribute("mode", "icons");
|
||||
view.setAttribute("iconsize", "small");
|
||||
view.setAttribute("context", "toolbar-context-menu");
|
||||
view.setAttribute("class", "chromeclass-toolbar");
|
||||
|
||||
let label = document.createElementNS(XUL_NS, "label");
|
||||
label.setAttribute("value", options.title);
|
||||
label.setAttribute("collapsed", "true");
|
||||
view.appendChild(label);
|
||||
|
||||
let closeButton = document.createElementNS(XUL_NS, "toolbarbutton");
|
||||
closeButton.setAttribute("id", "close-" + options.id);
|
||||
closeButton.setAttribute("class", "close-icon");
|
||||
closeButton.setAttribute("customizable", false);
|
||||
closeButton.addEventListener("command", collapseToolbar);
|
||||
|
||||
view.appendChild(closeButton);
|
||||
|
||||
// In order to have a close button not costumizable, aligned on the right,
|
||||
// leaving the customizable capabilities of Australis, we need to create
|
||||
// a toolbar inside a toolbar.
|
||||
// This is should be a temporary hack, we should have a proper XBL for toolbar
|
||||
// instead. See:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=982005
|
||||
let toolbar = document.createElementNS(XUL_NS, "toolbar");
|
||||
toolbar.setAttribute("id", "inner-" + options.id);
|
||||
toolbar.setAttribute("defaultset", options.items.join(","));
|
||||
toolbar.setAttribute("customizable", "true");
|
||||
toolbar.setAttribute("style", "-moz-appearance: none; overflow: hidden; border: 0;");
|
||||
toolbar.setAttribute("mode", "icons");
|
||||
toolbar.setAttribute("iconsize", "small");
|
||||
toolbar.setAttribute("context", "toolbar-context-menu");
|
||||
toolbar.setAttribute("flex", "1");
|
||||
|
||||
view.insertBefore(toolbar, closeButton);
|
||||
|
||||
const observer = new document.defaultView.MutationObserver(attributesChanged);
|
||||
observer.observe(view, { attributes: true,
|
||||
attributeFilter: ["collapsed", "toolbarname"] });
|
||||
|
||||
const toolbox = document.getElementById("navigator-toolbox");
|
||||
toolbox.appendChild(view);
|
||||
});
|
||||
const viewAdd = curry(flip(addView));
|
||||
|
||||
const removeView = curry((id, {document}) => {
|
||||
const view = document.getElementById(id);
|
||||
if (view) view.remove();
|
||||
});
|
||||
|
||||
const updateView = curry((id, {title, collapsed, isCustomizing}, {document}) => {
|
||||
const view = document.getElementById(id);
|
||||
|
||||
if (!view)
|
||||
return;
|
||||
|
||||
if (title)
|
||||
view.setAttribute("toolbarname", title);
|
||||
|
||||
if (collapsed !== void(0))
|
||||
view.setAttribute("collapsed", Boolean(collapsed));
|
||||
|
||||
if (isCustomizing !== void(0)) {
|
||||
view.querySelector("label").collapsed = !isCustomizing;
|
||||
view.querySelector("toolbar").style.visibility = isCustomizing
|
||||
? "hidden" : "visible";
|
||||
}
|
||||
});
|
||||
|
||||
const viewUpdate = curry(flip(updateView));
|
||||
|
||||
// Utility function used to register toolbar into CustomizableUI.
|
||||
const registerToolbar = state => {
|
||||
// If it's first additon register toolbar as customizableUI component.
|
||||
CustomizableUI.registerArea("inner-" + state.id, {
|
||||
type: CustomizableUI.TYPE_TOOLBAR,
|
||||
legacy: true,
|
||||
defaultPlacements: [...state.items]
|
||||
});
|
||||
};
|
||||
// Utility function used to unregister toolbar from the CustomizableUI.
|
||||
const unregisterToolbar = CustomizableUI.unregisterArea;
|
||||
|
||||
const reactor = new Reactor({
|
||||
onStep: (present, past) => {
|
||||
const delta = diff(past, present);
|
||||
|
||||
each(([id, update]) => {
|
||||
// If update is `null` toolbar is removed, in such case
|
||||
// we unregister toolbar and remove it from each window
|
||||
// it was added to.
|
||||
if (update === null) {
|
||||
unregisterToolbar("inner-" + id);
|
||||
each(removeView(id), values(past.windows));
|
||||
|
||||
send(output, object([id, null]));
|
||||
}
|
||||
else if (past.toolbars[id]) {
|
||||
// If `collapsed` state for toolbar was updated, persist
|
||||
// it for a future sessions.
|
||||
if (update.collapsed !== void(0))
|
||||
prefs.set(PREF_ROOT + id, update.collapsed);
|
||||
|
||||
// Reflect update in each window it was added to.
|
||||
each(updateView(id, update), values(past.windows));
|
||||
|
||||
send(output, object([id, update]));
|
||||
}
|
||||
// Hack: Mutation observers are invoked async, which means that if
|
||||
// client does `hide(toolbar)` & then `toolbar.destroy()` by the
|
||||
// time we'll get update for `collapsed` toolbar will be removed.
|
||||
// For now we check if `update.id` is present which will be undefined
|
||||
// in such cases.
|
||||
else if (update.id) {
|
||||
// If it is a new toolbar we create initial state by overriding
|
||||
// `collapsed` filed with value persisted in previous sessions.
|
||||
const state = patch(update, {
|
||||
collapsed: prefs.get(PREF_ROOT + id, update.collapsed),
|
||||
});
|
||||
|
||||
// Register toolbar and add it each window known in the past
|
||||
// (note that new windows if any will be handled in loop below).
|
||||
registerToolbar(state);
|
||||
each(addView(state), values(past.windows));
|
||||
|
||||
send(output, object([state.id, state]));
|
||||
}
|
||||
}, pairs(delta.toolbars));
|
||||
|
||||
// Add views to every window that was added.
|
||||
each(window => {
|
||||
if (window)
|
||||
each(viewAdd(window), values(past.toolbars));
|
||||
}, values(delta.windows));
|
||||
|
||||
each(([id, isCustomizing]) => {
|
||||
each(viewUpdate(getByOuterId(id), {isCustomizing: !!isCustomizing}),
|
||||
keys(present.toolbars));
|
||||
|
||||
}, pairs(delta.customizable))
|
||||
},
|
||||
onEnd: state => {
|
||||
each(id => {
|
||||
unregisterToolbar("inner-" + id);
|
||||
each(removeView(id), values(state.windows));
|
||||
}, keys(state.toolbars));
|
||||
}
|
||||
});
|
||||
reactor.run(State);
|
Загрузка…
Ссылка в новой задаче