1import { initEmblaCarousel } from '../../ui/embla.js'; 2import { createModal } from '../../ui/modal.js'; 3import { allInOneEvent, allInOneConversionEvent } from './data-layer-events.js'; 4import { DOMAIN_SEARCH_ACCEPTED_EVENT } from './domain-search-events.js'; 5 6const initGetStartedModal = () => { 7 const root = document.querySelector('[data-get-started-modal]'); 8 if (!root) return; 9 10 if (root.dataset.initialized === 'true') return; 11 root.dataset.initialized = 'true'; 12 13 const overlay = root.querySelector('[data-get-started-overlay]'); 14 const panel = root.querySelector('[role="dialog"]'); 15 const closeButton = root.querySelector('[data-modal-close]'); 16 const carouselRoot = root.querySelector('[data-get-started-carousel]'); 17 const progressBar = root.querySelector('[data-embla-progress]'); 18 const domainSearch = root.querySelector('[data-get-started-domain]'); 19 const domainSearchForm = domainSearch?.querySelector('[data-domain-search]'); 20 const nextButtons = root.querySelectorAll('[data-get-started-next]'); 21 const domainTriggers = root.querySelectorAll('[data-get-started-show-domain]'); 22 23 let embla = null; 24 let refreshTimer = null; 25 let visibilityTimer = null; 26 let carouselViewport = null; 27 let carouselContainer = null; 28 let autoHeightQuery = null; 29 let autoHeightEnabled = false; 30 let isOpen = false; 31 let trackedStep = null; 32 33 const trackSelectedStep = () => { 34 if (!isOpen) return; 35 const step = embla ? embla.selectedScrollSnap() + 1 : 1; 36 if (step === trackedStep) return; 37 trackedStep = step; 38 allInOneEvent(step); 39 }; 40 41 const refreshEmbla = () => { 42 if (!embla) return; 43 embla.reInit(); 44 }; 45 46 const scheduleRefresh = (delay = 0) => { 47 if (!embla) return; 48 if (refreshTimer) { 49 window.clearTimeout(refreshTimer); 50 } 51 refreshTimer = window.setTimeout(() => { 52 refreshEmbla(); 53 refreshTimer = null; 54 }, delay); 55 }; 56 57 const setCarouselVisibility = (visible) => { 58 if (!carouselViewport) return; 59 carouselViewport.style.visibility = visible ? '' : 'hidden'; 60 }; 61 62 const setContainerTransition = (enabled) => { 63 if (!carouselContainer) return; 64 carouselContainer.style.transition = enabled ? '' : 'none'; 65 }; 66 67 const shouldUseAutoHeight = () => window.matchMedia?.('(min-width: 768px)')?.matches; 68 69 const buildPlugins = () => { 70 const plugins = []; 71 if (typeof EmblaCarouselFade === 'function') { 72 plugins.push(EmblaCarouselFade()); 73 } 74 const useAutoHeight = shouldUseAutoHeight() && typeof EmblaCarouselAutoHeight === 'function'; 75 if (useAutoHeight) { 76 plugins.push(EmblaCarouselAutoHeight()); 77 } 78 return { plugins, useAutoHeight }; 79 }; 80 81 const destroyEmbla = () => { 82 if (!embla) return; 83 embla.destroy(); 84 embla = null; 85 }; 86 87 const createEmbla = ({ plugins, useAutoHeight }) => { 88 autoHeightEnabled = Boolean(useAutoHeight); 89 embla = initEmblaCarousel( 90 carouselRoot, 91 { watchDrag: false, startIndex: trackedStep ? trackedStep - 1 : 0 }, 92 { progress: { bar: progressBar } }, 93 plugins, 94 ); 95 embla?.on('select', trackSelectedStep); 96 if (!embla && progressBar) { 97 progressBar.style.width = '100%'; 98 } 99 if (!autoHeightEnabled && carouselContainer) { 100 carouselContainer.style.height = ''; 101 if (carouselViewport) { 102 carouselViewport.style.height = ''; 103 } 104 if (carouselRoot) { 105 carouselRoot.style.height = ''; 106 } 107 } 108 }; 109 110 const hideDomainSearch = () => { 111 if (domainSearch) { 112 domainSearch.classList.add('hidden'); 113 } 114 }; 115 116 const showDomainSearch = () => { 117 if (!domainSearch) return; 118 domainSearch.classList.remove('hidden'); 119 if (typeof domainSearch.scrollIntoView === 'function') { 120 domainSearch.scrollIntoView({ behavior: 'smooth', block: 'start' }); 121 } 122 }; 123 124 const initCarousel = () => { 125 if (!carouselRoot) { 126 if (progressBar) { 127 progressBar.style.width = '100%'; 128 } 129 return; 130 } 131 132 carouselViewport = carouselRoot.querySelector('[data-embla-viewport]'); 133 carouselContainer = carouselRoot.querySelector('[data-embla-container]'); 134 135 createEmbla(buildPlugins()); 136 137 if (embla) { 138 const images = carouselRoot.querySelectorAll('img');
139 images.forEach((img) => { 140 if (!(img instanceof HTMLImageElement)) return; 141 if (img.complete) return; 142 img.addEventListener( 143 'load', 144 () => { 145 scheduleRefresh(0); 146 }, 147 { once: true }, 148 ); 149 }); 150 } 151 152 if (window.matchMedia) { 153 autoHeightQuery = window.matchMedia('(min-width: 768px)'); 154 const handleChange = () => { 155 if (!carouselRoot) return; 156 destroyEmbla(); 157 createEmbla(buildPlugins()); 158 scheduleRefresh(0); 159 }; 160 if (autoHeightQuery && typeof autoHeightQuery.addEventListener === 'function') { 161 autoHeightQuery.addEventListener('change', handleChange); 162 } else if (autoHeightQuery && typeof autoHeightQuery.addListener === 'function') { 163 autoHeightQuery.addListener(handleChange); 164 } 165 } 166 }; 167 168 initCarousel(); 169 170 nextButtons.forEach((button) => { 171 button.addEventListener('click', (event) => { 172 event.preventDefault(); 173 hideDomainSearch(); 174 if (embla) { 175 embla.scrollNext(); 176 } 177 }); 178 }); 179 180 domainTriggers.forEach((button) => { 181 button.addEventListener('click', (event) => { 182 event.preventDefault(); 183 showDomainSearch(); 184 }); 185 }); 186 187 domainSearchForm?.addEventListener(DOMAIN_SEARCH_ACCEPTED_EVENT, () => { 188 const step = embla ? embla.selectedScrollSnap() + 1 : 1; 189 allInOneConversionEvent(step); 190 }); 191 192 const modal = createModal({ 193 root, 194 panel, 195 overlay, 196 openTriggerSelector: '[data-get-started-trigger]', 197 closeOnOverlay: true, 198 closeOnEsc: true, 199 trapFocus: true, 200 restoreFocus: true, 201 // The carousel is briefly visibility:hidden while Embla measures it, so its
202 // buttons cannot receive focus during the modal's opening animation. 203 initialFocus: () => closeButton || root.querySelector('[data-get-started-next]'), 204 onOpen: () => { 205 isOpen = false; 206 trackedStep = null; 207 hideDomainSearch(); 208 if (embla) { 209 setCarouselVisibility(false); 210 setContainerTransition(false); 211 embla.scrollTo(0); 212 // Modal is hidden on init; reInit after open to get correct height. 213 scheduleRefresh(0); 214 scheduleRefresh(120); 215 if (visibilityTimer) { 216 window.clearTimeout(visibilityTimer); 217 } 218 visibilityTimer = window.setTimeout(() => { 219 setContainerTransition(true); 220 setCarouselVisibility(true); 221 visibilityTimer = null; 222 }, 160); 223 } 224 isOpen = true; 225 trackSelectedStep(); 226 }, 227 onClose: () => { 228 isOpen = false; 229 hideDomainSearch(); 230 }, 231 }); 232 233 if (!modal) return; 234 235 // Close handled by shared modal close trigger. 236}; 237 238initGetStartedModal();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.