PageSourceSearch

https://r7h-gs-hojo-web-fspoc.jp/assets/js/functions.js

js r7h-gs-hojo-web-fspoc.jp collected 2026-09-29 20:57:39 UTC 21,198 bytes, 652 lines download raw bytes

1/* =============================
2   共通ユーティリティ
3============================= */
4// ハッシュから安全にID要素を取得(英数, _, -, : のみ許可)
5function getBySafeIdFromHash(hash) {
6  if (!hash || hash === '#' || hash.indexOf('#') !== 0) return null;
7  const id = decodeURIComponent(hash.slice(1));
8  if (!/^[A-Za-z0-9_\-:]+$/.test(id)) return null;
9  return document.getElementById(id);
10}
11// jQuery未定義対策(万一のガード)
12var $jq = (typeof window.jQuery === 'function') ? window.jQuery : null;
13
14/* =============================
15   スムーススクロール(セレクタ注入防止)
16============================= */
17$(function(){
18  $('a[href^="#"]').on('click', function(e){
19    e.preventDefault();
20    const targetEl = getBySafeIdFromHash($(this).attr('href'));
21    const adjust = (($('header').outerHeight() || 0) + 30);
22    const speed = 400;
23    const pos = targetEl ? $(targetEl).offset().top - adjust : 0;
24    $('html,body').animate({scrollTop: pos}, speed, 'swing');
25  });
26});
27
28/* =============================
29   アンカーリンクでヘッダー分ずらす(onload)
30============================= */
31$(window).on('load', function() {
32  const targetEl = getBySafeIdFromHash(location.hash);
33  if (!targetEl) return;
34  const headerHeight = (($('header').outerHeight() || 0) + 30);
35  const position = Math.floor($(targetEl).offset().top) - headerHeight;
36  $('html, body').animate({scrollTop: position}, 400);
37});
38
39/* =============================
40   タブ(#link指定時の動作)
41============================= */
42$(function(){
43  if (window.location.hash === '#link') {
44    $('#tab01').prop('checked', true);
45    const qa = document.getElementById('qa');
46    if (qa) {
47      $('html,body').animate({ scrollTop: $(qa).offset().top }, 'fast');
48    }
49  }
50});
51
52/* =============================
53   FAQ検索(動的検索 + 遷移両対応)統合版
54============================= */
55$(document).ready(function() {
56  /* -------- ユーティリティ -------- */
57  const INVALID_CHARS_RE = /[!"#$%&'()*+,\-./:;<=>?@[\\\]\^_`{|}~]/g;
58
59  function isOnFaqPage() {
60    // 例: /faq.html, /faq, /help/faq などを想定
61    const p = window.location.pathname.toLowerCase();
62    return p.includes('/faq') || /faq(?:\.html)?$/i.test(p);
63  }
64
65  function cleanKeyword(raw) {
66    return (raw || '').trim().replace(INVALID_CHARS_RE, '');
67  }
68
69  function showMessage(msg) {
70    $('#message').text(msg || '');
71  }
72
73  function makeQAListItem(question, answer) {
74    const li = document.createElement('li');
75    const dl = document.createElement('dl');
76    const dt = document.createElement('dt');
77    const dd = document.createElement('dd');
78    dt.textContent = question || '';
79    dd.textContent = answer || '';
80    dl.appendChild(dt); dl.appendChild(dd);
81    li.appendChild(dl);
82    return li;
83  }
84
85  function extractQA($el) {
86    // 1) dl構造を優先
87    const $dl = $el.is('dl') ? $el : $el.find('dl').first();
88    if ($dl.length) {
89      const q = ($dl.find('dt').first().text() || '').trim();
90      const a = ($dl.find('dd').first().text() || '').trim();
91      if (q || a) return { q, a };
92    }
93    // 2) よくあるクラス名
94    const qSel = ['.q', '.question', '.faq-question', '.title', '.faq-q', '.accordion-data_title'];
95    const aSel = ['.a', '.answer', '.faq-answer', '.content', '.desc', '.text', '.js-accordion_inner', '.accordion-data_content'];
96    let q = '', a = '';
97    for (const s of qSel) { const t = $el.find(s).first().text().trim(); if (t) { q = t; break; } }
98    for (const s of aSel) { const t = $el.find(s).first().text().trim(); if (t) { a = t; break; } }
99    // 3) フォールバック
100    if (!q && !a) {
101      const all = ($el.text() || '').trim();
102      if (all) {
103        const mid = Math.min(60, Math.floor(all.length / 2));
104        q = all.slice(0, mid).trim();
105        a = all.slice(mid).trim();
106      }
107    }
108    return { q, a };
109  }
110
111  /* -------- 入力値の制限 -------- */
112  $('#search-text').on('input', function () {
113    const before = $(this).val() || '';
114    const invalidChar = before.split('').find(c => INVALID_CHARS_RE.test(c));
115    if (invalidChar) {
116      showMessage(`"${invalidChar}" の文字は使えません、入力が制限されています。`);
117      const cleaned = before.replace(INVALID_CHARS_RE, '');
118      $(this).val(cleaned);
119    } else {
120      showMessage('');
121    }
122  });
123
124  /* -------- よくあるキーワードボタン -------- */
125  $('#keyword button').on('click', function() {
126    $('#search-text').val($(this).val()).trigger('input');
127  });
128
129  /* -------- 検索実行(FAQページ内での描画) -------- */
130  function performSearch() {
131    $('#output').text('検索結果');
132    const $list = $('#search-result__list ul');
133    const $scroll = $('#result-scroll');
134    const keyword = cleanKeyword($('#search-text').val());
135    $list.empty();
136    $('.no-result').remove();
137
138    let hit = 0;
139    if (keyword !== '') {
140      $('.search-target').each(function() {
141        const $t = $(this);
142        const haystack = ($t.text() || '');
143        if (haystack.indexOf(keyword) !== -1) {
144          const { q, a } = extractQA($t);
145          const li = makeQAListItem(q || haystack.slice(0, 60), a || '');
146          $list[0].appendChild(li);
147          hit++;
148        }
149      });
150      $('.faq #target-area').hide();
151    }
152
153    if (hit === 0) {
154      const li = document.createElement('li');
155      li.className = 'no-result';
156      li.textContent = '該当のキーワードを含んだFAQはありません。';
157      $list[0].appendChild(li);
158    }
159
160    if ($scroll.length) $scroll.scrollTop(0);
161
162    if (typeof window.gtag === 'function') {
163      window.gtag('event', 'search', { 'search_term': keyword });
164    }
165  }
166
167  /* -------- クリック&Enterの挙動(ページ判定付き) -------- */
168  // クリック: FAQならその場で検索、その他はfaq.htmlに遷移
169  $(document).on('click', '#searchBtn', function(e){
170    e.preventDefault();
171    const keyword = cleanKeyword($('#search-text').val());
172    if (!keyword) {
173      showMessage('検索キーワードを入力してください。');
174      return;
175    }
176
177    if (isOnFaqPage()) {
178      performSearch();
179    } else {
180      // faq.html への相対/絶対パスは環境に合わせて調整
181      const faqUrl = new URL('faq.html', window.location.href);
182      faqUrl.searchParams.set('search', keyword);
183      window.location.href = faqUrl.toString();
184    }
185  });
186
187  // Enter: FAQならその場で検索、その他は遷移
188  $('#search-text').on('keypress', function(e){
189    if (e.which === 13) {
190      e.preventDefault();
191      const keyword = cleanKeyword($('#search-text').val());
192      if (!keyword) {
193        showMessage('検索キーワードを入力してください。');
194        return;
195      }
196      if (isOnFaqPage()) {
197        performSearch();
198      } else {
199        const faqUrl = new URL('faq.html', window.location.href);
200        faqUrl.searchParams.set('search', keyword);
201        window.location.href = faqUrl.toString();
202      }
203    }
204  });
205
206  /* -------- ?search= の初期実行(FAQページのみ) -------- */
207  (function bootFromQuery(){
208    const params = new URLSearchParams(window.location.search);
209    const q = params.get('search') || '';
210    if (!q) return;
211    const cleaned = cleanKeyword(q);
212    if (cleaned !== q) {
213      showMessage('一部の記号は入力できないため、検索語から除外しました。');
214    }
215    $('#search-text').val(cleaned);
216    if (isOnFaqPage()) {
217      performSearch();
218    }
219  })();
220});
221
222
223
224/* =============================
225   メール(GA4イベント)
226============================= */
227$(document).ready(function() {
228  $('#link-mail').on('click', function() {
229    if (typeof window.gtag === 'function') {
230      window.gtag('event', 'email_click', { 'mail_type': 'データ送受信' });
231    }
232  });
233});
234
235/* =============================
236   ページ先頭ボタン
237============================= */
238$(function(){
239  const $btn = $('.pagetop');
240  $btn.hide();
241
242  $('.pagetop').on('click', function (e) {
243    e.preventDefault();
244    $('html, body').animate({ scrollTop: 0 }, 500);
245  });
246
247  let ticking = false;
248  $(window).on('scroll resize', function () {
249    if (ticking) return;
250    ticking = true;
251    requestAnimationFrame(function(){
252      if ($(window).scrollTop() > $('header').innerHeight()) {
253        $btn.fadeIn(500);
254      } else {
255        $btn.fadeOut(250);
256      }
257      ticking = false;
258    });
259  });
260});
261
262/* =============================
263   スマホメニュー開閉(toggle + body固定)
264============================= */
265$(function () {
266  var state = false;
267  var pos = 0;
268
269  $('.sp_nav').on('click', function () {
270    $('nav').toggleClass('open');
271
272    if (state === false) {
273      pos = $(window).scrollTop();
274      $('body').addClass('fixed').css({ top: -pos });
275      state = true;
276    } else {
277      $('body').removeClass('fixed').css({ top: 0 });
278      window.scrollTo(0, pos);
279      state = false;
280    }
281  });
282});
283
284/* =============================
285   レポート一覧 プルダウンフィルター
286============================= */
287$(document).ready(function () {
288  function updateFilteredList() {
289    var selectedYear = $('#yearFilter').val();
290    var selectedGenre = $('#genreFilter').val();
291
292    $('.filtered-item').hide().fadeIn();
293
294    if (selectedYear !== 'all') {
295      $('.filtered-item')
296        .filter(function () { return $(this).find('.year').text() !== selectedYear; })
297        .hide();
298    }
299
300    if (selectedGenre !== 'all') {
301      $('.filtered-item')
302        .filter(function () { return $(this).find('.genre').text() !== selectedGenre; })
303        .hide();
304    }
305
306    if (selectedGenre === 'all' && selectedYear === 'all') {
307      $('.order').fadeIn();
308    } else {
309      $('.order').hide();
310    }
311
312    $('.filtered-item:visible').fadeIn();
313  }
314
315  updateFilteredList();
316  $('#yearFilter, #genreFilter').on('change', updateFilteredList);
317});
318
319/* =============================
320   他言語切り替えボタン(スクロールで表示)
321============================= */
322$(function () {
323  var $btn = $('#scroll-lang');
324  $btn.hide();
325  $(window).on('scroll', function () {
326    if ($(this).scrollTop() > 150) { $btn.fadeIn(); } else { $btn.fadeOut(); }
327  });
328});
329
330/* =============================
331   アコーディオン
332============================= */
333$(function(){
334  // 動的生成にも対応
335  $(document).on('click', '.js-accordion_title', function() {
336    $(this).next('.js-accordion_inner').slideToggle();
337    $(this).toggleClass('open');
338  });
339});
340
341// ハッシュで対象アコーディオンを初期展開
342$(function () {
343  const urlHash = location.hash;
344  if (!urlHash) return;
345  const $target = $(urlHash);
346  if (!$target.length) return;
347
348  $target.find('.accordion-data_title').addClass('open')
349         .next('.js-accordion_inner').slideToggle();
350});
351
352/* =============================
353   SPメニュー(MicroModal利用部分の安全化)
354============================= */
355window.addEventListener('DOMContentLoaded', () => {
356  const anchorLinks = document.querySelectorAll('a[href^="#"]');
357  const anchorLinksArr = Array.from(anchorLinks);
358  const btn = document.querySelector('#js-buttonHamburger');
359  const overlay = document.querySelector('#js-overlay');
360
361  // クリック時にモーダルを閉じつつスクロール
362  anchorLinksArr.forEach((link) => {
363    link.addEventListener('click', (e) => {
364      e.preventDefault();
365      const targetEl = getBySafeIdFromHash(link.hash);
366      if (!targetEl) return;
367
368      const targetoffsetTop = window.pageYOffset + targetEl.getBoundingClientRect().top;
369
370      if (btn && overlay && btn.ariaExpanded === 'true') {
371        btn.ariaExpanded = 'false';
372        overlay.classList.remove('active');
373      }
374      anchorLinksArr.forEach((l) => { l.ariaExpanded = 'false'; });
375
376      if (window.MicroModal) {
377        window.MicroModal.close('modal-1');
378      }
379
380      window.scrollTo({ top: targetoffsetTop, behavior: 'smooth' });
381    });
382  });
383
384  if (btn && overlay) {
385    btn.addEventListener('click', function () {
386      if (btn.ariaExpanded === 'false') {
387        btn.ariaExpanded = 'true';
388        if (window.MicroModal) {
389          window.MicroModal.show('modal-1', { disableScroll: true, awaitOpenAnimation: true });
390        }
391        overlay.classList.add('active');
392      } else {
393        btn.ariaExpanded = 'false';
394        if (window.MicroModal) {
395          window.MicroModal.close('modal-1', { awaitCloseAnimation: true });
396        }
397        overlay.classList.remove('active');
398      }
399    });
400
401    overlay.addEventListener('click', function () {
402      btn.ariaExpanded = 'false';
403      if (window.MicroModal) {
404        window.MicroModal.close('modal-1', { awaitCloseAnimation: true });
405      }
406      overlay.classList.remove('active');
407    });
408
409    document.addEventListener('keydown', function (e) {
410      if (e.key === 'Escape') {
411        btn.ariaExpanded = 'false';
412        if (window.MicroModal) {
413          window.MicroModal.close('modal-1', { awaitCloseAnimation: true });
414        }
415        overlay.classList.remove('active');
416      }
417    });
418  }
419});
420
421/* =============================
422   採択情報一覧 プルダウンフィルター(Promise対応 + ガード)
423============================= */
424$(document).ready(function () {
425  // 初期化時に実行
426  saitakuFilteredList().then(checkHiddenItems);
427
428  // フィルター変更
429  $('#countryFilter, #jigyouFilter, #taisyoFilter, #fieldFilter, #kouboFilter')
430    .on('change', function () {
431      saitakuFilteredList().then(checkHiddenItems);
432    });
433
434  // フィルターが存在しないページでは 'all' 扱いにするヘルパー
435  function getFilterVal(selector) {
436    const $el = $(selector);
437    return ($el.length && $el.val() != null) ? $el.val() : 'all';
438  }
439
440  // Promise対応のフィルタ関数
441  function saitakuFilteredList() {
442    $('#noitem').hide();
443    return new Promise((resolve) => {
444      var selectedKoubo   = getFilterVal('#kouboFilter');
445      var selectedCountry = getFilterVal('#countryFilter');
446      var selectedJigyou  = getFilterVal('#jigyouFilter');
447      var selectedTaisyo  = getFilterVal('#taisyoFilter');
448      var selectedField   = getFilterVal('#fieldFilter');
449
450      $('.filtered-item').hide().fadeIn();
451
452		if (selectedKoubo !== 'all') {
453        $('.filtered-item').filter(function () {
454          return $(this).find('.koubo').text().trim() !== selectedKoubo;
455        }).hide();
456      }
457
458      if (selectedCountry !== 'all') {
459        // <option> から国名リストを取得
460        const countryList = $('#countryFilter option').map(function () {
461          return $(this).val().trim();
462        }).get();
463
464        $('.filtered-item').filter(function () {
465          const countryText = $(this).find('.country').text().trim();
466          const matchedCountries = countryList.filter(country => {
467            const regex = new RegExp(`(^|・|、|(|))${country}($|・|、|(|))`);
468            return regex.test(countryText);
469          });
470          return !matchedCountries.includes(selectedCountry);
471        }).hide();
472      }
473
474      if (selectedJigyou !== 'all') {
475        $('.filtered-item').filter(function () {
476          return $(this).find('.jigyou').text() !== selectedJigyou;
477        }).hide();
478      }
479
480      if (selectedTaisyo !== 'all') {
481        $('.filtered-item').filter(function () {
482          return $(this).find('.taisyo').text() !== selectedTaisyo;
483        }).hide();
484      }
485
486      if (selectedField !== 'all') {
487        $('.filtered-item').filter(function () {
488          return !$(this).find('.field').text().includes(selectedField);
489        }).hide();
490      }
491
492      const $visible = $('.filtered-item:visible');
493      if ($visible.length === 0) {
494        resolve();
495      } else {
496        $visible.fadeIn(() => resolve());
497      }
498    });
499  }
500
501	function checkHiddenItems() {
502
503	  const selectedKoubo   = getFilterVal('#kouboFilter');
504	  const selectedCountry = getFilterVal('#countryFilter');
505	  const selectedJigyou  = getFilterVal('#jigyouFilter');
506	  const selectedTaisyo  = getFilterVal('#taisyoFilter');
507	  const selectedField   = getFilterVal('#fieldFilter');
508
509	  const allDefault =
510		selectedKoubo === 'all' &&
511		selectedCountry === 'all' &&
512		selectedJigyou === 'all' &&
513		selectedTaisyo === 'all' &&
514		selectedField === 'all';
515
516	  const visibleCount = $('.filtered-item:visible').length;
517
518	  if (!allDefault && visibleCount === 0) {
519		$('#noitem').text('該当の採択情報はありません。').fadeIn();
520	  } else {
521		$('#noitem').hide();
522	  }
523	}
524});
525
526/* =============================
527   タブ切り替え(URLパラメータをホワイトリスト化)
528============================= */
529$(document).ready(function () {
530  const validTabIds = $('.tab-content').map(function(){ return this.id; }).get();
531
532  function safeTabId(raw, fallback) {
533    const id = (raw || '').trim();
534    return validTabIds.includes(id) ? id : fallback;
535  }
536
537  function activateTab(tabId) {
538    $('.tab').removeClass('active');
539    $(`.tab[data-tab="${tabId}"]`).addClass('active');
540    $('.tab-content').removeClass('active');
541    document.getElementById(tabId)?.classList.add('active');
542  }
543
544  const params = new URLSearchParams(window.location.search);
545  const defaultTab = validTabIds[0] || 'tab-fs';
546  const activeTab = safeTabId(params.get('tab'), defaultTab);
547  activateTab(activeTab);
548
549  $('[data-tab]').on('click', function () {
550    const tabId = safeTabId($(this).data('tab'), activeTab);
551    activateTab(tabId);
552    const newUrl = new URL(window.location.href);
553    newUrl.searchParams.set('tab', tabId);
554    history.replaceState(null, '', newUrl);
555  });
556});
557
558/* =============================
559   FAQタブ切り替え(URLパラメータをホワイトリスト化)
560============================= */
561$(document).ready(function () {
562  const validFaqIds = $('.faq-tab-content').map(function(){ return this.id; }).get();
563
564  function safeFaqId(raw, fallback) {
565    const id = (raw || '').trim();
566    return validFaqIds.includes(id) ? id : fallback;
567  }
568
569  function activateFaqTab(tabId) {
570    $('.faq-tab').removeClass('active');
571    $(`.faq-tab[data-tab-faq="${tabId}"]`).addClass('active');
572    $('.faq-tab-content').removeClass('active');
573    document.getElementById(tabId)?.classList.add('active');
574  }
575
576  const faqParams = new URLSearchParams(window.location.search);
577  const defaultFaq = validFaqIds[0] || 'faq-tab1';
578  const activeFaqTab = safeFaqId(faqParams.get('faq-tab'), defaultFaq);
579  activateFaqTab(activeFaqTab);
580
581  $('[data-tab-faq]').on('click', function () {
582    const tabId = safeFaqId($(this).data('tab-faq'), activeFaqTab);
583    activateFaqTab(tabId);
584
585    const newUrl = new URL(window.location.href);
586    newUrl.searchParams.set('faq-tab', tabId);
587    history.replaceState(null, '', newUrl);
588  });
589});
590
591/* =============================
592   事業全体の流れのフェードイン(外部SVGは同一オリジンのみ)
593============================= */
594document.addEventListener('DOMContentLoaded', function () {
595  const flowElement = document.getElementById('flow');
596  const objectElement = document.getElementById('flow-image');
597  if (!flowElement || !objectElement) return;
598
599  function applyFadeInClassSafe(objectEl) {
600    try {
601      const svgDoc = objectEl.contentDocument;
602      if (!svgDoc) return; // 未ロード
603      const target = svgDoc.getElementById('etc-group');
604      if (target) target.classList.add('fadeIn');
605    } catch (e) {
606      // 別オリジン等でアクセス不可。黙って無視
607    }
608  }
609
610  const observer = new IntersectionObserver((entries, obs) => {
611    entries.forEach(entry => {
612      if (entry.isIntersecting) {
613        if (objectElement.contentDocument) {
614          applyFadeInClassSafe(objectElement);
615        } else {
616          objectElement.addEventListener('load', () => applyFadeInClassSafe(objectElement), { once: true });
617        }
618        obs.unobserve(entry.target);
619      }
620    });
621  }, { root: null, rootMargin: '0% 0%', threshold: 0.5 });
622
623  observer.observe(flowElement);
624});
625
626
627/* =============================
628   ヒーローセクションバナー
629============================= */
630document.addEventListener('DOMContentLoaded', function() {
631    const tabButtons = document.querySelectorAll('.tab-button');
632
633    tabButtons.forEach(button => {
634        button.addEventListener('click', function() {
635            const targetTab = this.getAttribute('data-tab');
636
637            // すべてのタブボタンから active クラスを削除
638            tabButtons.forEach(btn => btn.classList.remove('active'));
639
640            // クリックされたタブボタンに active クラスを追加
641            this.classList.add('active');
642
643            // すべてのタブコンテンツから active クラスを削除
644            document.querySelectorAll('.banner-tab-content').forEach(content => {
645                content.classList.remove('active');
646            });
647
648            // 対応するタブコンテンツに active クラスを追加
649            document.getElementById(targetTab).classList.add('active');
650        });
651    });
652});

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.