Vanilla JavaScript to Save and Restore Caret Position When HTML Updated in Content Editable DIV

Viewed 235

I know there many similar questions with answers on StackOverflow and Google and I've tried many and none of them works. So I will post my own code below and hopefully someone can help. Thanks in advance.

Note: there might be typos in below codes as I manually typed this question. I've checked and apologise for any inconvenience if they confuse you.

The logic I'm trying to achieve in that contenteditable DIV:

  1. User types in the DIV
  2. Meanwhile an event listener is checking the text user enters and filter with pre-defined keywords array such as ['apple', 'banana']. Filtered text will be wrapped in the HTML and write back to the DIV.
  3. While achieving above, user doesn't lose mouse caret which means this scanning and replacing procedure should be constantly happening in the background and doesn't affect user's editing.

The code:

HTML (Before Process)

<div id="editor">
    Mr Jenkins has an apple and a banana.
</div>

HTML (After Processed)

<div id="editor">
    Mr Jenkins has an <u>apple</u> and a <u>banana</u>.
</div>

JavaScript (It's in vue so I skipped other parts here and display in vanilla way here)

Plan A to use pre-defined selection variable

This throws error "DOMException: Failed to execute 'collapse' on 'Selection': There is no child at offset 7"

// Global variable to store the offset value when DOM content loaded
// The input event of the #editor element is listened but skipped here
var caretPosition,
    selection = window.getSelection();

function scanText() {
    let editor = document.getElementById('editor');

    // Save the position
    caretPosition = selection.focusOffset;

    // Scan and replace text
    editor.innerHTML = filterText(); // Function filterText will return HTML as in above example

    // Restore the position
    selection.collapse(editor, caretPosition);
}

Plan B to use get selection on the go

This doesn't throw any errors but caret position is weird The focusOffset value is far smaller than the real focusOffset value. For example, if focusOffset in Plan A is 100, it's 9 in Plan B. So it always restore at the incorrect position

function scanText() {
    let editor = document.getElementById('editor');

    // Save the position
    caretPosition = window.getSelection().focusOffset;

    // Scan and replace text
    editor.innerHTML = filterText(); // Function filterText will return HTML as in above example

    // Restore the position
    window.getSelection().collapse(editor, caretPosition);
}

Plan C to use range

"IndexSizeError: Failed to execute 'setStart' on 'Range': There is no child at offset 5."

function scanText() {
    let editor = document.getElementById('editor'),
        offsets, range = new Range();

    // Save the range
    // It starts at 5 and ends at 101 though I only placed cursor at the end of the text
    if (window.getSelection().rangeCount) {
        offsets = [
            window.getSelection().getRangeAt(0).startOffset, // 5
            window.getSelection().getRangeAt(0).endOffset // 101
        ];
    }

    // Scan and replace text
    editor.innerHTML = filterText(); // Function filterText will return HTML as in above example

    // Restore the range
    range.setStart(editor, offsets[0]); // Set start container to editor and start offset as 5
    range.setEnd(editor, offsets[1]); // Set end container to editor and end offset as 101
}
0 Answers
Related