Search code examples
htmlcontenteditable

How to get <span> element when all spanned text selected?


In a content-editable div, I want to surround selected text with a <span>, or modify an existing <span> when I select all the text within it. I can do the first, but not the second.

In the second case, if I have A<span>B</span>C and I select B, the selection shows the anchorNode as A, the focusNode as C, and toString() returns B. The parent node is the enclosing paragraph, not the <span>. I cannot find any way to distinguish between selecting B in A<span>B</span>C and in ABC, since I can't find any way to discover the existence of the <span> element that surrounds B in the first case. There must be a way to do this, surely? Can someone tell me how to do this?

Here is the code:

var sel = window.getSelection();
var range = sel.getRangeAt(0);
var selected = range.compareBoundaryPoints(range.START_TO_END, range);
if (selected > 0) {    // some text is selected
  var el = range.commonAncestorContainer;
  //
  // would expect to find el == <span> if selection is B
  // in <p>A<span>B</span>C<p>, but find <p> instead!
  //
  //... code to surround selection with new span or to process
  //    existing span... but there is never an existing span!
}

Solution

  • I've come up with a rather horrible workaround for the above problem. To test if the selection is the whole of a <span> element, I clone the range, which, when it is the whole span, contains the span preceded and followed by empty strings. So what I do is to remove any of these, and check if what I'm left with is a single span element. If that's the case, I take the next element after the start of the original range, which will be the <span>:

    var el = range.commonAncestorContainer;
    var copy = range.cloneContents();
    while (copy.childNodes.length > 1 && copy.firstChild.nodeValue === "") {
      copy.removeChild(copy.firstChild);
    }
    while (copy.childNodes.length > 1 && copy.lastChild.nodeValue === "") {
      copy.removeChild(copy.lastChild);
    }
    if (copy.childNodes.length == 1 && copy.childNodes[0] instanceof HTMLSpanElement) {
      el = range.startContainer.nextSibling;
    }
    

    This works, but it's not very appealing. I'd be happy if anyone has a neater solution.