[basecamp/lexxy] プロンプトアイテムから複数のアタッチメント挿入とコンテンツタイプのカスタマイズが可能に
変更の背景
Lexxy(Lexical.jsベースのリッチテキストエディタライブラリ)では、従来、1つのプロンプトアイテムから1つのアタッチメントしか挿入できませんでした。この制約は、ユーザーグループのメンション機能など、1つの選択から複数のアタッチメントを生成したいユースケースに対応できないという課題がありました。
また、Action Textのアタッチメントで使用される content-type は application/vnd.actiontext.{type} という固定フォーマットでしたが、Rails以外のアプリケーションや独自の命名規則を持つプロジェクトでは、この名前空間をカスタマイズしたいというニーズがありました。
実装された機能
1. 複数アタッチメントのサポート
単一のプロンプトアイテム内に複数の <template type="editor"> 要素を配置できるようになりました。これにより、1つの選択操作で複数のアタッチメントを一括挿入できます。
実装例(グループメンション):
<lexxy-prompt-item search="<%= locals[:group_name] %>">
<template type="menu">
<span class="person person--prompt-item">
<span class="person--avatar"><%= "GR" %></span>
<span class="person--name"><%= locals[:group_name] %></span>
</span>
</template>
<% locals[:people].each do |person| %>
<template type="editor" sgid="<%= person.attachable_sgid %>" content-type="application/vnd.actiontext.group_mention">
<span class="person person--inline">
<span class="person--name"><%= person.name %></span>
</span>
</template>
<% end %>
</lexxy-prompt-item>
この例では、グループを選択すると、そのグループに属する全メンバーのアタッチメントが個別に挿入されます。各 <template type="editor"> には独自の sgid と content-type を指定できます。
2. コンテンツタイプ名前空間の設定可能化
グローバル設定で attachmentContentTypeNamespace を変更できるようになりました。
設定方法:
import * as Lexxy from "lexxy"
Lexxy.configure({
global: {
attachmentContentTypeNamespace: "myapp"
}
})
// デフォルトのcontent-typeが application/vnd.myapp.{type} になる
実装の詳細:
constructor({ tagName, sgid, contentType, innerHtml }, key) {
super(key)
const contentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace")
this.tagName = tagName || CustomActionTextAttachmentNode.TAG_NAME
this.sgid = sgid
this.contentType = contentType || `application/vnd.${contentTypeNamespace}.unknown`
this.innerHtml = innerHtml
}
デフォルト値は "actiontext" のため、既存の動作に影響はありません。
技術的な変更点
プロンプトアイテムの処理ロジック
従来の単一テンプレート処理から、複数テンプレートの配列処理に変更されました。
変更前:
const template = promptItem.querySelector("template[type='editor']")
this.#insertTemplateAsAttachment(promptItem, template, stringToReplace)
変更後:
const templates = Array.from(promptItem.querySelectorAll("template[type='editor']"))
const stringToReplace = `${this.trigger}${this.#editorContents.textBackUntil(this.trigger)}`
if (this.hasAttribute("insert-editable-text")) {
this.#insertTemplatesAsEditableText(templates, stringToReplace)
} else {
this.#insertTemplatesAsAttachments(templates, stringToReplace, promptItem.getAttribute("sgid"))
}
アタッチメント間のスペース挿入
複数のアタッチメントを挿入する際、各アタッチメント間に自動的にスペーサーテキストノードが挿入されます。
#insertTemplatesAsAttachments(templates, stringToReplace, fallbackSgid = null) {
this.#editor.update(() => {
const attachmentNodes = this.#buildAttachmentNodes(templates, fallbackSgid)
const spacedAttachmentNodes = attachmentNodes.flatMap(node => [ node, this.#getSpacerTextNode() ]).slice(0, -1)
this.#editorContents.replaceTextBackUntil(stringToReplace, spacedAttachmentNodes)
})
}
flatMap と slice(0, -1) のパターンにより、最後のアタッチメントの後ろにはスペースが挿入されないように制御されています。
sgid属性の優先順位
sgid 属性は以下の優先順位で解決されます:
-
<template type="editor">要素のsgid属性 -
<lexxy-prompt-item>要素のsgid属性(fallback)
これにより、グループ全体のsgidと個別メンバーのsgidを柔軟に使い分けられます。
システムテスト
以下のテストケースが追加され、動作が保証されています:
test "prompt with multiple attachables" do
find_editor.send "4"
click_on_prompt "Group 0"
find_editor.within_contents do
assert_selector %(action-text-attachment[content-type="application/vnd.actiontext.group_mention"]), count: 5
end
all("action-text-attachment").map { |el| el["sgid"] }.uniq.size == 5
end
test "global custom content-type of mentions" do
visit edit_post_path(posts(:empty), attachment_content_type_namespace: "myapp")
find_editor.send "1"
click_on_prompt "Peter Johnson"
assert_selector %(action-text-attachment[content-type="application/vnd.myapp.mention"])
assert_no_selector %(action-text-attachment[content-type="application/vnd.actiontext.mention"])
end
互換性
- 既存の単一アタッチメントプロンプトはそのまま動作します
- デフォルトの
content-type名前空間は"actiontext"のため、既存コードへの影響はありません -
sgid属性は<lexxy-prompt-item>レベルでの指定も引き続きサポートされます