2
0

build: 🏗️ Import typebot-js source

This commit is contained in:
Baptiste Arnaud
2022-03-10 17:47:59 +01:00
parent 31298e39c1
commit d134a265cd
34 changed files with 3321 additions and 64 deletions

View File

@ -0,0 +1,40 @@
import { ButtonParams } from "../../types";
export const createButton = (params?: ButtonParams): HTMLButtonElement => {
const button = document.createElement("button");
button.id = "typebot-bubble-button";
button.style.backgroundColor = params?.color ?? "#0042DA";
button.appendChild(createButtonIcon(params?.iconUrl));
button.appendChild(createCloseIcon());
return button;
};
const createButtonIcon = (src?: string): SVGElement | HTMLImageElement => {
if (!src) return createDefaultIcon();
const icon = document.createElement("img");
icon.classList.add("icon");
icon.src = src;
return icon;
};
const createDefaultIcon = (): SVGElement => {
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
icon.setAttribute("viewBox", "0 0 41 19");
icon.style.width = "63%";
icon.innerHTML = typebotLogoSvgTextContent();
icon.classList.add("icon");
return icon;
};
const createCloseIcon = (): SVGElement => {
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
icon.setAttribute("viewBox", "0 0 512 512");
icon.innerHTML = closeSvgPath;
icon.classList.add("close-icon");
return icon;
};
const typebotLogoSvgTextContent = () =>
`<rect x="40.29" y="0.967773" width="6.83761" height="30.7692" rx="3.4188" transform="rotate(90 40.29 0.967773)"></rect> <path fill-rule="evenodd" clip-rule="evenodd" d="M3.70884 7.80538C5.597 7.80538 7.12765 6.27473 7.12765 4.38658C7.12765 2.49842 5.597 0.967773 3.70884 0.967773C1.82069 0.967773 0.290039 2.49842 0.290039 4.38658C0.290039 6.27473 1.82069 7.80538 3.70884 7.80538Z" fill="white"></path> <rect x="0.290039" y="18.0615" width="6.83761" height="30.7692" rx="3.4188" transform="rotate(-90 0.290039 18.0615)" fill="white"></rect> <path fill-rule="evenodd" clip-rule="evenodd" d="M36.8712 11.2239C34.9831 11.2239 33.4524 12.7546 33.4524 14.6427C33.4524 16.5309 34.9831 18.0615 36.8712 18.0615C38.7594 18.0615 40.29 16.5309 40.29 14.6427C40.29 12.7546 38.7594 11.2239 36.8712 11.2239Z" fill="white"></path>`;
export const closeSvgPath = `<path d="M278.6 256l68.2-68.2c6.2-6.2 6.2-16.4 0-22.6-6.2-6.2-16.4-6.2-22.6 0L256 233.4l-68.2-68.2c-6.2-6.2-16.4-6.2-22.6 0-3.1 3.1-4.7 7.2-4.7 11.3 0 4.1 1.6 8.2 4.7 11.3l68.2 68.2-68.2 68.2c-3.1 3.1-4.7 7.2-4.7 11.3 0 4.1 1.6 8.2 4.7 11.3 6.2 6.2 16.4 6.2 22.6 0l68.2-68.2 68.2 68.2c6.2 6.2 16.4 6.2 22.6 0 6.2-6.2 6.2-16.4 0-22.6L278.6 256z"></path>`;

View File

@ -0,0 +1,28 @@
import { createIframe } from "../../iframe";
import { IframeParams } from "../../types";
export const createIframeContainer = (
params: IframeParams
): HTMLIFrameElement => {
const iframe = createIframe({ ...params, loadWhenVisible: true });
return iframe;
};
export const openIframe = (
bubble: HTMLDivElement,
iframe: HTMLIFrameElement
): void => {
loadTypebotIfFirstOpen(iframe);
bubble.classList.add("iframe-opened");
bubble.classList.remove("message-opened");
};
export const closeIframe = (bubble: HTMLDivElement): void => {
bubble.classList.remove("iframe-opened");
};
export const loadTypebotIfFirstOpen = (iframe: HTMLIFrameElement): void => {
if (!iframe.dataset.src) return;
iframe.src = iframe.dataset.src;
iframe.removeAttribute("data-src");
};

View File

@ -0,0 +1,134 @@
import {
BubbleActions,
BubbleParams,
localStorageKeys,
ProactiveMessageParams,
} from "../../types";
import { createButton } from "./button";
import {
closeIframe,
createIframeContainer,
loadTypebotIfFirstOpen,
openIframe,
} from "./iframe";
import {
createProactiveMessage,
openProactiveMessage,
} from "./proactiveMessage";
import "./style.css";
export const initBubble = (params: BubbleParams): BubbleActions => {
if (document.readyState !== "complete") {
window.addEventListener("load", () => initBubble(params));
return { close: () => {}, open: () => {} };
}
const existingBubble = document.getElementById("typebot-bubble") as
| HTMLDivElement
| undefined;
if (existingBubble) existingBubble.remove();
const { bubbleElement, proactiveMessageElement, iframeElement } =
createBubble(params);
!document.body
? (window.onload = () => document.body.appendChild(bubbleElement))
: document.body.appendChild(bubbleElement);
return getBubbleActions(
bubbleElement,
iframeElement,
proactiveMessageElement
);
};
const createBubble = (
params: BubbleParams
): {
bubbleElement: HTMLDivElement;
iframeElement: HTMLIFrameElement;
proactiveMessageElement?: HTMLDivElement;
} => {
const bubbleElement = document.createElement("div");
bubbleElement.id = "typebot-bubble";
const buttonElement = createButton(params.button);
bubbleElement.appendChild(buttonElement);
const proactiveMessageElement =
params.proactiveMessage && !hasBeenClosed()
? addProactiveMessage(params.proactiveMessage, bubbleElement)
: undefined;
const iframeElement = createIframeContainer(params);
buttonElement.addEventListener("click", () =>
onBubbleButtonClick(bubbleElement, iframeElement)
);
if (proactiveMessageElement)
proactiveMessageElement.addEventListener("click", () =>
onProactiveMessageClick(bubbleElement, iframeElement)
);
bubbleElement.appendChild(iframeElement);
return { bubbleElement, proactiveMessageElement, iframeElement };
};
const onBubbleButtonClick = (
bubble: HTMLDivElement,
iframe: HTMLIFrameElement
): void => {
loadTypebotIfFirstOpen(iframe);
bubble.classList.toggle("iframe-opened");
bubble.classList.remove("message-opened");
};
const onProactiveMessageClick = (
bubble: HTMLDivElement,
iframe: HTMLIFrameElement
): void => {
loadTypebotIfFirstOpen(iframe);
bubble.classList.add("iframe-opened");
bubble.classList.remove("message-opened");
};
export const getBubbleActions = (
bubbleElement?: HTMLDivElement,
iframeElement?: HTMLIFrameElement,
proactiveMessageElement?: HTMLDivElement
): BubbleActions => {
const existingBubbleElement =
bubbleElement ??
(document.querySelector("#typebot-bubble") as HTMLDivElement);
const existingIframeElement =
iframeElement ??
(existingBubbleElement.querySelector(
".typebot-iframe"
) as HTMLIFrameElement);
const existingProactiveMessage =
proactiveMessageElement ??
document.querySelector("#typebot-bubble .proactive-message");
return {
openProactiveMessage: existingProactiveMessage
? () => {
openProactiveMessage(existingBubbleElement);
}
: undefined,
open: () => {
openIframe(existingBubbleElement, existingIframeElement);
},
close: () => {
closeIframe(existingBubbleElement);
},
};
};
const addProactiveMessage = (
proactiveMessage: ProactiveMessageParams,
bubbleElement: HTMLDivElement
) => {
const proactiveMessageElement = createProactiveMessage(
proactiveMessage,
bubbleElement
);
bubbleElement.appendChild(proactiveMessageElement);
return proactiveMessageElement;
};
const hasBeenClosed = () => {
const closeDecisionFromStorage = localStorage.getItem(
localStorageKeys.rememberClose
);
return closeDecisionFromStorage ? true : false;
};

View File

@ -0,0 +1,66 @@
import { localStorageKeys, ProactiveMessageParams } from "../../types";
import { closeSvgPath } from "./button";
const createProactiveMessage = (
params: ProactiveMessageParams,
bubble: HTMLDivElement
): HTMLDivElement => {
const container = document.createElement("div");
container.classList.add("proactive-message");
if (params.delay !== undefined) setOpenTimeout(bubble, params);
if (params.avatarUrl) container.appendChild(createAvatar(params.avatarUrl));
if (params.rememberClose) setRememberCloseInStorage();
container.appendChild(createTextElement(params.textContent));
container.appendChild(createCloseButton(bubble));
return container;
};
const setOpenTimeout = (
bubble: HTMLDivElement,
params: ProactiveMessageParams
) => {
setTimeout(() => {
openProactiveMessage(bubble);
}, params.delay);
};
const createAvatar = (avatarUrl: string): HTMLImageElement => {
const element = document.createElement("img");
element.src = avatarUrl;
return element;
};
const createTextElement = (text: string): HTMLParagraphElement => {
const element = document.createElement("p");
element.innerHTML = text;
return element;
};
const createCloseButton = (bubble: HTMLDivElement): HTMLButtonElement => {
const button = document.createElement("button");
button.classList.add("close-button");
button.innerHTML = `<svg viewBox="0 0 512 512">${closeSvgPath}</svg>`;
button.addEventListener("click", (e) => onCloseButtonClick(e, bubble));
return button;
};
const openProactiveMessage = (bubble: HTMLDivElement): void => {
bubble.classList.add("message-opened");
};
const onCloseButtonClick = (
e: Event,
proactiveMessageElement: HTMLDivElement
) => {
e.stopPropagation();
closeProactiveMessage(proactiveMessageElement);
};
const closeProactiveMessage = (bubble: HTMLDivElement): void => {
bubble.classList.remove("message-opened");
};
const setRememberCloseInStorage = () =>
localStorage.setItem(localStorageKeys.rememberClose, "true");
export { createProactiveMessage, openProactiveMessage, closeProactiveMessage };

View File

@ -0,0 +1,185 @@
#typebot-bubble {
z-index: 99999;
position: fixed;
}
#typebot-bubble > button {
padding: 0px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 99999;
position: fixed;
bottom: 20px;
right: 20px;
width: 60px;
height: 60px;
border-radius: 100%;
background-color: rgb(230, 114, 0);
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 6px 0px,
rgba(0, 0, 0, 0.16) 0px 2px 32px 0px;
border: medium none;
}
#typebot-bubble button:hover {
filter: brightness(0.95);
}
#typebot-bubble button:active {
filter: brightness(0.75);
}
#typebot-bubble > button > .icon {
width: 80%;
transition: opacity 500ms ease-out 0s, transform 500ms ease-out 0s;
fill: white;
}
#typebot-bubble > button > img.icon {
border-radius: 100%;
}
#typebot-bubble.iframe-opened > button > .icon {
transform: rotate(90deg) scale(0);
opacity: 0;
}
#typebot-bubble > button > .close-icon {
position: absolute;
width: 80%;
height: 80%;
transform: rotate(-90deg) scale(0);
opacity: 0;
transition: opacity 500ms ease-out 0s, transform 500ms ease-out 0s;
fill: white;
}
#typebot-bubble.iframe-opened > button > .close-icon {
transform: rotate(90deg) scale(1);
opacity: 1;
}
#typebot-bubble > iframe {
visibility: hidden;
opacity: 0;
display: flex;
border-radius: 10px;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 99999;
border-radius: 10px;
position: fixed;
transform: translate(0, 100px);
transition: opacity 500ms ease-out 0s, transform 500ms ease-out 0s;
box-shadow: rgba(0, 0, 0, 0.16) 0px 5px 40px;
width: 400px;
max-height: 680px;
inset: auto 20px 90px auto;
height: calc(100% - 160px);
background-color: white;
}
#typebot-bubble.iframe-opened > iframe {
transform: translate(0, 0);
visibility: visible;
opacity: 1;
}
.typebot-chat-button.active .typebot-chat-icon {
transform: rotate(90deg) scale(0);
opacity: 0;
}
.typebot-chat-button:not(.active) .typebot-chat-close {
transform: rotate(-90deg) scale(0);
opacity: 0;
}
.typebot-iframe-container:not(.active) {
opacity: 0;
transform: translate(0, 100px);
}
.typebot-iframe-container.active {
opacity: 1;
transform: translate(0, 0);
}
/* Proactive message */
#typebot-bubble > .proactive-message {
font-size: 18px;
color: #303235;
opacity: 0;
visibility: hidden;
transform: translate(0, 10px);
transition: opacity 500ms ease-out, transform 500ms ease-out;
cursor: pointer;
font-weight: 300;
bottom: 90px;
right: 20px;
z-index: 99999;
position: fixed;
max-width: 280px;
background-color: white;
box-shadow: 0 3px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
padding: 16px;
display: flex;
align-items: center;
border-radius: 8px;
}
#typebot-bubble.message-opened > .proactive-message {
opacity: 1;
visibility: visible;
transform: translate(0, 0);
}
#typebot-bubble > .proactive-message > .close-button {
position: absolute;
top: -15px;
right: -7px;
width: 30px;
height: 30px;
background-color: rgb(237, 242, 247);
border-radius: 100%;
border: medium none;
outline: currentcolor none medium;
fill: #4a5568;
padding: 0px;
cursor: pointer;
padding: 2px;
}
#typebot-bubble > .proactive-message > img {
margin-right: 8px;
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 100%;
object-fit: cover;
}
@media screen and (max-width: 450px) {
#typebot-bubble > .proactive-message {
max-width: 200px;
font-size: 15px;
bottom: 70px;
right: 10px;
}
#typebot-bubble > button {
width: 50px !important;
height: 50px !important;
bottom: 10px !important;
right: 10px !important;
}
#typebot-bubble > iframe {
inset: 0 0 auto auto;
width: 100%;
height: calc(100% - 70px);
max-height: none;
}
}

View File

@ -0,0 +1,37 @@
import { createIframe } from "../../iframe";
import { IframeParams } from "../../types";
export const initContainer = (
containerId: string,
iframeParams: IframeParams
): HTMLElement | undefined => {
const { loadWhenVisible } = iframeParams;
const containerElement = document.getElementById(containerId);
if (!containerElement) return;
if (containerElement.children[0])
return containerElement.children[0] as HTMLIFrameElement;
const lazy = loadWhenVisible ?? true;
const iframeElement = createIframe({
...iframeParams,
loadWhenVisible: lazy,
});
if (lazy) observeOnScroll(iframeElement);
containerElement.appendChild(iframeElement);
return iframeElement;
};
const observeOnScroll = (iframeElement: HTMLIFrameElement) => {
const observer = new IntersectionObserver(
(entries) => {
if (entries.pop()?.isIntersecting === true) lazyLoadSrc(iframeElement);
},
{ threshold: [0] }
);
observer.observe(iframeElement);
};
const lazyLoadSrc = (iframeElement: HTMLIFrameElement) => {
if (!iframeElement.dataset.src) return;
iframeElement.src = iframeElement.dataset.src;
iframeElement.removeAttribute("data-src");
};

View File

@ -0,0 +1,87 @@
import { createIframe } from "../../iframe";
import { PopupActions, PopupParams } from "../../types";
import "./style.css";
export const initPopup = (params: PopupParams): PopupActions => {
if (document.readyState !== "complete") {
window.addEventListener("load", () => initPopup(params));
return { close: () => {}, open: () => {} };
}
const existingPopup = document.getElementById("typebot-popup");
if (existingPopup) existingPopup.remove();
const popupElement = createPopup(params);
!document.body
? (window.onload = () => document.body.append(popupElement))
: document.body.append(popupElement);
return {
open: () => openPopup(popupElement),
close: () => closePopup(popupElement),
};
};
const createPopup = (params: PopupParams): HTMLElement => {
const { delay } = params;
const overlayElement = createOverlayElement(delay);
listenForOutsideClicks(overlayElement);
const iframeElement = createIframe({
...params,
loadWhenVisible: true,
});
overlayElement.appendChild(iframeElement);
return overlayElement;
};
const createOverlayElement = (delay: number | undefined) => {
const overlayElement = document.createElement("div");
overlayElement.id = "typebot-popup";
if (delay !== undefined) setShowTimeout(overlayElement, delay);
return overlayElement;
};
export const openPopup = (popupElement: HTMLElement): void => {
const iframe = popupElement.children[0] as HTMLIFrameElement;
if (iframe.dataset.src) lazyLoadSrc(iframe);
document.body.style.overflowY = "hidden";
popupElement.classList.add("opened");
};
export const closePopup = (popupElement: HTMLElement): void => {
document.body.style.overflowY = "auto";
popupElement.classList.remove("opened");
};
const listenForOutsideClicks = (popupElement: HTMLDivElement) =>
popupElement.addEventListener("click", (e) => onPopupClick(e, popupElement));
const onPopupClick = (e: Event, popupElement: HTMLDivElement) => {
e.preventDefault();
const clickedElement = e.target as HTMLElement;
if (clickedElement.tagName !== "iframe") closePopup(popupElement);
};
const setShowTimeout = (overlayElement: HTMLDivElement, delay: number) => {
setTimeout(() => {
openPopup(overlayElement);
}, delay);
};
const lazyLoadSrc = (iframe: HTMLIFrameElement) => {
iframe.src = iframe.dataset.src as string;
iframe.removeAttribute("data-src");
};
export const getPopupActions = (
popupElement?: HTMLDivElement
): PopupActions => {
const existingPopupElement =
popupElement ??
(document.querySelector("#typebot-popup") as HTMLDivElement);
return {
open: () => {
openPopup(existingPopupElement);
},
close: () => {
closePopup(existingPopupElement);
},
};
};

View File

@ -0,0 +1,26 @@
#typebot-popup {
position: fixed;
top: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
visibility: hidden;
opacity: 0;
transition: opacity 200ms;
z-index: 99999;
}
#typebot-popup.opened {
opacity: 1;
visibility: visible;
}
#typebot-popup > iframe {
width: 100%;
height: 600px;
max-width: 800px;
border-radius: 10px;
}

View File

@ -0,0 +1,58 @@
import { DataFromTypebot, IframeCallbacks, IframeParams } from "../types";
import "./style.css";
export const createIframe = ({
backgroundColor,
viewerHost = "https://typebot-viewer.vercel.app",
...iframeParams
}: IframeParams): HTMLIFrameElement => {
const { publishId, loadWhenVisible, hiddenVariables } = iframeParams;
const iframeUrl = `${viewerHost}/${publishId}${parseQueryParams(
hiddenVariables
)}`;
const iframe = document.createElement("iframe");
iframe.setAttribute(loadWhenVisible ? "data-src" : "src", iframeUrl);
iframe.setAttribute("data-id", iframeParams.publishId);
const randomThreeLettersId = Math.random().toString(36).substring(7);
const uniqueId = `${publishId}-${randomThreeLettersId}`;
iframe.setAttribute("id", uniqueId);
if (backgroundColor) iframe.style.backgroundColor = backgroundColor;
iframe.classList.add("typebot-iframe");
const { onNewVariableValue, onVideoPlayed } = iframeParams;
listenForTypebotMessages({ onNewVariableValue, onVideoPlayed });
return iframe;
};
const parseQueryParams = (starterVariables?: {
[key: string]: string | undefined;
}): string => {
return parseHostnameQueryParam() + parseStarterVariables(starterVariables);
};
const parseHostnameQueryParam = () => {
return `?hn=${window.location.hostname}`;
};
const parseStarterVariables = (starterVariables?: {
[key: string]: string | undefined;
}) =>
starterVariables
? `&${Object.keys(starterVariables)
.filter((key) => starterVariables[key])
.map((key) => `${key}=${starterVariables[key]}`)
.join("&")}`
: "";
export const listenForTypebotMessages = (callbacks: IframeCallbacks) => {
window.addEventListener("message", (event) => {
const data = event.data as { from?: "typebot" } & DataFromTypebot;
if (data.from === "typebot") processMessage(event.data, callbacks);
});
};
const processMessage = (data: DataFromTypebot, callbacks: IframeCallbacks) => {
if (data.redirectUrl) window.open(data.redirectUrl);
if (data.newVariableValue && callbacks.onNewVariableValue)
callbacks.onNewVariableValue(data.newVariableValue);
if (data.videoPlayed && callbacks.onVideoPlayed) callbacks.onVideoPlayed();
};

View File

@ -0,0 +1,6 @@
.typebot-iframe {
width: 100%;
height: 100%;
border: none;
border-radius: inherit;
}

View File

@ -0,0 +1,13 @@
import { initContainer } from "./embedTypes/container";
import { initPopup, getPopupActions } from "./embedTypes/popup";
import { initBubble, getBubbleActions } from "./embedTypes/chat";
export {
initContainer,
initPopup,
initBubble,
getPopupActions,
getBubbleActions,
};
export * from "./types";

View File

@ -0,0 +1,60 @@
export type IframeParams = {
publishId: string;
viewerHost?: string;
backgroundColor?: string;
hiddenVariables?: { [key: string]: string | undefined };
customDomain?: string;
loadWhenVisible?: boolean;
} & IframeCallbacks;
export type IframeCallbacks = {
onNewVariableValue?: (v: Variable) => void;
onVideoPlayed?: () => void;
};
export type PopupParams = {
delay?: number;
} & IframeParams;
export type PopupActions = {
open: () => void;
close: () => void;
};
export type BubbleParams = {
button?: ButtonParams;
proactiveMessage?: ProactiveMessageParams;
} & IframeParams;
export type ButtonParams = {
color?: string;
iconUrl?: string;
};
export type ProactiveMessageParams = {
avatarUrl?: string;
textContent: string;
delay?: number;
rememberClose?: boolean;
};
export type BubbleActions = {
open: () => void;
close: () => void;
openProactiveMessage?: () => void;
};
export type Variable = {
name: string;
value: string;
};
export type DataFromTypebot = {
redirectUrl?: string;
newVariableValue?: Variable;
videoPlayed?: boolean;
};
export const localStorageKeys = {
rememberClose: "rememberClose",
};