1/** 2 * airo-video-slots.js â Runtime script for video slot patching. 3 * 4 * Fetches /airo-media.json to discover which media slots have mediaType 'video', 5 * then uses a MutationObserver to replace <img> elements referencing those slots 6 * with <video> elements (autoplay, muted, loop, playsInline). 7 * 8 * Loaded synchronously in <head> so the observer is active before React hydrates. 9 * Works in both dev and production modes â in dev mode the Vite plugin also handles 10 * video patching via HMR, but this script acts as a fallback for direct page loads 11 * (e.g. opening the preview link in a new tab without the builder parent frame). 12 */ 13;(function () { 14 var SLOT_PREFIX_IMAGES = '/airo-assets/images/' 15 var SLOT_PREFIX_VIDEOS = '/airo-assets/videos/' 16 var mediaTypes = {} 17 18 function extractSlotPath(url) { 19 if (!url) return null 20 var prefixes = [SLOT_PREFIX_IMAGES, SLOT_PREFIX_VIDEOS] 21 for (var i = 0; i < prefixes.length; i++) { 22 var idx = url.indexOf(prefixes[i]) 23 if (idx !== -1) { 24 var after = url.substring(idx + prefixes[i].length) 25 return after.split('?')[0] 26 } 27 } 28 return null 29 } 30 31 function patchImg(img) { 32 if (!img.src) return 33 if (img.getAttribute('data-airo-video-patched')) return 34 var slotPath = extractSlotPath(img.src) 35 if (!slotPath || mediaTypes[slotPath] !== 'video') return 36 37 // Remove any existing video for this slot to prevent duplicates after re-renders 38 var existing = img.parentNode && img.parentNode.querySelector('video[data-slot="' + slotPath + '"]') 39 if (existing) existing.remove() 40 41 var videoUrl = img.src.replace(SLOT_PREFIX_IMAGES, SLOT_PREFIX_VIDEOS) 42 var video = document.createElement('video') 43 video.src = videoUrl 44 video.autoplay = true 45 video.muted = true 46 video.loop = true 47 video.playsInline = true 48 video.className = img.className 49 video.style.cssText = img.style.cssText 50 if (img.width) video.width = img.width 51 if (img.height) video.height = img.height 52 video.setAttribute('data-airo-video', '') 53 video.setAttribute('data-slot', slotPath) 54 55 img.setAttribute('data-airo-video-patched', 'true') 56 img.style.display = 'none' 57 if (img.parentNode) { 58 img.parentNode.insertBefore(video, img.nextSibling) 59 } 60 } 61 62 function patchBgElement(el) { 63 var bgImage = window.getComputedStyle(el).backgroundImage 64 if (!bgImage || bgImage === 'none') return 65 if (bgImage.indexOf(SLOT_PREFIX_IMAGES) === -1 && bgImage.indexOf(SLOT_PREFIX_VIDEOS) === -1) return 66 var urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/) 67 if (!urlMatch || !urlMatch[1]) return 68 var slotPath = extractSlotPath(urlMatch[1]) 69 if (!slotPath || mediaTypes[slotPath] !== 'video') return 70 71 // If already patched and video exists, just re-hide background (React may have restored it) 72 if (el.getAttribute('data-airo-video-bg-patched') === slotPath) { 73 el.style.backgroundImage = 'none' 74 return 75 } 76 77 // Remove any existing bg video to prevent duplicates 78 var existing = el.querySelector('video[data-airo-bg-video]') 79 if (existing) existing.remove() 80 81 el.style.backgroundImage = 'none' 82 el.setAttribute('data-airo-video-bg-patched', slotPath) 83 var videoUrl = urlMatch[1].replace(SLOT_PREFIX_IMAGES, SLOT_PREFIX_VIDEOS) 84 if (videoUrl.indexOf(SLOT_PREFIX_VIDEOS) === -1) { 85 videoUrl = SLOT_PREFIX_VIDEOS + slotPath 86 } 87 var video = document.createElement('video') 88 video.src = videoUrl 89 video.autoplay = true 90 video.muted = true 91 video.loop = true 92 video.playsInline = true 93 video.setAttribute('data-airo-bg-video', '') 94 video.setAttribute('data-slot', slotPath) 95 video.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:-1;' 96 var pos = window.getComputedStyle(el).position 97 if (pos === 'static') el.style.position = 'relative' 98 el.insertBefore(video, el.firstChild) 99 } 100 101 function patchAll() { 102 document.querySelectorAll('img').forEach(patchImg) 103 // Check elements with inline background styles or already-patched elements 104 document.querySelectorAll('[style*="background"], [data-airo-video-bg-patched]').forEach(patchBgElement) 105 // Also scan all elements for CSS-class-based background-images referencing our slot prefixes. 106 // patchBgElement uses getComputedStyle internally and bails early if no matching URL is found, 107 // so this is safe but slightly more expensive â only run when we have video slots. 108 var hasVideoSlots = false 109 for (var k in mediaTypes) { 110 if (mediaTypes[k] === 'video') { hasVideoSlots = true; break } 111 } 112 if (hasVideoSlots) { 113 document.querySelectorAll('section, div, header, main, [class*="hero"], [class*="banner"], [class*="background"]').forEach(function (el) { 114 if (!el.getAttribute('data-airo-video-bg-patched') && !el.hasAttribute('style')) { 115 patchBgElement(el) 116 } 117 }) 118 } 119 } 120 121 // Fetch manifest then start observing 122 fetch('/airo-media.json') 123 .then(function (res) { 124 if (!res.ok) return {} 125 return res.json() 126 }) 127 .then(function (manifest) { 128 for (var key in manifest) { 129 if (manifest[key] && manifest[key].mediaType) { 130 mediaTypes[key] = manifest[key].mediaType 131 } 132 } 133 // Patch existing images
134 patchAll() 135 // Observe future DOM changes â childList for new nodes, attributes for style changes 136 var isPatching = false 137 var observer = new MutationObserver(function (mutations) { 138 if (isPatching) return 139 isPatching = true 140 try { 141 for (var i = 0; i < mutations.length; i++) { 142 var mutation = mutations[i] 143 if (mutation.type === 'childList') { 144 var added = mutation.addedNodes 145 for (var j = 0; j < added.length; j++) { 146 var node = added[j] 147 if (node instanceof HTMLImageElement) { 148 patchImg(node) 149 } else if (node instanceof HTMLElement) { 150 node.querySelectorAll('img').forEach(patchImg) 151 // Check added elements for background-image video slots 152 patchBgElement(node) 153 node.querySelectorAll('[style*="background"]').forEach(patchBgElement) 154 } 155 } 156 } else if (mutation.type === 'attributes' && mutation.attributeName === 'style') { 157 var target = mutation.target 158 if (target instanceof HTMLElement) { 159 // Re-check: React may have re-rendered and set background-image back 160 // Remove stale patch flag so patchBgElement re-evaluates 161 if (target.getAttribute('data-airo-video-bg-patched')) { 162 var bg = target.style.backgroundImage 163 if (bg && bg !== 'none' && (bg.indexOf(SLOT_PREFIX_IMAGES) !== -1 || bg.indexOf(SLOT_PREFIX_VIDEOS) !== -1)) { 164 target.removeAttribute('data-airo-video-bg-patched') 165 patchBgElement(target) 166 } 167 } else { 168 patchBgElement(target) 169 } 170 } 171 } 172 } 173 } finally { 174 isPatching = false 175 } 176 }) 177 observer.observe(document.documentElement, { 178 childList: true, 179 subtree: true, 180 attributes: true, 181 attributeFilter: ['style'] 182 }) 183 184 // Re-fetch manifest periodically to pick up changes (dev mode only â HMR may not work due to CORS) 185 // Use __AIRO_DEV_MODE__ flag set by dev-supervisor, falling back to localhost check 186 var isDevMode = window.__AIRO_DEV_MODE__ === true || location.hostname === 'localhost' || location.hostname === '127.0.0.1' 187 if (isDevMode) { 188 var pollFailures = 0 189 var pollIntervalId = setInterval(function () { 190 fetch('/airo-media.json').then(function (r) { 191 if (!r.ok) return {} 192 return r.json() 193 }).then(function (m) { 194 pollFailures = 0 195 var changed = false 196 for (var k in m) { 197 if (m[k] && m[k].mediaType && mediaTypes[k] !== m[k].mediaType) { 198 mediaTypes[k] = m[k].mediaType 199 changed = true 200 } 201 } 202 if (changed) patchAll() 203 }).catch(function (err) { 204 pollFailures++ 205 if (pollFailures === 1) { 206 // eslint-disable-next-line no-console 207 console.warn('[airo-video-slots] manifest poll failed:', err.message || err) 208 } 209 if (pollFailures >= 5) { 210 clearInterval(pollIntervalId) 211 } 212 }) 213 }, 3000) 214 } 215 }) 216 .catch(function () { 217 // Manifest not available â no video slots to patch 218 }) 219})()
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.