5

In Javascript, I'd like determine whether an element, say an A element, exists inside a given range/textRange. The aim is to determine if the user's current selection contains a link. I am building a rich text editor control.

The range object has a commonAncestorContainer (W3C) or parentElement() (Microsoft) method which returns the closest common anscestor of all elements in the range. However, looking inside this element for A elements won't work, since this common ancestor may also contain elements that aren't in the range, since the range can start or end part way through a parent.

How would you achieve this?

thomasrutter
  • 114,488
  • 30
  • 148
  • 167

5 Answers5

5

How about selection.containsNode? https://developer.mozilla.org/en/DOM/Selection/containsNode

something like:

var selection = window.getSelection();
var range = selection.getRangeAt(0);
var result = $('a', range.commonAncestorContainer).filter(function() {
  return selection.containsNode(this);
});
console.log(result);
sharp johnny
  • 814
  • 6
  • 16
  • This is good, but requires jQuery and won't work in any version of IE. Not sure how long WebKit's had `containsNode()`, but current versions support it. – Tim Down May 19 '11 at 09:24
  • containsNode() seems perfect, thanks, but is not cross-browser. Now I just need a Microsoft-equivalent (and maybe something for older Chrome/Safari?) – thomasrutter May 19 '11 at 10:10
  • I can't find any Microsoft (or Opera?) equivalent to containsNode() so I am going to have to go with some other solution. At the moment it's a messy one involving compareEndPoints/compareBoundaryPoints... – thomasrutter May 20 '11 at 04:07
  • @thomasrutter: That's pretty much your only option. It's going to be messy whatever. – Tim Down May 21 '11 at 11:24
3

I ended up going with a solution like this:

        var findinselection = function(tagname, container) {
            var
                i, len, el,
                rng = getrange(),
                comprng,
                selparent;
            if (rng) {
                selparent = rng.commonAncestorContainer || rng.parentElement();
                // Look for an element *around* the selected range
                for (el = selparent; el !== container; el = el.parentNode) {
                    if (el.tagName && el.tagName.toLowerCase() === tagname) {
                        return el;
                    }
                }
                // Look for an element *within* the selected range
                if (!rng.collapsed && (rng.text === undefined || rng.text) &&
                    selparent.getElementsByTagName) {
                    el = selparent.getElementsByTagName(tagname);
                    comprng = document.createRange ?
                        document.createRange() : document.body.createTextRange();
                    for (i = 0, len = el.length; i < len; i++) {

                        // determine if element el[i] is within the range
                        if (document.createRange) { // w3c
                            comprng.selectNodeContents(el[i]);
                            if (rng.compareBoundaryPoints(Range.END_TO_START, comprng) < 0 &&
                                rng.compareBoundaryPoints(Range.START_TO_END, comprng) > 0) {
                                return el[i];
                            }
                        }
                        else { // microsoft
                            comprng.moveToElementText(el[i]);
                            if (rng.compareEndPoints("StartToEnd", comprng) < 0 &&
                                rng.compareEndPoints("EndToStart", comprng) > 0) {
                                return el[i];
                            }
                        }
                    }
                }
            }
        };

Where getrange() is another function of mine to get the current selection as a range object.

To use, call it like

var link = findselection('a', editor);

Where editor is the contenteditable element, or body in a designmode iframe.

thomasrutter
  • 114,488
  • 30
  • 148
  • 167
  • That looks good to me. It's pretty much the same as what I've done here: http://stackoverflow.com/questions/5801347/how-to-get-selection-inside-a-div-using-jquery-javascript/5801903#5801903 – Tim Down Jun 06 '11 at 00:06
2

This is a bit of a pain in the bum to do cross-browser. You could use my Rangy library, which is probably overkill for just this task but does make it more straightforward and works in all major browsers. The following code assumes only one Range is selected:

var sel = rangy.getSelection();
if (sel.rangeCount) {
    var range = sel.getRangeAt(0);
    var links = range.getNodes([1], function(node) {
        return node.tagName.toLowerCase() == "a" && range.containsNode(node);
    });
}
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • Thanks for pointing out your rangy library. My word, it's huge! – thomasrutter May 20 '11 at 00:26
  • @thomasrutter: Yeah, it's a bit larger than I'd like and I think it puts people off. I will try and reduce it and maybe compress it more aggressively, but to my taste there isn't actually much waste. – Tim Down May 20 '11 at 08:30
  • I've been working on selection and range stuff 2 days in a row, trying to do something similar to the cssClassApplier module... Thanks for the link, this lib is really helpful. – Jérémie Parker May 08 '12 at 19:16
  • rangy seem an overkill to just knowing the selection `start`, `end` and the `text`. could you make it tiny? – Ari May 10 '16 at 01:23
0

I'm using this code that works with IE / Chrome / FF: (I'm using it to select rows <tr> in a table)

// yourLink is the DOM element you want to check
var selection = window.getSelection().getRangeAt(0)
var node = document.createRange()
node.selectNode(yourLink)
var s2s = selection.compareBoundaryPoints(Range.START_TO_END, node)
var s2e = selection.compareBoundaryPoints(Range.START_TO_START, node)
var e2s = selection.compareBoundaryPoints(Range.END_TO_START, node)
var e2e = selection.compareBoundaryPoints(Range.END_TO_END, node)
if ((s2s != s2e) || (e2s != e2e) || (s2s!=e2e))
    console.log("your node is inside selection")
Supersharp
  • 29,002
  • 9
  • 92
  • 134
0

In the case of the range on the searched element, what I wrote is useful. (But only for that case!)

First I wrote a function that returns the Node found in the range: getNodeFromRange(rangeObject). Using this function it was already easy to write the function that returns the desired Node: findTagInRange(tagName).

function getNodeFromRange(range) {
    if(range.endContainer.nodeType==Node.ELEMENT_NODE) {
        return range.endContainer;
    }
    if(range.endContainer.nodeType==Node.TEXT_NODE) {
        return range.endContainer.parentNode;
    }
    else {
        // the 'startContainer' it isn't on an Element (<p>, <div>, etc...)
        return;
    }
}

function findTagInRange(tagName, range) {
    var node = getNodeFromRange(range);
    if(node && typeof(node.tagName)!='undefiend' && node.tagName.toLowerCase()==tagName.toLowerCase()) {
        return $(node);
    }
    return;
}

And then I can use it as follows:

var link = findTagInRange('A', range);

And I see the determination of the range you've already solved. :)