diff options
Diffstat (limited to 'toolkit/jetpack/sdk/ui/frame')
-rw-r--r-- | toolkit/jetpack/sdk/ui/frame/model.js | 154 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/ui/frame/view.html | 18 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/ui/frame/view.js | 150 |
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); |