連続してペーストした画像を自動でギャラリーにまとめる

basecamp/lexxy

Lexxyで2枚以上の画像を連続してペーストすると、自動的にギャラリーとしてグループ化されるようになりました。従来はユーザーが手動でドラッグ操作してギャラリーを作成する必要がありましたが、この変更によりTrixと同等の直感的な動作が実現します。

背景

連続した画像ペーストがギャラリーとして扱われないという問題は、Basecampカード #9811488004 として報告されていました。Trixエディタでは複数画像を連続してペーストするとギャラリーが自動形成されますが、Lexxyではペーストのたびに独立したスタック型のアタッチメントが生成され、ユーザーが都度ドラッグ操作でギャラリーに変換しなければならない状態でした。

この問題の核心は、ペースト操作と通常のアップロード操作を GalleryUploader が区別していなかった点にあります。ツールバーからのアップロードで「Enterを押してギャラリーを抜け出した後に画像をアップロードする」という意図的な操作も、ペースト後の自動グループ化と同じ判定を受けてしまう可能性がありました。

技術的な変更

今回の変更の中心は、GalleryUploader の選択判定ロジックの拡張と、ペースト操作を識別する fromPaste フラグの導入です。

src/editor/contents/uploader.js において、GalleryUploader の静的メソッドが以下のように変更されました。まず、従来プライベートメソッドだった #isMultipleImageUpload#gallerySelection がパブリック化されています。

変更前:

static #gallerySelection(selection) {
  if (selection.isOnPreviewableImage) return true

  const { node: selectedNode } = selection.selectedNodeWithOffset()
  return $getNearestNodeOfType(selectedNode, ImageGalleryNode) !== null
}

変更後:

static gallerySelection(selection) {
  return selection.isOnPreviewableImage || this.selectionIsAfterGalleryEdge(selection)
}

static selectionIsAfterGalleryEdge(selection) {
  return selection.isAtNodeStart && ImageGalleryNode.canCollapseWith(selection.nodeBeforeCursor)
}

旧実装では $getNearestNodeOfType を使ってカーソルが ImageGalleryNode の内部にあるかを確認していましたが、新実装では selectionIsAfterGalleryEdge という概念を導入しています。カーソルがノードの先頭(isAtNodeStart)にあり、かつカーソル直前のノードがギャラリーまたはプレビュー可能な画像アタッチメントである場合に、そのギャラリーへの追加対象と判断します。

この「隣接」検出は fromPaste フラグでゲートされており、ClipboardContents#uploadFilesUploader.for の経路でフラグが伝播します。また src/editor/selection.js には isAtNodeStart ゲッターが追加され、アンカーノードのオフセットが 0 かどうかでカーソル位置を判定します。

get isAtNodeStart() {
  const { anchorNode, offset } = this.#getCollapsedSelectionData()
  return anchorNode && offset === 0
}

あわせて Uploader のコンストラクタに options = {} 引数が追加され、fromPaste をはじめとするオプションを受け取れるよう拡張されています。

設計判断

fromPaste フラグによるゲーティングが、この変更の最も重要な設計判断です。隣接する画像へのギャラリー追加を常に行うのではなく、ペースト操作のときだけ適用することで、ツールバーからの意図的なアップロード操作への影響を排除しています。

既存テスト delete at gallery end absorbs next image および delete at gallery end absorbs next gallery がカバーする「ギャラリー末尾でDeleteを押して次の画像を吸収する」挙動は、Enterを押してギャラリーを抜け出した後にツールバーでアップロードするユーザーの意図を守るための境界です。この境界を fromPaste フラグで明示的に実装することで、自動化の恩恵とユーザーの操作意図の尊重を両立しています。

また、プライベートメソッドだった #isMultipleImageUpload#gallerySelection のパブリック化は、fromPaste フラグを持つ派生クラスやテストコードからのアクセスを可能にするための変更です。JavaScriptのプライベートフィールド構文(#)が持つアクセス制限を、設計上の拡張ポイントのために緩和した判断といえます。

テスト側では test/browser/helpers/gallery_test_helpers.js が新設され、「Enterを押してギャラリー外に出てから画像をアップロードする」パターンを再現する uploadStandaloneAfter ヘルパーが抽出されました。既存テストも同ヘルパーを利用するよう更新されており、「スタンドアロンの画像をアップロードする」という意図がテストコードでも明確に表現されています。

まとめ

fromPaste フラグによる操作コンテキストの識別と selectionIsAfterGalleryEdge による隣接検出の組み合わせにより、ペースト操作のみをスコープとした自動ギャラリー化が実現されました。ユーザーの意図的な操作を変えることなく、ペーストという一般的なワークフローの体験を改善する、外科的に絞り込まれた設計です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
53e6a0a1

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

「総論(リード文)→各論(背景、技術的な変更、設計判断)→結論(まとめ)」という理想的な3部構成が明確に適用されています。各セクションの役割も適切です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライト(```javascript:path/to/file.js)およびPR番号のリンク記法([#1021](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Lexxyエディタの内部実装に関する詳細な解説であり、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクション、各パラグラフが「総論→各論」の構成になっており、トピックセンテンスが明確です。1段落1トピックの原則も守られており、非常に読みやすい構造です。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロックは、提供されたDiff情報と正確に一致しています。ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「GalleryUploader」「fromPasteフラグ」「ゲーティング」など、PRの文脈に沿った技術用語や一般的なプログラミング用語が正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

コード変更の意図やロジックの流れ(`fromPaste`フラグの伝播経路など)について、PR情報とDiff内容に基づいた技術的に正確な説明がなされています。

事実の突合 ⚠ WARNING

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

プライベートメソッドをパブリック化した理由について、PR情報に明記されていない推測が含まれています。ただし、技術的に自明な範囲の解説であり、記事の理解を助けています。

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

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

PR番号(#1021)やBasecampカード番号(#9811488004)などの固有名詞は、PR情報と正確に一致しています。

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

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

記事のタイトル「連続してペーストした画像を自動でギャラリーにまとめる」は、PRのタイトル「Group images pasted in succession into a gallery」の内容を的確に反映しています。

外部知識の正確性 ✓ PASS

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

記事の内容はPR情報とDiffに限定されており、バージョンサポートやリリース日程といったPRに記載のない外部知識の追記は見られません。

時間表現の正確性 ✓ PASS

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

「〜ようになりました」「従来は〜」といった時間表現は、PRによる変更の前後関係を正しく反映しています。