diff --git a/packages/lexical-clipboard/src/clipboard.ts b/packages/lexical-clipboard/src/clipboard.ts index c6f863dd42de..1723fa12982c 100644 --- a/packages/lexical-clipboard/src/clipboard.ts +++ b/packages/lexical-clipboard/src/clipboard.ts @@ -40,6 +40,7 @@ import { NodeSelection, RangeSelection, SELECTION_CHANGE_COMMAND, + SerializedElementNode, SerializedTextNode, } from 'lexical'; import {CAN_USE_DOM} from 'shared/canUseDOM'; @@ -408,7 +409,6 @@ function exportNodeToJSON(node: T): BaseSerializedNode { const serializedNode = node.exportJSON(); const nodeClass = node.constructor; - // @ts-expect-error TODO Replace Class utility type with InstanceType if (serializedNode.type !== nodeClass.getType()) { invariant( false, @@ -417,10 +417,9 @@ function exportNodeToJSON(node: T): BaseSerializedNode { ); } - // @ts-expect-error TODO Replace Class utility type with InstanceType - const serializedChildren = serializedNode.children; - if ($isElementNode(node)) { + const serializedChildren = (serializedNode as SerializedElementNode) + .children; if (!Array.isArray(serializedChildren)) { invariant( false, diff --git a/packages/lexical-code/src/__tests__/unit/LexicalCodeNode.test.ts b/packages/lexical-code/src/__tests__/unit/LexicalCodeNode.test.ts index e478af6d2813..b44bd3ad6060 100644 --- a/packages/lexical-code/src/__tests__/unit/LexicalCodeNode.test.ts +++ b/packages/lexical-code/src/__tests__/unit/LexicalCodeNode.test.ts @@ -21,9 +21,11 @@ import { $getNodeByKey, $getRoot, $getSelection, + $isElementNode, $isLineBreakNode, $isRangeSelection, $isTabNode, + $isTextNode, $setSelection, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, @@ -217,6 +219,7 @@ describe('LexicalCodeNode tests', () => { // TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection await editor.update(() => { const codeText = $getRoot().getFirstDescendant(); + invariant($isTextNode(codeText)); codeText.select(1, 'function'.length); }); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); @@ -240,6 +243,7 @@ describe('LexicalCodeNode tests', () => { // TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection await editor.update(() => { const codeText = $getRoot().getFirstDescendant(); + invariant($isTextNode(codeText)); codeText.select(0, 'function'.length); }); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); @@ -276,6 +280,7 @@ describe('LexicalCodeNode tests', () => { // TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection await editor.update(() => { const codeText = $getRoot().getFirstDescendant(); + invariant($isTextNode(codeText)); codeText.select(0, 0); }); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); @@ -365,6 +370,7 @@ describe('LexicalCodeNode tests', () => { // TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection await editor.update(() => { const codeText = $getRoot().getLastDescendant(); + invariant($isTextNode(codeText)); codeText.select(1, 1); }); await editor.dispatchCommand(KEY_TAB_COMMAND, shiftTabKeyboardEvent()); @@ -482,6 +488,7 @@ describe('LexicalCodeNode tests', () => { 'caret at start of line (first line)', () => { const code = $getRoot().getFirstChild(); + invariant($isElementNode(code)); code.selectStart(); }, () => { @@ -559,11 +566,13 @@ describe('LexicalCodeNode tests', () => { 'caret immediately before code (first line)', () => { const code = $getRoot().getFirstChild(); + invariant($isElementNode(code)); + const firstChild = code.getFirstChild(); + invariant($isTextNode(firstChild)); if (tabOrSpaces === 'tab') { - const firstTab = code.getFirstChild(); - firstTab.getNextSibling().selectNext(0, 0); + firstChild.getNextSibling().selectNext(0, 0); } else { - code.getFirstChild().select(4, 4); + firstChild.select(4, 4); } }, () => { @@ -575,6 +584,7 @@ describe('LexicalCodeNode tests', () => { expect(selection.isCollapsed()).toBe(true); if (moveTo === 'start') { const code = $getRoot().getFirstChild(); + invariant($isElementNode(code)); const firstChild = code.getFirstChild(); expect(selection.anchor.getNode().is(firstChild)).toBe(true); expect(selection.anchor.offset).toBe(0); @@ -629,11 +639,13 @@ describe('LexicalCodeNode tests', () => { 'caret in between space (first line)', () => { const code = $getRoot().getFirstChild(); + invariant($isElementNode(code)); + const firstChild = code.getFirstChild(); + invariant($isTextNode(firstChild)); if (tabOrSpaces === 'tab') { - const firstTab = code.getFirstChild(); - firstTab.selectNext(0, 0); + firstChild.selectNext(0, 0); } else { - code.getFirstChild().select(2, 2); + firstChild.select(2, 2); } }, () => { @@ -720,6 +732,7 @@ describe('LexicalCodeNode tests', () => { $isCodeHighlightNode(dfsNode.node), )[tabOrSpaces === 'tab' ? 0 : 1].node; const index = codeHighlight.getTextContent().indexOf('tion'); + invariant($isTextNode(codeHighlight)); codeHighlight.select(index, index); }, () => { @@ -761,6 +774,7 @@ describe('LexicalCodeNode tests', () => { $isCodeHighlightNode(dfsNode.node), )[tabOrSpaces === 'tab' ? 1 : 2].node; const index = codeHighlight.getTextContent().indexOf('oo'); + invariant($isTextNode(codeHighlight)); codeHighlight.select(index, index); }, () => { diff --git a/packages/lexical-html/src/index.ts b/packages/lexical-html/src/index.ts index 8dbf1b416a2c..4e1276806785 100644 --- a/packages/lexical-html/src/index.ts +++ b/packages/lexical-html/src/index.ts @@ -163,7 +163,7 @@ function getConversionFunction( if ( domConversion !== null && (currentConversion === null || - currentConversion.priority < domConversion.priority) + (currentConversion.priority ?? 0) < (domConversion.priority ?? 0)) ) { currentConversion = domConversion; } diff --git a/packages/lexical-link/src/index.ts b/packages/lexical-link/src/index.ts index 7db3bb1aab02..0bf5161d9577 100644 --- a/packages/lexical-link/src/index.ts +++ b/packages/lexical-link/src/index.ts @@ -367,7 +367,7 @@ export class AutoLinkNode extends LinkNode { ); if ($isElementNode(element)) { const linkNode = $createAutoLinkNode(this.__url, { - rel: this._rel, + rel: this.__rel, target: this.__target, title: this.__title, }); diff --git a/packages/lexical-list/src/LexicalListItemNode.ts b/packages/lexical-list/src/LexicalListItemNode.ts index 5419b49d48e2..6f4d7b036190 100644 --- a/packages/lexical-list/src/LexicalListItemNode.ts +++ b/packages/lexical-list/src/LexicalListItemNode.ts @@ -107,6 +107,7 @@ export class ListItemNode extends ElementNode { const parent = node.getParent(); if ($isListNode(parent)) { updateChildrenListItemValue(parent); + invariant($isListItemNode(node), 'node is not a ListItemNode'); if (parent.getListType() !== 'check' && node.getChecked() != null) { node.setChecked(undefined); } @@ -185,6 +186,10 @@ export class ListItemNode extends ElementNode { replaceWithNode.insertAfter(newList); } if (includeChildren) { + invariant( + $isElementNode(replaceWithNode), + 'includeChildren should only be true for ElementNodes', + ); this.getChildren().forEach((child: LexicalNode) => { replaceWithNode.append(child); }); diff --git a/packages/lexical-list/src/utils.ts b/packages/lexical-list/src/utils.ts index efe51f183d22..5f7437686614 100644 --- a/packages/lexical-list/src/utils.ts +++ b/packages/lexical-list/src/utils.ts @@ -6,7 +6,7 @@ * */ -import type {LexicalNode} from 'lexical'; +import type {LexicalNode, Spread} from 'lexical'; import invariant from 'shared/invariant'; @@ -124,6 +124,10 @@ export function $getAllListItems(node: ListNode): Array { return listItemNodes; } +const NestedListNodeBrand: unique symbol = Symbol.for( + '@lexical/NestedListNodeBrand', +); + /** * Checks to see if the passed node is a ListItemNode and has a ListNode as a child. * @param node - The node to be checked. @@ -131,7 +135,10 @@ export function $getAllListItems(node: ListNode): Array { */ export function isNestedListNode( node: LexicalNode | null | undefined, -): boolean { +): node is Spread< + {getFirstChild(): ListNode; [NestedListNodeBrand]: never}, + ListItemNode +> { return $isListItemNode(node) && $isListNode(node.getFirstChild()); } diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts index 72256297b49b..116b8f4c4cab 100644 --- a/packages/lexical-markdown/src/MarkdownImport.ts +++ b/packages/lexical-markdown/src/MarkdownImport.ts @@ -16,7 +16,7 @@ import type { import type {LexicalNode, TextNode} from 'lexical'; import {$createCodeNode} from '@lexical/code'; -import {$isListItemNode, $isListNode} from '@lexical/list'; +import {$isListItemNode, $isListNode, ListItemNode} from '@lexical/list'; import {$isQuoteNode} from '@lexical/rich-text'; import {$findMatchingParent} from '@lexical/utils'; import { @@ -145,7 +145,7 @@ function importBlocks( $isQuoteNode(previousNode) || $isListNode(previousNode) ) { - let targetNode: LexicalNode | null = previousNode; + let targetNode: typeof previousNode | ListItemNode | null = previousNode; if ($isListNode(previousNode)) { const lastDescendant = previousNode.getLastDescendant(); diff --git a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx index a7b1a68f7df4..ae5996f1cc08 100644 --- a/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx +++ b/packages/lexical-playground/src/plugins/TableCellResizer/index.tsx @@ -19,6 +19,7 @@ import { $isTableCellNode, $isTableRowNode, getCellFromTarget, + TableCellNode, } from '@lexical/table'; import { $getNearestNodeFromDOMNode, @@ -206,11 +207,11 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { throw new Error('Expected table row'); } - const rowCells = tableRow.getChildren(); + const rowCells = tableRow.getChildren(); const rowCellsSpan = rowCells.map((cell) => cell.getColSpan()); const aggregatedRowSpans = rowCellsSpan.reduce( - (rowSpans, cellSpan) => { + (rowSpans: number[], cellSpan) => { const previousCell = rowSpans[rowSpans.length - 1] ?? 0; rowSpans.push(previousCell + cellSpan); return rowSpans; @@ -316,14 +317,7 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element { document.addEventListener('mouseup', mouseUpHandler(direction)); }, - [ - activeCell, - draggingDirection, - resetState, - updateColumnWidth, - updateRowHeight, - mouseUpHandler, - ], + [activeCell, mouseUpHandler], ); const getResizers = useCallback(() => { diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx index 1e771494fc72..614bcd531892 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx @@ -757,20 +757,23 @@ export default function ToolbarPlugin({ // We split the first and last node by the selection // So that we don't format unselected text inside those nodes if ($isTextNode(node)) { + // Use a separate variable to ensure TS does not lose the refinement + let textNode = node; if (idx === 0 && anchor.offset !== 0) { - node = node.splitText(anchor.offset)[1] || node; + textNode = textNode.splitText(anchor.offset)[1] || textNode; } if (idx === nodes.length - 1) { - node = node.splitText(focus.offset)[0] || node; + textNode = textNode.splitText(focus.offset)[0] || textNode; } - if (node.__style !== '') { - node.setStyle(''); + if (textNode.__style !== '') { + textNode.setStyle(''); } - if (node.__format !== 0) { - node.setFormat(0); - $getNearestBlockElementAncestorOrThrow(node).setFormat(''); + if (textNode.__format !== 0) { + textNode.setFormat(0); + $getNearestBlockElementAncestorOrThrow(textNode).setFormat(''); } + node = textNode; } else if ($isHeadingNode(node) || $isQuoteNode(node)) { node.replace($createParagraphNode(), true); } else if ($isDecoratorBlockNode(node)) { diff --git a/packages/lexical-react/src/LexicalTablePlugin.ts b/packages/lexical-react/src/LexicalTablePlugin.ts index d98532af314a..955296cd1e57 100644 --- a/packages/lexical-react/src/LexicalTablePlugin.ts +++ b/packages/lexical-react/src/LexicalTablePlugin.ts @@ -17,6 +17,7 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import { $createTableCellNode, $createTableNodeWithDimensions, + $isTableCellNode, $isTableNode, applyTableHandlers, INSERT_TABLE_COMMAND, @@ -176,6 +177,10 @@ export function TablePlugin({ lastRowCell = cell; unmerged.push(cell); } else if (cell.getColSpan() > 1 || cell.getRowSpan() > 1) { + invariant( + $isTableCellNode(cell), + 'Expected TableNode cell to be a TableCellNode', + ); const newCell = $createTableCellNode(cell.__headerState); if (lastRowCell !== null) { lastRowCell.insertAfter(newCell); diff --git a/packages/lexical-react/src/LexicalTreeView.tsx b/packages/lexical-react/src/LexicalTreeView.tsx index 3660312a65f9..d5a3cc3e4d95 100644 --- a/packages/lexical-react/src/LexicalTreeView.tsx +++ b/packages/lexical-react/src/LexicalTreeView.tsx @@ -14,6 +14,7 @@ import type { LexicalNode, NodeSelection, RangeSelection, + TextNode, } from 'lexical'; import {$generateHtmlFromNodes} from '@lexical/html'; @@ -517,30 +518,30 @@ function printNode(node: LexicalNode) { } const FORMAT_PREDICATES = [ - (node: LexicalNode | RangeSelection) => node.hasFormat('bold') && 'Bold', - (node: LexicalNode | RangeSelection) => node.hasFormat('code') && 'Code', - (node: LexicalNode | RangeSelection) => node.hasFormat('italic') && 'Italic', - (node: LexicalNode | RangeSelection) => + (node: TextNode | RangeSelection) => node.hasFormat('bold') && 'Bold', + (node: TextNode | RangeSelection) => node.hasFormat('code') && 'Code', + (node: TextNode | RangeSelection) => node.hasFormat('italic') && 'Italic', + (node: TextNode | RangeSelection) => node.hasFormat('strikethrough') && 'Strikethrough', - (node: LexicalNode | RangeSelection) => + (node: TextNode | RangeSelection) => node.hasFormat('subscript') && 'Subscript', - (node: LexicalNode | RangeSelection) => + (node: TextNode | RangeSelection) => node.hasFormat('superscript') && 'Superscript', - (node: LexicalNode | RangeSelection) => + (node: TextNode | RangeSelection) => node.hasFormat('underline') && 'Underline', ]; const DETAIL_PREDICATES = [ - (node: LexicalNode) => node.isDirectionless() && 'Directionless', - (node: LexicalNode) => node.isUnmergeable() && 'Unmergeable', + (node: TextNode) => node.isDirectionless() && 'Directionless', + (node: TextNode) => node.isUnmergeable() && 'Unmergeable', ]; const MODE_PREDICATES = [ - (node: LexicalNode) => node.isToken() && 'Token', - (node: LexicalNode) => node.isSegmented() && 'Segmented', + (node: TextNode) => node.isToken() && 'Token', + (node: TextNode) => node.isSegmented() && 'Segmented', ]; -function printAllTextNodeProperties(node: LexicalNode) { +function printAllTextNodeProperties(node: TextNode) { return [ printFormatProperties(node), printDetailProperties(node), @@ -560,7 +561,7 @@ function printAllLinkNodeProperties(node: LinkNode) { .join(', '); } -function printDetailProperties(nodeOrSelection: LexicalNode) { +function printDetailProperties(nodeOrSelection: TextNode) { let str = DETAIL_PREDICATES.map((predicate) => predicate(nodeOrSelection)) .filter(Boolean) .join(', ') @@ -573,7 +574,7 @@ function printDetailProperties(nodeOrSelection: LexicalNode) { return str; } -function printModeProperties(nodeOrSelection: LexicalNode) { +function printModeProperties(nodeOrSelection: TextNode) { let str = MODE_PREDICATES.map((predicate) => predicate(nodeOrSelection)) .filter(Boolean) .join(', ') @@ -586,7 +587,7 @@ function printModeProperties(nodeOrSelection: LexicalNode) { return str; } -function printFormatProperties(nodeOrSelection: LexicalNode | RangeSelection) { +function printFormatProperties(nodeOrSelection: TextNode | RangeSelection) { let str = FORMAT_PREDICATES.map((predicate) => predicate(nodeOrSelection)) .filter(Boolean) .join(', ') diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index fb1340a3e214..11032c0c9d3d 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -648,7 +648,7 @@ export function registerRichText(editor: LexicalEditor): () => void { for (const node of nodes) { const element = $findMatchingParent( node, - (parentNode) => + (parentNode): parentNode is ElementNode => $isElementNode(parentNode) && !parentNode.isInline(), ); if (element !== null) { diff --git a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx index db768b278efa..0c4eeaeeda43 100644 --- a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx +++ b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx @@ -23,20 +23,23 @@ import {$createTableNodeWithDimensions} from '@lexical/table'; import { $createLineBreakNode, $createParagraphNode, + $createRangeSelection, $createTextNode, $getRoot, $getSelection, + $isElementNode, $isNodeSelection, $isRangeSelection, + $setSelection, ParagraphNode, } from 'lexical'; -import {$createRangeSelection, $setSelection} from 'lexical/src'; import { $assertRangeSelection, $createTestDecoratorNode, $createTestElementNode, createTestEditor, initializeClipboard, + invariant, TestComposer, } from 'lexical/src/__tests__/utils'; import * as React from 'react'; @@ -2328,12 +2331,12 @@ describe('LexicalSelection tests', () => { $setSelection(selection); setAnchorPoint({ key: text1.__key, - offset: text1.length, + offset: text1.__text.length, type: 'text', }); setFocusPoint({ key: text1.__key, - offset: text1.length, + offset: text1.__text.length, type: 'text', }); @@ -2407,7 +2410,7 @@ describe('LexicalSelection tests', () => { }); setFocusPoint({ key: text2.__key, - offset: text1.length, + offset: text1.__text.length, type: 'text', }); @@ -2483,7 +2486,7 @@ describe('LexicalSelection tests', () => { }); setFocusPoint({ key: text2.__key, - offset: text1.length, + offset: text1.__text.length, type: 'text', }); @@ -2507,8 +2510,11 @@ describe('LexicalSelection tests', () => { const root = $getRoot(); const table = $createTableNodeWithDimensions(1, 1); const row = table.getFirstChild(); + invariant($isElementNode(row)); const column = row.getFirstChild(); + invariant($isElementNode(column)); const paragraph = column.getFirstChild(); + invariant($isElementNode(paragraph)); if (paragraph.getFirstChild()) paragraph.getFirstChild().remove(); root.append(table); @@ -2546,8 +2552,11 @@ describe('LexicalSelection tests', () => { const root = $getRoot(); const table = $createTableNodeWithDimensions(1, 1); const row = table.getFirstChild(); + invariant($isElementNode(row)); const column = row.getFirstChild(); + invariant($isElementNode(column)); const paragraph = column.getFirstChild(); + invariant($isElementNode(paragraph)); const text = $createTextNode('foo'); root.append(table); paragraph.append(text); @@ -2556,12 +2565,12 @@ describe('LexicalSelection tests', () => { $setSelection(selectionz); setAnchorPoint({ key: text.__key, - offset: text.length, + offset: text.__text.length, type: 'text', }); setFocusPoint({ key: text.__key, - offset: text.length, + offset: text.__text.length, type: 'text', }); // @ts-ignore @@ -2597,6 +2606,7 @@ describe('LexicalSelection tests', () => { const table = $createTableNodeWithDimensions(1, 2); const row = table.getFirstChild(); + invariant($isElementNode(row)); const columns = row.getChildren(); root.append(table); @@ -2607,11 +2617,13 @@ describe('LexicalSelection tests', () => { const text4 = $createTextNode(); paragraph1.append(text3); paragraph2.append(text4); + invariant($isElementNode(column1)); column1.append(paragraph3, paragraph4); const column2 = columns[1]; const paragraph5 = $createParagraphNode(); const paragraph6 = $createParagraphNode(); + invariant($isElementNode(column2)); column2.append(paragraph5, paragraph6); const paragraph7 = $createParagraphNode(); @@ -2635,8 +2647,7 @@ describe('LexicalSelection tests', () => { $setBlocksType(selection, () => { return $createHeadingNode('h1'); }); - - expect(JSON.stringify(testEditor._pendingEditorState.toJSON())).toBe( + 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,"colSpan":1,"rowSpan":1,"backgroundColor":null,"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,"colSpan":1,"rowSpan":1,"backgroundColor":null,"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}}', ); }); @@ -2682,6 +2693,7 @@ describe('LexicalSelection tests', () => { const rootChildren = root.getChildren(); expect(rootChildren.length).toBe(1); + invariant($isElementNode(rootChildren[0])); expect(rootChildren[0].getType()).toBe('heading'); expect(rootChildren[0].getChildrenKeys()).toEqual( paragraphChildrenKeys, diff --git a/packages/lexical-selection/src/__tests__/unit/LexicalSelectionHelpers.test.ts b/packages/lexical-selection/src/__tests__/unit/LexicalSelectionHelpers.test.ts index 31b56b399009..b559ab6ab62a 100644 --- a/packages/lexical-selection/src/__tests__/unit/LexicalSelectionHelpers.test.ts +++ b/packages/lexical-selection/src/__tests__/unit/LexicalSelectionHelpers.test.ts @@ -21,6 +21,7 @@ import { $getRoot, $getSelection, $insertNodes, + $isElementNode, $isNodeSelection, $isRangeSelection, $setSelection, @@ -33,6 +34,7 @@ import { $createTestShadowRootNode, createTestEditor, createTestHeadlessEditor, + invariant, TestDecoratorNode, } from 'lexical/src/__tests__/utils'; @@ -2660,7 +2662,9 @@ describe('insertNodes', () => { }); await editor.update(() => { - const selection = $getRoot().getFirstChild().select(); + const selectionNode = $getRoot().getFirstChild(); + invariant($isElementNode(selectionNode)); + const selection = selectionNode.select(); selection.insertNodes([ $createParagraphNode().append($createTextNode('Text before')), ]); diff --git a/packages/lexical-selection/src/lexical-node.ts b/packages/lexical-selection/src/lexical-node.ts index 0aa20a6eb6e1..bc4a669c6ec2 100644 --- a/packages/lexical-selection/src/lexical-node.ts +++ b/packages/lexical-selection/src/lexical-node.ts @@ -27,6 +27,7 @@ import { RangeSelection, TextNode, } from 'lexical'; +import invariant from 'shared/invariant'; import {CSS_TO_STYLES} from './constants'; import { @@ -143,8 +144,13 @@ export function $isAtNodeEnd(point: Point): boolean { if (point.type === 'text') { return point.offset === point.getNode().getTextContentSize(); } + const node = point.getNode(); + invariant( + $isElementNode(node), + 'isAtNodeEnd: node must be a TextNode or ElementNode', + ); - return point.offset === point.getNode().getChildrenSize(); + return point.offset === node.getChildrenSize(); } /** diff --git a/packages/lexical-selection/src/range-selection.ts b/packages/lexical-selection/src/range-selection.ts index f6e5c63c7d64..f6286570947e 100644 --- a/packages/lexical-selection/src/range-selection.ts +++ b/packages/lexical-selection/src/range-selection.ts @@ -31,6 +31,7 @@ import { $isTextNode, $setSelection, } from 'lexical'; +import invariant from 'shared/invariant'; import {getStyleObjectFromCSS} from './utils'; @@ -72,6 +73,7 @@ export function $setBlocksType( if (!INTERNAL_$isBlock(node)) { continue; } + invariant($isElementNode(node), 'Expected block node to be an ElementNode'); const targetElement = createElement(); targetElement.setFormat(node.getFormatType()); @@ -285,6 +287,10 @@ export function $wrapNodesImpl( $removeParentEmptyElements(parent); } } else if (emptyElements.has(node.getKey())) { + invariant( + $isElementNode(node), + 'Expected node in emptyElements to be an ElementNode', + ); const targetElement = createElement(); targetElement.setFormat(node.getFormatType()); targetElement.setIndent(node.getIndent()); diff --git a/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts b/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts index 1e1d8c285f94..28783003cddd 100644 --- a/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts +++ b/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts @@ -11,12 +11,14 @@ import { $createTextNode, $getNodeByKey, $getRoot, + $isElementNode, LexicalEditor, NodeKey, } from 'lexical'; import { $createTestElementNode, initializeUnitTest, + invariant, } from 'lexical/src/__tests__/utils'; import {$dfs} from '../..'; @@ -173,6 +175,7 @@ describe('LexicalNodeHelpers tests', () => { const block2 = $getNodeByKey(block2Key); const block3 = $createTestElementNode(); + invariant($isElementNode(block1)); block1.append(block3); diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index c44ea0bec134..b284abe64cbc 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -266,10 +266,19 @@ export type DOMNodeToLexicalConversionMap = Record< * @param findFn - A testing function that returns true if the current node satisfies the testing parameters. * @returns A parent node that matches the findFn parameters, or null if one wasn't found. */ -export function $findMatchingParent( +export const $findMatchingParent: { + ( + startingNode: LexicalNode, + findFn: (node: LexicalNode) => node is T, + ): T | null; + ( + startingNode: LexicalNode, + findFn: (node: LexicalNode) => boolean, + ): LexicalNode | null; +} = ( startingNode: LexicalNode, findFn: (node: LexicalNode) => boolean, -): LexicalNode | null { +): LexicalNode | null => { let curr: ElementNode | LexicalNode | null = startingNode; while (curr !== $getRoot() && curr != null) { @@ -281,7 +290,7 @@ export function $findMatchingParent( } return null; -} +}; /** * Attempts to resolve nested element nodes of the same type into a single node of that type. @@ -383,7 +392,7 @@ export function $restoreEditorState( for (const [key, node] of editorState._nodeMap) { const clone = $cloneWithProperties(node); - if ($isTextNode(clone)) { + if ($isTextNode(clone) && $isTextNode(node)) { clone.__text = node.__text; } nodeMap.set(key, clone); diff --git a/packages/lexical-yjs/src/SyncCursors.ts b/packages/lexical-yjs/src/SyncCursors.ts index 372508503d14..431d3d362488 100644 --- a/packages/lexical-yjs/src/SyncCursors.ts +++ b/packages/lexical-yjs/src/SyncCursors.ts @@ -26,6 +26,7 @@ import { $isRangeSelection, $isTextNode, } from 'lexical'; +import invariant from 'shared/invariant'; import { compareRelativePositions, createAbsolutePositionFromRelativePosition, @@ -87,6 +88,7 @@ function createRelativePosition( point.type === 'element' ) { const parent = point.getNode(); + invariant($isElementNode(parent), 'Element point must be an element node'); let accumulatedOffset = 0; let i = 0; let node = parent.getFirstChild(); diff --git a/packages/lexical-yjs/src/Utils.ts b/packages/lexical-yjs/src/Utils.ts index ce11683bf74b..677905788f8f 100644 --- a/packages/lexical-yjs/src/Utils.ts +++ b/packages/lexical-yjs/src/Utils.ts @@ -24,7 +24,6 @@ import { $isRootNode, $isTextNode, createEditor, - Klass, NodeKey, } from 'lexical'; import invariant from 'shared/invariant'; @@ -77,7 +76,7 @@ function isExcludedProperty( } } - const nodeKlass = node.constructor as Klass; + const nodeKlass = node.constructor; const excludedProperties = binding.excludedProperties.get(nodeKlass); return excludedProperties != null && excludedProperties.has(name); } @@ -272,7 +271,8 @@ export function syncPropertiesFromYjs( if (isExcludedProperty(property, lexicalNode, binding)) { continue; } - const prevValue = lexicalNode[property]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const prevValue = (lexicalNode as any)[property]; let nextValue = sharedType instanceof YMap ? sharedType.get(property) @@ -298,7 +298,7 @@ export function syncPropertiesFromYjs( writableNode = lexicalNode.getWritable(); } - writableNode[property] = nextValue; + writableNode[property as keyof typeof writableNode] = nextValue; } } } @@ -324,8 +324,10 @@ export function syncPropertiesFromLexical( for (let i = 0; i < properties.length; i++) { const property = properties[i]; const prevValue = - prevLexicalNode === null ? undefined : prevLexicalNode[property]; - let nextValue = nextLexicalNode[property]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + prevLexicalNode === null ? undefined : (prevLexicalNode as any)[property]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let nextValue = (nextLexicalNode as any)[property]; if (prevValue !== nextValue) { if (nextValue instanceof EditorClass) { @@ -333,7 +335,6 @@ export function syncPropertiesFromLexical( let prevDoc; if (prevValue instanceof EditorClass) { - // @ts-expect-error Lexical node const prevKey = prevValue._key; prevDoc = yjsDocMap.get(prevKey); yjsDocMap.delete(prevKey); @@ -342,7 +343,6 @@ export function syncPropertiesFromLexical( // If we already have a document, use it. const doc = prevDoc || new Doc(); const key = doc.guid; - // @ts-expect-error Lexical node nextValue._key = key; yjsDocMap.set(key, doc); nextValue = doc; diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index df8600e26939..6efb1dd9f258 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -45,10 +45,18 @@ import {TabNode} from './nodes/LexicalTabNode'; export type Spread = Omit & T1; -export type Klass = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (...args: any[]): T; -} & Omit; +// https://github.com/microsoft/TypeScript/issues/3841 +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type KlassConstructor> = + GenericConstructor> & {[k in keyof Cls]: Cls[k]}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type GenericConstructor = new (...args: any[]) => T; + +export type Klass = InstanceType< + T['constructor'] +> extends T + ? T['constructor'] + : GenericConstructor & T['constructor']; export type EditorThemeClassName = string; @@ -63,6 +71,7 @@ export type TextNodeThemeClasses = { superscript?: EditorThemeClassName; underline?: EditorThemeClassName; underlineStrikethrough?: EditorThemeClassName; + [key: string]: EditorThemeClassName | undefined; }; export type EditorUpdateOptions = { @@ -519,6 +528,8 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor { return editor; } export class LexicalEditor { + ['constructor']!: KlassConstructor; + /** @internal */ _headless: boolean; /** @internal */ diff --git a/packages/lexical/src/LexicalEditorState.ts b/packages/lexical/src/LexicalEditorState.ts index 64c04469ee53..bc9f95c53c73 100644 --- a/packages/lexical/src/LexicalEditorState.ts +++ b/packages/lexical/src/LexicalEditorState.ts @@ -17,7 +17,7 @@ import type {SerializedRootNode} from './nodes/LexicalRootNode'; import invariant from 'shared/invariant'; -import {$isElementNode} from '.'; +import {$isElementNode, SerializedElementNode} from '.'; import {readEditorState} from './LexicalUpdates'; import {$getRoot} from './LexicalUtils'; import {$createRootNode} from './nodes/LexicalRootNode'; @@ -56,11 +56,12 @@ export function createEmptyEditorState(): EditorState { return new EditorState(new Map([['root', $createRootNode()]])); } -function exportNodeToJSON(node: LexicalNode): SerializedNode { +function exportNodeToJSON( + node: LexicalNode, +): SerializedNode { const serializedNode = node.exportJSON(); const nodeClass = node.constructor; - // @ts-expect-error TODO Replace Class utility type with InstanceType if (serializedNode.type !== nodeClass.getType()) { invariant( false, @@ -69,10 +70,9 @@ function exportNodeToJSON(node: LexicalNode): SerializedNode { ); } - // @ts-expect-error TODO Replace Class utility type with InstanceType - const serializedChildren = serializedNode.children; - if ($isElementNode(node)) { + const serializedChildren = (serializedNode as SerializedElementNode) + .children; if (!Array.isArray(serializedChildren)) { invariant( false, diff --git a/packages/lexical/src/LexicalEvents.ts b/packages/lexical/src/LexicalEvents.ts index 36c2bfed9857..e6f04ec7f886 100644 --- a/packages/lexical/src/LexicalEvents.ts +++ b/packages/lexical/src/LexicalEvents.ts @@ -19,6 +19,7 @@ import { IS_IOS, IS_SAFARI, } from 'shared/environment'; +import invariant from 'shared/invariant'; import { $getPreviousSelection, @@ -332,6 +333,10 @@ function onSelectionChange( selection.style = lastStyle; } else { if (anchor.type === 'text') { + invariant( + $isTextNode(anchorNode), + 'Point.getNode() must return TextNode when type is text', + ); selection.format = anchorNode.getFormat(); selection.style = anchorNode.getStyle(); } else if (anchor.type === 'element') { @@ -537,6 +542,10 @@ function onBeforeInput(event: InputEvent, editor: LexicalEditor): void { const anchorNode = selection.anchor.getNode(); anchorNode.markDirty(); selection.format = anchorNode.getFormat(); + invariant( + $isTextNode(anchorNode), + 'Anchor node must be a TextNode', + ); selection.style = anchorNode.getStyle(); } } else { @@ -847,7 +856,7 @@ function onCompositionStart( anchor.type === 'element' || !selection.isCollapsed() || node.getFormat() !== selection.format || - node.getStyle() !== selection.style + ($isTextNode(node) && node.getStyle() !== selection.style) ) { // We insert a zero width character, ready for the composition // to get inserted into the new node we create. If diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index 42419cbbe7f3..61a7d1e4dda0 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -13,7 +13,7 @@ import type { NodeSelection, RangeSelection, } from './LexicalSelection'; -import type {Klass} from 'lexical'; +import type {Klass, KlassConstructor} from 'lexical'; import invariant from 'shared/invariant'; @@ -125,7 +125,7 @@ export function removeNode( export type DOMConversion = { conversion: DOMConversionFn; - priority: 0 | 1 | 2 | 3 | 4; + priority?: 0 | 1 | 2 | 3 | 4; }; export type DOMConversionFn = ( @@ -159,8 +159,8 @@ export type DOMExportOutput = { export type NodeKey = string; export class LexicalNode { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [x: string]: any; + // Allow us to look up the type including static props + ['constructor']!: KlassConstructor; /** @internal */ __type: string; /** @internal */ @@ -206,8 +206,10 @@ export class LexicalNode { ); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static importDOM?: () => DOMConversionMap | null; + constructor(key?: NodeKey) { - // @ts-expect-error this.__type = this.constructor.getType(); this.__parent = null; this.__prev = null; @@ -217,11 +219,7 @@ export class LexicalNode { if (__DEV__) { if (this.__type !== 'root') { errorOnReadOnly(); - errorOnTypeKlassMismatch( - this.__type, - // @ts-expect-error - this.constructor, - ); + errorOnTypeKlassMismatch(this.__type, this.constructor); } } } @@ -234,6 +232,14 @@ export class LexicalNode { return this.__type; } + isInline(): boolean { + invariant( + false, + 'LexicalNode: Node %s does not implement .isInline().', + this.constructor.name, + ); + } + /** * Returns true if there is a path between this node and the RootNode, false otherwise. * This is a way of determining if the node is "attached" EditorState. Unattached nodes @@ -347,11 +353,15 @@ export class LexicalNode { * non-root ancestor of this node, or null if none is found. See {@link lexical!$isRootOrShadowRoot} * for more information on which Elements comprise "roots". */ - getTopLevelElement(): ElementNode | this | null { + getTopLevelElement(): ElementNode | null { let node: ElementNode | this | null = this; while (node !== null) { - const parent: ElementNode | this | null = node.getParent(); + const parent: ElementNode | null = node.getParent(); if ($isRootOrShadowRoot(parent)) { + invariant( + $isElementNode(node), + 'Children of root nodes must be elements', + ); return node; } node = parent; @@ -364,7 +374,7 @@ export class LexicalNode { * non-root ancestor of this node, or throws if none is found. See {@link lexical!$isRootOrShadowRoot} * for more information on which Elements comprise "roots". */ - getTopLevelElementOrThrow(): ElementNode | this { + getTopLevelElementOrThrow(): ElementNode { const parent = this.getTopLevelElement(); if (parent === null) { invariant( @@ -688,7 +698,6 @@ export class LexicalNode { return latestNode; } const constructor = latestNode.constructor; - // @ts-expect-error const mutableNode = constructor.clone(latestNode); mutableNode.__parent = parent; mutableNode.__next = latestNode.__next; @@ -712,6 +721,7 @@ export class LexicalNode { // Update reference in node map nodeMap.set(key, mutableNode); + // @ts-expect-error return mutableNode; } @@ -877,6 +887,10 @@ export class LexicalNode { writableReplaceWith.__parent = parentKey; writableParent.__size = size; if (includeChildren) { + invariant( + $isElementNode(this) && $isElementNode(writableReplaceWith), + 'includeChildren should only be true for ElementNodes', + ); this.getChildren().forEach((child: LexicalNode) => { writableReplaceWith.append(child); }); diff --git a/packages/lexical/src/LexicalReconciler.ts b/packages/lexical/src/LexicalReconciler.ts index f77bf78a5677..175da8fc53de 100644 --- a/packages/lexical/src/LexicalReconciler.ts +++ b/packages/lexical/src/LexicalReconciler.ts @@ -643,8 +643,9 @@ function reconcileNode( nextNode.__cachedText !== editorTextContent ) { // Cache the latest text content. - nextNode = nextNode.getWritable(); - nextNode.__cachedText = editorTextContent; + const nextRootNode = nextNode.getWritable(); + nextRootNode.__cachedText = editorTextContent; + nextNode = nextRootNode; } if (__DEV__) { diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts index 5c0a0a80039f..3f0746489100 100644 --- a/packages/lexical/src/LexicalSelection.ts +++ b/packages/lexical/src/LexicalSelection.ts @@ -399,7 +399,7 @@ export function DEPRECATED_$getGridCellNodeRect( colSpan: number; } | null { const [CellNode, , GridNode] = DEPRECATED_$getNodeTriplet(GridCellNode); - const rows = GridNode.getChildren(); + const rows = GridNode.getChildren(); const rowCount = rows.length; const columnCount = rows[0].getChildren().length; @@ -411,7 +411,7 @@ export function DEPRECATED_$getGridCellNodeRect( for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { const row = rows[rowIndex]; - const cells = row.getChildren(); + const cells = row.getChildren(); let columnIndex = 0; for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) { @@ -516,6 +516,11 @@ export class GridSelection implements BaseSelection { insertNodes(nodes: Array) { const focusNode = this.focus.getNode(); + invariant( + $isElementNode(focusNode), + 'Expected GridSelection focus to be an ElementNode', + ); + const selection = $normalizeSelection( focusNode.select(0, focusNode.getChildrenSize()), ); @@ -1538,14 +1543,14 @@ export class RangeSelection implements BaseSelection { const firstBlock = $getAncestor(this.anchor.getNode(), INTERNAL_$isBlock)!; // case where we insert inside a code block - if ('__language' in firstBlock) { + if ('__language' in firstBlock && $isElementNode(firstBlock)) { if ('__language' in nodes[0]) { this.insertText(nodes[0].getTextContent()); } else { const index = removeTextAndSplitBlock(this); firstBlock.splice(index, 0, nodes); const last = nodes[nodes.length - 1]!; - if (last.select) { + if ('select' in last && typeof last.select === 'function') { last.select(); } else last.selectNext(0, 0); } @@ -1554,7 +1559,7 @@ export class RangeSelection implements BaseSelection { const notInline = (node: LexicalNode) => ($isElementNode(node) || $isDecoratorNode(node)) && !node.isInline(); - const isMergeable = (node: LexicalNode) => + const isMergeable = (node: LexicalNode): node is ElementNode => $isElementNode(node) && INTERNAL_$isBlock(node) && !node.isEmpty() && @@ -1576,10 +1581,18 @@ export class RangeSelection implements BaseSelection { let currentBlock = firstBlock; for (const node of nodes) { if (node === firstNotInline && isMergeable(node)) { + invariant( + $isElementNode(currentBlock), + 'currentBlock must not be a DecoratorNode', + ); currentBlock.append(...node.getChildren()); } else if (notInline(node)) { currentBlock = currentBlock.insertAfter(node) as ElementNode; } else { + invariant( + $isElementNode(currentBlock), + 'currentBlock must not be a DecoratorNode', + ); currentBlock.append(node); } } @@ -1596,7 +1609,9 @@ export class RangeSelection implements BaseSelection { firstBlock.remove(); } - if (!nodeToSelect.select) { + if ( + !('select' in nodeToSelect && typeof nodeToSelect.select === 'function') + ) { nodeToSelect.selectNext(0, 0); } else { nodeToSelect.select(nodeToSelectSize, nodeToSelectSize); @@ -1624,6 +1639,7 @@ export class RangeSelection implements BaseSelection { } const index = removeTextAndSplitBlock(this); const block = $getAncestor(this.anchor.getNode(), INTERNAL_$isBlock)!; + invariant(!$isDecoratorNode(block), 'Ancestor must not be a DecoratorNode'); const firstToAppend = block.getChildAtIndex(index); const nodesToInsert = firstToAppend ? [firstToAppend, ...firstToAppend.getNextSiblings()] diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index 10e24a67461f..da2108213349 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -17,6 +17,7 @@ import type { NodeMutation, RegisteredNode, RegisteredNodes, + Spread, } from './LexicalEditor'; import type {EditorState} from './LexicalEditorState'; import type {LexicalNode, NodeKey, NodeMap} from './LexicalNode'; @@ -1322,14 +1323,23 @@ export function $getNearestRootOrShadowRoot( return parent; } -export function $isRootOrShadowRoot(node: null | LexicalNode): boolean { +const ShadowRootNodeBrand: unique symbol = Symbol.for( + '@lexical/ShadowRootNodeBrand', +); +type ShadowRootNode = Spread< + {isShadowRoot(): true; [ShadowRootNodeBrand]: never}, + ElementNode +>; +export function $isRootOrShadowRoot( + node: null | LexicalNode, +): node is RootNode | ShadowRootNode { return $isRootNode(node) || ($isElementNode(node) && node.isShadowRoot()); } export function $copyNode(node: T): T { - // @ts-ignore const copy = node.constructor.clone(node); $setNodeKey(copy, null); + // @ts-expect-error return copy; } @@ -1337,7 +1347,7 @@ export function $applyNodeReplacement( node: LexicalNode, ): N { const editor = getActiveEditor(); - const nodeType = (node.constructor as Klass).getType(); + const nodeType = node.constructor.getType(); const registeredNode = editor._nodes.get(nodeType); if (registeredNode === undefined) { invariant( @@ -1505,9 +1515,9 @@ export function $splitNode( 'Can not call $splitNode() on root element', ); - const recurse = ( - currentNode: LexicalNode, - ): [ElementNode, ElementNode, LexicalNode] => { + const recurse = ( + currentNode: T, + ): [ElementNode, ElementNode, T] => { const parent = currentNode.getParentOrThrow(); const isParentRoot = $isRootOrShadowRoot(parent); // The node we start split from (leaf) is moved, but its recursive @@ -1518,12 +1528,13 @@ export function $splitNode( : $copyNode(currentNode); if (isParentRoot) { + invariant( + $isElementNode(currentNode) && $isElementNode(nodeToMove), + 'Children of a root must be ElementNode', + ); + currentNode.insertAfter(nodeToMove); - return [ - currentNode as ElementNode, - nodeToMove as ElementNode, - nodeToMove, - ]; + return [currentNode, nodeToMove, nodeToMove]; } else { const [leftTree, rightTree, newParent] = recurse(parent); const nextSiblings = currentNode.getNextSiblings(); diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx index 388681541814..74035ad639af 100644 --- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx +++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx @@ -429,7 +429,7 @@ describe('LexicalEditor tests', () => { const child = paragraph.getLastDescendant(); if ( - child !== null && + $isTextNode(child) && child.hasFormat('bold') && !child.hasFormat('italic') ) { diff --git a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts index 2b7d9d2c94ab..003557b68c4f 100644 --- a/packages/lexical/src/__tests__/unit/LexicalNode.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalNode.test.ts @@ -93,7 +93,7 @@ describe('LexicalNode tests', () => { await editor.update(() => { const node = new LexicalNode('__custom_key__'); - expect(() => node.clone()).toThrow(); + expect(() => LexicalNode.clone(node)).toThrow(); }); }); diff --git a/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts b/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts index e9949792dbe6..385c6708db2e 100644 --- a/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts @@ -6,16 +6,18 @@ * */ -import {$createLinkNode} from '@lexical/link'; +import {$createLinkNode, $isLinkNode} from '@lexical/link'; import { $createParagraphNode, $createTextNode, $getRoot, + $isParagraphNode, + $isTextNode, LexicalEditor, RangeSelection, } from 'lexical'; -import {initializeUnitTest} from '../utils'; +import {initializeUnitTest, invariant} from '../utils'; describe('LexicalSelection tests', () => { initializeUnitTest((testEnv) => { @@ -99,7 +101,9 @@ describe('LexicalSelection tests', () => { }) => { await editor.update(() => { const paragraph = $getRoot().getFirstChildOrThrow(); + invariant($isParagraphNode(paragraph)); const linkNode = paragraph.getFirstChildOrThrow(); + invariant($isLinkNode(linkNode)); // Place the cursor at the start of the link node // For review: is there a way to select "outside" of the link @@ -139,7 +143,9 @@ describe('LexicalSelection tests', () => { }) => { await editor.update(() => { const paragraph = $getRoot().getFirstChildOrThrow(); + invariant($isParagraphNode(paragraph)); const textNode = paragraph.getFirstChildOrThrow(); + invariant($isTextNode(textNode)); // Place the cursor between the link and the first text node by // selecting the end of the text node @@ -177,7 +183,9 @@ describe('LexicalSelection tests', () => { }) => { await editor.update(() => { const paragraph = $getRoot().getFirstChildOrThrow(); + invariant($isParagraphNode(paragraph)); const textNode = paragraph.getFirstChildOrThrow(); + invariant($isTextNode(textNode)); // Place the cursor before the link element by selecting the end // of the text node @@ -217,7 +225,9 @@ describe('LexicalSelection tests', () => { }) => { await editor.update(() => { const paragraph = $getRoot().getFirstChildOrThrow(); + invariant($isParagraphNode(paragraph)); const textNode = paragraph.getLastChildOrThrow(); + invariant($isTextNode(textNode)); // Place the cursor between the link and the last text node by // selecting the start of the text node @@ -256,7 +266,9 @@ describe('LexicalSelection tests', () => { }) => { await editor.update(() => { const paragraph = $getRoot().getFirstChildOrThrow(); + invariant($isParagraphNode(paragraph)); const textNode = paragraph.getLastChildOrThrow(); + invariant($isTextNode(textNode)); // Place the cursor between the link and the last text node by // selecting the start of the text node @@ -295,7 +307,9 @@ describe('LexicalSelection tests', () => { }) => { await editor.update(() => { const paragraph = $getRoot().getFirstChildOrThrow(); + invariant($isParagraphNode(paragraph)); const linkNode = paragraph.getLastChildOrThrow(); + invariant($isLinkNode(linkNode)); // Place the cursor at the end of the link element // For review: not sure if there's a better way to select diff --git a/packages/lexical/src/__tests__/utils/index.tsx b/packages/lexical/src/__tests__/utils/index.tsx index 43123971ce27..a76e1f5c41d4 100644 --- a/packages/lexical/src/__tests__/utils/index.tsx +++ b/packages/lexical/src/__tests__/utils/index.tsx @@ -12,7 +12,10 @@ import {createHeadlessEditor} from '@lexical/headless'; import {AutoLinkNode, LinkNode} from '@lexical/link'; import {ListItemNode, ListNode} from '@lexical/list'; import {OverflowNode} from '@lexical/overflow'; -import {LexicalComposer} from '@lexical/react/src/LexicalComposer'; +import { + InitialConfigType, + LexicalComposer, +} from '@lexical/react/src/LexicalComposer'; import { createLexicalComposerContext, LexicalComposerContext, @@ -434,7 +437,7 @@ export function $createTestDecoratorNode(): TestDecoratorNode { return new TestDecoratorNode(); } -const DEFAULT_NODES = [ +const DEFAULT_NODES: NonNullable = [ HeadingNode, ListNode, ListItemNode, @@ -463,6 +466,9 @@ export function TestComposer({ theme: {}, }, children, +}: { + config?: Omit; + children: React.ComponentProps['children']; }) { const customNodes = config.nodes; return ( diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index f7c617965158..a350a6711ef7 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -18,6 +18,7 @@ export type { EditorThemeClasses, HTMLConfig, Klass, + KlassConstructor, LexicalCommand, LexicalEditor, LexicalNodeReplacement, diff --git a/packages/lexical/src/nodes/LexicalDecoratorNode.ts b/packages/lexical/src/nodes/LexicalDecoratorNode.ts index a03e91441556..1a13795ee752 100644 --- a/packages/lexical/src/nodes/LexicalDecoratorNode.ts +++ b/packages/lexical/src/nodes/LexicalDecoratorNode.ts @@ -6,7 +6,7 @@ * */ -import type {LexicalEditor} from '../LexicalEditor'; +import type {KlassConstructor, LexicalEditor} from '../LexicalEditor'; import type {NodeKey} from '../LexicalNode'; import {EditorConfig} from 'lexical'; @@ -16,6 +16,7 @@ import {LexicalNode} from '../LexicalNode'; /** @noInheritDoc */ export class DecoratorNode extends LexicalNode { + ['constructor']!: KlassConstructor>; constructor(key?: NodeKey) { super(key); } diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index 9e2a6e5d22d3..778a3d741339 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -13,7 +13,7 @@ import type { PointType, RangeSelection, } from '../LexicalSelection'; -import type {Spread} from 'lexical'; +import type {KlassConstructor, Spread} from 'lexical'; import invariant from 'shared/invariant'; @@ -60,6 +60,7 @@ export type ElementFormatType = /** @noInheritDoc */ export class ElementNode extends LexicalNode { + ['constructor']!: KlassConstructor; /** @internal */ __first: null | NodeKey; /** @internal */ diff --git a/packages/lexical/src/nodes/LexicalLineBreakNode.ts b/packages/lexical/src/nodes/LexicalLineBreakNode.ts index 497eff7684dd..caa0aa8c29d0 100644 --- a/packages/lexical/src/nodes/LexicalLineBreakNode.ts +++ b/packages/lexical/src/nodes/LexicalLineBreakNode.ts @@ -6,6 +6,7 @@ * */ +import type {KlassConstructor} from '../LexicalEditor'; import type { DOMConversionMap, DOMConversionOutput, @@ -21,6 +22,7 @@ export type SerializedLineBreakNode = SerializedLexicalNode; /** @noInheritDoc */ export class LineBreakNode extends LexicalNode { + ['constructor']!: KlassConstructor; static getType(): string { return 'linebreak'; } diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index de9585477aaa..5e38d9f3be75 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -8,6 +8,7 @@ import type { EditorConfig, + KlassConstructor, LexicalEditor, Spread, TextNodeThemeClasses, @@ -278,6 +279,7 @@ function wrapElementWith( /** @noInheritDoc */ export class TextNode extends LexicalNode { + ['constructor']!: KlassConstructor; __text: string; /** @internal */ __format: number; diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalGC.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalGC.test.tsx index 30ead6df4e24..ba6dbcf693bf 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalGC.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalGC.test.tsx @@ -11,12 +11,14 @@ import { $createTextNode, $getNodeByKey, $getRoot, + $isElementNode, } from 'lexical'; import { $createTestElementNode, generatePermutations, initializeUnitTest, + invariant, } from '../../../__tests__/utils'; describe('LexicalGC tests', () => { @@ -66,7 +68,9 @@ describe('LexicalGC tests', () => { expect(editor.getEditorState()._nodeMap.size).toBe(5); await editor.update(() => { const root = $getRoot(); - const subchild = root.getFirstChild().getChildAtIndex(i); + const firstChild = root.getFirstChild(); + invariant($isElementNode(firstChild)); + const subchild = firstChild.getChildAtIndex(i); expect(subchild.getTextContent()).toBe(['foo', 'bar', 'zzz'][i]); subchild.remove(); root.clear(); diff --git a/packages/lexical/src/nodes/__tests__/unit/LexicalTabNode.test.tsx b/packages/lexical/src/nodes/__tests__/unit/LexicalTabNode.test.tsx index 9da569a4ba6b..4812c7953c49 100644 --- a/packages/lexical/src/nodes/__tests__/unit/LexicalTabNode.test.tsx +++ b/packages/lexical/src/nodes/__tests__/unit/LexicalTabNode.test.tsx @@ -21,7 +21,9 @@ import { $getRoot, $getSelection, $insertNodes, + $isElementNode, $isRangeSelection, + $isTextNode, $setSelection, KEY_TAB_COMMAND, } from 'lexical'; @@ -135,6 +137,7 @@ describe('LexicalTabNode tests', () => { await editor.update(() => { const root = $getRoot(); const paragraph = root.getFirstChild(); + invariant($isElementNode(paragraph)); const heading = $createHeadingNode('h1'); const list = $createListNode('number'); const listItem = $createListItemNode(); @@ -183,6 +186,7 @@ describe('LexicalTabNode tests', () => { await editor.update(() => { $getSelection().insertText('foo'); const textNode = $getRoot().getLastDescendant(); + invariant($isTextNode(textNode)); textNode.select(1, 1); }); await editor.dispatchCommand( @@ -201,6 +205,7 @@ describe('LexicalTabNode tests', () => { await editor.update(() => { $getSelection().insertText('foo'); const textNode = $getRoot().getLastDescendant(); + invariant($isTextNode(textNode)); textNode.select(1, 2); }); await editor.dispatchCommand(