CkEditor 5 自定义拼写检查

问题描述

我正在使用 Angular 11 并与 CkEditor 5 组件 集成。我需要拼写检查功能,但注意到这不是免费的!所以我决定自己做。 参考下面的存储库,我写下了 ckeditor 组件的角度指令:

https://github.com/uladzimir-miadzinski/ngx-spellchecker/tree/master/src

我不得不进行一些调整以使其与 CkEditor 5 兼容(拼写检查存储库适用于 CkEditor 4!)。 所以在我的角度项目中,我有

    <ckeditor [editor]="editor"
        #htmlEditor
        [config]="ckEditorConfig"
        data="{{input}}"
        (ready)="CkEditorMy.onEditorReady($event)"
        (change)="onEditorChange($event)"
        appSpellCheck
        [htmlEditor]="htmlEditor">
    </ckeditor>

其中 appSpellCheck 是我的指令:

import { AfterViewInit,Directive,ElementRef,HostListener,Input,OnInit } from '@angular/core';
import { Misspelled,SpellCheckService } from '../../services/spell-check.service';
import { CkEditorMy } from '../lib/ckeditor';
import { debounce } from '../lib/delay.decorator';

enum KeyboardButtons {
    Delete = 46,Backspace = 8
}
interface StyleOptions {
    [key: string]: string;
}
interface SpellcheckerOptions {
    style: StyleOptions;
}
@Directive({
    selector: '[appSpellCheck]',})
export class SpellCheckDirective implements OnInit,AfterViewInit {

    @input() htmlEditor: any;
    @input() timeout = 1000;
    @input() options: SpellcheckerOptions = {
        style: {
            'text-decoration': 'none','background': 'transparent',// '#f8d2d4','border-bottom': '1px solid #e00','Box-shadow': 'inset 0 -1px 0 #e00','color': 'inherit','transition': 'background 0.1s cubic-bezier(.33,.66,1)'
        }
    };

    get errorStyle() {
        return `
        mark.tf-spellchecker-err {
            ${Object.entries(this.options.style).reduce((acc,[key,value]) => acc + `${key}:${value};`,'')}
        }
        `;
    }

    private _contentNode: Element;

    constructor(
        private el: ElementRef,private spellcheckService: SpellCheckService
    ) {
    }

    ngOnInit(): void {
        const el = this.el.nativeElement as HTMLInputElement;
        const styleInject = document.createElement('style');
        styleInject.innerHTML = this.errorStyle;
        el.after(styleInject);
        el.setAttribute('spellcheck','false');
    }

    ngAfterViewInit(): void {
        // This code cannot take content node since there is not one yet! It is created after...
        // this._contentNode = this.getCkEditorContentNode();
    }

    private getCkEditorContentNode() {
        const el = this.el.nativeElement as HTMLInputElement;
        const resetNode = this.getChildNodeByClass(el,"ck ck-reset");
        if (resetNode) {
            const mainNode = this.getChildNodeByClass(resetNode,"ck ck-editor__main");
            if (mainNode) {
                const contentNode = this.getChildNodeByClass(mainNode,"ck ck-content");
                if (contentNode) {
                    return contentNode;
                }
            }
        }
        return null;
    }

    private getChildNodeByClass(el: Element,className: string) {
        for (var i = 0; i < el.children.length; i++) {
            const childNode = el.children[i];
            if (childNode.className?.startsWith(className)) {
                return childNode;
            }
        }
        return null;
    }

    @HostListener('keypress')
    @debounce()
    async onKeyPress() {
      await this.spellcheck();
    }

    @HostListener('keyup',['$event'])
    async onKeyUp(e) {
        if (e.which === KeyboardButtons.Backspace || e.which === KeyboardButtons.Delete) {
            await this.spellcheck();
        }
    }

    @HostListener('focus')
    @debounce()
    async onFocus() {
        await this.spellcheck();
    }

    getMatches(word,text) {
        const regex = new RegExp(`\\b${word}\\b`,'gi');
        const result = [];

        let match = null;
        do {
            match = regex.exec(text);
            if (match) {
                result.push(match.index);
            }
        } while (match);
        return result;
    }

    private async spellcheck() {
        if (this.htmlEditor?.editorInstance) {
            // const el = this.el.nativeElement as HTMLInputElement;
            // const textToSend = el.innerText || '';
            let textToSend = CkEditorMy.getPlainText(this.htmlEditor.editorInstance);
            if (!this._contentNode) {
                this._contentNode = this.getCkEditorContentNode();
            }
            if (this._contentNode) {
                const misspelledWords = await this.spellcheckService.checkText(textToSend);
                await this.makeChildNodesWithSpelling(this._contentNode.childNodes,misspelledWords);
            }
        }
    }

    private async makeChildNodesWithSpelling(childNodes,misspelledWords: Misspelled[]) {
        for (let m = 0; m < misspelledWords.length; m++) {
            for (let i = 0; i < childNodes.length; i++) {
                await this.makeNodeWithSpelling(childNodes[i],misspelledWords[m]);
            }
        }
    }

    private async makeNodeWithSpelling(node,misspelled: Misspelled) {
        if (node.hasChildNodes()) {
            if (node.nodeName === 'MARK' && node.textContent === node.dataset.misspelled) {
                // value inside <mark> not changed
                return;
            } else if (node.nodeName === 'MARK') {
                // value inside <mark> was changed,need to update <mark> node with new spell,or correct
                const corrections: Misspelled[] = await this.spellcheckService.checkText(node.textContent);
                if (corrections.length) {
                    // word with another errors
                    node.dataset.missplled = corrections[0].word;
                    node.dataset.suggest = corrections[0].suggestions;
                } else {
                    // word without errors
                    const caret = this.getCaretCharacterOffsetWithin(this._contentNode as HTMLInputElement);
                    const textNodeFrag = document.createTextNode(node.textContent);
                    node.parentNode.replaceChild(textNodeFrag,node);
                    this.setCaretPosition(this._contentNode as HTMLInputElement,caret);
                }
            }
            await this.makeChildNodesWithSpelling(node.childNodes,[misspelled]);
        } else if (node.nodeValue) {
            // text value of node,the smallest part

            let newNodeValue = node.nodeValue;
            const matches = this.getMatches(misspelled.word,node.nodeValue);
            const mark = document.createElement('mark');

            // mark.dataset.suggest = misspelled.suggestions;
            mark.dataset.suggest = JSON.stringify(misspelled.suggestions);
            mark.dataset.misspelled = misspelled.word;
            mark.setAttribute('class','tf-spellchecker-err');
            for (let i = matches.length - 1; i >= 0; i--) {
                const matchIndex = matches[i];
                mark.innerHTML = newNodeValue.substr(matchIndex,misspelled.word.length);
                newNodeValue = newNodeValue.slice(0,matchIndex) + mark.outerHTML + newNodeValue.slice(matchIndex + misspelled.word.length);
            }
            const frag = document.createrange().createContextualFragment(newNodeValue);
            const caret = this.getCaretCharacterOffsetWithin(this._contentNode as HTMLInputElement);
            node.parentNode.replaceChild(frag,node);
            this.setCaretPosition(this._contentNode as HTMLInputElement,caret);
        }
    }

    private getCaretCharacterOffsetWithin(element) {
        var caretoffset = 0;
        var doc = element.ownerDocument || element.document;
        var win = doc.defaultview || doc.parentwindow;
        var sel;
        if (typeof win.getSelection !== 'undefined') {
            sel = win.getSelection();
            if (sel.rangeCount > 0) {
                var range = win.getSelection().getRangeAt(0);
                var preCaretRange = range.cloneRange();
                preCaretRange.selectNodeContents(element);
                preCaretRange.setEnd(range.endContainer,range.endOffset);
                caretoffset = preCaretRange.toString().length;
            }
        } else if ((sel = doc.selection) && sel.type != 'Control') {
            var textRange = sel.createrange();
            var preCaretTextRange = doc.body.createTextRange();
            preCaretTextRange.movetoElementText(element);
            preCaretTextRange.setEndPoint('EndToEnd',textRange);
            caretoffset = preCaretTextRange.text.length;
        }
        return caretoffset;
    }

    private getcaretposition() {
        if (window.getSelection && window.getSelection().getRangeAt) {
            var range = window.getSelection().getRangeAt(0);
            var selectedobj = window.getSelection();
            var rangeCount = 0;
            var childNodes = selectedobj.anchorNode.parentNode.childNodes;
            for (let i = 0; i < childNodes.length; i++) {
                if (childNodes[i] === selectedobj.anchorNode) {
                    break;
                }
                if ((childNodes[i] as HTMLInputElement).outerHTML) {
                    rangeCount += (childNodes[i] as HTMLInputElement).outerHTML.length;
                } else if (childNodes[i].nodeType === 3) {
                    rangeCount += childNodes[i].textContent.length;
                }
            }
            return range.startOffset + rangeCount;
        }
        return -1;
    }

    private setCaretPosition(el,sPos) {
        var charIndex = 0,range = document.createrange();
        range.setStart(el,0);
        range.collapse(true);
        var nodeStack = [el],node,foundStart = false,stop = false;

        while (!stop && (node = nodeStack.pop())) {
            if (node.nodeType == 3) {
                var nextCharIndex = charIndex + node.length;
                if (!foundStart && sPos >= charIndex && sPos <= nextCharIndex) {
                    range.setStart(node,sPos - charIndex);
                    foundStart = true;
                }
                if (foundStart && sPos >= charIndex && sPos <= nextCharIndex) {
                    range.setEnd(node,sPos - charIndex);
                    stop = true;
                }
                charIndex = nextCharIndex;
            } else {
                var i = node.childNodes.length;
                while (i--) {
                    nodeStack.push(node.childNodes[i]);
                }
            }
        }
        let selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
    }
}

问题:指令做了什么承诺,事实上,如果我把 debugger 放在 spellcheck() 中,我可以看到拼写错误的单词:

enter image description here

恢复执行有东西删除了我的工作!

enter image description here

我在 CkEditor 5 文档中找不到任何关于此的内容,由于我的企业政策,我需要免费使用此功能!谢谢你的帮助! 如果您需要我代码的其他部分来复制问题:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export interface Misspelled {
    suggestions: string[];
    word: string;
}
@Injectable({
    providedIn: 'root'
})
export class SpellCheckService {

    constructor(private http: HttpClient) {
    }

    async checkText(text: string): Promise<Misspelled[]> {
        return new Promise<Misspelled[]>((resolve,reject) => {
            resolve([{
                word: "Codice",suggestions: [
                    'CodiceBU','CodiceBUBU','CodiceBUBUBU',]
            }]);
        });
    }
}

export function debounce(delay: number = 300): MethodDecorator {
    return (target: Object,propertyKey: string | symbol,descriptor: PropertyDescriptor) => {
        const original = descriptor.value;
        descriptor.value = function (...args) {
            clearTimeout(this.__timeout__);
            this.__timeout__ = setTimeout(() => original.apply(this,args),delay);
        };
        return descriptor;
    };
}

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)