I am trying to create a spellchecker plugin. I already have a backend that can recieve words or sentences and returns a list of incorrect words [{"word":"sd","start":0,"end":2,"errorText":"Spelling Error","suggestions":null}]. I was partly successfull in creating a highlighter, however some things never work. The current version works pretty ok in a simple environment with only the spellchecker as a registered plugin (the major issue is that the cursor randomly jumps a few chars when a spelling mistake is detected). However if I try to put it in an editor with most of the public plugins registered (pretty much the playground), it breaks and sends requests in a loop after the second word.
I am not sure if this is the way how one should implement such a thing with lexial, maybe there is a better way. Thx for any help in advance :) ! This is my code (sorry for this eyesore, I have been changing this code for the last 3 days not worrying about quality)
import React, { useCallback, useMemo, useState, useEffect, useRef } from 'react'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
TextNode,
$applyNodeReplacement,
Spread,
COMMAND_PRIORITY_EDITOR,
createCommand,
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
$isTextNode
} from 'lexical';
import axios from "axios";
export type SerializedSpellCheckNode = Spread<
{
__errorType?: string;
},
SerializedTextNode
>;
export class SpellCheckNode extends TextNode {
__errorType?: string;
constructor(text: string, key?: NodeKey, errorType?: string) {
super(text, key);
this.__errorType = errorType;
}
static getType(): string {
return 'spellcheck';
}
static clone(node: SpellCheckNode): SpellCheckNode {
return new SpellCheckNode(node.__text, node.__key, node.__errorType);
}
static importJSON(serializedNode: SerializedSpellCheckNode): SpellCheckNode {
const node = $createSpellCheckNode(serializedNode.text, serializedNode.__errorType);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
exportJSON(): SerializedSpellCheckNode {
return {
...super.exportJSON(),
__errorType: this.__errorType,
type: 'spellcheck',
version: 1,
};
}
createDOM(_config: EditorConfig): HTMLElement {
const dom = super.createDOM(_config);
dom.className = "spell-error"
dom.title = this.__errorType;
return dom;
}
canInsertTextBefore(): boolean {
return false;
}
isTextEntity(): true {
return true;
}
}
function findFirstSpecialCharacterIndex(str) {
const specialCharacters = [' ','\n', '\r', '\t', '.', ',', ';', ':', '!', '?'];
for (let i = 0; i < str.length; i++) {
if (specialCharacters.includes(str[i])) {
return i;
}
}
return null; // If no special character is found
}
export function $createSpellCheckNode(text: string, errorType?: string): SpellCheckNode {
const spellCheckNode = new SpellCheckNode(text, null, errorType);
/*spellCheckNode.setMode('segmented').toggleDirectionless();*/
console.log("$applyNodeReplacement");
return $applyNodeReplacement(spellCheckNode);
}
export function $replaceWithSpellCheckNode(text: TextNode, errorType?: string): SpellCheckNode {
const spellCheckNode = new SpellCheckNode(text.__text, null, errorType);
spellCheckNode.setFormat(text.format);
spellCheckNode.setDetail(text.detail);
spellCheckNode.setMode(text.mode);
spellCheckNode.setStyle(text.style);
/*spellCheckNode.setMode('segmented').toggleDirectionless();*/
console.log("$applyNodeReplacement");
return $applyNodeReplacement(spellCheckNode);
}
export function $replaceSpellCheckNode(text: SpellCheckNode): TextNode{
const textNode = new TextNode(text.__text, null);
textNode.setFormat(text.format);
textNode.setDetail(text.detail);
textNode.setMode(text.mode);
textNode.setStyle(text.style);
return $applyNodeReplacement(textNode);
}
export function $isSpellCheckNode(node: LexicalNode): node is SpellCheckNode {
return node instanceof SpellCheckNode;
}
export const INSERT_VARIABLE_COMMAND = createCommand('insertSpellCheck');
export const UPDATE_VARIABLE_COMMAND = createCommand('updateSpellCheck');
export type SpellCheckPayload = {
errorType?: string;
node?: TextNode;
};
export const APPLY_SPELL_CHECK = createCommand('ApplySpellCheck');
export const REMOVE_SPELL_CHECK = createCommand('RemoveSpellCheck');
export type SpellErrorChangedPayload = {
response: any;
content: any;
textNode: any;
};
export function SpellCheckerPlugin() {
const [editor] = useLexicalComposerContext();
const sendRequestsToApi = async (inputText) => {
try {
console.log("sendRequestsToApi", inputText)
// Define the language (optional, adjust as needed)
const language = 'de_AT';
// Send HTTP GET requests for the word list to the API
return axios.post("/SpellCheck", {
inputText,
language,
});
} catch (error) {
console.error('Error during API request:', error.message);
}
};
useEffect(() => {
const AddSpellChecks = editor.registerNodeTransform(TextNode, (textNode) => {
const content = textNode.getTextContent();
console.log("registerNodeTransform, CheckSpellError", content)
sendRequestsToApi(content)?.then(response => {
console.log(response?.data)
editor.dispatchCommand(APPLY_SPELL_CHECK, { response, content, textNode })
})
});
const RemoveSpellChecks = editor.registerNodeTransform(SpellCheckNode, (textNode) => {
console.log("SpellCheck ")
const content = textNode.getTextContent();
const SpaceIndex = findFirstSpecialCharacterIndex(content);
if (SpaceIndex > -1)
{
const parts = textNode.splitText(SpaceIndex);
console.log("parts", parts)
}
sendRequestsToApi(content)?.then(response => {
console.log(response?.data)
editor.dispatchCommand(REMOVE_SPELL_CHECK, { response, content, textNode })
})
});
const RemoveSpellError = editor.registerCommand(REMOVE_SPELL_CHECK,
(payload: SpellErrorChangedPayload) => {
console.log("RemoveSpellError", payload, $isTextNode(payload.textNode.getNextSibling()))
//const applySpellCheckHighlight = (ErrorIndex, spellingMistake, textNode) => {
//}
if (payload.response.data.length == 0) {
const newNode = $replaceSpellCheckNode(payload.textNode);
payload.textNode.replace(newNode);
/*newNode.select();*/
console.log("RemoveSpellError","create text node from ")
}
return false;
}, COMMAND_PRIORITY_EDITOR);
const ApplySpellError = editor.registerCommand(APPLY_SPELL_CHECK,
(payload: SpellErrorChangedPayload) => {
const applySpellCheckHighlight = (ErrorIndex, spellingMistake, textNode) => {
const splitText = textNode.splitText(ErrorIndex);
console.log("applySpellCheckHighlight", ErrorIndex, ErrorIndex+spellingMistake.word.length, splitText)
let ErrorTextBlock = splitText[0];
if (splitText.length > 1) {
if (splitText[0].__text != spellingMistake) {
ErrorTextBlock = splitText[1];
}
}
const mentionNode = $replaceWithSpellCheckNode(ErrorTextBlock, spellingMistake.errorText);
if (ErrorTextBlock) {
ErrorTextBlock.replace(mentionNode);
}
mentionNode.select();
}
payload.response.data.forEach(spellingMistake => {
const ErrorIndex = payload.content.indexOf(spellingMistake.word);
console.log("apply", spellingMistake, payload.content, ErrorIndex);
if (ErrorIndex > -1) {
console.log("apply")
applySpellCheckHighlight(ErrorIndex, spellingMistake, payload.textNode)
}
})
return false;
}, COMMAND_PRIORITY_EDITOR);
return () => {
// Do not forget to unregister the listener when no longer needed!
AddSpellChecks();
RemoveSpellChecks();
RemoveSpellError();
ApplySpellError();
};
}, [editor]);
return null;
}