providers/core/core.view.js
/*globals document */
/*jslint indent:2,sloppy:true,node:true */
var util = require('../../src/util');
var PromiseCompat = require('es6-promise').Promise;
/**
* A freedom.js view is the interface for user interaction.
* A view exists as an iFrame, which is shown to the user in some way.
* communication between the view and the freedom.js module is performed
* through the HTML5 postMessage mechanism, which this provider translates
* to freedom.js message events.
* @Class View_unprivileged
* @constructor
* @private
* @param {View Provider} provider
* @param {provider:Provider,module:Module} cap The instantiator of the view.
* @param {Function} dispatchEvent Function to call to emit events.
*/
var Core_View = function (provider, cap, dispatchEvent) {
this.provider = provider;
this.dispatchEvent = dispatchEvent;
setTimeout(cap.provider.onClose.bind(
cap.provider,
this,
this.close.bind(this, function () {})
), 0);
this.module = cap.module;
util.handleEvents(this);
};
/**
* The is the default provider for core.view, unless overridden by context or
* a user supplied provider. The interface is documented at:
* https://github.com/freedomjs/freedom/wiki/freedom.js-Views
*
* Generally, a view provider consists of 3 methods:
* onOpen is called when a view should be shown.
* id - is a unique identifier for this view, used on subsequent calls
* for communication and to eventually close the view.
* name - is the name of the view (as defined in the manifest),
* in order to place it appropriately.
* page - is the resolved URL to open.
* resources - is an array of resolved URLs which are referenced.
* postMessage - is a function to call when messages are emitted
* by the window in which the view is opened.
* onOpen returns a promise that completes when the view is loaded.
* onMessage is called to send a message to an open view.
* id - is the unique identifier for the open view.
* message - is the message to postMessage to the view's window.
* onClose is called to close a view.
* id - is the unique identifier for the view.
*/
Core_View.provider = {
listener: undefined,
active: {},
onOpen: function (id, name, page, resources, postMessage) {
var container = document.body,
root,
frame;
if (!this.listener) {
this.listener = function (msg) {
var i;
for (i in this.active) {
if (this.active.hasOwnProperty(i) &&
this.active[i].source === msg.source) {
this.active[i].postMessage(msg.data);
}
}
}.bind(this);
window.addEventListener('message', this.listener, true);
}
// Views open by default in an element with their ID, or fill the page
// otherwise.
if (document.getElementById(name)) {
container = document.getElementById(name);
}
root = document.createElement("div");
root.style.width = "100%";
root.style.height = "100%";
root.style.display = "relative";
container.appendChild(root);
return new PromiseCompat(function (resolve, reject) {
frame = document.createElement("iframe");
frame.setAttribute("sandbox", "allow-scripts allow-forms");
frame.style.width = "100%";
frame.style.height = "100%";
frame.style.border = "0";
frame.style.background = "transparent";
frame.src = page;
frame.addEventListener('load', resolve, true);
frame.addEventListener('error', reject, true);
root.appendChild(frame);
this.active[id] = {
postMessage: postMessage,
container: container,
root: root,
source: frame.contentWindow
};
}.bind(this));
},
onMessage: function (id, message) {
this.active[id].source.postMessage(message, '*');
},
onClose: function (id) {
this.active[id].container.removeChild(this.active[id].root);
delete this.active[id];
if (Object.keys(this.active).length === 0) {
window.removeEventListener('message', this.listener, true);
this.listener = undefined;
}
}
};
/**
* Ask for this view to open a specific location, either a File relative to
* the loader, or an explicit code location.
* @method show
* @param {String} name The identifier of the view.
* @param {Function} continuation Function to call when view is loaded.
*/
Core_View.prototype.show = function (name, continuation) {
if (this.id) {
return continuation(undefined, {
errcode: 'ALREADY_OPEN',
message: 'Cannot show multiple views through one instance.'
});
}
this.id = util.getId();
var config = this.module.manifest.views,
toResolve = [];
if (!config || !config[name]) {
return continuation(undefined, {
errcode: 'NON_EXISTANT',
message: 'View not found: ' + name
});
}
if (config[name].main && config[name].files) {
toResolve = config[name].files.concat(config[name].main);
PromiseCompat.all(toResolve.map(function (fname) {
return this.module.resource.get(this.module.manifestId, fname);
}.bind(this))).then(function (files) {
this.provider.onOpen(this.id,
name,
files[files.length - 1],
files,
this.dispatchEvent.bind(this, 'message')).then(
function (c) {
// Make sure continuation is called without an argument.
c();
}.bind({}, continuation),
continuation.bind({}, undefined)
);
}.bind(this), function (err) {
this.module.debug.error('Unable to open view ' + name + ': ', err);
continuation(undefined, {
errcode: 'VIEW_MALFORMED',
message: 'Malformed View Declaration: ' + err
});
});
} else {
continuation(undefined, {
errcode: 'NON_EXISTANT',
message: 'View not found: ' + name
});
}
};
/**
* isSecure determines whether the module can have confidence that its
* communication with its view cannot be intercepted by an untrusted 3rd party.
* In practice, this means that its okay for the runtime to have access to the
* messages, and if the context is a web server or a browser extension then
* that context is trusted. However, if a provider wants to allow their e.g.
* social provider to be used on arbitrary websites, this mechanism means that
* if the website uses a trusted version of the freedom.js library, then the
* module can be used.
* @method isSecure
* @returns {Boolean} if the channel to the view is secure.
*/
Core_View.prototype.isSecure = function (continuation) {
continuation(false);
};
/**
* Send a message to an open view.
* @method postMessage
*/
Core_View.prototype.postMessage = function (msg, continuation) {
if (!this.id) {
return continuation(undefined, {
errcode: 'NOT_OPEN',
message: 'Cannot post message to uninitialized view.'
});
}
this.provider.onMessage(this.id, msg);
continuation();
};
/**
* Close an active view.
* @method close
*/
Core_View.prototype.close = function (continuation) {
if (!this.id) {
return continuation(undefined, {
errcode: 'NOT_OPEN',
message: 'Cannot close uninitialized view.'
});
}
this.provider.onClose(this.id);
delete this.id;
continuation();
};
/**
* Allow a web page to redefine behavior for how views are shown.
* @method register
* @static
* @param {Function} PageProvider The custom view behavior.
*/
Core_View.register = function (PageProvider) {
var provider = PageProvider ? new PageProvider() : Core_View.provider;
exports.provider = Core_View.bind(this, provider);
};
exports.provider = Core_View.bind(this, Core_View.provider);
exports.name = 'core.view';
exports.register = Core_View.register;
exports.flags = {provider: true, module: true};