Skip to content

Commit

Permalink
[lexical] Bug Fix: Normalize selection after applyDOMRange to account…
Browse files Browse the repository at this point in the history
… for Firefox differences (facebook#7050)
  • Loading branch information
etrepum authored Jan 14, 2025
1 parent bbc07af commit 65ce66a
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 91 deletions.
62 changes: 62 additions & 0 deletions packages/lexical-playground/__tests__/e2e/Links.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,68 @@ test.describe.parallel('Links', () => {
},
);

test(
`Can backspace across a link and it deletes text, not the whole link`,
{
tag: '@flaky',
},
async ({page}) => {
await focusEditor(page);
await page.keyboard.type(' abc def ');
await moveLeft(page, 5);
await selectCharacters(page, 'left', 3);

// link
await click(page, '.link');
await click(page, '.link-confirm');

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true"></span>
<a
class="PlaygroundEditorTheme__link PlaygroundEditorTheme__ltr"
dir="ltr"
href="https://"
rel="noreferrer">
<span data-lexical-text="true">abc</span>
</a>
<span data-lexical-text="true">def</span>
</p>
`,
);

await moveRight(page, 4);

await page.keyboard.press('Backspace');
await page.keyboard.press('Backspace');
await page.keyboard.press('Backspace');
await page.keyboard.press('Backspace');

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<span data-lexical-text="true"></span>
<a
class="PlaygroundEditorTheme__link PlaygroundEditorTheme__ltr"
dir="ltr"
href="https://"
rel="noreferrer">
<span data-lexical-text="true">ab</span>
</a>
<span data-lexical-text="true">f</span>
</p>
`,
);
},
);

test(`Can create a link then replace it with plain text`, async ({page}) => {
await focusEditor(page);
await page.keyboard.type(' abc ');
Expand Down
10 changes: 2 additions & 8 deletions packages/lexical-selection/src/__tests__/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,10 +890,7 @@ export function $setAnchorPoint(
return;
}

const anchor = selection.anchor;
anchor.type = point.type;
anchor.offset = point.offset;
anchor.key = point.key;
selection.anchor.set(point.key, point.offset, point.type);
}

export function $setFocusPoint(
Expand All @@ -911,8 +908,5 @@ export function $setFocusPoint(
return;
}

const focus = selection.focus;
focus.type = point.type;
focus.offset = point.offset;
focus.key = point.key;
selection.focus.set(point.key, point.offset, point.type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,16 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
// to use whatever was passed (e.g. keep selection on root node)
const selection = $createRangeSelection();
const type = $isElementNode(selectionNode) ? 'element' : 'text';
selection.anchor.key = selection.focus.key = selectionNode.getKey();
selection.anchor.offset = selection.focus.offset =
testCase.selectionOffset;
selection.anchor.type = selection.focus.type = type;
selection.anchor.set(
selectionNode.getKey(),
testCase.selectionOffset,
type,
);
selection.focus.set(
selectionNode.getKey(),
testCase.selectionOffset,
type,
);
$setSelection(selection);

$insertNodeToNearestRoot($createTestDecoratorNode());
Expand Down
2 changes: 1 addition & 1 deletion packages/lexical/flow/Lexical.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ declare class _Point {
is(point: PointType): boolean;
isBefore(b: PointType): boolean;
getNode(): LexicalNode;
set(key: NodeKey, offset: number, type: 'text' | 'element'): void;
set(key: NodeKey, offset: number, type: 'text' | 'element', onlyIfChanged?: boolean): void;
}

declare export function $createRangeSelection(): RangeSelection;
Expand Down
1 change: 0 additions & 1 deletion packages/lexical/src/LexicalMutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,6 @@ function flushMutations(

if (selection !== null) {
if (shouldRevertSelection) {
selection.dirty = true;
$setSelection(selection);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/lexical/src/LexicalNormalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ function $normalizePoint(point: PointType): void {
nextNode.__key,
nextOffsetAtEnd ? nextNode.getTextContentSize() : 0,
'text',
true,
);
break;
} else if (!$isElementNode(nextNode)) {
Expand All @@ -119,6 +120,7 @@ function $normalizePoint(point: PointType): void {
nextNode.__key,
nextOffsetAtEnd ? nextNode.getChildrenSize() : 0,
'element',
true,
);
}
}
122 changes: 59 additions & 63 deletions packages/lexical/src/LexicalSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from './LexicalEvents';
import {getIsProcessingMutations} from './LexicalMutations';
import {insertRangeAfter, LexicalNode} from './LexicalNode';
import {$normalizeSelection} from './LexicalNormalization';
import {
getActiveEditor,
getActiveEditorState,
Expand Down Expand Up @@ -73,7 +74,12 @@ export type TextPointType = {
isBefore: (point: PointType) => boolean;
key: NodeKey;
offset: number;
set: (key: NodeKey, offset: number, type: 'text' | 'element') => void;
set: (
key: NodeKey,
offset: number,
type: 'text' | 'element',
onlyIfChanged?: boolean,
) => void;
type: 'text';
};

Expand All @@ -84,7 +90,12 @@ export type ElementPointType = {
isBefore: (point: PointType) => boolean;
key: NodeKey;
offset: number;
set: (key: NodeKey, offset: number, type: 'text' | 'element') => void;
set: (
key: NodeKey,
offset: number,
type: 'text' | 'element',
onlyIfChanged?: boolean,
) => void;
type: 'element';
};

Expand Down Expand Up @@ -148,9 +159,22 @@ export class Point {
return node;
}

set(key: NodeKey, offset: number, type: 'text' | 'element'): void {
set(
key: NodeKey,
offset: number,
type: 'text' | 'element',
onlyIfChanged?: boolean,
): void {
const selection = this._selection;
const oldKey = this.key;
if (
onlyIfChanged &&
this.key === key &&
this.offset === offset &&
this.type === type
) {
return;
}
this.key = key;
this.offset = offset;
this.type = type;
Expand Down Expand Up @@ -244,17 +268,6 @@ function $transferStartingElementPointToTextPoint(
start.set(textNode.__key, 0, 'text');
}

function $setPointValues(
point: PointType,
key: NodeKey,
offset: number,
type: 'text' | 'element',
): void {
point.key = key;
point.offset = offset;
point.type = type;
}

export interface BaseSelection {
_cachedNodes: Array<LexicalNode> | null;
dirty: boolean;
Expand Down Expand Up @@ -547,10 +560,8 @@ export class RangeSelection implements BaseSelection {
focusNode: TextNode,
focusOffset: number,
): void {
$setPointValues(this.anchor, anchorNode.__key, anchorOffset, 'text');
$setPointValues(this.focus, focusNode.__key, focusOffset, 'text');
this._cachedNodes = null;
this.dirty = true;
this.anchor.set(anchorNode.__key, anchorOffset, 'text');
this.focus.set(focusNode.__key, focusOffset, 'text');
}

/**
Expand Down Expand Up @@ -642,19 +653,16 @@ export class RangeSelection implements BaseSelection {
return;
}
const [anchorPoint, focusPoint] = resolvedSelectionPoints;
$setPointValues(
this.anchor,
this.anchor.set(
anchorPoint.key,
anchorPoint.offset,
anchorPoint.type,
true,
);
$setPointValues(
this.focus,
focusPoint.key,
focusPoint.offset,
focusPoint.type,
);
this._cachedNodes = null;
this.focus.set(focusPoint.key, focusPoint.offset, focusPoint.type, true);
// Firefox will use an element point rather than a text point in some cases,
// so we normalize for that
$normalizeSelection(this);
}

/**
Expand Down Expand Up @@ -1143,10 +1151,10 @@ export class RangeSelection implements BaseSelection {
firstNode.isToken() &&
firstPoint.offset < firstNode.getTextContentSize()
) {
firstPoint.offset = 0;
firstPoint.set(firstNode.getKey(), 0, 'text');
}
if (lastPoint.offset > 0 && $isTextNode(lastNode) && lastNode.isToken()) {
lastPoint.offset = lastNode.getTextContentSize();
lastPoint.set(lastNode.getKey(), lastNode.getTextContentSize(), 'text');
}

for (const node of selectedNodes) {
Expand Down Expand Up @@ -1968,9 +1976,8 @@ function $swapPoints(selection: RangeSelection): void {
const anchorOffset = anchor.offset;
const anchorType = anchor.type;

$setPointValues(anchor, focus.key, focus.offset, focus.type);
$setPointValues(focus, anchorKey, anchorOffset, anchorType);
selection._cachedNodes = null;
anchor.set(focus.key, focus.offset, focus.type, true);
focus.set(anchorKey, anchorOffset, anchorType, true);
}

function moveNativeSelection(
Expand Down Expand Up @@ -2010,9 +2017,9 @@ function $updateCaretSelectionForUnicodeCharacter(
const text = anchorNode.getTextContent().slice(startOffset, endOffset);
if (!doesContainGrapheme(text)) {
if (isBackward) {
focus.offset = characterOffset;
focus.set(focus.key, characterOffset, focus.type);
} else {
anchor.offset = characterOffset;
anchor.set(anchor.key, characterOffset, anchor.type);
}
}
}
Expand Down Expand Up @@ -2233,13 +2240,13 @@ function resolveSelectionPointOnBoundary(
!isCollapsed &&
prevSibling.isInline()
) {
point.key = prevSibling.__key;
point.offset = prevSibling.getChildrenSize();
// @ts-expect-error: intentional
point.type = 'element';
point.set(prevSibling.__key, prevSibling.getChildrenSize(), 'element');
} else if ($isTextNode(prevSibling)) {
point.key = prevSibling.__key;
point.offset = prevSibling.getTextContent().length;
point.set(
prevSibling.__key,
prevSibling.getTextContent().length,
'text',
);
}
} else if (
(isCollapsed || !isBackward) &&
Expand All @@ -2249,19 +2256,19 @@ function resolveSelectionPointOnBoundary(
) {
const parentSibling = parent.getPreviousSibling();
if ($isTextNode(parentSibling)) {
point.key = parentSibling.__key;
point.offset = parentSibling.getTextContent().length;
point.set(
parentSibling.__key,
parentSibling.getTextContent().length,
'text',
);
}
}
} else if (offset === node.getTextContent().length) {
const nextSibling = node.getNextSibling();
const parent = node.getParent();

if (isBackward && $isElementNode(nextSibling) && nextSibling.isInline()) {
point.key = nextSibling.__key;
point.offset = 0;
// @ts-expect-error: intentional
point.type = 'element';
point.set(nextSibling.__key, 0, 'element');
} else if (
(isCollapsed || isBackward) &&
nextSibling === null &&
Expand All @@ -2271,8 +2278,7 @@ function resolveSelectionPointOnBoundary(
) {
const parentSibling = parent.getNextSibling();
if ($isTextNode(parentSibling)) {
point.key = parentSibling.__key;
point.offset = 0;
point.set(parentSibling.__key, 0, 'text');
}
}
}
Expand All @@ -2293,9 +2299,7 @@ function $normalizeSelectionPointsForBoundaries(
resolveSelectionPointOnBoundary(focus, !isBackward, isCollapsed);

if (isCollapsed) {
focus.key = anchor.key;
focus.offset = anchor.offset;
focus.type = anchor.type;
focus.set(anchor.key, anchor.offset, anchor.type);
}
const editor = getActiveEditor();

Expand All @@ -2306,13 +2310,8 @@ function $normalizeSelectionPointsForBoundaries(
) {
const lastAnchor = lastSelection.anchor;
const lastFocus = lastSelection.focus;
$setPointValues(
anchor,
lastAnchor.key,
lastAnchor.offset,
lastAnchor.type,
);
$setPointValues(focus, lastFocus.key, lastFocus.offset, lastFocus.type);
anchor.set(lastAnchor.key, lastAnchor.offset, lastAnchor.type, true);
focus.set(lastFocus.key, lastFocus.offset, lastFocus.type, true);
}
}
}
Expand Down Expand Up @@ -2774,12 +2773,9 @@ export function adjustPointOffsetForMergedSibling(
textLength: number,
): void {
if (point.type === 'text') {
point.key = key;
if (!isBefore) {
point.offset += textLength;
}
point.set(key, point.offset + (isBefore ? 0 : textLength), 'text');
} else if (point.offset > target.getIndexWithinParent()) {
point.offset -= 1;
point.set(point.key, point.offset - 1, 'element');
}
}

Expand Down
Loading

0 comments on commit 65ce66a

Please sign in to comment.