rangy: perform smallest possible expansion so that canSurroundContents() = true

StackOverflow https://stackoverflow.com/questions/21309701

  •  01-10-2022
  •  | 
  •  

Question

I'm trying to write a helperfunction that expands the selection to the smallest possible range for which range.canSurroundContents() === true.

Before embarking on this, is there any method in the Rangy library that does this that I'm overlooking?

Was it helpful?

Solution

This was the problem specified in the question:

I'm trying to write a helperfunction that expands the selection to the smallest possible range for which range.canSurroundContents() === true.

Here's a solution:

// Provides the depth of ``descendant`` relative to ``ancestor``
function getDepth(ancestor, descendant) {
    var ret = 0;
    while (descendant !== ancestor) {
        ret++;
        descendant = descendant.parentNode;
        if (!descendant)
            throw new Error("not a descendant of ancestor!");
    }
    return ret;
}


function smallestExpansion() {
    var range = rangy.getSelection().getRangeAt(0);
    if (range.canSurroundContents()) return;

    var common = range.commonAncestorContainer;
    var start_depth = getDepth(common, range.startContainer);
    var end_depth = getDepth(common, range.endContainer);

    while(!range.canSurroundContents()) {
        // In the following branches, we cannot just decrement the depth variables because the setStartBefore/setEndAfter may move the start or end of the range more than one level relative to ``common``. So we need to recompute the depth.
        if (start_depth > end_depth) {
            range.setStartBefore(range.startContainer.parentNode);
            start_depth = getDepth(common, range.startContainer);
        }
        else {
            range.setEndAfter(range.endContainer.parentNode);
            end_depth = getDepth(common, range.endContainer);
        }
    }
}

Here's a fiddle to illustrate. I've ignored the case where there a selection may have more than one range.

Update: The first code I posted was pessimistic in its approach in that it went for the common ancestor too quickly. The updated code gradually moves the ends of the range to the parent elements until the selection is such that a Node could surround the contents of the range (i.e. range.canSurroundContents() is true). So in a case like this:

<p>This i|s a <strong>te|st</strong> for selecting text.</p>

where the | symbols mark the start and end of the initial region. The updated code will end with a region covering s a <strong>test</strong>, which is just enough to satisfy the requirement (you could wrap the region in a <span> element if you wanted to). The old version would have gone for selecting all of the paragraph.

I believe it would be possible to avoid calling getDepth on each iteration by considering how setStartBefore and setEndAfter move the ends of the range but I've not bothered with this optimization.

OTHER TIPS

Working solution finds Least Common Ancestor, and works backwards to try and make it smaller.

var sel = rangy.getSelection(),
    range = sel.getRangeAt(0),
    startNode = range.startContainer,
    endNode = range.endContainer,
    commonAncestor = getCommonAncestor(range);
    range = getSmallestPossibleExpansion(range, commonAncestor, startNode, endNode);

function getRangeContainerElement(range) {
  var container = range.commonAncestorContainer;
  if (container.nodeType == 3) {
    container = container.parentNode;
  }
  return container;
}

function getChildOfAncestor(node, ancestor) {
  if (node.parentNode === ancestor) return node;
  return getChildOfAncestor(node.parentNode, ancestor);
}


function getSmallestPossibleExpansion(range, commonAncestor, startNode, endNode) {
  if (startNode === endNode) {
    if (!range.canSurroundContents()) {
      throw new Error("Sanity Check: startNode = endNOde. This should mean canSurroundContents() == true!");
    }
  } else {
    if (commonAncestor !== endNode) {
      //expand range of endpoint (to right) by including the 'anscestorOrSelf' from endNode that is the child of commonAncestor
      range.setEndAfter(getChildOfAncestor(endNode, commonAncestor));
    }
    if (commonAncestor !== startNode) { //NOTE: NOT ELSE IF!
      //expand range of startNode (to left) by including the 'anscestorOrSelf' from startNode that is the child of commonAncestor
      range.setStartBefore(getChildOfAncestor(startNode, commonAncestor));
    }
  }
  return range;
}

Sweet.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top