714 lines
22 KiB
TypeScript
714 lines
22 KiB
TypeScript
/// <reference types="../env" />
|
|
import type { BookerLayouts } from "@calcom/prisma/zod-utils";
|
|
|
|
import { FloatingButton } from "./FloatingButton/FloatingButton";
|
|
import { Inline } from "./Inline/inline";
|
|
import { ModalBox } from "./ModalBox/ModalBox";
|
|
import type { InterfaceWithParent, interfaceWithParent, UiConfig, EmbedThemeConfig } 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?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 =
|
|
import.meta.env.EMBED_PUBLIC_WEBAPP_URL || `https://${import.meta.env.EMBED_PUBLIC_VERCEL_URL}`;
|
|
|
|
customElements.define("cal-modal-box", ModalBox);
|
|
customElements.define("cal-floating-button", FloatingButton);
|
|
customElements.define("cal-inline", Inline);
|
|
|
|
declare module "*.css";
|
|
type Namespace = string;
|
|
type Config = {
|
|
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) {
|
|
throw new Error("Cal is not defined. This shouldn't happen");
|
|
}
|
|
|
|
// Store Commit Hash to know exactly what version of the code is running
|
|
// TODO: Ideally it should be the version as per package.json and then it can be renamed to version.
|
|
// But because it is built on local machine right now, it is much more reliable to have the commit hash.
|
|
globalCal.fingerprint = import.meta.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as string;
|
|
globalCal.__css = allCss;
|
|
document.head.appendChild(document.createElement("style")).innerHTML = css;
|
|
|
|
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.
|
|
* Extend the functionality of it as required by the embed.
|
|
* @param data
|
|
* @param schema
|
|
*/
|
|
function validate(data: Record<string, unknown>, schema: ValidationSchema) {
|
|
function checkType(value: unknown, expectedType: ValidationSchemaPropType) {
|
|
if (typeof expectedType === "string") {
|
|
return typeof value == expectedType;
|
|
} else {
|
|
return value instanceof expectedType;
|
|
}
|
|
}
|
|
|
|
function isUndefined(data: unknown) {
|
|
return typeof data === "undefined";
|
|
}
|
|
|
|
if (schema.required && isUndefined(data)) {
|
|
throw new Error("Argument is required");
|
|
}
|
|
|
|
for (const [prop, propSchema] of Object.entries(schema.props || {})) {
|
|
if (propSchema.required && isUndefined(data[prop])) {
|
|
throw new Error(`"${prop}" is required`);
|
|
}
|
|
let typeCheck = true;
|
|
if (propSchema.type && !isUndefined(data[prop])) {
|
|
if (propSchema.type instanceof Array) {
|
|
propSchema.type.forEach((type) => {
|
|
typeCheck = typeCheck || checkType(data[prop], type);
|
|
});
|
|
} else {
|
|
typeCheck = checkType(data[prop], propSchema.type);
|
|
}
|
|
}
|
|
if (!typeCheck) {
|
|
throw new Error(`"${prop}" is of wrong type.Expected type "${propSchema.type}"`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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[];
|
|
|
|
/**
|
|
* All types of config that are critical to be processed as soon as possible are provided as query params to the iframe
|
|
*/
|
|
export type PrefillAndIframeAttrsConfig = Record<string, string | string[] | Record<string, string>> & {
|
|
// TODO: iframeAttrs shouldn't be part of it as that configures the iframe element and not the iframed app.
|
|
iframeAttrs?: Record<string, string> & {
|
|
id?: string;
|
|
};
|
|
|
|
// TODO: It should have a dedicated prefill prop
|
|
// prefill: {},
|
|
|
|
// TODO: Move layout and theme as nested props of ui as it makes it clear that these two can be configured using `ui` instruction as well any time.
|
|
// ui: {layout; theme}
|
|
layout?: `${BookerLayouts}`;
|
|
theme?: EmbedThemeConfig;
|
|
};
|
|
|
|
export class Cal {
|
|
iframe?: HTMLIFrameElement;
|
|
|
|
__config: Config;
|
|
|
|
modalBox!: Element;
|
|
|
|
inlineEl!: Element;
|
|
|
|
namespace: string;
|
|
|
|
actionManager: SdkActionManager;
|
|
|
|
iframeReady!: boolean;
|
|
|
|
iframeDoQueue: DoInIframeArg[] = [];
|
|
|
|
api: CalApi;
|
|
|
|
static actionsManagers: Record<Namespace, SdkActionManager>;
|
|
|
|
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(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.api[method]) {
|
|
// Instead of throwing error, log and move forward in the queue
|
|
log(`Instruction ${method} not FOUND`);
|
|
}
|
|
try {
|
|
// 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);
|
|
}
|
|
return instruction;
|
|
}
|
|
|
|
processQueue(queue: IArguments[]) {
|
|
queue.forEach((instruction) => {
|
|
this.processInstruction(instruction);
|
|
});
|
|
|
|
queue.splice(0);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
/** @ts-ignore */ // We changed the definition of push here.
|
|
queue.push = (instruction) => {
|
|
this.processInstruction(instruction);
|
|
};
|
|
}
|
|
|
|
createIframe({
|
|
calLink,
|
|
queryObject = {},
|
|
calOrigin,
|
|
}: {
|
|
calLink: string;
|
|
queryObject?: PrefillAndIframeAttrsConfig & { guest?: string | string[] };
|
|
calOrigin?: string;
|
|
}) {
|
|
const iframe = (this.iframe = document.createElement("iframe"));
|
|
iframe.className = "cal-embed";
|
|
iframe.name = `cal-embed=${this.namespace}`;
|
|
const config = this.getConfig();
|
|
const { iframeAttrs, ...restQueryObject } = queryObject;
|
|
|
|
if (iframeAttrs && iframeAttrs.id) {
|
|
iframe.setAttribute("id", iframeAttrs.id);
|
|
}
|
|
|
|
// Prepare searchParams from config
|
|
const searchParams = new URLSearchParams();
|
|
for (const [key, value] of Object.entries(restQueryObject)) {
|
|
if (value === undefined) {
|
|
continue;
|
|
}
|
|
if (value instanceof Array) {
|
|
value.forEach((val) => searchParams.append(key, val));
|
|
} else {
|
|
searchParams.set(key, value as string);
|
|
}
|
|
}
|
|
|
|
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`;
|
|
}
|
|
urlInstance.searchParams.set("embed", this.namespace);
|
|
if (config.debug) {
|
|
urlInstance.searchParams.set("debug", "" + config.debug);
|
|
}
|
|
if (config.uiDebug) {
|
|
iframe.style.border = "1px solid green";
|
|
}
|
|
|
|
// Merge searchParams from config onto the URL which might have query params already
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
//@ts-ignore
|
|
for (const [key, value] of searchParams) {
|
|
urlInstance.searchParams.append(key, value);
|
|
}
|
|
iframe.src = urlInstance.toString();
|
|
return iframe;
|
|
}
|
|
|
|
getConfig() {
|
|
return this.__config;
|
|
}
|
|
|
|
doInIframe(doInIframeArg: DoInIframeArg) {
|
|
if (!this.iframeReady) {
|
|
this.iframeDoQueue.push(doInIframeArg);
|
|
return;
|
|
}
|
|
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: IArguments[]) {
|
|
this.__config = {
|
|
// Use WEBAPP_URL till full page reload problem with website URL is solved
|
|
calOrigin: WEBAPP_URL,
|
|
};
|
|
this.api = new CalApi(this);
|
|
this.namespace = namespace;
|
|
this.actionManager = new SdkActionManager(namespace);
|
|
|
|
Cal.actionsManagers = Cal.actionsManagers || {};
|
|
Cal.actionsManagers[namespace] = this.actionManager;
|
|
|
|
this.processQueue(q);
|
|
|
|
// 1. Initial iframe width and height would be according to 100% value of the parent element
|
|
// 2. Once webpage inside iframe renders, it would tell how much iframe height should be increased so that my entire content is visible without iframe scroll
|
|
// 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;
|
|
|
|
if (!iframe) {
|
|
// Iframe might be pre-rendering
|
|
return;
|
|
}
|
|
const unit = "px";
|
|
if (data.iframeHeight) {
|
|
iframe.style.height = data.iframeHeight + 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.
|
|
const spacingTopPlusBottom = 2 * 50; // 50 is the padding we want to keep to show close button comfortably. Make it same as top for bottom.
|
|
iframe.style.maxHeight = window.innerHeight - spacingTopPlusBottom + "px";
|
|
}
|
|
});
|
|
|
|
this.actionManager.on("__iframeReady", () => {
|
|
this.iframeReady = true;
|
|
this.doInIframe({ method: "parentKnowsIframeReady" } as const);
|
|
this.iframeDoQueue.forEach((doInIframeArg) => {
|
|
this.doInIframe(doInIframeArg);
|
|
});
|
|
});
|
|
|
|
this.actionManager.on("__routeChanged", () => {
|
|
const { top, height } = this.inlineEl.getBoundingClientRect();
|
|
// Try to readjust and scroll into view if more than 25% is hidden.
|
|
// Otherwise we assume that user might have positioned the content appropriately already
|
|
if (top < 0 && Math.abs(top / height) >= 0.25) {
|
|
this.inlineEl.scrollIntoView({ behavior: "smooth" });
|
|
}
|
|
});
|
|
|
|
this.actionManager.on("linkReady", () => {
|
|
this.modalBox?.setAttribute("state", "loaded");
|
|
this.inlineEl?.setAttribute("loading", "done");
|
|
});
|
|
|
|
this.actionManager.on("linkFailed", (e) => {
|
|
const iframe = this.iframe;
|
|
if (!iframe) {
|
|
return;
|
|
}
|
|
this.inlineEl?.setAttribute("data-error-code", e.detail.data.code);
|
|
this.modalBox?.setAttribute("data-error-code", e.detail.data.code);
|
|
this.inlineEl?.setAttribute("loading", "failed");
|
|
this.modalBox?.setAttribute("state", "failed");
|
|
});
|
|
}
|
|
}
|
|
|
|
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,
|
|
config,
|
|
}: {
|
|
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;
|
|
config?: PrefillAndIframeAttrsConfig;
|
|
}) {
|
|
// 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 (config) {
|
|
el.dataset.calConfig = JSON.stringify(config);
|
|
}
|
|
|
|
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<T extends keyof EventDataMap>({
|
|
action,
|
|
callback,
|
|
}: {
|
|
action: T;
|
|
callback: (arg0: CustomEvent<EventData<T>>) => void;
|
|
}) {
|
|
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: IArguments[];
|
|
/** If user registers multiple namespaces, those are available here */
|
|
instance?: Cal;
|
|
__css?: string;
|
|
fingerprint?: string;
|
|
__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;
|
|
}
|
|
}
|
|
|
|
export interface CalWindow extends Window {
|
|
Cal: GlobalCal;
|
|
}
|
|
|
|
globalCal.instance = new Cal("", globalCal.q);
|
|
for (const [ns, api] of Object.entries(globalCal.ns)) {
|
|
api.instance = new Cal(ns, api.q);
|
|
}
|
|
|
|
/**
|
|
* Intercepts all postmessages and fires action in corresponding actionManager
|
|
*/
|
|
window.addEventListener("message", (e) => {
|
|
const detail = e.data;
|
|
const fullType = detail.fullType;
|
|
const parsedAction = SdkActionManager.parseAction(fullType);
|
|
if (!parsedAction) {
|
|
return;
|
|
}
|
|
|
|
const actionManager = Cal.actionsManagers[parsedAction.ns];
|
|
globalCal.__logQueue = globalCal.__logQueue || [];
|
|
globalCal.__logQueue.push({ ...parsedAction, data: detail.data });
|
|
|
|
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 targetEl = e.target;
|
|
if (!(targetEl instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
const path = targetEl.dataset.calLink;
|
|
if (!path) {
|
|
return;
|
|
}
|
|
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: GlobalCalWithoutNs = globalCal;
|
|
|
|
if (namespace) {
|
|
api = globalCal.ns[namespace];
|
|
}
|
|
|
|
if (!api) {
|
|
throw new Error(`Namespace ${namespace} isn't defined`);
|
|
}
|
|
|
|
api("modal", {
|
|
calLink: path,
|
|
config,
|
|
uid: modalUniqueId,
|
|
calOrigin,
|
|
});
|
|
});
|