目次
ブックマーク
応援する
いいね!
コメント
シェア
通報

ふりがな付きテキストエディターVer.4.0

(あいさつはコピペで失礼します)

ネオページで小説を書いている皆さんこんにちは。




ふりがな付きテキストエディターを作りました。




オフラインでふりがな付きの文章を確認したいときに使えます。




文字数カウントも付けました。ネオページのカウントとほぼ近い文字数でカウントされます。(同じにするのが難しい……)




【主な機能】




○青空文庫形式のふりがな指定に対応 




○正確な文字数カウント


・漢字、ひらがな、カタカナ、記号を分類表示


・ふりがな指定記号は文字数に含まれません


・空白・改行も除外した正確なカウント




○使いやすいプレビュー機能


・リアルタイムでふりがな付き表示


・固定式/スクロール式の表示切り替え


・美しいレイアウトで読みやすい




【使い方】




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>漢字かんじ</code> または <code>漢字(かんじ)</code></p>

<p><code>漢字かんじ</code> - 青空文庫形式(|で範囲を明示)</p>

<p><code></code> - 傍点(強調表示)</p>

<p>例: <code>うつくしいはないている。</code></p>

<p>例: <code>おつかれ様でした。紫陽花あじさい綺麗きれいです。</code></p>

<p>例: <code>な内容です。</code></p>

<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="ここにふりがな指定のあるテキストを入力してください。&#10;&#10;例:&#10;うつくしいはないている。&#10;今日きょう天気てんきです。&#10;おつかれ様でした。紫陽花あじさい綺麗きれいです。&#10;これはな内容です。"

></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(/(.?)(.+?)/g, (match, char, dots) => {

return `<span class="emphasis">${dots}</span>`;

});


// 漢字ふりがな or 漢字ふりがな

processedText = processedText.replace(/[|]([^\s(]+)《(.+?)/g, '<ruby>$1<rt>$2</rt></ruby>');


// |漢字(かな) 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(/[一-龯々]+[あ-んア-ンー]+/g, function(match) {

return match.replace(/([一-龯々]+)[あ-んア-ンー]+/g, '$1');

})

.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>




(ここまでコピーします)

------------------------------------------------------


この作品に、最初のコメントを書いてみませんか?