/** * HPKIセカンド電子証明書サービス 統合FAQ * 医療機関・保険薬局向け / ベンダー向け をタブで切り替える共通アプリケーション */ (function () { 'use strict'; // タブ設定テーブル(データ・表示文言・テーマはここで一元管理) var TAB_CONFIG = { institution: { data: (typeof FAQ_DATA_INSTITUTION !== 'undefined') ? FAQ_DATA_INSTITUTION : [], tabLabel: '医療機関・保険薬局向け', title: 'クライアント証明書に関するFAQ', subtitle: '医療機関・保険薬局向け よくある質問と回答', docTitle: 'クライアント証明書に関するFAQ - HPKIセカンド電子証明書サービス', placeholder: 'キーワードで検索(例: 振込先、口座名義人、再発行、パスワード通知書)' }, vendor: { data: (typeof FAQ_DATA_VENDOR !== 'undefined') ? FAQ_DATA_VENDOR : [], tabLabel: 'ベンダー向け', title: 'HPKIセカンド電子証明書サービス 技術的質問 FAQ', subtitle: 'ベンダー向け よくある質問と回答', docTitle: 'FAQ - HPKIセカンド電子証明書サービス', placeholder: 'キーワードで検索(例: FIDO、マイナンバーカード、クライアント証明書)' } }; var TAB_ORDER = ['institution', 'vendor']; var DEFAULT_TAB = 'institution'; var currentTab = DEFAULT_TAB; var currentKeyword = ''; // 現在タブのカテゴリ表示順とアイコン番号(データの大分類出現順から動的導出) var categoryOrder = []; var categoryIcons = {}; // リンクアイコン(🔗) var LINK_ICON = '🔗'; /** * 現在タブのデータを取得 */ function activeData() { return TAB_CONFIG[currentTab].data; } /** * クエリパラメタ取得(ES5互換) */ function getQueryParam(name) { var m = new RegExp('[?&]' + name + '=([^&#]*)').exec(location.search); return m ? decodeURIComponent(m[1].replace(/\+/g, ' ')) : null; } /** * 現在タブのデータからカテゴリ表示順・アイコン番号を再計算 */ function recomputeCategories() { categoryOrder = []; categoryIcons = {}; activeData().forEach(function (item) { if (categoryOrder.indexOf(item.majorCategory) === -1) { categoryOrder.push(item.majorCategory); categoryIcons[item.majorCategory] = String(categoryOrder.length); } }); } /** * FAQデータをmajorCategory → minorCategoryでグループ化 */ function groupFAQ(data) { var grouped = {}; data.forEach(function (item) { var key = item.majorCategory; if (!grouped[key]) { grouped[key] = { code: item.majorCategory, name: item.majorCategoryName, minorCategories: {} }; } var minor = item.minorCategory || ''; if (!grouped[key].minorCategories[minor]) { grouped[key].minorCategories[minor] = []; } grouped[key].minorCategories[minor].push(item); }); return grouped; } /** * テキスト内のキーワードをハイライト */ function highlightText(text, keyword) { if (!keyword) return text; // HTMLタグ内および ... ブロック内(URL表示)はハイライトしない var parts = text.split(/(]*>[\s\S]*?<\/a>|<[^>]+>)/); return parts.map(function (part) { if (part.startsWith('<')) return part; var escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return part.replace(new RegExp(escaped, 'gi'), '$&'); }).join(''); } /** * FAQ全体をレンダリング */ function renderFAQ(data, keyword) { var contentEl = document.getElementById('faq-content'); var grouped = groupFAQ(data); var html = ''; var totalVisible = 0; categoryOrder.forEach(function (catCode) { var cat = grouped[catCode]; if (!cat) return; var catHtml = ''; var catHasItems = false; Object.keys(cat.minorCategories).forEach(function (minorName) { var items = cat.minorCategories[minorName]; var itemsHtml = ''; items.forEach(function (item) { var q = keyword ? highlightText(escapeHtml(item.question), keyword) : escapeHtml(item.question); var a = keyword ? highlightText(item.answer, keyword) : item.answer; itemsHtml += '
' + '' + q + '' + '' + '' + '
' + 'A' + a + '
' + '
'; totalVisible++; }); if (itemsHtml) { catHasItems = true; // 小分類名が空の場合は小分類見出しを出力しない if (minorName) { var minorTitle = keyword ? highlightText(escapeHtml(minorName), keyword) : escapeHtml(minorName); catHtml += '
' + '

' + minorTitle + '

' + itemsHtml + '
'; } else { catHtml += '
' + itemsHtml + '
'; } } }); if (catHasItems) { html += '
' + '

' + '' + categoryIcons[catCode] + '' + cat.name + '

' + catHtml + '
'; } }); if (!html) { html = '
' + '
🔍
' + '
該当するFAQが見つかりません
' + '
別のキーワードでお試しください
' + '
'; } contentEl.innerHTML = html; // 検索件数表示 var countEl = document.getElementById('search-count'); if (keyword) { countEl.textContent = totalVisible + ' 件の結果'; } else { countEl.textContent = '全 ' + activeData().length + ' 件'; } } /** * HTMLエスケープ(改行はスペースに変換) */ function escapeHtml(text) { var div = document.createElement('div'); div.textContent = text; return div.innerHTML.replace(/\n/g, ' '); } /** * キーワードでFAQをフィルタリング */ function filterFAQ(keyword) { currentKeyword = keyword; if (!keyword) { renderFAQ(activeData(), ''); return; } var lower = keyword.toLowerCase(); var filtered = activeData().filter(function (item) { var answerText = item.answer.replace(/<[^>]+>/g, '').toLowerCase(); return item.question.toLowerCase().indexOf(lower) !== -1 || answerText.indexOf(lower) !== -1; }); renderFAQ(filtered, keyword); } /** * サイドバーナビゲーション生成 */ function renderNav() { var navEl = document.getElementById('category-nav'); var grouped = groupFAQ(activeData()); var html = ''; navEl.innerHTML = html; // クリックイベント(スムーススクロール) navEl.querySelectorAll('.nav-link').forEach(function (link) { link.addEventListener('click', function (e) { e.preventDefault(); var targetId = this.getAttribute('href').substring(1); var targetEl = document.getElementById(targetId); if (targetEl) { targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // 検索をクリア var searchInput = document.getElementById('search-input'); if (searchInput.value) { searchInput.value = ''; filterFAQ(''); // スクロールし直す(再レンダリング後) setTimeout(function () { var el = document.getElementById(targetId); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 50); } }); }); } /** * テキストをクリップボードへコピー(非対応環境はフォールバック) */ function copyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text); } return new Promise(function (resolve, reject) { try { var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); resolve(); } catch (err) { reject(err); } }); } /** * コピーボタンに「コピー済み」フィードバックを表示 */ function showCopied(btn) { if (btn._resetTimer) clearTimeout(btn._resetTimer); btn.classList.add('copied'); btn.innerHTML = '✓'; // ✓ btn.setAttribute('aria-label', 'リンクをコピーしました'); btn._resetTimer = setTimeout(function () { btn.classList.remove('copied'); btn.innerHTML = LINK_ICON; btn.setAttribute('aria-label', 'このQ&Aへのリンクをコピー'); }, 1500); } /** * URLハッシュ(#faq-{id})に対応するQ&Aを開いてスクロール・強調 * ※ タブは呼び出し側で確定済みである前提 */ function openFAQFromHash() { var m = /^#faq-(\d+)$/.exec(location.hash); if (!m) return; // 検索中は解除して全件表示(対象が絞り込みで非表示の場合に備える) var searchInput = document.getElementById('search-input'); if (searchInput && searchInput.value) { searchInput.value = ''; filterFAQ(''); } var el = document.getElementById('faq-' + m[1]); if (!el) return; el.open = true; el.scrollIntoView({ behavior: 'smooth', block: 'start' }); el.classList.remove('faq-flash'); // リフローを挟んでアニメーションを再起動 void el.offsetWidth; el.classList.add('faq-flash'); } /** * タブを切り替える * @param {string} tab 'institution' | 'vendor' * @param {object} [opts] { updateUrl: boolean } */ function switchTab(tab, opts) { if (!TAB_CONFIG[tab]) tab = DEFAULT_TAB; currentTab = tab; var conf = TAB_CONFIG[tab]; // テーマ色分け document.body.setAttribute('data-theme', tab); // タブボタンの状態更新 var tabButtons = document.querySelectorAll('.faq-tab'); tabButtons.forEach(function (btn) { var selected = btn.getAttribute('data-tab') === tab; btn.setAttribute('aria-selected', selected ? 'true' : 'false'); btn.classList.toggle('active', selected); btn.tabIndex = selected ? 0 : -1; }); // ヘッダー・タイトル・プレースホルダ更新 var titleEl = document.getElementById('page-title'); var subtitleEl = document.getElementById('page-subtitle'); if (titleEl) titleEl.textContent = conf.title; if (subtitleEl) subtitleEl.textContent = conf.subtitle; document.title = conf.docTitle; var searchInput = document.getElementById('search-input'); if (searchInput) { searchInput.placeholder = conf.placeholder; searchInput.value = ''; } currentKeyword = ''; // データ再構築・描画 recomputeCategories(); renderNav(); renderFAQ(activeData(), ''); // URLへタブを反映(ハッシュはクリア) if (opts && opts.updateUrl) { var newUrl = location.pathname + '?tab=' + tab; history.replaceState(null, '', newUrl); } } /** * 初期化 */ document.addEventListener('DOMContentLoaded', function () { // 初期タブ:?tab= パラメタ → 不正/無指定は既定 var paramTab = getQueryParam('tab'); var initialTab = TAB_CONFIG[paramTab] ? paramTab : DEFAULT_TAB; switchTab(initialTab, { updateUrl: false }); // タブクリック var tablist = document.querySelector('.faq-tabs'); if (tablist) { tablist.addEventListener('click', function (e) { var btn = e.target.closest('.faq-tab'); if (!btn) return; var tab = btn.getAttribute('data-tab'); if (tab !== currentTab) { switchTab(tab, { updateUrl: true }); } }); // キーボード操作(左右矢印でタブ移動) tablist.addEventListener('keydown', function (e) { if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return; var idx = TAB_ORDER.indexOf(currentTab); if (idx === -1) return; var next = e.key === 'ArrowRight' ? (idx + 1) % TAB_ORDER.length : (idx - 1 + TAB_ORDER.length) % TAB_ORDER.length; e.preventDefault(); switchTab(TAB_ORDER[next], { updateUrl: true }); var btn = document.querySelector('.faq-tab[data-tab="' + TAB_ORDER[next] + '"]'); if (btn) btn.focus(); }); } var contentEl = document.getElementById('faq-content'); // コピーボタン(イベント委譲。クリックでアコーディオンは開閉しない) contentEl.addEventListener('click', function (e) { var btn = e.target.closest('.copy-link-btn'); if (!btn) return; e.preventDefault(); e.stopPropagation(); // タブを含むURLを生成(タブ間でidが重複するため) var url = location.origin + location.pathname + '?tab=' + currentTab + '#faq-' + btn.getAttribute('data-id'); copyToClipboard(url).then(function () { showCopied(btn); }).catch(function () { // コピー失敗時はURLを選択可能な状態で通知 window.prompt('以下のリンクをコピーしてください', url); }); }); // 直リンクで開いた場合・ハッシュ変更時に該当Q&Aを開く(タブ確定後) openFAQFromHash(); window.addEventListener('hashchange', openFAQFromHash); // 検索イベント var searchInput = document.getElementById('search-input'); var debounceTimer; searchInput.addEventListener('input', function () { clearTimeout(debounceTimer); var value = this.value.trim(); debounceTimer = setTimeout(function () { filterFAQ(value); }, 200); }); // Escキーで検索クリア searchInput.addEventListener('keydown', function (e) { if (e.key === 'Escape') { this.value = ''; filterFAQ(''); this.blur(); } }); }); })();