mirror of
https://github.com/wassname/talk.git
synced 2026-08-11 11:27:10 +08:00
* 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
165 lines
13 KiB
JavaScript
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;
|
|
}
|
|
}));
|