Feature: No way to add Grammarly-like contextual spell-check
- 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
Assessment
This issue has not been assessed yet.