6

I am trying to extract a single word from a content editable div at the position, when the mouse is clicked. For example:

Lorem ipsum dolor sit amet, cons|ectetur adipiscing elit. Cras vestibulum gravida tincidunt. Proin justo dolor, iaculis vulputate eleifend et, facilisis eu erat.*

Using the | to represent the caret, the function should return "consectetur".

My code:

window.onload = function () {
        document.getElementById("text-editor").onclick = function () {
            var caretPos = 0, containerEl = null, sel, range;
            if (window.getSelection) {
                sel = window.getSelection();
                if (sel.rangeCount) {
                    range = sel.getRangeAt(0);
                    if (range.commonAncestorContainer.parentNode == this) {
                        caretPos = range.endOffset;
                    }
                }
            } else if (document.selection && document.selection.createRange) {
                range = document.selection.createRange();
                if (range.parentElement() == this) {
                    var tempEl = document.createElement("span");
                    this.insertBefore(tempEl, this.firstChild);
                    var tempRange = range.duplicate();
                    tempRange.moveToElementText(tempEl);
                    tempRange.setEndPoint("EndToEnd", range);
                    caretPos = tempRange.text.length;
                }
            }
            var prevSpace, nextSpace, text = this.innerText;
            
            prevSpace = text.substring(0,caretPos).lastIndexOf(" ");
            nextSpace = text.indexOf(" ", caretPos + 1);
            nextSpace == -1 ? nextSpace = text.length - 1 : false;
            prevSpace++;
            console.log([prevSpace,caretPos,nextSpace].join("|"));
            var word = text.substring(prevSpace, nextSpace);
            //Removes punctuation and whitespace.
            var patt = new RegExp("([A-Za-z0-9']*)","g");
            word = patt.exec(word)[0];
            document.getElementById("current-word").innerHTML = word;
        };
    };

A function is bound to the mouse click event of the contenteditable div, which calculates the caret position and then finds the indexes of the preceding and following space characters (or beginning or end of the string altogether) and uses substring to determine the word. There is a quick regex match to remove punctuation and whitespace and we finally end up with the correct word.

This worked fine, when there was a single text node inside the contenteditable div, but as soon as I started dropping and other assorted tags, the part of the method that calculated the caret position stopped working, always calculating it to be 0. Is there a way to calculate the caret position in HTML like it did with the text?

If not, can anyone suggest an alternate method?

Dave Jarvis
  • 30,436
  • 41
  • 178
  • 315
Dan Prince
  • 29,491
  • 13
  • 89
  • 120
  • I asked a similar question and found another solution using Rangy: http://stackoverflow.com/questions/22076945/rangy-word-under-caret-again/22097787#22097787 – Sebastien Lorber Feb 28 '14 at 14:27

2 Answers2

17

You could use the new TextRange module of my Rangy library for this, although it's enormous overkill just for that one feature. Here's the code you'd need:

var sel = rangy.getSelection();
sel.expand("word");
var word = sel.text();
alert(word);

Otherwise, if you can live with no support for pre-Blink Opera (up to and including version 12) and Firefox < 4, you could use Selection.modify() (WebKit, Firefox) and the expand() method of TextRange (IE). Here's an example.

Demo: http://jsfiddle.net/timdown/dBgHn/1/

Code:

function getWord() {
    var sel, word = "";
    if (window.getSelection && (sel = window.getSelection()).modify) {
        var selectedRange = sel.getRangeAt(0);
        sel.collapseToStart();
        sel.modify("move", "backward", "word");
        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;
    }
    alert(word);
}
Dave Jarvis
  • 30,436
  • 41
  • 178
  • 315
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • @Tim Down - is there a way to change this function to return the positions of the word boundaries? The beginning and end of the word, as numbers. For example: checking `one tw|o three` would return: `[4,7]` – ragulka Jun 05 '13 at 06:38
  • @ragulka: Which version? Rangy or `Selection.modify()`? – Tim Down Jun 05 '13 at 10:32
  • @TimDown `Selection.modify` – ragulka Jun 05 '13 at 16:36
  • @ragulka: You could combine it with this function: http://stackoverflow.com/a/4812022/96100 – Tim Down Jun 07 '13 at 08:36
  • The docs state that the boundary for "word" is a white space character. But the expand doesn't seem to work for urls. Any ideas? – Adam Gotterer Dec 04 '13 at 22:09
  • @CrashRoX: `Selection.modify` or Rangy? If the former, which browser? – Tim Down Dec 04 '13 at 22:46
  • It's for a Chrome extension. I've played a little with modify but haven't had much luck. It's hard to tell which direction to modify from since you don't know how many "words" you need on either side of the mouse to complete the actual word. – Adam Gotterer Dec 04 '13 at 23:15
  • Magic! range.expand("word"); Also works in Chrome with regular Range objects. – Holtwick Dec 19 '13 at 16:45
  • @Holtwick: Yes. I've never been able to find any documentation for that. – Tim Down Dec 19 '13 at 17:13
  • @TimDown when I use your solution with the TextRange module it does work, but the caret is no longer there (I'm using this on an ), kind of similar to blurring the element (losing focus). – duality_ Mar 03 '14 at 09:54
  • on a textarea, it doesn't work really well, it goes for elements out the textarea, how can I fix that? – Aysennoussi Apr 12 '15 at 14:36
  • @Sekai: The task is easier in a textarea because you can simply use `selectionStart`, `selectionEnd` and `value` properties to reduce the problem to relatively simple string manipulation. However, Rangy only concerns itself with selections within regular content, not textareas and inputs. – Tim Down Apr 13 '15 at 09:27
  • Hey Tim, great answer as usual.. :) Just one problem here, if you look at this fiddle: http://jsfiddle.net/dBgHn/69/, if you click anywhere on the image, it still logs "there", i.e. the last word before the image. Is there no way to avoid this? – SexyBeast May 05 '17 at 23:13
  • 2
    And another weird thing, it recognizes special characters as delimiters, not just spaces. If a word starts with `@`, say like `@foo`, the word we get is `foo`, not `@foo`. – SexyBeast May 07 '17 at 15:04
0

BitBucket have released their Cursores.js library that does this, it's small and focused which is nice - http://cursores.bitbucket.org/

The only issue I have is that it doesn't pick up the token if there is no text to the left of the caret, for example "t|est" would work but "|test" wouldn't.

Kevin Ansfield
  • 2,343
  • 1
  • 19
  • 20