Wrox Home  
Search
Professional JavaScript for Web Developers
by Nicholas C. Zakas
April 2005, Paperback


Complex selection in DOM ranges

Creating complex ranges requires the use of range setStart() and setEnd() methods. Both methods accept two arguments: a reference node and an offset. For setStart(), the reference node becomes the startContainer, and the offset becomes the startOffset; for setEnd(), the reference node becomes the endContainer, and the offset becomes the endOffset.

Using these methods, it is possible to mimic selectNode() and selectNodeContents(). For example, the useRanges() function in the previous example can be rewritten using setStart() and setEnd():

function useRanges() {
    var oRange1 = document.createRange();
    var oRange2 = document.createRange();
    var oP1 = document.getElementById("p1");
    var iP1Index = -1;
    for (var i=0; i < oP1.parentNode.childNodes.length; i++) {
        if (oP1.parentNode.childNodes[i] == oP1) {
            iP1Index = i;
            break;
        }
    }
                
    oRange1.setStart(oP1.parentNode, iP1Index);
    oRange1.setEnd(oP1.parentNode, iP1Index + 1);
    oRange2.setStart(oP1, 0);
    oRange2.setEnd(oP1, oP1.childNodes.length);
    //textbox assignments here
}

Note that to select the node (using oRange1), you must first determine the index of the given node (oP1) in its parent node's childNodes collection. To select the node contents (using oRange2), no calculations are necessary. But you already know easier ways to select the node and node contents; the real power here is to be able to select only parts of nodes.

Recall the very first example mentioned in this section, selecting llo from Hello and Wo from World in the HTML code <p id="p1"><b>Hello</b> World</p>. Using setStart() and setEnd(), this is quite easy to accomplish.

The first step in the process is to get references to the text nodes containing Hello and World using the regular DOM methods:

var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;

The Hello text node is actually a grandchild of <p/> because it's apparently <b/>, so you can use oP1.firstChild to get <b/> and oP1.firstChild.firstChild to get the text node. The World text node is the second (and the last) child of <p/>, so you can use oP1.lastChild to retrieve it.

Next, create the range and set the appropriate offsets:

var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);

For setStart(), the offset is 2, because the first l in Hello is in position 2 (starting from H in position 0). For setEnd(), the offset is 3, indicating the first character that should not be selected, which is r in position 3. (There is actually a space in position 0. See Figure 3.)

JavaScript DOM Ranges : Figure 3
Figure 3

Because both oHello and oWorld are text nodes, they become the startContainer and endContainer for the range so that the startOffset and endOffset accurately look at the text contained within each node instead of looking for child nodes, which is what happens when an element is passed in. The commonAncestorContainer is the <p/> element, which is the first ancestor that contains both nodes.

Of course, just selecting sections of the document isn't very useful unless you can interact with the selection.

There is a bug in Mozilla's implementation of the DOM Range (bug #135928) that causes an error to occur when you try to use setStart() and setEnd() with the same text node. This bug has been resolved and this fix is included in a future Mozilla release.

Interacting with DOM range content

When a range is created, internally it creates a document fragment node onto which all the nodes in the selection are attached. Before this can happen, however, the range must make sure that the selection is well-formed.

You just learned that it is possible to select the entire area from the first letter l in Hello to the o in World, including the </b> end tag (see Figure 4). This would be impossible using the normal DOM methods described in the book Professional JavaScript for Web Developers (Wrox, 2005, ISBN: 0-7645-7908-8).

JavaScript DOM Ranges : Figure 4
Figure 4

The reason a range can get away with this trick is that it recognizes missing opening and closing tags. In the previous example, the range calculates that a <b> start tag is missing inside the selection, so the range dynamically adds it behind the scenes, along with a new </b> end tag to enclose He, thus altering the DOM to the following:

<p><b>He</b><b>llo</b> World</p>

The document fragment contained within the range is displayed in Figure 5.

JavaScript DOM Ranges : Figure 5
Figure 5

With the document fragment created, you can manipulate the contents of the range using a variety of methods.

The first method is the simplest to understand and use: deleteContents(). This method simply deletes the contents of the range from the document. In the previous example, calling deleteContents() on the range leaves this HTML in the page:

<p><b>He</b>rld</p>

Because the entire document fragment is removed, the range is kind enough to place the missing </b> tag into the document so it remains well-formed.

extractContents() is similar to deleteContents(). It also removes the range selection from the document and returns the range's document fragment as the function value. This allows you to insert the contents of the range somewhere else:

var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
var oFragment = oRange.extractContents();
document.body.appendChild(oFragment);

In this example, the fragment is extracted and added to the end of the document's <body/> element (remember, when a document fragment is passed into appendChild(), only the fragment's children are added, not the fragment itself). What you see in this example is the code <b>He</b>rld at the top of the page, and <b>llo</b> Wo at the bottom of the page.

Another option is to leave the fragment in place, but create a clone of it that can be inserted elsewhere in the document by using cloneContents():

var oP1 = document.getElementById("p1");
var oHello = oP1.firstChild.firstChild;
var oWorld = oP1.lastChild;
var oRange = document.createRange();
oRange.setStart(oHello, 2);
oRange.setEnd(oWorld, 3);
var oFragment = oRange.cloneContents();
document.body.appendChild(oFragment);

This method is very similar to deleteContents() because both return the range's document fragment. This results in <b>llo</> Wo being added to the end of the page; the original HTML code remains intact.

The document fragment and accompanying changes to the range selection do not happen until one of these methods is called. The original HTML remains intact right up until that point.