PageSourceSearch

https://canadastop100.com/assets/js/competition-lazyload.js?v=2026-01-19-1907

js canadastop100.com collected 2026-10-02 03:25:51 UTC 18,503 bytes, 503 lines download raw bytes

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">&times;</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.