Skip to main content
Glama
surveys.jsx42.8 kB
var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; import { doesSurveyUrlMatch } from '../posthog-surveys'; import { SurveyQuestionBranchingType, SurveyQuestionType, SurveyType, } from '../posthog-surveys-types'; import * as Preact from 'preact'; import { useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { addEventListener } from '../utils'; import { document as _document, window as _window } from '../utils/globals'; import { createLogger } from '../utils/logger'; import { isNull, isNumber } from '../utils/type-utils'; import { createWidgetShadow, createWidgetStyle } from './surveys-widget'; import { ConfirmationMessage } from './surveys/components/ConfirmationMessage'; import { Cancel } from './surveys/components/QuestionHeader'; import { LinkQuestion, MultipleChoiceQuestion, OpenTextQuestion, RatingQuestion, } from './surveys/components/QuestionTypes'; import { createShadow, defaultSurveyAppearance, dismissedSurveyEvent, getContrastingTextColor, getDisplayOrderQuestions, getSurveyResponseKey, getSurveySeen, hasWaitPeriodPassed, sendSurveyEvent, style, SURVEY_DEFAULT_Z_INDEX, SurveyContext, } from './surveys/surveys-utils'; import { prepareStylesheet } from './utils/stylesheet-loader'; var logger = createLogger('[Surveys]'); // We cast the types here which is dangerous but protected by the top level generateSurveys call var window = _window; var document = _document; function getPosthogWidgetClass(surveyId) { return ".PostHogWidget".concat(surveyId); } function getRatingBucketForResponseValue(responseValue, scale) { if (scale === 3) { if (responseValue < 1 || responseValue > 3) { throw new Error('The response must be in range 1-3'); } return responseValue === 1 ? 'negative' : responseValue === 2 ? 'neutral' : 'positive'; } else if (scale === 5) { if (responseValue < 1 || responseValue > 5) { throw new Error('The response must be in range 1-5'); } return responseValue <= 2 ? 'negative' : responseValue === 3 ? 'neutral' : 'positive'; } else if (scale === 7) { if (responseValue < 1 || responseValue > 7) { throw new Error('The response must be in range 1-7'); } return responseValue <= 3 ? 'negative' : responseValue === 4 ? 'neutral' : 'positive'; } else if (scale === 10) { if (responseValue < 0 || responseValue > 10) { throw new Error('The response must be in range 0-10'); } return responseValue <= 6 ? 'detractors' : responseValue <= 8 ? 'passives' : 'promoters'; } throw new Error('The scale must be one of: 3, 5, 7, 10'); } export function getNextSurveyStep(survey, currentQuestionIndex, response) { var _a, _b, _c, _d, _e; var question = survey.questions[currentQuestionIndex]; var nextQuestionIndex = currentQuestionIndex + 1; if (!((_a = question.branching) === null || _a === void 0 ? void 0 : _a.type)) { if (currentQuestionIndex === survey.questions.length - 1) { return SurveyQuestionBranchingType.End; } return nextQuestionIndex; } if (question.branching.type === SurveyQuestionBranchingType.End) { return SurveyQuestionBranchingType.End; } else if (question.branching.type === SurveyQuestionBranchingType.SpecificQuestion) { if (Number.isInteger(question.branching.index)) { return question.branching.index; } } else if (question.branching.type === SurveyQuestionBranchingType.ResponseBased) { // Single choice if (question.type === SurveyQuestionType.SingleChoice) { // :KLUDGE: for now, look up the choiceIndex based on the response // TODO: once QuestionTypes.MultipleChoiceQuestion is refactored, pass the selected choiceIndex into this method var selectedChoiceIndex = question.choices.indexOf("".concat(response)); if ((_c = (_b = question.branching) === null || _b === void 0 ? void 0 : _b.responseValues) === null || _c === void 0 ? void 0 : _c.hasOwnProperty(selectedChoiceIndex)) { var nextStep = question.branching.responseValues[selectedChoiceIndex]; // Specific question if (Number.isInteger(nextStep)) { return nextStep; } if (nextStep === SurveyQuestionBranchingType.End) { return SurveyQuestionBranchingType.End; } return nextQuestionIndex; } } else if (question.type === SurveyQuestionType.Rating) { if (typeof response !== 'number' || !Number.isInteger(response)) { throw new Error('The response type must be an integer'); } var ratingBucket = getRatingBucketForResponseValue(response, question.scale); if ((_e = (_d = question.branching) === null || _d === void 0 ? void 0 : _d.responseValues) === null || _e === void 0 ? void 0 : _e.hasOwnProperty(ratingBucket)) { var nextStep = question.branching.responseValues[ratingBucket]; // Specific question if (Number.isInteger(nextStep)) { return nextStep; } if (nextStep === SurveyQuestionBranchingType.End) { return SurveyQuestionBranchingType.End; } return nextQuestionIndex; } } return nextQuestionIndex; } logger.warn('Falling back to next question index due to unexpected branching type'); return nextQuestionIndex; } var SurveyManager = /** @class */ (function () { function SurveyManager(posthog) { var _this = this; this.surveyTimeouts = new Map(); this.canShowNextEventBasedSurvey = function () { var _a; // with event based surveys, we need to show the next survey without reloading the page. // A simple check for div elements with the class name pattern of PostHogSurvey_xyz doesn't work here // because preact leaves behind the div element for any surveys responded/dismissed with a <style> node. // To alleviate this, we check the last div in the dom and see if it has any elements other than a Style node. // if the last PostHogSurvey_xyz div has only one style node, we can show the next survey in the queue // without reloading the page. var surveyPopups = document.querySelectorAll("div[class^=PostHogSurvey]"); if (surveyPopups.length > 0) { return ((_a = surveyPopups[surveyPopups.length - 1].shadowRoot) === null || _a === void 0 ? void 0 : _a.childElementCount) === 1; } return true; }; this.handlePopoverSurvey = function (survey) { var _a, _b; var surveyWaitPeriodInDays = (_a = survey.conditions) === null || _a === void 0 ? void 0 : _a.seenSurveyWaitPeriodInDays; var lastSeenSurveyDate = localStorage.getItem("lastSeenSurveyDate"); if (!hasWaitPeriodPassed(lastSeenSurveyDate, surveyWaitPeriodInDays)) { return; } var surveySeen = getSurveySeen(survey); if (!surveySeen) { _this.clearSurveyTimeout(survey.id); _this.addSurveyToFocus(survey.id); var delaySeconds = ((_b = survey.appearance) === null || _b === void 0 ? void 0 : _b.surveyPopupDelaySeconds) || 0; var shadow_1 = createShadow(style(survey === null || survey === void 0 ? void 0 : survey.appearance), survey.id, undefined, _this.posthog); if (delaySeconds <= 0) { return Preact.render(<SurveyPopup key={'popover-survey'} posthog={_this.posthog} survey={survey} removeSurveyFromFocus={_this.removeSurveyFromFocus} isPopup={true}/>, shadow_1); } var timeoutId = setTimeout(function () { if (!doesSurveyUrlMatch(survey)) { return _this.removeSurveyFromFocus(survey.id); } // rendering with surveyPopupDelaySeconds = 0 because we're already handling the timeout here Preact.render(<SurveyPopup key={'popover-survey'} posthog={_this.posthog} survey={__assign(__assign({}, survey), { appearance: __assign(__assign({}, survey.appearance), { surveyPopupDelaySeconds: 0 }) })} removeSurveyFromFocus={_this.removeSurveyFromFocus} isPopup={true}/>, shadow_1); }, delaySeconds * 1000); _this.surveyTimeouts.set(survey.id, timeoutId); } }; this.handleWidget = function (survey) { var shadow = createWidgetShadow(survey, _this.posthog); var stylesheetContent = style(survey.appearance); var stylesheet = prepareStylesheet(document, stylesheetContent, _this.posthog); if (stylesheet) { shadow.appendChild(stylesheet); } Preact.render(<FeedbackWidget key={'feedback-survey'} posthog={_this.posthog} survey={survey} removeSurveyFromFocus={_this.removeSurveyFromFocus}/>, shadow); }; this.handleWidgetSelector = function (survey) { var _a, _b, _c; var selectorOnPage = ((_a = survey.appearance) === null || _a === void 0 ? void 0 : _a.widgetSelector) && document.querySelector(survey.appearance.widgetSelector); if (selectorOnPage) { if (document.querySelectorAll(".PostHogWidget".concat(survey.id)).length === 0) { _this.handleWidget(survey); } else if (document.querySelectorAll(".PostHogWidget".concat(survey.id)).length === 1) { // we have to check if user selector already has a survey listener attached to it because we always have to check if it's on the page or not if (!selectorOnPage.getAttribute('PHWidgetSurveyClickListener')) { var surveyPopup_1 = (_c = (_b = document .querySelector(getPosthogWidgetClass(survey.id))) === null || _b === void 0 ? void 0 : _b.shadowRoot) === null || _c === void 0 ? void 0 : _c.querySelector(".survey-form"); addEventListener(selectorOnPage, 'click', function () { if (surveyPopup_1) { surveyPopup_1.style.display = surveyPopup_1.style.display === 'none' ? 'block' : 'none'; addEventListener(surveyPopup_1, 'PHSurveyClosed', function () { _this.removeSurveyFromFocus(survey.id); surveyPopup_1.style.display = 'none'; }); } }); selectorOnPage.setAttribute('PHWidgetSurveyClickListener', 'true'); } } } }; /** * Checks the feature flags associated with this Survey to see if the survey can be rendered. * @param survey * @param instance */ this.canRenderSurvey = function (survey) { var renderReason = { visible: false, }; if (survey.end_date) { renderReason.disabledReason = "survey was completed on ".concat(survey.end_date); return renderReason; } if (survey.type != SurveyType.Popover) { renderReason.disabledReason = "Only Popover survey types can be rendered"; return renderReason; } var linkedFlagCheck = survey.linked_flag_key ? _this.posthog.featureFlags.isFeatureEnabled(survey.linked_flag_key) : true; if (!linkedFlagCheck) { renderReason.disabledReason = "linked feature flag ".concat(survey.linked_flag_key, " is false"); return renderReason; } var targetingFlagCheck = survey.targeting_flag_key ? _this.posthog.featureFlags.isFeatureEnabled(survey.targeting_flag_key) : true; if (!targetingFlagCheck) { renderReason.disabledReason = "targeting feature flag ".concat(survey.targeting_flag_key, " is false"); return renderReason; } var internalTargetingFlagCheck = survey.internal_targeting_flag_key ? _this.posthog.featureFlags.isFeatureEnabled(survey.internal_targeting_flag_key) : true; if (!internalTargetingFlagCheck) { renderReason.disabledReason = "internal targeting feature flag ".concat(survey.internal_targeting_flag_key, " is false"); return renderReason; } renderReason.visible = true; return renderReason; }; this.renderSurvey = function (survey, selector) { Preact.render(<SurveyPopup key={'popover-survey'} posthog={_this.posthog} survey={survey} removeSurveyFromFocus={_this.removeSurveyFromFocus} isPopup={false}/>, selector); }; this.callSurveysAndEvaluateDisplayLogic = function (forceReload) { var _a; if (forceReload === void 0) { forceReload = false; } (_a = _this.posthog) === null || _a === void 0 ? void 0 : _a.getActiveMatchingSurveys(function (surveys) { var nonAPISurveys = surveys.filter(function (survey) { return survey.type !== 'api'; }); // Create a queue of surveys sorted by their appearance delay. We will evaluate the display logic // for each survey in the queue in order, and only display one survey at a time. var nonAPISurveyQueue = _this.sortSurveysByAppearanceDelay(nonAPISurveys); nonAPISurveyQueue.forEach(function (survey) { var _a, _b, _c; // We only evaluate the display logic for one survey at a time if (!isNull(_this.surveyInFocus)) { return; } if (survey.type === SurveyType.Widget) { if (((_a = survey.appearance) === null || _a === void 0 ? void 0 : _a.widgetType) === 'tab' && document.querySelectorAll(".PostHogWidget".concat(survey.id)).length === 0) { _this.handleWidget(survey); } if (((_b = survey.appearance) === null || _b === void 0 ? void 0 : _b.widgetType) === 'selector' && ((_c = survey.appearance) === null || _c === void 0 ? void 0 : _c.widgetSelector)) { _this.handleWidgetSelector(survey); } } if (survey.type === SurveyType.Popover && _this.canShowNextEventBasedSurvey()) { _this.handlePopoverSurvey(survey); } }); }, forceReload); }; this.addSurveyToFocus = function (id) { if (!isNull(_this.surveyInFocus)) { logger.error("Survey ".concat(__spreadArray([], __read(_this.surveyInFocus), false), " already in focus. Cannot add survey ").concat(id, ".")); } _this.surveyInFocus = id; }; this.removeSurveyFromFocus = function (id) { if (_this.surveyInFocus !== id) { logger.error("Survey ".concat(id, " is not in focus. Cannot remove survey ").concat(id, ".")); } _this.clearSurveyTimeout(id); _this.surveyInFocus = null; }; this.posthog = posthog; // This is used to track the survey that is currently in focus. We only show one survey at a time. this.surveyInFocus = null; } SurveyManager.prototype.clearSurveyTimeout = function (surveyId) { var timeout = this.surveyTimeouts.get(surveyId); if (timeout) { clearTimeout(timeout); this.surveyTimeouts.delete(surveyId); } }; /** * Sorts surveys by their appearance delay in ascending order. If a survey does not have an appearance delay, * it is considered to have a delay of 0. * @param surveys * @returns The surveys sorted by their appearance delay */ SurveyManager.prototype.sortSurveysByAppearanceDelay = function (surveys) { return surveys.sort(function (a, b) { var _a, _b; return (((_a = a.appearance) === null || _a === void 0 ? void 0 : _a.surveyPopupDelaySeconds) || 0) - (((_b = b.appearance) === null || _b === void 0 ? void 0 : _b.surveyPopupDelaySeconds) || 0); }); }; // Expose internal state and methods for testing SurveyManager.prototype.getTestAPI = function () { return { addSurveyToFocus: this.addSurveyToFocus, removeSurveyFromFocus: this.removeSurveyFromFocus, surveyInFocus: this.surveyInFocus, surveyTimeouts: this.surveyTimeouts, canShowNextEventBasedSurvey: this.canShowNextEventBasedSurvey, handleWidget: this.handleWidget, handlePopoverSurvey: this.handlePopoverSurvey, handleWidgetSelector: this.handleWidgetSelector, sortSurveysByAppearanceDelay: this.sortSurveysByAppearanceDelay, }; }; return SurveyManager; }()); export { SurveyManager }; export var renderSurveysPreview = function (_a) { var _b, _c; var survey = _a.survey, parentElement = _a.parentElement, previewPageIndex = _a.previewPageIndex, forceDisableHtml = _a.forceDisableHtml, onPreviewSubmit = _a.onPreviewSubmit, posthog = _a.posthog; var stylesheetContent = style(survey.appearance); var stylesheet = prepareStylesheet(document, stylesheetContent, posthog); // Remove previously attached <style> Array.from(parentElement.children).forEach(function (child) { if (child instanceof HTMLStyleElement) { parentElement.removeChild(child); } }); if (stylesheet) { parentElement.appendChild(stylesheet); } var textColor = getContrastingTextColor(((_b = survey.appearance) === null || _b === void 0 ? void 0 : _b.backgroundColor) || defaultSurveyAppearance.backgroundColor || 'white'); Preact.render(<SurveyPopup key="surveys-render-preview" survey={survey} forceDisableHtml={forceDisableHtml} style={{ position: 'relative', right: 0, borderBottom: "1px solid ".concat((_c = survey.appearance) === null || _c === void 0 ? void 0 : _c.borderColor), borderRadius: 10, color: textColor, }} onPreviewSubmit={onPreviewSubmit} previewPageIndex={previewPageIndex} removeSurveyFromFocus={function () { }} isPopup={true}/>, parentElement); }; export var renderFeedbackWidgetPreview = function (_a) { var _b; var survey = _a.survey, root = _a.root, forceDisableHtml = _a.forceDisableHtml, posthog = _a.posthog; var stylesheetContent = createWidgetStyle((_b = survey.appearance) === null || _b === void 0 ? void 0 : _b.widgetColor); var stylesheet = prepareStylesheet(document, stylesheetContent, posthog); if (stylesheet) { root.appendChild(stylesheet); } Preact.render(<FeedbackWidget key={'feedback-render-preview'} forceDisableHtml={forceDisableHtml} survey={survey} readOnly={true} removeSurveyFromFocus={function () { }}/>, root); }; // This is the main exported function export function generateSurveys(posthog) { // NOTE: Important to ensure we never try and run surveys without a window environment if (!document || !window) { return; } var surveyManager = new SurveyManager(posthog); surveyManager.callSurveysAndEvaluateDisplayLogic(true); // recalculate surveys every second to check if URL or selectors have changed setInterval(function () { surveyManager.callSurveysAndEvaluateDisplayLogic(false); }, 1000); return surveyManager; } /** * This hook handles URL-based survey visibility after the initial mount. * The initial URL check is handled by the `getActiveMatchingSurveys` method in the `PostHogSurveys` class, * which ensures the URL matches before displaying a survey for the first time. * That is the method that is called every second to see if there's a matching survey. * * This separation of concerns means: * 1. Initial URL matching is done by `getActiveMatchingSurveys` before displaying the survey * 2. Subsequent URL changes are handled here to hide the survey as the user navigates */ export function useHideSurveyOnURLChange(_a) { var survey = _a.survey, removeSurveyFromFocus = _a.removeSurveyFromFocus, setSurveyVisible = _a.setSurveyVisible, _b = _a.isPreviewMode, isPreviewMode = _b === void 0 ? false : _b; useEffect(function () { var _a; if (isPreviewMode || !((_a = survey.conditions) === null || _a === void 0 ? void 0 : _a.url)) { return; } var checkUrlMatch = function () { var urlCheck = doesSurveyUrlMatch(survey); if (!urlCheck) { setSurveyVisible(false); return removeSurveyFromFocus(survey.id); } }; // Listen for browser back/forward browser history changes addEventListener(window, 'popstate', checkUrlMatch); // Listen for hash changes, for SPA frameworks that use hash-based routing // The hashchange event is fired when the fragment identifier of the URL has changed (the part of the URL beginning with and following the # symbol). addEventListener(window, 'hashchange', checkUrlMatch); // Listen for SPA navigation var originalPushState = window.history.pushState; var originalReplaceState = window.history.replaceState; window.history.pushState = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } originalPushState.apply(this, args); checkUrlMatch(); }; window.history.replaceState = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } originalReplaceState.apply(this, args); checkUrlMatch(); }; return function () { window.removeEventListener('popstate', checkUrlMatch); window.removeEventListener('hashchange', checkUrlMatch); window.history.pushState = originalPushState; window.history.replaceState = originalReplaceState; }; }, [isPreviewMode, survey, removeSurveyFromFocus, setSurveyVisible]); } export function usePopupVisibility(survey, posthog, millisecondDelay, isPreviewMode, removeSurveyFromFocus) { var _a = __read(useState(isPreviewMode || millisecondDelay === 0), 2), isPopupVisible = _a[0], setIsPopupVisible = _a[1]; var _b = __read(useState(false), 2), isSurveySent = _b[0], setIsSurveySent = _b[1]; useEffect(function () { if (!posthog) { logger.error('usePopupVisibility hook called without a PostHog instance.'); return; } if (isPreviewMode) { return; } var handleSurveyClosed = function () { removeSurveyFromFocus(survey.id); setIsPopupVisible(false); }; var handleSurveySent = function () { var _a, _b; if (!((_a = survey.appearance) === null || _a === void 0 ? void 0 : _a.displayThankYouMessage)) { removeSurveyFromFocus(survey.id); setIsPopupVisible(false); } else { setIsSurveySent(true); removeSurveyFromFocus(survey.id); if ((_b = survey.appearance) === null || _b === void 0 ? void 0 : _b.autoDisappear) { setTimeout(function () { setIsPopupVisible(false); }, 5000); } } }; var showSurvey = function () { var _a; // check if the url is still matching, necessary for delayed surveys, as the URL may have changed if (!doesSurveyUrlMatch(survey)) { return; } setIsPopupVisible(true); window.dispatchEvent(new Event('PHSurveyShown')); posthog.capture('survey shown', { $survey_name: survey.name, $survey_id: survey.id, $survey_iteration: survey.current_iteration, $survey_iteration_start_date: survey.current_iteration_start_date, sessionRecordingUrl: (_a = posthog.get_session_replay_url) === null || _a === void 0 ? void 0 : _a.call(posthog), }); localStorage.setItem('lastSeenSurveyDate', new Date().toISOString()); setTimeout(function () { var _a, _b; var inputField = (_b = (_a = document .querySelector(getPosthogWidgetClass(survey.id))) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('textarea, input[type="text"]'); if (inputField) { inputField.focus(); } }, 100); }; addEventListener(window, 'PHSurveyClosed', handleSurveyClosed); addEventListener(window, 'PHSurveySent', handleSurveySent); if (millisecondDelay > 0) { // This path is only used for direct usage of SurveyPopup, // not for surveys managed by SurveyManager var timeoutId_1 = setTimeout(showSurvey, millisecondDelay); return function () { clearTimeout(timeoutId_1); window.removeEventListener('PHSurveyClosed', handleSurveyClosed); window.removeEventListener('PHSurveySent', handleSurveySent); }; } else { // This is the path used for surveys managed by SurveyManager showSurvey(); return function () { window.removeEventListener('PHSurveyClosed', handleSurveyClosed); window.removeEventListener('PHSurveySent', handleSurveySent); }; } }, []); useHideSurveyOnURLChange({ survey: survey, removeSurveyFromFocus: removeSurveyFromFocus, setSurveyVisible: setIsPopupVisible, isPreviewMode: isPreviewMode, }); return { isPopupVisible: isPopupVisible, isSurveySent: isSurveySent, setIsPopupVisible: setIsPopupVisible }; } export function SurveyPopup(_a) { var _b, _c, _d, _e; var survey = _a.survey, forceDisableHtml = _a.forceDisableHtml, posthog = _a.posthog, style = _a.style, previewPageIndex = _a.previewPageIndex, removeSurveyFromFocus = _a.removeSurveyFromFocus, isPopup = _a.isPopup, _f = _a.onPreviewSubmit, onPreviewSubmit = _f === void 0 ? function () { } : _f, _g = _a.onPopupSurveyDismissed, onPopupSurveyDismissed = _g === void 0 ? function () { } : _g, _h = _a.onPopupSurveySent, onPopupSurveySent = _h === void 0 ? function () { } : _h; var isPreviewMode = Number.isInteger(previewPageIndex); // NB: The client-side code passes the millisecondDelay in seconds, but setTimeout expects milliseconds, so we multiply by 1000 var surveyPopupDelayMilliseconds = ((_b = survey.appearance) === null || _b === void 0 ? void 0 : _b.surveyPopupDelaySeconds) ? survey.appearance.surveyPopupDelaySeconds * 1000 : 0; var _j = usePopupVisibility(survey, posthog, surveyPopupDelayMilliseconds, isPreviewMode, removeSurveyFromFocus), isPopupVisible = _j.isPopupVisible, isSurveySent = _j.isSurveySent, setIsPopupVisible = _j.setIsPopupVisible; var shouldShowConfirmation = isSurveySent || previewPageIndex === survey.questions.length; var confirmationBoxLeftStyle = (style === null || style === void 0 ? void 0 : style.left) && isNumber(style === null || style === void 0 ? void 0 : style.left) ? { left: style.left - 40 } : {}; if (isPreviewMode) { style = style || {}; style.left = 'unset'; style.right = 'unset'; style.transform = 'unset'; } return isPopupVisible ? (<SurveyContext.Provider value={{ isPreviewMode: isPreviewMode, previewPageIndex: previewPageIndex, onPopupSurveyDismissed: function () { dismissedSurveyEvent(survey, posthog, isPreviewMode); onPopupSurveyDismissed(); }, isPopup: isPopup || false, onPreviewSubmit: onPreviewSubmit, onPopupSurveySent: function () { onPopupSurveySent(); }, }}> {!shouldShowConfirmation ? (<Questions survey={survey} forceDisableHtml={!!forceDisableHtml} posthog={posthog} styleOverrides={style}/>) : (<ConfirmationMessage header={((_c = survey.appearance) === null || _c === void 0 ? void 0 : _c.thankYouMessageHeader) || 'Thank you!'} description={((_d = survey.appearance) === null || _d === void 0 ? void 0 : _d.thankYouMessageDescription) || ''} forceDisableHtml={!!forceDisableHtml} contentType={(_e = survey.appearance) === null || _e === void 0 ? void 0 : _e.thankYouMessageDescriptionContentType} appearance={survey.appearance || defaultSurveyAppearance} styleOverrides={__assign(__assign({}, style), confirmationBoxLeftStyle)} onClose={function () { return setIsPopupVisible(false); }}/>)} </SurveyContext.Provider>) : null; } export function Questions(_a) { var _b, _c; var survey = _a.survey, forceDisableHtml = _a.forceDisableHtml, posthog = _a.posthog, styleOverrides = _a.styleOverrides; var textColor = getContrastingTextColor(((_b = survey.appearance) === null || _b === void 0 ? void 0 : _b.backgroundColor) || defaultSurveyAppearance.backgroundColor); var _d = __read(useState({}), 2), questionsResponses = _d[0], setQuestionsResponses = _d[1]; var _e = useContext(SurveyContext), previewPageIndex = _e.previewPageIndex, onPopupSurveyDismissed = _e.onPopupSurveyDismissed, isPopup = _e.isPopup, onPreviewSubmit = _e.onPreviewSubmit, onPopupSurveySent = _e.onPopupSurveySent; var _f = __read(useState(previewPageIndex || 0), 2), currentQuestionIndex = _f[0], setCurrentQuestionIndex = _f[1]; var surveyQuestions = useMemo(function () { return getDisplayOrderQuestions(survey); }, [survey]); // Sync preview state useEffect(function () { setCurrentQuestionIndex(previewPageIndex !== null && previewPageIndex !== void 0 ? previewPageIndex : 0); }, [previewPageIndex]); var onNextButtonClick = function (_a) { var _b, _c; var res = _a.res, displayQuestionIndex = _a.displayQuestionIndex, questionId = _a.questionId; if (!posthog) { logger.error('onNextButtonClick called without a PostHog instance.'); return; } if (!questionId) { logger.error('onNextButtonClick called without a questionId.'); return; } var responseKey = getSurveyResponseKey(questionId); setQuestionsResponses(__assign(__assign({}, questionsResponses), (_b = {}, _b[responseKey] = res, _b))); var nextStep = getNextSurveyStep(survey, displayQuestionIndex, res); if (nextStep === SurveyQuestionBranchingType.End) { sendSurveyEvent(__assign(__assign({}, questionsResponses), (_c = {}, _c[responseKey] = res, _c)), survey, posthog); onPopupSurveySent(); } else { setCurrentQuestionIndex(nextStep); } }; return (<form className="survey-form" style={isPopup ? __assign({ color: textColor, borderColor: (_c = survey.appearance) === null || _c === void 0 ? void 0 : _c.borderColor }, styleOverrides) : {}}> {surveyQuestions.map(function (question, displayQuestionIndex) { var _a; var isVisible = currentQuestionIndex === displayQuestionIndex; return (isVisible && (<div className="survey-box" style={isPopup ? { backgroundColor: ((_a = survey.appearance) === null || _a === void 0 ? void 0 : _a.backgroundColor) || defaultSurveyAppearance.backgroundColor, } : {}}> {isPopup && (<Cancel onClick={function () { onPopupSurveyDismissed(); }}/>)} {getQuestionComponent({ question: question, forceDisableHtml: forceDisableHtml, displayQuestionIndex: displayQuestionIndex, appearance: survey.appearance || defaultSurveyAppearance, onSubmit: function (res) { return onNextButtonClick({ res: res, displayQuestionIndex: displayQuestionIndex, questionId: question.id, }); }, onPreviewSubmit: onPreviewSubmit, })} </div>)); })} </form>); } export function FeedbackWidget(_a) { var _b, _c; var survey = _a.survey, forceDisableHtml = _a.forceDisableHtml, posthog = _a.posthog, readOnly = _a.readOnly, removeSurveyFromFocus = _a.removeSurveyFromFocus; var _d = __read(useState(true), 2), isFeedbackButtonVisible = _d[0], setIsFeedbackButtonVisible = _d[1]; var _e = __read(useState(false), 2), showSurvey = _e[0], setShowSurvey = _e[1]; var _f = __read(useState({}), 2), styleOverrides = _f[0], setStyle = _f[1]; var widgetRef = useRef(null); useEffect(function () { var _a, _b, _c, _d; if (!posthog) { logger.error('FeedbackWidget called without a PostHog instance.'); return; } if (readOnly) { return; } if (((_a = survey.appearance) === null || _a === void 0 ? void 0 : _a.widgetType) === 'tab') { if (widgetRef.current) { var widgetPos = widgetRef.current.getBoundingClientRect(); var style_1 = { top: '50%', left: parseInt("".concat(widgetPos.right - 360)), bottom: 'auto', borderRadius: 10, borderBottom: "1.5px solid ".concat(((_b = survey.appearance) === null || _b === void 0 ? void 0 : _b.borderColor) || '#c9c6c6'), }; setStyle(style_1); } } if (((_c = survey.appearance) === null || _c === void 0 ? void 0 : _c.widgetType) === 'selector') { var widget = (_d = document.querySelector(survey.appearance.widgetSelector || '')) !== null && _d !== void 0 ? _d : undefined; addEventListener(widget, 'click', function (event) { var _a, _b; // Calculate position based on the selector button var buttonRect = event.currentTarget.getBoundingClientRect(); var viewportHeight = window.innerHeight; // Get survey width from maxWidth or default to 300px var surveyWidth = parseInt(((_a = survey.appearance) === null || _a === void 0 ? void 0 : _a.maxWidth) || '300'); // Calculate horizontal center position of the button var buttonCenterX = buttonRect.left + buttonRect.width / 2; // Calculate horizontal center position var left = buttonCenterX - surveyWidth / 2; // Ensure the survey doesn't go off-screen horizontally var rightEdge = left + surveyWidth; if (rightEdge > window.innerWidth) { left = window.innerWidth - surveyWidth - 20; // 20px padding from right edge } if (left < 20) { left = 20; // 20px padding from left edge } // Determine if we should show above or below var showAbove = false; // Check if there's enough space below (need at least 300px) // If not enough space below, show above if (buttonRect.bottom + 300 > viewportHeight) { showAbove = true; } // Simple spacing between button and survey var spacing = 12; // Calculate positions var topPosition; if (showAbove) { // Problem: When showing above, we're trying to position based on an estimated height, // but we don't know the actual height of the survey yet. // Solution: Instead of using top positioning for above, use bottom positioning // This will anchor the survey to the bottom edge at the button's top position topPosition = null; // We'll use bottom positioning instead } else { // When showing below, position the top of the survey below the button plus spacing topPosition = buttonRect.bottom + window.scrollY + spacing; } // Set style overrides for positioning setStyle({ position: 'fixed', top: showAbove ? 'auto' : topPosition + 'px', left: left + 'px', right: 'auto', bottom: showAbove ? window.innerHeight - buttonRect.top + spacing + 'px' : 'auto', transform: 'none', border: "1.5px solid ".concat(((_b = survey.appearance) === null || _b === void 0 ? void 0 : _b.borderColor) || '#c9c6c6'), borderRadius: '10px', width: "".concat(surveyWidth, "px"), zIndex: SURVEY_DEFAULT_Z_INDEX, boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', maxHeight: showAbove ? "calc(100vh - 40px - ".concat(spacing * 2, "px)") : "calc(100vh - ".concat(topPosition, "px - 20px)"), }); setShowSurvey(!showSurvey); }); widget === null || widget === void 0 ? void 0 : widget.setAttribute('PHWidgetSurveyClickListener', 'true'); } }, []); useHideSurveyOnURLChange({ survey: survey, removeSurveyFromFocus: removeSurveyFromFocus, setSurveyVisible: setIsFeedbackButtonVisible, }); if (!isFeedbackButtonVisible) { return null; } var resetShowSurvey = function () { setShowSurvey(false); }; return (<Preact.Fragment> {((_b = survey.appearance) === null || _b === void 0 ? void 0 : _b.widgetType) === 'tab' && (<div className="ph-survey-widget-tab" ref={widgetRef} onClick={function () { return !readOnly && setShowSurvey(!showSurvey); }} style={{ color: getContrastingTextColor(survey.appearance.widgetColor) }}> <div className="ph-survey-widget-tab-icon"></div> {((_c = survey.appearance) === null || _c === void 0 ? void 0 : _c.widgetLabel) || ''} </div>)} {showSurvey && (<SurveyPopup key={'feedback-widget-survey'} posthog={posthog} survey={survey} forceDisableHtml={forceDisableHtml} style={styleOverrides} removeSurveyFromFocus={removeSurveyFromFocus} isPopup={true} onPopupSurveyDismissed={resetShowSurvey} onPopupSurveySent={resetShowSurvey}/>)} </Preact.Fragment>); } var getQuestionComponent = function (_a) { var _b, _c; var question = _a.question, forceDisableHtml = _a.forceDisableHtml, displayQuestionIndex = _a.displayQuestionIndex, appearance = _a.appearance, onSubmit = _a.onSubmit, onPreviewSubmit = _a.onPreviewSubmit; var questionComponents = (_b = {}, _b[SurveyQuestionType.Open] = OpenTextQuestion, _b[SurveyQuestionType.Link] = LinkQuestion, _b[SurveyQuestionType.Rating] = RatingQuestion, _b[SurveyQuestionType.SingleChoice] = MultipleChoiceQuestion, _b[SurveyQuestionType.MultipleChoice] = MultipleChoiceQuestion, _b); var commonProps = { question: question, forceDisableHtml: forceDisableHtml, appearance: appearance, onPreviewSubmit: function (res) { onPreviewSubmit(res); }, onSubmit: function (res) { onSubmit(res); }, }; var additionalProps = (_c = {}, _c[SurveyQuestionType.Open] = {}, _c[SurveyQuestionType.Link] = {}, _c[SurveyQuestionType.Rating] = { displayQuestionIndex: displayQuestionIndex }, _c[SurveyQuestionType.SingleChoice] = { displayQuestionIndex: displayQuestionIndex }, _c[SurveyQuestionType.MultipleChoice] = { displayQuestionIndex: displayQuestionIndex }, _c); var Component = questionComponents[question.type]; var componentProps = __assign(__assign({}, commonProps), additionalProps[question.type]); return <Component {...componentProps}/>; }; //# sourceMappingURL=surveys.jsx.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sadiuysal/mem0-mcp-server-ts'

If you have feedback or need assistance with the MCP directory API, please join our Discord server