Embed: Strictly type the codebase and fixes a few bugs found (#7536)
* Add strict types * Make it array * Make api consistent and support calOrigin in floatingPopup and modal * Support calOrigin everywhere and fix branding config issue * Fix styles not being uniformly applied across components * Fix unused code
This commit is contained in:
1 parent
4f590ebbf7
commit
5fceee27fe
21 files changed
+1034
-769
No files matched your search
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
|
||||
const height = getDimension(previewState.inline.height);
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
@@ -187,12 +187,12 @@ function MyComponent() {
|
||||
}) => {
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
const cal = await getCalApi();
|
||||
Cal("floatingButton", ${floatingButtonArg});
|
||||
cal("floatingButton", ${floatingButtonArg});
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
@@ -201,7 +201,7 @@ function MyComponent() {
|
||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
|
||||
@@ -45,7 +45,7 @@ export const createEmbedsFixture = (page: Page) => {
|
||||
}
|
||||
api("on", {
|
||||
action: "*",
|
||||
callback: (e: any) => {
|
||||
callback: (e) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
<!-- <link rel="prerender" href="http://localhost:3000/free"> -->
|
||||
<!-- <script src="./src/embed.ts" type="module"></script> -->
|
||||
<script>
|
||||
function generateRandomHexColor() {
|
||||
// Generate a random integer between 0 and 16777215 (FFFFFF in hex)
|
||||
const randomInt = Math.floor(Math.random() * 16777216);
|
||||
|
||||
// Convert the integer to a hex string with 6 digits and add leading zeros if necessary
|
||||
const hexString = randomInt.toString(16).padStart(6, '0');
|
||||
|
||||
// Return the hex string with a '#' prefix
|
||||
return `#${hexString}`;
|
||||
}
|
||||
if (!location.search.includes("nonResponsive")) {
|
||||
document.write('<meta name="viewport" content="width=device-width"/>');
|
||||
}
|
||||
@@ -90,6 +100,7 @@
|
||||
</div>
|
||||
<span style="display: block;" ><a href="?only=all">Render All embeds together</a><small> - It would be slow to load</small></span>
|
||||
<div>
|
||||
<a href="?only=ns:floatingButton">Floating Popup</a>
|
||||
|
||||
<h2>Popup Examples</h2>
|
||||
<button data-cal-namespace="popupAutoTheme" data-cal-link="free">Book with Free User[Auto Theme]</button>
|
||||
@@ -140,12 +151,12 @@
|
||||
Align Center
|
||||
</button>
|
||||
<button onclick="(function () {Cal.ns.second('ui', {styles:{enabledDateButton: {
|
||||
backgroundColor: '#D3D3D3',
|
||||
backgroundColor: generateRandomHexColor(),
|
||||
},
|
||||
disabledDateButton: {
|
||||
backgroundColor: 'lightslategray',
|
||||
backgroundColor: generateRandomHexColor(),
|
||||
},}})})()">
|
||||
Change Date Button Color
|
||||
Change 'enabledDateButton` and `disabledDateButton` background Color
|
||||
</button>
|
||||
|
||||
</div>
|
||||
@@ -189,318 +200,6 @@
|
||||
<div class="place">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
const callback = function (e) {
|
||||
const detail = e.detail;
|
||||
const namespace = detail.namespace || "default";
|
||||
|
||||
if (detail.type === "linkReady") {
|
||||
document.querySelector(`#cal-booking-place-${namespace} .loader`).remove();
|
||||
} else if (detail.type === "linkFailed") {
|
||||
document.querySelector(`#cal-booking-place-${namespace} .loader`).remove();
|
||||
}
|
||||
document.querySelector(`#cal-booking-place-${namespace} .last-action`).innerHTML = JSON.stringify(
|
||||
e.detail
|
||||
);
|
||||
};
|
||||
const searchParams = new URL(document.URL).searchParams;
|
||||
let only = window.only
|
||||
if (only === "all" || only === "ns:default") {
|
||||
Cal("init", {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal("inline", {
|
||||
elementOrSelector: "#cal-booking-place-default .place",
|
||||
calLink: "pro?case=1",
|
||||
config: {
|
||||
__autoScroll:true,
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-default-iframe"
|
||||
},
|
||||
name: "John",
|
||||
email: "johndoe@gmail.com",
|
||||
notes: "Test Meeting",
|
||||
guests: ["janedoe@example.com", "test@example.com"],
|
||||
theme: "dark",
|
||||
},
|
||||
});
|
||||
Cal("on", {
|
||||
action: "*",
|
||||
callback,
|
||||
});
|
||||
}
|
||||
if (only === "all" || only === "ns:second") {
|
||||
// Create a namespace "second". It can be accessed as Cal.ns.second with the exact same API as Cal
|
||||
Cal("init", "second", {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
// Bulk API is supported - Keep all configuration at one place.
|
||||
Cal.ns.second(
|
||||
[
|
||||
"inline",
|
||||
{
|
||||
elementOrSelector: "#cal-booking-place-second .place",
|
||||
calLink: "pro?case=2",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-second-iframe"
|
||||
},
|
||||
}
|
||||
},
|
||||
]
|
||||
);
|
||||
Cal.ns.second("on", {
|
||||
action: "*",
|
||||
callback,
|
||||
});
|
||||
}
|
||||
if (only === "all" || only === "ns:third") {
|
||||
// Create a namespace "third". It can be accessed as Cal.ns.second with the exact same API as Cal
|
||||
Cal("init", "third", {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal.ns.third(
|
||||
[
|
||||
"inline",
|
||||
{
|
||||
elementOrSelector: "#cal-booking-place-third .place",
|
||||
calLink: "pro/30min",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-third-iframe"
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
[
|
||||
"ui",
|
||||
{
|
||||
styles: {
|
||||
body: {
|
||||
background: "transparent",
|
||||
},
|
||||
branding: {
|
||||
brandColor: "#81e61c",
|
||||
lightColor: "#494545",
|
||||
lighterColor: "#4c4848",
|
||||
lightestColor: "#7c7777",
|
||||
highlightColor: "#9b0e0e",
|
||||
medianColor: "black",
|
||||
},
|
||||
enabledDateButton: {
|
||||
backgroundColor: "red"
|
||||
},
|
||||
disabledDateButton: {
|
||||
backgroundColor: "green"
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
Cal.ns.third("on", {
|
||||
action: "*",
|
||||
callback,
|
||||
});
|
||||
}
|
||||
if (only === "all" || only === "ns:fourth") {
|
||||
Cal("init", "fourth", {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
Cal.ns.fourth(
|
||||
[
|
||||
"inline",
|
||||
{
|
||||
elementOrSelector: "#cal-booking-place-fourth .place",
|
||||
calLink: "team/seeded-team",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-fourth-iframe"
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
[
|
||||
"ui",
|
||||
{
|
||||
styles: {
|
||||
body: {
|
||||
background: "transparent",
|
||||
},
|
||||
branding: {
|
||||
brandColor: "#81e61c",
|
||||
lightColor: "#494545",
|
||||
lighterColor: "#4c4848",
|
||||
lightestColor: "#7c7777",
|
||||
highlightColor: "#9b0e0e",
|
||||
medianColor: "black",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
Cal.ns.fourth("on", {
|
||||
action: "*",
|
||||
callback,
|
||||
});
|
||||
}
|
||||
if (only === "all" || only === "ns:fifth") {
|
||||
Cal("init", "fifth", {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
Cal.ns.fifth(
|
||||
[
|
||||
"inline",
|
||||
{
|
||||
elementOrSelector: "#cal-booking-place-fifth .place",
|
||||
calLink: "team/seeded-team/collective-seeded-team-event",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-fifth-iframe"
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
);
|
||||
Cal.ns.fifth("on", {
|
||||
action: "*",
|
||||
callback,
|
||||
});
|
||||
}
|
||||
if (only === "all" || only === "prerender-test") {
|
||||
Cal('init', 'prerendertestLightTheme', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
Cal.ns.prerendertestLightTheme("preload", {
|
||||
calLink: "free",
|
||||
});
|
||||
}
|
||||
|
||||
if (only === "all" || only === "inline-routing-form") {
|
||||
Cal('init', 'inline-routing-form', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
Cal.ns['inline-routing-form'](
|
||||
[
|
||||
"inline",
|
||||
{
|
||||
elementOrSelector: "#cal-booking-place-inline-routing-form .place",
|
||||
calLink: "forms/948ae412-d995-4865-875a-48302588de03",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-inline-routing-form-iframe"
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (only === "all" || only === "hideEventTypeDetails") {
|
||||
let identifier = "hideEventTypeDetails"
|
||||
Cal('init', identifier, {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
|
||||
Cal.ns.hideEventTypeDetails([
|
||||
['inline', {
|
||||
elementOrSelector: `#cal-booking-place-${identifier} .place`,
|
||||
calLink: "free/30min",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: `cal-booking-place-${identifier}-iframe`
|
||||
},
|
||||
}
|
||||
}],
|
||||
[
|
||||
'ui', {
|
||||
hideEventTypeDetails:true
|
||||
}
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
Cal('init', 'popupDarkTheme', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
|
||||
Cal('init', 'popupHideEventTypeDetails', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal.ns.popupHideEventTypeDetails('ui', {
|
||||
hideEventTypeDetails:true
|
||||
});
|
||||
|
||||
Cal('init', 'popupReschedule', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
|
||||
Cal('init', 'popupAutoTheme', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
Cal('init', 'popupTeamLinkLightTheme', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
Cal('init', 'popupTeamLinkDarkTheme', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
|
||||
Cal('init', 'popupTeamLinkDarkTheme', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
|
||||
Cal('init', 'popupTeamLinksList', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
|
||||
Cal('init', 'popupPaidEvent', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
|
||||
Cal("init", "floatingButton", {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal('init', 'routingFormAuto', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
|
||||
Cal('init', 'routingFormDark', {
|
||||
debug: 1,
|
||||
origin: "http://localhost:3000",
|
||||
})
|
||||
|
||||
|
||||
if (only === "all" || only == "ns:floatingButton") {
|
||||
Cal.ns.floatingButton("floatingButton", {
|
||||
calLink: "pro"
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<script></script>
|
||||
<script src="./playground.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,321 @@
|
||||
import type { GlobalCal, GlobalCalWithoutNs } from "./src/embed";
|
||||
|
||||
type A = GlobalCalWithoutNs;
|
||||
const Cal = window.Cal as GlobalCal;
|
||||
const callback = function (e) {
|
||||
const detail = e.detail;
|
||||
const namespace = detail.namespace || "default";
|
||||
|
||||
if (detail.type === "linkReady") {
|
||||
document.querySelector(`#cal-booking-place-${namespace} .loader`).remove();
|
||||
} else if (detail.type === "linkFailed") {
|
||||
document.querySelector(`#cal-booking-place-${namespace} .loader`).remove();
|
||||
}
|
||||
document.querySelector(`#cal-booking-place-${namespace} .last-action`).innerHTML = JSON.stringify(e.detail);
|
||||
};
|
||||
|
||||
const searchParams = new URL(document.URL).searchParams;
|
||||
const only = window.only;
|
||||
|
||||
if (only === "all" || only === "ns:default") {
|
||||
Cal("init", {
|
||||
debug: true,
|
||||
calOrigin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal("inline", {
|
||||
elementOrSelector: "#cal-booking-place-default .place",
|
||||
calLink: "pro?case=1",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-default-iframe",
|
||||
},
|
||||
name: "John",
|
||||
email: "johndoe@gmail.com",
|
||||
notes: "Test Meeting",
|
||||
guests: ["janedoe@example.com", "test@example.com"],
|
||||
theme: "dark",
|
||||
},
|
||||
});
|
||||
Cal("on", {
|
||||
action: "*",
|
||||
callback,
|
||||
});
|
||||
}
|
||||
if (only === "all" || only === "ns:second") {
|
||||
// Create a namespace "second". It can be accessed as Cal.ns.second with the exact same API as Cal
|
||||
Cal("init", "second", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
// Bulk API is supported - Keep all configuration at one place.
|
||||
// Not able to type Bulk Api correctly when the first argument itself is an array.
|
||||
Cal.ns.second([
|
||||
"inline",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
{
|
||||
elementOrSelector: "#cal-booking-place-second .place",
|
||||
calLink: "pro?case=2",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-second-iframe",
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
Cal.ns.second("on", {
|
||||
action: "*",
|
||||
callback,
|
||||
});
|
||||
}
|
||||
if (only === "all" || only === "ns:third") {
|
||||
// Create a namespace "third". It can be accessed as Cal.ns.second with the exact same API as Cal
|
||||
Cal("init", "third", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal.ns.third(
|
||||
[
|
||||
"inline",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
{
|
||||
elementOrSelector: "#cal-booking-place-third .place",
|
||||
calLink: "pro/30min",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-third-iframe",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"ui",
|
||||
{
|
||||
styles: {
|
||||
body: {
|
||||
background: "transparent",
|
||||
},
|
||||
branding: {
|
||||
brandColor: "#81e61c",
|
||||
lightColor: "#494545",
|
||||
lighterColor: "#4c4848",
|
||||
lightestColor: "#7c7777",
|
||||
highlightColor: "#9b0e0e",
|
||||
medianColor: "black",
|
||||
},
|
||||
enabledDateButton: {
|
||||
backgroundColor: "red",
|
||||
},
|
||||
disabledDateButton: {
|
||||
backgroundColor: "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
Cal.ns.third("on", {
|
||||
action: "*",
|
||||
callback,
|
||||
});
|
||||
}
|
||||
if (only === "all" || only === "ns:fourth") {
|
||||
Cal("init", "fourth", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
Cal.ns.fourth(
|
||||
[
|
||||
"inline",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
{
|
||||
elementOrSelector: "#cal-booking-place-fourth .place",
|
||||
calLink: "team/seeded-team",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-fourth-iframe",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"ui",
|
||||
{
|
||||
styles: {
|
||||
body: {
|
||||
background: "transparent",
|
||||
},
|
||||
branding: {
|
||||
brandColor: "#81e61c",
|
||||
lightColor: "#494545",
|
||||
lighterColor: "#4c4848",
|
||||
lightestColor: "#7c7777",
|
||||
highlightColor: "#9b0e0e",
|
||||
medianColor: "black",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
Cal.ns.fourth("on", {
|
||||
action: "*",
|
||||
callback,
|
||||
});
|
||||
}
|
||||
if (only === "all" || only === "ns:fifth") {
|
||||
Cal("init", "fifth", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
Cal.ns.fifth([
|
||||
"inline",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
{
|
||||
elementOrSelector: "#cal-booking-place-fifth .place",
|
||||
calLink: "team/seeded-team/collective-seeded-team-event",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-fifth-iframe",
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
Cal.ns.fifth("on", {
|
||||
action: "*",
|
||||
callback,
|
||||
});
|
||||
}
|
||||
if (only === "all" || only === "prerender-test") {
|
||||
Cal("init", "prerendertestLightTheme", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
Cal.ns.prerendertestLightTheme("preload", {
|
||||
calLink: "free",
|
||||
});
|
||||
}
|
||||
|
||||
if (only === "all" || only === "inline-routing-form") {
|
||||
Cal("init", "inline-routing-form", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
Cal.ns["inline-routing-form"]([
|
||||
"inline",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
{
|
||||
elementOrSelector: "#cal-booking-place-inline-routing-form .place",
|
||||
calLink: "forms/948ae412-d995-4865-875a-48302588de03",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: "cal-booking-place-inline-routing-form-iframe",
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (only === "all" || only === "hideEventTypeDetails") {
|
||||
const identifier = "hideEventTypeDetails";
|
||||
Cal("init", identifier, {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal.ns.hideEventTypeDetails(
|
||||
[
|
||||
"inline",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
{
|
||||
elementOrSelector: `#cal-booking-place-${identifier} .place`,
|
||||
calLink: "free/30min",
|
||||
config: {
|
||||
iframeAttrs: {
|
||||
id: `cal-booking-place-${identifier}-iframe`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"ui",
|
||||
{
|
||||
hideEventTypeDetails: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Cal("init", "popupDarkTheme", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal("init", "popupHideEventTypeDetails", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal.ns.popupHideEventTypeDetails("ui", {
|
||||
hideEventTypeDetails: true,
|
||||
});
|
||||
|
||||
Cal("init", "popupReschedule", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal("init", "popupAutoTheme", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
Cal("init", "popupTeamLinkLightTheme", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
Cal("init", "popupTeamLinkDarkTheme", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal("init", "popupTeamLinkDarkTheme", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal("init", "popupTeamLinksList", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal("init", "popupPaidEvent", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal("init", "floatingButton", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal("init", "routingFormAuto", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal("init", "routingFormDark", {
|
||||
debug: true,
|
||||
origin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
if (only === "all" || only == "ns:floatingButton") {
|
||||
Cal.ns.floatingButton("floatingButton", {
|
||||
calLink: "pro",
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,36 @@
|
||||
import getFloatingButtonHtml from "./FloatingButtonHtml";
|
||||
|
||||
type ModalTargetDatasetProps = {
|
||||
calLink: string;
|
||||
calNamespace: string;
|
||||
calOrigin: string;
|
||||
};
|
||||
|
||||
type CamelCase<T extends string> = T extends `${infer U}${infer V}` ? `${Uppercase<U>}${V}` : T;
|
||||
|
||||
type HyphenatedStringToCamelCase<S extends string> = S extends `${infer T}-${infer U}`
|
||||
? `${T}${HyphenatedStringToCamelCase<CamelCase<U>>}`
|
||||
: CamelCase<S>;
|
||||
|
||||
type HyphenatedDataStringToCamelCase<S extends string> = S extends `data-${infer U}`
|
||||
? HyphenatedStringToCamelCase<U>
|
||||
: S;
|
||||
|
||||
const dataAttributes = [
|
||||
"data-button-text",
|
||||
"data-hide-button-icon",
|
||||
"data-button-position",
|
||||
"data-button-color",
|
||||
"data-button-text-color",
|
||||
] as const;
|
||||
|
||||
type DataAttributes = (typeof dataAttributes)[number];
|
||||
type DataAttributesCamelCase = HyphenatedDataStringToCamelCase<DataAttributes>;
|
||||
|
||||
export type FloatingButtonDataset = {
|
||||
[key in DataAttributesCamelCase]: string;
|
||||
};
|
||||
|
||||
export class FloatingButton extends HTMLElement {
|
||||
static updatedClassString(position: string, classString: string) {
|
||||
return [
|
||||
@@ -8,66 +39,69 @@ export class FloatingButton extends HTMLElement {
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
// Button added here triggers the modal on click. So, it has to have the same data attributes as the modal target as well
|
||||
dataset!: DOMStringMap & FloatingButtonDataset & ModalTargetDatasetProps;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
static get observedAttributes() {
|
||||
return [
|
||||
"data-button-text",
|
||||
"data-hide-button-icon",
|
||||
"data-button-position",
|
||||
"data-button-color",
|
||||
"data-button-text-color",
|
||||
];
|
||||
return dataAttributes;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
||||
attributeChangedCallback(name: DataAttributes, oldValue: string, newValue: string) {
|
||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("#button");
|
||||
const buttonWrapperEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
||||
const buttonIconEl = this.shadowRoot?.querySelector<HTMLElement>("#button-icon");
|
||||
|
||||
if (!buttonEl) {
|
||||
throw new Error("#button not found");
|
||||
}
|
||||
if (!buttonWrapperEl) {
|
||||
throw new Error("button element not found");
|
||||
}
|
||||
if (!buttonIconEl) {
|
||||
throw new Error("#button-icon not found");
|
||||
}
|
||||
|
||||
if (name === "data-button-text") {
|
||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("#button");
|
||||
if (!buttonEl) {
|
||||
throw new Error("Button not found");
|
||||
}
|
||||
buttonEl.textContent = newValue;
|
||||
} else if (name === "data-hide-button-icon") {
|
||||
const buttonIconEl = this.shadowRoot?.querySelector<HTMLElement>("#button-icon");
|
||||
if (!buttonIconEl) {
|
||||
throw new Error("Button not found");
|
||||
}
|
||||
buttonIconEl.style.display = newValue == "true" ? "none" : "block";
|
||||
} else if (name === "data-button-position") {
|
||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
||||
if (!buttonEl) {
|
||||
throw new Error("Button not found");
|
||||
}
|
||||
buttonEl.className = FloatingButton.updatedClassString(newValue, buttonEl.className);
|
||||
buttonWrapperEl.className = FloatingButton.updatedClassString(newValue, buttonWrapperEl.className);
|
||||
} else if (name === "data-button-color") {
|
||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
||||
if (!buttonEl) {
|
||||
throw new Error("Button not found");
|
||||
}
|
||||
buttonEl.style.backgroundColor = newValue;
|
||||
buttonWrapperEl.style.backgroundColor = newValue;
|
||||
} else if (name === "data-button-text-color") {
|
||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
||||
if (!buttonEl) {
|
||||
throw new Error("Button not found");
|
||||
}
|
||||
buttonEl.style.color = newValue;
|
||||
buttonWrapperEl.style.color = newValue;
|
||||
} else {
|
||||
console.log("Unknown attribute changed", name, oldValue, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
assertHasShadowRoot(): asserts this is HTMLElement & { shadowRoot: ShadowRoot } {
|
||||
if (!this.shadowRoot) {
|
||||
throw new Error("No shadow root");
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const buttonText = this.dataset["buttonText"];
|
||||
const buttonPosition = this.dataset["buttonPosition"];
|
||||
const buttonColor = this.dataset["buttonColor"];
|
||||
const buttonTextColor = this.dataset["buttonTextColor"];
|
||||
const dataset = this.dataset as FloatingButtonDataset;
|
||||
const buttonText = dataset["buttonText"];
|
||||
const buttonPosition = dataset["buttonPosition"];
|
||||
const buttonColor = dataset["buttonColor"];
|
||||
const buttonTextColor = dataset["buttonTextColor"];
|
||||
|
||||
//TODO: Logic is duplicated over HTML generation and attribute change, keep it at one place
|
||||
const buttonHtml = `<style>${window.Cal!.__css}</style> ${getFloatingButtonHtml({
|
||||
buttonText: buttonText!,
|
||||
buttonClasses: [FloatingButton.updatedClassString(buttonPosition!, "")],
|
||||
buttonColor: buttonColor!,
|
||||
buttonTextColor: buttonTextColor!,
|
||||
const buttonHtml = `<style>${window.Cal.__css}</style> ${getFloatingButtonHtml({
|
||||
buttonText: buttonText,
|
||||
buttonClasses: [FloatingButton.updatedClassString(buttonPosition, "")],
|
||||
buttonColor: buttonColor,
|
||||
buttonTextColor: buttonTextColor,
|
||||
})}`;
|
||||
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot!.innerHTML = buttonHtml;
|
||||
this.assertHasShadowRoot();
|
||||
this.shadowRoot.innerHTML = buttonHtml;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,42 @@
|
||||
import loaderCss from "../loader.css";
|
||||
import loaderCss from "../loader.css?inline";
|
||||
import { getErrorString } from "../utils";
|
||||
import inlineHtml from "./inlineHtml";
|
||||
|
||||
export class Inline extends HTMLElement {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
static get observedAttributes() {
|
||||
return ["loading"];
|
||||
}
|
||||
|
||||
assertHasShadowRoot(): asserts this is HTMLElement & { shadowRoot: ShadowRoot } {
|
||||
if (!this.shadowRoot) {
|
||||
throw new Error("No shadow root");
|
||||
}
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
||||
this.assertHasShadowRoot();
|
||||
const loaderEl = this.shadowRoot.querySelector<HTMLElement>(".loader");
|
||||
const errorEl = this.shadowRoot.querySelector<HTMLElement>("#error");
|
||||
const slotEl = this.shadowRoot.querySelector<HTMLElement>("slot");
|
||||
if (!loaderEl || !slotEl || !errorEl) {
|
||||
throw new Error("One of loaderEl, slotEl or errorEl is missing");
|
||||
}
|
||||
if (name === "loading") {
|
||||
if (newValue == "done") {
|
||||
this.shadowRoot!.querySelector<HTMLElement>(".loader")!.style.display = "none";
|
||||
loaderEl.style.display = "none";
|
||||
} else if (newValue === "failed") {
|
||||
this.shadowRoot!.querySelector<HTMLElement>(".loader")!.style.display = "none";
|
||||
this.shadowRoot!.querySelector<HTMLElement>("#error")!.style.display = "block";
|
||||
this.shadowRoot!.querySelector<HTMLElement>("slot")!.style.visibility = "hidden";
|
||||
loaderEl.style.display = "none";
|
||||
slotEl.style.visibility = "hidden";
|
||||
errorEl.style.display = "block";
|
||||
const errorString = getErrorString(this.dataset.errorCode);
|
||||
this.shadowRoot!.querySelector<HTMLElement>("#error")!.innerText = errorString;
|
||||
errorEl.innerText = errorString;
|
||||
}
|
||||
}
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot!.innerHTML = `<style>${
|
||||
window.Cal!.__css
|
||||
}</style><style>${loaderCss}</style>${inlineHtml}`;
|
||||
this.assertHasShadowRoot();
|
||||
this.shadowRoot.innerHTML = `<style>${window.Cal.__css}</style><style>${loaderCss}</style>${inlineHtml}`;
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export class ModalBox extends HTMLElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const modalHtml = `<style>${window.Cal!.__css}</style><style>${loaderCss}</style>${modalBoxHtml}`;
|
||||
const modalHtml = `<style>${window.Cal.__css}</style><style>${loaderCss}</style>${modalBoxHtml}`;
|
||||
this.attachShadow({ mode: "open" });
|
||||
ModalBox.htmlOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
@@ -2,18 +2,40 @@ import { useRouter } from "next/router";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import type { Message } from "./embed";
|
||||
import { sdkActionManager } from "./sdk-event";
|
||||
|
||||
export interface UiConfig {
|
||||
export type UiConfig = {
|
||||
hideEventTypeDetails?: boolean;
|
||||
theme?: "dark" | "light" | "auto";
|
||||
styles?: EmbedStyles;
|
||||
}
|
||||
styles?: EmbedStyles & EmbedNonStylesConfig;
|
||||
};
|
||||
|
||||
type SetStyles = React.Dispatch<React.SetStateAction<EmbedStyles>>;
|
||||
type setNonStylesConfig = React.Dispatch<React.SetStateAction<EmbedNonStylesConfig>>;
|
||||
|
||||
const embedStore = {
|
||||
// Store all embed styles here so that as and when new elements are mounted, styles can be applied to it.
|
||||
styles: {} as EmbedStyles | undefined,
|
||||
nonStyles: {} as EmbedNonStylesConfig | undefined,
|
||||
namespace: null as string | null,
|
||||
embedType: undefined as undefined | null | string,
|
||||
// Store all React State setters here.
|
||||
reactStylesStateSetters: {} as Record<keyof EmbedStyles, SetStyles>,
|
||||
reactNonStylesStateSetters: {} as Record<keyof EmbedNonStylesConfig, setNonStylesConfig>,
|
||||
parentInformedAboutContentHeight: false,
|
||||
windowLoadEventFired: false,
|
||||
setTheme: undefined as ((arg0: string) => void) | undefined,
|
||||
theme: undefined as UiConfig["theme"],
|
||||
uiConfig: undefined as Omit<UiConfig, "styles" | "theme"> | undefined,
|
||||
setUiConfig: undefined as ((arg0: UiConfig) => void) | undefined,
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
CalEmbed: {
|
||||
__logQueue?: any[];
|
||||
embedStore: any;
|
||||
__logQueue?: unknown[];
|
||||
embedStore: typeof embedStore;
|
||||
};
|
||||
CalComPageStatus: string;
|
||||
isEmbed: () => boolean;
|
||||
@@ -23,28 +45,6 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const embedStore = {
|
||||
// Store all embed styles here so that as and when new elements are mounted, styles can be applied to it.
|
||||
styles: {},
|
||||
namespace: null,
|
||||
embedType: undefined,
|
||||
// Store all React State setters here.
|
||||
reactStylesStateSetters: {},
|
||||
parentInformedAboutContentHeight: false,
|
||||
windowLoadEventFired: false,
|
||||
} as {
|
||||
styles: UiConfig["styles"];
|
||||
namespace: string | null;
|
||||
embedType: undefined | null | string;
|
||||
reactStylesStateSetters: any;
|
||||
parentInformedAboutContentHeight: boolean;
|
||||
windowLoadEventFired: boolean;
|
||||
theme?: UiConfig["theme"];
|
||||
uiConfig?: Omit<UiConfig, "styles" | "theme">;
|
||||
setTheme?: (arg0: string) => void;
|
||||
setUiConfig?: (arg0: UiConfig) => void;
|
||||
};
|
||||
|
||||
let isSafariBrowser = false;
|
||||
const isBrowser = typeof window !== "undefined";
|
||||
|
||||
@@ -58,7 +58,7 @@ if (isBrowser) {
|
||||
}
|
||||
}
|
||||
|
||||
function runAsap(fn: (...arg: any) => void) {
|
||||
function runAsap(fn: (...arg: unknown[]) => void) {
|
||||
if (isSafariBrowser) {
|
||||
// https://adpiler.com/blog/the-full-solution-why-do-animations-run-slower-in-safari/
|
||||
return setTimeout(fn, 50);
|
||||
@@ -66,7 +66,7 @@ function runAsap(fn: (...arg: any) => void) {
|
||||
return requestAnimationFrame(fn);
|
||||
}
|
||||
|
||||
function log(...args: any[]) {
|
||||
function log(...args: unknown[]) {
|
||||
if (isBrowser) {
|
||||
const namespace = getNamespace();
|
||||
|
||||
@@ -95,7 +95,7 @@ interface EmbedStyles {
|
||||
}
|
||||
interface EmbedNonStylesConfig {
|
||||
/** Default would be center */
|
||||
align: "left";
|
||||
align?: "left";
|
||||
branding?: {
|
||||
brandColor?: string;
|
||||
lightColor?: string;
|
||||
@@ -108,10 +108,10 @@ interface EmbedNonStylesConfig {
|
||||
};
|
||||
}
|
||||
|
||||
const setEmbedStyles = (stylesConfig: UiConfig["styles"]) => {
|
||||
const setEmbedStyles = (stylesConfig: EmbedStyles) => {
|
||||
embedStore.styles = stylesConfig;
|
||||
for (const [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) {
|
||||
(setEmbedStyle as any)((styles: any) => {
|
||||
setEmbedStyle((styles) => {
|
||||
return {
|
||||
...styles,
|
||||
...stylesConfig,
|
||||
@@ -120,15 +120,48 @@ const setEmbedStyles = (stylesConfig: UiConfig["styles"]) => {
|
||||
}
|
||||
};
|
||||
|
||||
const registerNewSetter = (elementName: keyof EmbedStyles | keyof EmbedNonStylesConfig, setStyles: any) => {
|
||||
embedStore.reactStylesStateSetters[elementName] = setStyles;
|
||||
// It's possible that 'ui' instruction has already been processed and the registration happened due to some action by the user in iframe.
|
||||
// So, we should call the setter immediately with available embedStyles
|
||||
setStyles(embedStore.styles);
|
||||
const setEmbedNonStyles = (stylesConfig: EmbedNonStylesConfig) => {
|
||||
embedStore.nonStyles = stylesConfig;
|
||||
for (const [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) {
|
||||
setEmbedStyle((styles) => {
|
||||
return {
|
||||
...styles,
|
||||
...stylesConfig,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeFromEmbedStylesSetterMap = (elementName: keyof EmbedStyles | keyof EmbedNonStylesConfig) => {
|
||||
delete embedStore.reactStylesStateSetters[elementName];
|
||||
const registerNewSetter = (
|
||||
registration:
|
||||
| {
|
||||
elementName: keyof EmbedStyles;
|
||||
setState: SetStyles;
|
||||
styles: true;
|
||||
}
|
||||
| {
|
||||
elementName: keyof EmbedNonStylesConfig;
|
||||
setState: setNonStylesConfig;
|
||||
styles: false;
|
||||
}
|
||||
) => {
|
||||
// It's possible that 'ui' instruction has already been processed and the registration happened due to some action by the user in iframe.
|
||||
// So, we should call the setter immediately with available embedStyles
|
||||
if (registration.styles) {
|
||||
embedStore.reactStylesStateSetters[registration.elementName as keyof EmbedStyles] = registration.setState;
|
||||
registration.setState(embedStore.styles || {});
|
||||
return () => {
|
||||
delete embedStore.reactStylesStateSetters[registration.elementName];
|
||||
};
|
||||
} else {
|
||||
embedStore.reactNonStylesStateSetters[registration.elementName as keyof EmbedNonStylesConfig] =
|
||||
registration.setState;
|
||||
registration.setState(embedStore.nonStyles || {});
|
||||
|
||||
return () => {
|
||||
delete embedStore.reactNonStylesStateSetters[registration.elementName];
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function isValidNamespace(ns: string | null | undefined) {
|
||||
@@ -155,33 +188,28 @@ export const useEmbedUiConfig = () => {
|
||||
|
||||
// TODO: Make it usable as an attribute directly instead of styles value. It would allow us to go beyond styles e.g. for debugging we can add a special attribute indentifying the element on which UI config has been applied
|
||||
export const useEmbedStyles = (elementName: keyof EmbedStyles) => {
|
||||
const [styles, setStyles] = useState({} as EmbedStyles);
|
||||
const [, setStyles] = useState<EmbedStyles>({});
|
||||
|
||||
useEffect(() => {
|
||||
registerNewSetter(elementName, setStyles);
|
||||
// It's important to have an element's embed style be required in only one component. If due to any reason it is required in multiple components, we would override state setter.
|
||||
return () => {
|
||||
// Once the component is unmounted, we can remove that state setter.
|
||||
removeFromEmbedStylesSetterMap(elementName);
|
||||
};
|
||||
return registerNewSetter({ elementName, setState: setStyles, styles: true });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const styles = embedStore.styles || {};
|
||||
// Always read the data from global embedStore so that even across components, the same data is used.
|
||||
return styles[elementName] || {};
|
||||
};
|
||||
|
||||
export const useEmbedNonStylesConfig = (elementName: keyof EmbedNonStylesConfig) => {
|
||||
const [styles, setStyles] = useState({} as EmbedNonStylesConfig);
|
||||
const [, setNonStyles] = useState({} as EmbedNonStylesConfig);
|
||||
|
||||
useEffect(() => {
|
||||
registerNewSetter(elementName, setStyles);
|
||||
// It's important to have an element's embed style be required in only one component. If due to any reason it is required in multiple components, we would override state setter.
|
||||
return () => {
|
||||
// Once the component is unmounted, we can remove that state setter.
|
||||
removeFromEmbedStylesSetterMap(elementName);
|
||||
};
|
||||
return registerNewSetter({ elementName, setState: setNonStyles, styles: false });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return styles[elementName] || {};
|
||||
// Always read the data from global embedStore so that even across components, the same data is used.
|
||||
const nonStyles = embedStore.nonStyles || {};
|
||||
return nonStyles[elementName] || {};
|
||||
};
|
||||
|
||||
export const useIsBackgroundTransparent = () => {
|
||||
@@ -251,8 +279,8 @@ function unhideBody() {
|
||||
document.body.style.visibility = "visible";
|
||||
}
|
||||
|
||||
// If you add a method here, give type safety to parent manually by adding it to embed.ts. Look for "parentKnowsIframeReady" in it
|
||||
export const methods = {
|
||||
// It is a map of methods that can be called by parent using doInIframe({method: "methodName", arg: "argument"})
|
||||
const methods = {
|
||||
ui: function style(uiConfig: UiConfig) {
|
||||
// TODO: Create automatic logger for all methods. Useful for debugging.
|
||||
log("Method: ui called", uiConfig);
|
||||
@@ -277,8 +305,10 @@ export const methods = {
|
||||
}
|
||||
|
||||
setEmbedStyles(stylesConfig || {});
|
||||
setEmbedNonStyles(stylesConfig || {});
|
||||
},
|
||||
parentKnowsIframeReady: () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
parentKnowsIframeReady: (_unused: unknown) => {
|
||||
log("Method: `parentKnowsIframeReady` called");
|
||||
runAsap(function tryInformingLinkReady() {
|
||||
// TODO: Do it by attaching a listener for change in parentInformedAboutContentHeight
|
||||
@@ -293,7 +323,14 @@ export const methods = {
|
||||
},
|
||||
};
|
||||
|
||||
const messageParent = (data: any) => {
|
||||
export type InterfaceWithParent = {
|
||||
// Ensure that only one argument is read by the method
|
||||
[key in keyof typeof methods]: (firstAndOnlyArg: Parameters<(typeof methods)[key]>[number]) => void;
|
||||
};
|
||||
|
||||
export const interfaceWithParent: InterfaceWithParent = methods;
|
||||
|
||||
const messageParent = (data: CustomEvent["detail"]) => {
|
||||
parent.postMessage(
|
||||
{
|
||||
originator: "CAL",
|
||||
@@ -396,35 +433,18 @@ if (isBrowser) {
|
||||
|
||||
sdkActionManager?.on("*", (e) => {
|
||||
const detail = e.detail;
|
||||
//console.log(detail.fullType, detail.type, detail.data);
|
||||
log(detail);
|
||||
messageParent(detail);
|
||||
});
|
||||
|
||||
// This event should be fired whenever you want to let the content take automatic width which is available.
|
||||
// Because on cal-iframe we set explicty width to make it look inline and part of page, there is never space available for content to automatically expand
|
||||
// This is a HACK to quickly tell iframe to go full width and let iframe content adapt to that and set new width.
|
||||
sdkActionManager?.on("__refreshWidth", () => {
|
||||
// sdkActionManager?.fire("__dimensionChanged", {
|
||||
// iframeWidth: 100,
|
||||
// __unit: "%",
|
||||
// });
|
||||
// runAsap(() => {
|
||||
// sdkActionManager?.fire("__dimensionChanged", {
|
||||
// iframeWidth: 100,
|
||||
// __unit: "%",
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
window.addEventListener("message", (e) => {
|
||||
const data: Record<string, any> = e.data;
|
||||
const data: Message = e.data;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const method: keyof typeof methods = data.method;
|
||||
const method: keyof typeof interfaceWithParent = data.method;
|
||||
if (data.originator === "CAL" && typeof method === "string") {
|
||||
methods[method]?.(data.arg);
|
||||
interfaceWithParent[method]?.(data.arg as never);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
/// <reference types="../env" />
|
||||
import { FloatingButton } from "./FloatingButton/FloatingButton";
|
||||
import { Inline } from "./Inline/inline";
|
||||
import { ModalBox } from "./ModalBox/ModalBox";
|
||||
import type { methods, UiConfig } from "./embed-iframe";
|
||||
import type { InterfaceWithParent, interfaceWithParent, UiConfig } from "./embed-iframe";
|
||||
import css from "./embed.css";
|
||||
import type { EventData, EventDataMap } from "./sdk-action-manager";
|
||||
import { SdkActionManager } from "./sdk-action-manager";
|
||||
import allCss from "./tailwind.generated.css";
|
||||
import allCss from "./tailwind.generated.css?inline";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Rest<T extends any[]> = T extends [any, ...infer U] ? U : never;
|
||||
export type Message = {
|
||||
originator: string;
|
||||
method: keyof InterfaceWithParent;
|
||||
arg: InterfaceWithParent[keyof InterfaceWithParent];
|
||||
};
|
||||
// HACK: Redefine and don't import WEBAPP_URL as it causes import statement to be present in built file.
|
||||
// This is happening because we are not able to generate an App and a lib using single Vite Config.
|
||||
const WEBAPP_URL =
|
||||
@@ -18,10 +27,20 @@ customElements.define("cal-inline", Inline);
|
||||
declare module "*.css";
|
||||
type Namespace = string;
|
||||
type Config = {
|
||||
origin: string;
|
||||
calOrigin: string;
|
||||
debug?: boolean;
|
||||
uiDebug?: boolean;
|
||||
};
|
||||
type InitArgConfig = Partial<Config> & {
|
||||
origin?: string;
|
||||
};
|
||||
|
||||
type DoInIframeArg = {
|
||||
[K in keyof typeof interfaceWithParent]: {
|
||||
method: K;
|
||||
arg?: Parameters<(typeof interfaceWithParent)[K]>[0];
|
||||
};
|
||||
}[keyof typeof interfaceWithParent];
|
||||
|
||||
const globalCal = window.Cal;
|
||||
if (!globalCal || !globalCal.q) {
|
||||
@@ -35,9 +54,22 @@ globalCal.fingerprint = import.meta.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as strin
|
||||
globalCal.__css = allCss;
|
||||
document.head.appendChild(document.createElement("style")).innerHTML = css;
|
||||
|
||||
function log(...args: any[]) {
|
||||
function log(...args: unknown[]) {
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type ValidationSchemaPropType = string | Function;
|
||||
|
||||
type ValidationSchema = {
|
||||
required?: boolean;
|
||||
props?: Record<
|
||||
string,
|
||||
ValidationSchema & {
|
||||
type: ValidationSchemaPropType | ValidationSchemaPropType[];
|
||||
}
|
||||
>;
|
||||
};
|
||||
/**
|
||||
* //TODO: Warn about extra properties not part of schema. Helps in fixing wrong expectations
|
||||
* A very simple data validator written with intention of keeping payload size low.
|
||||
@@ -45,8 +77,8 @@ function log(...args: any[]) {
|
||||
* @param data
|
||||
* @param schema
|
||||
*/
|
||||
function validate(data: any, schema: Record<"props" | "required", any>) {
|
||||
function checkType(value: any, expectedType: any) {
|
||||
function validate(data: Record<string, unknown>, schema: ValidationSchema) {
|
||||
function checkType(value: unknown, expectedType: ValidationSchemaPropType) {
|
||||
if (typeof expectedType === "string") {
|
||||
return typeof value == expectedType;
|
||||
} else {
|
||||
@@ -54,7 +86,7 @@ function validate(data: any, schema: Record<"props" | "required", any>) {
|
||||
}
|
||||
}
|
||||
|
||||
function isUndefined(data: any) {
|
||||
function isUndefined(data: unknown) {
|
||||
return typeof data === "undefined";
|
||||
}
|
||||
|
||||
@@ -62,7 +94,7 @@ function validate(data: any, schema: Record<"props" | "required", any>) {
|
||||
throw new Error("Argument is required");
|
||||
}
|
||||
|
||||
for (const [prop, propSchema] of Object.entries<Record<"type" | "required", any>>(schema.props)) {
|
||||
for (const [prop, propSchema] of Object.entries(schema.props || {})) {
|
||||
if (propSchema.required && isUndefined(data[prop])) {
|
||||
throw new Error(`"${prop}" is required`);
|
||||
}
|
||||
@@ -82,9 +114,23 @@ function validate(data: any, schema: Record<"props" | "required", any>) {
|
||||
}
|
||||
}
|
||||
|
||||
export type Instruction = [method: string, argument: any] | [method: string, argument: any][];
|
||||
type SingleInstructionMap = {
|
||||
// TODO: This makes api("on", {}) loose it's generic type. Find a way to fix it.
|
||||
// e.g. api("on", { action: "__dimensionChanged", callback: (e) => { /* `e.detail.data` has all possible values for all events/actions */} });
|
||||
[K in keyof CalApi]: CalApi[K] extends (...args: never[]) => void ? [K, ...Parameters<CalApi[K]>] : never;
|
||||
};
|
||||
|
||||
type SingleInstruction = SingleInstructionMap[keyof SingleInstructionMap];
|
||||
|
||||
export type Instruction = SingleInstruction | SingleInstruction[];
|
||||
export type InstructionQueue = Instruction[];
|
||||
|
||||
type PrefillAndIframeAttrsConfig = Record<string, string | string[] | Record<string, string>> & {
|
||||
iframeAttrs?: Record<string, string> & {
|
||||
id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export class Cal {
|
||||
iframe?: HTMLIFrameElement;
|
||||
|
||||
@@ -100,35 +146,42 @@ export class Cal {
|
||||
|
||||
iframeReady!: boolean;
|
||||
|
||||
iframeDoQueue: { method: keyof typeof methods; arg: any }[] = [];
|
||||
iframeDoQueue: DoInIframeArg[] = [];
|
||||
|
||||
api: CalApi;
|
||||
|
||||
static actionsManagers: Record<Namespace, SdkActionManager>;
|
||||
|
||||
static getQueryObject(config: Record<string, string>) {
|
||||
static getQueryObject(config: PrefillAndIframeAttrsConfig) {
|
||||
config = config || {};
|
||||
return {
|
||||
...config,
|
||||
// guests is better for API but Booking Page accepts guest. So do the mapping
|
||||
guest: config.guests ?? undefined,
|
||||
};
|
||||
} as PrefillAndIframeAttrsConfig & { guest?: string | string[] };
|
||||
}
|
||||
|
||||
processInstruction(instruction: Instruction) {
|
||||
instruction = [].slice.call(instruction, 0);
|
||||
if (instruction[0] instanceof Array) {
|
||||
processInstruction(instructionAsArgs: IArguments | Instruction) {
|
||||
// The instruction is actually an array-like object(arguments). Make it an array.
|
||||
const instruction: Instruction = [].slice.call(instructionAsArgs);
|
||||
// If there are multiple instructions in the array, process them one by one
|
||||
if (typeof instruction[0] !== "string") {
|
||||
// It is an instruction
|
||||
instruction.forEach((instruction) => {
|
||||
this.processInstruction(instruction);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const [method, ...args] = instruction;
|
||||
if (!this[method]) {
|
||||
if (!this.api[method]) {
|
||||
// Instead of throwing error, log and move forward in the queue
|
||||
log(`Instruction ${method} not FOUND`);
|
||||
}
|
||||
try {
|
||||
(this[method] as (...args: any[]) => void)(...args);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore There can be any method which can have any number of arguments.
|
||||
this.api[method](...args);
|
||||
} catch (e) {
|
||||
// Instead of throwing error, log and move forward in the queue
|
||||
log(`Instruction couldn't be executed`, e);
|
||||
@@ -136,7 +189,7 @@ export class Cal {
|
||||
return instruction;
|
||||
}
|
||||
|
||||
processQueue(queue: InstructionQueue) {
|
||||
processQueue(queue: IArguments[]) {
|
||||
queue.forEach((instruction) => {
|
||||
this.processInstruction(instruction);
|
||||
});
|
||||
@@ -153,9 +206,11 @@ export class Cal {
|
||||
createIframe({
|
||||
calLink,
|
||||
queryObject = {},
|
||||
calOrigin,
|
||||
}: {
|
||||
calLink: string;
|
||||
queryObject?: Record<string, string | string[] | Record<string, string>>;
|
||||
queryObject?: PrefillAndIframeAttrsConfig & { guest?: string | string[] };
|
||||
calOrigin?: string;
|
||||
}) {
|
||||
const iframe = (this.iframe = document.createElement("iframe"));
|
||||
iframe.className = "cal-embed";
|
||||
@@ -163,7 +218,7 @@ export class Cal {
|
||||
const config = this.getConfig();
|
||||
const { iframeAttrs, ...restQueryObject } = queryObject;
|
||||
|
||||
if (iframeAttrs && typeof iframeAttrs !== "string" && !(iframeAttrs instanceof Array)) {
|
||||
if (iframeAttrs && iframeAttrs.id) {
|
||||
iframe.setAttribute("id", iframeAttrs.id);
|
||||
}
|
||||
|
||||
@@ -180,7 +235,7 @@ export class Cal {
|
||||
}
|
||||
}
|
||||
|
||||
const urlInstance = new URL(`${config.origin}/${calLink}`);
|
||||
const urlInstance = new URL(`${calOrigin || config.calOrigin}/${calLink}`);
|
||||
if (!urlInstance.pathname.endsWith("embed")) {
|
||||
// TODO: Make a list of patterns that are embeddable. All except that should be allowed with a warning that "The page isn't optimized for embedding"
|
||||
urlInstance.pathname = `${urlInstance.pathname}/embed`;
|
||||
@@ -203,252 +258,34 @@ export class Cal {
|
||||
return iframe;
|
||||
}
|
||||
|
||||
init(namespaceOrConfig?: string | Config, config: Config = {} as Config) {
|
||||
if (typeof namespaceOrConfig !== "string") {
|
||||
config = (namespaceOrConfig || {}) as Config;
|
||||
}
|
||||
const { origin, ...restConfig } = config;
|
||||
if (origin) {
|
||||
this.__config.origin = origin;
|
||||
}
|
||||
this.__config = { ...this.__config, ...restConfig };
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return this.__config;
|
||||
}
|
||||
|
||||
// TODO: Maintain exposed methods in a separate namespace, so that unexpected methods don't become instructions
|
||||
|
||||
/**
|
||||
* It is an instruction that adds embed iframe inline as last child of the element
|
||||
*/
|
||||
inline({
|
||||
calLink,
|
||||
elementOrSelector,
|
||||
config,
|
||||
}: {
|
||||
calLink: string;
|
||||
elementOrSelector: string | HTMLElement;
|
||||
config: Record<string, string>;
|
||||
}) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
validate(arguments[0], {
|
||||
required: true,
|
||||
props: {
|
||||
calLink: {
|
||||
// TODO: Add a special type calLink for it and validate that it doesn't start with / or https?://
|
||||
required: true,
|
||||
type: "string",
|
||||
},
|
||||
elementOrSelector: {
|
||||
required: true,
|
||||
type: ["string", HTMLElement],
|
||||
},
|
||||
config: {
|
||||
required: false,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
||||
config = config || {};
|
||||
// Keeping auto-scroll disabled for two reasons:
|
||||
// - If user scrolls the content to an appropriate position, it again resets it to default position which might not be for the liking of the user
|
||||
// - Sometimes, the position can be wrong(e.g. if there is a fixed position header on top coming above the iframe content).
|
||||
// Best solution might be to autoscroll only if the iframe is not fully visible, detection of full visibility might be tough
|
||||
|
||||
// We need to keep in mind that autoscroll is meant to solve the problem when on a certain view(which is availability page right now), the height goes too high and then suddenly it becomes normal
|
||||
(config as unknown as any).__autoScroll = !!(config as unknown as any).__autoScroll;
|
||||
config.embedType = "inline";
|
||||
const iframe = this.createIframe({ calLink, queryObject: Cal.getQueryObject(config) });
|
||||
iframe.style.height = "100%";
|
||||
iframe.style.width = "100%";
|
||||
const element =
|
||||
elementOrSelector instanceof HTMLElement
|
||||
? elementOrSelector
|
||||
: document.querySelector(elementOrSelector);
|
||||
if (!element) {
|
||||
throw new Error("Element not found");
|
||||
}
|
||||
element.classList.add("cal-inline-container");
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = `<cal-inline style="max-height:inherit;height:inherit;min-height:inherit;display:flex;position:relative;flex-wrap:wrap;width:100%"></cal-inline><style>.cal-inline-container::-webkit-scrollbar{display:none}.cal-inline-container{scrollbar-width:none}</style>`;
|
||||
this.inlineEl = template.content.children[0];
|
||||
(this.inlineEl as unknown as any).__CalAutoScroll = config.__autoScroll;
|
||||
this.inlineEl.appendChild(iframe);
|
||||
element.appendChild(template.content);
|
||||
}
|
||||
|
||||
floatingButton({
|
||||
calLink,
|
||||
buttonText = "Book my Cal",
|
||||
hideButtonIcon = false,
|
||||
attributes,
|
||||
buttonPosition = "bottom-right",
|
||||
buttonColor = "rgb(0, 0, 0)",
|
||||
buttonTextColor = "rgb(255, 255, 255)",
|
||||
}: {
|
||||
calLink: string;
|
||||
buttonText?: string;
|
||||
attributes?: Record<string, string>;
|
||||
hideButtonIcon?: boolean;
|
||||
buttonPosition?: "bottom-left" | "bottom-right";
|
||||
buttonColor: string;
|
||||
buttonTextColor: string;
|
||||
}) {
|
||||
// validate(arguments[0], {
|
||||
// required: true,
|
||||
// props: {
|
||||
// calLink: {
|
||||
// required: true,
|
||||
// type: "string",
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
let existingEl: HTMLElement | null = null;
|
||||
|
||||
if (attributes?.id) {
|
||||
existingEl = document.getElementById(attributes.id);
|
||||
}
|
||||
let el: HTMLElement;
|
||||
if (!existingEl) {
|
||||
el = document.createElement("cal-floating-button");
|
||||
el.dataset.calLink = calLink;
|
||||
el.dataset.calNamespace = this.namespace;
|
||||
if (attributes?.id) {
|
||||
el.id = attributes.id;
|
||||
}
|
||||
|
||||
document.body.appendChild(el);
|
||||
} else {
|
||||
el = existingEl;
|
||||
}
|
||||
|
||||
if (buttonText) {
|
||||
el.setAttribute("data-button-text", buttonText);
|
||||
}
|
||||
el.setAttribute("data-hide-button-icon", "" + hideButtonIcon);
|
||||
el.setAttribute("data-button-position", "" + buttonPosition);
|
||||
el.setAttribute("data-button-color", "" + buttonColor);
|
||||
el.setAttribute("data-button-text-color", "" + buttonTextColor);
|
||||
}
|
||||
|
||||
modal({ calLink, config = {}, uid }: { calLink: string; config?: Record<string, string>; uid: number }) {
|
||||
const existingModalEl = document.querySelector(`cal-modal-box[uid="${uid}"]`);
|
||||
if (existingModalEl) {
|
||||
existingModalEl.setAttribute("state", "started");
|
||||
return;
|
||||
}
|
||||
config.embedType = "modal";
|
||||
const iframe = this.createIframe({ calLink, queryObject: Cal.getQueryObject(config) });
|
||||
iframe.style.borderRadius = "8px";
|
||||
|
||||
iframe.style.height = "100%";
|
||||
iframe.style.width = "100%";
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = `<cal-modal-box uid="${uid}"></cal-modal-box>`;
|
||||
|
||||
this.modalBox = template.content.children[0];
|
||||
this.modalBox.appendChild(iframe);
|
||||
this.actionManager.on("__closeIframe", () => {
|
||||
this.modalBox.setAttribute("state", "closed");
|
||||
});
|
||||
document.body.appendChild(template.content);
|
||||
}
|
||||
|
||||
on({
|
||||
action,
|
||||
callback,
|
||||
}: {
|
||||
action: Parameters<SdkActionManager["on"]>[0];
|
||||
callback: Parameters<SdkActionManager["on"]>[1];
|
||||
}) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
validate(arguments[0], {
|
||||
required: true,
|
||||
props: {
|
||||
action: {
|
||||
required: true,
|
||||
type: "string",
|
||||
},
|
||||
callback: {
|
||||
required: true,
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.actionManager.on(action, callback);
|
||||
}
|
||||
|
||||
off({
|
||||
action,
|
||||
callback,
|
||||
}: {
|
||||
action: Parameters<SdkActionManager["on"]>[0];
|
||||
callback: Parameters<SdkActionManager["on"]>[1];
|
||||
}) {
|
||||
this.actionManager.off(action, callback);
|
||||
}
|
||||
|
||||
preload({ calLink }: { calLink: string }) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
validate(arguments[0], {
|
||||
required: true,
|
||||
props: {
|
||||
calLink: {
|
||||
type: "string",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const iframe = document.body.appendChild(document.createElement("iframe"));
|
||||
const config = this.getConfig();
|
||||
|
||||
const urlInstance = new URL(`${config.origin}/${calLink}`);
|
||||
urlInstance.searchParams.set("prerender", "true");
|
||||
iframe.src = urlInstance.toString();
|
||||
iframe.style.width = "0";
|
||||
iframe.style.height = "0";
|
||||
iframe.style.display = "none";
|
||||
}
|
||||
|
||||
ui(uiConfig: UiConfig) {
|
||||
validate(uiConfig, {
|
||||
required: true,
|
||||
props: {
|
||||
theme: {
|
||||
required: false,
|
||||
type: "string",
|
||||
},
|
||||
styles: {
|
||||
required: false,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.doInIframe({ method: "ui", arg: uiConfig });
|
||||
}
|
||||
|
||||
doInIframe({
|
||||
method,
|
||||
arg,
|
||||
}: // TODO: Need some TypeScript magic here to remove hardcoded types
|
||||
| { method: "ui"; arg: Parameters<(typeof methods)["ui"]>[0] }
|
||||
| { method: "parentKnowsIframeReady"; arg: undefined }) {
|
||||
doInIframe(doInIframeArg: DoInIframeArg) {
|
||||
if (!this.iframeReady) {
|
||||
this.iframeDoQueue.push({ method, arg });
|
||||
this.iframeDoQueue.push(doInIframeArg);
|
||||
return;
|
||||
}
|
||||
// TODO: Ensure that origin is as defined by user. Generally it would be cal.com but in case of self hosting it can be anything.
|
||||
this.iframe!.contentWindow!.postMessage({ originator: "CAL", method, arg }, "*");
|
||||
if (!this.iframe) {
|
||||
throw new Error("iframe doesn't exist. `createIframe` must be called before `doInIframe`");
|
||||
}
|
||||
if (this.iframe.contentWindow) {
|
||||
// TODO: Ensure that targetOrigin is as defined by user(and not *). Generally it would be cal.com but in case of self hosting it can be anything.
|
||||
// Maybe we can derive targetOrigin from __config.origin
|
||||
this.iframe.contentWindow.postMessage(
|
||||
{ originator: "CAL", method: doInIframeArg.method, arg: doInIframeArg.arg },
|
||||
"*"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(namespace: string, q: InstructionQueue) {
|
||||
constructor(namespace: string, q: IArguments[]) {
|
||||
this.__config = {
|
||||
// Use WEBAPP_URL till full page reload problem with website URL is solved
|
||||
origin: WEBAPP_URL,
|
||||
calOrigin: WEBAPP_URL,
|
||||
};
|
||||
this.api = new CalApi(this);
|
||||
this.namespace = namespace;
|
||||
this.actionManager = new SdkActionManager(namespace);
|
||||
|
||||
@@ -462,24 +299,17 @@ export class Cal {
|
||||
// 3. Parent window would check what iframe height can be set according to parent Element
|
||||
this.actionManager.on("__dimensionChanged", (e) => {
|
||||
const { data } = e.detail;
|
||||
const iframe = this.iframe!;
|
||||
const iframe = this.iframe;
|
||||
|
||||
if (!iframe) {
|
||||
// Iframe might be pre-rendering
|
||||
return;
|
||||
}
|
||||
let unit = "px";
|
||||
if (data.__unit) {
|
||||
unit = data.__unit;
|
||||
}
|
||||
const unit = "px";
|
||||
if (data.iframeHeight) {
|
||||
iframe.style.height = data.iframeHeight + unit;
|
||||
}
|
||||
|
||||
// if (data.iframeWidth) {
|
||||
// iframe.style.width = data.iframeWidth + unit;
|
||||
// }
|
||||
|
||||
if (this.modalBox) {
|
||||
// It ensures that if the iframe is so tall that it can't fit in the parent window without scroll. Then force the scroll by restricting the max-height to innerHeight
|
||||
// This case is reproducible when viewing in ModalBox on Mobile.
|
||||
@@ -490,9 +320,9 @@ export class Cal {
|
||||
|
||||
this.actionManager.on("__iframeReady", () => {
|
||||
this.iframeReady = true;
|
||||
this.doInIframe({ method: "parentKnowsIframeReady", arg: undefined });
|
||||
this.iframeDoQueue.forEach(({ method, arg }) => {
|
||||
this.doInIframe({ method, arg });
|
||||
this.doInIframe({ method: "parentKnowsIframeReady" } as const);
|
||||
this.iframeDoQueue.forEach((doInIframeArg) => {
|
||||
this.doInIframe(doInIframeArg);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -523,34 +353,276 @@ export class Cal {
|
||||
}
|
||||
}
|
||||
|
||||
export interface GlobalCal {
|
||||
(methodName: string, arg?: any): void;
|
||||
class CalApi {
|
||||
cal: Cal;
|
||||
|
||||
constructor(cal: Cal) {
|
||||
this.cal = cal;
|
||||
}
|
||||
|
||||
init(namespaceOrConfig?: string | InitArgConfig, config = {} as InitArgConfig) {
|
||||
if (typeof namespaceOrConfig !== "string") {
|
||||
config = (namespaceOrConfig || {}) as Config;
|
||||
}
|
||||
|
||||
const { calOrigin: calOrigin, origin: origin, ...restConfig } = config;
|
||||
|
||||
this.cal.__config.calOrigin = calOrigin || origin || this.cal.__config.calOrigin;
|
||||
|
||||
this.cal.__config = { ...this.cal.__config, ...restConfig };
|
||||
}
|
||||
/**
|
||||
* It is an instruction that adds embed iframe inline as last child of the element
|
||||
*/
|
||||
inline({
|
||||
calLink,
|
||||
elementOrSelector,
|
||||
config,
|
||||
}: {
|
||||
calLink: string;
|
||||
elementOrSelector: string | HTMLElement;
|
||||
config?: PrefillAndIframeAttrsConfig;
|
||||
}) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
validate(arguments[0], {
|
||||
required: true,
|
||||
props: {
|
||||
calLink: {
|
||||
// TODO: Add a special type calLink for it and validate that it doesn't start with / or https?://
|
||||
required: true,
|
||||
type: "string",
|
||||
},
|
||||
elementOrSelector: {
|
||||
required: true,
|
||||
type: ["string", HTMLElement],
|
||||
},
|
||||
config: {
|
||||
required: false,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
config = config || {};
|
||||
if (typeof config.iframeAttrs === "string" || config.iframeAttrs instanceof Array) {
|
||||
throw new Error("iframeAttrs should be an object");
|
||||
}
|
||||
config.embedType = "inline";
|
||||
const iframe = this.cal.createIframe({ calLink, queryObject: Cal.getQueryObject(config) });
|
||||
iframe.style.height = "100%";
|
||||
iframe.style.width = "100%";
|
||||
const element =
|
||||
elementOrSelector instanceof HTMLElement
|
||||
? elementOrSelector
|
||||
: document.querySelector(elementOrSelector);
|
||||
if (!element) {
|
||||
throw new Error("Element not found");
|
||||
}
|
||||
element.classList.add("cal-inline-container");
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = `<cal-inline style="max-height:inherit;height:inherit;min-height:inherit;display:flex;position:relative;flex-wrap:wrap;width:100%"></cal-inline><style>.cal-inline-container::-webkit-scrollbar{display:none}.cal-inline-container{scrollbar-width:none}</style>`;
|
||||
this.cal.inlineEl = template.content.children[0];
|
||||
this.cal.inlineEl.appendChild(iframe);
|
||||
element.appendChild(template.content);
|
||||
}
|
||||
|
||||
floatingButton({
|
||||
calLink,
|
||||
buttonText = "Book my Cal",
|
||||
hideButtonIcon = false,
|
||||
attributes,
|
||||
buttonPosition = "bottom-right",
|
||||
buttonColor = "rgb(0, 0, 0)",
|
||||
buttonTextColor = "rgb(255, 255, 255)",
|
||||
calOrigin,
|
||||
}: {
|
||||
calLink: string;
|
||||
buttonText?: string;
|
||||
attributes?: Record<"id", string> & Record<string | "id", string>;
|
||||
hideButtonIcon?: boolean;
|
||||
buttonPosition?: "bottom-left" | "bottom-right";
|
||||
buttonColor?: string;
|
||||
buttonTextColor?: string;
|
||||
calOrigin?: string;
|
||||
}) {
|
||||
// validate(arguments[0], {
|
||||
// required: true,
|
||||
// props: {
|
||||
// calLink: {
|
||||
// required: true,
|
||||
// type: "string",
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
let existingEl: HTMLElement | null = null;
|
||||
|
||||
if (attributes?.id) {
|
||||
existingEl = document.getElementById(attributes.id);
|
||||
}
|
||||
let el: FloatingButton;
|
||||
if (!existingEl) {
|
||||
el = document.createElement("cal-floating-button") as FloatingButton;
|
||||
// It makes it a target element that opens up embed modal on click
|
||||
el.dataset.calLink = calLink;
|
||||
el.dataset.calNamespace = this.cal.namespace;
|
||||
el.dataset.calOrigin = calOrigin ?? "";
|
||||
if (attributes?.id) {
|
||||
el.id = attributes.id;
|
||||
}
|
||||
|
||||
document.body.appendChild(el);
|
||||
} else {
|
||||
el = existingEl as FloatingButton;
|
||||
}
|
||||
const dataset = el.dataset;
|
||||
dataset["buttonText"] = buttonText;
|
||||
dataset["hideButtonIcon"] = "" + hideButtonIcon;
|
||||
dataset["buttonPosition"] = "" + buttonPosition;
|
||||
dataset["buttonColor"] = "" + buttonColor;
|
||||
dataset["buttonTextColor"] = "" + buttonTextColor;
|
||||
}
|
||||
|
||||
modal({
|
||||
calLink,
|
||||
calOrigin,
|
||||
config = {},
|
||||
uid,
|
||||
}: {
|
||||
calLink: string;
|
||||
config?: PrefillAndIframeAttrsConfig;
|
||||
uid?: string | number;
|
||||
calOrigin?: string;
|
||||
}) {
|
||||
uid = uid || 0;
|
||||
|
||||
const existingModalEl = document.querySelector(`cal-modal-box[uid="${uid}"]`);
|
||||
if (existingModalEl) {
|
||||
existingModalEl.setAttribute("state", "started");
|
||||
return;
|
||||
}
|
||||
if (typeof config.iframeAttrs === "string" || config.iframeAttrs instanceof Array) {
|
||||
throw new Error("iframeAttrs should be an object");
|
||||
}
|
||||
config.embedType = "modal";
|
||||
const iframe = this.cal.createIframe({ calLink, calOrigin, queryObject: Cal.getQueryObject(config) });
|
||||
iframe.style.borderRadius = "8px";
|
||||
|
||||
iframe.style.height = "100%";
|
||||
iframe.style.width = "100%";
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = `<cal-modal-box uid="${uid}"></cal-modal-box>`;
|
||||
|
||||
this.cal.modalBox = template.content.children[0];
|
||||
this.cal.modalBox.appendChild(iframe);
|
||||
this.cal.actionManager.on("__closeIframe", () => {
|
||||
this.cal.modalBox.setAttribute("state", "closed");
|
||||
});
|
||||
document.body.appendChild(template.content);
|
||||
}
|
||||
|
||||
on<T extends keyof EventDataMap>({
|
||||
action,
|
||||
callback,
|
||||
}: {
|
||||
action: T;
|
||||
callback: (arg0: CustomEvent<EventData<T>>) => void;
|
||||
}) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
validate(arguments[0], {
|
||||
required: true,
|
||||
props: {
|
||||
action: {
|
||||
required: true,
|
||||
type: "string",
|
||||
},
|
||||
callback: {
|
||||
required: true,
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.cal.actionManager.on(action, callback);
|
||||
}
|
||||
|
||||
off({ action, callback }: { action: never; callback: never }) {
|
||||
this.cal.actionManager.off(action, callback);
|
||||
}
|
||||
|
||||
preload({ calLink }: { calLink: string }) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
validate(arguments[0], {
|
||||
required: true,
|
||||
props: {
|
||||
calLink: {
|
||||
type: "string",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const iframe = document.body.appendChild(document.createElement("iframe"));
|
||||
const config = this.cal.getConfig();
|
||||
|
||||
const urlInstance = new URL(`${config.calOrigin}/${calLink}`);
|
||||
urlInstance.searchParams.set("prerender", "true");
|
||||
iframe.src = urlInstance.toString();
|
||||
iframe.style.width = "0";
|
||||
iframe.style.height = "0";
|
||||
iframe.style.display = "none";
|
||||
}
|
||||
|
||||
ui(uiConfig: UiConfig) {
|
||||
validate(uiConfig, {
|
||||
required: true,
|
||||
props: {
|
||||
theme: {
|
||||
required: false,
|
||||
type: "string",
|
||||
},
|
||||
styles: {
|
||||
required: false,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.cal.doInIframe({ method: "ui", arg: uiConfig });
|
||||
}
|
||||
}
|
||||
|
||||
// This is a full fledged Cal instance but doesn't have ns property because it would be nested inside an ns instance already
|
||||
export interface GlobalCalWithoutNs {
|
||||
<T extends keyof SingleInstructionMap>(methodName: T, ...arg: Rest<SingleInstructionMap[T]>): void;
|
||||
/** Marks that the embed.js is loaded. Avoids re-downloading it. */
|
||||
loaded?: boolean;
|
||||
/** Maintains a queue till the time embed.js isn't loaded */
|
||||
q?: InstructionQueue;
|
||||
q: IArguments[];
|
||||
/** If user registers multiple namespaces, those are available here */
|
||||
ns?: Record<string, GlobalCal>;
|
||||
instance?: Cal;
|
||||
__css?: string;
|
||||
fingerprint?: string;
|
||||
__logQueue?: any[];
|
||||
__logQueue?: unknown[];
|
||||
}
|
||||
|
||||
// Well Omit removes the Function Signature from a type if used. So, instead construct the types like this.
|
||||
type GlobalCalWithNs = GlobalCalWithoutNs & {
|
||||
ns: Record<string, GlobalCalWithoutNs>;
|
||||
};
|
||||
|
||||
export type GlobalCal = GlobalCalWithNs;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Cal?: GlobalCal;
|
||||
Cal: GlobalCal;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalWindow extends Window {
|
||||
Cal?: GlobalCal;
|
||||
Cal: GlobalCal;
|
||||
}
|
||||
|
||||
globalCal.instance = new Cal("", globalCal.q!);
|
||||
|
||||
for (const [ns, api] of Object.entries(globalCal.ns!)) {
|
||||
api.instance = new Cal(ns, api.q!);
|
||||
globalCal.instance = new Cal("", globalCal.q);
|
||||
for (const [ns, api] of Object.entries(globalCal.ns)) {
|
||||
api.instance = new Cal(ns, api.q);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,38 +643,45 @@ window.addEventListener("message", (e) => {
|
||||
if (!actionManager) {
|
||||
throw new Error("Unhandled Action" + parsedAction);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
actionManager.fire(parsedAction.type, detail.data);
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
const htmlElement = e.target;
|
||||
if (!(htmlElement instanceof HTMLElement)) {
|
||||
const targetEl = e.target;
|
||||
if (!(targetEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const path = htmlElement.dataset.calLink;
|
||||
const path = targetEl.dataset.calLink;
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
const modalUniqueId = ((htmlElement as unknown as any).uniqueId =
|
||||
(htmlElement as unknown as any).uniqueId || Date.now());
|
||||
const namespace = htmlElement.dataset.calNamespace;
|
||||
const configString = htmlElement.dataset.calConfig || "";
|
||||
const modalUniqueId = (targetEl.dataset.uniqueId = targetEl.dataset.uniqueId || String(Date.now()));
|
||||
const namespace = targetEl.dataset.calNamespace;
|
||||
const configString = targetEl.dataset.calConfig || "";
|
||||
const calOrigin = targetEl.dataset.calOrigin || "";
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(configString);
|
||||
} catch (e) {
|
||||
config = {};
|
||||
}
|
||||
let api = globalCal;
|
||||
|
||||
let api: GlobalCalWithoutNs = globalCal;
|
||||
|
||||
if (namespace) {
|
||||
api = globalCal.ns![namespace];
|
||||
api = globalCal.ns[namespace];
|
||||
}
|
||||
|
||||
if (!api) {
|
||||
throw new Error(`Namespace ${namespace} isn't defined`);
|
||||
}
|
||||
|
||||
api("modal", {
|
||||
calLink: path,
|
||||
config,
|
||||
uid: modalUniqueId,
|
||||
calOrigin,
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,9 @@ const WEBAPP_URL =
|
||||
import.meta.env.EMBED_PUBLIC_WEBAPP_URL || `https://${import.meta.env.EMBED_PUBLIC_VERCEL_URL}`;
|
||||
const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;
|
||||
|
||||
(window as any).fingerprint = import.meta.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as string;
|
||||
|
||||
// Install Cal Embed Code Snippet
|
||||
(function (C, A, L) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const p = function (a: any, ar: any) {
|
||||
a.q.push(ar);
|
||||
};
|
||||
@@ -13,7 +12,7 @@ const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_UR
|
||||
C.Cal =
|
||||
C.Cal ||
|
||||
function () {
|
||||
const cal = C.Cal!;
|
||||
const cal = C.Cal;
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const ar = arguments;
|
||||
if (!cal.loaded) {
|
||||
@@ -23,13 +22,16 @@ const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_UR
|
||||
cal.loaded = true;
|
||||
}
|
||||
if (ar[0] === L) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const api: { (): void; q?: any[] } = function () {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
p(api, arguments);
|
||||
};
|
||||
const namespace = ar[1];
|
||||
api!.q = api.q || [];
|
||||
typeof namespace === "string" ? (cal!.ns![namespace] = api) && p(api, ar) : p(cal, ar);
|
||||
api.q = api.q || [];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar);
|
||||
return;
|
||||
}
|
||||
p(cal, ar);
|
||||
@@ -37,20 +39,24 @@ const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_UR
|
||||
})(window, EMBED_LIB_URL, "init");
|
||||
|
||||
const previewWindow = window;
|
||||
previewWindow.Cal.fingerprint = import.meta.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as string;
|
||||
|
||||
previewWindow.Cal!("init", {
|
||||
previewWindow.Cal("init", {
|
||||
origin: WEBAPP_URL,
|
||||
});
|
||||
const searchParams = new URL(document.URL).searchParams;
|
||||
const embedType = searchParams.get("embedType");
|
||||
const calLink = searchParams.get("calLink");
|
||||
if (embedType! === "inline") {
|
||||
previewWindow.Cal!("inline", {
|
||||
if (!calLink) {
|
||||
throw new Error('Missing "calLink" query parameter');
|
||||
}
|
||||
if (embedType === "inline") {
|
||||
previewWindow.Cal("inline", {
|
||||
elementOrSelector: "#my-embed",
|
||||
calLink: calLink,
|
||||
});
|
||||
} else if (embedType === "floating-popup") {
|
||||
previewWindow.Cal!("floatingButton", {
|
||||
previewWindow.Cal("floatingButton", {
|
||||
calLink: calLink,
|
||||
attributes: {
|
||||
id: "my-floating-button",
|
||||
@@ -58,7 +64,7 @@ if (embedType! === "inline") {
|
||||
});
|
||||
} else if (embedType === "element-click") {
|
||||
const button = document.createElement("button");
|
||||
button.setAttribute("data-cal-link", calLink!);
|
||||
button.setAttribute("data-cal-link", calLink);
|
||||
button.innerHTML = "I am a button that exists on your website";
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
type Namespace = string;
|
||||
type CustomEventDetail = Record<string, any>;
|
||||
type CustomEventDetail = Record<string, unknown>;
|
||||
|
||||
function _fireEvent(fullName: string, detail: CustomEventDetail) {
|
||||
const event = new window.CustomEvent(fullName, {
|
||||
@@ -9,6 +9,53 @@ function _fireEvent(fullName: string, detail: CustomEventDetail) {
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
export type EventDataMap = {
|
||||
eventTypeSelected: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
eventType: any;
|
||||
};
|
||||
linkFailed: {
|
||||
code: string;
|
||||
msg: string;
|
||||
data: {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
linkReady: Record<string, never>;
|
||||
bookingSuccessful: {
|
||||
// TODO: Shouldn't send the entire booking and eventType objects, we should send specific fields from them.
|
||||
booking: unknown;
|
||||
eventType: unknown;
|
||||
date: string;
|
||||
duration: number | undefined;
|
||||
organizer: {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
};
|
||||
confirmed: boolean;
|
||||
};
|
||||
"*": Record<string, unknown>;
|
||||
__routeChanged: Record<string, never>;
|
||||
__windowLoadComplete: Record<string, never>;
|
||||
__closeIframe: Record<string, never>;
|
||||
__iframeReady: Record<string, never>;
|
||||
__dimensionChanged: {
|
||||
iframeHeight: number;
|
||||
iframeWidth: number;
|
||||
isFirstTime: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type EventData<T extends keyof EventDataMap> = {
|
||||
[K in T]: {
|
||||
type: string;
|
||||
namespace: string;
|
||||
fullType: string;
|
||||
data: EventDataMap[K];
|
||||
};
|
||||
}[T];
|
||||
|
||||
export class SdkActionManager {
|
||||
namespace: Namespace;
|
||||
|
||||
@@ -31,7 +78,7 @@ export class SdkActionManager {
|
||||
return this.namespace ? `CAL:${this.namespace}:${name}` : `CAL::${name}`;
|
||||
}
|
||||
|
||||
fire(name: string, data: CustomEventDetail) {
|
||||
fire<T extends keyof EventDataMap>(name: T, data: EventDataMap[T]) {
|
||||
const fullName = this.getFullActionName(name);
|
||||
const detail = {
|
||||
type: name,
|
||||
@@ -46,12 +93,12 @@ export class SdkActionManager {
|
||||
_fireEvent(this.getFullActionName("*"), detail);
|
||||
}
|
||||
|
||||
on(name: string, callback: (arg0: CustomEvent<CustomEventDetail>) => void) {
|
||||
on<T extends keyof EventDataMap>(name: T, callback: (arg0: CustomEvent<EventData<T>>) => void) {
|
||||
const fullName = this.getFullActionName(name);
|
||||
window.addEventListener(fullName, callback as EventListener);
|
||||
}
|
||||
|
||||
off(name: string, callback: (arg0: CustomEvent<CustomEventDetail>) => void) {
|
||||
off<T extends keyof EventDataMap>(name: T, callback: (arg0: CustomEvent<EventData<T>>) => void) {
|
||||
const fullName = this.getFullActionName(name);
|
||||
window.removeEventListener(fullName, callback as EventListener);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "@calcom/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "esnext",
|
||||
"paths": {
|
||||
"@lib/*": ["../../../apps/web/lib/*"]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
|
||||
<script type="module" src="./floating.tsx"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
/* First make sure that you have installed the package */
|
||||
|
||||
/* If you are using yarn */
|
||||
// yarn add @calcom/embed-react
|
||||
|
||||
/* If you are using npm */
|
||||
// npm install @calcom/embed-react
|
||||
import { useEffect } from "react";
|
||||
import * as React from "react";
|
||||
import ReactDom from "react-dom";
|
||||
|
||||
import { getCalApi } from "./src/index";
|
||||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const cal = await getCalApi("http://localhost:3000/embed/embed.js");
|
||||
cal("floatingButton", { calLink: "teampro/abc", calOrigin: "http://localhost:3000" });
|
||||
cal("ui", { styles: { branding: { brandColor: "#000000" } }, hideEventTypeDetails: false });
|
||||
})();
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
ReactDom.render(<App />, document.getElementById("root"));
|
||||
@@ -2,7 +2,7 @@
|
||||
<head>
|
||||
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
|
||||
<script type="module" src="./test-cal.tsx"></script>
|
||||
<script type="module" src="./inline.tsx"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import ReactDom from "react-dom";
|
||||
|
||||
import Cal, { getCalApi } from "./src/index";
|
||||
@@ -46,7 +48,6 @@ function App() {
|
||||
theme: "dark",
|
||||
}}
|
||||
/>
|
||||
<button data-cal-link="pro">Popup</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ type CalProps = {
|
||||
debug?: boolean;
|
||||
uiDebug?: boolean;
|
||||
};
|
||||
config?: any;
|
||||
config?: Record<string, string>;
|
||||
embedJsUrl?: string;
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
@@ -24,7 +24,7 @@ const Cal = function Cal(props: CalProps) {
|
||||
const Cal = useEmbed(embedJsUrl);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (!Cal || initializedRef.current) {
|
||||
if (!Cal || initializedRef.current || !ref.current) {
|
||||
return;
|
||||
}
|
||||
initializedRef.current = true;
|
||||
|
||||
@@ -5,9 +5,9 @@ import EmbedSnippet from "@calcom/embed-snippet";
|
||||
|
||||
import Cal from "./Cal";
|
||||
|
||||
export const getCalApi = (): Promise<GlobalCal> =>
|
||||
export const getCalApi = (embedJsUrl?: string): Promise<GlobalCal> =>
|
||||
new Promise(function tryReadingFromWindow(resolve) {
|
||||
EmbedSnippet();
|
||||
EmbedSnippet(embedJsUrl);
|
||||
const api = window.Cal;
|
||||
if (!api) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"extends": "@calcom/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ES2015",
|
||||
"moduleResolution": "Node",
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment,prefer-rest-params,prefer-const */
|
||||
import type { GlobalCal, GlobalCalWithoutNs } from "@calcom/embed-core";
|
||||
import type { Optional } from "@calcom/types/utils";
|
||||
|
||||
/**
|
||||
* As we want to keep control on the size of this snippet but we want some portion of it to be still readable.
|
||||
* So, write the code that you need directly but keep it short.
|
||||
*/
|
||||
import type { CalWindow } from "@calcom/embed-core";
|
||||
|
||||
const WEBAPP_URL =
|
||||
import.meta.env.EMBED_PUBLIC_WEBAPP_URL || `https://${import.meta.env.EMBED_PUBLIC_VERCEL_URL}`;
|
||||
@@ -12,37 +13,45 @@ const WEBAPP_URL =
|
||||
const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;
|
||||
|
||||
export default function EmbedSnippet(url = EMBED_LIB_URL) {
|
||||
(function (C: CalWindow, A, L) {
|
||||
let p = function (a: any, ar: any) {
|
||||
(function (C, A, L) {
|
||||
let p = function (a: GlobalCalWithoutNs, ar: IArguments) {
|
||||
a.q.push(ar);
|
||||
};
|
||||
let d = C.document;
|
||||
C.Cal =
|
||||
C.Cal ||
|
||||
function () {
|
||||
let cal = C.Cal!;
|
||||
let cal = C.Cal;
|
||||
let ar = arguments;
|
||||
if (!cal.loaded) {
|
||||
// 'ns' and 'q' are now definitely set with the following 2 lines, so you can safely assert in TypeScript that it's GlobalCal now.
|
||||
cal.ns = {};
|
||||
cal.q = cal.q || [];
|
||||
|
||||
//@ts-ignore
|
||||
d.head.appendChild(d.createElement("script")).src = A;
|
||||
cal.loaded = true;
|
||||
}
|
||||
|
||||
if (ar[0] === L) {
|
||||
const api: { (): void; q: any[] } = function () {
|
||||
const api: GlobalCalWithoutNs = function () {
|
||||
p(api, arguments);
|
||||
};
|
||||
const namespace = ar[1];
|
||||
api.q = api.q || [];
|
||||
typeof namespace === "string" ? (cal.ns![namespace] = api) && p(api, ar) : p(cal, ar);
|
||||
typeof namespace === "string"
|
||||
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
(cal.ns![namespace] = api) && p(api, ar)
|
||||
: p(cal as GlobalCal, ar);
|
||||
return;
|
||||
}
|
||||
p(cal, ar);
|
||||
p(cal as GlobalCal, ar);
|
||||
};
|
||||
})(
|
||||
window,
|
||||
window as Omit<Window, "Cal"> & {
|
||||
// Make 'ns' and 'q' optional as they are set through the snippet above
|
||||
Cal: Optional<GlobalCal, "ns" | "q">;
|
||||
},
|
||||
//! Replace it with "https://cal.com/embed.js" or the URL where you have embed.js installed
|
||||
url,
|
||||
"init"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "@calcom/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"baseUrl": ".",
|
||||
"module": "esnext",
|
||||
"declaration": true
|
||||
|
||||
Reference in new issue
Block a user