2
0

feat(lib): Add auto open delay for bubble embed

This commit is contained in:
Baptiste Arnaud
2022-03-14 11:38:57 +01:00
parent 80679dfbd0
commit d6b94130cb
8 changed files with 289 additions and 278 deletions

View File

@ -3,85 +3,91 @@ import {
BubbleParams,
localStorageKeys,
ProactiveMessageParams,
} from "../../types";
import { createButton } from "./button";
} from '../../types'
import { createButton } from './button'
import {
closeIframe,
createIframeContainer,
loadTypebotIfFirstOpen,
openIframe,
} from "./iframe";
} from './iframe'
import {
createProactiveMessage,
openProactiveMessage,
} from "./proactiveMessage";
import "./style.css";
} from './proactiveMessage'
import './style.css'
export const initBubble = (params: BubbleParams): BubbleActions => {
if (document.readyState !== "complete") {
window.addEventListener("load", () => initBubble(params));
return { close: () => {}, open: () => {} };
if (document.readyState !== 'complete') {
window.addEventListener('load', () => initBubble(params))
return { close: () => {}, open: () => {} }
}
const existingBubble = document.getElementById("typebot-bubble") as
const existingBubble = document.getElementById('typebot-bubble') as
| HTMLDivElement
| undefined;
if (existingBubble) existingBubble.remove();
| undefined
if (existingBubble) existingBubble.remove()
const { bubbleElement, proactiveMessageElement, iframeElement } =
createBubble(params);
createBubble(params)
if (
(params.autoOpenDelay || params.autoOpenDelay === 0) &&
!hasBeenClosed()
) {
setRememberCloseInStorage()
setTimeout(
() => openIframe(bubbleElement, iframeElement),
params.autoOpenDelay
)
}
!document.body
? (window.onload = () => document.body.appendChild(bubbleElement))
: document.body.appendChild(bubbleElement);
return getBubbleActions(
bubbleElement,
iframeElement,
proactiveMessageElement
);
};
: document.body.appendChild(bubbleElement)
return getBubbleActions(bubbleElement, iframeElement, proactiveMessageElement)
}
const createBubble = (
params: BubbleParams
): {
bubbleElement: HTMLDivElement;
iframeElement: HTMLIFrameElement;
proactiveMessageElement?: HTMLDivElement;
bubbleElement: HTMLDivElement
iframeElement: HTMLIFrameElement
proactiveMessageElement?: HTMLDivElement
} => {
const bubbleElement = document.createElement("div");
bubbleElement.id = "typebot-bubble";
const buttonElement = createButton(params.button);
bubbleElement.appendChild(buttonElement);
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", () =>
: undefined
const iframeElement = createIframeContainer(params)
buttonElement.addEventListener('click', () =>
onBubbleButtonClick(bubbleElement, iframeElement)
);
)
if (proactiveMessageElement)
proactiveMessageElement.addEventListener("click", () =>
proactiveMessageElement.addEventListener('click', () =>
onProactiveMessageClick(bubbleElement, iframeElement)
);
bubbleElement.appendChild(iframeElement);
return { bubbleElement, proactiveMessageElement, 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");
};
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");
};
loadTypebotIfFirstOpen(iframe)
bubble.classList.add('iframe-opened')
bubble.classList.remove('message-opened')
}
export const getBubbleActions = (
bubbleElement?: HTMLDivElement,
@ -90,29 +96,29 @@ export const getBubbleActions = (
): BubbleActions => {
const existingBubbleElement =
bubbleElement ??
(document.querySelector("#typebot-bubble") as HTMLDivElement);
(document.querySelector('#typebot-bubble') as HTMLDivElement)
const existingIframeElement =
iframeElement ??
(existingBubbleElement.querySelector(
".typebot-iframe"
) as HTMLIFrameElement);
'.typebot-iframe'
) as HTMLIFrameElement)
const existingProactiveMessage =
proactiveMessageElement ??
document.querySelector("#typebot-bubble .proactive-message");
document.querySelector('#typebot-bubble .proactive-message')
return {
openProactiveMessage: existingProactiveMessage
? () => {
openProactiveMessage(existingBubbleElement);
openProactiveMessage(existingBubbleElement)
}
: undefined,
open: () => {
openIframe(existingBubbleElement, existingIframeElement);
openIframe(existingBubbleElement, existingIframeElement)
},
close: () => {
closeIframe(existingBubbleElement);
closeIframe(existingBubbleElement)
},
};
};
}
}
const addProactiveMessage = (
proactiveMessage: ProactiveMessageParams,
@ -121,14 +127,17 @@ const addProactiveMessage = (
const proactiveMessageElement = createProactiveMessage(
proactiveMessage,
bubbleElement
);
bubbleElement.appendChild(proactiveMessageElement);
return proactiveMessageElement;
};
)
bubbleElement.appendChild(proactiveMessageElement)
return proactiveMessageElement
}
const hasBeenClosed = () => {
const closeDecisionFromStorage = localStorage.getItem(
localStorageKeys.rememberClose
);
return closeDecisionFromStorage ? true : false;
};
)
return closeDecisionFromStorage ? true : false
}
export const setRememberCloseInStorage = () =>
localStorage.setItem(localStorageKeys.rememberClose, 'true')

View File

@ -1,66 +1,64 @@
import { localStorageKeys, ProactiveMessageParams } from "../../types";
import { closeSvgPath } from "./button";
import { setRememberCloseInStorage } from '../chat/index'
import { 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 container = document.createElement('div')
container.classList.add('proactive-message')
if (params.delay !== undefined) setOpenTimeout(bubble, params)
if (params.avatarUrl) container.appendChild(createAvatar(params.avatarUrl))
container.appendChild(createTextElement(params.textContent))
container.appendChild(createCloseButton(bubble))
return container
}
const setOpenTimeout = (
bubble: HTMLDivElement,
params: ProactiveMessageParams
) => {
setTimeout(() => {
openProactiveMessage(bubble);
}, params.delay);
};
openProactiveMessage(bubble)
}, params.delay)
}
const createAvatar = (avatarUrl: string): HTMLImageElement => {
const element = document.createElement("img");
element.src = avatarUrl;
return element;
};
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 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 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");
};
bubble.classList.add('message-opened')
}
const onCloseButtonClick = (
e: Event,
proactiveMessageElement: HTMLDivElement
) => {
e.stopPropagation();
closeProactiveMessage(proactiveMessageElement);
};
e.stopPropagation()
closeProactiveMessage(proactiveMessageElement)
}
const closeProactiveMessage = (bubble: HTMLDivElement): void => {
bubble.classList.remove("message-opened");
};
setRememberCloseInStorage()
bubble.classList.remove('message-opened')
}
const setRememberCloseInStorage = () =>
localStorage.setItem(localStorageKeys.rememberClose, "true");
export { createProactiveMessage, openProactiveMessage, closeProactiveMessage };
export { createProactiveMessage, openProactiveMessage, closeProactiveMessage }

View File

@ -1,58 +1,58 @@
import { DataFromTypebot, IframeCallbacks, IframeParams } from "../types";
import "./style.css";
import { DataFromTypebot, IframeCallbacks, IframeParams } from '../types'
import './style.css'
export const createIframe = ({
backgroundColor,
viewerHost = "https://typebot-viewer.vercel.app",
viewerHost = 'https://typebot-viewer.vercel.app',
isV1,
...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 { publishId, loadWhenVisible, hiddenVariables } = iframeParams
const host = isV1 ? `https://bot.typebot.io` : viewerHost
const iframeUrl = `${host}/${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;
[key: string]: string | undefined
}): string => {
return parseHostnameQueryParam() + parseStarterVariables(starterVariables);
};
return parseHostnameQueryParam() + parseStarterVariables(starterVariables)
}
const parseHostnameQueryParam = () => {
return `?hn=${window.location.hostname}`;
};
return `?hn=${window.location.hostname}`
}
const parseStarterVariables = (starterVariables?: {
[key: string]: string | undefined;
[key: string]: string | undefined
}) =>
starterVariables
? `&${Object.keys(starterVariables)
.filter((key) => starterVariables[key])
.map((key) => `${key}=${starterVariables[key]}`)
.join("&")}`
: "";
.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);
});
};
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.redirectUrl) window.open(data.redirectUrl)
if (data.newVariableValue && callbacks.onNewVariableValue)
callbacks.onNewVariableValue(data.newVariableValue);
if (data.videoPlayed && callbacks.onVideoPlayed) callbacks.onVideoPlayed();
};
callbacks.onNewVariableValue(data.newVariableValue)
if (data.videoPlayed && callbacks.onVideoPlayed) callbacks.onVideoPlayed()
}

View File

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