mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-13 19:30:36 -05:00
Compare commits
1 Commits
codex/cust
...
cursor/cus
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cacedbd03a |
@@ -41,41 +41,113 @@ export const CustomScriptsInjector = ({
|
||||
|
||||
if (!scriptsToInject.trim()) return;
|
||||
|
||||
try {
|
||||
// Create a temporary container to parse the HTML
|
||||
const container = document.createElement("div");
|
||||
container.innerHTML = scriptsToInject;
|
||||
|
||||
// Process and inject script elements
|
||||
const scripts = container.querySelectorAll("script");
|
||||
scripts.forEach((script) => {
|
||||
const newScript = document.createElement("script");
|
||||
|
||||
// Copy all attributes (src, async, defer, type, etc.)
|
||||
Array.from(script.attributes).forEach((attr) => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
|
||||
// Copy inline script content
|
||||
if (script.textContent) {
|
||||
newScript.textContent = script.textContent;
|
||||
/**
|
||||
* Ensures document.body exists before executing the injection.
|
||||
* This prevents race conditions where custom scripts try to access document.body
|
||||
* before React hydration completes, which would cause:
|
||||
* - React error #454 (missing document.body)
|
||||
* - TypeError: can't access property "removeChild" of null
|
||||
*/
|
||||
const ensureBodyExists = (): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
// If body already exists, resolve immediately
|
||||
if (document.body) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
document.head.appendChild(newScript);
|
||||
// Wait for DOMContentLoaded
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => resolve(), { once: true });
|
||||
} else {
|
||||
// Document is already loaded but body doesn't exist yet (edge case)
|
||||
// Use setTimeout to defer until next tick
|
||||
setTimeout(() => resolve(), 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Process and inject non-script elements (noscript, meta, link, style, etc.)
|
||||
const nonScripts = container.querySelectorAll(":not(script)");
|
||||
nonScripts.forEach((el) => {
|
||||
const clonedEl = el.cloneNode(true) as Element;
|
||||
document.head.appendChild(clonedEl);
|
||||
});
|
||||
/**
|
||||
* Wraps inline script content to ensure safe execution after DOM is ready.
|
||||
* This prevents scripts from executing before document.body is available.
|
||||
*/
|
||||
const wrapScriptContent = (content: string): string => {
|
||||
// Don't wrap if the script already has DOM-ready checks
|
||||
if (
|
||||
content.includes("DOMContentLoaded") ||
|
||||
content.includes("document.readyState") ||
|
||||
content.includes("window.addEventListener('load'")
|
||||
) {
|
||||
return content;
|
||||
}
|
||||
|
||||
injectedRef.current = true;
|
||||
} catch (error) {
|
||||
// Log error but don't break the survey - self-hosted admins can check console
|
||||
console.warn("[Formbricks] Error injecting custom scripts:", error);
|
||||
}
|
||||
// Wrap the script to ensure it runs after DOM is ready
|
||||
return `
|
||||
(function() {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
${content}
|
||||
});
|
||||
} else {
|
||||
${content}
|
||||
}
|
||||
})();
|
||||
`;
|
||||
};
|
||||
|
||||
const injectScripts = async () => {
|
||||
try {
|
||||
// Wait for document.body to exist before injecting any scripts
|
||||
await ensureBodyExists();
|
||||
|
||||
// Defensive check: ensure body still exists
|
||||
if (!document.body) {
|
||||
console.warn("[Formbricks] document.body is not available, skipping script injection");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a temporary container to parse the HTML
|
||||
const container = document.createElement("div");
|
||||
container.innerHTML = scriptsToInject;
|
||||
|
||||
// Process and inject script elements
|
||||
const scripts = container.querySelectorAll("script");
|
||||
scripts.forEach((script) => {
|
||||
const newScript = document.createElement("script");
|
||||
|
||||
// Copy all attributes (src, async, defer, type, etc.)
|
||||
Array.from(script.attributes).forEach((attr) => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
|
||||
// Copy inline script content with safety wrapper
|
||||
if (script.textContent) {
|
||||
// Only wrap inline scripts (not external scripts with src attribute)
|
||||
if (!script.hasAttribute("src")) {
|
||||
newScript.textContent = wrapScriptContent(script.textContent);
|
||||
} else {
|
||||
newScript.textContent = script.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
document.head.appendChild(newScript);
|
||||
});
|
||||
|
||||
// Process and inject non-script elements (noscript, meta, link, style, etc.)
|
||||
const nonScripts = container.querySelectorAll(":not(script)");
|
||||
nonScripts.forEach((el) => {
|
||||
const clonedEl = el.cloneNode(true) as Element;
|
||||
document.head.appendChild(clonedEl);
|
||||
});
|
||||
|
||||
injectedRef.current = true;
|
||||
} catch (error) {
|
||||
// Log error but don't break the survey - self-hosted admins can check console
|
||||
console.warn("[Formbricks] Error injecting custom scripts:", error);
|
||||
}
|
||||
};
|
||||
|
||||
injectScripts();
|
||||
}, [projectScripts, surveyScripts, scriptsMode]);
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user