1/** 2 * IE Query Content Loader 3 * 4 * This script handles loading content for custom <ie-query> elements when they become visible in the viewport. 5 * It extracts data attributes from these elements to construct API URLs, fetches content from the server, 6 * and handles pagination for the loaded content. 7 */ 8(function() { 9 /** 10 * Creates an API URL based on the data attributes of an ie-query element 11 * @param {HTMLElement} element - The ie-query element 12 * @param {string|null} page - Optional page number for pagination 13 * @returns {string} The constructed API URL 14 */ 15 function createQueryUrl(element, page = null) { 16 const queryId = element.getAttribute('data-query-id') 17 const templateId = element.getAttribute('data-template-id') 18 const pageSize = element.getAttribute('data-page-size') 19 20 let url = `/api/v1/queries/compile?queryID=${queryId}` 21 22 if (templateId) { 23 url += `&templateID=${templateId}` 24 } 25 26 if (pageSize) { 27 url += `&pageSize=${pageSize}` 28 } 29 30 if (page) { 31 url += `&page=${page}` 32 } 33 34 const siteId = getQueryParam('siteId') 35 if (siteId) { 36 url += `&siteId=${siteId}` 37 } 38 39 if (getQueryParam('hash')) { 40 url += `&_=${Date.now()}` 41 } 42 43 return url 44 } 45 46 /** 47 * Retrieves a query parameter value from the URL 48 * @param paramName 49 * @returns {string} 50 */ 51 function getQueryParam(paramName) { 52 const urlParams = new URLSearchParams(window.location.search) 53 return urlParams.get(paramName) 54 } 55 56 /** 57 * Creates and displays a loading spinner overlay on top of the specified element 58 * @param {HTMLElement} element - The element to show the spinner in 59 */ 60 function showSpinner(element) { 61 // Set minimum height if element is too small or empty 62 const minHeight = 200 // px 63 const currentHeight = element.offsetHeight 64 const originalPosition = getComputedStyle(element).position 65 if (currentHeight < minHeight) { 66 element.style.minHeight = `${minHeight}px` 67 } 68 // If position is static, set it to relative for proper overlay positioning 69 if (originalPosition === 'static') { 70 element.style.position = 'relative' 71 } 72 element.style.display = 'block' 73 74 // Create overlay container 75 const overlay = document.createElement('div') 76 overlay.className = 'ie-query-spinner-overlay' 77 overlay.style.cssText = ` 78 position: absolute; 79 top: 0; 80 left: 0; 81 width: 100%; 82 height: 100%; 83 background-color: rgba(255, 255, 255, 0.7); 84 display: flex; 85 justify-content: center; 86 align-items: center; 87 z-index: 1000; 88 ` 89 90 // Create spinner element 91 const spinner = document.createElement('div') 92 spinner.className = 'ie-query-spinner' 93 94 // Create spinner animation 95 const spinnerAnimation = document.createElement('div') 96 spinnerAnimation.style.cssText = ` 97 width: 40px; 98 height: 40px; 99 border: 4px solid rgba(0, 0, 0, 0.1); 100 border-radius: 50%; 101 border-top-color: #333; 102 animation: ie-query-spin 1s ease-in-out infinite; 103 ` 104 105 // Add spinner animation style to page if not already there 106 if (!document.getElementById('ie-query-spinner-style')) { 107 const style = document.createElement('style') 108 style.id = 'ie-query-spinner-style' 109 style.textContent = ` 110 @keyframes ie-query-spin { 111 to { transform: rotate(360deg); } 112 } 113 ` 114 document.head.appendChild(style) 115 } 116 117 spinner.appendChild(spinnerAnimation) 118 overlay.appendChild(spinner) 119 element.appendChild(overlay) 120 } 121 122 /** 123 * Sets up click handlers for pagination links 124 * @param {HTMLElement} element - The ie-query element containing pagination links 125 */ 126 function setupPaginationHandlers(element) { 127 const pagerLinks = element.querySelectorAll('.pager a.page') 128 129 pagerLinks.forEach(link => { 130 link.addEventListener('click', function(e) { 131 e.preventDefault() 132 133 const page = this.getAttribute('data-page') 134 fetchAndUpdateContent(element, page) 135 }) 136 }) 137 } 138 139 /** 140 * Fetches content from the server and updates the ie-query element 141 * @param {HTMLElement} element - The ie-query element to update 142 * @param {string|null} page - Optional page number for pagination 143 * @returns {Promise<void>} 144 */ 145 function fetchAndUpdateContent(element, page = null) { 146 showSpinner(element) 147 148 const url = createQueryUrl(element, page) 149 150 return fetch(url, { 151 credentials: 'include' // This ensures cookies are sent with the request 152 }) 153 .then(response => { 154 if (!response.ok) { 155 element.innerHTML = `<div class="ie-query-error" style="padding: 10px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;"> 156 Error: Server responded with status ${response.status} 157 </div>` 158 return Promise.reject(new Error(`Server responded with ${response.status}`)) 159 } 160 return response.text() 161 }) 162 .then(html => { 163 // Parse HTML 164 const parser = new DOMParser() 165 const doc = parser.parseFromString(html, 'text/html') 166
167 // Find the div.ie-query element 168 const queryContent = doc.querySelector('div.ie-query') 169 if (queryContent) { 170 element.innerHTML = '' 171 element.appendChild(queryContent.cloneNode(true)) 172 173 // Set up pagination click handlers 174 setupPaginationHandlers(element) 175 } else { 176 element.innerHTML = 'Error: Content not found in response' 177 console.error('Could not find div.ie-query in response') 178 } 179 180 const styles = doc.querySelector('style[data-template-id]') 181 if (styles) { 182 const templateID = styles.getAttribute('data-template-id') 183 // Check if the style is already in the document and append it if not 184 if (!document.querySelector(`style[data-template-id="${templateID}"]`)) { 185 document.head.appendChild(styles.cloneNode(true)) 186 } 187 } 188 189 const script = doc.querySelector('script[data-template-id]') 190 if (script) { 191 const templateID = script.getAttribute('data-template-id') 192 // Check if the script is already in the document and append it if not 193 if (!document.querySelector(`script[data-template-id="${templateID}"]`)) { 194 const newScript = document.createElement('script') 195 newScript.setAttribute('data-template-id', templateID) 196 newScript.textContent = script.textContent 197 document.body.appendChild(newScript) 198 } 199 } 200 }) 201 .catch(error => { 202 console.error('Error fetching query content:', error)
203 // Only display the error if it hasn't already been displayed 204 if (!element.querySelector('.ie-query-error')) { 205 element.innerHTML = `<div class="ie-query-error" style="padding: 10px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;"> 206 Error loading content: ${error.message} 207 </div>` 208 } 209 }) 210 } 211 212 // Serial loading: multiple ie-query elements can all start out "visible" because 213 // they collapse to zero/small height before content arrives. Once the first one 214 // renders, the rest typically get pushed below the fold. We drain one at a time 215 // and re-check viewport position before each fetch so deferred elements only load 216 // after the user actually scrolls them back into view. 217 const loadedElements = new WeakSet() 218 const enqueuedElements = new WeakSet() 219 const loadQueue = [] 220 let isDraining = false 221 let intersectionObserver = null 222 223 /** 224 * Adds an element to the load queue and kicks the drain loop if idle. 225 * @param {HTMLElement} element 226 */ 227 function enqueueForLoad(element) { 228 if (loadedElements.has(element) || enqueuedElements.has(element)) { 229 return 230 } 231 enqueuedElements.add(element) 232 loadQueue.push(element) 233 drainLoadQueue() 234 } 235 236 /** 237 * Checks whether an element is within (or close to) the viewport. Mirrors the 238 * IntersectionObserver's rootMargin so the manual re-check stays consistent. 239 * @param {HTMLElement} element 240 * @returns {boolean} 241 */ 242 function isNearViewport(element) { 243 const margin = 100 244 const rect = element.getBoundingClientRect() 245 const viewportHeight = window.innerHeight || document.documentElement.clientHeight 246 return rect.bottom > -margin && rect.top < viewportHeight + margin 247 } 248 249 /** 250 * Drains the load queue one element at a time. Each iteration re-checks the 251 * element's current viewport position; elements pushed out of view by prior 252 * renders are skipped and will be re-enqueued by the IntersectionObserver when 253 * they next intersect. 254 */ 255 async function drainLoadQueue() { 256 if (isDraining) { 257 return 258 } 259 isDraining = true 260 try { 261 while (loadQueue.length > 0) { 262 const element = loadQueue.shift() 263 enqueuedElements.delete(element) 264 if (loadedElements.has(element)) { 265 continue 266 } 267 if (!isNearViewport(element)) { 268 // Pushed out of view by a prior render; wait for IO to re-fire. 269 continue 270 } 271 try { 272 await fetchAndUpdateContent(element) 273 loadedElements.add(element) 274 if (intersectionObserver) { 275 intersectionObserver.unobserve(element) 276 } 277 } catch (error) { 278 // Error is already rendered into the element; keep draining. 279 } 280 } 281 } finally { 282 isDraining = false 283 } 284 } 285 286 /** 287 * Sets up Intersection Observer to detect when ie-query elements become visible 288 */ 289 function setupIntersectionObserver() { 290 intersectionObserver = new IntersectionObserver((entries) => { 291 entries.forEach(entry => { 292 if (entry.isIntersecting) { 293 enqueueForLoad(entry.target) 294 } 295 }) 296 }, { 297 rootMargin: '100px', // Start loading slightly before element becomes visible 298 threshold: 0.1 // Trigger when at least 10% of the element is visible 299 }) 300 301 // Find all ie-query elements and observe them 302 const queryElements = document.querySelectorAll('ie-query') 303 queryElements.forEach(element => { 304 intersectionObserver.observe(element) 305 }) 306 } 307 308 // Initialize when the DOM is fully loaded 309 if (document.readyState === 'loading') { 310 document.addEventListener('DOMContentLoaded', setupIntersectionObserver) 311 } else { 312 // Document already loaded 313 setupIntersectionObserver() 314 } 315})()
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.