diff --git a/packages/lexical-list/src/LexicalListItemNode.ts b/packages/lexical-list/src/LexicalListItemNode.ts index 0e5153bc256..3947ba12a63 100644 --- a/packages/lexical-list/src/LexicalListItemNode.ts +++ b/packages/lexical-list/src/LexicalListItemNode.ts @@ -80,6 +80,9 @@ export class ListItemNode extends ElementNode { } element.value = this.__value; $setListItemThemeClassNames(element, config.theme, this); + if (this.__style !== '') { + element.style.cssText = this.__style; + } return element; } @@ -95,7 +98,9 @@ export class ListItemNode extends ElementNode { // @ts-expect-error - this is always HTMLListItemElement dom.value = this.__value; $setListItemThemeClassNames(dom, config.theme, this); - + if (prevNode.__style !== this.__style) { + dom.style.cssText = this.__style; + } return false; } diff --git a/packages/lexical-list/src/index.ts b/packages/lexical-list/src/index.ts index d04909343c5..6da9d883574 100644 --- a/packages/lexical-list/src/index.ts +++ b/packages/lexical-list/src/index.ts @@ -12,9 +12,15 @@ import type {LexicalCommand, LexicalEditor} from 'lexical'; import {mergeRegister} from '@lexical/utils'; import { + $getSelection, + $isRangeSelection, + $isTextNode, + COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_LOW, createCommand, INSERT_PARAGRAPH_COMMAND, + SELECTION_CHANGE_COMMAND, + TextNode, } from 'lexical'; import { @@ -58,6 +64,21 @@ export const REMOVE_LIST_COMMAND: LexicalCommand = createCommand( 'REMOVE_LIST_COMMAND', ); +function $checkSelectionListener(): boolean { + const selection = $getSelection(); + if ($isRangeSelection(selection) && selection.isCollapsed()) { + const node = selection.anchor.getNode(); + if ( + $isListItemNode(node) && + node.isEmpty() && + selection.style !== node.getStyle() + ) { + node.setStyle(selection.style); + } + } + return false; +} + export function registerList(editor: LexicalEditor): () => void { const removeListener = mergeRegister( editor.registerCommand( @@ -97,6 +118,29 @@ export function registerList(editor: LexicalEditor): () => void { }, COMMAND_PRIORITY_LOW, ), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + $checkSelectionListener, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerNodeTransform(ListItemNode, (node) => { + const firstChild = node.getFirstChild(); + if (firstChild && $isTextNode(firstChild)) { + const style = firstChild.getStyle(); + if (node.getStyle() !== style) { + node.setStyle(style); + } + } + }), + editor.registerNodeTransform(TextNode, (node) => { + const listItemParentNode = node.getParent(); + if ( + $isListItemNode(listItemParentNode) && + node.is(listItemParentNode.getFirstChild()) + ) { + listItemParentNode.markDirty(); + } + }), ); return removeListener; } diff --git a/packages/lexical-playground/__tests__/e2e/List.spec.mjs b/packages/lexical-playground/__tests__/e2e/List.spec.mjs index d4c1bc250e8..7079fbe7206 100644 --- a/packages/lexical-playground/__tests__/e2e/List.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/List.spec.mjs @@ -176,7 +176,7 @@ test.describe.parallel('Nested List', () => { await assertHTML( page, - '

World

', + '

World

', ); }); diff --git a/packages/lexical-selection/src/lexical-node.ts b/packages/lexical-selection/src/lexical-node.ts index 22074d1fd6e..75061fdf03a 100644 --- a/packages/lexical-selection/src/lexical-node.ts +++ b/packages/lexical-selection/src/lexical-node.ts @@ -17,6 +17,7 @@ import { $isTextNode, $isTokenOrSegmented, BaseSelection, + ElementNode, LexicalEditor, LexicalNode, Point, @@ -241,7 +242,7 @@ export function $addNodeStyle(node: TextNode): void { } function $patchStyle( - target: TextNode | RangeSelection, + target: TextNode | RangeSelection | ElementNode, patch: Record< string, | string @@ -263,7 +264,7 @@ function $patchStyle( } return styles; }, - {...prevStyles} || {}, + {...prevStyles}, ); const newCSSText = getCSSFromStyleObject(newStyles); target.setStyle(newCSSText); @@ -285,12 +286,16 @@ export function $patchStyleText( | null | (( currentStyleValue: string | null, - target: TextNode | RangeSelection, + target: TextNode | RangeSelection | ElementNode, ) => string) >, ): void { if (selection.isCollapsed() && $isRangeSelection(selection)) { $patchStyle(selection, patch); + const emptyNode = selection.anchor.getNode(); + if ($isElementNode(emptyNode) && emptyNode.isEmpty()) { + $patchStyle(emptyNode, patch); + } } else { $forEachSelectedTextNode((textNode) => { $patchStyle(textNode, patch); diff --git a/packages/lexical/src/LexicalEvents.ts b/packages/lexical/src/LexicalEvents.ts index 0ccd47e2910..cbeffe4766e 100644 --- a/packages/lexical/src/LexicalEvents.ts +++ b/packages/lexical/src/LexicalEvents.ts @@ -337,31 +337,27 @@ function onSelectionChange( anchor.offset === lastOffset && anchor.key === lastKey ) { - selection.format = lastFormat; - selection.style = lastStyle; + $updateSelectionFormatStyle(selection, lastFormat, lastStyle); } else { if (anchor.type === 'text') { invariant( $isTextNode(anchorNode), 'Point.getNode() must return TextNode when type is text', ); - selection.format = anchorNode.getFormat(); - selection.style = anchorNode.getStyle(); + $updateSelectionFormatStyleFromNode(selection, anchorNode); } else if (anchor.type === 'element' && !isRootTextContentEmpty) { invariant( $isElementNode(anchorNode), 'Point.getNode() must return ElementNode when type is element', ); const lastNode = anchor.getNode(); - selection.style = ''; if ( // This previously applied to all ParagraphNode lastNode.isEmpty() ) { - selection.format = lastNode.getTextFormat(); - selection.style = lastNode.getTextStyle(); + $updateSelectionFormatStyleFromNode(selection, lastNode); } else { - selection.format = 0; + $updateSelectionFormatStyle(selection, 0, ''); } } } @@ -411,6 +407,27 @@ function onSelectionChange( }); } +function $updateSelectionFormatStyle( + selection: RangeSelection, + format: number, + style: string, +) { + if (selection.format !== format || selection.style !== style) { + selection.format = format; + selection.style = style; + selection.dirty = true; + } +} + +function $updateSelectionFormatStyleFromNode( + selection: RangeSelection, + node: TextNode | ElementNode, +) { + const format = node.getFormat(); + const style = node.getStyle(); + $updateSelectionFormatStyle(selection, format, style); +} + // This is a work-around is mainly Chrome specific bug where if you select // the contents of an empty block, you cannot easily unselect anything. // This results in a tiny selection box that looks buggy/broken. This can @@ -578,12 +595,11 @@ function onBeforeInput(event: InputEvent, editor: LexicalEditor): void { if ($isRangeSelection(selection)) { const anchorNode = selection.anchor.getNode(); anchorNode.markDirty(); - selection.format = anchorNode.getFormat(); invariant( $isTextNode(anchorNode), 'Anchor node must be a TextNode', ); - selection.style = anchorNode.getStyle(); + $updateSelectionFormatStyleFromNode(selection, anchorNode); } } else { $setCompositionKey(null);