summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/ui/frame
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/jetpack/sdk/ui/frame')
-rw-r--r--toolkit/jetpack/sdk/ui/frame/model.js154
-rw-r--r--toolkit/jetpack/sdk/ui/frame/view.html18
-rw-r--r--toolkit/jetpack/sdk/ui/frame/view.js150
3 files changed, 322 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/ui/frame/model.js b/toolkit/jetpack/sdk/ui/frame/model.js
new file mode 100644
index 000000000..627310874
--- /dev/null
+++ b/toolkit/jetpack/sdk/ui/frame/model.js
@@ -0,0 +1,154 @@
+/* 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 { emit, off, setListeners } = require("../../event/core");
+const { Reactor, foldp, send, merges } = require("../../event/utils");
+const { Disposable } = require("../../core/disposable");
+const { OutputPort } = require("../../output/system");
+const { InputPort } = require("../../input/system");
+const { identify } = require("../id");
+const { pairs, object, map, each } = require("../../util/sequence");
+const { patch, diff } = require("diffpatcher/index");
+const { isLocalURL } = require("../../url");
+const { compose } = require("../../lang/functional");
+const { contract } = require("../../util/contract");
+const { id: addonID, data: { url: resolve }} = require("../../self");
+const { Frames } = require("../../input/frame");
+
+
+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/toolkit/jetpack/sdk/ui/frame/view.html b/toolkit/jetpack/sdk/ui/frame/view.html
new file mode 100644
index 000000000..2a405b583
--- /dev/null
+++ b/toolkit/jetpack/sdk/ui/frame/view.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ // HACK: This is not an ideal way to deliver chrome messages
+ // to an inner frame content but seems only way that would
+ // make `event.source` this (outer frame) window.
+ window.onmessage = function(event) {
+ var frame = document.querySelector("iframe");
+ var content = frame.contentWindow;
+ // If message is posted from chrome it has no `event.source`.
+ if (event.source === null)
+ content.postMessage(event.data, "*");
+ };
+ </script>
+ </head>
+ <body style="overflow: hidden"></body>
+</html>
diff --git a/toolkit/jetpack/sdk/ui/frame/view.js b/toolkit/jetpack/sdk/ui/frame/view.js
new file mode 100644
index 000000000..2eb4df2b7
--- /dev/null
+++ b/toolkit/jetpack/sdk/ui/frame/view.js
@@ -0,0 +1,150 @@
+/* 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 { subscribe, send, Reactor, foldp, lift, merges, keepIf } = require("../../event/utils");
+const { InputPort } = require("../../input/system");
+const { OutputPort } = require("../../output/system");
+const { LastClosed } = require("../../input/browser");
+const { pairs, keys, object, each } = require("../../util/sequence");
+const { curry, compose } = require("../../lang/functional");
+const { getFrameElement, getOuterId,
+ getByOuterId, getOwnerBrowserWindow } = require("../../window/utils");
+const { patch, diff } = require("diffpatcher/index");
+const { encode } = require("../../base64");
+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 onload() {
+ outerFrame.removeEventListener("load", onload, true);
+
+ 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);
+ }, 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);