ネイティブマルチ画像アップロードをギャラリー化するバッチAPIの導入
Lexxy が iOS/Android のネイティブブリッジで複数画像を選択した際に、画像が個別の添付としてスタックされる回帰を解消し、Web と同様に 1 つの画像ギャラリー として挿入できるようにしました。
背景
Web では すべてのアップロード入口 が Uploader.for(...) を通り、バッチに複数のプレビュー可能画像が含まれると GalleryUploader が選択されて ImageGalleryNode が生成されます。これに対し、ネイティブブリッジは各ファイルを個別に Contents#insertPendingAttachment(file) で渡すため isMultipleImageUpload が呼び出されず、ギャラリー形成が行われませんでした。Trix ではドキュメント全体を走査する attachmentGalleryFilter があったため回帰が起きませんでしたが、Lexxy では挿入時にのみギャラリー化が行われる設計のため、ネイティブ経路でギャラリーが失われたのです。
技術的な変更
バッチエントリポイントの追加
Contents に insertPendingAttachments(files) を実装し、受け取った FileList 全体を Uploader.for(editorElement, files, { pending: true }) に渡すことで、Web と同じ GalleryUploader パスを通ります。アップロードノードは uploadUrl を持たない pending モードで生成され、ホストアプリがアップロードを管理できるようになります。
+ $createPendingUploadNode(file) {
+ return $createActionTextAttachmentUploadNode({
+ file,
+ uploadUrl: null,
+ blobUrlTemplate: this.editorElement.blobUrlTemplate,
+ contentType: file.type,
+ })
+ }
+
+ insertPendingAttachment(file) {
+ ...
+ const uploadNode = this.$createPendingUploadNode(file)
+ ...
+ return nodeKey ? this.#pendingAttachmentHandle(nodeKey) : null
+ }
+
+ insertPendingAttachments(files) {
+ const fileList = Array.from(files)
+ if (!this.editorElement.supportsAttachments || fileList.length === 0) return []
+ let nodeKeys = []
+ this.editor.update(() => {
+ const uploader = Uploader.for(this.editorElement, fileList, { pending: true })
+ uploader.$uploadFiles()
+ nodeKeys = (uploader.nodes ?? []).map(node => node.getKey())
+ })
+ return nodeKeys.map(nodeKey => this.#pendingAttachmentHandle(nodeKey))
+ }
Uploader のオプション拡張
Uploader.for に options 引数を追加し、pending フラグが立っている場合は contents.$createPendingUploadNode を呼び出すよう分岐させました。これにより GalleryUploader でも同様の pending ノードが生成され、既存のギャラリー形成ロジックをそのまま再利用できます。
- static for(editorElement, files) {
+ static for(editorElement, files, options = {}) {
const UploaderKlass = GalleryUploader.handle(editorElement, files) ? GalleryUploader : Uploader
- return new UploaderKlass(editorElement, files)
+ return new UploaderKlass(editorElement, files, options)
}
@@
- $createUploadNodes() {
- this.nodes = this.files.map(file => this.contents.$createUploadNode(file))
+ $createUploadNodes() {
+ this.nodes = this.files.map(file => this.#createUploadNode(file))
}
+
+ #createUploadNode(file) {
+ return this.options.pending
+ ? this.contents.$createPendingUploadNode(file)
+ : this.contents.$createUploadNode(file)
+ }
テスト追加
native_bridge_gallery.test.js でバッチ API の 3 つのシナリオ を検証しました。(1)複数画像が単一ギャラリーになること、(2)ファイルごとにハンドルが返り、マテリアライズしてもギャラリーが維持されること、(3)空バッチは何も挿入しないこと。
+test.describe("Native bridge gallery (batch)", () => {
+ ...
+ test("inserts multiple images as a single gallery", async ({ page, editor }) => {
+ await insertPendingBatch(editor, [ image("a.png"), image("b.png") ])
+ await editor.flush()
+ await expect(page.locator(".attachment-gallery")).toHaveCount(1)
+ await expect(page.locator(".attachment-gallery figure.attachment")).toHaveCount(2)
+ })
+ ...
+})
設計判断
既存ギャラリーコードの再利用
GalleryUploader と ImageGalleryNode は 一切変更せず、新規ロジックは Uploader.for のオプション分岐 と $createPendingUploadNode のみです。これにより、Web とネイティブの挙動が完全に一致し、バグ修正が一元管理できます。
後方互換性の維持
単一ファイル向け insertPendingAttachment は残したままにし、古いネイティブクライアントが引き続き動作できるようにしました。新しいクライアントはバッチ API を呼び出す設計で、将来的に単一パスへ移行可能です。
アップロード所有権はホスト側に委譲
pending: true モードではノードに uploadUrl が設定されず、ホストアプリがアップロード進捗をハンドルします。これにより Lexxy 本体はアップロード実装に依存せず、プラットフォーム固有の最適化を維持できます。
まとめ
ネイティブブリッジが バッチで Uploader.for → GalleryUploader の経路 を通るようにしたことで、画像の複数選択時に Web と同様の 画像ギャラリー が生成されます。変更は既存のギャラリー実装をそのまま再利用し、オプション分岐とハンドル抽象化だけで済ませたため、既存コードへの影響は最小限です。後方互換性も保たれ、将来的に単一エントリポイントへ統合できる設計になっています。