PageSourceSearch

https://illi-stays.com/wp-content/themes/illistays/assets/js/concept.js?ver=20261009215205

js illi-stays.com collected 2026-10-09 21:52:34 UTC 20,297 bytes, 492 lines download raw bytes

1import { fixedBodyOn, fixedBodyOff } from './modules/m_fixedBody.js';
2
3// グローバル変数の定義
4let MAINSLIDER_INDEX = 0;
5let BEFORE_RESIZE_WIDTH = window.innerWidth;
6let mainSliderInstance = null;         // メインスライダーのインスタンスを保持
7let imageExpandedSliderInstance = null;  // モーダルスライダーのインスタンスを保持
8const SP_BREAKPOINT = 768;             // PC切り替えブレークポイント (768px以上でPC)
9
10// ----------------------------------------------------
11// ユーティリティ関数
12// ----------------------------------------------------
13
14// 画像のsrcを背景画像に設定する (ホバーズーム用)
15function insertImgSrc(sliderElement) {
16    const mainSlides = sliderElement.querySelectorAll(".swiper-slide");
17    mainSlides.forEach((elm) => {
18        const img = elm.querySelector("img");
19        if (!img || !(elm instanceof HTMLElement)) return;
20        elm.style.backgroundImage = `url('${img.getAttribute("src")}')`;
21    });
22}
23
24// マウスオーバーで画像をズームさせる (PCでのみ使用)
25function zoomImage(sliderElement) {
26    const slides = sliderElement.querySelectorAll(".swiper-slide");
27    slides.forEach((elm) => elm.addEventListener("mousemove", (event) => {
28        const zoomElm = event.currentTarget;
29        if (!zoomElm || !(zoomElm instanceof HTMLElement) || window.innerWidth < SP_BREAKPOINT) return; // SPでは無効
30
31        let offsetX = 0;
32        let offsetY = 0;
33        
34        if (event instanceof MouseEvent) {
35            offsetX = event.offsetX;
36            offsetY = event.offsetY;
37        } 
38        
39        const x = (offsetX / zoomElm.offsetWidth) * 100;
40        const y = (offsetY / zoomElm.offsetHeight) * 100;
41        zoomElm.style.backgroundPosition = `${x}% ${y}%`;
42    }));
43}
44
45// ----------------------------------------------------
46// 🌟 モーダルオープン処理を独立した関数として定義 (Swiper.on('click') から呼ばれる)
47// ----------------------------------------------------
48function handleModalOpen(event) {
49    const mainSliderElement = document.querySelector('#js-main-slider');
50    if (!mainSliderElement) return;
51    
52    // クリックされた要素が .swiper-slide 内の img 要素か確認
53    const clickedImg = event.target.closest('.swiper-slide img');
54    if (!clickedImg) return; 
55
56    fixedBodyOn();
57    
58    const expandedModal = document.querySelector('[data-modal-role="image-expanded"]');
59    if (expandedModal) {
60        expandedModal.style.display = 'block'; 
61    }
62    
63    const swiperSlide = clickedImg.closest(".swiper-slide");
64    if (!swiperSlide) return;
65
66    // スライドインデックスの計算: data-index属性から取得
67    const IMAGE_INDEX = parseInt(swiperSlide.getAttribute('data-index'), 10) || 0; 
68    
69    // モーダル内スライダーの初期化
70    const imageExpandedSliderElm = document.querySelector(".js-imageExpandedSlider");
71    if (imageExpandedSliderElm && imageExpandedSliderElm.classList.contains('swiper')) {
72        
73        // 既存のインスタンスがあれば破棄してから再生成
74        if (imageExpandedSliderInstance !== null) {
75            imageExpandedSliderInstance.destroy(true, true);
76        }
77        
78        imageExpandedSliderInstance = new Swiper(imageExpandedSliderElm, {
79            initialSlide: IMAGE_INDEX,
80            loop: true,
81            effect: "fade",
82            zoom: { maxRatio: 4 },
83            preventClicks: false, 
84            preventClicksPropagation: false, 
85            navigation: {
86                nextEl: '.js-expanded-slider-arrow--next',
87                prevEl: '.js-expanded-slider-arrow--prev',
88            },
89            on: {
90                slideChange: function() {
91                    const currentSlideNumber = this.realIndex + 1;
92                    const formattedCurrent = String(currentSlideNumber).padStart(2, '0');
93                    const currentSlideExpanded = document.querySelector('#numberSlidesExpanded .current-slide');
94                    if (currentSlideExpanded) { currentSlideExpanded.innerHTML = formattedCurrent; }
95                },
96                beforeInit: function() {
97                    const numOfSlidesExpanded = this.wrapperEl.querySelectorAll(".swiper-slide").length;
98                    const formattedTotal = String(numOfSlidesExpanded).padStart(2, '0');
99                    const totalSlidesExpanded = document.querySelector('#numberSlidesExpanded .total-slides');
100                    if (totalSlidesExpanded) { totalSlidesExpanded.innerHTML = formattedTotal; }
101                }
102            },
103        });
104    }
105}
106
107
108// ----------------------------------------------------
109// Swiper初期化・破棄を制御するメイン関数
110// ----------------------------------------------------
111function initMainSlider() {
112    const mainSliderElement = document.querySelector('#js-main-slider');
113    
114    if (!mainSliderElement || !mainSliderElement.classList.contains('swiper')) return;
115
116    const currentWidth = window.innerWidth;
117    
118    // 既存のインスタンスが存在する場合、破棄する
119    if (mainSliderInstance !== null) {
120        mainSliderInstance.destroy(true, true); 
121        mainSliderInstance = null;
122    }
123
124    const bar = document.querySelector('.main-slider .progressbar_in');
125    const speed = 3000;
126
127    // 共通の Swiper 設定オブジェクト
128    const commonSwiperConfig = {
129        initialSlide: MAINSLIDER_INDEX,
130        loop: true,
131        preventClicks: false, 
132        preventClicksPropagation: false, 
133        navigation: {
134            nextEl: '.main-slider .js-main-slider-arrow--next',
135            prevEl: '.main-slider .js-main-slider-arrow--prev',
136        },
137        on: {
138          slideChangeTransitionStart: function () {
139            // カスタムプログレスバーのリセット
140            if (bar) {
141                bar.style.transitionDuration = '0s';
142                bar.style.transform = 'scaleX(0)';
143            }
144          },
145          slideChangeTransitionEnd: function () {
146            // カスタムプログレスバーの開始
147            if (bar) {
148                bar.style.transitionDuration = speed + 'ms';
149                bar.style.transform = 'scaleX(1)';
150            }
151          },
152          slideChange: function() {
153            // スライド番号更新 (0埋め)
154            const currentSlideNumber = this.realIndex + 1;
155            const formattedCurrent = String(currentSlideNumber).padStart(2, '0');
156            
157            const currentSlideElement = document.querySelector('#numberSlides .current-slide');
158            if (currentSlideElement) { currentSlideElement.innerHTML = formattedCurrent; }
159            
160            // MAINSLIDER_INDEXの更新
161            MAINSLIDER_INDEX = this.realIndex;
162
163            // variation__panel--item のクリック処理 (元コードのロジックを維持)
164            const targetSelector = `.variation__panel--1 .variation__panel--item[data-panel-index="${this.realIndex}"]`;
165            const targetPanel = document.querySelector(targetSelector);
166            if (targetPanel) { targetPanel.click(); }
167          },
168          beforeInit: function() {
169            // 総スライド数更新 (0埋め)
170            const numOfSlides = this.wrapperEl.querySelectorAll(".swiper-slide").length;
171            const formattedTotal = String(numOfSlides).padStart(2, '0');
172            
173            const totalSlidesElement = document.querySelector('#numberSlides .total-slides');
174            if (totalSlidesElement) { totalSlidesElement.innerHTML = formattedTotal; }
175          },
176          // 🌟 PCでのスワイプによる誤クリックを防ぐため、Swiperの内部クリックイベントを使用
177          click: function(swiper, event) {
178            // スワイプ中でない場合、モーダルオープンの処理を実行
179            if (!swiper.animating && !swiper.isMoving && !swiper.isSwiping) {
180                handleModalOpen(event);
181            }
182          },
183        }
184    };
185
186    let mainSliderConfig = {};
187
188    // PC用設定 (>= 768px)
189    if (currentWidth >= SP_BREAKPOINT) {
190        mainSliderConfig = {
191            ...commonSwiperConfig,
192            effect: "slide", // PCでは通常スライド
193            slidesPerView: "auto", 
194            spaceBetween: 0,
195            centeredSlides: true,
196            // autoplay: false, (無効)
197        };
198    } 
199    // SP用設定 (< 768px)
200    else {
201        mainSliderConfig = {
202            ...commonSwiperConfig,
203            effect: "cards", // SPではcardsエフェクト
204            cardsEffect: {
205                slideShadows: false
206            },
207            slidesPerView: 1, // SPは1枚表示を明示
208            spaceBetween: 0,
209            watchSlidesProgress: true,
210            watchSlidesVisibility: true,
211            autoplay: { 
212              delay: 3000,
213              disableOnInteraction: false,
214            },
215        };
216    }
217
218    // Swiperインスタンスの生成
219    mainSliderInstance = new Swiper(mainSliderElement, mainSliderConfig);
220
221    // 初期化後のユーティリティ関数実行
222    insertImgSrc(mainSliderElement);
223    zoomImage(mainSliderElement);
224}
225
226
227// ----------------------------------------------------
228// DOMContentLoaded イベントリスナー (メイン処理)
229// ----------------------------------------------------
230document.addEventListener('DOMContentLoaded', function() {
231    
232    // ====================================================
233    // 1. DOM要素の参照 (VIEW MORE & SP キャプション)
234    // ====================================================
235    const itemWrappers = document.querySelectorAll('.concept-story-inner');
236    const viewMoreButton = document.getElementById('js-view-more-button');
237    const imageWraps = document.querySelectorAll('.concept-story-inner .concept_story_img_wrap');
238    
239    const mainSliderElement = document.querySelector('#js-main-slider'); // Swiper要素
240    
241    const loadCount = 3; // 1回あたりの追加表示件数
242
243    // ====================================================
244    // 2. VIEW MORE 機能のロジック
245    // ====================================================
246    if (viewMoreButton && itemWrappers.length > 0) {
247        
248        const getHiddenItems = () => {
249            return Array.from(itemWrappers).filter(item => item.classList.contains('is-hidden'));
250        };
251
252        viewMoreButton.addEventListener('click', function() {
253            const hiddenItems = getHiddenItems();
254            const itemsToShow = hiddenItems.slice(0, loadCount);
255
256            itemsToShow.forEach(item => {
257                item.classList.remove('is-hidden');
258            });
259
260            const remainingHidden = getHiddenItems().length;
261
262            if (remainingHidden === 0) {
263                this.style.display = 'none';
264                this.setAttribute('aria-hidden', 'true');
265            }
266        });
267    }
268
269    // ====================================================
270    // 3. SPタップ時のキャプション表示ロジック
271    // ====================================================
272    
273    imageWraps.forEach(function(wrapper) {
274        wrapper.addEventListener('click', function() {
275            const windowWidth = window.innerWidth;
276            
277            if (windowWidth <= SP_BREAKPOINT) {
278                this.classList.toggle('is-caption-open');
279
280                imageWraps.forEach(function(otherWrapper) {
281                    if (otherWrapper !== wrapper) {
282                        otherWrapper.classList.remove('is-caption-open');
283                    }
284                });
285            }
286        });
287    });
288    
289    document.addEventListener('click', function(e) {
290        const windowWidth = window.innerWidth;
291        
292        if (windowWidth <= SP_BREAKPOINT) {
293            const isInsideWrapper = e.target.closest('.concept_story_img_wrap'); 
294            
295            if (!isInsideWrapper) {
296                imageWraps.forEach(function(wrapper) {
297                    wrapper.classList.remove('is-caption-open');
298                });
299            }
300        }
301    });
302
303    // ====================================================
304    // 4. メインスライダー初期化 (トランプスライダー)
305    // ====================================================
306    initMainSlider(); 
307    
308    // ====================================================
309    // 6. モーダルクローズ処理
310    // ====================================================
311    document.querySelectorAll(".js-modalclose").forEach(closeButton => {
312        closeButton.addEventListener('click', function() {
313            fixedBodyOff();
314            
315            const modal = this.closest('.card-modal');
316            if (modal) {
317                modal.style.display = 'none';
318            }
319            
320            // モーダルを閉じるときにスライダーインスタンスを破棄
321            if (imageExpandedSliderInstance) {
322                imageExpandedSliderInstance.destroy(true, true);
323                imageExpandedSliderInstance = null;
324            }
325        });
326    });
327
328    // ====================================================
329    // 7. リサイズイベント処理
330    // ====================================================
331    window.addEventListener('resize', () => {
332        // リサイズ時の不要な再初期化を防ぐ
333        if (window.innerWidth !== BEFORE_RESIZE_WIDTH) {
334            BEFORE_RESIZE_WIDTH = window.innerWidth;
335            
336            // 画面幅が変わったときに Swiper を再初期化
337            initMainSlider(); 
338        }
339    });
340
341    // ====================================================
342    // 8. スクロール検知によるヘッダー処理
343    // ====================================================
344    
345    const headerScrolled = document.querySelector('header');
346
347    if (headerScrolled) {
348        const handleScroll = () => {
349            // window.innerHeight を使用して閾値判定
350            headerScrolled.classList.toggle('scrolled', window.scrollY > window.innerHeight);
351        };
352        // 1. ページ読み込み時に一度、即座にチェックを実行
353        handleScroll();  
354        // 2. スクロールするたびにチェックを実行
355        window.addEventListener('scroll', handleScroll);
356    }
357    // ====================================================
358
359
360    // ====================================================
361    // 9. 動画拡大処理 (座標計算による超高速アニメーション版)
362    // ====================================================
363    const fsVideoOpenBtn = document.querySelector('.js-button--full-screen-video-open');
364    const targetContainer = document.querySelector('.js-target--full-screen-video'); 
365    const fsVideoCloseBtn = document.querySelector('.js-button--full-screen-video-close');
366
367    let placeholder = document.createElement('div');
368    placeholder.className = 'video-placeholder';
369
370    if (fsVideoOpenBtn && targetContainer && fsVideoCloseBtn) {
371        
372        const handleResize = () => {
373            if (targetContainer.classList.contains('is-expanded')) {
374                // アスペクト比チェックによる強制横向きクラスの付与 (タブレット対策など)
375                if (window.innerHeight > window.innerWidth) {
376                    targetContainer.classList.add('is-force-landscape');
377                } else {
378                    targetContainer.classList.remove('is-force-landscape');
379                }
380            }
381        };
382
383        // --- 開く処理 ---
384        fsVideoOpenBtn.addEventListener('click', function() {
385            if (window.innerWidth >= 1025) return; // PCでは無効化
386
387            const rect = targetContainer.getBoundingClientRect();
388            
389            // プレースホルダーの設置
390            placeholder.style.width = targetContainer.offsetWidth + 'px';
391            placeholder.style.height = targetContainer.offsetHeight + 'px';
392            if(targetContainer.parentNode) {
393                targetContainer.parentNode.insertBefore(placeholder, targetContainer);
394            }
395
396            // 初期位置設定 (fixed & transform: none のための座標)
397            targetContainer.style.position = 'fixed';
398            targetContainer.style.top = rect.top + 'px';
399            targetContainer.style.left = rect.left + 'px';
400            targetContainer.style.width = rect.width + 'px';
401            targetContainer.style.height = rect.height + 'px';
402            targetContainer.style.zIndex = '99999';
403            targetContainer.style.transition = 'none';
404            
405            document.body.appendChild(targetContainer);
406            targetContainer.offsetHeight; // Force Reflow
407
408            // アニメーションON
409            targetContainer.style.transition = ''; 
410            
411            requestAnimationFrame(() => {
412                targetContainer.classList.add('is-expanded');
413                targetContainer.style.top = '';
414                targetContainer.style.left = '';
415                targetContainer.style.width = '';
416                targetContainer.style.height = '';
417                handleResize();
418            });
419
420            fixedBodyOn();
421            window.addEventListener('resize', handleResize);
422        });
423
424        // --- 閉じる処理 ---
425        fsVideoCloseBtn.addEventListener('click', function() {
426            const destRect = placeholder.getBoundingClientRect();
427            const videoEl = targetContainer.querySelector('video');
428
429            // 動画タグのアニメーションを「なし(0秒)」に強制設定
430            if(videoEl) {
431                videoEl.style.transition = 'none'; 
432            }
433
434            // クラス削除(これで動画は即座に0度・coverの状態に戻る)
435            targetContainer.classList.remove('is-expanded');
436            targetContainer.classList.remove('is-force-landscape');
437            
438            // コンテナ自体の移動アニメーションは 0.1s (CSSの設定) で実行
439            targetContainer.style.transition = ''; 
440            targetContainer.style.top = destRect.top + 'px';
441            targetContainer.style.left = destRect.left + 'px';
442            targetContainer.style.width = destRect.width + 'px';
443            targetContainer.style.height = destRect.height + 'px';
444            
445            fixedBodyOff();
446            window.removeEventListener('resize', handleResize);
447
448            // 0.1秒後に後片付け
449            setTimeout(() => {
450                if (placeholder.parentNode) {
451                    targetContainer.style = ''; // スタイル全解除
452                    
453                    // 次に開くときのために、videoのtransition設定(none)を消して元に戻す
454                    if(videoEl) {
455                        videoEl.style.transition = ''; 
456                    }
457
458                    placeholder.parentNode.insertBefore(targetContainer, placeholder);
459                    placeholder.parentNode.removeChild(placeholder);
460                }
461            }, 100); 
462        });
463    }
464
465    // ====================================================
466    // 10. 音声切り替え処理
467    // ====================================================
468    const soundToggleBtn = document.querySelector('.js-sound-toggle');
469    // targetContainerがセクション9で定義済み前提
470    const videoEl = targetContainer ? targetContainer.querySelector('video') : null;
471
472    if (soundToggleBtn && videoEl) {
473        soundToggleBtn.addEventListener('click', function(e) {
474            e.stopPropagation(); // 親要素へのバブリング防止(念のため)
475
476            // ミュート状態を反転
477            videoEl.muted = !videoEl.muted;
478
479            // クラスの付け替えによる表示切り替え
480            if (videoEl.muted) {
481                // OFF (Muted)
482                soundToggleBtn.classList.add('is-muted');
483                soundToggleBtn.classList.remove('is-unmuted');
484            } else {
485                // ON (Unmuted)
486                soundToggleBtn.classList.remove('is-muted');
487                soundToggleBtn.classList.add('is-unmuted');
488            }
489        });
490    }
491
492});

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.