[basecamp/lexxy] エディタのエッジケースとUI操作の不具合修正

basecamp/lexxy

修正の背景

Lexical.jsベースのリッチテキストエディタ「Lexxy」において、いくつかの小さいながらも重要な不具合が報告されていました。特に、ツールバーの状態不整合、blockquoteの解除時の予期しない動作、エディタの状態が壊れる問題、そしてキーボードショートカット使用時にundo/redoが二重に実行される問題などです。#479では、これらの問題を個別に修正しています。

主な修正内容

1. RootまたはShadowRootノードでの操作時のクラッシュ防止

エディタの選択範囲が特殊なノード(RootまたはShadowRoot)にある場合、getTopLevelElementOrThrow()を呼び出すとエラーが発生していました。これは、見出しやフォーマットを適用しようとした際にエディタが壊れる原因となっていました。

修正内容(src/editor/command_dispatcher.js):

const selection = $getSelection()
if (!$isRangeSelection(selection)) return

// RootまたはShadowRootの場合は新しいノードを挿入
if ($isRootOrShadowRoot(selection.anchor.getNode())) {
  selection.insertNodes([ $createHeadingNode("h2") ])
  return
}

const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow()

同様の修正が、カスタムブロック要素の挿入処理にも適用されています。

2. Blockquoteの解除処理の改善

Blockquoteを解除する際、内部のテキストノードや改行要素の処理が不適切で、予期しない結果となっていました。この修正では、以下のロジックが追加されました:

修正内容(src/editor/contents.js):

#unwrap(node) {
  const children = node.getChildren()

  if (children.length == 0) {
    // 空の場合は段落を挿入
    node.insertBefore($createParagraphNode())
  } else {
    children.forEach((child) => {
      if ($isTextNode(child) && child.getTextContent().trim() !== "") {
        // テキストノードは段落でラップ
        const newParagraph = $createParagraphNode()
        newParagraph.append(child)
        node.insertBefore(newParagraph)
      } else if (!$isLineBreakNode(child)) {
        // 改行以外の要素はそのまま挿入
        node.insertBefore(child)
      }
    })
  }

  node.remove()
}

これにより、blockquote内のテキストが適切に段落要素でラップされ、改行要素は除外されるようになりました。

3. エディタ初期化時の選択位置の修正

エディタに値を設定した際、選択範囲が不適切に設定されていました。

修正前:

root.append(...this.#parseHtmlIntoLexicalNodes(html))
root.select()

修正後:

root.append(...this.#parseHtmlIntoLexicalNodes(html))
root.selectEnd()

selectEnd()を使用することで、コンテンツの末尾にカーソルが配置されるようになりました。また、空のコンテンツ設定時に空の<p><br></p>が挿入されなくなり、テストの期待値も更新されています。

4. Undo/Redoの二重実行の防止

ツールバーのundo/redoボタンにdata-hotkey属性が設定されていたため、キーボードショートカット(Cmd+Z/Ctrl+Zなど)を使用すると、ブラウザのネイティブ動作とLexicalのコマンドが両方トリガーされ、二重に実行されていました。

修正内容(src/elements/toolbar.js):

<button class="lexxy-editor__toolbar-button" type="button" 
        name="undo" data-command="undo" title="Undo">
  <!-- data-hotkey属性を削除 -->
</button>

<button class="lexxy-editor__toolbar-button" type="button" 
        name="redo" data-command="redo" title="Redo">
  <!-- data-hotkey属性を削除 -->
</button>

キーボードショートカットの処理はLexicalのコマンドシステムに任せることで、この問題を解決しています。

5. スタイリングの微調整

Blockquote内の最後の段落の下マージンを削除し、より適切な視覚的表現を実現しています。

blockquote {
  border-left: 4px solid var(--lexxy-content-border-color);
  font-style: italic;
  margin: var(--lexxy-content-margin) 0;
  padding: 0.5lh 2ch;

  p:last-child {
    margin-block-end: 0;
  }
}

影響範囲

これらの修正により、以下のシナリオでの動作が改善されました:

  • 空のエディタや特殊な選択状態での見出し・フォーマット適用
  • Blockquoteの解除操作
  • エディタへの初期値設定
  • Undo/Redoのキーボードショートカット使用時の動作

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

ガイドライン準拠 ✓ PASS

記事構成とDiffDaily Styleへの準拠状況

記事構成の必須要素(Title, Context, Technical Detail)がすべて明確に記載されています。コードブロック前後の空行やファイル名付きシンタックスハイライトといったカスタムMarkdown構文も正しく使用されており、可読性が非常に高いです。

  • 記事構成(Title、Context、Technical Detail)
  • DiffDaily Styleガイド準拠
  • カスタムMarkdown活用
  • 対象読者への適合性
技術的整合性 ✓ PASS

技術的な正確性と表現の適切性

引用されているコードはPRの主題である「バグ修正」と一致しており、技術的な説明も正確です。特に、Undo/Redoの二重実行の原因(data-hotkeyとネイティブ動作の競合)とその解決策を明確に説明できており、技術的整合性は高いです。

  • 技術用語の正確性
  • コード例の正確性
  • 説明の技術的正確性
PR内容との整合性 ✓ PASS

元のPR情報との一致度

PRタイトル「Editor bugfixes」の内容を、5つの具体的な修正点に分解して詳細に解説しており、PRの内容と完全に一致しています。数値や固有名詞も正確で、ハルシネーションは検出されませんでした。

  • タイトル・説明の一致
  • Diff内容の正確な反映
  • 推測の排除