diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..aa242e73db5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,20 @@ +name: Publish to NPM +on: + push: + branches: + - next + - latest +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v3 + with: + node-version: '16.x' + registry-url: 'https://registry.npmjs.org' + - run: npm run prepare-release + - run: node ./scripts/npm/release.js --non-interactive --dry-run=${{ secrets.RELEASE_DRY_RUN }} --channel $GITHUB_REF_NAME + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml new file mode 100644 index 00000000000..4fda185f03c --- /dev/null +++ b/.github/workflows/version.yml @@ -0,0 +1,34 @@ +name: Create New Release Branch +on: + workflow_dispatch: + inputs: + increment: + description: 'Version Increment' + required: true + default: 'prerelease' + type: choice + options: + - prerelease + - patch + - minor +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ssh-key: ${{ secrets.SSH_KEY }} + fetch-depth: 0 + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v3 + with: + node-version: '16.x' + registry-url: 'https://registry.npmjs.org' + - run: | + git config user.name "Lexical GitHub Actions Bot" + git config user.email "<>" + - run: npm install + - run: npm run increment-version -- --i $INCREMENT + env: + INCREMENT: ${{ inputs.increment }} + - run: git push -u git@github.com:facebook/lexical.git --follow-tags diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f3bd81f6a..a089eb959f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.3.11 (September 5, 2022) +## 0.4.1 (September 5, 2022) - Fix breaking bug for `isEditable` mode in editor initialization (#2945) Tim Laubert - Fix Safari selection highlighting bug (#2943) John Flockton diff --git a/package-lock.json b/package-lock.json index 02ace05f1f7..2b9695d4793 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lexical/monorepo", - "version": "0.4.1", + "version": "0.4.2-next.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lexical/monorepo", - "version": "0.4.1", + "version": "0.4.2-next.0", "license": "MIT", "workspaces": [ "packages/*" diff --git a/package.json b/package.json index 489193d685e..99ba3c47c9a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/monorepo", "description": "Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.", - "version": "0.4.1", + "version": "0.4.2-next.0", "license": "MIT", "private": true, "workspaces": [ @@ -90,8 +90,11 @@ "prepare-release": "npm run build-release && node ./scripts/npm/prepare-release.js", "prepare": "husky install", "prepare-www": "node scripts/www/rewriteImports.js", - "changelog": "func() { git log --oneline ${1}...HEAD --pretty=format:\"- %s %an\"; }; func", + "changelog": "func() { git --no-pager log --oneline ${1}...HEAD --pretty=format:\"- %s %an\"; }; func", + "increment-version": "node ./scripts/npm/increment-version", + "update-changelog": "node ./scripts/npm/update-changelog", "update-version": "node ./scripts/updateVersion", + "postversion": "git checkout -b $npm_package_version && npm install && npm run update-version && npm run update-changelog && git add -A && git commit -m v${npm_package_version} && git tag -a v${npm_package_version} -m v${npm_package_version}", "release": "npm run prepare-release && node ./scripts/npm/release.js" }, "devDependencies": { diff --git a/packages/lexical-clipboard/package.json b/packages/lexical-clipboard/package.json index e4ba5936ee9..6f6929f7f58 100644 --- a/packages/lexical-clipboard/package.json +++ b/packages/lexical-clipboard/package.json @@ -9,16 +9,16 @@ "paste" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalClipboard.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "dependencies": { - "@lexical/utils": "0.4.1", - "@lexical/list": "0.4.1", - "@lexical/selection": "0.4.1", - "@lexical/html": "0.4.1" + "@lexical/utils": "0.4.2-next.0", + "@lexical/list": "0.4.2-next.0", + "@lexical/selection": "0.4.2-next.0", + "@lexical/html": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-clipboard/src/clipboard.ts b/packages/lexical-clipboard/src/clipboard.ts index 98bd0e48525..de74fdb0dad 100644 --- a/packages/lexical-clipboard/src/clipboard.ts +++ b/packages/lexical-clipboard/src/clipboard.ts @@ -24,22 +24,22 @@ import { } from '@lexical/selection'; import {$findMatchingParent} from '@lexical/utils'; import { - $createGridSelection, $createParagraphNode, $getRoot, $getSelection, $isDecoratorNode, $isElementNode, - $isGridCellNode, - $isGridNode, - $isGridRowNode, - $isGridSelection, $isLineBreakNode, $isRangeSelection, $isTextNode, $parseSerializedNode, $setSelection, - GridNode, + DEPRECATED_$createGridSelection, + DEPRECATED_$isGridCellNode, + DEPRECATED_$isGridNode, + DEPRECATED_$isGridRowNode, + DEPRECATED_$isGridSelection, + DEPRECATED_GridNode, SELECTION_CHANGE_COMMAND, } from 'lexical'; import invariant from 'shared/invariant'; @@ -148,15 +148,19 @@ export function $insertGeneratedNodes( selection: RangeSelection | GridSelection, ) { const isSelectionInsideOfGrid = - $isGridSelection(selection) || + DEPRECATED_$isGridSelection(selection) || ($findMatchingParent(selection.anchor.getNode(), (n) => - $isGridCellNode(n), + DEPRECATED_$isGridCellNode(n), ) !== null && $findMatchingParent(selection.focus.getNode(), (n) => - $isGridCellNode(n), + DEPRECATED_$isGridCellNode(n), ) !== null); - if (isSelectionInsideOfGrid && nodes.length === 1 && $isGridNode(nodes[0])) { + if ( + isSelectionInsideOfGrid && + nodes.length === 1 && + DEPRECATED_$isGridNode(nodes[0]) + ) { $mergeGridNodesStrategy(nodes, selection, false, editor); return; } @@ -216,11 +220,11 @@ function $basicInsertStrategy( if ($isRangeSelection(selection)) { selection.insertNodes(topLevelBlocks); - } else if ($isGridSelection(selection)) { + } else if (DEPRECATED_$isGridSelection(selection)) { // If there's an active grid selection and a non grid is pasted, add to the anchor. const anchorCell = selection.anchor.getNode(); - if (!$isGridCellNode(anchorCell)) { + if (!DEPRECATED_$isGridCellNode(anchorCell)) { invariant(false, 'Expected Grid Cell in Grid Selection'); } @@ -234,28 +238,30 @@ function $mergeGridNodesStrategy( isFromLexical: boolean, editor: LexicalEditor, ) { - if (nodes.length !== 1 || !$isGridNode(nodes[0])) { + if (nodes.length !== 1 || !DEPRECATED_$isGridNode(nodes[0])) { invariant(false, '$mergeGridNodesStrategy: Expected Grid insertion.'); } const newGrid = nodes[0]; const newGridRows = newGrid.getChildren(); const newColumnCount = newGrid - .getFirstChildOrThrow() + .getFirstChildOrThrow() .getChildrenSize(); const newRowCount = newGrid.getChildrenSize(); const gridCellNode = $findMatchingParent(selection.anchor.getNode(), (n) => - $isGridCellNode(n), + DEPRECATED_$isGridCellNode(n), ); const gridRowNode = - gridCellNode && $findMatchingParent(gridCellNode, (n) => $isGridRowNode(n)); + gridCellNode && + $findMatchingParent(gridCellNode, (n) => DEPRECATED_$isGridRowNode(n)); const gridNode = - gridRowNode && $findMatchingParent(gridRowNode, (n) => $isGridNode(n)); + gridRowNode && + $findMatchingParent(gridRowNode, (n) => DEPRECATED_$isGridNode(n)); if ( - !$isGridCellNode(gridCellNode) || - !$isGridRowNode(gridRowNode) || - !$isGridNode(gridNode) + !DEPRECATED_$isGridCellNode(gridCellNode) || + !DEPRECATED_$isGridRowNode(gridRowNode) || + !DEPRECATED_$isGridNode(gridNode) ) { invariant( false, @@ -285,13 +291,13 @@ function $mergeGridNodesStrategy( for (let r = fromY; r <= toY; r++) { const currentGridRowNode = gridRowNodes[r]; - if (!$isGridRowNode(currentGridRowNode)) { + if (!DEPRECATED_$isGridRowNode(currentGridRowNode)) { invariant(false, 'getNodes: expected to find GridRowNode'); } const newGridRowNode = newGridRows[newRowIdx]; - if (!$isGridRowNode(newGridRowNode)) { + if (!DEPRECATED_$isGridRowNode(newGridRowNode)) { invariant(false, 'getNodes: expected to find GridRowNode'); } @@ -302,13 +308,13 @@ function $mergeGridNodesStrategy( for (let c = fromX; c <= toX; c++) { const currentGridCellNode = gridCellNodes[c]; - if (!$isGridCellNode(currentGridCellNode)) { + if (!DEPRECATED_$isGridCellNode(currentGridCellNode)) { invariant(false, 'getNodes: expected to find GridCellNode'); } const newGridCellNode = newGridCellNodes[newColumnIdx]; - if (!$isGridCellNode(newGridCellNode)) { + if (!DEPRECATED_$isGridCellNode(newGridCellNode)) { invariant(false, 'getNodes: expected to find GridCellNode'); } @@ -336,7 +342,7 @@ function $mergeGridNodesStrategy( } if (newAnchorCellKey && newFocusCellKey) { - const newGridSelection = $createGridSelection(); + const newGridSelection = DEPRECATED_$createGridSelection(); newGridSelection.set(gridNode.getKey(), newAnchorCellKey, newFocusCellKey); $setSelection(newGridSelection); editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); diff --git a/packages/lexical-code/package.json b/packages/lexical-code/package.json index 6743f251aa9..1e339b21e4e 100644 --- a/packages/lexical-code/package.json +++ b/packages/lexical-code/package.json @@ -8,13 +8,13 @@ "code" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalCode.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "dependencies": { - "@lexical/utils": "0.4.1", + "@lexical/utils": "0.4.2-next.0", "prismjs": "^1.27.0" }, "repository": { diff --git a/packages/lexical-code/src/CodeNode.ts b/packages/lexical-code/src/CodeNode.ts index 55d9c127b80..484d981b723 100644 --- a/packages/lexical-code/src/CodeNode.ts +++ b/packages/lexical-code/src/CodeNode.ts @@ -290,12 +290,13 @@ export function $isCodeNode( } function convertPreElement(domNode: Node): DOMConversionOutput { - return {node: $createCodeNode()}; + return {node: $createCodeNode(), preformatted: true}; } function convertDivElement(domNode: Node): DOMConversionOutput { // domNode is a
since we matched it by nodeName const div = domNode as HTMLDivElement; + const isCode = isCodeElement(div); return { after: (childLexicalNodes) => { const domParent = domNode.parentNode; @@ -304,12 +305,13 @@ function convertDivElement(domNode: Node): DOMConversionOutput { } return childLexicalNodes; }, - node: isCodeElement(div) ? $createCodeNode() : null, + node: isCode ? $createCodeNode() : null, + preformatted: isCode, }; } function convertTableElement(): DOMConversionOutput { - return {node: $createCodeNode()}; + return {node: $createCodeNode(), preformatted: true}; } function convertCodeNoop(): DOMConversionOutput { diff --git a/packages/lexical-dragon/package.json b/packages/lexical-dragon/package.json index 21e3812d691..30c9d42a894 100644 --- a/packages/lexical-dragon/package.json +++ b/packages/lexical-dragon/package.json @@ -9,10 +9,10 @@ "accessibility" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalDragon.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-file/package.json b/packages/lexical-file/package.json index 9fbeb5be558..ee9a14987c2 100644 --- a/packages/lexical-file/package.json +++ b/packages/lexical-file/package.json @@ -10,10 +10,10 @@ "export" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalFile.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-hashtag/package.json b/packages/lexical-hashtag/package.json index 5714d7963fc..d15dc83f1d4 100644 --- a/packages/lexical-hashtag/package.json +++ b/packages/lexical-hashtag/package.json @@ -8,13 +8,13 @@ "hashtag" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalHashtag.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "dependencies": { - "@lexical/utils": "0.4.1" + "@lexical/utils": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-headless/package.json b/packages/lexical-headless/package.json index eadd6e3564b..d069387fbc0 100644 --- a/packages/lexical-headless/package.json +++ b/packages/lexical-headless/package.json @@ -8,10 +8,10 @@ "headless" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalHeadless.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-history/package.json b/packages/lexical-history/package.json index a4855a847ec..71d1fcc3101 100644 --- a/packages/lexical-history/package.json +++ b/packages/lexical-history/package.json @@ -8,13 +8,13 @@ "history" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalHistory.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "dependencies": { - "@lexical/utils": "0.4.1" + "@lexical/utils": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-html/package.json b/packages/lexical-html/package.json index 7e4c34ae74b..6e003b79f80 100644 --- a/packages/lexical-html/package.json +++ b/packages/lexical-html/package.json @@ -8,10 +8,10 @@ "html" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalHtml.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "repository": { "type": "git", @@ -19,6 +19,6 @@ "directory": "packages/lexical-html" }, "dependencies": { - "@lexical/selection": "0.4.1" + "@lexical/selection": "0.4.2-next.0" } } diff --git a/packages/lexical-html/src/index.ts b/packages/lexical-html/src/index.ts index d3a4d1fa27a..a8d92b002fa 100644 --- a/packages/lexical-html/src/index.ts +++ b/packages/lexical-html/src/index.ts @@ -172,6 +172,7 @@ function $createNodesFromDOM( editor: LexicalEditor, forChildMap: Map = new Map(), parentLexicalNode?: LexicalNode | null | undefined, + preformatted = false, ): Array { let lexicalNodes: Array = []; @@ -182,7 +183,7 @@ function $createNodesFromDOM( let currentLexicalNode = null; const transformFunction = getConversionFunction(node, editor); const transformOutput = transformFunction - ? transformFunction(node as HTMLElement) + ? transformFunction(node as HTMLElement, undefined, preformatted) : null; let postTransform = null; @@ -224,6 +225,8 @@ function $createNodesFromDOM( editor, new Map(forChildMap), currentLexicalNode, + preformatted || + (transformOutput && transformOutput.preformatted) === true, ), ); } diff --git a/packages/lexical-link/package.json b/packages/lexical-link/package.json index d4c87c9a888..ca2b4a47237 100644 --- a/packages/lexical-link/package.json +++ b/packages/lexical-link/package.json @@ -8,13 +8,13 @@ "link" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalLink.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "dependencies": { - "@lexical/utils": "0.4.1" + "@lexical/utils": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-list/package.json b/packages/lexical-list/package.json index 6b315ab47d6..d653fa8b45e 100644 --- a/packages/lexical-list/package.json +++ b/packages/lexical-list/package.json @@ -8,13 +8,13 @@ "list" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalList.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "dependencies": { - "@lexical/utils": "0.4.1" + "@lexical/utils": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-mark/package.json b/packages/lexical-mark/package.json index 452a1d2b99c..d0f82da65b1 100644 --- a/packages/lexical-mark/package.json +++ b/packages/lexical-mark/package.json @@ -8,13 +8,13 @@ "mark" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalMark.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "dependencies": { - "@lexical/utils": "0.4.1" + "@lexical/utils": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-markdown/package.json b/packages/lexical-markdown/package.json index f5b406de312..7c31ea82a4f 100644 --- a/packages/lexical-markdown/package.json +++ b/packages/lexical-markdown/package.json @@ -8,18 +8,18 @@ "markdown" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalMarkdown.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "dependencies": { - "@lexical/utils": "0.4.1", - "@lexical/code": "0.4.1", - "@lexical/text": "0.4.1", - "@lexical/rich-text": "0.4.1", - "@lexical/list": "0.4.1", - "@lexical/link": "0.4.1" + "@lexical/utils": "0.4.2-next.0", + "@lexical/code": "0.4.2-next.0", + "@lexical/text": "0.4.2-next.0", + "@lexical/rich-text": "0.4.2-next.0", + "@lexical/list": "0.4.2-next.0", + "@lexical/link": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts index aac45929f58..1e04937430d 100644 --- a/packages/lexical-markdown/src/MarkdownImport.ts +++ b/packages/lexical-markdown/src/MarkdownImport.ts @@ -27,6 +27,7 @@ import { $isParagraphNode, $isTextNode, } from 'lexical'; +import {IS_IOS, IS_SAFARI} from 'shared/environment'; import {PUNCTUATION_OR_SPACE, transformersByType} from './utils'; @@ -355,22 +356,36 @@ function createTextFormatTransformersIndex( const transformersByTag: Record = {}; const fullMatchRegExpByTag: Record = {}; const openTagsRegExp = []; + const escapeRegExp = `(?; -const replaceWithBlock = ( +const createBlockNode = ( createNode: (match: Array) => ElementNode, ): ElementTransformer['replace'] => { return (parentNode, children, match) => { const node = createNode(match); node.append(...children); - parentNode.replace(node); + if ($isParagraphNode(parentNode)) { + parentNode.replace(node); + } node.select(0, 0); }; }; @@ -165,7 +170,7 @@ export const HEADING: ElementTransformer = { return '#'.repeat(level) + ' ' + exportChildren(node); }, regExp: /^(#{1,6})\s/, - replace: replaceWithBlock((match) => { + replace: createBlockNode((match) => { const tag = ('h' + match[1].length) as HeadingTagType; return $createHeadingNode(tag); }), @@ -225,7 +230,7 @@ export const CODE: ElementTransformer = { ); }, regExp: /^```(\w{1,10})?\s/, - replace: replaceWithBlock((match) => { + replace: createBlockNode((match) => { return $createCodeNode(match ? match[1] : undefined); }), type: 'element', diff --git a/packages/lexical-offset/package.json b/packages/lexical-offset/package.json index f20d445fc42..bf28752a22d 100644 --- a/packages/lexical-offset/package.json +++ b/packages/lexical-offset/package.json @@ -8,10 +8,10 @@ "offset" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalOffset.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-overflow/package.json b/packages/lexical-overflow/package.json index 51a0c41396c..3f1fa7e2d16 100644 --- a/packages/lexical-overflow/package.json +++ b/packages/lexical-overflow/package.json @@ -8,10 +8,10 @@ "overflow" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalOverflow.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-plain-text/README.md b/packages/lexical-plain-text/README.md index c9a0de74969..d9ef073a86c 100644 --- a/packages/lexical-plain-text/README.md +++ b/packages/lexical-plain-text/README.md @@ -2,4 +2,4 @@ This package provides a starting point for Lexical users by registering listeners for a set of basic commands that cover simple text-editing behavior such as entering text, deleting characters, copy + paste, or changing the selection with arrow keys. -You can use this package as a starting point, and then add additional command listeners to customize the functionality of your editor. If you want to add rich-text features, such as headings, blockquotes, or formatted text, you may want to consider using [@lexical/rich-text](https://lexical.dev/docs/api/lexical-rich-text) instead. +You can use this package as a starting point, and then add additional command listeners to customize the functionality of your editor. If you want to add rich-text features, such as headings, blockquotes, or formatted text, you may want to consider using [@lexical/rich-text](https://lexical.dev/docs/packages/lexical-rich-text) instead. diff --git a/packages/lexical-plain-text/package.json b/packages/lexical-plain-text/package.json index ce503891e78..ea7ca60ace3 100644 --- a/packages/lexical-plain-text/package.json +++ b/packages/lexical-plain-text/package.json @@ -7,13 +7,13 @@ "plain-text" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalPlainText.js", "peerDependencies": { - "lexical": "0.4.1", - "@lexical/utils": "0.4.1", - "@lexical/selection": "0.4.1", - "@lexical/clipboard": "0.4.1" + "lexical": "0.4.2-next.0", + "@lexical/utils": "0.4.2-next.0", + "@lexical/selection": "0.4.2-next.0", + "@lexical/clipboard": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste.spec.mjs index c3128b5578a..7918172b3bb 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste.spec.mjs @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * */ +import {expect} from '@playwright/test'; import { moveLeft, @@ -1985,36 +1986,26 @@ test.describe('CopyAndPaste', () => { test('HTML Copy + paste multi line html with extra newlines', async ({ page, isPlainText, + isCollab, }) => { - test.skip(isPlainText); + test.skip(isPlainText || isCollab); await focusEditor(page); await pasteFromClipboard(page, { - 'text/html': ` -

Hello - -

+ 'text/html': + '

Hello\n

\n\n

\n\nWorld\n\n

\n\n

Hello\n\n World \n\nThere\n\n

', + }); -

World

+ const paragraphs = page.locator('div[contenteditable="true"] > p'); + await expect(paragraphs).toHaveCount(3); - `, + // Explicitly checking inner text, since regular assertHTML will prettify it and strip all + // extra newlines, which makes this test less acurate + await expect(paragraphs.nth(0)).toHaveText('Hello', {useInnerText: true}); + await expect(paragraphs.nth(1)).toHaveText('World', {useInnerText: true}); + await expect(paragraphs.nth(2)).toHaveText('Hello World There', { + useInnerText: true, }); - - await assertHTML( - page, - html` -

- Hello -

-

- World -

- `, - ); }); test('HTML Copy + paste in front of or after a link', async ({ diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index 8c775138e69..ebb6f6661f5 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -1,6 +1,6 @@ { "name": "lexical-playground", - "version": "0.4.1", + "version": "0.4.2-next.0", "private": true, "scripts": { "dev": "vite --host", @@ -11,22 +11,22 @@ }, "dependencies": { "@excalidraw/excalidraw": "0.11.0", - "@lexical/clipboard": "0.4.1", - "@lexical/code": "0.4.1", - "@lexical/file": "0.4.1", - "@lexical/hashtag": "0.4.1", - "@lexical/link": "0.4.1", - "@lexical/list": "0.4.1", - "@lexical/mark": "0.4.1", - "@lexical/overflow": "0.4.1", - "@lexical/plain-text": "0.4.1", - "@lexical/react": "0.4.1", - "@lexical/rich-text": "0.4.1", - "@lexical/selection": "0.4.1", - "@lexical/table": "0.4.1", - "@lexical/utils": "0.4.1", + "@lexical/clipboard": "0.4.2-next.0", + "@lexical/code": "0.4.2-next.0", + "@lexical/file": "0.4.2-next.0", + "@lexical/hashtag": "0.4.2-next.0", + "@lexical/link": "0.4.2-next.0", + "@lexical/list": "0.4.2-next.0", + "@lexical/mark": "0.4.2-next.0", + "@lexical/overflow": "0.4.2-next.0", + "@lexical/plain-text": "0.4.2-next.0", + "@lexical/react": "0.4.2-next.0", + "@lexical/rich-text": "0.4.2-next.0", + "@lexical/selection": "0.4.2-next.0", + "@lexical/table": "0.4.2-next.0", + "@lexical/utils": "0.4.2-next.0", "katex": "^0.15.2", - "lexical": "0.4.1", + "lexical": "0.4.2-next.0", "link-preview-generator": "1.0.7", "lodash-es": "^4.17.21", "prettier": "^2.3.2", diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index df0f1a5be65..f036a9a47db 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -51,6 +51,7 @@ import MentionsPlugin from './plugins/MentionsPlugin'; import PollPlugin from './plugins/PollPlugin'; import SpeechToTextPlugin from './plugins/SpeechToTextPlugin'; import TabFocusPlugin from './plugins/TabFocusPlugin'; +import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin'; import TableOfContentsPlugin from './plugins/TableOfContentsPlugin'; import {TablePlugin as NewTablePlugin} from './plugins/TablePlugin'; import ToolbarPlugin from './plugins/ToolbarPlugin'; @@ -186,6 +187,7 @@ export default function Editor(): JSX.Element { <> + diff --git a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx index 0496f3b79ef..f9a3ce9bdce 100644 --- a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx @@ -26,9 +26,9 @@ import { } from '@lexical/table'; import { $getSelection, - $isGridSelection, $isRangeSelection, $setSelection, + DEPRECATED_$isGridSelection, } from 'lexical'; import * as React from 'react'; import {ReactPortal, useCallback, useEffect, useRef, useState} from 'react'; @@ -72,7 +72,7 @@ function TableActionMenu({ editor.getEditorState().read(() => { const selection = $getSelection(); - if ($isGridSelection(selection)) { + if (DEPRECATED_$isGridSelection(selection)) { const selectionShape = selection.getShape(); updateSelectionCounts({ @@ -153,7 +153,7 @@ function TableActionMenu({ let tableRowIndex; - if ($isGridSelection(selection)) { + if (DEPRECATED_$isGridSelection(selection)) { const selectionShape = selection.getShape(); tableRowIndex = shouldInsertAfter ? selectionShape.toY @@ -189,7 +189,7 @@ function TableActionMenu({ let tableColumnIndex; - if ($isGridSelection(selection)) { + if (DEPRECATED_$isGridSelection(selection)) { const selectionShape = selection.getShape(); tableColumnIndex = shouldInsertAfter ? selectionShape.toX @@ -199,11 +199,14 @@ function TableActionMenu({ $getTableColumnIndexFromTableCellNode(tableCellNode); } + const grid = $getElementGridForTableNode(editor, tableNode); + $insertTableColumn( tableNode, tableColumnIndex, shouldInsertAfter, selectionCounts.columns, + grid, ); clearTableSelection(); diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx index bf64984832b..e3307d03ec4 100644 --- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx +++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx @@ -22,8 +22,8 @@ import { import { $getNearestNodeFromDOMNode, $getSelection, - $isGridSelection, COMMAND_PRIORITY_HIGH, + DEPRECATED_$isGridSelection, SELECTION_CHANGE_COMMAND, } from 'lexical'; import * as React from 'react'; @@ -67,7 +67,7 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { SELECTION_CHANGE_COMMAND, (payload) => { const selection = $getSelection(); - const isGridSelection = $isGridSelection(selection); + const isGridSelection = DEPRECATED_$isGridSelection(selection); if (isSelectingGrid !== isGridSelection) { updateIsSelectingGrid(isGridSelection); diff --git a/packages/lexical-react/flow/LexicalComposer.js.flow b/packages/lexical-react/flow/LexicalComposer.js.flow index a784c43c0ec..766c63a4940 100644 --- a/packages/lexical-react/flow/LexicalComposer.js.flow +++ b/packages/lexical-react/flow/LexicalComposer.js.flow @@ -14,7 +14,7 @@ import type {InitialEditorStateType as InitialEditorStateRichTextType} from '@le type Props = { initialConfig: $ReadOnly<{ editor__DEPRECATED?: LexicalEditor | null, - readOnly?: boolean, + editable?: boolean, namespace: string, nodes?: $ReadOnlyArray>, theme?: EditorThemeClasses, diff --git a/packages/lexical-react/flow/LexicalTableOfContents__EXPERIMENTAL.js.flow b/packages/lexical-react/flow/LexicalTableOfContents__EXPERIMENTAL.js.flow index e7d4fb2b334..89cf65fdd59 100644 --- a/packages/lexical-react/flow/LexicalTableOfContents__EXPERIMENTAL.js.flow +++ b/packages/lexical-react/flow/LexicalTableOfContents__EXPERIMENTAL.js.flow @@ -10,7 +10,7 @@ import type {HeadingTagType} from '@lexical/rich-text'; import type {NodeKey} from 'lexical'; -declare export function LexicalTableOfContentsPlugin({ +declare export default function LexicalTableOfContentsPlugin({ children: ( tableOfContents: Array<[NodeKey, string, HeadingTagType]>, ) => React$Node, diff --git a/packages/lexical-react/package.json b/packages/lexical-react/package.json index c0541f6b708..402377ba0ac 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -8,28 +8,28 @@ "rich-text" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "dependencies": { - "@lexical/clipboard": "0.4.1", - "@lexical/code": "0.4.1", - "@lexical/dragon": "0.4.1", - "@lexical/hashtag": "0.4.1", - "@lexical/history": "0.4.1", - "@lexical/link": "0.4.1", - "@lexical/list": "0.4.1", - "@lexical/mark": "0.4.1", - "@lexical/markdown": "0.4.1", - "@lexical/overflow": "0.4.1", - "@lexical/plain-text": "0.4.1", - "@lexical/rich-text": "0.4.1", - "@lexical/selection": "0.4.1", - "@lexical/table": "0.4.1", - "@lexical/text": "0.4.1", - "@lexical/utils": "0.4.1", - "@lexical/yjs": "0.4.1" + "@lexical/clipboard": "0.4.2-next.0", + "@lexical/code": "0.4.2-next.0", + "@lexical/dragon": "0.4.2-next.0", + "@lexical/hashtag": "0.4.2-next.0", + "@lexical/history": "0.4.2-next.0", + "@lexical/link": "0.4.2-next.0", + "@lexical/list": "0.4.2-next.0", + "@lexical/mark": "0.4.2-next.0", + "@lexical/markdown": "0.4.2-next.0", + "@lexical/overflow": "0.4.2-next.0", + "@lexical/plain-text": "0.4.2-next.0", + "@lexical/rich-text": "0.4.2-next.0", + "@lexical/selection": "0.4.2-next.0", + "@lexical/table": "0.4.2-next.0", + "@lexical/text": "0.4.2-next.0", + "@lexical/utils": "0.4.2-next.0", + "@lexical/yjs": "0.4.2-next.0" }, "peerDependencies": { - "lexical": "0.4.1", + "lexical": "0.4.2-next.0", "react": ">=17.x", "react-dom": ">=17.x" }, diff --git a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts index 07cb428ffe0..bf4ca437f45 100644 --- a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts +++ b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts @@ -133,8 +133,9 @@ function handleLinkCreation( ); } + const nodeFormat = node.__format; const linkNode = $createAutoLinkNode(match.url); - linkNode.append($createTextNode(match.text)); + linkNode.append($createTextNode(match.text).setFormat(nodeFormat)); middleNode.replace(linkNode); onChange(match.url, null); } diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx index a6931b0354e..1850ff44a36 100644 --- a/packages/lexical-react/src/LexicalContentEditable.tsx +++ b/packages/lexical-react/src/LexicalContentEditable.tsx @@ -58,7 +58,7 @@ export function ContentEditable({ testid, }: Props): JSX.Element { const [editor] = useLexicalComposerContext(); - const [isEditable, setEditable] = useState(true); + const [isEditable, setEditable] = useState(false); const ref = useCallback( (rootElement: null | HTMLElement) => { diff --git a/packages/lexical-react/src/LexicalTreeView.tsx b/packages/lexical-react/src/LexicalTreeView.tsx index c865d320d54..9e1c720f883 100644 --- a/packages/lexical-react/src/LexicalTreeView.tsx +++ b/packages/lexical-react/src/LexicalTreeView.tsx @@ -22,9 +22,9 @@ import { $getRoot, $getSelection, $isElementNode, - $isGridSelection, $isRangeSelection, $isTextNode, + DEPRECATED_$isGridSelection, } from 'lexical'; import * as React from 'react'; import {useEffect, useRef, useState} from 'react'; @@ -280,7 +280,7 @@ function generateContent(editorState: EditorState): string { ? ': null' : $isRangeSelection(selection) ? printRangeSelection(selection) - : $isGridSelection(selection) + : DEPRECATED_$isGridSelection(selection) ? printGridSelection(selection) : printObjectSelection(selection); }); diff --git a/packages/lexical-rich-text/README.md b/packages/lexical-rich-text/README.md index 84e406e61d2..ee4efe98710 100644 --- a/packages/lexical-rich-text/README.md +++ b/packages/lexical-rich-text/README.md @@ -2,4 +2,4 @@ This package provides a starting point for Lexical users by registering listeners for a set of basic commands that cover simple text-editing behavior such as entering text, deleting characters, copy + paste, or changing the selection with arrow keys. It also provides default behavior for rich text features, such as headings, formatted, text and blockquotes. -You can use this package as a starting point, and then add additional command listeners to customize the functionality of your editor. If you don't want or need rich text functionality, you may want to consider using [@lexical/plain-text](https://lexical.dev/docs/api/lexical-plain-text) instead. +You can use this package as a starting point, and then add additional command listeners to customize the functionality of your editor. If you don't want or need rich text functionality, you may want to consider using [@lexical/plain-text](https://lexical.dev/docs/packages/lexical-plain-text) instead. diff --git a/packages/lexical-rich-text/package.json b/packages/lexical-rich-text/package.json index a3913e292d2..f2ea540e9ec 100644 --- a/packages/lexical-rich-text/package.json +++ b/packages/lexical-rich-text/package.json @@ -7,13 +7,13 @@ "rich-text" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalRichText.js", "peerDependencies": { - "lexical": "0.4.1", - "@lexical/selection": "0.4.1", - "@lexical/clipboard": "0.4.1", - "@lexical/utils": "0.4.1" + "lexical": "0.4.2-next.0", + "@lexical/selection": "0.4.2-next.0", + "@lexical/clipboard": "0.4.2-next.0", + "@lexical/utils": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index 436f73d8224..ab094ded0cd 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -43,7 +43,6 @@ import { $getRoot, $getSelection, $isDecoratorNode, - $isGridSelection, $isNodeSelection, $isRangeSelection, $isRootNode, @@ -56,6 +55,7 @@ import { DELETE_CHARACTER_COMMAND, DELETE_LINE_COMMAND, DELETE_WORD_COMMAND, + DEPRECATED_$isGridSelection, DRAGSTART_COMMAND, DROP_COMMAND, ElementNode, @@ -425,7 +425,7 @@ function onPasteForRichText( : event.clipboardData; if ( clipboardData != null && - ($isRangeSelection(selection) || $isGridSelection(selection)) + ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) ) { $insertDataTransferForRichText(clipboardData, selection, editor); } @@ -581,11 +581,14 @@ export function registerRichText( if (typeof eventOrText === 'string') { if ($isRangeSelection(selection)) { selection.insertText(eventOrText); - } else if ($isGridSelection(selection)) { + } else if (DEPRECATED_$isGridSelection(selection)) { // TODO: Insert into the first cell & clear selection. } } else { - if (!$isRangeSelection(selection) && !$isGridSelection(selection)) { + if ( + !$isRangeSelection(selection) && + !DEPRECATED_$isGridSelection(selection) + ) { return false; } @@ -945,7 +948,10 @@ export function registerRichText( PASTE_COMMAND, (event) => { const selection = $getSelection(); - if ($isRangeSelection(selection) || $isGridSelection(selection)) { + if ( + $isRangeSelection(selection) || + DEPRECATED_$isGridSelection(selection) + ) { onPasteForRichText(event, editor); return true; } diff --git a/packages/lexical-selection/package.json b/packages/lexical-selection/package.json index 43677971a5f..4aecb801231 100644 --- a/packages/lexical-selection/package.json +++ b/packages/lexical-selection/package.json @@ -9,10 +9,10 @@ "selection" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalSelection.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx index dff147b2b58..c5ef23e13ee 100644 --- a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx +++ b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx @@ -12,10 +12,13 @@ import {useLexicalComposerContext} from '@lexical/react/src/LexicalComposerConte import {ContentEditable} from '@lexical/react/src/LexicalContentEditable'; import {HistoryPlugin} from '@lexical/react/src/LexicalHistoryPlugin'; import {RichTextPlugin} from '@lexical/react/src/LexicalRichTextPlugin'; +import {$createHeadingNode} from '@lexical/rich-text'; import { $addNodeStyle, $getSelectionStyleValueForProperty, + $wrapLeafNodesInElements, } from '@lexical/selection'; +import {$createTableNodeWithDimensions} from '@lexical/table'; import { $createLineBreakNode, $createParagraphNode, @@ -2251,4 +2254,336 @@ describe('LexicalSelection tests', () => { }); }); }); + + describe('$wrapLeafNodesInElements', () => { + test('Collapsed selection in text', async () => { + const testEditor = createTestEditor(); + const element = document.createElement('div'); + testEditor.setRootElement(element); + + await testEditor.update(() => { + const root = $getRoot(); + const paragraph1 = $createParagraphNode(); + const text1 = $createTextNode('text 1'); + const paragraph2 = $createParagraphNode(); + const text2 = $createTextNode('text 2'); + root.append(paragraph1, paragraph2); + paragraph1.append(text1); + paragraph2.append(text2); + + const selection = $createRangeSelection(); + $setSelection(selection); + setAnchorPoint({ + key: text1.__key, + offset: text1.length, + type: 'text', + }); + setFocusPoint({ + key: text1.__key, + offset: text1.length, + type: 'text', + }); + + $wrapLeafNodesInElements(selection, () => { + return $createHeadingNode('h1'); + }); + + const rootChildren = root.getChildren(); + expect(rootChildren[0].__type).toBe('heading'); + expect(rootChildren[1].__type).toBe('paragraph'); + expect(rootChildren.length).toBe(2); + }); + }); + + test('Collapsed selection in element', async () => { + const testEditor = createTestEditor(); + const element = document.createElement('div'); + testEditor.setRootElement(element); + + await testEditor.update(() => { + const root = $getRoot(); + const paragraph1 = $createParagraphNode(); + const paragraph2 = $createParagraphNode(); + root.append(paragraph1, paragraph2); + + const selection = $createRangeSelection(); + $setSelection(selection); + setAnchorPoint({ + key: 'root', + offset: 0, + type: 'element', + }); + setFocusPoint({ + key: 'root', + offset: 0, + type: 'element', + }); + + $wrapLeafNodesInElements(selection, () => { + return $createHeadingNode('h1'); + }); + + const rootChildren = root.getChildren(); + expect(rootChildren[0].__type).toBe('heading'); + expect(rootChildren[1].__type).toBe('paragraph'); + expect(rootChildren.length).toBe(2); + }); + }); + + test('Two elements, same top-element', async () => { + const testEditor = createTestEditor(); + const element = document.createElement('div'); + testEditor.setRootElement(element); + + await testEditor.update(() => { + const root = $getRoot(); + const paragraph1 = $createParagraphNode(); + const text1 = $createTextNode('text 1'); + const paragraph2 = $createParagraphNode(); + const text2 = $createTextNode('text 2'); + root.append(paragraph1, paragraph2); + paragraph1.append(text1); + paragraph2.append(text2); + + const selection = $createRangeSelection(); + $setSelection(selection); + setAnchorPoint({ + key: text1.__key, + offset: 0, + type: 'text', + }); + setFocusPoint({ + key: text2.__key, + offset: text1.length, + type: 'text', + }); + + $wrapLeafNodesInElements(selection, () => { + return $createHeadingNode('h1'); + }); + + const rootChildren = root.getChildren(); + expect(rootChildren[0].__type).toBe('heading'); + expect(rootChildren[1].__type).toBe('heading'); + expect(rootChildren.length).toBe(2); + }); + }); + + test('Two empty elements, same top-element', async () => { + const testEditor = createTestEditor(); + const element = document.createElement('div'); + testEditor.setRootElement(element); + + await testEditor.update(() => { + const root = $getRoot(); + const paragraph1 = $createParagraphNode(); + const paragraph2 = $createParagraphNode(); + root.append(paragraph1, paragraph2); + + const selection = $createRangeSelection(); + $setSelection(selection); + setAnchorPoint({ + key: paragraph1.__key, + offset: 0, + type: 'element', + }); + setFocusPoint({ + key: paragraph2.__key, + offset: 0, + type: 'element', + }); + + $wrapLeafNodesInElements(selection, () => { + return $createHeadingNode('h1'); + }); + + const rootChildren = root.getChildren(); + expect(rootChildren[0].__type).toBe('heading'); + expect(rootChildren[1].__type).toBe('heading'); + expect(rootChildren.length).toBe(2); + }); + }); + + test('Two elements, same top-element', async () => { + const testEditor = createTestEditor(); + const element = document.createElement('div'); + testEditor.setRootElement(element); + + await testEditor.update(() => { + const root = $getRoot(); + const paragraph1 = $createParagraphNode(); + const text1 = $createTextNode('text 1'); + const paragraph2 = $createParagraphNode(); + const text2 = $createTextNode('text 2'); + root.append(paragraph1, paragraph2); + paragraph1.append(text1); + paragraph2.append(text2); + + const selection = $createRangeSelection(); + $setSelection(selection); + setAnchorPoint({ + key: text1.__key, + offset: 0, + type: 'text', + }); + setFocusPoint({ + key: text2.__key, + offset: text1.length, + type: 'text', + }); + + $wrapLeafNodesInElements(selection, () => { + return $createHeadingNode('h1'); + }); + + const rootChildren = root.getChildren(); + expect(rootChildren[0].__type).toBe('heading'); + expect(rootChildren[1].__type).toBe('heading'); + expect(rootChildren.length).toBe(2); + }); + }); + + test('Collapsed in element inside top-element', async () => { + const testEditor = createTestEditor(); + const element = document.createElement('div'); + testEditor.setRootElement(element); + + await testEditor.update(() => { + const root = $getRoot(); + const table = $createTableNodeWithDimensions(1, 1); + const row = table.getFirstChild(); + const column = row.getFirstChild(); + const paragraph = column.getFirstChild(); + root.append(table); + + const selection = $createRangeSelection(); + $setSelection(selection); + setAnchorPoint({ + key: paragraph.__key, + offset: 0, + type: 'element', + }); + setFocusPoint({ + key: paragraph.__key, + offset: 0, + type: 'element', + }); + + const columnChildrenPrev = column.getChildren(); + expect(columnChildrenPrev[0].__type).toBe('paragraph'); + $wrapLeafNodesInElements(selection, () => { + return $createHeadingNode('h1'); + }); + + const columnChildrenAfter = column.getChildren(); + expect(columnChildrenAfter[0].__type).toBe('heading'); + expect(columnChildrenAfter.length).toBe(1); + }); + }); + + test('Collapsed in text inside top-element', async () => { + const testEditor = createTestEditor(); + const element = document.createElement('div'); + testEditor.setRootElement(element); + + await testEditor.update(() => { + const root = $getRoot(); + const table = $createTableNodeWithDimensions(1, 1); + const row = table.getFirstChild(); + const column = row.getFirstChild(); + const paragraph = column.getFirstChild(); + const text = $createTextNode('foo'); + root.append(table); + paragraph.append(text); + + const selectionz = $createRangeSelection(); + $setSelection(selectionz); + setAnchorPoint({ + key: text.__key, + offset: text.length, + type: 'text', + }); + setFocusPoint({ + key: text.__key, + offset: text.length, + type: 'text', + }); + // @ts-ignore + const selection = $getSelection() as RangeSelection; + + const columnChildrenPrev = column.getChildren(); + expect(columnChildrenPrev[0].__type).toBe('paragraph'); + $wrapLeafNodesInElements(selection, () => { + return $createHeadingNode('h1'); + }); + + const columnChildrenAfter = column.getChildren(); + expect(columnChildrenAfter[0].__type).toBe('heading'); + expect(columnChildrenAfter.length).toBe(1); + }); + }); + + test('Full editor selection with a mix of top-elements', async () => { + const testEditor = createTestEditor(); + const element = document.createElement('div'); + testEditor.setRootElement(element); + + await testEditor.update(() => { + const root = $getRoot(); + + const paragraph1 = $createParagraphNode(); + const paragraph2 = $createParagraphNode(); + const text1 = $createTextNode(); + const text2 = $createTextNode(); + paragraph1.append(text1); + paragraph2.append(text2); + root.append(paragraph1, paragraph2); + + const table = $createTableNodeWithDimensions(1, 2); + const row = table.getFirstChild(); + const columns = row.getChildren(); + root.append(table); + + const column1 = columns[0]; + const paragraph3 = $createParagraphNode(); + const paragraph4 = $createParagraphNode(); + const text3 = $createTextNode(); + const text4 = $createTextNode(); + paragraph1.append(text3); + paragraph2.append(text4); + column1.append(paragraph3, paragraph4); + + const column2 = columns[1]; + const paragraph5 = $createParagraphNode(); + const paragraph6 = $createParagraphNode(); + column2.append(paragraph5, paragraph6); + + const paragraph7 = $createParagraphNode(); + root.append(paragraph7); + + const selectionz = $createRangeSelection(); + $setSelection(selectionz); + setAnchorPoint({ + key: paragraph1.__key, + offset: 0, + type: 'element', + }); + setFocusPoint({ + key: paragraph7.__key, + offset: 0, + type: 'element', + }); + // @ts-ignore + const selection = $getSelection() as RangeSelection; + + $wrapLeafNodesInElements(selection, () => { + return $createHeadingNode('h1'); + }); + + expect(JSON.stringify(testEditor._pendingEditorState.toJSON())).toBe( + '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"headerState":3},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"headerState":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1}],"direction":null,"format":"","indent":0,"type":"table","version":1},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}', + ); + }); + }); + }); }); diff --git a/packages/lexical-selection/src/index.ts b/packages/lexical-selection/src/index.ts index bfd728ec5dc..4add886a2b5 100644 --- a/packages/lexical-selection/src/index.ts +++ b/packages/lexical-selection/src/index.ts @@ -7,31 +7,30 @@ * */ -import type { - ElementNode, - GridSelection, - LexicalEditor, - LexicalNode, - NodeKey, - NodeSelection, - Point, - RangeSelection, - TextNode, -} from 'lexical'; - import { $createTextNode, $getDecoratorNode, $getNodeByKey, $getPreviousSelection, + $hasAncestor, $isDecoratorNode, $isElementNode, - $isGridSelection, $isLeafNode, $isRangeSelection, $isRootNode, $isTextNode, + $isTopLevel, $setSelection, + DEPRECATED_$isGridSelection, + ElementNode, + GridSelection, + LexicalEditor, + LexicalNode, + NodeKey, + NodeSelection, + Point, + RangeSelection, + TextNode, } from 'lexical'; import invariant from 'shared/invariant'; @@ -281,7 +280,7 @@ function $cloneContentsImpl( nodeMap: Array.from(nodeMap.entries()), range, }; - } else if ($isGridSelection(selection)) { + } else if (DEPRECATED_$isGridSelection(selection)) { const nodeMap = selection.getNodes().map<[NodeKey, LexicalNode]>((node) => { const nodeKey = node.getKey(); @@ -575,7 +574,7 @@ export function $selectAll(selection: RangeSelection): void { function $removeParentEmptyElements(startingNode: ElementNode): void { let node: ElementNode | null = startingNode; - while (node !== null && !$isRootNode(node)) { + while (node !== null && !$isTopLevel(node)) { const latest = node.getLatest(); const parentNode: ElementNode | null = node.getParent(); @@ -587,10 +586,11 @@ function $removeParentEmptyElements(startingNode: ElementNode): void { } } +// TODO 0.6 Rename to $wrapDescendantNodesInElements export function $wrapLeafNodesInElements( selection: RangeSelection, createElement: () => ElementNode, - wrappingElement?: ElementNode, + wrappingElement: null | ElementNode = null, ): void { const nodes = selection.getNodes(); const nodesLength = nodes.length; @@ -621,6 +621,60 @@ export function $wrapLeafNodesInElements( return; } + let topLevelNode = null; + let descendants: LexicalNode[] = []; + for (let i = 0; i < nodesLength; i++) { + const node = nodes[i]; + // Determine whether wrapping has to be broken down into multiple chunks. This can happen if the + // user selected multiple top-level nodes that have to be treated separately as if they are + // their own branch. I.e. you don't want to wrap a whole table, but rather the contents of each + // of each of the cell nodes. + if ($isTopLevel(node)) { + $wrapLeafNodesInElementsImpl( + selection, + descendants, + descendants.length, + createElement, + wrappingElement, + ); + descendants = []; + topLevelNode = node; + } else if ( + topLevelNode === null || + (topLevelNode !== null && $hasAncestor(node, topLevelNode)) + ) { + descendants.push(node); + } else { + $wrapLeafNodesInElementsImpl( + selection, + descendants, + descendants.length, + createElement, + wrappingElement, + ); + descendants = [node]; + } + } + $wrapLeafNodesInElementsImpl( + selection, + descendants, + descendants.length, + createElement, + wrappingElement, + ); +} + +export function $wrapLeafNodesInElementsImpl( + selection: RangeSelection, + nodes: LexicalNode[], + nodesLength: number, + createElement: () => ElementNode, + wrappingElement: null | ElementNode = null, +): void { + if (nodes.length === 0) { + return; + } + const firstNode = nodes[0]; const elementMapping: Map = new Map(); const elements = []; @@ -636,17 +690,19 @@ export function $wrapLeafNodesInElements( target = target.getParentOrThrow(); } + let targetIsPrevSibling = false; while (target !== null) { const prevSibling = target.getPreviousSibling(); if (prevSibling !== null) { target = prevSibling; + targetIsPrevSibling = true; break; } target = target.getParentOrThrow(); - if ($isRootNode(target)) { + if ($isTopLevel(target)) { break; } } @@ -701,42 +757,53 @@ export function $wrapLeafNodesInElements( targetElement.setFormat(node.getFormatType()); targetElement.setIndent(node.getIndent()); elements.push(targetElement); - node.remove(); + node.remove(true); } } - if (wrappingElement) { + if (wrappingElement !== null) { for (let i = 0; i < elements.length; i++) { const element = elements[i]; wrappingElement.append(element); } } - // If our target is the root, let's see if we can re-adjust + // If our target is top level, let's see if we can re-adjust // so that the target is the first child instead. - if ($isRootNode(target)) { - const firstChild = target.getFirstChild(); - - if ($isElementNode(firstChild)) { - target = firstChild; - } - - if (firstChild === null) { - if (wrappingElement) { - target.append(wrappingElement); + if ($isTopLevel(target)) { + if (targetIsPrevSibling) { + if (wrappingElement !== null) { + target.insertAfter(wrappingElement); } else { - for (let i = 0; i < elements.length; i++) { + for (let i = elements.length - 1; i >= 0; i--) { const element = elements[i]; - target.append(element); + target.insertAfter(element); } } } else { - if (wrappingElement) { - firstChild.insertBefore(wrappingElement); + const firstChild = target.getFirstChild(); + + if ($isElementNode(firstChild)) { + target = firstChild; + } + + if (firstChild === null) { + if (wrappingElement) { + target.append(wrappingElement); + } else { + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + target.append(element); + } + } } else { - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - firstChild.insertBefore(element); + if (wrappingElement !== null) { + firstChild.insertBefore(wrappingElement); + } else { + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + firstChild.insertBefore(element); + } } } } @@ -1045,7 +1112,7 @@ export function $sliceSelectedTextNodeContent( textNode.isSelected() && !textNode.isSegmented() && !textNode.isToken() && - ($isRangeSelection(selection) || $isGridSelection(selection)) + ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) ) { const anchorNode = selection.anchor.getNode(); const focusNode = selection.focus.getNode(); diff --git a/packages/lexical-table/package.json b/packages/lexical-table/package.json index ddd45596927..5fc351d60fc 100644 --- a/packages/lexical-table/package.json +++ b/packages/lexical-table/package.json @@ -8,13 +8,13 @@ "table" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalTable.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "dependencies": { - "@lexical/utils": "0.4.1" + "@lexical/utils": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-table/src/LexicalTableCellNode.ts b/packages/lexical-table/src/LexicalTableCellNode.ts index c7c6b3c4a16..d6bd137a3b2 100644 --- a/packages/lexical-table/src/LexicalTableCellNode.ts +++ b/packages/lexical-table/src/LexicalTableCellNode.ts @@ -23,7 +23,7 @@ import { $createParagraphNode, $isElementNode, $isLineBreakNode, - GridCellNode, + DEPRECATED_GridCellNode, } from 'lexical'; export const TableCellHeaderStates = { @@ -46,7 +46,7 @@ export type SerializedTableCellNode = Spread< >; /** @noInheritDoc */ -export class TableCellNode extends GridCellNode { +export class TableCellNode extends DEPRECATED_GridCellNode { /** @internal */ __headerState: TableCellHeaderState; /** @internal */ @@ -198,6 +198,10 @@ export class TableCellNode extends GridCellNode { ); } + isTopLevel(): boolean { + return true; + } + collapseAtStart(): true { return true; } diff --git a/packages/lexical-table/src/LexicalTableNode.ts b/packages/lexical-table/src/LexicalTableNode.ts index 4dffa32e1a3..038dd086441 100644 --- a/packages/lexical-table/src/LexicalTableNode.ts +++ b/packages/lexical-table/src/LexicalTableNode.ts @@ -21,7 +21,7 @@ import type { } from 'lexical'; import {addClassNamesToElement} from '@lexical/utils'; -import {$getNearestNodeFromDOMNode, GridNode} from 'lexical'; +import {$getNearestNodeFromDOMNode, DEPRECATED_GridNode} from 'lexical'; import {$isTableCellNode} from './LexicalTableCellNode'; import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode'; @@ -36,7 +36,7 @@ export type SerializedTableNode = Spread< >; /** @noInheritDoc */ -export class TableNode extends GridNode { +export class TableNode extends DEPRECATED_GridNode { /** @internal */ __grid?: Grid; @@ -123,6 +123,10 @@ export class TableNode extends GridNode { return false; } + isTopLevel(): boolean { + return true; + } + getCordsFromCellNode( tableCellNode: TableCellNode, grid: Grid, diff --git a/packages/lexical-table/src/LexicalTableRowNode.ts b/packages/lexical-table/src/LexicalTableRowNode.ts index 85c0c7bc6cd..b52f6de121a 100644 --- a/packages/lexical-table/src/LexicalTableRowNode.ts +++ b/packages/lexical-table/src/LexicalTableRowNode.ts @@ -10,10 +10,10 @@ import type {Spread} from 'lexical'; import {addClassNamesToElement} from '@lexical/utils'; import { + DEPRECATED_GridRowNode, DOMConversionMap, DOMConversionOutput, EditorConfig, - GridRowNode, LexicalNode, NodeKey, SerializedElementNode, @@ -29,7 +29,7 @@ export type SerializedTableRowNode = Spread< >; /** @noInheritDoc */ -export class TableRowNode extends GridRowNode { +export class TableRowNode extends DEPRECATED_GridRowNode { /** @internal */ __height?: number; @@ -79,6 +79,10 @@ export class TableRowNode extends GridRowNode { return element; } + isTopLevel(): boolean { + return true; + } + setHeight(height: number): number | null | undefined { const self = this.getWritable(); self.__height = height; diff --git a/packages/lexical-table/src/LexicalTableSelection.ts b/packages/lexical-table/src/LexicalTableSelection.ts index 00157108011..600a9cf4db2 100644 --- a/packages/lexical-table/src/LexicalTableSelection.ts +++ b/packages/lexical-table/src/LexicalTableSelection.ts @@ -14,7 +14,6 @@ import type { } from 'lexical'; import { - $createGridSelection, $createParagraphNode, $createRangeSelection, $createTextNode, @@ -22,8 +21,9 @@ import { $getNodeByKey, $getSelection, $isElementNode, - $isGridSelection, $setSelection, + DEPRECATED_$createGridSelection, + DEPRECATED_$isGridSelection, SELECTION_CHANGE_COMMAND, } from 'lexical'; import {CAN_USE_DOM} from 'shared/canUseDOM'; @@ -303,7 +303,7 @@ export class TableSelection { ) { const focusNodeKey = focusTableCellNode.getKey(); - this.gridSelection = $createGridSelection(); + this.gridSelection = DEPRECATED_$createGridSelection(); this.focusCellNodeKey = focusNodeKey; this.gridSelection.set( @@ -335,7 +335,7 @@ export class TableSelection { if ($isTableCellNode(anchorTableCellNode)) { const anchorNodeKey = anchorTableCellNode.getKey(); - this.gridSelection = $createGridSelection(); + this.gridSelection = DEPRECATED_$createGridSelection(); this.anchorCellNodeKey = anchorNodeKey; } }); @@ -345,7 +345,7 @@ export class TableSelection { this.editor.update(() => { const selection = $getSelection(); - if (!$isGridSelection(selection)) { + if (!DEPRECATED_$isGridSelection(selection)) { invariant(false, 'Expected grid selection'); } @@ -378,7 +378,7 @@ export class TableSelection { const selection = $getSelection(); - if (!$isGridSelection(selection)) { + if (!DEPRECATED_$isGridSelection(selection)) { invariant(false, 'Expected grid selection'); } diff --git a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts index fdaa49ebd4a..e4703a3d6e2 100644 --- a/packages/lexical-table/src/LexicalTableSelectionHelpers.ts +++ b/packages/lexical-table/src/LexicalTableSelectionHelpers.ts @@ -25,13 +25,13 @@ import { $getPreviousSelection, $getSelection, $isElementNode, - $isGridSelection, $isParagraphNode, $isRangeSelection, $setSelection, COMMAND_PRIORITY_CRITICAL, CONTROLLED_TEXT_INSERTION_COMMAND, DELETE_CHARACTER_COMMAND, + DEPRECATED_$isGridSelection, FOCUS_COMMAND, FORMAT_TEXT_COMMAND, KEY_ARROW_DOWN_COMMAND, @@ -158,7 +158,7 @@ export function applyTableHandlers( const selection = $getSelection(); if ( - $isGridSelection(selection) && + DEPRECATED_$isGridSelection(selection) && selection.gridKey === tableSelection.tableNodeKey && rootElement.contains(event.target as Node) ) { @@ -257,7 +257,7 @@ export function applyTableHandlers( ); } } - } else if ($isGridSelection(selection) && event.shiftKey) { + } else if (DEPRECATED_$isGridSelection(selection) && event.shiftKey) { const tableCellNode = selection.focus.getNode(); if (!$isTableCellNode(tableCellNode)) { @@ -362,7 +362,7 @@ export function applyTableHandlers( ); } } - } else if ($isGridSelection(selection) && event.shiftKey) { + } else if (DEPRECATED_$isGridSelection(selection) && event.shiftKey) { const tableCellNode = selection.focus.getNode(); if (!$isTableCellNode(tableCellNode)) { @@ -462,7 +462,7 @@ export function applyTableHandlers( ); } } - } else if ($isGridSelection(selection) && event.shiftKey) { + } else if (DEPRECATED_$isGridSelection(selection) && event.shiftKey) { const tableCellNode = selection.focus.getNode(); if (!$isTableCellNode(tableCellNode)) { @@ -566,7 +566,7 @@ export function applyTableHandlers( ); } } - } else if ($isGridSelection(selection) && event.shiftKey) { + } else if (DEPRECATED_$isGridSelection(selection) && event.shiftKey) { const tableCellNode = selection.focus.getNode(); if (!$isTableCellNode(tableCellNode)) { @@ -607,7 +607,7 @@ export function applyTableHandlers( return false; } - if ($isGridSelection(selection)) { + if (DEPRECATED_$isGridSelection(selection)) { tableSelection.clearText(); return true; @@ -655,7 +655,7 @@ export function applyTableHandlers( return false; } - if ($isGridSelection(selection)) { + if (DEPRECATED_$isGridSelection(selection)) { event.preventDefault(); event.stopPropagation(); tableSelection.clearText(); @@ -688,7 +688,7 @@ export function applyTableHandlers( return false; } - if ($isGridSelection(selection)) { + if (DEPRECATED_$isGridSelection(selection)) { tableSelection.formatCells(payload); return true; @@ -719,7 +719,7 @@ export function applyTableHandlers( return false; } - if ($isGridSelection(selection)) { + if (DEPRECATED_$isGridSelection(selection)) { tableSelection.clearHighlight(); return false; @@ -804,11 +804,12 @@ export function applyTableHandlers( if ( selection !== prevSelection && - ($isGridSelection(selection) || $isGridSelection(prevSelection)) && + (DEPRECATED_$isGridSelection(selection) || + DEPRECATED_$isGridSelection(prevSelection)) && tableSelection.gridSelection !== selection ) { tableSelection.updateTableGridSelection( - $isGridSelection(selection) && tableNode.isSelected() + DEPRECATED_$isGridSelection(selection) && tableNode.isSelected() ? selection : null, ); @@ -1182,7 +1183,7 @@ function $isSelectionInTable( selection: null | GridSelection | RangeSelection | NodeSelection, tableNode: TableNode, ): boolean { - if ($isRangeSelection(selection) || $isGridSelection(selection)) { + if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) { const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode()); const isFocusInside = tableNode.isParentOf(selection.focus.getNode()); diff --git a/packages/lexical-table/src/LexicalTableUtils.ts b/packages/lexical-table/src/LexicalTableUtils.ts index dc63ee6bdff..288751b4d03 100644 --- a/packages/lexical-table/src/LexicalTableUtils.ts +++ b/packages/lexical-table/src/LexicalTableUtils.ts @@ -216,6 +216,7 @@ export function $insertTableColumn( targetIndex: number, shouldInsertAfter = true, columnCount: number, + grid: Grid, ): TableNode { const tableRows = tableNode.getChildren(); @@ -224,23 +225,33 @@ export function $insertTableColumn( if ($isTableRowNode(currentTableRowNode)) { for (let c = 0; c < columnCount; c++) { - let headerState = TableCellHeaderStates.NO_STATUS; + const tableRowChildren = currentTableRowNode.getChildren(); - if (r === 0) { - headerState |= TableCellHeaderStates.ROW; + if (targetIndex >= tableRowChildren.length || targetIndex < 0) { + throw new Error('Table column target index out of range'); } - const newTableCell = $createTableCellNode(headerState); + const targetCell = tableRowChildren[targetIndex]; - newTableCell.append($createParagraphNode()); + invariant($isTableCellNode(targetCell), 'Expected table cell'); - const tableRowChildren = currentTableRowNode.getChildren(); + const {left, right} = $getTableCellSiblingsFromTableCellNode( + targetCell, + grid, + ); - if (targetIndex >= tableRowChildren.length || targetIndex < 0) { - throw new Error('Table column target index out of range'); + let headerState = TableCellHeaderStates.NO_STATUS; + + if ( + (left && left.hasHeaderState(TableCellHeaderStates.ROW)) || + (right && right.hasHeaderState(TableCellHeaderStates.ROW)) + ) { + headerState |= TableCellHeaderStates.ROW; } - const targetCell = tableRowChildren[targetIndex]; + const newTableCell = $createTableCellNode(headerState); + + newTableCell.append($createParagraphNode()); if (shouldInsertAfter) { targetCell.insertAfter(newTableCell); diff --git a/packages/lexical-table/src/utils.ts b/packages/lexical-table/src/utils.ts deleted file mode 100644 index a232d1b4318..00000000000 --- a/packages/lexical-table/src/utils.ts +++ /dev/null @@ -1,276 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type {Grid} from './LexicalTableSelection'; -import type {LexicalNode} from 'lexical'; - -import {$findMatchingParent} from '@lexical/utils'; -import {$createParagraphNode, $createTextNode} from 'lexical'; -import invariant from 'shared/invariant'; - -import { - $createTableCellNode, - $isTableCellNode, - TableCellHeaderStates, - TableCellNode, -} from './LexicalTableCellNode'; -import {$createTableNode, $isTableNode, TableNode} from './LexicalTableNode'; -import { - $createTableRowNode, - $isTableRowNode, - TableRowNode, -} from './LexicalTableRowNode'; - -export function $createTableNodeWithDimensions( - rowCount: number, - columnCount: number, - includeHeaders = true, -): TableNode { - const tableNode = $createTableNode(); - - for (let iRow = 0; iRow < rowCount; iRow++) { - const tableRowNode = $createTableRowNode(); - - for (let iColumn = 0; iColumn < columnCount; iColumn++) { - let headerState = TableCellHeaderStates.NO_STATUS; - - if (includeHeaders) { - if (iRow === 0) headerState |= TableCellHeaderStates.ROW; - if (iColumn === 0) headerState |= TableCellHeaderStates.COLUMN; - } - - const tableCellNode = $createTableCellNode(headerState); - const paragraphNode = $createParagraphNode(); - - paragraphNode.append($createTextNode()); - - tableCellNode.append(paragraphNode); - - tableRowNode.append(tableCellNode); - } - - tableNode.append(tableRowNode); - } - - return tableNode; -} - -export function $getTableCellNodeFromLexicalNode( - startingNode: LexicalNode, -): TableCellNode | null { - const node = $findMatchingParent(startingNode, (n) => $isTableCellNode(n)); - - if ($isTableCellNode(node)) { - return node; - } - - return null; -} - -export function $getTableRowNodeFromTableCellNodeOrThrow( - startingNode: LexicalNode, -): TableRowNode { - const node = $findMatchingParent(startingNode, (n) => $isTableRowNode(n)); - - if ($isTableRowNode(node)) { - return node; - } - - throw new Error('Expected table cell to be inside of table row.'); -} - -export function $getTableNodeFromLexicalNodeOrThrow( - startingNode: LexicalNode, -): TableNode { - const node = $findMatchingParent(startingNode, (n) => $isTableNode(n)); - - if ($isTableNode(node)) { - return node; - } - - throw new Error('Expected table cell to be inside of table.'); -} - -export function $getTableRowIndexFromTableCellNode( - tableCellNode: TableCellNode, -): number { - const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode); - const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableRowNode); - return tableNode.getChildren().findIndex((n) => n.is(tableRowNode)); -} - -export function $getTableColumnIndexFromTableCellNode( - tableCellNode: TableCellNode, -): number { - const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode); - return tableRowNode.getChildren().findIndex((n) => n.is(tableCellNode)); -} - -export type TableCellSiblings = { - above: TableCellNode | null | undefined; - below: TableCellNode | null | undefined; - left: TableCellNode | null | undefined; - right: TableCellNode | null | undefined; -}; - -export function $getTableCellSiblingsFromTableCellNode( - tableCellNode: TableCellNode, - grid: Grid, -): TableCellSiblings { - const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode); - const {x, y} = tableNode.getCordsFromCellNode(tableCellNode, grid); - return { - above: tableNode.getCellNodeFromCords(x, y - 1, grid), - below: tableNode.getCellNodeFromCords(x, y + 1, grid), - left: tableNode.getCellNodeFromCords(x - 1, y, grid), - right: tableNode.getCellNodeFromCords(x + 1, y, grid), - }; -} - -export function $removeTableRowAtIndex( - tableNode: TableNode, - indexToDelete: number, -): TableNode { - const tableRows = tableNode.getChildren(); - - if (indexToDelete >= tableRows.length || indexToDelete < 0) { - throw new Error('Expected table cell to be inside of table row.'); - } - - const targetRowNode = tableRows[indexToDelete]; - targetRowNode.remove(); - return tableNode; -} - -export function $insertTableRow( - tableNode: TableNode, - targetIndex: number, - shouldInsertAfter = true, - rowCount: number, - grid: Grid, -): TableNode { - const tableRows = tableNode.getChildren(); - - if (targetIndex >= tableRows.length || targetIndex < 0) { - throw new Error('Table row target index out of range'); - } - - const targetRowNode = tableRows[targetIndex]; - - if ($isTableRowNode(targetRowNode)) { - for (let r = 0; r < rowCount; r++) { - const tableRowCells = targetRowNode.getChildren(); - const tableColumnCount = tableRowCells.length; - const newTableRowNode = $createTableRowNode(); - - for (let c = 0; c < tableColumnCount; c++) { - const tableCellFromTargetRow = tableRowCells[c]; - - invariant( - $isTableCellNode(tableCellFromTargetRow), - 'Expected table cell', - ); - - const {above, below} = $getTableCellSiblingsFromTableCellNode( - tableCellFromTargetRow, - grid, - ); - let headerState = TableCellHeaderStates.NO_STATUS; - - if ( - (above && above.hasHeaderState(TableCellHeaderStates.COLUMN)) || - (below && below.hasHeaderState(TableCellHeaderStates.COLUMN)) - ) { - headerState |= TableCellHeaderStates.COLUMN; - } - - const tableCellNode = $createTableCellNode(headerState); - - tableCellNode.append($createParagraphNode()); - - newTableRowNode.append(tableCellNode); - } - - if (shouldInsertAfter) { - targetRowNode.insertAfter(newTableRowNode); - } else { - targetRowNode.insertBefore(newTableRowNode); - } - } - } else { - throw new Error('Row before insertion index does not exist.'); - } - - return tableNode; -} - -export function $insertTableColumn( - tableNode: TableNode, - targetIndex: number, - shouldInsertAfter = true, - columnCount: number, -): TableNode { - const tableRows = tableNode.getChildren(); - - for (let r = 0; r < tableRows.length; r++) { - const currentTableRowNode = tableRows[r]; - - if ($isTableRowNode(currentTableRowNode)) { - for (let c = 0; c < columnCount; c++) { - let headerState = TableCellHeaderStates.NO_STATUS; - - if (r === 0) { - headerState |= TableCellHeaderStates.ROW; - } - - const newTableCell = $createTableCellNode(headerState); - - newTableCell.append($createParagraphNode()); - - const tableRowChildren = currentTableRowNode.getChildren(); - - if (targetIndex >= tableRowChildren.length || targetIndex < 0) { - throw new Error('Table column target index out of range'); - } - - const targetCell = tableRowChildren[targetIndex]; - - if (shouldInsertAfter) { - targetCell.insertAfter(newTableCell); - } else { - targetCell.insertBefore(newTableCell); - } - } - } - } - - return tableNode; -} - -export function $deleteTableColumn( - tableNode: TableNode, - targetIndex: number, -): TableNode { - const tableRows = tableNode.getChildren(); - - for (let i = 0; i < tableRows.length; i++) { - const currentTableRowNode = tableRows[i]; - - if ($isTableRowNode(currentTableRowNode)) { - const tableRowChildren = currentTableRowNode.getChildren(); - - if (targetIndex >= tableRowChildren.length || targetIndex < 0) { - throw new Error('Table column target index out of range'); - } - - tableRowChildren[targetIndex].remove(); - } - } - - return tableNode; -} diff --git a/packages/lexical-text/flow/LexicalText.js.flow b/packages/lexical-text/flow/LexicalText.js.flow index c9924b651ff..7730797a054 100644 --- a/packages/lexical-text/flow/LexicalText.js.flow +++ b/packages/lexical-text/flow/LexicalText.js.flow @@ -18,17 +18,6 @@ declare export function $findTextIntersectionFromCharacters( node: TextNode, offset: number, }; -declare export function $joinTextNodesInElementNode( - elementNode: ElementNode, - separator: string, - stopAt: TextNodeWithOffset, -): string; -declare export function $findNodeWithOffsetFromJoinedText( - offsetInJoinedText: number, - joinedTextLength: number, - separatorLength: number, - elementNode: ElementNode, -): ?TextNodeWithOffset; declare export function $isRootTextContentEmpty( isEditorComposing: boolean, trim?: boolean, diff --git a/packages/lexical-text/package.json b/packages/lexical-text/package.json index e37a8501e0c..53d0ed23769 100644 --- a/packages/lexical-text/package.json +++ b/packages/lexical-text/package.json @@ -9,10 +9,10 @@ "text" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalText.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-text/src/index.ts b/packages/lexical-text/src/index.ts index f576da3a34d..cb7cf531c44 100644 --- a/packages/lexical-text/src/index.ts +++ b/packages/lexical-text/src/index.ts @@ -7,13 +7,7 @@ * */ -import type { - ElementNode, - Klass, - LexicalEditor, - LexicalNode, - RootNode, -} from 'lexical'; +import type {Klass, LexicalEditor, LexicalNode, RootNode} from 'lexical'; import { $createTextNode, @@ -22,7 +16,6 @@ import { $isTextNode, TextNode, } from 'lexical'; -import invariant from 'shared/invariant'; export type TextNodeWithOffset = { node: TextNode; @@ -74,111 +67,6 @@ export function $findTextIntersectionFromCharacters( return null; } -// Return text content for child text nodes. Each non-text node is separated by input string. -// Caution, this function creates a string and should not be used within a tight loop. -// Use $getNodeWithOffsetsFromJoinedTextNodesFromElementNode below to convert -// indexes in the return string back into their corresponding node and offsets. -export function $joinTextNodesInElementNode( - elementNode: ElementNode, - separator: string, - stopAt: TextNodeWithOffset, -): string { - let textContent = ''; - const children = elementNode.getChildren(); - const length = children.length; - - for (let i = 0; i < length; ++i) { - const child = children[i]; - - if ($isTextNode(child)) { - const childTextContent = child.getTextContent(); - - if (child.is(stopAt.node)) { - if (stopAt.offset > childTextContent.length) { - invariant( - false, - 'Node %s and selection point do not match.', - child.__key, - ); - } - textContent += child.getTextContent().substr(0, stopAt.offset); - break; - } else { - textContent += childTextContent; - } - } else { - textContent += separator; - } - } - - return textContent; -} - -// This function converts the offsetInJoinedText to -// a node and offset result or null if not found. -// This function is to be used in conjunction with joinTextNodesInElementNode above. -// The joinedTextContent should be return value from joinTextNodesInElementNode. -// -// The offsetInJoinedText is relative to the entire string which -// itself is relevant to the parent ElementNode. -// -// Example: -// Given a Paragraph with 2 TextNodes. The first is Hello, the second is World. -// The joinedTextContent would be "HelloWorld" -// The offsetInJoinedText might be for the letter "e" = 1 or "r" = 7. -// The return values would be {TextNode1, 1} or {TextNode2,2}, respectively. - -export function $findNodeWithOffsetFromJoinedText( - offsetInJoinedText: number, - joinedTextLength: number, - separatorLength: number, - elementNode: ElementNode, -): TextNodeWithOffset | null { - const children = elementNode.getChildren(); - const childrenLength = children.length; - let runningLength = 0; - let isPriorNodeTextNode = false; - - for (let i = 0; i < childrenLength; ++i) { - // We must examine the offsetInJoinedText that is located - // at the length of the string. - // For example, given "hello", the length is 5, yet - // the caller still wants the node + offset at the - // right edge of the "o". - - if (runningLength > joinedTextLength) { - break; - } - - const child = children[i]; - const isChildNodeTestNode = $isTextNode(child); - const childContentLength = isChildNodeTestNode - ? child.getTextContent().length - : separatorLength; - - const newRunningLength = runningLength + childContentLength; - - const isJoinedOffsetWithinNode = - (isPriorNodeTextNode === false && runningLength === offsetInJoinedText) || - (runningLength === 0 && runningLength === offsetInJoinedText) || - (runningLength < offsetInJoinedText && - offsetInJoinedText <= newRunningLength); - - if (isJoinedOffsetWithinNode && $isTextNode(child)) { - // Check isTextNode again for flow. - - return { - node: child, - offset: offsetInJoinedText - runningLength, - }; - } - runningLength = newRunningLength; - isPriorNodeTextNode = isChildNodeTestNode; - } - - return null; -} - export function $isRootTextContentEmpty( isEditorComposing: boolean, trim = true, diff --git a/packages/lexical-utils/package.json b/packages/lexical-utils/package.json index 69fc093f4ed..017bb3d061c 100644 --- a/packages/lexical-utils/package.json +++ b/packages/lexical-utils/package.json @@ -8,14 +8,14 @@ "utils" ], "license": "MIT", - "version": "0.4.1", + "version": "0.4.2-next.0", "main": "LexicalUtils.js", "peerDependencies": { - "lexical": "0.4.1" + "lexical": "0.4.2-next.0" }, "dependencies": { - "@lexical/list": "0.4.1", - "@lexical/table": "0.4.1" + "@lexical/list": "0.4.2-next.0", + "@lexical/table": "0.4.2-next.0" }, "repository": { "type": "git", diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index 1bf60b07f69..807cb93409e 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -12,12 +12,12 @@ import { $getRoot, $getSelection, $isElementNode, - $isGridSelection, $isNodeSelection, $isRangeSelection, $isTextNode, $setSelection, createEditor, + DEPRECATED_$isGridSelection, EditorState, ElementNode, Klass, @@ -415,7 +415,10 @@ export function $insertBlockNode(node: T): T { if ($isRangeSelection(selection)) { const focusNode = selection.focus.getNode(); focusNode.getTopLevelElementOrThrow().insertAfter(node); - } else if ($isNodeSelection(selection) || $isGridSelection(selection)) { + } else if ( + $isNodeSelection(selection) || + DEPRECATED_$isGridSelection(selection) + ) { const nodes = selection.getNodes(); nodes[nodes.length - 1].getTopLevelElementOrThrow().insertAfter(node); } else { diff --git a/packages/lexical-website/docs/concepts/editor-state.md b/packages/lexical-website/docs/concepts/editor-state.md index a91ed62f64c..2e68b56d561 100644 --- a/packages/lexical-website/docs/concepts/editor-state.md +++ b/packages/lexical-website/docs/concepts/editor-state.md @@ -28,7 +28,14 @@ to deserialize stringified editor states. Here's an example of how you can initialize editor with some state and then persist it: ```js -// Create editor with initial state (e.g. loaded from backend) +// Get editor initial state (e.g. loaded from backend) +const loadContent = async () => { + // 'empty' editor + const value = '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'; + + return value; +} + const initialEditorState = await loadContent(); const editor = createEditor(...); registerRichText(editor, initialEditorState); diff --git a/packages/lexical-website/docs/concepts/read-only.md b/packages/lexical-website/docs/concepts/read-only.md index 3f3fb2952ba..51f142d3138 100644 --- a/packages/lexical-website/docs/concepts/read-only.md +++ b/packages/lexical-website/docs/concepts/read-only.md @@ -2,7 +2,7 @@ sidebar_position: 8 --- -# Read Model / Edit Mode +# Read Mode / Edit Mode Lexical supports two modes: diff --git a/packages/lexical-website/docs/concepts/serialization.md b/packages/lexical-website/docs/concepts/serialization.md index 023074cac86..c22e9381e64 100644 --- a/packages/lexical-website/docs/concepts/serialization.md +++ b/packages/lexical-website/docs/concepts/serialization.md @@ -108,6 +108,7 @@ export type DOMConversion = { export type DOMConversionFn = ( element: Node, parent?: Node, + preformatted?: boolean, ) => DOMConversionOutput; export type DOMConversionOutput = { @@ -147,12 +148,13 @@ static importDOM(): DOMConversionMap | null { If the imported `````` doesn't align with the expected GitHub code HTML, then we return null and allow the node to be handled by lower priority conversions. -Much like `exportDOM`, `importDOM` exposes APIs to allow for post-processing of converted Nodes. The conversion function returns a `DOMConversionOutput` which can specify a function to run for each converted child (forChild) or on all the child nodes after the conversion is complete (after). The key difference here is that ```forChild``` runs for every deeply nested child node of the current node, whereas ```after``` will run only once after the transformation of the node and all its children is complete. +Much like `exportDOM`, `importDOM` exposes APIs to allow for post-processing of converted Nodes. The conversion function returns a `DOMConversionOutput` which can specify a function to run for each converted child (forChild) or on all the child nodes after the conversion is complete (after). The key difference here is that ```forChild``` runs for every deeply nested child node of the current node, whereas ```after``` will run only once after the transformation of the node and all its children is complete. Finally, `preformatted` flag indicates that nested text content is preformatted (similar to `
` tag) and all newlines and spaces should be preserved as is.
 
 ```js
 export type DOMConversionFn = (
   element: Node,
   parent?: Node,
+  preformatted?: boolean,
 ) => DOMConversionOutput;
 
 export type DOMConversionOutput = {
diff --git a/packages/lexical-website/docs/getting-started/quick-start.md b/packages/lexical-website/docs/getting-started/quick-start.md
index 3113e253162..9ac7f74c7e7 100644
--- a/packages/lexical-website/docs/getting-started/quick-start.md
+++ b/packages/lexical-website/docs/getting-started/quick-start.md
@@ -105,7 +105,7 @@ editor.update(() => {
   root.append(paragraphNode);
 });
 ```
-**It's important to note that the core library (the 'lexical' package) does not listen for any commands or perform any updates to the editor state in response to user events out-of-the-box.** In order to see text and other content appear in the editor, you need to register [command listeners](https://lexical.dev/docs/concepts/commands#editorregistercommand) and update the editor in the callback. Lexical provides a couple of helper packages to make it easy wire up a lot of the basic commands you might want for [plain text](https://lexical.dev/docs/api/lexical-plain-text) or [rich text](https://lexical.dev/docs/api/lexical-rich-text) experiences.
+**It's important to note that the core library (the 'lexical' package) does not listen for any commands or perform any updates to the editor state in response to user events out-of-the-box.** In order to see text and other content appear in the editor, you need to register [command listeners](https://lexical.dev/docs/concepts/commands#editorregistercommand) and update the editor in the callback. Lexical provides a couple of helper packages to make it easy wire up a lot of the basic commands you might want for [plain text](https://lexical.dev/docs/packages/lexical-plain-text) or [rich text](https://lexical.dev/docs/packages/lexical-rich-text) experiences.
 
 If you want to know when the editor updates so you can react to the changes, you can add an update
 listener to the editor, as shown below:
diff --git a/packages/lexical-website/docs/react/plugins.md b/packages/lexical-website/docs/react/plugins.md
index 9a8fc0be30a..d83fb0399ad 100644
--- a/packages/lexical-website/docs/react/plugins.md
+++ b/packages/lexical-website/docs/react/plugins.md
@@ -15,6 +15,12 @@ import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
 ```
 
 ```jsx
+const initialConfig = {
+  namespace: 'MyEditor', 
+  theme,
+  onError,
+};
+
 
   }
@@ -26,6 +32,17 @@ import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
 
 ```
 
+> Note: Many plugins might require you to register the one or many Lexical nodes in order for the plugin to work. You can do this by passing a reference to the node to the `nodes` array in your initial editor configuration.
+
+```jsx
+const initialConfig = {
+  namespace: 'MyEditor', 
+  theme,
+  nodes: [ListNode, ListItemNode], // Pass the references to the nodes here
+  onError,
+};
+```
+
 ### `LexicalPlainTextPlugin`
 
 React wrapper for `@lexical/plain-text` that adds major features for plain text editing, including typing, deletion and copy/pasting
diff --git a/packages/lexical-yjs/package.json b/packages/lexical-yjs/package.json
index 0fda865287d..9d3d47283b9 100644
--- a/packages/lexical-yjs/package.json
+++ b/packages/lexical-yjs/package.json
@@ -11,13 +11,13 @@
     "crdt"
   ],
   "license": "MIT",
-  "version": "0.4.1",
+  "version": "0.4.2-next.0",
   "main": "LexicalYjs.js",
   "dependencies": {
-    "@lexical/offset": "0.4.1"
+    "@lexical/offset": "0.4.2-next.0"
   },
   "peerDependencies": {
-    "lexical": "0.4.1",
+    "lexical": "0.4.2-next.0",
     "yjs": ">=13.5.22"
   },
   "repository": {
diff --git a/packages/lexical-yjs/src/Bindings.ts b/packages/lexical-yjs/src/Bindings.ts
index 240872131ac..a131f2fb168 100644
--- a/packages/lexical-yjs/src/Bindings.ts
+++ b/packages/lexical-yjs/src/Bindings.ts
@@ -14,6 +14,7 @@ import type {Cursor} from './SyncCursors';
 import type {LexicalEditor, NodeKey} from 'lexical';
 import type {Doc} from 'yjs';
 
+import invariant from 'shared/invariant';
 import {WebsocketProvider} from 'y-websocket';
 import {XmlText} from 'yjs';
 
@@ -46,10 +47,10 @@ export function createBinding(
   doc: Doc | null | undefined,
   docMap: Map,
 ): Binding {
-  if (doc === undefined || doc === null) {
-    throw new Error('Should never happen');
-  }
-
+  invariant(
+    doc !== undefined && doc !== null,
+    'createBinding: doc is null or undefined',
+  );
   const rootXmlText = doc.get('root', XmlText) as XmlText;
   const root: CollabElementNode = $createCollabElementNode(
     rootXmlText,
diff --git a/packages/lexical-yjs/src/CollabDecoratorNode.ts b/packages/lexical-yjs/src/CollabDecoratorNode.ts
index 0338be82845..8f7e3ad0110 100644
--- a/packages/lexical-yjs/src/CollabDecoratorNode.ts
+++ b/packages/lexical-yjs/src/CollabDecoratorNode.ts
@@ -12,6 +12,7 @@ import type {DecoratorNode, NodeKey, NodeMap} from 'lexical';
 import type {XmlElement} from 'yjs';
 
 import {$getNodeByKey, $isDecoratorNode} from 'lexical';
+import invariant from 'shared/invariant';
 
 import {syncPropertiesFromLexical, syncPropertiesFromYjs} from './Utils';
 
@@ -86,11 +87,10 @@ export class CollabDecoratorNode {
     keysChanged: null | Set,
   ): void {
     const lexicalNode = this.getNode();
-
-    if (lexicalNode === null) {
-      throw new Error('Should never happen');
-    }
-
+    invariant(
+      lexicalNode !== null,
+      'syncPropertiesFromYjs: cound not find decorator node',
+    );
     const xmlElem = this._xmlElem;
     syncPropertiesFromYjs(binding, xmlElem, lexicalNode, keysChanged);
   }
diff --git a/packages/lexical-yjs/src/CollabElementNode.ts b/packages/lexical-yjs/src/CollabElementNode.ts
index 6a699639602..a23a35a12fd 100644
--- a/packages/lexical-yjs/src/CollabElementNode.ts
+++ b/packages/lexical-yjs/src/CollabElementNode.ts
@@ -21,6 +21,7 @@ import {
   $isElementNode,
   $isTextNode,
 } from 'lexical';
+import invariant from 'shared/invariant';
 import {YMap} from 'yjs/dist/src/internals';
 
 import {CollabDecoratorNode} from './CollabDecoratorNode';
@@ -97,10 +98,10 @@ export class CollabElementNode {
 
   getOffset(): number {
     const collabElementNode = this._parent;
-
-    if (collabElementNode === null) {
-      throw new Error('Should never happen');
-    }
+    invariant(
+      collabElementNode !== null,
+      'getOffset: cound not find collab element node',
+    );
 
     return collabElementNode.getChildOffset(this);
   }
@@ -110,12 +111,10 @@ export class CollabElementNode {
     keysChanged: null | Set,
   ): void {
     const lexicalNode = this.getNode();
-
-    if (lexicalNode === null) {
-      this.getNode();
-      throw new Error('Should never happen');
-    }
-
+    invariant(
+      lexicalNode !== null,
+      'syncPropertiesFromYjs: cound not find element node',
+    );
     syncPropertiesFromYjs(binding, this._xmlText, lexicalNode, keysChanged);
   }
 
@@ -232,11 +231,10 @@ export class CollabElementNode {
   syncChildrenFromYjs(binding: Binding): void {
     // Now diff the children of the collab node with that of our existing Lexical node.
     const lexicalNode = this.getNode();
-
-    if (lexicalNode === null) {
-      this.getNode();
-      throw new Error('Should never happen');
-    }
+    invariant(
+      lexicalNode !== null,
+      'syncChildrenFromYjs: cound not find element node',
+    );
 
     const key = lexicalNode.__key;
     const prevLexicalChildrenKeys = lexicalNode.__children;
@@ -285,7 +283,10 @@ export class CollabElementNode {
           } else if (childCollabNode instanceof CollabDecoratorNode) {
             childCollabNode.syncPropertiesFromYjs(binding, null);
           } else if (!(childCollabNode instanceof CollabLineBreakNode)) {
-            throw new Error('Should never happen');
+            invariant(
+              false,
+              'syncChildrenFromYjs: expected text, element, decorator, or linebreak collab node',
+            );
           }
         }
 
@@ -554,20 +555,16 @@ export class CollabElementNode {
     const child = children[index];
 
     if (child === undefined) {
-      if (collabNode !== undefined) {
-        this.append(collabNode);
-      } else {
-        throw new Error('Should never happen');
-      }
-
+      invariant(
+        collabNode !== undefined,
+        'splice: could not find collab element node',
+      );
+      this.append(collabNode);
       return;
     }
 
     const offset = child.getOffset();
-
-    if (offset === -1) {
-      throw new Error('Should never happen');
-    }
+    invariant(offset !== -1, 'splice: expected offset to be greater than zero');
 
     const xmlText = this._xmlText;
 
diff --git a/packages/lexical-yjs/src/CollabTextNode.ts b/packages/lexical-yjs/src/CollabTextNode.ts
index 4cc89960b85..37db36229e6 100644
--- a/packages/lexical-yjs/src/CollabTextNode.ts
+++ b/packages/lexical-yjs/src/CollabTextNode.ts
@@ -17,6 +17,7 @@ import {
   $isRangeSelection,
   $isTextNode,
 } from 'lexical';
+import invariant from 'shared/invariant';
 import simpleDiffWithCursor from 'shared/simpleDiffWithCursor';
 
 import {syncPropertiesFromLexical, syncPropertiesFromYjs} from './Utils';
@@ -144,10 +145,10 @@ export class CollabTextNode {
     keysChanged: null | Set,
   ): void {
     const lexicalNode = this.getNode();
-
-    if (lexicalNode === null) {
-      throw new Error('Should never happen');
-    }
+    invariant(
+      lexicalNode !== null,
+      'syncPropertiesAndTextFromYjs: cound not find decorator node',
+    );
 
     syncPropertiesFromYjs(binding, this._map, lexicalNode, keysChanged);
 
diff --git a/packages/lexical-yjs/src/SyncEditorStates.ts b/packages/lexical-yjs/src/SyncEditorStates.ts
index 18e5c4c4b08..58b146aad91 100644
--- a/packages/lexical-yjs/src/SyncEditorStates.ts
+++ b/packages/lexical-yjs/src/SyncEditorStates.ts
@@ -22,6 +22,7 @@ import {
   $isTextNode,
   $setSelection,
 } from 'lexical';
+import invariant from 'shared/invariant';
 import {WebsocketProvider} from 'y-websocket';
 import {Text as YText, YEvent, YMapEvent, YTextEvent, YXmlEvent} from 'yjs';
 
@@ -79,7 +80,7 @@ function syncEvent(binding: Binding, event: any): void {
       collabNode.syncPropertiesFromYjs(binding, attributesChanged);
     }
   } else {
-    throw new Error('Should never happen');
+    invariant(false, 'Expected text, element, or decorator event');
   }
 }
 
diff --git a/packages/lexical-yjs/src/Utils.ts b/packages/lexical-yjs/src/Utils.ts
index 1a601b15b27..39c377874ed 100644
--- a/packages/lexical-yjs/src/Utils.ts
+++ b/packages/lexical-yjs/src/Utils.ts
@@ -24,6 +24,7 @@ import {
   createEditor,
   NodeKey,
 } from 'lexical';
+import invariant from 'shared/invariant';
 import {Doc, Map as YMap, XmlElement, XmlText} from 'yjs';
 
 import {
@@ -76,11 +77,7 @@ export function getIndexOfYjsNode(
 
 export function $getNodeByKeyOrThrow(key: NodeKey): LexicalNode {
   const node = $getNodeByKey(key);
-
-  if (node === null) {
-    throw new Error('Should never happen');
-  }
-
+  invariant(node !== null, 'could not find node by key');
   return node;
 }
 
@@ -120,7 +117,7 @@ export function $createCollabNodeFromLexicalNode(
     collabNode = $createCollabDecoratorNode(xmlElem, parent, nodeType);
     collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
   } else {
-    throw new Error('Should never happen');
+    invariant(false, 'Expected text, element, decorator, or linebreak node');
   }
 
   collabNode._key = lexicalNode.__key;
@@ -134,11 +131,7 @@ function getNodeTypeFromSharedType(
     sharedType instanceof YMap
       ? sharedType.get('__type')
       : sharedType.getAttribute('__type');
-
-  if (type == null) {
-    throw new Error('Should never happen');
-  }
-
+  invariant(type != null, 'Expected shared type to include type attribute');
   return type;
 }
 
@@ -158,10 +151,7 @@ export function getOrInitCollabNodeFromSharedType(
     const registeredNodes = binding.editor._nodes;
     const type = getNodeTypeFromSharedType(sharedType);
     const nodeInfo = registeredNodes.get(type);
-
-    if (nodeInfo === undefined) {
-      throw new Error('Should never happen');
-    }
+    invariant(nodeInfo !== undefined, 'Node %s is not registered', type);
 
     const sharedParent = sharedType.parent;
     const targetParent =
@@ -172,21 +162,17 @@ export function getOrInitCollabNodeFromSharedType(
           )
         : parent || null;
 
-    if (!(targetParent instanceof CollabElementNode)) {
-      throw new Error('Should never happen');
-    }
+    invariant(
+      targetParent instanceof CollabElementNode,
+      'Expected parent to be a collab element node',
+    );
 
     if (sharedType instanceof XmlText) {
       return $createCollabElementNode(sharedType, targetParent, type);
     } else if (sharedType instanceof YMap) {
-      if (targetParent === null) {
-        throw new Error('Should never happen');
-      }
-
       if (type === 'linebreak') {
         return $createCollabLineBreakNode(sharedType, targetParent);
       }
-
       return $createCollabTextNode(sharedType, '', targetParent, type);
     } else if (sharedType instanceof XmlElement) {
       return $createCollabDecoratorNode(sharedType, targetParent, type);
@@ -208,11 +194,7 @@ export function createLexicalNodeFromCollabNode(
   const type = collabNode.getType();
   const registeredNodes = binding.editor._nodes;
   const nodeInfo = registeredNodes.get(type);
-
-  if (nodeInfo === undefined) {
-    throw new Error('createLexicalNode failed');
-  }
-
+  invariant(nodeInfo !== undefined, 'Node %s is not registered', type);
   const lexicalNode:
     | DecoratorNode
     | TextNode
diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow
index efbdc3a8f56..222a1d63c5b 100644
--- a/packages/lexical/flow/Lexical.js.flow
+++ b/packages/lexical/flow/Lexical.js.flow
@@ -418,36 +418,24 @@ declare export class GridSelection implements BaseSelection {
   getTextContent(): string;
 }
 
-declare export function $isGridSelection(
+declare export function DEPRECATED_$isGridSelection(
   x: ?mixed,
 ): boolean %checks(x instanceof GridSelection);
 
 declare export class NodeSelection implements BaseSelection {
   _nodes: Set;
   dirty: boolean;
-
   constructor(objects: Set): void;
-
   is(selection: null | RangeSelection | NodeSelection | GridSelection): boolean;
-
   add(key: NodeKey): void;
-
   delete(key: NodeKey): void;
-
   clear(): void;
-
   has(key: NodeKey): boolean;
-
   clone(): NodeSelection;
-
   extract(): Array;
-
   insertRawText(): void;
-
   insertText(): void;
-
   getNodes(): Array;
-
   getTextContent(): string;
 }
 
@@ -533,7 +521,7 @@ declare class _Point {
 
 declare export function $createRangeSelection(): RangeSelection;
 declare export function $createNodeSelection(): NodeSelection;
-declare export function $createGridSelection(): GridSelection;
+declare export function DEPRECATED_$createGridSelection(): GridSelection;
 declare export function $isRangeSelection(
   x: ?mixed,
 ): boolean %checks(x instanceof RangeSelection);
@@ -722,6 +710,7 @@ declare export class ElementNode extends LexicalNode {
   canInsertTextBefore(): boolean;
   canInsertTextAfter(): boolean;
   isInline(): boolean;
+  isTopLevel(): boolean;
   canSelectionRemove(): boolean;
   splice(
     start: number,
@@ -769,31 +758,32 @@ declare export function $isParagraphNode(
   node: ?LexicalNode,
 ): boolean %checks(node instanceof ParagraphNode);
 
-declare export class GridNode extends ElementNode {}
+declare export class deprecated_GridNode extends ElementNode {}
 
-declare export function $isGridNode(
+declare export function DEPRECATED_$isGridNode(
   node: ?LexicalNode,
-): boolean %checks(node instanceof GridNode);
+): boolean %checks(node instanceof deprecated_GridNode);
 
-declare export class GridRowNode extends ElementNode {}
+declare export class deprecated_GridRowNode extends ElementNode {}
 
-declare export function $isGridRowNode(
+declare export function DEPRECATED_$isGridRowNode(
   node: ?LexicalNode,
-): boolean %checks(node instanceof GridRowNode);
+): boolean %checks(node instanceof deprecated_GridRowNode);
 
-declare export class GridCellNode extends ElementNode {
+declare export class deprecated_GridCellNode extends ElementNode {
   __colSpan: number;
 
   constructor(colSpan: number, key?: NodeKey): void;
 }
 
-declare export function $isGridCellNode(
+declare export function DEPRECATED_$isGridCellNode(
   node: ?LexicalNode,
-): boolean %checks(node instanceof GridCellNode);
+): boolean %checks(node instanceof deprecated_GridCellNode);
 
 /**
  * LexicalUtils
  */
+export type EventHandler = (event: Event, editor: LexicalEditor) => void;
 declare export function $hasUpdateTag(tag: string): boolean;
 declare export function $addUpdateTag(tag: string): void;
 declare export function $getNearestNodeFromDOMNode(
@@ -818,8 +808,14 @@ declare export function $getDecoratorNode(
   isBackward: boolean,
 ): null | LexicalNode;
 declare export function generateRandomKey(): string;
-export type EventHandler = (event: Event, editor: LexicalEditor) => void;
-
+declare export function $isTopLevel(
+  node: LexicalNode,
+): boolean %checks(node instanceof ElementNode ||
+  node instanceof DecoratorNode);
+declare export function $hasAncestor(
+  child: LexicalNode,
+  targetNode: LexicalNode,
+): boolean;
 /**
  * LexicalVersion
  */
diff --git a/packages/lexical/package.json b/packages/lexical/package.json
index a301b352c3f..b97dcfbcb06 100644
--- a/packages/lexical/package.json
+++ b/packages/lexical/package.json
@@ -9,7 +9,7 @@
     "rich-text"
   ],
   "license": "MIT",
-  "version": "0.4.1",
+  "version": "0.4.2-next.0",
   "main": "Lexical.js",
   "repository": {
     "type": "git",
diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts
index d7f2b165947..b1e31bf3069 100644
--- a/packages/lexical/src/LexicalEditor.ts
+++ b/packages/lexical/src/LexicalEditor.ts
@@ -523,6 +523,8 @@ export class LexicalEditor {
 
     this._onError = onError;
     this._htmlConversions = htmlConversions;
+    // We don't actually make use of the `editable` argument above.
+    // Doing so, causes e2e tests around the lock to fail.
     this._editable = true;
     this._headless = false;
     this._window = null;
diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts
index 5a3ba623786..fa11aa61054 100644
--- a/packages/lexical/src/LexicalNode.ts
+++ b/packages/lexical/src/LexicalNode.ts
@@ -136,6 +136,7 @@ export type DOMConversion = {
 export type DOMConversionFn = (
   element: T,
   parent?: Node,
+  preformatted?: boolean,
 ) => DOMConversionOutput | null;
 
 export type DOMChildConversion = (
@@ -153,6 +154,7 @@ export type DOMConversionOutput = {
   after?: (childLexicalNodes: Array) => Array;
   forChild?: DOMChildConversion;
   node: LexicalNode | null;
+  preformatted?: boolean;
 };
 
 export type DOMExportOutput = {
diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts
index 78f90e9b2ef..fc7d2695470 100644
--- a/packages/lexical/src/LexicalSelection.ts
+++ b/packages/lexical/src/LexicalSelection.ts
@@ -22,17 +22,17 @@ import {
   $createTextNode,
   $isDecoratorNode,
   $isElementNode,
-  $isGridCellNode,
-  $isGridNode,
-  $isGridRowNode,
   $isLeafNode,
   $isLineBreakNode,
   $isRootNode,
   $isTextNode,
   $setSelection,
   DecoratorNode,
-  GridCellNode,
-  GridNode,
+  DEPRECATED_$isGridCellNode,
+  DEPRECATED_$isGridNode,
+  DEPRECATED_$isGridRowNode,
+  DEPRECATED_GridCellNode,
+  DEPRECATED_GridNode,
   TextNode,
 } from '.';
 import {DOM_ELEMENT_TYPE, TEXT_TYPE_TO_FORMAT} from './LexicalConstants';
@@ -369,7 +369,7 @@ export class GridSelection implements BaseSelection {
   is(
     selection: null | RangeSelection | NodeSelection | GridSelection,
   ): boolean {
-    if (!$isGridSelection(selection)) {
+    if (!DEPRECATED_$isGridSelection(selection)) {
       return false;
     }
     return this.gridKey === selection.gridKey && this.anchor.is(this.focus);
@@ -448,8 +448,8 @@ export class GridSelection implements BaseSelection {
     const nodesSet = new Set();
     const {fromX, fromY, toX, toY} = this.getShape();
 
-    const gridNode = $getNodeByKey(this.gridKey);
-    if (!$isGridNode(gridNode)) {
+    const gridNode = $getNodeByKey(this.gridKey);
+    if (!DEPRECATED_$isGridNode(gridNode)) {
       invariant(false, 'getNodes: expected to find GridNode');
     }
     nodesSet.add(gridNode);
@@ -459,13 +459,13 @@ export class GridSelection implements BaseSelection {
       const gridRowNode = gridRowNodes[r];
       nodesSet.add(gridRowNode);
 
-      if (!$isGridRowNode(gridRowNode)) {
+      if (!DEPRECATED_$isGridRowNode(gridRowNode)) {
         invariant(false, 'getNodes: expected to find GridRowNode');
       }
-      const gridCellNodes = gridRowNode.getChildren();
+      const gridCellNodes = gridRowNode.getChildren();
       for (let c = fromX; c <= toX; c++) {
         const gridCellNode = gridCellNodes[c];
-        if (!$isGridCellNode(gridCellNode)) {
+        if (!DEPRECATED_$isGridCellNode(gridCellNode)) {
           invariant(false, 'getNodes: expected to find GridCellNode');
         }
         nodesSet.add(gridCellNode);
@@ -498,7 +498,7 @@ export class GridSelection implements BaseSelection {
   }
 }
 
-export function $isGridSelection(x: unknown): x is GridSelection {
+export function DEPRECATED_$isGridSelection(x: unknown): x is GridSelection {
   return x instanceof GridSelection;
 }
 
@@ -2256,7 +2256,7 @@ export function $createNodeSelection(): NodeSelection {
   return new NodeSelection(new Set());
 }
 
-export function $createGridSelection(): GridSelection {
+export function DEPRECATED_$createGridSelection(): GridSelection {
   const anchor = $createPoint('root', 0, 'element');
   const focus = $createPoint('root', 0, 'element');
   return new GridSelection('root', anchor, focus);
@@ -2269,7 +2269,10 @@ export function internalCreateSelection(
   const lastSelection = currentEditorState._selection;
   const domSelection = getDOMSelection();
 
-  if ($isNodeSelection(lastSelection) || $isGridSelection(lastSelection)) {
+  if (
+    $isNodeSelection(lastSelection) ||
+    DEPRECATED_$isGridSelection(lastSelection)
+  ) {
     return lastSelection.clone();
   }
 
diff --git a/packages/lexical/src/LexicalUpdates.ts b/packages/lexical/src/LexicalUpdates.ts
index ee7a5b82434..f183c9d8106 100644
--- a/packages/lexical/src/LexicalUpdates.ts
+++ b/packages/lexical/src/LexicalUpdates.ts
@@ -508,7 +508,6 @@ export function commitPendingUpdates(editor: LexicalEditor): void {
   const dirtyElements = editor._dirtyElements;
   const normalizedNodes = editor._normalizedNodes;
   const tags = editor._updateTags;
-  const pendingDecorators = editor._pendingDecorators;
   const deferred = editor._deferred;
 
   if (needsUpdate) {
@@ -563,6 +562,10 @@ export function commitPendingUpdates(editor: LexicalEditor): void {
     );
   }
 
+  /**
+   * Capture pendingDecorators after garbage collecting detached decorators
+   */
+  const pendingDecorators = editor._pendingDecorators;
   if (pendingDecorators !== null) {
     editor._decorators = pendingDecorators;
     editor._pendingDecorators = null;
diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts
index d1b1d130d4e..008e75d6e4f 100644
--- a/packages/lexical/src/LexicalUtils.ts
+++ b/packages/lexical/src/LexicalUtils.ts
@@ -1205,7 +1205,10 @@ export function $maybeMoveChildrenSelectionToParent(
   return selection;
 }
 
-function $hasAncestor(child: LexicalNode, targetNode: LexicalNode): boolean {
+export function $hasAncestor(
+  child: LexicalNode,
+  targetNode: LexicalNode,
+): boolean {
   let parent = child.getParent();
   while (parent !== null) {
     if (parent.is(targetNode)) {
@@ -1228,3 +1231,10 @@ export function getWindow(editor: LexicalEditor): Window {
   }
   return windowObj;
 }
+
+export function $isTopLevel(node: LexicalNode): boolean {
+  return (
+    ($isElementNode(node) && node.isTopLevel()) ||
+    ($isDecoratorNode(node) && node.isTopLevel())
+  );
+}
diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx
index e6fed76098b..28e00080a8c 100644
--- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx
+++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx
@@ -17,7 +17,6 @@ import {
   TableRowNode,
 } from '@lexical/table';
 import {
-  $createGridSelection,
   $createLineBreakNode,
   $createNodeSelection,
   $createParagraphNode,
@@ -31,6 +30,7 @@ import {
   COMMAND_PRIORITY_EDITOR,
   COMMAND_PRIORITY_LOW,
   createCommand,
+  DEPRECATED_$createGridSelection,
   ElementNode,
   LexicalEditor,
   NodeKey,
@@ -1217,7 +1217,7 @@ describe('LexicalEditor tests', () => {
         await update(() => {
           const paragraph = $createParagraphNode();
           originalText = $createTextNode('Hello world');
-          const selection = $createGridSelection();
+          const selection = DEPRECATED_$createGridSelection();
           selection.set(
             originalText.getKey(),
             originalText.getKey(),
diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts
index 7f3f991e77e..cf4e10faa41 100644
--- a/packages/lexical/src/index.ts
+++ b/packages/lexical/src/index.ts
@@ -112,14 +112,14 @@ export {
 } from './LexicalEditor';
 export type {EventHandler} from './LexicalEvents';
 export {
-  $createGridSelection,
   $createNodeSelection,
   $createRangeSelection,
   $getPreviousSelection,
   $getSelection,
-  $isGridSelection,
   $isNodeSelection,
   $isRangeSelection,
+  DEPRECATED_$createGridSelection,
+  DEPRECATED_$isGridSelection,
 } from './LexicalSelection';
 export {$parseSerializedNode} from './LexicalUpdates';
 export {
@@ -128,7 +128,9 @@ export {
   $getNearestNodeFromDOMNode,
   $getNodeByKey,
   $getRoot,
+  $hasAncestor,
   $isLeafNode,
+  $isTopLevel,
   $nodesOfType,
   $setCompositionKey,
   $setSelection,
@@ -136,9 +138,18 @@ export {
 export {VERSION} from './LexicalVersion';
 export {$isDecoratorNode, DecoratorNode} from './nodes/LexicalDecoratorNode';
 export {$isElementNode, ElementNode} from './nodes/LexicalElementNode';
-export {$isGridCellNode, GridCellNode} from './nodes/LexicalGridCellNode';
-export {$isGridNode, GridNode} from './nodes/LexicalGridNode';
-export {$isGridRowNode, GridRowNode} from './nodes/LexicalGridRowNode';
+export {
+  DEPRECATED_$isGridCellNode,
+  DEPRECATED_GridCellNode,
+} from './nodes/LexicalGridCellNode';
+export {
+  DEPRECATED_$isGridNode,
+  DEPRECATED_GridNode,
+} from './nodes/LexicalGridNode';
+export {
+  DEPRECATED_$isGridRowNode,
+  DEPRECATED_GridRowNode,
+} from './nodes/LexicalGridRowNode';
 export type {SerializedLineBreakNode} from './nodes/LexicalLineBreakNode';
 export {
   $createLineBreakNode,
diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts
index 274be9554ad..bbd30e00eec 100644
--- a/packages/lexical/src/nodes/LexicalElementNode.ts
+++ b/packages/lexical/src/nodes/LexicalElementNode.ts
@@ -480,6 +480,9 @@ export class ElementNode extends LexicalNode {
   isInline(): boolean {
     return false;
   }
+  isTopLevel(): boolean {
+    return false;
+  }
   canMergeWith(node: ElementNode): boolean {
     return false;
   }
diff --git a/packages/lexical/src/nodes/LexicalGridCellNode.ts b/packages/lexical/src/nodes/LexicalGridCellNode.ts
index 79e798d6f1b..6bb77d3eaf0 100644
--- a/packages/lexical/src/nodes/LexicalGridCellNode.ts
+++ b/packages/lexical/src/nodes/LexicalGridCellNode.ts
@@ -23,7 +23,7 @@ export type SerializedGridCellNode = Spread<
 >;
 
 /** @noInheritDoc */
-export class GridCellNode extends ElementNode {
+export class DEPRECATED_GridCellNode extends ElementNode {
   /** @internal */
   __colSpan: number;
 
@@ -40,8 +40,8 @@ export class GridCellNode extends ElementNode {
   }
 }
 
-export function $isGridCellNode(
-  node: GridCellNode | LexicalNode | null | undefined,
-): node is GridCellNode {
-  return node instanceof GridCellNode;
+export function DEPRECATED_$isGridCellNode(
+  node: DEPRECATED_GridCellNode | LexicalNode | null | undefined,
+): node is DEPRECATED_GridCellNode {
+  return node instanceof DEPRECATED_GridCellNode;
 }
diff --git a/packages/lexical/src/nodes/LexicalGridNode.ts b/packages/lexical/src/nodes/LexicalGridNode.ts
index 60d0ba1f7b7..a454dbcf954 100644
--- a/packages/lexical/src/nodes/LexicalGridNode.ts
+++ b/packages/lexical/src/nodes/LexicalGridNode.ts
@@ -10,10 +10,10 @@ import type {LexicalNode} from '../LexicalNode';
 
 import {ElementNode} from './LexicalElementNode';
 
-export class GridNode extends ElementNode {}
+export class DEPRECATED_GridNode extends ElementNode {}
 
-export function $isGridNode(
+export function DEPRECATED_$isGridNode(
   node: LexicalNode | null | undefined,
-): node is GridNode {
-  return node instanceof GridNode;
+): node is DEPRECATED_GridNode {
+  return node instanceof DEPRECATED_GridNode;
 }
diff --git a/packages/lexical/src/nodes/LexicalGridRowNode.ts b/packages/lexical/src/nodes/LexicalGridRowNode.ts
index c17760496bc..003db5f66a7 100644
--- a/packages/lexical/src/nodes/LexicalGridRowNode.ts
+++ b/packages/lexical/src/nodes/LexicalGridRowNode.ts
@@ -10,10 +10,10 @@ import type {LexicalNode} from '../LexicalNode';
 
 import {ElementNode} from './LexicalElementNode';
 
-export class GridRowNode extends ElementNode {}
+export class DEPRECATED_GridRowNode extends ElementNode {}
 
-export function $isGridRowNode(
+export function DEPRECATED_$isGridRowNode(
   node: LexicalNode | null | undefined,
-): node is GridRowNode {
-  return node instanceof GridRowNode;
+): node is DEPRECATED_GridRowNode {
+  return node instanceof DEPRECATED_GridRowNode;
 }
diff --git a/packages/lexical/src/nodes/LexicalParagraphNode.ts b/packages/lexical/src/nodes/LexicalParagraphNode.ts
index b376d493314..6e7b85a0b12 100644
--- a/packages/lexical/src/nodes/LexicalParagraphNode.ts
+++ b/packages/lexical/src/nodes/LexicalParagraphNode.ts
@@ -68,6 +68,21 @@ export class ParagraphNode extends ElementNode {
     if (element && this.isEmpty()) {
       element.append(document.createElement('br'));
     }
+    if (element) {
+      const formatType = this.getFormatType();
+      element.style.textAlign = formatType;
+
+      const direction = this.getDirection();
+      if (direction) {
+        element.dir = direction;
+      }
+      const indent = this.getIndent();
+      if (indent > 0) {
+        // padding-inline-start is not widely supported in email HTML, but
+        // Lexical Reconciler uses padding-inline-start. Using text-indent instead.
+        element.style.textIndent = `${indent * 20}px`;
+      }
+    }
 
     return {
       element,
diff --git a/packages/lexical/src/nodes/LexicalRootNode.ts b/packages/lexical/src/nodes/LexicalRootNode.ts
index 3db1d98383b..04867b3b37a 100644
--- a/packages/lexical/src/nodes/LexicalRootNode.ts
+++ b/packages/lexical/src/nodes/LexicalRootNode.ts
@@ -37,6 +37,10 @@ export class RootNode extends ElementNode {
     this.__cachedText = null;
   }
 
+  isTopLevel(): boolean {
+    return true;
+  }
+
   getTopLevelElementOrThrow(): never {
     invariant(
       false,
diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts
index 4750fb7838f..62561c51be1 100644
--- a/packages/lexical/src/nodes/LexicalTextNode.ts
+++ b/packages/lexical/src/nodes/LexicalTextNode.ts
@@ -882,14 +882,17 @@ function convertBringAttentionToElement(domNode: Node): DOMConversionOutput {
     node: null,
   };
 }
-function convertTextDOMNode(domNode: Node): DOMConversionOutput {
-  const {parentElement} = domNode;
-  const textContent = domNode.textContent || '';
-  const textContentTrim = textContent.trim();
-  const isPre =
-    parentElement != null && parentElement.tagName.toLowerCase() === 'pre';
-  if (!isPre && textContentTrim.length === 0 && textContent.includes('\n')) {
-    return {node: null};
+function convertTextDOMNode(
+  domNode: Node,
+  _parent?: Node,
+  preformatted?: boolean,
+): DOMConversionOutput {
+  let textContent = domNode.textContent || '';
+  if (!preformatted) {
+    textContent = textContent.replace(/\r?\n/gm, ' ');
+    if (textContent.trim().length === 0) {
+      return {node: null};
+    }
   }
   return {node: $createTextNode(textContent)};
 }
diff --git a/packages/shared/package.json b/packages/shared/package.json
index fe537d69ccf..9249a901d3f 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -8,9 +8,9 @@
     "rich-text"
   ],
   "license": "MIT",
-  "version": "0.4.1",
+  "version": "0.4.2-next.0",
   "dependencies": {
-    "lexical": "0.4.1"
+    "lexical": "0.4.2-next.0"
   },
   "repository": {
     "type": "git",
diff --git a/scripts/build.js b/scripts/build.js
index 13e7e446a77..a6d9ab6c94d 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -234,8 +234,12 @@ async function build(name, inputFile, outputPath, outputFile, isProd) {
       isProd && compiler(closureOptions),
       {
         renderChunk(source) {
-          return `${getComment()}
-${source}`;
+          // Assets pipeline might use "export" word in the beginning of the line
+          // as a dependency, avoiding it with empty comment in front
+          const patchedSource = isWWW
+            ? source.replace(/^(export(?!s))/gm, '/**/$1')
+            : source;
+          return `${getComment()}\n${patchedSource}`;
         },
       },
     ],
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 1a26586f20c..e305b27dc7d 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -78,5 +78,21 @@
   "76": "append: attempting to append self",
   "77": "LexicalAutoLinkPlugin: AutoLinkNode not registered on editor",
   "78": "window object not found",
-  "79": "MarkdownShortcuts: missing dependency for transformer. Ensure node dependency is included in editor initial config."
+  "79": "MarkdownShortcuts: missing dependency for transformer. Ensure node dependency is included in editor initial config.",
+  "80": "Create node: Attempted to create node %s that was not configured to be used on the editor.",
+  "81": "createBinding: doc is null or undefined",
+  "82": "Expected text, element, or decorator event",
+  "83": "syncPropertiesFromYjs: cound not find decorator node",
+  "84": "syncPropertiesAndTextFromYjs: cound not find decorator node",
+  "85": "could not find node by key",
+  "86": "Expected text, element, decorator, or linebreak node",
+  "87": "Expected shared type to include type attribute",
+  "88": "Node %s is not registered",
+  "89": "Expected parent to be a collab element node",
+  "90": "getOffset: cound not find collab element node",
+  "91": "syncPropertiesFromYjs: cound not find element node",
+  "92": "syncChildrenFromYjs: cound not find element node",
+  "93": "syncChildrenFromYjs: expected text, element, decorator, or linebreak collab node",
+  "94": "splice: could not find collab element node",
+  "95": "splice: expected offset to be greater than zero"
 }
diff --git a/scripts/npm/increment-version.js b/scripts/npm/increment-version.js
new file mode 100644
index 00000000000..aea4a940d46
--- /dev/null
+++ b/scripts/npm/increment-version.js
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+'use strict';
+
+const {exec} = require('child-process-promise');
+const argv = require('minimist')(process.argv.slice(2));
+const increment = argv.i;
+const validIncrements = new Set(['minor', 'patch', 'prerelease']);
+if (!validIncrements.has(increment)) {
+  console.error(`Invalid value for increment: ${increment}`);
+  process.exit(1);
+}
+
+async function incrementVersion(increment) {
+  const preId = increment === 'prerelease' ? '--preid next' : '';
+  const workspaces = '';
+  const command = `npm --no-git-tag-version version ${increment} --include-workspace-root true ${preId} ${workspaces}`;
+  await exec(command);
+}
+
+incrementVersion(increment);
diff --git a/scripts/npm/release.js b/scripts/npm/release.js
index 03168c450cd..c88da0b221e 100644
--- a/scripts/npm/release.js
+++ b/scripts/npm/release.js
@@ -13,21 +13,40 @@
 const readline = require('readline');
 const {exec} = require('child-process-promise');
 const {LEXICAL_PKG, DEFAULT_PKGS} = require('./packages');
+const argv = require('minimist')(process.argv.slice(2));
+
+const nonInteractive = argv['non-interactive'];
+const dryRun = argv['dry-run'];
+const channel = argv.channel;
+const validChannels = new Set(['next', 'latest']);
+if (!validChannels.has(channel)) {
+  console.error(`Invalid release channel: ${channel}`);
+  process.exit(1);
+}
 
 async function publish() {
   const pkgs = [LEXICAL_PKG, ...DEFAULT_PKGS];
+  if (!nonInteractive) {
+    console.info(
+      `You're about to publish:
+    ${pkgs.join('\n')}
 
-  console.info(
-    `You're about to publish:
-${pkgs.join('\n')}
-
-Type "publish" to confirm.`,
-  );
-  await waitForInput();
+    Type "publish" to confirm.`,
+    );
+    await waitForInput();
+  }
 
   for (let i = 0; i < pkgs.length; i++) {
     const pkg = pkgs[i];
-    await exec(`cd ./packages/${pkg}/npm && npm publish --access public`);
+    console.info(`Publishing ${pkg}...`);
+    if (dryRun === undefined || dryRun === 0) {
+      await exec(
+        `cd ./packages/${pkg}/npm && npm publish --access public --tag ${channel}`,
+      );
+      console.info(`Done!`);
+    } else {
+      console.info(`Dry run - skipping publish step.`);
+    }
   }
 }
 
diff --git a/scripts/npm/update-changelog.js b/scripts/npm/update-changelog.js
new file mode 100644
index 00000000000..d9aa38519e9
--- /dev/null
+++ b/scripts/npm/update-changelog.js
@@ -0,0 +1,39 @@
+#!/usr/bin/env node
+
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+'use strict';
+
+const {exec} = require('child-process-promise');
+
+const isPrerelease = process.env.npm_package_version.indexOf('-') !== -1;
+
+async function updateChangelog() {
+  const date = (await exec(`git show --format=%as | head -1`)).stdout.trim();
+  const header = `## v${process.env.npm_package_version} (${date})`;
+  const previousReleaseHash = (
+    await exec(`git log -n 1 origin/latest --pretty=format:"%H"`)
+  ).stdout.trim();
+  const changelogContent = (
+    await exec(
+      `git --no-pager log --oneline ${previousReleaseHash}...HEAD~1 --pretty=format:\"- %s %an\"`,
+    )
+  ).stdout.trim();
+  const tmpFilePath = './changelog-tmp';
+  await exec(`echo "${header}\n" >> ${tmpFilePath}`);
+  await exec(`echo "${changelogContent}\n" >> ${tmpFilePath}`);
+  await exec(
+    `cat ./CHANGELOG.md >> ${tmpFilePath} && mv ${tmpFilePath} ./CHANGELOG.md`,
+  );
+  await exec(`git commit --amend --no-edit`);
+}
+
+if (!isPrerelease) {
+  updateChangelog();
+}
diff --git a/scripts/updateVersion.js b/scripts/updateVersion.js
index 2d3f2be2f38..e4607942ef5 100644
--- a/scripts/updateVersion.js
+++ b/scripts/updateVersion.js
@@ -39,13 +39,9 @@ const packages = {
 };
 
 function updateVersion() {
-  const version = getVersionFromFile();
-  // update monorepo package.json version
+  // get version from monorepo package.json version
   const basePackageJSON = fs.readJsonSync(`./package.json`);
-  basePackageJSON.version = version;
-  fs.writeJsonSync(`./package.json`, basePackageJSON, {
-    spaces: 2,
-  });
+  const version = basePackageJSON.version;
   // update individual packages
   Object.values(packages).forEach((pkg) => {
     const packageJSON = fs.readJsonSync(`./packages/${pkg}/package.json`);
@@ -75,14 +71,4 @@ function updateDependencies(packageJSON, version) {
   }
 }
 
-function getVersionFromFile() {
-  const fileContent = fs.readFileSync(
-    './packages/lexical/src/LexicalVersion.ts',
-    'utf8',
-  );
-  const regex = /VERSION = '(\d{1,3}\.\d{1,3}\.\d{1,3})'/;
-  const version = regex.exec(fileContent)[1];
-  return version;
-}
-
 updateVersion();
diff --git a/scripts/www/rewriteImports.js b/scripts/www/rewriteImports.js
index cca5d8bcbe8..47182a1810a 100644
--- a/scripts/www/rewriteImports.js
+++ b/scripts/www/rewriteImports.js
@@ -74,7 +74,7 @@ glob('packages/**/flow/*.flow', options, function (error1, files) {
           /from '@lexical\/react\/DEPRECATED_useLexicalHistory'/g,
           "from 'DEPRECATED_useLexicalHistory'",
         )
-        .replace(/from '@lexical\/react\/'/g, "from 'Lexical")
+        .replace(/from '@lexical\/react\/Lexical/g, "from 'Lexical")
         .replace(/from '@lexical\/utils\/'/g, "from 'LexicalUtils")
         .replace(/from '@lexical\/clipboard\'/g, "from 'LexicalClipboard'")
         .replace(/from '@lexical\/code\'/g, "from 'LexicalCode'")