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.