JavaScript | 1 日 90 分 × 7 日アプリ学習:DOM・イベント・ブラウザUI メモ帳アプリ編

Web APP JavaScript
スポンサーリンク
スポンサーリンク
  1. メモ帳アプリ 2日目のゴール
  2. 今日の全体ステップ
  3. 1日目のベース(HTML)はそのまま使う
  4. 状態と DOM 参照の定義(1日目と同じ+α)
  5. localStorage の保存・読み込み(復習+そのまま使う)
    1. 保存関数
    2. 読み込み関数
  6. メモ追加ロジック(addMemo)
  7. メモ削除ロジック(deleteMemo)
  8. 新しく追加する:メモ編集ロジック(updateMemo)
  9. メモ一覧の描画:編集 UI を含めて作る(renderMemos)
    1. この描画関数で押さえてほしいポイント
  10. 「メモを追加」ボタンのイベント設定(復習+そのまま)
  11. 初期処理:保存データを読み込んでから描画
  12. 2日目の完成コード(JavaScript全体)
  13. 2日目で本当に掴んでほしいこと
  14. メモ帳アプリ 3日目のゴール
  15. 今日の全体ステップ
  16. 3日目の HTML レイアウト
    1. ここで押さえておきたいポイント
  17. 状態と DOM 参照の定義
  18. localStorage の保存・読み込み(復習)
  19. メモ追加・編集・削除ロジック(2日目の復習)
    1. メモ追加
    2. メモ編集
    3. メモ削除
  20. 3日目の心臓部:検索+並び替え付きの描画(renderMemos)
    1. この関数で本当に掴んでほしいこと
  21. 検索入力欄のイベント設定
  22. 並び順ボタンのイベント設定(querySelectorAll+dataset+classList)
    1. ここで押さえてほしいこと
  23. 「メモを追加」ボタンのイベント設定(復習)
  24. 初期処理:保存データを読み込んでから描画
  25. 3日目の完成コード(JavaScript全体)
  26. 3日目で本当に掴んでほしいこと
  27. メモ帳アプリ 4日目のゴール
  28. 今日の全体ステップ
  29. 4日目の HTML レイアウト
    1. ここで押さえておきたいポイント
  30. 状態と DOM 参照の定義
  31. localStorage の保存・読み込み
  32. 色ラベルの選択値を取得する関数
  33. メモ追加ロジック:タグと色ラベルを含める
  34. メモ編集ロジック:タグと色も編集対象にする
  35. メモ削除ロジック(復習)
  36. 4日目の心臓部:タグ+色ラベル付きの描画(renderMemos)
    1. この関数で本当に掴んでほしいこと
  37. 検索入力欄のイベント設定(復習+タグも対象)
  38. 並び順ボタンのイベント設定(復習)
  39. タグフィルタボタンのイベント設定(querySelectorAll+dataset+classList)
  40. 「メモを追加」ボタンのイベント設定:タグと色を渡す
  41. 初期処理:保存データを読み込んでから描画
  42. 4日目の完成コード(JavaScript全体)
  43. 4日目で本当に掴んでほしいこと
  44. メモ帳アプリ 5日目のゴール
  45. 今日の全体ステップ
  46. 5日目の HTML レイアウト
    1. ここで押さえておきたいポイント
  47. 状態と DOM 参照の定義
  48. localStorage の保存・読み込み
  49. 色ラベルの選択値を取得する関数(復習)
  50. メモ追加ロジック:ピン/アーカイブの初期値を持たせる
  51. メモ編集ロジック:タグ・色はそのまま、ピン/アーカイブは別操作にする
  52. ピン状態を切り替えるロジック
  53. アーカイブ状態を切り替えるロジック
  54. メモ削除ロジック(復習)
  55. 5日目の心臓部:ピン+アーカイブ+表示モード付きの描画(renderMemos)
    1. この関数で本当に掴んでほしいこと
  56. 検索入力欄のイベント設定(復習)
  57. 並び順ボタンのイベント設定(復習)
  58. タグフィルタボタンのイベント設定(復習)
  59. 表示モードフィルタボタンのイベント設定
  60. 「メモを追加」ボタンのイベント設定
  61. 初期処理:保存データを読み込んでから描画
  62. 5日目の完成コード(JavaScript全体)
  63. 5日目で本当に掴んでほしいこと
  64. メモ帳アプリ 6日目のゴール
  65. 今日の全体ステップ
  66. 6日目の HTML レイアウト
    1. ここで押さえておきたいポイント
  67. 状態と DOM 参照の定義
  68. localStorage の保存・読み込み(メモ本体)
  69. 下書きの保存・読み込み
  70. 色ラベルの選択値を取得する関数(復習)
  71. 文字数カウンターの更新
    1. イベントでカウンターを動かす
  72. メモ追加ロジック:下書きのクリアも行う
  73. メモ編集・ピン・アーカイブ・削除ロジック(復習+削除確認)
  74. 全メモ削除ロジック
  75. 6日目の心臓部:描画(renderMemos)
  76. エクスポート機能:JSONを生成してテキストエリアに表示
  77. インポート機能:JSONから復元
  78. イベント設定まとめ
  79. 初期処理
  80. 6日目で本当に掴んでほしいこと
  81. メモ帳アプリ 7日目のゴール
  82. 今日の全体ステップ
  83. アプリの構造をざっくり整理する
  84. よく出てくる処理を小さな関数に切り出す
    1. 例1:ボタンの「アクティブ状態」を切り替える処理
  85. キーボード操作を追加する
    1. Enterキーでメモ追加
    2. Escキーで編集キャンセル
  86. 完成版テンプレート:HTML+JavaScript
    1. 完成版 HTML
  87. 7日目で本当に掴んでほしいこと

メモ帳アプリ 2日目のゴール

2日目のテーマは 「メモを“読むだけ”から、“編集できる”メモ帳に育てる」 ことです。

1日目で作ったメモ帳アプリは、

  • タイトル+本文を入力して追加
  • 一覧表示
  • 削除
  • localStorage に保存

までできる状態でした。

2日目ではここから一歩進めて、

  • メモを「編集」できるようにする
  • 編集用の UI を DOM で切り替える
  • dataset で「どのメモか」を特定する
  • classList で「編集モード」を見た目に反映する

というところまでを、ステップバイステップで作っていきます。

今日の全体ステップ

2日目の流れはこんな感じです。

  1. 1日目の完成コードをベースにする
  2. メモ1件分の表示に「編集ボタン」と「編集用フォーム」を追加する
  3. 「表示モード」と「編集モード」を切り替える処理を書く
  4. 編集内容を配列に反映し、localStorage に保存する
  5. 再描画して編集結果を画面に反映する

1日目のベース(HTML)はそのまま使う

HTML は 1日目と同じで構いません。 JavaScript側で「編集機能」を足していきます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>メモ帳アプリ 2日目</title>
  <style>
    .memo-item {
      border: 1px solid #ccc;
      padding: 8px;
      margin-bottom: 8px;
    }

    .memo-item-title {
      font-weight: bold;
      margin-bottom: 4px;
    }

    .memo-item-body {
      white-space: pre-wrap;
    }

    /* 編集モードのときに背景色を変える例 */
    .editing {
      background-color: #fff7e6;
    }
  </style>
</head>
<body>
  <h1>メモ帳アプリ(2日目)</h1>

  <div>
    <label for="memoTitleInput">タイトル:</label>
    <input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
  </div>

  <div>
    <label for="memoBodyInput">本文:</label><br>
    <textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
  </div>

  <div>
    <button id="addMemoButton">メモを追加</button>
  </div>

  <p id="errorMessage" style="color: red;"></p>

  <h2>メモ一覧</h2>
  <div id="memoList"></div>

  <script>
    // ここに JavaScript を書いていきます
  </script>
</body>
</html>

状態と DOM 参照の定義(1日目と同じ+α)

まずは、アプリの状態と DOM 参照を定義します。

// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;

const STORAGE_KEY = "my_memo_app_data";

// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
JavaScript

ここまでは 1日目と同じです。 この上に「編集機能」を積み上げていきます。

localStorage の保存・読み込み(復習+そのまま使う)

保存関数

// --- ストレージ:状態をlocalStorageに保存する ---
function saveToStorage() {
  const data = {
    memos: memos,
    nextMemoId: nextMemoId
  };

  const json = JSON.stringify(data);
  localStorage.setItem(STORAGE_KEY, json);
}
JavaScript

読み込み関数

// --- ストレージ:localStorageから状態を読み込む ---
function loadFromStorage() {
  const json = localStorage.getItem(STORAGE_KEY);

  if (json === null) {
    return;
  }

  try {
    const data = JSON.parse(json);

    if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
      memos = data.memos;
      nextMemoId = data.nextMemoId;
    }
  } catch (error) {
    console.error("保存データの読み込みに失敗しました:", error);
  }
}
JavaScript

localStorage 周りは 1日目と同じロジックをそのまま使います。 今日は「編集した内容を保存する」場面でこの関数が活躍します。

メモ追加ロジック(addMemo)

追加ロジックも 1日目と同じです。

// --- 操作ロジック:メモを追加する ---
function addMemo(title, body) {
  const newMemo = {
    id: nextMemoId,
    title: title,
    body: body,
    createdAt: Date.now()
  };

  nextMemoId = nextMemoId + 1;

  memos.push(newMemo);

  saveToStorage();
}
JavaScript

メモ削除ロジック(deleteMemo)

削除ロジックも 1日目と同じ形で使えます。

// --- 操作ロジック:メモを削除する ---
function deleteMemo(id) {
  const newMemos = memos.filter(function(memo) {
    return memo.id !== id;
  });

  memos.length = 0;
  newMemos.forEach(function(memo) {
    memos.push(memo);
  });

  saveToStorage();
  renderMemos();
}
JavaScript

新しく追加する:メモ編集ロジック(updateMemo)

2日目の主役となる関数です。 指定した ID のメモのタイトルと本文を更新します。

// --- 操作ロジック:メモを編集する ---
function updateMemo(id, newTitle, newBody) {
  memos.forEach(function(memo) {
    if (memo.id === id) {
      memo.title = newTitle;
      memo.body = newBody;
    }
  });

  // 編集したら保存
  saveToStorage();

  // 新しい状態で画面を作り直す
  renderMemos();
}
JavaScript

深掘りポイント:

  • 「どのメモか」を特定するために id を使う
  • forEach で配列を回しながら、 memo.id === id のものだけを書き換える
  • 編集後は必ず saveToStorage()renderMemos() の流れで 画面と保存内容を同期させる

メモ一覧の描画:編集 UI を含めて作る(renderMemos)

ここが 2日目の一番大事な部分です。 1日目の renderMemos() を「編集機能付き」に書き換えます。

// --- 描画:状態から画面(DOM)を作り直す ---
function renderMemos() {
  // 一覧をいったん空にする
  memoList.innerHTML = "";

  // memos配列の1件ごとにDOMを作る
  memos.forEach(function(memo) {
    // メモ全体を包むdiv
    const memoDiv = document.createElement("div");
    memoDiv.classList.add("memo-item");
    memoDiv.dataset.id = String(memo.id); // このメモのIDを埋め込む

    // --- 表示モード用の要素 ---
    const titleDiv = document.createElement("div");
    titleDiv.classList.add("memo-item-title");
    titleDiv.textContent = memo.title;

    const bodyDiv = document.createElement("div");
    bodyDiv.classList.add("memo-item-body");
    bodyDiv.textContent = memo.body;

    // --- 編集モード用の要素 ---
    const editTitleInput = document.createElement("input");
    editTitleInput.type = "text";
    editTitleInput.value = memo.title;
    editTitleInput.style.display = "none"; // 最初は非表示

    const editBodyTextarea = document.createElement("textarea");
    editBodyTextarea.rows = 4;
    editBodyTextarea.cols = 40;
    editBodyTextarea.value = memo.body;
    editBodyTextarea.style.display = "none"; // 最初は非表示

    // --- ボタン類 ---
    const editButton = document.createElement("button");
    editButton.textContent = "編集";

    const saveButton = document.createElement("button");
    saveButton.textContent = "保存";
    saveButton.style.display = "none"; // 編集モードでだけ表示

    const cancelButton = document.createElement("button");
    cancelButton.textContent = "キャンセル";
    cancelButton.style.display = "none"; // 編集モードでだけ表示

    const deleteButton = document.createElement("button");
    deleteButton.textContent = "削除";

    // --- 編集ボタンのイベント ---
    editButton.addEventListener("click", function() {
      // 表示モードの要素を隠す
      titleDiv.style.display = "none";
      bodyDiv.style.display = "none";

      // 編集モードの要素を表示する
      editTitleInput.style.display = "block";
      editBodyTextarea.style.display = "block";

      // ボタンの表示切り替え
      editButton.style.display = "none";
      deleteButton.style.display = "none";
      saveButton.style.display = "inline-block";
      cancelButton.style.display = "inline-block";

      // 編集中であることを見た目で分かるようにクラスを付ける
      memoDiv.classList.add("editing");

      // 現在の内容を入力欄にセット(念のため)
      editTitleInput.value = memo.title;
      editBodyTextarea.value = memo.body;

      // タイトル入力欄にフォーカス
      editTitleInput.focus();
    });

    // --- 保存ボタンのイベント ---
    saveButton.addEventListener("click", function() {
      const newTitle = editTitleInput.value;
      const newBody = editBodyTextarea.value;

      // タイトルと本文の両方が空ならエラー
      if (newTitle.trim() === "" && newBody.trim() === "") {
        alert("タイトルか本文のどちらかは入力してください。");
        return;
      }

      // IDを取り出す(datasetから)
      const idText = memoDiv.dataset.id;
      const id = Number(idText);

      // 状態を更新して再描画
      updateMemo(id, newTitle, newBody);
    });

    // --- キャンセルボタンのイベント ---
    cancelButton.addEventListener("click", function() {
      // 編集モードをやめて、元の表示に戻す

      // 表示モードの要素を表示
      titleDiv.style.display = "block";
      bodyDiv.style.display = "block";

      // 編集モードの要素を隠す
      editTitleInput.style.display = "none";
      editBodyTextarea.style.display = "none";

      // ボタンの表示切り替え
      editButton.style.display = "inline-block";
      deleteButton.style.display = "inline-block";
      saveButton.style.display = "none";
      cancelButton.style.display = "none";

      // 編集中クラスを外す
      memoDiv.classList.remove("editing");
    });

    // --- 削除ボタンのイベント ---
    deleteButton.addEventListener("click", function(event) {
      const memoElement = event.target.closest(".memo-item");
      const idText = memoElement.dataset.id;
      const id = Number(idText);

      deleteMemo(id);
    });

    // --- memoDivの中に要素を追加する ---
    // 表示モード用
    memoDiv.append(titleDiv);
    memoDiv.append(bodyDiv);

    // 編集モード用
    memoDiv.append(editTitleInput);
    memoDiv.append(editBodyTextarea);

    // ボタン類
    memoDiv.append(editButton);
    memoDiv.append(saveButton);
    memoDiv.append(cancelButton);
    memoDiv.append(deleteButton);

    // memoListの中にmemoDivを追加する
    memoList.append(memoDiv);
  });
}
JavaScript

この描画関数で押さえてほしいポイント

  • 表示モードと編集モードを「別のDOM要素」として用意しておき、style.display で切り替える
  • classList.add("editing") / classList.remove("editing") で、 編集中かどうかを見た目に反映できる
  • dataset.id に ID を埋め込んでおくことで、 ボタンから「どのメモか」を簡単に特定できる
  • 編集後は updateMemo()saveToStorage()renderMemos() の流れで 状態と画面を同期させる

DOM 操作・イベント・配列・localStorage が 1つの関数の中で自然につながっているのが分かると思います。

「メモを追加」ボタンのイベント設定(復習+そのまま)

追加ボタンのイベントは 1日目と同じです。

// --- イベント設定:メモを追加ボタン ---
addMemoButton.addEventListener("click", function() {
  const title = memoTitleInput.value;
  const body = memoBodyInput.value;

  errorMessage.textContent = "";

  if (title.trim() === "" && body.trim() === "") {
    errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
    return;
  }

  addMemo(title, body);

  memoTitleInput.value = "";
  memoBodyInput.value = "";

  renderMemos();
});
JavaScript

初期処理:保存データを読み込んでから描画

最後に、ページ読み込み時の流れを整えます。

// --- 初期処理 ---
loadFromStorage();
renderMemos();
JavaScript

2日目の完成コード(JavaScript全体)

HTML の <script> タグの中身として、 ここまでのコードをひとつにまとめるとこうなります。

<script>
  // --- アプリの状態(State) ---
  let memos = [];
  let nextMemoId = 1;

  const STORAGE_KEY = "my_memo_app_data";

  // --- DOM参照 ---
  const memoTitleInput = document.querySelector("#memoTitleInput");
  const memoBodyInput = document.querySelector("#memoBodyInput");
  const addMemoButton = document.querySelector("#addMemoButton");
  const errorMessage = document.querySelector("#errorMessage");
  const memoList = document.querySelector("#memoList");

  // --- ストレージ:保存 ---
  function saveToStorage() {
    const data = {
      memos: memos,
      nextMemoId: nextMemoId
    };

    const json = JSON.stringify(data);
    localStorage.setItem(STORAGE_KEY, json);
  }

  // --- ストレージ:読み込み ---
  function loadFromStorage() {
    const json = localStorage.getItem(STORAGE_KEY);

    if (json === null) {
      return;
    }

    try {
      const data = JSON.parse(json);

      if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
        memos = data.memos;
        nextMemoId = data.nextMemoId;
      }
    } catch (error) {
      console.error("保存データの読み込みに失敗しました:", error);
    }
  }

  // --- 操作ロジック:メモ追加 ---
  function addMemo(title, body) {
    const newMemo = {
      id: nextMemoId,
      title: title,
      body: body,
      createdAt: Date.now()
    };

    nextMemoId = nextMemoId + 1;

    memos.push(newMemo);

    saveToStorage();
  }

  // --- 操作ロジック:メモ編集 ---
  function updateMemo(id, newTitle, newBody) {
    memos.forEach(function(memo) {
      if (memo.id === id) {
        memo.title = newTitle;
        memo.body = newBody;
      }
    });

    saveToStorage();
    renderMemos();
  }

  // --- 操作ロジック:メモ削除 ---
  function deleteMemo(id) {
    const newMemos = memos.filter(function(memo) {
      return memo.id !== id;
    });

    memos.length = 0;
    newMemos.forEach(function(memo) {
      memos.push(memo);
    });

    saveToStorage();
    renderMemos();
  }

  // --- 描画 ---
  function renderMemos() {
    memoList.innerHTML = "";

    memos.forEach(function(memo) {
      const memoDiv = document.createElement("div");
      memoDiv.classList.add("memo-item");
      memoDiv.dataset.id = String(memo.id);

      const titleDiv = document.createElement("div");
      titleDiv.classList.add("memo-item-title");
      titleDiv.textContent = memo.title;

      const bodyDiv = document.createElement("div");
      bodyDiv.classList.add("memo-item-body");
      bodyDiv.textContent = memo.body;

      const editTitleInput = document.createElement("input");
      editTitleInput.type = "text";
      editTitleInput.value = memo.title;
      editTitleInput.style.display = "none";

      const editBodyTextarea = document.createElement("textarea");
      editBodyTextarea.rows = 4;
      editBodyTextarea.cols = 40;
      editBodyTextarea.value = memo.body;
      editBodyTextarea.style.display = "none";

      const editButton = document.createElement("button");
      editButton.textContent = "編集";

      const saveButton = document.createElement("button");
      saveButton.textContent = "保存";
      saveButton.style.display = "none";

      const cancelButton = document.createElement("button");
      cancelButton.textContent = "キャンセル";
      cancelButton.style.display = "none";

      const deleteButton = document.createElement("button");
      deleteButton.textContent = "削除";

      editButton.addEventListener("click", function() {
        titleDiv.style.display = "none";
        bodyDiv.style.display = "none";

        editTitleInput.style.display = "block";
        editBodyTextarea.style.display = "block";

        editButton.style.display = "none";
        deleteButton.style.display = "none";
        saveButton.style.display = "inline-block";
        cancelButton.style.display = "inline-block";

        memoDiv.classList.add("editing");

        editTitleInput.value = memo.title;
        editBodyTextarea.value = memo.body;
        editTitleInput.focus();
      });

      saveButton.addEventListener("click", function() {
        const newTitle = editTitleInput.value;
        const newBody = editBodyTextarea.value;

        if (newTitle.trim() === "" && newBody.trim() === "") {
          alert("タイトルか本文のどちらかは入力してください。");
          return;
        }

        const idText = memoDiv.dataset.id;
        const id = Number(idText);

        updateMemo(id, newTitle, newBody);
      });

      cancelButton.addEventListener("click", function() {
        titleDiv.style.display = "block";
        bodyDiv.style.display = "block";

        editTitleInput.style.display = "none";
        editBodyTextarea.style.display = "none";

        editButton.style.display = "inline-block";
        deleteButton.style.display = "inline-block";
        saveButton.style.display = "none";
        cancelButton.style.display = "none";

        memoDiv.classList.remove("editing");
      });

      deleteButton.addEventListener("click", function(event) {
        const memoElement = event.target.closest(".memo-item");
        const idText = memoElement.dataset.id;
        const id = Number(idText);

        deleteMemo(id);
      });

      memoDiv.append(titleDiv);
      memoDiv.append(bodyDiv);
      memoDiv.append(editTitleInput);
      memoDiv.append(editBodyTextarea);
      memoDiv.append(editButton);
      memoDiv.append(saveButton);
      memoDiv.append(cancelButton);
      memoDiv.append(deleteButton);

      memoList.append(memoDiv);
    });
  }

  // --- イベント設定:メモ追加ボタン ---
  addMemoButton.addEventListener("click", function() {
    const title = memoTitleInput.value;
    const body = memoBodyInput.value;

    errorMessage.textContent = "";

    if (title.trim() === "" && body.trim() === "") {
      errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
      return;
    }

    addMemo(title, body);

    memoTitleInput.value = "";
    memoBodyInput.value = "";

    renderMemos();
  });

  // --- 初期処理 ---
  loadFromStorage();
  renderMemos();
</script>

2日目で本当に掴んでほしいこと

  • DOM要素を「表示用」と「編集用」に分けて、style.displayclassList で切り替えると、UIの状態管理がぐっと楽になる
  • dataset に ID を埋め込んでおくことで、ボタンから「どのデータか」を簡単に特定できる
  • 配列(状態)→ localStorage(保存)→ DOM(描画)→ イベント(操作)という流れが、メモ帳アプリでも自然に回り始めている

この構造を一度掴んでしまえば、 ToDoアプリでも、メモ帳でも、家計簿でも、 「編集できる UI」を自分で設計していけるようになります。


メモ帳アプリ 3日目のゴール

3日目のテーマは 「メモが増えても“探せる・整理できる”メモ帳にする」 ことです。

1〜2日目で作ったメモ帳アプリは、

  • メモの追加
  • 編集
  • 削除
  • localStorage に保存

までできる状態でした。

3日目ではここから一歩進めて、

  • キーワードでメモを「検索」できる
  • 並び順(新しい順/古い順)を切り替えられる
  • querySelectorAll()datasetclassList を使って 「複数ボタンで状態を切り替える」パターンを身につける

というところまでを、ステップバイステップで作っていきます。

今日の全体ステップ

3日目の流れはこんな感じです。

  1. 検索用の入力欄と並び順ボタンを HTML に追加する
  2. JavaScript で検索キーワードと並び順モードを状態として持つ
  3. renderMemos() の中で
    • キーワードでフィルタ
    • 並び順モードでソート を行う
  4. 検索入力欄の input イベントで再描画する
  5. 並び順ボタンを querySelectorAll() でまとめて取得し、 datasetclassList で切り替える

3日目の HTML レイアウト

2日目までの HTML に、 「検索」と「並び順」の UI を追加します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>メモ帳アプリ 3日目</title>
  <style>
    .memo-item {
      border: 1px solid #ccc;
      padding: 8px;
      margin-bottom: 8px;
    }

    .memo-item-title {
      font-weight: bold;
      margin-bottom: 4px;
    }

    .memo-item-body {
      white-space: pre-wrap;
    }

    .editing {
      background-color: #fff7e6;
    }

    /* 並び順ボタンの選択状態を見た目で分かるようにする */
    .active-sort {
      background-color: #007bff;
      color: white;
    }
  </style>
</head>
<body>
  <h1>メモ帳アプリ(3日目)</h1>

  <div>
    <label for="memoTitleInput">タイトル:</label>
    <input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
  </div>

  <div>
    <label for="memoBodyInput">本文:</label><br>
    <textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
  </div>

  <div>
    <button id="addMemoButton">メモを追加</button>
  </div>

  <p id="errorMessage" style="color: red;"></p>

  <h2>検索</h2>
  <div>
    <input id="searchInput" type="text" placeholder="タイトル・本文から検索">
  </div>

  <h2>並び順</h2>
  <div id="sortButtons">
    <button data-sort="newest" class="active-sort">新しい順</button>
    <button data-sort="oldest">古い順</button>
  </div>

  <h2>メモ一覧</h2>
  <div id="memoList"></div>

  <script>
    // ここに JavaScript を書いていきます
  </script>
</body>
</html>

ここで押さえておきたいポイント

  • 検索用に #searchInput を追加
  • 並び順ボタンを #sortButtons の中に2つ配置
    • data-sort="newest"
    • data-sort="oldest"
  • 「今選ばれている並び順」を見た目で分かるように .active-sort クラスを用意

この「data属性+クラス+複数ボタン」の組み合わせが、 3日目の重要な練習ポイントです。

状態と DOM 参照の定義

3日目では、 「検索キーワード」と「並び順モード」を状態として追加します。

// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;

// 検索キーワード(空文字なら全件表示)
let currentKeyword = "";

// 並び順モード("newest" または "oldest")
let currentSort = "newest";

const STORAGE_KEY = "my_memo_app_data";

// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");

// 検索入力欄
const searchInput = document.querySelector("#searchInput");

// 並び順ボタン(複数)をまとめて取得
const sortButtons = document.querySelectorAll("#sortButtons button");
JavaScript

深掘りポイント:

  • 「検索キーワード」や「並び順モード」も、 立派な「アプリの状態」です
  • querySelectorAll() で複数ボタンをまとめて取得しておくと、 forEach() で一括処理できる

localStorage の保存・読み込み(復習)

検索キーワードや並び順は「一時的な状態」として扱い、 localStorage には保存しません。 保存するのはメモ一覧と次のIDだけです。

// --- ストレージ:状態をlocalStorageに保存する ---
function saveToStorage() {
  const data = {
    memos: memos,
    nextMemoId: nextMemoId
  };

  const json = JSON.stringify(data);
  localStorage.setItem(STORAGE_KEY, json);
}

// --- ストレージ:localStorageから状態を読み込む ---
function loadFromStorage() {
  const json = localStorage.getItem(STORAGE_KEY);

  if (json === null) {
    return;
  }

  try {
    const data = JSON.parse(json);

    if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
      memos = data.memos;
      nextMemoId = data.nextMemoId;
    }
  } catch (error) {
    console.error("保存データの読み込みに失敗しました:", error);
  }
}
JavaScript

メモ追加・編集・削除ロジック(2日目の復習)

メモ追加

// --- 操作ロジック:メモを追加する ---
function addMemo(title, body) {
  const newMemo = {
    id: nextMemoId,
    title: title,
    body: body,
    createdAt: Date.now()
  };

  nextMemoId = nextMemoId + 1;

  memos.push(newMemo);

  saveToStorage();
}
JavaScript

メモ編集

// --- 操作ロジック:メモを編集する ---
function updateMemo(id, newTitle, newBody) {
  memos.forEach(function(memo) {
    if (memo.id === id) {
      memo.title = newTitle;
      memo.body = newBody;
    }
  });

  saveToStorage();
  renderMemos();
}
JavaScript

メモ削除

// --- 操作ロジック:メモを削除する ---
function deleteMemo(id) {
  const newMemos = memos.filter(function(memo) {
    return memo.id !== id;
  });

  memos.length = 0;
  newMemos.forEach(function(memo) {
    memos.push(memo);
  });

  saveToStorage();
  renderMemos();
}
JavaScript

3日目の心臓部:検索+並び替え付きの描画(renderMemos)

ここが今日の一番大事な部分です。 renderMemos() の中で、

  1. 検索キーワードでフィルタ
  2. 並び順モードでソート
  3. DOM を生成

という流れを作ります。

// --- 描画:状態から画面(DOM)を作り直す ---
function renderMemos() {
  // 一覧をいったん空にする
  memoList.innerHTML = "";

  // 1. 検索キーワードでフィルタする
  let visibleMemos = memos;

  if (currentKeyword.trim() !== "") {
    const keywordLower = currentKeyword.toLowerCase();

    visibleMemos = memos.filter(function(memo) {
      // タイトルと本文を小文字にしてから検索
      const titleLower = memo.title.toLowerCase();
      const bodyLower = memo.body.toLowerCase();

      // キーワードがタイトルか本文に含まれているか
      return (
        titleLower.includes(keywordLower) ||
        bodyLower.includes(keywordLower)
      );
    });
  }

  // 2. 並び替えのためにコピーを作る(元の配列を壊さない)
  visibleMemos = visibleMemos.slice();

  // 3. 並び順モードに応じてソートする
  if (currentSort === "newest") {
    // 新しい順(createdAtが大きいものを前に)
    visibleMemos.sort(function(a, b) {
      return b.createdAt - a.createdAt;
    });
  } else if (currentSort === "oldest") {
    // 古い順(createdAtが小さいものを前に)
    visibleMemos.sort(function(a, b) {
      return a.createdAt - b.createdAt;
    });
  }

  // 4. ソート済みのvisibleMemosをもとにDOMを作る
  visibleMemos.forEach(function(memo) {
    const memoDiv = document.createElement("div");
    memoDiv.classList.add("memo-item");
    memoDiv.dataset.id = String(memo.id);

    // --- 表示モード用の要素 ---
    const titleDiv = document.createElement("div");
    titleDiv.classList.add("memo-item-title");
    titleDiv.textContent = memo.title;

    const bodyDiv = document.createElement("div");
    bodyDiv.classList.add("memo-item-body");
    bodyDiv.textContent = memo.body;

    // --- 編集モード用の要素 ---
    const editTitleInput = document.createElement("input");
    editTitleInput.type = "text";
    editTitleInput.value = memo.title;
    editTitleInput.style.display = "none";

    const editBodyTextarea = document.createElement("textarea");
    editBodyTextarea.rows = 4;
    editBodyTextarea.cols = 40;
    editBodyTextarea.value = memo.body;
    editBodyTextarea.style.display = "none";

    // --- ボタン類 ---
    const editButton = document.createElement("button");
    editButton.textContent = "編集";

    const saveButton = document.createElement("button");
    saveButton.textContent = "保存";
    saveButton.style.display = "none";

    const cancelButton = document.createElement("button");
    cancelButton.textContent = "キャンセル";
    cancelButton.style.display = "none";

    const deleteButton = document.createElement("button");
    deleteButton.textContent = "削除";

    // --- 編集ボタンのイベント ---
    editButton.addEventListener("click", function() {
      titleDiv.style.display = "none";
      bodyDiv.style.display = "none";

      editTitleInput.style.display = "block";
      editBodyTextarea.style.display = "block";

      editButton.style.display = "none";
      deleteButton.style.display = "none";
      saveButton.style.display = "inline-block";
      cancelButton.style.display = "inline-block";

      memoDiv.classList.add("editing");

      editTitleInput.value = memo.title;
      editBodyTextarea.value = memo.body;
      editTitleInput.focus();
    });

    // --- 保存ボタンのイベント ---
    saveButton.addEventListener("click", function() {
      const newTitle = editTitleInput.value;
      const newBody = editBodyTextarea.value;

      if (newTitle.trim() === "" && newBody.trim() === "") {
        alert("タイトルか本文のどちらかは入力してください。");
        return;
      }

      const idText = memoDiv.dataset.id;
      const id = Number(idText);

      updateMemo(id, newTitle, newBody);
    });

    // --- キャンセルボタンのイベント ---
    cancelButton.addEventListener("click", function() {
      titleDiv.style.display = "block";
      bodyDiv.style.display = "block";

      editTitleInput.style.display = "none";
      editBodyTextarea.style.display = "none";

      editButton.style.display = "inline-block";
      deleteButton.style.display = "inline-block";
      saveButton.style.display = "none";
      cancelButton.style.display = "none";

      memoDiv.classList.remove("editing");
    });

    // --- 削除ボタンのイベント ---
    deleteButton.addEventListener("click", function(event) {
      const memoElement = event.target.closest(".memo-item");
      const idText = memoElement.dataset.id;
      const id = Number(idText);

      deleteMemo(id);
    });

    // --- memoDivの中に要素を追加する ---
    memoDiv.append(titleDiv);
    memoDiv.append(bodyDiv);
    memoDiv.append(editTitleInput);
    memoDiv.append(editBodyTextarea);
    memoDiv.append(editButton);
    memoDiv.append(saveButton);
    memoDiv.append(cancelButton);
    memoDiv.append(deleteButton);

    memoList.append(memoDiv);
  });
}
JavaScript

この関数で本当に掴んでほしいこと

  • 「状態 → フィルタ → コピー → ソート → DOM生成」という流れが、 検索・並び替え付きのアプリの基本パターンになる
  • 検索キーワードは toLowerCase() で小文字に揃えてから includes() で判定すると、 大文字・小文字の違いを気にせず検索できる
  • 並び順は「状態(currentSort)」と「ボタンの見た目(classList)」を きちんと分けて考えると整理しやすい

検索入力欄のイベント設定

検索欄に文字が入力されるたびに、 currentKeyword を更新して renderMemos() を呼びます。

// --- イベント設定:検索入力欄 ---
searchInput.addEventListener("input", function() {
  // 入力されたキーワードを状態に保存
  currentKeyword = searchInput.value;

  // キーワードが変わったので、表示を更新
  renderMemos();
});
JavaScript

ポイント:

  • input イベントは「文字が入力されるたび」に発火する
  • 検索は「状態を変えて再描画する」だけで実現できる

並び順ボタンのイベント設定(querySelectorAll+dataset+classList)

ここで querySelectorAll()datasetclassList を フル活用します。

// --- イベント設定:並び順ボタン ---
sortButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    // ボタンに埋め込んでおいた並び順モードを取り出す
    const sortMode = button.dataset.sort; // "newest" または "oldest"

    // 状態を更新
    currentSort = sortMode;

    // 全ての並び順ボタンから「選択中クラス」を外す
    sortButtons.forEach(function(b) {
      b.classList.remove("active-sort");
    });

    // クリックされたボタンだけ「選択中クラス」を付ける
    button.classList.add("active-sort");

    // 新しい並び順で画面を作り直す
    renderMemos();
  });
});
JavaScript

ここで押さえてほしいこと

  • querySelectorAll() で複数ボタンをまとめて取得し、forEach() で一括処理する
  • dataset.sort に「このボタンが表す意味(newest / oldest)」を埋め込んでおくと、 イベントの中で簡単に取り出せる
  • classList.remove() / classList.add() で「選択中のボタン」を見た目で表現する

このパターンは、 タブ切り替え・フィルタボタン・モード切り替えなど、 いろいろな UI で使える強力な型です。

「メモを追加」ボタンのイベント設定(復習)

追加ボタンは 2日目と同じです。

// --- イベント設定:メモを追加ボタン ---
addMemoButton.addEventListener("click", function() {
  const title = memoTitleInput.value;
  const body = memoBodyInput.value;

  errorMessage.textContent = "";

  if (title.trim() === "" && body.trim() === "") {
    errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
    return;
  }

  addMemo(title, body);

  memoTitleInput.value = "";
  memoBodyInput.value = "";

  renderMemos();
});
JavaScript

初期処理:保存データを読み込んでから描画

最後に、ページ読み込み時の流れを整えます。

// --- 初期処理 ---
loadFromStorage();
renderMemos();
JavaScript

3日目の完成コード(JavaScript全体)

HTML の <script> タグの中身として、 ここまでのコードをひとつにまとめるとこうなります。

<script>
  // --- アプリの状態(State) ---
  let memos = [];
  let nextMemoId = 1;

  let currentKeyword = "";
  let currentSort = "newest";

  const STORAGE_KEY = "my_memo_app_data";

  // --- DOM参照 ---
  const memoTitleInput = document.querySelector("#memoTitleInput");
  const memoBodyInput = document.querySelector("#memoBodyInput");
  const addMemoButton = document.querySelector("#addMemoButton");
  const errorMessage = document.querySelector("#errorMessage");
  const memoList = document.querySelector("#memoList");

  const searchInput = document.querySelector("#searchInput");
  const sortButtons = document.querySelectorAll("#sortButtons button");

  // --- ストレージ:保存 ---
  function saveToStorage() {
    const data = {
      memos: memos,
      nextMemoId: nextMemoId
    };

    const json = JSON.stringify(data);
    localStorage.setItem(STORAGE_KEY, json);
  }

  // --- ストレージ:読み込み ---
  function loadFromStorage() {
    const json = localStorage.getItem(STORAGE_KEY);

    if (json === null) {
      return;
    }

    try {
      const data = JSON.parse(json);

      if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
        memos = data.memos;
        nextMemoId = data.nextMemoId;
      }
    } catch (error) {
      console.error("保存データの読み込みに失敗しました:", error);
    }
  }

  // --- 操作ロジック:メモ追加 ---
  function addMemo(title, body) {
    const newMemo = {
      id: nextMemoId,
      title: title,
      body: body,
      createdAt: Date.now()
    };

    nextMemoId = nextMemoId + 1;

    memos.push(newMemo);

    saveToStorage();
  }

  // --- 操作ロジック:メモ編集 ---
  function updateMemo(id, newTitle, newBody) {
    memos.forEach(function(memo) {
      if (memo.id === id) {
        memo.title = newTitle;
        memo.body = newBody;
      }
    });

    saveToStorage();
    renderMemos();
  }

  // --- 操作ロジック:メモ削除 ---
  function deleteMemo(id) {
    const newMemos = memos.filter(function(memo) {
      return memo.id !== id;
    });

    memos.length = 0;
    newMemos.forEach(function(memo) {
      memos.push(memo);
    });

    saveToStorage();
    renderMemos();
  }

  // --- 描画 ---
  function renderMemos() {
    memoList.innerHTML = "";

    let visibleMemos = memos;

    if (currentKeyword.trim() !== "") {
      const keywordLower = currentKeyword.toLowerCase();

      visibleMemos = memos.filter(function(memo) {
        const titleLower = memo.title.toLowerCase();
        const bodyLower = memo.body.toLowerCase();

        return (
          titleLower.includes(keywordLower) ||
          bodyLower.includes(keywordLower)
        );
      });
    }

    visibleMemos = visibleMemos.slice();

    if (currentSort === "newest") {
      visibleMemos.sort(function(a, b) {
        return b.createdAt - a.createdAt;
      });
    } else if (currentSort === "oldest") {
      visibleMemos.sort(function(a, b) {
        return a.createdAt - b.createdAt;
      });
    }

    visibleMemos.forEach(function(memo) {
      const memoDiv = document.createElement("div");
      memoDiv.classList.add("memo-item");
      memoDiv.dataset.id = String(memo.id);

      const titleDiv = document.createElement("div");
      titleDiv.classList.add("memo-item-title");
      titleDiv.textContent = memo.title;

      const bodyDiv = document.createElement("div");
      bodyDiv.classList.add("memo-item-body");
      bodyDiv.textContent = memo.body;

      const editTitleInput = document.createElement("input");
      editTitleInput.type = "text";
      editTitleInput.value = memo.title;
      editTitleInput.style.display = "none";

      const editBodyTextarea = document.createElement("textarea");
      editBodyTextarea.rows = 4;
      editBodyTextarea.cols = 40;
      editBodyTextarea.value = memo.body;
      editBodyTextarea.style.display = "none";

      const editButton = document.createElement("button");
      editButton.textContent = "編集";

      const saveButton = document.createElement("button");
      saveButton.textContent = "保存";
      saveButton.style.display = "none";

      const cancelButton = document.createElement("button");
      cancelButton.textContent = "キャンセル";
      cancelButton.style.display = "none";

      const deleteButton = document.createElement("button");
      deleteButton.textContent = "削除";

      editButton.addEventListener("click", function() {
        titleDiv.style.display = "none";
        bodyDiv.style.display = "none";

        editTitleInput.style.display = "block";
        editBodyTextarea.style.display = "block";

        editButton.style.display = "none";
        deleteButton.style.display = "none";
        saveButton.style.display = "inline-block";
        cancelButton.style.display = "inline-block";

        memoDiv.classList.add("editing");

        editTitleInput.value = memo.title;
        editBodyTextarea.value = memo.body;
        editTitleInput.focus();
      });

      saveButton.addEventListener("click", function() {
        const newTitle = editTitleInput.value;
        const newBody = editBodyTextarea.value;

        if (newTitle.trim() === "" && newBody.trim() === "") {
          alert("タイトルか本文のどちらかは入力してください。");
          return;
        }

        const idText = memoDiv.dataset.id;
        const id = Number(idText);

        updateMemo(id, newTitle, newBody);
      });

      cancelButton.addEventListener("click", function() {
        titleDiv.style.display = "block";
        bodyDiv.style.display = "block";

        editTitleInput.style.display = "none";
        editBodyTextarea.style.display = "none";

        editButton.style.display = "inline-block";
        deleteButton.style.display = "inline-block";
        saveButton.style.display = "none";
        cancelButton.style.display = "none";

        memoDiv.classList.remove("editing");
      });

      deleteButton.addEventListener("click", function(event) {
        const memoElement = event.target.closest(".memo-item");
        const idText = memoElement.dataset.id;
        const id = Number(idText);

        deleteMemo(id);
      });

      memoDiv.append(titleDiv);
      memoDiv.append(bodyDiv);
      memoDiv.append(editTitleInput);
      memoDiv.append(editBodyTextarea);
      memoDiv.append(editButton);
      memoDiv.append(saveButton);
      memoDiv.append(cancelButton);
      memoDiv.append(deleteButton);

      memoList.append(memoDiv);
    });
  }

  // --- イベント設定:メモ追加ボタン ---
  addMemoButton.addEventListener("click", function() {
    const title = memoTitleInput.value;
    const body = memoBodyInput.value;

    errorMessage.textContent = "";

    if (title.trim() === "" && body.trim() === "") {
      errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
      return;
    }

    addMemo(title, body);

    memoTitleInput.value = "";
    memoBodyInput.value = "";

    renderMemos();
  });

  // --- イベント設定:検索入力欄 ---
  searchInput.addEventListener("input", function() {
    currentKeyword = searchInput.value;
    renderMemos();
  });

  // --- イベント設定:並び順ボタン ---
  sortButtons.forEach(function(button) {
    button.addEventListener("click", function() {
      const sortMode = button.dataset.sort;

      currentSort = sortMode;

      sortButtons.forEach(function(b) {
        b.classList.remove("active-sort");
      });

      button.classList.add("active-sort");

      renderMemos();
    });
  });

  // --- 初期処理 ---
  loadFromStorage();
  renderMemos();
</script>

3日目で本当に掴んでほしいこと

  • 検索・並び替えは「状態を変えて再描画する」だけで実現できる
  • querySelectorAll()datasetclassList の組み合わせは、 複数ボタンでモードを切り替える UI の基本パターンになる
  • 配列(状態)→ localStorage(保存)→ DOM(描画)→ イベント(操作)という流れが、 メモ帳アプリの中で自然に回り始めている

この構造を一度体に入れてしまえば、 ToDoアプリでも、メモ帳でも、家計簿でも、 「検索・並び替え付きのアプリ」を自分で設計していけるようになります。


メモ帳アプリ 4日目のゴール

4日目のテーマは 「メモに“意味”を持たせて、タグや色で整理できるメモ帳にする」 ことです。

1〜3日目で作ったメモ帳アプリは、

  • 追加・編集・削除
  • 検索
  • 並び替え(新しい順/古い順)
  • localStorage に保存

までできる状態でした。

4日目ではここから一歩進めて、

  • メモに「タグ」と「色ラベル」を付けられる
  • タグで絞り込み表示ができる
  • 色ラベルを見た目に反映する
  • querySelectorAll()datasetclassList を 「タグフィルタボタン」にも応用する

というところまでを、ステップバイステップで作っていきます。

今日の全体ステップ

4日目の流れはこんな感じです。

  1. メモ追加フォームに「タグ」と「色ラベル」を追加する
  2. メモオブジェクトに tagcolor プロパティを追加する
  3. メモ一覧の表示にタグと色ラベルを反映する
  4. タグフィルタボタンを HTML に追加し、querySelectorAll() で取得する
  5. タグフィルタの状態を持ち、renderMemos() の中でフィルタする
  6. classListdataset で「選択中のタグ」を見た目に表現する

4日目の HTML レイアウト

3日目までの HTML に、 「タグ入力」と「色ラベル選択」と「タグフィルタ」を追加します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>メモ帳アプリ 4日目</title>
  <style>
    .memo-item {
      border: 1px solid #ccc;
      padding: 8px;
      margin-bottom: 8px;
    }

    .memo-item-title {
      font-weight: bold;
      margin-bottom: 4px;
    }

    .memo-item-body {
      white-space: pre-wrap;
    }

    .memo-meta {
      font-size: 12px;
      color: #666;
      margin-top: 4px;
    }

    .editing {
      background-color: #fff7e6;
    }

    /* 並び順ボタンの選択状態 */
    .active-sort {
      background-color: #007bff;
      color: white;
    }

    /* タグフィルタボタンの選択状態 */
    .active-tag {
      background-color: #28a745;
      color: white;
    }

    /* 色ラベル用のクラス */
    .color-red {
      border-left: 4px solid #ff4d4f;
    }

    .color-blue {
      border-left: 4px solid #1890ff;
    }

    .color-yellow {
      border-left: 4px solid #faad14;
    }

    .color-none {
      border-left: 4px solid transparent;
    }
  </style>
</head>
<body>
  <h1>メモ帳アプリ(4日目)</h1>

  <div>
    <label for="memoTitleInput">タイトル:</label>
    <input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
  </div>

  <div>
    <label for="memoBodyInput">本文:</label><br>
    <textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
  </div>

  <div>
    <label for="memoTagInput">タグ:</label>
    <input id="memoTagInput" type="text" placeholder="例:仕事 / プライベート / アイデア">
  </div>

  <div>
    <span>色ラベル:</span>
    <label>
      <input type="radio" name="memoColor" value="none" checked> なし
    </label>
    <label>
      <input type="radio" name="memoColor" value="red"> 赤
    </label>
    <label>
      <input type="radio" name="memoColor" value="blue"> 青
    </label>
    <label>
      <input type="radio" name="memoColor" value="yellow"> 黄
    </label>
  </div>

  <div>
    <button id="addMemoButton">メモを追加</button>
  </div>

  <p id="errorMessage" style="color: red;"></p>

  <h2>検索</h2>
  <div>
    <input id="searchInput" type="text" placeholder="タイトル・本文・タグから検索">
  </div>

  <h2>並び順</h2>
  <div id="sortButtons">
    <button data-sort="newest" class="active-sort">新しい順</button>
    <button data-sort="oldest">古い順</button>
  </div>

  <h2>タグフィルタ</h2>
  <div id="tagFilterButtons">
    <button data-tag="" class="active-tag">すべて</button>
    <button data-tag="仕事">仕事</button>
    <button data-tag="プライベート">プライベート</button>
    <button data-tag="アイデア">アイデア</button>
  </div>

  <h2>メモ一覧</h2>
  <div id="memoList"></div>

  <script>
    // ここに JavaScript を書いていきます
  </script>
</body>
</html>

ここで押さえておきたいポイント

  • メモ追加フォームに #memoTagInput と「色ラベル用のラジオボタン」を追加
  • タグフィルタ用に #tagFilterButtons を用意し、 各ボタンに data-tag 属性でタグ名を埋め込む
  • 「すべて」ボタンは data-tag="" として、 「タグフィルタなし」を表現する

タグフィルタボタンは、 3日目の「並び順ボタン」とほぼ同じパターンで扱います。

状態と DOM 参照の定義

4日目では、 「タグフィルタモード」と「色ラベル」を状態として扱います。

// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;

// 検索キーワード
let currentKeyword = "";

// 並び順モード("newest" / "oldest")
let currentSort = "newest";

// タグフィルタ(空文字なら「すべて」)
let currentTagFilter = "";

const STORAGE_KEY = "my_memo_app_data";

// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const memoTagInput = document.querySelector("#memoTagInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");

const searchInput = document.querySelector("#searchInput");
const sortButtons = document.querySelectorAll("#sortButtons button");
const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");

// 色ラベル用のラジオボタン(複数)
const memoColorRadios = document.querySelectorAll("input[name='memoColor']");
JavaScript

深掘りポイント:

  • 「タグフィルタ」も立派な状態
  • 色ラベルは「メモごとのプロパティ」として保存する
  • querySelectorAll() で複数ボタン・複数ラジオをまとめて扱う

localStorage の保存・読み込み

メモオブジェクトに tagcolor を追加するので、 保存・読み込みはそのままでも動きます(JSONがオブジェクトをそのまま保持してくれるため)。

// --- ストレージ:状態をlocalStorageに保存する ---
function saveToStorage() {
  const data = {
    memos: memos,
    nextMemoId: nextMemoId
  };

  const json = JSON.stringify(data);
  localStorage.setItem(STORAGE_KEY, json);
}

// --- ストレージ:localStorageから状態を読み込む ---
function loadFromStorage() {
  const json = localStorage.getItem(STORAGE_KEY);

  if (json === null) {
    return;
  }

  try {
    const data = JSON.parse(json);

    if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
      memos = data.memos;
      nextMemoId = data.nextMemoId;
    }
  } catch (error) {
    console.error("保存データの読み込みに失敗しました:", error);
  }
}
JavaScript

色ラベルの選択値を取得する関数

ラジオボタンから「選ばれている色ラベル」を取り出す小さな関数を作ります。

// --- ユーティリティ:選択されている色ラベルを取得する ---
function getSelectedColor() {
  let selectedValue = "none";

  memoColorRadios.forEach(function(radio) {
    if (radio.checked) {
      selectedValue = radio.value; // "none" / "red" / "blue" / "yellow"
    }
  });

  return selectedValue;
}
JavaScript

ポイント:

  • ラジオボタンは「同じ name を持つ複数の input」の中で どれか1つだけが checked になる
  • querySelectorAll()forEach() で「どれが選ばれているか」を調べる

メモ追加ロジック:タグと色ラベルを含める

メモオブジェクトに tagcolor を追加します。

// --- 操作ロジック:メモを追加する ---
function addMemo(title, body, tag, color) {
  const newMemo = {
    id: nextMemoId,
    title: title,
    body: body,
    tag: tag,        // タグ(文字列)
    color: color,    // 色ラベル("none" / "red" / "blue" / "yellow")
    createdAt: Date.now()
  };

  nextMemoId = nextMemoId + 1;

  memos.push(newMemo);

  saveToStorage();
}
JavaScript

深掘りポイント:

  • メモの「意味」を持たせるために、 タグや色ラベルをオブジェクトのプロパティとして持たせる
  • 後でフィルタや見た目に使えるように、 シンプルな文字列で持っておく

メモ編集ロジック:タグと色も編集対象にする

編集時にもタグと色を更新できるようにします。 (ここでは「編集フォームにタグと色も含める」形にします)

// --- 操作ロジック:メモを編集する ---
function updateMemo(id, newTitle, newBody, newTag, newColor) {
  memos.forEach(function(memo) {
    if (memo.id === id) {
      memo.title = newTitle;
      memo.body = newBody;
      memo.tag = newTag;
      memo.color = newColor;
    }
  });

  saveToStorage();
  renderMemos();
}
JavaScript

メモ削除ロジック(復習)

削除ロジックはそのまま使えます。

// --- 操作ロジック:メモを削除する ---
function deleteMemo(id) {
  const newMemos = memos.filter(function(memo) {
    return memo.id !== id;
  });

  memos.length = 0;
  newMemos.forEach(function(memo) {
    memos.push(memo);
  });

  saveToStorage();
  renderMemos();
}
JavaScript

4日目の心臓部:タグ+色ラベル付きの描画(renderMemos)

ここが今日の一番大事な部分です。 renderMemos() の中で、

  1. 検索キーワードでフィルタ
  2. タグフィルタでさらに絞り込み
  3. 並び順モードでソート
  4. DOM を生成(タグと色ラベルを反映)

という流れを作ります。

// --- 描画:状態から画面(DOM)を作り直す ---
function renderMemos() {
  memoList.innerHTML = "";

  let visibleMemos = memos;

  // 1. 検索キーワードでフィルタ(タイトル・本文・タグ)
  if (currentKeyword.trim() !== "") {
    const keywordLower = currentKeyword.toLowerCase();

    visibleMemos = visibleMemos.filter(function(memo) {
      const titleLower = memo.title.toLowerCase();
      const bodyLower = memo.body.toLowerCase();
      const tagLower = (memo.tag || "").toLowerCase();

      return (
        titleLower.includes(keywordLower) ||
        bodyLower.includes(keywordLower) ||
        tagLower.includes(keywordLower)
      );
    });
  }

  // 2. タグフィルタで絞り込み
  if (currentTagFilter !== "") {
    visibleMemos = visibleMemos.filter(function(memo) {
      return memo.tag === currentTagFilter;
    });
  }

  // 3. 並び替えのためにコピーを作る
  visibleMemos = visibleMemos.slice();

  if (currentSort === "newest") {
    visibleMemos.sort(function(a, b) {
      return b.createdAt - a.createdAt;
    });
  } else if (currentSort === "oldest") {
    visibleMemos.sort(function(a, b) {
      return a.createdAt - b.createdAt;
    });
  }

  // 4. ソート済みのvisibleMemosをもとにDOMを作る
  visibleMemos.forEach(function(memo) {
    const memoDiv = document.createElement("div");
    memoDiv.classList.add("memo-item");
    memoDiv.dataset.id = String(memo.id);

    // 色ラベルに応じてクラスを付ける
    const colorClass = "color-" + (memo.color || "none");
    memoDiv.classList.add(colorClass);

    // --- 表示モード用の要素 ---
    const titleDiv = document.createElement("div");
    titleDiv.classList.add("memo-item-title");
    titleDiv.textContent = memo.title;

    const bodyDiv = document.createElement("div");
    bodyDiv.classList.add("memo-item-body");
    bodyDiv.textContent = memo.body;

    const metaDiv = document.createElement("div");
    metaDiv.classList.add("memo-meta");
    metaDiv.textContent = `タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"}`;

    // --- 編集モード用の要素 ---
    const editTitleInput = document.createElement("input");
    editTitleInput.type = "text";
    editTitleInput.value = memo.title;
    editTitleInput.style.display = "none";

    const editBodyTextarea = document.createElement("textarea");
    editBodyTextarea.rows = 4;
    editBodyTextarea.cols = 40;
    editBodyTextarea.value = memo.body;
    editBodyTextarea.style.display = "none";

    const editTagInput = document.createElement("input");
    editTagInput.type = "text";
    editTagInput.value = memo.tag || "";
    editTagInput.style.display = "none";

    // 色ラベル編集用のラジオボタン群(メモごとに用意)
    const editColorContainer = document.createElement("div");
    editColorContainer.style.display = "none";

    const colors = ["none", "red", "blue", "yellow"];

    const editColorRadios = [];

    colors.forEach(function(color) {
      const label = document.createElement("label");
      const radio = document.createElement("input");
      radio.type = "radio";
      radio.name = "editColor-" + memo.id; // メモごとに名前を変える
      radio.value = color;

      if (memo.color === color || (!memo.color && color === "none")) {
        radio.checked = true;
      }

      label.append(radio);
      label.append(document.createTextNode(" " + color));

      editColorContainer.append(label);

      editColorRadios.push(radio);
    });

    // --- ボタン類 ---
    const editButton = document.createElement("button");
    editButton.textContent = "編集";

    const saveButton = document.createElement("button");
    saveButton.textContent = "保存";
    saveButton.style.display = "none";

    const cancelButton = document.createElement("button");
    cancelButton.textContent = "キャンセル";
    cancelButton.style.display = "none";

    const deleteButton = document.createElement("button");
    deleteButton.textContent = "削除";

    // --- 編集ボタンのイベント ---
    editButton.addEventListener("click", function() {
      titleDiv.style.display = "none";
      bodyDiv.style.display = "none";
      metaDiv.style.display = "none";

      editTitleInput.style.display = "block";
      editBodyTextarea.style.display = "block";
      editTagInput.style.display = "block";
      editColorContainer.style.display = "block";

      editButton.style.display = "none";
      deleteButton.style.display = "none";
      saveButton.style.display = "inline-block";
      cancelButton.style.display = "inline-block";

      memoDiv.classList.add("editing");

      editTitleInput.value = memo.title;
      editBodyTextarea.value = memo.body;
      editTagInput.value = memo.tag || "";
      editTitleInput.focus();
    });

    // --- 保存ボタンのイベント ---
    saveButton.addEventListener("click", function() {
      const newTitle = editTitleInput.value;
      const newBody = editBodyTextarea.value;
      const newTag = editTagInput.value;

      if (newTitle.trim() === "" && newBody.trim() === "") {
        alert("タイトルか本文のどちらかは入力してください。");
        return;
      }

      // 編集用ラジオボタンから選択された色を取得
      let newColor = "none";
      editColorRadios.forEach(function(radio) {
        if (radio.checked) {
          newColor = radio.value;
        }
      });

      const idText = memoDiv.dataset.id;
      const id = Number(idText);

      updateMemo(id, newTitle, newBody, newTag, newColor);
    });

    // --- キャンセルボタンのイベント ---
    cancelButton.addEventListener("click", function() {
      titleDiv.style.display = "block";
      bodyDiv.style.display = "block";
      metaDiv.style.display = "block";

      editTitleInput.style.display = "none";
      editBodyTextarea.style.display = "none";
      editTagInput.style.display = "none";
      editColorContainer.style.display = "none";

      editButton.style.display = "inline-block";
      deleteButton.style.display = "inline-block";
      saveButton.style.display = "none";
      cancelButton.style.display = "none";

      memoDiv.classList.remove("editing");
    });

    // --- 削除ボタンのイベント ---
    deleteButton.addEventListener("click", function(event) {
      const memoElement = event.target.closest(".memo-item");
      const idText = memoElement.dataset.id;
      const id = Number(idText);

      deleteMemo(id);
    });

    // --- memoDivの中に要素を追加する ---
    memoDiv.append(titleDiv);
    memoDiv.append(bodyDiv);
    memoDiv.append(metaDiv);

    memoDiv.append(editTitleInput);
    memoDiv.append(editBodyTextarea);
    memoDiv.append(editTagInput);
    memoDiv.append(editColorContainer);

    memoDiv.append(editButton);
    memoDiv.append(saveButton);
    memoDiv.append(cancelButton);
    memoDiv.append(deleteButton);

    memoList.append(memoDiv);
  });
}
JavaScript

この関数で本当に掴んでほしいこと

  • タグフィルタは「状態(currentTagFilter)」で管理し、 filter() で絞り込むだけで実現できる
  • 色ラベルは「クラス名(color-red など)」として DOM に反映すると、 CSS で柔軟に見た目を変えられる
  • 編集フォームにタグと色ラベルを含めることで、 メモの「意味」を後から変えられるようになる

検索入力欄のイベント設定(復習+タグも対象)

検索欄は、タイトル・本文・タグを対象にします。

// --- イベント設定:検索入力欄 ---
searchInput.addEventListener("input", function() {
  currentKeyword = searchInput.value;
  renderMemos();
});
JavaScript

並び順ボタンのイベント設定(復習)

並び順ボタンは 3日目と同じパターンです。

// --- イベント設定:並び順ボタン ---
sortButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const sortMode = button.dataset.sort;

    currentSort = sortMode;

    sortButtons.forEach(function(b) {
      b.classList.remove("active-sort");
    });

    button.classList.add("active-sort");

    renderMemos();
  });
});
JavaScript

タグフィルタボタンのイベント設定(querySelectorAll+dataset+classList)

ここで querySelectorAll()datasetclassList を タグフィルタに応用します。

// --- イベント設定:タグフィルタボタン ---
tagFilterButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const tag = button.dataset.tag; // "" / "仕事" / "プライベート" / "アイデア"

    // 状態を更新
    currentTagFilter = tag;

    // 全てのタグフィルタボタンから選択中クラスを外す
    tagFilterButtons.forEach(function(b) {
      b.classList.remove("active-tag");
    });

    // クリックされたボタンだけ選択中クラスを付ける
    button.classList.add("active-tag");

    // 新しいタグフィルタで画面を作り直す
    renderMemos();
  });
});
JavaScript

深掘りポイント:

  • dataset.tag に「このボタンが表すタグ」を埋め込んでおくと、 イベントの中で簡単に取り出せる
  • 「すべて」ボタンは data-tag="" として、 「タグフィルタなし」を表現する
  • classList.add("active-tag") / classList.remove("active-tag") で、 選択中のタグを見た目で分かりやすくする

「メモを追加」ボタンのイベント設定:タグと色を渡す

追加ボタンから addMemo() にタグと色を渡します。

// --- イベント設定:メモを追加ボタン ---
addMemoButton.addEventListener("click", function() {
  const title = memoTitleInput.value;
  const body = memoBodyInput.value;
  const tag = memoTagInput.value;
  const color = getSelectedColor();

  errorMessage.textContent = "";

  if (title.trim() === "" && body.trim() === "") {
    errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
    return;
  }

  addMemo(title, body, tag, color);

  memoTitleInput.value = "";
  memoBodyInput.value = "";
  memoTagInput.value = "";

  // 色ラベルを「なし」に戻す
  memoColorRadios.forEach(function(radio) {
    radio.checked = radio.value === "none";
  });

  renderMemos();
});
JavaScript

初期処理:保存データを読み込んでから描画

最後に、ページ読み込み時の流れを整えます。

// --- 初期処理 ---
loadFromStorage();
renderMemos();
JavaScript

4日目の完成コード(JavaScript全体)

HTML の <script> タグの中身として、 ここまでのコードをひとつにまとめるとこうなります。

<script>
  // --- アプリの状態(State) ---
  let memos = [];
  let nextMemoId = 1;

  let currentKeyword = "";
  let currentSort = "newest";
  let currentTagFilter = "";

  const STORAGE_KEY = "my_memo_app_data";

  // --- DOM参照 ---
  const memoTitleInput = document.querySelector("#memoTitleInput");
  const memoBodyInput = document.querySelector("#memoBodyInput");
  const memoTagInput = document.querySelector("#memoTagInput");
  const addMemoButton = document.querySelector("#addMemoButton");
  const errorMessage = document.querySelector("#errorMessage");
  const memoList = document.querySelector("#memoList");

  const searchInput = document.querySelector("#searchInput");
  const sortButtons = document.querySelectorAll("#sortButtons button");
  const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
  const memoColorRadios = document.querySelectorAll("input[name='memoColor']");

  // --- ストレージ:保存 ---
  function saveToStorage() {
    const data = {
      memos: memos,
      nextMemoId: nextMemoId
    };

    const json = JSON.stringify(data);
    localStorage.setItem(STORAGE_KEY, json);
  }

  // --- ストレージ:読み込み ---
  function loadFromStorage() {
    const json = localStorage.getItem(STORAGE_KEY);

    if (json === null) {
      return;
    }

    try {
      const data = JSON.parse(json);

      if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
        memos = data.memos;
        nextMemoId = data.nextMemoId;
      }
    } catch (error) {
      console.error("保存データの読み込みに失敗しました:", error);
    }
  }

  // --- ユーティリティ:選択されている色ラベルを取得 ---
  function getSelectedColor() {
    let selectedValue = "none";

    memoColorRadios.forEach(function(radio) {
      if (radio.checked) {
        selectedValue = radio.value;
      }
    });

    return selectedValue;
  }

  // --- 操作ロジック:メモ追加 ---
  function addMemo(title, body, tag, color) {
    const newMemo = {
      id: nextMemoId,
      title: title,
      body: body,
      tag: tag,
      color: color,
      createdAt: Date.now()
    };

    nextMemoId = nextMemoId + 1;

    memos.push(newMemo);

    saveToStorage();
  }

  // --- 操作ロジック:メモ編集 ---
  function updateMemo(id, newTitle, newBody, newTag, newColor) {
    memos.forEach(function(memo) {
      if (memo.id === id) {
        memo.title = newTitle;
        memo.body = newBody;
        memo.tag = newTag;
        memo.color = newColor;
      }
    });

    saveToStorage();
    renderMemos();
  }

  // --- 操作ロジック:メモ削除 ---
  function deleteMemo(id) {
    const newMemos = memos.filter(function(memo) {
      return memo.id !== id;
    });

    memos.length = 0;
    newMemos.forEach(function(memo) {
      memos.push(memo);
    });

    saveToStorage();
    renderMemos();
  }

  // --- 描画 ---
  function renderMemos() {
    memoList.innerHTML = "";

    let visibleMemos = memos;

    if (currentKeyword.trim() !== "") {
      const keywordLower = currentKeyword.toLowerCase();

      visibleMemos = visibleMemos.filter(function(memo) {
        const titleLower = memo.title.toLowerCase();
        const bodyLower = memo.body.toLowerCase();
        const tagLower = (memo.tag || "").toLowerCase();

        return (
          titleLower.includes(keywordLower) ||
          bodyLower.includes(keywordLower) ||
          tagLower.includes(keywordLower)
        );
      });
    }

    if (currentTagFilter !== "") {
      visibleMemos = visibleMemos.filter(function(memo) {
        return memo.tag === currentTagFilter;
      });
    }

    visibleMemos = visibleMemos.slice();

    if (currentSort === "newest") {
      visibleMemos.sort(function(a, b) {
        return b.createdAt - a.createdAt;
      });
    } else if (currentSort === "oldest") {
      visibleMemos.sort(function(a, b) {
        return a.createdAt - b.createdAt;
      });
    }

    visibleMemos.forEach(function(memo) {
      const memoDiv = document.createElement("div");
      memoDiv.classList.add("memo-item");
      memoDiv.dataset.id = String(memo.id);

      const colorClass = "color-" + (memo.color || "none");
      memoDiv.classList.add(colorClass);

      const titleDiv = document.createElement("div");
      titleDiv.classList.add("memo-item-title");
      titleDiv.textContent = memo.title;

      const bodyDiv = document.createElement("div");
      bodyDiv.classList.add("memo-item-body");
      bodyDiv.textContent = memo.body;

      const metaDiv = document.createElement("div");
      metaDiv.classList.add("memo-meta");
      metaDiv.textContent = `タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"}`;

      const editTitleInput = document.createElement("input");
      editTitleInput.type = "text";
      editTitleInput.value = memo.title;
      editTitleInput.style.display = "none";

      const editBodyTextarea = document.createElement("textarea");
      editBodyTextarea.rows = 4;
      editBodyTextarea.cols = 40;
      editBodyTextarea.value = memo.body;
      editBodyTextarea.style.display = "none";

      const editTagInput = document.createElement("input");
      editTagInput.type = "text";
      editTagInput.value = memo.tag || "";
      editTagInput.style.display = "none";

      const editColorContainer = document.createElement("div");
      editColorContainer.style.display = "none";

      const colors = ["none", "red", "blue", "yellow"];
      const editColorRadios = [];

      colors.forEach(function(color) {
        const label = document.createElement("label");
        const radio = document.createElement("input");
        radio.type = "radio";
        radio.name = "editColor-" + memo.id;
        radio.value = color;

        if (memo.color === color || (!memo.color && color === "none")) {
          radio.checked = true;
        }

        label.append(radio);
        label.append(document.createTextNode(" " + color));

        editColorContainer.append(label);

        editColorRadios.push(radio);
      });

      const editButton = document.createElement("button");
      editButton.textContent = "編集";

      const saveButton = document.createElement("button");
      saveButton.textContent = "保存";
      saveButton.style.display = "none";

      const cancelButton = document.createElement("button");
      cancelButton.textContent = "キャンセル";
      cancelButton.style.display = "none";

      const deleteButton = document.createElement("button");
      deleteButton.textContent = "削除";

      editButton.addEventListener("click", function() {
        titleDiv.style.display = "none";
        bodyDiv.style.display = "none";
        metaDiv.style.display = "none";

        editTitleInput.style.display = "block";
        editBodyTextarea.style.display = "block";
        editTagInput.style.display = "block";
        editColorContainer.style.display = "block";

        editButton.style.display = "none";
        deleteButton.style.display = "none";
        saveButton.style.display = "inline-block";
        cancelButton.style.display = "inline-block";

        memoDiv.classList.add("editing");

        editTitleInput.value = memo.title;
        editBodyTextarea.value = memo.body;
        editTagInput.value = memo.tag || "";
        editTitleInput.focus();
      });

      saveButton.addEventListener("click", function() {
        const newTitle = editTitleInput.value;
        const newBody = editBodyTextarea.value;
        const newTag = editTagInput.value;

        if (newTitle.trim() === "" && newBody.trim() === "") {
          alert("タイトルか本文のどちらかは入力してください。");
          return;
        }

        let newColor = "none";
        editColorRadios.forEach(function(radio) {
          if (radio.checked) {
            newColor = radio.value;
          }
        });

        const idText = memoDiv.dataset.id;
        const id = Number(idText);

        updateMemo(id, newTitle, newBody, newTag, newColor);
      });

      cancelButton.addEventListener("click", function() {
        titleDiv.style.display = "block";
        bodyDiv.style.display = "block";
        metaDiv.style.display = "block";

        editTitleInput.style.display = "none";
        editBodyTextarea.style.display = "none";
        editTagInput.style.display = "none";
        editColorContainer.style.display = "none";

        editButton.style.display = "inline-block";
        deleteButton.style.display = "inline-block";
        saveButton.style.display = "none";
        cancelButton.style.display = "none";

        memoDiv.classList.remove("editing");
      });

      deleteButton.addEventListener("click", function(event) {
        const memoElement = event.target.closest(".memo-item");
        const idText = memoElement.dataset.id;
        const id = Number(idText);

        deleteMemo(id);
      });

      memoDiv.append(titleDiv);
      memoDiv.append(bodyDiv);
      memoDiv.append(metaDiv);
      memoDiv.append(editTitleInput);
      memoDiv.append(editBodyTextarea);
      memoDiv.append(editTagInput);
      memoDiv.append(editColorContainer);
      memoDiv.append(editButton);
      memoDiv.append(saveButton);
      memoDiv.append(cancelButton);
      memoDiv.append(deleteButton);

      memoList.append(memoDiv);
    });
  }

  // --- イベント設定:メモ追加ボタン ---
  addMemoButton.addEventListener("click", function() {
    const title = memoTitleInput.value;
    const body = memoBodyInput.value;
    const tag = memoTagInput.value;
    const color = getSelectedColor();

    errorMessage.textContent = "";

    if (title.trim() === "" && body.trim() === "") {
      errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
      return;
    }

    addMemo(title, body, tag, color);

    memoTitleInput.value = "";
    memoBodyInput.value = "";
    memoTagInput.value = "";

    memoColorRadios.forEach(function(radio) {
      radio.checked = radio.value === "none";
    });

    renderMemos();
  });

  // --- イベント設定:検索入力欄 ---
  searchInput.addEventListener("input", function() {
    currentKeyword = searchInput.value;
    renderMemos();
  });

  // --- イベント設定:並び順ボタン ---
  sortButtons.forEach(function(button) {
    button.addEventListener("click", function() {
      const sortMode = button.dataset.sort;

      currentSort = sortMode;

      sortButtons.forEach(function(b) {
        b.classList.remove("active-sort");
      });

      button.classList.add("active-sort");

      renderMemos();
    });
  });

  // --- イベント設定:タグフィルタボタン ---
  tagFilterButtons.forEach(function(button) {
    button.addEventListener("click", function() {
      const tag = button.dataset.tag;

      currentTagFilter = tag;

      tagFilterButtons.forEach(function(b) {
        b.classList.remove("active-tag");
      });

      button.classList.add("active-tag");

      renderMemos();
    });
  });

  // --- 初期処理 ---
  loadFromStorage();
  renderMemos();
</script>

4日目で本当に掴んでほしいこと

  • タグや色ラベルのような「意味」を状態として持つと、 メモ帳が一気に“自分の生活にフィットするツール”になる
  • querySelectorAll()datasetclassList のパターンは、 並び順ボタンだけでなくタグフィルタにもそのまま応用できる
  • 配列(状態)→ localStorage(保存)→ DOM(描画)→ イベント(操作)という流れの上に、 「タグ」「色」「フィルタ」「検索」「並び替え」が自然に積み上がっている

ここまで来ると、 ToDoアプリでも、メモ帳でも、家計簿でも、 「自分のルールで整理できるアプリ」を 自分の手で設計していけるようになります。


メモ帳アプリ 5日目のゴール

5日目のテーマは 「メモの“重要度”や“状態”を管理できるメモ帳に育てる」 ことです。

1〜4日目で作ったメモ帳アプリは、

  • 追加・編集・削除
  • 検索
  • 並び替え(新しい順/古い順)
  • タグ・色ラベル
  • localStorage に保存

までできる状態でした。

5日目ではここから一歩進めて、

  • メモを「ピン留め(お気に入り)」できる
  • メモを「アーカイブ(非表示)」できる
  • ピン留めメモを上に表示する
  • アーカイブメモをフィルタで切り替える
  • datasetclassList を使って「状態付きのUI」を作る

というところまでを、ステップバイステップで作っていきます。

今日の全体ステップ

5日目の流れはこんな感じです。

  1. メモオブジェクトに pinnedarchived プロパティを追加する
  2. メモ一覧の表示に「ピンボタン」と「アーカイブボタン」を追加する
  3. ピン状態に応じてクラスを付け、見た目を変える
  4. アーカイブ状態に応じて表示/非表示を切り替える
  5. 「表示モード(通常/アーカイブ含む/アーカイブのみ)」をフィルタボタンで切り替える
  6. querySelectorAll()datasetclassList でフィルタボタンを制御する

5日目の HTML レイアウト

4日目までの HTML に、 「表示モード切り替え」の UI を追加します。 (タグフィルタはそのまま使ってもいいですし、いったん省略しても構いません)

ここでは、タグフィルタは残したまま、 新しく「表示モードフィルタ」を追加する形で説明します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>メモ帳アプリ 5日目</title>
  <style>
    .memo-item {
      border: 1px solid #ccc;
      padding: 8px;
      margin-bottom: 8px;
    }

    .memo-item-title {
      font-weight: bold;
      margin-bottom: 4px;
    }

    .memo-item-body {
      white-space: pre-wrap;
    }

    .memo-meta {
      font-size: 12px;
      color: #666;
      margin-top: 4px;
    }

    .editing {
      background-color: #fff7e6;
    }

    .active-sort {
      background-color: #007bff;
      color: white;
    }

    .active-tag {
      background-color: #28a745;
      color: white;
    }

    .active-view {
      background-color: #6f42c1;
      color: white;
    }

    .color-red {
      border-left: 4px solid #ff4d4f;
    }

    .color-blue {
      border-left: 4px solid #1890ff;
    }

    .color-yellow {
      border-left: 4px solid #faad14;
    }

    .color-none {
      border-left: 4px solid transparent;
    }

    /* ピン留めされたメモの見た目 */
    .pinned {
      border-top: 3px solid #ffc107;
      background-color: #fffbe6;
    }

    /* アーカイブされたメモの見た目(一覧に表示する場合) */
    .archived {
      opacity: 0.6;
    }
  </style>
</head>
<body>
  <h1>メモ帳アプリ(5日目)</h1>

  <div>
    <label for="memoTitleInput">タイトル:</label>
    <input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
  </div>

  <div>
    <label for="memoBodyInput">本文:</label><br>
    <textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
  </div>

  <div>
    <label for="memoTagInput">タグ:</label>
    <input id="memoTagInput" type="text" placeholder="例:仕事 / プライベート / アイデア">
  </div>

  <div>
    <span>色ラベル:</span>
    <label>
      <input type="radio" name="memoColor" value="none" checked> なし
    </label>
    <label>
      <input type="radio" name="memoColor" value="red"> 赤
    </label>
    <label>
      <input type="radio" name="memoColor" value="blue"> 青
    </label>
    <label>
      <input type="radio" name="memoColor" value="yellow"> 黄
    </label>
  </div>

  <div>
    <button id="addMemoButton">メモを追加</button>
  </div>

  <p id="errorMessage" style="color: red;"></p>

  <h2>検索</h2>
  <div>
    <input id="searchInput" type="text" placeholder="タイトル・本文・タグから検索">
  </div>

  <h2>並び順</h2>
  <div id="sortButtons">
    <button data-sort="newest" class="active-sort">新しい順</button>
    <button data-sort="oldest">古い順</button>
  </div>

  <h2>タグフィルタ</h2>
  <div id="tagFilterButtons">
    <button data-tag="" class="active-tag">すべて</button>
    <button data-tag="仕事">仕事</button>
    <button data-tag="プライベート">プライベート</button>
    <button data-tag="アイデア">アイデア</button>
  </div>

  <h2>表示モード</h2>
  <div id="viewFilterButtons">
    <button data-view="all" class="active-view">通常(アーカイブ以外)</button>
    <button data-view="withArchived">アーカイブも含める</button>
    <button data-view="archivedOnly">アーカイブのみ</button>
  </div>

  <h2>メモ一覧</h2>
  <div id="memoList"></div>

  <script>
    // ここに JavaScript を書いていきます
  </script>
</body>
</html>

ここで押さえておきたいポイント

  • 新しく #viewFilterButtons を追加し、 各ボタンに data-view 属性で表示モードを埋め込む
    • "all" … アーカイブ以外
    • "withArchived" … アーカイブも含めて表示
    • "archivedOnly" … アーカイブだけ表示
  • ピン留めされたメモには .pinned クラス、 アーカイブされたメモには .archived クラスを付けて見た目を変える

状態と DOM 参照の定義

5日目では、 「表示モード」と「ピン/アーカイブ状態」を扱います。

// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;

// 検索キーワード
let currentKeyword = "";

// 並び順モード("newest" / "oldest")
let currentSort = "newest";

// タグフィルタ(空文字なら「すべて」)
let currentTagFilter = "";

// 表示モード("all" / "withArchived" / "archivedOnly")
let currentViewMode = "all";

const STORAGE_KEY = "my_memo_app_data";

// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const memoTagInput = document.querySelector("#memoTagInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");

const searchInput = document.querySelector("#searchInput");
const sortButtons = document.querySelectorAll("#sortButtons button");
const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
const viewFilterButtons = document.querySelectorAll("#viewFilterButtons button");

const memoColorRadios = document.querySelectorAll("input[name='memoColor']");
JavaScript

深掘りポイント:

  • 「表示モード」も立派な状態
  • ピン/アーカイブは「メモごとのプロパティ」として持たせる
  • querySelectorAll() で複数ボタンをまとめて扱うパターンが どんどん再利用されている

localStorage の保存・読み込み

メモオブジェクトにプロパティが増えても、 JSON はそのまま保存・読み込みしてくれます。

// --- ストレージ:状態をlocalStorageに保存する ---
function saveToStorage() {
  const data = {
    memos: memos,
    nextMemoId: nextMemoId
  };

  const json = JSON.stringify(data);
  localStorage.setItem(STORAGE_KEY, json);
}

// --- ストレージ:localStorageから状態を読み込む ---
function loadFromStorage() {
  const json = localStorage.getItem(STORAGE_KEY);

  if (json === null) {
    return;
  }

  try {
    const data = JSON.parse(json);

    if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
      memos = data.memos;
      nextMemoId = data.nextMemoId;
    }
  } catch (error) {
    console.error("保存データの読み込みに失敗しました:", error);
  }
}
JavaScript

色ラベルの選択値を取得する関数(復習)

// --- ユーティリティ:選択されている色ラベルを取得する ---
function getSelectedColor() {
  let selectedValue = "none";

  memoColorRadios.forEach(function(radio) {
    if (radio.checked) {
      selectedValue = radio.value;
    }
  });

  return selectedValue;
}
JavaScript

メモ追加ロジック:ピン/アーカイブの初期値を持たせる

新しく追加するメモは、 最初は「ピンなし」「アーカイブなし」にします。

// --- 操作ロジック:メモを追加する ---
function addMemo(title, body, tag, color) {
  const newMemo = {
    id: nextMemoId,
    title: title,
    body: body,
    tag: tag,
    color: color,
    pinned: false,     // ピン留めされているかどうか
    archived: false,   // アーカイブされているかどうか
    createdAt: Date.now()
  };

  nextMemoId = nextMemoId + 1;

  memos.push(newMemo);

  saveToStorage();
}
JavaScript

深掘りポイント:

  • 「ピン留め」「アーカイブ」は 真偽値(true / false)で持つと扱いやすい
  • 初期値は false にしておき、 ボタン操作で true / false を切り替える

メモ編集ロジック:タグ・色はそのまま、ピン/アーカイブは別操作にする

編集フォームではタイトル・本文・タグ・色を編集し、 ピン/アーカイブは専用ボタンで切り替える形にします。

// --- 操作ロジック:メモを編集する ---
function updateMemo(id, newTitle, newBody, newTag, newColor) {
  memos.forEach(function(memo) {
    if (memo.id === id) {
      memo.title = newTitle;
      memo.body = newBody;
      memo.tag = newTag;
      memo.color = newColor;
    }
  });

  saveToStorage();
  renderMemos();
}
JavaScript

ピン状態を切り替えるロジック

// --- 操作ロジック:ピン状態を切り替える ---
function togglePinned(id) {
  memos.forEach(function(memo) {
    if (memo.id === id) {
      memo.pinned = !memo.pinned; // true/falseを反転
    }
  });

  saveToStorage();
  renderMemos();
}
JavaScript

アーカイブ状態を切り替えるロジック

// --- 操作ロジック:アーカイブ状態を切り替える ---
function toggleArchived(id) {
  memos.forEach(function(memo) {
    if (memo.id === id) {
      memo.archived = !memo.archived; // true/falseを反転
    }
  });

  saveToStorage();
  renderMemos();
}
JavaScript

深掘りポイント:

  • 「状態を反転する」パターンは memo.pinned = !memo.pinned のように書ける
  • ピン/アーカイブは「編集」とは別の操作として分けておくと、 UI が分かりやすくなる

メモ削除ロジック(復習)

// --- 操作ロジック:メモを削除する ---
function deleteMemo(id) {
  const newMemos = memos.filter(function(memo) {
    return memo.id !== id;
  });

  memos.length = 0;
  newMemos.forEach(function(memo) {
    memos.push(memo);
  });

  saveToStorage();
  renderMemos();
}
JavaScript

5日目の心臓部:ピン+アーカイブ+表示モード付きの描画(renderMemos)

ここが今日の一番大事な部分です。 renderMemos() の中で、

  1. 検索キーワードでフィルタ
  2. タグフィルタで絞り込み
  3. 表示モード(アーカイブの扱い)でさらに絞り込み
  4. 並び順モードでソート(ピン留めを優先)
  5. DOM を生成(ピン/アーカイブを見た目に反映)

という流れを作ります。

// --- 描画:状態から画面(DOM)を作り直す ---
function renderMemos() {
  memoList.innerHTML = "";

  let visibleMemos = memos;

  // 1. 検索キーワードでフィルタ(タイトル・本文・タグ)
  if (currentKeyword.trim() !== "") {
    const keywordLower = currentKeyword.toLowerCase();

    visibleMemos = visibleMemos.filter(function(memo) {
      const titleLower = memo.title.toLowerCase();
      const bodyLower = memo.body.toLowerCase();
      const tagLower = (memo.tag || "").toLowerCase();

      return (
        titleLower.includes(keywordLower) ||
        bodyLower.includes(keywordLower) ||
        tagLower.includes(keywordLower)
      );
    });
  }

  // 2. タグフィルタで絞り込み
  if (currentTagFilter !== "") {
    visibleMemos = visibleMemos.filter(function(memo) {
      return memo.tag === currentTagFilter;
    });
  }

  // 3. 表示モードで絞り込み
  if (currentViewMode === "all") {
    // アーカイブされていないメモだけ表示
    visibleMemos = visibleMemos.filter(function(memo) {
      return memo.archived === false;
    });
  } else if (currentViewMode === "archivedOnly") {
    // アーカイブされたメモだけ表示
    visibleMemos = visibleMemos.filter(function(memo) {
      return memo.archived === true;
    });
  } else if (currentViewMode === "withArchived") {
    // 何もしない(アーカイブも含めて表示)
  }

  // 4. 並び替えのためにコピーを作る
  visibleMemos = visibleMemos.slice();

  // ピン留めされたメモを優先して並べる
  visibleMemos.sort(function(a, b) {
    // まずピン状態で比較(trueを前に)
    if (a.pinned && !b.pinned) {
      return -1;
    }
    if (!a.pinned && b.pinned) {
      return 1;
    }

    // ピン状態が同じなら、createdAtで比較
    if (currentSort === "newest") {
      return b.createdAt - a.createdAt;
    } else {
      return a.createdAt - b.createdAt;
    }
  });

  // 5. ソート済みのvisibleMemosをもとにDOMを作る
  visibleMemos.forEach(function(memo) {
    const memoDiv = document.createElement("div");
    memoDiv.classList.add("memo-item");
    memoDiv.dataset.id = String(memo.id);

    // 色ラベルに応じてクラスを付ける
    const colorClass = "color-" + (memo.color || "none");
    memoDiv.classList.add(colorClass);

    // ピン状態に応じてクラスを付ける
    if (memo.pinned) {
      memoDiv.classList.add("pinned");
    }

    // アーカイブ状態に応じてクラスを付ける
    if (memo.archived) {
      memoDiv.classList.add("archived");
    }

    // --- 表示モード用の要素 ---
    const titleDiv = document.createElement("div");
    titleDiv.classList.add("memo-item-title");
    titleDiv.textContent = memo.title;

    const bodyDiv = document.createElement("div");
    bodyDiv.classList.add("memo-item-body");
    bodyDiv.textContent = memo.body;

    const metaDiv = document.createElement("div");
    metaDiv.classList.add("memo-meta");
    metaDiv.textContent =
      `タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"} / ` +
      `ピン: ${memo.pinned ? "ON" : "OFF"} / アーカイブ: ${memo.archived ? "ON" : "OFF"}`;

    // --- 編集モード用の要素 ---
    const editTitleInput = document.createElement("input");
    editTitleInput.type = "text";
    editTitleInput.value = memo.title;
    editTitleInput.style.display = "none";

    const editBodyTextarea = document.createElement("textarea");
    editBodyTextarea.rows = 4;
    editBodyTextarea.cols = 40;
    editBodyTextarea.value = memo.body;
    editBodyTextarea.style.display = "none";

    const editTagInput = document.createElement("input");
    editTagInput.type = "text";
    editTagInput.value = memo.tag || "";
    editTagInput.style.display = "none";

    const editColorContainer = document.createElement("div");
    editColorContainer.style.display = "none";

    const colors = ["none", "red", "blue", "yellow"];
    const editColorRadios = [];

    colors.forEach(function(color) {
      const label = document.createElement("label");
      const radio = document.createElement("input");
      radio.type = "radio";
      radio.name = "editColor-" + memo.id;
      radio.value = color;

      if (memo.color === color || (!memo.color && color === "none")) {
        radio.checked = true;
      }

      label.append(radio);
      label.append(document.createTextNode(" " + color));

      editColorContainer.append(label);

      editColorRadios.push(radio);
    });

    // --- ボタン類 ---
    const editButton = document.createElement("button");
    editButton.textContent = "編集";

    const saveButton = document.createElement("button");
    saveButton.textContent = "保存";
    saveButton.style.display = "none";

    const cancelButton = document.createElement("button");
    cancelButton.textContent = "キャンセル";
    cancelButton.style.display = "none";

    const deleteButton = document.createElement("button");
    deleteButton.textContent = "削除";

    const pinButton = document.createElement("button");
    pinButton.textContent = memo.pinned ? "ピン解除" : "ピン";

    const archiveButton = document.createElement("button");
    archiveButton.textContent = memo.archived ? "アーカイブ解除" : "アーカイブ";

    // --- 編集ボタンのイベント ---
    editButton.addEventListener("click", function() {
      titleDiv.style.display = "none";
      bodyDiv.style.display = "none";
      metaDiv.style.display = "none";

      editTitleInput.style.display = "block";
      editBodyTextarea.style.display = "block";
      editTagInput.style.display = "block";
      editColorContainer.style.display = "block";

      editButton.style.display = "none";
      deleteButton.style.display = "none";
      pinButton.style.display = "none";
      archiveButton.style.display = "none";
      saveButton.style.display = "inline-block";
      cancelButton.style.display = "inline-block";

      memoDiv.classList.add("editing");

      editTitleInput.value = memo.title;
      editBodyTextarea.value = memo.body;
      editTagInput.value = memo.tag || "";
      editTitleInput.focus();
    });

    // --- 保存ボタンのイベント ---
    saveButton.addEventListener("click", function() {
      const newTitle = editTitleInput.value;
      const newBody = editBodyTextarea.value;
      const newTag = editTagInput.value;

      if (newTitle.trim() === "" && newBody.trim() === "") {
        alert("タイトルか本文のどちらかは入力してください。");
        return;
      }

      let newColor = "none";
      editColorRadios.forEach(function(radio) {
        if (radio.checked) {
          newColor = radio.value;
        }
      });

      const idText = memoDiv.dataset.id;
      const id = Number(idText);

      updateMemo(id, newTitle, newBody, newTag, newColor);
    });

    // --- キャンセルボタンのイベント ---
    cancelButton.addEventListener("click", function() {
      titleDiv.style.display = "block";
      bodyDiv.style.display = "block";
      metaDiv.style.display = "block";

      editTitleInput.style.display = "none";
      editBodyTextarea.style.display = "none";
      editTagInput.style.display = "none";
      editColorContainer.style.display = "none";

      editButton.style.display = "inline-block";
      deleteButton.style.display = "inline-block";
      pinButton.style.display = "inline-block";
      archiveButton.style.display = "inline-block";
      saveButton.style.display = "none";
      cancelButton.style.display = "none";

      memoDiv.classList.remove("editing");
    });

    // --- 削除ボタンのイベント ---
    deleteButton.addEventListener("click", function(event) {
      const memoElement = event.target.closest(".memo-item");
      const idText = memoElement.dataset.id;
      const id = Number(idText);

      deleteMemo(id);
    });

    // --- ピンボタンのイベント ---
    pinButton.addEventListener("click", function(event) {
      const memoElement = event.target.closest(".memo-item");
      const idText = memoElement.dataset.id;
      const id = Number(idText);

      togglePinned(id);
    });

    // --- アーカイブボタンのイベント ---
    archiveButton.addEventListener("click", function(event) {
      const memoElement = event.target.closest(".memo-item");
      const idText = memoElement.dataset.id;
      const id = Number(idText);

      toggleArchived(id);
    });

    // --- memoDivの中に要素を追加する ---
    memoDiv.append(titleDiv);
    memoDiv.append(bodyDiv);
    memoDiv.append(metaDiv);

    memoDiv.append(editTitleInput);
    memoDiv.append(editBodyTextarea);
    memoDiv.append(editTagInput);
    memoDiv.append(editColorContainer);

    memoDiv.append(editButton);
    memoDiv.append(saveButton);
    memoDiv.append(cancelButton);
    memoDiv.append(deleteButton);
    memoDiv.append(pinButton);
    memoDiv.append(archiveButton);

    memoList.append(memoDiv);
  });
}
JavaScript

この関数で本当に掴んでほしいこと

  • 表示モード(currentViewMode)を状態として持ち、 filter() でアーカイブの扱いを切り替えるだけで 「通常/アーカイブ含む/アーカイブのみ」が実現できる
  • ピン留めは「ソートの優先度」として扱うと、 自然に「上に来る」挙動を作れる
  • ピン/アーカイブは classList で見た目に反映し、 ボタンのラベル(ピン/ピン解除など)も状態に応じて変える

検索入力欄のイベント設定(復習)

// --- イベント設定:検索入力欄 ---
searchInput.addEventListener("input", function() {
  currentKeyword = searchInput.value;
  renderMemos();
});
JavaScript

並び順ボタンのイベント設定(復習)

// --- イベント設定:並び順ボタン ---
sortButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const sortMode = button.dataset.sort;

    currentSort = sortMode;

    sortButtons.forEach(function(b) {
      b.classList.remove("active-sort");
    });

    button.classList.add("active-sort");

    renderMemos();
  });
});
JavaScript

タグフィルタボタンのイベント設定(復習)

// --- イベント設定:タグフィルタボタン ---
tagFilterButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const tag = button.dataset.tag;

    currentTagFilter = tag;

    tagFilterButtons.forEach(function(b) {
      b.classList.remove("active-tag");
    });

    button.classList.add("active-tag");

    renderMemos();
  });
});
JavaScript

表示モードフィルタボタンのイベント設定

ここで querySelectorAll()datasetclassList を 表示モードにも応用します。

// --- イベント設定:表示モードフィルタボタン ---
viewFilterButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const viewMode = button.dataset.view; // "all" / "withArchived" / "archivedOnly"

    currentViewMode = viewMode;

    viewFilterButtons.forEach(function(b) {
      b.classList.remove("active-view");
    });

    button.classList.add("active-view");

    renderMemos();
  });
});
JavaScript

深掘りポイント:

  • dataset.view に「このボタンが表す表示モード」を埋め込んでおくと、 イベントの中で簡単に取り出せる
  • classList.add("active-view") / classList.remove("active-view") で、 選択中の表示モードを見た目で分かりやすくする
  • 並び順ボタン・タグフィルタボタン・表示モードボタンが すべて同じパターンで書けていることに注目してほしい

「メモを追加」ボタンのイベント設定

// --- イベント設定:メモを追加ボタン ---
addMemoButton.addEventListener("click", function() {
  const title = memoTitleInput.value;
  const body = memoBodyInput.value;
  const tag = memoTagInput.value;
  const color = getSelectedColor();

  errorMessage.textContent = "";

  if (title.trim() === "" && body.trim() === "") {
    errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
    return;
  }

  addMemo(title, body, tag, color);

  memoTitleInput.value = "";
  memoBodyInput.value = "";
  memoTagInput.value = "";

  memoColorRadios.forEach(function(radio) {
    radio.checked = radio.value === "none";
  });

  renderMemos();
});
JavaScript

初期処理:保存データを読み込んでから描画

// --- 初期処理 ---
loadFromStorage();
renderMemos();
JavaScript

5日目の完成コード(JavaScript全体)

HTML の <script> タグの中身として、 ここまでのコードをひとつにまとめるとこうなります。

<script>
  // --- アプリの状態(State) ---
  let memos = [];
  let nextMemoId = 1;

  let currentKeyword = "";
  let currentSort = "newest";
  let currentTagFilter = "";
  let currentViewMode = "all";

  const STORAGE_KEY = "my_memo_app_data";

  // --- DOM参照 ---
  const memoTitleInput = document.querySelector("#memoTitleInput");
  const memoBodyInput = document.querySelector("#memoBodyInput");
  const memoTagInput = document.querySelector("#memoTagInput");
  const addMemoButton = document.querySelector("#addMemoButton");
  const errorMessage = document.querySelector("#errorMessage");
  const memoList = document.querySelector("#memoList");

  const searchInput = document.querySelector("#searchInput");
  const sortButtons = document.querySelectorAll("#sortButtons button");
  const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
  const viewFilterButtons = document.querySelectorAll("#viewFilterButtons button");
  const memoColorRadios = document.querySelectorAll("input[name='memoColor']");

  // --- ストレージ:保存 ---
  function saveToStorage() {
    const data = {
      memos: memos,
      nextMemoId: nextMemoId
    };

    const json = JSON.stringify(data);
    localStorage.setItem(STORAGE_KEY, json);
  }

  // --- ストレージ:読み込み ---
  function loadFromStorage() {
    const json = localStorage.getItem(STORAGE_KEY);

    if (json === null) {
      return;
    }

    try {
      const data = JSON.parse(json);

      if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
        memos = data.memos;
        nextMemoId = data.nextMemoId;
      }
    } catch (error) {
      console.error("保存データの読み込みに失敗しました:", error);
    }
  }

  // --- ユーティリティ:選択されている色ラベルを取得 ---
  function getSelectedColor() {
    let selectedValue = "none";

    memoColorRadios.forEach(function(radio) {
      if (radio.checked) {
        selectedValue = radio.value;
      }
    });

    return selectedValue;
  }

  // --- 操作ロジック:メモ追加 ---
  function addMemo(title, body, tag, color) {
    const newMemo = {
      id: nextMemoId,
      title: title,
      body: body,
      tag: tag,
      color: color,
      pinned: false,
      archived: false,
      createdAt: Date.now()
    };

    nextMemoId = nextMemoId + 1;

    memos.push(newMemo);

    saveToStorage();
  }

  // --- 操作ロジック:メモ編集 ---
  function updateMemo(id, newTitle, newBody, newTag, newColor) {
    memos.forEach(function(memo) {
      if (memo.id === id) {
        memo.title = newTitle;
        memo.body = newBody;
        memo.tag = newTag;
        memo.color = newColor;
      }
    });

    saveToStorage();
    renderMemos();
  }

  // --- 操作ロジック:ピン状態切り替え ---
  function togglePinned(id) {
    memos.forEach(function(memo) {
      if (memo.id === id) {
        memo.pinned = !memo.pinned;
      }
    });

    saveToStorage();
    renderMemos();
  }

  // --- 操作ロジック:アーカイブ状態切り替え ---
  function toggleArchived(id) {
    memos.forEach(function(memo) {
      if (memo.id === id) {
        memo.archived = !memo.archived;
      }
    });

    saveToStorage();
    renderMemos();
  }

  // --- 操作ロジック:メモ削除 ---
  function deleteMemo(id) {
    const newMemos = memos.filter(function(memo) {
      return memo.id !== id;
    });

    memos.length = 0;
    newMemos.forEach(function(memo) {
      memos.push(memo);
    });

    saveToStorage();
    renderMemos();
  }

  // --- 描画 ---
  function renderMemos() {
    memoList.innerHTML = "";

    let visibleMemos = memos;

    if (currentKeyword.trim() !== "") {
      const keywordLower = currentKeyword.toLowerCase();

      visibleMemos = visibleMemos.filter(function(memo) {
        const titleLower = memo.title.toLowerCase();
        const bodyLower = memo.body.toLowerCase();
        const tagLower = (memo.tag || "").toLowerCase();

        return (
          titleLower.includes(keywordLower) ||
          bodyLower.includes(keywordLower) ||
          tagLower.includes(keywordLower)
        );
      });
    }

    if (currentTagFilter !== "") {
      visibleMemos = visibleMemos.filter(function(memo) {
        return memo.tag === currentTagFilter;
      });
    }

    if (currentViewMode === "all") {
      visibleMemos = visibleMemos.filter(function(memo) {
        return memo.archived === false;
      });
    } else if (currentViewMode === "archivedOnly") {
      visibleMemos = visibleMemos.filter(function(memo) {
        return memo.archived === true;
      });
    }

    visibleMemos = visibleMemos.slice();

    visibleMemos.sort(function(a, b) {
      if (a.pinned && !b.pinned) {
        return -1;
      }
      if (!a.pinned && b.pinned) {
        return 1;
      }

      if (currentSort === "newest") {
        return b.createdAt - a.createdAt;
      } else {
        return a.createdAt - b.createdAt;
      }
    });

    visibleMemos.forEach(function(memo) {
      const memoDiv = document.createElement("div");
      memoDiv.classList.add("memo-item");
      memoDiv.dataset.id = String(memo.id);

      const colorClass = "color-" + (memo.color || "none");
      memoDiv.classList.add(colorClass);

      if (memo.pinned) {
        memoDiv.classList.add("pinned");
      }

      if (memo.archived) {
        memoDiv.classList.add("archived");
      }

      const titleDiv = document.createElement("div");
      titleDiv.classList.add("memo-item-title");
      titleDiv.textContent = memo.title;

      const bodyDiv = document.createElement("div");
      bodyDiv.classList.add("memo-item-body");
      bodyDiv.textContent = memo.body;

      const metaDiv = document.createElement("div");
      metaDiv.classList.add("memo-meta");
      metaDiv.textContent =
        `タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"} / ` +
        `ピン: ${memo.pinned ? "ON" : "OFF"} / アーカイブ: ${memo.archived ? "ON" : "OFF"}`;

      const editTitleInput = document.createElement("input");
      editTitleInput.type = "text";
      editTitleInput.value = memo.title;
      editTitleInput.style.display = "none";

      const editBodyTextarea = document.createElement("textarea");
      editBodyTextarea.rows = 4;
      editBodyTextarea.cols = 40;
      editBodyTextarea.value = memo.body;
      editBodyTextarea.style.display = "none";

      const editTagInput = document.createElement("input");
      editTagInput.type = "text";
      editTagInput.value = memo.tag || "";
      editTagInput.style.display = "none";

      const editColorContainer = document.createElement("div");
      editColorContainer.style.display = "none";

      const colors = ["none", "red", "blue", "yellow"];
      const editColorRadios = [];

      colors.forEach(function(color) {
        const label = document.createElement("label");
        const radio = document.createElement("input");
        radio.type = "radio";
        radio.name = "editColor-" + memo.id;
        radio.value = color;

        if (memo.color === color || (!memo.color && color === "none")) {
          radio.checked = true;
        }

        label.append(radio);
        label.append(document.createTextNode(" " + color));

        editColorContainer.append(label);

        editColorRadios.push(radio);
      });

      const editButton = document.createElement("button");
      editButton.textContent = "編集";

      const saveButton = document.createElement("button");
      saveButton.textContent = "保存";
      saveButton.style.display = "none";

      const cancelButton = document.createElement("button");
      cancelButton.textContent = "キャンセル";
      cancelButton.style.display = "none";

      const deleteButton = document.createElement("button");
      deleteButton.textContent = "削除";

      const pinButton = document.createElement("button");
      pinButton.textContent = memo.pinned ? "ピン解除" : "ピン";

      const archiveButton = document.createElement("button");
      archiveButton.textContent = memo.archived ? "アーカイブ解除" : "アーカイブ";

      editButton.addEventListener("click", function() {
        titleDiv.style.display = "none";
        bodyDiv.style.display = "none";
        metaDiv.style.display = "none";

        editTitleInput.style.display = "block";
        editBodyTextarea.style.display = "block";
        editTagInput.style.display = "block";
        editColorContainer.style.display = "block";

        editButton.style.display = "none";
        deleteButton.style.display = "none";
        pinButton.style.display = "none";
        archiveButton.style.display = "none";
        saveButton.style.display = "inline-block";
        cancelButton.style.display = "inline-block";

        memoDiv.classList.add("editing");

        editTitleInput.value = memo.title;
        editBodyTextarea.value = memo.body;
        editTagInput.value = memo.tag || "";
        editTitleInput.focus();
      });

      saveButton.addEventListener("click", function() {
        const newTitle = editTitleInput.value;
        const newBody = editBodyTextarea.value;
        const newTag = editTagInput.value;

        if (newTitle.trim() === "" && newBody.trim() === "") {
          alert("タイトルか本文のどちらかは入力してください。");
          return;
        }

        let newColor = "none";
        editColorRadios.forEach(function(radio) {
          if (radio.checked) {
            newColor = radio.value;
          }
        });

        const idText = memoDiv.dataset.id;
        const id = Number(idText);

        updateMemo(id, newTitle, newBody, newTag, newColor);
      });

      cancelButton.addEventListener("click", function() {
        titleDiv.style.display = "block";
        bodyDiv.style.display = "block";
        metaDiv.style.display = "block";

        editTitleInput.style.display = "none";
        editBodyTextarea.style.display = "none";
        editTagInput.style.display = "none";
        editColorContainer.style.display = "none";

        editButton.style.display = "inline-block";
        deleteButton.style.display = "inline-block";
        pinButton.style.display = "inline-block";
        archiveButton.style.display = "inline-block";
        saveButton.style.display = "none";
        cancelButton.style.display = "none";

        memoDiv.classList.remove("editing");
      });

      deleteButton.addEventListener("click", function(event) {
        const memoElement = event.target.closest(".memo-item");
        const idText = memoElement.dataset.id;
        const id = Number(idText);

        deleteMemo(id);
      });

      pinButton.addEventListener("click", function(event) {
        const memoElement = event.target.closest(".memo-item");
        const idText = memoElement.dataset.id;
        const id = Number(idText);

        togglePinned(id);
      });

      archiveButton.addEventListener("click", function(event) {
        const memoElement = event.target.closest(".memo-item");
        const idText = memoElement.dataset.id;
        const id = Number(idText);

        toggleArchived(id);
      });

      memoDiv.append(titleDiv);
      memoDiv.append(bodyDiv);
      memoDiv.append(metaDiv);
      memoDiv.append(editTitleInput);
      memoDiv.append(editBodyTextarea);
      memoDiv.append(editTagInput);
      memoDiv.append(editColorContainer);
      memoDiv.append(editButton);
      memoDiv.append(saveButton);
      memoDiv.append(cancelButton);
      memoDiv.append(deleteButton);
      memoDiv.append(pinButton);
      memoDiv.append(archiveButton);

      memoList.append(memoDiv);
    });
  }

  // --- イベント設定:メモ追加ボタン ---
  addMemoButton.addEventListener("click", function() {
    const title = memoTitleInput.value;
    const body = memoBodyInput.value;
    const tag = memoTagInput.value;
    const color = getSelectedColor();

    errorMessage.textContent = "";

    if (title.trim() === "" && body.trim() === "") {
      errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
      return;
    }

    addMemo(title, body, tag, color);

    memoTitleInput.value = "";
    memoBodyInput.value = "";
    memoTagInput.value = "";

    memoColorRadios.forEach(function(radio) {
      radio.checked = radio.value === "none";
    });

    renderMemos();
  });

  // --- イベント設定:検索入力欄 ---
  searchInput.addEventListener("input", function() {
    currentKeyword = searchInput.value;
    renderMemos();
  });

  // --- イベント設定:並び順ボタン ---
  sortButtons.forEach(function(button) {
    button.addEventListener("click", function() {
      const sortMode = button.dataset.sort;

      currentSort = sortMode;

      sortButtons.forEach(function(b) {
        b.classList.remove("active-sort");
      });

      button.classList.add("active-sort");

      renderMemos();
    });
  });

  // --- イベント設定:タグフィルタボタン ---
  tagFilterButtons.forEach(function(button) {
    button.addEventListener("click", function() {
      const tag = button.dataset.tag;

      currentTagFilter = tag;

      tagFilterButtons.forEach(function(b) {
        b.classList.remove("active-tag");
      });

      button.classList.add("active-tag");

      renderMemos();
    });
  });

  // --- イベント設定:表示モードフィルタボタン ---
  viewFilterButtons.forEach(function(button) {
    button.addEventListener("click", function() {
      const viewMode = button.dataset.view;

      currentViewMode = viewMode;

      viewFilterButtons.forEach(function(b) {
        b.classList.remove("active-view");
      });

      button.classList.add("active-view");

      renderMemos();
    });
  });

  // --- 初期処理 ---
  loadFromStorage();
  renderMemos();
</script>

5日目で本当に掴んでほしいこと

  • 「ピン」「アーカイブ」「表示モード」のような状態を すべて“ただのプロパティとフラグ”として扱えること
  • querySelectorAll()datasetclassList のパターンが、 並び順・タグフィルタ・表示モードフィルタに そのまま再利用できていること
  • 配列(状態)→ localStorage(保存)→ DOM(描画)→ イベント(操作)という流れの上に、 「重要度」「状態」「フィルタ」「検索」「並び替え」が 自然に積み上がっていること

ここまで来ると、 メモ帳アプリはもう「ただの練習用」ではなく、 自分の生活や仕事で本当に使えるツールになっていきます。 同じパターンを ToDoアプリや家計簿アプリにも どんどん応用してみてください。


メモ帳アプリ 6日目のゴール

6日目のテーマは 「メモ帳アプリを“使いやすく・壊れにくく・安心して使える”状態に仕上げる」 ことです。

1〜5日目で作ったメモ帳アプリは、

  • 追加・編集・削除
  • 検索・並び替え
  • タグ・色ラベル
  • ピン留め・アーカイブ
  • localStorage に保存

までできる状態でした。

6日目ではここから一歩進めて、

  • 入力中のメモを「下書き」として自動保存する
  • 文字数カウンターを表示する
  • 削除時に確認ダイアログを出す
  • 「全メモ削除」ボタンを追加し、安全に初期化できるようにする
  • メモ一覧を JSON として「エクスポート/インポート」できるようにする

というところまでを、ステップバイステップで作っていきます。

今日の全体ステップ

6日目の流れはこんな感じです。

  1. 入力フォームの内容を「下書き」として localStorage に保存する
  2. ページ読み込み時に「下書き」を復元する
  3. 入力中の文字数をリアルタイムで表示する
  4. 削除ボタンに確認ダイアログを追加する
  5. 「全メモ削除」ボタンを追加し、安全に初期化する
  6. JSON テキストエリアを使って「エクスポート/インポート」機能を作る

6日目の HTML レイアウト

5日目までの HTML に、 「文字数カウンター」「全削除ボタン」「エクスポート/インポート UI」を追加します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>メモ帳アプリ 6日目</title>
  <style>
    .memo-item {
      border: 1px solid #ccc;
      padding: 8px;
      margin-bottom: 8px;
    }

    .memo-item-title {
      font-weight: bold;
      margin-bottom: 4px;
    }

    .memo-item-body {
      white-space: pre-wrap;
    }

    .memo-meta {
      font-size: 12px;
      color: #666;
      margin-top: 4px;
    }

    .editing {
      background-color: #fff7e6;
    }

    .active-sort {
      background-color: #007bff;
      color: white;
    }

    .active-tag {
      background-color: #28a745;
      color: white;
    }

    .active-view {
      background-color: #6f42c1;
      color: white;
    }

    .color-red {
      border-left: 4px solid #ff4d4f;
    }

    .color-blue {
      border-left: 4px solid #1890ff;
    }

    .color-yellow {
      border-left: 4px solid #faad14;
    }

    .color-none {
      border-left: 4px solid transparent;
    }

    .pinned {
      border-top: 3px solid #ffc107;
      background-color: #fffbe6;
    }

    .archived {
      opacity: 0.6;
    }

    .counter {
      font-size: 12px;
      color: #666;
    }

    .danger {
      color: #c00;
      font-weight: bold;
    }

    textarea.export-area {
      width: 100%;
      height: 120px;
    }
  </style>
</head>
<body>
  <h1>メモ帳アプリ(6日目)</h1>

  <div>
    <label for="memoTitleInput">タイトル:</label>
    <input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
    <span id="titleCounter" class="counter"></span>
  </div>

  <div>
    <label for="memoBodyInput">本文:</label><br>
    <textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
    <div id="bodyCounter" class="counter"></div>
  </div>

  <div>
    <label for="memoTagInput">タグ:</label>
    <input id="memoTagInput" type="text" placeholder="例:仕事 / プライベート / アイデア">
  </div>

  <div>
    <span>色ラベル:</span>
    <label>
      <input type="radio" name="memoColor" value="none" checked> なし
    </label>
    <label>
      <input type="radio" name="memoColor" value="red"> 赤
    </label>
    <label>
      <input type="radio" name="memoColor" value="blue"> 青
    </label>
    <label>
      <input type="radio" name="memoColor" value="yellow"> 黄
    </label>
  </div>

  <div>
    <button id="addMemoButton">メモを追加</button>
    <button id="clearAllButton" class="danger">全メモ削除</button>
  </div>

  <p id="errorMessage" style="color: red;"></p>

  <h2>検索</h2>
  <div>
    <input id="searchInput" type="text" placeholder="タイトル・本文・タグから検索">
  </div>

  <h2>並び順</h2>
  <div id="sortButtons">
    <button data-sort="newest" class="active-sort">新しい順</button>
    <button data-sort="oldest">古い順</button>
  </div>

  <h2>タグフィルタ</h2>
  <div id="tagFilterButtons">
    <button data-tag="" class="active-tag">すべて</button>
    <button data-tag="仕事">仕事</button>
    <button data-tag="プライベート">プライベート</button>
    <button data-tag="アイデア">アイデア</button>
  </div>

  <h2>表示モード</h2>
  <div id="viewFilterButtons">
    <button data-view="all" class="active-view">通常(アーカイブ以外)</button>
    <button data-view="withArchived">アーカイブも含める</button>
    <button data-view="archivedOnly">アーカイブのみ</button>
  </div>

  <h2>メモ一覧</h2>
  <div id="memoList"></div>

  <h2>データのエクスポート/インポート</h2>
  <p>現在のメモデータを JSON としてコピーしたり、貼り付けて復元したりできます。</p>
  <div>
    <button id="exportButton">JSONを生成(エクスポート)</button>
    <button id="importButton">JSONから復元(インポート)</button>
  </div>
  <div>
    <textarea id="exportArea" class="export-area" placeholder="ここにJSONが表示されます/貼り付けてインポートできます"></textarea>
  </div>

  <script>
    // ここに JavaScript を書いていきます
  </script>
</body>
</html>

ここで押さえておきたいポイント

  • タイトルと本文の近くに「文字数カウンター」を置く
  • 「全メモ削除」ボタンは .danger クラスで注意喚起
  • JSON エクスポート/インポート用に #exportArea テキストエリアと #exportButton / #importButton を用意する

状態と DOM 参照の定義

6日目では、 「下書き」と「エクスポート/インポート」を扱います。

// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;

let currentKeyword = "";
let currentSort = "newest";
let currentTagFilter = "";
let currentViewMode = "all";

const STORAGE_KEY = "my_memo_app_data";
const DRAFT_STORAGE_KEY = "my_memo_app_draft";

// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const memoTagInput = document.querySelector("#memoTagInput");
const addMemoButton = document.querySelector("#addMemoButton");
const clearAllButton = document.querySelector("#clearAllButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");

const titleCounter = document.querySelector("#titleCounter");
const bodyCounter = document.querySelector("#bodyCounter");

const searchInput = document.querySelector("#searchInput");
const sortButtons = document.querySelectorAll("#sortButtons button");
const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
const viewFilterButtons = document.querySelectorAll("#viewFilterButtons button");
const memoColorRadios = document.querySelectorAll("input[name='memoColor']");

const exportButton = document.querySelector("#exportButton");
const importButton = document.querySelector("#importButton");
const exportArea = document.querySelector("#exportArea");
JavaScript

深掘りポイント:

  • 「下書き」はメモとは別のキー(DRAFT_STORAGE_KEY)で保存する
  • エクスポート/インポート用のテキストエリアも querySelector() で普通に扱える

localStorage の保存・読み込み(メモ本体)

メモ本体の保存・読み込みは 5日目と同じパターンです。

// --- ストレージ:状態をlocalStorageに保存する ---
function saveToStorage() {
  const data = {
    memos: memos,
    nextMemoId: nextMemoId
  };

  const json = JSON.stringify(data);
  localStorage.setItem(STORAGE_KEY, json);
}

// --- ストレージ:localStorageから状態を読み込む ---
function loadFromStorage() {
  const json = localStorage.getItem(STORAGE_KEY);

  if (json === null) {
    return;
  }

  try {
    const data = JSON.parse(json);

    if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
      memos = data.memos;
      nextMemoId = data.nextMemoId;
    }
  } catch (error) {
    console.error("保存データの読み込みに失敗しました:", error);
  }
}
JavaScript

下書きの保存・読み込み

入力中のタイトル・本文・タグ・色ラベルを 「下書き」として localStorage に保存します。

// --- ストレージ:下書きを保存する ---
function saveDraft() {
  const draft = {
    title: memoTitleInput.value,
    body: memoBodyInput.value,
    tag: memoTagInput.value,
    color: getSelectedColor()
  };

  const json = JSON.stringify(draft);
  localStorage.setItem(DRAFT_STORAGE_KEY, json);
}

// --- ストレージ:下書きを読み込む ---
function loadDraft() {
  const json = localStorage.getItem(DRAFT_STORAGE_KEY);

  if (json === null) {
    return;
  }

  try {
    const draft = JSON.parse(json);

    memoTitleInput.value = draft.title || "";
    memoBodyInput.value = draft.body || "";
    memoTagInput.value = draft.tag || "";

    memoColorRadios.forEach(function(radio) {
      radio.checked = radio.value === (draft.color || "none");
    });

    updateCounters();
  } catch (error) {
    console.error("下書きの読み込みに失敗しました:", error);
  }
}
JavaScript

深掘りポイント:

  • 「下書き」はメモ一覧とは別のオブジェクトとして保存する
  • ページを閉じても、次に開いたときに入力途中の内容が復元される
  • updateCounters() を呼んで、復元後の文字数表示も整える

色ラベルの選択値を取得する関数(復習)

// --- ユーティリティ:選択されている色ラベルを取得する ---
function getSelectedColor() {
  let selectedValue = "none";

  memoColorRadios.forEach(function(radio) {
    if (radio.checked) {
      selectedValue = radio.value;
    }
  });

  return selectedValue;
}
JavaScript

文字数カウンターの更新

タイトルと本文の文字数をリアルタイムで表示します。

// --- ユーティリティ:文字数カウンターを更新する ---
function updateCounters() {
  const titleLength = memoTitleInput.value.length;
  const bodyLength = memoBodyInput.value.length;

  titleCounter.textContent = `文字数: ${titleLength}`;
  bodyCounter.textContent = `本文の文字数: ${bodyLength}`;
}
JavaScript

イベントでカウンターを動かす

memoTitleInput.addEventListener("input", function() {
  updateCounters();
  saveDraft(); // 入力のたびに下書きを保存
});

memoBodyInput.addEventListener("input", function() {
  updateCounters();
  saveDraft();
});

memoTagInput.addEventListener("input", function() {
  saveDraft();
});

memoColorRadios.forEach(function(radio) {
  radio.addEventListener("change", function() {
    saveDraft();
  });
});
JavaScript

深掘りポイント:

  • 「入力のたびに下書きを保存する」ことで、 ブラウザを閉じても安心して続きが書ける
  • 文字数カウンターは「ただの value.length」だが、 UI の安心感が一気に上がる

メモ追加ロジック:下書きのクリアも行う

メモ追加後に、 フォームをクリアし、下書きも削除します。

// --- 操作ロジック:メモを追加する ---
function addMemo(title, body, tag, color) {
  const newMemo = {
    id: nextMemoId,
    title: title,
    body: body,
    tag: tag,
    color: color,
    pinned: false,
    archived: false,
    createdAt: Date.now()
  };

  nextMemoId = nextMemoId + 1;

  memos.push(newMemo);

  saveToStorage();
}
JavaScript

メモ編集・ピン・アーカイブ・削除ロジック(復習+削除確認)

編集・ピン・アーカイブは 5日目と同じです。 削除だけ「確認ダイアログ」を追加します。

// --- 操作ロジック:メモを編集する ---
function updateMemo(id, newTitle, newBody, newTag, newColor) {
  memos.forEach(function(memo) {
    if (memo.id === id) {
      memo.title = newTitle;
      memo.body = newBody;
      memo.tag = newTag;
      memo.color = newColor;
    }
  });

  saveToStorage();
  renderMemos();
}

// --- 操作ロジック:ピン状態切り替え ---
function togglePinned(id) {
  memos.forEach(function(memo) {
    if (memo.id === id) {
      memo.pinned = !memo.pinned;
    }
  });

  saveToStorage();
  renderMemos();
}

// --- 操作ロジック:アーカイブ状態切り替え ---
function toggleArchived(id) {
  memos.forEach(function(memo) {
    if (memo.id === id) {
      memo.archived = !memo.archived;
    }
  });

  saveToStorage();
  renderMemos();
}

// --- 操作ロジック:メモ削除(確認付き) ---
function deleteMemo(id) {
  const ok = window.confirm("このメモを削除しますか?(元に戻せません)");
  if (!ok) {
    return;
  }

  const newMemos = memos.filter(function(memo) {
    return memo.id !== id;
  });

  memos.length = 0;
  newMemos.forEach(function(memo) {
    memos.push(memo);
  });

  saveToStorage();
  renderMemos();
}
JavaScript

深掘りポイント:

  • window.confirm() は「OK/キャンセル」のシンプルな確認ダイアログ
  • 「元に戻せません」と明示することで、 ユーザーが誤操作をしにくくなる

全メモ削除ロジック

「全メモ削除」ボタンで、 アプリ全体を初期化できるようにします。

// --- 操作ロジック:全メモ削除 ---
function clearAllMemos() {
  const ok = window.confirm("本当に全てのメモを削除しますか?(元に戻せません)");
  if (!ok) {
    return;
  }

  memos.length = 0;
  nextMemoId = 1;

  saveToStorage();
  renderMemos();
}
JavaScript

6日目の心臓部:描画(renderMemos)

描画ロジックは 5日目のものをベースにします。 ここでは「削除ボタンに確認が付いた」ことだけ意識して見てください。

// --- 描画:状態から画面(DOM)を作り直す ---
function renderMemos() {
  memoList.innerHTML = "";

  let visibleMemos = memos;

  if (currentKeyword.trim() !== "") {
    const keywordLower = currentKeyword.toLowerCase();

    visibleMemos = visibleMemos.filter(function(memo) {
      const titleLower = memo.title.toLowerCase();
      const bodyLower = memo.body.toLowerCase();
      const tagLower = (memo.tag || "").toLowerCase();

      return (
        titleLower.includes(keywordLower) ||
        bodyLower.includes(keywordLower) ||
        tagLower.includes(keywordLower)
      );
    });
  }

  if (currentTagFilter !== "") {
    visibleMemos = visibleMemos.filter(function(memo) {
      return memo.tag === currentTagFilter;
    });
  }

  if (currentViewMode === "all") {
    visibleMemos = visibleMemos.filter(function(memo) {
      return memo.archived === false;
    });
  } else if (currentViewMode === "archivedOnly") {
    visibleMemos = visibleMemos.filter(function(memo) {
      return memo.archived === true;
    });
  }

  visibleMemos = visibleMemos.slice();

  visibleMemos.sort(function(a, b) {
    if (a.pinned && !b.pinned) {
      return -1;
    }
    if (!a.pinned && b.pinned) {
      return 1;
    }

    if (currentSort === "newest") {
      return b.createdAt - a.createdAt;
    } else {
      return a.createdAt - b.createdAt;
    }
  });

  visibleMemos.forEach(function(memo) {
    const memoDiv = document.createElement("div");
    memoDiv.classList.add("memo-item");
    memoDiv.dataset.id = String(memo.id);

    const colorClass = "color-" + (memo.color || "none");
    memoDiv.classList.add(colorClass);

    if (memo.pinned) {
      memoDiv.classList.add("pinned");
    }

    if (memo.archived) {
      memoDiv.classList.add("archived");
    }

    const titleDiv = document.createElement("div");
    titleDiv.classList.add("memo-item-title");
    titleDiv.textContent = memo.title;

    const bodyDiv = document.createElement("div");
    bodyDiv.classList.add("memo-item-body");
    bodyDiv.textContent = memo.body;

    const metaDiv = document.createElement("div");
    metaDiv.classList.add("memo-meta");
    metaDiv.textContent =
      `タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"} / ` +
      `ピン: ${memo.pinned ? "ON" : "OFF"} / アーカイブ: ${memo.archived ? "ON" : "OFF"}`;

    const editTitleInput = document.createElement("input");
    editTitleInput.type = "text";
    editTitleInput.value = memo.title;
    editTitleInput.style.display = "none";

    const editBodyTextarea = document.createElement("textarea");
    editBodyTextarea.rows = 4;
    editBodyTextarea.cols = 40;
    editBodyTextarea.value = memo.body;
    editBodyTextarea.style.display = "none";

    const editTagInput = document.createElement("input");
    editTagInput.type = "text";
    editTagInput.value = memo.tag || "";
    editTagInput.style.display = "none";

    const editColorContainer = document.createElement("div");
    editColorContainer.style.display = "none";

    const colors = ["none", "red", "blue", "yellow"];
    const editColorRadios = [];

    colors.forEach(function(color) {
      const label = document.createElement("label");
      const radio = document.createElement("input");
      radio.type = "radio";
      radio.name = "editColor-" + memo.id;
      radio.value = color;

      if (memo.color === color || (!memo.color && color === "none")) {
        radio.checked = true;
      }

      label.append(radio);
      label.append(document.createTextNode(" " + color));

      editColorContainer.append(label);

      editColorRadios.push(radio);
    });

    const editButton = document.createElement("button");
    editButton.textContent = "編集";

    const saveButton = document.createElement("button");
    saveButton.textContent = "保存";
    saveButton.style.display = "none";

    const cancelButton = document.createElement("button");
    cancelButton.textContent = "キャンセル";
    cancelButton.style.display = "none";

    const deleteButton = document.createElement("button");
    deleteButton.textContent = "削除";

    const pinButton = document.createElement("button");
    pinButton.textContent = memo.pinned ? "ピン解除" : "ピン";

    const archiveButton = document.createElement("button");
    archiveButton.textContent = memo.archived ? "アーカイブ解除" : "アーカイブ";

    editButton.addEventListener("click", function() {
      titleDiv.style.display = "none";
      bodyDiv.style.display = "none";
      metaDiv.style.display = "none";

      editTitleInput.style.display = "block";
      editBodyTextarea.style.display = "block";
      editTagInput.style.display = "block";
      editColorContainer.style.display = "block";

      editButton.style.display = "none";
      deleteButton.style.display = "none";
      pinButton.style.display = "none";
      archiveButton.style.display = "none";
      saveButton.style.display = "inline-block";
      cancelButton.style.display = "inline-block";

      memoDiv.classList.add("editing");

      editTitleInput.value = memo.title;
      editBodyTextarea.value = memo.body;
      editTagInput.value = memo.tag || "";
      editTitleInput.focus();
    });

    saveButton.addEventListener("click", function() {
      const newTitle = editTitleInput.value;
      const newBody = editBodyTextarea.value;
      const newTag = editTagInput.value;

      if (newTitle.trim() === "" && newBody.trim() === "") {
        alert("タイトルか本文のどちらかは入力してください。");
        return;
      }

      let newColor = "none";
      editColorRadios.forEach(function(radio) {
        if (radio.checked) {
          newColor = radio.value;
        }
      });

      const idText = memoDiv.dataset.id;
      const id = Number(idText);

      updateMemo(id, newTitle, newBody, newTag, newColor);
    });

    cancelButton.addEventListener("click", function() {
      titleDiv.style.display = "block";
      bodyDiv.style.display = "block";
      metaDiv.style.display = "block";

      editTitleInput.style.display = "none";
      editBodyTextarea.style.display = "none";
      editTagInput.style.display = "none";
      editColorContainer.style.display = "none";

      editButton.style.display = "inline-block";
      deleteButton.style.display = "inline-block";
      pinButton.style.display = "inline-block";
      archiveButton.style.display = "inline-block";
      saveButton.style.display = "none";
      cancelButton.style.display = "none";

      memoDiv.classList.remove("editing");
    });

    deleteButton.addEventListener("click", function(event) {
      const memoElement = event.target.closest(".memo-item");
      const idText = memoElement.dataset.id;
      const id = Number(idText);

      deleteMemo(id);
    });

    pinButton.addEventListener("click", function(event) {
      const memoElement = event.target.closest(".memo-item");
      const idText = memoElement.dataset.id;
      const id = Number(idText);

      togglePinned(id);
    });

    archiveButton.addEventListener("click", function(event) {
      const memoElement = event.target.closest(".memo-item");
      const idText = memoElement.dataset.id;
      const id = Number(idText);

      toggleArchived(id);
    });

    memoDiv.append(titleDiv);
    memoDiv.append(bodyDiv);
    memoDiv.append(metaDiv);
    memoDiv.append(editTitleInput);
    memoDiv.append(editBodyTextarea);
    memoDiv.append(editTagInput);
    memoDiv.append(editColorContainer);
    memoDiv.append(editButton);
    memoDiv.append(saveButton);
    memoDiv.append(cancelButton);
    memoDiv.append(deleteButton);
    memoDiv.append(pinButton);
    memoDiv.append(archiveButton);

    memoList.append(memoDiv);
  });
}
JavaScript

エクスポート機能:JSONを生成してテキストエリアに表示

メモ一覧を JSON にして、 テキストエリアに表示します。

// --- 操作ロジック:エクスポート(JSON生成) ---
function exportData() {
  const data = {
    memos: memos,
    nextMemoId: nextMemoId
  };

  const json = JSON.stringify(data, null, 2); // 見やすいように整形
  exportArea.value = json;
}
JavaScript

インポート機能:JSONから復元

テキストエリアに貼り付けた JSON から、 メモ一覧を復元します。

// --- 操作ロジック:インポート(JSONから復元) ---
function importData() {
  const text = exportArea.value;

  if (text.trim() === "") {
    alert("JSONが空です。エクスポートしたデータを貼り付けてください。");
    return;
  }

  try {
    const data = JSON.parse(text);

    if (!Array.isArray(data.memos) || typeof data.nextMemoId !== "number") {
      alert("JSONの形式が正しくありません。");
      return;
    }

    const ok = window.confirm("現在のメモを上書きして復元しますか?");
    if (!ok) {
      return;
    }

    memos.length = 0;
    data.memos.forEach(function(memo) {
      memos.push(memo);
    });

    nextMemoId = data.nextMemoId;

    saveToStorage();
    renderMemos();

    alert("復元が完了しました。");
  } catch (error) {
    console.error("インポートに失敗しました:", error);
    alert("JSONの解析に失敗しました。形式を確認してください。");
  }
}
JavaScript

深掘りポイント:

  • JSON.stringify()JSON.parse() を使うことで、 「アプリの状態を丸ごとテキストに変換/復元」できる
  • インポート時には必ず確認ダイアログを挟み、 誤操作でデータを消さないようにする

イベント設定まとめ

最後に、各ボタン・入力欄にイベントを設定します。

// --- メモ追加ボタン ---
addMemoButton.addEventListener("click", function() {
  const title = memoTitleInput.value;
  const body = memoBodyInput.value;
  const tag = memoTagInput.value;
  const color = getSelectedColor();

  errorMessage.textContent = "";

  if (title.trim() === "" && body.trim() === "") {
    errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
    return;
  }

  addMemo(title, body, tag, color);

  memoTitleInput.value = "";
  memoBodyInput.value = "";
  memoTagInput.value = "";

  memoColorRadios.forEach(function(radio) {
    radio.checked = radio.value === "none";
  });

  localStorage.removeItem(DRAFT_STORAGE_KEY); // 下書きを消す
  updateCounters();

  renderMemos();
});

// --- 全メモ削除ボタン ---
clearAllButton.addEventListener("click", function() {
  clearAllMemos();
});

// --- 検索入力欄 ---
searchInput.addEventListener("input", function() {
  currentKeyword = searchInput.value;
  renderMemos();
});

// --- 並び順ボタン ---
sortButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const sortMode = button.dataset.sort;

    currentSort = sortMode;

    sortButtons.forEach(function(b) {
      b.classList.remove("active-sort");
    });

    button.classList.add("active-sort");

    renderMemos();
  });
});

// --- タグフィルタボタン ---
tagFilterButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const tag = button.dataset.tag;

    currentTagFilter = tag;

    tagFilterButtons.forEach(function(b) {
      b.classList.remove("active-tag");
    });

    button.classList.add("active-tag");

    renderMemos();
  });
});

// --- 表示モードフィルタボタン ---
viewFilterButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const viewMode = button.dataset.view;

    currentViewMode = viewMode;

    viewFilterButtons.forEach(function(b) {
      b.classList.remove("active-view");
    });

    button.classList.add("active-view");

    renderMemos();
  });
});

// --- エクスポートボタン ---
exportButton.addEventListener("click", function() {
  exportData();
});

// --- インポートボタン ---
importButton.addEventListener("click", function() {
  importData();
});
JavaScript

初期処理

ページ読み込み時に、 保存データと下書きを読み込み、カウンターを更新します。

// --- 初期処理 ---
loadFromStorage();
loadDraft();
updateCounters();
renderMemos();
JavaScript

6日目で本当に掴んでほしいこと

  • 「下書き」「文字数カウンター」「削除確認」「全削除」「エクスポート/インポート」は すべて“状態+localStorage+DOM+イベント”の組み合わせで作れる
  • querySelector() / querySelectorAll() / addEventListener() / createElement() / append() / classList / dataset のパターンが、 ここまで来ると完全に「自分の道具」になっているはず
  • アプリは「機能が増える」だけでなく、 安心して使えるようにする工夫(確認・下書き・バックアップ)が とても大事だということ

この6日目までのメモ帳アプリは、 もう立派に「自分の生活や仕事で使えるプロダクト」です。 同じ考え方を ToDoアプリや家計簿アプリにも どんどん応用して、自分だけのツール群を育てていってください。


メモ帳アプリ 7日目のゴール

7日目のテーマは 「7日間で育ててきたメモ帳アプリを“完成版”としてまとめ上げる」 ことです。

1〜6日目で作ったメモ帳アプリはすでに、

  • 追加・編集・削除
  • 検索・並び替え
  • タグ・色ラベル
  • ピン留め・アーカイブ
  • 下書き保存・文字数カウンター
  • 全削除・エクスポート/インポート
  • localStorage による永続化

まで備えた、かなり実用的なツールになっています。

7日目ではここから仕上げとして、

  • コードを「読みやすく・直しやすく」整理する
  • よく使う処理を関数にまとめて再利用する
  • UI のちょっとした改善(キーボード操作・フォーカス)を入れる
  • 「アプリの構造」を意識して、全体を見渡せるようにする

というところまでを、ステップバイステップでやっていきます。

今日の全体ステップ

7日目の流れはこんな感じです。

  1. アプリ全体の構造を「役割ごと」に整理して眺める
  2. よく出てくる処理を小さな関数に切り出す
  3. イベント設定を「まとまったブロック」として整理する
  4. キーボード操作(Enterで追加、Escで編集キャンセル)を追加する
  5. フォーカスの動き(追加後にタイトルへ戻す)を整える
  6. 完成版の HTML+JavaScript をひとつのテンプレートとしてまとめる

アプリの構造をざっくり整理する

まず、メモ帳アプリのコードを「役割ごと」に分けて考えます。

  • 状態(State)
    • memos(メモ一覧)
    • nextMemoId(次に使うID)
    • currentKeyword(検索キーワード)
    • currentSort(並び順)
    • currentTagFilter(タグフィルタ)
    • currentViewMode(表示モード)
  • DOM参照
    • 入力フォーム(タイトル・本文・タグ・色)
    • ボタン類(追加・全削除・並び順・フィルタ・エクスポート・インポート)
    • メモ一覧表示領域
    • 文字数カウンター
    • JSONテキストエリア
  • ストレージ関連
    • メモ本体の保存・読み込み(STORAGE_KEY
    • 下書きの保存・読み込み(DRAFT_STORAGE_KEY
  • ユーティリティ関数
    • 色ラベル取得(getSelectedColor()
    • 文字数カウンター更新(updateCounters()
    • 下書き保存・読み込み(saveDraft() / loadDraft()
  • 操作ロジック
    • メモ追加・編集・削除
    • ピン状態切り替え・アーカイブ状態切り替え
    • 全メモ削除
    • エクスポート・インポート
  • 描画ロジック
    • renderMemos():状態から DOM を作り直す
  • イベント設定
    • 入力欄(タイトル・本文・タグ・色)
    • ボタン(追加・全削除・並び順・タグフィルタ・表示モード・エクスポート・インポート)

この「役割ごとの整理」ができると、 コードを読むとき・直すときに「どこを触ればいいか」が 一気に分かりやすくなります。

よく出てくる処理を小さな関数に切り出す

7日目では、 「同じような処理が何度も出てきているところ」を 小さな関数にまとめていきます。

例1:ボタンの「アクティブ状態」を切り替える処理

並び順ボタン・タグフィルタボタン・表示モードボタンは、 どれも同じパターンで「選択中のボタンにクラスを付ける」 処理を書いていました。

これをひとつの関数にまとめます。

// --- ユーティリティ:ボタン群のアクティブ状態を切り替える ---
function setActiveButton(buttons, activeButton, activeClassName) {
  buttons.forEach(function(button) {
    button.classList.remove(activeClassName);
  });

  activeButton.classList.add(activeClassName);
}
JavaScript

これを使うと、並び順ボタンのイベントはこう書けます。

sortButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const sortMode = button.dataset.sort;

    currentSort = sortMode;

    setActiveButton(sortButtons, button, "active-sort");

    renderMemos();
  });
});
JavaScript

タグフィルタボタンも同じように書けます。

tagFilterButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const tag = button.dataset.tag;

    currentTagFilter = tag;

    setActiveButton(tagFilterButtons, button, "active-tag");

    renderMemos();
  });
});
JavaScript

表示モードボタンも同じです。

viewFilterButtons.forEach(function(button) {
  button.addEventListener("click", function() {
    const viewMode = button.dataset.view;

    currentViewMode = viewMode;

    setActiveButton(viewFilterButtons, button, "active-view");

    renderMemos();
  });
});
JavaScript

深掘りポイント:

  • 「同じパターンが3回出てきたら、関数にまとめる」
  • setActiveButton() のような小さな関数が増えるほど、 コードは「読みやすく・直しやすく」なっていく

キーボード操作を追加する

アプリを少しだけ「気持ちよく」使えるように、 キーボード操作を追加します。

Enterキーでメモ追加

タイトル入力欄で Enter を押したら、 「メモを追加」ボタンを押したのと同じ動きをさせます。

memoTitleInput.addEventListener("keydown", function(event) {
  if (event.key === "Enter") {
    event.preventDefault(); // フォームの送信などを防ぐ

    addMemoFromForm();
  }
});
JavaScript

ここで、 「フォームの内容からメモを追加する処理」を addMemoFromForm() という関数にまとめておきます。

// --- ユーティリティ:フォームからメモを追加する ---
function addMemoFromForm() {
  const title = memoTitleInput.value;
  const body = memoBodyInput.value;
  const tag = memoTagInput.value;
  const color = getSelectedColor();

  errorMessage.textContent = "";

  if (title.trim() === "" && body.trim() === "") {
    errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
    return;
  }

  addMemo(title, body, tag, color);

  memoTitleInput.value = "";
  memoBodyInput.value = "";
  memoTagInput.value = "";

  memoColorRadios.forEach(function(radio) {
    radio.checked = radio.value === "none";
  });

  localStorage.removeItem(DRAFT_STORAGE_KEY);
  updateCounters();

  memoTitleInput.focus(); // 追加後にタイトル入力欄へフォーカスを戻す

  renderMemos();
}
JavaScript

「メモを追加」ボタンのイベントも、 この関数を呼ぶだけにします。

addMemoButton.addEventListener("click", function() {
  addMemoFromForm();
});
JavaScript

Escキーで編集キャンセル

編集モードのときに Esc を押したら、 「キャンセル」ボタンを押したのと同じ動きをさせます。

これは renderMemos() の中で、 編集用の要素を作るときに設定します。

cancelButton.addEventListener("click", function() {
  exitEditMode();
});

editTitleInput.addEventListener("keydown", function(event) {
  if (event.key === "Escape") {
    exitEditMode();
  }
});

editBodyTextarea.addEventListener("keydown", function(event) {
  if (event.key === "Escape") {
    exitEditMode();
  }
});
JavaScript

ここで exitEditMode() という関数を そのメモの編集状態を抜ける処理としてまとめます。

function exitEditMode() {
  titleDiv.style.display = "block";
  bodyDiv.style.display = "block";
  metaDiv.style.display = "block";

  editTitleInput.style.display = "none";
  editBodyTextarea.style.display = "none";
  editTagInput.style.display = "none";
  editColorContainer.style.display = "none";

  editButton.style.display = "inline-block";
  deleteButton.style.display = "inline-block";
  pinButton.style.display = "inline-block";
  archiveButton.style.display = "inline-block";
  saveButton.style.display = "none";
  cancelButton.style.display = "none";

  memoDiv.classList.remove("editing");
}
JavaScript

深掘りポイント:

  • 「ボタンとキーボードで同じ動きをさせる」ことで、 アプリがぐっと使いやすくなる
  • キーボード操作は keydown イベントで拾い、 event.key を見て分岐する

完成版テンプレート:HTML+JavaScript

ここまでの内容を踏まえて、 7日目の「完成版メモ帳アプリ」を ひとつのテンプレートとしてまとめます。

完成版 HTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>メモ帳アプリ 完成版</title>
  <style>
    .memo-item {
      border: 1px solid #ccc;
      padding: 8px;
      margin-bottom: 8px;
    }

    .memo-item-title {
      font-weight: bold;
      margin-bottom: 4px;
    }

    .memo-item-body {
      white-space: pre-wrap;
    }

    .memo-meta {
      font-size: 12px;
      color: #666;
      margin-top: 4px;
    }

    .editing {
      background-color: #fff7e6;
    }

    .active-sort {
      background-color: #007bff;
      color: white;
    }

    .active-tag {
      background-color: #28a745;
      color: white;
    }

    .active-view {
      background-color: #6f42c1;
      color: white;
    }

    .color-red {
      border-left: 4px solid #ff4d4f;
    }

    .color-blue {
      border-left: 4px solid #1890ff;
    }

    .color-yellow {
      border-left: 4px solid #faad14;
    }

    .color-none {
      border-left: 4px solid transparent;
    }

    .pinned {
      border-top: 3px solid #ffc107;
      background-color: #fffbe6;
    }

    .archived {
      opacity: 0.6;
    }

    .counter {
      font-size: 12px;
      color: #666;
      margin-left: 8px;
    }

    .danger {
      color: #c00;
      font-weight: bold;
    }

    textarea.export-area {
      width: 100%;
      height: 120px;
    }
  </style>
</head>
<body>
  <h1>メモ帳アプリ(7日間完成版)</h1>

  <div>
    <label for="memoTitleInput">タイトル:</label>
    <input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
    <span id="titleCounter" class="counter"></span>
  </div>

  <div>
    <label for="memoBodyInput">本文:</label><br>
    <textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
    <div id="bodyCounter" class="counter"></div>
  </div>

  <div>
    <label for="memoTagInput">タグ:</label>
    <input id="memoTagInput" type="text" placeholder="例:仕事 / プライベート / アイデア">
  </div>

  <div>
    <span>色ラベル:</span>
    <label>
      <input type="radio" name="memoColor" value="none" checked> なし
    </label>
    <label>
      <input type="radio" name="memoColor" value="red"> 赤
    </label>
    <label>
      <input type="radio" name="memoColor" value="blue"> 青
    </label>
    <label>
      <input type="radio" name="memoColor" value="yellow"> 黄
    </label>
  </div>

  <div>
    <button id="addMemoButton">メモを追加(Enterでも追加)</button>
    <button id="clearAllButton" class="danger">全メモ削除</button>
  </div>

  <p id="errorMessage" style="color: red;"></p>

  <h2>検索</h2>
  <div>
    <input id="searchInput" type="text" placeholder="タイトル・本文・タグから検索">
  </div>

  <h2>並び順</h2>
  <div id="sortButtons">
    <button data-sort="newest" class="active-sort">新しい順</button>
    <button data-sort="oldest">古い順</button>
  </div>

  <h2>タグフィルタ</h2>
  <div id="tagFilterButtons">
    <button data-tag="" class="active-tag">すべて</button>
    <button data-tag="仕事">仕事</button>
    <button data-tag="プライベート">プライベート</button>
    <button data-tag="アイデア">アイデア</button>
  </div>

  <h2>表示モード</h2>
  <div id="viewFilterButtons">
    <button data-view="all" class="active-view">通常(アーカイブ以外)</button>
    <button data-view="withArchived">アーカイブも含める</button>
    <button data-view="archivedOnly">アーカイブのみ</button>
  </div>

  <h2>メモ一覧</h2>
  <div id="memoList"></div>

  <h2>データのエクスポート/インポート</h2>
  <p>現在のメモデータを JSON としてコピーしたり、貼り付けて復元したりできます。</p>
  <div>
    <button id="exportButton">JSONを生成(エクスポート)</button>
    <button id="importButton">JSONから復元(インポート)</button>
  </div>
  <div>
    <textarea id="exportArea" class="export-area" placeholder="ここにJSONが表示されます/貼り付けてインポートできます"></textarea>
  </div>

  <script>
    // --- アプリの状態(State) ---
    let memos = [];
    let nextMemoId = 1;

    let currentKeyword = "";
    let currentSort = "newest";
    let currentTagFilter = "";
    let currentViewMode = "all";

    const STORAGE_KEY = "my_memo_app_data";
    const DRAFT_STORAGE_KEY = "my_memo_app_draft";

    // --- DOM参照 ---
    const memoTitleInput = document.querySelector("#memoTitleInput");
    const memoBodyInput = document.querySelector("#memoBodyInput");
    const memoTagInput = document.querySelector("#memoTagInput");
    const addMemoButton = document.querySelector("#addMemoButton");
    const clearAllButton = document.querySelector("#clearAllButton");
    const errorMessage = document.querySelector("#errorMessage");
    const memoList = document.querySelector("#memoList");

    const titleCounter = document.querySelector("#titleCounter");
    const bodyCounter = document.querySelector("#bodyCounter");

    const searchInput = document.querySelector("#searchInput");
    const sortButtons = document.querySelectorAll("#sortButtons button");
    const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
    const viewFilterButtons = document.querySelectorAll("#viewFilterButtons button");
    const memoColorRadios = document.querySelectorAll("input[name='memoColor']");

    const exportButton = document.querySelector("#exportButton");
    const importButton = document.querySelector("#importButton");
    const exportArea = document.querySelector("#exportArea");

    // --- ストレージ:保存 ---
    function saveToStorage() {
      const data = {
        memos: memos,
        nextMemoId: nextMemoId
      };

      const json = JSON.stringify(data);
      localStorage.setItem(STORAGE_KEY, json);
    }

    // --- ストレージ:読み込み ---
    function loadFromStorage() {
      const json = localStorage.getItem(STORAGE_KEY);

      if (json === null) {
        return;
      }

      try {
        const data = JSON.parse(json);

        if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
          memos = data.memos;
          nextMemoId = data.nextMemoId;
        }
      } catch (error) {
        console.error("保存データの読み込みに失敗しました:", error);
      }
    }

    // --- ストレージ:下書き保存 ---
    function saveDraft() {
      const draft = {
        title: memoTitleInput.value,
        body: memoBodyInput.value,
        tag: memoTagInput.value,
        color: getSelectedColor()
      };

      const json = JSON.stringify(draft);
      localStorage.setItem(DRAFT_STORAGE_KEY, json);
    }

    // --- ストレージ:下書き読み込み ---
    function loadDraft() {
      const json = localStorage.getItem(DRAFT_STORAGE_KEY);

      if (json === null) {
        return;
      }

      try {
        const draft = JSON.parse(json);

        memoTitleInput.value = draft.title || "";
        memoBodyInput.value = draft.body || "";
        memoTagInput.value = draft.tag || "";

        memoColorRadios.forEach(function(radio) {
          radio.checked = radio.value === (draft.color || "none");
        });

        updateCounters();
      } catch (error) {
        console.error("下書きの読み込みに失敗しました:", error);
      }
    }

    // --- ユーティリティ:選択されている色ラベルを取得 ---
    function getSelectedColor() {
      let selectedValue = "none";

      memoColorRadios.forEach(function(radio) {
        if (radio.checked) {
          selectedValue = radio.value;
        }
      });

      return selectedValue;
    }

    // --- ユーティリティ:文字数カウンター更新 ---
    function updateCounters() {
      const titleLength = memoTitleInput.value.length;
      const bodyLength = memoBodyInput.value.length;

      titleCounter.textContent = `文字数: ${titleLength}`;
      bodyCounter.textContent = `本文の文字数: ${bodyLength}`;
    }

    // --- ユーティリティ:ボタン群のアクティブ状態を切り替える ---
    function setActiveButton(buttons, activeButton, activeClassName) {
      buttons.forEach(function(button) {
        button.classList.remove(activeClassName);
      });

      activeButton.classList.add(activeClassName);
    }

    // --- 操作ロジック:メモ追加 ---
    function addMemo(title, body, tag, color) {
      const newMemo = {
        id: nextMemoId,
        title: title,
        body: body,
        tag: tag,
        color: color,
        pinned: false,
        archived: false,
        createdAt: Date.now()
      };

      nextMemoId = nextMemoId + 1;

      memos.push(newMemo);

      saveToStorage();
    }

    // --- 操作ロジック:メモ編集 ---
    function updateMemo(id, newTitle, newBody, newTag, newColor) {
      memos.forEach(function(memo) {
        if (memo.id === id) {
          memo.title = newTitle;
          memo.body = newBody;
          memo.tag = newTag;
          memo.color = newColor;
        }
      });

      saveToStorage();
      renderMemos();
    }

    // --- 操作ロジック:ピン状態切り替え ---
    function togglePinned(id) {
      memos.forEach(function(memo) {
        if (memo.id === id) {
          memo.pinned = !memo.pinned;
        }
      });

      saveToStorage();
      renderMemos();
    }

    // --- 操作ロジック:アーカイブ状態切り替え ---
    function toggleArchived(id) {
      memos.forEach(function(memo) {
        if (memo.id === id) {
          memo.archived = !memo.archived;
        }
      });

      saveToStorage();
      renderMemos();
    }

    // --- 操作ロジック:メモ削除(確認付き) ---
    function deleteMemo(id) {
      const ok = window.confirm("このメモを削除しますか?(元に戻せません)");
      if (!ok) {
        return;
      }

      const newMemos = memos.filter(function(memo) {
        return memo.id !== id;
      });

      memos.length = 0;
      newMemos.forEach(function(memo) {
        memos.push(memo);
      });

      saveToStorage();
      renderMemos();
    }

    // --- 操作ロジック:全メモ削除 ---
    function clearAllMemos() {
      const ok = window.confirm("本当に全てのメモを削除しますか?(元に戻せません)");
      if (!ok) {
        return;
      }

      memos.length = 0;
      nextMemoId = 1;

      saveToStorage();
      renderMemos();
    }

    // --- 操作ロジック:エクスポート ---
    function exportData() {
      const data = {
        memos: memos,
        nextMemoId: nextMemoId
      };

      const json = JSON.stringify(data, null, 2);
      exportArea.value = json;
    }

    // --- 操作ロジック:インポート ---
    function importData() {
      const text = exportArea.value;

      if (text.trim() === "") {
        alert("JSONが空です。エクスポートしたデータを貼り付けてください。");
        return;
      }

      try {
        const data = JSON.parse(text);

        if (!Array.isArray(data.memos) || typeof data.nextMemoId !== "number") {
          alert("JSONの形式が正しくありません。");
          return;
        }

        const ok = window.confirm("現在のメモを上書きして復元しますか?");
        if (!ok) {
          return;
        }

        memos.length = 0;
        data.memos.forEach(function(memo) {
          memos.push(memo);
        });

        nextMemoId = data.nextMemoId;

        saveToStorage();
        renderMemos();

        alert("復元が完了しました。");
      } catch (error) {
        console.error("インポートに失敗しました:", error);
        alert("JSONの解析に失敗しました。形式を確認してください。");
      }
    }

    // --- ユーティリティ:フォームからメモを追加する ---
    function addMemoFromForm() {
      const title = memoTitleInput.value;
      const body = memoBodyInput.value;
      const tag = memoTagInput.value;
      const color = getSelectedColor();

      errorMessage.textContent = "";

      if (title.trim() === "" && body.trim() === "") {
        errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
        return;
      }

      addMemo(title, body, tag, color);

      memoTitleInput.value = "";
      memoBodyInput.value = "";
      memoTagInput.value = "";

      memoColorRadios.forEach(function(radio) {
        radio.checked = radio.value === "none";
      });

      localStorage.removeItem(DRAFT_STORAGE_KEY);
      updateCounters();

      memoTitleInput.focus();

      renderMemos();
    }

    // --- 描画 ---
    function renderMemos() {
      memoList.innerHTML = "";

      let visibleMemos = memos;

      if (currentKeyword.trim() !== "") {
        const keywordLower = currentKeyword.toLowerCase();

        visibleMemos = visibleMemos.filter(function(memo) {
          const titleLower = memo.title.toLowerCase();
          const bodyLower = memo.body.toLowerCase();
          const tagLower = (memo.tag || "").toLowerCase();

          return (
            titleLower.includes(keywordLower) ||
            bodyLower.includes(keywordLower) ||
            tagLower.includes(keywordLower)
          );
        });
      }

      if (currentTagFilter !== "") {
        visibleMemos = visibleMemos.filter(function(memo) {
          return memo.tag === currentTagFilter;
        });
      }

      if (currentViewMode === "all") {
        visibleMemos = visibleMemos.filter(function(memo) {
          return memo.archived === false;
        });
      } else if (currentViewMode === "archivedOnly") {
        visibleMemos = visibleMemos.filter(function(memo) {
          return memo.archived === true;
        });
      }

      visibleMemos = visibleMemos.slice();

      visibleMemos.sort(function(a, b) {
        if (a.pinned && !b.pinned) {
          return -1;
        }
        if (!a.pinned && b.pinned) {
          return 1;
        }

        if (currentSort === "newest") {
          return b.createdAt - a.createdAt;
        } else {
          return a.createdAt - b.createdAt;
        }
      });

      visibleMemos.forEach(function(memo) {
        const memoDiv = document.createElement("div");
        memoDiv.classList.add("memo-item");
        memoDiv.dataset.id = String(memo.id);

        const colorClass = "color-" + (memo.color || "none");
        memoDiv.classList.add(colorClass);

        if (memo.pinned) {
          memoDiv.classList.add("pinned");
        }

        if (memo.archived) {
          memoDiv.classList.add("archived");
        }

        const titleDiv = document.createElement("div");
        titleDiv.classList.add("memo-item-title");
        titleDiv.textContent = memo.title;

        const bodyDiv = document.createElement("div");
        bodyDiv.classList.add("memo-item-body");
        bodyDiv.textContent = memo.body;

        const metaDiv = document.createElement("div");
        metaDiv.classList.add("memo-meta");
        metaDiv.textContent =
          `タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"} / ` +
          `ピン: ${memo.pinned ? "ON" : "OFF"} / アーカイブ: ${memo.archived ? "ON" : "OFF"}`;

        const editTitleInput = document.createElement("input");
        editTitleInput.type = "text";
        editTitleInput.value = memo.title;
        editTitleInput.style.display = "none";

        const editBodyTextarea = document.createElement("textarea");
        editBodyTextarea.rows = 4;
        editBodyTextarea.cols = 40;
        editBodyTextarea.value = memo.body;
        editBodyTextarea.style.display = "none";

        const editTagInput = document.createElement("input");
        editTagInput.type = "text";
        editTagInput.value = memo.tag || "";
        editTagInput.style.display = "none";

        const editColorContainer = document.createElement("div");
        editColorContainer.style.display = "none";

        const colors = ["none", "red", "blue", "yellow"];
        const editColorRadios = [];

        colors.forEach(function(color) {
          const label = document.createElement("label");
          const radio = document.createElement("input");
          radio.type = "radio";
          radio.name = "editColor-" + memo.id;
          radio.value = color;

          if (memo.color === color || (!memo.color && color === "none")) {
            radio.checked = true;
          }

          label.append(radio);
          label.append(document.createTextNode(" " + color));

          editColorContainer.append(label);

          editColorRadios.push(radio);
        });

        const editButton = document.createElement("button");
        editButton.textContent = "編集";

        const saveButton = document.createElement("button");
        saveButton.textContent = "保存";
        saveButton.style.display = "none";

        const cancelButton = document.createElement("button");
        cancelButton.textContent = "キャンセル";
        cancelButton.style.display = "none";

        const deleteButton = document.createElement("button");
        deleteButton.textContent = "削除";

        const pinButton = document.createElement("button");
        pinButton.textContent = memo.pinned ? "ピン解除" : "ピン";

        const archiveButton = document.createElement("button");
        archiveButton.textContent = memo.archived ? "アーカイブ解除" : "アーカイブ";

        function exitEditMode() {
          titleDiv.style.display = "block";
          bodyDiv.style.display = "block";
          metaDiv.style.display = "block";

          editTitleInput.style.display = "none";
          editBodyTextarea.style.display = "none";
          editTagInput.style.display = "none";
          editColorContainer.style.display = "none";

          editButton.style.display = "inline-block";
          deleteButton.style.display = "inline-block";
          pinButton.style.display = "inline-block";
          archiveButton.style.display = "inline-block";
          saveButton.style.display = "none";
          cancelButton.style.display = "none";

          memoDiv.classList.remove("editing");
        }

        editButton.addEventListener("click", function() {
          titleDiv.style.display = "none";
          bodyDiv.style.display = "none";
          metaDiv.style.display = "none";

          editTitleInput.style.display = "block";
          editBodyTextarea.style.display = "block";
          editTagInput.style.display = "block";
          editColorContainer.style.display = "block";

          editButton.style.display = "none";
          deleteButton.style.display = "none";
          pinButton.style.display = "none";
          archiveButton.style.display = "none";
          saveButton.style.display = "inline-block";
          cancelButton.style.display = "inline-block";

          memoDiv.classList.add("editing");

          editTitleInput.value = memo.title;
          editBodyTextarea.value = memo.body;
          editTagInput.value = memo.tag || "";
          editTitleInput.focus();
        });

        saveButton.addEventListener("click", function() {
          const newTitle = editTitleInput.value;
          const newBody = editBodyTextarea.value;
          const newTag = editTagInput.value;

          if (newTitle.trim() === "" && newBody.trim() === "") {
            alert("タイトルか本文のどちらかは入力してください。");
            return;
          }

          let newColor = "none";
          editColorRadios.forEach(function(radio) {
            if (radio.checked) {
              newColor = radio.value;
            }
          });

          const idText = memoDiv.dataset.id;
          const id = Number(idText);

          updateMemo(id, newTitle, newBody, newTag, newColor);
        });

        cancelButton.addEventListener("click", function() {
          exitEditMode();
        });

        editTitleInput.addEventListener("keydown", function(event) {
          if (event.key === "Escape") {
            exitEditMode();
          }
        });

        editBodyTextarea.addEventListener("keydown", function(event) {
          if (event.key === "Escape") {
            exitEditMode();
          }
        });

        deleteButton.addEventListener("click", function(event) {
          const memoElement = event.target.closest(".memo-item");
          const idText = memoElement.dataset.id;
          const id = Number(idText);

          deleteMemo(id);
        });

        pinButton.addEventListener("click", function(event) {
          const memoElement = event.target.closest(".memo-item");
          const idText = memoElement.dataset.id;
          const id = Number(idText);

          togglePinned(id);
        });

        archiveButton.addEventListener("click", function(event) {
          const memoElement = event.target.closest(".memo-item");
          const idText = memoElement.dataset.id;
          const id = Number(idText);

          toggleArchived(id);
        });

        memoDiv.append(titleDiv);
        memoDiv.append(bodyDiv);
        memoDiv.append(metaDiv);
        memoDiv.append(editTitleInput);
        memoDiv.append(editBodyTextarea);
        memoDiv.append(editTagInput);
        memoDiv.append(editColorContainer);
        memoDiv.append(editButton);
        memoDiv.append(saveButton);
        memoDiv.append(cancelButton);
        memoDiv.append(deleteButton);
        memoDiv.append(pinButton);
        memoDiv.append(archiveButton);

        memoList.append(memoDiv);
      });
    }

    // --- イベント設定:入力欄と下書き ---
    memoTitleInput.addEventListener("input", function() {
      updateCounters();
      saveDraft();
    });

    memoBodyInput.addEventListener("input", function() {
      updateCounters();
      saveDraft();
    });

    memoTagInput.addEventListener("input", function() {
      saveDraft();
    });

    memoColorRadios.forEach(function(radio) {
      radio.addEventListener("change", function() {
        saveDraft();
      });
    });

    memoTitleInput.addEventListener("keydown", function(event) {
      if (event.key === "Enter") {
        event.preventDefault();
        addMemoFromForm();
      }
    });

    // --- イベント設定:メモ追加ボタン ---
    addMemoButton.addEventListener("click", function() {
      addMemoFromForm();
    });

    // --- イベント設定:全メモ削除ボタン ---
    clearAllButton.addEventListener("click", function() {
      clearAllMemos();
    });

    // --- イベント設定:検索入力欄 ---
    searchInput.addEventListener("input", function() {
      currentKeyword = searchInput.value;
      renderMemos();
    });

    // --- イベント設定:並び順ボタン ---
    sortButtons.forEach(function(button) {
      button.addEventListener("click", function() {
        const sortMode = button.dataset.sort;

        currentSort = sortMode;

        setActiveButton(sortButtons, button, "active-sort");

        renderMemos();
      });
    });

    // --- イベント設定:タグフィルタボタン ---
    tagFilterButtons.forEach(function(button) {
      button.addEventListener("click", function() {
        const tag = button.dataset.tag;

        currentTagFilter = tag;

        setActiveButton(tagFilterButtons, button, "active-tag");

        renderMemos();
      });
    });

    // --- イベント設定:表示モードフィルタボタン ---
    viewFilterButtons.forEach(function(button) {
      button.addEventListener("click", function() {
        const viewMode = button.dataset.view;

        currentViewMode = viewMode;

        setActiveButton(viewFilterButtons, button, "active-view");

        renderMemos();
      });
    });

    // --- イベント設定:エクスポート/インポート ---
    exportButton.addEventListener("click", function() {
      exportData();
    });

    importButton.addEventListener("click", function() {
      importData();
    });

    // --- 初期処理 ---
    loadFromStorage();
    loadDraft();
    updateCounters();
    renderMemos();
    memoTitleInput.focus();
  </script>
</body>
</html>

7日目で本当に掴んでほしいこと

  • アプリは「機能を増やす」だけでなく、「構造を整理する」ことで完成度が上がる
  • querySelector() / querySelectorAll() / addEventListener() / createElement() / append() / classList / dataset の組み合わせで、 ここまでのメモ帳アプリ全体が成り立っていること
  • 小さなユーティリティ関数(setActiveButton()addMemoFromForm())を作ることで、 コードが読みやすく・直しやすく・育てやすくなること

この7日間のメモ帳アプリは、 そのまま「自分の生活で使えるプロダクト」として動かせますし、 ToDoアプリ・家計簿アプリ・習慣トラッカーなど、 他のアプリを作るときの強力な土台にもなります。

ここから先は、 「自分が欲しい機能」を自由に足していくフェーズです。 タグを複数にしたり、期限を付けたり、 メモをカテゴリごとに分けたり—— 同じ DOM+イベント+localStorage のパターンで どこまでも育てていけます。

タイトルとURLをコピーしました