Skip to content

Commit

Permalink
[lexical][lexical-overflow] Refactor: simplified removeText and inser…
Browse files Browse the repository at this point in the history
…tText rewrite (part 1) (#6456)

Co-authored-by: EgonBolton <43938777+EgonBolton@users.noreply.github.com>
Co-authored-by: Bob Ippolito <bob@redivi.com>
  • Loading branch information
3 people committed Sep 4, 2024
1 parent 4573fb0 commit bd507b9
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 76 deletions.
10 changes: 10 additions & 0 deletions packages/lexical-overflow/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
} from 'lexical';

import {$applyNodeReplacement, ElementNode} from 'lexical';
import invariant from 'shared/invariant';

export type SerializedOverflowNode = SerializedElementNode;

Expand Down Expand Up @@ -72,6 +73,15 @@ export class OverflowNode extends ElementNode {
excludeFromCopy(): boolean {
return true;
}

static transform(): (node: LexicalNode) => void {
return (node: LexicalNode) => {
invariant($isOverflowNode(node), 'node is not a OverflowNode');
if (node.isEmpty()) {
node.remove();
}
};
}
}

export function $createOverflowNode(): OverflowNode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ export class CollapsibleTitleNode extends ElementNode {
return true;
}

static transform(): (node: LexicalNode) => void {
return (node: LexicalNode) => {
invariant(
$isCollapsibleTitleNode(node),
'node is not a CollapsibleTitleNode',
);
if (node.isEmpty()) {
node.remove();
}
};
}

insertNewAfter(_: RangeSelection, restoreSelection = true): ElementNode {
const containerNode = this.getParentOrThrow();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,87 +31,35 @@ describe('LexicalNodeHelpers tests', () => {
initializeUnitTest(
(testEnv) => {
describe('merge', () => {
async function initializeEditorWithLeftRightOverflowNodes(): Promise<
[NodeKey, NodeKey]
> {
const editor: LexicalEditor = testEnv.editor;
let overflowLeftKey;
let overflowRightKey;

await editor.update(() => {
const root = $getRoot();
function $initializeEditorWithLeftRightOverflowNodes(): [
NodeKey,
NodeKey,
] {
const root = $getRoot();

const paragraph = $createParagraphNode();
const overflowLeft = $createOverflowNode();
const overflowRight = $createOverflowNode();
const paragraph = $createParagraphNode();
const overflowLeft = $createOverflowNode();
const overflowRight = $createOverflowNode();

overflowLeftKey = overflowLeft.getKey();
overflowRightKey = overflowRight.getKey();
root.append(paragraph);

root.append(paragraph);
paragraph.append(overflowLeft);
paragraph.append(overflowRight);

paragraph.append(overflowLeft);
paragraph.append(overflowRight);
});

return [overflowLeftKey!, overflowRightKey!];
return [overflowLeft.getKey(), overflowRight.getKey()];
}

it('merges an empty overflow node (left overflow selected)', async () => {
const editor: LexicalEditor = testEnv.editor;
const [overflowLeftKey, overflowRightKey] =
await initializeEditorWithLeftRightOverflowNodes();

await editor.update(() => {
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;

const text1 = $createTextNode('1');
const text2 = $createTextNode('2');

overflowRight.append(text1, text2);

text2.toggleFormat('bold'); // Prevent merging with text1

overflowLeft.select();
});

await editor.update(() => {
const paragraph = $getRoot().getFirstChild<ParagraphNode>()!;
const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;

$mergePrevious(overflowRight);

expect(paragraph.getChildrenSize()).toBe(1);
expect($isOverflowNode(paragraph.getFirstChild())).toBe(true);

const selection = $getSelection();

if (!$isRangeSelection(selection)) {
throw new Error('Lost selection');
}

if ($isNodeSelection(selection)) {
return;
}

expect(selection.anchor.key).toBe(overflowRightKey);
expect(selection.anchor.offset).toBe(0);
expect(selection.focus.key).toBe(overflowRightKey);
expect(selection.anchor.offset).toBe(0);
});
});

it('merges an overflow node (left overflow selected)', async () => {
const editor: LexicalEditor = testEnv.editor;
const [overflowLeftKey, overflowRightKey] =
await initializeEditorWithLeftRightOverflowNodes();
let overflowLeftKey: NodeKey;
let overflowRightKey: NodeKey;

let text1Key: NodeKey;

await editor.update(() => {
[overflowLeftKey, overflowRightKey] =
$initializeEditorWithLeftRightOverflowNodes();

const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
Expand Down Expand Up @@ -165,13 +113,15 @@ describe('LexicalNodeHelpers tests', () => {

it('merges an overflow node (left-right overflow selected)', async () => {
const editor: LexicalEditor = testEnv.editor;
const [overflowLeftKey, overflowRightKey] =
await initializeEditorWithLeftRightOverflowNodes();
let overflowLeftKey: NodeKey;
let overflowRightKey: NodeKey;

let text2Key: NodeKey;
let text3Key: NodeKey;

await editor.update(() => {
[overflowLeftKey, overflowRightKey] =
$initializeEditorWithLeftRightOverflowNodes();
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
Expand Down
26 changes: 26 additions & 0 deletions packages/lexical-react/src/shared/useCharacterLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ import {$rootTextContent} from '@lexical/text';
import {$dfs, mergeRegister} from '@lexical/utils';
import {
$getSelection,
$isElementNode,
$isLeafNode,
$isRangeSelection,
$isTextNode,
$setSelection,
COMMAND_PRIORITY_LOW,
DELETE_CHARACTER_COMMAND,
} from 'lexical';
import {useEffect} from 'react';
import invariant from 'shared/invariant';
Expand Down Expand Up @@ -92,6 +95,29 @@ export function useCharacterLimit(

lastComputedTextLength = textLength;
}),
editor.registerCommand(
DELETE_CHARACTER_COMMAND,
(isBackward) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
const anchorNode = selection.anchor.getNode();
const overflow = anchorNode.getParent();
const overflowParent = overflow ? overflow.getParent() : null;
const parentNext = overflowParent
? overflowParent.getNextSibling()
: null;
selection.deleteCharacter(isBackward);
if (overflowParent && overflowParent.isEmpty()) {
overflowParent.remove();
} else if ($isElementNode(parentNext) && parentNext.isEmpty()) {
parentNext.remove();
}
return true;
},
COMMAND_PRIORITY_LOW,
),
);
}, [editor, maxCharacters, remainingCharacters, strlen]);
}
Expand Down
110 changes: 107 additions & 3 deletions packages/lexical/src/LexicalSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,12 +709,56 @@ export class RangeSelection implements BaseSelection {
}

/**
* Attempts to insert the provided text into the EditorState at the current Selection as a new
* Lexical TextNode, according to a series of insertion heuristics based on the selection type and position.
* Insert the provided text into the EditorState at the current Selection.
*
* @param text the text to insert into the Selection
*/
insertText(text: string): void {
// Now that "removeText" has been improved and does not depend on
// insertText, insertText can be greatly simplified. The next
// commented version is a WIP (about 5 tests fail).
//
// this.removeText();
// if (text === '') {
// return;
// }
// const anchorNode = this.anchor.getNode();
// const textNode = $createTextNode(text);
// textNode.setFormat(this.format);
// textNode.setStyle(this.style);
// if ($isTextNode(anchorNode)) {
// const parent = anchorNode.getParentOrThrow();
// if (this.anchor.offset === 0) {
// if (parent.isInline() && !anchorNode.__prev) {
// parent.insertBefore(textNode);
// } else {
// anchorNode.insertBefore(textNode);
// }
// } else if (this.anchor.offset === anchorNode.getTextContentSize()) {
// if (parent.isInline() && !anchorNode.__next) {
// parent.insertAfter(textNode);
// } else {
// anchorNode.insertAfter(textNode);
// }
// } else {
// const [before] = anchorNode.splitText(this.anchor.offset);
// before.insertAfter(textNode);
// }
// } else {
// anchorNode.splice(this.anchor.offset, 0, [textNode]);
// }
// const nodeToSelect = textNode.isAttached() ? textNode : anchorNode;
// nodeToSelect.selectEnd();
// // When composing, we need to adjust the anchor offset so that
// // we correctly replace that right range.
// if (
// textNode.isComposing() &&
// this.anchor.type === 'text' &&
// anchorNode.getTextContent() !== ''
// ) {
// this.anchor.offset -= text.length;
// }

const anchor = this.anchor;
const focus = this.focus;
const format = this.format;
Expand Down Expand Up @@ -1061,7 +1105,67 @@ export class RangeSelection implements BaseSelection {
* Removes the text in the Selection, adjusting the EditorState accordingly.
*/
removeText(): void {
this.insertText('');
if (this.isCollapsed()) {
return;
}
const {anchor, focus} = this;
const selectedNodes = this.getNodes();
const firstPoint = this.isBackward() ? focus : anchor;
const lastPoint = this.isBackward() ? anchor : focus;
let firstNode = firstPoint.getNode();
let lastNode = lastPoint.getNode();
const firstBlock = $getAncestor(firstNode, INTERNAL_$isBlock);
const lastBlock = $getAncestor(lastNode, INTERNAL_$isBlock);

selectedNodes.forEach((node) => {
if (
!$hasAncestor(firstNode, node) &&
!$hasAncestor(lastNode, node) &&
node.getKey() !== firstNode.getKey() &&
node.getKey() !== lastNode.getKey()
) {
node.remove();
}
});

const fixText = (node: TextNode, del: number) => {
if (node.getTextContent() === '') {
node.remove();
} else if (del !== 0 && $isTokenOrSegmented(node)) {
const textNode = $createTextNode(node.getTextContent());
textNode.setFormat(node.getFormat());
textNode.setStyle(node.getStyle());
return node.replace(textNode);
}
};
if (firstNode === lastNode && $isTextNode(firstNode)) {
const del = Math.abs(focus.offset - anchor.offset);
firstNode.spliceText(firstPoint.offset, del, '', true);
fixText(firstNode, del);
return;
}
if ($isTextNode(firstNode)) {
const del = firstNode.getTextContentSize() - firstPoint.offset;
firstNode.spliceText(firstPoint.offset, del, '');
firstNode = fixText(firstNode, del) || firstNode;
}
if ($isTextNode(lastNode)) {
lastNode.spliceText(0, lastPoint.offset, '');
lastNode = fixText(lastNode, lastPoint.offset) || lastNode;
}
if (firstNode.isAttached() && $isTextNode(firstNode)) {
firstNode.selectEnd();
} else if (lastNode.isAttached() && $isTextNode(lastNode)) {
lastNode.selectStart();
}

// Merge blocks
const bothElem = $isElementNode(firstBlock) && $isElementNode(lastBlock);
if (bothElem && firstBlock !== lastBlock) {
firstBlock.append(...lastBlock.getChildren());
lastBlock.remove();
lastPoint.set(firstPoint.key, firstPoint.offset, firstPoint.type);
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/lexical/src/LexicalUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1678,7 +1678,7 @@ export function isBlockDomNode(node: Node) {
export function INTERNAL_$isBlock(
node: LexicalNode,
): node is ElementNode | DecoratorNode<unknown> {
if ($isRootNode(node) || ($isDecoratorNode(node) && !node.isInline())) {
if ($isDecoratorNode(node) && !node.isInline()) {
return true;
}
if (!$isElementNode(node) || $isRootOrShadowRoot(node)) {
Expand Down
Loading

0 comments on commit bd507b9

Please sign in to comment.