貼り付け時に入れ子リストを保持する処理を追加
Lexxy の貼り付け機能で、インデントされたリストが失われていた問題を修正し、入れ子 <ul>/<ol> が正しく保持されるようになりました。これにより、Gmail などからコピーした階層付き箇条書きがそのままエディタに反映されます。
背景
以前のリリース(PR #1119)で #stripStrayListChildren が導入され、 <ol>/<ul> の直下にある <li> 以外の要素(例: <br> やテキストノード)を除去することで、不要なリスト先頭の空要素を防いでいました。結果として、サブリストが親リストの直接子として存在するケース(Gmail が生成する HTML など)では、そのサブリストが削除され、階層が失われていました。複数の顧客から「インデントされた箇条書きが消える」報告が寄せられ、回帰として認識されました。
技術的な変更
src/editor/contents/pasted_content_formatter.js に新しいプライベートメソッド #nestStrayListChildren() が追加され、リスト直下にある入れ子 <ol>/<ul> を直前の <li> に移動させます。format() の実行順序も変更され、#nestStrayListChildren() が #stripStrayListChildren() の前に呼び出されるようになりました。
@@
format() {
this.#unwrapPlaceholderAnchors()
this.#stripTableCellColorStyles()
+ this.#nestStrayListChildren()
this.#stripStrayListChildren()
return this.doc
}
@@
// Some sources (e.g. Gmail) nest a sublist as a direct child of the parent
// <ol>/<ul> instead of inside a <li>. Move each nested list into its
// preceding <li> so the import preserves the nesting instead of dropping it.
#nestStrayListChildren() {
for (const list of this.doc.querySelectorAll("ol, ul")) {
for (const child of Array.from(list.children)) {
if (child.tagName !== "OL" && child.tagName !== "UL") continue
const previousItem = child.previousElementSibling
if (previousItem && previousItem.tagName === "LI") {
previousItem.appendChild(child)
}
}
}
}
テストコード test/browser/tests/paste/clean_invalid_pasted_list_html.test.js にも 2 つのシナリオが追加され、直接子として配置された <ul> と <ol> がそれぞれ正しくネストされた HTML に変換されることを検証しています。
@@
test.describe("Paste list with a nested list as a direct child", () => {
@@
await editor.paste("ignored", {
html: "<ul><li>First item</li><ul><li>Nested item</li></ul><li>Second item</li></ul>",
})
@@
await assertEditorHtml(
editor,
- '<ul><li value="1">First item<ul><li value="1">Nested item</li></ul></li><li value="2">Second item</li></ul>',
+ '<ul><li value="1">First item<ul><li value="1">Nested item</li></ul></li><li value="2">Second item</li></ul>',
)
@@
await editor.paste("ignored", {
html: "<ol><li>First item</li><ol><li>Nested item</li></ol><li>Second item</li></ol>",
})
@@
await assertEditorHtml(
editor,
- '<ol><li value="1">First item<ol><li value="1">Nested item</li></ol></li><li value="2">Second item</li></ol>',
+ '<ol><li value="1">First item<ol><li value="1">Nested item</li></ol></li><li value="2">Second item</li></ol>',
)
設計判断
#nestStrayListChildren() を追加し、既存の #stripStrayListChildren() の前に実行させるという選択は、リスト構造の保持と不要要素の除去という二つの目的を独立させるための最小限の侵襲です。メソッド自体は単純な DOM 移動ロジックだけで済み、他のフォーマッタ処理への副作用はありません。さらに、format() の呼び出し順序を変えるだけで済むため、既存のプラグインやカスタマイズとの後方互換性が保たれました。
まとめ
今回の変更により、Lexxy は直接子として配置された入れ子リストを失うことなく貼り付けられるようになり、ユーザーが期待する階層付き箇条書きの保存が可能になりました。#nestStrayListChildren() の導入は、既存のクリーンアップロジックを壊さずに機能拡張を実現した設計判断と言えるでしょう。