PageSourceSearch

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

js illi-stays.com collected 2026-10-09 21:52:29 UTC 29,582 bytes, 754 lines download raw bytes

1import { fixedBodyOn, fixedBodyOff } from './modules/m_fixedBody.js';
2import { createRangePicker, addDragRangeSupport } from './modules/calendar-common.js';
3
4let cleanupAvailabilityPicker = null;
5
6// ====================================================================
7// ★ 在庫チェック機能の定義 (トップレベル)
8// ====================================================================
9
10const resetBookingButtonStatus = function(buttonElement, message = '予約する', disabled = true) {
11    if (!buttonElement) return;
12    
13    // 状態を適用
14    buttonElement.textContent = message;
15    
16    if (disabled) {
17        // ボタンを無効化
18        buttonElement.style.opacity = '0.5';
19        buttonElement.style.backgroundColor = '#ccc';
20        buttonElement.style.pointerEvents = 'none'; 
21        buttonElement.removeAttribute('href'); // hrefを削除または無効な値に
22    } else {
23        // ボタンを有効化
24        buttonElement.style.opacity = '1';
25        buttonElement.style.backgroundColor = ''; 
26        buttonElement.style.pointerEvents = 'auto';
27        // 注意: href属性はcheckAvailability内で設定される
28    }
29};
30
31function debounce(func, delay) {
32    let timeoutId;
33    return function(...args) {
34        if (timeoutId) {
35            clearTimeout(timeoutId);
36        }
37        timeoutId = setTimeout(() => {
38            func.apply(this, args);
39        }, delay);
40    };
41}
42
43const checkAvailability = function(e) {
44    if (e && typeof e.stopPropagation === 'function') {
45        e.stopPropagation();
46    }
47    
48    const currentCheckinInput = document.getElementById('checkin');
49    const currentCheckoutInput = document.getElementById('checkout');
50    const liveBookingButton = document.getElementById('booking-button');
51    const resultDiv = document.getElementById('availability-result');
52    const postId = document.getElementById('post-id')?.value;
53    const priceResultEl = document.getElementById('price-result');
54    const priceValueEl  = document.getElementById('price-value');
55
56    const showPrice = (price) => {
57        if (!priceResultEl || !priceValueEl || !price) { hidePrice(); return; }
58        const nights = Math.round((new Date(checkout) - new Date(checkin)) / 86400000);
59        priceValueEl.textContent = (price * nights).toLocaleString();
60        priceResultEl.style.display = 'block';
61    };
62    const hidePrice = () => { if (priceResultEl) priceResultEl.style.display = 'none'; };
63
64    // 必須要素の存在チェック
65    if (!currentCheckinInput || !currentCheckoutInput || !postId || !liveBookingButton || !resultDiv) {
66        console.error("Required element missing for availability check.");
67        return;
68    }
69
70    const checkin = currentCheckinInput.value;
71    const checkout = currentCheckoutInput.value;
72
73    console.log("--- checkAvailability (Overlay) fired ---"); 
74    
75    // 日付未入力の場合
76    if (!checkin || !checkout) {
77        // ★修正点★: data-base-href に戻してボタンを有効化
78        const baseHref = liveBookingButton.getAttribute('data-base-href');
79        if (baseHref) {
80            liveBookingButton.setAttribute('href', baseHref);
81            resetBookingButtonStatus(liveBookingButton, '予約する', false);
82        } else {
83            resetBookingButtonStatus(liveBookingButton, '予約する', true);
84        }
85        resultDiv.textContent = '';
86        hidePrice();
87        return;
88    }
89
90    // クライアント側での日付検証
91    const today = new Date();
92    today.setHours(0, 0, 0, 0);
93    const checkinDate = new Date(checkin);
94
95    if (checkinDate < today) {
96        resultDiv.textContent = '❌ 過去の日付は選択できません。';
97        resultDiv.style.color = 'red';
98        resetBookingButtonStatus(liveBookingButton, '日付エラー', true);
99        hidePrice();
100        return;
101    }
102
103    if (checkinDate >= new Date(checkout)) {
104        resultDiv.textContent = '❌ チェックアウト日はチェックイン日より後に設定してください。';
105        resultDiv.style.color = 'red';
106        resetBookingButtonStatus(liveBookingButton, '日付エラー', true);
107        hidePrice();
108        return;
109    }
110
111    // サーバーサイドでの在庫チェックへ
112    const data = new URLSearchParams();
113    data.append('action', 'check_availability'); 
114    data.append('post_id', postId);
115    data.append('checkin', checkin);
116    data.append('checkout', checkout);
117    
118    // PHPから渡された illiStaysVars.ajaxUrl を使用
119    const ajaxUrl = (typeof illiStaysVars !== 'undefined' && illiStaysVars.ajaxUrl) ? illiStaysVars.ajaxUrl : '/wp-admin/admin-ajax.php';
120
121    resultDiv.textContent = '確認中...';
122    resultDiv.style.color = 'gray';
123    liveBookingButton.textContent = '確認中...';
124    // 確認中は無効化
125    resetBookingButtonStatus(liveBookingButton, '確認中...', true); 
126
127    fetch(ajaxUrl, {
128        method: 'POST',
129        body: data
130    })
131    .then(response => response.json())
132    .then(response => {
133        console.log('Ajax Response:', response); 
134
135        // 空室・満室に関わらず価格は常に表示(hotel-lowest-price は施設の固定フィールド)
136        showPrice(response.data?.price);
137
138        if (response.success && response.data.available) {
139            resultDiv.textContent = `✅ 空室あり`;
140            resultDiv.style.color = 'green';
141
142            // ★修正点★: data-base-href を使用して URL を生成
143            const baseHref = liveBookingButton.getAttribute('data-base-href');
144
145            if (baseHref) {
146                // ベースURLに日付パラメータを追加
147                const url = new URL(baseHref);
148                url.searchParams.set('start_date', checkin);
149                url.searchParams.set('end_date', checkout);
150
151                const finalUrl = url.toString();
152                console.log("Final Booking URL (Overlay):", finalUrl);
153
154                liveBookingButton.setAttribute('href', finalUrl);
155                resetBookingButtonStatus(liveBookingButton, '予約する', false); // 有効化
156            } else {
157                console.error("Base Href is missing.");
158                resultDiv.textContent = 'URLエラーが発生しました。';
159                resetBookingButtonStatus(liveBookingButton, 'エラー', true);
160            }
161
162        } else {
163            const message = response.data?.message || '選択された日程に空室がありません。';
164            resultDiv.textContent = `❌ ${message}`;
165            resultDiv.style.color = 'red';
166            resetBookingButtonStatus(liveBookingButton, '満室', true); // 無効化
167        }
168    })
169    .catch(error => {
170        console.error('Fetch Error:', error);
171        resultDiv.textContent = '通信エラーが発生しました。';
172        resultDiv.style.color = 'red';
173        resetBookingButtonStatus(liveBookingButton, 'エラー', true); // 無効化
174        hidePrice();
175    });
176};
177
178const debouncedCheckAvailability = debounce(checkAvailability, 300);
179
180// ★ appendTo 引数を追加:
181//   モーダル時は modalContent 要素を渡す(元の動作を維持)
182//   独立ページ時は null を渡す(appendTo なしで createRangePicker を呼ぶ)
183function initializeAvailabilityChecker(appendTo = null) {
184  const form = document.getElementById('availability-form');
185  const bookingButton = document.getElementById('booking-button'); 
186  const checkinEl = document.getElementById('checkin');
187  const checkoutEl = document.getElementById('checkout');
188
189  if (form && bookingButton && checkinEl && checkoutEl) {
190
191      // 前回のピッカーが残っていればクリーンアップ
192      if (cleanupAvailabilityPicker) {
193          cleanupAvailabilityPicker();
194          cleanupAvailabilityPicker = null;
195      }
196
197      // appendTo が null の場合(独立ページ)はオプションに含めない
198      const { picker, langObserver } = createRangePicker({
199          checkinEl,
200          checkoutEl,
201          ...(appendTo ? { appendTo } : {})
202      });
203      const dragCleanup = addDragRangeSupport(picker);
204
205      cleanupAvailabilityPicker = () => {
206          dragCleanup();
207          langObserver.disconnect();
208          picker.destroy();
209      };
210
211      // 以下、既存のロジックを維持
212      if (!bookingButton.getAttribute('data-base-href')) {
213            const baseHref = bookingButton.getAttribute('href') || '';
214            const cleanBaseHref = baseHref.split('?')[0]; 
215            bookingButton.setAttribute('data-base-href', cleanBaseHref);
216      }
217      const initialBaseHref = bookingButton.getAttribute('data-base-href');
218      if (initialBaseHref) {
219          bookingButton.setAttribute('href', initialBaseHref);
220          resetBookingButtonStatus(bookingButton, '予約する', false); 
221      }
222      form.addEventListener('submit', function(e) { e.preventDefault(); });
223  }
224}
225
226
227document.addEventListener('DOMContentLoaded', () => {
228  const modal = document.getElementById('hotelDetailModal');
229  const modalContent = modal ? document.getElementById('hotelDetailModalContent') : null;
230
231  // ====================================================================
232  // 閲覧履歴(モーダル・独立ページ共通)
233  // ====================================================================
234
235  const HISTORY_KEY = 'viewedHotelsHistory'; 
236  const MAX_HISTORY_ITEMS = 20;
237
238  function getHistory() {
239    const historyJson = localStorage.getItem(HISTORY_KEY);
240    return historyJson ? JSON.parse(historyJson) : [];
241  }
242
243  function saveHistory(historyArray) {
244    localStorage.setItem(HISTORY_KEY, JSON.stringify(historyArray));
245  }
246
247  function addToHistory(hotelData) {
248    let history = getHistory();
249    history = history.filter(item => item.id !== hotelData.id);
250    history.unshift(hotelData);
251    history = history.slice(0, MAX_HISTORY_ITEMS);
252    saveHistory(history);
253  }
254
255  // ★ scope 引数を追加: モーダル時は modal 要素、独立ページ時は document.body を渡す
256  function initializeHistoryView(scope, currentPostId) {
257    const history = getHistory();
258    const section = scope.querySelector('#recentlyViewedSection');
259    const listContainer = scope.querySelector('#recentlyViewedList');
260    
261    if (!section || !listContainer) return;
262
263    const filteredHistory = history.filter(item => item.id.toString() !== currentPostId);
264    const displayList = filteredHistory.slice(0, 6);
265
266    listContainer.innerHTML = '';
267
268    if (displayList.length > 0) {
269      section.style.display = 'block';
270      
271      displayList.forEach(item => {
272        const listItem = document.createElement('li');
273        listItem.className = 'recently-viewed-item';
274        
275        let imageHtml = '';
276        if (item.thumbnail) {
277          imageHtml = `
278            <div class="recently-viewed-item__image">
279              <img src="${item.thumbnail}" alt="">
280            </div>`;
281        } else {
282          imageHtml = `
283            <div class="recently-viewed-item__image" data-state="noimage">
284              <span>No Image</span>
285            </div>`;
286        }
287
288        let locationHtml = '';
289        if (item.location) {
290          locationHtml = `<p class="recently-viewed-item__location">${item.location}</p>`;
291        }
292
293        listItem.innerHTML = `
294          <a href="${item.url}" class="js-history-link" data-post-id="${item.id}">
295            ${imageHtml}
296            <div class="recently-viewed-item__text">
297              <p class="recently-viewed-item__title">${item.title}</p>
298              ${locationHtml}
299            </div>
300          </a>`;
301        
302        listContainer.appendChild(listItem);
303      });
304      } else {
305        section.style.display = 'none';
306      }
307  }
308
309  // ★ scope 引数を追加: モーダル時は modal 要素、独立ページ時は document.body を渡す
310  function initializeReadMore(scope) {
311    const descriptionAreas = scope.querySelectorAll('.rooms-detail-location__description-area');
312    if (!descriptionAreas.length) return;
313    descriptionAreas.forEach(area => {
314      const description = area.querySelector('.rooms-detail-location__description p');
315      const button = area.querySelector('.js-button-viremore--text');
316      if (!description || !button) return;
317      const lineHeight = parseFloat(window.getComputedStyle(description).lineHeight);
318      const fiveLinesHeight = lineHeight * 5;
319      if (description.scrollHeight > fiveLinesHeight) {
320        area.classList.add('is-clamp-text');
321        if (!button.dataset.initialized) {
322          button.addEventListener('click', () => area.classList.add('is-opened'), { once: true });
323          button.dataset.initialized = 'true';
324        }
325      }
326    });
327  }
328
329  // ★ scope 引数を追加: モーダル時は modal 要素、独立ページ時は document.body を渡す
330  function initializeRoomInfoTabs(scope) {
331    const tabList = scope.querySelector('.room-information-tabs');
332    if (!tabList) return;
333    if (tabList.dataset.initialized) return;
334    tabList.dataset.initialized = 'true';
335
336    tabList.addEventListener('click', function(event) {
337      const clickedTab = event.target.closest('button[role="tab"]');
338      if (!clickedTab) return;
339
340      if (window.innerWidth <= 768) {
341        const detailContainer = scope.querySelector('.rooms-detail');
342        if (detailContainer) {
343          detailContainer.scrollIntoView({ behavior: 'auto', block: 'start' });
344        }
345      } else {
346        const infoColumn = scope.querySelector('.rooms-detail-information');
347        if (infoColumn) {
348          infoColumn.scrollTop = 0;
349        }
350      }
351
352      const allTabs = tabList.querySelectorAll('button[role="tab"]');
353      allTabs.forEach(tab => {
354        tab.classList.remove('active');
355        tab.setAttribute('aria-selected', 'false');
356      });
357
358      const allContents = scope.querySelectorAll('.room-info-content');
359      allContents.forEach(content => content.classList.remove('active'));
360
361      clickedTab.classList.add('active');
362      clickedTab.setAttribute('aria-selected', 'true');
363
364      const targetIds = clickedTab.dataset.controls.split(' ');
365      targetIds.forEach(id => {
366        if (id) {
367          const targetContent = document.getElementById(id);
368          if (targetContent) {
369            targetContent.classList.add('active');
370            initializeSwiper(targetContent);
371            resizeAFrameScene(targetContent);
372          }
373        }
374      });
375    });
376  }
377  
378  function activateTabByHash(scope, hash) {
379    if (!hash) return;
380    const decoded = decodeURIComponent(hash.replace(/^#/, ''));
381    if (!decoded) return;
382    const tabs = scope.querySelectorAll('button[role="tab"]');
383    for (const tab of tabs) {
384      if (tab.textContent.trim() === decoded) {
385        tab.click();
386        return;
387      }
388    }
389  }
390
391  // ★ scope 引数を追加: モーダル時は modal 要素、独立ページ時は document.body を渡す
392  function initializeEquipmentReadMore(scope) {
393    const equipmentAreas = scope.querySelectorAll('.equipment-contents');
394    if (!equipmentAreas.length) return;
395    equipmentAreas.forEach(area => {
396      const items = area.querySelectorAll('.equipment-item');
397      const button = area.querySelector('.js-button-viremore--equipment-area');
398      if (!items.length || !button) return;
399      const itemCount = items.length;
400      const browserWidth = window.innerWidth;
401      let needsClamp = false;
402      if (browserWidth >= 769 && itemCount >= 10) needsClamp = true;
403      else if (browserWidth <= 768 && itemCount >= 11) needsClamp = true;
404      if (needsClamp) {
405        area.classList.add('is-clamp-equipment');
406        if (!button.dataset.initialized) {
407          button.addEventListener('click', () => {
408            area.classList.remove('is-clamp-equipment');
409            area.classList.add('is-opened-equipment');
410          }, { once: true });
411          button.dataset.initialized = 'true';
412        }
413      }
414    });
415  }
416
417  function initializeSwiper(container) {
418    const sliderContainer = container.querySelector('.room-detail-image-slide');
419    if (!sliderContainer || sliderContainer.dataset.swiperInitialized) return;
420    const swiperMainEl = sliderContainer.querySelector('.swiper-main');
421    const swiperThumbEl = sliderContainer.querySelector('.swiper-thumb');
422    if (!swiperMainEl || !swiperThumbEl) return;
423
424    const mySwiper_thumb = new Swiper(swiperThumbEl, {
425      slidesPerView: 5,
426      spaceBetween: 8,
427      breakpoints: {
428        769: { slidesPerView: 7 },
429      },
430    });
431
432    const mySwiper_main = new Swiper(swiperMainEl, {
433      grabCursor: false,
434      allowTouchMove: false,
435      navigation: {
436        nextEl: sliderContainer.querySelector('.swiper-button-next'),
437        prevEl: sliderContainer.querySelector('.swiper-button-prev'),
438      },
439      pagination: {
440        el: ".custom-fraction",
441        type: "fraction",
442        renderFraction:function (currentClass, totalClass) {
443          return '<span class="' + currentClass + '"></span>' + '|' + '<span class="' + totalClass + '"></span>';
444        }
445      },
446      thumbs: {
447        swiper: mySwiper_thumb,
448      },
449    });
450
451    sliderContainer.dataset.swiperInitialized = 'true';
452  }
453
454  function resizeAFrameScene(container) {
455    const aframeScene = container.querySelector('a-scene');
456    if (aframeScene && typeof aframeScene.resize === 'function') {
457      setTimeout(() => {
458        aframeScene.resize();
459      }, 100);
460    }
461  }
462
463  function trapFocus(element) {
464    const focusableElements = Array.from(
465      element.querySelectorAll('a, button, textarea, input, select, [tabindex]:not([tabindex="-1"])')
466    ).filter(el => !el.hasAttribute('disabled') && el.offsetParent !== null);
467    if (focusableElements.length === 0) return () => {};
468    const firstFocusableElement = focusableElements[0];
469    const lastFocusableElement = focusableElements[focusableElements.length - 1];
470    const handleFocusTrap = (event) => {
471      if (event.key === 'Tab') {
472        if (event.shiftKey) {
473          if (document.activeElement === firstFocusableElement) {
474            event.preventDefault();
475            lastFocusableElement.focus();
476          }
477        } else {
478          if (document.activeElement === lastFocusableElement) {
479            event.preventDefault();
480            firstFocusableElement.focus();
481          }
482        }
483      }
484    };
485    firstFocusableElement.focus();
486    element.addEventListener('keydown', handleFocusTrap);
487    return () => {
488      element.removeEventListener('keydown', handleFocusTrap);
489    };
490  }
491
492  // ★ scope・modalContent 引数を追加(モーダル専用関数)
493  async function loadContent(url, scope, modalContent) {
494    modalContent.innerHTML = `
495      <div class="loading-container">
496        <svg width="200" height="100" viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg">
497          <g transform="translate(22 10)">
498            <circle cx="18.445" cy="18.445" r="18.445" class="logo-shape shape-1" />
499            <path d="M39.408,0 H76.3 V36.89 H56.3 A20,20 0 0 1 39.408,18.445 Z" class="logo-shape shape-2" />
500            <path d="M78.813,0 H115.7 V36.89 H95.7 A20,20 0 0 1 78.813,18.445 Z" class="logo-shape shape-3" />            
501            <circle cx="136.665" cy="18.445" r="18.445" class="logo-shape shape-4" />
502            <path fill="#1a1311" d="M101.407,80.572l4.4-8.13-5.7-10.175h3.927l3.706,6.8,3.316-6.8h3.817l-9.65,18.305Zm-38.862-9.4,2.353-2.24a4.318,4.318,0,0,0,3.868,2.461,2.587,2.587,0,0,0,2.848-2.545,2.14,2.14,0,0,0-.717-1.659,8.3,8.3,0,0,0-2.849-1.354,8.9,8.9,0,0,1-3.069-1.6A4.3,4.3,0,0,1,63.512,60.8c0-2.71,1.827-5.42,5.918-5.42a
5027.089,7.089,0,0,1,5.225,2.157L72.775,60a3.6,3.6,0,0,0-3.014-1.577c-1.934,0-2.545,1.218-2.545,2.158a1.557,1.557,0,0,0,.581,1.244,6.354,6.354,0,0,0,2.212,1.051,10.725,10.725,0,0,1,3.735,1.851A5.02,5.02,0,0,1,75.32,68.6c0,3.6-2.6,6.057-6.416,6.057A7.592,7.592,0,0,1,62.545,71.169Zm52.877,1.467,1.77-2.045a3.712,3.712,0,0,0,2.654,1.381c.94,0,1.659-.468,1.659-1.3a1.337,1.337,0,0,0-1.051-1.218l-.773-.275a6.46,6.46,0,0,1-2.572-1.438,3.213,3.213,0,0,1-.912-2.3,3.554,3.554,0,0,1,1.244-2.71,4.938,4.938,0,0,1,3.124-.856,5.975,5.975,0,0,1,3.817,1.271l-1.66,1.965a2.8,2.8,0,0,0-1.963-.8c-.8,0-1.3.387-1.3.968,0,.3.139.774,1.079,1.079l.8.249a6.124,6.124,0,0,1,3.042,1.714,3.477,3.477,0,0,1,.608,1.991,3.977,3.977,0,0,1-1.326,3.039,5.029,5.029,0,0,1-3.6,1.272A6.054,6.054,0,0,1,115.422,72.636ZM85.16,68.24c0-3.54,2.405-6.36,5.946-6.36a4.331,4.331,0,0,1,3.842,1.935V62.266h3.375V74.239H94.947V72.691a4.287,4.287,0,0,1-3.676,1.935A6.075,6.075,0,0,1,85.16,68.24Zm3.484,0a3.319,3.319,0,1,0,3.318-3.373A3.278,3.278,0,0,0,88.644,68.24Zm-38.862,6V62.266h3.373V74.239Zm-19.731,0V62.266h3.373V74.239Zm47.78,0v-9.1H76.906V62.266h1.326V58.173h3.375v4.093h2.322v2.875H81.607v9.1Zm-34.631,0V54.079h3.375V74.238Zm-6.566,0V54.079h3.373V74.238ZM49.851,57.207a2.019,2.019,0,1,1,2.019,2.017A2.029,2.029,0,0,1,49.851,57.207Zm-19.731,0a2.019,2.019,0,1,1,2.019,2.017A2.03,2.03,0,0,1,30.12,57.207Z"/>
503          </g>
504        </svg>
505      </div>
506      `;
507    
508    const infoColumn = scope.querySelector('.rooms-detail-information');
509    if (infoColumn) {
510      infoColumn.scrollTop = 0;
511    }
512
513    try {
514      // const response = await fetch(url);
515       const response = await fetch(url, {
516        headers: {
517          'X-Requested-With': 'modal-request'
518        }
519      });
520      if (!response.ok) throw new Error('コンテンツの読み込みに失敗しました。');
521      const text = await response.text();
522      const parser = new DOMParser();
523      const doc = parser.parseFromString(text, 'text/html');
524      
525      const content = doc.querySelector('.content-main');
526
527      if (content) {
528        const postId = content.dataset.postId;
529        const postTitle = content.dataset.postTitle;
530        const postUrl = content.dataset.postUrl;
531        const postThumbnail = content.dataset.postThumbnail;
532        const postLocation = content.dataset.postLocation;
533
534        if (postId && postTitle && postUrl) {
535          addToHistory({
536            id: postId,
537            title: postTitle,
538            url: postUrl,
539            thumbnail: postThumbnail,
540            location: postLocation 
541          });
542
543          if (typeof handleMarkerClick === 'function') {
544            handleMarkerClick(postId);
545          }
546        }
547        
548        modalContent.innerHTML = content.innerHTML;
549        
550        initializeHistoryView(scope, postId); 
551        initializeReadMore(scope);
552        initializeRoomInfoTabs(scope);
553        initializeEquipmentReadMore(scope);
554
555        const activeTabContent = scope.querySelector('.room-info-content.active');
556        if (activeTabContent) {
557          initializeSwiper(activeTabContent);
558          // ★ 修正箇所: targetContent という未定義の変数名ミスを activeTabContent に修正しました
559          resizeAFrameScene(activeTabContent); 
560        }
561
562        // ★ エラーが解消されたため、ここからカレンダーの初期化(翻訳設定含む)が確実に実行されます
563        initializeAvailabilityChecker(modalContent);
564        
565      } else {
566        modalContent.innerHTML = '<p>コンテンツを取得できませんでした。</p>';
567      }
568    } catch (error) {
569      console.error('Fetch Error:', error);
570      modalContent.innerHTML = '<p>エラーが発生しました。ページを再読み込みしてください。</p>';
571    }
572  }
573
574  // ====================================================================
575  // ① モーダル
576  //    #hotelDetailModal が存在する場合のみ起動
577  // ====================================================================
578
579  if (modal) {
580    const closeButton = modal.querySelector('.hotel-detail-modal__close');
581    let lastFocusedElement, disableFocusTrap;
582
583    document.body.addEventListener('click', (e) => {
584      const trigger = e.target.closest('.js-show-hotel-detail-modal-toriger');
585      if (!trigger) return;
586      e.preventDefault();
587      lastFocusedElement = document.activeElement;
588      const url = trigger.href;
589      const hash = new URL(trigger.getAttribute('href'), location.href).hash;
590      modal.showModal();
591      fixedBodyOn();
592
593      modal.scrollTop = 0;
594
595      loadContent(url, modal, modalContent).then(() => {
596        disableFocusTrap = trapFocus(modal);
597        activateTabByHash(modal, hash);
598      });
599    });
600
601    modalContent.addEventListener('click', (e) => {
602      const historyLink = e.target.closest('.js-history-link');
603      if (!historyLink) return;
604
605      e.preventDefault(); 
606      const url = historyLink.href;
607
608      if (window.innerWidth <= 768) {
609        const detailContainer = modal.querySelector('.rooms-detail');
610        if (detailContainer) {
611          detailContainer.scrollIntoView({ behavior: 'auto', block: 'start' });
612        }
613      }
614      
615      loadContent(url, modal, modalContent);
616    });
617
618    const closeModal = () => {
619      modal.close();
620    };
621    closeButton.addEventListener('click', closeModal);
622    modal.addEventListener('click', (e) => {
623      if (e.target === modal) closeModal();
624    });
625    modal.addEventListener('close', () => {
626      fixedBodyOff();
627      if (typeof disableFocusTrap === 'function') disableFocusTrap();
628      if (cleanupAvailabilityPicker) {
629          cleanupAvailabilityPicker();
630          cleanupAvailabilityPicker = null;
631      }
632      modalContent.innerHTML = '';
633      if (lastFocusedElement) lastFocusedElement.focus();
634    });
635
636    console.log("★★ Event Delegation Listeners Registered (Overlay) ★★");
637
638    // DOMContentLoadedの最後に、AvailabilityCheckerを初期化
639    initializeAvailabilityChecker(modalContent);
640  }
641
642  // ====================================================================
643  // ② 独立詳細ページ
644  //    #hotelDetailModal が存在せず、.content-main が直接ページにある場合に起動
645  // ====================================================================
646
647  if (!modal) {
648    const standaloneContent = document.querySelector('.content-main');
649    if (!standaloneContent) return;
650
651    // 独立ページであることを示すclassを付与(CSS出しわけ用)
652    standaloneContent.classList.add('content-main--standalone');
653
654    const postId        = standaloneContent.dataset.postId;
655    const postTitle     = standaloneContent.dataset.postTitle;
656    const postUrl       = standaloneContent.dataset.postUrl;
657    const postThumbnail = standaloneContent.dataset.postThumbnail;
658    const postLocation  = standaloneContent.dataset.postLocation;
659
660    // 閲覧履歴に追加
661    if (postId && postTitle && postUrl) {
662      addToHistory({
663        id: postId,
664        title: postTitle,
665        url: postUrl,
666        thumbnail: postThumbnail,
667        location: postLocation
668      });
669    }
670
671    // モーダルと同じ初期化処理を document.body スコープで実行
672    initializeHistoryView(document.body, postId);
673    initializeReadMore(document.body);
674    initializeRoomInfoTabs(document.body);
675    initializeEquipmentReadMore(document.body);
676
677    const activeTabContent = document.querySelector('.room-info-content.active');
678    if (activeTabContent) {
679      initializeSwiper(activeTabContent);
680      resizeAFrameScene(activeTabContent);
681    }
682
683    // appendTo なし(独立ページではモーダルコンテナが存在しないため)でカレンダー初期化
684    initializeAvailabilityChecker(null);
685
686    activateTabByHash(document.body, window.location.hash);
687
688    console.log("★★ Standalone Detail Page Initialized ★★");
689  }
690
691  // change / input はモーダル・独立ページ共通で委譲
692  document.addEventListener('change', function(e) {
693    if (e.target && (e.target.id === 'checkin' || e.target.id === 'checkout')) {
694      debouncedCheckAvailability(e); 
695    }
696  });
697
698  document.addEventListener('input', function(e) {
699    if (e.target && (e.target.id === 'checkin' || e.target.id === 'checkout')) {
700      debouncedCheckAvailability(e);
701    }
702  });
703
704});
705
706
707// ====================================================================
708// ★ URLコピーボタンの処理
709// ====================================================================
710document.addEventListener('click', function(e) {
711    const copyBtn = e.target.closest('.js-copy-btn');
712    if (!copyBtn) return;
713
714    // 1. URL入力欄を探す
715    const targetInput = document.querySelector('.room-info-content.active .js-url-target') 
716                     || document.querySelector('.js-url-target');
717    
718    if (!targetInput) return;
719
720    // 2. 一時的に非表示からテキストに変更して選択
721    const originalType = targetInput.type;
722    targetInput.type = "text";
723    targetInput.style.position = "fixed";
724    targetInput.style.left = "-9999px";
725    
726    targetInput.select();
727    
728    try {
729        const successful = document.execCommand('copy');
730        if (successful) {
731            // ★成功した場合のみモーダルを表示
732            showClipboardMessage();
733        }
734    } catch (err) {
735        console.error('コピー失敗', err);
736    }
737
738    // 3. 元に戻す
739    targetInput.type = originalType;
740});
741
742// モーダル表示用関数
743function showClipboardMessage() {
744    const messageEl = document.getElementById('clipboardMessage');
745    if (!messageEl) return;
746
747    // クラスを追加して表示(CSS側で .is-show などを定義してください)
748    messageEl.classList.add('is-active'); 
749
750    // 2秒後にクラスを削除
751    setTimeout(() => {
752        messageEl.classList.remove('is-active');
753    }, 2000);
754}

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.