DiffDaily

Deep & Concise - OSS変更の定点観測

[basecamp/lexxy] プロンプトアイテムから複数のアタッチメント挿入とコンテンツタイプのカスタマイズが可能に

basecamp/lexxy

変更の背景

Lexxy(Lexical.jsベースのリッチテキストエディタライブラリ)では、従来、1つのプロンプトアイテムから1つのアタッチメントしか挿入できませんでした。この制約は、ユーザーグループのメンション機能など、1つの選択から複数のアタッチメントを生成したいユースケースに対応できないという課題がありました。

また、Action Textのアタッチメントで使用される content-typeapplication/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"> には独自の sgidcontent-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)
  })
}

flatMapslice(0, -1) のパターンにより、最後のアタッチメントの後ろにはスペースが挿入されないように制御されています。

sgid属性の優先順位

sgid 属性は以下の優先順位で解決されます:

  1. <template type="editor"> 要素の sgid 属性
  2. <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> レベルでの指定も引き続きサポートされます