mirror of
https://github.com/facebook/lexical.git
synced 2025-08-06 16:39:33 +08:00
[Breaking Changes][lexical][lexical-list][lexical-playground] Bug Fix: deleteCharacter through ListNode->ListItemNode (#7248)
This commit is contained in:
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 {
|
||||
deleteBackward,
|
||||
moveToLineBeginning,
|
||||
} from '../keyboardShortcuts/index.mjs';
|
||||
import {
|
||||
assertHTML,
|
||||
focusEditor,
|
||||
html,
|
||||
initialize,
|
||||
test,
|
||||
} from '../utils/index.mjs';
|
||||
|
||||
test.describe('Regression tests for #7246', () => {
|
||||
test.beforeEach(({isPlainText, isCollab, page}) =>
|
||||
initialize({isCollab, isPlainText, page}),
|
||||
);
|
||||
|
||||
test(`deleteCharacter merges children from block adjacent to ListNode`, async ({
|
||||
page,
|
||||
isCollab,
|
||||
isPlainText,
|
||||
}) => {
|
||||
test.skip(isCollab || isPlainText);
|
||||
await focusEditor(page);
|
||||
await page.keyboard.type('* list');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type('paragraph');
|
||||
const beforeHtml = html`
|
||||
<ul>
|
||||
<li dir="ltr" value="1"><span data-lexical-text="true">list</span></li>
|
||||
</ul>
|
||||
<p dir="ltr"><span data-lexical-text="true">paragraph</span></p>
|
||||
`;
|
||||
await assertHTML(page, beforeHtml, beforeHtml, {
|
||||
ignoreClasses: true,
|
||||
ignoreInlineStyles: true,
|
||||
});
|
||||
await moveToLineBeginning(page);
|
||||
await deleteBackward(page);
|
||||
const afterHtml = html`
|
||||
<ul>
|
||||
<li dir="ltr" value="1">
|
||||
<span data-lexical-text="true">listparagraph</span>
|
||||
</li>
|
||||
</ul>
|
||||
`;
|
||||
await assertHTML(page, afterHtml, afterHtml, {
|
||||
ignoreClasses: true,
|
||||
ignoreInlineStyles: true,
|
||||
});
|
||||
});
|
||||
});
|
@ -7,6 +7,9 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
$getSiblingCaret,
|
||||
$isElementNode,
|
||||
$rewindSiblingCaret,
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput,
|
||||
DOMExportOutput,
|
||||
@ -16,6 +19,7 @@ import {
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
RangeSelection,
|
||||
SerializedElementNode,
|
||||
Spread,
|
||||
} from 'lexical';
|
||||
@ -57,6 +61,30 @@ export class CollapsibleContainerNode extends ElementNode {
|
||||
return new CollapsibleContainerNode(node.__open, node.__key);
|
||||
}
|
||||
|
||||
isShadowRoot(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
collapseAtStart(selection: RangeSelection): boolean {
|
||||
// Unwrap the CollapsibleContainerNode by replacing it with the children
|
||||
// of its children (CollapsibleTitleNode, CollapsibleContentNode)
|
||||
const nodesToInsert: LexicalNode[] = [];
|
||||
for (const child of this.getChildren()) {
|
||||
if ($isElementNode(child)) {
|
||||
nodesToInsert.push(...child.getChildren());
|
||||
}
|
||||
}
|
||||
const caret = $rewindSiblingCaret($getSiblingCaret(this, 'previous'));
|
||||
caret.splice(1, nodesToInsert);
|
||||
// Merge the first child of the CollapsibleTitleNode with the
|
||||
// previous sibling of the CollapsibleContainerNode
|
||||
const [firstChild] = nodesToInsert;
|
||||
if (firstChild) {
|
||||
firstChild.selectStart().deleteCharacter(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement {
|
||||
// details is not well supported in Chrome #5582
|
||||
let dom: HTMLElement;
|
||||
|
@ -83,11 +83,6 @@ export class CollapsibleTitleNode extends ElementNode {
|
||||
return $createCollapsibleTitleNode().updateFromJSON(serializedNode);
|
||||
}
|
||||
|
||||
collapseAtStart(_selection: RangeSelection): boolean {
|
||||
this.getParentOrThrow().insertBefore(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
static transform(): (node: LexicalNode) => void {
|
||||
return (node: LexicalNode) => {
|
||||
invariant(
|
||||
|
@ -41,6 +41,7 @@ import {
|
||||
$setSelection,
|
||||
$updateRangeSelectionFromCaretRange,
|
||||
CaretRange,
|
||||
ChildCaret,
|
||||
NodeCaret,
|
||||
PointCaret,
|
||||
TextNode,
|
||||
@ -67,6 +68,7 @@ import {
|
||||
$getNodeFromDOM,
|
||||
$getRoot,
|
||||
$hasAncestor,
|
||||
$isRootOrShadowRoot,
|
||||
$isTokenOrSegmented,
|
||||
$setCompositionKey,
|
||||
doesContainSurrogatePair,
|
||||
@ -1721,18 +1723,27 @@ export class RangeSelection implements BaseSelection {
|
||||
.getTextSlices()
|
||||
.every((slice) => slice === null || slice.distance === 0)
|
||||
) {
|
||||
// debugger;
|
||||
// There's no text in the direction of the deletion so we can explore our options
|
||||
let state:
|
||||
| {type: 'initial'}
|
||||
| {
|
||||
type: 'merge-next-block';
|
||||
block: ElementNode;
|
||||
}
|
||||
| {
|
||||
type: 'merge-block';
|
||||
caret: ChildCaret<ElementNode, typeof direction>;
|
||||
block: ElementNode;
|
||||
} = {type: 'initial'};
|
||||
for (const caret of initialRange.iterNodeCarets('shadowRoot')) {
|
||||
if ($isChildCaret(caret)) {
|
||||
if (caret.origin.isInline()) {
|
||||
// fall through when descending an inline
|
||||
} else if (caret.origin.isShadowRoot()) {
|
||||
if (state.type === 'merge-block') {
|
||||
break;
|
||||
}
|
||||
// Don't merge with a shadow root block
|
||||
if (
|
||||
$isElementNode(initialRange.anchor.origin) &&
|
||||
@ -1747,20 +1758,15 @@ export class RangeSelection implements BaseSelection {
|
||||
initialRange.anchor.origin.remove();
|
||||
}
|
||||
return;
|
||||
} else if (state.type === 'merge-next-block') {
|
||||
$updateRangeSelectionFromCaretRange(
|
||||
this,
|
||||
$getCaretRange(
|
||||
!caret.origin.isEmpty() && state.block.isEmpty()
|
||||
? $rewindSiblingCaret(
|
||||
$getSiblingCaret(state.block, caret.direction),
|
||||
)
|
||||
: initialRange.anchor,
|
||||
caret,
|
||||
),
|
||||
);
|
||||
return this.removeText();
|
||||
} else if (
|
||||
state.type === 'merge-next-block' ||
|
||||
state.type === 'merge-block'
|
||||
) {
|
||||
// Keep descending ChildCaret to find which block to merge with
|
||||
state = {block: state.block, caret, type: 'merge-block'};
|
||||
}
|
||||
} else if (state.type === 'merge-block') {
|
||||
break;
|
||||
} else if ($isSiblingCaret(caret)) {
|
||||
if ($isElementNode(caret.origin)) {
|
||||
if (!caret.origin.isInline()) {
|
||||
@ -1797,6 +1803,19 @@ export class RangeSelection implements BaseSelection {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (state.type === 'merge-block') {
|
||||
const {caret, block} = state;
|
||||
$updateRangeSelectionFromCaretRange(
|
||||
this,
|
||||
$getCaretRange(
|
||||
!caret.origin.isEmpty() && block.isEmpty()
|
||||
? $rewindSiblingCaret($getSiblingCaret(block, caret.direction))
|
||||
: initialRange.anchor,
|
||||
caret,
|
||||
),
|
||||
);
|
||||
return this.removeText();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the deletion around decorators.
|
||||
@ -1875,19 +1894,20 @@ export class RangeSelection implements BaseSelection {
|
||||
|
||||
this.modify('extend', isBackward, 'lineboundary');
|
||||
|
||||
// If the selection starts at the beginning of a text node (offset 0),
|
||||
// extend the selection by one character in the specified direction.
|
||||
// This ensures that the parent element is deleted along with its content.
|
||||
// Otherwise, only the text content will be deleted, leaving an empty parent node.
|
||||
if (this.isCollapsed() && this.anchor.offset === 0) {
|
||||
this.modify('extend', isBackward, 'character');
|
||||
}
|
||||
|
||||
const useDeleteCharacter = this.isCollapsed() && this.anchor.offset === 0;
|
||||
// Adjusts selection to include an extra character added for element anchors to remove it
|
||||
if (anchorIsElement) {
|
||||
const startPoint = isBackward ? this.anchor : this.focus;
|
||||
startPoint.set(startPoint.key, startPoint.offset + 1, startPoint.type);
|
||||
}
|
||||
// If the selection starts at the beginning of a text node (offset 0),
|
||||
// use the deleteCharacter operation to handle all of the logic associated
|
||||
// with navigating through the parent element
|
||||
if (useDeleteCharacter) {
|
||||
// Remove the inserted space, if added above
|
||||
this.removeText();
|
||||
return this.deleteCharacter(isBackward);
|
||||
}
|
||||
}
|
||||
this.removeText();
|
||||
}
|
||||
@ -1972,7 +1992,7 @@ function $collapseAtStart(
|
||||
if (node.collapseAtStart(selection)) {
|
||||
return true;
|
||||
}
|
||||
if (!node.isInline()) {
|
||||
if ($isRootOrShadowRoot(node)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user