PageSourceSearch

https://madamefigaro.jp/wp-content/plugins/figaro-infinite-scroll/assets/js/infinite-scroll.js?ver=1.1.5

js madamefigaro.jp collected 2026-09-24 19:19:19 UTC 34,906 bytes, 1,150 lines download raw bytes

1/**
2 * Figaro Infinite Scroll JavaScript
3 * 無限スクロール機能の実装
4 *
5 * @package Figaro_Infinite_Scroll
6 */
7
8(function() {
9	'use strict';
10
11	/**
12	 * 無限スクロールクラス
13	 */
14	class InfiniteScroll {
15		constructor() {
16			// PHPから渡されたデータ
17			this.data = window.figaroInfiniteScroll || {};
18			this.articleIds = this.data.articleIds || [];
19			this.triggerThreshold = this.data.triggerThreshold || 0.7;
20			this.apiUrl = this.data.apiUrl || '';
21
22			// 状態管理
23			this.currentIndex = 0;
24			this.isLoading = false;
25			this.observer = null;
26			this.articleObserver = null;
27			this.lastScrollTime = 0;
28			this.scrollThrottle = 100; // 100ms間隔でURL更新チェック
29
30			// 初期化
31			this.init();
32		}
33
34		/**
35		 * 初期化
36		 */
37		init() {
38			if (this.articleIds.length === 0) {
39				return;
40			}
41
42			// 初回の記事セクションを監視対象に
43			this.observeCurrentArticle();
44
45			// 1記事目(ランディング記事)もURL更新監視対象に
46			const firstArticle = document.querySelector('.p-detail');
47			if (firstArticle) {
48				this.observeArticleForUrlUpdate(firstArticle);
49			}
50
51			// スクロールイベントリスナーを追加(URL更新用)
52			window.addEventListener('scroll', () => this.handleScroll(), { passive: true });
53
54			// 初回のURL更新チェック
55			this.updateCurrentArticleUrl();
56		}
57
58		/**
59		 * 現在の記事セクションを監視
60		 */
61		observeCurrentArticle() {
62			// 記事の最下部要素(パンくずリスト)を監視対象に
63			const breadcrumbs = document.querySelectorAll('.breadcrumb__wrapper');
64			if (breadcrumbs.length === 0) {
65				return;
66			}
67
68			const lastBreadcrumb = breadcrumbs[breadcrumbs.length - 1];
69
70			// Intersection Observerを作成
71			if (this.observer) {
72				this.observer.disconnect();
73			}
74
75			this.observer = new IntersectionObserver(
76				(entries) => this.handleIntersection(entries),
77				{
78					root: null,
79					rootMargin: '0px',
80				threshold: 0.6 // パンくずリストが60%見えたら発火
81				}
82			);
83
84			this.observer.observe(lastBreadcrumb);
85		}
86
87		/**
88		 * Intersection Observer のコールバック
89		 *
90		 * @param {IntersectionObserverEntry[]} entries
91		 */
92	handleIntersection(entries) {
93		entries.forEach(entry => {
94			// 要素が画面に入ったら次の記事を読み込み
95			// isLoadingチェックで二重発火を防止
96			if (entry.isIntersecting && !this.isLoading) {
97				this.loadNextArticle();
98			}
99		});
100	}
101
102
103		/**
104		 * 次の記事を読み込み
105		 */
106	async loadNextArticle() {
107		// ローディング中または記事が残っていない場合は何もしない
108		if (this.isLoading || this.currentIndex >= this.articleIds.length) {
109			// Observerを破棄して監視を停止
110			if (this.observer) {
111				this.observer.disconnect();
112			}
113			return;
114		}
115
116		this.isLoading = true;
117		const nextArticle = this.articleIds[this.currentIndex];
118		// nextArticleがundefinedの場合は早期リターン
119		if (!nextArticle) {
120			this.isLoading = false;
121			return;
122		}
123
124		// マルチサイト形式(オブジェクト)かレガシー形式(整数)かを判定
125		const isMultisite = typeof nextArticle === 'object' && nextArticle !== null && nextArticle.site_id && nextArticle.post_id;
126		const siteId = isMultisite ? nextArticle.site_id : 1; // レガシー形式はメインサイト (blog_id=1)
127		const postId = isMultisite ? nextArticle.post_id : nextArticle;
128
129		// デバッグログ
130		console.log('[Infinite Scroll] nextArticle:', nextArticle);
131		console.log('[Infinite Scroll] isMultisite:', isMultisite);
132		console.log('[Infinite Scroll] siteId:', siteId, 'postId:', postId);
133
134		try {
135			// マルチサイト対応: blog_idを含むURLを構築
136			const apiEndpoint = isMultisite ? `${this.apiUrl}${siteId}/${postId}` : `${this.apiUrl}${postId}`;
137			console.log('[Infinite Scroll] API Endpoint:', apiEndpoint);
138			const response = await fetch(apiEndpoint, {
139				method: 'GET',
140				headers: {
141					'Content-Type': 'application/json'
142				}
143			});
144
145			if (!response.ok) {
146				throw new Error(`HTTP error! status: ${response.status}`);
147			}
148
149			const data = await response.json();
150
151			if (data.success && data.html) {
152				// メタ情報を含めて記事を追加
153				const meta = {
154					title: data.title,
155					description: data.description,
156					og_image: data.og_image,
157					dataLayer: data.dataLayer,
158					post_id: postId // 広告用に記事IDを追加
159				};
160				this.appendArticle(data.html, meta);
161				this.currentIndex++;
162
163				// 新しい記事を監視対象に
164				this.observeCurrentArticle();
165			} else {
166				console.error('Failed to load article:', data);
167			}
168		} catch (error) {
169			console.error('Error loading article:', error);
170		} finally {
171			this.isLoading = false;
172		}
173	}
174
175		/**
176		 * 記事HTMLをDOMに追加
177		 *
178		 * @param {string} html
179		 * @param {object} meta - 記事のメタ情報(title, url, description, og_image)
180		 */
181		appendArticle(html, meta) {
182			// 最後の記事要素を探す
183			const articles = document.querySelectorAll('.p-detail');
184			if (articles.length === 0) {
185				console.error('article element not found');
186				return;
187			}
188
189			const lastArticle = articles[articles.length - 1];
190
191			// 新しい記事要素を作成
192			const tempDiv = document.createElement('div');
193			tempDiv.innerHTML = html.trim();
194			const newArticle = tempDiv.firstChild;
195
196			// メタ情報をdata属性として保存
197			if (meta) {
198				newArticle.dataset.metaTitle = meta.title || '';
199				newArticle.dataset.metaDescription = meta.description || '';
200				newArticle.dataset.metaOgImage = meta.og_image || '';
201				newArticle.dataset.datalayer = JSON.stringify(meta.dataLayer || {});
202			}
203
204		// 追加読込した記事にpadding-topを設定(PC:60px、SP:8vw)
205		const paddingTop = window.innerWidth >= 768 ? '60px' : '8vw';
206		newArticle.style.paddingTop = paddingTop;
207
208			// フェードイン用のスタイル
209			newArticle.style.opacity = '0';
210			newArticle.style.transform = 'translateY(20px)';
211			newArticle.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
212
213		// GAM動的スロット用の広告セクションを作成
214		const pageIndex = this.currentIndex + 1; // 次のページ番号(1ベース)
215		const postId = meta && meta.post_id ? meta.post_id : 'unknown';
216
217		const adSection = document.createElement('div');
218		adSection.className = 'b-ad gam-ad-slot-infinity-scroll';
219
220		// PC用スロット
221		const pcAdDiv = document.createElement('div');
222		pcAdDiv.id = `div-gpt-ad-pc-infinity-article-${postId}-${pageIndex}`;
223		pcAdDiv.className = 'gam-ad-slot gam-ad-slot-pc';
224		pcAdDiv.style.minWidth = '300px';
225		pcAdDiv.style.textAlign = 'center';
226		pcAdDiv.style.fontSize = '0';
227
228		// SP用スロット
229		const spAdDiv = document.createElement('div');
230		spAdDiv.id = `div-gpt-ad-sp-infinity-article-${postId}-${pageIndex}`;
231		spAdDiv.className = 'gam-ad-slot gam-ad-slot-sp';
232		spAdDiv.style.minWidth = '300px';
233		spAdDiv.style.textAlign = 'center';
234		spAdDiv.style.fontSize = '0';
235
236		// adSection.appendChild(pcAdDiv);
237		// adSection.appendChild(spAdDiv);
238
239		// 広告を先に挿入
240		lastArticle.insertAdjacentElement('afterend', adSection);
241
242		// 広告の後に記事を挿入
243		adSection.insertAdjacentElement('afterend', newArticle);
244
245		// GAM動的スロット呼び出し(少し遅延させる)
246		setTimeout(() => {
247			if (typeof dynamic_whatpagerAD === 'function') {
248				dynamic_whatpagerAD(postId, pageIndex);
249			} else {
250				console.warn('dynamic_whatpagerAD function not defined');
251			}
252
253			// Showcase 1st(本文直下)広告の動的呼び出し
254			if (typeof dynamic_showcase1stAD === 'function') {
255				dynamic_showcase1stAD(postId);
256			}
257
258			// Billboard 3rd / Showcase 3rd(パンくず前)広告の動的呼び出し
259			if (typeof dynamic_3rdAD === 'function') {
260				dynamic_3rdAD(postId);
261			}
262		}, 100);
263
264			// 少し遅延させてフェードイン
265			setTimeout(() => {
266				newArticle.style.opacity = '1';
267				newArticle.style.transform = 'translateY(0)';
268			}, 50);
269
270			// URL更新用の監視を開始
271			this.observeArticleForUrlUpdate(newArticle);
272
273			// ギャラリー/スライダーブロックの再初期化
274			this.reinitializeModules(newArticle);
275
276			// 目次ブロックの初期化
277			this.initTableOfContents(newArticle);
278
279			// SNS埋め込みの再処理
280			// innerHTMLで挿入した<script>は実行されないため、各プロバイダのスクリプトを明示的に呼び直す
281			this.reinitializeInstagramEmbeds(newArticle);
282			this.reinitializeTikTokEmbeds(newArticle);
283			this.reinitializeTwitterEmbeds(newArticle);
284
285			// ページャーブロックの広告監視を開始
286			if (window.figaroPagerAdLoader && typeof window.figaroPagerAdLoader.observePagers === 'function') {
287				window.figaroPagerAdLoader.observePagers(newArticle);
288			}
289		}
290
291		/**
292		 * Instagram埋め込みの再処理
293		 * 追加された記事内の blockquote.instagram-media を iframe 化する
294		 *
295		 * @param {HTMLElement} article - 新しく追加された記事要素
296		 */
297		reinitializeInstagramEmbeds(article) {
298			if (!article.querySelector('blockquote.instagram-media')) {
299				return;
300			}
301
302			if (window.instgrm && window.instgrm.Embeds && typeof window.instgrm.Embeds.process === 'function') {
303				window.instgrm.Embeds.process();
304				return;
305			}
306
307			// embed.jsが未ロードの場合は動的に注入(読み込み完了時にprocessが自動実行される)
308			if (!document.querySelector('script[src*="instagram.com/embed.js"]')) {
309				const script = document.createElement('script');
310				script.src = 'https://www.instagram.com/embed.js';
311				script.async = true;
312				document.body.appendChild(script);
313			}
314		}
315
316		/**
317		 * TikTok埋め込みの再処理
318		 * 追加された記事内の blockquote.tiktok-embed を iframe 化する
319		 *
320		 * TikTokには公式の再描画APIがないため、scriptタグを再注入してDOM全体を再スキャンさせる
321		 *
322		 * @param {HTMLElement} article - 新しく追加された記事要素
323		 */
324		reinitializeTikTokEmbeds(article) {
325			if (!article.querySelector('blockquote.tiktok-embed')) {
326				return;
327			}
328
329			// 既存のembed.jsを削除して再注入する(再注入時にDOM全体を再スキャンする)
330			const existing = document.querySelector('script[src*="tiktok.com/embed.js"]');
331			if (existing) {
332				existing.remove();
333			}
334
335			const script = document.createElement('script');
336			script.src = 'https://www.tiktok.com/embed.js';
337			script.async = true;
338			document.body.appendChild(script);
339		}
340
341		/**
342		 * Twitter/X埋め込みの再処理
343		 * 追加された記事内の blockquote.twitter-tweet を iframe 化する
344		 *
345		 * @param {HTMLElement} article - 新しく追加された記事要素
346		 */
347		reinitializeTwitterEmbeds(article) {
348			if (!article.querySelector('blockquote.twitter-tweet')) {
349				return;
350			}
351
352			// twttr.widgets.load(scope) でスコープ指定の再変換が可能(idempotent)
353			if (window.twttr && window.twttr.widgets && typeof window.twttr.widgets.load === 'function') {
354				window.twttr.widgets.load(article);
355				return;
356			}
357
358			// widgets.jsが未ロードの場合は動的に注入(読み込み完了時に全blockquoteを処理する)
359			if (!document.querySelector('script[src*="platform.twitter.com/widgets.js"]')) {
360				const script = document.createElement('script');
361				script.src = 'https://platform.twitter.com/widgets.js';
362				script.async = true;
363				document.body.appendChild(script);
364			}
365		}
366
367		/**
368		 * ギャラリー/スライダーモジュールの再初期化
369		 *
370		 * @param {HTMLElement} article - 新しく追加された記事要素
371		 */
372	reinitializeModules(article) {
373		// カスタムイベントをディスパッチ
374		// 注意: DOMContentLoadedイベントはディスパッチしない
375		// (新しいInfiniteScrollインスタンスが作成されてしまうため)
376		const contentAddedEvent = new CustomEvent('contentAdded', {
377			detail: { element: article },
378			bubbles: true
379		});
380		article.dispatchEvent(contentAddedEvent);
381
382		// 少し遅延してから、app.jsが自動初期化するのを待つ
383		// app.jsがMutationObserverを使っている可能性があるため
384		setTimeout(() => {
385			// それでも初期化されていない場合は手動で試みる
386			const moduleElements = article.querySelectorAll('[data-module]');
387			moduleElements.forEach(element => {
388				const moduleName = element.getAttribute('data-module');
389
390				// すでに初期化されているかチェック(Splideの場合)
391				if (moduleName === 'ArticleSlide' && element.classList.contains('is-initialized')) {					return;
392				}
393				// グローバルに初期化関数が存在すればそれを呼ぶ
394				if (typeof window.initModule === 'function') {					window.initModule(element, moduleName);
395				}
396				// または、モジュール名に基づいて個別に初期化
397				else {					this.initializeModuleByName(element, moduleName);
398				}
399			});
400		}, 100); // 100ms待機
401	}
402
403	/**
404	 * 目次(Table of Contents)ブロックの初期化
405	 * 無限スクロールで追加された記事内の見出しにIDを付与し、
406	 * 目次リンクのスムーススクロールを設定する
407	 *
408	 * @param {HTMLElement} article - 新しく追加された記事要素
409	 */
410	initTableOfContents(article) {
411		const tocBlock = article.querySelector('.b-toc');
412		if (!tocBlock) return;
413
414		// 記事内のコンテンツエリアを取得
415		const content = article.querySelector('.entry-content, .wp-block-post-content, .p-detail__main, main');
416		if (!content) return;
417
418		// 見出しを取得(目次ブロック内の見出しは除外)
419		const headings = content.querySelectorAll('h2, h3');
420		let h2Index = 0;
421		let h3Index = 0;
422
423		headings.forEach(function(heading) {
424			if (heading.closest('.b-toc')) return;
425
426			let id;
427			if (heading.tagName === 'H2') {
428				h2Index++;
429				h3Index = 0;
430				id = 'content' + String(h2Index).padStart(2, '0');
431			} else if (heading.tagName === 'H3') {
432				h3Index++;
433				id = 'content' + String(h2Index).padStart(2, '0') + '-' + String(h3Index).padStart(2, '0');
434			}
435
436			// 見出しを含む親セクションにIDを付与
437			const parentSection = heading.closest('section, .section');
438			if (parentSection && !parentSection.id) {
439				parentSection.id = id;
440			} else if (!parentSection) {
441				if (!heading.id) {
442					heading.id = id;
443				}
444			}
445		});
446
447		// 目次リンクにスムーススクロールを設定
448		const tocLinks = tocBlock.querySelectorAll('.b-toc__item a[href^="#"]');
449		tocLinks.forEach(function(link) {
450			link.addEventListener('click', function(e) {
451				const href = this.getAttribute('href');
452				if (!href || href === '#') return;
453
454				const targetId = href.substring(1);
455				// 同じ記事内の要素を検索
456				const targetElement = article.querySelector('#' + targetId);
457
458				if (targetElement) {
459					e.preventDefault();
460					targetElement.scrollIntoView({
461						behavior: 'smooth',
462						block: 'start'
463					});
464				}
465			});
466		});
467	}
468
469		/**
470		 * モジュール名に基づいて個別に初期化
471		 *
472		 * @param {HTMLElement} element
473		 * @param {string} moduleName
474		 */
475		initializeModuleByName(element, moduleName) {
476			// app.jsのモジュールクラスをグローバルから取得して使用
477			const moduleClasses = window.moduleClasses || {};
478
479			// app.jsのクラスが存在する場合はそれを使用
480			if (moduleClasses[moduleName]) {
481				try {
482					new moduleClasses[moduleName](element);
483					return;
484				} catch (error) {
485					console.error(`Failed to initialize ${moduleName} with app.js class:`, error);
486					// フォールバックとして従来の初期化を試みる
487				}
488			}
489
490			// フォールバック: 直接初期化
491			switch (moduleName) {
492				case 'CardSlide':
493					this.initSwiper(element);
494					break;
495				case 'ArticleSlide':
496					this.initSplide(element);
497					break;
498				case 'InlineModal':
499					this.initInlineModal(element);
500					break;
501				case 'VisualAccordion':
502					this.initVisualAccordion(element);
503					break;
504				case 'TagSlide':
505				case 'TagSlide02':
506				case 'TopMvSlide':
507				case 'TabSlide':
508				case 'EditSlide':
509				case 'PicksSlide':
510				case 'ThumbSlide':
511				case 'SpSlide':
512				case 'AllSlide':
513				case 'PickupSlide':
514					this.initSlick(element);
515					break;
516				default:			}
517		}
518
519		/**
520		 * Swiper の初期化
521		 *
522		 * @param {HTMLElement} element
523		 */
524		initSwiper(element) {
525			// Swiper v11 のグローバル変数を確認
526			const SwiperClass = window.Swiper || Swiper;
527			if (typeof SwiperClass !== 'undefined') {
528				try {
529					const swiper = new SwiperClass(element, {
530						slidesPerView: 'auto',
531						spaceBetween: 0,
532						freeMode: true,
533						navigation: {
534							nextEl: element.querySelector('.swiper-button-next'),
535							prevEl: element.querySelector('.swiper-button-prev'),
536						},
537					});				} catch (error) {
538					console.error('Swiper initialization failed:', error);
539				}
540			} else {
541				console.warn('Swiper library not found');
542			}
543		}
544
545		/**
546		 * Splide の初期化
547		 *
548		 * @param {HTMLElement} element
549		 */
550	initSplide(element) {
551		// Splide のグローバル変数を確認
552		const SplideClass = window.Splide || (typeof Splide !== 'undefined' ? Splide : null);
553		if (SplideClass === null) {
554			console.warn('Splide library not found');
555			return;
556		}
557
558		try {
559			// メインスライドの要素を取得 (サムネイルのSplideを除く)
560			const mainSplideEl = element.querySelector('.splide:not(.b-article--thumb__splide)');
561			// サムネイルスライドの要素を取得
562			const thumbSplideEl = element.querySelector('.b-article--thumb__splide');
563			const hasThumbnail = !!thumbSplideEl;
564
565			if (!mainSplideEl) {
566				console.warn('Main splide element not found in ArticleSlide');
567				return;
568			}
569
570			// スライド内の画像をすべて取得
571			const images = mainSplideEl.querySelectorAll('img');
572			const imagePromises = Array.from(images).map(img => {
573				if (img.complete) {
574					return Promise.resolve();
575				}
576				return new Promise((resolve, reject) => {
577					img.addEventListener('load', resolve);
578					img.addEventListener('error', resolve); // エラーでも続行
579					// タイムアウト設定(5秒)
580					setTimeout(resolve, 5000);
581				});
582			});
583
584			// すべての画像読み込み完了を待ってからSplideを初期化
585			Promise.all(imagePromises).then(() => {
586				// メインスライドのオプション (app.js の ArticleSlide と同じ設定)
587				const mainOptions = {
588					type: 'loop',
589					autoplay: false,
590					speed: 500,
591					pagination: false,
592					arrows: true,
593					autoHeight: true
594				};
595
596				// メインスライドの初期化
597				const splideMain = new SplideClass(mainSplideEl, mainOptions);
598
599				// サムネイルがある場合
600				if (hasThumbnail) {
601					// サムネイルスライドのオプション (app.js と同じ設定)
602					const thumbOptions = {
603						type: 'loop',
604						rewind: true,
605						autoWidth: true,
606						pagination: false,
607						arrows: false,
608						isNavigation: true,
609						focus: '0',
610						breakpoints: {
611							767: {
612								perPage: 3
613							}
614						}
615					};
616
617					// サムネイルスライドの初期化と同期
618					const splideThumb = new SplideClass(thumbSplideEl, thumbOptions);
619					splideMain.sync(splideThumb);
620					splideThumb.mount();
621				}
622
623				splideMain.mount();
624
625				// mount後に複数回refreshを実行して確実に高さを再計算
626				const refreshSplide = () => {
627					if (splideMain && typeof splideMain.refresh === 'function') {
628						splideMain.refresh();
629					}
630				};
631
632				// 段階的にrefreshを実行
633				setTimeout(refreshSplide, 100);
634				setTimeout(refreshSplide, 300);
635				setTimeout(refreshSplide, 500);
636
637				// アロー位置を手動で調整
638				const adjustArrows = () => {
639					const activeSlide = mainSplideEl.querySelector('.splide__slide.is-active');
640					const mainImage = activeSlide ? activeSlide.querySelector('.b-article--slide__img') : null;
641					const prevArrow = element.querySelector('.splide__arrow--prev');
642					const nextArrow = element.querySelector('.splide__arrow--next');
643
644					if (mainImage && prevArrow && nextArrow) {
645						const imgHeight = mainImage.offsetHeight;
646						const imgTopOffset = mainImage.offsetTop;
647						const arrowOffset = prevArrow.offsetHeight / 2;
648						const newTop = imgTopOffset + (imgHeight / 2) - arrowOffset;
649
650						prevArrow.style.top = `${newTop}px`;
651						nextArrow.style.top = `${newTop}px`;
652					}
653				};
654
655				// アロー位置調整も段階的に実行
656				setTimeout(adjustArrows, 150);
657				setTimeout(adjustArrows, 350);
658				setTimeout(adjustArrows, 550);
659			});
660		} catch (error) {
661			console.error('Splide initialization failed:', error);
662		}
663	}
664
665
666		/**
667		 * Slick の初期化
668		 *
669		 * @param {HTMLElement} element
670		 */
671		initSlick(element) {			if (typeof jQuery !== 'undefined' && typeof jQuery.fn.slick !== 'undefined') {
672				try {
673					jQuery(element).slick({
674						slidesToShow: 1,
675						slidesToScroll: 1,
676						arrows: true,
677						dots: false,
678						infinite: true,
679					});				} catch (error) {
680					console.error('Slick initialization failed:', error);
681				}
682			} else {
683				console.warn('Slick library not found');
684			}
685		}
686
687	/**
688	 * InlineModal の初期化
689	 * app.jsのグローバルリスナーは既に登録済みと想定し、
690	 * 新しいトリガーボタンに対してのみclickイベントを追加
691	 *
692	 * @param {HTMLElement} element - .js-InlineModal-btn 要素
693	 */
694	initInlineModal(element) {
695		try {
696			const modalId = element.dataset.modalOpen;
697			const group = element.dataset.modalGroup;
698			let groupModals = [];
699
700			if (group) {
701				groupModals = document.querySelectorAll(
702					`.js-InlineModal-wrapper[data-modal-group="${group}"]`
703				);
704			}
705
706			// clickイベントを追加(app.jsのInlineModal.open()を呼び出す想定)
707			element.addEventListener('click', (e) => {
708				e.preventDefault();
709
710				// app.jsのInlineModalが利用可能な場合はそれを使用
711				if (typeof window.InlineModal !== 'undefined' && typeof window.InlineModal.open === 'function') {
712					window.InlineModal.open(modalId, groupModals);
713				} else {
714					// フォールバック: シンプルなモーダル表示
715					console.warn('InlineModal from app.js not found, using fallback');
716					const targetModal = document.querySelector(`.js-InlineModal-wrapper[data-modal="${modalId}"]`);
717					if (targetModal) {
718						targetModal.classList.add('is-open');
719						document.body.style.overflow = 'hidden';
720					}
721				}
722			});		} catch (error) {
723			console.error('InlineModal initialization failed:', error);
724		}
725	}
726
727	/**
728	 * VisualAccordion の初期化
729	 * HTMLプロジェクトのVisualAccordion.jsを参考に実装
730	 *
731	 * @param {HTMLElement} rootElement - data-module="VisualAccordion"を持つ要素
732	 */
733	initVisualAccordion(rootElement) {
734		try {
735			const isOpenClass = 'is-open';
736			const isFadingClass = 'is-fading';
737			const FADE_DURATION = 200; // ms
738			const SLIDE_DURATION = 300; // ms
739
740			const mainVisualImg = rootElement.querySelector('.js-main-visual');
741			const visualLink = rootElement.querySelector('.js-visual-link');
742			const triggers = rootElement.querySelectorAll('.js-toggle-trigger');
743			const allItems = rootElement.querySelectorAll('[data-img-src]');
744			const firstItem = allItems[0];
745
746			let initialImageSrc = '';
747			let initialLinkHref = '#';
748
749			// ヘッダー要素の有無をチェック
750			const hasHeader = rootElement.querySelector('.b-accordion__header');
751
752			// 初期値の取得
753			if (rootElement.dataset.initialImgSrc) {
754				initialImageSrc = rootElement.dataset.initialImgSrc;
755				initialLinkHref = rootElement.dataset.initialLinkHref || '#';
756			} else if (firstItem) {
757				initialImageSrc = firstItem.dataset.imgSrc || '';
758				initialLinkHref = firstItem.dataset.href || '#';
759			}
760
761			// ビジュアル更新関数
762			const updateVisual = (newSrc, newHref) => {
763				const imgContainer = mainVisualImg ? mainVisualImg.parentNode : null;
764				if (mainVisualImg && newSrc && imgContainer) {
765					imgContainer.classList.add(isFadingClass);
766					setTimeout(() => {
767						mainVisualImg.src = newSrc;
768						if (visualLink) {
769							visualLink.href = newHref;
770						}
771						imgContainer.classList.remove(isFadingClass);
772					}, FADE_DURATION);
773				}
774			};
775
776			// スライドダウン関数
777			const slideDown = (el) => {
778				el.style.removeProperty('display');
779				let display = window.getComputedStyle(el).display;
780				if (display === 'none') display = 'block';
781				el.style.display = display;
782				let height = el.scrollHeight;
783
784				el.style.overflow = 'hidden';
785				el.style.height = 0;
786				el.style.paddingTop = 0;
787				el.style.paddingBottom = 0;
788				el.style.marginTop = 0;
789				el.style.marginBottom = 0;
790
791				el.offsetHeight; // reflow
792
793				el.style.transitionProperty = 'height, margin, padding';
794				el.style.transitionDuration = SLIDE_DURATION + 'ms';
795				el.style.transitionTimingFunction = 'ease';
796				el.style.height = height + 'px';
797
798				setTimeout(() => {
799					el.style.removeProperty('height');
800					el.style.removeProperty('overflow');
801					el.style.removeProperty('transition-duration');
802					el.style.removeProperty('transition-property');
803					el.style.removeProperty('transition-timing-function');
804					el.style.removeProperty('padding-top');
805					el.style.removeProperty('padding-bottom');
806					el.style.removeProperty('margin-top');
807					el.style.removeProperty('margin-bottom');
808				}, SLIDE_DURATION);
809			};
810
811			// スライドアップ関数
812			const slideUp = (el) => {
813				el.style.height = el.offsetHeight + 'px';
814				el.offsetHeight; // reflow
815				el.style.transitionProperty = 'height, margin, padding';
816				el.style.transitionDuration = SLIDE_DURATION + 'ms';
817				el.style.transitionTimingFunction = 'ease';
818				el.style.overflow = 'hidden';
819				el.style.height = 0;
820				el.style.paddingTop = 0;
821				el.style.paddingBottom = 0;
822				el.style.marginTop = 0;
823				el.style.marginBottom = 0;
824
825				setTimeout(() => {
826					el.style.display = 'none';
827					el.style.removeProperty('height');
828					el.style.removeProperty('padding-top');
829					el.style.removeProperty('padding-bottom');
830					el.style.removeProperty('margin-top');
831					el.style.removeProperty('margin-bottom');
832					el.style.removeProperty('overflow');
833					el.style.removeProperty('transition-duration');
834					el.style.removeProperty('transition-property');
835					el.style.removeProperty('transition-timing-function');
836				}, SLIDE_DURATION);
837			};
838
839			// アコーディオンを開く
840			const openAccordion = (item) => {
841				const content = item.querySelector('.js-toggle-content');
842				if (!content) return;
843
844				const newSrc = item.dataset.imgSrc;
845				const newHref = item.dataset.href || '#';
846
847				updateVisual(newSrc, newHref);
848				slideDown(content);
849				item.classList.add(isOpenClass);
850			};
851
852			// アコーディオンを閉じる
853			const closeAccordion = (item) => {
854				const content = item.querySelector('.js-toggle-content');
855				if (!content) return;
856
857				slideUp(content);
858				item.classList.remove(isOpenClass);
859			};
860
861			// 全て閉じているかチェック
862			const checkAllClosed = () => {
863				const openItems = rootElement.querySelectorAll(`.${isOpenClass}`);
864				if (openItems.length === 0) {
865					updateVisual(initialImageSrc, initialLinkHref);
866				}
867			};
868
869			// イベントリスナー設定
870			triggers.forEach(trigger => {
871				trigger.addEventListener('click', (event) => {
872					event.preventDefault();
873
874					const currentItem = trigger.closest('[data-img-src]');
875					if (!currentItem) return;
876
877					const isCurrentlyOpen = currentItem.classList.contains(isOpenClass);
878
879					// 他のアイテムを閉じる
880					allItems.forEach(item => {
881						if (item !== currentItem) {
882							closeAccordion(item);
883						}
884					});
885
886					// 現在のアイテムの開閉
887					if (isCurrentlyOpen) {
888						closeAccordion(currentItem);
889					} else {
890						openAccordion(currentItem);
891					}
892
893					checkAllClosed();
894				});
895			});
896
897			// 初期状態設定:ヘッダーがなく、かつリストアイテムが存在する場合、最初のアイテムを自動で開く
898			if (!hasHeader && firstItem) {
899				openAccordion(firstItem);
900			} else {
901				updateVisual(initialImageSrc, initialLinkHref);
902			}		} catch (error) {
903			console.error('VisualAccordion initialization failed:', error);
904		}
905	}
906
907
908		/**
909		 * 記事がビューポートに入ったらURL更新
910		 *
911		 * @param {HTMLElement} article
912		 */
913		observeArticleForUrlUpdate(article) {
914			// 記事上部監視用のObserverを作成(共有)
915			if (!this.articleObserver) {
916				this.articleObserver = new IntersectionObserver(
917					(entries) => this.handleArticleIntersection(entries),
918					{
919						root: null,
920						rootMargin: '0px',
921						threshold: 0 // 記事が1pxでも画面に入ったらURL更新
922					}
923				);
924			}
925
926			this.articleObserver.observe(article);
927		}
928
929		/**
930		 * 記事の可視性変化を処理(URL更新)
931		 *
932		 * @param {IntersectionObserverEntry[]} entries
933		 */
934		handleArticleIntersection(entries) {
935			// スクロール時と同じロジックを実行
936			this.updateCurrentArticleUrl();
937		}
938
939		/**
940		 * スクロールイベントハンドラー(throttle付き)
941		 */
942		handleScroll() {
943			const now = Date.now();
944			if (now - this.lastScrollTime < this.scrollThrottle) {
945				return;
946			}
947			this.lastScrollTime = now;
948			this.updateCurrentArticleUrl();
949		}
950
951		/**
952		 * 現在画面上端に最も近い記事のURLに更新
953		 */
954		updateCurrentArticleUrl() {
955			// 画面内のすべての記事を取得して、最も上にある記事を特定
956			const allArticles = document.querySelectorAll('.p-detail');
957			let topMostArticle = null;
958			let topMostPosition = Infinity;
959
960			allArticles.forEach((article, index) => {
961				const rect = article.getBoundingClientRect();
962
963				// 画面内にある記事(上端が画面下端より上、下端が画面上端より下)
964				if (rect.top < window.innerHeight && rect.bottom > 0) {
965					// 画面上端に最も近い記事を選択
966					const distanceFromTop = Math.abs(rect.top);
967					if (distanceFromTop < topMostPosition) {
968						topMostPosition = distanceFromTop;
969						topMostArticle = article;
970					}
971				}
972			});
973
974			// 最も上にある記事のURLに更新
975			if (topMostArticle) {
976				const url = topMostArticle.dataset.url;
977				const title = topMostArticle.dataset.title;
978
979				if (url) {
980					this.updateUrl(url, title, topMostArticle);
981				}
982			}
983		}
984
985		/**
986		 * URLとメタ情報を更新
987		 *
988		 * @param {string} url - 記事URL
989		 * @param {string} title - 記事タイトル
990		 * @param {HTMLElement} article - 記事要素
991		 */
992		updateUrl(url, title, article) {
993			// related=true パラメータを追加(2記事目以降)
994			const articleIndex = Array.from(document.querySelectorAll('.p-detail')).indexOf(article);
995			let newUrl = url;
996
997			if (articleIndex > 0) {
998				// 2記事目以降は ?related=true を追加
999				newUrl = url + (url.includes('?') ? '&' : '?') + 'related=true';
1000			}
1001
1002			// 現在のURLと同じ場合は更新しない
1003			if (window.location.href === newUrl) {
1004				return;
1005			}
1006
1007			// 2記事目以降の場合:現行サイト仕様に準拠(dataLayer → URL更新 → gtmRelatedPage)
1008			if (articleIndex > 0) {
1009				// 1. dataLayerにcxenseデータ相当をpush
1010				this.pushDataLayer(article);
1011
1012				// 2. URLを更新(履歴を置き換え)
1013				window.history.replaceState(
1014					{ path: newUrl },
1015					'',
1016					newUrl
1017				);
1018
1019				// 3. タイトルを更新
1020				if (title) {
1021					document.title = title;
1022				}
1023
1024				// 4. メタタグを更新
1025				this.updateMetaTags(article);
1026
1027				// 5. gtmRelatedPageイベントを送信
1028				window.dataLayer = window.dataLayer || [];
1029				window.dataLayer.push({
1030					'event': 'gtmRelatedPage',
1031					'page_location': newUrl,
1032					'page_title': title || document.title
1033				});
1034
1035				console.log('gtmRelatedPage event pushed:', newUrl);
1036			} else {
1037				// 1記事目(ランディング記事)の場合:イベント送信なし
1038				window.history.replaceState(
1039					{ path: newUrl },
1040					'',
1041					newUrl
1042				);
1043
1044				if (title) {
1045					document.title = title;
1046				}
1047
1048				this.updateMetaTags(article);
1049			}
1050		}
1051
1052		/**
1053		 * メタタグを更新
1054		 *
1055		 * @param {HTMLElement} article
1056		 */
1057		updateMetaTags(article) {
1058			const description = article.dataset.metaDescription;
1059			const ogImage = article.dataset.metaOgImage;
1060			const title = article.dataset.title;
1061			const url = article.dataset.url;
1062
1063			// description
1064			if (description) {
1065				let metaDesc = document.querySelector('meta[name="description"]');
1066				if (metaDesc) {
1067					metaDesc.setAttribute('content', description);
1068				}
1069
1070				let ogDesc = document.querySelector('meta[property="og:description"]');
1071				if (ogDesc) {
1072					ogDesc.setAttribute('content', description);
1073				}
1074			}
1075
1076			// og:image
1077			if (ogImage) {
1078				let ogImageTag = document.querySelector('meta[property="og:image"]');
1079				if (ogImageTag) {
1080					ogImageTag.setAttribute('content', ogImage);
1081				}
1082			}
1083
1084			// og:title
1085			if (title) {
1086				let ogTitle = document.querySelector('meta[property="og:title"]');
1087				if (ogTitle) {
1088					ogTitle.setAttribute('content', title);
1089				}
1090			}
1091
1092			// og:url(1記事目は元のURL、2記事目以降はrelated=true付き)
1093			if (url) {
1094				let ogUrl = document.querySelector('meta[property="og:url"]');
1095				if (ogUrl) {
1096					const articleIndex = Array.from(document.querySelectorAll('.p-detail')).indexOf(article);
1097					let newUrl = url;
1098					if (articleIndex > 0) {
1099						newUrl = url + (url.includes('?') ? '&' : '?') + 'related=true';
1100					}
1101					ogUrl.setAttribute('content', newUrl);
1102				}
1103			}
1104		}
1105
1106		/**
1107		 * dataLayerにデータを送信
1108		 *
1109		 * @param {HTMLElement} article
1110		 */
1111		pushDataLayer(article) {
1112			try {
1113				const datalayerJson = article.dataset.datalayer;
1114				if (!datalayerJson) {
1115					return;
1116				}
1117
1118				const datalayerData = JSON.parse(datalayerJson);
1119
1120				// window.dataLayerが存在しない場合は初期化
1121				window.dataLayer = window.dataLayer || [];
1122
1123				// dataLayerにpush
1124				window.dataLayer.push(datalayerData);
1125
1126				console.log('dataLayer pushed:', datalayerData);
1127			} catch (error) {
1128				console.error('Error pushing dataLayer:', error);
1129			}
1130		}
1131	}
1132
1133	// 初期化関数
1134	function init() {
1135		// 投稿詳細ページのみ実行
1136		if (document.body.classList.contains('single-post') || document.body.classList.contains('single')) {
1137			new InfiniteScroll();
1138		}
1139	}
1140
1141	// DOM読み込み完了後に初期化
1142	if (document.readyState === 'loading') {
1143		// まだ読み込み中の場合はイベントリスナー登録
1144		document.addEventListener('DOMContentLoaded', init);
1145	} else {
1146		// 既に読み込み完了している場合は即座に実行
1147		init();
1148	}
1149
1150})();

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.