# 機能設計書 76-カスタムウィジェット登録

## 概要

本ドキュメントは、サードパーティ製のカスタムウィジェット（コントロール・プレビュー・スキーマ）を登録する機能の設計を記述する。

### 本機能の処理概要

本機能は、グローバルレジストリの`registerWidget` APIを通じて、サードパーティ開発者が独自のウィジェットコンポーネント（コントロール・プレビュー・バリデーションスキーマ）をCMSに登録できる仕組みを提供する。登録されたウィジェットはconfig.ymlのfieldのwidget設定で使用可能になる。

**業務上の目的・背景**：Decap CMSは標準で文字列・テキスト・Markdown・数値・ブール・日時・セレクト等のウィジェットを提供するが、プロジェクト固有のカスタム入力UIが必要な場合がある。本機能によりサードパーティ開発者がカスタムウィジェットを開発・登録し、CMSの入力体験を拡張できる。

**機能の利用シーン**：
- アプリケーション初期化時にデフォルトウィジェットが一括登録される
- CMS.registerWidget() APIで外部からカスタムウィジェットを登録する
- config.ymlのfieldsでwidget名を指定して使用する

**主要な処理内容**：
1. **配列一括登録**: ウィジェットオブジェクトの配列を渡し、各要素を再帰的に登録
2. **文字列形式登録**: ウィジェット名（string）、コントロール、プレビュー、スキーマを個別に指定。コントロールに文字列を指定すると既存ウィジェットのコントロールを再利用
3. **オブジェクト形式登録**: name、controlComponent、previewComponent、schema、allowMapValue、globalStyles等をオブジェクトで一括指定
4. **ウィジェット取得**: 名前によるウィジェット検索（getWidget）、全ウィジェット一覧取得（getWidgets）、フォールバック付き解決（resolveWidget）

**関連システム・外部連携**：レジストリ（registry.js）と連携。immer（getWidgetsのproduce）に依存する。

**権限による制御**：ウィジェット登録はグローバルAPIとして公開されており、権限制御はない。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 6 | エントリ編集画面 | 参照画面 | 登録されたカスタムウィジェットのコントロールコンポーネントが入力フォームに表示される |
| 7 | エントリ新規作成画面 | 参照画面 | 新規エントリ作成時にカスタムウィジェットのコントロールが使用される |

## 機能種別

プラグイン登録 / ウィジェット拡張

## 入力仕様

### 入力パラメータ

**registerWidget（文字列形式）**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | ウィジェット名 | - |
| control | Component \| string | Yes | コントロールコンポーネント。文字列の場合は既存ウィジェットのcontrolを参照 | - |
| preview | Component | No | プレビューコンポーネント | - |
| schema | object | No | バリデーションスキーマ（デフォルト: {}） | - |

**registerWidget（オブジェクト形式）**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | ウィジェット名 | - |
| controlComponent | Component | Yes | コントロールコンポーネント | 未指定時Errorスロー |
| previewComponent | Component | No | プレビューコンポーネント | - |
| schema | object | No | バリデーションスキーマ（デフォルト: {}） | - |
| allowMapValue | boolean | No | Immutable.jsのMap値を許可するか | - |
| globalStyles | object | No | ウィジェット用グローバルスタイル | - |

**registerWidget（配列形式）**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | array | Yes | ウィジェットオブジェクトの配列 | 各要素がobjectであること |

### 入力データソース

- CMS.registerWidget() API（外部JavaScriptからの呼び出し）
- decap-cms-default-exportsパッケージ（デフォルトウィジェット一括登録）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| widget | object | { control, preview, schema, globalStyles, allowMapValue, ...options } |

### 出力先

registry.widgetsオブジェクト（インメモリ）に格納。getWidget(name)で取得可能。

## 処理フロー

### 処理シーケンス

```
1. registerWidget呼び出し
   └─ 引数の型を判定（配列/文字列/オブジェクト）
2. 配列の場合
   └─ 各要素を再帰的にregisterWidget呼び出し
3. 文字列の場合
   └─ controlが文字列なら既存ウィジェットのcontrolを参照コピー
   └─ registry.widgets[name]に{control, preview, schema}を格納
4. オブジェクトの場合
   └─ controlComponent必須チェック
   └─ 同名ウィジェット存在チェック（存在時警告）
   └─ registry.widgets[name]に全プロパティを格納
5. ウィジェット解決（resolveWidget）
   └─ 指定名 -> 'string' -> 'unknown' のフォールバックチェーン
```

### フローチャート

```mermaid
flowchart TD
    A[registerWidget 呼び出し] --> B{引数の型?}
    B -->|Array| C[各要素を再帰的にregisterWidget]
    B -->|string| D{controlの型?}
    B -->|object| E{controlComponent 指定?}
    D -->|string| F[既存ウィジェットのcontrolを参照]
    D -->|Component| G[そのまま使用]
    F --> H[registry.widgets に格納]
    G --> H
    E -->|Yes| I{同名ウィジェット存在?}
    E -->|No| J[Error: controlComponent未指定]
    I -->|Yes| K[console.warn: 上書き警告]
    I -->|No| L[registry.widgets に格納]
    K --> L
    C --> A
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-76-01 | controlComponent必須 | オブジェクト形式でcontrolComponentが未指定の場合、Errorをスロー | registerWidget(object形式)時 |
| BR-76-02 | 上書き警告 | 同一名のウィジェットが既に存在する場合、console.warnで警告し上書き | registerWidget(object形式)時 |
| BR-76-03 | 文字列参照コピー | controlに文字列を指定した場合、registry.widgets[control].controlを参照 | registerWidget(string形式)でcontrolがstring |
| BR-76-04 | フォールバック解決 | resolveWidgetはname -> 'string' -> 'unknown'の順でフォールバック | resolveWidget時 |
| BR-76-05 | 配列要素検証 | 配列形式の各要素がobjectでない場合、console.errorで警告 | registerWidget(array形式)時 |

### 計算ロジック

特になし。

## データベース操作仕様

### 操作別データベース影響一覧

本機能はインメモリレジストリへの登録であり、データベース操作は行わない。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | Error (throw) | オブジェクト形式でcontrolComponent未指定 | Widget "${name}" registered without `controlComponent` |
| - | console.error | 配列要素がobjectでない | Cannot register widget: ${widget} |
| - | console.error | 引数が配列/文字列/オブジェクトのいずれでもない | registerWidget failed, called with incorrect arguments |
| - | console.warn | 同名ウィジェットの上書き | Multiple widgets registered with name "${name}" |

### リトライ仕様

リトライは不要。

## トランザクション仕様

トランザクションは存在しない。

## パフォーマンス要件

- ウィジェット登録はO(1)のハッシュマップ操作
- getWidgets()はimmer produceで全エントリを変換するためO(n)

## セキュリティ考慮事項

- カスタムウィジェットのcontrolComponentにはReactコンポーネントが登録されるため、悪意のあるコードを含むウィジェットの登録には注意が必要
- globalStylesプロパティにより、ウィジェットがページ全体のスタイルに影響を与える可能性がある

## 備考

- registerWidgetは3つの呼び出しパターンをサポートするオーバーロード関数として実装されている
- controlに文字列を指定する「参照コピー」パターンにより、異なるプレビューを持つ同名controlの変種を容易に作成できる
- getWidgets()はimmerのproduceを使用してObject.entriesを{ name, ...value }形式の配列に変換する

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | registry.widgetsの構造（name -> { control, preview, schema, ... }のマッピング） |

**読解のコツ**: registry.widgetsはプレーンオブジェクトであり、キーがウィジェット名、値が{ control, preview, schema, globalStyles, allowMapValue, ...options }のオブジェクト。

**主要処理フロー**:
- **27行目**: widgets: {} - 空オブジェクトとして初期化

#### Step 2: registerWidgetの3パターンを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | registerWidgetの分岐ロジック |

**主要処理フロー**:
- **94行目**: registerWidget(name, control, preview, schema = {})
- **95-102行目**: 配列形式 - Array.isArrayチェック後、各要素をforEachで再帰呼び出し。非オブジェクト要素はconsole.error
- **103-107行目**: 文字列形式 - typeof name === 'string'。controlが文字列の場合、registry.widgets[control].controlを参照
- **108-137行目**: オブジェクト形式 - typeof name === 'object'。destructuringでwidgetName, controlComponent等を抽出
  - **118-123行目**: 同名ウィジェット上書き警告
  - **124-126行目**: controlComponent未指定時のErrorスロー
  - **127-134行目**: registry.widgets[widgetName]への格納
- **135-137行目**: それ以外の型 - console.errorで引数不正警告

#### Step 3: ウィジェット取得ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | getWidget, getWidgets, resolveWidgetの3つの取得パターン |

**主要処理フロー**:
- **139-141行目**: getWidget(name) - registry.widgets[name]を直接返す
- **142-146行目**: getWidgets() - immer produceでObject.entriesを{ name, ...value }配列に変換
- **147-149行目**: resolveWidget(name) - getWidget(name || 'string') || getWidget('unknown')

### プログラム呼び出し階層図

```
CMS.registerWidget() / decap-cms-default-exports
    |
    +-- registerWidget(name/array/object, control, preview, schema)
    |       |
    |       +-- [配列] forEach -> registerWidget(widget) [再帰]
    |       +-- [文字列] registry.widgets[name] = { control, preview, schema }
    |       +-- [オブジェクト] registry.widgets[widgetName] = { control, preview, schema, ... }
    |
    +-- getWidget(name) --> registry.widgets[name]
    +-- getWidgets() --> produce(Object.entries(...))
    +-- resolveWidget(name) --> getWidget(name || 'string') || getWidget('unknown')
```

### データフロー図

```
[入力]                              [処理]                          [出力]

CMS.registerWidget()        -->  registerWidget()             -->  registry.widgets
  (name, control, preview,       (型判定 + バリデーション         (インメモリ格納)
   schema)                        + レジストリ登録)
                                        |
config.yml                  -->  resolveWidget(widgetName)    -->  { control, preview, schema }
  fields[].widget設定               (フォールバック検索)            (エディタに提供)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| registry.js | `packages/decap-cms-core/src/lib/registry.js` | ソース | registerWidget/getWidget/resolveWidget実装 |
| bootstrap.js | `packages/decap-cms-core/src/bootstrap.js` | ソース | アプリ起動時のウィジェット解決 |
