From fed32bf06a4acbad6867a52c81bb1cffd34fdec0 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Wed, 2 Aug 2017 14:11:00 -0700 Subject: [PATCH] Bug 1350646: Part 6 - Remove SDK UI modules. r=Mossop MozReview-Commit-ID: Joln7vw9Y9r --HG-- extra : source : 35c4d4cd77c7d33aa1ba0fd93f0e369d3a452232 --- addon-sdk/moz.build | 46 - addon-sdk/source/lib/diffpatcher/diff.js | 45 - addon-sdk/source/lib/diffpatcher/index.js | 5 - addon-sdk/source/lib/diffpatcher/patch.js | 21 - addon-sdk/source/lib/diffpatcher/rebase.js | 36 - .../source/lib/diffpatcher/test/common.js | 3 - addon-sdk/source/lib/diffpatcher/test/diff.js | 59 - .../source/lib/diffpatcher/test/index.js | 14 - .../source/lib/diffpatcher/test/patch.js | 83 -- addon-sdk/source/lib/diffpatcher/test/tap.js | 3 - .../source/lib/framescript/context-menu.js | 215 --- .../source/lib/sdk/content/context-menu.js | 407 ------ addon-sdk/source/lib/sdk/context-menu.js | 1187 ----------------- .../source/lib/sdk/context-menu/context.js | 146 -- addon-sdk/source/lib/sdk/context-menu/core.js | 384 ------ .../source/lib/sdk/context-menu/readers.js | 112 -- addon-sdk/source/lib/sdk/context-menu@2.js | 32 - addon-sdk/source/lib/sdk/input/browser.js | 73 - .../source/lib/sdk/input/customizable-ui.js | 28 - addon-sdk/source/lib/sdk/input/frame.js | 85 -- addon-sdk/source/lib/sdk/input/system.js | 113 -- addon-sdk/source/lib/sdk/panel.js | 436 ------ addon-sdk/source/lib/sdk/panel/events.js | 27 - addon-sdk/source/lib/sdk/panel/utils.js | 451 ------- addon-sdk/source/lib/sdk/ui.js | 25 - addon-sdk/source/lib/sdk/ui/button/action.js | 114 -- .../source/lib/sdk/ui/button/contract.js | 73 - addon-sdk/source/lib/sdk/ui/button/toggle.js | 127 -- addon-sdk/source/lib/sdk/ui/button/view.js | 251 ---- .../source/lib/sdk/ui/button/view/events.js | 18 - addon-sdk/source/lib/sdk/ui/component.js | 182 --- addon-sdk/source/lib/sdk/ui/frame.js | 15 - addon-sdk/source/lib/sdk/ui/frame/model.js | 154 --- addon-sdk/source/lib/sdk/ui/frame/view.html | 18 - addon-sdk/source/lib/sdk/ui/frame/view.js | 145 -- addon-sdk/source/lib/sdk/ui/id.js | 27 - addon-sdk/source/lib/sdk/ui/sidebar.js | 304 ----- .../source/lib/sdk/ui/sidebar/actions.js | 10 - .../source/lib/sdk/ui/sidebar/contract.js | 27 - .../source/lib/sdk/ui/sidebar/namespace.js | 15 - addon-sdk/source/lib/sdk/ui/sidebar/utils.js | 8 - addon-sdk/source/lib/sdk/ui/sidebar/view.js | 214 --- addon-sdk/source/lib/sdk/ui/state.js | 239 ---- addon-sdk/source/lib/sdk/ui/state/events.js | 18 - addon-sdk/source/lib/sdk/ui/toolbar.js | 16 - addon-sdk/source/lib/sdk/ui/toolbar/model.js | 151 --- addon-sdk/source/lib/sdk/ui/toolbar/view.js | 248 ---- 47 files changed, 6410 deletions(-) delete mode 100644 addon-sdk/source/lib/diffpatcher/diff.js delete mode 100644 addon-sdk/source/lib/diffpatcher/index.js delete mode 100644 addon-sdk/source/lib/diffpatcher/patch.js delete mode 100644 addon-sdk/source/lib/diffpatcher/rebase.js delete mode 100644 addon-sdk/source/lib/diffpatcher/test/common.js delete mode 100644 addon-sdk/source/lib/diffpatcher/test/diff.js delete mode 100644 addon-sdk/source/lib/diffpatcher/test/index.js delete mode 100644 addon-sdk/source/lib/diffpatcher/test/patch.js delete mode 100644 addon-sdk/source/lib/diffpatcher/test/tap.js delete mode 100644 addon-sdk/source/lib/framescript/context-menu.js delete mode 100644 addon-sdk/source/lib/sdk/content/context-menu.js delete mode 100644 addon-sdk/source/lib/sdk/context-menu.js delete mode 100644 addon-sdk/source/lib/sdk/context-menu/context.js delete mode 100644 addon-sdk/source/lib/sdk/context-menu/core.js delete mode 100644 addon-sdk/source/lib/sdk/context-menu/readers.js delete mode 100644 addon-sdk/source/lib/sdk/context-menu@2.js delete mode 100644 addon-sdk/source/lib/sdk/input/browser.js delete mode 100644 addon-sdk/source/lib/sdk/input/customizable-ui.js delete mode 100644 addon-sdk/source/lib/sdk/input/frame.js delete mode 100644 addon-sdk/source/lib/sdk/input/system.js delete mode 100644 addon-sdk/source/lib/sdk/panel.js delete mode 100644 addon-sdk/source/lib/sdk/panel/events.js delete mode 100644 addon-sdk/source/lib/sdk/panel/utils.js delete mode 100644 addon-sdk/source/lib/sdk/ui.js delete mode 100644 addon-sdk/source/lib/sdk/ui/button/action.js delete mode 100644 addon-sdk/source/lib/sdk/ui/button/contract.js delete mode 100644 addon-sdk/source/lib/sdk/ui/button/toggle.js delete mode 100644 addon-sdk/source/lib/sdk/ui/button/view.js delete mode 100644 addon-sdk/source/lib/sdk/ui/button/view/events.js delete mode 100644 addon-sdk/source/lib/sdk/ui/component.js delete mode 100644 addon-sdk/source/lib/sdk/ui/frame.js delete mode 100644 addon-sdk/source/lib/sdk/ui/frame/model.js delete mode 100644 addon-sdk/source/lib/sdk/ui/frame/view.html delete mode 100644 addon-sdk/source/lib/sdk/ui/frame/view.js delete mode 100644 addon-sdk/source/lib/sdk/ui/id.js delete mode 100644 addon-sdk/source/lib/sdk/ui/sidebar.js delete mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/actions.js delete mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/contract.js delete mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/namespace.js delete mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/utils.js delete mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/view.js delete mode 100644 addon-sdk/source/lib/sdk/ui/state.js delete mode 100644 addon-sdk/source/lib/sdk/ui/state/events.js delete mode 100644 addon-sdk/source/lib/sdk/ui/toolbar.js delete mode 100644 addon-sdk/source/lib/sdk/ui/toolbar/model.js delete mode 100644 addon-sdk/source/lib/sdk/ui/toolbar/view.js diff --git a/addon-sdk/moz.build b/addon-sdk/moz.build index c08eea3c0c9d..61135328fe6c 100644 --- a/addon-sdk/moz.build +++ b/addon-sdk/moz.build @@ -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', diff --git a/addon-sdk/source/lib/diffpatcher/diff.js b/addon-sdk/source/lib/diffpatcher/diff.js deleted file mode 100644 index 967c137e65ce..000000000000 --- a/addon-sdk/source/lib/diffpatcher/diff.js +++ /dev/null @@ -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 diff --git a/addon-sdk/source/lib/diffpatcher/index.js b/addon-sdk/source/lib/diffpatcher/index.js deleted file mode 100644 index 91ddba42532f..000000000000 --- a/addon-sdk/source/lib/diffpatcher/index.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -exports.diff = require("./diff") -exports.patch = require("./patch") -exports.rebase = require("./rebase") diff --git a/addon-sdk/source/lib/diffpatcher/patch.js b/addon-sdk/source/lib/diffpatcher/patch.js deleted file mode 100644 index 9271e88932ce..000000000000 --- a/addon-sdk/source/lib/diffpatcher/patch.js +++ /dev/null @@ -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 diff --git a/addon-sdk/source/lib/diffpatcher/rebase.js b/addon-sdk/source/lib/diffpatcher/rebase.js deleted file mode 100644 index 03c756feec7c..000000000000 --- a/addon-sdk/source/lib/diffpatcher/rebase.js +++ /dev/null @@ -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 diff --git a/addon-sdk/source/lib/diffpatcher/test/common.js b/addon-sdk/source/lib/diffpatcher/test/common.js deleted file mode 100644 index dbc79013c508..000000000000 --- a/addon-sdk/source/lib/diffpatcher/test/common.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -require("test").run(require("./index")) diff --git a/addon-sdk/source/lib/diffpatcher/test/diff.js b/addon-sdk/source/lib/diffpatcher/test/diff.js deleted file mode 100644 index d1d67400556c..000000000000 --- a/addon-sdk/source/lib/diffpatcher/test/diff.js +++ /dev/null @@ -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") -} diff --git a/addon-sdk/source/lib/diffpatcher/test/index.js b/addon-sdk/source/lib/diffpatcher/test/index.js deleted file mode 100644 index c06407e7c5bb..000000000000 --- a/addon-sdk/source/lib/diffpatcher/test/index.js +++ /dev/null @@ -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") -} diff --git a/addon-sdk/source/lib/diffpatcher/test/patch.js b/addon-sdk/source/lib/diffpatcher/test/patch.js deleted file mode 100644 index dc2e38229d3a..000000000000 --- a/addon-sdk/source/lib/diffpatcher/test/patch.js +++ /dev/null @@ -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") -} diff --git a/addon-sdk/source/lib/diffpatcher/test/tap.js b/addon-sdk/source/lib/diffpatcher/test/tap.js deleted file mode 100644 index e550b82f5bf9..000000000000 --- a/addon-sdk/source/lib/diffpatcher/test/tap.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -require("retape")(require("./index")) diff --git a/addon-sdk/source/lib/framescript/context-menu.js b/addon-sdk/source/lib/framescript/context-menu.js deleted file mode 100644 index 049734d84793..000000000000 --- a/addon-sdk/source/lib/framescript/context-menu.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/content/context-menu.js b/addon-sdk/source/lib/sdk/content/context-menu.js deleted file mode 100644 index 4fc4bee40745..000000000000 --- a/addon-sdk/source/lib/sdk/content/context-menu.js +++ /dev/null @@ -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); - } -}); diff --git a/addon-sdk/source/lib/sdk/context-menu.js b/addon-sdk/source/lib/sdk/context-menu.js deleted file mode 100644 index 1cb9e735cd33..000000000000 --- a/addon-sdk/source/lib/sdk/context-menu.js +++ /dev/null @@ -1,1187 +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": "stable", - "engines": { - // TODO Fennec support Bug 788334 - "Firefox": "*", - "SeaMonkey": "*" - } -}; - -const { Class, mix } = require("./core/heritage"); -const { ns } = require("./core/namespace"); -lazyRequire(this, "./deprecated/api-utils", "validateOptions", "getTypeOf"); -lazyRequire(this, "./url", "URL", "isValidURI"); -lazyRequire(this, "./deprecated/window-utils", "WindowTracker", "browserWindowIterator"); -lazyRequire(this, "./window/utils", "isBrowser", "getInnerId"); -lazyRequire(this, "./util/match-pattern", "MatchPattern"); -const { EventTarget } = require("./event/target"); -lazyRequire(this, './event/core', "emit"); -const { when } = require('./system/unload'); -const { contract: loaderContract } = require('./content/loader'); -const { omit } = require('./util/object'); -lazyRequireModule(this, './self', "self"); -const { remoteRequire, processes } = require('./remote/parent'); -remoteRequire('sdk/content/context-menu'); - -// All user items we add have this class. -const ITEM_CLASS = "addon-context-menu-item"; - -// Items in the top-level context menu also have this class. -const TOPLEVEL_ITEM_CLASS = "addon-context-menu-item-toplevel"; - -// Items in the overflow submenu also have this class. -const OVERFLOW_ITEM_CLASS = "addon-context-menu-item-overflow"; - -// The class of the menu separator that separates standard context menu items -// from our user items. -const SEPARATOR_CLASS = "addon-context-menu-separator"; - -// If more than this number of items are added to the context menu, all items -// overflow into a "Jetpack" submenu. -const OVERFLOW_THRESH_DEFAULT = 10; -const OVERFLOW_THRESH_PREF = - "extensions.addon-sdk.context-menu.overflowThreshold"; - -// The label of the overflow sub-xul:menu. -// -// TODO: Localize these. -const OVERFLOW_MENU_LABEL = "Add-ons"; -const OVERFLOW_MENU_ACCESSKEY = "A"; - -// The class of the overflow sub-xul:menu. -const OVERFLOW_MENU_CLASS = "addon-content-menu-overflow-menu"; - -// The class of the overflow submenu's xul:menupopup. -const OVERFLOW_POPUP_CLASS = "addon-content-menu-overflow-popup"; - -// Holds private properties for API objects -var internal = ns(); - -// A little hacky but this is the last process ID that last opened the context -// menu -var lastContextProcessId = null; - -var uuidModule = require('./util/uuid'); -function uuid() { - return uuidModule.uuid().toString(); -} - -function getScheme(spec) { - try { - return URL(spec).scheme; - } - catch(e) { - return null; - } -} - -var Context = Class({ - initialize: function() { - internal(this).id = uuid(); - }, - - // Returns the node that made this context current - adjustPopupNode: function adjustPopupNode(popupNode) { - return popupNode; - }, - - // Returns whether this context is current for the current node - isCurrent: function isCurrent(state) { - return state; - } -}); - -// Matches when the context-clicked node doesn't have any of -// NON_PAGE_CONTEXT_ELTS in its ancestors -var PageContext = Class({ - extends: Context, - - serialize: function() { - return { - id: internal(this).id, - type: "PageContext", - args: [] - } - } -}); -exports.PageContext = PageContext; - -// Matches when there is an active selection in the window -var SelectionContext = Class({ - extends: Context, - - serialize: function() { - return { - id: internal(this).id, - type: "SelectionContext", - args: [] - } - } -}); -exports.SelectionContext = SelectionContext; - -// Matches when the context-clicked node or any of its ancestors matches the -// selector given -var SelectorContext = Class({ - extends: Context, - - initialize: function initialize(selector) { - Context.prototype.initialize.call(this); - let options = validateOptions({ selector: selector }, { - selector: { - is: ["string"], - msg: "selector must be a string." - } - }); - internal(this).selector = options.selector; - }, - - serialize: function() { - return { - id: internal(this).id, - type: "SelectorContext", - args: [internal(this).selector] - } - } -}); -exports.SelectorContext = SelectorContext; - -// Matches when the page url matches any of the patterns given -var URLContext = Class({ - extends: Context, - - initialize: function initialize(patterns) { - Context.prototype.initialize.call(this); - patterns = Array.isArray(patterns) ? patterns : [patterns]; - - try { - internal(this).patterns = patterns.map(p => new MatchPattern(p)); - } - catch (err) { - throw new Error("Patterns must be a string, regexp or an array of " + - "strings or regexps: " + err); - } - }, - - isCurrent: function isCurrent(url) { - return internal(this).patterns.some(p => p.test(url)); - }, - - serialize: function() { - return { - id: internal(this).id, - type: "URLContext", - args: [] - } - } -}); -exports.URLContext = URLContext; - -// Matches when the user-supplied predicate returns true -var PredicateContext = Class({ - extends: Context, - - initialize: function initialize(predicate) { - Context.prototype.initialize.call(this); - let options = validateOptions({ predicate: predicate }, { - predicate: { - is: ["function"], - msg: "predicate must be a function." - } - }); - internal(this).predicate = options.predicate; - }, - - isCurrent: function isCurrent(state) { - return internal(this).predicate(state); - }, - - serialize: function() { - return { - id: internal(this).id, - type: "PredicateContext", - args: [] - } - } -}); -exports.PredicateContext = PredicateContext; - -function removeItemFromArray(array, item) { - return array.filter(i => i !== item); -} - -// Converts anything that isn't false, null or undefined into a string -function stringOrNull(val) { - return val ? String(val) : val; -} - -// Shared option validation rules for Item, Menu, and Separator -var baseItemRules = { - parentMenu: { - is: ["object", "undefined"], - ok: function (v) { - if (!v) - return true; - return (v instanceof ItemContainer) || (v instanceof Menu); - }, - msg: "parentMenu must be a Menu or not specified." - }, - context: { - is: ["undefined", "object", "array"], - ok: function (v) { - if (!v) - return true; - let arr = Array.isArray(v) ? v : [v]; - return arr.every(o => o instanceof Context); - }, - msg: "The 'context' option must be a Context object or an array of " + - "Context objects." - }, - onMessage: { - is: ["function", "undefined"] - }, - contentScript: loaderContract.rules.contentScript, - contentScriptFile: loaderContract.rules.contentScriptFile -}; - -var labelledItemRules = mix(baseItemRules, { - label: { - map: stringOrNull, - is: ["string"], - ok: v => !!v, - msg: "The item must have a non-empty string label." - }, - accesskey: { - map: stringOrNull, - is: ["string", "undefined", "null"], - ok: (v) => { - if (!v) { - return true; - } - return typeof v == "string" && v.length === 1; - }, - msg: "The item must have a single character accesskey, or no accesskey." - }, - image: { - map: stringOrNull, - is: ["string", "undefined", "null"], - ok: function (url) { - if (!url) - return true; - return isValidURI(url); - }, - msg: "Image URL validation failed" - } -}); - -// Additional validation rules for Item -var itemRules = mix(labelledItemRules, { - data: { - map: stringOrNull, - is: ["string", "undefined", "null"] - } -}); - -// Additional validation rules for Menu -var menuRules = mix(labelledItemRules, { - items: { - is: ["array", "undefined"], - ok: function (v) { - if (!v) - return true; - return v.every(function (item) { - return item instanceof BaseItem; - }); - }, - msg: "items must be an array, and each element in the array must be an " + - "Item, Menu, or Separator." - } -}); - -// Returns true if any contexts match. If there are no contexts then a -// PageContext is tested instead -function hasMatchingContext(contexts, addonInfo) { - for (let context of contexts) { - if (!(internal(context).id in addonInfo.contextStates)) { - console.error("Missing state for context " + internal(context).id + " this is an error in the SDK modules."); - return false; - } - if (!context.isCurrent(addonInfo.contextStates[internal(context).id])) - return false; - } - - return true; -} - -// Tests whether an item should be visible or not based on its contexts and -// content scripts -function isItemVisible(item, addonInfo, usePageWorker) { - if (!item.context.length) { - if (!addonInfo.hasWorker) - return usePageWorker ? addonInfo.pageContext : true; - } - - if (!hasMatchingContext(item.context, addonInfo)) - return false; - - let context = addonInfo.workerContext; - if (typeof(context) === "string" && context != "") - item.label = context; - - return !!context; -} - -// Called when an item is clicked to send out click events to the content -// scripts -function itemActivated(item, clickedNode) { - let items = [internal(item).id]; - let data = item.data; - - while (item.parentMenu) { - item = item.parentMenu; - items.push(internal(item).id); - } - - let process = processes.getById(lastContextProcessId); - if (process) - process.port.emit('sdk/contextmenu/activateitems', items, data); -} - -function serializeItem(item) { - return { - id: internal(item).id, - contexts: item.context.map(c => c.serialize()), - contentScript: item.contentScript, - contentScriptFile: item.contentScriptFile, - }; -} - -// All things that appear in the context menu extend this -var BaseItem = Class({ - initialize: function initialize() { - internal(this).id = uuid(); - - internal(this).contexts = []; - if ("context" in internal(this).options && internal(this).options.context) { - let contexts = internal(this).options.context; - if (Array.isArray(contexts)) { - for (let context of contexts) - internal(this).contexts.push(context); - } - else { - internal(this).contexts.push(contexts); - } - } - - let parentMenu = internal(this).options.parentMenu; - if (!parentMenu) - parentMenu = contentContextMenu; - - parentMenu.addItem(this); - - Object.defineProperty(this, "contentScript", { - enumerable: true, - value: internal(this).options.contentScript - }); - - // Resolve URIs here as tests may have overriden self - let files = internal(this).options.contentScriptFile; - if (files) { - if (!Array.isArray(files)) - files = [files]; - files = files.map(self.data.url); - } - internal(this).options.contentScriptFile = files; - Object.defineProperty(this, "contentScriptFile", { - enumerable: true, - value: internal(this).options.contentScriptFile - }); - - // Notify all frames of this new item - sendItems([serializeItem(this)]); - }, - - destroy: function destroy() { - if (internal(this).destroyed) - return; - - // Tell all existing frames that this item has been destroyed - processes.port.emit("sdk/contextmenu/destroyitems", [internal(this).id]); - - if (this.parentMenu) - this.parentMenu.removeItem(this); - - internal(this).destroyed = true; - }, - - get context() { - let contexts = internal(this).contexts.slice(0); - contexts.add = (context) => { - internal(this).contexts.push(context); - // Notify all frames that this item has changed - sendItems([serializeItem(this)]); - }; - contexts.remove = (context) => { - internal(this).contexts = internal(this).contexts.filter(c => { - return c != context; - }); - // Notify all frames that this item has changed - sendItems([serializeItem(this)]); - }; - return contexts; - }, - - set context(val) { - internal(this).contexts = val.slice(0); - // Notify all frames that this item has changed - sendItems([serializeItem(this)]); - }, - - get parentMenu() { - return internal(this).parentMenu; - }, -}); - -function workerMessageReceived(process, id, args) { - if (internal(this).id != id) - return; - - emit(this, ...JSON.parse(args)); -} - -// All things that have a label on the context menu extend this -var LabelledItem = Class({ - extends: BaseItem, - implements: [ EventTarget ], - - initialize: function initialize(options) { - BaseItem.prototype.initialize.call(this); - EventTarget.prototype.initialize.call(this, options); - - internal(this).messageListener = workerMessageReceived.bind(this); - processes.port.on('sdk/worker/event', internal(this).messageListener); - }, - - destroy: function destroy() { - if (internal(this).destroyed) - return; - - processes.port.off('sdk/worker/event', internal(this).messageListener); - - BaseItem.prototype.destroy.call(this); - }, - - get label() { - return internal(this).options.label; - }, - - set label(val) { - internal(this).options.label = val; - - MenuManager.updateItem(this); - }, - - get accesskey() { - return internal(this).options.accesskey; - }, - - set accesskey(val) { - internal(this).options.accesskey = val; - - MenuManager.updateItem(this); - }, - - get image() { - return internal(this).options.image; - }, - - set image(val) { - internal(this).options.image = val; - - MenuManager.updateItem(this); - }, - - get data() { - return internal(this).options.data; - }, - - set data(val) { - internal(this).options.data = val; - } -}); - -var Item = Class({ - extends: LabelledItem, - - initialize: function initialize(options) { - internal(this).options = validateOptions(options, itemRules); - - LabelledItem.prototype.initialize.call(this, options); - }, - - toString: function toString() { - return "[object Item \"" + this.label + "\"]"; - }, - - get data() { - return internal(this).options.data; - }, - - set data(val) { - internal(this).options.data = val; - - MenuManager.updateItem(this); - }, -}); -exports.Item = Item; - -var ItemContainer = Class({ - initialize: function initialize() { - internal(this).children = []; - }, - - destroy: function destroy() { - // Destroys the entire hierarchy - for (let item of internal(this).children) - item.destroy(); - }, - - addItem: function addItem(item) { - let oldParent = item.parentMenu; - - // Don't just call removeItem here as that would remove the corresponding - // UI element which is more costly than just moving it to the right place - if (oldParent) - internal(oldParent).children = removeItemFromArray(internal(oldParent).children, item); - - let after = null; - let children = internal(this).children; - if (children.length > 0) - after = children[children.length - 1]; - - children.push(item); - internal(item).parentMenu = this; - - // If there was an old parent then we just have to move the item, otherwise - // it needs to be created - if (oldParent) - MenuManager.moveItem(item, after); - else - MenuManager.createItem(item, after); - }, - - removeItem: function removeItem(item) { - // If the item isn't a child of this menu then ignore this call - if (item.parentMenu !== this) - return; - - MenuManager.removeItem(item); - - internal(this).children = removeItemFromArray(internal(this).children, item); - internal(item).parentMenu = null; - }, - - get items() { - return internal(this).children.slice(0); - }, - - set items(val) { - // Validate the arguments before making any changes - if (!Array.isArray(val)) - throw new Error(menuOptionRules.items.msg); - - for (let item of val) { - if (!(item instanceof BaseItem)) - throw new Error(menuOptionRules.items.msg); - } - - // Remove the old items and add the new ones - for (let item of internal(this).children) - this.removeItem(item); - - for (let item of val) - this.addItem(item); - }, -}); - -var Menu = Class({ - extends: LabelledItem, - implements: [ItemContainer], - - initialize: function initialize(options) { - internal(this).options = validateOptions(options, menuRules); - - LabelledItem.prototype.initialize.call(this, options); - ItemContainer.prototype.initialize.call(this); - - if (internal(this).options.items) { - for (let item of internal(this).options.items) - this.addItem(item); - } - }, - - destroy: function destroy() { - ItemContainer.prototype.destroy.call(this); - LabelledItem.prototype.destroy.call(this); - }, - - toString: function toString() { - return "[object Menu \"" + this.label + "\"]"; - }, -}); -exports.Menu = Menu; - -var Separator = Class({ - extends: BaseItem, - - initialize: function initialize(options) { - internal(this).options = validateOptions(options, baseItemRules); - - BaseItem.prototype.initialize.call(this); - }, - - toString: function toString() { - return "[object Separator]"; - } -}); -exports.Separator = Separator; - -// Holds items for the content area context menu -var contentContextMenu = ItemContainer(); -exports.contentContextMenu = contentContextMenu; - -function getContainerItems(container) { - let items = []; - for (let item of internal(container).children) { - items.push(serializeItem(item)); - if (item instanceof Menu) - items = items.concat(getContainerItems(item)); - } - return items; -} - -// Notify all frames of these new or changed items -function sendItems(items) { - processes.port.emit("sdk/contextmenu/createitems", items); -} - -// Called when a new process is created and needs to get the current list of items -function remoteItemRequest(process) { - let items = getContainerItems(contentContextMenu); - if (items.length == 0) - return; - - process.port.emit("sdk/contextmenu/createitems", items); -} -processes.forEvery(remoteItemRequest); - -when(function() { - contentContextMenu.destroy(); -}); - -// App specific UI code lives here, it should handle populating the context -// menu and passing clicks etc. through to the items. - -function countVisibleItems(nodes) { - return Array.reduce(nodes, function(sum, node) { - return node.hidden ? sum : sum + 1; - }, 0); -} - -var MenuWrapper = Class({ - initialize: function initialize(winWrapper, items, contextMenu) { - this.winWrapper = winWrapper; - this.window = winWrapper.window; - this.items = items; - this.contextMenu = contextMenu; - this.populated = false; - this.menuMap = new Map(); - - // updateItemVisibilities will run first, updateOverflowState will run after - // all other instances of this module have run updateItemVisibilities - this._updateItemVisibilities = this.updateItemVisibilities.bind(this); - this.contextMenu.addEventListener("popupshowing", this._updateItemVisibilities, true); - this._updateOverflowState = this.updateOverflowState.bind(this); - this.contextMenu.addEventListener("popupshowing", this._updateOverflowState); - }, - - destroy: function destroy() { - this.contextMenu.removeEventListener("popupshowing", this._updateOverflowState); - this.contextMenu.removeEventListener("popupshowing", this._updateItemVisibilities, true); - - if (!this.populated) - return; - - // If we're getting unloaded at runtime then we must remove all the - // generated XUL nodes - let oldParent = null; - for (let item of internal(this.items).children) { - let xulNode = this.getXULNodeForItem(item); - oldParent = xulNode.parentNode; - oldParent.removeChild(xulNode); - } - - if (oldParent) - this.onXULRemoved(oldParent); - }, - - get separator() { - return this.contextMenu.querySelector("." + SEPARATOR_CLASS); - }, - - get overflowMenu() { - return this.contextMenu.querySelector("." + OVERFLOW_MENU_CLASS); - }, - - get overflowPopup() { - return this.contextMenu.querySelector("." + OVERFLOW_POPUP_CLASS); - }, - - get topLevelItems() { - return this.contextMenu.querySelectorAll("." + TOPLEVEL_ITEM_CLASS); - }, - - get overflowItems() { - return this.contextMenu.querySelectorAll("." + OVERFLOW_ITEM_CLASS); - }, - - getXULNodeForItem: function getXULNodeForItem(item) { - return this.menuMap.get(item); - }, - - // Recurses through the item hierarchy creating XUL nodes for everything - populate: function populate(menu) { - for (let i = 0; i < internal(menu).children.length; i++) { - let item = internal(menu).children[i]; - let after = i === 0 ? null : internal(menu).children[i - 1]; - this.createItem(item, after); - - if (item instanceof Menu) - this.populate(item); - } - }, - - // Recurses through the menu setting the visibility of items. Returns true - // if any of the items in this menu were visible - setVisibility: function setVisibility(menu, addonInfo, usePageWorker) { - let anyVisible = false; - - for (let item of internal(menu).children) { - let visible = isItemVisible(item, addonInfo[internal(item).id], usePageWorker); - - // Recurse through Menus, if none of the sub-items were visible then the - // menu is hidden too. - if (visible && (item instanceof Menu)) - visible = this.setVisibility(item, addonInfo, false); - - let xulNode = this.getXULNodeForItem(item); - xulNode.hidden = !visible; - - anyVisible = anyVisible || visible; - } - - return anyVisible; - }, - - // Works out where to insert a XUL node for an item in a browser window - insertIntoXUL: function insertIntoXUL(item, node, after) { - let menupopup = null; - let before = null; - - let menu = item.parentMenu; - if (menu === this.items) { - // Insert into the overflow popup if it exists, otherwise the normal - // context menu - menupopup = this.overflowPopup; - if (!menupopup) - menupopup = this.contextMenu; - } - else { - let xulNode = this.getXULNodeForItem(menu); - menupopup = xulNode.firstChild; - } - - if (after) { - let afterNode = this.getXULNodeForItem(after); - before = afterNode.nextSibling; - } - else if (menupopup === this.contextMenu) { - let topLevel = this.topLevelItems; - if (topLevel.length > 0) - before = topLevel[topLevel.length - 1].nextSibling; - else - before = this.separator.nextSibling; - } - - menupopup.insertBefore(node, before); - }, - - // Sets the right class for XUL nodes - updateXULClass: function updateXULClass(xulNode) { - if (xulNode.parentNode == this.contextMenu) - xulNode.classList.add(TOPLEVEL_ITEM_CLASS); - else - xulNode.classList.remove(TOPLEVEL_ITEM_CLASS); - - if (xulNode.parentNode == this.overflowPopup) - xulNode.classList.add(OVERFLOW_ITEM_CLASS); - else - xulNode.classList.remove(OVERFLOW_ITEM_CLASS); - }, - - // Creates a XUL node for an item - createItem: function createItem(item, after) { - if (!this.populated) - return; - - // Create the separator if it doesn't already exist - if (!this.separator) { - let separator = this.window.document.createElement("menuseparator"); - separator.setAttribute("class", SEPARATOR_CLASS); - - // Insert before the separator created by the old context-menu if it - // exists to avoid bug 832401 - let oldSeparator = this.window.document.getElementById("jetpack-context-menu-separator"); - if (oldSeparator && oldSeparator.parentNode != this.contextMenu) - oldSeparator = null; - this.contextMenu.insertBefore(separator, oldSeparator); - } - - let type = "menuitem"; - if (item instanceof Menu) - type = "menu"; - else if (item instanceof Separator) - type = "menuseparator"; - - let xulNode = this.window.document.createElement(type); - xulNode.setAttribute("class", ITEM_CLASS); - if (item instanceof LabelledItem) { - xulNode.setAttribute("label", item.label); - if (item.accesskey) - xulNode.setAttribute("accesskey", item.accesskey); - if (item.image) { - xulNode.setAttribute("image", item.image); - if (item instanceof Menu) - xulNode.classList.add("menu-iconic"); - else - xulNode.classList.add("menuitem-iconic"); - } - if (item.data) - xulNode.setAttribute("value", item.data); - - let self = this; - xulNode.addEventListener("command", function(event) { - // Only care about clicks directly on this item - if (event.target !== xulNode) - return; - - itemActivated(item, xulNode); - }); - } - - this.insertIntoXUL(item, xulNode, after); - this.updateXULClass(xulNode); - xulNode.data = item.data; - - if (item instanceof Menu) { - let menupopup = this.window.document.createElement("menupopup"); - xulNode.appendChild(menupopup); - } - - this.menuMap.set(item, xulNode); - }, - - // Updates the XUL node for an item in this window - updateItem: function updateItem(item) { - if (!this.populated) - return; - - let xulNode = this.getXULNodeForItem(item); - - // TODO figure out why this requires setAttribute - xulNode.setAttribute("label", item.label); - xulNode.setAttribute("accesskey", item.accesskey || ""); - - if (item.image) { - xulNode.setAttribute("image", item.image); - if (item instanceof Menu) - xulNode.classList.add("menu-iconic"); - else - xulNode.classList.add("menuitem-iconic"); - } - else { - xulNode.removeAttribute("image"); - xulNode.classList.remove("menu-iconic"); - xulNode.classList.remove("menuitem-iconic"); - } - - if (item.data) - xulNode.setAttribute("value", item.data); - else - xulNode.removeAttribute("value"); - }, - - // Moves the XUL node for an item in this window to its new place in the - // hierarchy - moveItem: function moveItem(item, after) { - if (!this.populated) - return; - - let xulNode = this.getXULNodeForItem(item); - let oldParent = xulNode.parentNode; - - this.insertIntoXUL(item, xulNode, after); - this.updateXULClass(xulNode); - this.onXULRemoved(oldParent); - }, - - // Removes the XUL nodes for an item in every window we've ever populated. - removeItem: function removeItem(item) { - if (!this.populated) - return; - - let xulItem = this.getXULNodeForItem(item); - - let oldParent = xulItem.parentNode; - - oldParent.removeChild(xulItem); - this.menuMap.delete(item); - - this.onXULRemoved(oldParent); - }, - - // Called when any XUL nodes have been removed from a menupopup. This handles - // making sure the separator and overflow are correct - onXULRemoved: function onXULRemoved(parent) { - if (parent == this.contextMenu) { - let toplevel = this.topLevelItems; - - // If there are no more items then remove the separator - if (toplevel.length == 0) { - let separator = this.separator; - if (separator) - separator.remove(); - } - } - else if (parent == this.overflowPopup) { - // If there are no more items then remove the overflow menu and separator - if (parent.childNodes.length == 0) { - let separator = this.separator; - separator.remove(); - this.contextMenu.removeChild(parent.parentNode); - } - } - }, - - // Recurses through all the items owned by this module and sets their hidden - // state - updateItemVisibilities: function updateItemVisibilities(event) { - try { - if (event.type != "popupshowing") - return; - if (event.target != this.contextMenu) - return; - - if (internal(this.items).children.length == 0) - return; - - if (!this.populated) { - this.populated = true; - this.populate(this.items); - } - - let mainWindow = event.target.ownerGlobal; - this.contextMenuContentData = mainWindow.gContextMenuContentData - if (!(self.id in this.contextMenuContentData.addonInfo)) { - console.warn("No context menu state data was provided."); - return; - } - let addonInfo = this.contextMenuContentData.addonInfo[self.id]; - lastContextProcessId = addonInfo.processID; - this.setVisibility(this.items, addonInfo.items, true); - } - catch (e) { - console.exception(e); - } - }, - - // Counts the number of visible items across all modules and makes sure they - // are in the right place between the top level context menu and the overflow - // menu - updateOverflowState: function updateOverflowState(event) { - try { - if (event.type != "popupshowing") - return; - if (event.target != this.contextMenu) - return; - - // The main items will be in either the top level context menu or the - // overflow menu at this point. Count the visible ones and if they are in - // the wrong place move them - let toplevel = this.topLevelItems; - let overflow = this.overflowItems; - let visibleCount = countVisibleItems(toplevel) + - countVisibleItems(overflow); - - if (visibleCount == 0) { - let separator = this.separator; - if (separator) - separator.hidden = true; - let overflowMenu = this.overflowMenu; - if (overflowMenu) - overflowMenu.hidden = true; - } - else if (visibleCount > MenuManager.overflowThreshold) { - this.separator.hidden = false; - let overflowPopup = this.overflowPopup; - if (overflowPopup) - overflowPopup.parentNode.hidden = false; - - if (toplevel.length > 0) { - // The overflow menu shouldn't exist here but let's play it safe - if (!overflowPopup) { - let overflowMenu = this.window.document.createElement("menu"); - overflowMenu.setAttribute("class", OVERFLOW_MENU_CLASS); - overflowMenu.setAttribute("label", OVERFLOW_MENU_LABEL); - overflowMenu.setAttribute("accesskey", OVERFLOW_MENU_ACCESSKEY); - this.contextMenu.insertBefore(overflowMenu, this.separator.nextSibling); - - overflowPopup = this.window.document.createElement("menupopup"); - overflowPopup.setAttribute("class", OVERFLOW_POPUP_CLASS); - overflowMenu.appendChild(overflowPopup); - } - - for (let xulNode of toplevel) { - overflowPopup.appendChild(xulNode); - this.updateXULClass(xulNode); - } - } - } - else { - this.separator.hidden = false; - - if (overflow.length > 0) { - // Move all the overflow nodes out of the overflow menu and position - // them immediately before it - for (let xulNode of overflow) { - this.contextMenu.insertBefore(xulNode, xulNode.parentNode.parentNode); - this.updateXULClass(xulNode); - } - this.contextMenu.removeChild(this.overflowMenu); - } - } - } - catch (e) { - console.exception(e); - } - } -}); - -// This wraps every window that we've seen -var WindowWrapper = Class({ - initialize: function initialize(window) { - this.window = window; - this.menus = [ - new MenuWrapper(this, contentContextMenu, window.document.getElementById("contentAreaContextMenu")), - ]; - }, - - destroy: function destroy() { - for (let menuWrapper of this.menus) - menuWrapper.destroy(); - }, - - getMenuWrapperForItem: function getMenuWrapperForItem(item) { - let root = item.parentMenu; - while (root.parentMenu) - root = root.parentMenu; - - for (let wrapper of this.menus) { - if (wrapper.items === root) - return wrapper; - } - - return null; - } -}); - -var MenuManager = { - windowMap: new Map(), - - get overflowThreshold() { - let prefs = require("./preferences/service"); - return prefs.get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); - }, - - // When a new window is added start watching it for context menu shows - onTrack: function onTrack(window) { - if (!isBrowser(window)) - return; - - // Generally shouldn't happen, but just in case - if (this.windowMap.has(window)) { - console.warn("Already seen this window"); - return; - } - - let winWrapper = WindowWrapper(window); - this.windowMap.set(window, winWrapper); - }, - - onUntrack: function onUntrack(window) { - if (!isBrowser(window)) - return; - - let winWrapper = this.windowMap.get(window); - // This shouldn't happen but protect against it anyway - if (!winWrapper) - return; - winWrapper.destroy(); - - this.windowMap.delete(window); - }, - - // Creates a XUL node for an item in every window we've already populated - createItem: function createItem(item, after) { - for (let [window, winWrapper] of this.windowMap) { - let menuWrapper = winWrapper.getMenuWrapperForItem(item); - if (menuWrapper) - menuWrapper.createItem(item, after); - } - }, - - // Updates the XUL node for an item in every window we've already populated - updateItem: function updateItem(item) { - for (let [window, winWrapper] of this.windowMap) { - let menuWrapper = winWrapper.getMenuWrapperForItem(item); - if (menuWrapper) - menuWrapper.updateItem(item); - } - }, - - // Moves the XUL node for an item in every window we've ever populated to its - // new place in the hierarchy - moveItem: function moveItem(item, after) { - for (let [window, winWrapper] of this.windowMap) { - let menuWrapper = winWrapper.getMenuWrapperForItem(item); - if (menuWrapper) - menuWrapper.moveItem(item, after); - } - }, - - // Removes the XUL nodes for an item in every window we've ever populated. - removeItem: function removeItem(item) { - for (let [window, winWrapper] of this.windowMap) { - let menuWrapper = winWrapper.getMenuWrapperForItem(item); - if (menuWrapper) - menuWrapper.removeItem(item); - } - } -}; - -WindowTracker(MenuManager); diff --git a/addon-sdk/source/lib/sdk/context-menu/context.js b/addon-sdk/source/lib/sdk/context-menu/context.js deleted file mode 100644 index 32674dfc75ee..000000000000 --- a/addon-sdk/source/lib/sdk/context-menu/context.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/context-menu/core.js b/addon-sdk/source/lib/sdk/context-menu/core.js deleted file mode 100644 index 2ac1e11f6a84..000000000000 --- a/addon-sdk/source/lib/sdk/context-menu/core.js +++ /dev/null @@ -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); diff --git a/addon-sdk/source/lib/sdk/context-menu/readers.js b/addon-sdk/source/lib/sdk/context-menu/readers.js deleted file mode 100644 index 5078f8f29d21..000000000000 --- a/addon-sdk/source/lib/sdk/context-menu/readers.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/context-menu@2.js b/addon-sdk/source/lib/sdk/context-menu@2.js deleted file mode 100644 index 45ad804e9bef..000000000000 --- a/addon-sdk/source/lib/sdk/context-menu@2.js +++ /dev/null @@ -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); diff --git a/addon-sdk/source/lib/sdk/input/browser.js b/addon-sdk/source/lib/sdk/input/browser.js deleted file mode 100644 index daea875bffbc..000000000000 --- a/addon-sdk/source/lib/sdk/input/browser.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/input/customizable-ui.js b/addon-sdk/source/lib/sdk/input/customizable-ui.js deleted file mode 100644 index a41d0971a5e6..000000000000 --- a/addon-sdk/source/lib/sdk/input/customizable-ui.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/input/frame.js b/addon-sdk/source/lib/sdk/input/frame.js deleted file mode 100644 index 50efaa745bce..000000000000 --- a/addon-sdk/source/lib/sdk/input/frame.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/input/system.js b/addon-sdk/source/lib/sdk/input/system.js deleted file mode 100644 index 66bc6daecadc..000000000000 --- a/addon-sdk/source/lib/sdk/input/system.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/panel.js b/addon-sdk/source/lib/sdk/panel.js deleted file mode 100644 index 7c3f1d2f0333..000000000000 --- a/addon-sdk/source/lib/sdk/panel.js +++ /dev/null @@ -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); -}); diff --git a/addon-sdk/source/lib/sdk/panel/events.js b/addon-sdk/source/lib/sdk/panel/events.js deleted file mode 100644 index 2f14ba22f73b..000000000000 --- a/addon-sdk/source/lib/sdk/panel/events.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/panel/utils.js b/addon-sdk/source/lib/sdk/panel/utils.js deleted file mode 100644 index 41daeef74710..000000000000 --- a/addon-sdk/source/lib/sdk/panel/utils.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/ui.js b/addon-sdk/source/lib/sdk/ui.js deleted file mode 100644 index 4eba63e9ca36..000000000000 --- a/addon-sdk/source/lib/sdk/ui.js +++ /dev/null @@ -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; }, -}); diff --git a/addon-sdk/source/lib/sdk/ui/button/action.js b/addon-sdk/source/lib/sdk/ui/button/action.js deleted file mode 100644 index 2e6d2ca8214b..000000000000 --- a/addon-sdk/source/lib/sdk/ui/button/action.js +++ /dev/null @@ -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); -}); diff --git a/addon-sdk/source/lib/sdk/ui/button/contract.js b/addon-sdk/source/lib/sdk/ui/button/contract.js deleted file mode 100644 index 2edc27f58b4a..000000000000 --- a/addon-sdk/source/lib/sdk/ui/button/contract.js +++ /dev/null @@ -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)); - diff --git a/addon-sdk/source/lib/sdk/ui/button/toggle.js b/addon-sdk/source/lib/sdk/ui/button/toggle.js deleted file mode 100644 index 0afff86fadea..000000000000 --- a/addon-sdk/source/lib/sdk/ui/button/toggle.js +++ /dev/null @@ -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); -}); diff --git a/addon-sdk/source/lib/sdk/ui/button/view.js b/addon-sdk/source/lib/sdk/ui/button/view.js deleted file mode 100644 index 26aefc3cc695..000000000000 --- a/addon-sdk/source/lib/sdk/ui/button/view.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/ui/button/view/events.js b/addon-sdk/source/lib/sdk/ui/button/view/events.js deleted file mode 100644 index 98909656a69a..000000000000 --- a/addon-sdk/source/lib/sdk/ui/button/view/events.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/ui/component.js b/addon-sdk/source/lib/sdk/ui/component.js deleted file mode 100644 index d1f12c95ec97..000000000000 --- a/addon-sdk/source/lib/sdk/ui/component.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/ui/frame.js b/addon-sdk/source/lib/sdk/ui/frame.js deleted file mode 100644 index 693e1d221b99..000000000000 --- a/addon-sdk/source/lib/sdk/ui/frame.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/ui/frame/model.js b/addon-sdk/source/lib/sdk/ui/frame/model.js deleted file mode 100644 index 688f4cfb8ca4..000000000000 --- a/addon-sdk/source/lib/sdk/ui/frame/model.js +++ /dev/null @@ -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); diff --git a/addon-sdk/source/lib/sdk/ui/frame/view.html b/addon-sdk/source/lib/sdk/ui/frame/view.html deleted file mode 100644 index 2a405b583e82..000000000000 --- a/addon-sdk/source/lib/sdk/ui/frame/view.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/addon-sdk/source/lib/sdk/ui/frame/view.js b/addon-sdk/source/lib/sdk/ui/frame/view.js deleted file mode 100644 index a9152d5e949a..000000000000 --- a/addon-sdk/source/lib/sdk/ui/frame/view.js +++ /dev/null @@ -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); diff --git a/addon-sdk/source/lib/sdk/ui/id.js b/addon-sdk/source/lib/sdk/ui/id.js deleted file mode 100644 index 36854fc271a6..000000000000 --- a/addon-sdk/source/lib/sdk/ui/id.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/ui/sidebar.js b/addon-sdk/source/lib/sdk/ui/sidebar.js deleted file mode 100644 index e30e2ae50e3a..000000000000 --- a/addon-sdk/source/lib/sdk/ui/sidebar.js +++ /dev/null @@ -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)); diff --git a/addon-sdk/source/lib/sdk/ui/sidebar/actions.js b/addon-sdk/source/lib/sdk/ui/sidebar/actions.js deleted file mode 100644 index 4a52984c92de..000000000000 --- a/addon-sdk/source/lib/sdk/ui/sidebar/actions.js +++ /dev/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'); diff --git a/addon-sdk/source/lib/sdk/ui/sidebar/contract.js b/addon-sdk/source/lib/sdk/ui/sidebar/contract.js deleted file mode 100644 index b59c37c0b182..000000000000 --- a/addon-sdk/source/lib/sdk/ui/sidebar/contract.js +++ /dev/null @@ -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.' - } -}); diff --git a/addon-sdk/source/lib/sdk/ui/sidebar/namespace.js b/addon-sdk/source/lib/sdk/ui/sidebar/namespace.js deleted file mode 100644 index d79725d1a44b..000000000000 --- a/addon-sdk/source/lib/sdk/ui/sidebar/namespace.js +++ /dev/null @@ -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); -}; diff --git a/addon-sdk/source/lib/sdk/ui/sidebar/utils.js b/addon-sdk/source/lib/sdk/ui/sidebar/utils.js deleted file mode 100644 index d6145c32e322..000000000000 --- a/addon-sdk/source/lib/sdk/ui/sidebar/utils.js +++ /dev/null @@ -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'); diff --git a/addon-sdk/source/lib/sdk/ui/sidebar/view.js b/addon-sdk/source/lib/sdk/ui/sidebar/view.js deleted file mode 100644 index 8f28b9aac3d2..000000000000 --- a/addon-sdk/source/lib/sdk/ui/sidebar/view.js +++ /dev/null @@ -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; -} diff --git a/addon-sdk/source/lib/sdk/ui/state.js b/addon-sdk/source/lib/sdk/ui/state.js deleted file mode 100644 index 3da71bd44c18..000000000000 --- a/addon-sdk/source/lib/sdk/ui/state.js +++ /dev/null @@ -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); - } -}); diff --git a/addon-sdk/source/lib/sdk/ui/state/events.js b/addon-sdk/source/lib/sdk/ui/state/events.js deleted file mode 100644 index 98909656a69a..000000000000 --- a/addon-sdk/source/lib/sdk/ui/state/events.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/ui/toolbar.js b/addon-sdk/source/lib/sdk/ui/toolbar.js deleted file mode 100644 index c1becab2d027..000000000000 --- a/addon-sdk/source/lib/sdk/ui/toolbar.js +++ /dev/null @@ -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; diff --git a/addon-sdk/source/lib/sdk/ui/toolbar/model.js b/addon-sdk/source/lib/sdk/ui/toolbar/model.js deleted file mode 100644 index 5c5428606db3..000000000000 --- a/addon-sdk/source/lib/sdk/ui/toolbar/model.js +++ /dev/null @@ -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); diff --git a/addon-sdk/source/lib/sdk/ui/toolbar/view.js b/addon-sdk/source/lib/sdk/ui/toolbar/view.js deleted file mode 100644 index e8e6a1138d4e..000000000000 --- a/addon-sdk/source/lib/sdk/ui/toolbar/view.js +++ /dev/null @@ -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);