1 2/** 3* ajax_log_stat(stat_name, stat_ids, stat_data, token) 4* 5* Record a client-side impression state to the server 6* See server handler: eluta.app.ajax_log_stat (implemented in mcsearch.py) 7* 8* Parameters: 9* - stat (string): A short event name identifying the action, generally 10* in the fornmat log_analysis is looking for, e.g. SPFE_IMPR. 11* 12* - stat_ids (string): One or more object (employer, position, etc.) IDs. 13* Multiple IDs can be passed by comma-separating them. 14* 15* - stat_data (string, optional): Any additional data to be logged. 16* 17* - token (string, optional): A JWT issued by ajax_jwt_issue('log_stat', ...) 18* If omitted, the function will attempt to use window.AJAX_LOG_STAT_TOKEN 19* 20* This basically mirrors the logStat function in mcsearch.py. 21* 22* Expected behavior: 23* - Sends a POST request to `ajax_dispatch` with audience = 'log_stat' 24* - Server validates: token, audience, stat name, stat_ids 25* - Server writes an event record via logStat() 26* (stat_name, stat_ids, data=stat_dataip, data='') 27* 28* Example: 29* ajax_log_stat('CTFE_IMPR', 'a1b2', '13'); 30* 31*/ 32function ajax_log_stat(stat_name, stat_ids, stat_data, token) { 33 token = token || window.AJAX_LOG_STAT_TOKEN; 34 35 try { 36 if (!token) { 37 console.warn("ajax_log_stat: missing token"); 38 return; 39 } 40 41 if (!stat_name || !stat_ids) { 42 console.warn("ajax_log_stat: missing stat or eid"); 43 return; 44 } 45 46 if (typeof stat_ids === 'string') { 47 stat_ids = stat_ids.split(','); 48 } 49 50 stat_data = stat_data || ''; 51 52 /* Prepare payload */ 53 const payload = { 54 stat_name: String(stat_name).trim(), 55 stat_ids: stat_ids, 56 stat_data: String(stat_data).trim() 57 }; 58 59 $.ajax({ 60 url: 'https://www.eluta.ca/ajax_dispatch', 61 method: 'POST', 62 contentType: 'application/json; charset=UTF-8', 63 data: JSON.stringify({ 64 _audience_: 'log_stat', 65 _token_: token, 66 payload: payload 67 }), 68 69 error(xhr, status, error) { 70 console.error( 71 "[ajax_log_stat] FAIL:", 72 status, 73 error, 74 xhr?.responseText 75 ); 76 } 77 }); 78 79 } catch (err) { 80 console.warn('ajax_log_stat failed:', err); 81 } 82} 83 84document.addEventListener("DOMContentLoaded", () => { 85 /** 86 * Helper function to wait for an element to appear in the DOM. 87 * @param {string} selector - The CSS selector for the target element. 88 * @param {number} timeout - The time (in milliseconds) to wait before rejecting. 89 * @returns {Promise<Element>} - Resolves with the target element if found within the timeout. 90 */ 91 const waitForElement = (selector, timeout = 5000) => { 92 return new Promise((resolve, reject) => { 93 const startTime = Date.now(); 94 const interval = setInterval(() => { 95 const element = document.querySelector(selector); 96 if (element) { 97 clearInterval(interval); 98 resolve(element); 99 } else if (Date.now() - startTime > timeout) { 100 clearInterval(interval); 101 reject(new Error(`Timeout: Target section ${selector} not found.`)); 102 } 103 }, 100); // Check every 100ms 104 }); 105 }; 106 107 /** 108 * Function to fetch and insert lazy-loaded content. 109 * @returns {Promise<void>} - Resolves when content is fully loaded and inserted. 110 */ 111 const loadContent = async () => { 112 /*const contentUrl = "./content.html";*/ 113 // Check if the preview parameter exists, loads content-preview for preview 114 const urlParams = new URLSearchParams(window.location.search); 115 const isPreview = urlParams.has("preview"); 116 const contentUrl = isPreview ? "./content-preview.html" : "./content.html"; 117 118 const lazyLoadContainer = document.getElementById("lazy-load-container"); 119 120 if (!lazyLoadContainer) { 121 console.warn("[LazyLoad] Lazy load container not found."); 122 return; 123 } 124 125 try { 126 const response = await fetch(contentUrl, { cache: "no-cache" }); 127 if (!response.ok) { 128 throw new Error(`Failed to load content: ${response.statusText}`); 129 } 130 131 const content = await response.text(); 132 lazyLoadContainer.innerHTML = content; 133 134 // Execute scripts within lazy-loaded content 135 const scripts = lazyLoadContainer.querySelectorAll("script");
136 scripts.forEach(script => { 137 const newScript = document.createElement("script"); 138 139 // EXCEPTION: allow competition-winners.js to load 140 if (script.src && script.src.includes("competition-winners.js")) { 141 newScript.src = script.src; 142 } else { 143 newScript.textContent = script.textContent; 144 } 145 146 script.replaceWith(newScript); 147 }); 148 149 // Initialize the article click handlers after content loads 150 initializeArticleExpansion(); 151 152 153 // Reset height of #winners after lazy load completes 154 setTimeout(() => { 155 const winnersSection = document.querySelector("#winners"); 156 if (winnersSection) { 157 winnersSection.style.height = "auto"; 158 } 159 }, 1000); // Delay to allow the content to fully render 160 161 } catch (error) { 162 console.error("[LazyLoad] Error during content load:", error); 163 } 164 }; 165 166 /** 167 * Initializes click event listeners for articles to expand them on click. 168 * Expands the `.featured-articles` section, hides the article grid, and displays 169 * the full article content with a "Back" link to return to the original state. 170 */ 171 const initializeArticleExpansion = () => { 172 const articles = document.querySelectorAll('.article'); 173 const featuredArticles = document.querySelector('.featured-articles'); 174 const articlesContainer = document.querySelector('.articles-container'); 175 const expandedArticle = document.querySelector('.expanded-article'); 176 177 if (!articles.length) { 178 console.warn("[Expansion] No articles found for expansion."); 179 return; 180 } 181 182 // Handle the Back link globally (attach it once) 183 expandedArticle.addEventListener('click', function (event) { 184 if (event.target.classList.contains('back-link')) { 185 // Smooth fade-out of the expanded article 186 expandedArticle.style.opacity = '0'; 187 featuredArticles.classList.remove('expanded'); 188 189 // Show the article grid smoothly after fade-out 190 setTimeout(() => { 191 expandedArticle.style.display = 'none'; 192 articlesContainer.style.display = 'grid'; 193 }, 400); // Matches the fade-out effect 194 } 195 }); 196 197 articles.forEach(article => { 198 article.addEventListener('click', function () { 199 const titleElement = this.querySelector('h3'); 200 const authorElement = this.querySelector('.author'); 201 const imgElement = this.querySelector('img'); 202 const fullTextElement = this.querySelector('.full-article-text'); 203 const bylineElement = fullTextElement.querySelector('.byline-content'); 204 const captionElement = this.querySelector('.image-caption-content'); 205 const articleType = this.getAttribute('data-article'); 206 207 if (titleElement && authorElement && imgElement && fullTextElement) { 208 const articleTitleElement = this.querySelector('h3'); 209 const expandedTitle = this.getAttribute('data-title') || articleTitleElement.innerText; // Use data-title if available 210 const author = authorElement.innerText; 211 const imgSrc = imgElement.src; 212 213 const leadParagraph = fullTextElement.querySelector('p.lead'); 214 const leadContent = leadParagraph ? `<div class="lead-text">${leadParagraph.outerHTML}</div>` : ''; 215 const imageCaption = captionElement ? captionElement.innerHTML : ''; 216 const bylineContent = bylineElement ? bylineElement.outerHTML.replace('display: none;', 'display: block;') : ''; 217 218 // Set character limits based on screen size 219 const getPageSizeInChars = () => window.innerWidth <= 400 ? 700 : 1200; 220 let pageSizeInChars = getPageSizeInChars(); 221 let currentPage = 0; 222 223 // Adjust character count dynamically on window resize 224 window.addEventListener('resize', () => { 225 pageSizeInChars = getPageSizeInChars(); 226 }); 227 228 // Static content is rendered only once 229 expandedArticle.innerHTML = ` 230 <div class="expanded-article-content"> 231 <button class="back-link">×</button> 232 <div class="expanded-article-image-wrapper"> 233 <img src="${imgSrc}" alt="${expandedTitle}"> 234 ${imageCaption ? `<p class="image-caption">${imageCaption}</p>` : ''} 235 </div> 236 <div class="expanded-article-text"> 237 <h2>${expandedTitle}</h2> 238 ${leadContent} 239 <div id="paginated-content"></div> 240 <div class="pagination-buttons"> 241 <button class="prev-page" disabled>â</button> 242 <button class="next-page">â</button> 243 </div> 244 </div> 245 </div> 246 `; 247 248 // Function to render full content without pagination for "Methodology" 249 const renderFullContent = () => { 250 const paginatedDiv = document.getElementById('paginated-content'); 251 paginatedDiv.innerHTML = ` 252 <div class="full-article-wrapper"> 253 ${fullTextElement.innerHTML} 254 ${bylineContent ? `<div class="byline-wrapper">${bylineContent}</div>` : ''} 255 </div> 256 `; 257 }; 258 259 // Function to split content by character count, ending on full sentences 260 const splitContentByCharacters = (content, maxChars) => { 261 const paragraphs = Array.from(content.querySelectorAll('p:not(.lead)')); 262 const pages = []; 263 let currentPageContent = ''; 264 let currentCharCount = 0; 265 266 paragraphs.forEach(paragraph => { 267 // Improved sentence splitting: handles punctuation, quotes, and edge cases 268 const sentences = paragraph.innerHTML.match(/(?:\d+\.\d+|[^.!?])+[.!?](?:["']|â|â)?|(?:\d+\.\d+|[^.!?])+$/g) || [paragraph.innerHTML]; 269 let paragraphContent = ''; 270
271 sentences.forEach(sentence => { 272 const charCount = sentence.length; 273 274 // If adding this sentence exceeds maxChars, push the current page content 275 if (currentCharCount + charCount > maxChars) { 276 if (currentPageContent.trim()) { 277 pages.push(currentPageContent.trim()); // Only push non-empty content 278 } 279 currentPageContent = ''; 280 currentCharCount = 0; 281 } 282 283 // Add sentence to the current paragraph 284 paragraphContent += `${sentence} `; 285 currentCharCount += charCount; 286 }); 287 288 // Add the accumulated paragraph to the current page 289 if (paragraphContent.trim()) { 290 currentPageContent += `<p>${paragraphContent.trim()}</p>`; 291 } 292 }); 293 294 // Push any remaining content as the final page 295 if (currentPageContent.trim()) { 296 pages.push(currentPageContent.trim()); 297 } 298 299 return pages; 300 }; 301 302 // Apply different logic for the "Methodology" article 303 if (articleType === "methodology") { 304 renderFullContent(); // Show full content without pagination 305 const paginationButtons = expandedArticle.querySelector('.pagination-buttons'); 306 if (paginationButtons) { 307 paginationButtons.style.display = 'none'; 308 } 309 } else { 310 const pageSizeInChars = window.innerWidth <= 400 ? 700 : 1500; 311 const pages = splitContentByCharacters(fullTextElement, pageSizeInChars); 312 let currentPage = 0; 313 314 /*const renderPage = (page) => { 315 const isLastPage = page === pages.length - 1; 316 317 const paginatedDiv = document.getElementById('paginated-content'); 318 const contentForPage = ` 319 ${pages[page]} 320 ${isLastPage && bylineContent ? `<div class="byline-wrapper">${bylineContent}</div>` : ''} 321 `; 322 323 paginatedDiv.innerHTML = contentForPage; 324 325 expandedArticle.querySelector('.prev-page').disabled = page === 0; 326 expandedArticle.querySelector('.next-page').disabled = isLastPage; 327 328 };*/ 329 // Ensure pagination-info is created before rendering pages 330 // Ensure pagination-info is created before rendering pages 331 const paginationButtons = expandedArticle.querySelector('.pagination-buttons'); 332 let paginationInfo = expandedArticle.querySelector('.pagination-info'); 333 334 if (!paginationInfo) { 335 paginationInfo = document.createElement('div'); 336 paginationInfo.classList.add('pagination-info'); 337 338 // Insert pagination info before the first button (prev button) 339 paginationButtons.insertBefore(paginationInfo, paginationButtons.firstChild); 340 } 341 342 const renderPage = (page) => { 343 if (!pages || pages.length === 0) return; // Ensure pages exist 344 345 const isLastPage = page === pages.length - 1; 346 const paginatedDiv = document.getElementById('paginated-content'); 347 348 paginatedDiv.innerHTML = ` 349 ${pages[page]} 350 ${isLastPage && bylineContent ? `<div class="byline-wrapper">${bylineContent}</div>` : ''} 351 `; 352 353 expandedArticle.querySelector('.prev-page').disabled = page === 0; 354 expandedArticle.querySelector('.next-page').disabled = isLastPage; 355 356 // Update pagination info 357 paginationInfo.textContent = `Page ${page + 1} of ${pages.length}`; 358 }; 359 360 // Ensure the first page is rendered immediately 361 renderPage(0); 362 363 364 365 expandedArticle.addEventListener('click', function (e) { 366 if (e.target.classList.contains('next-page')) { 367 if (currentPage < pages.length - 1) { 368 currentPage++; 369 renderPage(currentPage); 370 } 371 } 372 373 if (e.target.classList.contains('prev-page')) { 374 if (currentPage > 0) { 375 currentPage--; 376 renderPage(currentPage); 377 } 378 } 379 }); 380 } 381 382 // Expand and show content smoothly 383 featuredArticles.classList.add('expanded'); 384 setTimeout(() => { 385 articlesContainer.style.display = 'none'; 386 expandedArticle.style.display = 'block'; 387 expandedArticle.style.opacity = '1'; 388 }, 400); 389 390 // Back button handling 391 expandedArticle.addEventListener('click', function (e) { 392 if (e.target.classList.contains('back-link')) { 393 expandedArticle.style.opacity = '0'; 394 featuredArticles.classList.remove('expanded'); 395 setTimeout(() => { 396 expandedArticle.style.display = 'none'; 397 articlesContainer.style.display = 'grid'; 398 }, 400); 399 } 400 }); 401 402 } else { 403 console.error("[Expansion] Missing content in the article."); 404 } 405 }); 406 }); 407 /*Add link for articles*/ 408 const params = new URLSearchParams(window.location.search); 409 const articleFromUrl = params.get('article'); 410 411 if (articleFromUrl) { 412 const targetArticle = document.querySelector( 413 `.article[data-article="${articleFromUrl}"]` 414 ); 415 if (targetArticle) { 416 targetArticle.click(); 417 } 418 } 419 }; 420 421 /** 422 * Scrolls to the specified section smoothly. 423 * @param {string} targetId - The ID of the target section to scroll to. 424 */ 425 const scrollToSection = (targetId) => { 426 const targetSection = document.querySelector(targetId);
427 if (targetSection) { 428 targetSection.scrollIntoView({ behavior: "smooth", block: "start" }); 429 430 // Reset the height of #winners back to auto after scrolling completes 431 setTimeout(() => { 432 const winnersSection = document.querySelector("#winners"); 433 if (winnersSection) { 434 winnersSection.style.height = "auto"; 435 } 436 }, 1000); // 1 second delay to allow scrolling to complete 437 438 } else { 439 console.warn(`[Navbar] Target section ${targetId} not found for scrolling.`); 440 } 441 }; 442 443 /** 444 * Handles navbar link clicks with lazy-loading logic. 445 */ 446 const navbarLinks = document.querySelectorAll('.one-page-menu a[data-href]'); 447 navbarLinks.forEach(link => { 448 link.addEventListener("click", async (event) => { 449 event.preventDefault(); 450 const targetId = link.getAttribute("data-href"); 451 452 let targetSection = document.querySelector(targetId); 453 454 if (!targetSection) { 455 try { 456 await loadContent(); 457 targetSection = await waitForElement(targetId); 458 scrollToSection(targetId); 459 } catch (error) { 460 console.error("[Navbar] Error during lazy load or scrolling:", error); 461 } 462 } else { 463 scrollToSection(targetId); 464 } 465 }); 466 }); 467 468 /** 469 * Intersection Observer to trigger lazy loading when the container becomes visible. 470 */ 471 const lazyLoadContainer = document.getElementById("lazy-load-container"); 472 if (lazyLoadContainer) { 473 const observer = new IntersectionObserver( 474 (entries, observerInstance) => { 475 entries.forEach(entry => { 476 if (entry.isIntersecting) { 477 loadContent().catch(error => { 478 console.error("[LazyLoad] Error in IntersectionObserver:", error); 479 }); 480 observerInstance.unobserve(entry.target); 481 } 482 }); 483 }, 484 { root: null, threshold: 0.1 } // Trigger when 10% of the container is visible 485 ); 486 487 observer.observe(lazyLoadContainer); 488 } 489}); 490 491/*Helper to scroll to article container when link is used*/ 492 493window.addEventListener('load', () => { 494 const params = new URLSearchParams(window.location.search); 495 const articleFromUrl = params.get('article'); 496 if (!articleFromUrl) return; 497 498 const lazyLoadContainer = document.getElementById('lazy-load-container'); 499 if (!lazyLoadContainer) return; 500 501 // Scroll the lazy-load target into view to trigger your existing lazy logic 502 lazyLoadContainer.scrollIntoView({ behavior: 'smooth' }); 503});
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.