知恵袋 (前に戻る)

101新仕様20250828


1. HTML(index.html)

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>検索テスト</title>

<!-- スマホ画面のサイズ調整CSS -->
<link rel="stylesheet" href="responsive.css">
<!-- 上へ・下へボタンCSS -->
<link rel="stylesheet" href="scrollButtons.css">
<!-- 検索ボックスCSS -->
<link rel="stylesheet" href="searchBox.css">
</head>
<body id="top">

<!-- 🔍 検索ボックス -->
<div id="searchBox">
<input type="text" id="searchInput" placeholder="検索語を入力">
<button onclick="searchText()">検索</button>
<button onclick="prevResult()">← 前</button>
<button onclick="nextResult()">次 →</button>
</div><!-- /#searchBox 終わり -->

<!-- 本文(検索範囲) -->
<div id="content">
<p>これはテスト文章です。検索やスクロールのテストをします。</p>
<p>もう一つ段落を入れて、検索結果のハイライトを確認します。</p>
<p>もう一つ段落を入れて、検索結果のハイライトを確認します。</p>
<p>もう一つ段落を入れて、検索結果のハイライトを確認します。</p>
<p>もう一つ段落を入れて、検索結果のハイライトを確認します。</p>
</div><!-- /#content 終わり -->

<!-- 「下へ行く」ボタン -->
<a href="#bottom" id="goToBottomCSS" title="下へ行く">↓</a>

<!-- 「上へ戻る」ボタン -->
<a href="#top" id="backToTopCSS" title="上に戻る">↑</a>

<!-- ジャンプ先を空のアンカーで定義 -->
<a id="bottom"></a>

<!-- 検索スクリプト -->
<script src="search.js" charset="UTF-8"></script>
</body>
</html>



2. CSSファイル
responsive.css(スマホサイズ調整)

@charset "UTF-8";
/* --- 文字折り返し --- */
* {
word-break: break-all;
}

/* --- テーブルと画像を画面幅に収める --- */
table {
max-width: 800px;
width: 100% !important;
margin: 0 auto;
}
img {
max-width: 100% !important;
height: auto !important;
}

/* --- スマホ幅調整(767px以下) --- */
@media screen and (max-width: 767px) {
table {
max-width: 650px;
}
}



2. CSSファイル
scrollButtons.css(上へ・下へボタン)
@charset "UTF-8";
/* --- 上へ戻るボタン --- */
#backToTopCSS {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 99999; /* ← ここを100 → 99999に変更 */
display: block;
background-color: #333; /* --- 丸いボタン・濃い灰色 --- */
color: #fff; /* --- 矢印ボタン・白色 --- */
padding: 10px 15px;
border-radius: 50%;
font-size: 20px;
text-decoration: none;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
#backToTopCSS:hover {
background-color: #555; /* --- 丸いボタン・マウスを乗せた時・薄い灰色 --- */
}

/* --- 下へ行くボタン --- */
#goToBottomCSS {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 99999; /* ← ここを100 → 99999に変更 */
display: block;
background-color: #333;
color: #fff;
padding: 10px 15px;
border-radius: 50%;
font-size: 20px;
text-decoration: none;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
#goToBottomCSS:hover {
background-color: #555;
}



2. CSSファイル
searchBox.css(検索ボックス)
@charset "UTF-8";
/* --- 固定表示検索ボックス --- */
#searchBox {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: #fff;
border-bottom: 1px solid #ccc;
padding: 8px;
box-sizing: border-box;
z-index: 99999; /* ← ここを9999 → 99999に変更 */

/* 横並びデザイン */
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 5px;
justify-content: center;
margin: 10px 0;
}

/* --- 入力欄 --- */
#searchInput {
width: 50%;
min-width: 120px;
padding: 8px 12px;
font-size: 16px;
border: 1px solid #666;
border-radius: 6px;
}

/* --- ボタン --- */
#searchBox button {
padding: 6px 10px;
font-size: 14px;
cursor: pointer;
white-space: nowrap;
}

/* --- 本文との重なり防止 --- */
#content {
margin-top: 70px;
}

/* --- スマホ向け(480px以下) --- */
@media (max-width: 480px) {
#searchInput {
width: 55%;
font-size: 14px;
}
#searchBox button {
font-size: 12px;
padding: 6px 10px;
}
#content {
margin-top: 90px;
}
}



3. JavaScript(search.js)

// =============================
// 🔍 検索機能+ハイライト
// =============================
let results = [];
let currentIndex = 0;

// --- 正規表現のエスケープ ---
function escapeRegExp(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// --- 既存ハイライト解除 ---
function removeHighlights(root) {
root.querySelectorAll('mark').forEach(mark => {
const parent = mark.parentNode;
parent.replaceChild(document.createTextNode(mark.textContent), mark);
parent.normalize();
});
}

// --- テキストノードをハイライト ---
function highlightTextNodes(root, regex) {
const SKIP = /^(script|style|noscript|mark)$/i;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
if (node.parentNode && SKIP.test(node.parentNode.tagName)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});

const nodes = [];
let n;
while ((n = walker.nextNode())) nodes.push(n);

nodes.forEach(node => {
const text = node.nodeValue;
regex.lastIndex = 0;
if (!regex.test(text)) return;

const frag = document.createDocumentFragment();
let last = 0;
let m;
regex.lastIndex = 0;
while ((m = regex.exec(text)) !== null) {
if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index)));
const mark = document.createElement('mark');
mark.textContent = m[0];
frag.appendChild(mark);
last = m.index + m[0].length;
if (m[0].length === 0) { regex.lastIndex++; }
}
if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
node.parentNode.replaceChild(frag, node);
});
}

// --- 検索実行 ---
function searchText() {
const inputEl = document.getElementById('searchInput');
const term = (inputEl ? inputEl.value : '').trim();
const content = document.getElementById('content');
if (!content) return;

removeHighlights(content);
results = [];
currentIndex = 0;

if (!term) {
try { sessionStorage.removeItem('lastSearch'); } catch (e) {}
return;
}

const regex = new RegExp(escapeRegExp(term), 'gi');
highlightTextNodes(content, regex);

results = Array.from(content.querySelectorAll('mark'));
if (results.length > 0) {
results[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
results[0].style.backgroundColor = 'yellow';
}

try { sessionStorage.setItem('lastSearch', term); } catch (e) {}
}

// --- 次の検索結果 ---
function nextResult() {
if (results.length === 0) return;
results[currentIndex].style.backgroundColor = 'orange';
currentIndex = (currentIndex + 1) % results.length;
results[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
results[currentIndex].style.backgroundColor = 'yellow';
}

// --- 前の検索結果 ---
function prevResult() {
if (results.length === 0) return;
results[currentIndex].style.backgroundColor = 'orange';
currentIndex = (currentIndex - 1 + results.length) % results.length;
results[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
results[currentIndex].style.backgroundColor = 'yellow';
}

// --- ページ読み込み時 ---
document.addEventListener('DOMContentLoaded', () => {
const inputEl = document.getElementById('searchInput');
try {
const last = sessionStorage.getItem('lastSearch') || '';
if (inputEl) inputEl.value = last;
if (last) searchText();
} catch (e) {}

if (inputEl) {
inputEl.addEventListener('keydown', e => {
if (e.key === 'Enter') searchText();
});
}

// 検索ボックス高さに応じて本文margin調整
const searchBox = document.getElementById("searchBox");
const content = document.getElementById("content");
if (searchBox && content) {
const h = searchBox.offsetHeight;
content.style.marginTop = h + "px";
}
});



<script> タグは基本的に <body> の一番下(</body> の直前)に置くのが一般的です。
(理由:JS読み込みが完了するまでHTMLの表示が止まるのを防ぐため)



これ以下は、旧仕様


 ホームページ
サイト内(ページ内)検索HTML作成手順

①search-demo.htmlをルートに設置(テスト用なので不要・削除してもOK)
内容は yoko8zに有る。
このページの下段にも記述あり


②目的ページの</HEAD>の直前に

<style>
<!--
/* 固定表示検索ボックス */
#searchBox{
position: fixed; /* 画面上段に固定 */
top: 0;
left: 0;
width: 100%;
background-color: #fff;
border-bottom: 1px solid #ccc;
padding: 8px;
box-sizing: border-box;
z-index: 9999;
}

/* 入力欄 */
#searchInput{
padding: 8px 12px;
font-size: 16px;
width: 65%; /* 入力欄の幅調整 */
border: 1px solid #666;
border-radius: 6px;
}

/* ボタン */
#searchBox button{
padding: 8px 12px;
font-size: 14px;
margin-left: 4px;
cursor: pointer;
}

/* 本文との重なり防止 */
#content{
margin-top: 70px; /* 検索ボックスの高さに合わせる */
}

/* スマホ向けレスポンシブ調整 */
@media (max-width: 480px) {
#searchInput{
width: 55%;
font-size: 14px;
}
#searchBox button{
font-size: 12px;
padding: 6px 10px;
}
#content{
margin-top: 90px;
}
}
-->
</style>


<style>
<!--
/* 🔍 検索ボックス用スタイル */
#searchBox{
display: flex; /* 横並びにする */
flex-wrap: nowrap; /* 折り返さない */
align-items: center; /* ボタンを縦位置中央に */
gap: 5px; /* ボタンとの隙間 */
justify-content: center; /* 中央寄せ */
margin: 10px 0;
}

#searchBox input[type="text"]{
width: 50%; /* 入力欄を半分くらいに */
min-width: 120px; /* スマホでも潰れすぎないように下限幅 */
padding: 5px;
font-size: 14px;
}

#searchBox button{
padding: 5px 10px;
font-size: 14px;
white-space: nowrap; /* ボタン内の文字を折り返さない */
}
-->
</style>


を記述
内容はhttp://hamada777.g1.xrea.com/index2.htmlに有る



③<body>の下(<table>の上)に下記を記述

<!-- 🔍 固定表示検索ボックス -->
<div id="searchBox">
<input type="text" id="searchInput" placeholder="検索ワードを入力">
<button onclick="searchText()">検索</button>
<button onclick="nextResult()">次へ</button>
<button onclick="prevResult()">前へ</button>
</div>

<!-- ★検索対象を div#content で囲む -->
<div id="content">

このあと検索対象の最後を

</div>
<!-- ★ここまで -->

で閉じる。注意!は
囲んだdivと/divの間に別のdivと/divが入って
検索できなくなることがある。
間に別のdivと/divを入れないように!




④</body>の直前に下記scriptを2つ記述
(ホームページビルダーのプレビューでスクリプトエラーになっても問題ない)

<script>
// --- 安全ハイライト20250824改良版(属性は一切さわらない / リンク壊れません) ---
let results = [];
let currentIndex = 0;

function escapeRegExp(s){ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }

function removeHighlights(root){
root.querySelectorAll('mark').forEach(mark=>{
const parent = mark.parentNode;
parent.replaceChild(document.createTextNode(mark.textContent), mark);
parent.normalize();
});
}

function highlightTextNodes(root, regex){
const SKIP = /^(script|style|noscript|mark)$/i;

// 置換でツリーが変わると困るので、先に対象テキストノードを配列に退避
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node){
if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
if (node.parentNode && SKIP.test(node.parentNode.tagName)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
const nodes = [];
let n;
while ((n = walker.nextNode())) nodes.push(n);

nodes.forEach(node=>{
const text = node.nodeValue;
regex.lastIndex = 0;
if (!regex.test(text)) return;

const frag = document.createDocumentFragment();
let last = 0;
regex.lastIndex = 0;
let m;
while ((m = regex.exec(text)) !== null){
if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index)));
const mark = document.createElement('mark');
mark.textContent = m[0];
frag.appendChild(mark);
last = m.index + m[0].length;
if (m[0].length === 0) { regex.lastIndex++; } // 念のため無限ループ防止
}
if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
node.parentNode.replaceChild(frag, node);
});
}

function searchText(){
const inputEl = document.getElementById('searchInput');
const term = (inputEl ? inputEl.value : '').trim();
const content = document.getElementById('content');
if (!content) return;

// 既存ハイライト解除
removeHighlights(content);

results = [];
currentIndex = 0;

if (!term){
try{ sessionStorage.removeItem('lastSearch'); }catch(e){}
return;
}

// 正規表現は「リテラル検索」になるようエスケープ(部分一致・大小無視)
const regex = new RegExp(escapeRegExp(term), 'gi');

// テキストノードだけを置換(属性やタグ文字列は触らない)
highlightTextNodes(content, regex);

// 見つかった <mark> を収集して先頭へスクロール
results = Array.from(content.querySelectorAll('mark'));
if (results.length > 0){
results[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
results[0].style.backgroundColor = 'yellow';
}

// 次ページでも同じ語で自動検索するため保存
try{ sessionStorage.setItem('lastSearch', term); }catch(e){}
}

function nextResult(){
if (results.length === 0) return;
results[currentIndex].style.backgroundColor = 'orange';
currentIndex = (currentIndex + 1) % results.length;
results[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
results[currentIndex].style.backgroundColor = 'yellow';
}

function prevResult(){
if (results.length === 0) return;
results[currentIndex].style.backgroundColor = 'orange';
currentIndex = (currentIndex - 1 + results.length) % results.length;
results[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
results[currentIndex].style.backgroundColor = 'yellow';
}

// ページ読み込み時:前回語があれば自動再検索(要:各ページにこのスクリプト設置)
document.addEventListener('DOMContentLoaded', ()=>{
const inputEl = document.getElementById('searchInput');
try{
const last = sessionStorage.getItem('lastSearch') || '';
if (inputEl) inputEl.value = last;
if (last) searchText();
}catch(e){}
// Enter でも検索できるように(ボタンも従来どおり使えます)
if (inputEl){
inputEl.addEventListener('keydown', e=>{
if (e.key === 'Enter') searchText();
});
}
});
</script>


<script>
// 検索ボックスの高さを自動計算させる
window.addEventListener("load", function() {
var searchBox = document.getElementById("searchBox");
var content = document.getElementById("content");
if (searchBox && content) {
var h = searchBox.offsetHeight;
content.style.marginTop = h + "px";
}
});
</script>


このscriptは最初の版でバグあり
検索窓でヒットした項目をクリックすると
<mark>文字がURLに組み込まれて「ページが無い」のエラーになる

<script>
let results = []; // 検索ヒットした要素を保存バグ修正前の版
let currentIndex = 0; // 現在の位置

function searchText() {
const input = document.getElementById("searchInput").value.trim();
const content = document.getElementById("content");

// 前回のハイライトを解除
content.querySelectorAll("mark").forEach(mark => {
const parent = mark.parentNode;
parent.replaceChild(document.createTextNode(mark.textContent), mark);
parent.normalize();
});

results = [];
currentIndex = 0;

if (input === "") return;

// 正規表現で検索語をハイライト
const regex = new RegExp(input, "gi");
content.innerHTML = content.innerHTML.replace(regex, match => `<mark>${match}</mark>`);

results = Array.from(content.querySelectorAll("mark"));

if (results.length > 0) {
results[0].scrollIntoView({ behavior: "smooth", block: "center" });
results[0].style.backgroundColor = "yellow";
}
}

function nextResult() {
if (results.length === 0) return;
results[currentIndex].style.backgroundColor = "orange";
currentIndex = (currentIndex + 1) % results.length;
results[currentIndex].scrollIntoView({ behavior: "smooth", block: "center" });
results[currentIndex].style.backgroundColor = "yellow";
}

function prevResult() {
if (results.length === 0) return;
results[currentIndex].style.backgroundColor = "orange";
currentIndex = (currentIndex - 1 + results.length) % results.length;
results[currentIndex].scrollIntoView({ behavior: "smooth", block: "center" });
results[currentIndex].style.backgroundColor = "yellow";
}
</script>




①search-demo.html(参考保存のみ)

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>ページ内テキスト検索デモ(HTML+JSのみ)</title>
<style>
:root { --accent: #0d6efd; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; line-height: 1.7; margin: 0; }
header { position: sticky; top: 0; background: #fff; border-bottom: 1px solid #e5e7eb; z-index: 10; }
.container { max-width: 960px; margin: 0 auto; padding: 16px; }
.toolbar { display: grid; grid-template-columns: 1fr auto auto auto auto; gap: 8px; align-items: center; }
.toolbar input[type="text"] { padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 10px; }
.toolbar button, .toolbar label { padding: 10px 12px; border-radius: 10px; border: 1px solid #cbd5e1; background: #f8fafc; cursor: pointer; }
.toolbar button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.toolbar .count { font-variant-numeric: tabular-nums; color: #334155; padding: 0 8px; }
mark.find-hit { background: #fff59d; padding: 0 2px; border-radius: 3px; }
mark.find-current { background: #ffd54f; outline: 2px solid #ffb300; }
.content { padding: 24px 16px 64px; }
.hint { color: #64748b; font-size: 14px; }
.kbd { padding: 1px 6px; border: 1px solid #cbd5e1; border-radius: 6px; background: #fff; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
</style>
</head>
<body>
<header>
<div class="container">
<div class="toolbar" role="search">
<input id="q" type="text" placeholder="このページ内を検索… (例: フェーズドアレイ)" autocomplete="off" />
<button id="btnFind" class="primary" title="検索 (Enter)">検索</button>
<button id="btnPrev" title="前の一致 (Shift+Enter)">前へ</button>
<button id="btnNext" title="次の一致 (Enter)">次へ</button>
<button id="btnClear" title="結果を解除 (Esc)">解除</button>
<div class="count" id="counter" aria-live="polite"></div>
</div>
<div class="hint">Enter=検索/次へ、Shift+Enter=前へ、Esc=解除。 大文字小文字 <label><input type="checkbox" id="optCase" /> 区別する</label>・<label><input type="checkbox" id="optWord" /> 単語全体一致</label></div>
</div>
</header>

<main class="container content">
<!-- ★★ 検索対象はこの#content内だけです。自分のページでは本文をこのdivに入れてください。 -->
<article id="content">
<h1>ページ内テキスト検索デモ</h1>
<p>このサンプルは、サーバー側のプログラムなし(FTPアップロードのみ)で<strong>ページ内のテキストを検索</strong>し、
一致箇所へ自動スクロール&ハイライトします。日本語もOKです。</p>
<p>主なポイント:
<ul>
<li>検索語の全一致を順送り/逆送り(次へ/前へ)</li>
<li>大文字小文字の区別・単語全体一致の切替</li>
<li>Escで解除して元の本文に戻す(元HTMLを一時保存)</li>
<li>検索バーはページ上部に固定(<code>position: sticky</code>)</li>
</ul>
</p>
<h2>ダミー本文</h2>
<p>たとえば「アンテナ」「フェーズドアレイ」「Excel」などで試してみてください。検索結果は
<mark>強調表示</mark>され、現在位置はさらに濃い色で表示されます。</p>
<p>段落が長くてもOKです。複数の一致がある場合、Enterで次の一致へ自動スクロールします。</p>
<p>英語: phased array antenna / array factor / beam steering / FFT.
日本語: 位相制御 / 素子間隔 / 指向性。
</p>
</article>
</main>

<script>
(function(){
const $ = (sel, ctx=document) => ctx.querySelector(sel);
const $$ = (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel));

const box = $('#q');
const btnFind = $('#btnFind');
const btnNext = $('#btnNext');
const btnPrev = $('#btnPrev');
const btnClear = $('#btnClear');
const counter = $('#counter');
const optCase = $('#optCase');
const optWord = $('#optWord');
const content = $('#content');

let hits = []; // Array<HTMLElement mark.find-hit>
let pos = -1; // 現在の一致位置インデックス
let originalHTML = null; // 元のHTML(解除用)

function escapeRegExp(s){ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }

function buildRegex(query){
if(!query) return null;
const flags = optCase.checked ? 'g' : 'gi';
const src = optWord.checked ? `\\b${escapeRegExp(query)}\\b` : escapeRegExp(query);
try { return new RegExp(src, flags); } catch(e){ return null; }
}

function clearHighlights(){
if(originalHTML != null){
content.innerHTML = originalHTML;
} else {
// 念のため:markを外す(初回前はmark存在しない)
$$('.find-hit', content).forEach(m => {
const t = document.createTextNode(m.textContent);
m.replaceWith(t);
});
}
hits = [];
pos = -1;
originalHTML = null;
updateCounter();
}

function updateCounter(){
if(hits.length === 0){ counter.textContent = ''; return; }
counter.textContent = `${pos+1} / ${hits.length}`;
}

function scrollToCurrent(){
if(pos < 0 || pos >= hits.length) return;
hits.forEach(h => h.classList.remove('find-current'));
const el = hits[pos];
el.classList.add('find-current');
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}

function doFind(direction = 0){
const q = box.value.trim();
if(!q){ clearHighlights(); return; }

// 既存の結果が検索語・オプションと一致しているか簡易チェック
const sameQuery = content.dataset._lastQuery === q &&
content.dataset._lastCase === String(optCase.checked) &&
content.dataset._lastWord === String(optWord.checked);

if(!sameQuery){
// 初回または条件変更:ハイライトを作り直す
clearHighlights();
originalHTML = content.innerHTML; // 元を保存(解除で復元)

const regex = buildRegex(q);
if(!regex){ return; }

// テキストノードを走査してmark化
const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, {
acceptNode(node){
// 目に見えるテキストのみ(空白や改行のみは除外)
return /\S/.test(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
});

const textNodes = [];
while(walker.nextNode()) textNodes.push(walker.currentNode);

textNodes.forEach(node => {
const parent = node.parentNode;
let html = node.nodeValue;
if(!regex.test(html)) return; // 該当なし
// global RegExp は状態を持つので都度リセット
regex.lastIndex = 0;
const parts = [];
let lastIdx = 0;
let m;
while((m = regex.exec(html))){
const before = html.slice(lastIdx, m.index);
const hit = m[0];
parts.push(document.createTextNode(before));
const mark = document.createElement('mark');
mark.className = 'find-hit';
mark.textContent = hit;
parts.push(mark);
hits.push(mark);
lastIdx = m.index + hit.length;
}
parts.push(document.createTextNode(html.slice(lastIdx)));
const frag = document.createDocumentFragment();
parts.forEach(p => frag.appendChild(p));
parent.replaceChild(frag, node);
});

content.dataset._lastQuery = q;
content.dataset._lastCase = String(optCase.checked);
content.dataset._lastWord = String(optWord.checked);

if(hits.length === 0){
updateCounter();
return;
}
pos = 0; // 最初の一致へ
updateCounter();
scrollToCurrent();
return;
}

// 既存の結果で移動
if(hits.length === 0){ updateCounter(); return; }
if(direction === 0){ // Enter=次へ
pos = (pos + 1) % hits.length;
} else if(direction < 0){ // 前へ
pos = (pos - 1 + hits.length) % hits.length;
} else { // 明示的に次へ
pos = (pos + 1) % hits.length;
}
updateCounter();
scrollToCurrent();
}

// ---- イベント
btnFind.addEventListener('click', () => doFind(+1));
btnNext.addEventListener('click', () => doFind(+1));
btnPrev.addEventListener('click', () => doFind(-1));
btnClear.addEventListener('click', clearHighlights);

box.addEventListener('keydown', (e) => {
if(e.key === 'Enter'){
e.preventDefault();
doFind(e.shiftKey ? -1 : +1);
} else if(e.key === 'Escape'){
clearHighlights();
}
});

// ページ全体のショートカット(/ でフォーカス)
document.addEventListener('keydown', (e) => {
if(e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey){
e.preventDefault();
box.focus();
box.select();
}
});
})();
</script>
</body>
</html>

(前に戻る)

chie2.html(tvm)
<EOF>