Search code examples
javascriptregexcontenteditable

How to determine if cursor comes after certain characters in a contenteditable div


I'm building an autosuggest/autocomplete feature in a contenteditable div and I'm having a problem with determining when the cursor comes after characters that don't qualify for autocompletion. What I want to do is run my autosuggest ajax request only if the user is typing an "@" handle, such as "@someUser". The problem is that contenteditable divs contain actual html, so I'm tripping up when trying to determine if the last character before the cursor is one of my approved characters. My approved characters are: A-z0-9. I'm using this regex: /(&nbsp; $|&nbsp;$|\s$)/, but it only checks for spaces. I can't simply negate my approved characters (something like [^A-z0-9]) because the HTML of the contenteditable would cause false negatives (the contenteditable div can have something like <div>test</div> as its innerHTML). I created this demo to try to showcase the problem. Here is the code for it:

document.querySelector('button').addEventListener('click', handleClick);

function handleClick(e) {
  const inputField = document.querySelector('#edit-me');
  const text = inputField.innerHTML;
  if (!text) {
    alert('no text');
  }
  const afterInvalidChar = isAfterInvalidChar(text, getCaretIndex(inputField));
  if (afterInvalidChar) {
    alert('cursor is not after an accepted char');
  } else {
    alert('cursor is after an accepted char');
  }
}

function isAfterInvalidChar(text, caretPosition) {
    // first get text from the beginning until the caret
    let termToSearch = text.slice(0, caretPosition);
    console.log(termToSearch);
    alert('content before cursor: ' + termToSearch);
    const rgxToCheckEnding = /(&nbsp; $|&nbsp;$|\s$)/; // <-- this is where I'm tripping up, that regex only checks for spaces, and it's still not great
    if (rgxToCheckEnding.test(termToSearch)) {
        // the cursor is after a space, but I also need to check for anything
        // that's not one of my accepted contents
        return true;
    } else {
      return false;
    }
}

// source: https://stackoverflow.com/a/46902361/7987987
function getCaretIndex (node) {
    var range = window.getSelection().getRangeAt(0),
        preCaretRange = range.cloneRange(),
        caretIndex,
        tmp = document.createElement("div");

    preCaretRange.selectNodeContents(node);
    preCaretRange.setEnd(range.endContainer, range.endOffset);
    tmp.appendChild(preCaretRange.cloneContents());
    caretIndex = tmp.innerHTML.length;
    return caretIndex;
}
#edit-me {
  width: 200px;
  height: 200px;
  background-color: lightblue;
}
<h2>Cursor of contenteditable div</h2>
<p>Accepted chars: <code>A-z0-9</code></p>
<button type="button">is cursor after an accepted char?</button>
<div contenteditable id="edit-me"><div>

An accceptable solution should work with the following text in the contenteditable div (just basic tests but you get the point):

  • "test": correctly say the last char is approved
  • "test ": correctly say the last char is not approved
  • "test-": correctly say the last char is not approved
  • "test?": correctly say the last char is not approved

How can get this working for only my approved chars? I'm open to an entirely different strategy if it gets the job done. Thanks!


Solution

  • The problem is that you need to decode HTML entities.
    You can easily do it with this function

    function decodeHTML(htmlString) {
        var txt = document.createElement('div');
        txt.innerHTML = htmlString;
        return txt.textContent;
    };
    

    basically what it does is it makes an imaginary div and places the html code in the content of the div, and you get plain text when you request the contents using textContent. Now all you have to do is change termToSearch to decodeHTML(text.slice(0, caretPosition)); and make your regex to check the ending

    Please note that the function will not acctually make a div tag, but will still return the decoded values. The reason for this is because document.createElement() just makes an object of an HTML element rather than an actual element.