import { useState, useEffect, CSSProperties } from "react"; import { sdkActionManager } from "./sdk-event"; // Only allow certain styles to be modified so that when we make any changes to HTML, we know what all embed styles might be impacted. // Keep this list to minimum, only adding those styles which are really needed. interface EmbedStyles { body?: Pick; eventTypeListItem?: Pick; enabledDateButton?: Pick; disabledDateButton?: Pick; } type ElementName = keyof EmbedStyles; type ReactEmbedStylesSetter = React.Dispatch>; export interface UiConfig { theme: string; styles: EmbedStyles; } const embedStore = { // Store all embed styles here so that as and when new elements are mounted, styles can be applied to it. styles: {}, // Store all React State setters here. reactStylesStateSetters: {} as Record, }; const setEmbedStyles = (stylesConfig: UiConfig["styles"]) => { embedStore.styles = stylesConfig; for (let [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) { setEmbedStyle((styles) => { return { ...styles, ...stylesConfig, }; }); } }; const registerNewSetter = (elementName: ElementName, setStyles: ReactEmbedStylesSetter) => { 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 removeFromEmbedStylesSetterMap = (elementName: ElementName) => { delete embedStore.reactStylesStateSetters[elementName]; }; // 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: ElementName) => { const [styles, setStyles] = useState({} as 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 styles[elementName] || {}; }; // 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 = { ui: function style(uiConfig: UiConfig) { // TODO: Create automatic logger for all methods. Useful for debugging. console.log("Method: ui called", uiConfig); const stylesConfig = uiConfig.styles; // In case where parent gives instructions before setEmbedStyles is set. if (!setEmbedStyles) { return requestAnimationFrame(() => { style(uiConfig); }); } // body can't be styled using React state hook as it is generated by _document.tsx which doesn't support hooks. if (stylesConfig.body?.background) { document.body.style.background = stylesConfig.body.background as string; } setEmbedStyles(stylesConfig); }, parentKnowsIframeReady: () => { document.body.style.display = "block"; sdkActionManager?.fire("linkReady", {}); }, }; const messageParent = (data: any) => { parent.postMessage( { originator: "CAL", ...data, }, "*" ); }; function keepParentInformedAboutDimensionChanges() { let knownHiddenHeight: Number | null = null; let numDimensionChanges = 0; requestAnimationFrame(function informAboutScroll() { // Because of scroll="no", this much is hidden from the user. const hiddenHeight = document.documentElement.scrollHeight - window.innerHeight; // TODO: Handle width as well. if (knownHiddenHeight !== hiddenHeight) { knownHiddenHeight = hiddenHeight; numDimensionChanges++; // FIXME: This event shouldn't be subscribable by the user. Only by the SDK. sdkActionManager?.fire("dimension-changed", { hiddenHeight, }); } // Parent Counterpart would change the dimension of iframe and thus page's dimension would be impacted which is recursive. // It should stop ideally by reaching a hiddenHeight value of 0. // FIXME: If 0 can't be reached we need to just abandon our quest for perfect iframe and let scroll be there. Such case can be logged in the wild and fixed later on. if (numDimensionChanges > 50) { console.warn("Too many dimension changes detected."); return; } requestAnimationFrame(informAboutScroll); }); } if (typeof window !== "undefined" && !location.search.includes("prerender=true")) { sdkActionManager?.on("*", (e) => { const detail = e.detail; //console.log(detail.fullType, detail.type, detail.data); messageParent(detail); }); window.addEventListener("message", (e) => { const data: Record = e.data; if (!data) { return; } const method: keyof typeof methods = data.method; if (data.originator === "CAL" && typeof method === "string") { methods[method]?.(data.arg); } }); keepParentInformedAboutDimensionChanges(); sdkActionManager?.fire("iframeReady", {}); }