0

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!

Uche Ozoemena
  • 816
  • 2
  • 10
  • 25
  • Why not just watch the "onkeyup" of "edit-me" and check for the "@" (SHIFT + 2)? Then you could fire the autocomplete only when they're in that? – Colin G Jul 31 '20 at 18:00
  • Maybe I don't understand you fully but how then would I determine when the user is no longer typing after the "@"? Yes I can start checking after "@" but then when would I know to stop? Also if the user is deleting text and goes back into the text that comes after the "@" how would you handle that? Something like going from "@userH |" => "@user|" where `|` is the cursor. – Uche Ozoemena Jul 31 '20 at 18:05
  • 1
    And you also would have to handle pastes... There are ton of contingencies which is why you probably don't see a lot of this online. The way I would do it is to fire the autocomplete after the "@". Once it's active, you can continue the autocomplete as they type until they either click off, or they hit space, enter, or some illegal char, whereupon the autocomplete exits. I would also suggest having a delay on the autocomplete if you're using AJAX lookups because someone typing very fast messes it up. – Colin G Aug 01 '20 at 14:56

1 Answers1

1

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.

  • can you please create a demo that shows that function in action? It doesn't work for me because setting the `innerHTML` of a `textarea` element doesn't actually update the text in the `textarea`. I also tried setting the `value` property but that doesn't parse HTML either. – Uche Ozoemena Aug 03 '20 at 08:50
  • Thanks for your suggestion! I updated it to reflect what I mentioned in my first comment about the `textarea` not working well for me. Using a `div` instead of `textarea` and reading its `textContent` instead of `value` is what actually worked for me. – Uche Ozoemena Aug 03 '20 at 12:59