facebook / facebook/lexical

Feature: No way to add Grammarly-like contextual spell-check

Open
#4,103 25 comments 4 reactions 0 assignees View on GitHub
feature other
Dominant language
TypeScript
Stars
23.9k
Forks
2.2k
Avg merge
1d 14h
Merged PRs (30d)
55

Description

My use case is as follows:

1) Whenever the user updates the text of the editor in any way, a request is made with the new text content of the editor to our custom spellcheck API. This is throttled so that it doesn't actually occur for every character input, only once the user has stopped typing to prevent unnecessary network requests.

2) The API response returns a list of ranges. Example: `[[0, 5], [5, 9], [9, 15]]`. I also know if the range is "good text" or "bad text". Let's assume the [5, 9] range is a spelling error and the rest are normal text ranges.

3) I want to split the editor content into three elements, two normal `TextNodes` and then a custom node `BadSpellingNode` that extends `TextNode` that when hovered shows a contextual menu.

I have tried many solutions to accomplish this. I've tried every variation of listening approaches stated in the documentation. I feel this should be way simpler as I was able to easily accomplish this using `slate` but abandoned that library once I found that it wasn't typed properly and had a lot of churn resulting from the `plate` extension project.

Every approach that I utilize either results in an infinite loop: my listener triggers a side effect that updates the text and then triggers the same side effect again. In addition, computing where the caret/selection needs to be on each re-render of the editor (since it is lost and there are now multiple nodes) becomes extremely convoluted.

Am I doing something wrong?

This was previously quite easy in `slate`. Code isn't super clean here but this was my general approach.

```typescript
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {

if (leaf.bold) {
children = {children};
}

if (leaf.italic) {
children = {children};
}

if (leaf.underline) {
children = {children};
}

const ref = useRef(null);

const [open, setOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);

const handleMouseEnter = (event: React.MouseEvent) => {
setAnchorEl(event.currentTarget);
setOpen(true);
};

const handleMouseLeave = (event: React.MouseEvent) => {
setAnchorEl(null);
setOpen(false);
};
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {

if (leaf.bold) {
children = {children};
}

if (leaf.italic) {
children = {children};
}

if (leaf.underline) {
children = {children};
}

const ref = useRef(null);

const [open, setOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);

const handleMouseEnter = (event: React.MouseEvent) => {
setAnchorEl(event.currentTarget);
setOpen(true);
};

const handleMouseLeave = (event: React.MouseEvent) => {
setAnchorEl(null);
setOpen(false);
};

const strikeColor = 'red';

const editor = useSlate();

// Called when a user click to accept a spelling suggestion
const handleDelete = useCallback(
(event: React.MouseEvent, from: number, to: number, replacementWord: string) => {
event.preventDefault();
// Select the data to replace
Transforms.select(editor, {
anchor: { path: [0, 0], offset: from },
focus: { path: [0, 0], offset: to }
});
// Create a DataTransfer object
const dataTransfer = new DataTransfer();
dataTransfer.setData('text/plain', replacementWord);
// Replace the data
ReactEditor.insertTextData(editor, dataTransfer);
},
[editor]
);

const card = (





{originalWord}






handleDelete(
event,
leaf.wordRange[0],
leaf.wordRange[1],
leaf.replacementWord
)
}
>
{replacementWord}


{message}



{/* Learn More */}


);

return (
handleMouseEnter(event) : undefined
}
onMouseLeave={
leaf.badSpelling ? (event) => handleMouseLeave(event) : undefined
}
style={{
textDecoration: leaf.badSpelling ? 'red wavy underline' : undefined
}}
>
{children}


{({ TransitionProps }) => (

{card}

)}


);
};
const strikeColor = 'red';

const editor = useSlate();

// Called when a user click to accept a spelling suggestion
const handleDelete = useCallback(
(event: React.MouseEvent, from: number, to: number, replacementWord: string) => {
event.preventDefault();
// Select the data to replace
Transforms.select(editor, {
anchor: { path: [0, 0], offset: from },
focus: { path: [0, 0], offset: to }
});
// Create a DataTransfer object
const dataTransfer = new DataTransfer();
dataTransfer.setData('text/plain', replacementWord);
// Replace the data
ReactEditor.insertTextData(editor, dataTransfer);
},
[editor]
);

const card = (





{originalWord}






handleDelete(
event,
leaf.wordRange[0],
leaf.wordRange[1],
leaf.replacementWord
)
}
>
{replacementWord}


{message}



{/* Learn More */}


);

return (
handleMouseEnter(event) : undefined
}
onMouseLeave={
leaf.badSpelling ? (event) => handleMouseLeave(event) : undefined
}
style={{
textDecoration: leaf.badSpelling ? 'red wavy underline' : undefined
}}
>
{children}


{({ TransitionProps }) => (

{card}

)}


);
};
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.