import { Form, Response } from "@samelogic/models"; import { observe } from "selector-observer"; // import { nanoid } from "nanoid/async"; import { Root, createRoot } from "react-dom/client"; // import { MicrosurveyClient } from "./MicrosurveyClient"; import { MicrosurveyClient, MicrosurveyClientProps, } from "@samelogic/microsurvey-ui"; import { sendCompletedEvent, sendResponseEvent, sendViewedEvent } from "./api"; import tippy from "tippy.js"; // import "tippy.js/dist/tippy.css"; // optional for styling import "tippy.js/animations/perspective.css"; // optional for styling export class SurveyRunner { rootId: string = ""; public container?: HTMLElement; public emotionRoot?: HTMLElement; public shadowRoot?: HTMLElement; sessionId: string = ""; isRunning: boolean = false; runningElement: Element | null = null; identity?: string = undefined; domRoot: Root | null = null; constructor( private researchId: string, private form: Form ) { // this.rootId = "sl-root-" + nanoid(); this.rootId = "sl-root-" + uniqueId(); this.sessionId = uniqueId(); this.watchElement(); } private setup() { this.container = document.createElement("div"); this.container.setAttribute("id", this.rootId); document.body.appendChild(this.container); const shadowContainer = this.container.attachShadow({ mode: "open" }); const emotionRoot = document.createElement("div"); const shadowRootElement = document.createElement("div"); shadowContainer.appendChild(emotionRoot); shadowContainer.appendChild(shadowRootElement); this.emotionRoot = emotionRoot; this.shadowRoot = shadowRootElement; } private cleanup() { // only do shutdown if this is our running element if (this.domRoot) { this.domRoot.unmount(); this.domRoot = null; } this.isRunning = false; this.runningElement = null; if (this.container) { this.container.remove(); } } public setIdentity(identity?: string) { this.identity = identity; } private handleElementEvent = (event: Event, anchor: Element) => { // console.log("event click"); //TODO: event.preventDefault(); and event.preventPropagation(); here when needed to stop default behavior // if (this.props.preventDefault) { event.preventDefault(); // } // prevent multiple microsurveys from being triggered if (this.isRunning) return; // trigger microsurvey this.setup(); // renderForm(this.container!, event.target as HTMLElement, this.form); const domRoot = createRoot(this.shadowRoot!); if (!this.form.settings?.anchorEl) { console.log("samelogic: error, no anchor element specified"); return; } // const anchor = document.querySelector(this.form.settings?.anchorEl!); // console.log("anchor"); // console.log(anchor); const props: MicrosurveyClientProps = { form: this.form, // page?: number, // open?: boolean; anchorEl: anchor, container: null, styleRoot: this.emotionRoot, onClosed: () => { // console.log("form closed!"); // this.isRunning = false; this.cleanup(); }, onAnswer: (formResponse: Response) => { // console.log("form answer!"); // console.log(formResponse); sendResponseEvent( this.researchId, this.sessionId, formResponse, this.identity ); }, onSubmit: (formResponse: Response) => { // console.log("form completed!"); // console.log(formResponse); sendCompletedEvent( this.researchId, this.sessionId, formResponse, this.identity ); }, }; // start showing this.isRunning = true; this.runningElement = anchor; domRoot.render(); this.domRoot = domRoot; sendViewedEvent(this.researchId, this.sessionId); }; // https://github.com/samelogic/samelogic.js/blob/master/sdks/samelogic.js/src/workflows/Triggers/TriggerEvent/EventStrategies/PageEventStrategy.ts private watchPage() {} // https://github.com/samelogic/samelogic.js/blob/master/sdks/samelogic.js/src/workflows/Triggers/TriggerEvent/EventStrategies/ElementEventStrategy.ts private activeElements: Set = new Set(); private watchElement() { const selector = this.form.settings?.anchorEl; if (!selector) return; if (this.form.settings?.trigger?.type == "hover") { const tip = this.form.settings.trigger.hoverToolTip || " "; observe(selector, { add: (el) => { if (isWithinTippy(el) || this.activeElements.has(el)) return; const iconHtml = ` `; const tooltipContent = `
${iconHtml} ${tip}
`; const instance = tippy(el, { content: tooltipContent, allowHTML: true, interactive: true, hideOnClick: true, animation: "perspective", interactiveBorder: 12, interactiveDebounce: 1000, onShow: (instance) => { if (this.activeElements.has(el)) { instance.disable(); return false; } const box = instance.popper.querySelector( ".tippy-box" ) as HTMLElement; const content = box.querySelector( ".tippy-content" ) as HTMLElement; // Style the content box for rounded borders and drop shadow content.style.padding = "10px 20px"; content.style.borderRadius = "200px"; content.style.boxShadow = "0px 4px 8px rgba(0, 0, 0, 0.3)"; content.style.background = "rgba(0, 0, 0, 0.85)"; content.style.cursor = "pointer"; content.style.fontSize = "13px"; content.style.color = "#fff"; content.style.fontWeight = "550"; // Ensure the icon and text are aligned const icon = content.querySelector("svg"); if (icon) { icon.style.marginRight = "8px"; } }, }); instance.popper.addEventListener("click", (evt) => { if (!this.activeElements.has(el)) { this.handleElementEvent(evt, el); this.activeElements.add(el); instance.hide(); instance.disable(); } }); }, remove: (el) => { if (isWithinTippy(el)) return; if (this.runningElement == el) { this.cleanup(); } this.activeElements.delete(el); }, }); } else if (this.form.settings?.trigger?.type == "click") { observe(selector, { add: (el) => { if (isWithinTippy(el) || this.activeElements.has(el)) return; el.addEventListener("click", (evt) => { if (!this.activeElements.has(el)) { this.handleElementEvent(evt, el); this.activeElements.add(el); } }); }, remove: (el) => { if (isWithinTippy(el)) return; if (this.runningElement == el) { this.cleanup(); } this.activeElements.delete(el); }, }); } } } // https://paulius-repsys.medium.com/simplest-possible-way-to-generate-unique-id-in-javascript-a0d7566f3b0c function uniqueId(): string { const dateString = Date.now().toString(36); const randomness = Math.random().toString(36).substr(2); return dateString + randomness; } function isWithinTippy(el: Element) { // check all the way up top // if tippy is found, return true while (el != null) { if (el.classList.contains("tippy-content")) { return true; } if (el.id.startsWith("tippy")) return true; if (el instanceof HTMLElement && el.dataset.tippy) return true; el = el.parentElement!; } return false; }