<body>
<div id="search-container" style="display: none;">
<div class="search-input-group">
<div class="input-and-button-wrapper">
<input type="text" id="partial-search-input" placeholder="あいまいキーワードを入力">
<button onclick="searchCSV('partial')">あいまい検索</button>
</div>
</div>
<div class="search-input-group">
<div class="input-and-button-wrapper">
<input type="text" id="exact-search-input" placeholder="完全一致キーワードを入力">
<button onclick="searchCSV('exact')">完全一致検索</button>
</div>
</div>
</div>
<div id="csv-output">
</div>
<div id="pagination" style="display: none;">
<button onclick="prevPage()">前へ</button>
<span id="page-info"></span>
<button onclick="nextPage()">次へ</button>
</div>
<style>
/* ここから追加・修正するCSS */
/* 1. ウェブフォントの定義 */
/* PMingLiU-ExtBフォントをウェブフォントとして読み込む設定です。 */
/* PMingLiU-ExtB.ttf ファイルをウェブサーバーの適切な場所に配置してください。 */
/* 例: /wp-content/themes/cocoon-child-master/fonts/PMingLiU-ExtB.ttf */
/* 可能であれば、ファイルサイズが小さい .woff2 や .woff 形式も用意すると、読み込みが高速になります。 */
/* 例えば、フォントコンバーター (例: Transfonter, Font Squirrel の Webfont Generator) を利用して変換できます。 */
@font-face {
font-family: 'PMingLiU-ExtB-02'; /* このフォント名でCSSから参照します */
src: url('/wp-content/themes/cocoon-child-master/fonts/PMingLiU-ExtB-02.woff2') format('woff2'),
url('/wp-content/themes/cocoon-child-master/fonts/PMingLiU-ExtB-02.woff') format('woff'),
url('/wp-content/themes/cocoon-child-master/fonts/PMingLiU-ExtB-02.ttf') format('truetype');
font-weight: normal;
font-style: normal;
/* font-display: swap; を追加すると、フォントが読み込まれるまでシステムのフォントを表示します */
font-display: swap;
}
/* 2. 漢字(喃字)表記の列にフォントを適用 */
/* displayOrder配列のインデックス2 (つまり表示上3番目の列) が「漢字(喃字)表記」なので、 */
/* :nth-child(3) を使ってその列のセルにフォントを適用します。 */
body .csv-output table td:nth-child(3) {
font-family: 'PMingLiU-ExtB-02', 'NomNaTong-Regular', 'Noto Sans CJK JP', sans-serif !important;
}
/* 複数の代替フォントを指定することで、 */
/* PMingLiU-ExtBが利用できない場合でも、 */
/* 次善の表示を試みます。 */
/* Noto Serif CJK JP や Noto Sans CJK JP は、Google Fontsから利用できる高機能なフォントです。 */
/* 必要に応じてシステムの日本語フォント(例: 'Hiragino Kaku Gothic ProN', 'Meiryo')なども追加できます。 */
/* 既存のCSSはそのまま */
/* 検索バーの長さを固定するCSS */
#partial-search-input,
#exact-search-input {
max-width: 50ch;
width: 100%;
box-sizing: border-box;
margin-right: 10px;
height: 3vh;
}
/* 検索ボタンの縦の長さを7割に設定 */
.search-input-group button {
height: 3vh;
padding: 0 15px;
white-space: nowrap;
flex-shrink: 0;
}
/* 検索インプットの親要素の幅も調整すると、全体がスッキリします */
.search-input-group {
margin-bottom: 10px;
}
/* 新しいラッパー要素のスタイル */
.input-and-button-wrapper {
display: flex;
align-items: center;
max-width: calc(50ch + 100px);
}
/* 音声ボタンとセル内の文字の間のスペースを確保するためのスタイル */
.csv-output table td div {
display: flex;
align-items: center;
gap: 10px;
}
/* セル内のテキスト表示を1行にし、必要に応じてスクロールバーを表示する */
.csv-output table td span {
display: block;
white-space: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
flex-grow: 1;
scrollbar-width: thin;
scrollbar-color: grey lightgrey;
}
/* Webkit系ブラウザ(Chrome, Safari)でのスクロールバーのスタイル */
.csv-output table td span::-webkit-scrollbar {
height: 5px;
}
.csv-output table td span::-webkit-scrollbar-track {
background: #f1f1f1;
}
.csv-output table td span::-webkit-scrollbar-thumb {
background: #888;
border-radius: 5px;
}
.csv-output table td span::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* ハイライト用のスタイル */
.highlight {
background-color: yellow;
font-weight: bold;
}
/* 新しい音声ボタンのスタイル */
.speak-button {
background-color: #4CAF50;
color: white;
border: none;
padding: 5px 8px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 0;
cursor: pointer;
border-radius: 4px;
line-height: 1;
flex-shrink: 0;
}
.speak-button:hover {
background-color: #45a049;
}
/* 音声列のtdの幅を調整 (既存の音声列) */
.csv-output table td:nth-child(4) {
width: 40px;
text-align: center;
}
/* 音声2列のtdの幅を調整 (新しい音声2列) */
.csv-output table td:nth-child(8) {
width: 40px;
text-align: center;
}
/* ベトナム語表記の列の幅を調整 */
.csv-output table td:nth-child(2) {
min-width: 150px;
}
</style>
<script>
console.log("スクリプトが読み込まれました。");
let csvData = [];
let filteredData = [];
let currentPage = 1;
const rowsPerPage = 10;
const searchContainer = document.getElementById('search-container');
const partialSearchInput = document.getElementById('partial-search-input');
const exactSearchInput = document.getElementById('exact-search-input');
const csvOutputDiv = document.getElementById('csv-output');
const paginationDiv = document.getElementById('pagination');
const pageInfoSpan = document.getElementById('page-info');
// 無声調アルファベットの列(インデックス2)をヘッダーから除外
// 変更点1: headers配列に「音声」と「音声2」を追加し、表示順序を調整
const headers = ["漢字語/固有語/混合語", "ベトナム語表記", "漢字(喃字)表記", "音声", "対義語", "日本語", "ひらがな/アルファベット", "音声2", "類語/ジャンル"];
const voicelessAlphabetColumnIndex = 2; // 無声調アルファベット列の元のインデックス
let totalRowCount = 0;
const vietnameseColumnIndex = 1; // 元のCSVにおけるベトナム語表記の列インデックス
const hiraganaAlphabetColumnIndex = 6; // 元のCSVにおけるひらがな/アルファベットの列インデックス
let speechUtterance = null;
let speechUtterance2 = null; // 新しい音声合成インスタンス
let currentSearchType = ''; // 現在の検索タイプを保持する変数
if ('speechSynthesis' in window) {
console.log("音声合成APIが利用可能です。");
speechUtterance = new SpeechSynthesisUtterance();
speechUtterance.lang = 'vi-VN';
speechUtterance2 = new SpeechSynthesisUtterance(); // 音声2用のインスタンス
speechUtterance2.lang = 'ja-JP'; // 日本語を設定
window.speechSynthesis.onvoiceschanged = function () {
console.log("音声リストが変更されました。");
const voices = window.speechSynthesis.getVoices();
let vietnameseVoice = null;
let japaneseVoice = null; // 日本語の音声
for (let i = 0; i < voices.length; i++) {
if (voices[i].lang === 'vi-VN') {
if (voices[i].name.toLowerCase().includes('female')) {
vietnameseVoice = voices[i];
break;
}
} else if (voices[i].lang === 'vi-VN') {
vietnameseVoice = voices[i];
}
if (voices[i].lang === 'ja-JP') { // 日本語の音声を検索
if (voices[i].name.toLowerCase().includes('female')) { // 女性の声を優先
japaneseVoice = voices[i];
// 女性の声が見つかったら、より自然な声が見つかるまで検索を続けるか、ここでbreakしてもよい
} else if (!japaneseVoice) { // まだ日本語音声が見つかっていない場合、最初の日本語音声を候補にする
japaneseVoice = voices[i];
}
}
}
if (vietnameseVoice) {
speechUtterance.voice = vietnameseVoice;
} else {
console.log('ベトナム語の女性の声が見つかりませんでした。');
}
if (japaneseVoice) {
speechUtterance2.voice = japaneseVoice;
} else {
console.log('日本語の声が見つかりませんでした。');
}
};
} else {
console.log("音声合成APIは利用できません。");
alert('このブラウザは音声読み上げに対応していません。');
}
// CSVファイルを自動的に読み込む
const csvFileUrl = '/wp-content/themes/cocoon-child-master/read_csv.php'; // あなたのCSVファイルの正確なURLに置き換えてください
fetch(csvFileUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status} - Could not load CSV from: ${csvFileUrl}`);
}
return response.text();
})
.then(data => {
console.log("CSVファイルが自動的に読み込まれました。");
const lines = data.trim().split('\n');
console.log("読み込まれた行数:", lines.length);
if (lines.length > 0) {
console.log("CSVデータが存在します。");
csvData = lines.map(line => parseCSVLine(line));
console.log("CSVデータがパースされました。最初の行:", csvData[0]);
searchContainer.style.display = 'block';
console.log("検索コンテナが表示されました。");
filteredData = csvData.slice(1); // ヘッダー行を除外
currentPage = 1;
totalRowCount = csvData.length - 1; // ヘッダー行を除いた実際のデータ行数
displayCSV(filteredData, '', ''); // 初期表示はハイライトなし
console.log("displayCSV関数が呼び出されました。");
updatePagination();
} else {
console.log("CSVファイルが空です。");
alert('CSVファイルが空です。');
searchContainer.style.display = 'none';
csvOutputDiv.innerHTML = '';
paginationDiv.style.display = 'none';
csvData = [];
filteredData = [];
currentPage = 1;
totalRowCount = 0;
displayCSV([], '', '');
}
})
.catch(error => {
console.error("CSVファイルの読み込みエラー:", error);
alert(`CSVファイルの読み込みに失敗しました。ファイルが正しい場所にあるか、またはアクセス可能か確認してください: ${csvFileUrl}`);
searchContainer.style.display = 'none';
csvOutputDiv.innerHTML = '';
paginationDiv.style.display = 'none';
});
partialSearchInput.addEventListener('keypress', function (event) {
if (event.key === 'Enter') {
searchCSV('partial');
}
});
exactSearchInput.addEventListener('keypress', function (event) {
if (event.key === 'Enter') {
searchCSV('exact');
}
});
function parseCSVLine(line) {
const values = [];
let currentValue = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',') {
if (!inQuotes) {
values.push(currentValue.trim());
currentValue = '';
} else {
currentValue += char;
}
} else {
currentValue += char;
}
}
values.push(currentValue.trim());
return values;
}
function speakVietnamese(text) {
if (speechUtterance) {
if (window.speechSynthesis) {
speechUtterance.text = text;
speechUtterance.rate = 0.6;
window.speechSynthesis.speak(speechUtterance);
}
}
}
function speakJapanese(text) {
if (speechUtterance2) {
if (window.speechSynthesis) {
speechUtterance2.text = text;
speechUtterance2.rate = 1.0; // 日本語は通常の速度で
window.speechSynthesis.speak(speechUtterance2);
}
}
}
function displayCSV(data, partialSearchTerm, exactSearchTerm) {
console.log("displayCSVが開始されました。表示するデータ件数:", data.length);
const startIndex = (currentPage - 1) * rowsPerPage;
const endIndex = startIndex + rowsPerPage;
const currentPageData = data.slice(startIndex, endIndex);
const currentFilteredRowCount = filteredData.length; // 検索結果の総件数
const table = document.createElement('table');
table.id = 'csv-table';
console.log("テーブル要素が作成されました:", table);
const thead = table.createTHead();
const headerRow = thead.insertRow();
// 変更点2: ヘッダーをループして表示
headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header;
headerRow.appendChild(th);
});
const tbody = table.createTBody();
currentPageData.forEach(row => {
const dataRow = tbody.insertRow();
// 表示する列のインデックスと元のCSVの列インデックスのマッピング
// 元のCSVの列: 0:漢字語/固有語/混合語, 1:ベトナム語表記, 2:無声調..., 3:漢字(喃字)表記, 4:対義語..., 5:日本語, 6:ひらがな/アルファベット, 7:類語/ジャンル
// 表示順序: 0, 1, 3(漢字...), '音声', 4(対義語...), 5(日本語), 6(ひらがな), '音声2', 7(類語)
const displayOrder = [0, 1, 3, '音声', 4, 5, 6, '音声2', 7];
displayOrder.forEach((displayColumn, displayIdx) => {
const td = document.createElement('td');
let cellContent = '';
let originalCellData = ''; // 音声読み上げのために元のセルデータを保持
if (displayColumn === '音声') {
// 変更点3: 「音声」列に音声ボタンを追加
if ('speechSynthesis' in window) {
const speakButton = document.createElement('button');
speakButton.classList.add('speak-button');
speakButton.textContent = '🔊';
// ベトナム語表記の列の元のデータ (元のCSVのインデックス1) を取得
const vietnameseTextForSpeech = row[vietnameseColumnIndex] || '';
speakButton.onclick = function () {
speakVietnamese(vietnameseTextForSpeech); // 元のテキストを読み上げる
};
td.appendChild(speakButton);
}
} else if (displayColumn === '音声2') { // 新しく追加した「音声2」列
if ('speechSynthesis' in window) {
const speakButton2 = document.createElement('button');
speakButton2.classList.add('speak-button');
speakButton2.textContent = '🔊';
// ひらがな/アルファベットの列の元のデータ (元のCSVのインデックス6) を取得
const hiraganaAlphabetTextForSpeech = row[hiraganaAlphabetColumnIndex] || '';
speakButton2.onclick = function () {
speakJapanese(hiraganaAlphabetTextForSpeech); // 日本語を読み上げる
};
td.appendChild(speakButton2);
}
} else {
const originalIndex = displayColumn;
if (originalIndex >= row.length) {
// CSVのデータが不足している場合
cellContent = '';
originalCellData = '';
} else {
cellContent = row[originalIndex];
originalCellData = row[originalIndex]; // 元のデータを保持
}
let termToHighlight = '';
// どの検索タイプで検索が行われたかに応じてハイライトするキーワードを設定
if (currentSearchType === 'partial' && partialSearchTerm) {
termToHighlight = partialSearchTerm;
} else if (currentSearchType === 'exact' && exactSearchTerm) {
termToHighlight = exactSearchTerm;
}
if (termToHighlight) { // 検索キーワードが存在する場合のみハイライトを適用
cellContent = highlightText(cellContent, termToHighlight);
}
const cellText = document.createElement('span');
cellText.innerHTML = cellContent; // innerHTMLでハイライトを適用
// ベトナム語表記の列にのみテキストと音声ボタンをdivで囲む
if (displayColumn === vietnameseColumnIndex) {
const container = document.createElement('div');
container.appendChild(cellText); // ハイライト済みのテキストを追加
td.appendChild(container);
} else {
td.appendChild(cellText);
}
}
dataRow.appendChild(td);
});
});
const tfoot = table.createTFoot();
const footerRow = tfoot.insertRow();
const footerCell = footerRow.insertCell();
footerCell.colSpan = headers.length; // 表示されているヘッダーの数にcolSpanを合わせる
footerCell.style.textAlign = 'left';
footerCell.innerHTML = `総行数: ${totalRowCount} 行 <span style="margin-left: 20px;">該当行数: ${currentFilteredRowCount} 行</span>`;
csvOutputDiv.innerHTML = '';
csvOutputDiv.appendChild(table);
console.log("テーブルがcsvOutputDivに追加されました。");
}
// highlightText関数は変更なし
function highlightText(text, term) {
if (!term) {
return text;
}
// termを正規表現でエスケープ
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(escapedTerm, 'gi');
return text.replace(regex, '<span class="highlight">$&</span>');
}
function searchCSV(searchTypeParam) {
currentSearchType = searchTypeParam; // 現在の検索タイプを保存
let partialTerm = partialSearchInput.value.toLowerCase().trim();
let exactTerm = exactSearchInput.value.toLowerCase().trim();
filteredData = csvData.slice(1).filter(row => {
let partialMatch = false;
let exactMatch = false;
// 検索対象の列を定義 (元のCSVの列インデックスに基づきます)
const searchableColumns = [0, 1, 2, 3, 4, 5, 6, 7];
if (partialTerm !== "") {
partialMatch = searchableColumns.some(colIndex => {
return row[colIndex] && row[colIndex].toLowerCase().includes(partialTerm);
});
} else {
partialMatch = true;
}
if (exactTerm !== "") {
exactMatch = searchableColumns.some(colIndex => {
return row[colIndex] && row[colIndex].toLowerCase() === exactTerm;
});
} else {
exactMatch = true;
}
if (searchTypeParam === 'partial') {
return partialMatch;
} else if (searchTypeParam === 'exact') {
return exactMatch;
}
return false;
});
currentPage = 1;
displayCSV(filteredData, partialTerm, exactTerm);
updatePagination();
}
function updatePagination() {
const totalPages = Math.ceil(filteredData.length / rowsPerPage);
if (totalPages > 1) {
paginationDiv.style.display = 'block';
pageInfoSpan.textContent = `${currentPage} / ${totalPages} ページ`;
} else {
paginationDiv.style.display = 'none';
}
}
function nextPage() {
const totalPages = Math.ceil(filteredData.length / rowsPerPage);
if (currentPage < totalPages) {
currentPage++;
let partialTerm = partialSearchInput.value.toLowerCase();
let exactTerm = exactSearchInput.value.toLowerCase();
displayCSV(filteredData, partialTerm, exactTerm);
updatePagination();
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
let partialTerm = partialSearchInput.value.toLowerCase();
let exactTerm = exactSearchInput.value.toLowerCase();
displayCSV(filteredData, partialTerm, exactTerm);
updatePagination();
}
}
function updateTotalRowCount(count) {
totalRowCount = count;
}
</script>
</body>
コメント