メンション挿入時に先行書式が継承されるバグを修正
メンションを入力したときに、行頭の書式(イタリックや太字)がそれ以降のプレーンテキストまで誤って適用される不具合を解消しました。これにより、テキストの部分的な装飾が期待通りに保持されます。
背景
テキストが書式付きの単語で始まり、続く部分が無装飾の場合、@ トリガーでメンションを挿入すると、最初の単語の書式が全体に拡散してしまうバグが報告されました。具体例として、*Italics* not applied to words other than the first にメンションを加えると、全体がイタリックになる現象が確認されています。原因は Contents#replaceTextBackUntil → #performTextReplacement → #cloneTextNodeFormatting の流れで、テキストノードの書式取得ロジックが選択中のタイピング書式を優先し、フォーマットが 0(プレーン)になると段落の書式にフォールバックしていたためです。
このロジックの欠陥により、プレーンテキストでも段落レベルのイタリックが継承され、期待しない装飾が付与されていました。結果としてユーザーはメンション挿入後にテキスト全体の見た目が変わるという不具合に直面していました。修正により、既存の書式が正確に保持され、エディタの WYSIWYG 挙動が改善されます。
技術的な変更
#cloneTextNodeFormatting が選択状態に依存せず、ノード自身の getFormat() と getStyle() を直接参照するように変更されました。これに伴い、呼び出し側から不要となった selection 引数が削除され、メソッドシグネチャが簡素化されています。
@@ -244,14 +244,13 @@ export default class Contents {
replaceTextBackUntil(stringToReplace, replacementNodes) {
replacementNodes = Array.isArray(replacementNodes) ? replacementNodes : [ replacementNodes ]
-
- const selection = $getSelection()
const { anchorNode, offset } = this.#getTextAnchorData()
if (!anchorNode) return
@@
- this.#performTextReplacement(anchorNode, selection, lastIndex, stringToReplace, replacementNodes)
+ this.#performTextReplacement(anchorNode, lastIndex, stringToReplace, replacementNodes)
}
@@
- #performTextReplacement(anchorNode, selection, startIndex, stringToReplace, replacementNodes) {
+ #performTextReplacement(anchorNode, startIndex, stringToReplace, replacementNodes) {
@@
- const textNodeBefore = this.#cloneTextNodeFormatting(anchorNode, selection, textBeforeString)
- const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterString || " ")
+ const textNodeBefore = this.#cloneTextNodeFormatting(anchorNode, textBeforeString)
+ const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, textAfterString || " ")
@@
- #cloneTextNodeFormatting(anchorNode, selection, text) {
- const parent = anchorNode.getParent()
- const fallbackFormat = parent?.getTextFormat?.() || 0
- const fallbackStyle = parent?.getTextStyle?.() || ""
- const format = $isRangeSelection(selection) && selection.format ? selection.format : (anchorNode.getFormat() || fallbackFormat)
- const style = $isRangeSelection(selection) && selection.style ? selection.style : (anchorNode.getStyle() || fallbackStyle)
-
+ #cloneTextNodeFormatting(anchorNode, text) {
@@
- return $createTextNode(text)
- .setFormat(format)
+ return $createTextNode(text)
+ .setFormat(anchorNode.getFormat())
.setDetail(anchorNode.getDetail())
.setMode(anchorNode.getMode())
#cloneTextNodeFormatting が取得する書式は anchorNode.getFormat() のみとなり、フォールバックロジックが除去されています。その結果、プレーンテキストは段落書式に汚染されず、元ノードと同一の書式で再生成されます。加えて、テスト mention_preserves_earlier_formatting.test.js が追加され、イタリックが最初の語だけに残ることを自動的に検証しています。
import { test } from "../../test_helper.js"
import { expect } from "@playwright/test"
test.describe("Mentions preserve earlier formatting", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/mentions.html")
await page.waitForSelector("lexxy-editor[connected]")
})
test("inserting a mention does not apply the first word's format to plain text before it", async ({ page, editor }) => {
await editor.setValue("<p><em>Italics</em> not applied to words other than the first</p>")
await editor.focus()
await editor.send("End")
await editor.send(" ")
await editor.send("@")
const popover = page.locator(".lexxy-prompt-menu--visible")
await expect(popover).toBeVisible({ timeout: 5_000 })
await editor.send("Enter")
await editor.flush()
await expect(editor.content.locator("action-text-attachment")).toBeVisible({ timeout: 5_000 })
const italicTexts = await editor.content.evaluate((el) => {
return Array.from(el.querySelectorAll("em, i")).map((node) => node.textContent.trim())
})
expect(italicTexts).toEqual([ "Italics" ])
})
})
設計判断
書式コピー処理を 選択状態に依存しないシンプルな実装 に変更した点が本修正の核です。元の実装は selection.format が存在しない場合に段落書式へフォールバックしていたため、0(プレーン)が falsy と扱われ不適切な書式が付与されていました。今回の変更でフォーマット取得を anchorNode のみとし、余計なフォールバックや条件分岐を排除したことで、ロジックが明確化されました。
この決定は 後方互換性を保ちつつバグを根本的に排除 するという設計方針に沿っています。selection パラメータの削除に伴い、呼び出し側のシグネチャが簡素化され、将来的なメンテナンスコストが低減します。一方で、特定のケースで選択中の書式を意図的に利用したい拡張は別途実装が必要になる点がトレードオフです。
総じて、不要な依存を排除し データソースの単一化 を図った設計判断と評価でき、エディタの安定性向上に直結しています。
まとめ
本PRはメンション挿入時に先行書式が誤って拡散するバグを、テキストノードの書式コピーロジックをシンプル化することで修正しました。選択状態に依存しない実装に切り替えたことで、既存機能への影響を最小限に抑えつつ正しい書式保持が保証され、テストでの検証も追加されています。