Bug 1350646: Part 6 - Remove SDK UI modules. r=Mossop

MozReview-Commit-ID: Joln7vw9Y9r

--HG--
extra : source : 35c4d4cd77c7d33aa1ba0fd93f0e369d3a452232
This commit is contained in:
Kris Maglione 2017-08-02 14:11:00 -07:00
Родитель c6d9379091
Коммит fed32bf06a
47 изменённых файлов: 0 добавлений и 6410 удалений

Просмотреть файл

@ -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);