build: 🏗️ Import typebot-js source
This commit is contained in:
24
.github/workflows/publish-lib-to-npm.yml
vendored
Normal file
24
.github/workflows/publish-lib-to-npm.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
name: Publish package to NPM
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./packages/typebot-js
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12
|
||||||
|
- run: yarn
|
||||||
|
- run: yarn test
|
||||||
|
- run: yarn build
|
||||||
|
- uses: JS-DevTools/npm-publish@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.NPM_TOKEN }}
|
@ -28,7 +28,7 @@
|
|||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss": "^8.4.6",
|
"postcss": "^8.4.6",
|
||||||
"rollup": "^2.67.2",
|
"rollup": "^2.70.0",
|
||||||
"rollup-plugin-dts": "^4.1.0",
|
"rollup-plugin-dts": "^4.1.0",
|
||||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
|
2
packages/typebot-js/.eslintignore
Normal file
2
packages/typebot-js/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.js
|
||||||
|
*.html
|
12
packages/typebot-js/.eslintrc.js
Normal file
12
packages/typebot-js/.eslintrc.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["@typescript-eslint", "prettier"],
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["off"],
|
||||||
|
},
|
||||||
|
};
|
10
packages/typebot-js/.npmignore
Normal file
10
packages/typebot-js/.npmignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.github
|
||||||
|
src
|
||||||
|
tests
|
||||||
|
.eslintignore
|
||||||
|
.eslintrc.js
|
||||||
|
jest.config.js
|
||||||
|
rollup.config.js
|
||||||
|
tsconfig.json
|
||||||
|
examples
|
||||||
|
__mocks__
|
37
packages/typebot-js/README.md
Normal file
37
packages/typebot-js/README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Typebot JS library
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/typebot-js) [](https://bundlephobia.com/result?p=typebot-js@latest) [](https://travis-ci.com/plausible/typebot-js)
|
||||||
|
|
||||||
|
Frontend library to embed typebots from [Typebot](https://www.typebot.io/).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install, simply run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install typebot-js
|
||||||
|
|
||||||
|
yarn add typebot-js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
It exposes 3 functions:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
initContainer();
|
||||||
|
initPopup();
|
||||||
|
initBubble();
|
||||||
|
```
|
||||||
|
|
||||||
|
You can configure them directly in the "Share" tab of your typebot.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { initContainer } from "typebot-js";
|
||||||
|
|
||||||
|
const plausible = initContainer("container-id", {
|
||||||
|
publishId: "my-app.com",
|
||||||
|
});
|
||||||
|
```
|
1
packages/typebot-js/__mocks__/styleMock.js
Normal file
1
packages/typebot-js/__mocks__/styleMock.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = {};
|
39
packages/typebot-js/examples/chatEmbedExample.html
Normal file
39
packages/typebot-js/examples/chatEmbedExample.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||||
|
/>
|
||||||
|
<title>TITLE</title>
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="../dist/index.umd.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
const { open } = Typebot.initBubble({
|
||||||
|
publishId: "feedback-form",
|
||||||
|
button: {
|
||||||
|
color: "green",
|
||||||
|
iconUrl: "https://image.flaticon.com/icons/png/512/5138/5138352.png",
|
||||||
|
},
|
||||||
|
proactiveMessage: {
|
||||||
|
avatarUrl:
|
||||||
|
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80",
|
||||||
|
textContent: "Hey what's up?",
|
||||||
|
delay: 1000,
|
||||||
|
rememberClose: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<div style="width: 100%; height: 300vh; background-color: aquamarine">
|
||||||
|
<button onclick="(()=>{open()})()" style="width: 200px; height: 60px">
|
||||||
|
OPEN CHAT
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
39
packages/typebot-js/examples/containerEmbedExample.html
Normal file
39
packages/typebot-js/examples/containerEmbedExample.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html style="overflow-y: scroll">
|
||||||
|
<head>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||||
|
/>
|
||||||
|
<title>TITLE</title>
|
||||||
|
<style type="text/css">
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="width: 100%; height: 300vh; background-color: aquamarine">
|
||||||
|
<div id="typebot-container" style="width: 1000px; height: 600px"></div>
|
||||||
|
<div id="typebot-container-2" style="width: 1000px; height: 600px"></div>
|
||||||
|
</div>
|
||||||
|
<div id="typebot-container-3" style="width: 1000px; height: 600px"></div>
|
||||||
|
<script src="../dist/index.umd.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
Typebot.initContainer("typebot-container", { publishId: "jamhouse" });
|
||||||
|
Typebot.initContainer("typebot-container-2", {
|
||||||
|
publishId: "lead-generation",
|
||||||
|
});
|
||||||
|
Typebot.initContainer("typebot-container-3", {
|
||||||
|
publishId: "feedback-form",
|
||||||
|
loadWhenVisible: false,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
27
packages/typebot-js/examples/popupEmbedExample.html
Normal file
27
packages/typebot-js/examples/popupEmbedExample.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||||
|
/>
|
||||||
|
<title>TITLE</title>
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="../dist/index.umd.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const { open } = Typebot.initPopup({
|
||||||
|
publishId: "feedback-form",
|
||||||
|
delay: 1000,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<div style="width: 100%; height: 300vh; background-color: aquamarine">
|
||||||
|
<button onclick="(()=>{open()})()">CLICK</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
packages/typebot-js/jest.config.js
Normal file
7
packages/typebot-js/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: "ts-jest",
|
||||||
|
testEnvironment: "jsdom",
|
||||||
|
moduleNameMapper: {
|
||||||
|
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js",
|
||||||
|
},
|
||||||
|
};
|
29
packages/typebot-js/package.json
Normal file
29
packages/typebot-js/package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "typebot-js",
|
||||||
|
"version": "2.1.3",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"unpkg": "dist/index.umd.min.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "yarn lint && rollup -c",
|
||||||
|
"lint": "eslint src --ext .ts && eslint tests --ext .ts",
|
||||||
|
"test": "yarn jest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-typescript": "^8.3.1",
|
||||||
|
"@types/jest": "^27.4.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
||||||
|
"@typescript-eslint/parser": "^5.14.0",
|
||||||
|
"eslint": "<8.0.0",
|
||||||
|
"eslint-plugin-functional": "^4.2.0",
|
||||||
|
"eslint-plugin-jest": "^26.1.1",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"jest": "^27.5.1",
|
||||||
|
"rollup": "^2.70.0",
|
||||||
|
"rollup-plugin-styles": "^4.0.0",
|
||||||
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"ts-jest": "^27.1.3",
|
||||||
|
"ts-loader": "^9.2.7",
|
||||||
|
"typescript": "^4.6.2"
|
||||||
|
}
|
||||||
|
}
|
26
packages/typebot-js/rollup.config.js
Normal file
26
packages/typebot-js/rollup.config.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import typescript from "@rollup/plugin-typescript";
|
||||||
|
import { terser } from "rollup-plugin-terser";
|
||||||
|
import styles from "rollup-plugin-styles";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// ES Modules
|
||||||
|
{
|
||||||
|
input: "src/index.ts",
|
||||||
|
output: {
|
||||||
|
file: "dist/index.js",
|
||||||
|
format: "cjs",
|
||||||
|
},
|
||||||
|
plugins: [typescript({ tsconfig: "./tsconfig.json" }), styles()],
|
||||||
|
},
|
||||||
|
|
||||||
|
// UMD
|
||||||
|
{
|
||||||
|
input: "src/index.ts",
|
||||||
|
output: {
|
||||||
|
file: "dist/index.umd.min.js",
|
||||||
|
format: "umd",
|
||||||
|
name: "Typebot",
|
||||||
|
},
|
||||||
|
plugins: [typescript({ tsconfig: "./tsconfig.json" }), terser(), styles()],
|
||||||
|
},
|
||||||
|
];
|
40
packages/typebot-js/src/embedTypes/chat/button.ts
Normal file
40
packages/typebot-js/src/embedTypes/chat/button.ts
Normal 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>`;
|
28
packages/typebot-js/src/embedTypes/chat/iframe.ts
Normal file
28
packages/typebot-js/src/embedTypes/chat/iframe.ts
Normal 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");
|
||||||
|
};
|
134
packages/typebot-js/src/embedTypes/chat/index.ts
Normal file
134
packages/typebot-js/src/embedTypes/chat/index.ts
Normal 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;
|
||||||
|
};
|
66
packages/typebot-js/src/embedTypes/chat/proactiveMessage.ts
Normal file
66
packages/typebot-js/src/embedTypes/chat/proactiveMessage.ts
Normal 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 };
|
185
packages/typebot-js/src/embedTypes/chat/style.css
Normal file
185
packages/typebot-js/src/embedTypes/chat/style.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
37
packages/typebot-js/src/embedTypes/container/index.ts
Normal file
37
packages/typebot-js/src/embedTypes/container/index.ts
Normal 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");
|
||||||
|
};
|
87
packages/typebot-js/src/embedTypes/popup/index.ts
Normal file
87
packages/typebot-js/src/embedTypes/popup/index.ts
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
26
packages/typebot-js/src/embedTypes/popup/style.css
Normal file
26
packages/typebot-js/src/embedTypes/popup/style.css
Normal 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;
|
||||||
|
}
|
58
packages/typebot-js/src/iframe/index.ts
Normal file
58
packages/typebot-js/src/iframe/index.ts
Normal 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();
|
||||||
|
};
|
6
packages/typebot-js/src/iframe/style.css
Normal file
6
packages/typebot-js/src/iframe/style.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.typebot-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
13
packages/typebot-js/src/index.ts
Normal file
13
packages/typebot-js/src/index.ts
Normal 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";
|
60
packages/typebot-js/src/types.ts
Normal file
60
packages/typebot-js/src/types.ts
Normal 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",
|
||||||
|
};
|
40
packages/typebot-js/tests/chat/button.spec.ts
Normal file
40
packages/typebot-js/tests/chat/button.spec.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import * as Typebot from "../../src";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have the corresponding custom color", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
Typebot.initBubble({
|
||||||
|
button: { color: "#222222" },
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
const buttonElement = document.querySelector(
|
||||||
|
"#typebot-bubble > button"
|
||||||
|
) as HTMLElement;
|
||||||
|
expect(buttonElement.style.backgroundColor).toBe("rgb(34, 34, 34)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have the default svg icon", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
Typebot.initBubble({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
const buttonIconElement = document.querySelector(
|
||||||
|
"#typebot-bubble > button > .icon"
|
||||||
|
) as HTMLElement;
|
||||||
|
expect(buttonIconElement.tagName).toBe("svg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have the corresponding custom icon", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
Typebot.initBubble({
|
||||||
|
button: { iconUrl: "https://web.com/icon.png" },
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
const buttonIconElement = document.querySelector(
|
||||||
|
"#typebot-bubble > button > .icon"
|
||||||
|
) as HTMLImageElement;
|
||||||
|
expect(buttonIconElement.src).toBe("https://web.com/icon.png");
|
||||||
|
});
|
91
packages/typebot-js/tests/chat/commands.spec.ts
Normal file
91
packages/typebot-js/tests/chat/commands.spec.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import * as Typebot from "../../src";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("openBubble", () => {
|
||||||
|
it("should add the opened bubble", () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
const { open } = Typebot.initBubble({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
const bubble = document.getElementById("typebot-bubble") as HTMLDivElement;
|
||||||
|
expect(bubble.classList.contains("iframe-opened")).toBe(false);
|
||||||
|
open();
|
||||||
|
expect(bubble.classList.contains("iframe-opened")).toBe(true);
|
||||||
|
expect(open).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hide the proactive message", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
const { open, openProactiveMessage } = Typebot.initBubble({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
proactiveMessage: {
|
||||||
|
textContent: "Hi click here!",
|
||||||
|
avatarUrl: "https://website.com/my-avatar.png",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const bubble = document.getElementById("typebot-bubble") as HTMLDivElement;
|
||||||
|
if (openProactiveMessage) openProactiveMessage();
|
||||||
|
expect(bubble.classList.contains("message-opened")).toBe(true);
|
||||||
|
open();
|
||||||
|
expect(bubble.classList.contains("message-opened")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("closeBubble", () => {
|
||||||
|
it("should remove the corresponding class", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
const { close, open } = Typebot.initBubble({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
open();
|
||||||
|
const bubble = document.getElementById("typebot-bubble") as HTMLDivElement;
|
||||||
|
expect(bubble.classList.contains("iframe-opened")).toBe(true);
|
||||||
|
close();
|
||||||
|
expect(bubble.classList.contains("iframe-opened")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("openProactiveMessage", () => {
|
||||||
|
it("should add the opened className", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const { openProactiveMessage } = Typebot.initBubble({
|
||||||
|
proactiveMessage: {
|
||||||
|
textContent: "Hi click here!",
|
||||||
|
},
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
const bubble = document.getElementById("typebot-bubble") as HTMLDivElement;
|
||||||
|
if (openProactiveMessage) openProactiveMessage();
|
||||||
|
expect(bubble.classList.contains("message-opened")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't be returned if no message", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const { openProactiveMessage } = Typebot.initBubble({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
expect(openProactiveMessage).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Request commands afterwards", () => {
|
||||||
|
it("should return defined commands", () => {
|
||||||
|
Typebot.initBubble({
|
||||||
|
proactiveMessage: {
|
||||||
|
textContent: "Hi click here!",
|
||||||
|
},
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { close, open, openProactiveMessage } = Typebot.getBubbleActions();
|
||||||
|
expect(close).toBeDefined();
|
||||||
|
expect(open).toBeDefined();
|
||||||
|
expect(openProactiveMessage).toBeDefined();
|
||||||
|
open();
|
||||||
|
const bubble = document.getElementById("typebot-bubble") as HTMLDivElement;
|
||||||
|
expect(bubble.classList.contains("iframe-opened")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
30
packages/typebot-js/tests/chat/index.spec.ts
Normal file
30
packages/typebot-js/tests/chat/index.spec.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import * as Typebot from "../../src";
|
||||||
|
|
||||||
|
describe("initBubble", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
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 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);
|
||||||
|
expect(frames[0].dataset.src).toBe(
|
||||||
|
"https://typebot-viewer.vercel.app/typebot-id2?hn=localhost"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
104
packages/typebot-js/tests/chat/proactiveMessage.spec.ts
Normal file
104
packages/typebot-js/tests/chat/proactiveMessage.spec.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import * as Typebot from "../../src";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create the message", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
Typebot.initBubble({
|
||||||
|
proactiveMessage: { textContent: "Hi click here!" },
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
const paragraphElement = document.querySelector(
|
||||||
|
"#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();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have the corresponding avatar", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
Typebot.initBubble({
|
||||||
|
proactiveMessage: {
|
||||||
|
textContent: "Hi click here!",
|
||||||
|
avatarUrl: "https://website.com/my-avatar.png",
|
||||||
|
},
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
const avatarElement = document.querySelector(
|
||||||
|
"#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);
|
||||||
|
Typebot.initBubble({
|
||||||
|
proactiveMessage: {
|
||||||
|
textContent: "Hi click here!",
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
Typebot.initBubble({
|
||||||
|
proactiveMessage: {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("show after the corresponding delay", async () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
Typebot.initBubble({
|
||||||
|
proactiveMessage: {
|
||||||
|
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");
|
||||||
|
});
|
86
packages/typebot-js/tests/container.spec.ts
Normal file
86
packages/typebot-js/tests/container.spec.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { initContainer } from "../src/embedTypes/container";
|
||||||
|
|
||||||
|
const observe = jest.fn();
|
||||||
|
const unobserve = jest.fn();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
window.IntersectionObserver = jest.fn(() => ({
|
||||||
|
observe,
|
||||||
|
unobserve,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
})) as any;
|
||||||
|
|
||||||
|
describe("initContainer", () => {
|
||||||
|
beforeEach(() => (document.body.innerHTML = ``));
|
||||||
|
|
||||||
|
it("should initialize a valid typebot container", () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
const containerId = "container-id";
|
||||||
|
document.body.innerHTML = `<div id="${containerId}"></div>`;
|
||||||
|
const iframe = initContainer(containerId, { publishId: "typebot-id" });
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
expect(container?.children).toHaveLength(1);
|
||||||
|
expect(container?.children[0].tagName).toBe("IFRAME");
|
||||||
|
expect(iframe).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined if container doesn't exist", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const containerId = "container-id";
|
||||||
|
const iframe = initContainer(containerId, { publishId: "typebot-id" });
|
||||||
|
expect(iframe).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return exisiting container", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const containerId = "container-id";
|
||||||
|
document.body.innerHTML = `<div id="${containerId}"></div>`;
|
||||||
|
const iframe1 = initContainer(containerId, { publishId: "typebot-id" });
|
||||||
|
const iframe2 = initContainer(containerId, { publishId: "typebot-id" });
|
||||||
|
expect(iframe1?.id).toBe(iframe2?.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create multiple containers correctly", () => {
|
||||||
|
expect.assertions(5);
|
||||||
|
const firstId = "container-1";
|
||||||
|
const secondId = "container-2";
|
||||||
|
document.body.innerHTML = `<div id="${firstId}"></div><div id="${secondId}"></div>`;
|
||||||
|
const firstIframeElement = initContainer(firstId, {
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
const secondIframeElement = initContainer(firstId, {
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
const thirdIframeElement = initContainer(secondId, {
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
expect(firstIframeElement).toBeDefined();
|
||||||
|
expect(secondIframeElement).toBeDefined();
|
||||||
|
expect(thirdIframeElement).toBeDefined();
|
||||||
|
expect(firstIframeElement?.id).toBe(secondIframeElement?.id);
|
||||||
|
expect(firstIframeElement?.id).not.toBe(thirdIframeElement?.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be lazy loading by default", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
const containerId = "container";
|
||||||
|
document.body.innerHTML = `<div id="${containerId}"></div>`;
|
||||||
|
const iframe = initContainer(containerId, {
|
||||||
|
publishId: "typebot-id",
|
||||||
|
}) as HTMLIFrameElement;
|
||||||
|
expect(iframe.dataset.src).toBeDefined();
|
||||||
|
expect(iframe.src).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't be lazy if setting param to false", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
const containerId = "container";
|
||||||
|
document.body.innerHTML = `<div id="${containerId}"></div>`;
|
||||||
|
const iframe = initContainer(containerId, {
|
||||||
|
publishId: "typebot-id",
|
||||||
|
loadWhenVisible: false,
|
||||||
|
}) as HTMLIFrameElement;
|
||||||
|
expect(iframe.dataset.src).toBeUndefined();
|
||||||
|
expect(iframe.src).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
132
packages/typebot-js/tests/iframe.spec.ts
Normal file
132
packages/typebot-js/tests/iframe.spec.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { createIframe } from "../src/iframe";
|
||||||
|
|
||||||
|
describe("createIframe", () => {
|
||||||
|
it("should create a valid iframe element", () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
const iframeElement = createIframe({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
expect(iframeElement.tagName).toBe("IFRAME");
|
||||||
|
expect(iframeElement.getAttribute("data-id")).toBe("typebot-id");
|
||||||
|
expect(iframeElement.getAttribute("src")).toBe(
|
||||||
|
"https://typebot-viewer.vercel.app/typebot-id?hn=localhost"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse the right src prop if custom domain and starterVariables", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const iframes = [
|
||||||
|
createIframe({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
hiddenVariables: { var1: "value1", var2: "value2", var3: undefined },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
expect(iframes[0].getAttribute("src")).toBe(
|
||||||
|
"https://typebot-viewer.vercel.app/typebot-id?hn=localhost&var1=value1&var2=value2"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have a custom background color if defined", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const iframeElement = createIframe({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
backgroundColor: "green",
|
||||||
|
});
|
||||||
|
expect(iframeElement.style.backgroundColor).toBe("green");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have a lazy loading behavior if defined", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
const iframeElement = createIframe({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
loadWhenVisible: true,
|
||||||
|
});
|
||||||
|
expect(iframeElement.getAttribute("data-src")).toBe(
|
||||||
|
"https://typebot-viewer.vercel.app/typebot-id?hn=localhost"
|
||||||
|
);
|
||||||
|
expect(iframeElement.getAttribute("src")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redirect on event", async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
createIframe({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
window.open = jest.fn();
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
from: "typebot",
|
||||||
|
redirectUrl: "https://google.fr",
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
expect(window.open).toHaveBeenCalledWith("https://google.fr");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trigger var callback on var event", async () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
let n, v;
|
||||||
|
createIframe({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
onNewVariableValue: ({ name, value }) => {
|
||||||
|
v = value;
|
||||||
|
n = name;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
from: "typebot",
|
||||||
|
newVariableValue: { name: "varName", value: "varValue" },
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
expect(n).toBe("varName");
|
||||||
|
expect(v).toBe("varValue");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should notify when video played", async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
let hit = false;
|
||||||
|
createIframe({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
onVideoPlayed: () => {
|
||||||
|
hit = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
from: "typebot",
|
||||||
|
videoPlayed: true,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
expect(hit).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't execute callbacks if event from other than typebot", async () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
let n, v;
|
||||||
|
createIframe({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
onNewVariableValue: ({ name, value }) => {
|
||||||
|
v = value;
|
||||||
|
n = name;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.open = jest.fn();
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
redirectUrl: "https://google.fr",
|
||||||
|
newVariableValue: { name: "varName", value: "varValue" },
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
expect(window.open).not.toHaveBeenCalled();
|
||||||
|
expect(n).toBeUndefined();
|
||||||
|
expect(v).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
111
packages/typebot-js/tests/popup.spec.ts
Normal file
111
packages/typebot-js/tests/popup.spec.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { getPopupActions, initPopup } from "../src/embedTypes/popup";
|
||||||
|
|
||||||
|
describe("initPopup", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the popupElement with lazy iframe", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
initPopup({ publishId: "typebot-id" });
|
||||||
|
const popupElement = document.getElementById("typebot-popup");
|
||||||
|
const iframeElement = popupElement?.children[0] as HTMLIFrameElement;
|
||||||
|
expect(popupElement).toBeTruthy();
|
||||||
|
expect(iframeElement.dataset.src).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should overwrite if exists", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
initPopup({ publishId: "typebot-id", hiddenVariables: { test1: "yo" } });
|
||||||
|
initPopup({ publishId: "typebot-id2" });
|
||||||
|
const elements = document.getElementsByTagName("iframe");
|
||||||
|
expect(elements).toHaveLength(1);
|
||||||
|
expect(elements[0].dataset.src).toBe(
|
||||||
|
"https://typebot-viewer.vercel.app/typebot-id2?hn=localhost"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't have opened classname if no delay", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
initPopup({ publishId: "typebot-id" });
|
||||||
|
const popupElement = document.getElementById("typebot-popup");
|
||||||
|
expect(popupElement?.classList.contains("opened")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have the opened classname after the delay", async () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
initPopup({ delay: 500, publishId: "typebot-id" });
|
||||||
|
const popupElement = document.getElementById("typebot-popup");
|
||||||
|
expect(popupElement?.classList.contains("opened")).toBe(false);
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
expect(popupElement?.classList.contains("opened")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("openPopup", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add opened className and lazy load", () => {
|
||||||
|
expect.assertions(5);
|
||||||
|
const { open } = initPopup({ publishId: "typebot-id" });
|
||||||
|
const popupElement = document.getElementById("typebot-popup");
|
||||||
|
expect(popupElement?.children[0].getAttribute("data-src")).toBeTruthy();
|
||||||
|
open();
|
||||||
|
expect(popupElement?.classList.contains("opened")).toBe(true);
|
||||||
|
expect(document.body.style.overflowY).toBe("hidden");
|
||||||
|
expect(popupElement?.children[0].getAttribute("data-src")).toBeFalsy();
|
||||||
|
expect(open).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should still work if initializing a second time", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
initPopup({ publishId: "typebot-id" });
|
||||||
|
const { open } = initPopup({ publishId: "typebot-id" });
|
||||||
|
const popupElement = document.getElementById("typebot-popup");
|
||||||
|
open();
|
||||||
|
expect(popupElement?.classList.contains("opened")).toBe(true);
|
||||||
|
expect(document.body.style.overflowY).toBe("hidden");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("closePopup", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn remove opened className", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
const { close } = initPopup({ publishId: "typebot-id" });
|
||||||
|
const popupElement = document.getElementById("typebot-popup");
|
||||||
|
close();
|
||||||
|
expect(popupElement?.classList.contains("opened")).toBe(false);
|
||||||
|
expect(document.body.style.overflowY).toBe("auto");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should still work if initializing a second time", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
initPopup({ publishId: "typebot-id" });
|
||||||
|
const { close } = initPopup({ publishId: "typebot-id" });
|
||||||
|
const popupElement = document.getElementById("typebot-popup");
|
||||||
|
close();
|
||||||
|
expect(popupElement?.classList.contains("opened")).toBe(false);
|
||||||
|
expect(document.body.style.overflowY).toBe("auto");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Request commands afterwards", () => {
|
||||||
|
it("should return defined commands", () => {
|
||||||
|
initPopup({
|
||||||
|
publishId: "typebot-id",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { close, open } = getPopupActions();
|
||||||
|
expect(close).toBeDefined();
|
||||||
|
expect(open).toBeDefined();
|
||||||
|
open();
|
||||||
|
const popup = document.getElementById("typebot-popup") as HTMLDivElement;
|
||||||
|
expect(popup.classList.contains("opened")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
10
packages/typebot-js/tsconfig.json
Normal file
10
packages/typebot-js/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"declarationDir": ".",
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*.ts"],
|
||||||
|
"exclude": ["./tests/**/*"]
|
||||||
|
}
|
Reference in New Issue
Block a user