UNPKG

popostmate

Version:

A powerful, simple, promise-based postMessage library

501 lines (406 loc) 15 kB
/** popostmate - A powerful, simple, promise-based postMessage library @version v2.0.0 @link https://github.com/vpopolin/postmate @author Jacob Kelley <jakie8@gmail.com> @license MIT **/ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Postmate = factory()); }(this, (function () { 'use strict'; /** * The type of messages our frames our sending * @type {String} */ var messageType = 'application/x-postmate-v1+json'; /** * A unique message ID that is used to ensure responses are sent to the correct requests * @type {Number} */ var _messageId = 0; /** * Increments and returns a message ID * @return {Number} A unique ID for a message */ var generateNewMessageId = function generateNewMessageId() { return ++_messageId; }; /** * A unique child ID that is used to ensure responses are received from the correct child iframes * @type {Number} */ var _childId = 0; /** * Increments and returns a message ID * @return {Number} A unique ID for a message */ function childId() { return ++_childId; } /** * Takes a URL and returns the origin * @param {String} url The full URL being requested * @return {String} The URLs origin */ var resolveOrigin = function resolveOrigin(url) { var a = document.createElement('a'); a.href = url; var protocol = a.protocol.length > 4 ? a.protocol : window.location.protocol; var host = a.host.length ? a.port === '80' || a.port === '443' ? a.hostname : a.host : window.location.host; return a.origin || protocol + "//" + host; }; var messageTypes = { handshake: 1, 'handshake-reply': 1, call: 1, emit: 1, reply: 1, request: 1 }; /** * Ensures that a message is safe to interpret * @param {Object} message The postmate message being sent * @param {String|Boolean} allowedOrigin The whitelisted origin or false to skip origin check * @return {Boolean} */ var sanitize = function sanitize(message, allowedOrigin) { if (typeof allowedOrigin === 'string' && message.origin !== allowedOrigin) return false; if (!message.data) return false; if (typeof message.data === 'object' && !('postmate' in message.data)) return false; if (message.data.type !== messageType) return false; if (!messageTypes[message.data.postmate]) return false; return true; }; /** * Ensure that the logger have basic methods * @param {Logger} logger * @returns {Logger} */ var SanitizeLogger = function SanitizeLogger(logger) { var loggerMethods = ['debug', 'error']; loggerMethods.forEach(function (methodName) { if (logger[methodName] === undefined || typeof logger[methodName] !== 'function') { logger[methodName] = function emptyMethod() {}; } }); return logger; }; /** * Takes a model, and searches for a value by the property * @param {Object} model The dictionary to search against * @param {String} property A path within a dictionary (i.e. 'window.location.href') * @param {Object} data Additional information from the get request that is * passed to functions in the child model * @return {Promise} */ var resolveValue = function resolveValue(model, property) { var unwrappedContext = typeof model[property] === 'function' ? model[property]() : model[property]; return Postmate.Promise.resolve(unwrappedContext); }; /** * Composes an API to be used by the parent * @param {Object} info Information on the consumer */ var ParentAPI = /*#__PURE__*/function () { function ParentAPI(info) { var _this = this; this.parent = info.parent; this.frame = info.frame; this.child = info.child; this.childOrigin = info.childOrigin; this.childId = info.childId; this.logger = info.logger; this.events = {}; this.logger.debug('Parent: Registering API'); this.logger.debug('Parent: Awaiting messages...'); this.listener = function (e) { if (!sanitize(e, _this.childOrigin)) return false; /** * the assignments below ensures that e, data, and value are all defined */ var _ref = ((e || {}).data || {}).value || {}, data = _ref.data, name = _ref.name; if (e.data.postmate === 'emit' && e.data.childId === _this.childId) { _this.logger.debug("Parent: Received event emission: " + name); if (name in _this.events) { _this.events[name].forEach(function (callback) { callback.call(_this, data); }); } } }; this.parent.addEventListener('message', this.listener, false); this.logger.debug('Parent: Awaiting event emissions from Child'); } var _proto = ParentAPI.prototype; _proto.get = function get(property) { var _this2 = this; return new Postmate.Promise(function (resolve) { // Extract data from response and kill listeners var uid = generateNewMessageId(); var transact = function transact(e) { if (e.data.uid === uid && e.data.postmate === 'reply' && e.data.childId === _this2.childId) { _this2.parent.removeEventListener('message', transact, false); resolve(e.data.value); } }; // Prepare for response from Child... _this2.parent.addEventListener('message', transact, false); // Then ask child for information _this2.child.postMessage({ postmate: 'request', type: messageType, property: property, uid: uid }, _this2.childOrigin); }); }; _proto.call = function call(property, data) { // Send information to the child this.child.postMessage({ postmate: 'call', type: messageType, property: property, data: data }, this.childOrigin); }; _proto.on = function on(eventName, callback) { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push(callback); }; _proto.destroy = function destroy() { this.logger.debug('Parent: Destroying Postmate instance'); window.removeEventListener('message', this.listener, false); this.frame.parentNode.removeChild(this.frame); }; return ParentAPI; }(); /** * Composes an API to be used by the child * @param {Object} info Information on the consumer */ var ChildAPI = /*#__PURE__*/function () { function ChildAPI(info) { var _this3 = this; this.model = info.model; this.parent = info.parent; this.parentOrigin = info.parentOrigin; this.child = info.child; this.childId = info.childId; this.logger = info.logger; this.logger.debug('Child: Registering API'); this.logger.debug('Child: Awaiting messages...'); this.child.addEventListener('message', function (e) { if (!sanitize(e, _this3.parentOrigin)) return; _this3.logger.debug('Child: Received request', e.data); var _e$data = e.data, property = _e$data.property, uid = _e$data.uid, data = _e$data.data; if (e.data.postmate === 'call') { if (property in _this3.model && typeof _this3.model[property] === 'function') { _this3.model[property](data); } return; } // Reply to Parent resolveValue(_this3.model, property).then(function (value) { return e.source.postMessage({ property: property, postmate: 'reply', type: messageType, childId: _this3.childId, uid: uid, value: value }, e.origin); }); }); } var _proto2 = ChildAPI.prototype; _proto2.emit = function emit(name, data) { this.logger.debug("Child: Emitting Event \"" + name + "\"", data); this.parent.postMessage({ postmate: 'emit', type: messageType, childId: this.childId, value: { name: name, data: data } }, this.parentOrigin); }; return ChildAPI; }(); /** * The entry point of the Parent. * @type {Class} */ var Postmate = /*#__PURE__*/function () { /** * The maximum number of attempts to send a handshake request to the parent * @type {Number} */ // Internet Explorer craps itself /** * Sets options related to the Parent * @param {Object} object The element to inject the frame into, and the url * @return {Promise} */ function Postmate(_ref2) { var _ref2$container = _ref2.container, container = _ref2$container === void 0 ? typeof container !== 'undefined' ? container : document.body : _ref2$container, model = _ref2.model, url = _ref2.url, name = _ref2.name, _ref2$classListArray = _ref2.classListArray, classListArray = _ref2$classListArray === void 0 ? [] : _ref2$classListArray, _ref2$logger = _ref2.logger, logger = _ref2$logger === void 0 ? {} : _ref2$logger; // eslint-disable-line no-undef this.parent = window; this.frame = document.createElement('iframe'); this.frame.name = name || ''; if (classListArray.length > 0) { // check for IE 11. See issue#207 this.frame.classList.add.apply(this.frame.classList, classListArray); } container.appendChild(this.frame); this.child = this.frame.contentWindow || this.frame.contentDocument.parentWindow; this.model = model || {}; this.childId = childId(); this.logger = SanitizeLogger(logger); return this.sendHandshake(url); } /** * Begins the handshake strategy * @param {String} url The URL to send a handshake request to * @return {Promise} Promise that resolves when the handshake is complete */ var _proto3 = Postmate.prototype; _proto3.sendHandshake = function sendHandshake(url) { var _this4 = this; var childOrigin = resolveOrigin(url); var attempt = 0; var responseInterval; return new Postmate.Promise(function (resolve, reject) { var reply = function reply(e) { if (!sanitize(e, childOrigin)) return false; if (e.data.childId !== _this4.childId) return false; if (e.data.postmate === 'handshake-reply') { clearInterval(responseInterval); _this4.logger.debug('Parent: Received handshake reply from Child'); _this4.parent.removeEventListener('message', reply, false); _this4.childOrigin = e.origin; _this4.logger.debug('Parent: Saving Child origin', _this4.childOrigin); return resolve(new ParentAPI(_this4)); } _this4.logger.error('Parent: Failed handshake'); return reject('Failed handshake'); }; _this4.parent.addEventListener('message', reply, false); var doSend = function doSend() { if (++attempt > Postmate.maxHandshakeRequests) { clearInterval(responseInterval); _this4.logger.error('Parent: Handshake Timeout Reached'); return reject('Handshake Timeout Reached'); } _this4.logger.debug("Parent: Sending handshake attempt " + attempt, { childOrigin: childOrigin }); _this4.child.postMessage({ postmate: 'handshake', type: messageType, model: _this4.model, childId: _this4.childId }, childOrigin); }; var loaded = function loaded() { doSend(); responseInterval = setInterval(doSend, 500); }; if (_this4.frame.attachEvent) { _this4.frame.attachEvent('onload', loaded); } else { _this4.frame.addEventListener('load', loaded); } _this4.logger.debug('Parent: Loading frame', { url: url }); _this4.frame.src = url; }); }; return Postmate; }(); /** * The entry point of the Child * @type {Class} */ Postmate.maxHandshakeRequests = 5; Postmate.Promise = function () { try { return window ? window.Promise : Promise; } catch (e) { return null; } }(); Postmate.Model = /*#__PURE__*/function () { /** * Initializes the child, model, parent, and responds to the Parents handshake * @param {Object} model Hash of values, functions, or promises * @return {Promise} The Promise that resolves when the handshake has been received */ function Model(model, logger) { if (logger === void 0) { logger = {}; } this.child = window; this.model = model; this.parent = this.child.parent; this.logger = SanitizeLogger(logger); return this.sendHandshakeReply(); } /** * Responds to a handshake initiated by the Parent * @return {Promise} Resolves an object that exposes an API for the Child */ var _proto4 = Model.prototype; _proto4.sendHandshakeReply = function sendHandshakeReply() { var _this5 = this; return new Postmate.Promise(function (resolve, reject) { var shake = function shake(e) { if (!e.data.postmate) { return; } if (e.data.postmate === 'handshake') { _this5.logger.debug('Child: Received handshake from Parent'); _this5.child.removeEventListener('message', shake, false); _this5.logger.debug('Child: Sending handshake reply to Parent'); e.source.postMessage({ postmate: 'handshake-reply', type: messageType, childId: e.data.childId }, e.origin); _this5.childId = e.data.childId; _this5.parentOrigin = e.origin; // Extend model with the one provided by the parent var defaults = e.data.model; if (defaults) { Object.keys(defaults).forEach(function (key) { _this5.model[key] = defaults[key]; }); _this5.logger.debug('Child: Inherited and extended model from Parent'); } _this5.logger.debug('Child: Saving Parent origin', _this5.parentOrigin); return resolve(new ChildAPI(_this5)); } _this5.logger.error('Child : Handshake Reply Failed'); return reject('Handshake Reply Failed'); }; _this5.child.addEventListener('message', shake, false); }); }; return Model; }(); return Postmate; })));