隣接するメンション間でのドラッグドロップを正しく処理するように修正

basecamp/lexxy

Lexxy のインライン @mention が連続している場合でも、ドラッグ操作で位置入れ替えができるようになりました。ドロップインジケータはメンションの境界にのみ表示され、無意味な内部位置へのドロップは防止されます。

背景

連続した メンション 同士の間に空白がなくなると、ドラッグで移動させようとしても何も起こらず、ドロップカーソルがメンション内部に現れるという問題が報告されました。これは、ブラウザが隙間を caret として認識できず、Lexical がデコレータ内部のポイントを編集可能位置に変換できなかったためです。結果として、ユーザーはメンションの順序を編集できず、チャット入力体験が劣化していました。

技術的な変更

ドロップ位置の解決ロジックに 隣接デコレータ検出 を追加し、カーソルがデコレータ内部にある場合はその前後にスナップするようにしました。具体的には src/editor/attachments/custom/drag_and_drop.js に以下のコードが加わります。

@@
-    if (caret.node === rootElement) {
-      return this.#nearestLineCaret(rootElement, event.clientY)
-    } else {
-      return caret
-    }
+    if (caret.node === rootElement) {
+      return this.#nearestLineCaret(rootElement, event.clientY)
+    }
+
+    // 隣接するメンション間で caret がデコレータ内部に入った場合は
+    // その前後にスナップする。
+    const decorator = this.#decoratorElementContaining(caret.node)
+    if (decorator) {
+      return this.#dropPointBesideDecorator(decorator, event.clientX)
+    } else {
+      return caret
+    }

新たに導入したヘルパー #decoratorElementContaining#dropPointBesideDecorator は、デコレータ要素を取得し、マウス X 座標が要素の中心より左か右かで "before" / "after" を決定します。

#decoratorElementContaining(node) {
  const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement
  return element?.closest("[data-lexxy-decorator][data-lexical-node-key]")
}

#dropPointBesideDecorator(decorator, clientX) {
  const rect = decorator.getBoundingClientRect()
  const placement = clientX > rect.left + rect.width / 2 ? "after" : "before"
  return { decoratorKey: decorator.dataset.lexicalNodeKey, placement }
}

#moveAttachment も拡張し、dropPointdecoratorKey が含まれる場合は #moveBesideNode を呼び出し、Lexical のノード API でメンションを隣接位置へ移動させます。これにより、テキスト選択を組み立て直す必要がなくなり、確実にノード単位で並び替えが行われます。

@@
-      const selection = $createRangeSelectionFromDom({
-        anchorNode: dropPoint.node,
-        anchorOffset: dropPoint.offset,
-        focusNode: dropPoint.node,
-        focusOffset: dropPoint.offset
-      }, this.#editor)
-      if (!selection) return
+      if (dropPoint.decoratorKey) {
+        this.#moveBesideNode(draggedNode, dropPoint)
+      } else {
+        this.#moveToCaret(draggedNode, dropPoint)
+      }

さらに、CustomActionTextAttachmentNode#createDOM でメンション要素に draggable 属性と data-lexical-node-key を付与し、ブラウザ側からドラッグ開始が認識されるようにしました。これに合わせて MentionDragAndDrop コントローラ(高優先度)を追加し、DRAGSTARTDROP をインラインメンション向けに処理します。

設計判断

既存の ブロックレベル添付ファイルハンドラfigure.attachment に限定されており、インラインメンションに影響しないように設計されています。新たに導入した MentionDragAndDropExtension はリッチテキストモードでのみ有効化し、ブロックハンドラと競合しないよう 高優先度 で登録しました。これにより、従来のブロック添付ロジックはそのまま残しつつ、インラインデコレータ専用のドラッグロジックを追加できました。変更点は最小限に抑えられ、後方互換性とコードベースのシンプルさが保たれています。

まとめ

今回の修正により、隣接したメンション間でもドラッグで順序入れ替えが可能になり、ドロップインジケータはメンションの端に正しく表示されます。DOM に draggable とノードキーを付与し、Lexical のノード API で位置を確定する設計は、既存ブロックハンドラを壊さずインライン操作を拡張する安全なアプローチです。これにより、チャット編集におけるメンション操作性が大幅に向上しました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
43cce15e

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文があり、背景・技術的な変更・設計判断・まとめのセクションが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトは正しい形式(```javascript:src/...`)で使用され、PRリンクも#1154が含まれたリンクになっています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

内容はエンジニア向けに技術的な詳細が中心で、初心者向けの過度な説明はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論→結論の構成になり、段落はテーマごとに1トピックで始まり、長さも適切に分割されています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードブロックは提供されたDiffと内容が一致しており、追加・変更箇所が正しく反映されています。コメントの言語が異なる点は情報の欠落ではなく表現の違いです。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている用語(decorator、caret、drop point など)はPRで使用されたものと一致し、誤用は見られません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な説明はPRの目的・原因・解決策と合致しており、根拠が示されています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事の主張はすべてPRタイトル・Description・Diffで裏付けられており、推測や外部知識の付加はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 #1154 が正しく記載され、他の数値・固有名詞の誤りはありません。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR の内容(隣接メンション間でのドラッグドロップ修正)を適切に要約しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

LTS やリリース日程など、PRに記載のない外部情報は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間的な表現は使用されておらず、PRの記述と整合性の問題はありません。