@builder.io/sdk
Version:
1,510 lines • 63 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var throttle = _interopDefault(require('lodash-es/throttle'));
var last = _interopDefault(require('lodash-es/last'));
var kebabCase = _interopDefault(require('lodash-es/kebabCase'));
var uniqueSelector = _interopDefault(require('unique-selector'));
var Cookies = _interopDefault(require('js-cookie'));
var stringify = _interopDefault(require('json-stable-stringify'));
var includes = _interopDefault(require('lodash-es/includes'));
var omit = _interopDefault(require('lodash-es/omit'));
var round = _interopDefault(require('lodash-es/round'));
var sortBy = _interopDefault(require('lodash-es/sortBy'));
var queryString = _interopDefault(require('query-string'));
var rxjs = require('rxjs');
var parser = _interopDefault(require('ua-parser-js'));
var url = require('url');
require('whatwg-fetch');
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
var __assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
var Animator = /** @class */ (function () {
function Animator() {
}
Animator.prototype.bindAnimations = function (animations) {
for (var _i = 0, animations_1 = animations; _i < animations_1.length; _i++) {
var animation = animations_1[_i];
switch (animation.trigger) {
case 'pageLoad':
this.triggerAnimation(animation);
break;
case 'hover':
this.bindHoverAnimation(animation);
break;
case 'scrollInView':
this.bindScrollInViewAnimation(animation);
break;
}
}
};
Animator.prototype.warnElementNotPresent = function (id) {
console.warn("Cannot animate element: element with ID " + id + " not found!");
};
Animator.prototype.augmentAnimation = function (animation, element) {
var stylesUsed = this.getAllStylesUsed(animation);
var computedStyle = getComputedStyle(element);
// const computedStyle = getComputedStyle(element);
// // FIXME: this will break if original load is in one reponsive size then resize to another hmmm
// Need to use transform instead of left since left can change on screen sizes
var firstStyles = animation.steps[0].styles;
var lastStyles = last(animation.steps).styles;
var bothStyles = [firstStyles, lastStyles];
// FIXME: this won't work as expected for augmented animations - may need the editor itself to manage this
for (var _i = 0, bothStyles_1 = bothStyles; _i < bothStyles_1.length; _i++) {
var styles = bothStyles_1[_i];
for (var _a = 0, stylesUsed_1 = stylesUsed; _a < stylesUsed_1.length; _a++) {
var style = stylesUsed_1[_a];
if (!(style in styles)) {
styles[style] = computedStyle[style];
}
}
}
};
Animator.prototype.getAllStylesUsed = function (animation) {
var properties = [];
for (var _i = 0, _a = animation.steps; _i < _a.length; _i++) {
var step = _a[_i];
for (var key in step.styles) {
if (properties.indexOf(key) === -1) {
properties.push(key);
}
}
}
return properties;
};
Animator.prototype.triggerAnimation = function (animation) {
var element = document.getElementById(animation.elementId);
if (!element) {
this.warnElementNotPresent(animation.elementId);
return;
}
this.augmentAnimation(animation, element);
// TODO: do this properly, may have other animations of different properties
// TODO: only override the properties
// TODO: if there is an entrance and hover animation, the transition duration will get effed
// element.setAttribute('style', '');
var styledUsed = this.getAllStylesUsed(animation);
element.style.transition = 'none';
element.style.transitionDelay = '0';
Object.assign(element.style, animation.steps[0].styles);
setTimeout(function () {
element.style.transition = "all " + animation.duration + "s " + kebabCase(animation.easing);
if (animation.delay) {
element.style.transitionDelay = animation.delay + 's';
}
Object.assign(element.style, animation.steps[1].styles);
// TODO: maybe remove/reset transitoin property after animation duration
setTimeout(function () {
element.style.transition = '';
element.style.transitionDelay = '';
}, (animation.delay || 0) * 1000 + animation.duration * 1000);
});
};
Animator.prototype.bindHoverAnimation = function (animation) {
// TODO: unbind on element remove
var element = document.getElementById(animation.elementId);
if (!element) {
this.warnElementNotPresent(animation.elementId);
return;
}
this.augmentAnimation(animation, element);
var defaultState = animation.steps[0].styles;
var hoverState = animation.steps[1].styles;
function attachDefaultState() {
Object.assign(element.style, defaultState);
}
function attachHoverState() {
Object.assign(element.style, hoverState);
}
attachDefaultState();
element.addEventListener('mouseenter', attachHoverState);
element.addEventListener('mouseleave', attachDefaultState);
setTimeout(function () {
element.style.transition = "all " + animation.duration + "s " + kebabCase(animation.easing);
if (animation.delay) {
element.style.transitionDelay = animation.delay + 's';
}
});
};
// TODO: unbind on element remove
Animator.prototype.bindScrollInViewAnimation = function (animation) {
var element = document.getElementById(animation.elementId);
if (!element) {
this.warnElementNotPresent(animation.elementId);
return;
}
this.augmentAnimation(animation, element);
var triggered = false;
// TODO: roll all of these in one for more efficiency of checking all the rects
var onScroll = throttle(function () {
if (!triggered && isScrolledIntoView(element)) {
triggered = true;
Object.assign(element.style, animation.steps[1].styles);
document.removeEventListener('scroll', onScroll);
setTimeout(function () {
element.style.transition = '';
element.style.transitionDelay = '';
}, animation.duration * 1000);
}
}, 100, { leading: false });
// TODO: fully in view or partially
function isScrolledIntoView(elem) {
var rect = elem.getBoundingClientRect();
var windowHeight = window.innerHeight;
var thresholdPrecent = 0.2;
var threshold = thresholdPrecent * windowHeight;
// TODO: partial in view? or what if element is larger than screen itself
return (rect.bottom > threshold && rect.top < windowHeight - threshold // Element is peeking top or bottom
);
}
var defaultState = animation.steps[0].styles;
function attachDefaultState() {
Object.assign(element.style, defaultState);
}
attachDefaultState();
setTimeout(function () {
element.style.transition = "all " + animation.duration + "s " + kebabCase(animation.easing);
if (animation.delay) {
element.style.transitionDelay = animation.delay + 's';
}
});
// TODO: one listener for everything
document.addEventListener('scroll', onScroll, { capture: true, passive: true });
element.addEventListener('remove', function () {
document.removeEventListener('scroll', onScroll);
});
// Do an initial check
onScroll();
};
return Animator;
}());
var version = "2";
var isVnode = isVirtualNode;
function isVirtualNode(x) {
return x && x.type === "VirtualNode" && x.version === version
}
var isWidget_1 = isWidget;
function isWidget(w) {
return w && w.type === "Widget"
}
var isThunk_1 = isThunk;
function isThunk(t) {
return t && t.type === "Thunk"
}
var isVhook = isHook;
function isHook(hook) {
return hook &&
(typeof hook.hook === "function" && !hook.hasOwnProperty("hook") ||
typeof hook.unhook === "function" && !hook.hasOwnProperty("unhook"))
}
var vnode = VirtualNode;
var noProperties = {};
var noChildren = [];
function VirtualNode(tagName, properties, children, key, namespace) {
this.tagName = tagName;
this.properties = properties || noProperties;
this.children = children || noChildren;
this.key = key != null ? String(key) : undefined;
this.namespace = (typeof namespace === "string") ? namespace : null;
var count = (children && children.length) || 0;
var descendants = 0;
var hasWidgets = false;
var hasThunks = false;
var descendantHooks = false;
var hooks;
for (var propName in properties) {
if (properties.hasOwnProperty(propName)) {
var property = properties[propName];
if (isVhook(property) && property.unhook) {
if (!hooks) {
hooks = {};
}
hooks[propName] = property;
}
}
}
for (var i = 0; i < count; i++) {
var child = children[i];
if (isVnode(child)) {
descendants += child.count || 0;
if (!hasWidgets && child.hasWidgets) {
hasWidgets = true;
}
if (!hasThunks && child.hasThunks) {
hasThunks = true;
}
if (!descendantHooks && (child.hooks || child.descendantHooks)) {
descendantHooks = true;
}
} else if (!hasWidgets && isWidget_1(child)) {
if (typeof child.destroy === "function") {
hasWidgets = true;
}
} else if (!hasThunks && isThunk_1(child)) {
hasThunks = true;
}
}
this.count = count + descendants;
this.hasWidgets = hasWidgets;
this.hasThunks = hasThunks;
this.hooks = hooks;
this.descendantHooks = descendantHooks;
}
VirtualNode.prototype.version = version;
VirtualNode.prototype.type = "VirtualNode";
var vtext = VirtualText;
function VirtualText(text) {
this.text = String(text);
}
VirtualText.prototype.version = version;
VirtualText.prototype.type = "VirtualText";
// CREDIT: adopted from https://raw.githubusercontent.com/marcelklehr/vdom-virtualize/master/index.js
// TODO: move to util - firefox friendly getBoundingClientRect
function getClientRect(element) {
if (!element) {
return null;
}
var properties = ['top', 'left', 'bottom', 'right', 'height', 'width'];
var newObject = {};
var rect = element.getBoundingClientRect();
for (var _i = 0, properties_1 = properties; _i < properties_1.length; _i++) {
var key = properties_1[_i];
newObject[key] = rect[key];
}
return newObject;
}
function createVNode(domNode, deep, addBuilderMetadata) {
if (deep === void 0) { deep = false; }
if (addBuilderMetadata === void 0) { addBuilderMetadata = true; }
if (domNode.nodeType === 1)
return createFromElement(domNode, deep);
if (domNode.nodeType === 3)
return createFromTextNode(domNode);
return;
}
function createFromTextNode(tNode) {
return new vtext(tNode.nodeValue);
}
function createFromElement(el, deep, addBuilderMetadata) {
if (deep === void 0) { deep = false; }
if (addBuilderMetadata === void 0) { addBuilderMetadata = true; }
var tagName = el.tagName;
var namespace = el.namespaceURI === 'http://www.w3.org/1999/xhtml' ? null : el.namespaceURI;
var properties = getElementProperties(el);
var children = [];
if (deep) {
for (var i = 0; i < el.childNodes.length; i++) {
children.push(createVNode(el.childNodes[i]));
}
}
if (addBuilderMetadata && properties.id && properties.id.indexOf('builder-') === 0) {
properties.builderMetadata = {
clientRect: getClientRect(el),
// TODO: only do this for the element(s) necessary
selector: uniqueSelector(el),
// TODO: only do this for the element(s) necessary
computedStyle: getComputedStyle(el),
};
}
return new vnode(tagName, properties, children, null, namespace);
}
function getElementProperties(el) {
var obj = {};
for (var i = 0; i < props.length; i++) {
var propName = props[i];
if (!el[propName]) {
continue;
}
if (propName === 'style') {
var css = {};
if ('undefined' !== typeof el.style.length) {
for (var j = 0; j < el.style.length; j++) {
var styleProp = el.style[j];
css[styleProp] = el.style.getPropertyValue(styleProp); // TODO: add support for "!important" via getPropertyPriority()!
}
}
if (Object.keys(css).length) {
obj[propName] = css;
}
continue;
}
// https://msdn.microsoft.com/en-us/library/cc848861%28v=vs.85%29.aspx
// The img element does not support the HREF content attribute.
// In addition, the href property is read-only for the img Document Object Model (DOM) object
if (el.tagName.toLowerCase() === 'img' && propName === 'href') {
continue;
}
if (propName === 'attributes') {
var atts = Array.prototype.slice.call(el[propName]);
var hash = {};
for (var k = 0; k < atts.length; k++) {
var name_1 = atts[k].name;
if (obj[name_1] || obj[attrBlacklist[name_1]]) {
continue;
}
hash[name_1] = el.getAttribute(name_1);
}
obj[propName] = hash;
continue;
}
if (propName === 'tabIndex' && el.tabIndex === -1) {
continue;
}
// Special case: contentEditable
// browser use 'inherit' by default on all nodes, but does not allow setting it to ''
// diffing virtualize dom will trigger error
// ref: https://github.com/Matt-Esch/virtual-dom/issues/176
if (propName === 'contentEditable' && el[propName] === 'inherit') {
continue;
}
if ('object' === typeof el[propName]) {
continue;
}
// default: just copy the property
obj[propName] = el[propName];
}
return obj;
}
/**
* DOMNode property white list
* Taken from https://github.com/Raynos/react/blob/dom-property-config/src/browser/ui/dom/DefaultDOMPropertyConfig.js
*/
var props = [
'accept',
'accessKey',
'action',
'alt',
'async',
'autoComplete',
'autoPlay',
'cellPadding',
'cellSpacing',
'checked',
'className',
'colSpan',
'content',
'contentEditable',
'controls',
'crossOrigin',
'data',
'defer',
'dir',
'download',
'draggable',
'encType',
'formNoValidate',
'href',
'hrefLang',
'htmlFor',
'httpEquiv',
'icon',
'id',
'label',
'lang',
'list',
'loop',
'max',
'mediaGroup',
'method',
'min',
'multiple',
'muted',
'name',
'noValidate',
'pattern',
'placeholder',
'poster',
'preload',
'radioGroup',
'readOnly',
'rel',
'required',
'rowSpan',
'sandbox',
'scope',
'scrollLeft',
'scrolling',
'scrollTop',
'selected',
'span',
'spellCheck',
'src',
'srcDoc',
'srcSet',
'start',
'step',
'style',
'tabIndex',
'target',
'title',
'type',
'value',
// Non-standard Properties
'autoCapitalize',
'autoCorrect',
'property',
'attributes',
];
var attrBlacklist = {
class: 'className',
};
// TODO: listen for mutations?
// TODO: listen to resize?
var EventCapturer = /** @class */ (function () {
function EventCapturer(events) {
var _this = this;
this.events = [
'mousemove',
'keydown',
'mousedown',
'click',
'mouseup',
'scroll',
'mouseover',
'mouseout',
'dblclick',
'resize',
'contextmenu',
];
this.element = window;
// TODO: maybe take in message to turn this off and on
// In edit mode likely want to capture everything
this.captureEvents = false;
this.usePassive = false;
this.includePath = true;
// FIXME: this causing infinite loop - why?
this.observeMutations = false;
this.handleEvent = function (event) {
// TODO: maybe need to put this on window or document to ensure *all*
// events are caught before reaching anywhere else
if (_this.captureEvents && event.type !== 'scroll' && event.type !== 'resize') {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
}
var message = _this.formatMessage(event);
_this.postMessage(message);
};
if (events) {
this.events = events;
}
}
EventCapturer.prototype.getElementForEvent = function (eventName) {
return eventName === 'resize' || eventName === 'scroll' ? window : this.element;
};
// TODO: in edit mode make these not passive and make them preventDefault() and stopPropagation()
EventCapturer.prototype.listen = function () {
for (var _i = 0, _a = this.events; _i < _a.length; _i++) {
var event_1 = _a[_i];
this.getElementForEvent(event_1).addEventListener(event_1, this.handleEvent, {
capture: true,
passive: this.usePassive,
});
}
if (this.observeMutations) {
this.startObservingMutations();
}
return this;
};
EventCapturer.prototype.removeListeners = function () {
for (var _i = 0, _a = this.events; _i < _a.length; _i++) {
var event_2 = _a[_i];
this.getElementForEvent(event_2).removeEventListener(event_2, this.handleEvent);
}
this.stopObservingMutations();
};
EventCapturer.prototype.startObservingMutations = function () {
var _this = this;
if (!this.observer && typeof MutationObserver !== 'undefined') {
this.observer = new MutationObserver(function () {
_this.triggerMutationEvent();
});
this.observer.observe(document.documentElement, {
attributes: true,
childList: true,
subtree: true,
characterData: true,
});
}
};
EventCapturer.prototype.stopObservingMutations = function () {
if (this.observer) {
this.observer.disconnect();
this.observer = undefined;
}
};
EventCapturer.prototype.getBuilderClientRects = function () {
var _this = this;
// TODO: only get those within editing canvas
// possibly add class "editing" to canvas when editing it (by selector)
var map = {};
[].slice.call(document.querySelectorAll('[id*="builder-"]')).forEach(function (el) {
map[el.id] = _this.getClientRect(el);
});
return map;
};
EventCapturer.prototype.triggerMutationEvent = function () {
window.parent.postMessage({
type: 'builder.documentMutation',
data: {
builderElementRectMap: this.fastClone(this.getBuilderClientRects()),
},
}, '*');
};
EventCapturer.prototype.postMessage = function (message) {
window.parent.postMessage(message, '*');
};
EventCapturer.prototype.formatMessage = function (event) {
return {
type: 'builder.documentEvent',
data: {
event: this.virtualizeEvent(event),
},
};
};
EventCapturer.prototype.findParent = function (target, callback, checkElement) {
if (checkElement === void 0) { checkElement = true; }
if (!(target instanceof HTMLElement)) {
return null;
}
var parent = checkElement ? target : target.parentElement;
do {
if (!parent) {
return null;
}
var matches = callback(parent);
if (matches) {
return parent;
}
} while ((parent = parent.parentElement));
return null;
};
EventCapturer.prototype.getCssForElement = function (el, seekProperty) {
if (seekProperty === void 0) { seekProperty = null; }
var sheets = document.styleSheets;
var ret = [];
for (var i in sheets) {
var sheet = sheets[i];
var rules = sheet.rules || sheet.cssRules;
for (var ruleIndex in rules) {
// TODO: handle other rules like media queries etc? are the style rules nested in those?
var rule = rules[ruleIndex];
if (!rule.style) {
continue;
}
if (seekProperty && !rule.style[seekProperty]) {
continue;
}
if (rule.selectorText && el.matches(rule.selectorText)) {
ret.push(rule);
}
}
}
return ret;
};
EventCapturer.prototype.findPageBlockParent = function (target) {
var _this = this;
var innerWidth = window.innerWidth;
return this.findParent(target, function (el) {
if (el.tagName === 'BUILDER-CANVAS') {
return true;
}
var rect = _this.getClientRect(el);
return (
// At least 50% of page width
el.clientWidth / innerWidth > 0.5 &&
// At least 30px tall
rect.height > 30 &&
// Has no parent whose bottom lines is within 10px of it's buttom
// (i.e. get topmost parent that shares the same bottom point)
!_this.findParent(el, function (parent) {
return parent !== el && Math.abs(_this.getClientRect(parent).bottom - rect.bottom) < 10;
}, false));
});
};
EventCapturer.prototype.findBuilderParent = function (target) {
return this.findParent(target, function (el) { return !!el.getAttribute('builder-model'); });
};
EventCapturer.prototype.findBuilderRenderParent = function (target) {
return this.findParent(target, function (el) { return !!el.getAttribute('builder-render'); });
};
EventCapturer.prototype.findContentEditableParent = function (target) {
return this.findParent(target, function (el) { return !!el.getAttribute('contenteditable'); });
};
EventCapturer.prototype.fastClone = function (obj) {
return JSON.parse(JSON.stringify(obj));
};
// For firefox support, we need to copy manually, ClientRect in firefox
// is not enumerable or copyable
EventCapturer.prototype.getClientRect = function (element) {
if (!element) {
return null;
}
var properties = ['top', 'left', 'bottom', 'right', 'height', 'width'];
var newObject = {};
var rect = element.getBoundingClientRect();
for (var _i = 0, properties_1 = properties; _i < properties_1.length; _i++) {
var key = properties_1[_i];
newObject[key] = rect[key];
}
return newObject;
};
EventCapturer.prototype.getAllParents = function (element) {
var el = element;
var els = [];
while (el) {
els.push(el);
el = el.parentElement;
}
return els;
};
EventCapturer.prototype.virtualizeEvent = function (event) {
// TODO: only get target on mouseover too?
var target = event.target;
var builderTarget = event.type !== 'mouseover' && this.findBuilderParent(target);
var builderRenderTarget = event.type !== 'mouseover' && this.findBuilderRenderParent(target);
// This still needed?
var pageBlockTarget = event.type === 'mouseover' && this.findPageBlockParent(target);
if (event.type === 'keydown') ;
var path = this.includePath && event.type !== 'mousemove'
? this.getAllParents(event.target).map(function (el) { return createVNode(el); })
: [];
// PERF: don't gather all of these things on every mousemove
// TODO: mousemove
return this.fastClone(__assign({
// TODO: stop sending computed style except just the one element that needs it
// TODO: no more fast cloning if can avoid
path: path, which: event.which, type: event.type, altKey: event.altKey, metaKey: event.metaKey, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, clientX: event.clientX, clientY: event.clientY }, (event.type !== 'mousemove' && {
target: createVNode(target),
targetRect: target.getBoundingClientRect && this.getClientRect(target),
targetSelector: uniqueSelector(target),
builderTarget: (builderTarget && createVNode(builderTarget)) || undefined,
builderTargetSelector: (builderTarget && uniqueSelector(builderTarget)) || undefined,
builderTargetRect: (builderTarget && this.getClientRect(builderTarget)) || undefined,
builderRenderTarget: (builderRenderTarget && createVNode(builderRenderTarget)) ||
undefined,
builderRenderTargetSelector: (builderRenderTarget && uniqueSelector(builderRenderTarget)) || undefined,
builderRenderTargetRect: (builderRenderTarget && this.getClientRect(builderRenderTarget)) || undefined,
// builderElementRectMap: this.getBuilderClientRects(),
// TODO: only grab these from mouseenter and mouseleave events because they are expensive
pageBlockTarget: (pageBlockTarget && createVNode(pageBlockTarget)) || undefined,
pageBlockTargetRect: (pageBlockTarget && this.getClientRect(pageBlockTarget)) || undefined,
pageBlockTargetSelector: (pageBlockTarget && uniqueSelector(pageBlockTarget)) || undefined,
})));
};
return EventCapturer;
}());
var isSafari = typeof window !== 'undefined' &&
/^((?!chrome|android).)*safari/i.test(window.navigator.userAgent);
function nextTick(fn) {
// TODO: should this be setImmediate instead? Forgot if that is micro or macro task
if (typeof process !== 'undefined' && process.nextTick) {
console.log('process.nextTick?');
process.nextTick(fn);
return;
}
// FIXME: fix the real safari issue of this randomly not working
if (isSafari || typeof MutationObserver === 'undefined') {
console.log('isSafari or no mutation observer, using setTimeout');
setTimeout(fn);
return;
}
var called = 0;
var observer = new MutationObserver(function () { return fn(); });
var element = document.createTextNode('');
observer.observe(element, {
characterData: true,
});
// tslint:disable-next-line
element.data = String((called = ++called));
}
var sessionStorageKey = 'builderSessionId';
// Annoying workaround for module loading
var stableStringify = stringify.default || stringify;
var anyParser = parser;
var isBrowser = typeof window !== 'undefined';
var isIframe = isBrowser && window.top !== window.self;
var fetch = (isBrowser && window.fetch) || require('node-fetch').default;
// Workaround for oddly random module loading issues
var UaParser = typeof anyParser.default === 'function' ? anyParser.default : anyParser;
function BuilderComponent(info) {
if (info === void 0) { info = {}; }
return function (component) {
var spec = __assign({}, info, { class: component });
if (!spec.name) {
spec.name = component.name;
}
if (!Builder.components.find(function (item) { return item.name === spec.name; })) {
Builder.components.push(spec);
// TODO: serialize component name and inputs
if (isBrowser) {
window.parent.postMessage({
type: 'builder.registerComponent',
data: omit(spec, 'class'),
}, '*');
}
}
};
}
var Builder = /** @class */ (function () {
function Builder(apiKey) {
if (apiKey === void 0) { apiKey = null; }
var _this = this;
this.apiKey = apiKey;
this.eventsQueue = [];
this.throttledClearEventsQueue = throttle(function () {
_this.processEventsQueue();
}, 100);
this.isUsed = false;
this.sessionId = this.getSessionId();
this.canTrack$ = new rxjs.BehaviorSubject(!this.browserTrackingDisabled);
this.editingMode$ = new rxjs.BehaviorSubject(isIframe);
// TODO: decorator to do this stuff with the get/set (how do with typing too? compiler?)
this.editingModel$ = new rxjs.BehaviorSubject(null);
this.userAgent = (typeof navigator === 'object' && navigator.userAgent) || '';
this.autoTrack = !this.isDevelopmentEnv;
this.blockContentLoading = '';
this.observersByModelType = {};
this.getContentQueue = null;
this.priorContentQueue = null;
// get env() {
// // builder.env query param, hash, cookie
// return 'prod';
// }
this.env = 'production';
// TODO: how prune deprecated tests
this.testCookiePrefix = 'builder.tests';
this.cookieQueue = [];
if (isBrowser) {
this.bindMessageListeners();
}
if (isIframe) {
this.loadFullStory();
this.messageFrameLoaded();
}
// TODO: on destroy clear subscription
this.canTrack$.subscribe(function (value) {
if (value) {
if (typeof sessionStorage !== 'undefined') {
if (!sessionStorage.getItem(sessionStorageKey)) {
sessionStorage.setItem(sessionStorageKey, _this.sessionId);
}
}
if (_this.eventsQueue.length) {
_this.throttledClearEventsQueue();
}
if (_this.cookieQueue.length) {
_this.cookieQueue.forEach(function (item) {
_this.setCookie(item[0], item[1]);
});
_this.cookieQueue.length = 0;
}
}
});
}
Builder.loadGoogleFont = function (fontName) {
if (!this.isBrowser) {
return;
}
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = "https://fonts.googleapis.com/css?family=" + fontName.replace(/\s/g, '+');
document.body.appendChild(link);
};
Object.defineProperty(Builder, "editingPage", {
get: function () {
return this._editingPage;
},
set: function (editingPage) {
this._editingPage = editingPage;
if (isBrowser && isIframe) {
if (editingPage) {
document.body.classList.add('builder-editing-page');
}
else {
document.body.classList.remove('builder-editing-page');
}
}
},
enumerable: true,
configurable: true
});
// TODO: style guide, etc off this system as well?
Builder.component = function (info) {
var _this = this;
if (info === void 0) { info = {}; }
return function (component) {
var spec = __assign({}, info, { class: component });
if (!spec.name) {
spec.name = component.name;
}
if (!_this.components.find(function (item) { return item.name === spec.name; })) {
_this.components.push(spec);
// TODO: serialize component name and inputs
if (isBrowser) {
window.parent.postMessage({
type: 'builder.registerComponent',
data: omit(spec, 'class'),
}, '*');
}
}
};
};
Object.defineProperty(Builder, "Component", {
get: function () {
return this.component;
},
enumerable: true,
configurable: true
});
Builder.prototype.processEventsQueue = function () {
if (!this.eventsQueue.length) {
return;
}
var events = this.eventsQueue;
this.eventsQueue = [];
// TODO: centralize this
var host = this.env === 'development' ? 'http://localhost:5000' : 'https://builder.io';
fetch(host + "/api/v1/track", {
method: 'POST',
body: JSON.stringify({ events: events }),
headers: {
'content-type': 'application/json',
},
mode: 'cors',
});
};
Object.defineProperty(Builder.prototype, "browserTrackingDisabled", {
get: function () {
return navigator.doNotTrack === '1';
},
enumerable: true,
configurable: true
});
Object.defineProperty(Builder.prototype, "canTrack", {
get: function () {
return this.canTrack$.value && !this.browserTrackingDisabled;
},
set: function (canTrack) {
if (this.canTrack !== canTrack) {
this.canTrack$.next(canTrack);
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Builder.prototype, "editingMode", {
get: function () {
return this.editingMode$.value;
},
set: function (value) {
if (value !== this.editingMode) {
this.editingMode$.next(value);
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Builder.prototype, "editingModel", {
get: function () {
return this.editingModel$.value;
},
set: function (value) {
if (value !== this.editingModel) {
this.editingModel$.next(value);
}
},
enumerable: true,
configurable: true
});
Builder.prototype.findParentElement = function (target, callback, checkElement) {
if (checkElement === void 0) { checkElement = true; }
if (!(target instanceof HTMLElement)) {
return null;
}
var parent = checkElement ? target : target.parentElement;
do {
if (!parent) {
return null;
}
var matches = callback(parent);
if (matches) {
return parent;
}
} while ((parent = parent.parentElement));
return null;
};
Builder.prototype.findBuilderParent = function (target) {
return this.findParentElement(target, function (el) { return Boolean(el.id && el.id.indexOf('builder-') === 0); });
};
Builder.prototype.setUserAgent = function (userAgent) {
this.userAgent = userAgent;
};
Builder.prototype.track = function (eventName, properties) {
if (properties === void 0) { properties = {}; }
// TODO: queue up track requests and fire them off when canTrack set to true - otherwise may get lots of clicks with no impressions
if (isIframe || !isBrowser) {
return;
}
// batch events
this.eventsQueue.push({
type: 'impression',
data: __assign({}, properties, { userAttributes: this.getUserAttributes(), sessionId: this.sessionId }),
});
if (this.canTrack) {
this.throttledClearEventsQueue();
}
};
Builder.prototype.getSessionId = function () {
var _this = this;
// TODO: don't set this until gdpr allowed....
var sessionId = typeof sessionStorage !== 'undefined' && sessionStorage.getItem(sessionStorageKey);
if (!sessionId) {
sessionId = btoa(String(Date.now() + Math.random()));
}
// Give the app a second to start up and set canTrack to false if needed
setTimeout(function () {
if (_this.canTrack && typeof sessionStorage !== 'undefined') {
sessionStorage.set(sessionStorageKey, sessionId);
}
});
return sessionId;
};
Builder.prototype.trackImpression = function (contentId, variationId) {
if (isIframe || !isBrowser) {
return;
}
// TODO: use this.track method
this.eventsQueue.push({
type: 'impression',
data: {
contentId: contentId,
variationId: variationId !== contentId ? variationId : undefined,
ownerId: this.apiKey,
userAttributes: this.getUserAttributes(),
sessionId: this.sessionId,
},
});
this.throttledClearEventsQueue();
};
Object.defineProperty(Builder.prototype, "isDevelopmentEnv", {
// TODO: set this for QA
get: function () {
// Automatic determining of development environment
return (Builder.isIframe ||
(Builder.isBrowser && (location.hostname === 'localhost' || location.port !== '')));
},
enumerable: true,
configurable: true
});
Builder.prototype.trackInteraction = function (contentId, variationId, alreadyTrackedOne, event) {
if (alreadyTrackedOne === void 0) { alreadyTrackedOne = false; }
if (isIframe || !isBrowser) {
return;
}
var target = event && event.target;
var targetBuilderElement = target && this.findBuilderParent(target);
var metadata = {};
if (event) {
var clientX = event.clientX, clientY = event.clientY;
if (target) {
var targetRect = target.getBoundingClientRect();
var xOffset = clientX - targetRect.left;
var yOffset = clientY - targetRect.top;
var xRatio = round(xOffset / targetRect.width, 4);
var yRatio = round(yOffset / targetRect.height, 4);
metadata.targetOffset = {
x: xRatio,
y: yRatio,
};
}
if (targetBuilderElement) {
var targetRect = targetBuilderElement.getBoundingClientRect();
var xOffset = clientX - targetRect.left;
var yOffset = clientY - targetRect.top;
var xRatio = round(xOffset / targetRect.width, 4);
var yRatio = round(yOffset / targetRect.height, 4);
metadata.builderTargetOffset = {
x: xRatio,
y: yRatio,
};
}
}
// TODO: use this.track method
this.eventsQueue.push({
type: 'click',
data: {
contentId: contentId,
variationId: variationId !== contentId ? variationId : undefined,
ownerId: this.apiKey,
unique: !alreadyTrackedOne,
targetSelector: (target && uniqueSelector(target)) || undefined,
targetBuilderElement: (targetBuilderElement && targetBuilderElement.id) || undefined,
userAttributes: this.getUserAttributes(),
metadata: metadata,
sessionId: this.sessionId,
},
});
this.throttledClearEventsQueue();
};
Builder.prototype.component = function (info) {
if (info === void 0) { info = {}; }
return Builder.component(info);
};
Builder.prototype.messageFrameLoaded = function () {
window.parent.postMessage({
type: 'builder.loaded',
data: {
value: true,
},
}, '*');
};
Builder.prototype.bindMessageListeners = function () {
var _this = this;
// TODO: handle race condition of content already loading
// TODO: move to another file
if (isBrowser) {
addEventListener('message', function (event) {
// FIXME: what was this for?
// if (!this.isUsed) {
// return;
// }
var url$$1 = new URL(event.origin);
var allowedHosts = ['builder.io', 'localhost'];
if (!includes(allowedHosts, url$$1.hostname)) {
return;
}
var data = event.data;
if (data) {
switch (data.type) {
case 'builder.ping': {
window.parent.postMessage({
type: 'builder.pong',
data: {},
}, '*');
break;
}
case 'builder.triggerAnimation': {
Builder.animator.triggerAnimation(data.data);
break;
}
case 'builder.contentUpdate':
var model = data.data.modelName;
var contentData = data.data.data; // hmmm...
var observer = _this.observersByModelType[model];
if (observer) {
observer.next([contentData]);
}
break;
case 'builder.getComponents':
// TODO: serialize component name and inputs
window.parent.postMessage({
type: 'builder.components',
data: Builder.components.map(function (item) { return omit(item, 'class'); }),
}, '*');
break;
case 'builder.editingModel':
_this.editingModel = data.data.model;
break;
case 'builder.registerComponent':
var componentData = data.data;
Builder.components.push(componentData);
break;
case 'builder.blockContentLoading':
if (typeof data.data.model === 'string') {
_this.blockContentLoading = data.data.model;
}
break;
case 'builder.editingMode':
var editingMode = data.data;
if (editingMode) {
_this.editingMode = true;
document.body.classList.add('builder-editing');
}
else {
_this.editingMode = false;
document.body.classList.remove('builder-editing');
}
break;
case 'builder.editingPageMode':
var editingPageMode = data.data;
Builder.editingPage = editingPageMode;
break;
case 'builder.overrideUserAttributes':
var userAttributes = data.data;
Object.assign(Builder.overrideUserAttributes, data.data);
_this.flushGetContentQueue(true);
// TODO: refetch too
break;
case 'builder.overrideTestGroup':
var _a = data.data, variationId = _a.variationId, contentId = _a.contentId;
if (variationId && contentId) {
_this.setTestCookie(contentId, variationId);
_this.flushGetContentQueue(true);
}
case 'builder.evaluate': {
var text_1 = data.data.text;
var args = data.data.arguments || [];
var id_1 = data.data.id;
// tslint:disable-next-line:no-function-constructor-with-string-args
var fn = new Function(text_1);
var result = fn.apply(_this, args);
if (result && typeof result.then === 'function') {
result
.then(function (finalResult) {
window.parent.postMessage({
type: 'builder.evaluateResult',
data: {
result: finalResult,
id: id_1,
text: text_1,
},
}, '*');
})
.catch(console.error);
}
else {
window.parent.postMessage({
type: 'builder.evaluateResult',
data: {
result: result,
id: id_1,
text: text_1,
},
}, '*');
}
}
}
}
});
}
};
Builder.prototype.init = function (apiKey, canTrack) {
if (canTrack === void 0) { canTrack = true; }
this.canTrack = canTrack;
this.apiKey = apiKey;
return this;
};
Builder.prototype.getLocation = function () {
return (typeof location === 'object' && url.parse(location.href)) || {};
};
Builder.prototype.getUserAttributes = function (userAgent) {
if (userAgent === void 0) { userAgent = this.userAgent; }
this.isUsed = true;
if (!userAgent) {
console.warn('No user agent set! For help on how to set this please contact steve@builder.io');
}
var ua = new UaParser(userAgent);
// FIXME
var url$$1 = this.getLocation();
var device = ua.getDevice();
// TODO: get these from exension as well
return __assign({ queryString: url$$1.search, urlPath: url$$1.pathname,
// Removinf for now because of cache keys
// referrer: document.referrer,
// language: navigator.language.split('-')[0],
device: device.type || 'desktop', operatingSystem: (ua.getOS().name || '').toLowerCase() || undefined, browser: (ua.getBrowser().name || '').toLowerCase() || undefined }, Builder.overrideUserAttributes);
};
Builder.prototype.setUserAttributes = function (options) {
Object.assign(Builder.overrideUserAttributes, options);
};
Builder.prototype.loadFullStory = function () {
// TODO: check that the iframe's parent is buidler.io
if (!Builder.isIframe) {
return;
}
// If fullstory already loaded return
if (window['_fs_org']) {
return;
}
var script = document.createElement('script');
script.innerHTML = "\n window['_fs_run_in_iframe'] = true\n window['_fs_debug'] = false;\n window['_fs_host'] = 'fullstory.com';\n window['_fs_org'] = 'B9193';\n window['_fs_namespace'] = 'FS';\n (function(m,n,e,t,l,o,g,y){\n if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window[\"_fs_namespace\"].');} return;}\n g=m[e]=function(a,b){g.q?g.q.push([a,b]):g._api(a,b);};g.q=[];\n o=n.createElement(t);o.async=1;o.src='https://'+_fs_host+'/s/fs.js';\n y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);\n g.identify=function(i,v){g(l,{uid:i});if(v)g(l,v)};g.setUserVars=function(v){g(l,v)};\n y=\"rec\";g.shutdown=function(i,v){g(y,!1)};g.restart=function(i,v){g(y,!0)};\n y=\"consent\";g[y]=function(a){g(y,!arguments.length||a)};\n g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};\n g.clearUserCookie=function(){};\n })(window,document,window['_fs_namespace'],'script','user');\n ";
document.head.appendChild(script);
// const script2 = document.createElement('script');
// script2.innerHTML = `
// (function(h,o,t,j,a,r){
// h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
// h._hjSettings={hjid:939017,hjsv:6};
// a=o.getElementsByTagName('head')[0];
// r=o.createElement('script');r.async=1;
// r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
// a.appendChild(r);
// })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
// `;
// document.head.appendChild(script2);
};
// TODO: also take priority or group name/number so can
// group fast stuff to show right away and slow or lower priority after'
// how does graphql defer work?
// TODO: add defaultContent to regular getContent method
Builder.prototype.queueGetContent = function (modelName, options) {
var _this = this;
if (options === void 0) { options = {}; }
// TODO: possibly in node send this to getContent instead, though that would have the consequence of less cache
// warming / utilization
var initialContent = options.initialContent;
if (!initialContent) {
if (!this.getContentQueue) {
this.getContentQueue = [];
nextTick(function () {
_this.flushGetContentQueue();
});
}
this.getContentQueue.push(modelName);
}
return new rxjs.Observable(function (observer) {
_this.observersByModelType[modelName] = observer;
if (initialContent) {
nextTick(function () {
observer.next(initialContent);
});
}
});
};
Builder.prototype.requestUrl = function (url$$1) {
return fetch(url$$1).then(function (res) { return res.json(); });
};
Object.defineProperty(Builder.prototype, "host", {
get: function () {
// TODO: how test images....
return this.env === 'development' ? 'http://localhost:5000' : 'https://builder.io';
},
enumerable: true,
configurable: true
});
Builder.prototype.flushGetContentQueue = function (usePastQueue) {
var _this = this;
if (usePastQueue === void 0) { usePastQueue = false; }
if (!this.apiKey) {
throw new Error('Builder needs to be initialized with an API key!');
}
if (!usePastQueue && !this.getContentQueue) {
return;
}
var queryParams = {};
var pageQueryParams = typeof location !== 'undefined' ? queryString.parse(location.search) : undefined || {};
// TODO: merge in the attribute from query string ones
queryParams.userAttributes = stableStringify(this.getUserAttributes());
var queue = (usePastQueue ? this.priorContentQueue : this.getContentQueue) || [];
if (!usePastQueue) {
this.priorContentQueue = queue;
this.getContentQueue = null;
}
// TODO: cachebust if bd.noCache in request, also perhaps if in iframe
// if (options.cachebust) {
// queryParams.t = Date.now().toString();
// }
var cachebust = isIframe || pageQueryParams.cachebust;
if (cachebust) {
queryParams.cachebuster = Date.now().toString();
}
var hasParams = Object.keys(queryParams).length > 0;
var host = this.getLocation().host === 'localhost:4205' ? 'http://localhost:5000' : 'https://builder.io';
var modelNames = queue.join(',');
// FIXME: have a "core" SDK that doesn't implement http,
// so SDKs like angular can use it's own http method
var promise = this.requestUrl(host + "/api/v1/content/" + this.apiKey + "/" + modelNames +
(queryParams && hasParams ? "?" + queryString.stringify(queryParams) : ''))
.then(function (result) {
for (var _i = 0, queue_1 = queue; _i < queue_1.length; _i++) {
var modelName = queue_1[_i];
if (modelName === _this.blockContentLoading) {
continue;
}
var observer = _this.observersByModelType[modelName];
if (!observer) {
return;
}
var data = result[modelName];
var sorted = sortBy(data, function (item) { return item.priority; });
var testModifiedResults = _this.processResultsForTests(sorted);
observer.next(testModifiedResults);
// observer.next(sorted);
}
})
.catch(function (err) {
for (var _i = 0, queue_2 = queue; _i < queue_2.length; _i++) {
var modelName = queue_2[_i];
var observer = _this.observersByModelType[modelName];
if (!observer) {
return;
}
observer.error(err);
}
});
};
Builder.prototype.processResultsForTests = function (results) {
var _this = this;
var mappedResults = results.map(function (item) {
if (!item.variations) {
return item;
}
var cookieValue = _this.getTestCookie(item.id);
var cookieVariation = cookieValue === item.id ? item : item.variations[cookieValue];
if (cookieVariation) {
return __assign({}, item, { data: cookieVariation.data, variationId: cookieValue });
}
if (item.variations) {
var n = 0;
var random = Math.random();
for (var id in item.variations) {
var variation = item.variations[id];
var testRatio = variation.testRatio;
n += testRatio;
if (random < n) {
_this.setTestCookie(item.id, variation.id);
return __assign({}, item, { data: variation.data, variationId: variation.id });
}
}
}
_this.setTestCookie(item.id, item.id);
return item;
});
if (isIframe) {
window.parent.postMessage({ type: 'builder.contentResults', data: { results: mappedResults } }, '*');
}
return mappedResults;
};
Builder.prototype.getTestCookie = function (contentId) {
return this.getCookie(this.testCookiePrefix + "." + contentId);
};
Builder.prototype.setTestCookie = function (contentId, variationId) {
if (!this.canTrack) {
this.cookieQueue.push([contentId, variationId]);
return;
}
return this.setCookie(this.testCookiePrefix + "." + contentId, variationId, {
expires: 30,
});
};
Builder.prototype.getCookie = function (name) {
return Cookies.get(name);
};
Builder.prototype.setCookie = function (name, value, options /* Cookies.CookieAttributes */) {
return Cookies.set(name, value, options);
};
// TODO:˝ param overrides
// TODO: gather user attributes
// Forward query to a server call so params like no cache or overrides can be extraced and applied
Builder.prototype.getContent = function (modelName, options) {
var _this = this;
if (options === void 0) { options = {}; }
if (!this.apiKey) {
throw new Error('Builder needs to be initialized with an API key!');
}
var queryParams = (typeof options.queryString === 'string'
? queryString.parse(options.queryString)
: typeof location !== 'undefined'
? queryString.parse(location.search)
: undefined) || {};
// TODO: merge in the attribute from query string ones
queryParams.userAttributes = stableStringify(this.getUserAttributes());
// TODO: cachebust if bd.noCache in request, also perhaps if in iframe
if (options.cachebust || isIframe) {
queryParams.cachebuster = Date.now().toString();
}
var hasParams = Object.keys(queryParams).length > 0;
// TODO: check for query params or global variable for overriding content (e.g. what to use for preview)
// TODO: query params and sorting
// TODO: with extension set up live connection to this query and send updates
// TODO: return observable with subscribe function that returns unsubscriber?
// TODO: (shorter than uuid) org namespace
// /api/v1/everlane-1/homepage-2
// "space" names? everlane-qa everlane-prod etc
// TODO: if in preview (maybe any iframe?) always no cache
// HACK: have a global option for setting dev
var host = options.dev || this.getLocation().host === 'localhost:4205'
? 'http://localhost:5000'
: 'https://builder.io';
return new rxjs.Observable(function (observer) {
// TODO: will there by use cases of multiple separate requests for same model type?
_this.observersByModelType[modelName] = observer;
// FIXME: have a "core" SDK that doesn't implement http,
// so SDKs like angular can use it's own http method
var promise = _this.requestUrl(host + "/api/v1/content/" + _this.apiKey + "/" + modelName +
(queryParams && hasParams ? "?" + queryString.stringify(queryParams) : ''))
.then(function (data) { return data[modelName]; })
.then(function (list) { return sortBy(list, function (item) { return item.priority; }); })
.then(function (result) {
if (modelName === _this.blockContentLoading) {
return;
}
var testModifiedResults = _this.processResultsForTests(result);
observer.next(testModifiedResults);
})
.catch(function (err) {
observer.error(err);
});
});
};
Builder.eventCapturer = isIframe ? new EventCapturer().listen() : null;
Builder.animator = new Animator();
Builder.components = [];
Builder.nextTick = nextTick;
Builder._editingPage = false;
Builder.isIframe = isIframe;
Builder.isBrowser = isBrowser;
Builder.overrideUserAttributes = {};
return Builder;
}());
var builder = new Builder();
exports.Builder = Builder;
exports.BuilderComponent = BuilderComponent;
exports.isBrowser = isBrowser;
exports.builder = builder;