Ordered List の初期値での番号付与バグ修正
Lexxy で <ol> をエディタの初期 value としてロードした際、リスト項目がすべて value="1" になる問題を、内部フォーム値の取得タイミングとエディタ更新のフローを調整することで解消した。
背景
バグの現象: 初期 value に含まれる順序付きリストは、エディタが生成した HTML が <li value="1"> を全項目に付与し、連番が失われる。
根本原因: #initialEditorState コールバック内で editor.update() が走り、その中で editor.read() が呼ばれると、Lexical は半完了の更新をコミットし、ノード変換(特に ListNode の updateChildrenListItemValue)をスキップする。その結果、<li> の value 属性はデフォルト 1 のままになる。
影響範囲: 編集操作で生成したリストは正しく番号付与されるが、最初から HTML を value 属性で渡すケースだけが対象となり、ユーザーがエディタを初期化するときに不整合が顕在化する。
技術的な変更
内部フォーム値取得のタイミング変更: $initialEditorState のコールバックから #setInternalFormValue(this.#readSanitizedEditorValue(editor)) を除去し、エディタ構築直後に #setInternalFormValue(this.value, { suppressEvent: true }) を実行するようにした。これにより editor.read() が完了した更新上で走り、すべてのリスト変換が適用された状態で内部値がシードされる。
@@ -409,7 +411,6 @@
$initialEditorState: (editor) => {
this.#configureSanitizer(editor)
this.#loadInitialValue(editor)
- this.#setInternalFormValue(this.#readSanitizedEditorValue(editor))
},
#readSanitizedEditorValue のシグネチャ簡素化: 引数 editor を削除し、常に this.editor を使用するようにした。呼び出し側が editor を渡す必要がなくなることで、コードパスが一本化された。
@@ -338,8 +338,8 @@
- #readSanitizedEditorValue(editor = this.editor) {
- return editor?.read(() => {
+ #readSanitizedEditorValue() {
+ return this.editor?.read(() => {
return sanitize($generateHtmlFromNodes(this.editor, null))
}) ?? null
}
#setInternalFormValue にオプション導入: 新たに { suppressEvent = false } を受け取り、変更検知ロジックを html !== this.#previousInternalFormValue に簡素化した。初期シード時に suppressEvent: true を渡すことで、不要な lexxy:change イベントの発火を防止しつつ内部値は更新される。
@@ -477,13 +478,13 @@
- #setInternalFormValue(html) {
- const changed = this.#previousInternalFormValue !== null && html !== this.#previousInternalFormValue
+ #setInternalFormValue(html, { suppressEvent = false } = {}) {
+ const changed = html !== this.#previousInternalFormValue
this.internals.setFormValue(html)
this.#previousInternalFormValue = html
- if (changed) {
+ if (changed) {
#initialize の呼び出し順序調整: 旧来は最初に #synchronizeWithChanges() が走っていたが、内部フォーム値設定を先に行い、続いて #synchronizeWithChanges() を呼び出すよう変更した。これにより、最初の変更リスナーが登録される前に正しいシードが確定する。
@@ -371,7 +371,9 @@
- this.#resetBeforeTurboCaches()
+ this.#resetBeforeTurboCaches()
+
+ this.#setInternalFormValue(this.value, { suppressEvent: true })
+ this.#synchronizeWithChanges()
テストの追加: ordered_list_initial_value.test.js と対応する HTML フィクスチャを新規作成し、初期値として渡した <ol> が value="1", value="2", value="3" と連番になることを確認した。フル Playwright と Rails テストスイートともに全ケースがパスし、回帰防止が保証された。
設計判断
スパースなイベント抑制: 初期シード時に lexxy:change を発火させない設計は、09c4b609 で導入された「spurious change 防止」ロジックと整合性がある。#setInternalFormValue に suppressEvent オプションを持たせることで、既存コードはデフォルトでイベントを発行しつつ、必要な箇所だけ抑制できる。
後方互換性の確保: メソッドシグネチャの変更は引数が省略可能になるだけで、外部から #setInternalFormValue を直接呼んでいるケースは少なく、#readSanitizedEditorValue も内部のみ使用されるため、公開 API への影響は最小。既存のエディタ初期化フローは変わらず動作し、変更は内部実装のタイミング最適化に留まる。
最小侵襲の実装: 変更箇所は 3 つのメソッドと呼び出し順序に限定され、ロジック自体は追加の条件分岐やオプション処理だけで実装コストが低い。これにより、バグ修正とコードベースの安定性が同時に確保された。
まとめ
初期 value に含まれる順序付きリストが正しく番号付与されなかった根本原因は、エディタ更新中の editor.read() が変換をスキップしたことにあった。内部フォーム値の取得を更新完了後に行い、#setInternalFormValue に抑制オプションを導入したことで、変換が正しく適用されつつ不要なイベントを防げた。テスト追加により回帰が防止され、既存挙動との互換性も維持された。