LexicaljS - Spellchecker

92 Views Asked by At

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;
}
0

There are 0 best solutions below