diff --git a/.eleventy.js b/.eleventy.js index 01105ef345..94f317a883 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -141,6 +141,7 @@ module.exports = function(eleventyConfig) { eleventyConfig.addPassthroughCopy("src/js/ai-expert-modal.js"); eleventyConfig.addPassthroughCopy("src/js/hm-promo-banner.js"); eleventyConfig.addPassthroughCopy("src/js/nav-tracking.js"); + eleventyConfig.addPassthroughCopy("src/js/signup-popup.js"); // Watch content images for the image pipeline eleventyConfig.addWatchTarget("src/**/*.{svg,webp,png,jpeg,gif}"); diff --git a/nuxt/nuxt.config.ts b/nuxt/nuxt.config.ts index 9ec6ed4072..bcc399b859 100644 --- a/nuxt/nuxt.config.ts +++ b/nuxt/nuxt.config.ts @@ -291,6 +291,9 @@ export default defineNuxtConfig({ // Explicit nav-click tracking. Source is src/js/nav-tracking.js; // prod:eleventy-nuxt copies the 11ty output into nuxt/public/. { src: '/js/nav-tracking.js', defer: true }, + // Opens sign-up in a small popup window on desktop/tablet. + // Source is src/js/signup-popup.js; copied the same way as nav-tracking.js. + { src: '/js/signup-popup.js', defer: true }, ] } }, diff --git a/src/_includes/layouts/base.njk b/src/_includes/layouts/base.njk index bf62556b3b..12a5426a8e 100644 --- a/src/_includes/layouts/base.njk +++ b/src/_includes/layouts/base.njk @@ -158,6 +158,8 @@ eleventyComputed: + + {%- if not DEV_MODE -%} {% include "analytics/head.html" %} diff --git a/src/js/signup-popup.js b/src/js/signup-popup.js new file mode 100644 index 0000000000..4e1d77959a --- /dev/null +++ b/src/js/signup-popup.js @@ -0,0 +1,115 @@ +// Opens the sign-up page in a small popup window on desktop/tablet, centered +// over the visible page content, with a dark overlay behind it. +// On mobile, leaves the link's default navigation untouched. +document.addEventListener('DOMContentLoaded', () => { + // Tracks the one popup/overlay pair that can be open at a time, so + // there's a single thing to clean up instead of separate mutable + // variables that a second click could overwrite and orphan. + let session = null + + function closeSession () { + if (!session) { + return + } + if (!session.popup.closed) { + session.popup.close() + } + clearInterval(session.pollClosed) + session.overlay.remove() + session = null + } + + function openPopup (href) { + if (session) { + if (!session.popup.closed) { + // Already have one open - bring it forward instead of + // opening a second popup and orphaning this one's overlay. + session.popup.focus() + return + } + closeSession() + } + + const width = 420 + const height = Math.min(900, window.innerHeight * 0.75) + + // outerWidth/outerHeight include browser chrome (toolbars, a + // vertical tab strip, etc). Subtracting innerWidth/innerHeight + // estimates that chrome so we can center over the visible page + // content instead of the full browser window. + const chromeWidth = window.outerWidth - window.innerWidth + const chromeHeight = window.outerHeight - window.innerHeight + const viewportLeft = window.screenX + chromeWidth + const viewportTop = window.screenY + chromeHeight + + const left = viewportLeft + (window.innerWidth - width) / 2 + const top = viewportTop + (window.innerHeight - height) / 2 + + const popup = window.open( + href, + 'flowfuse-signup', + `width=${width},height=${height},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no` + ) + + if (!popup) { + return + } + + const overlay = document.createElement('div') + overlay.setAttribute('id', 'signup-popup-overlay') + overlay.style.position = 'fixed' + overlay.style.inset = '0' + overlay.style.background = 'rgba(0, 0, 0, 0.45)' + overlay.style.zIndex = '9999' + overlay.addEventListener('click', closeSession) + document.body.appendChild(overlay) + + const pollClosed = setInterval(() => { + if (popup.closed) { + closeSession() + } + }, 100) + + session = { popup, overlay, pollClosed } + popup.focus() + } + + // Delegated on `document` (rather than binding each matching link once) + // so this also catches anchors rendered after this script ran - e.g. + // Nuxt's client-side route navigation swapping in a new CtaSignUp link + // without a full page load. + document.addEventListener('click', (event) => { + const link = event.target.closest('a[href*="/account/create"]') + if (!link) { + return + } + + const isDesktopOrTablet = window.matchMedia('(min-width: 768px)').matches + if (!isDesktopOrTablet) { + return + } + + event.preventDefault() + + // Explicit signal for the product to key its popup-specific + // layout off, instead of `window.opener` — that's also set for + // an ordinary ctrl/cmd-click "open in new tab", which isn't + // this popup at all. + const popupUrl = new URL(link.href) + popupUrl.searchParams.set('context', 'popup') + + openPopup(popupUrl.href) + }) + + // Deliberate trade-off, not an oversight: closing on any window focus + // (e.g. switching tabs/apps and coming back) can cancel an in-progress, + // not-yet-submitted sign-up. That's judged preferable to the + // alternative - the popup falling behind this window with no auto-close, + // which leaves the overlay covering the page with no visible popup and + // no clear reason why, reading as the site being stuck rather than a + // sign-up that's still open one window over. Nothing of substance is + // lost by closing early here: this only ever covers the initial, + // unsubmitted form step. Added once (not per click) so listeners don't + // accumulate across retries. + window.addEventListener('focus', closeSession) +})