連続してペーストした画像を自動でギャラリーにまとめる
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 フラグでゲートされており、Clipboard → Contents#uploadFiles → Uploader.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 による隣接検出の組み合わせにより、ペースト操作のみをスコープとした自動ギャラリー化が実現されました。ユーザーの意図的な操作を変えることなく、ペーストという一般的なワークフローの体験を改善する、外科的に絞り込まれた設計です。