<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>4マスメモ</title>
<link rel="stylesheet" href="../style.css?20250630">
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jspdf-autotable@3.5.28/dist/jspdf.plugin.autotable.min.js"></script>
<script src="https://◯◯◯◯◯.github.io/4masumemo/NotoSansJP-Regular-base64_2.js"></script>
<style>
table.memo.normal { width: 100%; max-width: 900px; border-collapse:collapse; margin:24px auto; }
table.memo.wide { width: 100%; max-width: 1200px; border-collapse:collapse; margin:24px auto; }
.memo th, .memo td { border:1px solid #ccc; text-align:center; vertical-align: top; }
.memo th { background:#e9ecef; }
.memo td {
padding: 0;
vertical-align: top; /* ← 追加 */
}
.memo td textarea {
width: 100%;
min-height: 480px;
resize: none;
overflow: hidden;
box-sizing: border-box;
border: none;
outline: none;
box-shadow: none;
background-color: transparent;
padding: 8px; /* ← 内側に余白をつける! */
}
.memo td textarea:focus {
background-color: rgba(0, 123, 255, 0.05);
outline: none;
box-shadow: none;
border: none;
}
.save-btn {
display:block; width:100%; max-width:200px; margin:16px auto; padding:8px 0;
}
#toast {
position:fixed; left:50%; bottom:40px; transform:translateX(-50%);
background:#333; color:#fff; padding:8px 16px; border-radius:4px;
font-size:.9rem; opacity:0; pointer-events:none; transition:opacity .3s;
}
#toast.show { opacity:1; }
.title-bar {
display: flex;
align-items: center;
gap: 12px;
margin-left: 16px;
}
.hamburger-menu {
font-size: 20px;
cursor: pointer;
padding: 4px;
margin: 0;
line-height: 1; /* ← 高さを詰める */
display: flex;
align-items: center;
}
.menu-list {
display: none;
position: absolute;
top: 30px;
left: 0;
background: white;
border: 1px solid #ccc;
border-radius: 6px;
list-style: none;
padding: 0;
margin: 0;
min-width: 180px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
z-index: 1000;
}
.hamburger-menu.open .menu-list { display: block; }
.menu-list li {
padding: 10px;
cursor: pointer;
}
.menu-list li:hover {
background-color: #f0f0f0;
}
</style>
</head>
<body>
<div id="header-placeholder"></div>
<div class="page-wrapper">
<aside id="sidebar"></aside>
<div class="content-wrapper">
<div class="main" id="main">
<div class="title-bar">
<div class="hamburger-menu" onclick="toggleMenu(this)">☰
<ul class="menu-list">
<li onclick="setMemoWidth('wide')">横幅を大きくする</li>
<li onclick="setMemoWidth('normal')">横幅を通常に戻す</li>
<li id="savePdfBtn">PDFで保存</li> <!-- ← 追加 -->
</ul>
</div>
<h2>4マスメモ</h2>
</div>
<button id="saveTop" class="save-btn">保存</button>
<form id="memoForm">
<table class="memo normal" id="memoTable">
<thead><tr><th>項目 1</th><th>項目 2</th></tr></thead>
<tbody>
<tr><td><textarea data-cell="0-0"></textarea></td>
<td><textarea data-cell="0-1"></textarea></td></tr>
<tr><td><textarea data-cell="1-0"></textarea></td>
<td><textarea data-cell="1-1"></textarea></td></tr>
</tbody>
</table>
</form>
<button id="saveBottom" class="save-btn">保存</button>
<button id="htmlBtn" class="save-btn">HTMLで保存</button>
<button id="pdfBtn" class="save-btn">PDF で保存</button>
</div>
</div>
</div>
<div id="toast">保存しました</div>
<script>
const form = document.getElementById('memoForm');
const toast = document.getElementById('toast');
const lsKey = 'memo_2x2';
function showToast() {
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2000);
}
function autoResizeTextarea(el){
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
}
function loadMemo(){
const data = JSON.parse(localStorage.getItem(lsKey) || '{}');
form.querySelectorAll('textarea').forEach(ta => {
ta.value = data[ta.dataset.cell] || '';
autoResizeTextarea(ta);
});
}
function saveMemo(){
const data = {};
form.querySelectorAll('textarea').forEach(ta => {
data[ta.dataset.cell] = ta.value;
});
localStorage.setItem(lsKey, JSON.stringify(data));
showToast();
}
document.getElementById('saveTop').onclick =
document.getElementById('saveBottom').onclick = e => {
e.preventDefault();
saveMemo();
};
form.querySelectorAll('textarea').forEach(t => {
autoResizeTextarea(t);
t.addEventListener('input', () => autoResizeTextarea(t));
});
loadMemo();
document.getElementById('htmlBtn').onclick = () => {
saveMemo();
const vals = [...form.querySelectorAll('textarea')].map(t => t.value.replace(/\n/g, '<br>'));
const html = `<!DOCTYPE html><html lang="ja"><head><meta charset="UTF-8"><title>4マスメモ - HTML出力</title>
<style>body{font-family:sans-serif;padding:20px}
h1{font-size:20px;margin-bottom:20px}
table{border-collapse:collapse;width:100%;max-width:900px;margin-top:20px;table-layout:fixed}
th,td{border:1px solid #999;padding:10px;font-size:14px;vertical-align:top;width:50%;word-wrap:break-word}
th{background:#f2f2f2}</style></head><body>
<h1>メモ</h1><table><thead><tr><th>項目 1</th><th>項目 2</th></tr></thead>
<tbody><tr><td>${vals[0]}</td><td>${vals[1]}</td></tr><tr><td>${vals[2]}</td><td>${vals[3]}</td></tr></tbody></table></body></html>`;
const blob = new Blob([html], {type: 'text/html;charset=utf-8'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `memo_2x2_${Date.now()}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
document.getElementById("savePdfBtn").addEventListener("click", () => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ unit: 'pt', format: 'a4' });
// ✅ 日本語フォントを埋め込み
doc.addFileToVFS("NotoSansJP-Regular.ttf", window.NotoSansJP_Base64);
doc.addFont("NotoSansJP-Regular.ttf", "NotoSansJP", "normal");
doc.setFont("NotoSansJP");
doc.setFontSize(16);
// ✅ テキストエリアからデータ収集
const tableData = [];
document.querySelectorAll(".memo tr").forEach((row) => {
const rowData = [];
row.querySelectorAll("td textarea").forEach((textarea) => {
rowData.push((textarea.value || '').replace(/\r\n|\r/g, '\n'));
});
if (rowData.length > 0) tableData.push(rowData);
});
// ✅ PDF表として出力
doc.autoTable({
head: [['項目 1', '項目 2']],
body: tableData,
startY: 60,
styles: {
font: "NotoSansJP",
fontSize: 10,
cellPadding: 8,
valign: 'top',
lineColor: [0, 0, 0],
lineWidth: 0.2
},
headStyles: {
fillColor: [100, 100, 100],
fontStyle: 'normal',
halign: 'center'
},
columnStyles: {
0: { cellWidth: doc.internal.pageSize.getWidth() / 2 - 40 },
1: { cellWidth: doc.internal.pageSize.getWidth() / 2 - 40 }
},
margin: { left: 40, right: 40 }
});
// ✅ PDFとして保存
doc.save("4masu-memo.pdf");
});
document.getElementById('pdfBtn').onclick = () => {
saveMemo();
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ unit: 'pt', format: 'a4' });
doc.addFileToVFS("NotoSansJP-Regular.ttf", window.NotoSansJP_Base64);
doc.addFont("NotoSansJP-Regular.ttf", "NotoSansJP", "normal");
doc.setFont("NotoSansJP");
doc.setFontSize(16);
doc.text('メモ', 40, 40);
const vals = [...document.querySelectorAll('textarea')].map(t =>
(t.value || '').replace(/\r\n|\r/g, '\n')
);
const body = [[vals[0], vals[1]], [vals[2], vals[3]]];
doc.autoTable({
head: [['項目 1', '項目 2']],
body,
startY: 60,
styles: {
font: "NotoSansJP",
fontSize: 10,
cellPadding: 8,
valign: 'top',
lineColor: [0, 0, 0], // 黒色の罫線
lineWidth: 0.2 // 線の太さ(0.1〜0.5程度がおすすめ)
},
headStyles: {
fillColor: [100, 100, 100],
fontStyle: 'normal',
halign: 'center'
},
columnStyles: {
0: { cellWidth: doc.internal.pageSize.getWidth() / 2 - 40 },
1: { cellWidth: doc.internal.pageSize.getWidth() / 2 - 40 }
},
margin: { left: 40, right: 40 }
});
doc.save(`memo_2x2_${Date.now()}.pdf`);
showToast();
};
function setMemoWidth(size) {
const memoTable = document.getElementById('memoTable');
const mainDiv = document.getElementById('main'); // ← 修正ポイント
memoTable.classList.remove('normal', 'wide');
memoTable.classList.add(size);
// 幅の切替を確実に
if (size === 'wide') {
mainDiv.style.width = '1200px';
mainDiv.style.maxWidth = '100%';
} else {
mainDiv.style.width = '850px';
mainDiv.style.maxWidth = '100%';
}
localStorage.setItem('memoWidth', size);
}
function loadMemoWidth() {
const savedWidth = localStorage.getItem('memoWidth') || 'normal';
setMemoWidth(savedWidth);
}
function toggleMenu(btn) {
btn.classList.toggle('open');
}
document.addEventListener('click', e => {
if (!e.target.closest('.hamburger-menu')) {
document.querySelector('.hamburger-menu').classList.remove('open');
}
});
document.addEventListener('DOMContentLoaded', () => {
loadMemoWidth();
});
</script>
<script src="../load_header.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>4マスメモ</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="../style.css?20250630">
<script defer src="../header.js"></script>
<script defer src="../sidebar.js"></script>
<!-- jsPDF & autoTable(PDF 出力用) -->
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
<script src="data/jspdf-font-noto.js"></script> <!-- NotoSansJP 登録 -->
<script src="https://cdn.jsdelivr.net/npm/jspdf-autotable@3.5.28/dist/jspdf.plugin.autotable.min.js"></script>
<style>
/* ■ 表(2×2)専用スタイル ●*/
table.memo{
width:100%;
max-width:1200px; /* ★ 横幅をゆったり広げる */
border-collapse:collapse;
margin:24px auto;
}
.memo th,.memo td{border:1px solid #ccc;padding:6px;text-align:center}
.memo th{background:#e9ecef}
.memo td textarea {
width: 100%;
min-height: 480px; /* 初期高さ(任意) */
resize: none;
overflow: hidden; /* スクロールバー非表示 */
border: none;
box-sizing: border-box;
}
.save-btn{display:block;width:100%;max-width:200px;margin:16px auto;padding:8px 0}
#toast{position:fixed;left:50%;bottom:40px;transform:translateX(-50%);
background:#333;color:#fff;padding:8px 16px;border-radius:4px;font-size:.9rem;
opacity:0;pointer-events:none;transition:opacity .3s}
#toast.show{opacity:1}
</style>
</head>
<body>
<div id="header-placeholder"></div>
<div class="page-wrapper">
<aside id="sidebar">
<!-- Sidebar content will be injected here by JS -->
</aside>
<div class="content-wrapper">
<div class="main" style="width:900px;">
<button id="saveTop" class="save-btn">保存</button>
<h2 class="head-i">4マスメモ</h2>
<a href="shoshiki-memo.html">書式メモ</a>
<form id="memoForm">
<table class="memo">
<!-- ★ ヘッダー行を追加 -->
<thead>
<tr><th>項目 1</th><th>項目 2</th></tr>
</thead>
<tbody>
<tr>
<td><textarea data-cell="0-0"></textarea></td>
<td><textarea data-cell="0-1"></textarea></td>
</tr>
<tr>
<td><textarea data-cell="1-0"></textarea></td>
<td><textarea data-cell="1-1"></textarea></td>
</tr>
</tbody>
</table>
</form>
<button id="saveBottom" class="save-btn">保存</button>
<button id="htmlBtn" class="save-btn">HTMLで保存</button>
<button id="pdfBtn" class="save-btn">PDF で保存</button>
</main>
</div>
<div id="toast">保存しました</div>
<script>
/* ---------- 保存/読込 ---------- */
const form = document.getElementById('memoForm');
const toast = document.getElementById('toast');
const lsKey = 'memo_2x2';
function showToast(){toast.classList.add('show');setTimeout(()=>toast.classList.remove('show'),2000);}
function loadMemo(){
const data = JSON.parse(localStorage.getItem(lsKey) || '{}');
form.querySelectorAll('textarea').forEach(ta=>{
ta.value = data[ta.dataset.cell] || '';
});
}
function autoResizeTextarea(el) {
el.style.height = 'auto'; // 一旦リセット
el.style.height = (el.scrollHeight) + 'px';
}
// 初期化 & 入力時に高さ調整
form.querySelectorAll('textarea').forEach(textarea => {
autoResizeTextarea(textarea); // ページ表示時
textarea.addEventListener('input', () => autoResizeTextarea(textarea));
});
function saveMemo(){
const data = {};
form.querySelectorAll('textarea').forEach(ta=>{
data[ta.dataset.cell] = ta.value;
});
localStorage.setItem(lsKey, JSON.stringify(data));
showToast();
}
loadMemo();
document.getElementById('saveTop').onclick =
document.getElementById('saveBottom').onclick = e=>{e.preventDefault();saveMemo();};
/* ---------- HTML 出力 ---------- */
document.getElementById('htmlBtn').onclick = () => {
saveMemo(); // 現在の内容を保存
// テキスト取得
const vals = [...form.querySelectorAll('textarea')].map(t =>
t.value.replace(/\n/g, '<br>')
);
// HTML内容を作成
let html = `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>4マスメモ - HTML出力</title>
<style>
body { font-family: sans-serif; padding: 20px; }
h1 { font-size: 20px; margin-bottom: 20px; }
table { border-collapse: collapse; width: 100%; max-width: 900px; margin-top: 20px; }
th, td { border: 1px solid #999; padding: 10px; font-size: 14px; vertical-align: top; }
th { background: #f2f2f2; }
</style>
</head>
<body>
<h1>メモ</h1>
<table>
<thead>
<tr><th>項目 1</th><th>項目 2</th></tr>
</thead>
<tbody>
<tr><td>${vals[0]}</td><td>${vals[1]}</td></tr>
<tr><td>${vals[2]}</td><td>${vals[3]}</td></tr>
</tbody>
</table>
</body>
</html>
`;
// Blob を使ってHTMLファイルとしてダウンロード
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `memo_2x2_${Date.now()}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
/* ---------- PDF 出力 ---------- */
document.getElementById('pdfBtn').onclick = ()=>{
saveMemo();
const { jsPDF } = window.jspdf;
const doc = new jsPDF({unit:'pt',format:'a4'});
doc.setFont('NotoSansJP','normal').setFontSize(12);
const vals=[...form.querySelectorAll('textarea')].map(t=>t.value.replace(/\n/g,' / '));
const body=[[vals[0],vals[1]],[vals[2],vals[3]]];
doc.autoTable({
head: [['項目 1','項目 2']], /* ヘッダーも反映 */
body,
startY:60,
styles:{font:'NotoSansJP',cellPadding:5,valign:'top'},
columnStyles:{0:{cellWidth:430},1:{cellWidth:430}} /* ★ 備考:横幅拡大 */
});
doc.save('memo_2x2.pdf');
};
</script>
</div>
</div>
</div>
<script src="../load_header.js"></script>
</body>
</html>
コメント