PageSourceSearch

https://www.cbe.ab.ca/api/v1/queries/ie-query.js

js cbe.ab.ca collected 2026-09-24 08:59:20 UTC 11,937 bytes, 315 lines download raw bytes

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.