# elements/

管理画面向けのカスタム要素（Web Components）を置くディレクトリ。`el.value` の getter/setter や
`change` イベントを持つ、外部（テーマ JS・プラグイン）から直接読み書きできる再利用可能な要素を提供する。
ただし unit-editor / block-editor（下記参照）は `el.value` を持たず、`el.editor` と `change` イベントの
みを提供する（内部状態の直列化コストが大きいため、今回のスコープでは見送った。詳細は「将来課題」参照）。

## 旧マーク経路（`.js-admin-*` / `dispatch/`）は廃止済み（タグ/カテゴリー/サブカテゴリー/関連エントリー）

タグ / カテゴリー / サブカテゴリー / 関連エントリーの 4 フィールドは、`.js-admin-tag-select` 等の
マーク要素を `dispatch/` が `querySelectorAll` で探して React をマウントする方式から、本カスタム
要素へ完全移行した。`dispatch-tag-select.tsx` / `dispatch-category-select.tsx` /
`dispatch-sub-category-select.tsx` / `dispatch-related-entry.tsx` と、それぞれの `.js-target` /
`.js-value` を前提にした「古いインターフェース」は削除済みで、後方互換のための分岐も残していない。
テンプレート側（`admin/include/{tag,category,sub-category}/select.html`、
`admin/entry/related-table.html`）は `<acms-tag-select>` 等のカスタム要素タグを直接出力する。

初期化はブラウザ標準の Custom Elements ライフサイクル（`connectedCallback`）に一本化されており、
`acmsAdminDelayedContents` を購読した再走査は不要になった。カスタムフィールドの追加・タブ・ダイアログ
などで動的に挿入されるマークアップも、そのマークアップに `<acms-tag-select>` 等のタグが含まれていれば
ブラウザが自動で `connectedCallback` を呼ぶため、挿入経路を問わず自動初期化される。

`<acms-block-editor-menu-type-select>`（編集設定画面のブロック type セレクト拡張、下記参照）も同じ
理由で旧 `querySelectorAll` + `acmsAdminDelayedContents` 購読方式からカスタム要素へ移行した。

## unit-editor は旧マーク経路を廃止済み、block-editor は自動アップグレードで統一

`<acms-unit-editor>` と `<acms-block-editor>` は上記 4 フィールドと同じ「属性駆動 + マウント処理の
共有関数化」の方針で追加したが、旧マーク経路の扱いは異なる。

- **unit-editor**: `#js-unit-editor` マークを走査していた `dispatch-unit-editor.tsx` は削除済み。
  このマークは `admin/_layouts/entry/edit.html`（本リポジトリ内のテンプレート 1 箇所のみ）でしか
  使われておらず、CSS・他 JS・PHP からの依存も無いことを確認済みのため、4 フィールドと同様に完全移行
  できた。テンプレートは `<acms-unit-editor>` を直接出力する。
- **unit-inplace-editor / unit-config-editor**: 同じ方針で `ACMS.Config.unitInplaceEditorMark`
  （`#js-unit-inplace-editor`）を走査していた `dispatch-unit-inplace-editor.tsx`、
  `#js-unit-config-editor` を走査していた `dispatch-unit-config-editor.tsx` も削除済み。
  それぞれ `ajax/unit/inplace/update.html` / `insert.html`、`admin/config/unit.html` /
  `admin/config/deprecated/unit.html` が `<acms-unit-inplace-editor>` / `<acms-unit-config-editor>`
  を直接出力する。旧 `#js-unit-config-editor` は `mode:loop` により同一ページに同一 ID の要素が
  複数レンダリングされていたが、タグ名ベースになったことで id 重複自体は実害のない副産物として解消
  される（各要素の `connectedCallback` は独立して発火するため元から動作に問題は無かった）。
  どちらも `onLegacyContentMount` 以外のコールバックは持たず、`<acms-unit-editor>` のような
  `editor` / `change` の公開 API も無い（フォーム送信用の hidden input は内部コンポーネント自身が
  描画する自己完結型のため）。
- **block-editor**: `.js-block-editor` は
  [公式ドキュメント](https://developer.a-blogcms.jp/document/block-editor/)で公開されている
  サイト構築者向けの仕様であり、a-blog cms 本体のテンプレートでは使われていないものの、
  `submodules/custom-field-maker`（カスタムフィールドの HTML スニペットを生成するコード生成ツール）
  が出力する HTML を経由してサイト制作者のテーマファイルや DB 設定（保存済みのカスタムユニット定義等）に
  存在する。本リポジトリの外側にあるためマーク自体は撤去できないが、初期化経路を二重に持つのではなく、
  `upgrade-legacy-marks.ts` の `upgradeLegacyBlockEditors()` が `.js-block-editor` 要素を見つけ次第
  `<acms-block-editor>` へ置換することで、以後の初期化は他のカスタム要素と同じ
  `connectedCallback` 1 本に合流する。既存のカスタムフィールド・カスタムユニットのテンプレートは
  何も変更しなくてもそのまま動き続ける。将来的には `<acms-block-editor>` を直接テンプレートに
  書くことを推奨する（`upgradeLegacyBlockEditors` を経由しない分、初期化が一手早い）。
  - `.js-block-editor` の旧構造（`data-target`/`data-html` セレクタ属性 + `data-target` が指す
    空のマウント先コンテナ div）と `<acms-block-editor>` の新構造（マウント先セレクタ属性を持たず、
    子孫の hidden input を自動検出）は異なるため、単純なタグ名の差し替えでは済まない。
    `upgradeLegacyBlockEditors()` は `data-target`/`data-html` 属性を除去し、`data-target` が
    指していた空のコンテナを取り除き、`data-html` が指していた input（値の同期先）を他の
    hidden input より先に配置してから `<acms-block-editor>` へ置換する（フォーム送信用の
    hidden input 等、それ以外の子ノードはそのまま移送する）。`data-target`/`data-html` を
    持たない要素（既に新構造で書かれたマーク）はこの変換をスキップしてそのまま移送する。
  - `upgradeLegacyBlockEditors()` は `registerAcmsElements()` の後、`admin.js` の `ACMS.Ready` 内で
    ページ読み込み時に一度（`document` 全体）呼ばれるほか、`.js-block-editor` は素の `<div>` として
    動的に挿入され得るため（カスタムフィールドの追加・タブ・ダイアログの遅延コンテンツ、
    custom-field-maker のライブプレビュー、カスタムユニット内の再初期化）、
    `acmsAdminDelayedContents` / `acmsCustomFieldMakerPreview` の購読先、および
    `features/unit-editor/unit-defs/custom/use-buildin-js.ts`（カスタムユニット内の組み込みJS
    再初期化）からもその都度呼び出す。
  - 冪等性は「置換後のタグ名（`acms-block-editor`）を見て弾く」ことに加え、「既に
    `acms-block-editor` の子孫であるか」（`closest()`）も見て弾くことで担保している。`class` は
    `connectedCallback` でマウントコンテナへ転送された後 host 側から取り除かれるが、転送先の
    マウントコンテナ自身が `ACMS.Config.blockEditorMark`（クラス名に設定している場合）と同じ
    セレクタに一致し得るため、子孫判定も必要になる。
  - `data-target` が指すコンテナに既にコンテンツがある要素は「マウント済み」とみなし置換しない
    （`features/block-editor/mount-block-editor.tsx` の二重マウント防止ガードと同じ判定）。
- **`ACMS.Config.unitEditorMark` / `blockEditorMark` の扱い**: `unitEditorMark` は参照元
  （`dispatch-unit-editor.tsx`）ごと削除したため `config.js` からも削除した。`blockEditorMark` は
  `upgradeLegacyBlockEditors()` が引き続き参照する（サイト制作者が独自のマークへ変更している
  可能性があるため）ので残している。

## 登録

`registerAcmsElements()`（`register.ts`）を呼ぶと 9 つのカスタム要素が `customElements.define` される。
`admin.js` の `ACMS.Ready(...)` 内で一度だけ呼び出している。`customElements.get()` で登録済みかを
確認してから `define` するため、誤って複数回呼び出しても例外にはならない（冪等）。

`ACMS.Ready` 内で呼ぶ理由: 各要素は `target` 属性が指す**外部**の input を DOM から解決する
（後述）。admin バンドルは `<head>` に `async defer` で読み込まれるため、パースの途中で実行され
得る。`registerAcmsElements()` をトップレベルで同期的に呼ぶと、`target` が指す input がまだ
パースされていない時点で `connectedCallback` が走り、解決に失敗する恐れがある。`ACMS.Ready` は
実質 DOMContentLoaded 相当のタイミングまで待つため、この経路であれば安全。

## 共通の設計

- **Light DOM**（Shadow DOM は使わない）: 管理画面のスタイルは `acms.css`（グローバル前提）に依存するため、
  Shadow DOM でカプセル化するとスタイルが当たらなくなる。
- **host は display: contents**: 各要素は自身に見た目の意味を持たない「マウント地点」なので、
  host 自身のボックスをレイアウトから除外し、子孫の描画結果だけが親の flex/grid 等に直接参加する
  ようにする。Shadow DOM 専用の `:host` 疑似クラスは Light DOM では使えないため、CSS で個々のタグ名を
  列挙する代わりに、`register.ts` の `registerAcmsElements()` が `ELEMENTS`（`customElements.define`
  に使う唯一の情報源）からタグ名一覧を組み立てて `<style id="acms-elements-host-display-contents">`
  を1つ注入する。新しい要素を `ELEMENTS` に追加すれば define と同時にこのスタイルにも自動で追従する。
  host に `display: contents` を当てても子孫への `color`/`font` 等の継承は通常どおり効くが、
  host 自身に `class` で背景・border 等のボックス系スタイルを当てたい場合は効かなくなる点に注意
  （ボックスとして描画されるのは子孫の実要素側のため）。
- **host には標準の class を持たせず、root-class という独自属性を実際にレンダリングされる
  要素へ転送する**: host は上記の通り `display: contents` でボックスを持たないため、host に
  標準の `class` を当てても見た目には反映されない。そのためこれらの要素は host 自身に `class`
  を持たせる設計を採らず、テンプレート側が最初から `root-class` を独自属性として書く
  （例: `<acms-block-editor root-class="acms-admin-form-box acms-admin-form-width-full">`）。
  `root` という名前は Vue.js の Fallthrough Attributes（親から渡された `class` 等を、単一
  ルート要素を持つコンポーネントの「ルート要素」へ自動マージする仕組み）に倣い、「レンダリング
  される主要素」を指す役割名としてある。将来、同じ要素内で役割の異なる別の転送先（例:
  ドロップダウンメニュー）が必要になった場合は、`root-class` とは別の役割名の属性を追加する
  （`menu-class` のような転送先ごとの専用属性を増やす設計であり、単一の `class` から複数の
  意味を汲み取る設計にはしない）。
  `element-helpers.ts` の `readForwardedHostAttributes(host, attributeNames)` が host の
  `root-class` 等の独自属性を読み取り、React の props 相当のオブジェクト（`root-class` →
  `className`）に変換する汎用ヘルパーを提供する。各カスタム要素は自身のファイル内で
  `const FORWARDED_HOST_ATTRIBUTES = ['root-class'] as const;` のように**転送したい属性名を
  個別に宣言**し、実際にレンダリングされる要素（React コンポーネントの `className` prop、または
  `<acms-block-editor>` のマウントコンテナ・`<acms-block-editor-menu-type-select>` の `<select>`
  のような直接生成する DOM ノード）へ渡す。属性ごとに着地先が異なる（tag-select 等は React
  コンポーネント自身の props、block-editor はマウントコンテナへの直接代入）ため、転送先の実装は
  要素ごとに書く必要があるが、「host のどの属性を読むか」を決める部分だけは共通ヘルパーに寄せてある。
  `root-class` は標準の `class` と違い host 自身の見た目に意味を持つ標準属性ではないため、
  読み取り後に host から取り除く必要はない（対になる remove 関数は無い）。旧設計では標準の
  `class` を転送後に host から削除していたが、グループユニットの外に出す操作等でこの要素が
  物理的に DOM 移動されると、Custom Elements の仕様により disconnectedCallback →
  connectedCallback が再発火し、1 回目の connectedCallback で `class` は既に消えているため
  2 回目以降の転送でクラスが失われる不具合があった（block-editor の枠が消えるなどの見た目の
  不具合として顕在化した）。`root-class` を host から一切取り除かない今の設計では、何度
  connectedCallback が再発火しても同じ値をそのまま読み直せるため、この不具合自体が起こり得ない。
  `id` は現状どの要素にも転送していない。tag/category/sub-category-select は既に `data-id`
  （HTML 標準の `id` と衝突するため）を「内部要素の id」として使っており、単純な `id` 転送を追加すると
  意味が重複・衝突する。`UnitConfigEditor` の `id` prop も DOM の `id` 属性とは無関係の別概念
  （エディタ/設定の識別子）のため、同様に単純な転送はできない。`id` 等を転送したくなった場合は、
  対象コンポーネントの既存 props と衝突しない新しいキーを個別に設計すること。
- **属性駆動（target 参照方式）**: 各要素は自身の内部に値を保持せず、`target`（CSS セレクタ）
  が指す**外部の実 input**（テンプレートが別途レンダリングする、フォーム送信の実体）へ選択値を
  読み書きする。旧デザイン（要素内部に `.js-target` / `.js-value` を自前生成する自己完結型）は廃止した。
- **属性は data-\* を使わない**: HTML 標準で `data-*` が要求されるのは「標準の HTML 要素（`<div>`
  等）に独自データを埋め込む」場合であり、カスタム要素自身の属性はその要素の作者が定める公開 API
  なので `data-*` である必要はない（Shoelace / Material Web 等の主要な Web Components ライブラリも
  `variant` / `size` のような非 `data-*` 属性を広く使っている）。旧マーク要素（`div` + `data-*`）
  からの移行時に `data-*` の名前をそのまま踏襲していたが、`data-プレフィックスを外した独自属性名`
  にリネーム済み（`data-target` → `target` 等）。**HTML 標準のグローバル属性と衝突する `id` /
  `title` の 2 つのみ**、意味の食い違い・既存 DOM API との衝突を避けるため `data-id` / `data-title`
  のまま維持している（下記「要素一覧」の該当箇所に個別に明記）。
- **マウント処理の共有**: 各要素の「属性を読み、target 先の input を解決し、React を
  マウントして値を同期する」ロジックは `features/{tag,category,sub-category,related-entry}/mount-*.tsx`
  の `mountXxxFromAttributes` 関数に切り出してあり、カスタム要素からのみ呼ばれる（旧 `dispatch/` は
  削除済み）。`mountXxxFromAttributes` は `HTMLElement.dataset` 互換（camelCase キー）のオブジェクトを
  第2引数に取るが、実際に渡されるのは `element-helpers.ts` の `readAttributesAsDataset()` が
  非 `data-*` 属性から組み立てた同型のオブジェクトである（`mountXxxFromAttributes` 自体は
  `data-*` 由来かどうかを区別しない）。`mountXxxField` 自体（React のマウント・アンマウント・
  再マウント）はさらにその内側の共有ロジックで、`defaultValue` 等は呼び出し時に渡された options
  から都度組み立てる。
- **属性は初期値、プロパティが正**: `value` はプロパティ経由の代入が正となる。要素が
  `customElements.define` される前にプロパティが代入された場合に備え、`connectedCallback` で
  upgrade 処理（`element-helpers.ts` の `upgradeProperty`）を行う。属性（`default-value` 等）は
  初回接続時の初期値としてのみ読む。接続前に `value` が代入された場合は `default-value` 相当
  として保持し、接続時に反映する。
- **defaultValue は再レンダリング時に都度読み直す**: `disabled` の切り替えなど、値そのものとは
  無関係な再レンダリングであっても、React 側の実装（`mountXxxField`）は Suspense の key を変えて
  丸ごと再マウントする方式のため、`defaultValue` を固定値としてキャッシュしてしまうとユーザーが
  それまでに選択した内容が初期値に巻き戻ってしまう。そのため各要素は `defaultValue` を毎回
  `target` 先 input の現在値から読み直してから `update()` を呼ぶ。
- **hidden input は要素の外にある**: tag-select / category-select / sub-category-select は
  自身の内部に hidden input を持たない。フォーム送信の実体は `target` が指す、テンプレートが
  別途レンダリングする hidden input（例: `<input id="entry-tag-value" type="hidden" name="tag" ...>`）
  であり、要素はその `value` を書き換えるだけ。related-entry だけは元から例外で、`RelatedEntries`
  コンポーネント自身が `related[]` 等の hidden input を描画する。
- **既存の CustomEvent は維持**: 選択変更時に飛ぶ `acms.tag.change` / `acms.category.change` /
  `acms.sub-category.change` / `acms.related-entry.change`（いずれも `target` 先の input や
  要素自身に `bubbles: true` で発火）は名前・タイミングとも変更していない。カスタム要素はこれらを
  検知して標準の `change` イベント（CustomEvent, bubbles）に変換して自分自身から発火する。
- **単一要素の初期化失敗はページ全体を止めない**: `target` 属性が無い／参照先の input が
  見つからない場合、`mountXxxFromAttributes` は Error を投げるが、各要素の `connectedCallback` は
  それを catch して `console.error` に落とす。Custom Elements のライフサイクルコールバックは
  そもそも呼び出し元へ同期的に例外を伝播できない（ブラウザが reaction として個別に報告する）ため、
  1 つの壊れたマークアップが他の要素の初期化を巻き込んで止めることはない。
- **disconnectedCallback は後片付け全体をマイクロタスクへ遅らせ、isConnected で「移動」と
  「本当の削除」を区別する**: これらの要素はカスタムユニットの中に埋め込まれる等で、自身を
  内包する外側の React ツリーによって物理的に DOM 移動されることがある。Custom Elements の
  仕様上、物理移動でも disconnectedCallback → connectedCallback が同期的に（同じタスク内で）
  再発火する。ここで `this.controller` を同期的に `null` にすると、再発火した
  `connectedCallback` の「既にマウント済みなら何もしない」ガード（`if (this.controller) return;`）
  が効かなくなり、まだ `unmount()` されていない旧 React root が残ったまま同じ host（あるいは
  block-editor のマウントコンテナのような子要素）に新しい root を二重に作ってしまう。加えて
  `root.unmount()` 自体を同期実行すると、外側の React コミットが完了する前に内側の root を
  畳もうとし、React が「Attempted to synchronously unmount a root while React was already
  rendering」という警告を出す。両方を避けるため、`controller` の nulling を含む後片付け全体を
  `queueMicrotask` へ遅らせ、そのコールバックの中で `this.isConnected` を見て「マイクロタスクが
  実行される前に再接続された（＝物理的な移動だった）」場合は何もせず、既存の root をそのまま
  生かす。本当に取り外された場合だけ、その時点で初めて `unmount()` と `resetMountMarker` 等の
  後片付けを行う。

  ```ts
  disconnectedCallback(): void {
    const controller = this.controller;
    queueMicrotask(() => {
      if (this.isConnected) {
        return; // 物理的な移動だった。既存の root はそのまま生きている。
      }
      controller?.unmount();
      this.controller = null;
      resetMountMarker(this);
    });
  }
  ```

  自前の React root を持たない `<acms-block-editor-menu-type-select>`（素の `<select>` を
  生成するだけ）は対象外。

## 要素一覧

属性はテンプレートが出力する独自属性（`data-*` ではない）と 1:1 対応する（`target` 以外は省略可）。
`data-id` / `data-title` の 2 つのみ、HTML 標準のグローバル属性と衝突するため `data-*` のまま。

### `<acms-tag-select>`

```html
<acms-tag-select target="[name='tag']" default-value="foo,bar"></acms-tag-select>
<input type="hidden" name="tag" value="foo,bar">
```

- `target`（必須, CSS セレクタ）— 選択結果を書き込む外部 input
- `default-value` — 初期表示の値（カンマ区切りのタグ名）
- `input-id` / `is-disabled` / `form` / `name` / `menu-portal-target` —
  内部の `TagSelect` コンポーネントへそのまま渡す表示オプション
- `data-id`（HTML 標準の `id` と衝突するため `data-*` のまま） — `TagSelect` 内部の要素 ID
- `value: string`（get/set）— target 先 input の値表現と同一
- `disabled: boolean`（get/set、`disabled` 属性と同期）
- `change` イベント

### `<acms-category-select>`

```html
<acms-category-select target="[name='category_id']" default-value="5" narrow-down="true"></acms-category-select>
<input type="hidden" name="category_id" value="5">
```

- `target`（必須, CSS セレクタ。マッチした全 input に同じ値を書き込む）
- `default-value` / `narrow-down` / `no-option` / `is-creatable` /
  `is-clearable` / `mt-option` / `input-id` / `is-disabled` /
  `form` / `name` / `menu-portal-target`
- `data-id`（HTML 標準の `id` と衝突するため `data-*` のまま） — `CategorySelect` 内部の要素 ID
- `value: string`（get/set）
- `disabled: boolean`（get/set）
- `change` イベント

### `<acms-sub-category-select>`

```html
<acms-sub-category-select
  target="[name='sub_category_id']"
  default-value="1,2"
  default-label="ラベル1,ラベル2"
  category-id="[name='category_id']"
></acms-sub-category-select>
<input type="hidden" name="sub_category_id" value="1,2">
```

- `target`（必須, CSS セレクタ）
- `default-value` / `default-label`（defaultValue の各 ID に対応するラベル、初期表示のみ）
- `category-id`（CSS セレクタ、既定値 `[name=category_id]`）— 連動するカテゴリー選択の input。
  その input が `acms.category.change` を発火すると、選択肢の絞り込みが自動で追従する。
- `input-id` / `is-disabled` / `form` / `name` / `menu-portal-target` /
  `limit`
- `value: string`（get/set）
- `disabled: boolean`（get/set）
- `change` イベント

### `<acms-related-entry-editor>`

```html
<acms-related-entry-editor
  type="news"
  data-title="関連ニュース"
  module-id="1"
  ctx="index"
  max-item="5"
  json-id="news-related-entries-json"
></acms-related-entry-editor>
<script type="text/template" id="news-related-entries-json">[...]</script>
```

```js
const el = document.querySelector('acms-related-entry-editor');
el.value = [{ id: 1, title: '...', categoryName: '...', image: '...', url: '...' }];
el.addEventListener('change', () => console.log(el.value));
```

- `json-id`（必須）— 初期 entries（JSON 配列）を持つ `<script>` の id
- `type` / `module-id` / `ctx` / `thumbnail` / `max-item`
- `data-title`（HTML 標準の `title`、ツールチップと衝突するため `data-*` のまま） — 関連エントリーの
  タイトル
- `value: RelatedEntryType[]`（get/set）— 関連エントリーの配列（`{ id, title, categoryName, image, url }`）
- `disabled` は未対応（`RelatedEntries` 自体に disabled 概念が無いため）
- `change` イベント
- フォーム送信用の hidden input（`related[]` / `related_type[]` / `loaded_realted_entries[]`）は
  `RelatedEntries` コンポーネントが描画する。要素固有の hidden input は無い。

### `<acms-unit-editor>`

```html
<acms-unit-editor default-value="[...]" settings="{...}"></acms-unit-editor>
```

```js
const el = document.querySelector('acms-unit-editor');
el.addEventListener('change', () => console.log(el.editor?.state.units));
console.log(el.editor); // unit-editor コアの Editor インスタンス（window.ACMS.unitEditor と同一）
```

- `default-value`（ユニットツリーの JSON、既定 `[]`）/ `settings`（UnitEditorSettings の
  JSON、既定は `defaultEditorSettings`）— 初期表示のみ
- `editor: Editor | null`（get のみ）— unit-editor コアの Editor インスタンス。マウント完了前は null
- `change` イベント（ユニットツリーの transaction を検知して発火）
- `value` は持たない（詳細は「将来課題」参照）
- 自身が直接マウント先になる（内部に `.acms-admin-unit-editor` ラッパーを UnitEditor コンポーネントが
  描画する。tag-select 等と異なり `target` は使わない）

### `<acms-unit-inplace-editor>`

```html
<acms-unit-inplace-editor unit-id="{id}" default-value="[...]" settings="{...}"></acms-unit-inplace-editor>
```

- `unit-id`（対象ユニットのID、既定 `''`）/ `default-value`（ユニットツリーの JSON、既定
  `[]`）/ `settings`（UnitEditorSettings の JSON、既定は `defaultEditorSettings`）— 初期表示のみ
- 外部 API・`change` イベントは無い（フォーム送信用の hidden input は InplaceEditor 自身が描画する）
- 自身が直接マウント先になる（`target` は使わない）
- ダイレクト編集ダイアログが開かれるたびに Ajax レスポンス（`ajax/unit/inplace/update.html` /
  `insert.html`）としてこのタグが挿入されるため、`acmsInplaceDialogOpen` を購読して明示的に
  dispatch する必要は無い（ブラウザの Custom Elements ライフサイクルが自動で `connectedCallback` を
  呼ぶ）

### `<acms-unit-config-editor>`

```html
<acms-unit-config-editor data-id="insert" label="新規エントリー作成" default-value="[...]" settings="{...}"></acms-unit-config-editor>
```

- `data-id`（必須, HTML 標準の `id` と衝突するため `data-*` のまま）/ `label`（既定 `''`）/
  `default-value`（UnitConfigList の JSON、既定 `[]`）/ `settings`（UnitConfigEditorSettings の
  JSON、既定は `defaultConfigEditorSettings`）
- 外部 API・`change` イベントは無い（フォーム送信用の hidden input は UnitConfigEditor 自身が描画する）
- 自身が直接マウント先になる（`target` は使わない）
- コンフィグ画面（新規エントリー作成 / ユニット追加ごと）に応じて同一ページに複数個レンダリングされる
  想定（旧 `#js-unit-config-editor` は `mode:loop` により同一 ID で複数出力されていた）。それぞれ独立
  した `connectedCallback` でマウントされる

### `<acms-block-editor>`

```html
<acms-block-editor settings="{...}">
  <input type="hidden" name="body" value="{body}">
</acms-block-editor>
```

```js
const el = document.querySelector('acms-block-editor');
el.addEventListener('change', () => console.log(el.editor?.getHTML()));
console.log(el.editor); // tiptap の Editor インスタンス
```

- 自身が直接マウント先になる（`target` のようなマウント先セレクタ属性は持たない。
  `<acms-unit-editor>` と同じ設計）
- 子孫で最初に見つかった `input[type="hidden"]` が HTML を同期する対象になる（`html` のような
  セレクタ属性は持たない。値は input の現在値を defaultValue として使う）
- `settings` 等その他の属性は BlockEditor コンポーネントへそのまま渡す（`attributesToProps` 経由。
  data-* ではない独自属性で、`<acms-unit-editor>` 等と同じ設計）
- `editor: Editor | null`（get のみ）— tiptap の Editor インスタンス。マウント完了前は null
- `change` イベント（hidden input への同期完了後に発火）
- `value` は持たない（詳細は「将来課題」参照）
- 子孫に hidden input 以外のコンテンツが既にある場合はマウントしない（二重マウント防止。
  `upgradeLegacyBlockEditors()` の置換前ガードも同じ判定を使う）
- React は自身ではなく、自身の中に動的に追加する専用のマウントコンテナへレンダリングする。
  自身を React のマウント先にすると、テンプレートが用意した hidden input が React のレンダリング
  結果で上書きされて消えてしまうため（`<acms-unit-editor>` のようにコンポーネント自身が
  hidden input を描画する自己完結型設計とは異なり、この要素は外部が用意した hidden input を
  読み書きする設計のため）
- `.js-block-editor`（[公式ドキュメント](https://developer.a-blogcms.jp/document/block-editor/)で
  公開されているサイト構築者向け仕様。`submodules/custom-field-maker` が生成する既存 HTML・
  サイト制作者のテーマ・DB 設定に存在）は、`data-target`（マウント先コンテナのセレクタ）と
  `data-html`（同期先 input のセレクタ）+ 専用のマウント先コンテナ div という旧構造のままだが、
  `upgradeLegacyBlockEditors()` がこの旧構造を検出すると、マウント先コンテナ div を除去し
  `data-target`/`data-html` を持たない新構造へ変換したうえで `<acms-block-editor>` へ置換する。
  この変換時、`data-target`/`data-html` 以外の `data-*`（公開仕様の `data-settings` を含む）は
  `data-` を外した素の属性名（`settings` 等）へ付け替えて移送する（`<acms-block-editor>` 自身の
  属性は data-* ではなく素の属性が公開 API のため）。既存のカスタムフィールド・カスタムユニット
  テンプレートは `.js-block-editor` のままで構わない。新規テンプレートでは `<acms-block-editor>` を
  直接、上記のシンプルな形で書くことを推奨する。
  詳細は上記「unit-editor は旧マーク経路を廃止済み、block-editor は自動アップグレードで統一」を参照

### `<acms-block-editor-menu-type-select>`

```html
<acms-block-editor-menu-type-select value="callout">
  <select name="block_editor_block_menu_type[]">
    <option value="paragraph">段落</option>
    ...
  </select>
</acms-block-editor-menu-type-select>
```

- `value`（保存済みの type。省略可） — ハードコードされた `<option>` に無い type（プラグイン提供の
  type）が保存されている場合に、選択肢を追加した後で `select.value` へ反映するために使う
- 外部 API・`change` イベントは無い
- **自身が直接マウント先にならない唯一の例外**: 子孫の `<select>` を探して装飾するだけのラッパーで、
  内部に React はマウントしない。`<select is="...">` のような Customized built-in element（同じ
  `<select>` タグをそのままカスタム要素化する方式）は Safari が一切対応していないため、他の要素と
  同じ Autonomous custom element（新しいタグ名で `<select>` を子孫に持つ）で実装している
- `connectedCallback` で、プラグインが `ctx.blockEditor.registerBlockDefinition` で登録した
  ブロックタイプを子孫の `<select>` へ「プラグイン」optgroup として追加する（動的 import で
  ブロックエディター本体を読み込むため、admin.js の初期バンドルには含まれない）
- サーバー駆動の原則により、どの type を実際にメニューへ出すかは引き続き管理者がこの画面で選び
  config として保存する。ここで行うのは「選べる選択肢を増やす」ことだけ（詳細は
  `packages/frontend/plugins/HOOKS.md`「カスタムブロックの作り方」参照）

## 外部（テーマ JS・プラグイン）からの利用例

```js
const tagSelect = document.querySelector('acms-tag-select');

// 読み取り（target 先 input の現在値）
console.log(tagSelect.value); // "foo,bar"

// 書き込み（target 先 input と表示の両方に反映される）
tagSelect.value = 'foo,bar,baz';

// 変更の購読
tagSelect.addEventListener('change', () => {
  console.log('tags changed:', tagSelect.value);
});
```

## 将来課題

現状は `target` が指す hidden input でフォーム送信を担っている（tag / category /
sub-category）。将来的には `static formAssociated = true` と `ElementInternals`
（`attachInternals().setFormValue(...)`）に置き換えることで、外部 hidden input を介さずカスタム要素
自身がフォーム値として送信される形にできる余地があるが、今回はスコープ外とした。

`<acms-unit-editor>` / `<acms-block-editor>` は `el.value` による内容の全面 read/write を実装して
いない。マウントの宣言化と `el.editor` / `change` イベントまでを今回のスコープとし、ユニットツリー
全体（unit-editor）・HTML 全体（block-editor）を都度シリアライズして返す `value` API は、内部状態が
大きく変更頻度も高いため、コストと需要を見極めてから改めて検討する。
