PageSourceSearch

https://sasklakes.ca/min-assets/js/custom-map-list/?t=5041

js sasklakes.ca collected 2026-10-06 08:48:51 UTC 9,541 bytes, 259 lines download raw bytes

1customElements.define('custom-map-list', class extends HTMLElement {
2    constructor() {
3        super();
4        this.list = this.querySelector('.list');
5        this._listData = null; // internal storage for reactive list data
6        this.loaded = false;
7    }
8
9    set listData(val) {
10        this._listData = val;
11        this.render();
12    }
13
14    set appendToSource({ source, newItems }) {
15        this.appendItemsToSource(source, newItems);
16    }
17
18    get listData() {
19        return this._listData;
20    }
21
22    async connectedCallback() {
23        if (this.loaded) return;
24        this.loaded = true;
25        this.groupCount = this.querySelectorAll('.results-group').length;
26
27        // click handling for items & group headers
28        this.list.addEventListener('click', (e) => {
29            const item = e.target.closest('.list-group-item');
30            const header = e.target.closest('[data-action="toggle-group"]');
31
32            if (item) {
33                const source = item.dataset.source;
34                const id = item.dataset.id;
35                this.dispatchEvent(new CustomEvent('itemClicked', { detail: { source, id } }));
36                this.activateItem(source, id);
37            } else if (header) {
38                const group = header.closest('.results-group');
39                if (group.classList.contains('disabled')) {
40                    this.dispatchEvent(new CustomEvent('updateSourceVisibility', {
41                        detail: {
42                            source: group.getAttribute('data-source'),
43                            enabled: true,
44                            origin: 'list'
45                        }
46                    }));
47                };
48                this.toggleGroup(group);
49            }
50        });
51
52        this.addEventListener('updateSourceVisibility', (e) => {
53            const { source, enabled } = e.detail;
54
55            const group = this.querySelector(`.results-group[data-source="${source}"]`);
56            if (group) {
57                if (enabled) {
58                    group.classList.remove('disabled');
59                } else {
60                    group.classList.add('disabled');
61                    this.collapseGroup(group);
62                }
63            }
64        });
65
66        this.addEventListener('resize', () => {
67            const group = this.querySelector('.results-group:not(.collapsed)');
68            if (group) {
69                this.fixGroupHeight(group);
70            }
71        });
72
73        this.initObservers();
74    }
75
76    initObservers() {
77        const options = { root: null, rootMargin: '0px', threshold: 0.1 };
78        this.observer = new IntersectionObserver(entries => {
79            entries.forEach(entry => {
80                if (entry.isIntersecting) {
81                    const groupEl = entry.target.closest('.results-group');
82                    const source = groupEl?.dataset.source;
83                    if (source) {
84                        const sentinel = groupEl.querySelector('.load-more-sentinel');
85                        if (sentinel) {
86                            sentinel.innerHTML = '<div class="text-center text-muted py-2">Loading...</div>';
87                        }
88                        this.dispatchEvent(new CustomEvent('loadMore', {
89                            detail: { source },
90                            bubbles: true
91                        }));
92                    }
93                }
94            });
95        }, options);
96
97        requestAnimationFrame(() => {
98            this.list.querySelectorAll('.load-more-sentinel').forEach(sentinel => {
99                this.observer.observe(sentinel);
100
101                const groupEl = sentinel.closest('.results-group');
102                if (groupEl && this.isInView(sentinel)) {
103                    const source = groupEl.dataset.source;
104                    if (source) {
105                        this.dispatchEvent(new CustomEvent('loadMore', { detail: { source }, bubbles: true }));
106                    }
107                }
108            });
109        });
110    }
111
112    isInView(el) {
113        const rect = el.getBoundingClientRect();
114        return rect.bottom > 0 && rect.top < window.innerHeight;
115    }
116
117
118    async render() {
119
120        if (!this._listData) return;
121
122        this.list.innerHTML = ''; // clear list
123
124        // disconnect old observer to prevent memory leaks
125        if (this.observer) {
126            this.observer.disconnect();
127        }
128
129        // reinit observer
130        const options = { root: null, rootMargin: '0px', threshold: 0.1 };
131        this.observer = new IntersectionObserver(entries => {
132            entries.forEach(entry => {
133                if (entry.isIntersecting) {
134                    const groupEl = entry.target.closest('.results-group');
135                    const source = groupEl?.dataset.source;
136                    if (source) {
137                        this.dispatchEvent(new CustomEvent('loadMore', {
138                            detail: { source },
139                            bubbles: true
140                        }));
141                    }
142                }
143            });
144        }, options);
145
146        for (const src of this._listData) {
147            // create group
148            const group = document.createElement('div');
149            group.className = 'results-group';
150            group.dataset.source = src.slug;
151
152            // header
153            const header = document.createElement('div');
154            header.className = 'results-group-header d-flex align-items-center justify-content-between';
155            header.setAttribute('role', 'button');
156            header.dataset.action = 'toggle-group';
157            header.innerHTML = `<div class="fw-semibold">${src.title} <span class="text-muted">(${src.totalItems || 0})</span></div><span class="group-chevron">▾</span>`;
158            group.appendChild(header);
159
160            // items container
161            const itemsContainer = document.createElement('div');
162            itemsContainer.className = 'results-group-items';
163
164            const itemsWrapper = document.createElement('div');
165            itemsWrapper.className = 'items';
166            itemsContainer.appendChild(itemsWrapper);
167            if (src.items && src.items) {
168                for (const item of src.items) {
169                    itemsWrapper.appendChild(this.buildItemHTML(item, src));
170                }
171            }
172
173            group.appendChild(itemsContainer);
174
175            // sentinel
176            const sentinel = document.createElement('div');
177            sentinel.className = 'load-more-sentinel';
178            itemsContainer.appendChild(sentinel);
179
180            // **observe the new sentinel**
181            this.observer.observe(sentinel);
182
183            this.list.appendChild(group);
184        }
185    }
186
187    buildItemHTML(item, src) {
188        let html;
189        if (src.templateFn) {
190            item.source = src.slug;
191            html = src.templateFn(item);
192        } else {
193            html = `<pre>${JSON.stringify(item, null, 2)}</pre>`;
194        }
195        const div = document.createElement('div');
196        div.innerHTML = html;
197        return div.firstElementChild;
198    }
199
200    appendItemsToSource(src, items) {
201        const groupEl = this.list.querySelector(`.results-group[data-source="${src.slug}"]`);
202        if (!groupEl) return;
203        const itemsContainer = groupEl.querySelector('.results-group-items .items');
204        for (const item of items) {
205            itemsContainer.appendChild(this.buildItemHTML(item, src));
206        }
207
208        if (items.length < 50) {
209            const sentinel = groupEl.querySelector('.load-more-sentinel');
210            if (sentinel) {
211                this.observer.unobserve(sentinel);
212                sentinel.innerHTML = '<div class="text-center text-muted py-2">No more items</div>';
213            }
214        }
215    }
216
217    toggleGroup(group) {
218        if (this.groupCount > 1) {
219            const chevron = group.querySelector('.group-chevron');
220            group.classList.toggle('collapsed');
221            chevron.textContent = group.classList.contains('collapsed') ? '▸' : '▾';
222
223            if (!group.classList.contains('collapsed')) {
224                this.querySelectorAll('.results-group').forEach(g => {
225                    if (g !== group) {
226                        this.collapseGroup(g);
227                    }
228                });
229            }
230        }
231    }
232
233    collapseGroup(group) {
234        group.classList.add('collapsed');
235        const c = group.querySelector('.group-chevron');
236        if (c) c.textContent = '▸';
237    }
238
239    fixGroupHeight(group) {
240        const content = group.querySelector('.results-group-items');
241        if (content) {
242            let availableHeight = this.clientHeight;
243            const headers = Array.from(this.querySelectorAll('.results-group-header'));
244
245            headers.forEach(h => {
246                availableHeight -= h.offsetHeight;
247            });
248
249            content.style.maxHeight = availableHeight + 'px';
250        }
251    }
252
253    activateItem(source, id) {
254        const item = this.list.querySelector(`.list-group-item[data-source="${source}"][data-id="${id}"]`);
255        if (!item) return;
256        this.list.querySelectorAll('.list-group-item').forEach(i => i.classList.remove('active'));
257        item.classList.add('active');
258    }
259});

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.