/** * Bootstraps the widget onto a host's webpage via an iframe. * * There is some magic around enabling the widget to appear resizable / dynamically grow/shrink * from within the iframe. This is managed between the bootstrap (parent) and the iframe (child), * where the child updates the parent when the user's mouse is hovering over or not hovering over part of the widget content, * allowing the parent to decide whether or not to allow pointer events on the child iframe. * This means the iframe defaults to a larger size (which is invisible) but only accepts event when a user is hovering over * a portion of it with actual content. * * The iframe and widget communicate bidirectionally to communicate state (e.g., loaded, hover state, expanded/collapsed). * This two-way signaling enables dynamic adjustments to iframe interactivity, visibility, and dimensions based on user interactions and internal widget state. */ (() => { // @ts-ignore window.ov = window.ov || (function () { const iframe = document.createElement("iframe"); // @ts-ignore window.isOutverseLoaded = false; /** Send message to iframe - waits until widget is ready to receive before sending */ const sendMessage = ( /** @type {{ metadata?: any; authedPayload?: string; signature?: string; userId?: string; buttonText?: string; isVisible?: boolean; placeholder?: string; isTabletOrDesktop?: boolean; hiddenMode?: boolean; theme?: "light" | "dark", isExpanded?: boolean, dragEvent?: { type: "dragenter" | "dragleave" | "dragover" | "drop" , files?: FileList }, widgetEvent?: { type: "hover" | "unhover" | "click" }, colors?: { accent?: string, text?: string }, homeHeading?: string, homeSubHeading?: string }} */ message ) => { // @ts-ignore if (window.isOutverseLoaded) { iframe.contentWindow?.postMessage(message, "https://app.outverse.com/zedonk-demo"); } else { window.addEventListener( "outverseLoaded", () => { iframe.contentWindow?.postMessage(message, "https://app.outverse.com/zedonk-demo"); }, { once: true } ); } }; /** * @param {DragEvent | MouseEvent} event * @param {HTMLIFrameElement} iframe */ function isEventOverIframe(event, iframe) { const x = event.clientX; const y = event.clientY; const rect = iframe.getBoundingClientRect(); return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; } // widget state let isWidgetOpen = false; let isExpanded = false; // dimensions for the widget const DIMENSIONS = { expanded: { width: 764, height: window.innerHeight }, default: { width: 495, height: 790 }, }; // function to update iframe dimensions function setIframeDimensions() { let width, height; const matchIsTabletOrDesktop = window.matchMedia("(min-width: 640px)"); const isTabletOrDesktop = matchIsTabletOrDesktop?.matches; if (isTabletOrDesktop) { const { width: expandedWidth, height: expandedHeight } = DIMENSIONS.expanded; const { width: defaultWidth, height: defaultHeight } = DIMENSIONS.default; if (isExpanded) { width = expandedWidth; height = expandedHeight; } else { width = defaultWidth; height = defaultHeight; } // tablet or desktop: set max height const maxWidth = Math.min(window.innerWidth, width); const maxHeight = Math.min(window.innerHeight, height); iframe.style.transition = "height 0.5s cubic-bezier(0.19, 1, 0.28, 1), width 0.5s cubic-bezier(0.19, 1, 0.28, 1)"; iframe.style.height = `${maxHeight}px`; iframe.style.width = `${maxWidth}px`; } else { // mobile iframe.style.transition = "none"; iframe.style.pointerEvents = "auto"; if (isWidgetOpen) { iframe.style.height = "100%"; iframe.style.width = "100%"; } else { iframe.style.width = "80px"; iframe.style.height = "80px"; } } // notify the iframe about the parent's device on iframe dimension update if (!iframe.contentWindow) return; iframe.contentWindow.postMessage( { isTabletOrDesktop, }, "*" ); return { isTabletOrDesktop }; } return { init: function () { const loadWidget = () => { // create an iframe of a set size, but not visible / interactive const iframeStyle = iframe.style; iframeStyle.boxSizing = "borderBox"; iframeStyle.position = "fixed"; iframeStyle.right = "0"; iframeStyle.bottom = "0"; iframeStyle.border = "0"; iframeStyle.margin = "0"; iframeStyle.padding = "0"; iframeStyle.zIndex = "99999999"; // hiden until widget page has signalled load successful iframeStyle.display = "none"; iframeStyle.outline = "none"; // not interactive (allows user to interact with parent + scroll under it, etc.) iframeStyle.pointerEvents = "none"; // set the iframe id iframe.id = "ov-iframe"; // set the iframe src to the workspace-specific widget URL iframe.src = `https://app.outverse.com/zedonk-demo/widget`; document.body.appendChild(iframe); // use window resize event listener to update iframe dimensions window.addEventListener("resize", setIframeDimensions); // listen for interactions within the iframe to make it interactive // this relies on the widget signalling when the user is "inside" the widget area // and allows for the widget to be smaller than the iframe itself without having the change its // size. This makes it much easier to manage the widget size and position within the iframe bounds. window.addEventListener("message", function (e) { // @ts-ignore const key = e.message ? "message" : "data"; // @ts-ignore const data = e[key]; if (typeof data !== "object") return; // update iframe interactivity based on widget signals if (data.type === "OUTVERSE_HOVER") { // notify the iframe that the user is hovering over the widget sendMessage({ widgetEvent: { type: "hover" } }); // enable pointer events on the iframe iframe.style.pointerEvents = "auto"; } else if (data.type === "OUTVERSE_UNHOVER") { // notify the iframe that the user is no longer hovering over the widget sendMessage({ widgetEvent: { type: "unhover" } }); iframe.style.pointerEvents = "none"; } else if (data.type === "OUTVERSE_POINTER_EVENTS") { iframe.style.pointerEvents = "auto"; } else if (data.type === "OUTVERSE_EXPAND") { isExpanded = true; setIframeDimensions(); } else if (data.type === "OUTVERSE_COLLAPSE") { isExpanded = false; setIframeDimensions(); } else if (data.type === "OUTVERSE_OPEN") { isWidgetOpen = true; setIframeDimensions(); // notify the iframe about the parent's widget expanded state sendMessage({ isExpanded: isExpanded }); } else if (data.type === "OUTVERSE_CLOSE") { isWidgetOpen = false; setIframeDimensions(); } else if (data.type === "OUTVERSE_LOADED") { // @ts-ignore if (window.isOutverseLoaded) return; // once content loaded make it visible iframe.style.display = "block"; // @ts-ignore window.isOutverseLoaded = true; window.dispatchEvent(new CustomEvent("outverseLoaded")); // set initial iframe dimensions setIframeDimensions(); } }); window.addEventListener("dragover", function (event) { if (!isWidgetOpen) return; const isOverIframe = isEventOverIframe(event, iframe); if (isOverIframe) { // Send message to iframe event.preventDefault(); sendMessage({ dragEvent: { type: "dragenter" } }); } else { // Optionally, send dragleave if leaving the iframe sendMessage({ dragEvent: { type: "dragleave" } }); } }); window.addEventListener("drop", function (event) { if (!isWidgetOpen) return; const isOverIframe = isEventOverIframe(event, iframe); if (isOverIframe) { event.preventDefault(); const files = event.dataTransfer?.files; // Send message to iframe sendMessage({ dragEvent: { type: "drop", files } }); } }); // forward mousemove events to the iframe for hover detection document.addEventListener("mousemove", function (e) { // @ts-ignore if (!window.isOutverseLoaded || !iframe.contentWindow) return; iframe.contentWindow.postMessage( { coord: { x: e.clientX - iframe.offsetLeft, y: e.clientY - iframe.offsetTop, }, touchDevice: false, }, "*" ); }); document.addEventListener("touchstart", function (e) { // @ts-ignore if (!window.isOutverseLoaded || !iframe.contentWindow) return; iframe.contentWindow.postMessage( { coord: { x: e.touches[0].clientX - iframe.offsetLeft, y: e.touches[0].clientY - iframe.offsetTop, }, touchDevice: true, }, "*" ); }); }; // initialise widget after page load complete if (document.readyState === "complete") { loadWidget(); } else { document.addEventListener("readystatechange", () => { if (document.readyState === "complete") { loadWidget(); } }); } }, // public widget API methods setMetadata: function (/** @type {any} */ metadata) { sendMessage({ metadata }); }, setUserId: function (/** @type {string} */ userId) { sendMessage({ userId }); }, signInUser: function ( /** @type {string} */ authedPayload, /** @type {string} */ signature ) { sendMessage({ authedPayload, signature }); }, setButtonText: function (/** @type {string} */ buttonText) { sendMessage({ buttonText }); }, setTheme: function (/** @type {"dark" | "light"} */ theme) { sendMessage({ theme }); }, setAccentColors: function (/** @type {string} */ accent, /** @type {string} */ text) { sendMessage({ colors: { accent, text } }); }, /** Open widget */ open: function () { // initial opening state, do not delete const dimensionsResult = setIframeDimensions(); // Check if result exists before using its property if (dimensionsResult?.isTabletOrDesktop) { isWidgetOpen = true; iframe.style.pointerEvents = "auto"; sendMessage({ isVisible: true }); } }, /** Close widget */ close: function () { sendMessage({ isVisible: false }); }, hide: function () { sendMessage({ hiddenMode: true }); }, unhide: function () { sendMessage({ hiddenMode: false }); }, isReady: function () { // @ts-ignore return !!window.isOutverseLoaded; }, setZIndex: function (/** @type {string} */ zIndex) { iframe.style.zIndex = zIndex; }, setWidgetPlaceholder: function (/** @type {string} */ placeholder) { sendMessage({ placeholder }); }, setHomeHeading: function (/** @type {string} */ heading) { sendMessage({ homeHeading: heading }); }, setHomeSubHeading: function (/** @type {string} */ subheading) { sendMessage({ homeSubHeading: subheading }); }, // setTriggerMessage: function(triggerMessage, isProactive = false) { // sendMessage({ triggerMessage, isProactive }) // }, /** add a custom callback to widget events */ setEventListener: function (/** @type {(payload: any) => void} */ callback) { function handler(/** @type {any} */ e) { // @ts-ignore const key = e.message ? "message" : "data"; // @ts-ignore const data = e[key]; if (typeof data === "object" && data.type === "USER_ACTION" && data.payload) callback(data.payload); } window.addEventListener("message", handler); return handler; }, /** remove a custom callback */ removeEventListener: function (/** @type {(e: any) => void} */ handler) { window.removeEventListener("message", handler); }, }; })(); // @ts-ignore window.ov.init(); })();