文本操作:从剪贴板检测替换

Text operations: Detect replacement from clipboard

一般信息
致力于我自己的操作转换算法的实现。对于那些不知道这是什么的人:当多个用户同时处理同一个文档时,此算法会尝试保留每个用户的意图并确保所有用户最终使用同一个文档。

问题
首先,我需要一种检测文本操作的正确方法。就像插入和删除一样。显然,我需要确切地知道发生在哪个位置,以便服务器可以正确转换每个操作以保留其他用户的意图。

到目前为止,我的代码在这方面做得相当不错。但是在选择文本范围并将其替换为另一个文本范围时会遇到麻烦。我为此依赖 input 事件,它似乎无法同时检测到删除和插入操作。执行此操作时,它会检测到对所选文本的删除操作。但它不检测从剪贴板粘贴的文本的插入操作。

我的问题是:我该如何解决这个问题?

我的代码(到目前为止)

let txtArea = {};
let cursorPos = {};
let clientDoc = ""; // Shadow DOC

document.addEventListener("DOMContentLoaded", function(event){
  txtArea = document.getElementById("test");
    clientDoc = txtArea.value;

    txtArea.addEventListener("input", function(){ handleInput(); });
    txtArea.addEventListener("click", function(){ handleSelect(); });
});

/* Gets cursor position / selected text range */
function handleSelect(){
    cursorPos = getCursorPos(txtArea);
}

/* Check whether the operation is insert or delete */
function handleInput(){
    if(txtArea.value > clientDoc){
        handleOperation("insert");
    } else {
        handleOperation("delete");
    }
}

/* Checks text difference to know exactly what happened */
function handleOperation(operation){
    let lines = "";
    if(operation === "insert"){
        lines = getDifference(clientDoc, txtArea.value);
    } else if(operation === "delete"){
        lines = getDifference(txtArea.value, clientDoc);
    }
    const obj = {
        operation: operation,
        lines: lines,
        position: cursorPos
    };
    clientDoc = txtArea.value;
    console.log(obj);
}

/* Simple function to get difference between 2 strings */
function getDifference(a, b)
{
    let i = 0;
    let j = 0;
    let result = "";

    while (j < b.length)
    {
        if (a[i] != b[j] || i == a.length){
            result += b[j];
        } else {
            i++;
        }
        j++;
    }
    return result;
}

/* Function to get cursor position / selection range */
function getCursorPos(input) {
    if ("selectionStart" in input && document.activeElement == input) {
        return {
            start: input.selectionStart,
            end: input.selectionEnd
        };
    }
    else if (input.createTextRange) {
        var sel = document.selection.createRange();
        if (sel.parentElement() === input) {
            var rng = input.createTextRange();
            rng.moveToBookmark(sel.getBookmark());
            for (var len = 0;
                     rng.compareEndPoints("EndToStart", rng) > 0;
                     rng.moveEnd("character", -1)) {
                len++;
            }
            rng.setEndPoint("StartToStart", input.createTextRange());
            for (var pos = { start: 0, end: len };
                     rng.compareEndPoints("EndToStart", rng) > 0;
                     rng.moveEnd("character", -1)) {
                pos.start++;
                pos.end++;
            }
            return pos;
        }
    }
    return -1;
}
#test {
    width: 600px;
    height: 400px;
}
<textarea id="test">test</textarea>

自己设法解决了问题,但不完全确定这是否是最佳解决方案。我在代码中使用注释来解释我是如何解决它的:

function handleOperation(operation){
    let lines = "";

    if(operation === "insert"){
        lines = getDifference(clientDoc, txtArea.value);
    } else if(operation === "delete"){
        lines = getDifference(txtArea.value, clientDoc);
    }

    // This handles situations where text is being selected and replaced
    if(operation === "delete"){

        // Create temporary shadow doc with the delete operation finished
        const tempDoc = clientDoc.substr(0, cursorPos.start) + clientDoc.substr(cursorPos.end);

        // In case the tempDoc is different from the actual textarea value, we know for sure we missed an insert operation
        if(tempDoc !== txtArea.value){
            let foo = "";
            if(tempDoc.length > txtArea.value.length){
                foo = getDifference(txtArea.value, tempDoc);
            } else {
                foo = getDifference(tempDoc, txtArea.value);
            }
            console.log("char(s) replaced detected: "+foo);
        }
    } else if(operation === "insert"){

        // No need for a temporary shadow doc. Insert will always add length to our shadow doc. So if anything is replaced,
        // the actual textarea length will never match
        if(clientDoc.length + lines.length !== txtArea.value.length){
            let foo = "";
            if(clientDoc.length > txtArea.value.length){
                foo = getDifference(txtArea.value, clientDoc);
            } else {
                foo = getDifference(clientDoc, txtArea.value);
            }
            console.log("char(s) removed detected: "+foo);
        }
    }
    const obj = {
        operation: operation,
        lines: lines,
        position: cursorPos
    };

    // Update our shadow doc
    clientDoc = txtArea.value;

    // Debugging
    console.log(obj);
}

如果你能给我更好的解决方案/提示/建议,我仍然非常愿意。