1document.addEventListener('DOMContentLoaded', async () => { 2 const CONFIG = { 3 SCROLL_THRESHOLD: 100, 4 SLIDE_INTERVAL: 4000, 5 BREAKPOINT_PC: '(min-width: 768px)', 6 MEDIA_REDUCED_MOTION: '(prefers-reduced-motion: reduce)', 7 8 // åä¸ãã¡ã¤ã³å éç¨ã®ããã®ã«ã¼ãç¸å¯¾ãã¹ 9 DEFAULT_JSON_URL: '/_sitecommon/event-banner/json/events-data.json' 10 }; 11 12 const banner = document.getElementById('floatingBanner'); 13 if (!banner) return; 14 15 const track = document.getElementById('floatingBannerTrack'); 16 const closeBtn = document.getElementById('floatingBannerClose'); 17 const footers = document.querySelectorAll('footer'); 18 const footer = footers.length > 0 ? footers[footers.length - 1] : null; 19 20 if (!track) return; 21 22 // ========================================== 23 // 1. ã»ãã¥ãªã㣠& ã¦ã¼ãã£ãªãã£ãã«ãã¼ 24 // ========================================== 25 const isValidURL = (urlStr) => { 26 if (typeof urlStr !== 'string' || urlStr.trim() === '') return false; 27 if (urlStr.startsWith('/') && !urlStr.startsWith('//')) return true; 28 29 try { 30 const parsed = new URL(urlStr); 31 return ['http:', 'https:'].includes(parsed.protocol); 32 } catch { 33 return false; 34 } 35 }; 36 37 const parseDateTime = (dateStr) => { 38 if (!dateStr) return NaN; 39 const normalized = dateStr.trim().replace(/-/g, '/'); 40 return Date.parse(normalized); 41 }; 42 43 // ========================================== 44 // 2. å¤é¨ JSON ãã¼ã¿ã®éåæåå¾ (Fetch) 45 // ========================================== 46 let bannerData = []; 47 try { 48 const jsonUrl = banner.getAttribute('data-json-src') || CONFIG.DEFAULT_JSON_URL; 49 const response = await fetch(jsonUrl); 50 51 if (!response.ok) { 52 throw new Error(`HTTP error! status: ${response.status}`); 53 } 54 55 const parsed = await response.json(); 56 if (!Array.isArray(parsed)) { 57 throw new Error('Banner data must be an array'); 58 } 59 bannerData = parsed; 60 } catch (error) { 61 console.error('Failed to load banner data:', error); 62 banner.remove(); 63 return; 64 } 65 66 // ========================================== 67 // 3. ãã¼ã¿ãµãã¿ã¤ãºã¨DOMã®åçæ§ç¯ 68 // ========================================== 69 const REGEX_SANITIZE_ATTR = /[^a-zA-Z0-9,\s_-]/g; 70 const rawActiveBanners = banner.getAttribute('data-active-banner'); 71 const activeBannersSet = rawActiveBanners 72 ? new Set(rawActiveBanners.replace(REGEX_SANITIZE_ATTR, '').split(',').map(s => s.trim())) 73 : null; 74 75 const fragment = document.createDocumentFragment(); 76 const now = Date.now(); 77 const slides = []; 78 79 bannerData.forEach(item => { 80 const itemRaw = item.item ? item.item.trim() : ''; 81 if (itemRaw !== '' && activeBannersSet) { 82 const itemTypes = itemRaw.split(',').map(s => s.trim()); 83 if (!itemTypes.some(type => activeBannersSet.has(type))) return; 84 } 85 86 // ãéè¦ï¼ããã¼ã¯releaseã®æç¡ãä¸ååããã常㫠bnrEnd ææ¥ã§æ¶å»ããã 87 const bnrStartLimit = item.bntStart || item.bnrStart || item.start; 88 const bnrEndLimit = item.bnrEnd || item.end; 89 90 if (bnrStartLimit && now < parseDateTime(bnrStartLimit)) return; 91 if (bnrEndLimit && now > parseDateTime(bnrEndLimit)) return; 92 93 const targetImgSrc = isValidURL(item.imgSrc) ? item.imgSrc : ''; 94 if (!targetImgSrc) return; 95 96 const a = document.createElement('a'); 97 a.href = isValidURL(item.href) ? item.href : '#'; 98 a.className = 'floating-banner__slide'; 99 if (item.quo) a.classList.add('is-quo'); 100 101 // 常ã«å¥ã¿ãã§éã 102 a.setAttribute('target', '_blank'); 103 a.setAttribute('rel', 'noopener noreferrer'); 104 105 const img = document.createElement('img'); 106 img.src = targetImgSrc; 107 img.alt = typeof item.alt === 'string' ? item.alt.trim() : ''; 108 img.loading = 'eager'; 109 110 a.appendChild(img); 111 fragment.appendChild(a); 112 slides.push(a); 113 }); 114 115 if (slides.length === 0) { 116 banner.remove(); 117 return; 118 } 119 120 track.appendChild(fragment); 121 122 // ========================================== 123 // 4. ã¢ãã¡ã¼ã·ã§ã³å¶å¾¡ & ã¤ãã³ããã³ããªã³ã° 124 // ========================================== 125 const mediaQuery = window.matchMedia(CONFIG.BREAKPOINT_PC); 126 const motionQuery = window.matchMedia(CONFIG.MEDIA_REDUCED_MOTION); 127 128 let isManuallyClosed = false;
129 let isHovered = false; 130 let isScrollPassed = false; 131 let isFooterVisible = false; 132 let currentIndex = 0; 133 let timerId = null; 134 135 const activateSlide = (index) => { 136 slides.forEach((slide, idx) => { 137 slide.classList.toggle('is-active', idx === index); 138 }); 139 }; 140 141 activateSlide(0); 142 143 const stopAutoplay = () => { 144 if (timerId) { 145 clearInterval(timerId); 146 timerId = null; 147 } 148 }; 149 150 const startAutoplay = () => { 151 stopAutoplay(); 152 if (slides.length <= 1) return; 153 if (!mediaQuery.matches || motionQuery.matches) return; 154 if (isHovered || isManuallyClosed) return; 155 if (!banner.classList.contains('is-visible')) return; 156 157 timerId = setInterval(() => { 158 currentIndex = (currentIndex + 1) % slides.length; 159 activateSlide(currentIndex); 160 }, CONFIG.SLIDE_INTERVAL); 161 }; 162 163 const updateBannerVisibility = () => { 164 if (isManuallyClosed) return; 165 const shouldShow = isScrollPassed && !isFooterVisible; 166 const isCurrentlyVisible = banner.classList.contains('is-visible'); 167 168 if (shouldShow && !isCurrentlyVisible) { 169 banner.classList.add('is-visible'); 170 startAutoplay(); 171 } else if (!shouldShow && isCurrentlyVisible) { 172 banner.classList.remove('is-visible'); 173 stopAutoplay(); 174 } 175 }; 176 177 if (footer) { 178 const footerObserver = new IntersectionObserver( 179 (entries) => { 180 isFooterVisible = entries[0].isIntersecting; 181 updateBannerVisibility(); 182 }, 183 { root: null, rootMargin: '0px', threshold: 0 } 184 ); 185 footerObserver.observe(footer); 186 } 187 188 let ticking = false; 189 const onScroll = () => { 190 const passed = window.scrollY >= CONFIG.SCROLL_THRESHOLD; 191 if (passed !== isScrollPassed) { 192 isScrollPassed = passed; 193 updateBannerVisibility(); 194 } 195 ticking = false; 196 }; 197 198 window.addEventListener('scroll', () => { 199 if (!ticking) { 200 window.requestAnimationFrame(onScroll); 201 ticking = true; 202 } 203 }, { passive: true }); 204 205 banner.addEventListener('mouseenter', () => { 206 if (mediaQuery.matches) { 207 isHovered = true; 208 stopAutoplay(); 209 } 210 }); 211 212 banner.addEventListener('mouseleave', () => { 213 if (mediaQuery.matches) { 214 isHovered = false; 215 startAutoplay(); 216 } 217 }); 218 219 closeBtn?.addEventListener('click', (e) => { 220 e.stopPropagation(); 221 isManuallyClosed = true; 222 banner.classList.remove('is-visible'); 223 stopAutoplay(); 224 }); 225 226 mediaQuery.addEventListener('change', () => { 227 if (mediaQuery.matches) startAutoplay(); 228 else stopAutoplay(); 229 }); 230 231 document.addEventListener('visibilitychange', () => { 232 if (document.hidden) stopAutoplay(); 233 else startAutoplay(); 234 }); 235 236 isScrollPassed = window.scrollY >= CONFIG.SCROLL_THRESHOLD; 237 updateBannerVisibility(); 238});
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.