@flourish/sdk
Version:
The Flourish SDK
529 lines (528 loc) • 22.3 kB
JavaScript
;
/* This file is used by the story player, and must be IE-compatible */
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const customer_analytics_1 = require("./customer_analytics");
const dompurify_1 = __importDefault(require("dompurify"));
const parse_query_params_1 = __importDefault(require("./parse_query_params"));
var is_fixed_height;
var is_amp;
function isFixedHeight() {
if (is_fixed_height == undefined) {
var params = (0, parse_query_params_1.default)();
// "referrer" in params implies this is an Embedly embed
// Check whether embedding site is known to support dynamic resizing
if ("referrer" in params) {
is_fixed_height = /^https:\/\/medium.com\//.test(params.referrer);
}
else {
is_fixed_height = !("auto" in params);
}
}
return is_fixed_height;
}
function getHeightForBreakpoint(width) {
var breakpoint_width = width || window.innerWidth;
if (breakpoint_width > 999) {
return 650;
}
if (breakpoint_width > 599) {
return 575;
}
return 400;
}
function initScrolly(opts) {
if (!opts) {
return;
}
if (window.top === window.self) {
return;
}
var embedded_window = window;
if (embedded_window.location.pathname == "srcdoc") {
embedded_window = embedded_window.parent;
}
var message = {
sender: "Flourish",
method: "scrolly",
captions: opts.captions,
hasScrollyTransformFix: opts.hasScrollyTransformFix,
};
embedded_window.parent.postMessage(JSON.stringify(message), "*");
}
function notifyParentWindow(height, opts) {
if (window.top === window.self) {
return;
}
var embedded_window = window;
if (embedded_window.location.pathname == "srcdoc") {
embedded_window = embedded_window.parent;
}
if (is_amp) {
// Message is not stringified for AMP
height = parseInt(height, 10);
embedded_window.parent.postMessage({
sentinel: "amp",
type: "embed-size",
height: height,
}, "*");
return;
}
var message = {
sender: "Flourish",
context: "iframe.resize",
method: "resize", // backwards compatibility
height: height,
src: embedded_window.location.toString(),
};
if (opts) {
for (var name in opts) {
message[name] = opts[name];
}
}
embedded_window.parent.postMessage(JSON.stringify(message), "*");
}
function isSafari() {
// Some example user agents:
// Safari iOS: Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1
// Chrome OS X: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
// Embedded WkWebview on iOS: Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/16D5039a
return (navigator.userAgent.indexOf("Safari") !== -1 || navigator.userAgent.indexOf("iPhone") !== -1) && navigator.userAgent.indexOf("Chrome") == -1;
}
function isString(s) {
return typeof s === "string" || s instanceof String;
}
function isPossibleHeight(n) {
if (typeof n === "number") {
return !isNaN(n) && (n >= 0);
}
else if (isString(n)) {
// First regex checks there is at least one digit in n and rejectsedge cases like "" and "px" that would pass second regex
// Given first regex, second regex makes sure that n is either a pure number or a number with a valid CSS unit
// Units based on https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units#lengths plus %
return /\d/.test(n) && /^[0-9]*(\.[0-9]*)?(cm|mm|Q|in|pc|pt|px|em|ex|ch|rem|lh|vw|vh|vmin|vmax|%)?$/i.test(n);
}
return false;
}
function validateWarnMessage(message) {
if (message.method !== "warn") {
console.warn("BUG: validateWarnMessage called for method" + message.method);
return false;
}
if ((message.message != null) && !isString(message.message)) {
return false;
}
if ((message.explanation != null) && !isString(message.explanation)) {
return false;
}
return true;
}
function validateResizeMessage(message) {
if (message.method !== "resize") {
console.warn("BUG: validateResizeMessage called for method" + message.method);
return false;
}
if (!isString(message.src)) {
return false;
}
if (!isString(message.context)) {
return false;
}
if (!isPossibleHeight(message.height)) {
return false;
}
return true;
}
function validateSetSettingMessage(_message) {
throw new Error("Validation for setSetting is not implemented yet; see issue #4328");
}
function validateScrolly(message) {
if (message.method !== "scrolly") {
console.warn("BUG: validateScrolly called for method" + message.method);
return false;
}
if (!Array.isArray(message.captions)) {
return false;
}
return true;
}
function validateCustomerAnalyticsMessage(message) {
if (message.method !== "customerAnalytics") {
console.warn("BUG: validateCustomerAnalyticsMessage called for method" + message.method);
return false;
}
// We don't consume customer analytics messages; they're just passed
// on, and their structure is up to the customer, so there's no
// point in validating them.
return true;
}
function validateRequestUpload(message) {
if (message.method !== "request-upload") {
console.warn("BUG: validateResizeMessage called for method" + message.method);
return false;
}
// FIXME: when adding validation for setSetting (see above) we should
// also validate that this is a valid setting name of appropriate type
if (!isString(message.name)) {
return false;
}
if (!(message.accept == null || isString(message.accept))) {
return false;
}
return true;
}
function getMessageValidators(methods) {
var available_message_validators = {
"warn": validateWarnMessage,
"resize": validateResizeMessage,
"setSetting": validateSetSettingMessage,
"customerAnalytics": validateCustomerAnalyticsMessage,
"request-upload": validateRequestUpload,
"scrolly": validateScrolly,
};
var validators = {};
for (var i = 0; i < methods.length; i++) {
var method = methods[i];
if (available_message_validators[method]) {
validators[method] = available_message_validators[method];
}
else {
throw new Error("No validator found for method " + method);
}
}
return validators;
}
function startEventListeners(callback, allowed_methods, embed_domain) {
var message_validators = getMessageValidators(allowed_methods);
window.addEventListener("message", function (event) {
var is_accepted_event_origin = (function () {
if (event.origin == document.location.origin) {
return true;
}
// If company has configured a custom origin for downloaded projects, allow it
if (embed_domain) {
const origin = event.origin.toLowerCase();
embed_domain = embed_domain.toLowerCase();
// Allow the domain itself…
if (origin.endsWith("//" + embed_domain)) {
return true;
}
// and subdomains
if (origin.endsWith("." + embed_domain)) {
return true;
}
}
if (event.origin.match(/\/\/localhost:\d+$|\/\/(?:public|app)\.local\.flourish-internal\.com$|\/\/flourish-api\.com$|\.flourish\.(?:local(:\d+)?|net|rocks|studio)$|\.uri\.sh$|\/\/flourish-user-templates\.com$/)) {
return true;
}
return false;
})();
// event.source is null when the message is sent by an extension
// https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Using_window.postMessage_in_extensions
if (event.source == null) {
return;
}
if (!is_accepted_event_origin) {
return;
}
var message;
try {
message = typeof event.data === "object" ? event.data : JSON.parse(event.data);
}
catch (e) {
console.warn("Unexpected non-JSON message: " + JSON.stringify(event.data));
return;
}
if (message.sender !== "Flourish") {
return;
}
if (!message.method) {
console.warn("The 'method' property was missing from message", message);
return;
}
if (!Object.prototype.hasOwnProperty.call(message_validators, message.method)) {
console.warn("No validator implemented for message", message);
return;
}
if (!message_validators[message.method](message)) {
console.warn("Validation failed for the message", message);
return;
}
var frames = document.querySelectorAll("iframe");
for (var i = 0; i < frames.length; i++) {
if (frames[i].contentWindow == event.source || frames[i].contentWindow == event.source.parent) {
callback(message, frames[i]);
return;
}
}
console.warn("could not find frame", message);
});
if (isSafari()) {
window.addEventListener("resize", onSafariWindowResize);
onSafariWindowResize();
}
}
function onSafariWindowResize() {
// Ensure all iframes without explicit width attribute are sized to fit their container
var containers = document.querySelectorAll(".flourish-embed");
for (var i = 0; i < containers.length; i++) {
var container = containers[i];
if (container.getAttribute("data-width")) {
continue;
}
var iframe = container.querySelector("iframe");
// When embeds are dynamically loaded, we might have a container without a
// loaded iframe yet
if (!iframe) {
continue;
}
var computed_style = window.getComputedStyle(container);
var width = container.offsetWidth - parseFloat(computed_style.paddingLeft) - parseFloat(computed_style.paddingRight);
iframe.style.width = width + "px";
}
}
function createScrolly(iframe, captions, hasScrollyTransformFix) {
var parent = iframe.parentNode;
// Fallback to avoid any situation where the scrolly gets initialised twice
if (parent.classList.contains("fl-scrolly-wrapper")) {
console.warn("createScrolly is being called more than once per story. This should not happen.");
return;
}
parent.classList.add("fl-scrolly-wrapper");
parent.style.position = "relative";
parent.style.paddingBottom = "1px";
parent.style.transform = "translate3d(0, 0, 0)"; // Workaround for Safari https://stackoverflow.com/questions/50224855/not-respecting-z-index-on-safari-with-position-sticky
// On Windows + Chromium, there is a bug where dropdowns open in the wrong
// place in scrollies that is related to the sticky position of the iframe.
// This workaround retriggers the transform to fix the dropdown positioning
// (specifically, the transform is toggled between 0 and 1).
const is_windows = navigator.platform.indexOf("Win") > -1;
const is_chromium = !!window.chrome &&
(navigator.userAgent.indexOf("Chrome") > -1 ||
navigator.userAgent.indexOf("Edg") > -1 ||
navigator.userAgent.indexOf("OPR") > -1);
if (is_windows && is_chromium && hasScrollyTransformFix) {
let scroll_timeout;
let transform = 0;
window.addEventListener("scroll", function () {
clearTimeout(scroll_timeout);
scroll_timeout = setTimeout(() => {
transform = transform === 0 ? 1 : 0;
parent.style.transform = `translateZ(${transform}px)`;
}, 100);
});
}
iframe.style.position = "sticky";
var h = parent.getAttribute("data-height") || null;
if (!h) { // Scrollies require fixed height to work well, so if not height set …
h = "80vh"; // … use a sensible fallback
iframe.style.height = h; // And update the iframe height directly
}
iframe.style.top = "calc(50vh - " + h + "/2)";
var credit = parent.querySelector(".flourish-credit");
if (credit) {
credit.style.position = "sticky";
credit.style.top = "calc(50vh + " + h + "/2)";
}
captions.forEach(function (d, i) {
var has_content = typeof d == "string" && d.trim() != "";
var step = document.createElement("div");
step.setAttribute("data-slide", i);
step.classList.add("fl-scrolly-caption");
step.style.position = "relative";
step.style.transform = "translate3d(0,0,0)"; // Workaround for Safari https://stackoverflow.com/questions/50224855/not-respecting-z-index-on-safari-with-position-sticky
step.style.textAlign = "center";
step.style.maxWidth = "500px";
step.style.height = "auto";
step.style.marginTop = "0";
step.style.marginBottom = has_content ? "100vh" : "50vh";
step.style.marginLeft = "auto";
step.style.marginRight = "auto";
var caption = document.createElement("div");
// eslint-disable-next-line flourish/dompurify
caption.innerHTML = dompurify_1.default.sanitize(d, { ADD_ATTR: ["target"] });
caption.style.visibility = has_content ? "" : "hidden";
caption.style.display = "inline-block";
caption.style.paddingTop = "1.25em";
caption.style.paddingRight = "1.25em";
caption.style.paddingBottom = "1.25em";
caption.style.paddingLeft = "1.25em";
caption.style.background = "rgba(255,255,255,0.9)";
caption.style.boxShadow = "0px 0px 10px rgba(0,0,0,0.2)";
caption.style.borderRadius = "10px";
caption.style.textAlign = "center";
caption.style.maxWidth = "100%";
caption.style.margin = "0 20px";
caption.style.overflowX = "hidden";
step.appendChild(caption);
parent.appendChild(step);
});
initIntersection(parent);
}
function initIntersection(container) {
var t = "0%"; // Trigger when hits viewport; could be set by user in the future
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
var iframe = container.querySelector("iframe");
if (iframe) {
iframe.src = iframe.src.replace(/#slide-.*/, "") + "#slide-" + entry.target.getAttribute("data-slide");
}
}
});
}, { rootMargin: "0px 0px -" + t + " 0px" });
var steps = container.querySelectorAll(".fl-scrolly-caption");
for (var i = 0; i < steps.length; i++) {
observer.observe(steps[i]);
}
// Set a max width on any images in the captions, to avoid ugly overflowing
// in the rare cases where the
// This won't happen much, but it is possible to paste an image into a
// story caption, so better to handle this nicely since there's no other
// way for the user to set it.
var images = container.querySelectorAll(".fl-scrolly-caption img");
images.forEach(function (img) { img.style.maxWidth = "100%"; });
}
function createEmbedIframe(embed_url, container, width, height, play_on_load) {
var iframe = document.createElement("iframe");
iframe.setAttribute("scrolling", "no");
iframe.setAttribute("frameborder", "0");
iframe.setAttribute("title", "Interactive or visual content");
iframe.setAttribute("sandbox", "allow-same-origin allow-forms allow-scripts allow-downloads allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation");
container.appendChild(iframe);
// If the iframe doesn't have an offset parent, either the element or a parent
// is set to display: none. This can cause problems with visualisation loading, so
// we need to poll for the iframe being displayed before loading the visualisation.
// FIXME: In Chrome, fixed position elements also return null for `offsetParent`.
// The chances of an embed which is both position: fixed and display: none are
// pretty small, so fuhgeddaboudit . If it's an issue in the future, we'll have to
// recurse through the parent elements to make sure the iframe is displaying.
if (iframe.offsetParent || getComputedStyle(iframe).position === "fixed") {
setIframeContent(embed_url, container, iframe, width, height, play_on_load);
}
else {
var poll_item = {
embed_url: embed_url,
container: container,
iframe: iframe,
width: width,
height: height,
play_on_load: play_on_load,
};
// If this is the first embed on the page which is isn't displayed, set up a
// list of hidden iframes to poll
if (!window._flourish_poll_items) {
window._flourish_poll_items = [poll_item];
}
else {
// Otherwise, add this to the list of iframes which are being polled
window._flourish_poll_items.push(poll_item);
}
if (window._flourish_poll_items.length > 1) {
// If there were already items in the array then we have already started
// polling in a different embed script, so we can return. This iframe will
// have its contents set by the other embed script.
return iframe;
}
// Poll to see whether any of the iframes have started displaying
var interval = setInterval(function () {
window._flourish_poll_items = window._flourish_poll_items.filter(function (item) {
if (!item.iframe.offsetParent) {
// It's still not displaying, so return true to leave it in the array
return true;
}
// It's displaying, so set the content, and return false to remove it from
// the array
setIframeContent(item.embed_url, item.container, item.iframe, item.width, item.height, item.play_on_load);
return false;
});
if (!window._flourish_poll_items.length) {
// All of the iframes are displaying, so we can stop polling. If another
// embed is added later, a new interval will be created by that embed script.
clearInterval(interval);
}
}, 500);
}
return iframe;
}
function setIframeContent(embed_url, container, iframe, width, height, play_on_load) {
var width_in_px;
if (width && typeof width === "number") {
width_in_px = width;
width = "" + width + "px";
}
// The regular expression below detects widths that have been explicitly
// expressed in px units. (It turns out CSS is more complicated than you may
// have realised.)
else if (width && width.match(/^[ \t\r\n\f]*([+-]?\d+|\d*\.\d+(?:[eE][+-]?\d+)?)(?:\\?[Pp]|\\0{0,4}[57]0(?:\r\n|[ \t\r\n\f])?)(?:\\?[Xx]|\\0{0,4}[57]8(?:\r\n|[ \t\r\n\f])?)[ \t\r\n\f]*$/)) {
width_in_px = parseFloat(width);
}
if (height && typeof height === "number") {
height = "" + height + "px";
}
// Odd design decision in Safari means need to set fixed width rather than %
// as will try and size iframe to content otherwise. Must also set scrolling=no
if (width) {
iframe.style.width = width;
}
else if (isSafari()) {
iframe.style.width = container.offsetWidth + "px";
}
else {
iframe.style.width = "100%";
}
var fixed_height = !!height;
if (!fixed_height) {
if (embed_url.match(/\?/)) {
embed_url += "&auto=1";
}
else {
embed_url += "?auto=1";
}
// For initial height, use our standard breakpoints, based on the explicit
// pixel width if we know it, or the iframe's measured width if not.
height = getHeightForBreakpoint(width_in_px || iframe.offsetWidth) + "px";
}
if (height) {
if (height.charAt(height.length - 1) === "%") {
height = (parseFloat(height) / 100) * container.parentNode.offsetHeight + "px";
}
iframe.style.height = height;
}
iframe.setAttribute("src", embed_url + (play_on_load ? "#play-on-load" : ""));
// Send postMessage after iframe loads to notify it that credit is handled externally
iframe.addEventListener("load", function () {
try {
iframe.contentWindow.postMessage({
sender: "Flourish",
method: "flourish:creditHandledExternally",
}, "*");
}
catch (e) {
// Silently fail if postMessage is blocked
if (console && console.warn) {
console.warn("Could not send credit postMessage:", e);
}
}
}, { once: true });
return iframe;
}
function initEmbedding() {
is_amp = window.location.hash == "#amp=1";
return {
createEmbedIframe: createEmbedIframe,
isFixedHeight: isFixedHeight,
getHeightForBreakpoint: getHeightForBreakpoint,
startEventListeners: startEventListeners,
notifyParentWindow: notifyParentWindow,
initScrolly: initScrolly,
createScrolly: createScrolly,
isSafari: isSafari,
initCustomerAnalytics: customer_analytics_1.initCustomerAnalytics,
addAnalyticsListener: customer_analytics_1.addAnalyticsListener,
sendCustomerAnalyticsMessage: customer_analytics_1.sendCustomerAnalyticsMessage,
};
}
exports.default = initEmbedding;