(あいさつはコピペで失礼します)
ネオページで小説を書いている皆さんこんにちは。
ふりがな付きテキストエディターを作りました。
オフラインでふりがな付きの文章を確認したいときに使えます。
文字数カウントも付けました。ネオページのカウントとほぼ近い文字数でカウントされます。(同じにするのが難しい……)
【主な機能】
○青空文庫形式のふりがな指定に対応
○正確な文字数カウント
・漢字、ひらがな、カタカナ、記号を分類表示
・ふりがな指定記号は文字数に含まれません
・空白・改行も除外した正確なカウント
○使いやすいプレビュー機能
・リアルタイムでふりがな付き表示
・固定式/スクロール式の表示切り替え
・美しいレイアウトで読みやすい
【使い方】
1. 下のHTMLコードを全てコピー
2. メモ帳などのテキストエディターに貼り付け
3. 「○○.html」という名前で保存(○○は任意の名前)
4. 保存したファイルをダブルクリックでブラウザが開きます
【注意事項】
・PC専用です(スマートフォンでは正常に動作しません)
・インターネット接続不要でオフラインで使用可能
・Chrome、Firefox、Edgeなどの主要ブラウザで動作確認済み
【こんな方におすすめ】
・小説にふりがなを付けた状態を確認したい方
・正確な文字数を知りたい方
・美しいレイアウトでプレビューしたい方
・青空文庫形式にも対応したい方
ぜひ小説執筆にお役立てください。
バグなどのご報告もコメントをいただければ検討いたします。(できないことも多々あります)
Ver.4.0における改善点
・保存機能を強化して上書きボタンも設置しました。
・保存と読み込みのボタンがスクロールに追従するようにしました。
------------------------------------------------------
(ここからコピーします)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ふりがな付きテキストエディター(保存機能強化版)</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', Meiryo, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 300;
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.container {
display: flex;
min-height: 600px;
margin: 20px;
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.panel {
flex: 1 1 0;
padding: 30px;
overflow: auto;
background: #fff;
min-width: 180px;
position: relative;
}
.input-panel {
background: #f8f9fa;
}
.resizer {
width: 20px;
background: repeating-linear-gradient(
135deg,
#667eea 0 5px,
#fff 5px 10px
);
cursor: col-resize;
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 10;
transition: background 0.3s;
user-select: none;
}
.resizer:hover {
background: repeating-linear-gradient(
135deg,
#ff6600 0 5px,
#fff 5px 10px
);
}
.resizer-grip {
width: 8px;
height: 48px;
background: #333;
border-radius: 4px;
box-shadow: 0 0 0 2px #fff, 0 0 6px rgba(102, 126, 234, 0.5);
opacity: 0.8;
}
.resizer:hover .resizer-grip {
box-shadow: 0 0 0 2px #fff, 0 0 6px rgba(255, 102, 0, 0.5);
}
.section-title {
font-size: 1.3em;
margin-bottom: 20px;
color: #2c3e50;
font-weight: 500;
display: flex;
align-items: center;
gap: 10px;
}
.section-title::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.instructions {
background: #e8f4fd;
border: 1px solid #bee5eb;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
.instructions h3 {
color: #0c5460;
margin-bottom: 10px;
font-size: 1.1em;
}
.instructions p {
color: #0c5460;
line-height: 1.6;
margin-bottom: 8px;
}
.instructions code {
background: rgba(12, 84, 96, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
/* テキストエリア上部のコントロール */
.input-controls {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 15px;
flex-wrap: wrap;
gap: 10px;
}
.left-controls {
display: flex;
gap: 10px;
align-items: center;
}
.control-btn {
padding: 8px 16px;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
font-family: inherit;
display: flex;
align-items: center;
gap: 6px;
}
.clear-btn {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3);
}
.clear-btn:hover {
background: linear-gradient(135deg, #c0392b, #a93226);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(231, 76, 60, 0.4);
}
/* 隠しファイル入力 */
.hidden-file-input {
display: none;
}
.input-textarea {
width: 100%;
height: 400px;
padding: 20px;
border: 2px solid #e9ecef;
border-radius: 15px;
font-size: 16px;
line-height: 1.8;
font-family: inherit;
resize: vertical;
transition: all 0.3s ease;
background: white;
}
.input-textarea.scroll-mode {
height: auto;
min-height: 400px;
resize: none;
}
.input-textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.preview-controls {
display: flex;
flex-direction: column;
gap: 10px;
position: absolute;
top: 20px;
right: 30px;
z-index: 20;
}
.control-row {
display: flex;
align-items: center;
gap: 15px;
}
.furigana-toggle {
background: #f6f7fb;
border: 1px solid #bbb;
border-radius: 20px;
padding: 6px 16px;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
display: flex;
align-items: center;
gap: 8px;
user-select: none;
}
.furigana-toggle input[type="checkbox"] {
accent-color: #667eea;
width: 18px;
height: 18px;
}
.hide-furigana ruby rt {
display: none;
}
.mode-toggle {
display: flex;
background: #f8f9fa;
border-radius: 25px;
padding: 4px;
border: 2px solid #e9ecef;
}
.mode-btn {
padding: 8px 16px;
border: none;
background: transparent;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
color: #666;
}
.mode-btn.active {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.mode-btn:hover:not(.active) {
background: #e9ecef;
color: #333;
}
.output-container {
height: 400px;
padding: 20px;
border: 2px solid #e9ecef;
border-radius: 15px;
background: white;
font-size: 16px;
line-height: 2.2;
overflow-y: auto;
margin-top: 60px;
}
.output-container.scroll-mode {
height: auto;
min-height: 400px;
overflow-y: visible;
margin-top: 60px;
}
ruby {
position: relative;
}
rt {
font-size: 0.6em;
color: #666;
font-weight: normal;
}
.emphasis {
text-emphasis: filled circle;
text-emphasis-position: over right;
-webkit-text-emphasis: filled circle;
-webkit-text-emphasis-position: over right;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 30px 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px;
border-radius: 15px;
text-align: center;
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
transform: translateY(0);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-number {
font-size: 2.5em;
font-weight: bold;
margin-bottom: 10px;
}
.stat-label {
font-size: 1.1em;
opacity: 0.9;
}
/* フローティングボタン群 */
.floating-buttons {
position: fixed;
top: 50%;
right: 20px;
transform: translateY(-50%);
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
background: rgba(255, 255, 255, 0.95);
padding: 15px;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.floating-btn {
padding: 12px 16px;
border: none;
border-radius: 15px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s ease;
font-family: inherit;
display: flex;
align-items: center;
gap: 8px;
min-width: 140px;
justify-content: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.save-new-btn {
background: linear-gradient(135deg, #27ae60, #2ecc71);
color: white;
}
.save-new-btn:hover {
background: linear-gradient(135deg, #229954, #27ae60);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.4);
}
.save-overwrite-btn {
background: linear-gradient(135deg, #f39c12, #e67e22);
color: white;
}
.save-overwrite-btn:hover {
background: linear-gradient(135deg, #d68910, #d35400);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(243, 156, 18, 0.4);
}
.save-overwrite-btn:disabled {
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
cursor: not-allowed;
transform: none;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.load-file-btn {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
}
.load-file-btn:hover {
background: linear-gradient(135deg, #2980b9, #2471a3);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
}
.current-file-info {
font-size: 11px;
color: #666;
text-align: center;
margin-top: 5px;
padding: 5px;
background: rgba(102, 126, 234, 0.1);
border-radius: 8px;
max-width: 140px;
word-break: break-all;
}
.back-to-top {
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
z-index: 999;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.back-to-top:hover {
background: linear-gradient(135deg, #5a67d8, #6b46c1);
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
.back-to-top:active {
transform: translateY(-1px);
}
/* 「一番下へいく」ボタン用CSS追加 */
.back-to-bottom {
position: fixed;
bottom: 90px;
right: 30px;
width: 50px;
height: 50px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
z-index: 999;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.back-to-bottom:hover {
background: linear-gradient(135deg, #5a67d8, #6b46c1);
transform: translateY(3px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
.back-to-bottom:active {
transform: translateY(1px);
}
/* 通知スタイル */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
color: white;
font-size: 14px;
z-index: 1001;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
}
.notification.show {
opacity: 1;
transform: translateX(0);
}
.notification.success {
background: linear-gradient(135deg, #28a745, #20c997);
}
.notification.error {
background: linear-gradient(135deg, #dc3545, #c82333);
}
.notification.info {
background: linear-gradient(135deg, #17a2b8, #138496);
}
@media (max-width: 768px) {
.container {
flex-direction: column;
margin: 10px;
}
.resizer {
width: 100%;
height: 20px;
cursor: row-resize;
background: repeating-linear-gradient(
45deg,
#667eea 0 5px,
#fff 5px 10px
);
}
.resizer:hover {
background: repeating-linear-gradient(
45deg,
#ff6600 0 5px,
#fff 5px 10px
);
}
.resizer-grip {
width: 48px;
height: 8px;
}
.preview-controls {
position: relative;
top: 0;
right: 0;
margin-bottom: 20px;
flex-direction: row;
flex-wrap: wrap;
}
.control-row {
flex-direction: column;
gap: 10px;
}
.output-container {
margin-top: 0;
}
.output-container.scroll-mode {
margin-top: 0;
}
.header h1 {
font-size: 2em;
}
.stats {
grid-template-columns: 1fr 1fr;
gap: 15px;
margin: 20px 10px;
}
.floating-buttons {
position: fixed;
bottom: 150px;
right: 10px;
left: 10px;
top: auto;
transform: none;
flex-direction: row;
justify-content: space-between;
padding: 10px;
}
.floating-btn {
min-width: auto;
flex: 1;
font-size: 12px;
padding: 10px 8px;
}
.current-file-info {
display: none;
}
.back-to-top {
bottom: 80px;
right: 20px;
width: 45px;
height: 45px;
font-size: 18px;
}
.back-to-bottom {
bottom: 20px;
right: 20px;
width: 45px;
height: 45px;
font-size: 18px;
}
.input-controls {
flex-direction: column;
align-items: stretch;
}
.left-controls {
justify-content: center;
}
}
</style>
</head>
<body>
<div class="header">
<h1>ふりがな付きテキストエディター(保存機能強化版)</h1>
<p>小説や文章にふりがなを付けて、美しくレイアウトしましょう</p>
</div>
<div class="container">
<div class="panel input-panel" id="inputPanel">
<div class="section-title">テキスト入力</div>
<div class="instructions">
<h3>ふりがなの指定方法</h3>
<p><code>
<p><code>
<p><code>
<p>例: <code>
<p>例: <code>お
<p>例: <code>
<p style="margin-top: 15px; font-size: 0.9em; color: #666;">
<strong>注意:</strong> このふりがなの付け方は青空文庫形式に近いですが、
<a href="https://www.neopage.com/announcements/1092527420616626176"
target="_blank"
style="color: #667eea; text-decoration: none;">
ネオページが提示する付け方
</a>
が基本となっています。
</p>
</div>
<div class="input-controls">
<div class="left-controls">
<button class="control-btn clear-btn" id="clearBtn">
🗑️ クリア
</button>
</div>
</div>
<textarea
class="input-textarea"
id="inputTextarea"
placeholder="ここにふりがな指定のあるテキストを入力してください。 例:
></textarea>
</div>
<div class="resizer" id="resizer">
<div class="resizer-grip"></div>
</div>
<div class="panel preview-panel" id="previewPanel">
<div class="section-title">プレビュー</div>
<div class="preview-controls">
<div class="control-row">
<label class="furigana-toggle">
<input type="checkbox" id="furiganaToggle" checked>
ふりがな表示
</label>
</div>
<div class="mode-toggle">
<button class="mode-btn active" id="normalModeBtn">通常モード</button>
<button class="mode-btn" id="scrollModeBtn">スクロールモード</button>
</div>
</div>
<div class="output-container" id="outputContainer">
<p style="color: #999; text-align: center; margin-top: 150px;">
左のテキストエリアに文章を入力すると、<br>
ふりがな付きでここに表示されます
</p>
</div>
</div>
</div>
<!-- フローティングボタン群 -->
<div class="floating-buttons">
<button class="floating-btn save-new-btn" id="saveNewBtn">
💾 名前を付けて保存
</button>
<button class="floating-btn save-overwrite-btn" id="saveOverwriteBtn" disabled>
🔄 上書き保存
</button>
<button class="floating-btn load-file-btn" id="loadFileBtn">
📁 ファイル読み込み
</button>
<div class="current-file-info" id="currentFileInfo" style="display: none;">
未保存
</div>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number" id="totalChars">0</div>
<div class="stat-label">総文字数</div>
</div>
<div class="stat-card">
<div class="stat-number" id="kanjiCount">0</div>
<div class="stat-label">漢字</div>
</div>
<div class="stat-card">
<div class="stat-number" id="kanaCount">0</div>
<div class="stat-label">仮名</div>
</div>
<div class="stat-card">
<div class="stat-number" id="otherCount">0</div>
<div class="stat-label">記号・その他</div>
</div>
</div>
<button class="back-to-top" id="backToTop" title="トップに戻る">↑</button>
<button class="back-to-bottom" id="backToBottom" title="一番下へいく">↓</button>
<!-- 隠しファイル入力 -->
<input type="file" class="hidden-file-input" id="fileInput" accept=".txt">
<!-- 通知要素 -->
<div class="notification" id="notification"></div>
<script>
// --- DOM要素の取得 ---
const inputTextarea = document.getElementById('inputTextarea');
const outputContainer = document.getElementById('outputContainer');
const clearBtn = document.getElementById('clearBtn');
const saveNewBtn = document.getElementById('saveNewBtn');
const saveOverwriteBtn = document.getElementById('saveOverwriteBtn');
const loadFileBtn = document.getElementById('loadFileBtn');
const fileInput = document.getElementById('fileInput');
const furiganaToggle = document.getElementById('furiganaToggle');
const normalModeBtn = document.getElementById('normalModeBtn');
const scrollModeBtn = document.getElementById('scrollModeBtn');
const totalCharsEl = document.getElementById('totalChars');
const kanjiCountEl = document.getElementById('kanjiCount');
const kanaCountEl = document.getElementById('kanaCount');
const otherCountEl = document.getElementById('otherCount');
const notification = document.getElementById('notification');
const backToTop = document.getElementById('backToTop');
const backToBottom = document.getElementById('backToBottom');
const resizer = document.getElementById('resizer');
const inputPanel = document.getElementById('inputPanel');
const previewPanel = document.getElementById('previewPanel');
const currentFileInfo = document.getElementById('currentFileInfo');
// --- グローバル変数 ---
let currentFileName = null;
let lastSavedContent = null;
// --- ふりがな・傍点変換関数 ---
function parseText(text) {
let processedText = text;
// 傍点(
processedText = processedText.replace(/(.?)
return `<span class="emphasis">${dots}</span>`;
});
//
processedText = processedText.replace(/[|
// |漢字(かな) or |漢字(かな) → ルビ化しない(そのまま表示)
processedText = processedText.replace(/[||]([^\s《(]+)\((.+?)\)/g, '$1($2)');
// |(かな) or |(かな) → ルビ化しない(そのまま表示)
processedText = processedText.replace(/[||]\((.+?)\)/g, '($1)');
//
processedText = processedText.replace(/([一-龯々〆〇]+)《(.+?)》/g, '<ruby>$1<rt>$2</rt></ruby>');
// 漢字(かな) ただし直前が「|」の場合は既に処理済みなのでここは漢字の直後の(かな)のみ
processedText = processedText.replace(/([一-龯々〆〇]+)\(([\u3040-\u309F\u30A0-\u30FFー]+)\)/g, '<ruby>$1<rt>$2</rt></ruby>');
// |や|単体を除去(青空文庫対応)
processedText = processedText.replace(/[||]/g, '');
// 改行
processedText = processedText.replace(/\n/g, '<br>');
return processedText;
}
// --- プレビュー更新 ---
function updatePreview() {
const text = inputTextarea.value;
if (!text.trim()) {
outputContainer.innerHTML = `
<p style="color: #999; text-align: center; margin-top: 150px;">
左のテキストエリアに文章を入力すると、<br>
ふりがな付きでここに表示されます
</p>
`;
updateStats('');
updateFileInfo();
return;
}
outputContainer.innerHTML = parseText(text);
updateStats(text);
updateFileInfo();
adjustTextareaHeight();
}
// --- ファイル情報更新 ---
function updateFileInfo() {
const hasChanges = lastSavedContent !== inputTextarea.value;
if (currentFileName) {
currentFileInfo.style.display = 'block';
currentFileInfo.textContent = currentFileName + (hasChanges ? ' (変更あり)' : '');
saveOverwriteBtn.disabled = false;
} else {
currentFileInfo.style.display = 'block';
currentFileInfo.textContent = '未保存' + (inputTextarea.value.trim() ? ' (変更あり)' : '');
saveOverwriteBtn.disabled = true;
}
}
// --- 統計情報更新 ---
function updateStats(text) {
// ふりがな指定記号を除去してから文字数をカウント
let cleanText = text
.replace(/
return match.replace(/
})
.replace(/|[一-龯々]+\([あ-んア-ンー]+\)/g, function(match) {
return match.replace(/|([一-龯々]+)\([あ-んア-ンー]+\)/g, '$1');
})
.replace(/《[あ-んア-ンー]+》/g, '')
.replace(/\(([あ-んア-ンー]+)\)/g, '')
.replace(/[||]/g, '')
.replace(/[\s\n\r\t]/g, '');
let kanjiCount = 0;
let kanaCount = 0;
let otherCount = 0;
for (let char of cleanText) {
if (/[一-龯々〆〇]/.test(char)) {
kanjiCount++;
} else if (/[あ-んぁ-ゖゝゞ]/.test(char)) {
kanaCount++;
} else if (/[ア-ンァ-ヺヽヾー]/.test(char)) {
kanaCount++;
} else if (/[a-zA-Z]/.test(char)) {
otherCount++;
} else if (/[0-90-9]/.test(char)) {
otherCount++;
} else if (/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~!"#$%&'()*+,-./:;<=>?@[¥]^_`{|}〜、。・「」『』【】〔〕…‥''""※→←↑↓∴∵≠≤≥∞∝∫∮Σ√∛∜±×÷°′″‰%‱℃℉]/.test(char)) {
otherCount++;
}
}
const totalChars = kanjiCount + kanaCount + otherCount;
totalCharsEl.textContent = totalChars.toLocaleString();
kanjiCountEl.textContent = kanjiCount.toLocaleString();
kanaCountEl.textContent = kanaCount.toLocaleString();
otherCountEl.textContent = otherCount.toLocaleString();
}
// --- textareaの高さを自動調整 ---
function adjustTextareaHeight() {
const textarea = inputTextarea;
const outputContainer = document.getElementById('outputContainer');
if (textarea.classList.contains('scroll-mode')) {
setTimeout(() => {
const outputHeight = outputContainer.scrollHeight;
const textareaMinHeight = Math.max(400, textarea.scrollHeight + 4);
const targetHeight = Math.max(textareaMinHeight, outputHeight);
textarea.style.height = targetHeight + 'px';
}, 50);
} else {
textarea.style.height = '400px';
}
}
// --- ファイルダウンロード機能 ---
function downloadFile(content, filename) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// --- ふりがな表示切替 ---
furiganaToggle.addEventListener('change', function() {
if (furiganaToggle.checked) {
previewPanel.classList.remove('hide-furigana');
} else {
previewPanel.classList.add('hide-furigana');
}
});
// --- モード切替 ---
normalModeBtn.addEventListener('click', function() {
normalModeBtn.classList.add('active');
scrollModeBtn.classList.remove('active');
inputTextarea.classList.remove('scroll-mode');
outputContainer.classList.remove('scroll-mode');
adjustTextareaHeight();
});
scrollModeBtn.addEventListener('click', function() {
scrollModeBtn.classList.add('active');
normalModeBtn.classList.remove('active');
inputTextarea.classList.add('scroll-mode');
outputContainer.classList.add('scroll-mode');
adjustTextareaHeight();
});
// --- 入力イベント ---
inputTextarea.addEventListener('input', updatePreview);
// --- クリアボタン ---
clearBtn.addEventListener('click', function() {
if (confirm('テキストを全て消去します。よろしいですか?')) {
inputTextarea.value = '';
currentFileName = null;
lastSavedContent = null;
updatePreview();
showNotification('テキストをクリアしました', 'info');
}
});
// --- 名前を付けて保存ボタン ---
saveNewBtn.addEventListener('click', function() {
const text = inputTextarea.value;
if (!text.trim()) {
showNotification('保存するテキストがありません', 'error');
return;
}
const filename = prompt('ファイル名を入力してください:', currentFileName || 'ふりがな文書.txt');
if (filename) {
const finalFilename = filename.endsWith('.txt') ? filename : filename + '.txt';
downloadFile(text, finalFilename);
currentFileName = finalFilename;
lastSavedContent = text;
updateFileInfo();
showNotification(`"${finalFilename}" として保存しました`, 'success');
}
});
// --- 上書き保存ボタン ---
saveOverwriteBtn.addEventListener('click', function() {
const text = inputTextarea.value;
if (!text.trim()) {
showNotification('保存するテキストがありません', 'error');
return;
}
if (!currentFileName) {
showNotification('上書き保存するファイルが指定されていません', 'error');
return;
}
downloadFile(text, currentFileName);
lastSavedContent = text;
updateFileInfo();
showNotification(`"${currentFileName}" を上書き保存しました`, 'success');
});
// --- ファイル読み込みボタン ---
loadFileBtn.addEventListener('click', function() {
fileInput.click();
});
fileInput.addEventListener('change', function() {
const file = fileInput.files[0];
if (!file) return;
if (file.type !== 'text/plain') {
showNotification('テキストファイル(.txt)のみ対応しています', 'error');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
inputTextarea.value = e.target.result;
currentFileName = file.name;
lastSavedContent = e.target.result;
updatePreview();
showNotification(`"${file.name}" を読み込みました`, 'success');
};
reader.readAsText(file, 'utf-8');
});
// --- 通知表示 ---
function showNotification(message, type = 'info') {
notification.textContent = message;
notification.className = `notification ${type}`;
setTimeout(() => {
notification.classList.add('show');
}, 10);
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
// --- リサイズ機能 ---
let isResizing = false;
let lastDownX = 0;
resizer.addEventListener('mousedown', function(e) {
isResizing = true;
lastDownX = e.clientX;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', function(e) {
if (!isResizing) return;
const dx = e.clientX - lastDownX;
lastDownX = e.clientX;
const leftWidth = inputPanel.offsetWidth + dx;
const rightWidth = previewPanel.offsetWidth - dx;
if (leftWidth > 120 && rightWidth > 120) {
inputPanel.style.flex = 'none';
previewPanel.style.flex = 'none';
inputPanel.style.width = leftWidth + 'px';
previewPanel.style.width = rightWidth + 'px';
}
});
document.addEventListener('mouseup', function() {
isResizing = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
// タッチ操作対応
resizer.addEventListener('touchstart', function(e) {
isResizing = true;
lastDownX = e.touches[0].clientX;
e.preventDefault();
});
document.addEventListener('touchmove', function(e) {
if (!isResizing) return;
const dx = e.touches[0].clientX - lastDownX;
lastDownX = e.touches[0].clientX;
const leftWidth = inputPanel.offsetWidth + dx;
const rightWidth = previewPanel.offsetWidth - dx;
if (leftWidth > 120 && rightWidth > 120) {
inputPanel.style.flex = 'none';
previewPanel.style.flex = 'none';
inputPanel.style.width = leftWidth + 'px';
previewPanel.style.width = rightWidth + 'px';
}
});
document.addEventListener('touchend', function() {
isResizing = false;
});
// --- トップに戻る ---
backToTop.addEventListener('click', function() {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// --- 一番下へいく ---
backToBottom.addEventListener('click', function() {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
});
// --- 初期化 ---
window.addEventListener('load', function() {
const sampleText = `
お
これは
inputTextarea.value = sampleText;
lastSavedContent = null; // 初期状態では保存されていない
updatePreview();
inputTextarea.addEventListener('input', adjustTextareaHeight);
window.addEventListener('resize', adjustTextareaHeight);
});
</script>
</body>
</html>
(ここまでコピーします)
------------------------------------------------------