feat(forms): add Form A iframe fallback safeguards
This commit is contained in:
10 files changed
+299
-59
No files matched your search
@@ -13,18 +13,6 @@ const { config, heading, description, variant = 'standalone' } = Astro.props;
|
||||
if (!config.enabled || !config.url || !config.title || !config.fallbackText || !config.fallbackLinkText) {
|
||||
throw new Error('JotformIframe requires an enabled, fully validated Form A iframe configuration.');
|
||||
}
|
||||
|
||||
const escapeHtml = (value: string) => value.replace(/[&<>"']/g, (character) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
})[character] ?? character);
|
||||
|
||||
// Astro treats iframe descendants as raw text. set:html emits standards-based
|
||||
// iframe fallback markup while preserving the fixed, server-validated URL.
|
||||
const fallbackMarkup = `<p class="jotform-iframe__fallback">${escapeHtml(config.fallbackText)} <a href="${escapeHtml(config.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(config.fallbackLinkText)}</a></p>`;
|
||||
---
|
||||
|
||||
<section
|
||||
@@ -47,10 +35,67 @@ const fallbackMarkup = `<p class="jotform-iframe__fallback">${escapeHtml(config.
|
||||
loading="eager"
|
||||
referrerpolicy="no-referrer"
|
||||
sandbox="allow-forms allow-scripts allow-same-origin"
|
||||
set:html={fallbackMarkup}
|
||||
data-jotform-iframe-frame
|
||||
></iframe>
|
||||
<p class="jotform-iframe__fallback" data-jotform-iframe-fallback hidden aria-live="polite" role="status">
|
||||
{config.fallbackText} <a href={config.url} target="_blank" rel="noopener noreferrer">{config.fallbackLinkText}</a>
|
||||
</p>
|
||||
<noscript>
|
||||
<p class="jotform-iframe__fallback jotform-iframe__fallback--noscript">
|
||||
{config.fallbackText} <a href={config.url} target="_blank" rel="noopener noreferrer">{config.fallbackLinkText}</a>
|
||||
</p>
|
||||
</noscript>
|
||||
</section>
|
||||
|
||||
<script is:inline>
|
||||
// A frame `load` only proves that the browser completed a document load. It
|
||||
// cannot prove that cross-origin Jotform initialized correctly or accepted
|
||||
// a submission, so this controls the hosted-form fallback only.
|
||||
(() => {
|
||||
const fallbackTimeoutMs = 15_000;
|
||||
|
||||
const initialize = (container) => {
|
||||
if (container.dataset.jotformFallbackInitialized === 'true') return;
|
||||
|
||||
const frame = container.querySelector('[data-jotform-iframe-frame]');
|
||||
const fallback = container.querySelector('[data-jotform-iframe-fallback]');
|
||||
if (!(frame instanceof HTMLIFrameElement) || !(fallback instanceof HTMLElement)) return;
|
||||
|
||||
container.dataset.jotformFallbackInitialized = 'true';
|
||||
fallback.hidden = true;
|
||||
|
||||
let disposed = false;
|
||||
const hideFallback = () => {
|
||||
fallback.hidden = true;
|
||||
container.dataset.jotformFallback = 'hidden';
|
||||
};
|
||||
const showFallback = () => {
|
||||
if (disposed) return;
|
||||
fallback.hidden = false;
|
||||
container.dataset.jotformFallback = 'visible';
|
||||
};
|
||||
const timer = window.setTimeout(showFallback, fallbackTimeoutMs);
|
||||
const onLoad = () => {
|
||||
window.clearTimeout(timer);
|
||||
// A late frame load supersedes the transport timeout. It is still not
|
||||
// treated as a success, conversion, or cross-origin health signal.
|
||||
hideFallback();
|
||||
};
|
||||
const dispose = () => {
|
||||
disposed = true;
|
||||
window.clearTimeout(timer);
|
||||
frame.removeEventListener('load', onLoad);
|
||||
};
|
||||
|
||||
frame.addEventListener('load', onLoad, { once: true });
|
||||
window.addEventListener('pagehide', dispose, { once: true });
|
||||
document.addEventListener('astro:before-swap', dispose, { once: true });
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-jotform-form-a]').forEach(initialize);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Keep adapter styling route-local: the campaign and main-site bundles must not share campaign CSS. -->
|
||||
<style is:inline>
|
||||
.jotform-iframe {
|
||||
@@ -99,6 +144,10 @@ const fallbackMarkup = `<p class="jotform-iframe__fallback">${escapeHtml(config.
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.jotform-iframe__fallback[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jotform-iframe__description {
|
||||
color: var(--color-muted);
|
||||
font-size: .9rem;
|
||||
|
||||
@@ -27,6 +27,7 @@ export type FormAIframeReason =
|
||||
| 'enabled'
|
||||
| 'mode-not-isolated-test'
|
||||
| 'isolated-test-not-approved'
|
||||
| 'live-approval-not-disabled'
|
||||
| 'invalid-environment-role'
|
||||
| 'indexing-enabled'
|
||||
| 'route-not-authorized'
|
||||
@@ -132,6 +133,7 @@ export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIfr
|
||||
});
|
||||
const config = resolveFormMode(options.rawMode, options.liveApproved);
|
||||
if (config.mode !== 'isolated-test') return disabled('mode-not-isolated-test');
|
||||
if (options.liveApproved?.trim().toLowerCase() !== 'false') return disabled('live-approval-not-disabled');
|
||||
if (!isTrue(options.isolatedTestApproved)) return disabled('isolated-test-not-approved');
|
||||
if (options.environmentRole?.trim().toLowerCase() !== isolatedTestEnvironmentRole) return disabled('invalid-environment-role');
|
||||
if (isTrue(options.allowIndexing)) return disabled('indexing-enabled');
|
||||
|
||||
Reference in new issue
Block a user