PageSourceSearch

https://www.skypce.net/_sitecommon/event-banner/js/event-banner.js?260928

js skypce.net collected 2026-10-01 16:09:29 UTC 7,140 bytes, 238 lines download raw bytes

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.