Last active
January 21, 2026 04:16
-
-
Save 20m61/e5a6b57fe7e257eae2ec336101705d2d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <!-- | |
| ============================================ | |
| JavaScript入門 Ver.2.4 | |
| ============================================ | |
| 【このファイルについて】 | |
| このファイルは、JavaScriptの基本を学ぶための | |
| インタラクティブな教材です。 | |
| HTML、CSS、JavaScriptが1つのファイルにまとまっており、 | |
| ブラウザで開くだけで動作します。 | |
| 【ファイル構成】 | |
| 1. HTML部分(1063行目〜): ページの構造・コンテンツ | |
| 2. CSS部分(32行目〜1060行目): 見た目・レイアウト | |
| 3. JavaScript部分(2360行目〜): 動作・インタラクション | |
| 【学習内容】 | |
| 1) 変数と文字列操作 - const/let、テンプレートリテラル | |
| 2) 条件分岐 (if文) - 比較演算子、論理演算子 | |
| 3) 繰り返し (for文) - ループ処理、配列操作 | |
| 4) 関数と引数 - 関数定義、引数、戻り値 | |
| 5) class - constructor、メソッド、インスタンス | |
| 6) 非同期処理 (setTimeout) - コールバック関数 | |
| 7) データの永続化 (localStorage) - setItem/getItem/removeItem | |
| 8) Cookie - document.cookie、有効期限、データ保存 | |
| 9) 配列操作とlocalStorage - push/splice、TODOアプリ | |
| 10) fetch APIと外部API連携 - async/await、Promise | |
| 11) 高度な配列操作 - map/filter/Set、Fisher-Yatesシャッフル | |
| 【使用している主な技術】 | |
| - HTML5: セマンティックタグ、アクセシビリティ属性 | |
| - CSS3: Grid/Flexbox、カスタムプロパティ、メディアクエリ | |
| - JavaScript (ES6+): アロー関数、分割代入、async/await | |
| - Web API: localStorage、Clipboard API、Fetch API | |
| 【外部依存】 | |
| - Open-Meteo API(天気情報取得、無料・APIキー不要) | |
| ============================================ | |
| --> | |
| <html lang="ja"> | |
| <head> | |
| <!-- 文字コード:UTF-8を指定(日本語対応) --> | |
| <meta charset="utf-8" /> | |
| <!-- レスポンシブ対応:デバイス幅に合わせて表示 --> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>JavaScript入門 Ver.2.4</title> | |
| <style> | |
| /* ============================================ | |
| CSS変数(カスタムプロパティ)の定義 | |
| ============================================ | |
| 【CSS変数とは】 | |
| CSSで再利用可能な値を定義する機能。 | |
| JavaScriptの変数のように、値に名前を付けて管理できる。 | |
| 【:root セレクタ】 | |
| 文書のルート要素(HTML要素)を指す特殊なセレクタ。 | |
| ここで定義した変数は、ページ全体で使用できる。 | |
| 【変数の定義方法】 | |
| --変数名: 値; | |
| 【変数の使用方法】 | |
| プロパティ: var(--変数名); | |
| 【メリット】 | |
| - 色やサイズを一箇所で管理できる | |
| - テーマ変更が容易(ダークモードなど) | |
| - コードの可読性が向上 | |
| */ | |
| :root { | |
| /* | |
| システムフォントスタック | |
| - system-ui: OSのシステムフォント(最も高速) | |
| - -apple-system: macOS/iOS用フォールバック | |
| - sans-serif: 最終フォールバック(ゴシック系) | |
| Webフォントを読み込まないため、表示が高速 | |
| */ | |
| font-family: system-ui, -apple-system, sans-serif; | |
| /* | |
| メインカラー(プライマリカラー) | |
| #2563eb = RGB(37, 99, 235) - 青系 | |
| ボタンやリンクなど、強調したい要素に使用 | |
| */ | |
| --primary: #2563eb; | |
| /* | |
| 背景色 | |
| #f8fafc = RGB(248, 250, 252) - 非常に薄いグレー | |
| 真っ白より目に優しく、コンテンツとの対比を作る | |
| */ | |
| --bg: #f8fafc; | |
| } | |
| /* ============================================ | |
| 基本スタイル(リセット & ベース) | |
| ============================================ | |
| 【bodyへのスタイル設定】 | |
| ページ全体に適用される基本的なスタイル。 | |
| ここで設定した値は、子要素に継承されるものが多い。 | |
| 【marginとpaddingの違い】 | |
| - margin: 要素の「外側」の余白 | |
| - padding: 要素の「内側」の余白 | |
| ┌─────────────────────────┐ | |
| │ margin │ | |
| │ ┌─────────────────┐ │ | |
| │ │ padding │ │ | |
| │ │ ┌───────────┐ │ │ | |
| │ │ │ content │ │ │ | |
| │ │ └───────────┘ │ │ | |
| │ └─────────────────┘ │ | |
| └─────────────────────────┘ | |
| */ | |
| body { | |
| /* | |
| margin: 0 auto の意味 | |
| - 0: 上下のマージンは0 | |
| - auto: 左右のマージンは自動計算(=中央寄せ) | |
| max-widthと組み合わせることで、 | |
| 大画面でコンテンツが中央に配置される | |
| */ | |
| margin: 0 auto; | |
| /* | |
| padding: 24px | |
| 内側の余白を24ピクセルに設定。 | |
| コンテンツが画面端にくっつかないようにする | |
| */ | |
| padding: 24px; | |
| /* | |
| line-height: 1.6 | |
| 行間を文字サイズの1.6倍に設定。 | |
| 1.0だと行がくっつき、2.0だと空きすぎ。 | |
| 1.5〜1.8が読みやすいとされる | |
| */ | |
| line-height: 1.6; | |
| /* | |
| var(--bg) でCSS変数を参照 | |
| :rootで定義した --bg の値が適用される | |
| */ | |
| background: var(--bg); | |
| /* | |
| #333 = RGB(51, 51, 51) | |
| 真っ黒(#000)より柔らかく、読みやすい | |
| */ | |
| color: #333; | |
| /* | |
| max-width: 1800px | |
| コンテンツの最大幅を制限。 | |
| 4Kモニターなどで横に広がりすぎるのを防ぐ | |
| */ | |
| max-width: 1800px; | |
| } | |
| /* | |
| ============================================ | |
| メディアクエリ(レスポンシブデザイン) | |
| ============================================ | |
| 【メディアクエリとは】 | |
| 画面サイズや端末の特性に応じて、 | |
| 異なるCSSを適用する仕組み。 | |
| 【書き方】 | |
| @media (条件) { | |
| 適用するスタイル | |
| } | |
| 【主な条件】 | |
| - min-width: 指定値以上の幅で適用 | |
| - max-width: 指定値以下の幅で適用 | |
| 【ブレークポイント(よく使われる値)】 | |
| - 480px: スマートフォン | |
| - 768px: タブレット | |
| - 1024px: ノートPC | |
| - 1400px: デスクトップ | |
| */ | |
| /* 大画面(1400px以上):余白を増やして見やすく */ | |
| @media (min-width: 1400px) { | |
| body { | |
| /* | |
| padding: 32px 40px は | |
| padding: 32px 40px 32px 40px の省略形 | |
| 上下32px、左右40px | |
| */ | |
| padding: 32px 40px; | |
| } | |
| } | |
| /* モバイル(480px以下):余白を減らして画面を有効活用 */ | |
| @media (max-width: 480px) { | |
| body { | |
| padding: 12px; | |
| } | |
| } | |
| /* | |
| ============================================ | |
| 見出しスタイル | |
| ============================================ | |
| 【rem単位とは】 | |
| - rem = "root em"の略 | |
| - ルート要素(html)のフォントサイズを基準にした単位 | |
| - ブラウザのデフォルトは通常16px | |
| - 1.8rem = 16px × 1.8 = 約29px | |
| 【rem vs px vs em】 | |
| - px: 絶対単位、どの画面でも同じサイズ | |
| - em: 親要素のフォントサイズ基準(入れ子で計算が複雑に) | |
| - rem: ルート要素基準(計算がシンプルで推奨) | |
| */ | |
| h1 { | |
| /* | |
| margin: 0 0 8px 0 は | |
| margin: 上 右 下 左 の順(時計回り) | |
| 上0、右0、下8px、左0 | |
| */ | |
| margin: 0 0 8px 0; | |
| /* 1.8rem = 約29px(ルートが16pxの場合) */ | |
| font-size: 1.8rem; | |
| /* 濃い紺色 - 黒より柔らかい印象 */ | |
| color: #1e293b; | |
| } | |
| /* モバイルではタイトルを小さくして画面に収める */ | |
| @media (max-width: 480px) { | |
| h1 { | |
| /* 1.4rem = 約22px */ | |
| font-size: 1.4rem; | |
| } | |
| } | |
| /* ============================================ | |
| レスポンシブグリッドレイアウト | |
| ============================================ | |
| 【CSS Gridとは】 | |
| 2次元のレイアウトを作成するためのCSS機能。 | |
| 行(row)と列(column)の両方を同時に制御できる。 | |
| 【Flexboxとの違い】 | |
| - Flexbox: 1次元(横方向 or 縦方向)のレイアウト | |
| - Grid: 2次元(縦横同時)のレイアウト | |
| 【グリッドの基本概念】 | |
| ┌─────┬─────┬─────┐ | |
| │ 1 │ 2 │ 3 │ ← 行(row) | |
| ├─────┼─────┼─────┤ | |
| │ 4 │ 5 │ 6 │ | |
| └─────┴─────┴─────┘ | |
| ↑ | |
| 列(column) | |
| 【fr単位】 | |
| "fraction"(分数)の略。 | |
| 利用可能なスペースを均等に分割する。 | |
| 1fr 1fr = 50% 50% | |
| 1fr 2fr = 33% 67% | |
| 【ブレークポイント設計】 | |
| - 1400px以上: 4列(大型デスクトップ) | |
| - 1100-1399px: 3列(デスクトップ) | |
| - 901-1099px: 2列(タブレット横/小型デスクトップ) | |
| - 900px以下: 1列(タブレット縦/スマートフォン) | |
| */ | |
| .row { | |
| /* | |
| display: grid でグリッドコンテナを作成。 | |
| 子要素は自動的にグリッドアイテムになる | |
| */ | |
| display: grid; | |
| /* | |
| repeat(2, 1fr) は "1fr 1fr" と同じ意味 | |
| repeat(繰り返し回数, 値) | |
| 2列のグリッドを作成し、各列は均等幅 | |
| */ | |
| grid-template-columns: repeat(2, 1fr); | |
| /* | |
| gap: グリッドアイテム間の余白 | |
| row-gap と column-gap を一括指定 | |
| */ | |
| gap: 20px; | |
| margin-top: 20px; | |
| } | |
| /* 大画面(1400px以上):4列レイアウト */ | |
| @media (min-width: 1400px) { | |
| .row { | |
| /* 4列の均等幅グリッド */ | |
| grid-template-columns: repeat(4, 1fr); | |
| } | |
| } | |
| /* | |
| 複合メディアクエリ | |
| and で複数の条件を組み合わせる | |
| 「1100px以上 かつ 1399px以下」の範囲で適用 | |
| */ | |
| @media (min-width: 1100px) and (max-width: 1399px) { | |
| .row { | |
| /* 3列の均等幅グリッド */ | |
| grid-template-columns: repeat(3, 1fr); | |
| } | |
| } | |
| /* 小画面(900px以下):1列レイアウト(カード縦積み) */ | |
| @media (max-width: 900px) { | |
| .row { | |
| /* 1列 = カードが縦に並ぶ */ | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* ============================================ | |
| カードコンポーネント | |
| ============================================ | |
| 【コンポーネント設計】 | |
| 「カード」は再利用可能なUIパーツ。 | |
| 同じスタイルを複数の場所で使い回せる。 | |
| 【カードデザインの要素】 | |
| - 白背景: コンテンツを目立たせる | |
| - ボーダー: 境界を明確にする | |
| - 角丸: 柔らかい印象を与える | |
| - 影: 浮いているような立体感 | |
| 【Flexboxとの組み合わせ】 | |
| カード内の要素を縦に並べ、 | |
| 間隔を均等にするためにFlexboxを使用 | |
| */ | |
| .card { | |
| /* 白背景でコンテンツを目立たせる */ | |
| background: #fff; | |
| /* | |
| border: 太さ スタイル 色 | |
| - 1px: 細いボーダー | |
| - solid: 実線(dashedは破線、dottedは点線) | |
| - #e2e8f0: 薄いグレー | |
| */ | |
| border: 1px solid #e2e8f0; | |
| /* | |
| border-radius: 角丸の半径 | |
| 大きいほど丸くなる(50%で円形) | |
| 12pxは程よい丸み | |
| */ | |
| border-radius: 12px; | |
| /* カード内側の余白 */ | |
| padding: 20px; | |
| /* | |
| box-shadow: 影の設定 | |
| 構文: 水平 垂直 ぼかし 広がり 色 | |
| 0 4px 6px -1px rgba(0, 0, 0, 0.05) | |
| - 0: 水平方向のずれなし | |
| - 4px: 下に4pxずれる | |
| - 6px: 6pxのぼかし | |
| - -1px: 影を1px縮小(自然な見た目に) | |
| - rgba(0,0,0,0.05): 5%の黒(非常に薄い) | |
| */ | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); | |
| /* | |
| Flexboxレイアウトを有効化 | |
| 子要素の配置を柔軟に制御できる | |
| */ | |
| display: flex; | |
| /* | |
| flex-direction: 子要素の並び方向 | |
| - row: 横方向(デフォルト) | |
| - column: 縦方向 | |
| - row-reverse: 横方向(逆順) | |
| - column-reverse: 縦方向(逆順) | |
| */ | |
| flex-direction: column; | |
| /* | |
| gap: Flex/Gridアイテム間の余白 | |
| margin を個別に設定するより簡潔 | |
| */ | |
| gap: 12px; | |
| } | |
| /* モバイルではカードをコンパクトに */ | |
| @media (max-width: 480px) { | |
| .card { | |
| padding: 14px; | |
| border-radius: 8px; | |
| } | |
| } | |
| /* | |
| ============================================ | |
| グリッドアイテムの配置制御 | |
| ============================================ | |
| 【grid-column プロパティ】 | |
| グリッドアイテムが占める列の範囲を指定する。 | |
| 【書き方】 | |
| grid-column: 開始 / 終了; | |
| 【特殊な値】 | |
| - span N: N列分の幅を占める | |
| - -1: 最後の列ライン(終端) | |
| 【グリッドラインの考え方】 | |
| 4列グリッドの場合: | |
| ライン: 1 2 3 4 5(-1) | |
| | | | | | | |
| v v v v v | |
| ┌────┬────┬────┬────┐ | |
| │ 1 │ 2 │ 3 │ 4 │ | |
| └────┴────┴────┴────┘ | |
| grid-column: 1 / -1 = 1番目から最後まで(全幅) | |
| */ | |
| /* 全幅カード(ログセクション用) */ | |
| .card.full-width { | |
| /* | |
| 1 / -1 = 1番目のラインから最後のラインまで | |
| 何列グリッドでも全幅になる汎用的な指定 | |
| */ | |
| grid-column: 1 / -1; | |
| } | |
| /* 2列幅カード(シャッフルセクション用) */ | |
| .card.half-width { | |
| /* | |
| span 2 = 2列分の幅を占める | |
| 開始位置は自動配置に任せる | |
| */ | |
| grid-column: span 2; | |
| } | |
| /* 小画面では2列幅カードも全幅に(1列レイアウト時) */ | |
| @media (max-width: 900px) { | |
| .card.half-width { | |
| grid-column: 1 / -1; | |
| } | |
| } | |
| /* カード内の見出し */ | |
| .card h2 { | |
| margin: 0 0 10px 0; | |
| font-size: 1.25rem; | |
| color: #0f172a; | |
| /* 下線 */ | |
| border-bottom: 2px solid #f1f5f9; | |
| padding-bottom: 8px; | |
| } | |
| /* ============================================ | |
| UIパーツ(再利用可能な小さなコンポーネント) | |
| ============================================ | |
| 【ユーティリティクラス】 | |
| 特定の機能を持つ小さなクラス。 | |
| 組み合わせて使うことで柔軟なデザインが可能。 | |
| */ | |
| /* | |
| 学習ポイントのバッジ | |
| 各カードで「何を学ぶか」を示すラベル | |
| */ | |
| .point { | |
| /* 少し小さめの文字 */ | |
| font-size: 0.85rem; | |
| /* グレーがかった文字色 */ | |
| color: #475569; | |
| /* 薄いグレー背景 */ | |
| background: #f1f5f9; | |
| /* 内側余白: 上下4px 左右10px */ | |
| padding: 4px 10px; | |
| /* 小さめの角丸 */ | |
| border-radius: 6px; | |
| /* | |
| display: inline-block | |
| - inline: 文字と同じように横に並ぶ | |
| - block: 改行して縦に並ぶ | |
| - inline-block: 横に並びつつ、幅・高さを指定可能 | |
| */ | |
| display: inline-block; | |
| /* 太字 */ | |
| font-weight: 600; | |
| /* | |
| align-self: Flexboxの子要素の個別配置 | |
| flex-start = 開始位置(左寄せ) | |
| 親要素がflex-direction: columnの場合、 | |
| 子要素はデフォルトで横幅いっぱいに広がるが、 | |
| align-self: flex-startで内容に合わせた幅になる | |
| */ | |
| align-self: flex-start; | |
| } | |
| /* | |
| 汎用グリッド | |
| フォーム要素を並べるための小さなグリッド | |
| */ | |
| .grid { | |
| display: grid; | |
| gap: 12px; | |
| } | |
| /* | |
| 2列グリッド | |
| .grid と組み合わせて使用: class="grid two" | |
| */ | |
| .two { | |
| grid-template-columns: 1fr 1fr; | |
| } | |
| /* 狭い画面では2列グリッドを1列に変更 */ | |
| @media (max-width: 520px) { | |
| .two { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* | |
| カード内コンテンツ | |
| カードの残りスペースを全て使うコンテナ | |
| */ | |
| .card-content { | |
| /* | |
| flex: 1 の意味 | |
| flex: flex-grow flex-shrink flex-basis の省略形 | |
| flex: 1 = flex: 1 1 0% | |
| flex-grow: 1 = 余ったスペースを均等に分配 | |
| これにより、カード内の残りの高さを全て使う | |
| */ | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| /* | |
| コードブロックをカード下部に配置 | |
| どのカードもコードブロックが下に揃う | |
| */ | |
| .card pre { | |
| /* | |
| margin-top: auto の魔法 | |
| Flexbox内で margin: auto を使うと、 | |
| 余ったスペースを全てマージンに割り当てる。 | |
| 結果として、要素が下に押し下げられる。 | |
| ┌─────────────┐ | |
| │ タイトル │ | |
| │ 内容... │ | |
| │ │ ← margin-top: auto が | |
| │ │ このスペースを埋める | |
| │ [コード] │ | |
| └─────────────┘ | |
| */ | |
| margin-top: auto; | |
| } | |
| /* ============================================ | |
| ボタンスタイル | |
| ============================================ | |
| 【ボタンのデザイン原則】 | |
| 1. クリック可能であることが視覚的にわかる | |
| 2. 状態変化(ホバー、クリック、無効化)を示す | |
| 3. タッチデバイスでも押しやすいサイズ | |
| 4. アクションの種類を色で区別(通常/強調/危険) | |
| 【擬似クラス(Pseudo-classes)】 | |
| 要素の特定の状態にスタイルを適用する | |
| - :hover - マウスが乗っている時 | |
| - :active - クリック中 | |
| - :focus - フォーカスされている時 | |
| - :disabled - 無効化されている時 | |
| - :focus-visible - キーボードでフォーカスされた時 | |
| */ | |
| button { | |
| /* 内側の余白: 上下10px 左右16px */ | |
| padding: 10px 16px; | |
| /* 角丸 */ | |
| border-radius: 8px; | |
| /* ボーダー */ | |
| border: 1px solid #cbd5e1; | |
| /* 背景色 */ | |
| background: #fff; | |
| /* | |
| cursor: pointer | |
| マウスカーソルを「指」の形にして | |
| クリック可能であることを示す | |
| 他の値: | |
| - default: 矢印(デフォルト) | |
| - text: Iビーム(テキスト選択) | |
| - not-allowed: 禁止マーク | |
| - grab/grabbing: 掴む手 | |
| */ | |
| cursor: pointer; | |
| /* 太字 - 読みやすく、クリック可能な印象 */ | |
| font-weight: 600; | |
| /* 文字色 */ | |
| color: #334155; | |
| /* | |
| transition: アニメーション設定 | |
| transition: プロパティ 時間 イージング; | |
| all 0.2s = 全プロパティを0.2秒でアニメーション | |
| ホバー時などの変化が滑らかになる | |
| */ | |
| transition: all 0.2s; | |
| /* | |
| touch-action: manipulation | |
| スマートフォンでのダブルタップズームを無効化。 | |
| ボタンを素早く連打できるようになる。 | |
| (300msの遅延が解消される) | |
| */ | |
| touch-action: manipulation; | |
| } | |
| /* | |
| モバイルではタップしやすく | |
| 指でのタップは、マウスより精度が低いため、 | |
| ターゲット(押せる領域)を大きくする。 | |
| AppleのHIGでは44×44pt以上を推奨。 | |
| */ | |
| @media (max-width: 480px) { | |
| button { | |
| padding: 12px 14px; | |
| font-size: 1rem; | |
| } | |
| } | |
| /* | |
| ホバー時のスタイル | |
| マウスが上に乗った時、視覚的なフィードバックを与える | |
| */ | |
| button:hover { | |
| /* 背景を少し暗く */ | |
| background: #f8fafc; | |
| /* ボーダーを少し濃く */ | |
| border-color: #94a3b8; | |
| } | |
| /* | |
| クリック時のスタイル(押し込み効果) | |
| ボタンを押した感覚をUIで再現 | |
| */ | |
| button:active { | |
| /* | |
| transform: 要素を変形させる | |
| translateY(1px) = Y軸方向に1px移動(下に押し込まれる) | |
| 他の変形: | |
| - translateX(): X軸移動 | |
| - scale(): 拡大縮小 | |
| - rotate(): 回転 | |
| - skew(): 傾斜 | |
| */ | |
| transform: translateY(1px); | |
| } | |
| /* | |
| プライマリボタン(メインアクション用) | |
| ページ内で最も重要なアクションに使用。 | |
| 目立つ色で「これを押してください」を表現。 | |
| */ | |
| button.primary { | |
| /* CSS変数で定義した青色 */ | |
| background: var(--primary); | |
| /* 白文字(コントラスト確保) */ | |
| color: #fff; | |
| /* ボーダーなし(塗りつぶしボタン) */ | |
| border: none; | |
| } | |
| button.primary:hover { | |
| /* | |
| filter: 視覚効果を適用 | |
| brightness(1.1) = 明度を110%に(少し明るく) | |
| 他のフィルター: | |
| - blur(): ぼかし | |
| - contrast(): コントラスト | |
| - grayscale(): グレースケール | |
| - drop-shadow(): 影 | |
| */ | |
| filter: brightness(1.1); | |
| } | |
| /* | |
| 無効化状態のボタン | |
| クリック不可であることを視覚的に示す | |
| */ | |
| button:disabled { | |
| /* 半透明にして「使えない」感を出す */ | |
| opacity: 0.5; | |
| /* カーソルを「禁止」マークに */ | |
| cursor: not-allowed; | |
| } | |
| /* | |
| フォーカススタイル(アクセシビリティ) | |
| 【アクセシビリティとは】 | |
| 障害の有無に関わらず、誰でも使えるようにすること。 | |
| キーボードのみで操作するユーザーにとって、 | |
| フォーカス表示は「今どこにいるか」を知る唯一の手段。 | |
| 【:focus vs :focus-visible】 | |
| - :focus: マウスクリックでもキーボードでも適用 | |
| - :focus-visible: キーボード操作時のみ適用 | |
| :focus-visible を使うと、マウスクリック時は | |
| アウトラインが表示されず、見た目がスッキリする | |
| */ | |
| button:focus-visible, | |
| input:focus-visible, | |
| select:focus-visible, | |
| textarea:focus-visible { | |
| /* | |
| outline: アウトライン(輪郭線) | |
| borderと似ているが、要素のサイズに影響しない | |
| */ | |
| outline: 2px solid var(--primary); | |
| /* | |
| outline-offset: アウトラインと要素の間隔 | |
| 少し離すことで、より目立つ | |
| */ | |
| outline-offset: 2px; | |
| } | |
| /* | |
| 危険なアクション用ボタン(削除など) | |
| 「取り消しできない操作」を警告する赤系の配色 | |
| */ | |
| button.danger { | |
| /* 赤文字 */ | |
| color: #dc2626; | |
| /* 薄い赤のボーダー */ | |
| border-color: #fecaca; | |
| /* 非常に薄い赤背景 */ | |
| background: #fef2f2; | |
| } | |
| /* | |
| 小さいボタン | |
| スペースが限られている場所で使用 | |
| */ | |
| button.sm { | |
| padding: 4px 10px; | |
| font-size: 0.85rem; | |
| } | |
| /* ============================================ | |
| フォーム要素 | |
| ============================================ | |
| 【フォーム要素の種類】 | |
| - input: テキスト、数値、日付など様々な入力 | |
| - select: ドロップダウン選択 | |
| - textarea: 複数行テキスト入力 | |
| - button: ボタン(別途スタイル定義済み) | |
| 【input type属性の種類】 | |
| - text: 1行テキスト(デフォルト) | |
| - number: 数値(スピンボタン付き) | |
| - email: メールアドレス(バリデーション付き) | |
| - password: パスワード(マスク表示) | |
| - date: 日付選択 | |
| - checkbox: チェックボックス | |
| - radio: ラジオボタン | |
| - file: ファイル選択 | |
| */ | |
| input, | |
| select, | |
| textarea { | |
| /* 内側の余白 */ | |
| padding: 10px; | |
| /* 角丸 */ | |
| border-radius: 8px; | |
| /* ボーダー */ | |
| border: 1px solid #cbd5e1; | |
| /* 親要素の幅いっぱい */ | |
| width: 100%; | |
| /* | |
| box-sizing: ボックスモデルの計算方法 | |
| 【content-box】(デフォルト) | |
| width = コンテンツ幅のみ | |
| 実際の幅 = width + padding + border | |
| 【border-box】(推奨) | |
| width = コンテンツ + padding + border | |
| 実際の幅 = widthの値そのまま | |
| ┌─ border ─────────────────┐ | |
| │ ┌─ padding ─────────┐ │ | |
| │ │ ┌─ content ──┐ │ │ | |
| │ │ │ │ │ │ | |
| │ │ └────────────┘ │ │ | |
| │ └───────────────────┘ │ | |
| └──────────────────────────┘ | |
| width: 100% + padding: 10px の場合、 | |
| content-boxだと親要素からはみ出すが、 | |
| border-boxならちょうど収まる | |
| */ | |
| box-sizing: border-box; | |
| /* | |
| font-family: inherit | |
| 親要素のフォントを継承する。 | |
| フォーム要素はブラウザデフォルトのフォントが | |
| 適用されるため、明示的に継承させる。 | |
| */ | |
| font-family: inherit; | |
| /* | |
| font-size: 16px - 重要! | |
| iOSでは、フォーム要素のフォントサイズが | |
| 16px未満だと、入力時に自動ズームされる。 | |
| (ユーザーが見やすいように、という配慮だが、 | |
| レイアウトが崩れる原因になる) | |
| 16px以上にすることでズームを防止できる。 | |
| */ | |
| font-size: 16px; | |
| } | |
| /* モバイルではフォーム要素を大きく(タッチしやすく) */ | |
| @media (max-width: 480px) { | |
| input, | |
| select, | |
| textarea { | |
| padding: 12px; | |
| } | |
| } | |
| /* | |
| テキストエリアのリサイズ設定 | |
| resize: リサイズの方向を制御 | |
| - none: リサイズ不可 | |
| - both: 縦横両方可 | |
| - horizontal: 横方向のみ | |
| - vertical: 縦方向のみ(推奨) | |
| 横方向のリサイズはレイアウトを崩す原因になるため、 | |
| 縦方向のみに制限するのが一般的 | |
| */ | |
| textarea { | |
| resize: vertical; | |
| } | |
| /* ============================================ | |
| 状態表示エリア | |
| ============================================ | |
| 【状態表示のデザインパターン】 | |
| ユーザーにフィードバックを与えるための領域。 | |
| 色分けにより、状態を一目で判断できるようにする。 | |
| 【色の意味(一般的な慣習)】 | |
| - グレー: 中立・待機状態 | |
| - 緑: 成功・完了 | |
| - オレンジ/黄: 警告 | |
| - 赤: エラー・危険 | |
| 【min-heightの重要性】 | |
| メッセージがない状態でも高さを確保することで、 | |
| メッセージ表示時にレイアウトがずれるのを防ぐ | |
| */ | |
| .status-area { | |
| /* 最小高さを確保(レイアウトずれ防止) */ | |
| min-height: 24px; | |
| font-size: 0.95rem; | |
| margin-top: 4px; | |
| } | |
| /* | |
| ミュート(控えめ)スタイル | |
| 目立たせたくないテキストに使用。 | |
| 初期状態の説明文や、補助的な情報に適用。 | |
| */ | |
| .muted { | |
| /* グレーがかった文字色 */ | |
| color: #64748b; | |
| /* 少し小さめ */ | |
| font-size: 0.9rem; | |
| } | |
| /* | |
| 成功スタイル | |
| 処理が正常に完了したことを示す。 | |
| 緑色 = 「OK」「進め」の意味(信号と同じ) | |
| */ | |
| .ok { | |
| color: #16a34a; | |
| /* 太字で強調 */ | |
| font-weight: 600; | |
| } | |
| /* | |
| 警告スタイル | |
| 注意が必要な状態を示す。 | |
| オレンジ色 = 「注意」「慎重に」の意味 | |
| */ | |
| .warn { | |
| color: #ea580c; | |
| font-weight: 600; | |
| } | |
| /* ============================================ | |
| コードブロック & コピーボタン | |
| ============================================ | |
| 【コードブロックのデザイン】 | |
| - ダーク背景: 一般的なIDEやターミナルの見た目 | |
| - 等幅フォント: コードの整列を保持 | |
| - 横スクロール: 長い行も切れずに表示 | |
| 【preタグとcodeタグ】 | |
| - <pre>: 整形済みテキスト(空白・改行を保持) | |
| - <code>: インラインのコード表記 | |
| - <pre><code>: ブロックレベルのコード表示 | |
| */ | |
| pre { | |
| /* | |
| ダークな背景色 | |
| #1e293b = RGB(30, 41, 59) | |
| 一般的なコードエディタの配色に近い | |
| */ | |
| background: #1e293b; | |
| /* | |
| 明るい文字色 | |
| ダーク背景とのコントラストを確保 | |
| */ | |
| color: #e2e8f0; | |
| border-radius: 8px; | |
| padding: 12px; | |
| /* | |
| overflow-x: auto | |
| コンテンツが横幅を超えた場合の動作 | |
| - visible: はみ出して表示(デフォルト) | |
| - hidden: 隠す | |
| - scroll: 常にスクロールバー表示 | |
| - auto: 必要な時だけスクロールバー表示 | |
| 長いコードも横スクロールで見られる | |
| */ | |
| overflow-x: auto; | |
| /* デフォルトのマージンを消す */ | |
| margin: 0; | |
| font-size: 0.9rem; | |
| /* | |
| 等幅フォント(Monospace) | |
| 全ての文字が同じ幅で表示される。 | |
| コードのインデントや整列が崩れない。 | |
| - ui-monospace: システムの等幅フォント | |
| - monospace: 一般的な等幅フォント(フォールバック) | |
| */ | |
| font-family: ui-monospace, monospace; | |
| /* | |
| position: relative | |
| 子要素(コピーボタン)を絶対配置するための基準点。 | |
| relativeを指定しないと、ボタンはページ全体を | |
| 基準に配置されてしまう。 | |
| 【positionの値】 | |
| - static: 通常の配置(デフォルト) | |
| - relative: 現在位置を基準に移動可能 | |
| - absolute: 親要素を基準に配置 | |
| - fixed: ビューポートを基準に固定 | |
| - sticky: スクロールに応じて固定 | |
| */ | |
| position: relative; | |
| /* | |
| -webkit-overflow-scrolling: touch | |
| iOSでの慣性スクロールを有効にする。 | |
| 指を離してもスクロールが続く自然な動き。 | |
| (Webkit系ブラウザ専用の非標準プロパティ) | |
| */ | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| @media (max-width: 480px) { | |
| pre { | |
| font-size: 0.8rem; | |
| padding: 10px; | |
| } | |
| } | |
| /* | |
| コピーボタン(ホバー時に表示) | |
| コードをクリップボードにコピーする機能 | |
| 【UXの工夫】 | |
| - 普段は非表示(邪魔にならない) | |
| - ホバー時に表示(必要な時だけ) | |
| - 半透明で暗い背景に馴染む | |
| */ | |
| .copy-btn { | |
| /* | |
| position: absolute | |
| 親要素(pre)を基準に配置。 | |
| top/right で右上に固定。 | |
| */ | |
| position: absolute; | |
| top: 6px; | |
| right: 6px; | |
| /* | |
| rgba(255, 255, 255, 0.15) | |
| RGB + Alpha(透明度) | |
| - 255, 255, 255: 白 | |
| - 0.15: 15%の不透明度(85%透明) | |
| */ | |
| background: rgba(255, 255, 255, 0.15); | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| color: #fff; | |
| font-size: 0.75rem; | |
| padding: 4px 8px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| /* | |
| opacity: 0 で完全に透明(非表示) | |
| display: none と違い、要素は存在している。 | |
| transitionでアニメーション可能。 | |
| */ | |
| opacity: 0; | |
| /* opacityの変化を0.2秒でアニメーション */ | |
| transition: opacity 0.2s; | |
| } | |
| /* | |
| 親要素(pre)にホバーするとボタンを表示 | |
| 「親:hover 子」のセレクタで、 | |
| 親にマウスが乗った時に子のスタイルを変更 | |
| */ | |
| pre:hover .copy-btn { | |
| opacity: 1; | |
| } | |
| .copy-btn:hover { | |
| /* ボタン自体にホバーした時はさらに明るく */ | |
| background: rgba(255, 255, 255, 0.3); | |
| } | |
| /* ============================================ | |
| ログセクション | |
| ============================================ | |
| 【ログ表示の目的】 | |
| ブラウザの開発者ツール(Console)を開かなくても、 | |
| アプリの動作状況を画面上で確認できるようにする。 | |
| 【デザインの意図】 | |
| - ターミナル/コンソール風の見た目 | |
| - 暗い背景 + 薄い文字 = 目に優しい | |
| - 固定高さ + スクロール = 大量のログに対応 | |
| */ | |
| .log-container { | |
| margin-top: 0; | |
| } | |
| /* | |
| ログヘッダー(タイトルとボタン) | |
| 【Flexboxの配置テクニック】 | |
| justify-content: space-between で | |
| 子要素を両端に配置(中央に空きができる) | |
| ┌──────────────────────────────────┐ | |
| │ タイトル [ボタン群] │ | |
| │ ←────────────────────────────→ │ | |
| │ space-between による配置 │ | |
| └──────────────────────────────────┘ | |
| */ | |
| .log-header { | |
| display: flex; | |
| /* 両端に配置(中央にスペース) */ | |
| justify-content: space-between; | |
| /* 上下中央揃え */ | |
| align-items: center; | |
| /* | |
| flex-wrap: wrap | |
| 子要素が収まらない場合、折り返す。 | |
| 狭い画面でも崩れずに表示される。 | |
| */ | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| /* ボタングループを横並びに */ | |
| .log-buttons { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| /* モバイルではヘッダーを縦並びに */ | |
| @media (max-width: 480px) { | |
| .log-header { | |
| /* 縦方向に並べる */ | |
| flex-direction: column; | |
| /* | |
| align-items: stretch | |
| 子要素を横幅いっぱいに広げる。 | |
| (columnの場合、alignは横方向に作用) | |
| */ | |
| align-items: stretch; | |
| } | |
| .log-buttons { | |
| /* ボタンを右寄せ */ | |
| justify-content: flex-end; | |
| } | |
| } | |
| /* | |
| ログ表示エリア | |
| ターミナル風のデザイン | |
| */ | |
| .log { | |
| /* 固定高さ(これ以上は伸びない) */ | |
| height: 180px; | |
| /* | |
| overflow-y: auto | |
| 縦方向のスクロール設定 | |
| 内容が収まらない場合のみスクロールバー表示 | |
| */ | |
| overflow-y: auto; | |
| /* 非常に暗い背景(ほぼ黒) */ | |
| background: #0f172a; | |
| color: #94a3b8; | |
| border-radius: 12px; | |
| padding: 16px; | |
| font-family: ui-monospace, monospace; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| border: 1px solid #334155; | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| @media (max-width: 480px) { | |
| .log { | |
| height: 150px; | |
| padding: 12px; | |
| font-size: 12px; | |
| } | |
| } | |
| /* ============================================ | |
| TODOリストのスタイル | |
| ============================================ */ | |
| #todoList { | |
| /* デフォルトの箇条書きマークを消す */ | |
| list-style: none; | |
| padding: 0; | |
| margin: 0; | |
| } | |
| #todoList li { | |
| display: flex; | |
| /* タイトルと削除ボタンを両端に */ | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 8px 12px; | |
| border-bottom: 1px solid #f1f5f9; | |
| background: #fff; | |
| transition: background 0.2s; | |
| } | |
| #todoList li:hover { | |
| background: #f8fafc; | |
| } | |
| /* 完了したTODOは取り消し線 */ | |
| #todoList li.done span { | |
| text-decoration: line-through; | |
| color: #94a3b8; | |
| } | |
| /* ============================================ | |
| シャッフルリストのスタイル | |
| ============================================ */ | |
| #shuffleList li { | |
| padding: 6px 0; | |
| border-bottom: 1px dotted #e2e8f0; | |
| /* クリック可能を示す */ | |
| cursor: pointer; | |
| } | |
| #shuffleList li:hover { | |
| background: #f1f5f9; | |
| } | |
| /* 除外された項目は薄く表示 */ | |
| #shuffleList li.excluded { | |
| text-decoration: line-through; | |
| color: #cbd5e1; | |
| opacity: 0.6; | |
| } | |
| /* ============================================ | |
| 天気予報セクションのスタイル | |
| ============================================ */ | |
| .weather-detail { | |
| /* 初期状態は非表示 */ | |
| display: none; | |
| background: #f8fafc; | |
| border-radius: 8px; | |
| padding: 12px; | |
| } | |
| /* 天気アイコンと気温の横並び */ | |
| .weather-main { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 8px; | |
| } | |
| /* 天気絵文字アイコン */ | |
| .weather-icon { | |
| font-size: 2rem; | |
| } | |
| /* 気温表示 */ | |
| .weather-temp { | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| } | |
| /* 湿度・風速などの補助情報 */ | |
| .weather-sub { | |
| font-size: 0.9rem; | |
| } | |
| /* モバイルでは天気アイコンを大きく */ | |
| @media (max-width: 480px) { | |
| .weather-icon { | |
| font-size: 2.5rem; | |
| } | |
| .weather-temp { | |
| font-size: 1.3rem; | |
| } | |
| } | |
| /* ============================================ | |
| 参考情報セクションのスタイル | |
| ============================================ */ | |
| /* ============================================ | |
| 参考情報セクションのスタイル | |
| ============================================ | |
| 【このセクションについて】 | |
| 用語集、メソッド一覧、チートシート、クイズなど | |
| 学習の補助となる情報をタブ形式でまとめたエリア | |
| 【含まれるコンポーネント】 | |
| - タブナビゲーション(tab-nav, tab-btn) | |
| - タブコンテンツ(tab-content) | |
| - 用語カード(term-card) | |
| - メソッドテーブル(method-table) | |
| - チートシート(cheatsheet-grid) | |
| - クイズ(quiz-container) | |
| - アコーディオン(accordion) | |
| */ | |
| /* 参考情報全体のコンテナ | |
| メインコンテンツとの区切りを明確にするため | |
| 上部にボーダーと余白を設定 | |
| */ | |
| .reference-section { | |
| /* margin-top: 親要素との上方向の外側余白 */ | |
| margin-top: 40px; | |
| /* padding-top: 内側の上方向余白 */ | |
| padding-top: 30px; | |
| /* border-top: 上部に3pxの実線ボーダー | |
| セクションの区切りを視覚的に表現 */ | |
| border-top: 3px solid #e2e8f0; | |
| } | |
| .reference-section h2 { | |
| font-size: 1.6rem; | |
| color: #1e293b; | |
| margin-bottom: 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| /* ============================================ | |
| タブナビゲーション | |
| ============================================ | |
| 【タブUIの構造】 | |
| ┌──────┐ ┌──────┐ ┌──────┐ | |
| │ Tab1 │ │ Tab2 │ │ Tab3 │ ← ボタン群 | |
| └──────┴─┴──────┴─┴──────┴────── | |
| │ コンテンツ領域 │ ← 選択されたタブの内容 | |
| └────────────────────────────────┘ | |
| 【アクセシビリティ】 | |
| - role="tablist": タブ一覧であることを支援技術に伝達 | |
| - role="tab": 各タブボタンの役割 | |
| - aria-selected: 選択状態をスクリーンリーダーに伝達 | |
| */ | |
| /* タブボタンを横並びにするコンテナ */ | |
| .tab-nav { | |
| /* Flexboxで横並び配置 */ | |
| display: flex; | |
| /* flex-wrap: wrap で折り返しを許可 | |
| 画面幅が狭い場合、ボタンが自動的に次の行へ */ | |
| flex-wrap: wrap; | |
| /* ボタン間の隙間 */ | |
| gap: 8px; | |
| margin-bottom: 20px; | |
| /* 下部にボーダーを引いてコンテンツとの境界を明示 */ | |
| border-bottom: 2px solid #e2e8f0; | |
| padding-bottom: 12px; | |
| } | |
| /* 各タブボタンのスタイル */ | |
| .tab-btn { | |
| padding: 10px 18px; | |
| /* 2pxのボーダーで囲む */ | |
| border: 2px solid #e2e8f0; | |
| background: #fff; | |
| /* border-radius: 上左 上右 下右 下左 | |
| 上だけ角丸にしてタブらしい見た目に */ | |
| border-radius: 8px 8px 0 0; | |
| /* cursor: pointer でクリックできることを示す */ | |
| cursor: pointer; | |
| font-weight: 600; | |
| /* 非選択時は薄めの色 */ | |
| color: #64748b; | |
| /* all 0.2s: すべてのプロパティにトランジション適用 */ | |
| transition: all 0.2s; | |
| font-size: 0.95rem; | |
| } | |
| /* ホバー時のスタイル */ | |
| .tab-btn:hover { | |
| background: #f1f5f9; | |
| color: #334155; | |
| } | |
| /* 選択中(アクティブ)のタブ | |
| .activeクラスはJavaScriptで切り替え */ | |
| .tab-btn.active { | |
| /* メインカラーで塗りつぶし */ | |
| background: var(--primary); | |
| color: #fff; | |
| border-color: var(--primary); | |
| } | |
| /* モバイル対応:幅480px以下でコンパクトに */ | |
| @media (max-width: 480px) { | |
| .tab-btn { | |
| padding: 8px 12px; | |
| font-size: 0.85rem; | |
| /* flex: 1 で均等幅に */ | |
| flex: 1; | |
| text-align: center; | |
| } | |
| } | |
| /* ============================================ | |
| タブコンテンツ | |
| ============================================ | |
| 【表示/非表示の仕組み】 | |
| - デフォルトは display: none で非表示 | |
| - .active クラスが付くと display: block で表示 | |
| - JavaScriptでクラスを切り替えて制御 | |
| 【アニメーション】 | |
| タブ切替時にフェードインアニメーションを適用 | |
| コンテンツの切り替わりを滑らかに見せる | |
| */ | |
| /* タブの中身(デフォルトは非表示) */ | |
| .tab-content { | |
| /* display: none で完全に非表示 | |
| visibility: hidden との違い: | |
| - none: 要素が存在しないかのように扱われる(スペースも消える) | |
| - hidden: 見えないが、スペースは確保される */ | |
| display: none; | |
| /* アニメーションは表示時に適用される */ | |
| animation: fadeIn 0.3s ease; | |
| } | |
| /* アクティブ(表示中)のタブコンテンツ */ | |
| .tab-content.active { | |
| display: block; | |
| } | |
| /* @keyframes: CSSアニメーションの定義 | |
| アニメーション名 fadeIn を定義し、 | |
| 上のanimationプロパティで使用 | |
| 【アニメーションの流れ】 | |
| from → to の間を滑らかに補間 | |
| - opacity: 0→1(透明→不透明) | |
| - translateY: 10px→0(下から上へ移動) | |
| */ | |
| @keyframes fadeIn { | |
| /* アニメーション開始時 */ | |
| from { | |
| /* 完全に透明 */ | |
| opacity: 0; | |
| /* Y軸方向に10px下にずらす */ | |
| transform: translateY(10px); | |
| } | |
| /* アニメーション終了時 */ | |
| to { | |
| /* 完全に不透明 */ | |
| opacity: 1; | |
| /* 元の位置に戻す */ | |
| transform: translateY(0); | |
| } | |
| } | |
| /* ============================================ | |
| 用語カード(用語集タブ) | |
| ============================================ | |
| 【カードの構造】 | |
| ┌─────────────────────────────┐ | |
| │ 用語名 [カテゴリ] │ ← term-header | |
| │ 簡単な説明 │ ← term-brief | |
| │ ................................│ ← 点線ボーダー | |
| │ 詳細説明(展開時のみ表示) │ ← term-detail | |
| │ [詳細を見る ▼] │ ← term-toggle | |
| └─────────────────────────────┘ | |
| 【インタラクション】 | |
| - クリックで展開/折りたたみ | |
| - ホバーで浮き上がり効果 | |
| - カテゴリ別に色分け | |
| */ | |
| /* カードを並べるグリッドコンテナ */ | |
| .term-grid { | |
| display: grid; | |
| /* auto-fill: 入るだけカードを並べる | |
| minmax(280px, 1fr): 最小280px、最大は均等分配 | |
| これにより、画面幅に応じて1〜3列に自動調整 */ | |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
| gap: 16px; | |
| } | |
| /* 各用語カード */ | |
| .term-card { | |
| background: #fff; | |
| border: 1px solid #e2e8f0; | |
| border-radius: 10px; | |
| padding: 16px; | |
| /* すべてのプロパティにトランジション */ | |
| transition: all 0.2s; | |
| /* クリックできることを示す */ | |
| cursor: pointer; | |
| } | |
| /* ホバー時:浮き上がり効果 | |
| box-shadow + translateY で立体感を演出 */ | |
| .term-card:hover { | |
| /* box-shadow: x方向 y方向 ぼかし 広がり 色 | |
| 下方向に影を落として浮いているように見せる */ | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
| /* 少し上に移動 */ | |
| transform: translateY(-2px); | |
| } | |
| /* 展開時のスタイル(.expandedクラスはJSで付与) */ | |
| .term-card.expanded { | |
| background: #f8fafc; | |
| /* メインカラーでボーダーを強調 */ | |
| border-color: var(--primary); | |
| } | |
| /* カードヘッダー:用語名とカテゴリを横並び */ | |
| .term-header { | |
| display: flex; | |
| /* space-between: 両端に配置 */ | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| /* 用語名 */ | |
| .term-name { | |
| font-weight: 700; | |
| color: #1e293b; | |
| font-size: 1.05rem; | |
| } | |
| /* カテゴリバッジ(共通スタイル) */ | |
| .term-category { | |
| font-size: 0.75rem; | |
| padding: 3px 8px; | |
| /* pill型(丸い両端)のバッジ */ | |
| border-radius: 12px; | |
| font-weight: 600; | |
| } | |
| /* カテゴリ別の色分け | |
| 背景と文字色のコントラストに注意 | |
| アクセシビリティを考慮した配色 */ | |
| /* 基本:青系 */ | |
| .term-category.basic { | |
| background: #dbeafe; | |
| color: #1d4ed8; | |
| } | |
| /* 配列:緑系 */ | |
| .term-category.array { | |
| background: #dcfce7; | |
| color: #16a34a; | |
| } | |
| /* DOM:オレンジ系 */ | |
| .term-category.dom { | |
| background: #fef3c7; | |
| color: #d97706; | |
| } | |
| /* 非同期:紫系 */ | |
| .term-category.async { | |
| background: #ede9fe; | |
| color: #7c3aed; | |
| } | |
| /* ストレージ:ピンク系 */ | |
| .term-category.storage { | |
| background: #fce7f3; | |
| color: #db2777; | |
| } | |
| /* 簡単な説明文 */ | |
| .term-brief { | |
| color: #64748b; | |
| font-size: 0.9rem; | |
| margin-top: 8px; | |
| } | |
| /* 詳細説明エリア(デフォルトは非表示) */ | |
| .term-detail { | |
| display: none; | |
| margin-top: 12px; | |
| padding-top: 12px; | |
| /* dashed: 点線ボーダー */ | |
| border-top: 1px dashed #e2e8f0; | |
| } | |
| /* カード展開時に詳細を表示 */ | |
| .term-card.expanded .term-detail { | |
| display: block; | |
| } | |
| /* 詳細内のサンプルコード */ | |
| .term-detail pre { | |
| margin-top: 10px; | |
| font-size: 0.85rem; | |
| } | |
| /* 「詳細を見る」トグルテキスト */ | |
| .term-toggle { | |
| font-size: 0.8rem; | |
| color: var(--primary); | |
| margin-top: 8px; | |
| /* inline-block: 横幅を内容に合わせつつ、marginが効く */ | |
| display: inline-block; | |
| } | |
| /* ============================================ | |
| メソッド一覧テーブル | |
| ============================================ | |
| 【テーブルの構造】 | |
| ┌────────┬────────┬────────┐ | |
| │ メソッド │ 説明 │ 例 │ ← ヘッダー(sticky) | |
| ├────────┼────────┼────────┤ | |
| │ push │ 追加 │ arr.. │ ← データ行 | |
| └────────┴────────┴────────┘ | |
| 【レスポンシブ対応】 | |
| - overflow-x: auto でテーブルがはみ出す場合は横スクロール | |
| - -webkit-overflow-scrolling でiOSのスムーズスクロール | |
| */ | |
| /* テーブルコンテナ:横スクロール対応 */ | |
| .method-table-container { | |
| /* auto: 内容がはみ出す場合のみスクロールバー表示 */ | |
| overflow-x: auto; | |
| /* iOS Safari用:スムーズな慣性スクロール */ | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| /* テーブル本体 */ | |
| .method-table { | |
| width: 100%; | |
| /* border-collapse: collapse でセル間の隙間を消す | |
| separate との違い: | |
| - collapse: セル同士がくっつく(border共有) | |
| - separate: セル間に隙間あり(border-spacing設定可) */ | |
| border-collapse: collapse; | |
| font-size: 0.9rem; | |
| } | |
| /* テーブルヘッダー・データセル共通 */ | |
| .method-table th, | |
| .method-table td { | |
| padding: 12px; | |
| /* text-align: 水平方向の配置 | |
| left/center/right で指定 */ | |
| text-align: left; | |
| border-bottom: 1px solid #e2e8f0; | |
| } | |
| /* テーブルヘッダー */ | |
| .method-table th { | |
| background: #f8fafc; | |
| font-weight: 600; | |
| color: #475569; | |
| /* position: sticky でスクロール時に固定表示 | |
| top: 0 で画面上部に固定 */ | |
| position: sticky; | |
| top: 0; | |
| } | |
| /* 行ホバー時のハイライト */ | |
| .method-table tr:hover { | |
| background: #f8fafc; | |
| } | |
| /* テーブル内のコード表示 */ | |
| .method-table code { | |
| /* ダークな背景で目立たせる */ | |
| background: #1e293b; | |
| color: #e2e8f0; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 0.85rem; | |
| } | |
| /* ============================================ | |
| 検索・フィルター | |
| ============================================ | |
| 【UIの構成】 | |
| ┌──────────────┐ ┌───┐ ┌───┐ ┌───┐ | |
| │ 検索ボックス │ │すべて│ │基本│ │配列│ ← フィルタータグ | |
| └──────────────┘ └───┘ └───┘ └───┘ | |
| 【機能】 | |
| - 検索:入力文字で用語をフィルタリング | |
| - タグ:カテゴリでフィルタリング | |
| */ | |
| /* フィルターエリア全体 */ | |
| .search-filter { | |
| display: flex; | |
| gap: 12px; | |
| margin-bottom: 16px; | |
| /* 画面幅が狭い場合は折り返し */ | |
| flex-wrap: wrap; | |
| } | |
| /* 検索入力フィールド */ | |
| .search-filter input { | |
| /* flex: 1 で残りスペースを占有 */ | |
| flex: 1; | |
| /* 最小幅を確保 */ | |
| min-width: 200px; | |
| } | |
| /* フィルタータグコンテナ */ | |
| .filter-tags { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| } | |
| /* 各フィルタータグ(ピル型ボタン) */ | |
| .filter-tag { | |
| padding: 6px 12px; | |
| /* 丸い端のピル型 */ | |
| border-radius: 20px; | |
| font-size: 0.8rem; | |
| cursor: pointer; | |
| border: 1px solid #e2e8f0; | |
| background: #fff; | |
| transition: all 0.2s; | |
| } | |
| /* ホバー時・選択時のスタイル */ | |
| .filter-tag:hover, | |
| .filter-tag.active { | |
| background: var(--primary); | |
| color: #fff; | |
| border-color: var(--primary); | |
| } | |
| /* ============================================ | |
| クイズセクション(理解度チェック) | |
| ============================================ | |
| 【クイズUIの構造】 | |
| ┌─────────────────────────────────┐ | |
| │ Q. 問題文 │ ← quiz-question | |
| │ ┌─────────────────────────────┐ │ | |
| │ │ ○ 選択肢A │ │ ← quiz-option | |
| │ └─────────────────────────────┘ │ | |
| │ ┌─────────────────────────────┐ │ | |
| │ │ ○ 選択肢B │ │ | |
| │ └─────────────────────────────┘ │ | |
| │ 正解! 解説... │ ← quiz-result | |
| │ [1/5] [次の問題 →] │ ← quiz-nav | |
| └─────────────────────────────────┘ | |
| 【グラデーション背景】 | |
| linear-gradient(135deg, #667eea 0%, #764ba2 100%) | |
| - 135deg: 左上から右下への斜め方向 | |
| - #667eea: 青紫(開始色) | |
| - #764ba2: 紫(終了色) | |
| 視覚的にインタラクティブな要素であることを強調 | |
| */ | |
| /* クイズコンテナ:グラデーション背景 */ | |
| .quiz-container { | |
| /* linear-gradient: グラデーション | |
| 角度 色1 位置, 色2 位置 の形式 */ | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border-radius: 16px; | |
| padding: 24px; | |
| /* 白文字で読みやすく */ | |
| color: #fff; | |
| } | |
| /* 問題文 */ | |
| .quiz-question { | |
| font-size: 1.1rem; | |
| margin-bottom: 16px; | |
| font-weight: 500; | |
| } | |
| /* 選択肢コンテナ */ | |
| .quiz-options { | |
| display: grid; | |
| gap: 10px; | |
| } | |
| /* 各選択肢(半透明の白背景) */ | |
| .quiz-option { | |
| /* rgba(r, g, b, a): aはアルファ(透明度)0〜1 | |
| 0.15 = 15%の不透明度(85%透明) */ | |
| background: rgba(255, 255, 255, 0.15); | |
| border: 2px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 10px; | |
| padding: 14px 18px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-size: 0.95rem; | |
| } | |
| /* 選択肢ホバー時 */ | |
| .quiz-option:hover { | |
| background: rgba(255, 255, 255, 0.25); | |
| border-color: rgba(255, 255, 255, 0.5); | |
| } | |
| /* 正解時のスタイル(緑系) */ | |
| .quiz-option.correct { | |
| /* 緑の半透明背景 */ | |
| background: rgba(34, 197, 94, 0.4); | |
| border-color: #22c55e; | |
| } | |
| /* 不正解時のスタイル(赤系) */ | |
| .quiz-option.wrong { | |
| /* 赤の半透明背景 */ | |
| background: rgba(239, 68, 68, 0.4); | |
| border-color: #ef4444; | |
| } | |
| /* 回答後の選択肢無効化 */ | |
| .quiz-option.disabled { | |
| /* pointer-events: none でクリック無効化 */ | |
| pointer-events: none; | |
| opacity: 0.7; | |
| } | |
| /* 結果表示エリア(デフォルト非表示) */ | |
| .quiz-result { | |
| margin-top: 16px; | |
| padding: 12px; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| display: none; | |
| } | |
| /* 結果表示時 */ | |
| .quiz-result.show { | |
| display: block; | |
| } | |
| /* 正解時の結果背景 */ | |
| .quiz-result.correct { | |
| background: rgba(34, 197, 94, 0.3); | |
| } | |
| /* 不正解時の結果背景 */ | |
| .quiz-result.wrong { | |
| background: rgba(239, 68, 68, 0.3); | |
| } | |
| /* クイズナビゲーション(進捗・次へボタン) */ | |
| .quiz-nav { | |
| display: flex; | |
| /* 左右に分散配置 */ | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-top: 20px; | |
| } | |
| /* 進捗表示(1/5など) */ | |
| .quiz-progress { | |
| font-size: 0.9rem; | |
| /* 少し薄くして目立ちすぎないように */ | |
| opacity: 0.8; | |
| } | |
| /* 次の問題ボタン */ | |
| .quiz-next-btn { | |
| background: rgba(255, 255, 255, 0.2); | |
| color: #fff; | |
| border: 2px solid rgba(255, 255, 255, 0.4); | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.2s; | |
| } | |
| .quiz-next-btn:hover { | |
| background: rgba(255, 255, 255, 0.3); | |
| } | |
| /* 無効化時のボタン */ | |
| .quiz-next-btn:disabled { | |
| opacity: 0.5; | |
| /* not-allowed: 禁止マークカーソル */ | |
| cursor: not-allowed; | |
| } | |
| /* ============================================ | |
| チートシート | |
| ============================================ | |
| 【チートシートの構造】 | |
| ┌────────────────────────────┐ | |
| │ ■ カテゴリ名 │ ← cheatsheet-header(ダーク背景) | |
| ├────────────────────────────┤ | |
| │ 説明 値/コード │ ← cheatsheet-item | |
| │ 説明 値/コード │ | |
| │ 説明 値/コード │ | |
| └────────────────────────────┘ | |
| 【用途】 | |
| よく使う構文やメソッドを一覧形式で素早く参照できる | |
| */ | |
| /* チートシートカードを並べるグリッド */ | |
| .cheatsheet-grid { | |
| display: grid; | |
| /* 最小300px、最大均等分配で自動配置 */ | |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
| gap: 16px; | |
| } | |
| /* 各チートシートカード */ | |
| .cheatsheet-card { | |
| background: #fff; | |
| border: 1px solid #e2e8f0; | |
| border-radius: 10px; | |
| /* overflow: hidden でヘッダーの角丸を維持 */ | |
| overflow: hidden; | |
| } | |
| /* カードヘッダー(ダークなグラデーション) */ | |
| .cheatsheet-header { | |
| /* ダーク系のグラデーションで目立たせる */ | |
| background: linear-gradient(135deg, #1e293b, #334155); | |
| color: #fff; | |
| padding: 12px 16px; | |
| font-weight: 600; | |
| font-size: 0.95rem; | |
| } | |
| /* カード本体 */ | |
| .cheatsheet-body { | |
| padding: 12px 16px; | |
| } | |
| /* 各項目(キー:値のペア) */ | |
| .cheatsheet-item { | |
| display: flex; | |
| /* 左右に分散配置 */ | |
| justify-content: space-between; | |
| padding: 8px 0; | |
| border-bottom: 1px solid #f1f5f9; | |
| font-size: 0.9rem; | |
| } | |
| /* 最後の項目は下線なし | |
| :last-child は親要素の最後の子要素を選択 */ | |
| .cheatsheet-item:last-child { | |
| border-bottom: none; | |
| } | |
| /* キー部分(説明テキスト) */ | |
| .cheatsheet-key { | |
| color: #64748b; | |
| } | |
| /* 値部分(コードや構文) */ | |
| .cheatsheet-value { | |
| /* モノスペースフォントでコードらしく */ | |
| font-family: ui-monospace, monospace; | |
| color: #1e293b; | |
| font-weight: 500; | |
| } | |
| /* ============================================ | |
| アコーディオン(折りたたみパネル) | |
| ============================================ | |
| 【アコーディオンの構造】 | |
| 閉じた状態: | |
| ┌────────────────────────────┐ | |
| │ タイトル ▼ │ ← accordion-header | |
| └────────────────────────────┘ | |
| 開いた状態: | |
| ┌────────────────────────────┐ | |
| │ タイトル ▲ │ ← accordion-header | |
| ├────────────────────────────┤ | |
| │ コンテンツ内容... │ ← accordion-content | |
| └────────────────────────────┘ | |
| 【CSSのみでの実装パターン】 | |
| - .openクラスの有無で表示/非表示を切り替え | |
| - JavaScriptでクラスをtoggle | |
| */ | |
| /* アコーディオン外枠 */ | |
| .accordion { | |
| border: 1px solid #e2e8f0; | |
| border-radius: 10px; | |
| /* overflow: hidden で子要素の角丸を維持 */ | |
| overflow: hidden; | |
| margin-bottom: 12px; | |
| } | |
| /* アコーディオンヘッダー(クリック可能部分) */ | |
| .accordion-header { | |
| background: #f8fafc; | |
| padding: 14px 18px; | |
| cursor: pointer; | |
| display: flex; | |
| /* タイトルとアイコンを両端に配置 */ | |
| justify-content: space-between; | |
| align-items: center; | |
| font-weight: 600; | |
| color: #334155; | |
| transition: background 0.2s; | |
| } | |
| /* ヘッダーホバー時 */ | |
| .accordion-header:hover { | |
| background: #f1f5f9; | |
| } | |
| /* 開閉アイコン(▼マーク) */ | |
| .accordion-icon { | |
| /* 回転アニメーション */ | |
| transition: transform 0.3s; | |
| } | |
| /* 開いた時:アイコンを180度回転(▼→▲) */ | |
| .accordion.open .accordion-icon { | |
| transform: rotate(180deg); | |
| } | |
| /* コンテンツ部分(デフォルトは非表示) */ | |
| .accordion-content { | |
| display: none; | |
| padding: 16px 18px; | |
| background: #fff; | |
| /* ヘッダーとの境界線 */ | |
| border-top: 1px solid #e2e8f0; | |
| } | |
| /* 開いた時にコンテンツを表示 */ | |
| .accordion.open .accordion-content { | |
| display: block; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ============================================ | |
| ヘッダー部分 | |
| ============================================ --> | |
| <header> | |
| <h1>JavaScript入門:Ver 2.4</h1> | |
| <p class="muted"> | |
| コードブロックの右上や、ログのヘッダーにあるボタンでテキストをコピーできます。 | |
| <button | |
| onclick="navigator.clipboard.writeText(document.documentElement.outerHTML).then(()=>{this.textContent='コピー完了!';setTimeout(()=>this.textContent='このページのコード全体をコピー',2000)}).catch(()=>alert('コピーできませんでした'))" | |
| class="sm" style="margin-left:8px;">このページのコード全体をコピー</button> | |
| </p> | |
| </header> | |
| <!-- ============================================ | |
| メインコンテンツ | |
| ============================================ | |
| .row クラスでグリッドレイアウトを適用 | |
| 各 section.card が1つの学習カード | |
| --> | |
| <main class="row"> | |
| <!-- ======================================== | |
| セクション1: あいさつ | |
| ======================================== | |
| 【学習内容】 | |
| - 変数の宣言と代入 | |
| - 文字列の連結(テンプレートリテラル) | |
| - イベントリスナーの基本 | |
| --> | |
| <section class="card"> | |
| <h2>1) あいさつ</h2> | |
| <span class="point">学び:変数 / 文字列操作 / イベント</span> | |
| <div class="card-content"> | |
| <!-- label要素でinputを囲むと、ラベルクリックでフォーカスが当たる --> | |
| <label> | |
| 名前: | |
| <input id="nameInput" placeholder="例:太郎 / ゲスト" /> | |
| </label> | |
| <button id="helloBtn" class="primary">あいさつを表示</button> | |
| <!-- aria-live="polite" でスクリーンリーダーに変更を通知 --> | |
| <div id="helloOut" class="status-area muted" aria-live="polite">ここに結果が出るよ</div> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| セクション2: 条件分岐 | |
| ======================================== | |
| 【学習内容】 | |
| - if文による条件分岐 | |
| - 比較演算子(>=, <=, &&) | |
| - Number()による数値変換 | |
| --> | |
| <section class="card"> | |
| <h2>2) 条件分岐</h2> | |
| <span class="point">学び:if文 / 比較演算子 / 数値変換</span> | |
| <div class="card-content"> | |
| <div class="grid two"> | |
| <label> | |
| 年齢: | |
| <!-- type="number" で数値入力に特化、min="0"で負の数を防止 --> | |
| <input id="ageInput" type="number" min="0" placeholder="例:18" /> | |
| </label> | |
| <label> | |
| 判定ルール: | |
| <!-- select要素でプルダウンメニュー --> | |
| <select id="ruleSelect"> | |
| <option value="adult">成人判定(18以上)</option> | |
| <option value="teen">ティーン判定(13〜19)</option> | |
| </select> | |
| </label> | |
| </div> | |
| <button id="judgeBtn" class="primary">判定する</button> | |
| <div id="judgeOut" class="status-area muted" aria-live="polite">ここに判定が出るよ</div> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| セクション3: 繰り返し | |
| ======================================== | |
| 【学習内容】 | |
| - for文による繰り返し処理 | |
| - 配列へのpush()メソッド | |
| - join()による配列の文字列化 | |
| --> | |
| <section class="card"> | |
| <h2>3) 繰り返し</h2> | |
| <span class="point">学び:for文 / 配列への追加</span> | |
| <div class="card-content"> | |
| <label> | |
| N(回数): | |
| <input id="loopN" type="number" min="1" max="50" placeholder="例:10" /> | |
| </label> | |
| <button id="loopBtn" class="primary">1〜Nを表示</button> | |
| <div id="loopOut" class="status-area muted" aria-live="polite">ここに出るよ</div> | |
| <!-- サンプルコード表示 --> | |
| <pre><code>// for文の書き方 | |
| for (let i = 1; i <= N; i++) { | |
| console.log(i); | |
| }</code></pre> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| セクション4: 計算(関数と引数) | |
| ======================================== | |
| 【学習内容】 | |
| - 関数の定義と呼び出し | |
| - 引数の受け渡し | |
| - 入力値のバリデーション | |
| --> | |
| <section class="card"> | |
| <h2>4) 計算</h2> | |
| <span class="point">学び:関数 / 引数 / 入力チェック</span> | |
| <div class="card-content"> | |
| <div class="grid two"> | |
| <input id="aInput" type="number" placeholder="数値A" aria-label="数値A" /> | |
| <input id="bInput" type="number" placeholder="数値B" aria-label="数値B" /> | |
| </div> | |
| <button id="sumBtn" class="primary">A + B を計算</button> | |
| <div id="sumOut" class="status-area muted" aria-live="polite">結果待ち...</div> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| セクション5: class体験 | |
| ======================================== | |
| 【学習内容】 | |
| - classによるオブジェクト指向プログラミング | |
| - constructor(コンストラクタ)による初期化 | |
| - メソッドの定義と呼び出し | |
| - インスタンスの作成(new キーワード) | |
| - thisキーワードの理解 | |
| 【classとは】 | |
| classは「オブジェクトの設計図」です。 | |
| 例えば「人」というclassを作ると、 | |
| 「太郎さん」「花子さん」などの具体的な人(インスタンス)を | |
| 同じ設計図から何人でも作れます。 | |
| 【用語解説】 | |
| - class: 設計図(テンプレート) | |
| - インスタンス: classから作った具体的なオブジェクト | |
| - constructor: インスタンス作成時に自動で呼ばれる初期化処理 | |
| - メソッド: classに属する関数 | |
| - this: そのインスタンス自身を指すキーワード | |
| 【なぜclassを使うのか】 | |
| - 同じ構造のオブジェクトを簡単に複数作れる | |
| - データ(プロパティ)と処理(メソッド)をまとめて管理できる | |
| - コードの再利用性が高まる | |
| --> | |
| <section class="card"> | |
| <h2>5) class体験</h2> | |
| <span class="point">学び:class / constructor / メソッド / インスタンス</span> | |
| <div class="card-content"> | |
| <!-- 機能説明:classの概念を簡潔に説明 --> | |
| <div class="muted"> | |
| classはオブジェクトの設計図。Personクラスで名前と年齢を持つインスタンスを作成してみよう。 | |
| </div> | |
| <!-- 入力フォーム:Personのプロパティを入力 --> | |
| <div class="grid two"> | |
| <!-- 名前入力:constructorの第1引数になる --> | |
| <input id="classNameInput" placeholder="名前(例:太郎)" aria-label="名前" /> | |
| <!-- 年齢入力:type="number"で数値入力に特化 --> | |
| <input id="classAgeInput" type="number" placeholder="年齢(例:20)" aria-label="年齢" /> | |
| </div> | |
| <!-- 操作ボタン --> | |
| <div class="grid two"> | |
| <!-- 作成ボタン:new Person()でインスタンスを生成 --> | |
| <button id="createPersonBtn" class="primary">Personを作成</button> | |
| <!-- 挨拶ボタン:作成したインスタンスのgreet()メソッドを呼び出し --> | |
| <button id="greetPersonBtn">挨拶させる</button> | |
| </div> | |
| <!-- 状態表示:作成結果やエラーを表示 --> | |
| <div id="classStatus" class="status-area muted" aria-live="polite">状態表示エリア</div> | |
| <!-- サンプルコード:classの基本構文 --> | |
| <pre><code>// クラスの定義(設計図を作る) | |
| class Person { | |
| // constructor: インスタンス生成時に呼ばれる | |
| constructor(name, age) { | |
| // this.xxx でインスタンスのプロパティを設定 | |
| this.name = name; | |
| this.age = age; | |
| } | |
| // メソッド: インスタンスが持つ関数 | |
| greet() { | |
| return `こんにちは、${this.name}です`; | |
| } | |
| } | |
| // インスタンスの作成(設計図から実体を作る) | |
| const taro = new Person("太郎", 20); | |
| taro.greet(); // → "こんにちは、太郎です" | |
| taro.name; // → "太郎" | |
| taro.age; // → 20</code></pre> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| セクション6: 非同期処理 | |
| ======================================== | |
| 【学習内容】 | |
| - setTimeout()による遅延実行 | |
| - コールバック関数の概念 | |
| - アロー関数の書き方 | |
| --> | |
| <section class="card"> | |
| <h2>6) 非同期処理</h2> | |
| <span class="point">学び:setTimeout / コールバック関数</span> | |
| <div class="card-content"> | |
| <button id="timeoutBtn" class="primary">2秒後にメッセージ</button> | |
| <div id="timeoutOut" class="status-area muted" aria-live="polite">ボタンを押してね</div> | |
| <pre><code>// 2秒待つ書き方 | |
| setTimeout(() => { | |
| alert("2秒たった!"); | |
| }, 2000);</code></pre> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| セクション7: メモ帳(永続化) | |
| ======================================== | |
| 【学習内容】 | |
| - localStorageの基本操作(setItem, getItem, removeItem) | |
| - ページリロード後もデータが残る仕組み | |
| - ブラウザのストレージAPI | |
| --> | |
| <section class="card"> | |
| <h2>7) メモ帳 (永続化)</h2> | |
| <span class="point">学び:localStorageの基本</span> | |
| <div class="card-content"> | |
| <!-- メモ入力欄:aria-labelで支援技術向けのラベルを設定 --> | |
| <input id="memoInput" placeholder="リロードしても残るよ" aria-label="メモ入力" /> | |
| <div class="grid two"> | |
| <!-- 保存ボタン:localStorageに書き込み --> | |
| <button id="saveMemoBtn" class="primary">保存</button> | |
| <!-- 削除ボタン:dangerクラスで赤系の見た目、localStorageからデータ削除 --> | |
| <button id="clearMemoBtn" class="danger" aria-label="メモを削除">削除</button> | |
| </div> | |
| <!-- 状態表示:aria-liveでスクリーンリーダーに変更を通知 --> | |
| <div id="memoStatus" class="status-area muted" aria-live="polite">状態表示エリア</div> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| セクション8: Cookie体験 | |
| ======================================== | |
| 【学習内容】 | |
| - document.cookieの読み書き | |
| - Cookie の有効期限(max-age / expires) | |
| - Cookie の取得・削除方法 | |
| - localStorageとの違いを理解する | |
| 【Cookieとは】 | |
| Cookieはブラウザに保存される小さなテキストデータです。 | |
| HTTPリクエスト時にサーバーへ自動的に送信されるため、 | |
| ログイン状態の維持やユーザー追跡などに使われます。 | |
| 【localStorageとの違い】 | |
| ┌─────────────────┬──────────────────┬──────────────────┐ | |
| │ │ Cookie │ localStorage │ | |
| ├─────────────────┼──────────────────┼──────────────────┤ | |
| │ 容量 │ 約4KB │ 約5MB │ | |
| │ サーバー送信 │ 自動送信される │ 送信されない │ | |
| │ 有効期限 │ 設定可能 │ なし(永続) │ | |
| │ 主な用途 │ 認証・追跡 │ アプリデータ │ | |
| └─────────────────┴──────────────────┴──────────────────┘ | |
| 【Cookieの属性】 | |
| - max-age: 有効期限(秒数で指定、0で削除) | |
| - expires: 有効期限(日時で指定) | |
| - path: Cookieが有効なパス | |
| - secure: HTTPS接続時のみ送信 | |
| - SameSite: クロスサイトリクエスト制御 | |
| 【注意点】 | |
| - Cookieは文字列として保存される(オブジェクトはJSON変換が必要) | |
| - document.cookie への代入は「追加」であり「上書き」ではない | |
| - 取得時は全Cookieが1つの文字列で返される | |
| --> | |
| <section class="card"> | |
| <h2>8) Cookie体験</h2> | |
| <span class="point">学び:Cookie / 有効期限 / データ保存</span> | |
| <div class="card-content"> | |
| <!-- 機能説明:Cookieの特徴を簡潔に説明 --> | |
| <div class="muted"> | |
| Cookieはサーバーに自動送信される小さなデータ。有効期限を設定でき、ブラウザを閉じても保持される。 | |
| </div> | |
| <!-- 入力フォーム:Cookie名と値を入力 --> | |
| <div class="grid two"> | |
| <!-- Cookie名:key=value の key部分 --> | |
| <input id="cookieNameInput" placeholder="Cookie名(例:username)" aria-label="Cookie名" /> | |
| <!-- Cookie値:key=value の value部分 --> | |
| <input id="cookieValueInput" placeholder="値(例:太郎)" aria-label="Cookie値" /> | |
| </div> | |
| <!-- 操作ボタン:3列グリッドで配置 --> | |
| <div class="grid three"> | |
| <!-- 保存ボタン:document.cookie に代入 --> | |
| <button id="setCookieBtn" class="primary">Cookieを保存</button> | |
| <!-- 取得ボタン:document.cookie を読み取り --> | |
| <button id="getCookieBtn">Cookieを取得</button> | |
| <!-- 削除ボタン:max-age=0 で期限切れにする --> | |
| <button id="deleteCookieBtn" class="danger" aria-label="Cookieを削除">削除</button> | |
| </div> | |
| <!-- 状態表示:操作結果を表示 --> | |
| <div id="cookieStatus" class="status-area muted" aria-live="polite">状態表示エリア</div> | |
| <!-- サンプルコード:Cookieの基本操作 --> | |
| <pre><code>// Cookieの設定(7日間有効) | |
| // max-age: 秒単位で有効期限を指定 | |
| document.cookie = "name=value; max-age=" + (7*24*60*60); | |
| // Cookieの取得(全Cookie が文字列で返る) | |
| console.log(document.cookie); | |
| // → "name=value; other=data" | |
| // Cookieの削除(max-age=0 で即期限切れ) | |
| document.cookie = "name=; max-age=0";</code></pre> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| セクション9: TODOアプリ | |
| ======================================== | |
| 【学習内容】 | |
| - 配列の操作(push, splice) | |
| - localStorageによるデータ永続化 | |
| - キーボードイベント(Enterキー) | |
| - DOM要素の動的生成 | |
| --> | |
| <section class="card"> | |
| <h2>9) TODOアプリ</h2> | |
| <span class="point">学び:配列操作 / localStorage / Enterキー対応</span> | |
| <div class="card-content"> | |
| <div class="grid two"> | |
| <!-- aria-labelで支援技術向けのラベルを設定 --> | |
| <input id="todoInput" placeholder="TODOを入力 (Enterで追加)" aria-label="TODO項目" /> | |
| <button id="todoAddBtn" class="primary">追加</button> | |
| </div> | |
| <div class="grid two"> | |
| <div class="muted" style="font-size:0.8rem; align-self:center;">クリックで完了/未完了</div> | |
| <button id="todoClearAllBtn" class="danger sm" aria-label="全てのTODOを削除">全削除</button> | |
| </div> | |
| <div id="todoMsg" class="status-area muted" aria-live="polite"></div> | |
| <!-- TODOリストはJavaScriptで動的に生成 --> | |
| <ul id="todoList"></ul> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| セクション10: 天気予報 | |
| ======================================== | |
| 【学習内容】 | |
| - fetch APIによるHTTPリクエスト | |
| - async/awaitによる非同期処理 | |
| - 外部API(Open-Meteo)との連携 | |
| - JSONデータのパース | |
| - エラーハンドリング(try-catch-finally) | |
| --> | |
| <section class="card"> | |
| <h2>10) 天気予報</h2> | |
| <span class="point">学び:fetch API / async/await / 外部API連携</span> | |
| <div class="card-content"> | |
| <div class="grid two"> | |
| <label> | |
| 都市: | |
| <!-- 都市選択:valueに緯度,経度を持たせてAPIに渡す --> | |
| <select id="weatherCity"> | |
| <option value="35.6762,139.6503">東京</option> | |
| <option value="34.6937,135.5023">大阪</option> | |
| <option value="35.1815,136.9066">名古屋</option> | |
| <option value="43.0642,141.3469">札幌</option> | |
| <option value="33.5904,130.4017">福岡</option> | |
| <option value="26.2124,127.6809">那覇</option> | |
| </select> | |
| </label> | |
| <!-- 取得ボタン:クリックでOpen-Meteo APIを呼び出し --> | |
| <button id="weatherBtn" class="primary">天気を取得</button> | |
| </div> | |
| <!-- 状態表示:取得中/成功/エラーを表示 --> | |
| <div id="weatherOut" class="status-area muted" aria-live="polite">ボタンを押すと天気情報を取得します</div> | |
| <!-- 天気詳細表示エリア:初期状態はdisplay:noneで非表示 --> | |
| <div id="weatherDetail" class="weather-detail"> | |
| <div class="weather-main"> | |
| <!-- 天気アイコン:絵文字で表示 --> | |
| <span id="weatherIcon" class="weather-icon"></span> | |
| <div> | |
| <!-- 気温表示 --> | |
| <div id="weatherTemp" class="weather-temp"></div> | |
| <!-- 天気の説明(晴れ、曇りなど) --> | |
| <div id="weatherDesc" class="muted"></div> | |
| </div> | |
| </div> | |
| <!-- 補助情報:湿度と風速 --> | |
| <div class="grid two weather-sub"> | |
| <div>湿度: <span id="weatherHumidity"></span></div> | |
| <div>風速: <span id="weatherWind"></span></div> | |
| </div> | |
| </div> | |
| <!-- サンプルコード:fetch APIの基本的な使い方 --> | |
| <pre><code>// fetch APIの使い方 | |
| const res = await fetch(url); | |
| const data = await res.json(); | |
| console.log(data);</code></pre> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| セクション11: 応用シャッフル | |
| ======================================== | |
| 【学習内容】 | |
| - 高度な配列操作(map, filter) | |
| - Setを使った重複削除 | |
| - Fisher-Yatesシャッフルアルゴリズム | |
| - 複雑な状態管理 | |
| - localStorageによる複数データの永続化 | |
| 【機能説明】 | |
| - 左側:マスターデータの入力・保存 | |
| - 右側:シャッフル結果の表示・除外切替 | |
| - マスターと現在の並び順を別々に保存 | |
| --> | |
| <section class="card half-width"> | |
| <h2>11) 応用シャッフル (重複排除・状態保存)</h2> | |
| <span class="point">学び:高度な配列操作 / Set(重複削除) / 状態管理</span> | |
| <!-- 機能の説明文 --> | |
| <div class="muted"> | |
| 1行1項目。保存すると「マスター」になり、シャッフルすると「今の並び」が保存される。<br> | |
| リストをクリックすると「一時除外」もできるよ。 | |
| </div> | |
| <!-- 2カラムレイアウト:左が入力、右が結果 --> | |
| <div class="grid two" style="align-items: start;"> | |
| <!-- 左カラム:マスターデータ入力エリア --> | |
| <div class="grid"> | |
| <label class="muted">マスターデータ入力:</label> | |
| <!-- テキストエリア:1行1項目で入力 --> | |
| <!-- は改行コード(placeholder内で改行表示) --> | |
| <textarea id="shuffleInput" rows="6" | |
| placeholder="例: Aさん Bさん Cさん"></textarea> | |
| <div class="grid two"> | |
| <!-- 保存ボタン:Setで重複を自動削除して保存 --> | |
| <button id="shuffleSaveBtn" class="primary">保存 (重複は統合)</button> | |
| <!-- 戻すボタン:保存済みマスターをテキストエリアに復元 --> | |
| <button id="shuffleLoadMasterBtn">入力欄に戻す</button> | |
| </div> | |
| <!-- 入力側の状態表示 --> | |
| <div id="shuffleInputStatus" class="status-area muted" aria-live="polite"></div> | |
| </div> | |
| <!-- 右カラム:シャッフル結果表示エリア --> | |
| <div class="grid"> | |
| <label class="muted">現在の並び順 (クリックで除外切替):</label> | |
| <!-- リスト表示コンテナ --> | |
| <div | |
| style="border:1px solid #ddd; border-radius:8px; padding:10px; min-height:140px; background:#fafafa;"> | |
| <!-- 順序付きリスト:JavaScriptで動的に生成 --> | |
| <ol id="shuffleList" style="margin:0; padding-left:24px;"></ol> | |
| </div> | |
| <div class="grid two"> | |
| <!-- シャッフル実行ボタン:Fisher-Yatesアルゴリズムで並び替え --> | |
| <button id="shuffleRunBtn" class="primary">新規シャッフル</button> | |
| <!-- 全削除ボタン:マスターと状態両方を削除 --> | |
| <button id="shuffleClearAllBtn" class="danger" aria-label="シャッフルの全データを削除">全データ削除</button> | |
| </div> | |
| <!-- 結果側の状態表示 --> | |
| <div id="shuffleResultStatus" class="status-area muted" aria-live="polite"></div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ======================================== | |
| ログセクション | |
| ======================================== | |
| 【役割】 | |
| - アプリ全体の動作ログを表示 | |
| - ブラウザのコンソール代わりとして機能 | |
| - 各機能の実行結果をタイムスタンプ付きで記録 | |
| 【機能】 | |
| - ログのクリップボードコピー | |
| - ログのクリア(消去) | |
| - aria-liveでスクリーンリーダー対応 | |
| --> | |
| <section class="card full-width log-container"> | |
| <!-- ログヘッダー:タイトルとボタンを横並び --> | |
| <div class="log-header"> | |
| <h2 style="margin:0; border:none; padding:0;">ログ (画面内コンソール)</h2> | |
| <div class="log-buttons"> | |
| <!-- コピーボタン:ログ全体をクリップボードにコピー --> | |
| <button id="copyLogBtn" class="sm" aria-label="ログをクリップボードにコピー">ログをコピー</button> | |
| <!-- クリアボタン:ログを消去 --> | |
| <button id="clearLogBtn" class="sm" aria-label="ログをクリア">クリア</button> | |
| </div> | |
| </div> | |
| <!-- ログ表示エリア:overflow-yでスクロール可能 --> | |
| <div id="log" class="log" aria-live="polite"></div> | |
| </section> | |
| </main> | |
| <!-- ============================================ | |
| 参考情報セクション | |
| ============================================ | |
| 用語集、メソッド一覧、クイズなど | |
| 学習の補助となる情報をまとめたセクション | |
| --> | |
| <section class="reference-section"> | |
| <h2>📚 JavaScript リファレンス</h2> | |
| <p class="muted" style="margin-bottom: 20px;"> | |
| このページで使用しているJavaScriptの用語やメソッドをまとめました。カードをクリックすると詳細が表示されます。 | |
| </p> | |
| <!-- タブナビゲーション --> | |
| <nav class="tab-nav" role="tablist"> | |
| <button class="tab-btn active" data-tab="glossary" role="tab" aria-selected="true">用語集</button> | |
| <button class="tab-btn" data-tab="methods" role="tab" aria-selected="false">メソッド一覧</button> | |
| <button class="tab-btn" data-tab="cheatsheet" role="tab" aria-selected="false">チートシート</button> | |
| <button class="tab-btn" data-tab="quiz" role="tab" aria-selected="false">理解度チェック</button> | |
| </nav> | |
| <!-- ======================================== | |
| タブ1: 用語集 | |
| ======================================== --> | |
| <div class="tab-content active" id="tab-glossary" role="tabpanel"> | |
| <div class="search-filter"> | |
| <input type="text" id="termSearch" placeholder="用語を検索..." aria-label="用語検索"> | |
| <div class="filter-tags"> | |
| <span class="filter-tag active" data-filter="all">すべて</span> | |
| <span class="filter-tag" data-filter="basic">基本</span> | |
| <span class="filter-tag" data-filter="array">配列</span> | |
| <span class="filter-tag" data-filter="dom">DOM</span> | |
| <span class="filter-tag" data-filter="async">非同期</span> | |
| <span class="filter-tag" data-filter="storage">ストレージ</span> | |
| </div> | |
| </div> | |
| <div class="term-grid" id="termGrid"> | |
| <!-- 基本 --> | |
| <div class="term-card" data-category="basic"> | |
| <div class="term-header"> | |
| <span class="term-name">const / let</span> | |
| <span class="term-category basic">基本</span> | |
| </div> | |
| <p class="term-brief">変数を宣言するキーワード</p> | |
| <div class="term-detail"> | |
| <p><strong>const</strong>: 再代入不可の変数(定数)を宣言</p> | |
| <p><strong>let</strong>: 再代入可能な変数を宣言</p> | |
| <pre><code>const name = "太郎"; // 変更不可 | |
| let age = 20; // 変更可能 | |
| age = 21; // OK</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="basic"> | |
| <div class="term-header"> | |
| <span class="term-name">テンプレートリテラル</span> | |
| <span class="term-category basic">基本</span> | |
| </div> | |
| <p class="term-brief">バッククォートで文字列を作成</p> | |
| <div class="term-detail"> | |
| <p>バッククォート(`)で囲み、<code>${}</code>で変数を埋め込める</p> | |
| <pre><code>const name = "太郎"; | |
| const msg = `こんにちは、${name}さん!`; | |
| // → "こんにちは、太郎さん!"</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="basic"> | |
| <div class="term-header"> | |
| <span class="term-name">アロー関数</span> | |
| <span class="term-category basic">基本</span> | |
| </div> | |
| <p class="term-brief">() => {} 形式の関数定義</p> | |
| <div class="term-detail"> | |
| <p>従来のfunctionより短く書ける。イベントリスナーでよく使用</p> | |
| <pre><code>// 従来の書き方 | |
| function add(a, b) { | |
| return a + b; | |
| } | |
| // アロー関数 | |
| const add = (a, b) => a + b;</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="basic"> | |
| <div class="term-header"> | |
| <span class="term-name">三項演算子</span> | |
| <span class="term-category basic">基本</span> | |
| </div> | |
| <p class="term-brief">条件 ? 真 : 偽 の形式</p> | |
| <div class="term-detail"> | |
| <p>if文を1行で書ける便利な記法</p> | |
| <pre><code>const age = 20; | |
| const status = age >= 18 ? "成人" : "未成年"; | |
| // → "成人"</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="basic"> | |
| <div class="term-header"> | |
| <span class="term-name">分割代入</span> | |
| <span class="term-category basic">基本</span> | |
| </div> | |
| <p class="term-brief">配列やオブジェクトから値を取り出す</p> | |
| <div class="term-detail"> | |
| <p>複数の値を一度に変数に代入できる</p> | |
| <pre><code>// 配列の分割代入 | |
| const [a, b] = [1, 2]; | |
| // a = 1, b = 2 | |
| // オブジェクトの分割代入 | |
| const { name, age } = { name: "太郎", age: 20 }; | |
| // name = "太郎", age = 20</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="basic"> | |
| <div class="term-header"> | |
| <span class="term-name">スプレッド構文</span> | |
| <span class="term-category basic">基本</span> | |
| </div> | |
| <p class="term-brief">...を使って配列や引数を展開</p> | |
| <div class="term-detail"> | |
| <p>配列のコピーや結合、関数の可変長引数に使用</p> | |
| <pre><code>const arr1 = [1, 2]; | |
| const arr2 = [...arr1, 3, 4]; | |
| // → [1, 2, 3, 4] | |
| function sum(...nums) { | |
| return nums.reduce((a, b) => a + b); | |
| }</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <!-- 配列 --> | |
| <div class="term-card" data-category="array"> | |
| <div class="term-header"> | |
| <span class="term-name">配列 (Array)</span> | |
| <span class="term-category array">配列</span> | |
| </div> | |
| <p class="term-brief">複数の値を順序付けて格納</p> | |
| <div class="term-detail"> | |
| <p>[]で作成。インデックスは0から始まる</p> | |
| <pre><code>const fruits = ["りんご", "みかん", "ぶどう"]; | |
| console.log(fruits[0]); // "りんご" | |
| console.log(fruits.length); // 3</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="array"> | |
| <div class="term-header"> | |
| <span class="term-name">forEach</span> | |
| <span class="term-category array">配列</span> | |
| </div> | |
| <p class="term-brief">配列の各要素に処理を実行</p> | |
| <div class="term-detail"> | |
| <p>for文より直感的に書ける。戻り値はない</p> | |
| <pre><code>const nums = [1, 2, 3]; | |
| nums.forEach((n, i) => { | |
| console.log(`${i}: ${n}`); | |
| }); | |
| // 0: 1 | |
| // 1: 2 | |
| // 2: 3</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="array"> | |
| <div class="term-header"> | |
| <span class="term-name">map</span> | |
| <span class="term-category array">配列</span> | |
| </div> | |
| <p class="term-brief">配列を変換して新しい配列を返す</p> | |
| <div class="term-detail"> | |
| <p>元の配列は変更せず、新しい配列を生成</p> | |
| <pre><code>const nums = [1, 2, 3]; | |
| const doubled = nums.map(n => n * 2); | |
| // → [2, 4, 6]</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="array"> | |
| <div class="term-header"> | |
| <span class="term-name">filter</span> | |
| <span class="term-category array">配列</span> | |
| </div> | |
| <p class="term-brief">条件に合う要素だけを抽出</p> | |
| <div class="term-detail"> | |
| <p>trueを返した要素だけで新しい配列を作成</p> | |
| <pre><code>const nums = [1, 2, 3, 4, 5]; | |
| const evens = nums.filter(n => n % 2 === 0); | |
| // → [2, 4]</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="array"> | |
| <div class="term-header"> | |
| <span class="term-name">Set</span> | |
| <span class="term-category array">配列</span> | |
| </div> | |
| <p class="term-brief">重複のない値の集合</p> | |
| <div class="term-detail"> | |
| <p>配列から重複を削除するのに便利</p> | |
| <pre><code>const arr = [1, 2, 2, 3, 3, 3]; | |
| const unique = [...new Set(arr)]; | |
| // → [1, 2, 3]</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <!-- DOM --> | |
| <div class="term-card" data-category="dom"> | |
| <div class="term-header"> | |
| <span class="term-name">DOM</span> | |
| <span class="term-category dom">DOM</span> | |
| </div> | |
| <p class="term-brief">Document Object Model</p> | |
| <div class="term-detail"> | |
| <p>HTMLをJavaScriptで操作するための仕組み。ページの要素を取得・変更できる</p> | |
| <pre><code>// 要素の取得 | |
| const el = document.getElementById("myId"); | |
| // 内容の変更 | |
| el.textContent = "新しいテキスト";</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="dom"> | |
| <div class="term-header"> | |
| <span class="term-name">addEventListener</span> | |
| <span class="term-category dom">DOM</span> | |
| </div> | |
| <p class="term-brief">イベントリスナーを登録</p> | |
| <div class="term-detail"> | |
| <p>クリックやキー入力などのイベントを監視</p> | |
| <pre><code>const btn = document.getElementById("btn"); | |
| btn.addEventListener("click", () => { | |
| alert("クリックされました!"); | |
| });</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="dom"> | |
| <div class="term-header"> | |
| <span class="term-name">createElement</span> | |
| <span class="term-category dom">DOM</span> | |
| </div> | |
| <p class="term-brief">新しいHTML要素を動的に作成</p> | |
| <div class="term-detail"> | |
| <p>JavaScriptでHTML要素を生成する</p> | |
| <pre><code>const div = document.createElement("div"); | |
| div.textContent = "動的に作成"; | |
| div.className = "my-class"; | |
| document.body.appendChild(div);</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="dom"> | |
| <div class="term-header"> | |
| <span class="term-name">textContent vs innerHTML</span> | |
| <span class="term-category dom">DOM</span> | |
| </div> | |
| <p class="term-brief">テキストとHTMLの違い</p> | |
| <div class="term-detail"> | |
| <p><strong>textContent</strong>: 安全(XSS対策)<br> | |
| <strong>innerHTML</strong>: HTMLとして解釈(危険な場合あり) | |
| </p> | |
| <pre><code>// 安全 | |
| el.textContent = "<script>alert('!')</script>"; | |
| // → そのまま文字として表示 | |
| // 危険(ユーザー入力には使わない) | |
| el.innerHTML = "<b>太字</b>"; | |
| // → HTMLとして解釈される</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <!-- 非同期 --> | |
| <div class="term-card" data-category="async"> | |
| <div class="term-header"> | |
| <span class="term-name">setTimeout</span> | |
| <span class="term-category async">非同期</span> | |
| </div> | |
| <p class="term-brief">指定時間後に処理を実行</p> | |
| <div class="term-detail"> | |
| <p>ミリ秒単位で遅延実行</p> | |
| <pre><code>setTimeout(() => { | |
| console.log("2秒後に実行"); | |
| }, 2000); | |
| console.log("これが先に実行される");</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="async"> | |
| <div class="term-header"> | |
| <span class="term-name">Promise</span> | |
| <span class="term-category async">非同期</span> | |
| </div> | |
| <p class="term-brief">非同期処理の結果を表すオブジェクト</p> | |
| <div class="term-detail"> | |
| <p>成功(resolve)か失敗(reject)の状態を持つ</p> | |
| <pre><code>const promise = new Promise((resolve, reject) => { | |
| if (成功) { | |
| resolve("成功データ"); | |
| } else { | |
| reject("エラー"); | |
| } | |
| }); | |
| promise | |
| .then(data => console.log(data)) | |
| .catch(err => console.error(err));</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="async"> | |
| <div class="term-header"> | |
| <span class="term-name">async / await</span> | |
| <span class="term-category async">非同期</span> | |
| </div> | |
| <p class="term-brief">Promiseをシンプルに書く</p> | |
| <div class="term-detail"> | |
| <p>非同期処理を同期的に書けるシンタックスシュガー</p> | |
| <pre><code>async function getData() { | |
| try { | |
| const res = await fetch(url); | |
| const data = await res.json(); | |
| return data; | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| }</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="async"> | |
| <div class="term-header"> | |
| <span class="term-name">fetch</span> | |
| <span class="term-category async">非同期</span> | |
| </div> | |
| <p class="term-brief">HTTPリクエストを送信</p> | |
| <div class="term-detail"> | |
| <p>Web APIからデータを取得する標準的な方法</p> | |
| <pre><code>const response = await fetch("https://api.example.com/data"); | |
| const data = await response.json(); | |
| // POSTリクエスト | |
| await fetch(url, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ key: "value" }) | |
| });</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <!-- ストレージ --> | |
| <div class="term-card" data-category="storage"> | |
| <div class="term-header"> | |
| <span class="term-name">localStorage</span> | |
| <span class="term-category storage">ストレージ</span> | |
| </div> | |
| <p class="term-brief">ブラウザにデータを永続保存</p> | |
| <div class="term-detail"> | |
| <p>ページを閉じてもデータが残る。文字列のみ保存可能</p> | |
| <pre><code>// 保存 | |
| localStorage.setItem("key", "value"); | |
| // 取得 | |
| const value = localStorage.getItem("key"); | |
| // 削除 | |
| localStorage.removeItem("key"); | |
| // オブジェクトの保存 | |
| localStorage.setItem("obj", JSON.stringify({ a: 1 })); | |
| const obj = JSON.parse(localStorage.getItem("obj"));</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <div class="term-card" data-category="storage"> | |
| <div class="term-header"> | |
| <span class="term-name">JSON</span> | |
| <span class="term-category storage">ストレージ</span> | |
| </div> | |
| <p class="term-brief">JavaScript Object Notation</p> | |
| <div class="term-detail"> | |
| <p>データ交換フォーマット。文字列とオブジェクトを相互変換</p> | |
| <pre><code>// オブジェクト → 文字列 | |
| const str = JSON.stringify({ name: "太郎", age: 20 }); | |
| // → '{"name":"太郎","age":20}' | |
| // 文字列 → オブジェクト | |
| const obj = JSON.parse('{"name":"太郎","age":20}'); | |
| // → { name: "太郎", age: 20 }</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <!-- Cookie --> | |
| <div class="term-card" data-category="storage"> | |
| <div class="term-header"> | |
| <span class="term-name">Cookie</span> | |
| <span class="term-category storage">ストレージ</span> | |
| </div> | |
| <p class="term-brief">サーバーとやり取りする小さなデータ</p> | |
| <div class="term-detail"> | |
| <p>有効期限を設定可能。HTTPリクエストに自動で送信される。4KB程度の容量制限あり</p> | |
| <pre><code>// Cookieの設定(有効期限7日) | |
| document.cookie = "username=太郎; max-age=" + (7*24*60*60); | |
| // Cookieの取得(全てのCookieが文字列で返る) | |
| console.log(document.cookie); | |
| // → "username=太郎; theme=dark" | |
| // 特定のCookieを取得する関数 | |
| function getCookie(name) { | |
| const cookies = document.cookie.split("; "); | |
| for (const c of cookies) { | |
| const [key, value] = c.split("="); | |
| if (key === name) return value; | |
| } | |
| return null; | |
| } | |
| // Cookieの削除(過去の日時を設定) | |
| document.cookie = "username=; max-age=0";</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| <!-- class --> | |
| <div class="term-card" data-category="basics"> | |
| <div class="term-header"> | |
| <span class="term-name">class</span> | |
| <span class="term-category basics">基礎構文</span> | |
| </div> | |
| <p class="term-brief">オブジェクトの設計図を定義</p> | |
| <div class="term-detail"> | |
| <p>ES6で導入。constructorで初期化、メソッドを定義してインスタンスを作成</p> | |
| <pre><code>// クラスの定義 | |
| class Person { | |
| // コンストラクタ(初期化処理) | |
| constructor(name, age) { | |
| this.name = name; | |
| this.age = age; | |
| } | |
| // メソッドの定義 | |
| greet() { | |
| return `こんにちは、${this.name}です`; | |
| } | |
| // 静的メソッド(インスタンス不要) | |
| static createAnonymous() { | |
| return new Person("匿名", 0); | |
| } | |
| } | |
| // インスタンスの作成 | |
| const taro = new Person("太郎", 20); | |
| console.log(taro.greet()); // → "こんにちは、太郎です" | |
| // 継承 | |
| class Student extends Person { | |
| constructor(name, age, grade) { | |
| super(name, age); // 親クラスのコンストラクタを呼び出し | |
| this.grade = grade; | |
| } | |
| }</code></pre> | |
| </div> | |
| <span class="term-toggle">詳細を見る ▼</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ======================================== | |
| タブ2: メソッド一覧 | |
| ======================================== --> | |
| <div class="tab-content" id="tab-methods" role="tabpanel"> | |
| <div class="accordion"> | |
| <div class="accordion-header"> | |
| <span>📝 文字列メソッド</span> | |
| <span class="accordion-icon">▼</span> | |
| </div> | |
| <div class="accordion-content"> | |
| <div class="method-table-container"> | |
| <table class="method-table"> | |
| <thead> | |
| <tr> | |
| <th>メソッド</th> | |
| <th>説明</th> | |
| <th>例</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><code>trim()</code></td> | |
| <td>前後の空白を削除</td> | |
| <td><code>" hello ".trim()</code> → <code>"hello"</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>split()</code></td> | |
| <td>文字列を分割して配列に</td> | |
| <td><code>"a,b,c".split(",")</code> → <code>["a","b","c"]</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>toLowerCase()</code></td> | |
| <td>小文字に変換</td> | |
| <td><code>"ABC".toLowerCase()</code> → <code>"abc"</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>toUpperCase()</code></td> | |
| <td>大文字に変換</td> | |
| <td><code>"abc".toUpperCase()</code> → <code>"ABC"</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>includes()</code></td> | |
| <td>文字列を含むか判定</td> | |
| <td><code>"hello".includes("ell")</code> → <code>true</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>replace()</code></td> | |
| <td>文字列を置換</td> | |
| <td><code>"hello".replace("l", "L")</code> → <code>"heLlo"</code></td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="accordion"> | |
| <div class="accordion-header"> | |
| <span>📦 配列メソッド</span> | |
| <span class="accordion-icon">▼</span> | |
| </div> | |
| <div class="accordion-content"> | |
| <div class="method-table-container"> | |
| <table class="method-table"> | |
| <thead> | |
| <tr> | |
| <th>メソッド</th> | |
| <th>説明</th> | |
| <th>元配列を変更?</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><code>push()</code></td> | |
| <td>末尾に要素を追加</td> | |
| <td>する</td> | |
| </tr> | |
| <tr> | |
| <td><code>pop()</code></td> | |
| <td>末尾の要素を削除して返す</td> | |
| <td>する</td> | |
| </tr> | |
| <tr> | |
| <td><code>shift()</code></td> | |
| <td>先頭の要素を削除して返す</td> | |
| <td>する</td> | |
| </tr> | |
| <tr> | |
| <td><code>unshift()</code></td> | |
| <td>先頭に要素を追加</td> | |
| <td>する</td> | |
| </tr> | |
| <tr> | |
| <td><code>splice()</code></td> | |
| <td>要素の追加/削除</td> | |
| <td>する</td> | |
| </tr> | |
| <tr> | |
| <td><code>slice()</code></td> | |
| <td>部分配列を取得</td> | |
| <td>しない</td> | |
| </tr> | |
| <tr> | |
| <td><code>map()</code></td> | |
| <td>各要素を変換</td> | |
| <td>しない</td> | |
| </tr> | |
| <tr> | |
| <td><code>filter()</code></td> | |
| <td>条件に合う要素を抽出</td> | |
| <td>しない</td> | |
| </tr> | |
| <tr> | |
| <td><code>forEach()</code></td> | |
| <td>各要素に処理を実行</td> | |
| <td>しない</td> | |
| </tr> | |
| <tr> | |
| <td><code>find()</code></td> | |
| <td>条件に合う最初の要素</td> | |
| <td>しない</td> | |
| </tr> | |
| <tr> | |
| <td><code>findIndex()</code></td> | |
| <td>条件に合う最初のインデックス</td> | |
| <td>しない</td> | |
| </tr> | |
| <tr> | |
| <td><code>reduce()</code></td> | |
| <td>配列を1つの値に集約</td> | |
| <td>しない</td> | |
| </tr> | |
| <tr> | |
| <td><code>join()</code></td> | |
| <td>配列を文字列に結合</td> | |
| <td>しない</td> | |
| </tr> | |
| <tr> | |
| <td><code>includes()</code></td> | |
| <td>要素が含まれるか判定</td> | |
| <td>しない</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="accordion"> | |
| <div class="accordion-header"> | |
| <span>🌐 DOM操作メソッド</span> | |
| <span class="accordion-icon">▼</span> | |
| </div> | |
| <div class="accordion-content"> | |
| <div class="method-table-container"> | |
| <table class="method-table"> | |
| <thead> | |
| <tr> | |
| <th>メソッド</th> | |
| <th>説明</th> | |
| <th>戻り値</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><code>getElementById()</code></td> | |
| <td>IDで要素を取得</td> | |
| <td>Element / null</td> | |
| </tr> | |
| <tr> | |
| <td><code>querySelector()</code></td> | |
| <td>CSSセレクタで要素を取得</td> | |
| <td>Element / null</td> | |
| </tr> | |
| <tr> | |
| <td><code>querySelectorAll()</code></td> | |
| <td>CSSセレクタで全要素を取得</td> | |
| <td>NodeList</td> | |
| </tr> | |
| <tr> | |
| <td><code>createElement()</code></td> | |
| <td>新しい要素を作成</td> | |
| <td>Element</td> | |
| </tr> | |
| <tr> | |
| <td><code>appendChild()</code></td> | |
| <td>子要素を追加</td> | |
| <td>追加した要素</td> | |
| </tr> | |
| <tr> | |
| <td><code>removeChild()</code></td> | |
| <td>子要素を削除</td> | |
| <td>削除した要素</td> | |
| </tr> | |
| <tr> | |
| <td><code>addEventListener()</code></td> | |
| <td>イベントリスナーを登録</td> | |
| <td>undefined</td> | |
| </tr> | |
| <tr> | |
| <td><code>setAttribute()</code></td> | |
| <td>属性を設定</td> | |
| <td>undefined</td> | |
| </tr> | |
| <tr> | |
| <td><code>getAttribute()</code></td> | |
| <td>属性を取得</td> | |
| <td>文字列 / null</td> | |
| </tr> | |
| <tr> | |
| <td><code>classList.add()</code></td> | |
| <td>クラスを追加</td> | |
| <td>undefined</td> | |
| </tr> | |
| <tr> | |
| <td><code>classList.remove()</code></td> | |
| <td>クラスを削除</td> | |
| <td>undefined</td> | |
| </tr> | |
| <tr> | |
| <td><code>classList.toggle()</code></td> | |
| <td>クラスを切り替え</td> | |
| <td>boolean</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="accordion"> | |
| <div class="accordion-header"> | |
| <span>🔢 数値・Mathメソッド</span> | |
| <span class="accordion-icon">▼</span> | |
| </div> | |
| <div class="accordion-content"> | |
| <div class="method-table-container"> | |
| <table class="method-table"> | |
| <thead> | |
| <tr> | |
| <th>メソッド</th> | |
| <th>説明</th> | |
| <th>例</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><code>Number()</code></td> | |
| <td>数値に変換</td> | |
| <td><code>Number("42")</code> → <code>42</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>parseInt()</code></td> | |
| <td>整数に変換</td> | |
| <td><code>parseInt("42.5")</code> → <code>42</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>parseFloat()</code></td> | |
| <td>小数に変換</td> | |
| <td><code>parseFloat("3.14")</code> → <code>3.14</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>Number.isFinite()</code></td> | |
| <td>有限数か判定</td> | |
| <td><code>Number.isFinite(42)</code> → <code>true</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>Math.floor()</code></td> | |
| <td>切り捨て</td> | |
| <td><code>Math.floor(3.7)</code> → <code>3</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>Math.ceil()</code></td> | |
| <td>切り上げ</td> | |
| <td><code>Math.ceil(3.2)</code> → <code>4</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>Math.round()</code></td> | |
| <td>四捨五入</td> | |
| <td><code>Math.round(3.5)</code> → <code>4</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>Math.random()</code></td> | |
| <td>0以上1未満の乱数</td> | |
| <td><code>Math.random()</code> → <code>0.xxx</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>Math.min()</code></td> | |
| <td>最小値</td> | |
| <td><code>Math.min(1, 2, 3)</code> → <code>1</code></td> | |
| </tr> | |
| <tr> | |
| <td><code>Math.max()</code></td> | |
| <td>最大値</td> | |
| <td><code>Math.max(1, 2, 3)</code> → <code>3</code></td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ======================================== | |
| タブ3: チートシート | |
| ======================================== --> | |
| <div class="tab-content" id="tab-cheatsheet" role="tabpanel"> | |
| <div class="cheatsheet-grid"> | |
| <div class="cheatsheet-card"> | |
| <div class="cheatsheet-header">🔤 変数宣言</div> | |
| <div class="cheatsheet-body"> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">定数(再代入不可)</span> | |
| <span class="cheatsheet-value">const x = 1</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">変数(再代入可)</span> | |
| <span class="cheatsheet-value">let x = 1</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">非推奨</span> | |
| <span class="cheatsheet-value">var x = 1</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="cheatsheet-card"> | |
| <div class="cheatsheet-header">📊 データ型</div> | |
| <div class="cheatsheet-body"> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">文字列</span> | |
| <span class="cheatsheet-value">"hello" / 'hello'</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">数値</span> | |
| <span class="cheatsheet-value">42 / 3.14</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">真偽値</span> | |
| <span class="cheatsheet-value">true / false</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">配列</span> | |
| <span class="cheatsheet-value">[1, 2, 3]</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">オブジェクト</span> | |
| <span class="cheatsheet-value">{a: 1, b: 2}</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">null/undefined</span> | |
| <span class="cheatsheet-value">null / undefined</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="cheatsheet-card"> | |
| <div class="cheatsheet-header">⚡ 比較演算子</div> | |
| <div class="cheatsheet-body"> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">等しい(型も比較)</span> | |
| <span class="cheatsheet-value">===</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">等しくない</span> | |
| <span class="cheatsheet-value">!==</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">より大きい</span> | |
| <span class="cheatsheet-value">></span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">以上</span> | |
| <span class="cheatsheet-value">>=</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">より小さい</span> | |
| <span class="cheatsheet-value"><</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">以下</span> | |
| <span class="cheatsheet-value"><=</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="cheatsheet-card"> | |
| <div class="cheatsheet-header">🔗 論理演算子</div> | |
| <div class="cheatsheet-body"> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">AND(かつ)</span> | |
| <span class="cheatsheet-value">&&</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">OR(または)</span> | |
| <span class="cheatsheet-value">||</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">NOT(否定)</span> | |
| <span class="cheatsheet-value">!</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">Nullish合体</span> | |
| <span class="cheatsheet-value">??</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="cheatsheet-card"> | |
| <div class="cheatsheet-header">🔁 ループ構文</div> | |
| <div class="cheatsheet-body"> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">for文</span> | |
| <span class="cheatsheet-value">for(let i=0; i<n; i++)</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">for...of</span> | |
| <span class="cheatsheet-value">for(const x of arr)</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">for...in</span> | |
| <span class="cheatsheet-value">for(const k in obj)</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">while</span> | |
| <span class="cheatsheet-value">while(条件) {...}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="cheatsheet-card"> | |
| <div class="cheatsheet-header">📥 関数定義</div> | |
| <div class="cheatsheet-body"> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">関数宣言</span> | |
| <span class="cheatsheet-value">function f() {}</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">関数式</span> | |
| <span class="cheatsheet-value">const f = function() {}</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">アロー関数</span> | |
| <span class="cheatsheet-value">const f = () => {}</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">省略形</span> | |
| <span class="cheatsheet-value">const f = x => x * 2</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="cheatsheet-card"> | |
| <div class="cheatsheet-header">🗃️ localStorage</div> | |
| <div class="cheatsheet-body"> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">保存</span> | |
| <span class="cheatsheet-value">setItem(key, val)</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">取得</span> | |
| <span class="cheatsheet-value">getItem(key)</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">削除</span> | |
| <span class="cheatsheet-value">removeItem(key)</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">全削除</span> | |
| <span class="cheatsheet-value">clear()</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="cheatsheet-card"> | |
| <div class="cheatsheet-header">🌐 fetch API</div> | |
| <div class="cheatsheet-body"> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">GET</span> | |
| <span class="cheatsheet-value">await fetch(url)</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">JSON取得</span> | |
| <span class="cheatsheet-value">await res.json()</span> | |
| </div> | |
| <div class="cheatsheet-item"> | |
| <span class="cheatsheet-key">ステータス確認</span> | |
| <span class="cheatsheet-value">res.ok / res.status</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ======================================== | |
| タブ4: 理解度チェック(クイズ) | |
| ======================================== --> | |
| <div class="tab-content" id="tab-quiz" role="tabpanel"> | |
| <div class="quiz-container" id="quizContainer"> | |
| <div class="quiz-question" id="quizQuestion">問題を読み込み中...</div> | |
| <div class="quiz-options" id="quizOptions"></div> | |
| <div class="quiz-result" id="quizResult"></div> | |
| <div class="quiz-nav"> | |
| <span class="quiz-progress" id="quizProgress">1 / 10</span> | |
| <button class="quiz-next-btn" id="quizNextBtn" disabled>次の問題 →</button> | |
| </div> | |
| </div> | |
| <div style="margin-top: 20px; padding: 16px; background: #f8fafc; border-radius: 10px;"> | |
| <h3 style="margin: 0 0 12px 0; font-size: 1rem; color: #475569;">スコア履歴</h3> | |
| <div id="quizHistory" class="muted">まだクイズを完了していません</div> | |
| </div> | |
| </div> | |
| </section> | |
| <script> | |
| // ============================================ | |
| // JavaScript入門 Ver.2.4 - メインスクリプト | |
| // ============================================ | |
| // このスクリプトは以下の機能を提供します: | |
| // 0. 初期セットアップ(コピー機能・ログ) | |
| // 1. あいさつ機能 | |
| // 2. 条件分岐デモ | |
| // 3. 繰り返しデモ | |
| // 4. TODOアプリ | |
| // 5. 計算機能 | |
| // 6. 非同期処理デモ | |
| // 7. メモ帳(永続化) | |
| // 8. 天気予報(外部API連携) | |
| // 9. 応用シャッフル | |
| // ============================================ | |
| // ========================================== | |
| // 0. 初期セットアップ (コピー機能・ログ) | |
| // ========================================== | |
| // ページ読み込み時に実行される初期化処理 | |
| // - 全ての<pre>タグにコピーボタンを追加 | |
| // - ログ機能のセットアップ | |
| // --- コードブロックへのコピーボタン追加 --- | |
| // querySelectorAll: 指定したセレクタに一致する全要素を取得 | |
| // forEach: 配列の各要素に対して関数を実行 | |
| document.querySelectorAll('pre').forEach(pre => { | |
| // 既にコピーボタンがある場合はスキップ(二重追加防止) | |
| if (pre.querySelector('.copy-btn')) return; | |
| // ボタン要素を動的に作成 | |
| // document.createElement: 新しいHTML要素を作成 | |
| const btn = document.createElement('button'); | |
| btn.className = 'copy-btn'; // CSSクラスを設定 | |
| btn.textContent = 'Copy'; // ボタンのテキスト | |
| btn.title = 'コードをクリップボードにコピー'; // ツールチップ | |
| btn.setAttribute('aria-label', 'コードをコピー'); // アクセシビリティ用 | |
| // クリックイベントリスナーを追加 | |
| // addEventListener: イベント発生時に実行する関数を登録 | |
| btn.addEventListener('click', () => { | |
| // <code>タグがあればその中身、なければpre全体のテキストを取得 | |
| // 三項演算子: 条件 ? 真の場合 : 偽の場合 | |
| const code = pre.querySelector('code') ? pre.querySelector('code').innerText : pre.innerText; | |
| // Clipboard API でテキストをコピー | |
| // navigator.clipboard.writeText: クリップボードにテキストを書き込む | |
| // .then(): 成功時の処理(Promiseチェーン) | |
| navigator.clipboard.writeText(code).then(() => { | |
| // コピー成功:ボタンテキストを一時的に変更 | |
| const originalText = btn.textContent; | |
| btn.textContent = 'Copied!'; | |
| // 2秒後に元のテキストに戻す | |
| setTimeout(() => btn.textContent = originalText, 2000); | |
| }).catch(err => { | |
| // コピー失敗時のエラーハンドリング | |
| console.error('コピー失敗:', err); | |
| // file:// プロトコルなどで動かない場合の案内を追加 | |
| alert('コピーできませんでした。ブラウザのセキュリティ制限(file://など)の可能性があります。'); | |
| }); | |
| }); | |
| // 作成したボタンをpreの子要素として追加 | |
| // appendChild: 要素を親要素の最後の子として追加 | |
| pre.appendChild(btn); | |
| }); | |
| // --- ログ表示機能 (XSS対策済み) --- | |
| // 画面内にコンソール風のログを表示する機能 | |
| // 【セキュリティ】innerHTMLを使わず、textContentで安全にテキストを表示 | |
| // DOM要素の取得 | |
| // getElementById: 指定したIDを持つ要素を1つ取得 | |
| // ログ表示エリア | |
| const logEl = document.getElementById("log"); | |
| // クリアボタン | |
| const clearLogBtn = document.getElementById("clearLogBtn"); | |
| // コピーボタン | |
| const copyLogBtn = document.getElementById("copyLogBtn"); | |
| /** | |
| * ログを画面とコンソールに出力する関数 | |
| * @param {...any} args - 出力する値(複数指定可能) | |
| * | |
| * 【使い方】 | |
| * log("メッセージ"); | |
| * log("ラベル:", { key: "value" }); | |
| * | |
| * 【スプレッド構文 ...args】 | |
| * 引数を配列として受け取る。何個でも引数を渡せる。 | |
| */ | |
| function log(...args) { | |
| // ブラウザのコンソールにも出力(開発者ツールで確認可能) | |
| console.log(...args); | |
| // 引数を文字列に変換して結合 | |
| // map: 配列の各要素を変換した新しい配列を返す | |
| // typeof: 値の型を文字列で返す("string", "number", "object"など) | |
| // JSON.stringify: オブジェクトをJSON文字列に変換 | |
| const line = args.map(a => (typeof a === "string" ? a : JSON.stringify(a))).join(" "); | |
| // 現在時刻を取得(HH:MM:SS形式) | |
| // new Date(): 現在日時のDateオブジェクトを作成 | |
| // toLocaleTimeString(): ロケールに応じた時刻文字列を返す | |
| const time = new Date().toLocaleTimeString(); | |
| // 【重要】XSS対策 | |
| // innerHTMLではなく、要素を作ってtextContentで設定する | |
| // textContent: タグを含む文字列もそのまま文字として表示(安全) | |
| // innerHTML: タグとして解釈される(危険 - 悪意あるスクリプトが実行される可能性) | |
| // ログ1行分のコンテナを作成 | |
| const row = document.createElement("div"); | |
| row.style.borderBottom = "1px solid #1e293b"; | |
| row.style.padding = "2px 0"; | |
| // 時間表示用のspan要素 | |
| const timeSpan = document.createElement("span"); | |
| // グレー色 | |
| timeSpan.style.color = "#64748b"; | |
| // テンプレートリテラルで時刻を埋め込み | |
| timeSpan.textContent = `[${time}] `; | |
| // メッセージ表示用のspan要素 | |
| // textContentを使うことで、HTMLタグが含まれていても安全に文字として表示 | |
| const msgSpan = document.createElement("span"); | |
| msgSpan.textContent = line; | |
| // 要素を組み立てる | |
| row.appendChild(timeSpan); | |
| row.appendChild(msgSpan); | |
| // ログエリアに追加 | |
| logEl.appendChild(row); | |
| // 自動スクロール:最新のログが見えるように最下部へスクロール | |
| // scrollTop: 要素のスクロール位置(上端からの距離) | |
| // scrollHeight: 要素の内容全体の高さ | |
| logEl.scrollTop = logEl.scrollHeight; | |
| } | |
| // --- ログクリアボタン --- | |
| clearLogBtn.addEventListener("click", () => { | |
| // innerHTML = "" で子要素を全て削除 | |
| logEl.innerHTML = ""; | |
| // クリアしたことをログに記録 | |
| log("LOG: cleared"); | |
| }); | |
| // --- ログコピーボタン --- | |
| copyLogBtn.addEventListener("click", () => { | |
| // innerText: 要素内の表示テキストを取得(CSSで非表示のものは含まない) | |
| const text = logEl.innerText; | |
| // クリップボードにコピー | |
| navigator.clipboard.writeText(text).then(() => { | |
| // 成功時:ボタンテキストを一時的に変更 | |
| const originalText = copyLogBtn.textContent; | |
| copyLogBtn.textContent = 'コピー完了!'; | |
| setTimeout(() => copyLogBtn.textContent = originalText, 2000); | |
| }).catch(err => { | |
| // 失敗時:エラーメッセージを表示 | |
| console.error('ログコピー失敗:', err); | |
| alert('コピーできませんでした。ブラウザのセキュリティ制限の可能性があります。'); | |
| }); | |
| }); | |
| // ========================================== | |
| // 1. あいさつ | |
| // ========================================== | |
| // 【学習ポイント】 | |
| // - 変数の宣言と代入 | |
| // - 文字列の連結(テンプレートリテラル `${}`) | |
| // - イベントリスナーの基本 | |
| // - 論理OR演算子(||)によるデフォルト値設定 | |
| // DOM要素の取得 | |
| // 名前入力欄 | |
| const nameInput = document.getElementById("nameInput"); | |
| // あいさつボタン | |
| const helloBtn = document.getElementById("helloBtn"); | |
| // 結果表示エリア | |
| const helloOut = document.getElementById("helloOut"); | |
| // ボタンクリック時の処理 | |
| helloBtn.addEventListener("click", () => { | |
| // 入力値を取得し、前後の空白を削除 | |
| // trim(): 文字列の前後の空白を削除 | |
| // ||(論理OR): 左辺がfalsy(空文字など)なら右辺を返す | |
| const name = nameInput.value.trim() || "ゲスト"; | |
| // テンプレートリテラルで文字列を作成 | |
| // バッククォート(``)で囲み、${変数}で値を埋め込む | |
| const msg = `こんにちは、${name}さん!`; | |
| // 結果を画面に表示 | |
| // テキストを設定 | |
| helloOut.textContent = msg; | |
| // CSSクラスを変更(緑色になる) | |
| helloOut.className = "status-area ok"; | |
| // ログに記録 | |
| log("HELLO:", msg); | |
| }); | |
| // ========================================== | |
| // 2. 条件分岐 | |
| // ========================================== | |
| // 【学習ポイント】 | |
| // - if文による条件分岐 | |
| // - else if による複数条件 | |
| // - 比較演算子(>=, <=, ===) | |
| // - 論理演算子(&&) | |
| // - Number()による型変換 | |
| // - 早期リターンパターン | |
| // DOM要素の取得 | |
| // 年齢入力欄 | |
| const ageInput = document.getElementById("ageInput"); | |
| // ルール選択 | |
| const ruleSelect = document.getElementById("ruleSelect"); | |
| // 判定ボタン | |
| const judgeBtn = document.getElementById("judgeBtn"); | |
| // 結果表示 | |
| const judgeOut = document.getElementById("judgeOut"); | |
| judgeBtn.addEventListener("click", () => { | |
| // 入力値を取得 | |
| const val = ageInput.value; | |
| // Number()で数値に変換 | |
| // 文字列 "18" → 数値 18 | |
| // 空文字や文字列 → NaN(Not a Number) | |
| const age = Number(val); | |
| // --- 入力バリデーション(検証) --- | |
| // 早期リターン: 不正な入力の場合は早めにreturnして処理を終了 | |
| // これにより、メインの処理が深いネストにならない | |
| // Number.isFinite(): 有限の数値かどうかを判定(NaNやInfinityはfalse) | |
| if (val === "" || !Number.isFinite(age)) { | |
| judgeOut.textContent = "年齢を数字で入力してね!"; | |
| judgeOut.className = "status-area warn"; | |
| return; // ここで関数を終了 | |
| } | |
| // 負の数チェック | |
| if (age < 0) { | |
| judgeOut.textContent = "0以上の数字にしてね"; | |
| judgeOut.className = "status-area warn"; | |
| return; | |
| } | |
| // --- メインの判定処理 --- | |
| const rule = ruleSelect.value; // "adult" または "teen" | |
| let result = ""; // letは再代入可能な変数宣言 | |
| // if-else if による条件分岐 | |
| if (rule === "adult") { | |
| // 成人判定(18歳以上) | |
| // 三項演算子: 条件 ? 真の場合 : 偽の場合 | |
| result = (age >= 18) ? "成人です (18歳以上) ✅" : "未成年です"; | |
| } else if (rule === "teen") { | |
| // ティーン判定(13〜19歳) | |
| // && (AND): 両方の条件が真のときに真 | |
| result = (age >= 13 && age <= 19) ? "ティーンです (13-19歳) ✨" : "ティーンではありません"; | |
| } | |
| // 結果を表示 | |
| judgeOut.textContent = result; | |
| judgeOut.className = "status-area ok"; | |
| // ログに記録(オブジェクト省略記法: { age, result } は { age: age, result: result } と同じ) | |
| log("JUDGE:", { age, result }); | |
| }); | |
| // ========================================== | |
| // 3. 繰り返し | |
| // ========================================== | |
| // 【学習ポイント】 | |
| // - for文による繰り返し処理 | |
| // - 配列の作成と操作 | |
| // - push()メソッドで配列に要素を追加 | |
| // - join()メソッドで配列を文字列に変換 | |
| // - Math.min()で最小値を取得(上限設定) | |
| // DOM要素の取得 | |
| // 回数入力欄 | |
| const loopN = document.getElementById("loopN"); | |
| // 実行ボタン | |
| const loopBtn = document.getElementById("loopBtn"); | |
| // 結果表示 | |
| const loopOut = document.getElementById("loopOut"); | |
| loopBtn.addEventListener("click", () => { | |
| // 入力値を数値に変換 | |
| const N = Number(loopN.value); | |
| // バリデーション | |
| if (!loopN.value || N < 1) { | |
| loopOut.textContent = "1以上の数字を入れてね"; | |
| loopOut.className = "status-area warn"; | |
| return; | |
| } | |
| // 上限を100に制限(画面が埋まりすぎるのを防ぐ) | |
| // Math.min(): 引数の中で最小の値を返す | |
| const cap = Math.min(N, 100); | |
| // 空の配列を作成 | |
| // [] は空の配列リテラル | |
| const nums = []; | |
| // for文による繰り返し | |
| // 構文: for (初期化; 条件; 更新) { 処理 } | |
| // i = 1 で開始、i <= cap の間繰り返し、毎回 i++ で1増やす | |
| for (let i = 1; i <= cap; i++) { | |
| // push(): 配列の末尾に要素を追加 | |
| nums.push(i); | |
| } | |
| // 結果を表示 | |
| // join(", "): 配列の要素を ", " で区切って文字列に変換 | |
| // [1, 2, 3].join(", ") → "1, 2, 3" | |
| loopOut.textContent = `結果:${nums.join(", ")}`; | |
| loopOut.className = "status-area ok"; | |
| // ログに記録 | |
| log("LOOP:", `1 to ${cap}`); | |
| }); | |
| // ========================================== | |
| // 4. TODOアプリ | |
| // ========================================== | |
| // 【学習ポイント】 | |
| // - 配列の操作(push, splice, forEach) | |
| // - オブジェクトの配列 | |
| // - localStorageによるデータ永続化 | |
| // - JSON.stringify / JSON.parse による変換 | |
| // - DOM要素の動的生成 | |
| // - キーボードイベント(Enter, Space) | |
| // - try-catch によるエラーハンドリング | |
| // DOM要素の取得 | |
| // 入力欄 | |
| const todoInput = document.getElementById("todoInput"); | |
| // 追加ボタン | |
| const todoAddBtn = document.getElementById("todoAddBtn"); | |
| // 全削除ボタン | |
| const todoClearAllBtn = document.getElementById("todoClearAllBtn"); | |
| // TODOリスト表示エリア | |
| const todoList = document.getElementById("todoList"); | |
| // メッセージ表示 | |
| const todoMsg = document.getElementById("todoMsg"); | |
| // localStorageのキー(一意な名前を付ける) | |
| const TODO_KEY = "js_intro_todos_v2"; | |
| // TODOを格納する配列 | |
| // 各TODOは { title: "タイトル", done: false } の形式 | |
| let todos = []; | |
| // --- ページ読み込み時のデータ復元 --- | |
| // try-catch: エラーが発生しても処理を続行できる | |
| try { | |
| // localStorageからデータを取得 | |
| // localStorage.getItem(): 保存されたデータを取得(なければnull) | |
| const saved = localStorage.getItem(TODO_KEY); | |
| if (saved) { | |
| // JSON文字列をオブジェクトに変換 | |
| // JSON.parse(): '{"a":1}' → { a: 1 } | |
| todos = JSON.parse(saved); | |
| } | |
| } catch (e) { | |
| // JSON.parseが失敗した場合などのエラーハンドリング | |
| log("TODO: Load Error", e); | |
| } | |
| /** | |
| * TODOをlocalStorageに保存する関数 | |
| */ | |
| function saveTodos() { | |
| // オブジェクトをJSON文字列に変換して保存 | |
| // JSON.stringify(): { a: 1 } → '{"a":1}' | |
| // localStorage.setItem(キー, 値): データを保存 | |
| localStorage.setItem(TODO_KEY, JSON.stringify(todos)); | |
| } | |
| /** | |
| * TODOリストを画面に描画する関数 | |
| * データが変更されるたびに呼び出す | |
| */ | |
| function renderTodos() { | |
| // リストをクリア | |
| todoList.innerHTML = ""; | |
| // 配列の各要素に対して処理を実行 | |
| // forEach(コールバック関数): 配列の各要素に関数を適用 | |
| // (t, index) => ... : t=現在の要素, index=添字(0から始まる番号) | |
| todos.forEach((t, index) => { | |
| // リスト項目を作成 | |
| const li = document.createElement("li"); | |
| // 完了済みなら "done" クラスを追加(取り消し線のスタイルが適用される) | |
| if (t.done) li.className = "done"; | |
| // --- TODOタイトル部分 --- | |
| const span = document.createElement("span"); | |
| span.textContent = t.title; | |
| // クリック可能を示すカーソル | |
| span.style.cursor = "pointer"; | |
| // 残りのスペースを全て使う | |
| span.style.flex = "1"; | |
| // アクセシビリティ属性 | |
| // チェックボックスとして認識 | |
| span.setAttribute("role", "checkbox"); | |
| // 状態を伝える | |
| span.setAttribute("aria-checked", t.done ? "true" : "false"); | |
| // キーボードフォーカス可能 | |
| span.setAttribute("tabindex", "0"); | |
| // クリックで完了/未完了を切り替え | |
| // !t.done: 論理NOT演算子(trueならfalse、falseならtrue) | |
| span.addEventListener("click", () => { | |
| // 状態を反転 | |
| t.done = !t.done; | |
| // 保存 | |
| saveTodos(); | |
| // 再描画 | |
| renderTodos(); | |
| }); | |
| // キーボード操作対応(Enter または Space) | |
| span.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter" || e.key === " ") { | |
| // デフォルトの動作を防止(スクロールなど) | |
| e.preventDefault(); | |
| t.done = !t.done; | |
| saveTodos(); | |
| renderTodos(); | |
| } | |
| }); | |
| // --- 削除ボタン --- | |
| const delBtn = document.createElement("button"); | |
| delBtn.textContent = "削除"; | |
| delBtn.className = "danger sm"; | |
| // スクリーンリーダー用 | |
| delBtn.setAttribute("aria-label", `${t.title}を削除`); | |
| delBtn.addEventListener("click", (e) => { | |
| // イベントの伝播を停止(親要素のクリックイベントを発火させない) | |
| e.stopPropagation(); | |
| // splice(開始位置, 削除数): 配列から要素を削除 | |
| // index番目から1つ削除 | |
| todos.splice(index, 1); | |
| saveTodos(); | |
| renderTodos(); | |
| log("TODO: deleted", t.title); | |
| }); | |
| // 要素を組み立ててリストに追加 | |
| li.appendChild(span); | |
| li.appendChild(delBtn); | |
| todoList.appendChild(li); | |
| }); | |
| } | |
| /** | |
| * 新しいTODOを追加する関数 | |
| */ | |
| function addTodo() { | |
| const title = todoInput.value.trim(); | |
| // 空文字チェック | |
| if (!title) { | |
| todoMsg.textContent = "文字を入力してね!"; | |
| todoMsg.className = "status-area warn"; | |
| return; | |
| } | |
| // 配列に新しいTODOを追加 | |
| // { title, done: false } は { title: title, done: false } の省略記法 | |
| todos.push({ title, done: false }); | |
| saveTodos(); | |
| renderTodos(); | |
| // 入力欄をクリア | |
| todoInput.value = ""; | |
| todoMsg.textContent = ""; | |
| log("TODO: added", title); | |
| } | |
| // --- イベントリスナーの設定 --- | |
| // 追加ボタンクリック | |
| todoAddBtn.addEventListener("click", addTodo); | |
| // Enterキーで追加 | |
| todoInput.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter") addTodo(); | |
| }); | |
| // 全削除ボタン | |
| todoClearAllBtn.addEventListener("click", () => { | |
| // confirm(): 確認ダイアログを表示(OK→true, キャンセル→false) | |
| if (!confirm("本当に全てのTODOを消しますか?")) return; | |
| // 配列を空にする | |
| todos = []; | |
| saveTodos(); | |
| renderTodos(); | |
| log("TODO: Cleared All"); | |
| }); | |
| // 初期表示 | |
| renderTodos(); | |
| // ========================================== | |
| // 5. 計算 | |
| // ========================================== | |
| // 【学習ポイント】 | |
| // - 関数の定義と呼び出し | |
| // - 引数(パラメータ)の受け渡し | |
| // - 算術演算子(+, -, *, /) | |
| // - 複数条件のバリデーション(||演算子) | |
| // DOM要素の取得 | |
| // 数値A入力欄 | |
| const aInput = document.getElementById("aInput"); | |
| // 数値B入力欄 | |
| const bInput = document.getElementById("bInput"); | |
| // 計算ボタン | |
| const sumBtn = document.getElementById("sumBtn"); | |
| // 結果表示 | |
| const sumOut = document.getElementById("sumOut"); | |
| sumBtn.addEventListener("click", () => { | |
| // 入力値を数値に変換 | |
| const a = Number(aInput.value); | |
| const b = Number(bInput.value); | |
| // バリデーション | |
| // || (OR): いずれかの条件が真なら真 | |
| // 空文字チェックと有効な数値かのチェックを組み合わせる | |
| if (aInput.value === "" || bInput.value === "" || !Number.isFinite(a) || !Number.isFinite(b)) { | |
| sumOut.textContent = "数値を入力してね"; | |
| sumOut.className = "status-area warn"; | |
| return; | |
| } | |
| // 計算結果を表示 | |
| // a + b: 数値の足し算 | |
| sumOut.textContent = `結果:${a + b}`; | |
| sumOut.className = "status-area ok"; | |
| log("SUM:", a + b); | |
| }); | |
| // ========================================== | |
| // 6. 非同期処理 | |
| // ========================================== | |
| // 【学習ポイント】 | |
| // - setTimeout()による遅延実行 | |
| // - コールバック関数の概念 | |
| // - アロー関数の書き方 () => {} | |
| // - 非同期処理中のUI状態管理(ボタン無効化) | |
| // DOM要素の取得 | |
| // 実行ボタン | |
| const timeoutBtn = document.getElementById("timeoutBtn"); | |
| // 結果表示 | |
| const timeoutOut = document.getElementById("timeoutOut"); | |
| timeoutBtn.addEventListener("click", () => { | |
| // 処理中はボタンを無効化(二重クリック防止) | |
| // disabled = true: ボタンをクリック不可にする | |
| timeoutBtn.disabled = true; | |
| // 待機中のメッセージを表示 | |
| timeoutOut.textContent = "2秒待機中..."; | |
| timeoutOut.className = "status-area muted"; | |
| // setTimeout(コールバック関数, ミリ秒) | |
| // 指定したミリ秒後にコールバック関数を実行 | |
| // 2000ミリ秒 = 2秒 | |
| // | |
| // 【重要】setTimeout は「非同期」処理 | |
| // - この行を実行した直後、処理は次の行に進む | |
| // - 2秒後に「コールバック関数」が実行される | |
| // - JavaScriptは「待たない」のが特徴 | |
| setTimeout(() => { | |
| // 2秒後に実行される処理 | |
| timeoutOut.textContent = "お待たせ!2秒経ちました ⏰"; | |
| timeoutOut.className = "status-area ok"; | |
| // ボタンを再び有効化 | |
| timeoutBtn.disabled = false; | |
| log("TIMEOUT: done"); | |
| }, 2000); | |
| // ※ この位置に log("直後") を書くと、 | |
| // "2秒待機中..." と "直後" が先に実行され、 | |
| // 2秒後に "done" が実行される | |
| }); | |
| // ========================================== | |
| // 7. メモ帳(永続化) | |
| // ========================================== | |
| // 【学習ポイント】 | |
| // - localStorage の基本操作 | |
| // - setItem(キー, 値): データを保存 | |
| // - getItem(キー): データを取得 | |
| // - removeItem(キー): データを削除 | |
| // - ページをリロードしてもデータが残る仕組み | |
| // | |
| // 【localStorageの特徴】 | |
| // - ブラウザに保存される(サーバーには送られない) | |
| // - 同じドメインでのみアクセス可能 | |
| // - 文字列のみ保存可能(オブジェクトはJSON.stringifyで変換) | |
| // - 容量は約5MB(ブラウザによる) | |
| // DOM要素の取得 | |
| // メモ入力欄 | |
| const memoInput = document.getElementById("memoInput"); | |
| // 保存ボタン | |
| const saveMemoBtn = document.getElementById("saveMemoBtn"); | |
| // 削除ボタン | |
| const clearMemoBtn = document.getElementById("clearMemoBtn"); | |
| // 状態表示 | |
| const memoStatus = document.getElementById("memoStatus"); | |
| // localStorageのキー | |
| const MEMO_KEY = "js_intro_memo"; | |
| // --- ページ読み込み時:保存データがあれば復元 --- | |
| const savedMemo = localStorage.getItem(MEMO_KEY); | |
| if (savedMemo) { | |
| // 入力欄に値を設定 | |
| memoInput.value = savedMemo; | |
| memoStatus.textContent = "保存データを復元しました ✅"; | |
| } | |
| // --- 保存ボタン --- | |
| saveMemoBtn.addEventListener("click", () => { | |
| // setItem(キー, 値): localStorageに保存 | |
| // 既に同じキーが存在する場合は上書き | |
| localStorage.setItem(MEMO_KEY, memoInput.value); | |
| memoStatus.textContent = "保存しました ✅"; | |
| memoStatus.className = "status-area ok"; | |
| log("MEMO: saved"); | |
| }); | |
| // --- 削除ボタン --- | |
| clearMemoBtn.addEventListener("click", () => { | |
| // removeItem(キー): localStorageからデータを削除 | |
| localStorage.removeItem(MEMO_KEY); | |
| // 入力欄もクリア | |
| memoInput.value = ""; | |
| memoStatus.textContent = "削除しました 🗑️"; | |
| memoStatus.className = "status-area warn"; | |
| log("MEMO: cleared"); | |
| }); | |
| // ========================================== | |
| // 8. 天気予報(外部API連携) | |
| // ========================================== | |
| // 【学習ポイント】 | |
| // - fetch API によるHTTPリクエスト | |
| // - async/await による非同期処理 | |
| // - Promiseの基本 | |
| // - try-catch-finally によるエラーハンドリング | |
| // - 外部API(Open-Meteo)との連携 | |
| // - JSONデータのパースと利用 | |
| // | |
| // 【async/await とは】 | |
| // - async: 関数を非同期関数として定義 | |
| // - await: Promiseの結果を待つ(同期的に書ける) | |
| // - 内部的にはPromiseを使っているが、書き方がシンプル | |
| // DOM要素の取得 | |
| // 都市選択 | |
| const weatherCity = document.getElementById("weatherCity"); | |
| // 取得ボタン | |
| const weatherBtn = document.getElementById("weatherBtn"); | |
| // 状態表示 | |
| const weatherOut = document.getElementById("weatherOut"); | |
| // 詳細表示エリア | |
| const weatherDetail = document.getElementById("weatherDetail"); | |
| // 天気アイコン | |
| const weatherIcon = document.getElementById("weatherIcon"); | |
| // 気温 | |
| const weatherTemp = document.getElementById("weatherTemp"); | |
| // 天気説明 | |
| const weatherDesc = document.getElementById("weatherDesc"); | |
| // 湿度 | |
| const weatherHumidity = document.getElementById("weatherHumidity"); | |
| // 風速 | |
| const weatherWind = document.getElementById("weatherWind"); | |
| /** | |
| * 天気コードからアイコンと説明を取得する関数 | |
| * @param {number} code - Open-Meteo APIの天気コード | |
| * @returns {Object} - { icon: "絵文字", desc: "説明" } | |
| * | |
| * WMO Weather interpretation codes (WW): | |
| * https://open-meteo.com/en/docs#weathervariables | |
| */ | |
| function getWeatherInfo(code) { | |
| // 天気コードと表示内容のマッピング(オブジェクト) | |
| // キー: 天気コード, 値: { icon, desc } | |
| const weatherMap = { | |
| 0: { icon: "☀️", desc: "快晴" }, | |
| 1: { icon: "🌤️", desc: "ほぼ晴れ" }, | |
| 2: { icon: "⛅", desc: "一部曇り" }, | |
| 3: { icon: "☁️", desc: "曇り" }, | |
| 45: { icon: "🌫️", desc: "霧" }, | |
| 48: { icon: "🌫️", desc: "霧氷" }, | |
| 51: { icon: "🌧️", desc: "小雨" }, | |
| 53: { icon: "🌧️", desc: "雨" }, | |
| 55: { icon: "🌧️", desc: "強い雨" }, | |
| 61: { icon: "🌧️", desc: "小雨" }, | |
| 63: { icon: "🌧️", desc: "雨" }, | |
| 65: { icon: "🌧️", desc: "大雨" }, | |
| 71: { icon: "🌨️", desc: "小雪" }, | |
| 73: { icon: "🌨️", desc: "雪" }, | |
| 75: { icon: "❄️", desc: "大雪" }, | |
| 77: { icon: "🌨️", desc: "霧雪" }, | |
| 80: { icon: "🌦️", desc: "にわか雨" }, | |
| 81: { icon: "🌦️", desc: "にわか雨" }, | |
| 82: { icon: "⛈️", desc: "激しいにわか雨" }, | |
| 85: { icon: "🌨️", desc: "にわか雪" }, | |
| 86: { icon: "🌨️", desc: "激しいにわか雪" }, | |
| 95: { icon: "⛈️", desc: "雷雨" }, | |
| 96: { icon: "⛈️", desc: "雷雨(ひょう)" }, | |
| 99: { icon: "⛈️", desc: "激しい雷雨" } | |
| }; | |
| // マッピングにない場合はデフォルト値を返す | |
| // || : 左辺がfalsyなら右辺を返す | |
| return weatherMap[code] || { icon: "❓", desc: "不明" }; | |
| } | |
| // --- 天気取得ボタン --- | |
| // async: この関数は非同期処理を含むことを宣言 | |
| weatherBtn.addEventListener("click", async () => { | |
| // 分割代入: "35.6762,139.6503" → lat="35.6762", lon="139.6503" | |
| // split(","): 文字列を "," で分割して配列にする | |
| const [lat, lon] = weatherCity.value.split(","); | |
| // 選択されたオプションのテキストを取得 | |
| // selectedIndex: 選択中のオプションのインデックス | |
| const cityName = weatherCity.options[weatherCity.selectedIndex].text; | |
| // UI状態を「取得中」に変更 | |
| weatherBtn.disabled = true; | |
| weatherOut.textContent = "取得中..."; | |
| weatherOut.className = "status-area muted"; | |
| weatherDetail.style.display = "none"; | |
| weatherDetail.classList.remove("show"); | |
| // --- try-catch-finally --- | |
| // try: エラーが発生する可能性のある処理 | |
| // catch: エラーが発生した場合の処理 | |
| // finally: 成功・失敗に関わらず必ず実行される処理 | |
| try { | |
| // Open-Meteo API のURL(無料・APIキー不要) | |
| // テンプレートリテラルでパラメータを埋め込む | |
| const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&timezone=Asia/Tokyo`; | |
| // fetch(): HTTPリクエストを送信 | |
| // await: レスポンスが返ってくるまで待機 | |
| const res = await fetch(url); | |
| // レスポンスのステータスをチェック | |
| // res.ok: ステータスコードが200番台ならtrue | |
| if (!res.ok) { | |
| // throw: エラーを発生させる(catchブロックに飛ぶ) | |
| throw new Error(`HTTP error: ${res.status}`); | |
| } | |
| // JSONをパース(文字列 → オブジェクト) | |
| // res.json() も非同期処理なので await が必要 | |
| const data = await res.json(); | |
| // APIレスポンスから現在の天気データを取得 | |
| const current = data.current; | |
| // 天気コードから表示用の情報を取得 | |
| const info = getWeatherInfo(current.weather_code); | |
| // 画面に表示 | |
| weatherIcon.textContent = info.icon; | |
| weatherTemp.textContent = `${current.temperature_2m}°C`; | |
| weatherDesc.textContent = `${cityName}:${info.desc}`; | |
| weatherHumidity.textContent = `${current.relative_humidity_2m}%`; | |
| weatherWind.textContent = `${current.wind_speed_10m} km/h`; | |
| // 詳細エリアを表示 | |
| weatherDetail.style.display = "block"; | |
| weatherDetail.classList.add("show"); | |
| weatherOut.textContent = "取得成功!"; | |
| weatherOut.className = "status-area ok"; | |
| log("WEATHER:", cityName, `${current.temperature_2m}°C`, info.desc); | |
| } catch (err) { | |
| // エラー発生時の処理 | |
| // err.message: エラーメッセージ | |
| weatherOut.textContent = `エラー: ${err.message}`; | |
| weatherOut.className = "status-area warn"; | |
| log("WEATHER ERROR:", err.message); | |
| } finally { | |
| // 成功・失敗に関わらず実行される | |
| // ボタンを再び有効化 | |
| weatherBtn.disabled = false; | |
| } | |
| }); | |
| // ========================================== | |
| // 9. 応用シャッフル(重複排除・状態保存) | |
| // ========================================== | |
| // 【学習ポイント】 | |
| // - 高度な配列操作(map, filter, split, join) | |
| // - Set を使った重複削除 | |
| // - Fisher-Yates シャッフルアルゴリズム | |
| // - 複雑な状態管理(マスターデータと現在の状態を分離) | |
| // - localStorageによる複数データの永続化 | |
| // | |
| // 【Fisher-Yates シャッフルとは】 | |
| // 配列をランダムに並び替える最も効率的なアルゴリズム | |
| // - 末尾から順に、それより前のランダムな位置の要素と交換 | |
| // - 計算量 O(n) で偏りのない完全なランダム化が可能 | |
| // localStorageのキー(2種類のデータを別々に保存) | |
| // マスターデータ(元のリスト) | |
| const SHUFFLE_MASTER_KEY = "js_intro_shuffle_master"; | |
| // 現在の状態(シャッフル後 + 除外状態) | |
| const SHUFFLE_STATE_KEY = "js_intro_shuffle_state"; | |
| // DOM要素の取得 | |
| // テキストエリア | |
| const shuffleInput = document.getElementById("shuffleInput"); | |
| // 結果リスト | |
| const shuffleList = document.getElementById("shuffleList"); | |
| // 保存ボタン | |
| const sSaveBtn = document.getElementById("shuffleSaveBtn"); | |
| // 入力欄に戻すボタン | |
| const sLoadMasterBtn = document.getElementById("shuffleLoadMasterBtn"); | |
| // シャッフル実行ボタン | |
| const sRunBtn = document.getElementById("shuffleRunBtn"); | |
| // 全削除ボタン | |
| const sClearBtn = document.getElementById("shuffleClearAllBtn"); | |
| // 入力側の状態表示 | |
| const sInStatus = document.getElementById("shuffleInputStatus"); | |
| // 結果側の状態表示 | |
| const sResStatus = document.getElementById("shuffleResultStatus"); | |
| // 現在の並び順を保持する配列 | |
| // 各要素は { text: "項目名", excluded: false } の形式 | |
| let currentShuffleItems = []; | |
| /** | |
| * 初期化関数:ページ読み込み時に前回の状態を復元 | |
| */ | |
| function initShuffle() { | |
| const savedState = localStorage.getItem(SHUFFLE_STATE_KEY); | |
| if (savedState) { | |
| currentShuffleItems = JSON.parse(savedState); | |
| // 画面に表示 | |
| renderShuffleList(); | |
| sResStatus.textContent = "前回の並び順を復元しました"; | |
| } | |
| } | |
| /** | |
| * シャッフルリストを画面に描画する関数 | |
| */ | |
| function renderShuffleList() { | |
| // リストをクリア | |
| shuffleList.innerHTML = ""; | |
| // データがない場合のメッセージ | |
| if (currentShuffleItems.length === 0) { | |
| sResStatus.textContent = "データがありません"; | |
| return; | |
| } | |
| // 各項目を描画 | |
| currentShuffleItems.forEach((item, idx) => { | |
| const li = document.createElement("li"); | |
| li.textContent = item.text; | |
| // アクセシビリティ属性(除外切替がチェックボックスとして機能) | |
| li.setAttribute("role", "checkbox"); | |
| // 除外されていない = チェックされている状態 | |
| li.setAttribute("aria-checked", item.excluded ? "false" : "true"); | |
| // キーボードフォーカス可能 | |
| li.setAttribute("tabindex", "0"); | |
| // 除外状態に応じてスタイルを変更 | |
| if (item.excluded) { | |
| // 取り消し線のスタイル | |
| li.classList.add("excluded"); | |
| li.title = "除外中 (クリックで復活)"; | |
| } else { | |
| li.title = "クリックで除外"; | |
| } | |
| // クリックで除外状態を切り替え | |
| li.addEventListener("click", () => { | |
| // 状態反転 | |
| item.excluded = !item.excluded; | |
| // 保存 | |
| saveShuffleState(); | |
| // 再描画 | |
| renderShuffleList(); | |
| }); | |
| // キーボード操作対応 | |
| li.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter" || e.key === " ") { | |
| e.preventDefault(); | |
| item.excluded = !item.excluded; | |
| saveShuffleState(); | |
| renderShuffleList(); | |
| } | |
| }); | |
| shuffleList.appendChild(li); | |
| }); | |
| } | |
| /** | |
| * 現在の状態をlocalStorageに保存 | |
| */ | |
| function saveShuffleState() { | |
| localStorage.setItem(SHUFFLE_STATE_KEY, JSON.stringify(currentShuffleItems)); | |
| } | |
| /** | |
| * 入力テキストをパースして重複を除去した配列を返す | |
| * @param {string} text - 改行区切りのテキスト | |
| * @returns {string[]} - 重複を除去した配列 | |
| */ | |
| function parseInput(text) { | |
| // 1. split("\n"): 改行で分割 → ["Aさん", "Bさん", "Cさん", ""] | |
| // 2. map(s => s.trim()): 各要素の前後空白を削除 | |
| // 3. filter(s => s !== ""): 空文字を除去 | |
| const lines = text.split("\n").map(s => s.trim()).filter(s => s !== ""); | |
| // Set を使って重複を除去 | |
| // new Set(配列): 重複のないSetオブジェクトを作成 | |
| // Array.from(Set): SetをArrayに変換 | |
| // 例: ["A", "B", "A", "C"] → Set{"A", "B", "C"} → ["A", "B", "C"] | |
| return Array.from(new Set(lines)); | |
| } | |
| // --- 保存ボタン:マスターデータとして保存 --- | |
| sSaveBtn.addEventListener("click", () => { | |
| // 入力をパースして重複削除 | |
| const uniqueLines = parseInput(shuffleInput.value); | |
| // 空チェック | |
| if (uniqueLines.length === 0) { | |
| sInStatus.textContent = "入力が空です"; | |
| sInStatus.className = "status-area warn"; | |
| return; | |
| } | |
| // マスターデータとして保存 | |
| localStorage.setItem(SHUFFLE_MASTER_KEY, JSON.stringify(uniqueLines)); | |
| sInStatus.textContent = `保存完了 (重複なし ${uniqueLines.length}件)`; | |
| sInStatus.className = "status-area ok"; | |
| log("SHUFFLE: Master saved", uniqueLines.length); | |
| }); | |
| // --- 入力欄に戻すボタン:マスターデータをテキストエリアに復元 --- | |
| sLoadMasterBtn.addEventListener("click", () => { | |
| const raw = localStorage.getItem(SHUFFLE_MASTER_KEY); | |
| if (!raw) { | |
| sInStatus.textContent = "保存されたマスターデータがありません"; | |
| return; | |
| } | |
| const lines = JSON.parse(raw); | |
| // join("\n"): 配列を改行で連結して文字列にする | |
| shuffleInput.value = lines.join("\n"); | |
| sInStatus.textContent = "入力欄に戻しました"; | |
| log("SHUFFLE: Loaded to textarea"); | |
| }); | |
| // --- シャッフル実行ボタン --- | |
| sRunBtn.addEventListener("click", () => { | |
| // マスターデータを取得 | |
| const raw = localStorage.getItem(SHUFFLE_MASTER_KEY); | |
| if (!raw) { | |
| sResStatus.textContent = "先に左側でデータを保存してください!"; | |
| sResStatus.className = "status-area warn"; | |
| return; | |
| } | |
| const lines = JSON.parse(raw); | |
| // --- Fisher-Yates シャッフルアルゴリズム --- | |
| // 配列の末尾から順に処理 | |
| for (let i = lines.length - 1; i > 0; i--) { | |
| // 0 から i までのランダムな整数を生成 | |
| // Math.random(): 0以上1未満のランダムな小数 | |
| // Math.floor(): 小数点以下を切り捨て | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| // 分割代入を使った要素の交換 | |
| // [a, b] = [b, a] で a と b の値を入れ替える | |
| [lines[i], lines[j]] = [lines[j], lines[i]]; | |
| } | |
| // シャッフル後の配列を状態オブジェクトの配列に変換 | |
| // map(): 配列の各要素を変換した新しい配列を返す | |
| currentShuffleItems = lines.map(text => ({ text, excluded: false })); | |
| // 保存して描画 | |
| saveShuffleState(); | |
| renderShuffleList(); | |
| sResStatus.textContent = "シャッフル完了!"; | |
| sResStatus.className = "status-area ok"; | |
| log("SHUFFLE: New shuffle run"); | |
| }); | |
| // --- 全削除ボタン --- | |
| sClearBtn.addEventListener("click", () => { | |
| // 確認ダイアログ | |
| if (!confirm("保存データと現在の並び順、すべて削除しますか?")) return; | |
| // 両方のデータを削除 | |
| localStorage.removeItem(SHUFFLE_MASTER_KEY); | |
| localStorage.removeItem(SHUFFLE_STATE_KEY); | |
| // 画面をクリア | |
| shuffleInput.value = ""; | |
| currentShuffleItems = []; | |
| renderShuffleList(); | |
| sInStatus.textContent = "全削除しました"; | |
| sResStatus.textContent = ""; | |
| log("SHUFFLE: All cleared"); | |
| }); | |
| // --- 初期化を実行 --- | |
| initShuffle(); | |
| // ========================================== | |
| // セクション8: Cookie体験 | |
| // ========================================== | |
| // | |
| // 【学習内容】 | |
| // - document.cookie の読み書き | |
| // - Cookie の有効期限設定(max-age) | |
| // - Cookie の取得・削除方法 | |
| // - localStorageとの違い | |
| // | |
| // 【Cookieとは】 | |
| // ブラウザに保存される小さなテキストデータ(約4KB)。 | |
| // HTTPリクエスト時にサーバーへ自動送信されるため、 | |
| // ログイン状態の維持やユーザー追跡に使われる。 | |
| // | |
| // 【localStorageとの主な違い】 | |
| // - Cookie: サーバーに自動送信、有効期限あり、約4KB | |
| // - localStorage: 送信されない、永続保存、約5MB | |
| // | |
| // 【document.cookie の特殊な動作】 | |
| // - 読み取り: 全Cookieが "key1=value1; key2=value2" 形式で返る | |
| // - 書き込み: 代入すると「追加/更新」される(全体置換ではない) | |
| // - 削除: max-age=0 を設定して期限切れにする | |
| // | |
| // 【Cookieの主な属性】 | |
| // - max-age: 有効期限(秒)。0で即削除 | |
| // - path: 有効なURLパス(デフォルト: 現在のパス) | |
| // - secure: HTTPS接続時のみ送信 | |
| // - SameSite: クロスサイトリクエスト制御 | |
| // --- 要素の取得 --- | |
| // getElementById: IDで要素を取得(最も高速な取得方法) | |
| const cookieNameInput = document.getElementById("cookieNameInput"); | |
| const cookieValueInput = document.getElementById("cookieValueInput"); | |
| const setCookieBtn = document.getElementById("setCookieBtn"); | |
| const getCookieBtn = document.getElementById("getCookieBtn"); | |
| const deleteCookieBtn = document.getElementById("deleteCookieBtn"); | |
| const cookieStatus = document.getElementById("cookieStatus"); | |
| /** | |
| * 特定のCookieを取得するヘルパー関数 | |
| * | |
| * 【なぜこの関数が必要か】 | |
| * document.cookie は全Cookieを1つの文字列で返すため、 | |
| * 特定のCookieを取得するには文字列を解析する必要がある。 | |
| * | |
| * 【処理の流れ】 | |
| * 1. "key1=value1; key2=value2" を "; " で分割 | |
| * 2. 各Cookieを "=" で分割して key と value に分ける | |
| * 3. 指定されたnameと一致するkeyを探す | |
| * | |
| * @param {string} name - 取得したいCookie名 | |
| * @returns {string|null} - 値が見つかれば返す、なければnull | |
| */ | |
| function getCookie(name) { | |
| // document.cookie は "key1=value1; key2=value2" 形式の文字列 | |
| // split("; "): セミコロン+スペースで分割して配列にする | |
| const cookies = document.cookie.split("; "); | |
| // for...of: 配列の各要素をループ処理 | |
| for (const cookie of cookies) { | |
| // 分割代入: "key=value" を ["key", "value"] に分割 | |
| const [key, value] = cookie.split("="); | |
| if (key === name) { | |
| // decodeURIComponent: URLエンコードされた値をデコード | |
| // 例: "%E5%A4%AA%E9%83%8E" → "太郎" | |
| return decodeURIComponent(value); | |
| } | |
| } | |
| // 見つからなかった場合はnullを返す | |
| return null; | |
| } | |
| // --- Cookieを保存ボタン --- | |
| // クリックイベントでCookieを設定 | |
| setCookieBtn.addEventListener("click", () => { | |
| // trim(): 前後の空白を削除 | |
| const name = cookieNameInput.value.trim(); | |
| const value = cookieValueInput.value.trim(); | |
| // バリデーション: Cookie名は必須 | |
| if (!name) { | |
| cookieStatus.textContent = "Cookie名を入力してください"; | |
| cookieStatus.className = "status-area warn"; | |
| return; // 早期リターンで処理を中断 | |
| } | |
| // --- Cookie設定(7日間有効)--- | |
| // max-age: 秒単位で有効期限を指定 | |
| // 7日 = 7 * 24時間 * 60分 * 60秒 = 604800秒 | |
| const maxAge = 7 * 24 * 60 * 60; | |
| // encodeURIComponent: 特殊文字をURLエンコード | |
| // 例: "太郎" → "%E5%A4%AA%E9%83%8E" | |
| // これにより日本語や特殊文字も安全に保存できる | |
| // | |
| // path=/: ドメイン全体でCookieを有効にする | |
| // 指定しないと現在のパスでのみ有効になる | |
| document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; max-age=${maxAge}; path=/`; | |
| cookieStatus.textContent = `Cookie "${name}" を保存しました(7日間有効)`; | |
| cookieStatus.className = "status-area ok"; | |
| log(`COOKIE: Set "${name}" = "${value}"`); | |
| }); | |
| // --- Cookieを取得ボタン --- | |
| getCookieBtn.addEventListener("click", () => { | |
| const name = cookieNameInput.value.trim(); | |
| // Cookie名が空の場合は全Cookie表示 | |
| if (!name) { | |
| // document.cookie: 全Cookieを文字列で取得 | |
| // || 演算子: 空文字列の場合は右辺を返す | |
| const allCookies = document.cookie || "(Cookieなし)"; | |
| cookieStatus.textContent = `全Cookie: ${allCookies}`; | |
| cookieStatus.className = "status-area muted"; | |
| log("COOKIE: Show all"); | |
| return; | |
| } | |
| // 特定のCookieを取得 | |
| const value = getCookie(name); | |
| if (value !== null) { | |
| // Cookie が見つかった | |
| cookieStatus.textContent = `"${name}" = "${value}"`; | |
| cookieStatus.className = "status-area ok"; | |
| log(`COOKIE: Get "${name}" = "${value}"`); | |
| } else { | |
| // Cookie が見つからなかった | |
| cookieStatus.textContent = `Cookie "${name}" は見つかりませんでした`; | |
| cookieStatus.className = "status-area warn"; | |
| log(`COOKIE: "${name}" not found`); | |
| } | |
| }); | |
| // --- Cookieを削除ボタン --- | |
| deleteCookieBtn.addEventListener("click", () => { | |
| const name = cookieNameInput.value.trim(); | |
| if (!name) { | |
| cookieStatus.textContent = "削除するCookie名を入力してください"; | |
| cookieStatus.className = "status-area warn"; | |
| return; | |
| } | |
| // --- Cookieの削除方法 --- | |
| // max-age=0 を設定すると即座に期限切れになり、 | |
| // ブラウザがCookieを削除する | |
| // | |
| // 注意: path も設定時と同じ値を指定する必要がある | |
| // 異なるpathで設定されたCookieは削除できない | |
| document.cookie = `${encodeURIComponent(name)}=; max-age=0; path=/`; | |
| cookieStatus.textContent = `Cookie "${name}" を削除しました`; | |
| cookieStatus.className = "status-area ok"; | |
| log(`COOKIE: Deleted "${name}"`); | |
| }); | |
| // ========================================== | |
| // セクション5: class体験 | |
| // ========================================== | |
| // | |
| // 【学習内容】 | |
| // - ES6 class構文によるオブジェクト指向プログラミング | |
| // - constructor(コンストラクタ)による初期化 | |
| // - メソッドの定義と呼び出し | |
| // - インスタンスの作成(new キーワード) | |
| // - thisキーワードの理解 | |
| // | |
| // 【classとは】 | |
| // classは「オブジェクトの設計図」。 | |
| // 同じ構造を持つオブジェクト(インスタンス)を | |
| // 効率よく作成するための仕組み。 | |
| // | |
| // 【イメージ】 | |
| // class Person = 「人」の設計図 | |
| // ↓ new Person("太郎", 20) | |
| // インスタンス = 「太郎さん」という具体的な人 | |
| // ↓ new Person("花子", 25) | |
| // インスタンス = 「花子さん」という具体的な人 | |
| // | |
| // 【classを使うメリット】 | |
| // 1. 同じ構造のオブジェクトを簡単に複数作れる | |
| // 2. データ(プロパティ)と処理(メソッド)をまとめられる | |
| // 3. コードの再利用性・保守性が高まる | |
| // | |
| // 【用語解説】 | |
| // - class: 設計図(テンプレート) | |
| // - インスタンス: classから作った具体的なオブジェクト | |
| // - constructor: new時に自動実行される初期化処理 | |
| // - メソッド: classに属する関数 | |
| // - this: そのインスタンス自身を指すキーワード | |
| // - プロパティ: インスタンスが持つデータ(this.xxx) | |
| // --- 要素の取得 --- | |
| const classNameInput = document.getElementById("classNameInput"); | |
| const classAgeInput = document.getElementById("classAgeInput"); | |
| const createPersonBtn = document.getElementById("createPersonBtn"); | |
| const greetPersonBtn = document.getElementById("greetPersonBtn"); | |
| const classStatus = document.getElementById("classStatus"); | |
| /** | |
| * Personクラス - 人を表すクラス(設計図) | |
| * | |
| * 【classの構成要素】 | |
| * 1. constructor: 初期化処理(new時に自動実行) | |
| * 2. メソッド: インスタンスが使える関数 | |
| * 3. プロパティ: this.xxx で定義するデータ | |
| */ | |
| class Person { | |
| /** | |
| * コンストラクタ - インスタンス生成時に自動で呼ばれる | |
| * | |
| * 【役割】 | |
| * - インスタンスの初期化(プロパティの設定) | |
| * - 引数を受け取り、this.xxx に格納 | |
| * | |
| * 【呼ばれるタイミング】 | |
| * new Person("太郎", 20) と書くと、 | |
| * constructor("太郎", 20) が自動実行される | |
| * | |
| * @param {string} name - 名前 | |
| * @param {number} age - 年齢 | |
| */ | |
| constructor(name, age) { | |
| // this: 「このインスタンス自身」を指す特別なキーワード | |
| // | |
| // new Person("太郎", 20) の場合: | |
| // this.name = "太郎" → このインスタンスのnameプロパティに"太郎"を代入 | |
| // this.age = 20 → このインスタンスのageプロパティに20を代入 | |
| this.name = name; | |
| this.age = age; | |
| } | |
| /** | |
| * 挨拶メソッド | |
| * | |
| * 【メソッドとは】 | |
| * classの中で定義する関数。 | |
| * インスタンスから呼び出せる: taro.greet() | |
| * | |
| * 【thisの参照先】 | |
| * メソッド内のthisは、そのメソッドを呼び出したインスタンスを指す | |
| * taro.greet() → this は taro インスタンス | |
| * | |
| * @returns {string} - 挨拶文 | |
| */ | |
| greet() { | |
| // テンプレートリテラルで文字列を組み立て | |
| // this.name, this.age でプロパティにアクセス | |
| return `こんにちは、${this.name}です。${this.age}歳です。`; | |
| } | |
| /** | |
| * 自己紹介メソッド | |
| * @returns {string} - 詳細な自己紹介 | |
| */ | |
| introduce() { | |
| return `私は${this.name}、${this.age}歳です。よろしくお願いします!`; | |
| } | |
| } | |
| // 現在のPersonインスタンスを保持する変数 | |
| // let: 再代入可能な変数(後で new Person() の結果を代入する) | |
| // null: まだインスタンスが作られていない状態を表す | |
| let currentPerson = null; | |
| // --- Personを作成ボタン --- | |
| createPersonBtn.addEventListener("click", () => { | |
| // 入力値の取得と整形 | |
| const name = classNameInput.value.trim(); | |
| // parseInt(文字列, 基数): 文字列を整数に変換 | |
| // 第2引数の10は「10進数として解釈」の意味 | |
| const age = parseInt(classAgeInput.value, 10); | |
| // バリデーション: 名前は必須 | |
| if (!name) { | |
| classStatus.textContent = "名前を入力してください"; | |
| classStatus.className = "status-area warn"; | |
| return; | |
| } | |
| // バリデーション: 年齢は0以上の数値 | |
| // isNaN(): Not a Number(数値でない)かどうかを判定 | |
| if (isNaN(age) || age < 0) { | |
| classStatus.textContent = "有効な年齢を入力してください"; | |
| classStatus.className = "status-area warn"; | |
| return; | |
| } | |
| // --- インスタンスの作成 --- | |
| // new キーワード: classから新しいインスタンスを生成 | |
| // | |
| // 【処理の流れ】 | |
| // 1. 新しい空のオブジェクトが作られる | |
| // 2. constructorが呼ばれる(引数: "太郎", 20) | |
| // 3. constructor内でthis.name, this.ageが設定される | |
| // 4. 完成したインスタンスがcurrentPersonに代入される | |
| currentPerson = new Person(name, age); | |
| classStatus.textContent = `Person "${name}"(${age}歳)を作成しました!`; | |
| classStatus.className = "status-area ok"; | |
| log(`CLASS: Created Person("${name}", ${age})`); | |
| }); | |
| // --- 挨拶させるボタン --- | |
| greetPersonBtn.addEventListener("click", () => { | |
| // インスタンスが作成されているかチェック | |
| if (!currentPerson) { | |
| classStatus.textContent = "先にPersonを作成してください"; | |
| classStatus.className = "status-area warn"; | |
| return; | |
| } | |
| // --- メソッドの呼び出し --- | |
| // インスタンス.メソッド名() でメソッドを実行 | |
| // currentPerson.greet() → Personクラスのgreet()が実行される | |
| // greet()内のthisはcurrentPersonを指す | |
| const greeting = currentPerson.greet(); | |
| classStatus.textContent = greeting; | |
| classStatus.className = "status-area ok"; | |
| // プロパティへのアクセス: インスタンス.プロパティ名 | |
| // currentPerson.name → "太郎" など | |
| log(`CLASS: ${currentPerson.name}.greet() → "${greeting}"`); | |
| }); | |
| // アプリ起動完了をログに記録 | |
| log("READY: Ver2.4 起動完了"); | |
| // ========================================== | |
| // 参考情報セクション(タブ・フィルター・クイズ) | |
| // ========================================== | |
| // 【このセクションで学べること】 | |
| // - タブUIの実装(aria属性によるアクセシビリティ対応) | |
| // - フィルター機能(data-*属性の活用) | |
| // - 検索機能(リアルタイムフィルタリング) | |
| // - アコーディオン(折りたたみパネル) | |
| // - クイズアプリ(状態管理、スコア保存) | |
| // --- タブ切り替え機能 --- | |
| // 【タブUIの仕組み】 | |
| // 1. 複数のタブボタン(.tab-btn)がある | |
| // 2. 各ボタンは data-tab 属性で対応するコンテンツを指定 | |
| // 3. クリックで全タブを非アクティブ化 → 選択タブをアクティブ化 | |
| // querySelectorAll: CSSセレクタに一致する全要素をNodeListで取得 | |
| const tabBtns = document.querySelectorAll('.tab-btn'); | |
| const tabContents = document.querySelectorAll('.tab-content'); | |
| // 各タブボタンにクリックイベントを設定 | |
| tabBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| // data-tab 属性の値を取得 | |
| // HTML: <button data-tab="glossary"> | |
| // JS: btn.dataset.tab → "glossary" | |
| const targetTab = btn.dataset.tab; | |
| // 全てのタブボタンとコンテンツを非アクティブに | |
| // forEach で全要素をループ処理 | |
| tabBtns.forEach(b => { | |
| b.classList.remove('active'); | |
| // aria-selected: 選択状態をスクリーンリーダーに伝える | |
| b.setAttribute('aria-selected', 'false'); | |
| }); | |
| tabContents.forEach(c => c.classList.remove('active')); | |
| // クリックされたタブをアクティブに | |
| btn.classList.add('active'); | |
| btn.setAttribute('aria-selected', 'true'); | |
| // テンプレートリテラルでID名を組み立て | |
| document.getElementById(`tab-${targetTab}`).classList.add('active'); | |
| }); | |
| }); | |
| // --- 用語カードの展開/折りたたみ --- | |
| // 【toggle()メソッド】 | |
| // classList.toggle(クラス名): クラスがあれば削除、なければ追加 | |
| // if文を書かずに状態を切り替えられる便利なメソッド | |
| const termCards = document.querySelectorAll('.term-card'); | |
| termCards.forEach(card => { | |
| card.addEventListener('click', () => { | |
| // expandedクラスをトグル(切り替え) | |
| card.classList.toggle('expanded'); | |
| // トグルテキストを更新 | |
| const toggle = card.querySelector('.term-toggle'); | |
| if (toggle) { | |
| // 三項演算子で表示テキストを切り替え | |
| toggle.textContent = card.classList.contains('expanded') ? '閉じる ▲' : '詳細を見る ▼'; | |
| } | |
| }); | |
| }); | |
| // --- 用語フィルター機能 --- | |
| // 【data-* 属性によるフィルタリング】 | |
| // HTML側で data-category="basic" のように分類情報を持たせ、 | |
| // JavaScript で dataset.category として参照できる | |
| // | |
| // 【メリット】 | |
| // - HTMLとロジックを分離できる | |
| // - カテゴリ追加時にHTMLを編集するだけで対応可能 | |
| const filterTags = document.querySelectorAll('.filter-tag'); | |
| const termGrid = document.getElementById('termGrid'); | |
| filterTags.forEach(tag => { | |
| tag.addEventListener('click', () => { | |
| // data-filter属性の値を取得 | |
| const filter = tag.dataset.filter; | |
| // 全フィルタータグを非アクティブにしてから、 | |
| // クリックされたものをアクティブに | |
| filterTags.forEach(t => t.classList.remove('active')); | |
| tag.classList.add('active'); | |
| // カードをフィルタリング | |
| const cards = termGrid.querySelectorAll('.term-card'); | |
| cards.forEach(card => { | |
| // 'all'なら全て表示、それ以外はカテゴリが一致するもののみ表示 | |
| if (filter === 'all' || card.dataset.category === filter) { | |
| card.style.display = 'block'; | |
| } else { | |
| card.style.display = 'none'; | |
| } | |
| }); | |
| }); | |
| }); | |
| // --- 用語検索機能 --- | |
| // 【リアルタイム検索の実装】 | |
| // 'input'イベント: キー入力のたびに発火 | |
| // toLowerCase(): 大文字小文字を無視して検索 | |
| // includes(): 文字列が含まれるかをチェック | |
| const termSearch = document.getElementById('termSearch'); | |
| if (termSearch) { | |
| // inputイベント: 値が変わるたびに発火(リアルタイム検索に最適) | |
| termSearch.addEventListener('input', (e) => { | |
| // 検索クエリを小文字に変換(大文字小文字を無視するため) | |
| const query = e.target.value.toLowerCase(); | |
| const cards = termGrid.querySelectorAll('.term-card'); | |
| cards.forEach(card => { | |
| // 用語名と概要テキストを取得して小文字化 | |
| const name = card.querySelector('.term-name').textContent.toLowerCase(); | |
| const brief = card.querySelector('.term-brief').textContent.toLowerCase(); | |
| // どちらかにクエリが含まれていれば表示 | |
| if (name.includes(query) || brief.includes(query)) { | |
| card.style.display = 'block'; | |
| } else { | |
| card.style.display = 'none'; | |
| } | |
| }); | |
| // 検索中はカテゴリフィルターをリセット | |
| if (query) { | |
| filterTags.forEach(t => t.classList.remove('active')); | |
| } else { | |
| // 検索クリア時は「すべて」を再選択 | |
| filterTags.forEach(t => { | |
| if (t.dataset.filter === 'all') t.classList.add('active'); | |
| }); | |
| } | |
| }); | |
| } | |
| // --- アコーディオン機能 --- | |
| // 【アコーディオンUIの実装パターン】 | |
| // - ヘッダーをクリックで開閉を切り替え | |
| // - toggle()で.openクラスを切り替え | |
| // - CSSで.openの有無に応じて表示/非表示を制御 | |
| const accordions = document.querySelectorAll('.accordion'); | |
| accordions.forEach(acc => { | |
| // 各アコーディオン内のヘッダー要素を取得 | |
| const header = acc.querySelector('.accordion-header'); | |
| header.addEventListener('click', () => { | |
| // openクラスをトグル | |
| // CSSで .accordion.open .accordion-content { display: block; } | |
| acc.classList.toggle('open'); | |
| }); | |
| }); | |
| // --- クイズ機能 --- | |
| // 【クイズアプリの設計】 | |
| // 1. quizData: 問題と選択肢、正解、解説を配列で管理 | |
| // 2. currentQuizIndex: 現在の問題番号を追跡 | |
| // 3. quizScore: 正解数をカウント | |
| // 4. quizAnswered: 回答済みフラグ(二重回答防止) | |
| // | |
| // 【状態管理のポイント】 | |
| // - グローバル変数で状態を保持 | |
| // - 状態変更時に画面を再描画(loadQuiz関数) | |
| // - スコア履歴はlocalStorageに保存 | |
| // クイズの問題データ(配列オブジェクト) | |
| const quizData = [ | |
| { | |
| question: "constとletの違いは何ですか?", | |
| options: [ | |
| "constは再代入不可、letは再代入可能", | |
| "constは数値用、letは文字列用", | |
| "constはグローバル、letはローカル", | |
| "違いはない" | |
| ], | |
| correct: 0, | |
| explanation: "constは定数として宣言され再代入できません。letは変更可能な変数です。" | |
| }, | |
| { | |
| question: "配列の末尾に要素を追加するメソッドは?", | |
| options: [ | |
| "add()", | |
| "push()", | |
| "append()", | |
| "insert()" | |
| ], | |
| correct: 1, | |
| explanation: "push()メソッドは配列の末尾に要素を追加します。" | |
| }, | |
| { | |
| question: "「===」と「==」の違いは?", | |
| options: [ | |
| "===は型も比較、==は値のみ比較", | |
| "===は数値のみ、==は文字列のみ", | |
| "違いはない", | |
| "===は配列用、==はオブジェクト用" | |
| ], | |
| correct: 0, | |
| explanation: "===(厳密等価)は型と値を両方比較します。==(等価)は型変換してから比較します。" | |
| }, | |
| { | |
| question: "非同期処理を待機するキーワードは?", | |
| options: [ | |
| "wait", | |
| "pause", | |
| "await", | |
| "hold" | |
| ], | |
| correct: 2, | |
| explanation: "awaitはPromiseの結果を待機します。async関数内でのみ使用可能です。" | |
| }, | |
| { | |
| question: "localStorageに保存できるデータ型は?", | |
| options: [ | |
| "オブジェクトのみ", | |
| "数値のみ", | |
| "文字列のみ", | |
| "すべての型" | |
| ], | |
| correct: 2, | |
| explanation: "localStorageは文字列のみ保存可能です。オブジェクトはJSON.stringifyで変換が必要です。" | |
| }, | |
| { | |
| question: "配列[1,2,3]を[2,4,6]に変換するメソッドは?", | |
| options: [ | |
| "filter()", | |
| "forEach()", | |
| "map()", | |
| "reduce()" | |
| ], | |
| correct: 2, | |
| explanation: "map()は各要素を変換した新しい配列を返します。例: arr.map(n => n * 2)" | |
| }, | |
| { | |
| question: "DOM要素を取得する getElementById の戻り値(要素がない場合)は?", | |
| options: [ | |
| "undefined", | |
| "null", | |
| "false", | |
| "エラー" | |
| ], | |
| correct: 1, | |
| explanation: "getElementById()は要素が見つからない場合nullを返します。" | |
| }, | |
| { | |
| question: "XSS対策として安全なのはどちら?", | |
| options: [ | |
| "innerHTML", | |
| "textContent", | |
| "どちらも同じ", | |
| "どちらも危険" | |
| ], | |
| correct: 1, | |
| explanation: "textContentはHTMLタグを文字として扱うため安全です。innerHTMLは実行される可能性があります。" | |
| }, | |
| { | |
| question: "try-catch-finally の finally ブロックはいつ実行される?", | |
| options: [ | |
| "エラーが発生した時のみ", | |
| "エラーが発生しなかった時のみ", | |
| "常に実行される", | |
| "returnがない場合のみ" | |
| ], | |
| correct: 2, | |
| explanation: "finallyブロックは成功・失敗に関わらず必ず実行されます。クリーンアップ処理に便利です。" | |
| }, | |
| { | |
| question: "配列から重複を削除するのに使えるのは?", | |
| options: [ | |
| "Array.unique()", | |
| "new Set()", | |
| "array.distinct()", | |
| "array.removeDuplicates()" | |
| ], | |
| correct: 1, | |
| explanation: "Setは重複を許さないデータ構造です。[...new Set(arr)]で重複削除できます。" | |
| } | |
| ]; | |
| // --- クイズの状態変数 --- | |
| // 現在の問題インデックス(0から始まる) | |
| let currentQuizIndex = 0; | |
| // 正解数 | |
| let quizScore = 0; | |
| // 回答済みフラグ(同じ問題で複数回答を防ぐ) | |
| let quizAnswered = false; | |
| // --- DOM要素の取得 --- | |
| // 問題文表示 | |
| const quizQuestion = document.getElementById('quizQuestion'); | |
| // 選択肢コンテナ | |
| const quizOptions = document.getElementById('quizOptions'); | |
| // 結果表示 | |
| const quizResult = document.getElementById('quizResult'); | |
| // 進捗表示(1/10など) | |
| const quizProgress = document.getElementById('quizProgress'); | |
| // 次の問題ボタン | |
| const quizNextBtn = document.getElementById('quizNextBtn'); | |
| // スコア履歴表示 | |
| const quizHistory = document.getElementById('quizHistory'); | |
| /** | |
| * クイズを読み込んで画面に表示する関数 | |
| * 問題の切り替え時に毎回呼び出される | |
| */ | |
| function loadQuiz() { | |
| // 全問終了チェック | |
| if (currentQuizIndex >= quizData.length) { | |
| // クイズ終了 → 結果表示 | |
| showQuizResult(); | |
| return; | |
| } | |
| // 現在の問題を取得 | |
| const q = quizData[currentQuizIndex]; | |
| // 問題文と進捗を更新 | |
| quizQuestion.textContent = `Q${currentQuizIndex + 1}. ${q.question}`; | |
| quizProgress.textContent = `${currentQuizIndex + 1} / ${quizData.length}`; | |
| // 選択肢エリアをリセット | |
| quizOptions.innerHTML = ''; | |
| quizResult.classList.remove('show', 'correct', 'wrong'); | |
| quizAnswered = false; | |
| quizNextBtn.disabled = true; | |
| // 各選択肢を動的に生成 | |
| q.options.forEach((opt, i) => { | |
| const optEl = document.createElement('div'); | |
| optEl.className = 'quiz-option'; | |
| optEl.textContent = opt; | |
| // アクセシビリティ属性 | |
| // role="button": クリック可能な要素であることを伝達 | |
| optEl.setAttribute('role', 'button'); | |
| // tabindex="0": キーボードでフォーカス可能に | |
| optEl.setAttribute('tabindex', '0'); | |
| // クリックイベント | |
| optEl.addEventListener('click', () => selectAnswer(i)); | |
| // キーボード操作対応 | |
| optEl.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| selectAnswer(i); | |
| } | |
| }); | |
| quizOptions.appendChild(optEl); | |
| }); | |
| } | |
| /** | |
| * 回答を選択した時の処理 | |
| * @param {number} index - 選択した選択肢のインデックス | |
| */ | |
| function selectAnswer(index) { | |
| // 既に回答済みなら処理しない(二重回答防止) | |
| if (quizAnswered) return; | |
| quizAnswered = true; | |
| const q = quizData[currentQuizIndex]; | |
| const options = quizOptions.querySelectorAll('.quiz-option'); | |
| // 全選択肢を無効化し、正解/不正解のスタイルを適用 | |
| options.forEach((opt, i) => { | |
| // pointer-events: none で追加クリックを防止 | |
| opt.classList.add('disabled'); | |
| // 正解の選択肢は緑に | |
| if (i === q.correct) { | |
| opt.classList.add('correct'); | |
| // 不正解を選んだ場合、その選択肢を赤に | |
| } else if (i === index && index !== q.correct) { | |
| opt.classList.add('wrong'); | |
| } | |
| }); | |
| // 結果表示を更新 | |
| if (index === q.correct) { | |
| quizScore++; | |
| quizResult.textContent = `正解! ${q.explanation}`; | |
| quizResult.className = 'quiz-result show correct'; | |
| } else { | |
| quizResult.textContent = `不正解... ${q.explanation}`; | |
| quizResult.className = 'quiz-result show wrong'; | |
| } | |
| // 次へボタンを有効化 | |
| quizNextBtn.disabled = false; | |
| // 最後の問題なら「結果を見る」に変更 | |
| if (currentQuizIndex >= quizData.length - 1) { | |
| quizNextBtn.textContent = '結果を見る'; | |
| } | |
| } | |
| /** | |
| * クイズ終了時の結果画面を表示 | |
| */ | |
| function showQuizResult() { | |
| // 正答率を計算(0〜100%) | |
| // Math.round(): 小数点以下を四捨五入 | |
| const percentage = Math.round((quizScore / quizData.length) * 100); | |
| // 成績に応じたメッセージとアイコンを設定 | |
| let message = ''; | |
| let emoji = ''; | |
| // if-else if チェーンで範囲判定 | |
| if (percentage >= 90) { | |
| emoji = '🎉'; | |
| message = '素晴らしい!完璧です!'; | |
| } else if (percentage >= 70) { | |
| emoji = '👍'; | |
| message = 'よくできました!'; | |
| } else if (percentage >= 50) { | |
| emoji = '📚'; | |
| message = 'もう少し復習しましょう!'; | |
| } else { | |
| emoji = '💪'; | |
| message = '基礎から見直してみましょう!'; | |
| } | |
| // 結果画面を表示 | |
| quizQuestion.textContent = `${emoji} クイズ終了!`; | |
| // innerHTML でHTMLを直接設定(ユーザー入力を含まないため安全) | |
| // テンプレートリテラルで複数行のHTMLを記述 | |
| quizOptions.innerHTML = ` | |
| <div style="text-align: center; padding: 20px;"> | |
| <div style="font-size: 3rem; margin-bottom: 16px;">${emoji}</div> | |
| <div style="font-size: 1.5rem; font-weight: bold; margin-bottom: 8px;"> | |
| ${quizScore} / ${quizData.length} 正解 (${percentage}%) | |
| </div> | |
| <div style="margin-bottom: 20px;">${message}</div> | |
| </div> | |
| `; | |
| quizResult.classList.remove('show'); | |
| quizNextBtn.textContent = 'もう一度挑戦'; | |
| quizNextBtn.disabled = false; | |
| // --- スコア履歴をlocalStorageに保存 --- | |
| // JSON.parse(... || '[]'): 保存データがなければ空配列を使用 | |
| const history = JSON.parse(localStorage.getItem('quiz_history') || '[]'); | |
| // 新しいスコアを追加 | |
| history.push({ | |
| score: quizScore, | |
| total: quizData.length, | |
| // toLocaleString(): ロケールに応じた日時文字列 | |
| date: new Date().toLocaleString() | |
| }); | |
| // 最新5件のみ保持(古いものを削除) | |
| // shift(): 配列の先頭要素を削除 | |
| if (history.length > 5) history.shift(); | |
| localStorage.setItem('quiz_history', JSON.stringify(history)); | |
| updateQuizHistory(); | |
| } | |
| /** | |
| * スコア履歴を画面に表示する関数 | |
| */ | |
| function updateQuizHistory() { | |
| const history = JSON.parse(localStorage.getItem('quiz_history') || '[]'); | |
| if (history.length === 0) { | |
| quizHistory.textContent = 'まだクイズを完了していません'; | |
| return; | |
| } | |
| // map() + join() パターン | |
| // 配列の各要素をHTMLに変換し、結合して一つの文字列に | |
| quizHistory.innerHTML = history.map(h => | |
| `<div style="padding: 4px 0; border-bottom: 1px solid #e2e8f0;"> | |
| ${h.date}: ${h.score}/${h.total}問正解 (${Math.round(h.score / h.total * 100)}%) | |
| </div>` | |
| ).join(''); // join(''): 配列を区切り文字なしで連結 | |
| } | |
| // --- 次の問題ボタン --- | |
| quizNextBtn.addEventListener('click', () => { | |
| // 最後の問題の後なら、リセットしてやり直し | |
| if (currentQuizIndex >= quizData.length - 1 && quizAnswered) { | |
| // 状態をリセット | |
| currentQuizIndex = 0; | |
| quizScore = 0; | |
| quizNextBtn.textContent = '次の問題 →'; | |
| } else { | |
| // 次の問題へ | |
| currentQuizIndex++; | |
| } | |
| loadQuiz(); | |
| }); | |
| // --- 初期化(ページ読み込み時に実行) --- | |
| loadQuiz(); // 最初の問題を表示 | |
| updateQuizHistory(); // 履歴を表示 | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment