Skip to content

Instantly share code, notes, and snippets.

@20m61
Last active January 21, 2026 04:16
Show Gist options
  • Select an option

  • Save 20m61/e5a6b57fe7e257eae2ec336101705d2d to your computer and use it in GitHub Desktop.

Select an option

Save 20m61/e5a6b57fe7e257eae2ec336101705d2d to your computer and use it in GitHub Desktop.
<!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項目で入力 -->
<!-- &#13;&#10; は改行コード(placeholder内で改行表示) -->
<textarea id="shuffleInput" rows="6"
placeholder="例:&#13;&#10;Aさん&#13;&#10;Bさん&#13;&#10;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 = "&lt;script&gt;alert('!')&lt;/script&gt;";
// → そのまま文字として表示
// 危険(ユーザー入力には使わない)
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">&gt;</span>
</div>
<div class="cheatsheet-item">
<span class="cheatsheet-key">以上</span>
<span class="cheatsheet-value">&gt;=</span>
</div>
<div class="cheatsheet-item">
<span class="cheatsheet-key">より小さい</span>
<span class="cheatsheet-value">&lt;</span>
</div>
<div class="cheatsheet-item">
<span class="cheatsheet-key">以下</span>
<span class="cheatsheet-value">&lt;=</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&lt;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}&current=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