For WordPress Sites
Overview
This guide will help community newsrooms quickly set up their WordPress sites to publish InformUp articles with embedded interactive surveys. The setup consists of two parts:
- One-time site setup - Add survey infrastructure to your WordPress site
- Per-article setup - Add survey questions to individual articles
Important: Survey embeds are designed for web display only and should not be included in email newsletters as they will not display correctly.
Part 1: One-Time Site Setup
Step 1: Access Your WordPress Admin Panel
- Log into your WordPress admin dashboard (typically
yoursite.com/wp-admin) - Navigate to Appearance → Theme Editor or Appearance → Customize → Additional CSS
Step 2: Add Formbricks Survey Script
Add the following code to your site's header (typically in header.php or through a header/footer plugin):
<!-- START Formbricks Surveys -->
<script type="text/javascript">
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://app.formbricks.com/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:"cmb87q82g0111z401srx7yr8u",appUrl:"https://app.formbricks.com"}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
</script>
<!-- END Formbricks Surveys -->
Alternative Methods to Add Header Code:
- Plugin Method: Install a plugin like "Insert Headers and Footers" or "Header Footer Code Manager"
- Theme Customizer: Some themes allow adding header scripts through Appearance → Customize → Header
- Functions.php: Add via your theme's functions.php file (requires PHP knowledge)
Step 3: Verify Installation
After adding the script, visit your site and check the browser console (F12 → Console tab) to ensure there are no errors related to Formbricks.
Part 2: Article Setup
For each InformUp article you publish, you'll receive:
- The article content
- HTML code blocks for embedded survey questions
- Reporter information
Step 1: Create New Post/Article
- In WordPress Admin, go to Posts → Add New
- Add the article title
- Add the article content in the editor
Step 2: Add Reporter Information
Reporter Details:
- Name: Amy Whipple
- Bio: part-time writer, part-time writing instructor, full-time awesome // 🏳️🌈 // she/her
- Photo:
Add this information to your article's byline or author section according to your site's format.
Step 3: Insert Survey Questions
Survey questions should be inserted after each news brief within the article. You'll receive code blocks with each article. For testing, you can use the examples at the end of this document
Step 4: How to Insert HTML Code in WordPress
Method A: Classic Editor
- Switch to "Text" or "HTML" view (tab at top right of editor)
- Paste the code block where you want the survey to appear
- Switch back to "Visual" view to continue editing
Method B: Block Editor (Gutenberg)
- Click the "+" button to add a new block
- Search for "Custom HTML" block
- Paste the survey code into the HTML block
- The survey will appear when you preview or publish
Method C: Page Builders
- Elementor: Use the HTML widget
- Divi: Use the Code module
- WPBakery: Use the Raw HTML element
Step 5: Preview and Publish
- Click "Preview" to see how the article looks with embedded surveys
- Verify surveys are loading correctly
- Test that survey questions appear and are interactive
- Publish when ready
Important Notes
Language Settings
- Surveys default to Spanish language
- Readers can change the language using the language selector within the survey interface
Survey Completion
- After completing all survey questions, readers are automatically redirected to InformUp's website
- This allows them to sign up for updates and engage further with community initiatives
Email Newsletters
⚠️ Important: Do not include survey code blocks in email newsletters. They require JavaScript to function and will not display in email clients. Include only the article text in emails with a link to the full article on your website.
Troubleshooting
Survey not loading?
- Check browser console for JavaScript errors (F12 → Console)
- Verify the Formbricks script is in your site header
- Ensure you're viewing the published page (not just the editor preview)
- Clear your browser cache
Survey displays incorrectly? - Make sure you're pasting code in HTML/Text view, not Visual view
- Check that the entire code block was copied (including opening and closing tags)
- Verify no other plugins are conflicting with JavaScript
Need to edit survey placement? - You can move the HTML blocks like any other content block
- Surveys will display wherever you place the code
Contact & Support
For technical support or questions about article content and surveys, please reach out right away with any quesitons
- Email: chris@informup.org
Quick Checklist
One-Time Setup ✓
- Added Formbricks script to site header
- Verified script loads without errors
- Tested with a sample article
Per Article ✓
- Created new post
- Added article content
- Added reporter information
- Inserted Block 1 after first topic
- Inserted Block 2 for additional questions (if needed)
- Inserted Block 3 at article end for sequential questions
- Previewed article with working surveys
- Published article
- Excluded survey code from email version
Code samples
Survey Block 1 (First Question)
After the first article topic/section, switch to HTML/Code view and paste:
Question 1 code
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Formbricks Modular Survey Embed</title>
<style>
.gh-content {<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Formbricks Modular Survey Embed</title>
<style>
.gh-content {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.loading {
text-align: center;
color: #6b7280;
font-size: 16px;
}
/* Pre-allocate space for surveys to prevent reflow */
#survey-init,
#survey-question-2,
#survey-sequential {
min-height: 400px; /* Reserve space before survey loads */
visibility: hidden; /* Hide during render to prevent scroll adjustment */
}
/* Make visible and remove min-height once survey is rendered */
#survey-init.survey-loaded,
#survey-question-2.survey-loaded,
#survey-sequential.survey-loaded {
min-height: 0;
visibility: visible;
}
.error {
background: #fef2f2;
border: 1px solid #fecaca;
color: #dc2626;
padding: 16px;
border-radius: 8px;
margin: 20px 0;
}
.success {
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: #059669;
padding: 16px;
border-radius: 8px;
margin: 20px 0;
}
.survey-embed {
border-radius: 12px;
overflow: visible !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* Fix for Formbricks container height - prevent scrollbars */
.survey-embed > div,
.survey-embed .fb-survey,
.survey-embed .fb-survey-inline,
.survey-embed [class*="fb-"],
#survey-init > div,
#survey-init [class*="fb-"] {
height: auto !important;
min-height: auto !important;
max-height: none !important;
overflow: visible !important;
}
/* Ensure all Formbricks content containers expand */
.fb-survey-content,
.fb-card,
.fb-question-container,
[class*="fb-inline"] {
height: auto !important;
min-height: auto !important;
max-height: none !important;
overflow: visible !important;
}
/* Enhanced auto-scroll prevention styles */
.no-auto-scroll {
scroll-behavior: auto !important;
}
.no-auto-scroll,
.no-auto-scroll * {
scroll-behavior: auto !important;
}
/* Prevent any element from causing scroll during loading */
.preventing-focus {
overflow: hidden !important;
}
.preventing-focus body {
overflow: hidden !important;
position: fixed !important;
width: 100% !important;
}
/* Prevent focus outline during initial load */
.preventing-focus *:focus {
outline: none !important;
}
/* Custom dropdown styling to match Formbricks */
.fb-select.fb-form-control {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 16px;
font-family: inherit;
background-color: #ffffff;
color: #1a202c;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
appearance: none;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="%23718096"><path d="M4.5 6.5L8 10l3.5-3.5H4.5z"/></svg>');
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
cursor: pointer;
}
.fb-select.fb-form-control:focus {
outline: none;
border-color: var(--fb-brand-color, #00C4B8);
box-shadow: 0 0 0 3px rgba(0, 196, 184, 0.1);
}
.fb-select.fb-form-control:required:invalid {
border-color: #e53e3e;
}
.fb-form-field {
margin-bottom: 16px;
}
.fb-navigation-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.fb-button {
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.fb-button-primary {
background-color: var(--fb-brand-color, #00C4B8);
color: white;
}
.fb-button-primary:hover:not(:disabled) {
background-color: #008f87;
}
.fb-button-primary:disabled {
background-color: #a0aec0;
cursor: not-allowed;
}
.fb-button-secondary {
background-color: transparent;
color: var(--fb-brand-color, #00C4B8);
border-color: var(--fb-brand-color, #00C4B8);
}
.fb-button-secondary:hover {
background-color: var(--fb-brand-color, #00C4B8);
color: white;
}
</style>
<!-- Inter font - using Bunny Fonts (privacy-friendly, GDPR compliant) -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700" rel="stylesheet" />
<script>
console.log("📝 Clean survey embed - no modifications applied");
</script>
</head>
<body>
<div class="gh-content">
<h3>Question 1</h3>
<div id="survey-init">
<div class="loading">Loading survey...</div>
</div>
</div>
<!-- =============================================== -->
<!-- BLOCK 1: SURVEY MANAGER + FIRST QUESTION -->
<!-- Copy this block for the FIRST survey instance -->
<!-- =============================================== -->
<script>
(function () {
// IIFE to avoid global scope pollution
console.log("📦 [BLOCK 1] Survey Manager + First Question");
// ========================================
// 🔧 CONFIGURATION - EDIT THESE VALUES
// ========================================
const SURVEY_CONFIG = {
formbricksBaseUrl: "https://app.formbricks.com",
environmentId: "cmb87q82g0111z401srx7yr8u",
targetVariableName: "meeting",
targetVariableValue: "11.17.25",
debug: true,
defaultLanguage: "es", // Default language for questions/answers (en, es, fr, de, etc.). Falls back to "default" then "en"
};
// User Profile Configuration
const USER_PROFILE_CONFIG = {
enabled: false, // Enable user profile integration
apiEndpoint: "/members/api/session/", // Ghost Members API endpoint
saveToProfile: true, // Save survey responses to user profile
prepopulateFromProfile: true, // Pre-fill questions from existing profile data
profileFieldMapping: {
// Map question IDs to profile field names
// Example: 'eahj8ktqtt503sq3uqukkh2k': 'political_preference'
},
};
const BLOCK_1_CONFIG = {
containerId: "survey-init", // Change this for each block
questionIndex: 0, // Which question to show (0-based)
hideNextButton: true, // Hide next/submit buttons
autoSave: true, // Enable auto-save
autoSaveDelay: 1000, // Auto-save delay in ms
useSingleSelectDropdown: false, // Render single-select questions as dropdowns
dropdownPlaceholder: "Please select an option...", // Placeholder text for dropdowns
};
// ========================================
// END CONFIGURATION
// ========================================
// Global error handler to catch Formbricks internal errors
window.addEventListener('unhandledrejection', function(event) {
// Check if error is from Formbricks
if (event.reason && event.reason.message &&
(event.reason.message.includes('headline') ||
event.reason.message.includes('e.headline'))) {
console.warn('⚠️ Caught Formbricks internal error, suppressing:', event.reason.message);
event.preventDefault(); // Prevent error from bubbling
return false;
}
});
// Global survey manager - DO NOT EDIT BELOW THIS LINE
if (!window.FormbricksSurveyManager) {
window.FormbricksSurveyManager = {
survey: null,
formbricksLoaded: false,
loadPromise: null,
instances: new Map(),
sharedResponseId: null, // Shared response ID across all blocks
currentUser: null, // Current logged-in user data
userProfile: null, // User profile data for pre-population
currentLanguage: SURVEY_CONFIG.defaultLanguage || "en", // Global language state, synced across all blocks
debug(message, data = null, level = "info") {
if (!SURVEY_CONFIG.debug) return;
console.log(`[MANAGER][${level.toUpperCase()}]`, message, data);
},
setLanguage(newLanguage) {
console.log(`🌍 [MANAGER] setLanguage called: ${this.currentLanguage} → ${newLanguage}`);
this.debug(`🌍 Language changed from ${this.currentLanguage} to ${newLanguage}`);
this.currentLanguage = newLanguage;
console.log(`🔄 [MANAGER] Re-rendering ${this.instances.size} survey instances`);
// Re-render all survey instances with new language
this.instances.forEach((instance, containerId) => {
console.log(`🔄 [MANAGER] Calling reRenderWithLanguage on ${containerId}`);
this.debug(`🔄 Re-rendering ${containerId} with language: ${newLanguage}`);
instance.reRenderWithLanguage(newLanguage);
});
console.log('✅ [MANAGER] Language change complete');
},
async loadFormbricksJS() {
if (this.formbricksLoaded) return;
if (this.loadPromise) return this.loadPromise;
// Check if global preloader already loaded the library
if (
window.formbricksPreloaderReady &&
typeof window.formbricksSurveys !== "undefined"
) {
this.debug("✅ Library already loaded by global preloader");
this.formbricksLoaded = true;
return Promise.resolve("preloaded");
}
// If preloader is still loading, wait for it
if (
window.formbricksPreloaderInitialized &&
!window.formbricksPreloaderError
) {
this.debug("⏳ Waiting for global preloader to finish...");
this.loadPromise = new Promise((resolve) => {
window.addEventListener(
"formbricksPreloaderReady",
() => {
this.debug("✅ Global preloader finished loading");
this.formbricksLoaded = true;
resolve("preloaded");
},
{ once: true },
);
// Timeout after 5 seconds and fall back to manual loading
setTimeout(() => {
if (!this.formbricksLoaded) {
this.debug(
"⚠️ Preloader timeout, falling back to manual load",
);
window.removeEventListener(
"formbricksPreloaderReady",
() => {},
);
resolve(this.loadFormbricksManually());
}
}, 5000);
});
return this.loadPromise;
}
// No preloader or preloader failed, load manually
return this.loadFormbricksManually();
},
async loadFormbricksManually() {
this.debug("Loading Formbricks JS library...");
const endpoints = [
"/js/surveys.umd.cjs",
"/api/packages/surveys",
"/_next/static/js/surveys.js",
"/surveys.js",
];
for (const endpoint of endpoints) {
const fullUrl = `${SURVEY_CONFIG.formbricksBaseUrl}${endpoint}`;
this.debug(`Trying endpoint: ${fullUrl}`);
try {
const response = await fetch(fullUrl, { method: "HEAD" });
if (response.ok) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = fullUrl;
script.onload = () => {
this.debug(`✅ Library loaded from: ${fullUrl}`);
this.formbricksLoaded = true;
resolve(fullUrl);
};
script.onerror = () => {
reject(
new Error(`Script execution failed: ${fullUrl}`),
);
};
document.head.appendChild(script);
});
}
} catch (error) {
this.debug(
`Failed to load from ${fullUrl}:`,
error.message,
"warning",
);
}
}
throw new Error(
"Could not load Formbricks JS library from any endpoint",
);
},
async fetchSurvey() {
if (this.survey) {
this.debug("Using cached survey");
return this.survey;
}
const url = `${SURVEY_CONFIG.formbricksBaseUrl}/api/v1/client/${SURVEY_CONFIG.environmentId}/environment`;
this.debug("Fetching surveys from:", url);
this.debug("🔍 Looking for variable:", {
name: SURVEY_CONFIG.targetVariableName,
value: SURVEY_CONFIG.targetVariableValue,
});
try {
const response = await fetch(url);
if (!response.ok)
throw new Error(
`HTTP ${response.status}: ${response.statusText}`,
);
const result = await response.json();
const surveys = result?.data?.data?.surveys || [];
this.debug(`Found ${surveys.length} total surveys`);
if (surveys.length === 0)
throw new Error("No surveys found in this environment");
// 🔍 DEBUG: Show all surveys and their variables
surveys.forEach((survey, index) => {
this.debug(`Survey ${index + 1}:`, {
id: survey.id,
name: survey.name,
status: survey.status,
createdAt: survey.createdAt,
updatedAt: survey.updatedAt,
variables: survey.variables || [],
questionCount: survey.questions?.length || 0,
});
});
// 🎯 FIND MATCHING SURVEYS: Only exact matches
const matchingSurveys = surveys.filter((survey) => {
if (survey.status !== "inProgress") {
this.debug(
`❌ Survey ${survey.id} skipped: status = ${survey.status}`,
);
return false;
}
if (!Array.isArray(survey.variables)) {
this.debug(
`❌ Survey ${survey.id} skipped: no variables array`,
);
return false;
}
const hasMatch = survey.variables.some((variable) => {
const nameMatch =
variable.name === SURVEY_CONFIG.targetVariableName;
const valueMatch =
String(variable.value).trim() ===
SURVEY_CONFIG.targetVariableValue.trim();
const bothMatch = nameMatch && valueMatch;
this.debug(`🔍 Survey ${survey.id} variable check:`, {
variable: variable,
nameMatch: nameMatch,
valueMatch: valueMatch,
bothMatch: bothMatch,
});
return bothMatch;
});
if (hasMatch) {
this.debug(
`✅ Survey ${survey.id} MATCHES target criteria`,
);
} else {
this.debug(`❌ Survey ${survey.id} does not match`);
}
return hasMatch;
});
this.debug(
`Found ${matchingSurveys.length} surveys matching criteria`,
);
if (matchingSurveys.length === 0) {
// 🚫 NO FALLBACK - Strict error
const activeCount = surveys.filter(
(s) => s.status === "inProgress",
).length;
throw new Error(
`No survey found with exact match:\n` +
` Variable: ${SURVEY_CONFIG.targetVariableName} = "${SURVEY_CONFIG.targetVariableValue}"\n` +
` Found ${surveys.length} total surveys, ${activeCount} active\n` +
` Check console for detailed survey list`,
);
}
// 📊 SELECT MOST RECENT: Sort by updatedAt, then createdAt
const sortedMatches = matchingSurveys.sort((a, b) => {
// First try updatedAt (most recent activity)
const aUpdated = new Date(a.updatedAt || a.createdAt);
const bUpdated = new Date(b.updatedAt || b.createdAt);
if (aUpdated.getTime() !== bUpdated.getTime()) {
return bUpdated.getTime() - aUpdated.getTime(); // Most recent first
}
// If updatedAt is the same, use createdAt
return (
new Date(b.createdAt).getTime() -
new Date(a.createdAt).getTime()
);
});
const targetSurvey = sortedMatches[0];
this.debug("✅ Using most recent survey:", {
id: targetSurvey.id,
name: targetSurvey.name,
createdAt: targetSurvey.createdAt,
updatedAt: targetSurvey.updatedAt,
isNewest:
sortedMatches.length > 1
? `YES (newest of ${sortedMatches.length} matches)`
: "ONLY MATCH",
matchingVariable: targetSurvey.variables.find(
(v) =>
v.name === SURVEY_CONFIG.targetVariableName &&
String(v.value).trim() ===
SURVEY_CONFIG.targetVariableValue.trim(),
),
});
// Log all survey properties to check for hidden fields configuration
console.log("📋 FULL SURVEY OBJECT:", targetSurvey);
console.log(
"🔍 Survey hiddenFields:",
targetSurvey.hiddenFields,
);
console.log("🔍 Survey fields:", targetSurvey.fields);
console.log("🔍 Survey config:", targetSurvey.config);
this.survey = targetSurvey;
return targetSurvey;
} catch (error) {
this.debug("Fetch error:", error.message, "error");
throw error;
}
},
async loadUserProfile() {
if (!USER_PROFILE_CONFIG.enabled) {
this.debug("👤 User profile integration disabled");
return null;
}
try {
this.debug("👤 Loading user profile...");
// Check if Ghost members are available
if (
typeof window.ghost === "undefined" ||
!window.ghost.members
) {
this.debug(
"👤 Ghost members not available, trying direct API call",
);
return await this.fetchUserProfileDirect();
}
// Use Ghost members API
const member = await window.ghost.members.getCurrentMember();
if (member) {
this.currentUser = member;
this.userProfile = member.profile || {};
this.debug("👤 User profile loaded:", {
email: member.email,
profileKeys: Object.keys(this.userProfile),
});
return this.userProfile;
} else {
this.debug("👤 No logged-in user found");
return null;
}
} catch (error) {
this.debug(
"👤 Error loading user profile:",
error.message,
"error",
);
return null;
}
},
async fetchUserProfileDirect() {
try {
const response = await fetch(USER_PROFILE_CONFIG.apiEndpoint, {
method: "GET",
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
if (response.ok) {
const userData = await response.json();
this.currentUser = userData;
this.userProfile =
userData.profile || userData.metadata || {};
this.debug(
"👤 User profile fetched directly:",
userData.email,
);
return this.userProfile;
} else {
this.debug("👤 User not authenticated (direct fetch)");
return null;
}
} catch (error) {
this.debug(
"👤 Direct profile fetch failed:",
error.message,
"error",
);
return null;
}
},
async saveUserProfile(questionId, selectedValue, question) {
if (
!USER_PROFILE_CONFIG.enabled ||
!USER_PROFILE_CONFIG.saveToProfile
) {
return;
}
if (!this.currentUser) {
this.debug("👤 Cannot save to profile: no logged-in user");
return;
}
try {
this.debug("👤 Saving to user profile...", {
questionId,
selectedValue,
});
// Get profile field name from mapping
const profileField =
USER_PROFILE_CONFIG.profileFieldMapping[questionId];
if (!profileField) {
this.debug(
"👤 No profile field mapping for question:",
questionId,
);
return;
}
// Update local profile data
this.userProfile[profileField] = selectedValue;
// Save to Ghost profile (this would need Ghost API integration)
// For now, we'll save to localStorage as a fallback
const profileKey = `informup_profile_${this.currentUser.email}`;
localStorage.setItem(
profileKey,
JSON.stringify(this.userProfile),
);
this.debug("👤 Profile saved successfully:", {
profileField,
selectedValue,
});
} catch (error) {
this.debug(
"👤 Error saving to profile:",
error.message,
"error",
);
}
},
getProfileValue(questionId) {
if (
!USER_PROFILE_CONFIG.enabled ||
!USER_PROFILE_CONFIG.prepopulateFromProfile
) {
return null;
}
if (!this.userProfile) {
return null;
}
const profileField =
USER_PROFILE_CONFIG.profileFieldMapping[questionId];
if (!profileField) {
return null;
}
const value = this.userProfile[profileField];
this.debug("👤 Retrieved profile value:", {
questionId,
profileField,
value,
});
return value;
},
async initializeAndRenderQuestion(
containerId,
questionIndex,
config = {},
) {
this.debug(
`Initializing question ${questionIndex} in container ${containerId}`,
);
try {
// Load Formbricks library
await this.loadFormbricksJS();
// Load user profile (parallel with survey fetch)
const [survey, userProfile] = await Promise.all([
this.fetchSurvey(),
this.loadUserProfile(),
]);
// Create and render question instance
const instance = new FormbricksSurveyInstance(
containerId,
questionIndex,
survey,
config,
);
this.instances.set(containerId, instance);
await instance.render();
this.debug(`✅ Successfully initialized ${containerId}`);
} catch (error) {
this.debug(
`❌ Failed to initialize ${containerId}:`,
error.message,
"error",
);
this.showError(containerId, error.message);
}
},
async renderQuestion(containerId, questionIndex, config = {}) {
this.debug(
`Rendering question ${questionIndex} in container ${containerId}`,
);
if (!this.survey) {
throw new Error(
"Survey not initialized. Use initializeAndRenderQuestion for the first block.",
);
}
if (!this.formbricksLoaded) {
throw new Error(
"Formbricks library not loaded. Use initializeAndRenderQuestion for the first block.",
);
}
try {
const instance = new FormbricksSurveyInstance(
containerId,
questionIndex,
this.survey,
config,
);
this.instances.set(containerId, instance);
await instance.render();
this.debug(`✅ Successfully rendered ${containerId}`);
} catch (error) {
this.debug(
`❌ Failed to render ${containerId}:`,
error.message,
"error",
);
this.showError(containerId, error.message);
}
},
async renderSequentialQuestions(
containerId,
startQuestionIndex,
config = {},
) {
this.debug(
`Rendering sequential questions starting from ${startQuestionIndex} in container ${containerId}`,
);
if (!this.survey) {
throw new Error(
"Survey not initialized. Use initializeAndRenderQuestion for the first block.",
);
}
if (!this.formbricksLoaded) {
throw new Error(
"Formbricks library not loaded. Use initializeAndRenderQuestion for the first block.",
);
}
try {
const instance = new FormbricksSequentialSurveyInstance(
containerId,
startQuestionIndex,
this.survey,
config,
);
this.instances.set(containerId, instance);
await instance.render();
this.debug(
`✅ Successfully rendered sequential ${containerId}`,
);
} catch (error) {
this.debug(
`❌ Failed to render sequential ${containerId}:`,
error.message,
"error",
);
this.showError(containerId, error.message);
}
},
showError(containerId, message) {
const container = document.getElementById(containerId);
if (container) {
container.innerHTML = `
<div style="background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; padding: 16px; border-radius: 8px; margin: 20px 0;">
<strong>Error:</strong> ${message}
</div>
`;
}
},
};
// Survey Instance Class
class FormbricksSurveyInstance {
constructor(containerId, questionIndex, survey, config = {}) {
this.containerId = containerId;
this.embedId = `${containerId}-embed`;
this.questionIndex = questionIndex;
this.survey = survey;
this.originalSurvey = JSON.parse(JSON.stringify(survey)); // Keep original for re-translation
this.config = {
hideNextButton: true,
autoSave: true,
autoSaveDelay: 1000,
...config,
};
this.responseData = {};
this.saveTimeout = null;
this.cleanupIntervals = [];
this.isSaving = false;
this.languageCache = new Map(); // Cache rendered HTML by language
}
debug(message, data = null, level = "info") {
if (!SURVEY_CONFIG.debug) return;
console.log(
`[${this.containerId}][${level.toUpperCase()}]`,
message,
data,
);
}
translateText(textObj, language) {
if (typeof textObj === "string") return textObj;
if (typeof textObj === "object" && textObj !== null) {
// Try in order: language -> default -> en -> first available
return (
textObj[language] ||
textObj.default ||
textObj.en ||
Object.values(textObj)[0] ||
""
);
}
return textObj || "";
}
translateSurvey(survey, language) {
// Create a deep copy and translate all text fields
const translated = JSON.parse(JSON.stringify(survey));
// Translate questions - keep as objects with .default property for Formbricks
if (translated.questions) {
translated.questions = translated.questions.map(q => {
const newQuestion = { ...q };
// Translate headline - keep as object but update .default
if (q.headline) {
newQuestion.headline = {
default: this.translateText(q.headline, language)
};
}
// Translate subheader
if (q.subheader) {
newQuestion.subheader = {
default: this.translateText(q.subheader, language)
};
}
// Translate choice labels
if (q.choices) {
newQuestion.choices = q.choices.map(choice => ({
...choice,
label: {
default: this.translateText(choice.label, language)
}
}));
}
return newQuestion;
});
}
return translated;
}
prepareSingleQuestion() {
if (
!this.originalSurvey.questions ||
this.originalSurvey.questions.length <= this.questionIndex
) {
throw new Error(
`Question index ${this.questionIndex} not found in survey`,
);
}
const question = this.originalSurvey.questions[this.questionIndex];
this.debug(
"Creating single question survey for question type:",
question.type,
);
const singleQuestionSurvey = {
...this.originalSurvey,
questions: [question],
welcomeCard: { enabled: false },
thankyouCard: { enabled: false },
};
// Translate survey to current language from original data
const language = window.FormbricksSurveyManager.currentLanguage || "en";
this.debug(`🌍 Translating from original survey to language: ${language}`);
return this.translateSurvey(singleQuestionSurvey, language);
}
async render() {
this.debug("Attempting to render survey");
const container = document.getElementById(this.containerId);
container.innerHTML = `<div id="${this.embedId}" class="survey-embed"></div>`;
const singleQuestionSurvey = this.prepareSingleQuestion();
const question = singleQuestionSurvey.questions[0];
// Branch rendering based on question type and configuration
if (this.shouldRenderAsDropdown(question)) {
this.debug("Rendering as custom dropdown");
await this.renderCustomDropdown(question);
} else {
this.debug("Rendering with native Formbricks JS");
await this.renderWithFormbricks(singleQuestionSurvey);
}
}
async renderWithFormbricks(singleQuestionSurvey) {
if (
typeof window.formbricksSurveys === "undefined" ||
!window.formbricksSurveys.renderSurveyInline
) {
throw new Error("Formbricks survey library not available");
}
const surveyProps = {
survey: singleQuestionSurvey,
containerId: this.embedId,
mode: "inline",
languageCode: "default", // Always use default - we translate the survey ourselves
styling: this.survey.styling || {},
onDisplay: () => {
this.debug("🎯 Survey displayed");
// Cache the rendered HTML for current language
setTimeout(() => {
const embedContainer = document.getElementById(this.embedId);
const currentLang = window.FormbricksSurveyManager.currentLanguage;
if (embedContainer && !this.languageCache.has(currentLang)) {
this.languageCache.set(currentLang, {
html: embedContainer.innerHTML,
timestamp: Date.now()
});
console.log(`💾 [${this.containerId}] Cached HTML for language: ${currentLang}`);
}
// Make container visible now that content is rendered
const container = document.getElementById(this.containerId);
if (container) {
container.classList.add("survey-loaded");
this.debug("✅ Made survey visible after render");
}
}, 50);
// Set up language change detection
this.setupLanguageChangeDetection();
this.setupAutoSave();
this.setupPeriodicSave();
},
onResponse: (response) => {
this.debug("📝 Formbricks onResponse received", response);
if (response.data) {
// Merge with existing response data, preserving what we've collected manually
this.responseData = {
...this.responseData,
...response.data,
};
this.debug(
"💾 Merged responseData with Formbricks data:",
this.responseData,
);
if (this.config.autoSave) {
clearTimeout(this.saveTimeout);
this.saveResponse("native-response");
}
}
},
onFinished: () => {
this.debug("🎉 Survey finished");
this.showSuccess("Thank you for your response!");
},
onClose: () => this.debug("Survey closed"),
onError: (error) => {
this.debug("Survey error", error, "error");
this.showError(
"An error occurred while displaying the survey",
);
},
};
try {
window.formbricksSurveys.renderSurveyInline(surveyProps);
this.debug("Successfully called renderSurveyInline");
// Make visible immediately in case onDisplay doesn't fire due to Formbricks errors
setTimeout(() => {
const container = document.getElementById(this.containerId);
if (container && !container.classList.contains('survey-loaded')) {
container.classList.add("survey-loaded");
this.debug("✅ Force-made survey visible (fallback)");
}
this.setupUICleanup();
}, 500);
} catch (error) {
// Make visible even on error
const container = document.getElementById(this.containerId);
if (container) {
container.classList.add("survey-loaded");
}
this.debug(
"Error calling renderSurveyInline",
error.message,
"error",
);
throw new Error("Failed to render survey");
}
}
setupUICleanup() {
// Prevent auto-focus
this.preventAutoFocus();
this.hideOptionalRequiredText();
this.fixSurveyContainerHeight();
if (this.config.hideNextButton) {
this.hideNextButtons();
}
const cleanupInterval = setInterval(() => {
this.hideOptionalRequiredText();
this.fixSurveyContainerHeight();
if (this.config.hideNextButton) {
this.hideNextButtons();
}
}, 3000);
this.cleanupIntervals.push(cleanupInterval);
setTimeout(() => clearInterval(cleanupInterval), 60000);
}
fixSurveyContainerHeight() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
// Remove height constraints from all survey containers
const selectors = [
this.embedId,
".fb-survey",
".fb-survey-inline",
".fb-survey-content",
".fb-card",
".fb-question-container",
'[class*="fb-inline"]',
'[class*="fb-survey"]',
];
selectors.forEach((selector) => {
const elements = selector.startsWith("#")
? [document.getElementById(selector.slice(1))]
: embedContainer.querySelectorAll(selector);
if (elements) {
elements.forEach((el) => {
if (el && el.style) {
el.style.height = "auto";
el.style.minHeight = "auto";
el.style.maxHeight = "none";
el.style.overflow = "visible";
}
});
}
});
this.debug("📏 Fixed survey container heights to auto-expand");
}
hideOptionalRequiredText() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const walker = document.createTreeWalker(
embedContainer,
NodeFilter.SHOW_TEXT,
null,
false,
);
const textNodes = [];
let node;
while ((node = walker.nextNode())) {
const text = node.textContent.toLowerCase().trim();
if (
text.includes("optional") ||
text.includes("required") ||
text === "*"
) {
textNodes.push(node);
}
}
textNodes.forEach((textNode) => {
const parent = textNode.parentElement;
if (parent && parent.textContent.trim().length < 20) {
parent.style.display = "none";
this.debug(
"🧹 Hidden optional/required text:",
textNode.textContent.trim(),
);
}
});
}
hideNextButtons() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
setTimeout(() => {
const allButtons = embedContainer.querySelectorAll("button");
allButtons.forEach((button) => {
const buttonText = button.textContent.toLowerCase().trim();
if (
buttonText.includes("next") ||
buttonText.includes("submit") ||
buttonText.includes("continue") ||
(buttonText === "" && button.type === "submit")
) {
button.style.display = "none";
button.style.visibility = "hidden";
this.debug(
`✓ Hidden button: "${button.textContent.trim()}"`,
);
}
});
}, 1000);
}
setupAutoSave() {
if (!this.config.autoSave) return;
this.debug("🔧 Setting up auto-save handlers");
setTimeout(() => {
this.setupTextAutoSave();
this.setupChoiceAutoSave();
this.setupNPSAutoSave();
this.setupRadioCheckboxAutoSave();
this.setupSelectAutoSave();
this.setupUniversalClickHandler();
}, 500);
}
setupTextAutoSave() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const textInputs = embedContainer.querySelectorAll(
'textarea, input[type="text"], input:not([type])',
);
this.debug(
`📝 Setting up text auto-save for ${textInputs.length} inputs`,
);
textInputs.forEach((input) => {
// Auto-save on input with delay
input.addEventListener("input", () => {
clearTimeout(this.saveTimeout);
if (input.value.trim()) {
const questionId =
this.findQuestionId(input) ||
this.survey.questions[this.questionIndex]?.id ||
"text-response";
this.responseData[questionId] = input.value;
this.debug(`💾 Auto-saving text: "${input.value}"`);
this.saveTimeout = setTimeout(() => {
this.saveResponse("auto-text");
}, this.config.autoSaveDelay);
}
});
// Immediate save on blur (when user clicks away)
input.addEventListener("blur", () => {
if (input.value.trim()) {
const questionId =
this.findQuestionId(input) ||
this.survey.questions[this.questionIndex]?.id ||
"text-response";
this.responseData[questionId] = input.value;
this.debug(`💾 Immediate save on blur: "${input.value}"`);
clearTimeout(this.saveTimeout);
this.saveResponse("blur-text");
}
});
// Save on Enter key
input.addEventListener("keypress", (e) => {
if (e.key === "Enter" && input.value.trim()) {
const questionId =
this.findQuestionId(input) ||
this.survey.questions[this.questionIndex]?.id ||
"text-response";
this.responseData[questionId] = input.value;
this.debug(`💾 Save on Enter: "${input.value}"`);
clearTimeout(this.saveTimeout);
this.saveResponse("enter-text");
}
});
});
}
setupChoiceAutoSave() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
// Set up click handlers for choice elements
const setupClickHandlers = () => {
const choices = embedContainer.querySelectorAll(
'button, div[role="button"], [data-value], .choice-option, [class*="choice"], [class*="option"]',
);
this.debug(
`🎯 Setting up click handlers for ${choices.length} choice elements`,
);
choices.forEach((choice) => {
if (!choice.hasAttribute("data-auto-save-attached")) {
choice.setAttribute("data-auto-save-attached", "true");
choice.addEventListener("click", () => {
// Small delay to allow Formbricks to update selection state
setTimeout(() => {
const choiceValue =
choice.getAttribute("data-value") ||
choice.getAttribute("value") ||
choice.textContent.trim();
// Skip language selector options
const choiceClass = choice.className || "";
const choiceAria = choice.getAttribute("aria-label") || "";
const isLanguage =
choiceClass.toLowerCase().includes("language") ||
choiceClass.toLowerCase().includes("lang") ||
choiceAria.toLowerCase().includes("language") ||
choiceValue === "English" ||
choiceValue === "Spanish; Castilian" ||
choiceValue === "Spanish" ||
choiceValue === "Español";
if (isLanguage) {
this.debug(`🚫 Skipping language option in choice handler: "${choiceValue}"`);
return;
}
if (choiceValue) {
const questionId =
this.findQuestionId(choice) ||
this.survey.questions[this.questionIndex]?.id ||
"choice-response";
this.responseData[questionId] = choiceValue;
this.debug(`🎯 Choice clicked: "${choiceValue}"`);
// Immediate save on choice selection
clearTimeout(this.saveTimeout);
this.saveResponse("click-choice");
}
}, 100);
});
}
});
};
// Initial setup
setTimeout(setupClickHandlers, 500);
// Observer for dynamically added choices
const observer = new MutationObserver((mutations) => {
let shouldSetupHandlers = false;
mutations.forEach((mutation) => {
if (
mutation.type === "childList" &&
mutation.addedNodes.length > 0
) {
shouldSetupHandlers = true;
}
// Also watch for selection changes
if (
mutation.type === "attributes" &&
(mutation.attributeName === "data-selected" ||
mutation.attributeName === "aria-checked" ||
mutation.attributeName === "class")
) {
const element = mutation.target;
const isSelected =
element.getAttribute("data-selected") === "true" ||
element.getAttribute("aria-checked") === "true" ||
element.classList.contains("selected");
if (isSelected) {
const choiceValue =
element.getAttribute("data-value") ||
element.getAttribute("value") ||
element.textContent.trim();
if (choiceValue) {
const questionId =
this.findQuestionId(element) ||
this.survey.questions[this.questionIndex]?.id ||
"choice-response";
this.responseData[questionId] = choiceValue;
this.debug(
`🎯 Choice selected via attribute: "${choiceValue}"`,
);
clearTimeout(this.saveTimeout);
this.saveResponse("attr-choice");
}
}
}
});
if (shouldSetupHandlers) {
setTimeout(setupClickHandlers, 100);
}
});
observer.observe(embedContainer, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["data-selected", "aria-checked", "class"],
});
this.cleanupIntervals.push({ type: "observer", observer });
}
setupNPSAutoSave() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
// Set up click handlers for NPS buttons
const setupNPSClickHandlers = () => {
const npsButtons = embedContainer.querySelectorAll(
'button[class*="nps"], [role="button"][class*="nps"], button[data-value], .nps-option, [class*="rating"] button',
);
this.debug(
`📊 Setting up NPS click handlers for ${npsButtons.length} buttons`,
);
npsButtons.forEach((button) => {
if (!button.hasAttribute("data-nps-save-attached")) {
button.setAttribute("data-nps-save-attached", "true");
button.addEventListener("click", () => {
setTimeout(() => {
const npsValue = parseInt(
button.getAttribute("data-value") ||
button.textContent.trim(),
);
if (
!isNaN(npsValue) &&
npsValue >= 0 &&
npsValue <= 10
) {
const questionId =
this.findQuestionId(button) ||
this.survey.questions[this.questionIndex]?.id ||
"nps-response";
this.responseData[questionId] = npsValue;
this.debug(`📊 NPS clicked: ${npsValue}`);
// Immediate save on NPS selection
clearTimeout(this.saveTimeout);
this.saveResponse("click-nps");
}
}, 100);
});
}
});
};
// Initial setup
setTimeout(setupNPSClickHandlers, 500);
// Observer for dynamically added NPS buttons and selection changes
const observer = new MutationObserver((mutations) => {
let shouldSetupHandlers = false;
mutations.forEach((mutation) => {
if (
mutation.type === "childList" &&
mutation.addedNodes.length > 0
) {
shouldSetupHandlers = true;
}
// Watch for selection changes on NPS buttons
if (
mutation.type === "attributes" &&
(mutation.attributeName === "data-selected" ||
mutation.attributeName === "aria-checked" ||
mutation.attributeName === "class")
) {
const element = mutation.target;
const isSelected =
element.getAttribute("data-selected") === "true" ||
element.getAttribute("aria-checked") === "true" ||
element.classList.contains("selected");
if (isSelected) {
const npsValue = parseInt(
element.getAttribute("data-value") ||
element.textContent.trim(),
);
if (!isNaN(npsValue) && npsValue >= 0 && npsValue <= 10) {
const questionId =
this.findQuestionId(element) ||
this.survey.questions[this.questionIndex]?.id ||
"nps-response";
this.responseData[questionId] = npsValue;
this.debug(
`📊 NPS selected via attribute: ${npsValue}`,
);
clearTimeout(this.saveTimeout);
this.saveResponse("attr-nps");
}
}
}
});
if (shouldSetupHandlers) {
setTimeout(setupNPSClickHandlers, 100);
}
});
observer.observe(embedContainer, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["data-selected", "aria-checked", "class"],
});
this.cleanupIntervals.push({ type: "observer", observer });
}
setupRadioCheckboxAutoSave() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const radioCheckboxes = embedContainer.querySelectorAll(
'input[type="radio"], input[type="checkbox"]',
);
this.debug(
`📋 Setting up radio/checkbox auto-save for ${radioCheckboxes.length} inputs`,
);
radioCheckboxes.forEach((input) => {
input.addEventListener("change", () => {
const questionId =
this.findQuestionId(input) ||
this.survey.questions[this.questionIndex]?.id ||
"radio-checkbox-response";
if (input.type === "radio") {
this.responseData[questionId] = input.value;
this.debug(`📋 Radio selected: "${input.value}"`);
} else if (input.type === "checkbox") {
if (!this.responseData[questionId])
this.responseData[questionId] = [];
if (input.checked) {
if (
!this.responseData[questionId].includes(input.value)
) {
this.responseData[questionId].push(input.value);
}
} else {
this.responseData[questionId] = this.responseData[
questionId
].filter((v) => v !== input.value);
}
this.debug(
`📋 Checkbox updated: "${input.value}" = ${input.checked}`,
);
}
clearTimeout(this.saveTimeout);
this.saveResponse("change-radio-checkbox");
});
});
}
setupSelectAutoSave() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const selects = embedContainer.querySelectorAll("select");
this.debug(
`📋 Setting up select auto-save for ${selects.length} dropdowns`,
);
selects.forEach((select) => {
select.addEventListener("change", () => {
const questionId =
this.findQuestionId(select) ||
this.survey.questions[this.questionIndex]?.id ||
"select-response";
this.responseData[questionId] = select.value;
this.debug(`📋 Select changed: "${select.value}"`);
clearTimeout(this.saveTimeout);
this.saveResponse("change-select");
});
});
}
setupUniversalClickHandler() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
this.debug("🌐 Setting up universal click handler");
embedContainer.addEventListener("click", (e) => {
const target = e.target;
// Skip if it's already handled by specific handlers
if (
target.hasAttribute("data-auto-save-attached") ||
target.hasAttribute("data-nps-save-attached")
) {
return;
}
// Skip navigation/action buttons and language selectors
const value =
target.getAttribute("data-value") ||
target.getAttribute("value") ||
target.textContent.trim();
const targetClass = target.className || "";
const targetAria = target.getAttribute("aria-label") || "";
// Check if this is a language selector
const isLanguageSelector =
targetClass.toLowerCase().includes("language") ||
targetClass.toLowerCase().includes("lang") ||
targetAria.toLowerCase().includes("language") ||
targetAria.toLowerCase().includes("lang") ||
value === "English" ||
value === "Spanish; Castilian" ||
value === "Spanish" ||
value === "Español" ||
value === "French" ||
value === "Français" ||
value === "German" ||
value === "Deutsch";
if (isLanguageSelector) {
this.debug(`🚫 Skipping language selector: "${value}"`);
return;
}
const isNavigationButton =
value &&
(value.toLowerCase() === "next" ||
value.toLowerCase() === "submit" ||
value.toLowerCase() === "continue" ||
value.toLowerCase() === "back" ||
value.toLowerCase() === "previous" ||
target.type === "submit" ||
target.classList.contains("submit-button") ||
target.classList.contains("next-button") ||
target.classList.contains("continue-button"));
if (isNavigationButton) {
this.debug(`🚫 Skipping navigation button: "${value}"`);
return;
}
// Handle clicks on interactive elements that might not be caught otherwise
if (
target.tagName === "BUTTON" ||
target.tagName === "A" ||
target.getAttribute("role") === "button" ||
target.getAttribute("role") === "option" ||
target.classList.contains("clickable") ||
target.classList.contains("option") ||
target.classList.contains("choice")
) {
setTimeout(() => {
if (value) {
const questionId =
this.findQuestionId(target) ||
this.survey.questions[this.questionIndex]?.id ||
"universal-response";
this.responseData[questionId] = value;
this.debug(`🌐 Universal click captured: "${value}"`);
clearTimeout(this.saveTimeout);
this.saveResponse("universal-click");
}
}, 150);
}
});
}
setupPeriodicSave() {
this.debug("⏰ Setting up periodic backup save");
const periodicSaveInterval = setInterval(() => {
if (Object.keys(this.responseData).length > 0) {
this.debug("⏰ Periodic backup save triggered");
this.saveResponse("periodic-backup");
}
}, 30000); // Save every 30 seconds if there's data
this.cleanupIntervals.push(periodicSaveInterval);
}
findQuestionId(element) {
let current = element;
while (
current &&
current !== document.getElementById(this.embedId)
) {
if (
current.getAttribute &&
current.getAttribute("data-question-id")
) {
return current.getAttribute("data-question-id");
}
current = current.parentElement;
}
return null;
}
async saveResponse(trigger = "background") {
console.log(`🚀 [${this.containerId}] SAVE ATTEMPT: ${trigger}`);
console.log(
`📊 [${this.containerId}] ResponseData:`,
this.responseData,
);
console.log(
`🆔 [${this.containerId}] Shared Response ID:`,
window.FormbricksSurveyManager.sharedResponseId,
);
if (Object.keys(this.responseData).length === 0) {
this.debug("⚠️ No response data to save");
return;
}
if (this.isSaving) {
this.debug("⏳ Save already in progress, skipping");
return;
}
this.isSaving = true;
try {
// Check if user is signed in
const isSignedIn = await this.checkUserSignedIn();
console.log("🔐 User signed in status:", isSignedIn);
// Get referrer URL (fallback to "direct" if empty)
const referrerUrl = document.referrer || "direct";
// Log to help debug
console.log("🔍 Hidden fields being added:", {
url: window.location.href,
signed_in: isSignedIn ? "1" : "0",
referrer: referrerUrl,
});
// Revert to the working approach: put hidden fields in data object like the URL was working
const dataWithHiddenFields = {
...this.responseData,
url: window.location.href,
signed_in: isSignedIn ? "1" : "0", // String like URL
referrer: referrerUrl, // Add referrer URL
language: window.FormbricksSurveyManager.currentLanguage || "en", // Add current language
};
console.log(
"📦 Data being sent to Formbricks:",
dataWithHiddenFields,
);
console.log("🌐 Current URL:", window.location.href);
console.log(
"🔑 Signed in value being sent:",
dataWithHiddenFields.signed_in,
);
console.log("🔗 Referrer being sent:", referrerUrl);
const requestData = {
data: dataWithHiddenFields,
finished: false,
meta: {
trigger: trigger,
timestamp: new Date().toISOString(),
containerId: this.containerId,
questionIndex: this.questionIndex,
pageUrl: window.location.href,
},
};
console.log(
"📤 Data with hidden fields:",
dataWithHiddenFields,
);
let response;
const sharedResponseId =
window.FormbricksSurveyManager.sharedResponseId;
if (sharedResponseId) {
// Update existing shared response
response = await fetch(
`${SURVEY_CONFIG.formbricksBaseUrl}/api/v1/client/${SURVEY_CONFIG.environmentId}/responses/${sharedResponseId}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestData),
},
);
} else {
// Create new shared response
response = await fetch(
`${SURVEY_CONFIG.formbricksBaseUrl}/api/v1/client/${SURVEY_CONFIG.environmentId}/responses`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
surveyId: this.survey.id,
...requestData,
}),
},
);
}
if (response.ok) {
const result = await response.json();
console.log(`✅ [${this.containerId}] SAVE SUCCESSFUL`);
console.log(`📋 [${this.containerId}] API Response:`, result);
// Store shared response ID if this was the first save
if (!sharedResponseId) {
const responseId =
result.data?.id ||
result.id ||
result.responseId ||
result.data?.responseId;
if (responseId) {
window.FormbricksSurveyManager.sharedResponseId =
responseId;
console.log(
`🆔 [${this.containerId}] Created shared response ID: ${responseId}`,
);
this.debug(
`🆔 Created shared response ID: ${responseId}`,
);
} else {
console.warn(
`⚠️ [${this.containerId}] Could not extract response ID from:`,
result,
);
}
} else {
this.debug(
`🔄 Updated shared response: ${sharedResponseId}`,
);
}
} else {
const errorText = await response.text();
console.error(
`❌ [${this.containerId}] SAVE FAILED: ${response.status}`,
errorText,
);
}
} catch (error) {
console.error(
`💥 [${this.containerId}] SAVE EXCEPTION:`,
error,
);
} finally {
this.isSaving = false;
}
}
showSuccess(message) {
const container = document.getElementById(this.containerId);
if (container) {
container.innerHTML = `
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; color: #059669; padding: 16px; border-radius: 8px; margin: 20px 0;">
${message}
</div>
`;
}
}
showError(message) {
const container = document.getElementById(this.containerId);
if (container) {
container.innerHTML = `
<div style="background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; padding: 16px; border-radius: 8px; margin: 20px 0;">
<strong>Error:</strong> ${message}
</div>
`;
}
}
async checkUserSignedIn() {
try {
// Check multiple methods to detect if user is signed in
// Method 1: Check for member_status in URL (most reliable)
// ANY member_status parameter means user is logged in (free, paid, complimentary, etc.)
const urlParams = new URLSearchParams(window.location.search);
const memberStatus = urlParams.get("member_status");
if (memberStatus) {
this.debug(
`👤 User IS signed in (member_status: ${memberStatus})`,
);
return true;
}
// Method 2: Check Ghost members API
if (
typeof window.ghost !== "undefined" &&
window.ghost.members
) {
try {
const member =
await window.ghost.members.getCurrentMember();
if (member) {
this.debug(
"👤 User is signed in (via Ghost members API)",
);
return true;
}
} catch (e) {
// Continue to next method
}
}
// Method 3: Check Ghost session API
try {
const response = await fetch("/members/api/session/", {
method: "GET",
credentials: "include",
});
if (response.ok) {
const data = await response.json();
if (data && data.email) {
this.debug("👤 User is signed in (via session API)");
return true;
}
}
} catch (e) {
// Continue to next method
}
this.debug("👤 User is NOT signed in");
return false;
} catch (error) {
this.debug("⚠️ Error checking sign-in status:", error.message);
return false;
}
}
preventAutoFocus() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
// Temporarily disable focus on all focusable elements
const focusableElements = embedContainer.querySelectorAll(
"input, textarea, button, select, [tabindex]",
);
focusableElements.forEach((element) => {
const originalTabIndex = element.tabIndex;
element.tabIndex = -1;
element.setAttribute(
"data-original-tabindex",
originalTabIndex,
);
// Restore after a delay
setTimeout(() => {
const savedTabIndex = element.getAttribute(
"data-original-tabindex",
);
element.tabIndex =
savedTabIndex === "null" ? 0 : parseInt(savedTabIndex);
element.removeAttribute("data-original-tabindex");
}, 1000);
});
}
setupLanguageChangeDetection() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
this.debug("🌍 Setting up language change detection");
// Map language names to codes
const languageMap = {
'english': 'en',
'spanish': 'es',
'spanish; castilian': 'es',
'español': 'es',
'french': 'fr',
'français': 'fr',
'german': 'de',
'deutsch': 'de',
'portuguese': 'pt',
'português': 'pt',
'italian': 'it',
'italiano': 'it',
'chinese': 'zh',
'中文': 'zh',
'japanese': 'ja',
'日本語': 'ja'
};
// Listen for ALL button clicks
embedContainer.addEventListener('click', (e) => {
const target = e.target.closest('button');
if (!target) return;
const buttonClass = target.className || "";
const buttonAria = target.getAttribute('aria-label') || "";
const buttonText = target.textContent.trim();
// Detect if this is a language-related click
const isLanguageContext =
buttonAria.toLowerCase().includes('language') ||
buttonAria.toLowerCase().includes('lang') ||
buttonClass.toLowerCase().includes('language') ||
buttonClass.toLowerCase().includes('lang');
// Or if the button text matches a language name
const textLower = buttonText.toLowerCase();
const matchesLanguage = languageMap.hasOwnProperty(textLower);
if (isLanguageContext || matchesLanguage) {
console.log('🌍 LANGUAGE-RELATED CLICK:', buttonText || buttonAria);
setTimeout(() => {
// Map language name to code
const langCode = languageMap[textLower] ||
target.getAttribute('data-language') ||
target.getAttribute('lang') ||
target.getAttribute('value');
if (langCode && langCode !== window.FormbricksSurveyManager.currentLanguage) {
console.log(`🌍 LANGUAGE CHANGE: ${window.FormbricksSurveyManager.currentLanguage} → ${langCode}`);
window.FormbricksSurveyManager.setLanguage(langCode);
}
}, 300);
}
});
}
detectCurrentLanguage() {
// Try to detect the currently selected language from Formbricks UI
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return null;
// Look for selected language indicators
const selectedLangElement = embedContainer.querySelector(
'[data-language][data-selected="true"], [class*="language"][class*="selected"], button[aria-pressed="true"][class*="language"]'
);
if (selectedLangElement) {
const lang = selectedLangElement.getAttribute('data-language') ||
selectedLangElement.getAttribute('lang') ||
selectedLangElement.textContent.trim().toLowerCase();
this.debug(`🔍 Detected language from UI: ${lang}`);
return lang;
}
return null;
}
reRenderWithLanguage(newLanguage) {
console.log(`🔄 [${this.containerId}] reRenderWithLanguage called with: ${newLanguage}`);
this.debug(`🔄 Updating text content with language: ${newLanguage}`);
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) {
console.error(`❌ Embed container not found: ${this.embedId}`);
return;
}
// Get the question data from original survey
const question = this.originalSurvey.questions[this.questionIndex];
// Update headline text - it's inside a <p class="fb-text-base fb-font-semibold">
const headline = embedContainer.querySelector('p.fb-text-base.fb-font-semibold');
if (headline && question.headline) {
const translatedHeadline = this.translateText(question.headline, newLanguage);
headline.textContent = translatedHeadline;
console.log(`📝 [${this.containerId}] Updated headline to: ${translatedHeadline.substring(0, 50)}...`);
} else {
console.warn(`⚠️ [${this.containerId}] Could not find headline element`);
}
// Update subheader if exists
const subheader = embedContainer.querySelector('[class*="subheader"], .fb-question-subheader');
if (subheader && question.subheader) {
const translatedSubheader = this.translateText(question.subheader, newLanguage);
subheader.textContent = translatedSubheader;
console.log(`📝 [${this.containerId}] Updated subheader`);
}
// Update choice labels - these are inside <span class="fb-flex fb-items-center fb-text-sm">
if (question.choices && question.choices.length > 0) {
const choiceSpans = embedContainer.querySelectorAll('span.fb-flex.fb-items-center.fb-text-sm');
console.log(`🔍 [${this.containerId}] Found ${choiceSpans.length} choice spans for ${question.choices.length} choices`);
question.choices.forEach((choice, index) => {
if (choiceSpans[index]) {
const translatedLabel = this.translateText(choice.label, newLanguage);
choiceSpans[index].textContent = translatedLabel;
console.log(`📝 [${this.containerId}] Updated choice ${index}: ${translatedLabel.substring(0, 30)}...`);
}
});
}
console.log(`✅ [${this.containerId}] Language updated without re-render!`);
}
shouldRenderAsDropdown(question) {
this.debug(
"🔍 Checking if question should render as dropdown...",
);
this.debug(
"Config useSingleSelectDropdown:",
this.config.useSingleSelectDropdown,
);
this.debug("Question object:", question);
if (!this.config.useSingleSelectDropdown) {
this.debug("❌ Dropdown disabled in config");
return false;
}
if (!question || !question.type) {
this.debug("❌ No question or question type found");
return false;
}
// Check for single-select question types that should be converted to dropdowns
const singleSelectTypes = [
"multiplechoicesingle", // Fixed: all lowercase to match normalized type
"multiplechoicemulti", // Fixed: all lowercase to match normalized type
"singleselect", // Fixed: all lowercase to match normalized type
"radio",
"choice",
"select",
];
const questionType = question.type.toLowerCase();
const isSingleSelect = singleSelectTypes.includes(questionType);
const hasChoices =
question.choices && question.choices.length > 0;
this.debug(`📋 Question analysis:`);
this.debug(
` - Type: "${question.type}" (normalized: "${questionType}")`,
);
this.debug(` - Is single select: ${isSingleSelect}`);
this.debug(` - Has choices: ${hasChoices}`);
this.debug(
` - Choices count: ${question.choices ? question.choices.length : 0}`,
);
if (question.choices) {
this.debug(` - Choices:`, question.choices);
}
const shouldRender = isSingleSelect && hasChoices;
this.debug(
`🎯 Final decision: ${shouldRender ? "RENDER AS DROPDOWN" : "RENDER WITH FORMBRICKS"}`,
);
return shouldRender;
}
async renderCustomDropdown(question) {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) {
throw new Error("Embed container not found");
}
// Helper function to extract text from object or return string
const extractText = (textObj) => {
if (typeof textObj === "string") return textObj;
if (typeof textObj === "object" && textObj !== null) {
const lang = SURVEY_CONFIG.defaultLanguage || "en";
// Try in order: configured language -> default -> en -> english -> first available
return (
textObj[lang] ||
textObj.default ||
textObj.en ||
textObj.english ||
Object.values(textObj)[0] ||
"[No text available]"
);
}
return textObj || "";
};
const questionText = extractText(
question.headline || question.text,
);
const questionSubtext = question.subheader
? extractText(question.subheader)
: "";
// Check for pre-existing value from user profile
const profileValue =
window.FormbricksSurveyManager.getProfileValue(question.id);
this.debug("🎨 Rendering dropdown with extracted text:", {
questionText,
questionSubtext,
choicesCount: question.choices.length,
profileValue,
});
// Create Formbricks-style HTML structure
const html = `
<div class="fb-survey fb-survey-inline" style="--fb-brand-color: #00C4B8;">
<div class="fb-survey-content">
<div class="fb-question-container">
<div class="fb-question-text">
<h3 class="fb-question-headline">${questionText || "Please select an option"}</h3>
${questionSubtext ? `<p class="fb-question-subheader">${questionSubtext}</p>` : ""}
</div>
<div class="fb-question-form">
<div class="fb-form-field">
<select
id="custom-dropdown-${question.id}"
class="fb-select fb-form-control"
data-question-id="${question.id}"
${question.required !== false ? "required" : ""}
>
<option value="" disabled ${profileValue ? "" : "selected"}>
${this.config.dropdownPlaceholder}
</option>
${question.choices
.map((choice) => {
const choiceValue =
choice.id ||
choice.value ||
extractText(choice.label);
const choiceLabel = extractText(
choice.label || choice.value || choice,
);
const isSelected =
profileValue && profileValue === choiceValue;
this.debug("Choice mapping:", {
choiceValue,
choiceLabel,
isSelected,
originalChoice: choice,
});
return `<option value="${choiceValue}" ${isSelected ? "selected" : ""}>${choiceLabel}</option>`;
})
.join("")}
</select>
</div>
</div>
</div>
</div>
</div>
`;
embedContainer.innerHTML = html;
// Pre-populate response data if profile value exists
if (profileValue) {
this.responseData[question.id] = profileValue;
this.debug("👤 Pre-populated response from profile:", {
questionId: question.id,
value: profileValue,
});
}
// Set up event handling
this.setupCustomDropdownEvents(question);
// Call the same setup as regular questions
setTimeout(() => this.setupUICleanup(), 100);
this.debug("Custom dropdown rendered successfully");
}
setupCustomDropdownEvents(question) {
const dropdown = document.getElementById(
`custom-dropdown-${question.id}`,
);
if (!dropdown) return;
dropdown.addEventListener("change", (event) => {
const selectedValue = event.target.value;
this.debug(`Dropdown selection: ${selectedValue}`);
// Store response data
this.responseData[question.id] = selectedValue;
// Save to user profile if enabled
if (window.FormbricksSurveyManager) {
window.FormbricksSurveyManager.saveUserProfile(
question.id,
selectedValue,
question,
);
}
// Trigger auto-save if enabled
if (this.config.autoSave) {
clearTimeout(this.saveTimeout);
this.saveTimeout = setTimeout(() => {
this.saveResponse("dropdown-change");
}, this.config.autoSaveDelay);
}
// Find the choice object for the selected value
const selectedChoice = question.choices.find(
(choice) =>
(choice.id || choice.value || choice.label) ===
selectedValue,
);
this.debug("Response data updated:", this.responseData);
});
}
}
// Sequential Survey Instance Class
class FormbricksSequentialSurveyInstance {
constructor(containerId, startQuestionIndex, survey, config = {}) {
this.containerId = containerId;
this.embedId = `${containerId}-embed`;
this.currentQuestionIndex = startQuestionIndex;
this.survey = survey;
this.originalSurvey = JSON.parse(JSON.stringify(survey)); // Keep original for re-translation
this.config = {
hideNextButton: false,
autoSave: true,
autoSaveDelay: 1000,
enableNavigation: true,
completionUrl: "https://informup.org/survey-complete/",
...config,
};
this.responseData = {};
this.saveTimeout = null;
this.cleanupIntervals = [];
this.isSaving = false;
this.closeObserverSetup = false; // Track if close button observer is set up
this.languageCache = new Map(); // Cache rendered HTML by language and question index
}
debug(message, data = null, level = "info") {
if (!SURVEY_CONFIG.debug) return;
console.log(
`[${this.containerId}][${level.toUpperCase()}]`,
message,
data,
);
}
async render() {
this.debug(
`Rendering question ${this.currentQuestionIndex} of ${this.survey.questions.length}`,
);
await this.renderCurrentQuestion();
}
async renderCurrentQuestion() {
if (this.currentQuestionIndex >= this.survey.questions.length) {
this.showComplete();
return;
}
const singleQuestionSurvey = this.prepareSingleQuestion(
this.currentQuestionIndex,
);
const question = singleQuestionSurvey.questions[0];
const container = document.getElementById(this.containerId);
container.innerHTML = `<div id="${this.embedId}" class="survey-embed"></div>`;
// Branch rendering based on question type and configuration
if (this.shouldRenderAsDropdown(question)) {
this.debug("Rendering sequential question as custom dropdown");
await this.renderCustomDropdown(question);
} else {
this.debug(
"Rendering sequential question with native Formbricks JS",
);
await this.renderSequentialWithFormbricks(singleQuestionSurvey);
}
}
async renderSequentialWithFormbricks(singleQuestionSurvey) {
const surveyProps = {
survey: singleQuestionSurvey,
containerId: this.embedId,
mode: "inline",
languageCode: "default", // Always use default - we translate the survey ourselves
styling: this.survey.styling || {},
onDisplay: () => {
this.debug("🎯 Sequential question displayed");
// Cache the rendered HTML for current language + question
setTimeout(() => {
const embedContainer = document.getElementById(this.embedId);
const currentLang = window.FormbricksSurveyManager.currentLanguage;
const cacheKey = `${currentLang}-q${this.currentQuestionIndex}`;
if (embedContainer && !this.languageCache.has(cacheKey)) {
this.languageCache.set(cacheKey, {
html: embedContainer.innerHTML,
questionIndex: this.currentQuestionIndex,
timestamp: Date.now()
});
console.log(`💾 [${this.containerId}] Cached HTML for: ${cacheKey}`);
}
// Make container visible now that content is rendered
const container = document.getElementById(this.containerId);
if (container) {
container.classList.add("survey-loaded");
this.debug(
"✅ Made sequential survey visible after render",
);
}
}, 50);
// Set up language change detection
this.setupLanguageChangeDetection();
this.setupAutoSave();
this.setupNavigationHandler();
},
onResponse: (response) => {
this.debug("📝 Sequential response received", response);
if (response.data) {
this.responseData = {
...this.responseData,
...response.data,
};
this.debug(
"💾 Merged sequential responseData:",
this.responseData,
);
if (this.config.autoSave) {
clearTimeout(this.saveTimeout);
this.saveResponse("native-response");
}
}
},
onFinished: () => {
this.debug(
"🎉 Question finished - preventing default completion",
);
// Don't automatically move to next question here
// The navigation handler will handle this to avoid conflicts
},
onClose: () => this.debug("Question closed"),
onError: (error) => {
this.debug("Question error", error, "error");
this.showError(
"An error occurred while displaying the question",
);
},
};
try {
window.formbricksSurveys.renderSurveyInline(surveyProps);
this.debug("Successfully rendered sequential question");
// Make visible immediately in case onDisplay doesn't fire due to Formbricks errors
setTimeout(() => {
const container = document.getElementById(this.containerId);
if (container && !container.classList.contains('survey-loaded')) {
container.classList.add("survey-loaded");
this.debug("✅ Force-made sequential survey visible (fallback)");
}
this.setupUICleanup();
}, 500);
} catch (error) {
// Make visible even on error
const container = document.getElementById(this.containerId);
if (container) {
container.classList.add("survey-loaded");
}
this.debug(
"Error rendering sequential question",
error.message,
"error",
);
throw new Error("Failed to render sequential question");
}
}
translateText(textObj, language) {
if (typeof textObj === "string") return textObj;
if (typeof textObj === "object" && textObj !== null) {
// Try in order: language -> default -> en -> first available
return (
textObj[language] ||
textObj.default ||
textObj.en ||
Object.values(textObj)[0] ||
""
);
}
return textObj || "";
}
translateSurvey(survey, language) {
// Create a deep copy and translate all text fields
const translated = JSON.parse(JSON.stringify(survey));
// Translate questions - keep as objects with .default property for Formbricks
if (translated.questions) {
translated.questions = translated.questions.map(q => {
const newQuestion = { ...q };
// Translate headline - keep as object but update .default
if (q.headline) {
newQuestion.headline = {
default: this.translateText(q.headline, language)
};
}
// Translate subheader
if (q.subheader) {
newQuestion.subheader = {
default: this.translateText(q.subheader, language)
};
}
// Translate choice labels
if (q.choices) {
newQuestion.choices = q.choices.map(choice => ({
...choice,
label: {
default: this.translateText(choice.label, language)
}
}));
}
return newQuestion;
});
}
return translated;
}
prepareSingleQuestion(questionIndex) {
if (
!this.originalSurvey.questions ||
this.originalSurvey.questions.length <= questionIndex
) {
throw new Error(
`Question index ${questionIndex} not found in survey`,
);
}
const question = this.originalSurvey.questions[questionIndex];
this.debug(
"Creating sequential single question survey for question type:",
question.type,
);
const singleQuestionSurvey = {
...this.originalSurvey,
questions: [question],
welcomeCard: { enabled: false },
thankyouCard: { enabled: false },
};
// Translate survey to current language from original data
const language = window.FormbricksSurveyManager.currentLanguage || "en";
this.debug(`🌍 [SEQUENTIAL] Translating from original survey to language: ${language}`);
return this.translateSurvey(singleQuestionSurvey, language);
}
setupNavigationHandler() {
if (!this.config.enableNavigation) return;
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
// Listen for next button clicks with event capture to intercept before Formbricks
embedContainer.addEventListener(
"click",
(e) => {
const target = e.target;
const buttonText = target.textContent.toLowerCase().trim();
if (
(buttonText.includes("next") ||
buttonText.includes("submit") ||
buttonText.includes("continue")) &&
(target.tagName === "BUTTON" || target.type === "submit")
) {
this.debug(`🔄 Navigation button clicked: "${buttonText}"`);
// Prevent default Formbricks navigation behavior
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
// Extract any response data from the current question before moving
this.extractCurrentQuestionData();
// Save current response before moving to next
if (Object.keys(this.responseData).length > 0) {
this.saveResponse("navigation-save").then(() => {
setTimeout(() => this.nextQuestion(), 200);
});
} else {
setTimeout(() => this.nextQuestion(), 200);
}
return false;
}
},
true,
); // Use capture phase to intercept before Formbricks handlers
}
extractCurrentQuestionData() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const questionId =
this.survey.questions[this.currentQuestionIndex]?.id ||
`question-${this.currentQuestionIndex}`;
// Extract text input values
const textInputs = embedContainer.querySelectorAll(
'textarea, input[type="text"], input:not([type])',
);
textInputs.forEach((input) => {
if (input.value.trim()) {
this.responseData[questionId] = input.value.trim();
this.debug(
`📤 Extracted text value: "${input.value.trim()}"`,
);
}
});
// Extract radio button values
const checkedRadio = embedContainer.querySelector(
'input[type="radio"]:checked',
);
if (checkedRadio) {
this.responseData[questionId] = checkedRadio.value;
this.debug(`📤 Extracted radio value: "${checkedRadio.value}"`);
}
// Extract checkbox values
const checkedCheckboxes = embedContainer.querySelectorAll(
'input[type="checkbox"]:checked',
);
if (checkedCheckboxes.length > 0) {
this.responseData[questionId] = Array.from(
checkedCheckboxes,
).map((cb) => cb.value);
this.debug(
`📤 Extracted checkbox values: ${this.responseData[questionId]}`,
);
}
// Extract select values
const selects = embedContainer.querySelectorAll("select");
selects.forEach((select) => {
if (select.value) {
this.responseData[questionId] = select.value;
this.debug(`📤 Extracted select value: "${select.value}"`);
}
});
// Extract selected choice buttons
const selectedChoices = embedContainer.querySelectorAll(
'[data-selected="true"], .selected, [aria-checked="true"]',
);
selectedChoices.forEach((choice) => {
const value =
choice.getAttribute("data-value") ||
choice.textContent.trim();
if (
value &&
!value.toLowerCase().includes("next") &&
!value.toLowerCase().includes("submit")
) {
this.responseData[questionId] = value;
this.debug(`📤 Extracted choice value: "${value}"`);
}
});
}
nextQuestion() {
this.currentQuestionIndex++;
this.debug(`Moving to question ${this.currentQuestionIndex}`);
// Clear current question's cleanup
this.cleanupCurrentQuestion();
// Render next question
this.renderCurrentQuestion();
}
cleanupCurrentQuestion() {
this.cleanupIntervals.forEach((item) => {
if (item.type === "observer") {
item.observer.disconnect();
} else {
clearInterval(item);
}
});
this.cleanupIntervals = [];
clearTimeout(this.saveTimeout);
}
showComplete() {
this.debug(
`🎉 Survey complete! Redirecting to: ${this.config.completionUrl}`,
);
// Show temporary completion message while saving and redirecting
const container = document.getElementById(this.containerId);
if (container) {
container.innerHTML = `
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; color: #059669; padding: 24px; border-radius: 8px; margin: 20px 0; text-align: center;">
<h3 style="margin: 0 0 10px 0;">Survey Complete!</h3>
<p style="margin: 0;">Thank you for completing all questions. Redirecting...</p>
</div>
`;
}
// Final save and then redirect
if (Object.keys(this.responseData).length > 0) {
this.saveResponse("survey-complete")
.then(() => {
// Short delay to ensure save completes, then redirect
setTimeout(() => {
window.location.href = this.config.completionUrl;
}, 500);
})
.catch(() => {
// Even if save fails, still redirect
setTimeout(() => {
window.location.href = this.config.completionUrl;
}, 500);
});
} else {
// No data to save, redirect immediately
setTimeout(() => {
window.location.href = this.config.completionUrl;
}, 1000);
}
}
setupUICleanup() {
// Prevent auto-focus
this.preventAutoFocus();
// Immediate cleanup
if (this.config.hideNextButton) {
this.hideNextButtons();
}
this.hideOptionalRequiredText();
// More frequent cleanup for sequential questions (every 1 second for first 10 seconds)
const frequentCleanupInterval = setInterval(() => {
if (this.config.hideNextButton) {
this.hideNextButtons();
}
this.hideOptionalRequiredText();
}, 1000);
this.cleanupIntervals.push(frequentCleanupInterval);
// Stop frequent cleanup after 10 seconds and switch to normal interval
setTimeout(() => {
clearInterval(frequentCleanupInterval);
// Continue with less frequent cleanup
const normalCleanupInterval = setInterval(() => {
if (this.config.hideNextButton) {
this.hideNextButtons();
}
this.hideOptionalRequiredText();
}, 3000);
this.cleanupIntervals.push(normalCleanupInterval);
// Stop normal cleanup after 60 seconds total
setTimeout(() => clearInterval(normalCleanupInterval), 50000);
}, 10000);
}
hideOptionalRequiredText() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const walker = document.createTreeWalker(
embedContainer,
NodeFilter.SHOW_TEXT,
null,
false,
);
const textNodes = [];
let node;
while ((node = walker.nextNode())) {
const text = node.textContent.toLowerCase().trim();
if (
text.includes("optional") ||
text.includes("required") ||
text === "*"
) {
textNodes.push(node);
}
}
textNodes.forEach((textNode) => {
const parent = textNode.parentElement;
if (parent && parent.textContent.trim().length < 20) {
parent.style.display = "none";
this.debug(
"🧹 Hidden optional/required text:",
textNode.textContent.trim(),
);
}
});
}
hideNextButtons() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
setTimeout(() => {
const allButtons = embedContainer.querySelectorAll("button");
allButtons.forEach((button) => {
const buttonText = button.textContent.toLowerCase().trim();
if (
buttonText.includes("next") ||
buttonText.includes("submit") ||
buttonText.includes("continue") ||
(buttonText === "" && button.type === "submit")
) {
button.style.display = "none";
button.style.visibility = "hidden";
this.debug(
`✓ Hidden button: "${button.textContent.trim()}"`,
);
}
});
}, 1000);
}
setupAutoSave() {
if (!this.config.autoSave) return;
this.debug("🔧 Setting up sequential auto-save handlers");
setTimeout(() => {
this.setupTextAutoSave();
this.setupChoiceAutoSave();
this.setupNPSAutoSave();
this.setupRadioCheckboxAutoSave();
this.setupSelectAutoSave();
}, 500);
}
setupTextAutoSave() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const textInputs = embedContainer.querySelectorAll(
'textarea, input[type="text"], input:not([type])',
);
this.debug(
`📝 Setting up sequential text auto-save for ${textInputs.length} inputs`,
);
textInputs.forEach((input) => {
input.addEventListener("input", () => {
clearTimeout(this.saveTimeout);
if (input.value.trim()) {
const questionId =
this.survey.questions[this.currentQuestionIndex]?.id ||
`question-${this.currentQuestionIndex}`;
this.responseData[questionId] = input.value;
this.debug(
`💾 Sequential auto-saving text: "${input.value}"`,
);
this.saveTimeout = setTimeout(() => {
this.saveResponse("auto-text");
}, this.config.autoSaveDelay);
}
});
input.addEventListener("blur", () => {
if (input.value.trim()) {
const questionId =
this.survey.questions[this.currentQuestionIndex]?.id ||
`question-${this.currentQuestionIndex}`;
this.responseData[questionId] = input.value;
this.debug(
`💾 Sequential immediate save on blur: "${input.value}"`,
);
clearTimeout(this.saveTimeout);
this.saveResponse("blur-text");
}
});
});
}
setupChoiceAutoSave() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const setupClickHandlers = () => {
const choices = embedContainer.querySelectorAll(
'button, div[role="button"], [data-value], .choice-option, [class*="choice"], [class*="option"]',
);
this.debug(
`🎯 Setting up sequential click handlers for ${choices.length} choice elements`,
);
choices.forEach((choice) => {
if (!choice.hasAttribute("data-sequential-save-attached")) {
choice.setAttribute(
"data-sequential-save-attached",
"true",
);
choice.addEventListener("click", () => {
const buttonText = choice.textContent
.toLowerCase()
.trim();
// Skip navigation buttons - they're handled separately
if (
buttonText.includes("next") ||
buttonText.includes("submit") ||
buttonText.includes("continue")
) {
return;
}
setTimeout(() => {
const choiceValue =
choice.getAttribute("data-value") ||
choice.getAttribute("value") ||
choice.textContent.trim();
if (choiceValue) {
const questionId =
this.survey.questions[this.currentQuestionIndex]
?.id || `question-${this.currentQuestionIndex}`;
this.responseData[questionId] = choiceValue;
this.debug(
`🎯 Sequential choice clicked: "${choiceValue}"`,
);
clearTimeout(this.saveTimeout);
this.saveResponse("click-choice");
}
}, 100);
});
}
});
};
setTimeout(setupClickHandlers, 500);
const observer = new MutationObserver(() => {
setTimeout(setupClickHandlers, 100);
});
observer.observe(embedContainer, {
childList: true,
subtree: true,
});
this.cleanupIntervals.push({ type: "observer", observer });
}
setupNPSAutoSave() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const setupNPSClickHandlers = () => {
const npsButtons = embedContainer.querySelectorAll(
'button[class*="nps"], [role="button"][class*="nps"], button[data-value], .nps-option, [class*="rating"] button',
);
this.debug(
`📊 Setting up sequential NPS click handlers for ${npsButtons.length} buttons`,
);
npsButtons.forEach((button) => {
if (!button.hasAttribute("data-sequential-nps-attached")) {
button.setAttribute("data-sequential-nps-attached", "true");
button.addEventListener("click", () => {
setTimeout(() => {
const npsValue = parseInt(
button.getAttribute("data-value") ||
button.textContent.trim(),
);
if (
!isNaN(npsValue) &&
npsValue >= 0 &&
npsValue <= 10
) {
const questionId =
this.survey.questions[this.currentQuestionIndex]
?.id || `question-${this.currentQuestionIndex}`;
this.responseData[questionId] = npsValue;
this.debug(`📊 Sequential NPS clicked: ${npsValue}`);
clearTimeout(this.saveTimeout);
this.saveResponse("click-nps");
}
}, 100);
});
}
});
};
setTimeout(setupNPSClickHandlers, 500);
const observer = new MutationObserver(() => {
setTimeout(setupNPSClickHandlers, 100);
});
observer.observe(embedContainer, {
childList: true,
subtree: true,
});
this.cleanupIntervals.push({ type: "observer", observer });
}
setupRadioCheckboxAutoSave() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const radioCheckboxes = embedContainer.querySelectorAll(
'input[type="radio"], input[type="checkbox"]',
);
this.debug(
`📋 Setting up sequential radio/checkbox auto-save for ${radioCheckboxes.length} inputs`,
);
radioCheckboxes.forEach((input) => {
input.addEventListener("change", () => {
const questionId =
this.survey.questions[this.currentQuestionIndex]?.id ||
`question-${this.currentQuestionIndex}`;
if (input.type === "radio") {
this.responseData[questionId] = input.value;
this.debug(
`📋 Sequential radio selected: "${input.value}"`,
);
} else if (input.type === "checkbox") {
if (!this.responseData[questionId])
this.responseData[questionId] = [];
if (input.checked) {
if (
!this.responseData[questionId].includes(input.value)
) {
this.responseData[questionId].push(input.value);
}
} else {
this.responseData[questionId] = this.responseData[
questionId
].filter((v) => v !== input.value);
}
this.debug(
`📋 Sequential checkbox updated: "${input.value}" = ${input.checked}`,
);
}
clearTimeout(this.saveTimeout);
this.saveResponse("change-radio-checkbox");
});
});
}
setupSelectAutoSave() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
const selects = embedContainer.querySelectorAll("select");
this.debug(
`📋 Setting up sequential select auto-save for ${selects.length} dropdowns`,
);
selects.forEach((select) => {
select.addEventListener("change", () => {
const questionId =
this.survey.questions[this.currentQuestionIndex]?.id ||
`question-${this.currentQuestionIndex}`;
this.responseData[questionId] = select.value;
this.debug(`📋 Sequential select changed: "${select.value}"`);
clearTimeout(this.saveTimeout);
this.saveResponse("change-select");
});
});
}
async saveResponse(trigger = "background") {
console.log(
`🚀 [${this.containerId}] SEQUENTIAL SAVE ATTEMPT: ${trigger}`,
);
console.log(
`📊 [${this.containerId}] Sequential ResponseData:`,
this.responseData,
);
console.log(
`🆔 [${this.containerId}] Shared Response ID:`,
window.FormbricksSurveyManager.sharedResponseId,
);
if (Object.keys(this.responseData).length === 0) {
this.debug("⚠️ No sequential response data to save");
return;
}
if (this.isSaving) {
this.debug("⏳ Sequential save already in progress, skipping");
return;
}
this.isSaving = true;
try {
// Check if user is signed in
const isSignedIn = await this.checkUserSignedIn();
console.log("🔐 User signed in status:", isSignedIn);
// Get referrer URL (fallback to "direct" if empty)
const referrerUrl = document.referrer || "direct";
// Log to help debug
console.log("🔍 Hidden fields being added:", {
url: window.location.href,
signed_in: isSignedIn ? "1" : "0",
referrer: referrerUrl,
});
// Create data with hidden fields (same approach as working URL)
const dataWithHiddenFields = {
...this.responseData,
url: window.location.href,
signed_in: isSignedIn ? "1" : "0", // String like URL
referrer: referrerUrl, // Add referrer URL
language: window.FormbricksSurveyManager.currentLanguage || "en", // Add current language
};
console.log(
"📦 Data being sent to Formbricks:",
dataWithHiddenFields,
);
console.log("🌐 Current URL:", window.location.href);
console.log(
"🔑 Signed in value being sent:",
dataWithHiddenFields.signed_in,
);
console.log("🔗 Referrer being sent:", referrerUrl);
const requestData = {
data: dataWithHiddenFields,
finished: false,
meta: {
trigger: trigger,
timestamp: new Date().toISOString(),
containerId: this.containerId,
currentQuestion: this.currentQuestionIndex,
isSequential: true,
pageUrl: window.location.href,
},
};
let response;
const sharedResponseId =
window.FormbricksSurveyManager.sharedResponseId;
if (sharedResponseId) {
// Update existing shared response
response = await fetch(
`${SURVEY_CONFIG.formbricksBaseUrl}/api/v1/client/${SURVEY_CONFIG.environmentId}/responses/${sharedResponseId}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestData),
},
);
} else {
// Create new shared response
response = await fetch(
`${SURVEY_CONFIG.formbricksBaseUrl}/api/v1/client/${SURVEY_CONFIG.environmentId}/responses`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
surveyId: this.survey.id,
...requestData,
}),
},
);
}
if (response.ok) {
const result = await response.json();
console.log(
`✅ [${this.containerId}] SEQUENTIAL SAVE SUCCESSFUL`,
);
console.log(
`📋 [${this.containerId}] Sequential API Response:`,
result,
);
// Store shared response ID if this was the first save
if (!sharedResponseId) {
const responseId =
result.data?.id ||
result.id ||
result.responseId ||
result.data?.responseId;
if (responseId) {
window.FormbricksSurveyManager.sharedResponseId =
responseId;
console.log(
`🆔 [${this.containerId}] Created shared response ID: ${responseId}`,
);
this.debug(
`🆔 Created shared response ID: ${responseId}`,
);
} else {
console.warn(
`⚠️ [${this.containerId}] Could not extract response ID from:`,
result,
);
}
} else {
this.debug(
`🔄 Updated shared response: ${sharedResponseId}`,
);
}
} else {
const errorText = await response.text();
console.error(
`❌ [${this.containerId}] SEQUENTIAL SAVE FAILED: ${response.status}`,
errorText,
);
}
} catch (error) {
console.error(
`💥 [${this.containerId}] SEQUENTIAL SAVE EXCEPTION:`,
error,
);
} finally {
this.isSaving = false;
}
}
showError(message) {
const container = document.getElementById(this.containerId);
if (container) {
container.innerHTML = `
<div style="background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; padding: 16px; border-radius: 8px; margin: 20px 0;">
<strong>Error:</strong> ${message}
</div>
`;
}
}
async checkUserSignedIn() {
try {
// Check multiple methods to detect if user is signed in
// Method 1: Check for member_status in URL (most reliable)
// ANY member_status parameter means user is logged in (free, paid, complimentary, etc.)
const urlParams = new URLSearchParams(window.location.search);
const memberStatus = urlParams.get("member_status");
if (memberStatus) {
this.debug(
`👤 User IS signed in (member_status: ${memberStatus})`,
);
return true;
}
// Method 2: Check Ghost members API
if (
typeof window.ghost !== "undefined" &&
window.ghost.members
) {
try {
const member =
await window.ghost.members.getCurrentMember();
if (member) {
this.debug(
"👤 User is signed in (via Ghost members API)",
);
return true;
}
} catch (e) {
// Continue to next method
}
}
// Method 3: Check Ghost session API
try {
const response = await fetch("/members/api/session/", {
method: "GET",
credentials: "include",
});
if (response.ok) {
const data = await response.json();
if (data && data.email) {
this.debug("👤 User is signed in (via session API)");
return true;
}
}
} catch (e) {
// Continue to next method
}
this.debug("👤 User is NOT signed in");
return false;
} catch (error) {
this.debug("⚠️ Error checking sign-in status:", error.message);
return false;
}
}
preventAutoFocus() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
// Temporarily disable focus on all focusable elements
const focusableElements = embedContainer.querySelectorAll(
"input, textarea, button, select, [tabindex]",
);
focusableElements.forEach((element) => {
const originalTabIndex = element.tabIndex;
element.tabIndex = -1;
element.setAttribute(
"data-original-tabindex",
originalTabIndex,
);
// Restore tabindex after a delay
setTimeout(() => {
const savedTabIndex = element.getAttribute(
"data-original-tabindex",
);
element.tabIndex =
savedTabIndex === "null" ? 0 : parseInt(savedTabIndex);
element.removeAttribute("data-original-tabindex");
}, 1000);
});
}
setupLanguageChangeDetection() {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return;
this.debug("🌍 [SEQUENTIAL] Setting up language change detection");
// Map language names to codes
const languageMap = {
'english': 'en',
'spanish': 'es',
'spanish; castilian': 'es',
'español': 'es',
'french': 'fr',
'français': 'fr',
'german': 'de',
'deutsch': 'de',
'portuguese': 'pt',
'português': 'pt',
'italian': 'it',
'italiano': 'it',
'chinese': 'zh',
'中文': 'zh',
'japanese': 'ja',
'日本語': 'ja'
};
// Listen for ALL button clicks
embedContainer.addEventListener('click', (e) => {
const target = e.target.closest('button');
if (!target) return;
const buttonClass = target.className || "";
const buttonAria = target.getAttribute('aria-label') || "";
const buttonText = target.textContent.trim();
// Detect if this is a language-related click
const isLanguageContext =
buttonAria.toLowerCase().includes('language') ||
buttonAria.toLowerCase().includes('lang') ||
buttonClass.toLowerCase().includes('language') ||
buttonClass.toLowerCase().includes('lang');
// Or if the button text matches a language name
const textLower = buttonText.toLowerCase();
const matchesLanguage = languageMap.hasOwnProperty(textLower);
if (isLanguageContext || matchesLanguage) {
console.log('🌍 [SEQUENTIAL] LANGUAGE-RELATED CLICK:', buttonText || buttonAria);
setTimeout(() => {
// Map language name to code
const langCode = languageMap[textLower] ||
target.getAttribute('data-language') ||
target.getAttribute('lang') ||
target.getAttribute('value');
if (langCode && langCode !== window.FormbricksSurveyManager.currentLanguage) {
console.log(`🌍 [SEQUENTIAL] LANGUAGE CHANGE: ${window.FormbricksSurveyManager.currentLanguage} → ${langCode}`);
window.FormbricksSurveyManager.setLanguage(langCode);
}
}, 300);
}
});
}
detectCurrentLanguage() {
// Try to detect the currently selected language from Formbricks UI
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) return null;
// Look for selected language indicators
const selectedLangElement = embedContainer.querySelector(
'[data-language][data-selected="true"], [class*="language"][class*="selected"], button[aria-pressed="true"][class*="language"]'
);
if (selectedLangElement) {
const lang = selectedLangElement.getAttribute('data-language') ||
selectedLangElement.getAttribute('lang') ||
selectedLangElement.textContent.trim().toLowerCase();
this.debug(`🔍 [SEQUENTIAL] Detected language from UI: ${lang}`);
return lang;
}
return null;
}
reRenderWithLanguage(newLanguage) {
console.log(`🔄 [${this.containerId}] SEQUENTIAL reRenderWithLanguage called with: ${newLanguage}`);
this.debug(`🔄 [SEQUENTIAL] Updating text content with language: ${newLanguage}`);
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) {
console.error(`❌ Embed container not found: ${this.embedId}`);
return;
}
// Get the current question data from original survey
const question = this.originalSurvey.questions[this.currentQuestionIndex];
// Update headline text - try multiple selectors
const headlineSelectors = [
'h1[class*="headline"]',
'h2[class*="headline"]',
'h3[class*="headline"]',
'[class*="headline"]',
'.fb-question-headline',
'h1', 'h2', 'h3'
];
let headline = null;
for (const selector of headlineSelectors) {
headline = embedContainer.querySelector(selector);
if (headline && headline.textContent.trim().length > 10) {
break; // Found a headline with actual content
}
}
if (headline && question.headline) {
const translatedHeadline = this.translateText(question.headline, newLanguage);
headline.textContent = translatedHeadline;
console.log(`📝 [${this.containerId}] SEQUENTIAL updated headline to: ${translatedHeadline.substring(0, 50)}...`);
} else {
console.warn(`⚠️ [${this.containerId}] SEQUENTIAL could not find headline element`);
// Debug: Show what elements we DO have
const allHeadings = embedContainer.querySelectorAll('h1, h2, h3, h4, h5, h6, [class*="text"], [class*="question"]');
console.log(`🔍 [${this.containerId}] SEQUENTIAL found ${allHeadings.length} potential headline elements:`);
allHeadings.forEach((el, i) => {
console.log(` ${i}: <${el.tagName}> class="${el.className}" text="${el.textContent.substring(0, 60)}..."`);
});
}
// Update subheader if exists
const subheader = embedContainer.querySelector('[class*="subheader"], .fb-question-subheader');
if (subheader && question.subheader) {
const translatedSubheader = this.translateText(question.subheader, newLanguage);
subheader.textContent = translatedSubheader;
console.log(`📝 [${this.containerId}] SEQUENTIAL updated subheader`);
}
// Update choice labels
if (question.choices && question.choices.length > 0) {
const labels = embedContainer.querySelectorAll('label span, label');
question.choices.forEach((choice, index) => {
if (labels[index]) {
const translatedLabel = this.translateText(choice.label, newLanguage);
// Find the text node within the label (avoid changing nested elements)
const textNode = Array.from(labels[index].childNodes).find(n => n.nodeType === Node.TEXT_NODE);
if (textNode) {
textNode.textContent = translatedLabel;
} else {
labels[index].textContent = translatedLabel;
}
console.log(`📝 [${this.containerId}] SEQUENTIAL updated choice ${index}: ${translatedLabel.substring(0, 30)}...`);
}
});
}
console.log(`✅ [${this.containerId}] SEQUENTIAL language updated without re-render!`);
}
shouldRenderAsDropdown(question) {
this.debug(
"🔍 [SEQUENTIAL] Checking if question should render as dropdown...",
);
this.debug(
"Config useSingleSelectDropdown:",
this.config.useSingleSelectDropdown,
);
this.debug("Question object:", question);
if (!this.config.useSingleSelectDropdown) {
this.debug("❌ [SEQUENTIAL] Dropdown disabled in config");
return false;
}
if (!question || !question.type) {
this.debug(
"❌ [SEQUENTIAL] No question or question type found",
);
return false;
}
// Check for single-select question types that should be converted to dropdowns
const singleSelectTypes = [
"multiplechoicesingle", // Fixed: all lowercase to match normalized type
"multiplechoicemulti", // Fixed: all lowercase to match normalized type
"singleselect", // Fixed: all lowercase to match normalized type
"radio",
"choice",
"select",
];
const questionType = question.type.toLowerCase();
const isSingleSelect = singleSelectTypes.includes(questionType);
const hasChoices =
question.choices && question.choices.length > 0;
this.debug(`📋 [SEQUENTIAL] Question analysis:`);
this.debug(
` - Type: "${question.type}" (normalized: "${questionType}")`,
);
this.debug(` - Is single select: ${isSingleSelect}`);
this.debug(` - Has choices: ${hasChoices}`);
this.debug(
` - Choices count: ${question.choices ? question.choices.length : 0}`,
);
if (question.choices) {
this.debug(` - Choices:`, question.choices);
}
const shouldRender = isSingleSelect && hasChoices;
this.debug(
`🎯 [SEQUENTIAL] Final decision: ${shouldRender ? "RENDER AS DROPDOWN" : "RENDER WITH FORMBRICKS"}`,
);
return shouldRender;
}
async renderCustomDropdown(question) {
const embedContainer = document.getElementById(this.embedId);
if (!embedContainer) {
throw new Error("Embed container not found");
}
// Helper function to extract text from object or return string
const extractText = (textObj) => {
if (typeof textObj === "string") return textObj;
if (typeof textObj === "object" && textObj !== null) {
const lang = SURVEY_CONFIG.defaultLanguage || "en";
// Try in order: configured language -> default -> en -> english -> first available
return (
textObj[lang] ||
textObj.default ||
textObj.en ||
textObj.english ||
Object.values(textObj)[0] ||
"[No text available]"
);
}
return textObj || "";
};
const questionText = extractText(
question.headline || question.text,
);
const questionSubtext = question.subheader
? extractText(question.subheader)
: "";
// Check for pre-existing value from user profile
const profileValue =
window.FormbricksSurveyManager.getProfileValue(question.id);
this.debug(
"🎨 [SEQUENTIAL] Rendering dropdown with extracted text:",
{
questionText,
questionSubtext,
choicesCount: question.choices.length,
profileValue,
},
);
// Create Formbricks-style HTML structure
const html = `
<div class="fb-survey fb-survey-inline" style="--fb-brand-color: #00C4B8;">
<div class="fb-survey-content">
<div class="fb-question-container">
<div class="fb-question-text">
<h3 class="fb-question-headline">${questionText || "Please select an option"}</h3>
${questionSubtext ? `<p class="fb-question-subheader">${questionSubtext}</p>` : ""}
</div>
<div class="fb-question-form">
<div class="fb-form-field">
<select
id="custom-dropdown-${question.id}"
class="fb-select fb-form-control"
data-question-id="${question.id}"
${question.required !== false ? "required" : ""}
>
<option value="" disabled ${profileValue ? "" : "selected"}>
${this.config.dropdownPlaceholder}
</option>
${question.choices
.map((choice) => {
const choiceValue =
choice.id ||
choice.value ||
extractText(choice.label);
const choiceLabel = extractText(
choice.label || choice.value || choice,
);
const isSelected =
profileValue && profileValue === choiceValue;
this.debug("[SEQUENTIAL] Choice mapping:", {
choiceValue,
choiceLabel,
isSelected,
originalChoice: choice,
});
return `<option value="${choiceValue}" ${isSelected ? "selected" : ""}>${choiceLabel}</option>`;
})
.join("")}
</select>
</div>
${
this.config.enableNavigation
? `
<div class="fb-navigation-buttons" style="margin-top: 16px;">
${
this.currentQuestionIndex > 0
? '<button type="button" class="fb-button fb-button-secondary" onclick="window.sequentialInstance.previousQuestion()">Previous</button>'
: ""
}
<button type="button" class="fb-button fb-button-primary" onclick="window.sequentialInstance.nextQuestion()" disabled id="next-btn-${question.id}">Next</button>
</div>
`
: ""
}
</div>
</div>
</div>
</div>
`;
embedContainer.innerHTML = html;
// Pre-populate response data if profile value exists
if (profileValue) {
this.responseData[question.id] = profileValue;
this.debug(
"👤 [SEQUENTIAL] Pre-populated response from profile:",
{ questionId: question.id, value: profileValue },
);
}
// Set up event handling
this.setupCustomDropdownEvents(question);
// Call the same setup as regular questions but skip hiding next buttons if navigation is enabled
setTimeout(() => this.setupUICleanup(), 100);
this.debug("Sequential custom dropdown rendered successfully");
}
setupCustomDropdownEvents(question) {
const dropdown = document.getElementById(
`custom-dropdown-${question.id}`,
);
const nextBtn = document.getElementById(
`next-btn-${question.id}`,
);
if (!dropdown) return;
dropdown.addEventListener("change", (event) => {
const selectedValue = event.target.value;
this.debug(`Sequential dropdown selection: ${selectedValue}`);
// Store response data
this.responseData[question.id] = selectedValue;
// Save to user profile if enabled
if (window.FormbricksSurveyManager) {
window.FormbricksSurveyManager.saveUserProfile(
question.id,
selectedValue,
question,
);
}
// Enable next button if navigation is enabled
if (nextBtn) {
nextBtn.disabled = false;
}
// Trigger auto-save if enabled
if (this.config.autoSave) {
clearTimeout(this.saveTimeout);
this.saveTimeout = setTimeout(() => {
this.saveResponse("sequential-dropdown-change");
}, this.config.autoSaveDelay);
}
this.debug(
"Sequential response data updated:",
this.responseData,
);
});
// Store reference for navigation methods
window.sequentialInstance = this;
}
}
// Page cleanup
window.addEventListener("beforeunload", () => {
if (window.FormbricksSurveyManager) {
window.FormbricksSurveyManager.instances.forEach((instance) => {
if (instance.cleanupIntervals) {
instance.cleanupIntervals.forEach((item) => {
if (item.type === "observer") {
item.observer.disconnect();
} else {
clearInterval(item);
}
});
}
clearTimeout(instance.saveTimeout);
});
}
});
}
// Initialize first question
window.FormbricksSurveyManager.initializeAndRenderQuestion(
BLOCK_1_CONFIG.containerId,
BLOCK_1_CONFIG.questionIndex,
{
hideNextButton: BLOCK_1_CONFIG.hideNextButton,
autoSave: BLOCK_1_CONFIG.autoSave,
autoSaveDelay: BLOCK_1_CONFIG.autoSaveDelay,
useSingleSelectDropdown: BLOCK_1_CONFIG.useSingleSelectDropdown,
dropdownPlaceholder: BLOCK_1_CONFIG.dropdownPlaceholder,
},
);
})(); // End IIFE
</script>
</body>
</html>
Question 2 code
For additional individual questions, insert this code:
<!-- =============================================== -->
<!-- BLOCK 2: ADDITIONAL QUESTION -->
<!-- Copy this block for additional questions -->
<!-- =============================================== -->
<div class="gh-content">
<h3>Question 2</h3>
<div id="survey-question-2">
<div class="loading">Loading survey...</div>
</div>
</div>
<script>
console.log("📝 [BLOCK 2] Additional Question");
// ========================================
// 🔧 CONFIGURATION - EDIT THESE VALUES
// ========================================
const BLOCK_2_CONFIG = {
containerId: "survey-question-2", // Change this for each block
questionIndex: 1, // Which question to show (0-based)
hideNextButton: true, // Hide next/submit buttons
autoSave: true, // Enable auto-save
autoSaveDelay: 1000, // Auto-save delay in ms
useSingleSelectDropdown: false, // Render single-select questions as dropdowns
dropdownPlaceholder: "Please select an option...", // Placeholder text for dropdowns
};
// ========================================
// END CONFIGURATION
// ========================================
// Wait for manager to be ready, then render question
(function checkManagerAndRender() {
if (
window.FormbricksSurveyManager &&
window.FormbricksSurveyManager.survey
) {
console.log("✅ [BLOCK 2] Manager ready, rendering question");
window.FormbricksSurveyManager.renderQuestion(
BLOCK_2_CONFIG.containerId,
BLOCK_2_CONFIG.questionIndex,
{
hideNextButton: BLOCK_2_CONFIG.hideNextButton,
autoSave: BLOCK_2_CONFIG.autoSave,
autoSaveDelay: BLOCK_2_CONFIG.autoSaveDelay,
useSingleSelectDropdown: BLOCK_2_CONFIG.useSingleSelectDropdown,
dropdownPlaceholder: BLOCK_2_CONFIG.dropdownPlaceholder,
},
);
} else {
console.log("⏳ [BLOCK 2] Waiting for manager...");
setTimeout(checkManagerAndRender, 500);
}
})();
</script>
Question 3 code
<!-- =============================================== -->
<!-- BLOCK 3: SEQUENTIAL QUESTIONS WITH NAVIGATION -->
<!-- This block shows questions one by one with Next buttons -->
<!-- =============================================== -->
<div class="gh-content">
<h3>Question 3</h3>
<div id="survey-sequential">
<div class="loading">Loading survey...</div>
</div>
</div>
<script>
console.log("🔄 [BLOCK 3] Sequential Questions");
// ========================================
// 🔧 CONFIGURATION - EDIT THESE VALUES
// ========================================
const BLOCK_3_CONFIG = {
containerId: "survey-sequential", // Change this for each block
startQuestionIndex: 2, // Which question to start with (0-based)
hideNextButton: false, // Show next/submit buttons for navigation
autoSave: true, // Enable auto-save
autoSaveDelay: 1000, // Auto-save delay in ms
enableNavigation: true, // Enable sequential navigation
completionUrl: "https://informup.org/survey-complete/", // URL to redirect to when survey is complete
useSingleSelectDropdown: false, // Render single-select questions as dropdowns
dropdownPlaceholder: "Please select an option...", // Placeholder text for dropdowns
};
// ========================================
// END CONFIGURATION
// ========================================
// Wait for manager to be ready, then render sequential questions
(function checkManagerAndRenderSequential() {
if (
window.FormbricksSurveyManager &&
window.FormbricksSurveyManager.survey
) {
console.log("✅ [BLOCK 3] Manager ready, rendering sequential questions");
window.FormbricksSurveyManager.renderSequentialQuestions(
BLOCK_3_CONFIG.containerId,
BLOCK_3_CONFIG.startQuestionIndex,
{
hideNextButton: BLOCK_3_CONFIG.hideNextButton,
autoSave: BLOCK_3_CONFIG.autoSave,
autoSaveDelay: BLOCK_3_CONFIG.autoSaveDelay,
enableNavigation: BLOCK_3_CONFIG.enableNavigation,
completionUrl: BLOCK_3_CONFIG.completionUrl,
useSingleSelectDropdown: BLOCK_3_CONFIG.useSingleSelectDropdown,
dropdownPlaceholder: BLOCK_3_CONFIG.dropdownPlaceholder,
},
);
} else {
console.log("⏳ [BLOCK 3] Waiting for manager...");
setTimeout(checkManagerAndRenderSequential, 500);
}
})();
</script>