"use strict";
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-underscore-dangle */
/* eslint-disable no-param-reassign */
(() => {
const contentBoxStyles = `
/* Important - e.g. Webflow applies a box sizing that messes this up
certain elements look particularly bad without thise
*/
box-sizing: content-box;
`;
const spinAnimationStyles = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
`;
const loadingStyles = `
position: absolute;
top: 50%;
left: 50%;
border: 6px solid #aaa;
border-radius: 50%;
border-top: 6px solid #fff;
width: 20px;
height: 20px;
-webkit-animation: spin 2s linear infinite; /* Safari */
animation: spin 2s linear infinite;
/* Important - e.g. Webflow applies a box sizing that messes this up */
box-sizing: content-box;
`;
const getMobilePopupStyles = (embedType) => `
@media(max-width: 480px) {
.fillout-embed-${embedType},
.fillout-embed-dynamic-${embedType} {
width: 100vw !important;
height: 100vh !important;
}
.fillout-embed-${embedType} .fillout-embed-iframe-container,
.fillout-embed-dynamic-${embedType} .fillout-embed-iframe-container {
max-width: 100vw;
transition: unset;
/* we make the iframe container full width on small screens, but not
full height, because we do want to leave room for the X icon (for us we
don't want to overlay it on top of logos or back buttons) */
width: 100% !important;
/* we leave some wiggle room here (the icon is ~24px) */
height: calc(100vh - 40px) !important;
margin-top: 40px !important;
}
.fillout-embed-${embedType} .fillout-embed-iframe-container iframe,
.fillout-embed-dynamic-${embedType} .fillout-embed-iframe-container iframe {
border-radius: 0;
}
/* on small devices we position the X above the form, and no right padding */
.fillout-embed-${embedType} .fillout-embed-${embedType}-close-icon,
.fillout-embed-dynamic-${embedType} .fillout-embed-${embedType}-close-icon {
color: #fff !important;
position: absolute;
top: -38px !important;
right: 20px !important;
left: unset !important;
width: 24px;
height: 24px;
cursor: pointer;
background: #171717;
border-radius: 50%;
padding: 6px 6px 6px 6px;
${contentBoxStyles}
}
.fillout-embed-slider .fillout-embed-iframe-container iframe {}
}
`;
const popupBackgroundStyles = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, .65);
transition: opacity .25s ease-in-out;
z-index: 10000000000000;
display: flex;
justify-content: center;
`;
const popupStyles = `
${spinAnimationStyles}
.noscroll {
overflow: hidden;
}
.fillout-embed-popup {
${popupBackgroundStyles}
align-items: center;
}
.fillout-embed-dynamic-popup {
${popupBackgroundStyles}
align-items: flex-start;
}
.fillout-embed-popup .fillout-embed-iframe-container {
position: relative;
transition: opacity .25s ease-in-out;
min-width: 360px;
min-height: 360px
}
.fillout-embed-dynamic-popup .fillout-embed-iframe-container {
position: relative;
transition: opacity .25s ease-in-out;
min-width: 360px;
min-height: 360px;
/* Symmetrical padding on top and bottom, fixed to prevent too much movement */
margin-top: 40px;
max-height: calc(100vh - 80px);
}
.fillout-embed-popup .fillout-embed-iframe-container iframe,
.fillout-embed-dynamic-popup .fillout-embed-iframe-container iframe {
width: 100%;
height: 100%;
border: none;
overflow: hidden;
border-radius: 10px;
}
.fillout-embed-popup .fillout-embed-popup-close-icon,
.fillout-embed-dynamic-popup .fillout-embed-popup-close-icon {
position: absolute;
width: 24px;
height: 24px;
text-align: center;
cursor: pointer;
transition: opacity .5s ease-in-out;
text-decoration: none;
color: #fff !important;
top: -15px;
right: -15px;
background: #171717;
border-radius: 50%;
padding: 6px 6px 6px 6px;
${contentBoxStyles}
}
.fillout-embed-popup .fillout-embed-popup-close-icon:hover,
.fillout-embed-dynamic-popup .fillout-embed-popup-close-icon:hover {
transform: scale(1.05);
}
.fillout-embed-popup .fillout-embed-loading,
.fillout-embed-dynamic-popup .fillout-embed-loading {
${loadingStyles}
}
${getMobilePopupStyles('popup')}
`;
const sliderStyles = `
${spinAnimationStyles}
.noscroll {
overflow: hidden;
}
.fillout-embed-slider {
${popupBackgroundStyles}
}
.fillout-embed-slider .fillout-embed-iframe-container {
position: absolute;
top: 0px;
/* slides in from the right always at the moment, can add a feature for
left transition later */
transition: transform .35s ease-in-out;
height: 100%;
opacity: 1;
}
.fillout-embed-slider .fillout-embed-iframe-container iframe {
width: 100%;
height: 100%;
border: none;
overflow: hidden;
border-radius: 0px;
}
.fillout-embed-slider .fillout-embed-slider-close-icon {
/* for sliders, the close icon is a little tab with an "X" in it centered
vertically on the edge of the slider (unless on mobile, where we copy
the styling of popup) */
position: absolute !important;
width: 24px;
height: 24px;
text-align: center;
cursor: pointer;
transition: opacity .5s ease-in-out;
text-decoration: none;
color: #fff !important;
top: 50%;
background: #171717;
padding: 20px 4px 20px 4px;
${contentBoxStyles}
}
.fillout-embed-slider .fillout-embed-slider-close-icon:hover {
transform: scaleY(1.05);
}
.fillout-embed-slider .fillout-embed-loading {
${loadingStyles}
}
${getMobilePopupStyles('slider')}
`;
const standardStyles = `
${spinAnimationStyles}
.fillout-embed-standard {
/* This will take up the full size of whatever div you're inserting the
* iframe into. That div will grow in size depending on how large theh
* iframe is.*/
width: 100%;
height: 100%;
}
.fillout-embed-standard .fillout-embed-iframe-container {
position: relative;
transition: opacity .25s ease-in-out;
width: 100%;
height: 100%;
}
.fillout-embed-standard .fillout-embed-iframe-container iframe {
width: 100%;
height: 100%;
border: none;
overflow: hidden;
border-radius: 10px;
}
.fillout-embed-standard .fillout-embed-loading {
${loadingStyles}
}
`;
const XIcon = `
`;
if (typeof window === 'undefined')
return;
// as long as the window is valid (and not embedding in e.g. nextJS improperly
// or something similar), we initialize:
// - popups
// - slide overs
// - etc.
// by adding onclick handlers for all of them corresponding to the button
// or element that is supposed to open them up, and also creates the
// elements, with opacity 0 set initially
const generateEmbedId = () => {
const min = 10000000000000;
const max = 99999999999999;
const randomNumber = Math.floor(Math.random() * (max - min + 1)) + min;
return `${randomNumber}`;
};
const getParentUrl = () => {
var _a;
let parentUrl;
if (typeof window === 'undefined') {
return parentUrl;
}
try {
// helps for capturing parent URL if we're in an iframe (sometimes)
parentUrl = (_a = window.top) === null || _a === void 0 ? void 0 : _a.location.href;
}
catch (e) {
parentUrl = window.location.href;
}
return parentUrl;
};
// fillout config
const getConfig = (element) => {
const sliderDirection = element.dataset.filloutSliderDirection === 'left' ? 'left' : 'right';
const buttonSize = element.dataset.filloutButtonSize || 'medium';
const buttonColor = element.dataset.filloutButtonColor || '#3b82f6';
const popupSize = element.dataset.filloutPopupSize || 'large';
const hexToRgb = (hex) => {
// just in case someone passes in rgba() format, which we used to use in
// the past in the snippet
if (typeof hex !== 'string' ||
!/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(hex)) {
return [59, 130, 246]; // default color values for '#3b82f6'
}
let bigint = parseInt(hex.slice(1), 16);
let r = (bigint >> 16) & 255;
let g = (bigint >> 8) & 255;
let b = bigint & 255;
return [r, g, b];
};
const getLuminance = (hexColor) => {
let [r, g, b] = hexToRgb(hexColor);
// Calculate relative luminance
// sRGB formula
const getComponent = (color) => {
color /= 255;
return color <= 0.03928
? color / 12.92
: Math.pow((color + 0.055) / 1.055, 2.4);
};
r = getComponent(r);
g = getComponent(g);
b = getComponent(b);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
// smartly compute this to be black or white depending on the button color
const buttonTextColor = getLuminance(buttonColor) > 0.5 ? 'black' : 'white';
return {
initialized: element.dataset.filloutInitialized !== undefined,
inheritParameters: element.dataset.filloutInheritParameters !== undefined,
dynamicResize: element.dataset.filloutDynamicResize !== undefined,
flowPublicIdentifier: element.dataset.filloutId,
buttonText: element.dataset.filloutButtonText,
buttonFloat: element.dataset.filloutButtonFloat,
buttonColor,
buttonSize,
buttonTextColor,
sliderDirection,
domain: element.dataset.filloutDomain,
popupSize,
preview: element.dataset.filloutPreview !== undefined,
};
};
const getSharedIframeSrc = (configDomain, flowPublicIdentifier, inheritParameters, target) => {
let domain = 'https://embed.fillout.com';
if (configDomain) {
if (configDomain === 'localhost:3000') {
domain = `http://${configDomain}`;
}
else {
domain = `https://${configDomain}`;
}
}
const formLink = `${domain}/t/${flowPublicIdentifier}`;
const iframeSrc = new URL(formLink);
// if we're passed the option to inherit search parameters, then we add
// those here as well.
if (inheritParameters) {
const params = new URL(window.location.href).searchParams;
for (const [key, value] of params.entries()) {
iframeSrc.searchParams.append(key, value);
}
}
// we convert data- attributes into URL parameters. we use the ones passed
// directly to the embed as taking priority (since explicitly set)
const DATA_PREFIX = 'data-';
for (const attribute of target.attributes) {
if (attribute.name.startsWith('data-') &&
!attribute.name.startsWith('data-fillout')) {
iframeSrc.searchParams.append(attribute.name.slice(DATA_PREFIX.length), attribute.value);
}
}
return iframeSrc;
};
// make popup/slider button
const initializePopupButton = (element, onclick) => {
// leave previous button snippet as it is
if (element.tagName !== 'DIV') {
element.onclick = onclick;
return;
}
const { buttonText, buttonColor, buttonTextColor, buttonFloat, buttonSize, } = getConfig(element);
const button = document.createElement('button');
button.innerText = buttonText || 'Open Demo';
Object.assign(button.style, Object.assign(Object.assign({ cursor: 'pointer', fontFamily: 'Helvetica, Arial, sans-serif' }, (buttonSize === 'small'
? {
padding: '8px 12px 8px 12px',
fontSize: '16px',
borderRadius: '28px',
}
: buttonSize === 'large'
? {
padding: '12px 14px 12px 14px',
fontSize: '20px',
borderRadius: '32px',
}
: {
padding: '10px 14px 10px 14px',
fontSize: '18px',
borderRadius: '32px',
})), { display: 'inline-block', maxWidth: '100%', whitespace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', textDecoration: 'none', color: buttonTextColor || '#ffffff', fontWeight: 'bold', textAlign: 'center', margin: '0', border: 'none' }));
if (buttonFloat) {
Object.assign(button.style, {
'bottom-right': {
position: 'fixed',
bottom: '32px',
right: '32px',
zIndex: '9999999',
},
'bottom-left': {
position: 'fixed',
bottom: '32px',
left: '32px',
zIndex: '9999999',
},
}[buttonFloat]);
}
// dynamically add a hover effect depending on the button background color
// but make sure to assign it without interfering with any other buttons
// they mave have embedded on this page (assign unique id)
const buttonId = generateEmbedId();
const buttonClassName = `fillout-embed-popup-button-${buttonId}`;
button.className = buttonClassName;
const lighten = (color, amount) => {
return ('#' +
color
.replace(/^#/, '')
.replace(/../g, color => ('0' +
Math.min(255, Math.max(0, parseInt(color, 16) + amount)).toString(16)).slice(-2)));
};
const style = document.createElement('style');
style.textContent = `
.${buttonClassName} {
margin-right: 10px !important;
font-size: 18px !important;
line-height: 32px !important;
font-weight: 700 !important;
font-family: 'Manrope', sans-serif !important;
padding: 4px 42px 4px 42px !important;
text-align: center !important;
display: inline-block !important;
border-radius: 30px 30px 30px 0 !important;
color: var(--e-global-color-white) !important;
transition: all 0.3s ease-in-out !important;
background-image: linear-gradient(to right, var(--e-gradient-1) 0%, var(--e-gradient-2) 100%) !important;
}
.${buttonClassName}:hover {
background-color: ${lighten(buttonColor, 10)};
}
`;
document.head.appendChild(style);
button.onclick = onclick;
element.appendChild(button);
};
// Popups and sliders are quite similar in their setup, so we bundle in the
// code together here and just adjust classes dependent on the embed type
const initializePopupLikeTarget = (target, embedType) => {
const { flowPublicIdentifier, initialized, inheritParameters, sliderDirection, domain, dynamicResize, popupSize, preview, } = getConfig(target);
if (initialized)
return;
const popupContainer = document.createElement('div');
popupContainer.className = dynamicResize
? `fillout-embed-dynamic-${embedType}`
: `fillout-embed-${embedType}`;
popupContainer.style.opacity = '0';
const popupLoading = document.createElement('div');
popupLoading.className = 'fillout-embed-loading';
popupLoading.style.display = 'block';
popupContainer.appendChild(popupLoading);
const iframeContainer = document.createElement('div');
iframeContainer.className = 'fillout-embed-iframe-container';
iframeContainer.style.opacity = '1';
popupContainer.appendChild(iframeContainer);
const iframe = document.createElement('iframe');
const iframeSrc = getSharedIframeSrc(domain, flowPublicIdentifier, inheritParameters, target);
const embedId = generateEmbedId();
iframeSrc.searchParams.append('fillout-embed-id', `${embedId}`);
iframeSrc.searchParams.append('fillout-embed-type', embedType);
if (preview) {
iframeSrc.searchParams.append('fillout-embed-preview', 'yes');
}
const parentPage = getParentUrl();
if (parentPage) {
iframeSrc.searchParams.append('fillout-embed-parent-page', parentPage);
}
if (dynamicResize && embedType === 'popup') {
iframeSrc.searchParams.append('fillout-embed-dynamic-resize', 'true');
// transition for when the height changes (potentially)
iframeContainer.style.transition = 'height 150ms ease';
// listen for changes from the iframe, and upon changes, we resize the
// target element here (based on height)
const receiveMessage = (event) => {
if ((!preview && event.origin !== new URL(iframeSrc.toString()).origin) ||
event.data.embedId !== embedId) {
// only for this iframe in question
return;
}
const newHeight = event.data.size;
iframeContainer.style.height = `${newHeight
? Math.min(newHeight + 48, window.innerHeight - 80)
: window.innerHeight - 80}px`;
};
window.addEventListener('message', receiveMessage, false);
}
iframe.src = iframeSrc.toString();
iframe.allow = 'microphone; camera; geolocation';
iframe.style.border = '0px';
iframe.title = `${flowPublicIdentifier}`;
const closeIcon = document.createElement('a');
closeIcon.className = `fillout-embed-${embedType}-close-icon`;
closeIcon.innerHTML = XIcon;
// wait to display it until the iframe is loaded in
closeIcon.style.opacity = '0';
iframeContainer.appendChild(closeIcon);
iframe.addEventListener('load', () => {
if (popupLoading) {
popupLoading.style.display = 'none';
}
if (closeIcon) {
closeIcon.style.opacity = '1';
}
if (embedType === 'slider') {
iframeContainer.style.transform = 'translateX(0)';
}
}, true);
iframeContainer.appendChild(iframe);
if (embedType === 'popup') {
// later on, will offer this as variable sizing, which is why we compute
// it on the fly here. default is not full width but slightly skinnier
if (popupSize === 'medium') {
// roughly the size at which scheduling pages work well, and we don't
// run the risk of cutting anyone's form width short
iframeContainer.style.width =
// 80vw of 1200 ~ 960px (we get progressively smaller with some padding
// on the RHS)
window.innerWidth < 1200 ? '80vw' : '1024px';
}
else if (popupSize === 'small') {
// roughly inline with others we've seen, and smaller than this seems
// a tad silly
iframeContainer.style.width =
// 900 * 60vw ~= 540px
// 600 * 80vw ~= 480px (about as small as we want to get)
window.innerWidth < 600
? '80vw'
: window.innerWidth < 900
? '60vw'
: '560px';
}
else {
iframeContainer.style.width = 'calc(100% - 160px)';
}
iframeContainer.style.height = 'calc(100% - 80px)';
}
else if (embedType === 'slider') {
// later on will offer different sizing for this too maybe
iframeContainer.style.width = '80vw';
if (sliderDirection === 'left') {
iframeContainer.style.left = '0px';
iframeContainer.style.transform = 'translateX(-100%)';
closeIcon.style.right = '-32px';
closeIcon.style.borderTopRightRadius = '15px';
closeIcon.style.borderBottomRightRadius = '15px';
}
else {
iframeContainer.style.right = '0px';
iframeContainer.style.transform = 'translateX(100%)';
closeIcon.style.left = '-32px';
closeIcon.style.borderTopLeftRadius = '15px';
closeIcon.style.borderBottomLeftRadius = '15px';
}
}
// close handlers
const closePopup = () => {
document.body.classList.remove('noscroll');
if (embedType === 'popup') {
popupContainer.style.opacity = '0';
}
else if (embedType === 'slider') {
if (sliderDirection === 'left') {
iframeContainer.style.transform = 'translateX(-100%)';
}
else {
iframeContainer.style.transform = 'translateX(100%)';
}
}
// more time needed to slide away for slider
const timeout = embedType === 'popup' ? 250 : 350;
setTimeout(() => {
// before removing it, reset the styles for everything so that they
// work nicely out of the box when it comes back up (since we're
// reusing the same container
popupLoading.style.display = 'block';
closeIcon.style.opacity = '0';
popupContainer.remove();
// we give enough time for the opacity animation to finish
}, timeout);
};
// onclick here, we also close the container (either the X or clicking
// outside is fine).
popupContainer.onclick = () => {
closePopup();
};
closeIcon.onclick = () => {
closePopup();
};
initializePopupButton(target, () => {
document.body.appendChild(popupContainer);
document.body.classList.add('noscroll');
popupContainer.style.opacity = '1';
});
target.setAttribute('data-fillout-initialized', 'true');
};
const initializeStandardTarget = (target, isFullScreen) => {
const { initialized, flowPublicIdentifier, inheritParameters, dynamicResize, domain, preview, } = getConfig(target);
if (initialized)
return;
const standardContainer = document.createElement('div');
standardContainer.className = 'fillout-embed-standard';
standardContainer.style.opacity = '0';
const standardLoading = document.createElement('div');
standardLoading.className = 'fillout-embed-loading';
standardLoading.style.display = 'block';
standardContainer.appendChild(standardLoading);
// relative while we show loading icon
target.style.position = 'relative';
const iframeContainer = document.createElement('div');
iframeContainer.className = 'fillout-embed-iframe-container';
iframeContainer.style.opacity = '1';
standardContainer.appendChild(iframeContainer);
const iframe = document.createElement('iframe');
const iframeSrc = getSharedIframeSrc(domain, flowPublicIdentifier, inheritParameters, target);
const embedId = generateEmbedId();
iframeSrc.searchParams.append('fillout-embed-id', `${embedId}`);
iframeSrc.searchParams.append('fillout-embed-type', 'standard');
if (preview) {
iframeSrc.searchParams.append('fillout-embed-preview', 'yes');
}
const parentPage = getParentUrl();
if (parentPage) {
iframeSrc.searchParams.append('fillout-embed-parent-page', parentPage);
}
if (dynamicResize) {
iframeSrc.searchParams.append('fillout-embed-dynamic-resize', 'true');
// transition for when the height changes (potentially)
target.style.transition = 'height 150ms ease';
// listen for changes from the iframe, and upon changes, we resize the
// target element here (based on height)
const receiveMessage = (event) => {
if ((!preview && event.origin !== new URL(iframeSrc.toString()).origin) ||
event.data.embedId !== embedId) {
// only for this iframe in question
return;
}
const newHeight = event.data.size;
target.style.height = `${newHeight}px`;
};
window.addEventListener('message', receiveMessage, false);
}
iframe.src = iframeSrc.toString();
iframe.allow = 'microphone; camera; geolocation';
iframe.style.border = '0px';
if (isFullScreen) {
iframe.style.borderRadius = '0px';
}
iframe.title = `${flowPublicIdentifier}`;
iframe.addEventListener('load', () => {
if (standardLoading) {
standardLoading.style.display = 'none';
}
}, true);
iframeContainer.appendChild(iframe);
target.appendChild(standardContainer);
standardContainer.style.opacity = '1';
// so that any other script imports don't accidentally try initializing
// this again
target.setAttribute('data-fillout-initialized', 'true');
};
// @ts-ignore
const popupsInitialized = window.__filloutPopupsInitialized;
const popupTargets = document.querySelectorAll("[data-fillout-embed-type='popup']");
if (popupTargets.length > 0) {
if (!popupsInitialized) {
// add the popup stylesheet
const popupStylesheet = document.createElement('style');
popupStylesheet.innerHTML = popupStyles;
document.head.appendChild(popupStylesheet);
// @ts-ignore
window.__filloutPopupEmbedsInitialized = true;
}
popupTargets.forEach(target => {
if (target instanceof HTMLElement) {
initializePopupLikeTarget(target, 'popup');
}
});
}
// @ts-ignore
const slidersInitialized = window.__filloutSlidersInitialized;
const sliderTargets = document.querySelectorAll("[data-fillout-embed-type='slider']");
if (sliderTargets.length > 0) {
if (!slidersInitialized) {
const sliderStylesheet = document.createElement('style');
sliderStylesheet.innerHTML = sliderStyles;
document.head.appendChild(sliderStylesheet);
// @ts-ignore
window.__filloutSlidersInitialized = true;
}
sliderTargets.forEach(target => {
if (target instanceof HTMLElement) {
initializePopupLikeTarget(target, 'slider');
}
});
}
// @ts-ignore
const standardInitialized = window.__filloutStandardInitialized;
const standardTargets = document.querySelectorAll("[data-fillout-embed-type='standard']");
if (standardTargets.length > 0) {
if (!standardInitialized) {
const standardStylesheet = document.createElement('style');
standardStylesheet.innerHTML = standardStyles;
// TODO mobile styles?
document.head.appendChild(standardStylesheet);
// @ts-ignore
window.__filloutStandardInitialized = true;
}
standardTargets.forEach(target => {
if (target instanceof HTMLElement) {
initializeStandardTarget(target);
}
});
}
// @ts-ignore
const fullScreenInitialized = window.__filloutFullScreenInitialized;
const fullScreenTargets = document.querySelectorAll("[data-fillout-embed-type='fullscreen']");
if (fullScreenTargets.length > 0) {
// pretty much everything is the same as the standard embed
if (!fullScreenInitialized) {
// only need to add styles once
const standardStylesheet = document.createElement('style');
standardStylesheet.innerHTML = standardStyles;
// TODO mobile styles?
document.head.appendChild(standardStylesheet);
// @ts-ignore
window.__filloutFullScreenInitialized = true;
}
fullScreenTargets.forEach(target => {
if (target instanceof HTMLElement) {
initializeStandardTarget(target, true);
}
});
}
})();
//# sourceMappingURL=embed.js.map