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,7 +3,7 @@ name: Publish package to NPM
on:
push:
tags:
- 'v*.*.*'
- 'js-lib-v*.*.*'
jobs:
publish:
@ -15,10 +15,11 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- run: yarn
- run: yarn test
- run: yarn build
- uses: JS-DevTools/npm-publish@v1
with:
package: './packages/typebot-js/package.json'
token: ${{ secrets.NPM_TOKEN }}

View File

@ -1,6 +1,6 @@
{
"name": "typebot-js",
"version": "2.1.3",
"version": "2.1.4",
"main": "dist/index.js",
"unpkg": "dist/index.umd.min.js",
"license": "MIT",

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',
}

View File

@ -1,30 +1,58 @@
import * as Typebot from "../../src";
import * as Typebot from '../../src'
describe("initBubble", () => {
describe('initBubble', () => {
beforeEach(() => {
document.body.innerHTML = "";
});
document.body.innerHTML = ''
})
it("should initialize a bubble embed", () => {
expect.assertions(2);
Typebot.initBubble({ publishId: "typebot-id" });
const bubbleElement = document.getElementById("typebot-bubble");
const frame = document.getElementsByTagName("iframe")[0];
expect(frame).toBeDefined();
expect(bubbleElement).toBeDefined();
});
it('should initialize a bubble embed', () => {
expect.assertions(2)
Typebot.initBubble({ publishId: 'typebot-id' })
const bubbleElement = document.getElementById('typebot-bubble')
const frame = document.getElementsByTagName('iframe')[0]
expect(frame).toBeDefined()
expect(bubbleElement).toBeDefined()
})
it("should overwrite bubble if exists", () => {
expect.assertions(2);
it('should overwrite bubble if exists', () => {
expect.assertions(2)
Typebot.initBubble({
publishId: "typebot-id",
hiddenVariables: { var1: "test" },
});
Typebot.initBubble({ publishId: "typebot-id2" });
const frames = document.getElementsByTagName("iframe");
expect(frames).toHaveLength(1);
publishId: 'typebot-id',
hiddenVariables: { var1: 'test' },
})
Typebot.initBubble({ publishId: 'typebot-id2' })
const frames = document.getElementsByTagName('iframe')
expect(frames).toHaveLength(1)
expect(frames[0].dataset.src).toBe(
"https://typebot-viewer.vercel.app/typebot-id2?hn=localhost"
);
});
});
'https://typebot-viewer.vercel.app/typebot-id2?hn=localhost'
)
})
it('show open after the corresponding delay', async () => {
expect.assertions(3)
Typebot.initBubble({
autoOpenDelay: 1000,
publishId: 'typebot-id',
})
const bubble = document.querySelector('#typebot-bubble') as HTMLDivElement
expect(bubble.classList.contains('iframe-opened')).toBe(false)
await new Promise((r) => setTimeout(r, 1000))
expect(bubble.classList.contains('iframe-opened')).toBe(true)
const rememberCloseDecisionFromStorage = localStorage.getItem(
Typebot.localStorageKeys.rememberClose
)
expect(rememberCloseDecisionFromStorage).toBe('true')
})
it('should remember close decision if set to true', async () => {
expect.assertions(1)
localStorage.setItem(Typebot.localStorageKeys.rememberClose, 'true')
Typebot.initBubble({
autoOpenDelay: 1000,
publishId: 'typebot-id',
})
const bubble = document.querySelector('#typebot-bubble') as HTMLDivElement
await new Promise((r) => setTimeout(r, 1500))
expect(bubble.classList.contains('iframe-opened')).toBe(false)
})
})

View File

@ -1,104 +1,77 @@
import * as Typebot from "../../src";
import * as Typebot from '../../src'
beforeEach(() => {
document.body.innerHTML = "";
});
document.body.innerHTML = ''
})
it("should create the message", () => {
expect.assertions(2);
it('should create the message', () => {
expect.assertions(2)
Typebot.initBubble({
proactiveMessage: { textContent: "Hi click here!" },
publishId: "typebot-id",
});
proactiveMessage: { textContent: 'Hi click here!' },
publishId: 'typebot-id',
})
const paragraphElement = document.querySelector(
"#typebot-bubble > .proactive-message > p"
);
'#typebot-bubble > .proactive-message > p'
)
const closeButton = document.querySelector(
"#typebot-bubble > .proactive-message > .close-button"
);
expect(paragraphElement?.textContent).toBe("Hi click here!");
expect(closeButton).toBeTruthy();
});
'#typebot-bubble > .proactive-message > .close-button'
)
expect(paragraphElement?.textContent).toBe('Hi click here!')
expect(closeButton).toBeTruthy()
})
it("should have the corresponding avatar", () => {
expect.assertions(1);
it('should have the corresponding avatar', () => {
expect.assertions(1)
Typebot.initBubble({
proactiveMessage: {
textContent: "Hi click here!",
avatarUrl: "https://website.com/my-avatar.png",
textContent: 'Hi click here!',
avatarUrl: 'https://website.com/my-avatar.png',
},
publishId: "typebot-id",
});
publishId: 'typebot-id',
})
const avatarElement = document.querySelector(
"#typebot-bubble > .proactive-message > img"
) as HTMLImageElement;
expect(avatarElement.src).toBe("https://website.com/my-avatar.png");
});
'#typebot-bubble > .proactive-message > img'
) as HTMLImageElement
expect(avatarElement.src).toBe('https://website.com/my-avatar.png')
})
it("shouldn't have opened class if delay not defined", () => {
expect.assertions(1);
expect.assertions(1)
Typebot.initBubble({
proactiveMessage: {
textContent: "Hi click here!",
textContent: 'Hi click here!',
},
publishId: "typebot-id",
});
const bubble = document.querySelector("#typebot-bubble") as HTMLDivElement;
expect(bubble.classList.contains("message-opened")).toBe(false);
});
publishId: 'typebot-id',
})
const bubble = document.querySelector('#typebot-bubble') as HTMLDivElement
expect(bubble.classList.contains('message-opened')).toBe(false)
})
it("should show almost immediately if delay is 0", async () => {
expect.assertions(1);
it('should show almost immediately if delay is 0', async () => {
expect.assertions(1)
Typebot.initBubble({
proactiveMessage: {
textContent: "Hi click here!",
textContent: 'Hi click here!',
delay: 0,
},
publishId: "typebot-id",
});
const bubble = document.querySelector("#typebot-bubble") as HTMLDivElement;
await new Promise((r) => setTimeout(r, 1));
expect(bubble.classList.contains("message-opened")).toBe(true);
});
publishId: 'typebot-id',
})
const bubble = document.querySelector('#typebot-bubble') as HTMLDivElement
await new Promise((r) => setTimeout(r, 1))
expect(bubble.classList.contains('message-opened')).toBe(true)
})
it("show after the corresponding delay", async () => {
expect.assertions(2);
it('show after the corresponding delay', async () => {
expect.assertions(2)
Typebot.initBubble({
proactiveMessage: {
textContent: "Hi click here!",
textContent: 'Hi click here!',
delay: 1000,
},
publishId: "typebot-id",
});
const bubble = document.querySelector("#typebot-bubble") as HTMLDivElement;
expect(bubble.classList.contains("message-opened")).toBe(false);
await new Promise((r) => setTimeout(r, 1000));
expect(bubble.classList.contains("message-opened")).toBe(true);
});
it("should remember close decision if set to true", () => {
expect.assertions(2);
Typebot.initBubble({
proactiveMessage: {
textContent: "Hi click here!",
delay: 1000,
},
publishId: "typebot-id",
});
const rememberCloseDecisionFromStorage = localStorage.getItem(
Typebot.localStorageKeys.rememberClose
);
expect(rememberCloseDecisionFromStorage).toBeNull();
Typebot.initBubble({
proactiveMessage: {
textContent: "Hi click here!",
delay: 1000,
rememberClose: true,
},
publishId: "typebot-id",
});
const refreshedRememberCloseDecisionFromStorage = localStorage.getItem(
Typebot.localStorageKeys.rememberClose
);
expect(refreshedRememberCloseDecisionFromStorage).toBe("true");
});
publishId: 'typebot-id',
})
const bubble = document.querySelector('#typebot-bubble') as HTMLDivElement
expect(bubble.classList.contains('message-opened')).toBe(false)
await new Promise((r) => setTimeout(r, 1000))
expect(bubble.classList.contains('message-opened')).toBe(true)
})