PageSourceSearch

https://www.nise.go.jp/wp-content/themes/twentytwenty-five-child/js/custom.js?ver=7.1.2

js nise.go.jp collected 2026-10-01 11:45:50 UTC 33,821 bytes, 908 lines download raw bytes

1document.addEventListener("DOMContentLoaded", () => {
2  // --- 変数定義 ---
3  const header = document.querySelector("header");
4  const mainContent =
5    document.querySelector("main") ||
6    document.querySelector(".wp-site-blocks > :not(header):not(footer)");
7  const footer = document.querySelector("footer");
8  let toTopButtonContainer = null;
9  // 現在のドメインを取得 (小文字化して比較を容易にする)
10  const currentHostname = location.hostname.toLowerCase();
11  const REM_OFFSET_PX = 0;
12  const isEn = location.pathname.includes("/english/");
13
14  const uiDict = {
15    skipNav: isEn ? "Jump to Navigation" : "ナビゲーションへジャンプ",
16    skipMain: isEn ? "Jump to Main Content" : "メインコンテンツへジャンプ",
17    skipFooter: isEn ? "Jump to Footer" : "フッターへジャンプ",
18    skipAria: isEn ? "Quick Menu" : "クイックメニュー",
19  };
20
21  // =========================================================
22  // 【追加】A. アクセシビリティ:ID/属性付与とスキップリンク挿入
23  // =========================================================
24  if (header) {
25    header.id = "site-navigation";
26    header.setAttribute("tabindex", "-1");
27  }
28  if (mainContent) {
29    mainContent.id = "main-content";
30    mainContent.setAttribute("tabindex", "-1");
31  }
32  if (footer) {
33    footer.id = "site-footer";
34    footer.setAttribute("tabindex", "-1");
35  }
36
37  const oldSkipLink =
38    document.getElementById("wp-skip-link") ||
39    document.querySelector(".skip-link.screen-reader-text");
40  if (oldSkipLink) {
41    oldSkipLink.remove();
42  }
43
44  // スキップリンクをbodyの先頭に挿入
45  const skipLinksHTML = `
46    <nav class="skip-links" aria-label="${uiDict.skipAria}">
47      <a class="skip-link screen-reader-text" href="#site-navigation">${uiDict.skipNav}</a>
48      <a class="skip-link screen-reader-text" href="#main-content">${uiDict.skipMain}</a>
49      <a class="skip-link screen-reader-text" href="#site-footer">${uiDict.skipFooter}</a>
50    </nav>`;
51  document.body.insertAdjacentHTML("afterbegin", skipLinksHTML);
52
53  // =========================================================
54  // ヘッダー/スクロールオフセット関数: mainのpaddingとCSS変数を設定
55  // =========================================================
56  const setMainPaddingAndScrollOffset = () => {
57    if (header && mainContent) {
58      // 1. ヘッダーの正確な高さを取得
59      const headerHeight = header.offsetHeight;
60
61      // 2. 管理バー(wpadminbar)の高さを取得(ログイン時のみ)
62      const adminBar = document.getElementById("wpadminbar");
63      const adminBarHeight = adminBar ? adminBar.offsetHeight : 0;
64
65      // 3. 全体のオフセットを計算
66      // 余裕を持たせるための buffer(20px)を足しています
67      const buffer = 20;
68      const totalOffsetHeight = headerHeight + adminBarHeight + buffer;
69
70      // ① main要素にpadding-topを設定(ヘッダーで隠れる領域を確保)
71      // padding-topが消される対策として !important を使用
72      mainContent.style.setProperty(
73        "padding-top",
74        `${headerHeight}px`,
75        "important",
76      );
77
78      // ② CSSカスタムプロパティとしてヘッダーの高さを設定(ページ内リンクのスクロールオフセット用)
79      document.documentElement.style.setProperty(
80        "--header-height",
81        `${totalOffsetHeight}px`,
82      );
83    }
84  };
85
86  setMainPaddingAndScrollOffset();
87  window.addEventListener("resize", setMainPaddingAndScrollOffset);
88
89  // =========================================================
90  // 1. リンク処理(PDF/Excel/外部リンクの target="_blank" 付与)
91  // =========================================================
92  document.querySelectorAll("a").forEach((link) => {
93    const url = link.getAttribute("href");
94
95    // hrefがない、またはアンカーリンク (#) の場合はスキップ
96    if (!url || url.startsWith("#")) {
97      return;
98    }
99
100    // target="_blank" と rel="noopener noreferrer" を設定するヘルパー関数
101    const setTargetBlank = (className) => {
102      link.setAttribute("target", "_blank");
103      link.setAttribute("rel", "noopener noreferrer");
104      if (className) {
105        link.classList.add(className);
106      }
107    };
108
109    const linkText = link.textContent;
110
111    // 正規表現: [数字] というパターンにマッチし、かつ M や K が含まれていない場合
112    // 例: [259] はマッチするが、[1M] や [500K] はマッチしない
113    const sizeMatch = linkText.match(/\[(\d+)\]/);
114
115    if (sizeMatch) {
116      const fullMatch = sizeMatch[0]; // "[259]"
117      const numericValue = sizeMatch[1]; // "259"
118
119      // [数字] を [数字K] に置換してテキストを更新
120      link.innerHTML = link.innerHTML.replace(fullMatch, `[${numericValue}K]`);
121    }
122    // --- ファイル拡張子で識別 ---
123    const fileMatch = url.match(/\.(pdf|doc|docx|xls|xlsx|zip)$/i);
124    if (fileMatch) {
125      const extension = fileMatch[1].toLowerCase();
126
127      // 読み上げ用のマッピング
128      const extensionNames = {
129        pdf: "PDFファイル",
130        doc: "Wordファイル",
131        docx: "Wordファイル",
132        xls: "Excelファイル",
133        xlsx: "Excelファイル",
134        zip: "ZIP圧縮ファイル",
135      };
136
137      const fileTypeDescription = extensionNames[extension] || "ファイル";
138
139      // 【修正版】ブラウザ読み上げにも認識される「透明テキスト」を生成
140      const readableSuffix = document.createElement("span");
141      readableSuffix.className = "visual-hidden-text"; // 新しいクラス名
142      readableSuffix.textContent = ` (${fileTypeDescription})`;
143
144      link.appendChild(readableSuffix);
145
146      link.classList.add("file-link", `file-link-${extension}`);
147      setTargetBlank();
148      return;
149    }
150
151    // --- 外部リンク(ドメインが異なる場合)で識別 ---
152    // URLが http:// または https:// で始まり、かつ現在のドメインを含まない場合
153    if (
154      url.startsWith("http") &&
155      url.toLowerCase().indexOf(currentHostname) === -1
156    ) {
157      link.classList.add("external-link");
158      setTargetBlank();
159    }
160  });
161
162  // =========================================================
163  // 2. トップへ戻るボタンの追加とDOM操作
164  // =========================================================
165
166  // a. フッターへの「トップへ戻る」ボタンの追加
167  if (footer) {
168    const toTopDiv = document.createElement("div");
169    toTopDiv.className = "to_top";
170    toTopDiv.innerHTML =
171      '<a href="#" id="totop"><i class="fa-solid fa-arrow-up"></i></a>';
172    footer.appendChild(toTopDiv);
173    toTopButtonContainer = toTopDiv; // 後で使用するために保存
174  }
175
176  // b. 「トップへ戻る」ボタンのクリック処理 (スムーズスクロール)
177  const toTopLink = document.getElementById("totop");
178  if (toTopLink) {
179    toTopLink.addEventListener("click", (e) => {
180      e.preventDefault();
181      window.scrollTo({
182        top: 0,
183        behavior: "smooth",
184      });
185    });
186  }
187
188  // =========================================================
189  // 3. スクロールイベントの処理
190  // =========================================================
191  window.addEventListener("scroll", () => {
192    const scrollY = window.scrollY;
193
194    // 【ヘッダー処理】
195    if (header) {
196      if (scrollY > 10) {
197        header.classList.add("bg");
198        header.classList.add("hide-header");
199      } else {
200        header.classList.remove("bg");
201        header.classList.remove("hide-header");
202      }
203    }
204
205    // 【トップへ戻るボタンの表示/非表示】
206    if (toTopButtonContainer) {
207      // スクロール量が300pxを超えたら表示
208      if (scrollY > 300) {
209        toTopButtonContainer.classList.add("fadein");
210      } else {
211        toTopButtonContainer.classList.remove("fadein");
212      }
213    }
214  });
215
216  // =========================================================
217  // 4. テーブル列のクラス付与処理
218  // =========================================================
219  const figureElement = document.querySelector("figure.wp-block-table");
220
221  if (figureElement) {
222    const shouldSkipTable =
223      figureElement.classList.contains("nowrap") ||
224      figureElement.classList.contains("timetable");
225
226    if (!shouldSkipTable) {
227      const tableContainer = figureElement.querySelector("table");
228
229      if (tableContainer) {
230        if (!tableContainer.classList.contains("has-fixed-layout")) {
231          applyClassToFirstAndLastColumns(tableContainer);
232        }
233      }
234    }
235  }
236
237  // =========================================================
238  // 5. グローバルメニューのカレント表示処理
239  // =========================================================
240  const globalMenuLinks = document.querySelectorAll(".global-menu ul li a");
241  const currentPath = window.location.pathname.replace(/\/$/, "") || "/";
242
243  globalMenuLinks.forEach((link) => {
244    const href = link.getAttribute("href");
245    if (!href) return;
246
247    try {
248      const linkUrl = new URL(href, window.location.origin);
249      const linkPath = linkUrl.pathname.replace(/\/$/, "") || "/";
250
251      if (linkPath === currentPath) {
252        link.classList.add("current");
253      }
254    } catch (e) {
255      console.error("Invalid URL in global menu:", href);
256    }
257  });
258
259  // =========================================================
260  // 6. お知らせセクションのタブ切り替え&もっと見る機能
261  // =========================================================
262  const newsSection = document.getElementById("news");
263
264  if (newsSection) {
265    // コンテンツブロックの取得
266    const contentDivs = Array.from(
267      newsSection.querySelectorAll(".news-right-col > .wp-block-group"),
268    );
269    const tabs = newsSection.querySelectorAll(
270      ".news-left-col .wp-block-list li",
271    );
272
273    // ボタン要素の取得
274    const expandBtnContainer = newsSection.querySelector(".news-expand-btn");
275    const expandBtn = expandBtnContainer
276      ? expandBtnContainer.querySelector(".wp-block-button__link")
277      : null;
278
279    // --- 初期化処理 ---
280    contentDivs.forEach((div) => (div.style.display = "none"));
281
282    const allContent = contentDivs.find((div) =>
283      div.classList.contains("list-all"),
284    );
285    if (allContent) allContent.style.display = "block";
286
287    tabs.forEach((tab) => {
288      if (tab.classList.contains("list-all")) tab.classList.add("active");
289    });
290
291    // --- 関数: ボタンの状態をリセットする ---
292    const resetExpandButton = () => {
293      if (expandBtn) {
294        expandBtn.textContent = "お知らせ表示を増やす";
295        expandBtn.classList.remove("is-open");
296      }
297      if (expandBtnContainer) {
298        expandBtnContainer.style.display = "block"; // 常に表示しておく
299      }
300    };
301
302    // --- タブクリック処理 ---
303    tabs.forEach((tab) => {
304      tab.addEventListener("click", function () {
305        tabs.forEach((t) => t.classList.remove("active"));
306        this.classList.add("active");
307
308        let targetClass = null;
309        this.classList.forEach((cls) => {
310          if (cls.startsWith("list-")) targetClass = cls;
311        });
312
313        if (targetClass) {
314          // 全コンテンツを非表示&展開状態リセット
315          contentDivs.forEach((div) => {
316            div.style.display = "none";
317            div.classList.remove("is-expanded");
318          });
319
320          // ボタンの状態をリセット(重要:タブを変えたら「増やす」に戻す)
321          resetExpandButton();
322
323          const targetDiv = contentDivs.find((div) =>
324            div.classList.contains(targetClass),
325          );
326          if (targetDiv) {
327            targetDiv.style.opacity = 0;
328            targetDiv.style.display = "block";
329            requestAnimationFrame(() => {
330              targetDiv.style.transition = "opacity 0.5s ease";
331              targetDiv.style.opacity = 1;
332            });
333          }
334        }
335      });
336    });
337
338    // --- 「お知らせ表示を増やす/減らす」ボタン処理 ---
339    if (expandBtn) {
340      expandBtn.addEventListener("click", function (e) {
341        e.preventDefault();
342
343        // 現在表示されているコンテンツを探す
344        const visibleDiv = contentDivs.find(
345          (div) => div.style.display !== "none",
346        );
347
348        if (visibleDiv) {
349          // すでに開いているかチェック
350          if (visibleDiv.classList.contains("is-expanded")) {
351            // ▼ 閉じるときの処理
352            visibleDiv.classList.remove("is-expanded");
353            this.textContent = "お知らせ表示を増やす"; // テキスト変更
354            this.classList.remove("is-open"); // アイコン用クラス削除
355
356            // オプション: 閉じたときにリストの先頭付近へスクロールさせるならここに記述
357            // visibleDiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
358          } else {
359            // ▼ 開くときの処理
360            visibleDiv.classList.add("is-expanded");
361            this.textContent = "お知らせ表示を減らす"; // テキスト変更
362            this.classList.add("is-open"); // アイコン用クラス追加
363          }
364        }
365      });
366    }
367  }
368
369  // =========================================================
370  // 7. 検索ボックスとリンク処理 (自動生成 & 挙動切り替え)
371  // =========================================================
372  const searchLinkContainer = document.querySelector(".link_search");
373  const searchLink = searchLinkContainer
374    ? searchLinkContainer.querySelector("a")
375    : null;
376
377  if (searchLink && header) {
378    // A. 検索ボックスのHTMLを生成してヘッダーに追加
379
380    // 1. トップページのURLをロゴから自動取得する(なければ直接 /nise/ を指定)
381    const logoAnchor = document.querySelector(".logo-area figure a");
382    const homeUrl = logoAnchor ? logoAnchor.getAttribute("href") : "/";
383
384    // 2. action属性に homeUrl を埋め込む
385    const searchBoxHtml = `
386      <div id="header-search-box" aria-hidden="true" style="display: none;">
387        <div class="search-box-inner">
388          <form role="search" method="get" class="search-form" action="${homeUrl}">
389            <label>
390              <span class="screen-reader-text">検索:</span>
391              <input type="search" class="search-field" placeholder="キーワードを入力..." value="" name="s" />
392            </label>
393            <button type="submit" class="search-submit" aria-label="検索">
394              <i class="fas fa-search"></i>
395            </button>
396          </form>
397          <button id="search-close-btn" aria-label="閉じる"><i class="fas fa-times"></i></button>
398        </div>
399      </div>
400    `;
401
402    // ヘッダーの末尾に挿入
403    header.insertAdjacentHTML("beforeend", searchBoxHtml);
404
405    const searchBox = document.getElementById("header-search-box");
406    const searchInput = searchBox.querySelector("input[type='search']");
407    const closeBtn = document.getElementById("search-close-btn");
408
409    // B. ページタイプの判定
410    const isHome = document.body.classList.contains("home");
411    const isSearch = document.body.classList.contains("search"); // 検索結果ページかどうか
412
413    if (isSearch) {
414      // --- パターン0: 検索結果ページの場合 (クリック無効化) ---
415      searchLink.removeAttribute("href"); // リンク先を削除
416      searchLink.style.pointerEvents = "none"; // クリックできないようにする
417      searchLink.style.cursor = "default"; // カーソルを通常に戻す
418      searchLink.style.opacity = "0.5"; // 少し薄くして無効感を出す
419      searchLink.setAttribute("aria-disabled", "true");
420    } else if (isHome) {
421      // --- パターン1: トップページの場合 (ページ内リンク) ---
422
423      // hrefを書き換え
424      searchLink.setAttribute("href", "#search");
425
426      searchLink.addEventListener("click", (e) => {
427        e.preventDefault();
428        const targetSection = document.getElementById("search");
429        if (targetSection) {
430          // ヘッダーの高さを考慮してスクロール
431          const headerHeight = header.offsetHeight;
432          const targetPosition =
433            targetSection.getBoundingClientRect().top +
434            window.scrollY -
435            headerHeight;
436
437          window.scrollTo({
438            top: targetPosition,
439            behavior: "smooth",
440          });
441        }
442      });
443    } else {
444      // --- パターン2: その他の下層ページの場合 (検索ボックス表示切り替え) ---
445
446      // ★追加: アクセシビリティ対策。ページ内リンクを解除し、ボタンとしての役割を明示する
447      searchLink.setAttribute("href", "javascript:void(0);"); // または "#" でも良いが javascript:void(0) が確実
448      searchLink.setAttribute("role", "button"); // リンクではなくボタンであることを伝える
449      searchLink.setAttribute("aria-haspopup", "true"); // 押すと何か(検索ボックス)が出ることを伝える
450      searchLink.setAttribute("aria-expanded", "false"); // 現在は閉じていることを伝える
451
452      const toggleSearchBox = (show) => {
453        if (show) {
454          searchBox.style.display = "block";
455          searchBox.setAttribute("aria-hidden", "false");
456          searchLink.setAttribute("aria-expanded", "true"); // ★状態を更新
457          requestAnimationFrame(() => {
458            searchBox.classList.add("is-visible");
459          });
460          if (searchInput) searchInput.focus();
461        } else {
462          searchBox.classList.remove("is-visible");
463          searchBox.setAttribute("aria-hidden", "true");
464          searchLink.setAttribute("aria-expanded", "false"); // ★状態を更新
465          setTimeout(() => {
466            searchBox.style.display = "none";
467          }, 300);
468        }
469      };
470
471      // 検索アイコンクリック時
472      searchLink.addEventListener("click", (e) => {
473        e.preventDefault();
474        const isHidden = searchBox.style.display === "none";
475        toggleSearchBox(isHidden);
476      });
477
478      // 閉じるボタンクリック時
479      if (closeBtn) {
480        closeBtn.addEventListener("click", (e) => {
481          e.preventDefault();
482          toggleSearchBox(false);
483        });
484      }
485
486      // 外側をクリックしたら閉じる処理
487      document.addEventListener("click", (e) => {
488        if (
489          searchBox.style.display !== "none" &&
490          !searchBox.contains(e.target) &&
491          !searchLinkContainer.contains(e.target)
492        ) {
493          toggleSearchBox(false);
494        }
495      });
496    }
497  }
498
499  // =========================================================
500  // 8. タグのテキストに基づくクラス付与 (自動色分け)
501  // =========================================================
502  const tagLinks = document.querySelectorAll(".taxonomy-post_tag a");
503
504  // テキストと付与したいクラスのマッピング設定
505  const tagMap = {
506    お知らせ: "tag-news",
507    イベント: "tag-event",
508    研究: "tag-study",
509    研修: "tag-training",
510    情報提供: "tag-information",
511  };
512
513  tagLinks.forEach((link) => {
514    // 前後の空白を削除してテキストを取得
515    const text = link.textContent.trim();
516    const className = tagMap[text];
517
518    if (className) {
519      // 1. リンク(aタグ)自体にクラスを付与
520      link.classList.add(className);
521
522      // 2. 親の li.wp-block-post にもクラスを付与
523      // (これにより既存のCSS設計である li.tag-event ... が機能します)
524      const parentLi = link.closest("li.wp-block-post");
525      if (parentLi) {
526        parentLi.classList.add(className);
527      }
528    }
529  });
530
531  // =========================================================
532  // 9. ロゴエリア全体へのリンク追加(改良版)
533  // =========================================================
534  const initLogoLinks = () => {
535    // 【追加】URLに /english/ が含まれている場合は処理を中断する
536    if (window.location.pathname.includes("/english/")) {
537      return;
538    }
539    // 全てのロゴエリアを対象にする
540    const logoAreas = document.querySelectorAll(".logo-area");
541
542    logoAreas.forEach((logoArea) => {
543      // 既に有効なリンク(中身があるリンク)が設定されている場合はスキップ
544      if (logoArea.querySelector("a[data-custom-link='true']")) return;
545
546      // リンク先URLの決定
547      // 1. 既存のaタグがあればそのhrefを取得
548      // 2. なければサイトトップ (https://joc30.xsrv.jp/nise/)
549      const existingAnchor = logoArea.querySelector("a");
550      const targetUrl = existingAnchor
551        ? existingAnchor.getAttribute("href")
552        : "/";
553
554      // 既存の「空のaタグ」などは削除して中身を整理する
555      const nav = logoArea.querySelector("nav"); // ナビゲーション(メニュー)はリンクに含めない
556      const figure = logoArea.querySelector("figure");
557      const logoText = logoArea.querySelector(".logo-text");
558
559      // リンクに含める要素(画像とテキスト)を特定
560      const linkableElements = [figure, logoText].filter((el) => el !== null);
561
562      if (linkableElements.length > 0) {
563        // 新しいリンクを作成
564        const wrapperLink = document.createElement("a");
565        wrapperLink.href = targetUrl;
566        wrapperLink.setAttribute("data-custom-link", "true");
567        wrapperLink.style.display = "contents"; // レイアウト崩れ防止
568        wrapperLink.style.textDecoration = "none";
569        wrapperLink.style.color = "inherit";
570
571        // 画像とテキストをリンクの中に移動
572        linkableElements.forEach((el) => {
573          // 既存のaタグがあればその中身だけ取り出す、なければ要素ごと移動
574          const innerA = el.querySelector("a");
575          if (innerA) {
576            // 空のaタグや既存のaタグを消して中身を直接wrapperに移動
577            while (innerA.firstChild) {
578              wrapperLink.appendChild(innerA.firstChild);
579            }
580            innerA.remove();
581          }
582          wrapperLink.appendChild(el);
583        });
584
585        // ロゴエリアの先頭(ナビゲーションの前)にリンクを挿入
586        logoArea.insertBefore(wrapperLink, logoArea.firstChild);
587      }
588    });
589  };
590
591  initLogoLinks();
592
593  // =========================================================
594  // 10. パンくずリストの移動 (h1の下へ)
595  // =========================================================
596  const breadcrumb = document.querySelector(".bread");
597  const pageTitle = document.querySelector("h1");
598
599  // パンくずリストとh1の両方が存在する場合のã
599¿å®Ÿè¡Œ
600  if (breadcrumb && pageTitle) {
601    // h1要素の「直後 (afterend)」にパンくずリストを挿入(移動)する
602    pageTitle.insertAdjacentElement("afterend", breadcrumb);
603  }
604
605  // =========================================================
606  // 11. H1をJS計算(px)で強制的に全幅にする
607  // =========================================================
608  const mainH1 = document.querySelector("main h1");
609
610  if (mainH1) {
611    const applyFullWidthStyles = () => {
612      // 1. 画面の幅(スクロールバーを含まない描画領域)を取得
613      const windowWidth = document.documentElement.clientWidth;
614      const halfWidth = windowWidth / 2;
615
616      // 2. スタイルを !important 付きで適用
617      // width: 画面幅px
618      mainH1.style.setProperty("width", `${windowWidth}px`, "important");
619
620      // 中央配置のロジック: 親要素の幅に関係なく画面中央に配置
621      mainH1.style.setProperty("position", "relative", "important");
622      mainH1.style.setProperty("left", "50%", "important");
623      mainH1.style.setProperty("right", "auto", "important");
624
625      // マージンで左に画面半分戻す
626      mainH1.style.setProperty("margin-left", `-${halfWidth}px`, "important");
627      mainH1.style.setProperty("margin-right", `-${halfWidth}px`, "important");
628
629      // レイアウト崩れ防止
630      mainH1.style.setProperty("box-sizing", "border-box", "important");
631      mainH1.style.setProperty("max-width", "none", "important"); // WPがmax-widthを入れている場合の対策
632
633      // 親要素の影響で横スクロールバーが出ないように body の横スクロールを抑制
634      document.body.style.overflowX = "hidden";
635    };
636
637    // 親要素(main)の overflow: hidden があると左右が切れるため解除
638    if (mainContent) {
639      mainContent.style.setProperty("overflow", "visible", "important");
640    }
641
642    // 実行
643    applyFullWidthStyles();
644
645    // 画面リサイズ時に再計算して適用
646    window.addEventListener("resize", () => {
647      // リサイズ処理は頻繁に走るため、requestAnimationFrameで最適化
648      requestAnimationFrame(applyFullWidthStyles);
649    });
650  }
651
652  // =========================================================
653  // 【修正版】12. スマホ用テーブル:上部にスクロールバーとヒントを表示
654  // =========================================================
655  const handleTableUpperScroll = () => {
656    const isMobile = window.innerWidth <= 768;
657    const tables = document.querySelectorAll("figure.wp-block-table");
658
659    tables.forEach((fig) => {
660      // 1. スクロールヒント(文字)の処理
661      let hint = fig.querySelector(".scroll-hint");
662      if (isMobile && fig.scrollWidth > fig.clientWidth) {
663        if (!hint) {
664          hint = document.createElement("span");
665          hint.className = "scroll-hint";
666          hint.textContent = isEn ? "Scroll ▶" : "スクロール ▶";
667          fig.prepend(hint); // テーブルの上部(中)に配置
668
669          fig.addEventListener(
670            "scroll",
671            function fadeHint() {
672              if (fig.scrollLeft > 20) {
673                hint.style.opacity = "0";
674                hint.style.pointerEvents = "none";
675                fig.removeEventListener("scroll", fadeHint);
676              }
677            },
678            { passive: true },
679          );
680        }
681      } else if (hint) {
682        hint.remove();
683      }
684
685      // 2. 上部スクロールバー(ダミー)の処理
686      let doubleScroll = fig.previousElementSibling;
687      if (isMobile && fig.scrollWidth > fig.clientWidth) {
688        // ダミー要素がまだなければ作成
689        if (
690          !doubleScroll ||
691          !doubleScroll.classList.contains("double-scroll-wrapper")
692        ) {
693          doubleScroll = document.createElement("div");
694          doubleScroll.className = "double-scroll-wrapper";
695          doubleScroll.innerHTML = '<div class="double-scroll-inner"></div>';
696          fig.parentNode.insertBefore(doubleScroll, fig);
697        }
698
699        const inner = doubleScroll.querySelector(".double-scroll-inner");
700        // テーブルの「中身の幅」をダミーの中身に適用
701        inner.style.width = fig.scrollWidth + "px";
702
703        // 同期処理:上が動けば下も動く
704        doubleScroll.onscroll = () => {
705          fig.scrollLeft = doubleScroll.scrollLeft;
706        };
707        // 同期処理:下が動けば上も動く
708        fig.onscroll = () => {
709          doubleScroll.scrollLeft = fig.scrollLeft;
710        };
711      } else if (
712        doubleScroll &&
713        doubleScroll.classList.contains("double-scroll-wrapper")
714      ) {
715        doubleScroll.remove();
716      }
717    });
718  };
719
720  handleTableUpperScroll();
721  window.addEventListener("resize", () => {
722    requestAnimationFrame(handleTableUpperScroll);
723  });
724
725  // =========================================================
726  // 【デバッグ版】13. ハッシュリンクによるアコーディオンの自動展開
727  // =========================================================// =========================================================
728  // 統合版:H2アコーディオン制御 + ハッシュリンク自動展開
729  // =========================================================
730  const accordionTriggers = document.querySelectorAll(
731    "h2.js-accordion-trigger",
732  );
733
734  accordionTriggers.forEach((trigger) => {
735    trigger.setAttribute("tabindex", "0");
736    trigger.setAttribute("role", "button");
737
738    // 初期状態の設定
739    const shouldOpen =
740      trigger.classList.contains("is-open") || accordionTriggers.length === 1;
741    if (!trigger.hasAttribute("aria-expanded")) {
742      trigger.setAttribute("aria-expanded", shouldOpen ? "true" : "false");
743    }
744    if (shouldOpen) {
745      trigger.classList.add("is-active");
746    }
747
748    // トグル関数
749    const toggleAccordion = function (e) {
750      if (e && e.target && e.target.tagName.toLowerCase() === "a") return;
751
752      const isActive = trigger.classList.toggle("is-active");
753      trigger.setAttribute("aria-expanded", isActive ? "true" : "false");
754
755      // 【統合ポイント】もし直後にWP標準アコーディオンがあれば、それも連動させる
756      const nextAccordion = trigger.nextElementSibling?.querySelector(
757        "button.wp-block-accordion-heading__toggle",
758      );
759      if (nextAccordion) {
760        // 標準アコーディオンの状態とH2の状態を同期させる
761        const isNowActive = trigger.classList.contains("is-active");
762        if (
763          nextAccordion.getAttribute("aria-expanded") !== String(isNowActive)
764        ) {
765          nextAccordion.click();
766        }
767      }
768    };
769
770    trigger.addEventListener("click", toggleAccordion);
771    trigger.addEventListener("keydown", (e) => {
772      if (e.key === "Enter" || e.key === " ") {
773        if (e.key === " ") e.preventDefault();
774        toggleAccordion(e);
775      }
776    });
777  });
778
779  // --- ハッシュリンクがある場合に自動で開く処理 ---
780  const handleInitialHash = () => {
781    const hash = window.location.hash;
782    if (!hash) return;
783
784    const target = document.querySelector(hash);
785    if (!target) return;
786
787    // 1. ターゲットがH2(トリガー)ならそれをクリック
788    if (target.classList.contains("js-accordion-trigger")) {
789      setTimeout(() => {
790        target.click(); // 上記のtoggleAccordionが走る
791      }, 100);
792    }
793  };
794
795  handleInitialHash();
796  window.addEventListener("hashchange", handleInitialHash);
797
798  // --- DOMContentLoaded イベントリスナーの終了 ---
799});
800
801// =========================================================
802// 関数定義: テーブルの最初と最後の列にクラスを付与
803// =========================================================
804function applyClassToFirstAndLastColumns(
805  tableElement,
806  firstClassName = "first-col",
807  lastClassName = "last-col",
808) {
809  const rows = tableElement.querySelectorAll("tr");
810
811  // 1. 最大列数を計算する
812  let maxCols = 0;
813  rows.forEach((row) => {
814    const rowLength = row.querySelectorAll("th, td").length;
815    if (rowLength > maxCols) {
816      maxCols = rowLength;
817    }
818  });
819
820  // 2. トラッカーの初期設定
821  const firstTrackers = new Map();
822  const lastTrackers = new Map();
823
824  rows.forEach((row) => {
825    let logicalColIndex = 0;
826    const cells = row.querySelectorAll("th, td");
827
828    // A. First Child Logic
829    while (firstTrackers.get(logicalColIndex) > 0) {
830      firstTrackers.set(
831        logicalColIndex,
832        firstTrackers.get(logicalColIndex) - 1,
833      );
834      logicalColIndex++;
835    }
836
837    cells.forEach((cell) => {
838      while (firstTrackers.get(logicalColIndex) > 0) {
839        firstTrackers.set(
840          logicalColIndex,
841          firstTrackers.get(logicalColIndex) - 1,
842        );
843        logicalColIndex++;
844      }
845
846      if (logicalColIndex === 0) {
847        cell.classList.add(firstClassName);
848      }
849
850      const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
851      const colspan = parseInt(cell.getAttribute("colspan")) || 1;
852
853      if (rowspan > 1) {
854        if (colspan === 1) {
855          firstTrackers.set(logicalColIndex, rowspan - 1);
856        } else {
857          for (let c = 0; c < colspan; c++) {
858            firstTrackers.set(logicalColIndex + c, rowspan - 1);
859          }
860        }
861      }
862      logicalColIndex += colspan;
863    });
864
865    // B. Last Child Logic
866    let lastColIndex = maxCols - 1;
867    while (lastTrackers.get(lastColIndex) > 0) {
868      lastTrackers.set(lastColIndex, lastTrackers.get(lastColIndex) - 1);
869      lastColIndex--;
870    }
871
872    let currentLogicalIndex = maxCols;
873    for (let i = cells.length - 1; i >= 0; i--) {
874      const cell = cells[i];
875      const colspan = parseInt(cell.getAttribute("colspan")) || 1;
876      const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
877
878      currentLogicalIndex -= colspan;
879      lastColIndex = currentLogicalIndex + colspan - 1;
880
881      while (lastTrackers.get(lastColIndex) > 0) {
882        lastTrackers.set(lastColIndex, lastTrackers.get(lastColIndex) - 1);
883        lastColIndex--;
884      }
885
886      if (lastColIndex === maxCols - 1) {
887        cell.classList.add(lastClassName);
888      }
889
890      if (rowspan > 1) {
891        for (let c = 0; c < colspan; c++) {
892          lastTrackers.set(currentLogicalIndex + c, rowspan - 1);
893        }
894      }
895    }
896
897    // C. Clean up
898    const cleanUpTracker = (tracker) => {
899      for (let [key, value] of tracker.entries()) {
900        if (value <= 0) {
901          tracker.delete(key);
902        }
903      }
904    };
905    cleanUpTracker(firstTrackers);
906    cleanUpTracker(lastTrackers);
907  });
908}

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.