diff --git a/app/client/packages/design-system/theming/src/utils/cssRule.ts b/app/client/packages/design-system/theming/src/utils/cssRule.ts index 6724b6487c0d..7ff3a01a6f89 100644 --- a/app/client/packages/design-system/theming/src/utils/cssRule.ts +++ b/app/client/packages/design-system/theming/src/utils/cssRule.ts @@ -1,15 +1,30 @@ import kebabCase from "lodash/kebabCase"; import isObject from "lodash/isObject"; import type { Theme } from "../theme"; +import { objectKeys } from "@appsmith/utils"; +import type { TypographyVariantMetric } from "../token"; export const cssRule = (tokens: Theme) => { let styles = ""; - Object.values(tokens).forEach((token) => { + objectKeys(tokens).forEach((tokenKey) => { + const token = tokens[tokenKey]; + if (token == null) return; if (isObject(token)) { - Object.keys(token).forEach((key) => { + if (tokenKey === "typography") { + styles += objectKeys(token as NonNullable).reduce( + (prev: string, key) => { + return `${prev} --font-size-${key}: ${(token[key] as TypographyVariantMetric).fontSize};`; + }, + "", + ); + + return; + } + + objectKeys(token).forEach((key) => { //@ts-expect-error: type mismatch styles += `--${kebabCase(token[key].type)}-${kebabCase(key)}: ${ //@ts-expect-error: type mismatch diff --git a/app/client/src/layoutSystems/anvil/editor/styles.module.css b/app/client/src/layoutSystems/anvil/editor/styles.module.css index 3d2ccdd43a39..b711d3b71775 100644 --- a/app/client/src/layoutSystems/anvil/editor/styles.module.css +++ b/app/client/src/layoutSystems/anvil/editor/styles.module.css @@ -8,7 +8,7 @@ This is a temporary solution. According to the product requirements, we need to make AI chat widget interactive. This code can be deleted when full-fledged inline editing feature is implemented. */ - & [data-widget-name*="AIChat"] > * { + & :is([data-widget-name*="AIChat"], [data-widget-name*="Custom"]) > * { pointer-events: all; } } diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/appsmithConsole.js b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/appsmithConsole.js new file mode 100644 index 000000000000..1c27b1b5eb03 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/appsmithConsole.js @@ -0,0 +1,35 @@ +(function (nativeConsole) { + const postMessage = (method, args) => { + window.parent.postMessage( + { + type: "CUSTOM_WIDGET_CONSOLE_EVENT", + data: { + type: method, + args: args.map((d) => ({ + message: d, + })), + }, + }, + "*", + ); + }; + + const createProxy = (method) => + new Proxy(nativeConsole[method], { + apply(target, _this, args) { + try { + postMessage(method, args); + } finally { + return Reflect.apply(target, _this, args); + } + }, + }); + + ["log", "warn", "info"].forEach((method) => { + nativeConsole[method] = createProxy(method); + }); + + window.addEventListener("error", (event) => { + postMessage("error", [event.message]); + }); +})(window.console); diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/constants.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/constants.ts new file mode 100644 index 000000000000..16e5e6ee3a8b --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/constants.ts @@ -0,0 +1,18 @@ +export const CUSTOM_WIDGET_LOAD_EVENTS = { + STARTED: "started", + DOM_CONTENTED_LOADED: "DOMContentLoaded", + COMPLETED: "completed", +}; + +export const getAppsmithScriptSchema = (model: Record) => ({ + appsmith: { + mode: "", + model: model, + onUiChange: Function, + onModelChange: Function, + onThemeChange: Function, + updateModel: Function, + triggerEvent: Function, + onReady: Function, + }, +}); diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetScript.test.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetScript.test.ts new file mode 100644 index 000000000000..963e6478d036 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetScript.test.ts @@ -0,0 +1,353 @@ +import { + createChannelToParent, + generateAppsmithCssVariables, + EVENTS, + main, +} from "./customWidgetscript"; + +jest.mock("queue-microtask", () => ({ + queueMicrotask: jest.fn().mockImplementationOnce((fn) => fn()), +})); + +declare global { + interface Window { + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + appsmith: any; + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + triggerEvent: any; + } +} + +describe("createChannelToParent", () => { + beforeEach(() => { + const events = new Map(); + + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window.addEventListener = (type: string, handler: any) => { + events.set(type, handler); + }; + + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window.triggerEvent = (type: string, event: any) => { + events.get(type)(event); + }; + }); + + it("should check the onMessage function", () => { + const channel = createChannelToParent(); + + const handler = jest.fn(); + + channel.onMessage("test", handler); + + expect(channel.onMessageMap.get("test")[0]).toBe(handler); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: "test", + }, + }); + + expect(handler).toHaveBeenCalledWith({ + type: "test", + }); + }); + + it("should check the postMessage function", async () => { + const channel = createChannelToParent(); + + window.parent.postMessage = jest.fn().mockImplementationOnce((data) => { + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK, + key: data.key, + success: true, + }, + }); + }); + + channel.postMessage("test1", { index: 1 }); + + channel.postMessage("test2", { index: 2 }); + + return new Promise((resolve) => { + setTimeout(() => { + expect(window.parent.postMessage).toHaveBeenCalledWith( + { + type: "test1", + data: { index: 1 }, + key: expect.any(Number), + }, + "*", + ); + + expect(window.parent.postMessage).toHaveBeenCalledWith( + { + type: "test2", + data: { index: 2 }, + key: expect.any(Number), + }, + "*", + ); + + resolve(true); + }); + }); + }); +}); + +describe("generateAppsmithCssVariables", () => { + it("should generate CSS variables in the style element", () => { + const source = { + key1: "value1", + key2: 42, + key3: [], + }; + + const provider = "model"; + + generateAppsmithCssVariables(provider)(source); + + expect( + document + .getElementById(`appsmith-${provider}-css-tokens`) + ?.innerHTML.replace(/\s+/g, "") + .replace(/\n/g, ""), + ).toBe(`:root{--appsmith-model-key1:value1;--appsmith-model-key2:42;}`); + }); +}); + +describe("CustomWidgetScript", () => { + beforeAll(() => { + const events = new Map(); + + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window.addEventListener = (type: string, handler: any) => { + events.set(type, handler); + }; + + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window.triggerEvent = (type: string, event: any) => { + events.get(type)(event); + }; + + window.parent.postMessage = jest.fn().mockImplementationOnce((data) => { + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK, + key: data.key, + success: true, + }, + }); + }); + + main(); + }); + + it("should check API functions - onReady and init", () => { + const handler = jest.fn(); + + window.appsmith.onReady(handler); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_READY_ACK, + model: { + test: 1, + }, + ui: { + width: 1, + height: 2, + }, + mode: "test", + theme: { + color: "#fff", + }, + }, + }); + + expect(window.appsmith.mode).toBe("test"); + + expect(window.appsmith.model).toEqual({ + test: 1, + }); + + expect(window.appsmith.ui).toEqual({ + width: 1, + height: 2, + }); + + expect(handler).toHaveBeenCalled(); + }); + + it("should check API functions - onModelChange", () => { + const handler = jest.fn(); + + const unlisten = window.appsmith.onModelChange(handler); + + expect(handler).toHaveBeenCalledWith({ + test: 1, + }); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_MODEL_CHANGE, + model: { + test: 2, + }, + }, + }); + + expect(window.appsmith.model).toEqual({ + test: 2, + }); + + expect(handler).toHaveBeenCalledWith( + { + test: 2, + }, + { + test: 1, + }, + ); + + handler.mockClear(); + unlisten(); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_MODEL_CHANGE, + model: { + test: 3, + }, + }, + }); + + expect(window.appsmith.model).toEqual({ + test: 3, + }); + + expect(handler).not.toHaveBeenCalled(); + }); + + it("should check API functions - onThemeChange", () => { + const handler = jest.fn(); + + const unlisten = window.appsmith.onThemeChange(handler); + + expect(handler).toHaveBeenCalledWith({ + color: "#fff", + }); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_THEME_UPDATE, + theme: { + color: "#000", + }, + }, + }); + + expect(window.appsmith.theme).toEqual({ + color: "#000", + }); + + expect(handler).toHaveBeenCalledWith( + { + color: "#000", + }, + { + color: "#fff", + }, + ); + + handler.mockClear(); + unlisten(); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_THEME_UPDATE, + theme: { + color: "#f0f", + }, + }, + }); + + expect(window.appsmith.theme).toEqual({ + color: "#f0f", + }); + + expect(handler).not.toHaveBeenCalled(); + }); + + it("should check API functions - updateModel", async () => { + window.appsmith.updateModel({ + test: 4, + }); + + return new Promise((resolve) => { + setTimeout(() => { + expect(window.parent.postMessage).toHaveBeenCalledWith( + { + type: EVENTS.CUSTOM_WIDGET_UPDATE_MODEL, + data: { + test: 4, + }, + key: expect.any(Number), + }, + "*", + ); + + expect(window.appsmith.model).toEqual({ + test: 4, + }); + + resolve(true); + }); + }); + }); + + it("should check API functions - triggerEvent", async () => { + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window.parent.postMessage as any).mockClear(); + + window.appsmith.triggerEvent("test", { + test: 5, + }); + + return new Promise((resolve) => { + setTimeout(() => { + expect(window.parent.postMessage).toHaveBeenCalledWith( + { + type: EVENTS.CUSTOM_WIDGET_TRIGGER_EVENT, + data: { + eventName: "test", + contextObj: { + test: 5, + }, + }, + key: expect.any(Number), + }, + "*", + ); + + resolve(true); + }); + }); + }); +}); diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetscript.js b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetscript.js new file mode 100644 index 000000000000..dcabe42ca8e2 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetscript.js @@ -0,0 +1,347 @@ +// Custom widget events definition +export const EVENTS = { + CUSTOM_WIDGET_READY: "CUSTOM_WIDGET_READY", + CUSTOM_WIDGET_READY_ACK: "CUSTOM_WIDGET_READY_ACK", + CUSTOM_WIDGET_UPDATE_MODEL: "CUSTOM_WIDGET_UPDATE_MODEL", + CUSTOM_WIDGET_TRIGGER_EVENT: "CUSTOM_WIDGET_TRIGGER_EVENT", + CUSTOM_WIDGET_MODEL_CHANGE: "CUSTOM_WIDGET_MODEL_CHANGE", + CUSTOM_WIDGET_UI_CHANGE: "CUSTOM_WIDGET_UI_CHANGE", + CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK: "CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK", + CUSTOM_WIDGET_CONSOLE_EVENT: "CUSTOM_WIDGET_CONSOLE_EVENT", + CUSTOM_WIDGET_THEME_UPDATE: "CUSTOM_WIDGET_THEME_UPDATE", + CUSTOM_WIDGET_UPDATE_HEIGHT: "CUSTOM_WIDGET_UPDATE_HEIGHT", +}; + +// Function to create a communication channel to the parent +export const createChannelToParent = () => { + const onMessageMap = new Map(); + + // Function to register an event handler for a message type + function onMessage(type, fn) { + let eventHandlerList = onMessageMap.get(type); + + if (eventHandlerList && eventHandlerList instanceof Array) { + eventHandlerList.push(fn); + } else { + eventHandlerList = [fn]; + onMessageMap.set(type, eventHandlerList); + } + + return () => { + // Function to unsubscribe an event handler + const index = eventHandlerList.indexOf(fn); + + eventHandlerList.splice(index, 1); + }; + } + + // Listen for 'message' events and dispatch to registered event handlers + window.addEventListener("message", (event) => { + if (event.source === window.parent) { + const handlerList = onMessageMap.get(event.data.type); + + if (handlerList) { + handlerList.forEach((fn) => fn(event.data)); + } + } + }); + // Queue to hold postMessage requests + const postMessageQueue = []; + // Flag to indicate if the flush is scheduled + let isFlushScheduled = false; + + /* + * Function to schedule microtask to flush postMessageQueue + * to ensure the order of message processed on the parent + */ + const scheduleMicrotaskToflushPostMessageQueue = () => { + if (!isFlushScheduled) { + isFlushScheduled = true; + queueMicrotask(() => { + (async () => { + while (postMessageQueue.length > 0) { + const message = postMessageQueue.shift(); + + await new Promise((resolve) => { + const key = Math.random(); + const unlisten = onMessage( + EVENTS.CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK, + (data) => { + if (data.key === key && data.success) { + unlisten(); + resolve(); + } + }, + ); + + // Send the message to the parent + window.parent.postMessage( + Object.assign(Object.assign({}, message), { key }), + "*", + ); + }); + } + + isFlushScheduled = false; + })(); + }); + } + }; + + return { + onMessageMap, + postMessage: (type, data) => { + try { + // Try block to catch non clonealbe data error while postmessaging + // throw error if the data is not cloneable + postMessageQueue.push({ + type, + data, + }); + + scheduleMicrotaskToflushPostMessageQueue(); + } catch (e) { + throw e; + } + }, + onMessage, + }; +}; + +/* + * Function to initialize the script + * wrapping this inside a function to make it testable + */ +export function main() { + // Create a communication channel to the parent + const channel = createChannelToParent(); + /* + * Variables to hold the subscriber functions + */ + const modelSubscribers = []; + const uiSubscribers = []; + const themeSubscribers = []; + /* + * Variables to hold ready function and state + */ + let onReady; + let isReady = false; + let isReadyCalled = false; + + const heightObserver = new ResizeObserver(() => { + const height = document.body.clientHeight; + + channel.postMessage(EVENTS.CUSTOM_WIDGET_UPDATE_HEIGHT, { + height, + }); + }); + + // Callback for when the READY_ACK message is received + channel.onMessage(EVENTS.CUSTOM_WIDGET_READY_ACK, (event) => { + window.appsmith.model = event.model; + window.appsmith.ui = event.ui; + window.appsmith.theme = event.theme; + window.appsmith.mode = event.mode; + heightObserver.observe(window.document.body); + + // Subscribe to model and UI changes + window.appsmith.onModelChange(generateAppsmithCssVariables("model")); + window.appsmith.onUiChange(generateAppsmithCssVariables("ui")); + window.appsmith.onThemeChange(generateAppsmithCssVariables("theme")); + + // Set the widget as ready + isReady = true; + + if (!isReadyCalled && onReady) { + onReady(); + isReadyCalled = true; + } + }); + // Callback for when MODEL_CHANGE message is received + channel.onMessage(EVENTS.CUSTOM_WIDGET_MODEL_CHANGE, (event) => { + if (event.model) { + const prevModel = window.appsmith.model; + + window.appsmith.model = event.model; + + // Notify model subscribers + modelSubscribers.forEach((fn) => { + fn(event.model, prevModel); + }); + } + }); + // Callback for when UI_CHANGE message is received + channel.onMessage(EVENTS.CUSTOM_WIDGET_UI_CHANGE, (event) => { + if (event.ui) { + const prevUi = window.appsmith.ui; + + window.appsmith.ui = event.ui; + // Notify UI subscribers + uiSubscribers.forEach((fn) => { + fn(event.ui, prevUi); + }); + } + }); + + channel.onMessage(EVENTS.CUSTOM_WIDGET_THEME_UPDATE, (event) => { + if (event.theme) { + const prevTheme = window.appsmith.theme; + + window.appsmith.theme = event.theme; + // Notify theme subscribers + themeSubscribers.forEach((fn) => { + fn(event.theme, prevTheme); + }); + } + + if (event.cssTokens) { + const el = document.querySelector("[data-appsmith-theme]"); + + if (el) { + el.innerHTML = event.cssTokens; + } else { + // Use appendChild instead of innerHTML to add the style element + const styleElement = document.createElement("style"); + + styleElement.setAttribute("data-appsmith-theme", ""); + styleElement.innerHTML = event.cssTokens; + document.head.appendChild(styleElement); + } + } + }); + + if (!window.appsmith) { + // Define appsmith global object + Object.defineProperty(window, "appsmith", { + configurable: false, + writable: false, + value: { + mode: "", + theme: {}, + onThemeChange: (fn) => { + if (typeof fn !== "function") { + throw new Error("onThemeChange expects a function as parameter"); + } + + themeSubscribers.push(fn); + fn(window.appsmith.theme); + + return () => { + // Unsubscribe from theme changes + const index = themeSubscribers.indexOf(fn); + + if (index > -1) { + themeSubscribers.splice(index, 1); + } + }; + }, + onUiChange: (fn) => { + if (typeof fn !== "function") { + throw new Error("onUiChange expects a function as parameter"); + } + + uiSubscribers.push(fn); + fn(window.appsmith.ui); + + return () => { + // Unsubscribe from UI changes + const index = uiSubscribers.indexOf(fn); + + if (index > -1) { + uiSubscribers.splice(index, 1); + } + }; + }, + onModelChange: (fn) => { + if (typeof fn !== "function") { + throw new Error("onModelChange expects a function as parameter"); + } + + modelSubscribers.push(fn); + fn(window.appsmith.model); + + return () => { + // Unsubscribe from model changes + const index = modelSubscribers.indexOf(fn); + + if (index > -1) { + modelSubscribers.splice(index, 1); + } + }; + }, + updateModel: (obj) => { + if (!obj || typeof obj !== "object") { + throw new Error("updateModel expects an object as parameter"); + } + + appsmith.model = Object.assign( + Object.assign({}, appsmith.model), + obj, + ); + + // Send an update model message to the parent + channel.postMessage(EVENTS.CUSTOM_WIDGET_UPDATE_MODEL, obj); + }, + triggerEvent: (eventName, contextObj) => { + if (typeof eventName !== "string") { + throw new Error("eventName should be a string"); + } else if (contextObj && typeof contextObj !== "object") { + throw new Error("contextObj should be an object"); + } + + // Send a trigger event message to the parent + channel.postMessage(EVENTS.CUSTOM_WIDGET_TRIGGER_EVENT, { + eventName, + contextObj, + }); + }, + model: {}, + ui: {}, + onReady: (fn) => { + if (typeof fn !== "function") { + throw new Error("onReady expects a function as parameter"); + } + + onReady = fn; + + if (isReady && !isReadyCalled && onReady) { + onReady(); + isReadyCalled = true; + } + }, + }, + }); + } + + // Listen for the 'load' event and send READY message to the parent + window.addEventListener("load", () => { + channel.postMessage(EVENTS.CUSTOM_WIDGET_READY); + }); +} + +/* + * Function to create appsmith css variables for model and ui primitive values + * variables get regenerated every time the model or ui changes. + */ +export const generateAppsmithCssVariables = (provider) => (source) => { + let cssTokens = document.getElementById(`appsmith-${provider}-css-tokens`); + + if (!cssTokens) { + cssTokens = document.createElement("style"); + cssTokens.id = `appsmith-${provider}-css-tokens`; + window.document.head.appendChild(cssTokens); + } + + const cssTokensContent = Object.keys(source).reduce((acc, key) => { + if (typeof source[key] === "string" || typeof source[key] === "number") { + return ` + ${acc} + --appsmith-${provider}-${key}: ${source[key]}; + `; + } else { + return acc; + } + }, ""); + + cssTokens.innerHTML = `:root {${cssTokensContent}}`; +}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx new file mode 100644 index 000000000000..ac763a2df101 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx @@ -0,0 +1,210 @@ +import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; +import styled from "styled-components"; +import kebabCase from "lodash/kebabCase"; + +// @ts-expect-error Cannot find module due to raw-loader +import script from "!!raw-loader!./customWidgetscript.js"; + +// @ts-expect-error Cannot find module due to raw-loader +import appsmithConsole from "!!raw-loader!./appsmithConsole.js"; + +// @ts-expect-error Cannot find module due to raw-loader +import css from "!!raw-loader!./reset.css"; +import clsx from "clsx"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { EVENTS } from "./customWidgetscript"; +import { getAppsmithConfigs } from "ee/configs"; +import styles from "./styles.module.css"; +import { cssRule, ThemeContext } from "@appsmith/wds-theming"; +import { useCustomWidgetHeight } from "./useCustomWidgetHeight"; +import type { COMPONENT_SIZE } from "../constants"; + +const Container = styled.div` + height: 100%; + width: 100%; +`; + +const { disableIframeWidgetSandbox } = getAppsmithConfigs(); + +export function CustomComponent(props: CustomComponentProps) { + const { size } = props; + const iframe = useRef(null); + const theme = useContext(ThemeContext); + const { search } = window.location; + const queryParams = new URLSearchParams(search); + const isEmbed = queryParams.get("embed") === "true"; + const componentHeight = useCustomWidgetHeight(size, isEmbed); + const [loading, setLoading] = React.useState(true); + const [isIframeReady, setIsIframeReady] = useState(false); + + const cssTokens = useMemo(() => { + const tokens = cssRule(theme); + const prefixedTokens = tokens.replace(/--/g, "--appsmith-theme-"); + + return `:root {${prefixedTokens}}`; + }, [theme]); + + useEffect(() => { + const handler = (event: MessageEvent) => { + const iframeWindow = + iframe.current?.contentWindow || + iframe.current?.contentDocument?.defaultView; + + if (event.source === iframeWindow) { + // Sending acknowledgement for all messages since we're queueing all the postmessage from iframe + iframe.current?.contentWindow?.postMessage( + { + type: EVENTS.CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK, + key: event.data.key, + success: true, + }, + "*", + ); + + const message = event.data; + + switch (message.type) { + case EVENTS.CUSTOM_WIDGET_READY: + setIsIframeReady(true); + iframe.current?.contentWindow?.postMessage( + { + type: EVENTS.CUSTOM_WIDGET_READY_ACK, + model: props.model, + ui: {}, + mode: props.renderMode, + theme, + }, + "*", + ); + + if ( + props.renderMode === "DEPLOYED" || + props.renderMode === "EDITOR" + ) { + AnalyticsUtil.logEvent("CUSTOM_WIDGET_LOAD_INIT", { + widgetId: props.widgetId, + renderMode: props.renderMode, + }); + } + + break; + case EVENTS.CUSTOM_WIDGET_UPDATE_MODEL: + props.update(message.data); + break; + case EVENTS.CUSTOM_WIDGET_TRIGGER_EVENT: + props.execute(message.data.eventName, message.data.contextObj); + break; + case EVENTS.CUSTOM_WIDGET_UPDATE_HEIGHT: + const height = message.data.height; + + if (props.renderMode !== "BUILDER" && height) { + iframe.current?.style.setProperty("height", `${height}px`); + } + + break; + case "CUSTOM_WIDGET_CONSOLE_EVENT": + props.onConsole && + props.onConsole(message.data.type, message.data.args); + break; + } + } + }; + + window.addEventListener("message", handler, false); + + return () => window.removeEventListener("message", handler, false); + }, [props.model]); + + useEffect(() => { + if (iframe.current && iframe.current.contentWindow && isIframeReady) { + iframe.current.contentWindow.postMessage( + { + type: EVENTS.CUSTOM_WIDGET_MODEL_CHANGE, + model: props.model, + }, + "*", + ); + } + }, [props.model]); + + useEffect(() => { + if (iframe.current && iframe.current.contentWindow && isIframeReady) { + iframe.current.contentWindow.postMessage( + { + type: EVENTS.CUSTOM_WIDGET_THEME_UPDATE, + theme, + }, + "*", + ); + } + }, [theme, isIframeReady]); + + const srcDoc = ` + + + + + + + + + ${props.srcDoc.html} + + + + + `; + + useEffect(() => { + setLoading(true); + }, [srcDoc]); + + return ( + +