Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get word starting with special characters at caret in contenteditable div

I have a contenteditable div, and I need to know the word at the current caret position. I tried this solution, but the problem is, it doesn't recognize special characters like @ and ~. So if a word starts with ~, like ~fool, I am getting fool, whereas I expected ~fool. So I tried to modify the solution by taking into account that if after moving the selection back, the character encountered is not a space, I would continue moving backwards until a space is encountered. That would make the start of the selection. Similarly then I would keep moving forward until I find a space, and that would mark the end of selection. Then the selection would give me the word. To get the caret position, I used this solution. Combined, my code now looks like this:

function getCaretPosition(editableDiv) {
  var caretPos = 0,
    sel, range;
  if (window.getSelection) {
    sel = window.getSelection();
    if (sel.rangeCount) {
      range = sel.getRangeAt(0);
      if (range.commonAncestorContainer.parentNode == editableDiv) {
        caretPos = range.endOffset;
      }
    }
  } else if (document.selection && document.selection.createRange) {
    range = document.selection.createRange();
    if (range.parentElement() == editableDiv) {
      var tempEl = document.createElement("span");
      editableDiv.insertBefore(tempEl, editableDiv.firstChild);
      var tempRange = range.duplicate();
      tempRange.moveToElementText(tempEl);
      tempRange.setEndPoint("EndToEnd", range);
      caretPos = tempRange.text.length;
    }
  }
  return caretPos;
}

function getCurrentWord() {
    var sel, word = "";
    if (window.getSelection && (sel = window.getSelection()).modify) {
        var selectedRange = sel.getRangeAt(0);
        sel.collapseToStart();
        sel.modify("move", "backward", "word");

        while (sel.toString() != " " && getCaretPosition($("#editor").get(0)) != 0) {
            sel.modify("move", "backward", "character");
            (sel = window.getSelection()).modify;
        }
        sel.modify("move", "forward", "character");      
        sel.modify("extend", "forward", "word");
        word = sel.toString();

        // Restore selection
        sel.removeAllRanges();
        sel.addRange(selectedRange);
    } else if ((sel = document.selection) && sel.type != "Control") {
        var range = sel.createRange();
        range.collapse(true);
        range.expand("word");
        word = range.text;
    }
    return word;
}

$(function () {

    $(document).on('keyup keydown paste cut mouseup',"#editor", function () {
        var word = getCurrentWord();
        console.log(word);
    });
});

However this is not working at all. That is problem no. 1. Problem 2 is, even if there is an image in the pic and user clicks on the pic, the handler keeps returning the last word before the image, whereas I am expecting a blank string. Can anyone help me fix these two issues?

like image 590
SexyBeast Avatar asked May 07 '17 17:05

SexyBeast


2 Answers

I modified the getCurrentWord() function to use basic String methods to get the word from the caret position. The function accepts the element and position and returns the word at that position.

Below is the updated function.

function getCurrentWord(el, position) {
    // Get content of div
    var content = el.textContent;

    // Check if clicked at the end of word
    position = content[position] === ' ' ? position - 1 : position;

    // Get the start and end index
    var startPosition = content.lastIndexOf(' ', position);
    var endPosition = content.indexOf(' ', position);

    // Special cases
    startPosition = startPosition === content.length ? 0 : startPosition;
    endPosition = endPosition === -1 ? content.length : endPosition;

    return content.substring(startPosition + 1, endPosition);
}

The function first gets the content of the element. Next, it checks if the user has clicked at the end of a word, if yes, then subtract one from the position so that the indexOf and lastIndexOf will work correctly on space.

For start and end positions, there are two special cases which need to be handled. First, click on the last element. For this, the startPosition will be -1 since there could be no space after the last word.

Second, when clicking on the first word, the endPosition will be -1 because there could be no space before the first word.

These two conditions will work even if there is space before first character and space after the last character.

indexOf and lastIndexOf are used to find the space before and after word and using those indexes substring will give the word at that position.

Here's live demo to test.

$(function() {
  function getCaretPosition(editableDiv) {
    var caretPos = 0,
      sel, range;
    if (window.getSelection) {
      sel = window.getSelection();
      if (sel.rangeCount) {
        range = sel.getRangeAt(0);
        if (range.commonAncestorContainer.parentNode == editableDiv) {
          caretPos = range.endOffset;
        }
      }
    } else if (document.selection && document.selection.createRange) {
      range = document.selection.createRange();
      if (range.parentElement() == editableDiv) {
        var tempEl = document.createElement("span");
        editableDiv.insertBefore(tempEl, editableDiv.firstChild);
        var tempRange = range.duplicate();
        tempRange.moveToElementText(tempEl);
        tempRange.setEndPoint("EndToEnd", range);
        caretPos = tempRange.text.length;
      }
    }
    return caretPos;
  }

  function getCurrentWord(el, position) {
    var word = '';

    // Get content of div
    var content = el.textContent;

    // Check if clicked at the end of word
    position = content[position] === ' ' ? position - 1 : position;

    // Get the start and end index
    var startPosition = content.lastIndexOf(' ', position);
    startPosition = startPosition === content.length ? 0 : startPosition;
    var endPosition = content.indexOf(' ', position);
    endPosition = endPosition === -1 ? content.length : endPosition;

    return content.substring(startPosition + 1, endPosition);
  }


  $('#editor').on('keyup keydown paste cut mouseup', function() {
    var caretPosition = getCaretPosition(this);
    var word = getCurrentWord(this, caretPosition);
    console.log(word);
  });
});
div {
  font-size: 18px;
  line-height: 1.5em;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="editor" contenteditable>



  Lorem !p$um dolor $!t @met, con$ectetur @d!p!$!c!ng el!t, $ed do e!u$mod tempor !nc!d!dunt ut l@bore et dolore m@gn@ @l!qu@. Ut en!m @d m!n!m ven!@m, qu!$ no$trud exerc!t@t!on ull@mco l@bor!$ n!$! ut @l!qu!p ex e@ commodo con$equ@t. Du!$ @ute !rure dolor
  !n reprehender!t !n volupt@te vel!t e$$e c!llum dolore eu fug!@t null@ p@r!@tur.
</div>
like image 139
Tushar Avatar answered Oct 21 '22 10:10

Tushar


Here's a pretty barebones example. There's a div with example text:

<div contenteditable onclick="getCaretCharacterOffsetWithin(this)">some ~test content</div>

Here's the script. When we click on the div, wherever the cursor lands we get the current position and spit out the word we're in. This word can include special characters and is only space delineated.

function getCaretCharacterOffsetWithin(element) {
    var caretOffset = 0;
    var doc = element.ownerDocument || element.document;
    var win = doc.defaultView || doc.parentWindow;
    var sel;
    if (typeof win.getSelection != "undefined") {
        sel = win.getSelection();
        if (sel.rangeCount > 0) {
            var range = win.getSelection().getRangeAt(0);
            var preCaretRange = range.cloneRange();
            preCaretRange.selectNodeContents(element);
            preCaretRange.setEnd(range.endContainer, range.endOffset);
            caretOffset = preCaretRange.toString().length;
        }
    } else if ( (sel = doc.selection) && sel.type != "Control") {
        var textRange = sel.createRange();
        var preCaretTextRange = doc.body.createTextRange();
        preCaretTextRange.moveToElementText(element);
        preCaretTextRange.setEndPoint("EndToEnd", textRange);
        caretOffset = preCaretTextRange.text.length;
    }
  console.log('caretOffset', caretOffset);
  word = getWordAtPosition(caretOffset, element);
  console.log('word', word);
    return caretOffset;
}

function getWordAtPosition(position, element) {
  var total_text = element.innerHTML;
  var current_word = "";
  var i = 0;
  var word_found = false;
  while(i < total_text.length) {
    if(total_text[i] != ' ')
      current_word += total_text[i];
    else if(word_found)
      return current_word;
    else
      current_word = "";
    if(i == position)
      word_found = true;
    i++;
  }
  return current_word;
}

Heres an example of it working:

https://codepen.io/anon/pen/dWdyLV

like image 2
Bryant James Avatar answered Oct 21 '22 10:10

Bryant James