fix: Namespaced react embeds not working with Floating and Element Click Popups (#13386)
* Fix embed queue * Fix getCalApi namespace use * Fixes * Add more e2e * Fix test * Fix syntax error in generated code * Make getCalApi argument optional * Fix types * Add more namespacing tests --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
This commit is contained in:
co-authored by
Joe Au-Yeung
parent
be128181e2
commit
4f69c0e502
@@ -3,12 +3,17 @@ import type { Page } from "@playwright/test";
|
||||
export const createEmbedsFixture = (page: Page) => {
|
||||
return {
|
||||
/**
|
||||
* @deprecated Use gotoPlayground instead
|
||||
* @deprecated
|
||||
* Use 'gotoPlayground' instead, to navigate. It calls `addEmbedListeners` automatically.
|
||||
*/
|
||||
async addEmbedListeners(calNamespace: string) {
|
||||
await page.addInitScript(
|
||||
({ calNamespace }: { calNamespace: string }) => {
|
||||
console.log("PlaywrightTest:", "Adding listener for __iframeReady on namespace:", calNamespace);
|
||||
console.log(
|
||||
"PlaywrightTest - InitScript:",
|
||||
"Adding listener for __iframeReady on namespace:",
|
||||
calNamespace
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
window.eventsFiredStoreForPlaywright = window.eventsFiredStoreForPlaywright || {};
|
||||
@@ -43,14 +48,18 @@ export const createEmbedsFixture = (page: Page) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
api = window.Cal.ns[calNamespace];
|
||||
console.log("Using api from namespace-", { calNamespace, api });
|
||||
}
|
||||
if (!api) {
|
||||
console.log(`namespace "${calNamespace}" not found yet - Trying again`);
|
||||
setTimeout(tryAddingListener, 500);
|
||||
return;
|
||||
}
|
||||
console.log("PlaywrightTest:", `Adding listener for __iframeReady on namespace:${calNamespace}`);
|
||||
if (!api) {
|
||||
throw new Error(`namespace "${calNamespace}" not found`);
|
||||
}
|
||||
api("on", {
|
||||
action: "*",
|
||||
callback: (e) => {
|
||||
console.log("Playwright Embed Fixture: Received event", 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
|
||||
|
||||
@@ -16,7 +16,7 @@ global $post;extract(shortcode_atts(array(
|
||||
'for' => $post->post_title,
|
||||
), $atts));
|
||||
if(empty($content)) $content='Embed Cal.com';
|
||||
|
||||
// TODO: How to reuse embed-snippet export here?
|
||||
return '<script>(function (C, A, L){let p=function (a, ar){a.q.push(ar);}; let d=C.document; C.Cal=C.Cal || function (){let cal=C.Cal; let ar=arguments; if (!cal.loaded){cal.ns={}; cal.q=cal.q || []; d.head.appendChild(d.createElement("script")).src=A; cal.loaded=true;}if (ar[0]===L){const api=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); return;}p(cal, ar);};})(window, "https://cal.com/embed.js", "init"); Cal("init") </script> <script>Cal("inline",{calLink: '.$content.'});</script>';
|
||||
}
|
||||
add_shortcode('cal', 'cal_shortcode');
|
||||
@@ -20,43 +20,126 @@
|
||||
(() => {
|
||||
const url = new URL(document.URL);
|
||||
// Only run the example specified by only=, avoids distraction and faster to test.
|
||||
const only = (window.only = url.searchParams.get("only"));
|
||||
const only = (window.only = url.searchParams.get("only") || '');
|
||||
const elementIdentifier = only !== "all" ? only.replace("ns:", "") : null;
|
||||
if (elementIdentifier) {
|
||||
location.hash = "#cal-booking-place-" + elementIdentifier + "-iframe";
|
||||
}
|
||||
})();
|
||||
|
||||
function addInlineEmbedInNewNamespaceWithoutReload(selector) {
|
||||
Cal("init", "withoutReloadNamespace", {
|
||||
debug: true,
|
||||
calOrigin: "http://localhost:3000",
|
||||
});
|
||||
|
||||
Cal.ns.withoutReloadNamespace("inline", {
|
||||
elementOrSelector: selector,
|
||||
calLink: "pro?case=addInlineEmbedInANewNamespaceWithoutReload",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
(function (C, A, L) {
|
||||
// Put the snippet in a function so that it can be re-executed for scenario testing
|
||||
// TODO: How to reuse embed-snippet export here?
|
||||
function embedSnippet() {
|
||||
(function (C, A, L) {
|
||||
let p = function (a, ar) {
|
||||
a.q.push(ar);
|
||||
};
|
||||
let d = C.document;
|
||||
C.Cal =
|
||||
C.Cal ||
|
||||
function () {
|
||||
let cal = C.Cal;
|
||||
let ar = arguments;
|
||||
if (!cal.loaded) {
|
||||
cal.ns = {};
|
||||
cal.q = cal.q || [];
|
||||
d.head.appendChild(d.createElement("script")).src = A;
|
||||
cal.loaded = true;
|
||||
}
|
||||
if (ar[0] === L) {
|
||||
const api = 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);
|
||||
return;
|
||||
}
|
||||
p(cal, ar);
|
||||
};
|
||||
})(window, "//localhost:3000/embed/embed.js", "init");
|
||||
</script>
|
||||
C.Cal =
|
||||
C.Cal ||
|
||||
function () {
|
||||
let cal = C.Cal;
|
||||
let ar = arguments;
|
||||
if (!cal.loaded) {
|
||||
cal.ns = {};
|
||||
cal.q = cal.q || [];
|
||||
d.head.appendChild(d.createElement("script")).src = A;
|
||||
cal.loaded = true;
|
||||
}
|
||||
if (ar[0] === L) {
|
||||
const api = function () {
|
||||
p(api, arguments);
|
||||
};
|
||||
const namespace = ar[1];
|
||||
api.q = api.q || [];
|
||||
if (typeof namespace === "string"){
|
||||
// Make sure that even after re-execution of the snippet, the namespace is not overridden
|
||||
cal.ns[namespace] = cal.ns[namespace] || api;
|
||||
p(cal.ns[namespace], ar);
|
||||
p(cal, ['initNamespace', namespace])
|
||||
} else p(cal, ar);
|
||||
return;
|
||||
}
|
||||
p(cal, ar);
|
||||
};
|
||||
})(window, "//localhost:3000/embed/embed.js", "init");
|
||||
}
|
||||
embedSnippet();
|
||||
</script>
|
||||
<script>
|
||||
function doubleInstallSnippetWithInlineEmbedWithNonDefaultNamespace(selector) {
|
||||
Cal("init", "doubleInstall", {
|
||||
debug: true,
|
||||
calOrigin: "http://localhost:3000",
|
||||
});
|
||||
Cal.ns.doubleInstall("inline", {
|
||||
elementOrSelector: selector,
|
||||
calLink: "pro?case=doubleInstallSnippetWithInlineEmbedWithNonDefaultNamespace",
|
||||
});
|
||||
embedSnippet();
|
||||
Cal("init", "doubleInstall", {
|
||||
debug: true,
|
||||
calOrigin: "http://localhost:3000",
|
||||
});
|
||||
Cal.ns.doubleInstall("inline", {
|
||||
elementOrSelector: selector,
|
||||
calLink: "pro",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function doubleInstallSnippetWithInlineEmbed(selector) {
|
||||
Cal("init", {
|
||||
debug: true,
|
||||
calOrigin: "http://localhost:3000",
|
||||
});
|
||||
Cal("inline", {
|
||||
elementOrSelector: selector,
|
||||
calLink: "pro?case=doubleInstallSnippetWithInlineEmbed",
|
||||
});
|
||||
embedSnippet();
|
||||
Cal("init", {
|
||||
debug: true,
|
||||
calOrigin: "http://localhost:3000",
|
||||
});
|
||||
Cal("inline", {
|
||||
elementOrSelector: selector,
|
||||
calLink: "pro",
|
||||
});
|
||||
}
|
||||
|
||||
function addTwoInlineEmbedWithTwoDifferentNamespacesWithDifferentInitConfig() {
|
||||
Cal("init", "namespace-init-test-1", {
|
||||
debug: true,
|
||||
calOrigin: "http://localhost:3000",
|
||||
});
|
||||
Cal("init", "namespace-init-test-2", {
|
||||
debug: true,
|
||||
calOrigin: "http://127.0.0.1:3000",
|
||||
});
|
||||
Cal.ns['namespace-init-test-2']("inline", {
|
||||
elementOrSelector: '#two-different-namespace-with-different-init-config-content-2',
|
||||
calLink: "pro",
|
||||
});
|
||||
Cal.ns['namespace-init-test-1']("inline", {
|
||||
elementOrSelector: '#two-different-namespace-with-different-init-config-content-1',
|
||||
calLink: "pro",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
@@ -82,6 +165,25 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<button id="add-inline-embed-in-a-new-namespace-without-reload-button" onclick="addInlineEmbedInNewNamespaceWithoutReload('#add-inline-embed-in-a-new-namespace-without-reload-button-content')">Add Inline Embed in a new namespace without reload</button>
|
||||
<div id="add-inline-embed-in-a-new-namespace-without-reload-button-content"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="double-install-snippet-with-inline-embed-default-namespace-button" onclick="doubleInstallSnippetWithInlineEmbed('#double-install-snippet-with-inline-embed-default-namespace-content')">Double Install Embed Snippet with inline embed - Default Namespace</button>
|
||||
<div id="double-install-snippet-with-inline-embed-default-namespace-content"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="double-install-snippet-with-inline-embed-non-default-namespace-button" onclick="doubleInstallSnippetWithInlineEmbedWithNonDefaultNamespace('#double-install-snippet-with-inline-embed-non-default-namespace-content')">Double Install Embed Snippet with inline embed - Non Default Namespace</button>
|
||||
<div id="double-install-snippet-with-inline-embed-non-default-namespace-content"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="two-different-namespace-with-different-init-config" onclick="addTwoInlineEmbedWithTwoDifferentNamespacesWithDifferentInitConfig()">Two different namespace with two different config</button>
|
||||
<div id="two-different-namespace-with-different-init-config-area">
|
||||
<div id="two-different-namespace-with-different-init-config-content-1"></div>
|
||||
<div id="two-different-namespace-with-different-init-config-content-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span style="display: block"><a href="?color-scheme=dark">With Dark Color Scheme for the Page</a></span>
|
||||
<span style="display: block"><a href="?nonResponsive">Non responsive version of this page here</a></span>
|
||||
<span style="display: block"
|
||||
@@ -175,6 +277,7 @@
|
||||
<button data-cal-namespace="upcomingBookings" data-cal-config='{"theme":"dark"}' data-cal-link="bookings/upcoming">Show Upcoming Bookings</button>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<h2>Inline Embed Examples</h2>
|
||||
<div id="namespaces-test">
|
||||
<div class="inline-embed-container" id="cal-booking-place-default">
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { test } from "@calcom/web/playwright/lib/fixtures";
|
||||
|
||||
import { getEmbedIframe } from "../lib/testUtils";
|
||||
|
||||
test.describe("Namespacing", () => {
|
||||
test.describe("Inline Embed", () => {
|
||||
test("Add inline embed using a namespace without reload", async ({ page, embeds }) => {
|
||||
const calNamespace = "withoutReloadNamespace";
|
||||
await embeds.gotoPlayground({ calNamespace, url: "/" });
|
||||
await page.click("#add-inline-embed-in-a-new-namespace-without-reload-button");
|
||||
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
|
||||
expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
|
||||
pathname: "/pro",
|
||||
searchParams: {
|
||||
case: "addInlineEmbedInANewNamespaceWithoutReload",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Double install Embed Snippet with inline embed using a namespace", async ({ page, embeds }) => {
|
||||
const calNamespace = "doubleInstall";
|
||||
await embeds.gotoPlayground({ calNamespace, url: "/" });
|
||||
await page.click("#double-install-snippet-with-inline-embed-non-default-namespace-button");
|
||||
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
|
||||
expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
|
||||
pathname: "/pro",
|
||||
searchParams: {
|
||||
case: "doubleInstallSnippetWithInlineEmbedWithNonDefaultNamespace",
|
||||
},
|
||||
});
|
||||
expect(await page.locator("iframe").count()).toBe(1);
|
||||
});
|
||||
|
||||
test("Double install Embed Snippet with inline embed without a namespace(i.e. default namespace)", async ({
|
||||
page,
|
||||
embeds,
|
||||
}) => {
|
||||
const calNamespace = "";
|
||||
await embeds.gotoPlayground({ calNamespace, url: "/" });
|
||||
await page.click("#double-install-snippet-with-inline-embed-default-namespace-button");
|
||||
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
|
||||
expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
|
||||
pathname: "/pro",
|
||||
searchParams: {
|
||||
case: "doubleInstallSnippetWithInlineEmbed",
|
||||
},
|
||||
});
|
||||
expect(await page.locator("iframe").count()).toBe(1);
|
||||
});
|
||||
});
|
||||
test("Different namespaces can have different init configs", async ({ page, embeds }) => {
|
||||
await Promise.all([
|
||||
embeds.addEmbedListeners("namespace-init-test-1"),
|
||||
embeds.addEmbedListeners("namespace-init-test-2"),
|
||||
]);
|
||||
|
||||
await page.goto("/");
|
||||
await page.click("#two-different-namespace-with-different-init-config");
|
||||
const namespace1IframeSrc = await page.locator("iframe").nth(0).getAttribute("src");
|
||||
const namespace2IframeSrc = await page.locator("iframe").nth(1).getAttribute("src");
|
||||
expect(namespace1IframeSrc).toContain("http://localhost:3000/pro");
|
||||
expect(namespace2IframeSrc).toContain("http://127.0.0.1:3000/pro");
|
||||
});
|
||||
});
|
||||
@@ -211,7 +211,7 @@ export class Cal {
|
||||
return instruction;
|
||||
}
|
||||
|
||||
processQueue(queue: IArguments[]) {
|
||||
processQueue(queue: Queue) {
|
||||
queue.forEach((instruction) => {
|
||||
this.processInstruction(instruction);
|
||||
});
|
||||
@@ -315,7 +315,7 @@ export class Cal {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(namespace: string, q: IArguments[]) {
|
||||
constructor(namespace: string, q: Queue) {
|
||||
this.__config = {
|
||||
// Use WEBAPP_URL till full page reload problem with website URL is solved
|
||||
calOrigin: WEBAPP_URL,
|
||||
@@ -413,9 +413,22 @@ class CalApi {
|
||||
this.cal = cal;
|
||||
}
|
||||
|
||||
/**
|
||||
* If namespaceOrConfig is a string, config is available in config argument
|
||||
* If namespaceOrConfig is an object, namespace is assumed to be default and config isn't provided
|
||||
*/
|
||||
init(namespaceOrConfig?: string | InitArgConfig, config = {} as InitArgConfig) {
|
||||
let initForNamespace = "";
|
||||
if (typeof namespaceOrConfig !== "string") {
|
||||
config = (namespaceOrConfig || {}) as Config;
|
||||
} else {
|
||||
initForNamespace = namespaceOrConfig;
|
||||
}
|
||||
|
||||
// Just in case 'init' instruction belongs to another namespace, ignore it
|
||||
// Though it shouldn't happen normally as the snippet takes care of delegating the init instruction to appropriate namespace queue
|
||||
if (initForNamespace !== this.cal.namespace) {
|
||||
return;
|
||||
}
|
||||
|
||||
CalApi.initializedNamespaces.push(this.cal.namespace);
|
||||
@@ -426,6 +439,16 @@ class CalApi {
|
||||
|
||||
this.cal.__config = { ...this.cal.__config, ...restConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when a non-default namespace is to be initialized
|
||||
* It allows default queue to take care of instantiation of the non-default namespace queue
|
||||
*/
|
||||
initNamespace(namespace: string) {
|
||||
// Creating this instance automatically starts processing the queue for the namespace
|
||||
globalCal.ns[namespace].instance =
|
||||
globalCal.ns[namespace].instance || new Cal(namespace, globalCal.ns[namespace].q);
|
||||
}
|
||||
/**
|
||||
* It is an instruction that adds embed iframe inline as last child of the element
|
||||
*/
|
||||
@@ -458,6 +481,12 @@ class CalApi {
|
||||
},
|
||||
});
|
||||
|
||||
// If someone re-executes inline embed instruction, we want to ensure that duplicate inlineEl isn't added to the page per namespace
|
||||
if (this.cal.inlineEl && document.body.contains(this.cal.inlineEl)) {
|
||||
console.warn("Inline embed already exists. Ignoring this call");
|
||||
return;
|
||||
}
|
||||
|
||||
config = config || {};
|
||||
if (typeof config.iframeAttrs === "string" || config.iframeAttrs instanceof Array) {
|
||||
throw new Error("iframeAttrs should be an object");
|
||||
@@ -766,13 +795,16 @@ class CalApi {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type Queue = any[];
|
||||
|
||||
// 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: IArguments[];
|
||||
q: Queue;
|
||||
/** If user registers multiple namespaces, those are available here */
|
||||
instance?: Cal;
|
||||
__css?: string;
|
||||
@@ -800,8 +832,11 @@ export interface CalWindow extends Window {
|
||||
const DEFAULT_NAMESPACE = "";
|
||||
|
||||
globalCal.instance = new Cal(DEFAULT_NAMESPACE, globalCal.q);
|
||||
|
||||
// Namespaces created before embed.js executes are instantiated here for old Embed Snippets which don't use 'initNamespace' instruction
|
||||
// Snippets that support 'initNamespace' instruction don't really need this but it is okay if it's done because it's idempotent
|
||||
for (const [ns, api] of Object.entries(globalCal.ns)) {
|
||||
api.instance = new Cal(ns, api.q);
|
||||
api.instance = api.instance ?? new Cal(ns, api.q);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,10 +6,12 @@ const embedLibUrl = searchParams.get("embedLibUrl");
|
||||
if (!bookerUrl || !embedLibUrl) {
|
||||
throw new Error('Can\'t Preview: Missing "bookerUrl" or "embedLibUrl" query parameter');
|
||||
}
|
||||
// TODO: Reuse the embed code snippet from the embed-snippet package - Not able to use it because of circular dependency
|
||||
// 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) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
const p = function (a, ar) {
|
||||
a.q.push(ar);
|
||||
};
|
||||
const d = C.document;
|
||||
@@ -17,6 +19,7 @@ if (!bookerUrl || !embedLibUrl) {
|
||||
C.Cal ||
|
||||
function () {
|
||||
const cal = C.Cal;
|
||||
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const ar = arguments;
|
||||
if (!cal.loaded) {
|
||||
@@ -26,22 +29,25 @@ if (!bookerUrl || !embedLibUrl) {
|
||||
cal.loaded = true;
|
||||
}
|
||||
if (ar[0] === L) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const api: { (): void; q?: any[] } = function () {
|
||||
const api = function () {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
p(api, arguments);
|
||||
};
|
||||
const namespace = ar[1];
|
||||
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);
|
||||
api.q = api.q || [];
|
||||
if (typeof namespace === "string") {
|
||||
// Make sure that even after re-execution of the snippet, the namespace is not overridden
|
||||
cal.ns[namespace] = cal.ns[namespace] || api;
|
||||
p(cal.ns[namespace], ar);
|
||||
p(cal, ["initNamespace", namespace]);
|
||||
} else p(cal, ar);
|
||||
return;
|
||||
}
|
||||
p(cal, ar);
|
||||
};
|
||||
})(window, embedLibUrl, "init");
|
||||
|
||||
})(window, "//localhost:3000/embed/embed.js", "init");
|
||||
const previewWindow = window;
|
||||
previewWindow.Cal.fingerprint = process.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as string;
|
||||
|
||||
|
||||
@@ -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="./element-click.tsx"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @fileoverview This file is an example file and tells how to use the element-click popup embed in a React application. This is also used by playwright e2e
|
||||
*/
|
||||
import { useEffect } from "react";
|
||||
import * as React from "react";
|
||||
import ReactDom from "react-dom";
|
||||
|
||||
import { getCalApi } from "./src/index";
|
||||
|
||||
const calNamespace = "element-click";
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const cal = await getCalApi({
|
||||
embedJsUrl: "http://localhost:3000/embed/embed.js",
|
||||
namespace: calNamespace,
|
||||
});
|
||||
cal("ui", { styles: { branding: { brandColor: "#000000" } }, hideEventTypeDetails: false });
|
||||
})();
|
||||
}, []);
|
||||
return (
|
||||
<button
|
||||
data-cal-namespace={calNamespace}
|
||||
data-cal-link="pro"
|
||||
data-cal-config='{"layout":"month_view", "theme":"dark"}'>
|
||||
Click me
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDom.render(<App />, document.getElementById("root"));
|
||||
@@ -1,10 +1,6 @@
|
||||
/* 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
|
||||
/**
|
||||
* @fileoverview This file is an example file and tells how to use floating popup button in a React application. This is also used by playwright e2e
|
||||
*/
|
||||
import { useEffect } from "react";
|
||||
import * as React from "react";
|
||||
import ReactDom from "react-dom";
|
||||
@@ -14,8 +10,17 @@ 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" });
|
||||
const cal = await getCalApi({
|
||||
namespace: "floating",
|
||||
embedJsUrl: "http://localhost:3000/embed/embed.js",
|
||||
});
|
||||
cal("floatingButton", {
|
||||
calLink: "pro",
|
||||
calOrigin: "http://localhost:3000",
|
||||
config: {
|
||||
theme: "dark",
|
||||
},
|
||||
});
|
||||
cal("ui", { styles: { branding: { brandColor: "#000000" } }, hideEventTypeDetails: false });
|
||||
})();
|
||||
}, []);
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<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="./inline.tsx"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<h1>Playground has following demos</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="./inline.html">Inline</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./floating.html">Floating Button Popup</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./element-click.html">Element Click Popup</a>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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="./inline.tsx"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @fileoverview This file is an example file and tells how to use the Cal component in a React application. This is also used by playwright e2e
|
||||
*/
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
@@ -7,7 +10,9 @@ import ReactDom from "react-dom";
|
||||
// There are tests in test/built which verifiy that the types from built package are correctly generated and exported correctly.
|
||||
import Cal, { getCalApi } from "./src/index";
|
||||
|
||||
const api = getCalApi();
|
||||
const api = getCalApi({
|
||||
namespace: "inline",
|
||||
});
|
||||
|
||||
function App() {
|
||||
const [, setLoaded] = useState(false);
|
||||
@@ -37,6 +42,7 @@ function App() {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
api.then((api) => {
|
||||
api("off", {
|
||||
@@ -54,6 +60,7 @@ function App() {
|
||||
<Cal
|
||||
calOrigin="http://localhost:3000"
|
||||
embedJsUrl="//localhost:3000/embed/embed.js"
|
||||
namespace="inline"
|
||||
style={{ width: "100%", height: "100%", overflow: "scroll" }}
|
||||
calLink="pro"
|
||||
config={{
|
||||
|
||||
@@ -3,22 +3,63 @@ import { expect } from "@playwright/test";
|
||||
import { getEmbedIframe } from "@calcom/embed-core/playwright/lib/testUtils";
|
||||
import { test } from "@calcom/web/playwright/lib/fixtures";
|
||||
|
||||
test.describe("Inline Embed", () => {
|
||||
test("should verify that the iframe got created with correct URL", async ({
|
||||
page,
|
||||
embeds: { getActionFiredDetails, addEmbedListeners },
|
||||
}) => {
|
||||
//TODO: Do it with page.goto automatically
|
||||
await addEmbedListeners("");
|
||||
await page.goto("/");
|
||||
const calNamespace = "";
|
||||
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
|
||||
expect(embedIframe).toBeEmbedCalLink("", getActionFiredDetails, {
|
||||
pathname: "/pro",
|
||||
searchParams: {
|
||||
theme: "dark",
|
||||
},
|
||||
test.describe("React Embed", () => {
|
||||
test.describe("Inline", () => {
|
||||
test("should verify that the iframe got created with correct URL - namespaced", async ({
|
||||
page,
|
||||
embeds,
|
||||
}) => {
|
||||
const calNamespace = "inline";
|
||||
await embeds.gotoPlayground({ url: "/inline.html", calNamespace });
|
||||
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
|
||||
expect(embedIframe).toBeEmbedCalLink("", embeds.getActionFiredDetails, {
|
||||
pathname: "/pro",
|
||||
searchParams: {
|
||||
theme: "dark",
|
||||
},
|
||||
});
|
||||
// expect(await page.screenshot()).toMatchSnapshot("react-component-inline.png");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Floating button Popup", () => {
|
||||
test("should verify that the iframe got created with correct URL - namespaced", async ({
|
||||
page,
|
||||
embeds,
|
||||
}) => {
|
||||
const calNamespace = "floating";
|
||||
await page.waitForLoadState();
|
||||
await embeds.gotoPlayground({ url: "/floating.html", calNamespace });
|
||||
|
||||
await page.click("text=Book my Cal");
|
||||
|
||||
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
|
||||
expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
|
||||
pathname: "/pro",
|
||||
searchParams: {
|
||||
theme: "dark",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Element Click Popup", () => {
|
||||
test("should verify that the iframe got created with correct URL - namespaced", async ({
|
||||
page,
|
||||
embeds,
|
||||
}) => {
|
||||
const calNamespace = "element-click";
|
||||
await embeds.gotoPlayground({ url: "/element-click.html", calNamespace });
|
||||
await page.waitForLoadState();
|
||||
await page.click("text=Click me");
|
||||
|
||||
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
|
||||
expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
|
||||
pathname: "/pro",
|
||||
searchParams: {
|
||||
theme: "dark",
|
||||
},
|
||||
});
|
||||
});
|
||||
// expect(await page.screenshot()).toMatchSnapshot("react-component-inline.png");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import type { GlobalCal } from "@calcom/embed-core";
|
||||
import type { GlobalCal, GlobalCalWithoutNs } from "@calcom/embed-core";
|
||||
import EmbedSnippet from "@calcom/embed-snippet";
|
||||
|
||||
import Cal from "./Cal";
|
||||
|
||||
export const getCalApi = (embedJsUrl?: string): Promise<GlobalCal> =>
|
||||
new Promise(function tryReadingFromWindow(resolve) {
|
||||
EmbedSnippet(embedJsUrl);
|
||||
const api = window.Cal;
|
||||
export function getCalApi(options?: {
|
||||
embedJsUrl?: string;
|
||||
namespace?: string;
|
||||
}): Promise<GlobalCal | GlobalCalWithoutNs>;
|
||||
export function getCalApi(embedJsUrl: string): Promise<GlobalCal | GlobalCalWithoutNs>;
|
||||
|
||||
export function getCalApi(
|
||||
optionsOrEmbedJsUrl?:
|
||||
| {
|
||||
embedJsUrl?: string;
|
||||
namespace?: string;
|
||||
}
|
||||
| string
|
||||
): Promise<GlobalCal | GlobalCalWithoutNs> {
|
||||
const options =
|
||||
typeof optionsOrEmbedJsUrl === "string" ? { embedJsUrl: optionsOrEmbedJsUrl } : optionsOrEmbedJsUrl ?? {};
|
||||
|
||||
const { namespace = "", embedJsUrl } = options;
|
||||
return new Promise(function tryReadingFromWindow(resolve) {
|
||||
const globalCal = EmbedSnippet(embedJsUrl);
|
||||
globalCal("init", namespace);
|
||||
const api = namespace ? globalCal.ns[namespace as keyof typeof globalCal.ns] : globalCal;
|
||||
if (!api) {
|
||||
setTimeout(() => {
|
||||
tryReadingFromWindow(resolve);
|
||||
@@ -17,5 +35,6 @@ export const getCalApi = (embedJsUrl?: string): Promise<GlobalCal> =>
|
||||
}
|
||||
resolve(api);
|
||||
});
|
||||
}
|
||||
|
||||
export default Cal;
|
||||
|
||||
@@ -4,7 +4,7 @@ Vanilla JS embed snippet that is responsible to fetch @calcom/embed-core and thu
|
||||
|
||||
## Development
|
||||
|
||||
`yarn build` will generate dist/snippet.es.js
|
||||
`yarn build` will generate dist/snippet.es.js. If you are going to test react embeds, make sure that you have built it so that they get the upto-date snippet
|
||||
|
||||
- which can be used as `<script type="module" src=...`
|
||||
- You can also copy the appropriate portion of the code and install it directly as `<script>CODE_SUGGESTED_TO_BE_COPIED</script>`
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment,prefer-rest-params,prefer-const */
|
||||
import type { GlobalCal, GlobalCalWithoutNs } from "@calcom/embed-core";
|
||||
import type { GlobalCal, GlobalCalWithoutNs, Queue } from "@calcom/embed-core";
|
||||
// FIXME: embed-snippet is a published package and shouldn't import from @calcom/types which is unpublished
|
||||
// This isn't a problem at the moment because embed-snippet isn't directly imported and embed-react which uses it doesn't depend on this
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -15,9 +17,19 @@ const WEBAPP_URL =
|
||||
|
||||
const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;
|
||||
|
||||
type QueuePushArg = {
|
||||
[k: number]: Queue[number];
|
||||
};
|
||||
|
||||
/**
|
||||
* When modifying this snippet, make sure to keep the snippets in following places in sync
|
||||
* 1. EmbedTabs.tsx
|
||||
* 2. embed-core/index.html
|
||||
* 3. app-store/wordpress/plugin.php
|
||||
*/
|
||||
export default function EmbedSnippet(url = EMBED_LIB_URL) {
|
||||
(function (C, A, L) {
|
||||
let p = function (a: GlobalCalWithoutNs, ar: IArguments) {
|
||||
let p = function (a: GlobalCalWithoutNs, ar: QueuePushArg) {
|
||||
a.q.push(ar);
|
||||
};
|
||||
let d = C.document;
|
||||
@@ -42,10 +54,13 @@ export default function EmbedSnippet(url = EMBED_LIB_URL) {
|
||||
};
|
||||
const namespace = ar[1];
|
||||
api.q = api.q || [];
|
||||
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);
|
||||
if (typeof namespace === "string") {
|
||||
// Make sure that even after re-execution of the snippet, the namespace is not overridden
|
||||
cal.ns![namespace] = cal.ns![namespace] || api;
|
||||
p(cal.ns![namespace], ar);
|
||||
// Inform the default namespace queue to initialize this namespace
|
||||
p(cal as GlobalCal, ["initNamespace", namespace]);
|
||||
} else p(cal as GlobalCal, ar);
|
||||
return;
|
||||
}
|
||||
p(cal as GlobalCal, ar);
|
||||
|
||||
@@ -30,13 +30,14 @@ export const Codes = {
|
||||
const width = getDimension(previewState.inline.width);
|
||||
const height = getDimension(previewState.inline.height);
|
||||
const namespaceProp = `${namespace ? `namespace="${namespace}"` : ""}`;
|
||||
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
import { useEffect } from "react";
|
||||
export default function MyApp() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
const cal = await getCalApi();
|
||||
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
@@ -58,13 +59,14 @@ export const Codes = {
|
||||
uiInstructionCode: string;
|
||||
namespace: string;
|
||||
}) => {
|
||||
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
|
||||
return code`
|
||||
import { getCalApi } from "@calcom/embed-react";
|
||||
import { useEffect } from "react";
|
||||
export default function MyApp() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""});
|
||||
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
|
||||
${getApiName({ namespace, mainApiName: "cal" })}("floatingButton", ${floatingButtonArg});
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
@@ -84,13 +86,14 @@ export const Codes = {
|
||||
embedCalOrigin: string;
|
||||
namespace: string;
|
||||
}) => {
|
||||
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
|
||||
return code`
|
||||
import { getCalApi } from "@calcom/embed-react";
|
||||
import { useEffect } from "react";
|
||||
export default function MyApp() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""});
|
||||
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
@@ -196,3 +199,9 @@ const code = (partsWithoutBlock: TemplateStringsArray, ...blocksOrVariables: str
|
||||
}
|
||||
return constructedCode.join("");
|
||||
};
|
||||
|
||||
function getArgumentForGetCalApi(namespace: string) {
|
||||
const libUrl = IS_SELF_HOSTED ? embedLibUrl : undefined;
|
||||
const argumentForGetCalApi = namespace ? { namespace, embedLibUrl: libUrl } : { embedLibUrl: libUrl };
|
||||
return argumentForGetCalApi;
|
||||
}
|
||||
|
||||
@@ -275,7 +275,9 @@ const getInstructionString = ({
|
||||
function useGetEmbedSnippetString(namespace: string | null) {
|
||||
const bookerUrl = useBookerUrl();
|
||||
// TODO: Import this string from @calcom/embed-snippet
|
||||
return `(function (C, A, L) { let p = function (a, ar) { a.q.push(ar); }; let d = C.document; C.Cal = C.Cal || function () { let cal = C.Cal; let ar = arguments; if (!cal.loaded) { cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement("script")).src = A; cal.loaded = true; } if (ar[0] === L) { const api = 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); return; } p(cal, ar); }; })(window, "${embedLibUrl}", "init");
|
||||
// Right now the problem is that embed-snippet export is not minified and has comments which makes it unsuitable for giving it to users.
|
||||
// If we can minify that during build time and then import the built code here, that could work
|
||||
return `(function (C, A, L) { let p = function (a, ar) { a.q.push(ar); }; let d = C.document; C.Cal = C.Cal || function () { let cal = C.Cal; let ar = arguments; if (!cal.loaded) { cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement("script")).src = A; cal.loaded = true; } if (ar[0] === L) { const api = function () { p(api, arguments); }; const namespace = ar[1]; api.q = api.q || []; if(typeof namespace === "string"){cal.ns[namespace] = cal.ns[namespace] || api;p(cal.ns[namespace], ar);p(cal, ["initNamespace", namespace]);} else p(cal, ar); return;} p(cal, ar); }; })(window, "${embedLibUrl}", "init");
|
||||
Cal("init", ${namespace ? `"${namespace}",` : ""} {origin:"${bookerUrl}"});
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user