PageSourceSearch

https://buysell-kaitori.com/wp-content/themes/bsportal/assets/js/new/store-list.js

js buysell-kaitori.com collected 2026-10-02 14:39:34 UTC 39,761 bytes, 1,037 lines download raw bytes

1// Google Maps側のコールバック(callback=initMap)は async 属性の関係で
2// DOMContentLoaded より先に発火することがあるため、
3// スクリプト読み込み直後から呼び出せる状態にしておく
4window.initMap = function() {
5  window.__googleMapsReady = true;
6  if (typeof window.__runInitMap === 'function') {
7    window.__runInitMap();
8  }
9};
10
11document.addEventListener('DOMContentLoaded', function() {
12
13  const apiStoresUrl = '/wp-json/bsportal/v1/stores';
14  const apiCitiesUrl = '/wp-json/bsportal/v1/cities';
15  
16  const prefSelect = document.getElementById('pref-select');
17  const citySelect = document.getElementById('city-select');
18  const moreBtn = document.querySelector('.js-store-more-btn');
19  const mapBox = document.querySelector('.p-store-purchase__result_map');
20
21  const jsPref = document.querySelector('.js-pref');
22  const jsCity = document.querySelector('.js-city');
23  const jsNearby = document.querySelector('.js-nearby');
24  
25  let currentPage = 1;
26  let currentLat = null;
27  let currentLng = null;
28  let currentSearchType = 'area';
29
30  let cachedTemplateHTML = '';
31  let cachedMapTemplateHTML = '';
32
33  const templateElement = document.getElementById('template_list');
34  const mapTemplateElement = document.getElementById('template_map');
35
36  const isTaxonomyPage = window.location.pathname.includes('/store-area/');
37
38  if (templateElement) {
39    cachedTemplateHTML = templateElement.innerHTML;
40    templateElement.remove();
41  }
42  if (mapTemplateElement) {
43    cachedMapTemplateHTML = mapTemplateElement.innerHTML;
44    mapTemplateElement.remove();
45  }
46
47  // 都道府県選択時の処理
48  if (prefSelect && !isTaxonomyPage) {
49    prefSelect.addEventListener('change', function() {
50      const pref = this.value;
51      
52      // 市区町村セレクトを初期化
53      if (citySelect) {
54        citySelect.innerHTML = '<option value="">市区町村を選択</option>';
55        citySelect.disabled = true;
56      }
57
58      if (!pref) {
59        // 都道府県が空の場合は全件表示等で更新
60        currentPage = 1;
61        currentLat = null;
62        currentLng = null;
63        currentSearchType = 'area';
64        fetchStoreList({
65          searchType: currentSearchType,
66          pref: '',
67          city: '',
68          page: currentPage,
69          isAppend: false
70        });
71        return;
72      }
73
74      // 市区町村の取得リクエスト (REST API)
75      const formData = new FormData();
76      formData.append('pref', pref);
77
78      fetch(apiCitiesUrl, {
79        method: 'POST',
80        body: formData
81      })
82      .then(response => response.json())
83      .then(data => {
84        if (data.success && data.data.length > 0 && citySelect) {
85          data.data.forEach(city => {
86            const option = document.createElement('option');
87            option.value = city;
88            option.textContent = city;
89            citySelect.appendChild(option);
90          });
91          citySelect.disabled = false;
92        }
93      })
94      .catch(error => console.error('Error fetching cities:', error));
95
96      // 都道府県が変更されたタイミングで店舗一覧を更新
97      currentPage = 1;
98      currentLat = null;
99      currentLng = null;
100      currentSearchType = 'area';
101      fetchStoreList({
102        searchType: currentSearchType,
103        pref: pref,
104        city: '',
105        page: currentPage,
106        isAppend: false
107      });
108    });
109  }
110
111  // 市区町村選択時の処理
112  if (citySelect) {
113    citySelect.addEventListener('change', function() {
114      const pref = prefSelect ? prefSelect.value : '';
115      const city = this.value;
116      currentPage = 1;
117      currentLat = null;
118      currentLng = null;
119      currentSearchType = 'area';
120      fetchStoreList({
121        searchType: currentSearchType,
122        pref: pref,
123        city: city,
124        page: currentPage,
125        isAppend: false
126      });
127    });
128  }
129
130  // 新しいデザイン用の店舗リストをREST APIで取得するベース関数
131  function fetchStoreList(params) {
132    const {
133      searchType = 'area',
134      pref = '',
135      city = '',
136      page = 1,
137      isAppend = false,
138      lat = null,
139      lng = null,
140      bounds = null, // { swLat, swLng, neLat, neLng }
140 地図の表示範囲(現在地検索でのドラッグ/ズーム再検索用)
141      mapOnly = false // true の場合、地図とその下のカードだけを更新し、一覧タブ・件数・検索条件表示は変更しない
142    } = params;
143
144    if (mapBox) {
145      const hasSearched = searchType === 'nearby' || !!pref || !!city;
146      mapBox.classList.toggle('is-searched', hasSearched);
147    }
148
149    if (!isAppend && !mapOnly) {
150      if (jsPref && jsCity && jsNearby) {
151        if (searchType === 'nearby') {
152          jsPref.style.display = 'none';
153          jsCity.style.display = 'none';
154          jsNearby.style.display = '';
155        } else {
156          jsNearby.style.display = 'none';
157          if (pref || city) {
158            const formDataSlug = new FormData();
159            if (pref) formDataSlug.append('pref', pref);
160            if (city) formDataSlug.append('city', city);
161            fetch('/wp-json/bsportal/v1/area-slug', {
162              method: 'POST',
163              body: formDataSlug
164            })
165            .then(res => res.json())
166            .then(slugData => {
167              if (slugData.success) {
168                const prefSlug = slugData.data.pref_slug;
169                const citySlug = slugData.data.city_slug;
170                if (pref) {
171                  jsPref.innerHTML = prefSlug ? `<a href="/store-area/${prefSlug}/">${pref}</a>` : pref;
172                  jsPref.style.display = '';
173                } else {
174                  jsPref.textContent = 'なし';
175                  jsPref.style.display = '';
176                }
177                if (city) {
178                  jsCity.innerHTML = `/ ${city}`;
179                  jsCity.style.display = '';
180                } else {
181                  jsCity.style.display = 'none';
182                }
183              }
184            })
185            .catch(error => console.error('Error fetching area slug:', error));
186          } else {
187            jsPref.textContent = 'なし';
188            jsPref.style.display = '';
189            jsCity.style.display = 'none';
190          }
191        }
192      }
193    }
194
195    const formData = new FormData();
196    formData.append('search_type', searchType);
197    formData.append('page', page);
198
199    if (searchType === 'area') {
200      formData.append('pref', pref);
201      formData.append('city', city);
202    } else if (searchType === 'nearby' && lat !== null && lng !== null) {
203      formData.append('lat', lat);
204      formData.append('lng', lng);
205    }
206
207    if (bounds) {
208      formData.append('sw_lat', bounds.swLat);
209      formData.append('sw_lng', bounds.swLng);
210      formData.append('ne_lat', bounds.neLat);
211      formData.append('ne_lng', bounds.neLng);
212    }
213
214    fetch(apiStoresUrl, {
215      method: 'POST',
216      body: formData
217    })
218    .then(response => response.json())
219    .then(data => {
220      if (data.success) {
221
222        // 店舗数(.js-store-count)
223        if (!mapOnly) {
224          const countElements = document.querySelectorAll('.js-store-count');
225          countElements.forEach(el => {
226            el.textContent = data.data.total_count || 0;
227          });
228        }
229
230        const target = document.getElementById('js_store_result');
231        const mapTarget = document.getElementById('js_store_map');
232        const MAP_LIST_LIMIT = 10; // マップ用リストの最大表示件数
233        let currentMapListCount = 0;
234
235        // 前回の検索結果をクリア
236        if (target && !isAppend && !mapOnly) {
237          target.innerHTML = '';
238        }
239        if (mapTarget && !isAppend) {
240          if (window.jQuery && $(mapTarget).hasClass('slick-initialized')) {
241            $(mapTarget).slick('unslick');
242          }
243          mapTarget.innerHTML = '';
244        } else if (mapTarget && isAppend) {
245          if (window.jQuery && $(mapTarget).hasClass('slick-initialized')) {
246            currentMapListCount = $(mapTarget).slick('getSlick').slideCount;
247          } else {
248            currentMapListCount = mapTarget.children.length;
249          }
250        }
251
252        if (cachedTemplateHTML && target && data.data && data.data.list.length > 0) {
253          const templateHTML = cachedTemplateHTML;
254          const mapTemplateHTML = cachedMapTemplateHTML;
255          let combinedHTML = '';
256          let combinedMapHTML = '';
257
258
259          // 値が空だった場合にタグごと削除するキーの配列
260          const removeIfEmptyKeys = ['store_km', 'store_post_tel'];
261
262          const processTemplate = (html, store) => {
263            let itemHTML = html;
264
265            // 値が無ければ目印をつけておく(後のDOM操作でタグごと削除するため)
266            removeIfEmptyKeys.forEach(key => {
267              if (!store[key]) {
268                const regex = new RegExp(`{{${key}}}`, 'g');
269                itemHTML = itemHTML.replace(regex, `{{REMOVE_${key.toUpperCase()}}}`);
270              }
271            });
272
273            // テンプレート内の {{key}} を store オブジェクトの値で置換
274            for (const key in store) {
275              const regex = new RegExp(`{{${key}}}`, 'g');
276              let value = store[key];
277
278              // {{store_rate_***}} が空ならハイフンに
279              if (key.startsWith('store_rate_') && !value) {
280                value = '-';
281              }
282
283              itemHTML = itemHTML.replace(regex, value || '');
284            }
285
286            // store_service の配列内のフラグでtrueなら {{store_service_***}} をis-activeに置換
287            const serviceMatches = itemHTML.match(/{{store_service_([^}]+)}}/g);
288            if (serviceMatches) {
289              const uniqueMatches = [...new Set(serviceMatches)]; // 重複を排除
290              uniqueMatches.forEach(placeholder => {
291                const serviceName = placeholder.replace('{{store_service_', '').replace('}}', '');
292                if (Array.isArray(store.store_service) && store.store_service.includes(serviceName)) {
293                  itemHTML = itemHTML.split(placeholder).join('is-active');
294                } else {
295                  itemHTML = itemHTML.split(placeholder).join('');
296                }
297              });
298            }
299
300            // 文字列をDOMに変換して操作しやすくする
301            const tempDiv = document.createElement('div');
302            tempDiv.innerHTML = itemHTML;
303
304
305            const detailWrap = tempDiv.querySelector('.p-store-purchase__result_item_rating_detail');
306
307            const serviceItem =
308              detailWrap?.children[0];
309
310            const atmosphereItem =
311              detailWrap?.children[1];
312
313            const priceItem =
314              detailWrap?.children[2];
315
316            const hasService = store.store_rate_service;
317            const hasAtmosphere = store.store_rate_atmosphere;
318            const hasPrice = store.store_rate_purchase_price;
319
320            if (!hasService && serviceItem) {
321              serviceItem.style.display = 'none';
322            }
323
324            if (!hasAtmosphere && atmosphereItem) {
325              atmosphereItem.style.display = 'none';
326            }
327
328            if (!hasPrice && priceItem) {
329              priceItem.style.display = 'none';
330            }
331
332            if (!hasService && !hasAtmosphere && !hasPrice && detailWrap) {
333              detailWrap.style.display = 'none';
334            }
335
336            const ratingWraps = tempDiv.querySelectorAll(
337              '.p-store-purchase__result_item_rating-wrap, .p-store-purchase__result_map_list_item_rating'
338            );
339
340            if (!store.store_place_id) {
341              ratingWraps.forEach(el => {
342                el.style.display = 'none';
343              });
344            }
345
346            // store_place_rating評価から星の画像を作成
347            const rating = parseFloat(store.store_place_rating) || 0;
348            const fullStar = tempDiv.querySelector('.js-star-icon-full');
349            const halfStar = tempDiv.querySelector('.js-star-icon-half');
350            const noneStar = tempDiv.querySelector('.js-star-icon-none');
351
352            // 3種類の星アイコンがテンプレート内に存在する場合のみ処理を実行
353            if (fullStar && halfStar && noneStar) {
354              const starContainer = fullStar.parentElement;
355              starContainer.innerHTML = ''; // テンプレートの元アイコンを一旦クリア
356
357              for (let i = 1; i <= 5; i++) {
358                if (rating >= i) {
359                  starContainer.appendChild(fullStar.cloneNode(true));
360                } else if (rating >= i - 0.5) {
361                  starContainer.appendChild(halfStar.cloneNode(true));
362                } else {
363                  starContainer.appendChild(noneStar.cloneNode(true));
364                }
365              }
366              tempDiv.innerHTML = tempDiv.innerHTML.replace('{{star_icons}}', starContainer.innerHTML);
367            }
368
369            // バイセル直営店
370            let fcEl = null;
371            const elements = tempDiv.querySelectorAll('*');
372            elements.forEach(el => {
373              if (el.textContent.includes('{{store_post_fc_text}}')) {
374                const hasChildWithText = Array.from(el.children).some(child => child.textContent.includes('{{store_post_fc_text}}'));
375                if (!hasChildWithText) {
376                  fcEl = el;
377                }
378              }
379            });
380
381            if (fcEl) {
382              if (!store.store_post_fc) {
383                fcEl.innerHTML = fcEl.innerHTML.replace('{{store_post_fc_text}}', 'バイセル直営店');
384              } else {
385                fcEl.remove();
386                const webBtn = tempDiv.querySelector('.p-store-purchase__result_item_link_btn--web');
387                if (webBtn) {
388                  webBtn.remove();
389                }
390
391                const linkWrap = tempDiv.querySelector(
392                  '.p-store-purchase__result_item_link'
393                );
394
395                if (linkWrap) {
396                  linkWrap.classList.add('is-single');
397                }
398              }
399            }
400
401            // 本日予約空き時間あり削除
402            let reserveEl = null;
403            elements.forEach(el => {
404              if (el.textContent.includes('本日予約空き時間あり')) {
405                const hasChildWithText = Array.from(el.children).some(child => child.textContent.includes('本日予約空き時間あり'));
406                if (!hasChildWithText) {
407                  reserveEl = el;
408                }
409              }
410            });
411            if (reserveEl) {
412              reserveEl.remove();
413            }
414
415            // 値が無ければタグごと削除
416            removeIfEmptyKeys.forEach(key => {
417              const marker = `{{REMOVE_${key.toUpperCase()}}}`;
418              const original = `{{${key}}}`;
419              let targetEl = null;
420              const freeTextEl = tempDiv.querySelector('.p-store-purchase__result_item_link_btn_sm');
421              if (freeTextEl && !store.store_post_tel_free_text) {
422                freeTextEl.remove();
423              }
424              const currentElements = tempDiv.querySelectorAll('*');
425              currentElements.forEach(el => {
426                if (el.textContent.includes(marker) || el.textContent.includes(original)) {
427                  const hasChildWithText = Array.from(el.children).some(child => 
428                    child.textContent.includes(marker) || child.textContent.includes(original)
429                  );
430                  if (!hasChildWithText) {
431                    targetEl = el;
432                  }
433                }
434              });
435              if (targetEl) {
436                targetEl.remove();
437              }
438            });
439
440            return tempDiv.innerHTML;
441          };
442
443          const mapStoreList = []; // マップのマーカー用リスト
444          const isUnlimitedMapList = (searchType === 'area' && pref !== '');
445          // 現在地検索で地図の表示範囲(bounds)を指定した再検索かどうか
446          const isBoundsSearch = (searchType === 'nearby' && !!bounds);
447          // エリア(都道府県)検索、または表示範囲での再検索時は、APIが返す絞り込み条件に合致する全件(map_list)を地図に使う
448          const hasFullMapList = (isUnlimitedMapList || isBoundsSearch) && Array.isArray(data.data.map_list);
449
450          const setStoreExtraFields = (store) => {
451            store.store_post_fc_label = store.store_post_fc
452            ? '電話番号'
453            : '通話無料';
454
455            store.store_post_tel_free_text = store.store_post_fc ? '' : '(無料)';
456          };
457
458          data.data.list.forEach(store => {
459            setStoreExtraFields(store);
460            combinedHTML += processTemplate(templateHTML, store);
461
462            // map_list がある場合、地図用のHTMLはこのループでは作らず後続の全件ループで作る
463            if (!hasFullMapList && mapTemplateHTML && (isUnlimitedMapList || currentMapListCount < MAP_LIST_LIMIT)) {
464              combinedMapHTML += processTemplate(mapTemplateHTML, store);
465              mapStoreList.push(store);
466              currentMapListCount++;
467            }
468          });
469
470          // エリア内の全店舗ピンを表示(もっと見るでの再読み込み時は再構築しない)
471          if (hasFullMapList && !isAppend && mapTemplateHTML) {
472            data.data.map_list.forEach(store => {
473              setStoreExtraFields(store);
474              combinedMapHTML += processTemplate(mapTemplateHTML, store);
475              mapStoreList.push(store);
476            });
477          }
478
479          // 生成したHTMLを js_store_result の中に挿入
480          if (isAppend) {
481            target.insertAdjacentHTML('beforeend', combinedHTML);
482
483            // #js_store_map 版で反映(エリア検索で全件表示済みの場合は追加しない)
484            if (mapTarget && window.jQuery && !hasFullMapList) {
485              if ($(mapTarget).hasClass('slick-initialized')) {
486                const tempContainer = document.createElement('div');
487                tempContainer.innerHTML = combinedMapHTML;
488                Array.from(tempContainer.children).forEach(child => {
489                  $(mapTarget).slick('slickAdd', child.outerHTML);
490                });
491              } else {
492                mapTarget.insertAdjacentHTML('beforeend', combinedMapHTML);
493              }
494            }
495          } else {
496            if (!mapOnly) {
497              target.innerHTML = combinedHTML;
498            }
499
500            // #js_store_map 版で反映
501            if (mapTarget) {
502              mapTarget.innerHTML = combinedMapHTML;
503              if (window.jQuery) {
504                $(mapTarget).slick({
505                  arrows: false,
506                  dots: false,
507                  infinite: false,
508                  variableWidth: true,
509                });
510                // スライド変更時にマーカーをフォーカス
511                $(mapTarget).off('afterChange.mapSync').on('afterChange.mapSync', function(event, slick, currentSlide) {
512                  if (typeof focusMapMarker === 'function') {
513                    focusMapMarker(currentSlide);
514                  }
515                });
516              }
517            }
518          }
519
520          // マーカーを地図に反映(エリア検索で全件表示済みの場合、もっと見る時は更新しない)
521          // mapOnly(地図の表示範囲での再検索)の場合は、ユーザーが動かした地図の位置・ズームを変更しない
522          if (typeof updateMap === 'function' && !(isAppend && hasFullMapList)) {
523            updateMap(mapStoreList, isAppend && !hasFullMapList, { skipFitBounds: mapOnly });
524          }
525        }
526
527        // 「もっと見る」ボタンの表示制御
528        if (!mapOnly && moreBtn) {
529          const currentLoaded = page * 10;
530          if (currentLoaded >= data.data.total_count) {
531            moreBtn.style.display = 'none'; // 全件読み込み完了したら非表示
532          } else {
533            moreBtn.style.display = ''; // まだ続きがあれば表示
534          }
535        }
536
537      } else {
538        console.error('API error response:', data.data);
539      }
540    })
541    .catch(error => console.error('API fetch error:', error));
542  }
543
544  // js-store-more-btnで追加読み込み
545  if (moreBtn) {
546    moreBtn.addEventListener('click', function(e) {
547      e.preventDefault();
548      currentPage++;
549      const pref = prefSelect ? prefSelect.value : '';
550      const city = citySelect ? citySelect.value : '';
551      fetchStoreList({
552        searchType: currentSearchType,
553        pref: pref,
554        city: city,
555        page: currentPage,
556        isAppend: true,
557        lat: currentLat,
558        lng: currentLng
559      });
560    });
561  }
562
563  // 「検索条件クリア」ボタンがクリックされたとき
564  const resetBtn = document.querySelector('.js-search-reset');
565  if (resetBtn) {
566    resetBtn.addEventListener('click', function(e) {
567      e.preventDefault();
568      
569      // セレクトボックスを初期状態に戻す
570      if (prefSelect) prefSelect.value = '';
571      if (citySelect) {
572        citySelect.innerHTML = '<option value="">市区町村を選択</option>';
573        citySelect.disabled = true;
574      }
575
576      // パラメータをリセットして全件取得
577      currentPage = 1;
578      currentLat = null;
579      currentLng = null;
580      currentSearchType = 'area';
581
582      fetchStoreList({
583        searchType: currentSearchType,
584        pref: '',
585        city: '',
586        page: currentPage,
587        isAppend: false
588      });
589    });
590  }
591
592
593  const initialPref = prefSelect ? prefSelect.value : '';
594  const initialCity = citySelect ? citySelect.value : '';
595  currentSearchType = 'area';
596  fetchStoreList({
597    searchType: currentSearchType,
598    pref: initialPref,
599    city: initialCity,
600    page: currentPage,
601    isAppend: false
602  });
603
604
605
606  // #gmap に google map api で地図表示
607  let map;
608  let markers = [];
609  let infoWindows = []; // 複数の吹き出しを管理するための配列
610  let currentLocationMarker = null; // 現在地マーカーを管理する変数
611  let suppressMapMoveRefetch = false; // fitBounds等、自分のコードによる地図移動ではドラッグ/ズーム再検索を発生させないためのフラグ
612  let userMovedMap = false; // ドラッグ/ズーム、または現在地への再センタリングで地図が動いたことを示すフラグ(idle時にこれを見て再取得するか判断する)
613  let pendingMapUpdate = null; // Google Maps本体の読み込みが間に合わなかった場合に、地図が使えるようになってから反映するための保留データ
614
615  function initMap() {
616    const mapElement = document.getElementById('gmap');
617    if (!mapElement) return;
618    mapElement.style.height = '100%';
619
620    // 初期位置
621    const initialLocation = { lat: 35.681236, lng: 139.767125 };
622
623    map = new google.maps.Map(mapElement, {
624      center: initialLocation,
625      zoom: 8,
626    });
627
628    window.shopMap = map;
629    window.shopMapCenter = initialLocation;
630
631    // InfoWindowの閉じるボタン(×ボタン)を非表示にするCSSを動的に追加
632    const style = document.createElement('style');
633    style.innerHTML = '.gm-style .gm-ui-hover-effect { display: none !important; } .gm-style-iw { padding: 0 !important; } .gm-style .gm-style-iw-d { overflow: hidden !important; } .gm-style-iw-chr { display:none !important; }';
634    document.head.appendChild(style);
635
636    // 現在地検索中に地図をドラッグ/ズームしたら、表示中の範囲内の店舗を再取得する
637    map.addListener('dragend', function() {
638      userMovedMap = true;
639    });
640
641    map.addListener('zoom_changed', function() {
642      // fitBounds()やsetZoom()など、自分のコードによるズーム変更は対象外にする
643      if (suppressMapMoveRefetch) return;
644      userMovedMap = true;
645    });
646
647    map.addListener('idle', function() {
648      if (!userMovedMap) return;
649      userMovedMap = false;
650
651      if (currentSearchType !== 'nearby') return;
652
653      const mapBounds = map.getBounds();
654      if (!mapBounds) return;
655      const sw = mapBounds.getSouthWest();
656      const ne = mapBounds.getNorthEast();
657
658      fetchStoreList({
659        searchType: 'nearby',
660        lat: currentLat,
661        lng: currentLng,
662        isAppend: false,
663        mapOnly: true,
664        bounds: {
665          swLat: sw.lat(),
666          swLng: sw.lng(),
667          neLat: ne.lat(),
668          neLng: ne.lng()
669        }
670      });
671    });
672
673    // Google Maps本体の読み込みが間に合わず保留されていた更新があれば、ここで反映する
674    if (pendingMapUpdate) {
675      const pending = pendingMapUpdate;
676      pendingMapUpdate = null;
677      updateMap(pending.storeList, pending.isAppend, pending.options);
678    }
679  }
680
681  // 取得した店舗データをもとにマップのマーカーを更新
682  function updateMap(storeList, isAppend, options = {}) {
683    const skipFitBounds = options.skipFitBounds || false;
684
685    // Google Maps本体の読み込みがまだ完了していない場合は、読み込み完了後に反映できるよう保留する
686    if (!map) {
687      pendingMapUpdate = { storeList, isAppend, options };
688      return;
689    }
690
691    if (!isAppend) {
692      // 新規検索時は既存のマーカーをクリア
693      markers.forEach(marker => marker.setMap(null));
694      infoWindows.forEach(iw => iw.close());
695      markers = [];
696      infoWindows = [];
697      // 現在地のマーカーもクリア
698      if (currentLocationMarker) {
699        currentLocationMarker.setMap(null);
700        currentLocationMarker = null;
701      }
702    }
703
704    storeList.forEach(store => {
705      const markerIndex = markers.length; // 現在のマーカーのインデックス
706      const lat = parseFloat(store.store_lat); 
707      const lng = parseFloat(store.store_lng); 
708
709      // 緯度・経度が有効な数値であり、かつ 0,0 ではない場合のみマーカーを追加
710      if (Number.isFinite(lat) && Number.isFinite(lng) && lat !== 0 && lng !== 0) {
711        const position = { lat: lat, lng: lng };
712
713        // FCかどうかでマーカー画像を切り替え(パスは実際の画像に合わせて変更してください)
714        const isFC = store.store_post_fc;
715        const iconPath = isFC ? '/wp-content/themes/bsportal/assets/img/new/store-purchase/pin_fc.svg' : '/wp-content/themes/bsportal/assets/img/new/store-purchase/pin_direct.svg';
716
717        const markerIcon = {
718          url: iconPath,
719          scaledSize: new google.maps.Size(35, 41)
720        };
721
722        const marker = new google.maps.Marker({
723          position: position,
724          map: map,
725          title: store.store_post_name,
726          icon: markerIcon,
727          zIndex: 1
728        });
729
730        const storeName = store.store_post_name;
731        const storeAddress = `${store.store_post_address1 || ''}${store.store_post_address2 || ''}${store.store_post_address3 || ''}`;
732        const storeLink = store.store_post_slag ? `/store/${store.store_post_slag}/` : '#';
733        
734        const infoWindow = new google.maps.InfoWindow({
735          content: `
736          <div style="padding: 10px; font-size: 14px;">
737            <h3 style="margin:0;font-weight:700;font-size:1.4rem;">${storeName}</h3>
738          </div>
739          `
740        });
741        
742        // マーカークリック時に該当のスライドへ移動し、吹き出しを開く
743        marker.addListener('click', () => {
744          const mapTarget = document.getElementById('js_store_map');
745          if (mapTarget && window.jQuery && $(mapTarget).hasClass('slick-initialized')) {
746            $(mapTarget).slick('slickGoTo', markerIndex);
747          }
748          
749          infoWindows.forEach(iw => iw.close());
750          infoWindow.open(map, marker);
751        });
752
753        markers.push(marker);
754        infoWindows.push(infoWindow);
755      }
756    });
757
758    // 現在地が取得できている場合は、デフォルトのマーカーを追加
759    if (currentSearchType === 'nearby' && currentLat !== null && currentLng !== null) {
760      if (!currentLocationMarker) {
761        currentLocationMarker = new google.maps.Marker({
762          position: { lat: currentLat, lng: currentLng },
763          map: map,
764          title: '現在地',
765        });
766      }
767    }
768
769    // マーカーが1つ以上ある場合のみ地図の表示範囲を調整
770    if (markers.length > 0) {
771      const bounds = new google.maps.LatLngBounds();
772      // 現在表示されているすべてのマーカーを範囲に含める
773      markers.forEach(marker => {
774        bounds.extend(marker.getPosition());
775      });
776
777      // 現在地検索の場合は、現在地も範囲に含める
778      if (currentSearchType === 'nearby' && currentLat !== null && currentLng !== null) {
779        bounds.extend(new google.maps.LatLng(currentLat, currentLng));
780      }
781
782      // タブ切り替え時に使用できるようにboundsをグローバルに保持
783      window.shopMapBounds = bounds;
784
785      // 地図の表示範囲での再検索(ドラッグ/ズーム)の場合は、ユーザーが動かした地図の位置・ズームを変更しない
786      if (!skipFitBounds) {
787        const mapElement = document.getElementById('gmap');
788        if (mapElement && mapElement.offsetWidth > 0 && mapElement.offsetHeight > 0) {
789          suppressMapMoveRefetch = true;
790          map.fitBounds(bounds, { top: 50, right: 50, bottom: 150, left: 50 });
791        }
792
793        if (!isAppend) {
794          // 1店舗だけヒットした場合などにズームしすぎないように制限
795          const listener = google.maps.event.addListener(map, 'idle', function () {
796            if (map.getZoom() > 15) map.setZoom(15);
797            window.shopMapCenter = map.getCenter();
798            google.maps.event.removeListener(listener);
799            suppressMapMoveRefetch = false;
800          });
801        } else {
802          suppressMapMoveRefetch = false;
803        }
804      }
805    }
806  }
807
808  // 特定のマーカーにフォーカスする関数
809  function focusMapMarker(index) {
810    if (map && markers[index]) {
811      // マーカーを画面中央に移動
812      map.panTo(markers[index].getPosition());
813      
814      // 他の吹き出しを全て閉じ、対象の吹き出しを開く
815      infoWindows.forEach(iw => iw.close());
816      infoWindows[index].open(map, markers[index]);
817
818      // InfoWindowとマーカーのz-indexを操作して前面に表示
819      infoWindows.forEach((iw, i) => {
820        iw.setZIndex(i === index ? 999 : 1);
821      });
822      markers.forEach((m, i) => {
823        m.setZIndex(i === index ? 999 : 1);
824      });
825    }
826  }
827
828  // 地図のリサイズと表示領域の調整を行うグローバル関数
829  function resizeAndFitMap() {
830    if (window.shopMap) {
831      // このリサイズ/再フィットによる地図移動を、ユーザーのドラッグ/ズームと誤検知しないよう抑制する
832      suppressMapMoveRefetch = true;
833      google.maps.event.trigger(window.shopMap, 'resize');
834      // boundsが設定されていればfitBoundsを、そうでなければ中心位置を再設定
835      if (window.shopMapBounds) {
836        window.shopMap.fitBounds(window.shopMapBounds, { top: 50, right: 50, bottom: 150, left: 50 });
837      } else if (window.shopMapCenter) {
838        window.shopMap.setCenter(window.shopMapCenter);
839      }
840      google.maps.event.addListenerOnce(window.shopMap, 'idle', function() {
841        suppressMapMoveRefetch = false;
842      });
843    }
844  }
845  window.resizeAndFitMap = resizeAndFitMap;
846
847  // Google Maps側のコールバックから呼べるように、実際の初期化処理を登録する
848  // (window.initMap 自体はファイル冒頭で先に定義済み。initMapCalled で二重実行を防ぐ)
849  let initMapCalled = false;
850  function runInitMapOnce() {
851    if (initMapCalled) return;
852    initMapCalled = true;
853    initMap();
854  }
855  window.__runInitMap = runInitMapOnce;
856
857  // すでにコールバックが先に発火していた場合、またはAPIが既に読み込み済みの場合は直接実行
858  if (window.__googleMapsReady || (typeof google !== 'undefined' && google.maps)) {
859    runInitMapOnce();
860  }
861
862  // 「近くのお店を探す」機能(現在地取得)
863  const searchNearbyButtons = document.querySelectorAll('.js-search-nearby');
864  searchNearbyButtons.forEach(function(searchNearbyButton) {
865    searchNearbyButton.addEventListener('click', function(e) {
866      e.preventDefault();
867
868      if (navigator.geolocation) {
869        navigator.geolocation.getCurrentPosition(
870          function(position) {
871            const lat = position.coords.latitude;
872            const lng = position.coords.longitude;
873            
874            if (prefSelect) prefSelect.value = '';
875            if (citySelect) {
876              citySelect.innerHTML = '<option value="">市区町村を選択</option>';
877              citySelect.disabled = true;
878            }
879
880            currentPage = 1;
881            currentLat = lat;
882            currentLng = lng;
883            currentSearchType = 'nearby';
884            // 以前の地図操作で立ったままになっている可能性のあるフラグをリセットしておく
885            userMovedMap = false;
886            fetchStoreList({
887              searchType: currentSearchType,
888              pref: '',
889              city: '',
890              page: currentPage,
891              isAppend: false,
892              lat: currentLat,
893              lng: currentLng
894            });
895
896            // 地図タブに切り替える
897            if (window.jQuery) {
898              const $mapTabBtn = window.jQuery('.p-store-purchase__result_tab_btn').eq(1);
899              if ($mapTabBtn.length && !$mapTabBtn.hasClass('is-active')) {
900                $mapTabBtn.trigger('click');
901              }
902            }
903
904            // #store-result にスクロールする
905            const storeResultEl = document.getElementById('store-result');
906            if (storeResultEl) {
907              const header = document.querySelector('.l-header');
908              const position = storeResultEl.getBoundingClientRect().top + window.pageYOffset;
909
910              // スクロール中、scroll.js側のヘッダー固定表示ロジックと競合して重なりが発生することがあるため、
911              // アニメーション中は一時的にヘッダーを非表示にしておく
912              if (header) {
913                header.style.visibility = 'hidden';
914              }
915
916              window.scrollTo({
917                top: position,
918                behavior: 'smooth'
919              });
920
921              // 着地後、最終的なスクロール位置に応じたヘッダーの状態を直接指定してから表示に戻す
922              setTimeout(function() {
923                if (!header) return;
924                const HEADER_STICKY_TRIGGER = 600; // scroll.js の trigger と同じ値
925                if (window.pageYOffset <= HEADER_STICKY_TRIGGER) {
926                  header.classList.remove('is-sticky', 'is-hide');
927                } else {
928                  header.classList.remove('is-sticky');
929                  header.classList.add('is-hide');
930                }
931                header.style.visibility = '';
932              }, 600);
933            }
934          },
935          function(error) {
936            let message = '位置情報の取得に失敗しました。';
937            switch (error.code) {
938              case error.PERMISSION_DENIED:
939                message = '位置情報の利用が許可されていません。ブラウザの設定をご確認ください。';
940                break;
941              case error.POSITION_UNAVAILABLE:
942                message = '現在位置を取得できませんでした。';
943                break;
944              case error.TIMEOUT:
945                message = '位置情報の取得がタイムアウトしました。';
946                break;
947            }
948            alert(message);
949          }
950        );
951      } else {
952        alert('お使いのブラウザでは位置情報取得機能をご利用いただけません。');
953      }
954    });
955  });
956
957  const currentButtons = document.querySelectorAll('.js-current');
958  if (currentButtons.length > 0) {
959    currentButtons.forEach(button => {
960      button.addEventListener('click', function(e) {
961        e.preventDefault();
962
963        if (currentSearchType === 'nearby') {
964          if (navigator.geolocation) {
965            navigator.geolocation.getCurrentPosition(
966              function(position) {
967                const lat = position.coords.latitude;
968                const lng = position.coords.longitude;
969
970                // 現在地を最新の位置に更新(現在地マーカー・距離表示にも反映される)
971                currentLat = lat;
972                currentLng = lng;
973
974                if (window.shopMap) {
975                  // 地図が落ち着いた(idle)タイミングで、この位置を中心とした範囲の店舗を再取得させる
976                  userMovedMap = true;
977                  window.shopMap.panTo({ lat: lat, lng: lng });
978                }
979              },
980              function(error) {
981                let message = '位置情報の取得に失敗しました。';
982                switch (error.code) {
983                  case error.PERMISSION_DENIED:
984                    message = '位置情報の利用が許可されていません。ブラウザの設定をご確認ください。';
985                    break;
986                  case error.POSITION_UNAVAILABLE:
987                    message = '現在位置を取得できませんでした。';
988                    break;
989                  case error.TIMEOUT:
990                    message = '位置情報の取得がタイムアウトしました。';
991                    break;
992                }
993                alert(message);
994              }
995            );
996          } else {
997            alert('お使いのブラウザでは位置情報取得機能をご利用いただけません。');
998          }
999        } else {
1000          const searchNearbyBtn = document.querySelector('.js-search-nearby');
1001          if (searchNearbyBtn) {
1002            searchNearbyBtn.click();
1003          }
1004        }
1005      });
1006    });
1007  }
1008
1009  // 「エリアから探す」ボタン(#search)クリック時:スクロール+エリア検索のアコーディオンを開く
1010  const openAreaSearchButtons = document.querySelectorAll('.js-open-area-search');
1011  openAreaSearchButtons.forEach(function(button) {
1012    button.addEventListener('click', function(e) {
1013      const target = document.getElementById('search');
1014      if (!target) return;
1015
1016      e.preventDefault();
1017
1018      const destination = target.getBoundingClientRect().top + window.pageYOffset;
1019
1020      if (window.jQuery) {
1021        window.jQuery('html').css('scroll-behavior', 'auto');
1022        window.jQuery('html, body').stop().animate({ scrollTop: destination }, 500, function() {
1023          window.jQuery('html').css('scroll-behavior', '');
1024        });
1025      } else {
1026        window.scrollTo({ top: destination, behavior: 'smooth' });
1027      }
1028
1029      // 実際のエリア検索アコーディオンを開く(すでに開いていれば何もしない)
1030      const accordionBtn = document.querySelector('.p-store-purchase__search_form_accordion');
1031      if (accordionBtn && !accordionBtn.classList.contains('is-open')) {
1032        accordionBtn.click();
1033      }
1034    });
1035  });
1036
1037});

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.