memo.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>メモツール</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- 浮遊タブ -->
<div class="floating-tabs">
<!-- タブエリア -->
<div class="tab-list" id="tabContainer">
<button class="tab-btn" data-id="1" draggable="true">1</button>
<button class="tab-btn" data-id="2" draggable="true">2</button>
<button class="tab-btn" data-id="3" draggable="true">3</button>
<button class="tab-btn" data-id="4" draggable="true">4</button>
<button class="tab-btn" data-id="5" draggable="true">5</button>
<button class="tab-btn" data-id="6" draggable="true">6</button>
<button class="tab-btn" data-id="7" draggable="true">7</button>
<button class="tab-btn" data-id="8" draggable="true">8</button>
<button class="tab-btn" data-id="9" draggable="true">9</button>
<button class="tab-btn" data-id="10" draggable="true">10</button>
</div>
<!-- 固定ボタン -->
<div class="tab-tools">
<button class="tab-btn width-btn" id="toggleWidthBtn" title="幅切り替え">📐</button>
<button class="tab-btn save-btn" id="saveJsonBtn">json</button>
<button class="tab-btn save-btn" id="saveHtmlBtn">html</button>
<button class="tab-btn reset-btn" id="resetAllBtn" title="すべてリセット">
🗑️
</button>
<button class="tab-btn import-btn" id="importJsonBtn" title="JSONから復元(インポート)">
📥
</button>
<button class="tab-btn" id="open-current-folder" title="このページのフォルダを開く">
📁
</button>
</div>
</div>
<!-- メインメモ -->
<textarea id="memo" placeholder="ここにメモを書いてください"></textarea>
<!-- ★ 外部JS -->
<script src="app.js"></script>
<script src="reset.js"></script>
<script src="search-all-tabs.js"></script>
<script src="import-json.js"></script>
<script src="export-html.js"></script>
<script src="open-folder.js"></script>
</body>
</html>
favicon.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="4" y="4" width="22" height="24" rx="2" fill="#FCEEA7"/>
<line x1="8" y1="12" x2="22" y2="12" stroke="#DCCB8C" stroke-width="2"/>
<line x1="8" y1="18" x2="22" y2="18" stroke="#DCCB8C" stroke-width="2"/>
<line x1="8" y1="24" x2="16" y2="24" stroke="#DCCB8C" stroke-width="2"/>
<path d="M22 2 L30 10 L16 24 L8 26 L10 18 Z" fill="#FF9800"/>
<path d="M8 26 L10 24 L12 26 Z" fill="#5D4037"/> </svg>
search-all-tabs.js
document.addEventListener("DOMContentLoaded", () => {
/* =========================
Ctrl + F フック
========================= */
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "f") {
e.preventDefault();
openSearchModal();
}
});
});
function openSearchModal() {
if (document.getElementById("searchModal")) return;
const modal = document.createElement("div");
modal.id = "searchModal";
modal.innerHTML = `
<div class="search-overlay">
<div class="search-box">
<input type="text" id="searchInput" placeholder="全タブ検索">
<div id="searchResults"></div>
<button id="closeSearch">閉じる</button>
</div>
</div>
`;
document.body.appendChild(modal);
const input = document.getElementById("searchInput");
const results = document.getElementById("searchResults");
input.focus();
input.addEventListener("input", () => {
searchAllTabs(input.value, results);
});
document.getElementById("closeSearch").addEventListener("click", closeSearchModal);
document.addEventListener("keydown", escCloseHandler);
}
function searchAllTabs(keyword, resultBox) {
resultBox.innerHTML = "";
if (!keyword.trim()) return;
const {
KEY_PREFIX,
NAME_PREFIX,
getTabButtons,
switchTab
} = window.MemoApp;
const lower = keyword.toLowerCase();
getTabButtons().forEach(btn => {
const id = btn.dataset.id;
const name = localStorage.getItem(NAME_PREFIX + id) || btn.textContent;
const text = localStorage.getItem(KEY_PREFIX + id) || "";
if (
name.toLowerCase().includes(lower) ||
text.toLowerCase().includes(lower)
) {
const item = document.createElement("div");
item.className = "search-item";
item.textContent = `[${name}] ${text.slice(0, 40)}`;
item.addEventListener("click", () => {
closeSearchModal();
switchTab(id);
// ★ ハイライト
highlightInTextarea(window.MemoApp.textarea, keyword);
});
resultBox.appendChild(item);
}
});
}
function closeSearchModal() {
const modal = document.getElementById("searchModal");
if (modal) modal.remove();
removeHighlightOverlay();
document.removeEventListener("keydown", escCloseHandler);
}
function escCloseHandler(e) {
if (e.key === "Escape") {
closeSearchModal();
}
}
function highlightInTextarea(textarea, keyword) {
removeHighlightOverlay();
if (!keyword) return;
const value = textarea.value;
if (!value) return;
// 正規表現用にエスケープ
const escaped = keyword.replace(/[.*+?^${}()|[\]\]/g, "$&");
const regex = new RegExp(escaped, "gi");
if (!regex.test(value)) return;
const overlay = document.createElement("div");
overlay.id = "highlightOverlay";
const style = getComputedStyle(textarea);
overlay.style.position = "fixed";
overlay.style.inset = "0";
overlay.style.pointerEvents = "none";
overlay.style.whiteSpace = "pre-wrap";
overlay.style.font = style.font;
overlay.style.padding = style.padding;
overlay.style.lineHeight = style.lineHeight;
overlay.style.background = "transparent";
overlay.style.color = "transparent";
overlay.style.zIndex = "2";
// ★ 全一致をハイライト
overlay.innerHTML = escapeHtml(value).replace(regex, match => {
return `<span class="search-highlight">${escapeHtml(match)}</span>`;
});
document.body.appendChild(overlay);
}
function removeHighlightOverlay() {
const overlay = document.getElementById("highlightOverlay");
if (overlay) overlay.remove();
}
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
reset.js
document.addEventListener("DOMContentLoaded", () => {
const resetAllBtn = document.getElementById("resetAllBtn");
if (!resetAllBtn) return;
resetAllBtn.addEventListener("click", () => {
const ok = confirm(
"すべてのタブをリセットします。\n\n" +
"・タブ名\n" +
"・並び順\n" +
"・メモ内容\n\n" +
"すべて削除され、元に戻せません。\n\n" +
"本当に実行しますか?"
);
if (!ok) return;
const {
KEY_PREFIX,
NAME_PREFIX,
ORDER_KEY,
CURRENT_TAB_KEY,
getTabButtons,
tabContainer,
textarea,
switchTab
} = window.MemoApp;
/* =========
localStorage 削除
========= */
getTabButtons().forEach(btn => {
const id = btn.dataset.id;
localStorage.removeItem(KEY_PREFIX + id);
localStorage.removeItem(NAME_PREFIX + id);
});
localStorage.removeItem(ORDER_KEY);
localStorage.removeItem(CURRENT_TAB_KEY);
/* =========
UI 初期化
========= */
// タブ順をID順に戻す
const buttons = Array.from(getTabButtons());
buttons
.sort((a, b) => Number(a.dataset.id) - Number(b.dataset.id))
.forEach(btn => tabContainer.appendChild(btn));
// タブ名・active解除
getTabButtons().forEach(btn => {
btn.textContent = btn.dataset.id;
btn.classList.remove("active");
});
// textarea 初期化
textarea.value = "";
// タブ1を初期化
window.MemoApp.currentTab = "1";
localStorage.setItem(CURRENT_TAB_KEY, "1");
switchTab("1", true);
});
});
open-folder.js
document.addEventListener("DOMContentLoaded", () => {
const openFolderBtn = document.getElementById("open-current-folder");
if (!openFolderBtn) return;
openFolderBtn.addEventListener("click", () => {
// file:// 以外では無効
if (location.protocol !== "file:") {
alert("この機能はローカル環境専用です");
return;
}
// file:///D:/path/to/file.html → D:/path/to/
let folderPath = decodeURIComponent(location.pathname);
// Windows 用に先頭の / を除去
if (folderPath.startsWith("/")) {
folderPath = folderPath.slice(1);
}
// ファイル名を削除
folderPath = folderPath.replace(/\/[^\/]+$/, "");
// Explorer で開く
location.href = "explorer:" + folderPath;
});
});
import-json.js
document.addEventListener("DOMContentLoaded", () => {
const importBtn = document.getElementById("importJsonBtn");
if (!importBtn) return;
importBtn.addEventListener("click", () => {
const ok = confirm(
"JSONファイルからメモを復元します。\n\n" +
"現在の内容はすべて上書きされます。\n\n" +
"続行しますか?"
);
if (!ok) return;
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.addEventListener("change", () => {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
restoreFromJson(data);
} catch (e) {
alert("JSONファイルの形式が正しくありません");
}
};
reader.readAsText(file, "utf-8");
});
input.click();
});
});
/* =========================
JSON復元処理
========================= */
function restoreFromJson(data) {
const {
KEY_PREFIX,
NAME_PREFIX,
ORDER_KEY,
CURRENT_TAB_KEY,
getTabButtons,
tabContainer,
textarea,
switchTab
} = window.MemoApp;
/* =========
既存データ削除
========= */
getTabButtons().forEach(btn => {
const id = btn.dataset.id;
localStorage.removeItem(KEY_PREFIX + id);
localStorage.removeItem(NAME_PREFIX + id);
});
localStorage.removeItem(ORDER_KEY);
localStorage.removeItem(CURRENT_TAB_KEY);
/* =========
データ復元
========= */
const ids = Object.keys(data);
ids.forEach(id => {
const item = data[id];
if (item.text) {
localStorage.setItem(KEY_PREFIX + id, item.text);
}
if (item.name) {
localStorage.setItem(NAME_PREFIX + id, item.name);
}
});
/* =========
タブ並び順復元
========= */
ids.forEach(id => {
const btn = tabContainer.querySelector(`.tab-btn[data-id="${id}"]`);
if (btn) tabContainer.appendChild(btn);
});
localStorage.setItem(ORDER_KEY, JSON.stringify(ids));
/* =========
UI反映
========= */
getTabButtons().forEach(btn => {
btn.textContent =
localStorage.getItem(NAME_PREFIX + btn.dataset.id) ||
btn.dataset.id;
btn.classList.remove("active");
});
textarea.value = "";
const firstTab = ids[0] || "1";
window.MemoApp.currentTab = firstTab;
localStorage.setItem(CURRENT_TAB_KEY, firstTab);
switchTab(firstTab, true);
}
export-html.js
document.addEventListener("DOMContentLoaded", () => {
const saveHtmlBtn = document.getElementById("saveHtmlBtn");
if (!saveHtmlBtn) return;
saveHtmlBtn.addEventListener("click", () => {
const {
KEY_PREFIX,
NAME_PREFIX,
getTabButtons,
textarea,
currentTab
} = window.MemoApp;
// 念のため現在タブ保存
localStorage.setItem(KEY_PREFIX + currentTab, textarea.value);
let htmlBody = "";
getTabButtons().forEach(btn => {
const id = btn.dataset.id;
const title = localStorage.getItem(NAME_PREFIX + id) || `タブ ${id}`;
const text = localStorage.getItem(KEY_PREFIX + id) || "";
htmlBody += `
<section class="memo-section">
<h2>${escapeHtml(title)}</h2>
<pre>${escapeHtml(text)}</pre>
</section>
`;
});
const html = `<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>メモ</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: system-ui, -apple-system, "Segoe UI",
"Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
padding: 24px;
line-height: 1.7;
background: #ffffff;
color: #111;
}
.memo-section {
margin-bottom: 32px;
}
h2 {
font-size: 18px;
border-left: 4px solid #3b82f6;
padding-left: 10px;
}
pre {
white-space: pre-wrap;
word-break: break-word;
background: #f5f5f5;
padding: 16px;
border-radius: 6px;
}
</style>
</head>
<body>
<h1>メモ</h1>
${htmlBody}
</body>
</html>`;
const blob = new Blob([html], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const ts = makeTimestamp();
a.download = `simple-memo_${ts}.html`;
a.click();
URL.revokeObjectURL(url);
});
});
/* =========================
HTMLエスケープ
========================= */
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function makeTimestamp() {
const d = new Date();
const pad = n => String(n).padStart(2, "0");
return (
d.getFullYear() +
pad(d.getMonth() + 1) +
pad(d.getDate()) + "_" +
pad(d.getHours()) +
pad(d.getMinutes()) +
pad(d.getSeconds())
);
}
app.js
document.addEventListener("DOMContentLoaded", () => {
const textarea = document.getElementById("memo");
const tabContainer = document.getElementById("tabContainer");
const saveJsonBtn = document.getElementById("saveJsonBtn");
const KEY_PREFIX = "simple_memo_tab_";
const NAME_PREFIX = "simple_memo_tab_name_";
const ORDER_KEY = "simple_memo_tab_order";
const CURRENT_TAB_KEY = "simple_memo_current_tab";
let currentTab = localStorage.getItem(CURRENT_TAB_KEY) || "1";
// ===== export 用に公開 =====
window.MemoApp = {};
window.MemoApp.KEY_PREFIX = KEY_PREFIX;
window.MemoApp.NAME_PREFIX = NAME_PREFIX;
window.MemoApp.ORDER_KEY = ORDER_KEY;
window.MemoApp.CURRENT_TAB_KEY = CURRENT_TAB_KEY;
window.MemoApp.getTabButtons = getTabButtons;
window.MemoApp.tabContainer = tabContainer;
window.MemoApp.textarea = textarea;
window.MemoApp.switchTab = switchTab;
Object.defineProperty(window.MemoApp, "currentTab", {
get() {
return currentTab;
},
set(val) {
currentTab = val;
}
});
/* =========================
タイムスタンプ作成
========================= */
function makeTimestamp() {
const d = new Date();
const pad = n => String(n).padStart(2, "0");
const Y = d.getFullYear();
const M = pad(d.getMonth() + 1);
const D = pad(d.getDate());
const h = pad(d.getHours());
const m = pad(d.getMinutes());
const s = pad(d.getSeconds());
return `${Y}${M}${D}_${h}${m}${s}`;
}
/* =========================
タブ切り替え
========================= */
function switchTab(id, skipSave = false) {
if (!skipSave) {
localStorage.setItem(KEY_PREFIX + currentTab, textarea.value);
}
currentTab = id;
localStorage.setItem(CURRENT_TAB_KEY, currentTab);
textarea.value = localStorage.getItem(KEY_PREFIX + currentTab) || "";
getTabButtons().forEach(btn => {
btn.classList.toggle("active", btn.dataset.id === currentTab);
});
updateDocumentTitle(); // ★ 追加
}
function updateDocumentTitle() {
const {
NAME_PREFIX,
currentTab
} = window.MemoApp;
const name = localStorage.getItem(NAME_PREFIX + currentTab) || `メモ ${currentTab}`;
document.title = name + " - シンプルメモ";
}
/* =========================
タブ名の復元
========================= */
function loadTabNames() {
getTabButtons().forEach(btn => {
const name = localStorage.getItem(NAME_PREFIX + btn.dataset.id);
if (name) btn.textContent = name;
});
}
/* =========================
タブ順序の復元
========================= */
function loadTabOrder() {
const order = JSON.parse(localStorage.getItem(ORDER_KEY));
if (!order) return;
order.forEach(id => {
const btn = tabContainer.querySelector(`.tab-btn[data-id="${id}"]`);
if (btn) tabContainer.appendChild(btn);
});
}
function getTabButtons() {
return tabContainer.querySelectorAll(".tab-btn[data-id]");
}
function renameTab(btn) {
const currentName = btn.textContent;
const name = prompt("タブ名を変更", currentName);
if (!name) return;
btn.textContent = name;
localStorage.setItem(NAME_PREFIX + btn.dataset.id, name);
updateDocumentTitle(); // ★ 追加
}
/* =========================
タブイベント
========================= */
getTabButtons().forEach(btn => {
// 通常クリック:タブ切り替え
btn.addEventListener("click", () => {
switchTab(btn.dataset.id);
});
// 右クリック:名前変更
btn.addEventListener("contextmenu", e => {
e.preventDefault();
renameTab(btn);
});
// ダブルクリック:名前変更
btn.addEventListener("dblclick", e => {
e.preventDefault();
renameTab(btn);
});
});
/* =========================
入力時保存
========================= */
textarea.addEventListener("input", () => {
localStorage.setItem(KEY_PREFIX + currentTab, textarea.value);
});
/* =========================
JSON保存
========================= */
saveJsonBtn.addEventListener("click", () => {
localStorage.setItem(KEY_PREFIX + currentTab, textarea.value);
const data = {};
getTabButtons().forEach(btn => {
const id = btn.dataset.id;
data[id] = {
name: localStorage.getItem(NAME_PREFIX + id) || id,
text: localStorage.getItem(KEY_PREFIX + id) || ""
};
});
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
const ts = makeTimestamp();
a.download = `simple-memo-backup_${ts}.json`;
a.click();
});
/* =========================
ドラッグ&ドロップ
========================= */
let dragged = null;
tabContainer.addEventListener("dragstart", e => {
if (!e.target.dataset.id) return;
dragged = e.target;
e.target.classList.add("dragging");
});
tabContainer.addEventListener("dragend", e => {
e.target.classList.remove("dragging");
saveTabOrder();
});
tabContainer.addEventListener("dragover", e => {
e.preventDefault();
const target = e.target.closest(".tab-btn[data-id]");
if (!target || target === dragged) return;
const rect = target.getBoundingClientRect();
const next = (e.clientX - rect.left) > rect.width / 2;
tabContainer.insertBefore(dragged, next ? target.nextSibling : target);
});
function saveTabOrder() {
const order = [...tabContainer.querySelectorAll(".tab-btn[data-id]")]
.map(btn => btn.dataset.id);
localStorage.setItem(ORDER_KEY, JSON.stringify(order));
}
/* =========================
初期化
========================= */
loadTabOrder();
loadTabNames();
switchTab(currentTab, true);
});
/* =========================
Ctrl + S 無効化
========================= */
document.addEventListener("keydown", (e) => {
// Ctrl + S / Cmd + S(Mac対応)
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
e.preventDefault();
// 任意:自分の保存処理を呼ぶならここ
// document.getElementById("saveJsonBtn")?.click();
console.log("Ctrl+S blocked");
}
});
// ===== 幅切り替え =====
const toggleWidthBtn = document.getElementById("toggleWidthBtn");
const WIDTH_MODE_KEY = "simple_memo_width_mode";
// 復元
if (localStorage.getItem(WIDTH_MODE_KEY) === "narrow") {
document.body.classList.add("narrow");
}
// クリックで切り替え
toggleWidthBtn.addEventListener("click", () => {
document.body.classList.toggle("narrow");
if (document.body.classList.contains("narrow")) {
localStorage.setItem(WIDTH_MODE_KEY, "narrow");
} else {
localStorage.setItem(WIDTH_MODE_KEY, "full");
}
});
style.css
/* =========================
リセット
========================= */
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body {
font-family: system-ui, -apple-system, "Segoe UI",
"Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
background: #ffffff;
}
/* =========================
浮遊タブ全体(背景なし・右寄せ)
========================= */
.floating-tabs {
position: fixed;
top: 10px;
right: 10px;
z-index: 1000;
display: flex;
align-items: center;
justify-content: flex-end; /* ★ 全部右寄せ */
gap: 6px;
padding: 0; /* ★ 背景がないので余白も不要 */
background: none; /* ★ 薄い背景を削除 */
backdrop-filter: none; /* ★ ぼかしも不要 */
}
/* タブリスト(右側に並ぶだけ) */
.tab-list {
display: flex;
gap: 6px;
}
/* 右端固定ツール(そのまま) */
.tab-tools {
display: flex;
gap: 6px;
margin-left: 6px; /* タブとの区切り */
}
/* =========================
ボタン共通
========================= */
.tab-btn {
min-width: 34px;
height: 26px;
padding: 0 8px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 12px;
line-height: 1;
white-space: nowrap;
background: #4b5563;
color: #ffffff;
}
/* アクティブタブ */
.tab-btn.active {
background: #3b82f6;
font-weight: 600;
}
/* 保存・フォルダは少し目立たせる */
.tab-tools .tab-btn {
background: #111827;
}
.tab-tools .tab-btn:hover {
background: #1f2937;
}
/* ドラッグ中 */
.tab-btn.dragging {
opacity: 0.5;
}
/* =========================
メモエリア
========================= */
textarea {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
border: none;
resize: none;
outline: none;
/* ★ 上部にタブ分の余白を確保 */
padding: 56px 38px 20px 25px;
font-size: 16px;
line-height: 1.7;
background: #ffffff;
}
/* =========================
全タブ検索
========================= */
.search-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.search-box {
background: #fff;
width: 480px;
max-width: 90%;
border-radius: 10px;
padding: 16px;
}
#searchInput {
width: 100%;
padding: 10px;
font-size: 16px;
margin-bottom: 10px;
}
#searchResults {
max-height: 260px;
overflow-y: auto;
}
.search-item {
padding: 6px 8px;
border-bottom: 1px solid #eee;
cursor: pointer;
font-size: 14px;
}
.search-item:hover {
background: #f3f4f6;
}
/* =========================
検索ハイライト
========================= */
.search-highlight {
background: #ffeb3b;
color: #000;
padding: 0 2px;
border-radius: 2px;
}
/* ===== 幅制限モード ===== */
body.narrow textarea {
max-width: 1000px;
margin: 0 auto;
left: 50%;
right: auto;
transform: translateX(-50%);
box-shadow: 0 0 0 1px #e5e7eb;
padding:55px 20px 20px;
}
/* ボタンアイコン用(任意) */
.width-btn {
background: #6366f1;
font-weight: bold;
}