Files
talk/client/coral-embed-api/index.js
T
Benjamin Goering f7cddad659 Futureproof Embed Code
* Add client/coral-embed-api module. It's meant to be included in an HTML page and define window.Coral
* Serve coral-embed-api at /Coral.js
* Update / handler (article view) to use new embed format
* Update Talk Admin embed code section to use new embed format
2017-02-04 00:47:32 +08:00

165 lines
13 KiB
JavaScript

/* eslint-disable no-var */
/**
* @file Exposes the main 'window.Coral.Talk' API that developers can use to
* render Talk streams in their webpages
* @todo Currently implemented to be included directly via <script> tag
* (including copypasta dependencies like pym.js), but later there will be a
* build step and this code may use import statements
*/
// using umd.js (https://github.com/umdjs/umd/blob/master/templates/returnExports.js)
(function (root, factory) {
/* eslint-disable */
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
root.Coral = factory();
}
/* eslint-enable */
}(this, function () {
// This function should return value of window.Coral
let pym = requirePym();
let Coral = {};
let Talk = Coral.Talk = {};
/**
* Render a Talk stream
* @param {HTMLElement} el - Element to render the stream in
* @param {Object} opts - Configuration options for talk
* @param {String} opts.talk - Talk base URL
* @param {String} [opts.title] - Title of Stream (rendered in iframe)
* @param {String} [opts.asset] - parent Asset ID or URL. Comments in the
* stream will replies to this asset
*/
Talk.render = function (el, opts) {
console.log('Coral.Talk.render()', el, opts);
if ( ! el) {
throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.');
}
if (typeof el !== 'object') {
throw new Error(`Coral.Talk.render() expected HTMLElement but got ${ el } (${ typeof el})`);
}
opts = opts || {};
// @todo infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
if (! opts.talk) {
throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL');
}
// ensure el has an id, as pym can't directly accept the HTMLElement
if ( ! el.id) {el.id = `_${ String(Math.random())}`;}
let asset = opts.asset || window.location;
let pymParent = new pym.Parent(
el.id,
buildStreamIframeUrl(opts.talk, asset),
{
title: opts.title,
asset_url: asset,
id: `${el.id }_` + 'iframe',
name: `${el.id }_` + 'iframe'
}
);
configurePymParent(pymParent, asset);
};
return Coral;
// build the URL to load in the pym iframe
function buildStreamIframeUrl(talkBaseUrl, asset) {
let iframeUrl = [
talkBaseUrl,
(talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/'
'embed/stream?asset_url=',
encodeURIComponent(asset)
].join('');
return iframeUrl;
}
// Set up postMessage listeners/handlers on the pymParent
// e.g. to resize the iframe, and navigate the host page
function configurePymParent(pymParent, assetUrl) {
let notificationOffset = 200;
let ready = false;
// Resize parent iframe height when child height changes
pymParent.onMessage('height', function(height) {
// TODO: In local testing, this is firing nonstop. Maybe there's a bug on the inside?
// Or it's by design of pym... but that's very wasteful of CPU and DOM reflows (jank)
pymParent.el.querySelector('iframe').height = `${height }px`;
});
// Helps child show notifications at the right scrollTop
pymParent.onMessage('getPosition', function() {
let position = viewport().height + document.body.scrollTop;
if (position > notificationOffset) {
position = position - notificationOffset;
}
pymParent.sendMessage('position', position);
});
// Tell child when parent's DOMContentLoaded
pymParent.onMessage('childReady', function () {
var interval = setInterval(function () {
if (ready) {
window.clearInterval(interval);
// @todo - It's weird to me that this is sent here in addition to the iframe URL. Could it just be in one place?
pymParent.sendMessage('DOMContentLoaded', assetUrl);
}
}, 100);
});
// When end-user clicks link in iframe, open it in parent context
pymParent.onMessage('navigate', function (url) {
window.open(url, '_blank').focus();
});
// wait till images and other iframes are loaded before scrolling the page.
// or do we want to be more aggressive and scroll when we hit DOM ready?
document.addEventListener('DOMContentLoaded', function () {
ready = true;
});
// get dimensions of viewport
function viewport() {
let e = window, a = 'inner';
if ( !( 'innerWidth' in window ) ){
a = 'client';
e = document.documentElement || document.body;
}
return {width : e[ `${a}Width` ], height : e[ `${a}Height` ]};
}
}
// return a reference to pym.js
function requirePym() {
let pym;
// fake AMD `define` so that the pym.js copypasta doesn't create a global
function define(createPym) {
pym = createPym();
}
define.amd = true;
/* eslint-disable */
/* ! pym.js - v1.1.2 - 2016-10-25 */
!function(a){typeof define == 'function' && define.amd ? define(a) : typeof module != 'undefined' && module.exports ? module.exports = a() : window.pym = a.call(this);}(function(){let a = 'xPYMx', b = {}, c = function(a){let b = new RegExp(`[\\?&]${ a.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')}=([^&#]*)`), c = b.exec(location.search);return c === null ? '' : decodeURIComponent(c[1].replace(/\+/g, ' '));}, d = function(a, b){if(b.xdomain === '*' || a.origin.match(new RegExp(`${b.xdomain}$`))){return!0;}}, e = function(b, c, d){let e = ['pym', b, c, d];return e.join(a);}, f = function(b){let c = ['pym', b, '(\\S+)', '(.*)'];return new RegExp(`^${ c.join(a)}$`);}, g = function(){for(var a = b.autoInitInstances.length, c = a - 1;c >= 0;c--){let d = b.autoInitInstances[c];d.el.getElementsByTagName('iframe').length && d.el.getElementsByTagName('iframe')[0].contentWindow || b.autoInitInstances.splice(c, 1);}};return b.autoInitInstances = [], b.autoInit = function(){let a = document.querySelectorAll('[data-pym-src]:not([data-pym-auto-initialized])'), c = a.length;g();for(var d = 0;d < c;++d){let e = a[d];e.setAttribute('data-pym-auto-initialized', ''), e.id === '' && (e.id = `pym-${d}`);let f = e.getAttribute('data-pym-src'), h = {xdomain:'string', title:'string', name:'string', id:'string', sandbox:'string', allowfullscreen:'boolean'}, i = {};for(let j in h){if(e.getAttribute(`data-pym-${j}`) !== null){switch(h[j]){case'boolean':i[j] = !(e.getAttribute(`data-pym-${j}`) === 'false');break;case'string':i[j] = e.getAttribute(`data-pym-${j}`);break;default:console.err('unrecognized attribute type');}}}let k = new b.Parent(e.id, f, i);b.autoInitInstances.push(k);}return b.autoInitInstances;}, b.Parent = function(a, b, c){this.id = a, this.url = b, this.el = document.getElementById(a), this.iframe = null, this.settings = {xdomain:'*'}, this.messageRegex = f(this.id), this.messageHandlers = {}, c = c || {}, this._constructIframe = function(){let a = this.el.offsetWidth.toString();this.iframe = document.createElement('iframe');let b = '', c = this.url.indexOf('#');for(c > -1 && (b = this.url.substring(c, this.url.length), this.url = this.url.substring(0, c)), this.url.indexOf('?') < 0 ? this.url += '?' : this.url += '&', this.iframe.src = `${this.url}initialWidth=${a}&childId=${this.id}&parentTitle=${encodeURIComponent(document.title)}&parentUrl=${encodeURIComponent(window.location.href)}${b}`, this.iframe.setAttribute('width', '100%'), this.iframe.setAttribute('scrolling', 'no'), this.iframe.setAttribute('marginheight', '0'), this.iframe.setAttribute('frameborder', '0'), this.settings.title && this.iframe.setAttribute('title', this.settings.title), void 0 !== this.settings.allowfullscreen && this.settings.allowfullscreen !== !1 && this.iframe.setAttribute('allowfullscreen', ''), void 0 !== this.settings.sandbox && typeof this.settings.sandbox == 'string' && this.iframe.setAttribute('sandbox', this.settings.sandbox), this.settings.id && (document.getElementById(this.settings.id) || this.iframe.setAttribute('id', this.settings.id)), this.settings.name && this.iframe.setAttribute('name', this.settings.name);this.el.firstChild;){this.el.removeChild(this.el.firstChild);}this.el.appendChild(this.iframe), window.addEventListener('resize', this._onResize);}, this._onResize = function(){this.sendWidth();}.bind(this), this._fire = function(a, b){if(a in this.messageHandlers){for(var c = 0;c < this.messageHandlers[a].length;c++){this.messageHandlers[a][c].call(this, b);}}}, this.remove = function(){window.removeEventListener('message', this._processMessage), window.removeEventListener('resize', this._onResize), this.el.removeChild(this.iframe), g();}, this._processMessage = function(a){if(d(a, this.settings) && typeof a.data == 'string'){let b = a.data.match(this.messageRegex);if(!b || b.length !== 3){return!1;}let c = b[1], e = b[2];this._fire(c, e);}}.bind(this), this._onHeightMessage = function(a){let b = parseInt(a);this.iframe.setAttribute('height', `${b}px`);}, this._onNavigateToMessage = function(a){document.location.href = a;}, this._onScrollToChildPosMessage = function(a){let b = document.getElementById(this.id).getBoundingClientRect().top + window.pageYOffset, c = b + parseInt(a);window.scrollTo(0, c);}, this.onMessage = function(a, b){a in this.messageHandlers || (this.messageHandlers[a] = []), this.messageHandlers[a].push(b);}, this.sendMessage = function(a, b){this.el.getElementsByTagName('iframe').length && (this.el.getElementsByTagName('iframe')[0].contentWindow ? this.el.getElementsByTagName('iframe')[0].contentWindow.postMessage(e(this.id, a, b), '*') : this.remove());}, this.sendWidth = function(){let a = this.el.offsetWidth.toString();this.sendMessage('width', a);};for(let h in c){this.settings[h] = c[h];}return this.onMessage('height', this._onHeightMessage), this.onMessage('navigateTo', this._onNavigateToMessage), this.onMessage('scrollToChildPos', this._onScrollToChildPosMessage), window.addEventListener('message', this._processMessage, !1), this._constructIframe(), this;}, b.Child = function(b){this.parentWidth = null, this.id = null, this.parentTitle = null, this.parentUrl = null, this.settings = {renderCallback:null, xdomain:'*', polling:0}, this.timerId = null, this.messageRegex = null, this.messageHandlers = {}, b = b || {}, this.onMessage = function(a, b){a in this.messageHandlers || (this.messageHandlers[a] = []), this.messageHandlers[a].push(b);}, this._fire = function(a, b){if(a in this.messageHandlers){for(var c = 0;c < this.messageHandlers[a].length;c++){this.messageHandlers[a][c].call(this, b);}}}, this._processMessage = function(a){if(d(a, this.settings) && typeof a.data == 'string'){let b = a.data.match(this.messageRegex);if(b && b.length === 3){let c = b[1], e = b[2];this._fire(c, e);}}}.bind(this), this._onWidthMessage = function(a){let b = parseInt(a);b !== this.parentWidth && (this.parentWidth = b, this.settings.renderCallback && this.settings.renderCallback(b), this.sendHeight());}, this.sendMessage = function(a, b){window.parent.postMessage(e(this.id, a, b), '*');}, this.sendHeight = function(){let a = document.getElementsByTagName('body')[0].offsetHeight.toString();return this.sendMessage('height', a), a;}.bind(this), this.scrollParentTo = function(a){this.sendMessage('navigateTo', `#${a}`);}, this.navigateParentTo = function(a){this.sendMessage('navigateTo', a);}, this.scrollParentToChildEl = function(a){let b = document.getElementById(a).getBoundingClientRect().top + window.pageYOffset;this.scrollParentToChildPos(b);}, this.scrollParentToChildPos = function(a){this.sendMessage('scrollToChildPos', a.toString());}, this._markWhetherEmbedded = function(a){let b, c = document.getElementsByTagName('html')[0], d = c.className;try{b = window.self !== window.top ? 'embedded' : 'not-embedded';}catch(a){b = 'embedded';}d.indexOf(b) < 0 && (c.className = d ? `${d} ${b}` : b, a && a(b));}, this.remove = function(){window.removeEventListener('message', this._processMessage), this.timerId && clearInterval(this.timerId);}, this.id = c('childId') || b.id, this.messageRegex = new RegExp(`^pym${ a }${this.id }${a }(\\S+)${ a}(.*)$`);let f = parseInt(c('initialWidth'));this.parentUrl = c('parentUrl'), this.parentTitle = c('parentTitle'), this.onMessage('width', this._onWidthMessage);for(let g in b){this.settings[g] = b[g];}return window.addEventListener('message', this._processMessage, !1), this.settings.renderCallback && this.settings.renderCallback(f), this.sendHeight(), this.settings.polling && (this.timerId = window.setInterval(this.sendHeight, this.settings.polling)), this._markWhetherEmbedded(b.onMarkedEmbeddedStatus), this;}, b.autoInit(), b;});
/* eslint-enable */
return pym;
}
}));