diff --git a/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs b/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs index 150f5cbc43e..e5e8240f08c 100644 --- a/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs @@ -11,6 +11,7 @@ import { moveToEditorBeginning, moveToEnd, moveToStart, + pressShiftEnter, selectAll, selectCharacters, } from '../keyboardShortcuts/index.mjs'; @@ -296,6 +297,57 @@ test.describe('CodeBlock', () => { ); }); + test('Can select a line within line breaks and convert to code block', async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type('aaa'); + await pressShiftEnter(page); + await page.keyboard.type('bbb'); + await pressShiftEnter(page); + await page.keyboard.type('ccc'); + await moveLeft(page, 4); + await selectCharacters(page, 'left', 3); + + await assertHTML( + page, + html` +

+ aaa +
+ bbb +
+ ccc +

+ `, + ); + + await toggleCodeBlock(page); + + await assertHTML( + page, + html` +

+ aaa +

+ + bbb + +

+ ccc +

+ `, + ); + }); + test('Can switch highlighting language in a toolbar', async ({ page, isRichText, diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts b/packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts index 7fd7b173733..05878e260b5 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts @@ -25,10 +25,19 @@ import {$getNearestBlockElementAncestorOrThrow} from '@lexical/utils'; import { $addUpdateTag, $createParagraphNode, + $createRangeSelection, $getSelection, + $isElementNode, + $isLineBreakNode, + $isParagraphNode, $isRangeSelection, $isTextNode, + $setSelection, + $splitNode, + ElementNode, LexicalEditor, + LexicalNode, + RangeSelection, SKIP_DOM_SELECTION_TAG, SKIP_SELECTION_FOCUS_TAG, } from 'lexical'; @@ -236,6 +245,62 @@ export const formatQuote = (editor: LexicalEditor, blockType: string) => { } }; +function $splitParagraphsByLineBreaks(selection: RangeSelection): void { + const blocks: Set = new Set(); + for (const node of selection.getNodes()) { + const block = $isParagraphNode(node) ? node : $findParagraphParent(node); + if (block !== null) { + blocks.add(block); + } + } + for (const point of [selection.anchor, selection.focus]) { + const block = $findParagraphParent(point.getNode()); + if (block !== null) { + blocks.add(block); + } + } + + const anchorKey = selection.anchor.key; + const anchorOffset = selection.anchor.offset; + const anchorType = selection.anchor.type; + const focusKey = selection.focus.key; + const focusOffset = selection.focus.offset; + const focusType = selection.focus.type; + + for (const block of blocks) { + const children = block.getChildren(); + const lbIndices: number[] = []; + for (let i = 0; i < children.length; i++) { + if ($isLineBreakNode(children[i])) { + lbIndices.push(i); + } + } + if (lbIndices.length === 0) { + continue; + } + for (let j = lbIndices.length - 1; j >= 0; j--) { + const [, rightBlock] = $splitNode(block, lbIndices[j]); + const firstChild = rightBlock.getFirstChild(); + if ($isLineBreakNode(firstChild)) { + firstChild.remove(); + } + } + } + + const newSelection = $createRangeSelection(); + newSelection.anchor.set(anchorKey, anchorOffset, anchorType); + newSelection.focus.set(focusKey, focusOffset, focusType); + $setSelection(newSelection); +} + +function $findParagraphParent(node: LexicalNode): ElementNode | null { + if ($isParagraphNode(node)) { + return node; + } + const parent = node.getParent(); + return $isElementNode(parent) && $isParagraphNode(parent) ? parent : null; +} + export const formatCode = (editor: LexicalEditor, blockType: string) => { if (blockType !== 'code') { editor.update(() => { @@ -247,6 +312,11 @@ export const formatCode = (editor: LexicalEditor, blockType: string) => { if (!$isRangeSelection(selection) || selection.isCollapsed()) { $setBlocksType(selection, () => $createCodeNode()); } else { + $splitParagraphsByLineBreaks(selection); + selection = $getSelection(); + if (!$isRangeSelection(selection)) { + return; + } const textContent = selection.getTextContent(); const codeNode = $createCodeNode(); selection.insertNodes([codeNode]);