Using the range object in Mozilla
In this column on the DOM Range Object I will be discussing the application of the Range object with JavaScript for HTML and XML documents. I have found that the Range object is possibly the singularly most powerful object given to us by the W3C DOM Level 2 Recommendation. The only drawback is that no browser currently fully supports it. At least, until now that is. [Article written by Jeffrey M. Yates 26 March 2001]
Introduction
The Range object is a standard Document Object Model object described by the World Wide Web Consortium’s Document Object Model Level 2 Transversal and Range Specification recommendation. Those browsers based upon Mozilla’s code (the Gecko engine) currently implement the Range object. These browsers include but are not limited to, Mozilla, Netscape 6.x, K-Mellon, Galeon and possibly many more.
One of the projects that I have taken on is to write a WYSIWYG (What You See Is What You Get) Rich Text Editor that is cross-browser compatible. In order to do this I have to be able to accept input from the user of what text they have selected via the mouse, as well as a method of selecting text via script for keyboard navigation. In order to do this I needed a working Range object (Microsoft’s version of this is the TextRange object).
Notice: Since the writing of this article it has been found that the Range object has an additional bug. All of the comparison operations of a Range object fail if the range is selecting the contents of an attribute node.
From what I have been able to find out the problem lies with the parentNode property of the children of the Attr node. The parentNode property is always returning null. Until this is corrected I cannot “Patch” Mozilla’s Range object to compensate for this.
This means that the Range object is useless for implementations dealing with attributes, such as the text within an input element or textarea element.
Target audience
This article is for intermediate and advance level JavaScript programmers. The reader is assumed to understand basic JavaScript programming language (statements, syntax, operators, object inheritance, ect.) and the W3C DOM Level 2 Recommendation. Some of the content that goes into advanced programming techniques I will explain or refer the reader to other online resources that does. I suggest all readers to look though the W3C’s Document Object Model Range document if you are not familiar with the Range Object recommendation.
The Range Object
A Range represents a “selection” of a portion of a web document or a document fragment. This range is described as all of the content between two boundary-points, the start and end boundary-points. The range object was created to facilitate editing operations to the DOM, such as cutting, copying, pasting and deleting of content.
Creating an instance of a Range object
The Range object, like most DOM objects, does not have a directly accessible constructor. Instead, the constructor is a method of other DOM objects. In this case (like many other’s) it is a method attached to the Document object. This method is the createRange( ) method.
- Document.createRange() – This method is used to create an instance of a Range object. This method returns a non-initialized Range object. Note: Other than the setting methods for a Range object described below, most (if not all) methods of a range object cannot be used UNTIL both the start and end boundary-points have been defined.
Properties of a Range object
A range is defined by five read-only properties of the Range object: startContainer, endContainer, startOffset, endOffset and the commonAncestorContainer property, and one “state” property, the collapsed property.
- startContainer: This property is a reference to a Node within the document or document fragment. The start of the range is contained within this node and the boundary point is determined by the startOffset property.
- endContainer: This property is a reference to a Node within the document or document fragment. The end of the range is contained within this node and the boundary point is determined by the endOffset property.
- startOffset: This property has one of two meanings, depending upon the type of node that the startContainer references. If the startContainer is a Text node, Comment node or a CDATASection node then this offset represents the number of characters from the beginning of the node to where the boundary point lies. Any other type of node and it represents the child node index number that the boundary point lies BEFORE. If the offset is equal to the number of child nodes then the boundary point lies after the last child node.
- endOffset: This property has one of two meanings, depending upon the type of node that the endContainer references. If the endContainer is a Text node, Comment node or a CDATASection node then this offset represents the number of characters from the beginning of the node to where the boundary point lies. Any other type of node and it represents the child node index number that the boundary point lies BEFORE. If the offset is equal to the number of child nodes then the boundary point lies after the last child node.
- commonAncestorContainer: This property references the first ancestor that contains both the startContainer node and the endContainer node.
- collapsed: This read-only property indicates that both of the range boundary-points are at the same point in the DOM. Thus collapsed returns true if the startContainer is the same node as the endContainer, and both the startOffset and endOffset have the same value.
It is important to realize that the range boundary points respond automatically to the mutation of the range contents. An example would be if the start container were removed from the DOM. When this happens the start boundary-point would be shifted to AFTER where the start container lied within the DOM before it’s removal. The mutations that the Range object compensates for are Node deletions, insertions and CharacterData modifications. For more information read Range modification under document mutation.
Visual Representation Of A Range
So far I have been talking about a range as if it something concrete instead of a concept. It is not concrete, but it can be visualized in a concrete manor. There are two methods of visually describing a range, a markup “view” and a Node “view”. Most of the time I will be discussing ranges using the markup view but it is important to note that the range is ALWAYS dealing with the Node view. For a side-by-side comparison of the two I have reproduced the graphic provided in the W3C DOM Level 2 Range Recommendation here.
The Markup View of a Range
Since a range marks all of the markup between two boundary-points in the DOM it is convenient to show this range as within the markup that the range is contained within. I will show this visually by making the markup that the range selects bold as follows:
<BODY><H1>Title</H1><P>Blah xyz.</P></BODY>
In this markup the following text is highlighted: “tle
Bl”. This represents the range. Its startContainer would be the first child node of the H1 element with a startOffset of 2. Its endContainer will be the first child node of the P element with an offset of 2. Its commonAncestorContainer will be the BODY element. I will now walk you though why this is.
The range starts within the text contained within the H1 element. Since the text is only partially selected the start container CAN NOT BE the H1 element itself. Since the text is represented in DOM as a text node the start container is this text node. Since this text node is the first (and in this case only) child node of the H1 element, the start container is the first child node of the H1 element. Notice that the range starts with the third character of this text node. Thus the offset for the start of the range is after the second character but before the third character, thus the offset is 2. You can follow this same logic for the end container and offset.
The common ancestor container of the range is the first element that is a parent of both the start container and the end container. From the start container (a text node) it’s first parent node is the H1 element. This element is only partially selected AND does not contain the end of the range. This being the case it cannot be the ancestor container. The parent node of the H1 element is the BODY element. Following the same logic it is found that the end container also has the BODY element as its parent, thus the BODY element is the common ancestor container.
As you can see, viewing the range in a markup “view” is rather convenient when using a word processor. The Node view on the other hand requires the use of graphics. In this article I will not be using the Node view for this reason.
Manipulating the Range Boundary-Points
Since the properties of a Range object are read-only it would be a useless object if the programmer could not manipulate what part of the DOM that the range selects. Since above I have stated that this is a most powerful object then this must not be a limitation of this object. The Range Recommendation has provided the Range object with a number of methods for manipulating it.
Methods for setting the Range
There are nine functions defined by the W3C that are to be used for selecting the boundary points of a range.
- setStart( refNode, offset ) – Sets the startContainer to the supplied refNode node with an offset of offset.
- setEnd( refNode, offset ) – Sets the endContainer to the supplied refNode node with an offset of offset.
- setStartBefore( refNode ) – Sets the startContainer to the parent of the supplied refNode node with an offset that corrisponds to the child index number of the supplied refNode node.
- setStartAfter( refNode ) – Sets the endContainer to the parent of the supplied refNode node with an offset that corrisponds to the one greater than the child index number of the supplied refNode node.
- setEndBefore( refNode ) – Sets the endContainer to the parent of the supplied refNode node with an offset that corrisponds to the child index number of the supplied refNode node.
- setEndAfter( refNode ) – Sets the startContainer to the parent of the supplied refNode node with an offset that corrisponds to the one greater than the child index number of the supplied refNode node.
- selectNode( refNode ) – Sets the startContainer to the parent of the supplied refNode node with an offset that corrisponds to the child index number of the supplied refNode node and sets the endContainer to the parent of the supplied refNode node with an offset that corrisponds to the one greater than the child index number of the supplied refNode node.
- selectNodeContents( refNode ) – Sets the startContainer and endContainer to the supplied refNode node with a startOffset of 0 and an endOffset of the number of child nodes the node contains or the number of characters that the node contains.
- collapse( toStart ) – Sets the start and end container to the same node and the start and end offsets to the same value. If the boolean value toStart is true then the end container and offset are set to equal the start container and offset respectivally. If the boolean value toStart is fals then the start container and offset are set to equal the end container and offset respectivally. This allows you to programatically “collapse” the range to a singe point within the DOM.
When manipulating the boundary points it is important that the programmer keeps in mind if you attempt to place the starting boundary point AFTER the end boundary point an error will be thrown. The same is true if trying to set the end boundary point before the start boundary point. This cannot be stressed enough. It has caused me numerous errors in my coding until this sunk in.
The Editing Methods of the Range Object
After selecting a fragment of the DOM the Range object allows you to manipulate the contents of the DOM that the range selects. The operations that can be accomplished loosely correspond to deletion, cutting, copying, inserting and surrounding the contents with another node. Even though these operations sound the same as text editors such as FrontPage, they are not the same. More on the differences as I discuss each operation.
The Range object has five methods for manipulating the contents of a range: deleteContents, extractContents, cloneContents, insertNode and surroundContents. Each of these methods are described below.
- extractContents( ) – This method is used to remove the contents of the range from the DOM and return it inside of a document fragement. Only fully selected nodes are removed. Partially selected character data nodes are split and the portion of the node that is part of the range is fully selected. Partially selected nodes are cloned. Below is an example of extracting a ranges contents. Note: bold font indicates the range.
The DOM before the range extraction: <BODY><H1>Title</H1><P>Blah xyz.</p></body>
The DOM after the range extraction: <BODY><P id=”first”>Ti</P><P id=”second”>ah xyz.</P></BODY>
The document fragment returned: <BODY><P id=”first”>tle</P><P id=”second”>ah xyz.</P></BODY>
Notice how, after the range is extracted, you still have two P nodes. The contents of the partially selected nodes are NOT combined. This is important.
- deleteContents( ) – This method is the same as the extractContents method with the exception that the contents are not returned by the function.
- cloneContents( ) – This method returns a document fragment that would contain the same values as the extrectContents method with the exception that all of the nodes are clones of the nodes in the DOM and the origional nodes are not removed.
- insertNode( newNode ) – This method inserts the provided newNode at the start boundary-point described by the startContainer and startOffset properties. If the start boundary-point is within a text node (not just a character data node) the text node is split and the new node is inserted between them. From my interpretation of the recommendation the range start boundary is shifted to the before the newly inserted node.
- surroundContents( newParent ) – This method moves all of the content of the range into a new node and that new node is then inserted at the start boundary-point. This operation would be equivalent to newParent.appendChild(Range.extractContent()); Range.insertNode(newParent); After the surroundContent method is completed the range boundary points are shifted to fully select the supplied newParent node.
Miscellaneous Range Methods
There are a few methods that do not fall under the broad categories above. I will discuss each below.
- compareBoundaryPoints( how, sourceRange ) – This method compairs the boundary points of the current range (the Range object that the method is called from) to the supplied sourceRange range. The how argument is used to tell the method how to do the comparison. The values that the how argument can take are Range.START_TO_START, Range.START_TO_END, Range.END_TO_END and Range.END_TO_START. This argument returns a -1 for before, 0 for equal and 1 for after. The compairison is the sourceRange vs the current range, so START_TO_END means compare the start of the sourceRange to the end of the current range. I feel that they defined this backwards, but that is the way it is defined.
- cloneRange( ) – This method returns a Range object that has the same boundary points as the current range. If you change the boundary points of either range (the origional or the clone) it is NOT reflected in the other. These two objects are totally independent of each other.
- detach( ) – This method releases the DOM from keeping track of the range’s boundary points. This method has been included because the browser overhead for keeping track of the mutation of the DOM and updating the Ranges boundary points can become quite large when dealing with multiple ranges. It is good programing practice to detach the range when you are done using it.
- toString( ) – This method returns the text only values of the range. This text does not include any markup.
Mozilla’s Extensions to the Range Object
Every implementation of W3C’s Document Object Model will include their own extensions to the implementation. The reason why is that the programmers find that in order to get the job done they need more than what the Recommendation calls for. In this section I will list Mozilla’s extension to the Range object. I have not been able to find any documentation on these methods. The following is what I have been able to figure out by testing purposes only.
- createContextualFragment( fragmentString ) – This method calls the Mozilla parser and parses the code (the fragmentString argument) and returns the parsed code inside of a document fragment.
- isValidFragment( fragmentString ) – This method calls the Mozilla parser to determine if the fragmentString argument contains valid code. If it is then the method returns true, else it returns false.
- isPointInRange( refNode, offset ) – This method returns true if the point described by the refNode node and an offset within the node falls between or equal to the boundary-points of the range.
- comparePoint( refNode, offset) – This method returns –1, 0 or 1 depending on if the point described by the refNode node and an offset within the node is before, same as, or after the range respectively.
- intersectsNode( refNode ) – This method returns true if the refNode node is fully or partially selected by the range, false if not.
- compareNode(in Node n) – This method is used for reporting HOW the node intersects the range. If the node starts before the range and ends before or inside the range then compareNode returns Range.NODE_BEFORE (the integer 0). If the node starts inside or after the range and it’s end is after the range then compareNode returns Range.NODE_AFTER (the integer 1). If the node starts before the range and ends after the range then compareNode returns Range.NODE_BEFORE_AND_AFTER (the integer 2). If the node is completely selected by the range than compareNode returns Range.NODE_INSIDE (the integer 3).
Making Mozilla’s Implementation of the Range Object Work
At the time of this writing (March 23, 2001) the current build of Mozilla (build 2001031604) does not implement all of the methods of the Range object correctly, or at all. The methods effected are extractContents (bug #58969), cloneContents (bug #58970), insertNode (bug #58972), surroundContents (bug #58974) and deleteContents (bug #43535). These bugs also are in Netscape 6 and 6.01 since this browser is based upon the Mozilla browser’s code.
Related Objects to the Range Object
There is one object that is highly related to the Range object. This object is the Selection object. This object represents the graphically displayed selection within the browser. This selection can be set via the mouse by the user, or it can be set programmatically via JavaScript. Either way it shows up within the browser as highlighted content.
How is this object related to the Range object? It is related because its methods take the Range object as it’s arguments and uses Range object’s as it’s return values. Where the Range object is a powerful tool, the Selection object is the handle to the tool. Below is a quick breakdown of the properties and methods of the Selection object.
Below is a listing of the properties and methods of the Selection object in Mozilla based browsers. I have not found any actual documentation on this object so what you find below is what I have been able to find out experimentally. As I find out more I will update the descriptions below.
- Window.getSelection( ) – This method of the Window object returns the window?s Selection object. The user cannot create nor destroy selection objects. They can only get a reference to the window?s instance of the Selection object.
- Selection.anchorNode – This property is a reference to the Node that the currently active selection starts in. This is equivalent to the startContainer property of the Range object.
- Selection.anchorOffset – This property is the offset within the anchorNode node that the active selection starts at. This is equivalent to the startOffset property of the Range object.
- Selection.focusNode – This property is a reference to the Node that the currently active selection ends in. This is equivalent to the endContainer property of the Range object.
- Selection.focusOffset – This property is the offset within the focusNode node that the active selection ends at. This is equivalent to the endOffset property of the Range object.
- Selection.isCollapsed – This Boolean value indicates that the current selection is collapsed to a single point.
- Selection.rangeCount – This property contains a count of the number of Range objects make up the selection. In the western world the user can only make one selection at a time, but programmatically more than one can be selected at a time.
- Selection.getRangeAt( index ) – This method is used to retrieve one of the Range objects being used to represent the current selection. The value of index must be between 0 and Selection.rangeCount-1.
- Selection.collapse( parentNode, offset ) – This method is used to collapse the active selection to a single point described by the parentNode node and an offset within this node.
- Selection.extend( parentNode, offset ) – This method moves the end of the selection to the indicated parentNode node at the given offset within that node.
- Selection.collapseToStart( ) – This method is used to collapse the selection to a single point at the beginning of the selection.
- Selection.collpaseToEnd( ) – This method is used to collapse the selection to a single point at the end of the selection.
- Selection.containsNode( node, recursive ) – This method is used to test to see if a node is located within the current selection. The Boolean argument ?recursive? use is unknown to me at this time. If the node is found to be within the selection true is returned, else false is returned.
- Selection.selectAllChildren( parentNode ) – This method is used to make the currently active selection contain the contents of the provided parentNode node.
- Selection.addRange( range ) -This method is used to add a range object to the active selection. Even though a user cannot make more than one range active at a time, you can do so using this method.
- Selection.removeRange( range ) – This method removes the range from the current selection. Its contents are not removed from the document.
- Selection.removeAllRanges( ) – This method removes all ranges from the current selection. This effectively makes the selection to ?nothing is selected?.
- Selection.deleteFromDocument( ) -This method removes all of the selection?s contents from the document.
Wrap-up
By this time I am hopeful that I have convinced you of the power of the Range object. With the power of this object in the hands of web designers the web will be opened up to an age of dynamic web pages where the page viewer has the ability to change the content as needed. I see a future for web pages to be given the power of true applications with full user editing capabilities. I see a lifting of the chains of static content controlled only by the author of the page. Eventually the end user will be given control.
Where to go from here? This is hard for me to say. For me I will continue developing my DHTML cross platform rich text editor that can be used for e-mail, message boards, text editors, and other user applications. If you, the reader, can think of any other good uses for the Range object please visit www.pbwizard.com and talk to me.
Jeff Yates
If you enjoyed this post, please consider to leave a comment or subscribe to the feed and get future articles delivered to your feed reader.
