Skip to content

Commit

Permalink
[lexical][lexical-playground] Bug Fix: Allow setEditorState to work c…
Browse files Browse the repository at this point in the history
…orrectly inside of an update (#6876)
  • Loading branch information
etrepum authored Nov 30, 2024
1 parent 6cbfc01 commit 8e0e300
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 36 deletions.
128 changes: 128 additions & 0 deletions packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
toggleItalic,
toggleStrikethrough,
toggleUnderline,
undo,
} from '../keyboardShortcuts/index.mjs';
import {
assertHTML,
Expand Down Expand Up @@ -155,4 +156,131 @@ test.describe('Autocomplete', () => {
`,
);
});
test('Undo does not cause an exception', async ({
page,
isPlainText,
isCollab,
}) => {
test.skip(isPlainText);
// Autocomplete has known issues in collab https://github.com/facebook/lexical/issues/6844
test.skip(isCollab);
await focusEditor(page);
await toggleBold(page);
await toggleItalic(page);
await toggleUnderline(page);
await toggleStrikethrough(page);
await increaseFontSize(page);

await page.keyboard.type('Test');
await sleep(500);

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<strong
class="PlaygroundEditorTheme__textUnderlineStrikethrough PlaygroundEditorTheme__textBold PlaygroundEditorTheme__textItalic"
style="font-size: 17px;"
data-lexical-text="true">
Test
</strong>
<strong
class="PlaygroundEditorTheme__textUnderlineStrikethrough PlaygroundEditorTheme__textBold PlaygroundEditorTheme__textItalic PlaygroundEditorTheme__autocomplete"
style="font-size: 17px;"
data-lexical-text="true">
imonials (TAB)
</strong>
</p>
`,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<strong
class="PlaygroundEditorTheme__textUnderlineStrikethrough PlaygroundEditorTheme__textBold PlaygroundEditorTheme__textItalic"
style="font-size: 17px;"
data-lexical-text="true">
Test
</strong>
<span data-lexical-text="true"></span>
</p>
`,
);

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

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<strong
class="PlaygroundEditorTheme__textUnderlineStrikethrough PlaygroundEditorTheme__textBold PlaygroundEditorTheme__textItalic"
style="font-size: 17px;"
data-lexical-text="true">
Test
</strong>
<strong
class="PlaygroundEditorTheme__textUnderlineStrikethrough PlaygroundEditorTheme__textBold PlaygroundEditorTheme__textItalic"
style="font-size: 17px;"
data-lexical-text="true">
imonials
</strong>
</p>
`,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<strong
class="PlaygroundEditorTheme__textUnderlineStrikethrough PlaygroundEditorTheme__textBold PlaygroundEditorTheme__textItalic"
style="font-size: 17px;"
data-lexical-text="true">
Test
</strong>
<span data-lexical-text="true"></span>
</p>
`,
);

await undo(page);

await assertHTML(
page,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<strong
class="PlaygroundEditorTheme__textUnderlineStrikethrough PlaygroundEditorTheme__textBold PlaygroundEditorTheme__textItalic"
style="font-size: 17px;"
data-lexical-text="true">
Test
</strong>
<strong
class="PlaygroundEditorTheme__textUnderlineStrikethrough PlaygroundEditorTheme__textBold PlaygroundEditorTheme__textItalic PlaygroundEditorTheme__autocomplete"
style="font-size: 17px;"
data-lexical-text="true">
imonials (TAB)
</strong>
</p>
`,
html`
<p
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
<strong
class="PlaygroundEditorTheme__textUnderlineStrikethrough PlaygroundEditorTheme__textBold PlaygroundEditorTheme__textItalic"
style="font-size: 17px;"
data-lexical-text="true">
Test
</strong>
<span data-lexical-text="true"></span>
</p>
`,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$isAtNodeEnd} from '@lexical/selection';
import {mergeRegister} from '@lexical/utils';
import {
$addUpdateTag,
$createTextNode,
$getNodeByKey,
$getSelection,
Expand All @@ -31,6 +32,8 @@ import {
} from '../../nodes/AutocompleteNode';
import {addSwipeRightListener} from '../../utils/swipe';

const HISTORY_MERGE = {tag: 'history-merge'};

declare global {
interface Navigator {
userAgentData?: {
Expand Down Expand Up @@ -130,34 +133,27 @@ export default function AutocompletePlugin(): JSX.Element | null {
// Outdated or no suggestion
return;
}
editor.update(
() => {
const selection = $getSelection();
const [hasMatch, match] = $search(selection);
if (
!hasMatch ||
match !== lastMatch ||
!$isRangeSelection(selection)
) {
// Outdated
return;
}
const selectionCopy = selection.clone();
const prevNode = selection.getNodes()[0] as TextNode;
prevNodeFormat = prevNode.getFormat();
const node = $createAutocompleteNode(
formatSuggestionText(newSuggestion),
uuid,
)
.setFormat(prevNodeFormat)
.setStyle(`font-size: ${toolbarState.fontSize}`);
autocompleteNodeKey = node.getKey();
selection.insertNodes([node]);
$setSelection(selectionCopy);
lastSuggestion = newSuggestion;
},
{tag: 'history-merge'},
);
editor.update(() => {
const selection = $getSelection();
const [hasMatch, match] = $search(selection);
if (!hasMatch || match !== lastMatch || !$isRangeSelection(selection)) {
// Outdated
return;
}
const selectionCopy = selection.clone();
const prevNode = selection.getNodes()[0] as TextNode;
prevNodeFormat = prevNode.getFormat();
const node = $createAutocompleteNode(
formatSuggestionText(newSuggestion),
uuid,
)
.setFormat(prevNodeFormat)
.setStyle(`font-size: ${toolbarState.fontSize}`);
autocompleteNodeKey = node.getKey();
selection.insertNodes([node]);
$setSelection(selectionCopy);
lastSuggestion = newSuggestion;
}, HISTORY_MERGE);
}

function $handleAutocompleteNodeTransform(node: AutocompleteNode) {
Expand Down Expand Up @@ -187,10 +183,12 @@ export default function AutocompletePlugin(): JSX.Element | null {
}
})
.catch((e) => {
console.error(e);
if (e !== 'Dismissed') {
console.error(e);
}
});
lastMatch = match;
});
}, HISTORY_MERGE);
}
function $handleAutocompleteIntent(): boolean {
if (lastSuggestion === null || autocompleteNodeKey === null) {
Expand Down Expand Up @@ -219,13 +217,15 @@ export default function AutocompletePlugin(): JSX.Element | null {
editor.update(() => {
if ($handleAutocompleteIntent()) {
e.preventDefault();
} else {
$addUpdateTag(HISTORY_MERGE.tag);
}
});
}
function unmountSuggestion() {
editor.update(() => {
$clearSuggestion();
});
}, HISTORY_MERGE);
}

const rootElem = editor.getRootElement();
Expand Down
22 changes: 18 additions & 4 deletions packages/lexical/src/LexicalEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import invariant from 'shared/invariant';

import {$getRoot, $getSelection, TextNode} from '.';
import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
import {createEmptyEditorState} from './LexicalEditorState';
import {cloneEditorState, createEmptyEditorState} from './LexicalEditorState';
import {addRootElementEvents, removeRootElementEvents} from './LexicalEvents';
import {$flushRootMutations, initMutationObserver} from './LexicalMutations';
import {LexicalNode} from './LexicalNode';
Expand Down Expand Up @@ -1112,6 +1112,16 @@ export class LexicalEditor {
);
}

// Ensure that we have a writable EditorState so that transforms can run
// during a historic operation
let writableEditorState = editorState;
if (writableEditorState._readOnly) {
writableEditorState = cloneEditorState(editorState);
writableEditorState._selection = editorState._selection
? editorState._selection.clone()
: null;
}

$flushRootMutations(this);
const pendingEditorState = this._pendingEditorState;
const tags = this._updateTags;
Expand All @@ -1121,11 +1131,10 @@ export class LexicalEditor {
if (tag != null) {
tags.add(tag);
}

$commitPendingUpdates(this);
}

this._pendingEditorState = editorState;
this._pendingEditorState = writableEditorState;
this._dirtyType = FULL_RECONCILE;
this._dirtyElements.set('root', false);
this._compositionKey = null;
Expand All @@ -1134,7 +1143,12 @@ export class LexicalEditor {
tags.add(tag);
}

$commitPendingUpdates(this);
// Only commit pending updates if not already in an editor.update
// (e.g. dispatchCommand) otherwise this will cause a second commit
// with an already read-only state and selection
if (!this._updating) {
$commitPendingUpdates(this);
}
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2549,7 +2549,8 @@ describe('LexicalEditor tests', () => {
`{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello world","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`,
);
editor.setEditorState(state);
expect(editor._editorState).toBe(state);
// A writable version of the EditorState may have been created, we settle for equal serializations
expect(editor._editorState.toJSON()).toEqual(state.toJSON());
expect(editor._pendingEditorState).toBe(null);
});

Expand Down

0 comments on commit 8e0e300

Please sign in to comment.