Jump to content
Main menu
Main menu
move to sidebar
hide
Navigation
Main page
Recent changes
Random page
freem
Search
Search
Appearance
Create account
Log in
Personal tools
Create account
Log in
Pages for logged out editors
learn more
Contributions
Talk
Editing
Openai/6926aad9-faf0-8007-bcb8-9e64cd1f86b1
(section)
Add languages
Page
Discussion
English
Read
Edit
Edit source
View history
Tools
Tools
move to sidebar
hide
Actions
Read
Edit
Edit source
View history
General
What links here
Related changes
Special pages
Page information
Appearance
move to sidebar
hide
Warning:
You are not logged in. Your IP address will be publicly visible if you make any edits. If you
log in
or
create an account
, your edits will be attributed to your username, along with other benefits.
Anti-spam check. Do
not
fill this in!
=== Assistant: localStorage版と見た目・挙動はほぼ同じで、保存先だけ IndexedDB に切り替えた版を丸ごと置きますね。 === localStorage版と見た目・挙動はほぼ同じで、保存先だけ IndexedDB に切り替えた版を丸ごと置きますね。 index.htmlとして保存 → ブラウザで開けばそのまま動きます。 ==== IndexedDB 版 index.html ==== <syntaxhighlight lang="html"><!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>ブラウザ完結メモアプリ (IndexedDB)</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <style> :root { --border-color: #ddd; --accent-color: #2563eb; --bg-sidebar: #f5f5f5; --bg-active: #e0edff; --text-muted: #666; --radius: 8px; --shadow: 0 8px 16px rgba(0,0,0,0.06); } * { box-sizing: border-box; } body { margin: 0; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f3f4f6; color: #111827; height: 100vh; display: flex; align-items: stretch; justify-content: center; } .app-shell { margin: 16px; background: white; border-radius: var(--radius); box-shadow: var(--shadow); display: flex; flex: 1; max-width: 1200px; min-height: 0; } .sidebar { width: 280px; min-width: 220px; max-width: 360px; border-right: 1px solid var(--border-color); background: var(--bg-sidebar); display: flex; flex-direction: column; } .sidebar-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border-color); } .sidebar-title { font-size: 15px; font-weight: 600; } .new-btn { border: none; border-radius: 999px; padding: 6px 12px; font-size: 13px; cursor: pointer; background: var(--accent-color); color: white; display: inline-flex; align-items: center; gap: 4px; } .new-btn span { font-size: 16px; line-height: 1; transform: translateY(-1px); } .memo-list { flex: 1; overflow-y: auto; padding: 8px; } .memo-item { background: white; border-radius: 6px; padding: 8px 10px; margin-bottom: 6px; cursor: pointer; display: flex; align-items: center; gap: 8px; border: 1px solid transparent; transition: background 0.1s ease, border-color 0.1s ease, transform 0.05s ease; } .memo-item:hover { background: #eef2ff; transform: translateY(-1px); } .memo-item.active { background: var(--bg-active); border-color: var(--accent-color); } .memo-main { flex: 1; min-width: 0; } .memo-title { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .memo-meta { font-size: 11px; color: var(--text-muted); margin-top: 2px; } .memo-delete { border: none; background: transparent; cursor: pointer; padding: 4px; border-radius: 999px; font-size: 14px; color: #9ca3af; flex-shrink: 0; } .memo-delete:hover { background: #fee2e2; color: #b91c1c; } .editor-pane { flex: 1; display: flex; flex-direction: column; min-width: 0; } .editor-header { padding: 10px 16px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; justify-content: space-between; gap: 8px; } .editor-title { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .editor-meta { font-size: 11px; color: var(--text-muted); } .editor-body { flex: 1; display: flex; flex-direction: column; min-height: 0; } .editor-textarea { flex: 1; padding: 12px 16px; border: none; resize: none; font-family: inherit; font-size: 14px; line-height: 1.6; outline: none; min-height: 0; } .editor-textarea::placeholder { color: #9ca3af; } .status-bar { font-size: 11px; color: var(--text-muted); padding: 4px 10px 8px 16px; border-top: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; } @media (max-width: 768px) { .app-shell { flex-direction: column; } .sidebar { width: 100%; max-width: none; border-right: none; border-bottom: 1px solid var(--border-color); } } </style> </head> <body> <div class="app-shell"> <!-- 左ペイン:メモ一覧 --> <aside class="sidebar"> <div class="sidebar-header"> <div class="sidebar-title">メモ一覧</div> <button class="new-btn" id="newMemoBtn"><span>+</span>新規</button> </div> <div class="memo-list" id="memoList"></div> </aside> <!-- 右ペイン:エディタ --> <main class="editor-pane"> <div class="editor-header"> <div class="editor-title" id="currentTitle">メモが選択されていません</div> <div class="editor-meta" id="currentMeta"></div> </div> <div class="editor-body"> <textarea id="editor" class="editor-textarea" placeholder="メモを書き始めてください…(自動保存されます)" ></textarea> <div class="status-bar"> <span id="statusText">準備完了</span> <span id="charCount">0 文字</span> </div> </div> </main> </div> <script> (function () { // ==== IndexedDB 設定 ==== const DB_NAME = "memoAppDB.v1"; const DB_VERSION = 1; const MEMOS_STORE = "memos"; const STATE_STORE = "state"; let dbPromise = null; function openDB() { if (!("indexedDB" in window)) { alert("このブラウザは IndexedDB に対応していません。"); throw new Error("IndexedDB not supported"); } if (dbPromise) return dbPromise; dbPromise = new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = (event) => { const db = request.result; if (!db.objectStoreNames.contains(MEMOS_STORE)) { db.createObjectStore(MEMOS_STORE, { keyPath: "id" }); } if (!db.objectStoreNames.contains(STATE_STORE)) { db.createObjectStore(STATE_STORE, { keyPath: "key" }); } }; request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { reject(request.error); }; }); return dbPromise; } async function getAllMemos() { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(MEMOS_STORE, "readonly"); const store = tx.objectStore(MEMOS_STORE); const req = store.getAll(); req.onsuccess = () => { resolve(req.result || []); }; req.onerror = () => reject(req.error); }); } async function putMemo(memo) { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(MEMOS_STORE, "readwrite"); const store = tx.objectStore(MEMOS_STORE); store.put(memo); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } async function deleteMemoFromDB(id) { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(MEMOS_STORE, "readwrite"); const store = tx.objectStore(MEMOS_STORE); store.delete(id); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } async function getState(key) { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STATE_STORE, "readonly"); const store = tx.objectStore(STATE_STORE); const req = store.get(key); req.onsuccess = () => { const result = req.result; resolve(result ? result.value : null); }; req.onerror = () => reject(req.error); }); } async function setState(key, value) { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STATE_STORE, "readwrite"); const store = tx.objectStore(STATE_STORE); store.put({ key, value }); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } // ==== アプリ状態 ==== let state = { memos: {}, // id -> { id, content, updatedAt } order: [], // 表示順 id[] currentId: null, }; const memoListEl = document.getElementById("memoList"); const editorEl = document.getElementById("editor"); const newMemoBtn = document.getElementById("newMemoBtn"); const currentTitleEl = document.getElementById("currentTitle"); const currentMetaEl = document.getElementById("currentMeta"); const statusTextEl = document.getElementById("statusText"); const charCountEl = document.getElementById("charCount"); let saveTimer = null; let isLoading = true; // ==== ユーティリティ ==== function createId() { return ( "m_" + Date.now().toString(36) + "_" + Math.random().toString(16).slice(2) ); } function getTitleFromContent(content) { const firstLine = (content || "").split(/\r?\n/)[0].trim(); return firstLine || "無題メモ"; } function formatDate(ts) { const d = new Date(ts); const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, "0"); const dd = String(d.getDate()).padStart(2, "0"); const hh = String(d.getHours()).padStart(2, "0"); const mi = String(d.getMinutes()).padStart(2, "0"); return <code>${yyyy}/${mm}/${dd} ${hh}:${mi}</code>; } function setStatus(text) { statusTextEl.textContent = text; } function updateCharCount() { charCountEl.textContent = <code>${editorEl.value.length} 文字</code>; } function rebuildOrder() { const arr = Object.values(state.memos); arr.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)); state.order = arr.map((m) => m.id); } // ==== CRUD ==== async function createMemo(initialContent = "") { const id = createId(); const now = Date.now(); const memo = { id, content: initialContent, updatedAt: now, }; state.memos[id] = memo; rebuildOrder(); state.currentId = id; renderList(); renderCurrentMemo(); setStatus("保存中…"); try { await putMemo(memo); await setState("currentId", id); setStatus("保存しました"); } catch (e) { console.error("Failed to create memo:", e); setStatus("保存に失敗しました"); } } async function deleteMemo(id) { if (!state.memos[id]) return; const ok = confirm("このメモを削除しますか?"); if (!ok) return; setStatus("保存中…"); try { await deleteMemoFromDB(id); } catch (e) { console.error("Failed to delete from DB:", e); // DB 失敗しても UI は進めてしまう(シンプル優先) } delete state.memos[id]; rebuildOrder(); if (state.order.length === 0) { state.currentId = null; renderList(); renderCurrentMemo(); // 1つも無くなったら新しく1つ作る await createMemo(); return; } if (state.currentId === id) { state.currentId = state.order[0] || null; } renderList(); renderCurrentMemo(); try { await setState("currentId", state.currentId); setStatus("保存しました"); } catch (e) { console.error("Failed to save currentId:", e); setStatus("保存に失敗しました"); } } function selectMemo(id) { if (!state.memos[id]) return; state.currentId = id; renderList(); renderCurrentMemo(); // currentId は裏で保存しておくだけ setState("currentId", id).catch((e) => { console.error("Failed to save currentId:", e); }); } function updateCurrentMemoContent(content) { const id = state.currentId; if (!id) return; const memo = state.memos[id]; if (!memo) return; memo.content = content; memo.updatedAt = Date.now(); renderCurrentMetaOnly(); scheduleSaveCurrentMemo(); } function scheduleSaveCurrentMemo() { if (saveTimer) clearTimeout(saveTimer); setStatus("保存中…"); saveTimer = setTimeout(() => { saveTimer = null; saveCurrentMemoToDB(); }, 500); } async function saveCurrentMemoToDB() { const id = state.currentId; if (!id) return; const memo = state.memos[id]; if (!memo) return; try { rebuildOrder(); renderList(); renderCurrentMetaOnly(); await putMemo(memo); setStatus("保存しました"); } catch (e) { console.error("Failed to save memo:", e); setStatus("保存に失敗しました"); } } // ==== 描画 ==== function renderList() { memoListEl.innerHTML = ""; if (state.order.length === 0) { const emptyEl = document.createElement("div"); emptyEl.style.padding = "12px"; emptyEl.style.fontSize = "13px"; emptyEl.style.color = "#9ca3af"; emptyEl.textContent = "メモがありません。「新規」ボタンから作成できます。"; memoListEl.appendChild(emptyEl); return; } state.order.forEach((id) => { const memo = state.memos[id]; if (!memo) return; const itemEl = document.createElement("div"); itemEl.className = "memo-item" + (id === state.currentId ? " active" : ""); itemEl.addEventListener("click", () => selectMemo(id)); const mainEl = document.createElement("div"); mainEl.className = "memo-main"; const titleEl = document.createElement("div"); titleEl.className = "memo-title"; titleEl.textContent = getTitleFromContent(memo.content); const metaEl = document.createElement("div"); metaEl.className = "memo-meta"; metaEl.textContent = <code>最終更新: ${formatDate(memo.updatedAt)}</code>; mainEl.appendChild(titleEl); mainEl.appendChild(metaEl); const deleteBtn = document.createElement("button"); deleteBtn.className = "memo-delete"; deleteBtn.innerHTML = "✕"; deleteBtn.title = "削除"; deleteBtn.addEventListener("click", (e) => { e.stopPropagation(); deleteMemo(id); }); itemEl.appendChild(mainEl); itemEl.appendChild(deleteBtn); memoListEl.appendChild(itemEl); }); } function renderCurrentMetaOnly() { const id = state.currentId; if (!id || !state.memos[id]) { currentMetaEl.textContent = ""; return; } const memo = state.memos[id]; currentTitleEl.textContent = getTitleFromContent(memo.content); currentMetaEl.textContent = <code>最終更新: ${formatDate(memo.updatedAt)}</code>; } function renderCurrentMemo() { const id = state.currentId; if (!id || !state.memos[id]) { currentTitleEl.textContent = "メモが選択されていません"; currentMetaEl.textContent = ""; editorEl.value = ""; editorEl.disabled = true; updateCharCount(); return; } const memo = state.memos[id]; editorEl.disabled = false; editorEl.value = memo.content; currentTitleEl.textContent = getTitleFromContent(memo.content); currentMetaEl.textContent = <code>最終更新: ${formatDate(memo.updatedAt)}</code>; updateCharCount(); } // ==== イベント ==== newMemoBtn.addEventListener("click", () => { createMemo(); }); editorEl.addEventListener("input", () => { if (isLoading) return; updateCharCount(); updateCurrentMemoContent(editorEl.value); }); window.addEventListener("beforeunload", () => { // 残っているデバウンスがあれば即保存をキック(完了は保証されない) if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; saveCurrentMemoToDB(); } }); // ==== 初期化 ==== async function init() { isLoading = true; setStatus("読み込み中…"); try { const memos = await getAllMemos(); state.memos = {}; memos.forEach((m) => { state.memos[m.id] = m; }); rebuildOrder(); const storedId = await getState("currentId"); if (storedId && state.memos[storedId]) { state.currentId = storedId; } else { state.currentId = state.order[0] || null; } if (!state.currentId) { await createMemo(); } else { renderList(); renderCurrentMemo(); } setStatus("自動保存が有効です"); } catch (e) { console.error("Failed to initialize app:", e); state.memos = {}; state.order = []; state.currentId = null; await createMemo(); setStatus("自動保存が有効です"); } finally { isLoading = false; } } init(); })(); </script> </body> </html> </syntaxhighlight> ===== ざっくり変更点 ===== * localStorage は使わず、 - DB名: memoAppDB.v1 - ストア: - memos … メモ本体(id, content, updatedAt) - state … currentId だけ保存 という IndexedDB の 2 ストア構成にしています。 * 表示順は updatedAt でソートして state.order を組み立て。 * 自動保存はこれまで通り 500ms デバウンスで、裏で putMemo() を叩く形。 このままでも 10万文字クラスのメモを複数持てるので、 「さらにタグ付けしたい」「検索入れたい」「バックアップ用にJSONエクスポートしたい」 みたいなのがあれば、それ前提の拡張版も書きます。
Summary:
Please note that all contributions to freem are considered to be released under the Creative Commons Attribution-ShareAlike 4.0 (see
Freem:Copyrights
for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)