エディタDOM構造の簡素化とコードリファクタリング
LexxyエディタのDOM構造から不要な<div>ラッピングを削除し、Lexicalの更新ループを最適化する変更が行われました。これにより、エディタの初期化処理が簡潔になり、不要な再レンダリングが削減されます。
背景
Lexxyエディタは、RailsヘルパーとJavaScript APIの両方で、エディタの値を二重の<div>で囲んでいました。Railsヘルパーのlexxy_rich_textarea_tagは値を<div>で囲み、JavaScript側のset value()でも再度<div>で囲んでいたため、実際のコンテンツが不要な階層の中に埋もれていました。
また、editor.update()内で呼び出される関数が、さらに新しい更新ループを作成していたため、パフォーマンスの観点から改善の余地がありました。#724はこれらの問題を解消しています。
技術的な変更
DOM構造の簡素化
Railsヘルパー側の変更:
lib/lexxy/rich_text_area_tag.rbで、値を<div>で囲む処理が削除されました。
value = render_custom_attachments_in(value)
-value = "<div>#{value}</div>" if value
options[:name] ||= name
options[:value] ||= value
JavaScript側の変更:
src/elements/editor.jsの#parseHtmlIntoLexicalNodesメソッドで、HTMLパース時の<div>ラッピングが削除されました。
#parseHtmlIntoLexicalNodes(html) {
if (!html) html = "<p></p>"
- const nodes = $generateNodesFromDOM(this.editor, parseHtml(`<div>${html}</div>`))
+ const nodes = $generateNodesFromDOM(this.editor, parseHtml(`${html}`))
この変更に伴い、生のテキストノードを直接処理できるよう、新たに#wrapTextNodeメソッドと#unwrapDecoratorNodeメソッドが追加されました。
// Raw string values produce TextNodes which cannot be appended directly to the RootNode.
// We wrap those in <p>
#wrapTextNode(node) {
if (!$isTextNode(node)) return node
const paragraph = $createParagraphNode()
paragraph.append(node)
return paragraph
}
// Custom decorator block elements such as action-text-attachments get wrapped into <p> automatically by Lexical.
// We unwrap those.
#unwrapDecoratorNode(node) {
if ($isParagraphNode(node) && node.getChildrenSize() === 1) {
const child = node.getFirstChild()
if ($isDecoratorNode(child) && !child.isInline()) {
return child
}
}
return node
}
更新ループの最適化
editor.update()内でのみ使用される関数が、新しい更新ループを作成しないようリファクタリングされました。
コマンドディスパッチャーの変更:
src/editor/command_dispatcher.jsのdispatchRotateHeadingFormatメソッドから、editor.update()の呼び出しが削除されました。このメソッドは既にeditor.update()のコンテキスト内で呼ばれるため、二重のループが不要です。
dispatchRotateHeadingFormat() {
- this.editor.update(() => {
- const selection = $getSelection()
- if (!$isRangeSelection(selection)) return
+ const selection = $getSelection()
+ if (!$isRangeSelection(selection)) return
- if ($isRootOrShadowRoot(selection.anchor.getNode())) {
- selection.insertNodes([ $createHeadingNode("h2") ])
- return
- }
+ if ($isRootOrShadowRoot(selection.anchor.getNode())) {
+ selection.insertNodes([ $createHeadingNode("h2") ])
+ return
+ }
Contentsクラスの変更:
同様に、src/editor/contents.jsのinsertAtCursorメソッドも、editor.update()の呼び出しが削除されました。配列の最後の要素へのアクセスはat(-1)メソッドに変更されています。
insertAtCursor(node) {
- this.editor.update(() => {
- const selection = $getSelection()
- const selectedNodes = selection?.getNodes()
+ const selection = $getSelection()
+ const selectedNodes = selection?.getNodes()
- if ($isRangeSelection(selection)) {
- $insertNodes([ node ])
- } else if ($isNodeSelection(selection) && selectedNodes && selectedNodes.length > 0) {
- const lastNode = selectedNodes.at(-1)
- lastNode.insertAfter(node)
- } else {
- const root = $getRoot()
- root.append(node)
- }
- })
+ if ($isRangeSelection(selection)) {
+ $insertNodes([ node ])
+ } else if ($isNodeSelection(selection) && selectedNodes && selectedNodes.length > 0) {
+ const lastNode = selectedNodes.at(-1)
+ lastNode.insertAfter(node)
+ } else {
+ const root = $getRoot()
+ root.append(node)
+ }
}
その他の改善
WrappedTableNodeの警告抑制:
src/nodes/wrapped_table_node.jsで、$config()とimportDOM()メソッドが追加されました。
$config() {
return this.config("wrapped_table_node", { extends: TableNode })
}
static importDOM() {
return super.importDOM()
}
src/extensions/tables_lexical_extension.jsでは、ノード置換設定にwithKlassプロパティが追加されました。
WrappedTableNode,
{
replace: TableNode,
- with: () => new WrappedTableNode()
+ with: () => new WrappedTableNode(),
+ withKlass: WrappedTableNode
},
Stimulusデバッグの無効化:
開発環境でのコンソール出力を削減するため、test/dummy/app/javascript/controllers/application.jsでStimulusのデバッグモードがオフになりました。
-application.debug = true
+application.debug = false
設計判断
値のラッピングを両レイヤーで削除する方針が採用されました。RailsヘルパーとJavaScript APIの両方で<div>ラッピングを削除することで、エディタのDOM構造がシンプルになり、デバッグやスタイリングが容易になります。
既存の更新ループ内で実行される関数については、新しいeditor.update()を呼び出さないよう統一されました。これにより、不要な再レンダリングが削減され、パフォーマンスが向上します。関数が既に更新コンテキスト内で呼ばれることを前提とした設計に変更されています。
テストコードも、新しいDOM構造に合わせて更新されています。test/helpers/lexxy/tag_helper_test.rbでは、値が直接<p>Sample Content</p>として扱われるようになり、余分な<div>のアサーションが削除されました。
これらの変更は、エディタの内部構造を簡素化しつつ、外部APIの互換性を維持した慎重な設計といえます。
まとめ
本PRは、LexxyエディタのDOM構造とレンダリングパイプラインを最適化する変更です。不要な<div>ラッピングの削除と更新ループの効率化により、エディタの初期化が高速化され、コードの可読性も向上しています。WrappedTableNodeのコンソール警告も抑制され、より静かな開発環境が実現されました。