PageSourceSearch

https://www.heritagemuseum.gov.hk/assets/js/dynamic-popup.js

js heritagemuseum.gov.hk collected 2026-10-02 07:47:30 UTC 15,389 bytes, 300 lines download raw bytes

1console.log('dynamic-popup.js loaded at ' + new Date().toLocaleString('en-US', { timeZone: 'Asia/Hong_Kong' }));
2
3// Counter for unique modal and carousel IDs
4let modalCounter = 0;
5
6// Function to create and show a modal with a carousel
7function createCarouselModal(slides, triggerIndex) {
8    modalCounter++;
9    const modalId = `dynamicModal-${modalCounter}`;
10    const carouselId = `carousel-${modalCounter}`;
11    const counterId = `counter-${modalCounter}`;
12
13    console.log(`Creating modal ${modalId} with carousel ${carouselId}, counter ${counterId}, triggerIndex: ${triggerIndex}, slides:`, slides);
14
15    // Validate slides array
16    if (!Array.isArray(slides) || slides.length === 0) {
17        console.error(`Invalid slides array for modal ${modalId}:`, slides);
18        slides = [{ url: 'placeholder.jpg', title: '', description: '', year: '' }];
19        triggerIndex = 0;
20    }
21
22    // Create modal HTML with dynamic counter ID and fade effect
23    let currentLang=getLang();
24    let closeAriaLabel= currentLang == "zh-hk" ? '關閉' : currentLang == "zh-cn" ? '关闭' : 'Close'
25    const modalHtml = `
26      <div class="modal fade dynamic-modal" id="${modalId}" tabindex="-1" aria-labelledby="${modalId}-label" aria-hidden="true">
27        <div class="modal-dialog modal-fullscreen">
28          <div class="modal-content">
29            <div class="modal-header">
30              <div class="carousel-controls">
31                <button class="carousel-control-prev" type="button" data-bs-target="#${carouselId}" data-bs-slide="prev">
32                  <span class="carousel-control-prev-icon" aria-hidden="true"></span>
33                  <span class="visually-hidden">Previous</span>
34                </button>
35                <div class="custom-counter" id="${counterId}">1 / ${slides.length}</div>
36                <button class="carousel-control-next" type="button" data-bs-target="#${carouselId}" data-bs-slide="next">
37                  <span class="carousel-control-next-icon" aria-hidden="true"></span>
38                  <span class="visually-hidden">Next</span>
39                </button>
40              </div>
41              <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${closeAriaLabel}"></button>
42            </div>
43            <div class="modal-body">
44              <div id="${carouselId}" class="carousel slide carousel-fade" data-bs-interval="false" data-bs-ride="false" data-bs-pause="hover" data-bs-touch="true">
45                <div class="carousel-inner" id="${carouselId}-inner"></div>
46                <div class="carousel-indicators" id="${carouselId}-indicators">
47                  ${slides.map((_, index) => `
48                    <button type="button" data-bs-target="#${carouselId}" data-bs-slide-to="${index}" ${index === 0 ? 'class="active" aria-current="true"' : ''} aria-label="Slide ${index + 1}"></button>
49                  `).join('')}
50                </div>
51            </div>
52          </div>
53        </div>
54      </div>
55    `;
56
57    // Append modal to body
58    if (!document.body) {
59        console.error(`document.body is not available for modal ${modalId}`);
60        return null;
61    }
62    try {
63        document.body.insertAdjacentHTML('beforeend', modalHtml);
64        console.log(`Modal ${modalId} appended to DOM`);
65    } catch (error) {
66        console.error(`Error appending modal ${modalId} to DOM:`, error);
67        return null;
68    }
69
70    // Delay to ensure DOM is ready
71    return new Promise((resolve) => {
72        setTimeout(() => {
73            // Get DOM elements
74            const modalElement = document.getElementById(modalId);
75            const carouselElement = document.getElementById(carouselId);
76            const inner = document.getElementById(`${carouselId}-inner`);
77            const counter = document.getElementById(counterId);
78            const closeButton = modalElement ? modalElement.querySelector('.btn-close') : null;
79
80            // Validate elements
81            if (!modalElement || !carouselElement || !inner || !counter || !closeButton) {
82                console.error(`Missing DOM elements for modal ${modalId}: modal=${!!modalElement}, carousel=${!!carouselElement}, inner=${!!inner}, counter=${!!counter}, closeButton=${!!closeButton}`);
83                return resolve(null);
84            }
85            console.log(`Found counter ${counterId} for carousel ${carouselId}`);
86
87            // Populate carousel items
88            try {
89                slides.forEach((slide, index) => {
90                    const isActive = index === 0 ? 'active' : '';
91                    const itemHtml = `
92                        <div class="carousel-item ${isActive}">
93                            <img src="${slide.url || 'placeholder.jpg'}" alt="${slide.title || 'Slide ' + (index + 1)}" class="d-block">
94                            
95                            <p class="carousel-item-description">${slide.description || ''}</p>
96                        </div>
97                    `;//<p class="carousel-item-year"> ${slide.year === "" ? '' : 'Year: ' + slide.year}</p>
98                    inner.insertAdjacentHTML('beforeend', itemHtml);
99                });
100                console.log(`Carousel ${carouselId} populated with ${slides.length} slides`);
101            } catch (error) {
102                console.error(`Error populating carousel ${carouselId}:`, error);
103                inner.innerHTML = '<div class="carousel-item active"><div>No slides available</div></div>';
104                counter.textContent = '1 / 1';
105                return resolve(null);
106            }
107
108            // Show modal
109            if (!window.bootstrap || !window.bootstrap.Modal) {
110                console.error(`Bootstrap Modal not available for modal ${modalId}. Ensure Bootstrap JS is loaded.`);
111                counter.textContent = `1 / ${slides.length}`;
112                alert('Bootstrap Modal not available. Please ensure Bootstrap JS is included.');
113                return resolve(null);
114            }
115
116            let modalInstance;
117            try {
118                modalInstance = new bootstrap.Modal(modalElement, {
119                    keyboard: true,
120                    backdrop: true
121                });
122                modalInstance.show();
123                console.log(`Modal ${modalId} shown`);
124            } catch (error) {
125                console.error(`Error initializing modal ${modalId}:`, error);
126                counter.textContent = `1 / ${slides.length}`;
127                alert(`Failed to show modal ${modalId}. Check console for details.`);
128                return resolve(null);
129            }
130
131            // Initialize carousel
132            let bsCarousel;
133            try {
134                bsCarousel = new bootstrap.Carousel(carouselElement, {
135                    interval: false, // Disable auto-slide
136                    touch: true,
137                    keyboard: true,
138                    pause: 'hover',
139                    ride: false
140                });
141                console.log(`Carousel ${carouselId} initialized`);
142
143                // Update counter on slide change completion
144                carouselElement.addEventListener('slid.bs.carousel', (event) => {
145                    const counterElement = document.getElementById(counterId);
146                    if (counterElement) {
147                        counterElement.textContent = `${event.to + 1} / ${slides.length}`;
148                        console.log(`Slid to: ${event.to + 1} / ${slides.length} in carousel ${carouselId}`);
149                    } else {
150                        console.error(`Counter element ${counterId} not found during slid event for carousel ${carouselId}`);
151                    }
152                });
153                console.log(`Added slid.bs.carousel listener for carousel ${carouselId}`);
154
155                // Debug: Log slide event
156                carouselElement.addEventListener('slide.bs.carousel', (event) => {
157                    console.log(`Slide event triggered: Moving to ${event.to + 1} / ${slides.length} in carousel ${carouselId}`);
158                });
159                console.log(`Added slide.bs.carousel listener for carousel ${carouselId}`);
160
161                // Fallback: Manual counter update for controls and indicators
162                const controls = carouselElement.querySelectorAll('.carousel-control-prev, .carousel-control-next, .carousel-indicators button');
163                controls.forEach((control) => {
164                    control.addEventListener('click', () => {
165                        setTimeout(() => {
166                            const activeIndex = Array.from(carou
166selElement.querySelectorAll('.carousel-item')).findIndex(item => item.classList.contains('active'));
167                            const counterElement = document.getElementById(counterId);
168                            if (counterElement && activeIndex >= 0) {
169                                counterElement.textContent = `${activeIndex + 1} / ${slides.length}`;
170                                console.log(`Manual counter update: ${activeIndex + 1} / ${slides.length} in carousel ${carouselId}`);
171                            } else {
172                                console.error(`Manual counter update failed: counter=${!!counterElement}, activeIndex=${activeIndex} in carousel ${carouselId}`);
173                            }
174                        }, 50);
175                    });
176                });
177                console.log(`Added manual control listeners for carousel ${carouselId}`);
178
179                // Set initial slide and counter
180                counter.textContent = `1 / ${slides.length}`;
181                console.log(`Initial counter set to: 1 / ${slides.length} for carousel ${carouselId}`);
182                bsCarousel.to(0);
183            } catch (error) {
184                console.error(`Error initializing carousel ${carouselId}:`, error);
185                counter.textContent = `1 / ${slides.length}`;
186                return resolve(null);
187            }
188
189            // Explicitly handle close button click
190            closeButton.addEventListener('click', () => {
191                console.log(`Close button clicked for modal ${modalId}`);
192                modalInstance.hide();
193                $('html').css('overflow-y', 'auto');
194            }, { once: true });
195
196            // Clean up modal when hidden
197            modalElement.addEventListener('hidden.bs.modal', () => {
198                console.log(`Modal ${modalId} removed`);
199                modalElement.remove();
200            }, { once: true });
201
202            resolve(modalInstance);
203        }, 100);
204    });
205}
206
207// Attach click handlers to gallery triggers
208function attachGalleryTriggers() {
209    console.log('Attaching gallery trigger listeners');
210    const triggers = document.querySelectorAll('.gallery-trigger');
211    if (triggers.length === 0) {
212        // console.warn('No gallery-trigger elements found. Retrying in 500ms...');
213        setTimeout(attachGalleryTriggers, 500);
214        return;
215    }
216    triggers.forEach((trigger, index) => {
217        // Remove existing listeners to prevent duplicates
218        trigger.removeEventListener('click', trigger.clickHandler);
219        trigger.clickHandler = (event) => {
220            event.preventDefault();
221            console.log('View Gallery clicked:', {
222                slide: trigger.dataset.slide,
223                desc: trigger.dataset.desc,
224                // year: trigger.dataset.year,
225                title: trigger.dataset.title,
226                triggerIndex: index
227            });
228            try {
229                // Parse comma-separated data attributes with fallback
230                const urls = trigger.dataset.slide ? trigger.dataset.slide.split(',').map(s => s.trim() || 'placeholder.jpg') : ['placeholder.jpg'];
231                const descriptionJsonParse=(desc)=>{
232                    let description=desc;
233                    try{
234                        description=JSON.parse(description).map(s => s.trim() || '');
235                    }catch(e){
236                        description=description.split(',').map(s => s.trim() || '');
237                    }
238                    return description;
239                }
240                const descriptions = trigger.dataset.desc ? /*trigger.dataset.desc.split('\\')*/
241                    trigger.dataset.desc.includes("[") && trigger.dataset.desc.includes("]")
242                        ? descriptionJsonParse(trigger.dataset.desc)
243                        : trigger.dataset.desc.split(',').map(s => s.trim() || '') 
244                        : [''];
245                // const years = trigger.dataset.year ? trigger.dataset.year.split(',').map(s => s.trim() || '') : [''];
246                const titles = trigger.dataset.title ? trigger.dataset.title.split(',').map(s => s.trim() || '') : [''];
247
248                // Validate at least one valid URL
249                if (!urls.some(url => url && url !== 'placeholder.jpg')) {
250                    console.error('No valid slide URLs provided for trigger index:', index);
251                    return;
252                }
253
254                // Normalize array lengths to the longest array
255                const maxLength = Math.max(urls.length, descriptions.length,  titles.length);//years.length,
256                urls.length = maxLength;
257                descriptions.length = maxLength;
258                // years.length = maxLength;
259                titles.length = maxLength;
260
261                // Fill missing values with fallbacks
262                for (let i = 0; i < maxLength; i++) {
263                    urls[i] = urls[i] || 'placeholder.jpg';
264                    descriptions[i] = descriptions[i] || '';
265                    // years[i] = years[i] && !isNaN(parseInt(years[i], 10)) ? parseInt(years[i], 10) : '';
266                    titles[i] = titles[i] || '';
267                }
268
269                // Combine into slides array
270                const slides = urls.map((url, i) => ({
271                    url,
272                    description: descriptions[i],
273                    // year: years[i],
274                    title: titles[i]
275                }));
276                console.log(`Parsed slides for trigger index ${index}:`, slides);
277
278                // Create modal asynchronously
279                createCarouselModal(slides, 0).then((modalInstance) => {
280                    if (!modalInstance) {
281                        console.error(`Failed to create modal for trigger index ${index}`);
282                    }
283                });
284            } catch (error) {
285                console.error('Error parsing data attributes for trigger index ' + index + ':', error);
286            }
287        };
288        trigger.addEventListener('click', trigger.clickHandler);
289    });
290    console.log(`Attached listeners to ${triggers.length} gallery-trigger elements`);
291}
292
293// Initialize triggers immediately and retry if DOM not ready
294if (document.readyState === 'loading') {
295    console.log('DOM not ready, waiting for DOMContentLoaded');
296    document.addEventListener('DOMContentLoaded', attachGalleryTriggers);
297} else {
298    console.log('DOM ready, attaching triggers immediately');
299    attachGalleryTriggers();
300}

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.