153 lines
5.6 KiB
TypeScript
153 lines
5.6 KiB
TypeScript
![]() |
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<CSSProperties, "background" | "backgroundColor">;
|
||
|
eventTypeListItem?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||
|
enabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||
|
disabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||
|
}
|
||
|
|
||
|
type ElementName = keyof EmbedStyles;
|
||
|
|
||
|
type ReactEmbedStylesSetter = React.Dispatch<React.SetStateAction<EmbedStyles>>;
|
||
|
|
||
|
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<ElementName, ReactEmbedStylesSetter>,
|
||
|
};
|
||
|
|
||
|
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<string, any> = 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", {});
|
||
|
}
|