11

标题可能有点误导;我的拼写检查器更关注格式而不是拼写(大写字母、标点符号和空格、撇号、将互联网俚语转换为完整单词、经常打乱的单词等)。然而,基本原则适用。

基本上,我正在构建的 JS/jQuery 检查器会在键入单词时纠正它们(在单词之后键入空格或标点符号之后)。

然而,就像任何自动更正一样,它必然会遇到错误。我什至没有考虑创建功能来确定“它的”或“它的”在给定情况下是否更合适(尽管如果存在这样的插件或代码片段,请给我指出一个)。

所以我想让它成为一个“屈服”的自动更正(因为缺乏更好的名字的知识)。基本上;

  1. 用户键入一个会触发检查器的单词,然后键入一个空格。
  2. 检查员纠正这个词。
  3. 用户认为这是一个错误并将其纠正回来(通过退格整个单词或部分单词,或突出显示它,或者他们觉得编辑它很舒服)。
  4. 用户继续键入,检查器不会再次触摸该单词的该实例。

现在最简单的当然是完全禁用对该单词的检查,但我希望检查器更正它的未来实例。我正在寻找的是检测用户将自动更正的单词(无论是在键入之后还是之后)编辑回自动更正之前的状态,然后学习不理会该单词的特定实例。

我什至不知道从哪里开始。我正在考虑一个 contenteditable,每个单词都包含在一个 span 中,自动更正的单词具有特殊的类和包含原始单词的 data-* 属性,监听自动更正单词的编辑,如果它被编辑回等于 data-*值,添加一个类,使其不在未来的自动更正轮次中。

我在想虽然这可能是不必要的复杂,或者至少不是阻力最小的路径。这样做最聪明的方法是什么?

4

2 回答 2

7

span乍一看,您建议的方法(将每个单词分开并在其中存储其他数据)似乎是最明智的方法。在编辑器级别,您只需要确保所有文本都在 somespan中,并且每个文本只包含一个单词(如有必要,将其拆分)。在单词级别上,只需监听spans(绑定inputpropertyChange)的变化并根据其类/数据采取行动。

然而,真正的痛苦是保持插入符号位置一致。当您使用 更改 atextarea或元素的内容时contentEditable,插入符号的移动相当不可预测,并且没有简单的(跨浏览器)方法来跟踪插入符号。我在 SO 和其他地方都搜索了解决方案,我找到的最简单的工作解决方案是这篇博文。不幸的是,它仅适用于textarea,因此无法使用“跨度中的每个单词”解决方案。

所以,我建议采用以下方法:

  • 在 中保留一个单词列表Array,其中每个单词都存储当前值和原始值;
  • 当内容发生textarea变化时,保持不变的单词集,其余的重做;
  • 仅当插入符号就在非单词字符之后(改进空间)并且您没有击中时才应用拼写检查backspace
  • 如果用户对更正不满意,点击一次即可撤消,除非修改backspace,否则不会再次检查。
    • 如果一次完成了许多更正(例如,如果大量文本被复制粘贴),则每个更正都backspace将撤消一次更正,直到没有留下任何一个。
    • 点击任何其他键将提交更正,因此如果用户仍然不满意,他将不得不返回并再次更改它。
    • 注意:与OP要求不同,如果用户输入非单词字符,更改后的版本将再次自动更正;他需要打backspace一次来“保护”它。

我在jsFiddle创建了一个简单的概念验证。详情如下。请注意,您可以将其与其他方法结合使用(例如,检测“向下箭头”键并显示带有一些自动更正选项的菜单)等。


详细解释概念验证的步骤:

  • 在 中保留一个单词列表Array,其中每个单词都存储当前值和原始值;

    var words = [];
    

    此正则表达式将文本拆分为单词(每个单词都有一个word属性和sp一个;后者存储紧随其后的非单词字符)

    delimiter:/^(\w+)(\W+)(.*)$/,
    ...
    regexSplit:function(regex,text) {
        var ret = [];
        for ( var match = regex.exec(text) ; match ; match = regex.exec(text) ) {
            ret.push({
                word:match[1],
                sp:match[2],
                length:match[1].length + match[2].length
            });
            text = match[3];
        }
        if ( text )
            ret.push({word:text, sp:'', length:text.length});
         return ret;
    }
    
  • 当内容发生textarea变化时,保持不变的单词集,其余的重做;

        // Split all the text
        var split = $.autocorrect.regexSplit(options.delimiter, $this.val());
        // Find unchanged words in the beginning of the field
        var start = 0;
        while ( start < words.length && start < split.length ) {
            if ( !words[start].equals(split[start]) )
                break;
            start++;
        }
        // Find unchanged words in the end of the field
        var end = 0;
        while ( 0 < words.length - end && 0 < split.length - end ) {
            if ( !words[words.length-end-1].equals(split[split.length-end-1]) ||
                 words.length-end-1 < start )
                break;
            end++;
        }
        // Autocorrects words in-between
        var toSplice = [start, words.length-end - start];
        for ( var i = start ; i < split.length-end ; i++ )
            toSplice.push({
                word:check(split[i], i),
                sp:split[i].sp,
                original:split[i].word,
                equals:function(w) {
                    return this.word == w.word && this.sp == w.sp;
                }
            });
        words.splice.apply(words, toSplice);
        // Updates the text, preserving the caret position
        updateText();
    
  • 仅当插入符号就在非单词字符之后(改进空间)并且您没有击中时才应用拼写检查backspace

    var caret = doGetCaretPosition(this);
    var atFirstSpace = caret >= 2 &&
                       /\w\W/.test($this.val().substring(caret-2,caret));
    function check(word, index) {
        var w = (atFirstSpace && !backtracking ) ?
                options.checker(word.word) :
                word.word;
        if ( w != word.word )
            stack.push(index); // stack stores a list of auto-corrections
        return w;
    }
    
  • 如果用户对更正不满意,点击一次即可撤消,除非修改backspace,否则不会再次检查。

    $(this).keydown(function(e) {
        if ( e.which == 8 ) {
            if ( stack.length > 0 ) {
                var last = stack.pop();
                words[last].word = words[last].original;
                updateText(last);
                return false;
            }
            else
                backtracking = true;
            stack = [];
        }
    });
    
  • 的代码updateText只是将所有单词再次连接成一个字符串,并将值设置回textarea. 如果没有任何更改,或者在最后一次完成/撤消自动更正之后放置插入符号,则保留插入符号,以说明文本长度的变化:

    function updateText(undone) {
        var caret = doGetCaretPosition(element);
        var text = "";
        for ( var i = 0 ; i < words.length ; i++ )
            text += words[i].word + words[i].sp;
        $this.val(text);
        // If a word was autocorrected, put the caret right after it
        if ( stack.length > 0 || undone !== undefined ) {
            var last = undone !== undefined ? undone : stack[stack.length-1];
            caret = 0;
            for ( var i = 0 ; i < last ; i++ )
                caret += words[i].word.length + words[i].sp.length;
            caret += words[last].word.length + 1;
        }
        setCaretPosition(element,caret);
    }
    
  • 最终的插件结构:

    $.fn.autocorrect = function(options) {
        options = $.extend({
            delimiter:/^(\w+)(\W+)(.*)$/,
            checker:function(x) { return x; }
        }, options);
        return this.each(function() {
            var element = this, $this = $(this);
            var words = [];
            var stack = [];
            var backtracking = false;
            function updateText(undone) { ... }
            $this.bind("input propertyChange", function() {
                stack = [];
                // * Only apply the spell check if the caret...
                // * When the contents of the `textarea` changes...
                backtracking = false;
            });
            // * If the user was unsatisfied with the correction...
        });
    };
    $.autocorrect = {
        regexSplit:function(regex,text) { ... }
    };
    
于 2012-11-30T21:07:12.587 回答
0

假设您只提交插入符号左侧的单词,您是否可以禁用拼写检查器,直到键入空格字符或移动文本框插入符号?

我不确定这是否是您想要的答案。

于 2012-11-30T20:55:10.840 回答