Interactive Documents II: Changing Document Content

(Dynamic HTML: A Primer - Updated 8/19/1997)

Both Netscape and Microsoft are counting on the browser becoming an important part of the everyday user interface. Netscape has discussed ending Microsoft's dominance of the operating systems market by replacing the operating system with the browser. For its part, Microsoft has made good on its promise to integrate the browser and the Windows interface with shell extensions to Internet Explorer 4.0. Making the browser the interface, however, will require much more than using the browser to handle slow, simple transactions managed mostly by the server. For the browser to become remotely as useful as the current GUI interfaces, it must be nearly as flexible. Dynamic HTML gives developers the flexibility they need to create powerful applications that share work smoothly across client and server.

Just as the ability to manipulate properties has made many new things possible, the ability to change HTML code after the document has loaded makes Dynamic HTML powerful enough to become an interface in its own right. Developers can use the textRange object to modify their code and rebuild their pages many times from a common base. This approach may not always be the best way to do it—there are still many reasons to use multiple pages instead of a single 1000K file — but this new flexibility means that a single page is now capable of doing what it once took many pages to do. Internet Explorer 4.0 has not reached the point where you can write a word processor or a spreadsheet with HTML, but it's the closest any browser has come yet.

Remember, examples in this chapter are guaranteed to work only in Internet Explorer 4.0. If you want to use some of these techniques in mixed-browser environments, you should implement the kind of browser detection code explained in Appendix A.

textRanges and the Object Model

We first visited the textRange object and the innerHTML and outerHTML properties in Chapter 4, and we saw an example of how not to use them in Chapter 7. Although the ability to change text on the fly gives developers tremendous power with a fairly manageable learning curve, it's not always the best solution. If your task involves displaying data selectively, you'll be much happier with the style properties we discussed in Chapter 7. On the other hand, if your task involves changing the structure of your document or if you need to present information created by an object (especially an ActiveX object), this kind of manipulation is what you're looking for. This chapter will provide a complete explanation of the capacities of the textRange object and related properties, and then we'll use it in Chapter 9 to build interactive HTML interfaces.

The textRange object is not yet a standard in anyone's eyes except Microsoft's. Be warned: information on textRanges is subject to the same kind of changes as the information on event models presented in Chapter 5.

Using Properties to manipulate text and HTML

The greatest improvement Microsoft has made to its still-evolving object model is the addition of four simple properties: innerHTML, outerHTML, innerText, and outerText. While textRange objects still offer advanced developers powerful tools for manipulating text, these properties make text manipulation far more accessible. We'll use them to create a very simple set of examples in a test environment before moving on to the more stranger but more powerful tools available in the textRange object.

Our initial examples will be simple: a page with some text and a few controls to let you modify it. If you need to experiment with what will happen when you paste HTML into a document, you'll be much better off checking it out with a simple testbed application such as this one than your fully developed Web application. After you've made the parts work in isolation, you can put them together with the confidence that each part works well on its own.

<HTML>
<HEAD><TITLE>textRange object testing zone</TITLE></HEAD>
<BODY BGCOLOR=#FFFFFF>
<H1 ID=headline>Testing Zone...</H1>
<P ID=para1>Please put on your helmet. HTML may shift <SPAN ID=span1 STYLE="color:red">violently</SPAN> in this location.</P>
<P ID=para2>Management is not responsible for developers who choose not to wear protective <SPAN ID=span2>headgear</SPAN>.</P>
<INPUT TYPE="button" VALUE="Action 1" onclick="action1()">
<INPUT TYPE="button" VALUE="Action 2" onclick="action2()">
<INPUT TYPE="TEXT" ID=entryBox>
<SCRIPT LANGUAGE="JavaScript">
function action1(){
}
function action2(){
}
</SCRIPT>
</BODY>
</HTML>

Within this simple framework, we'll explore the possibilities of the textRange object and prepare ourselves for real work. You need only enter the function code in the examples that follow, and our prefab laboratory will do the rest. Clicking on Action 1 will launch the action1() function, and clicking on Action 2 will launch action2() (see Figure 8.1).

Our first bit of code will just let you replace the contents of span1:

function action1(){
span1.innerText=entryBox.value; //uses new property to change content
}

You can see the results in Figure 8.2. As you can see, typing in HTML has no effect here, because the tags appear purely as text. To make the tags have an effect, you need to use innerHTML instead of innerText:

function action2(){
span1.innerHTML=entryBox.value;//uses new property to change HTML
}

If you try this example, type in some HTML, clicking Action1 will put the HTML verbatim into the text of the document. Clicking Action2 will put the HTML in the document and apply formatting as necessary, as shown in Figure 8.3.

The outerText and outerHTML behave similarly, except that they demolish the outer HTML tags. Try replacing innerText and innerHTML with their 'outer' counterparts in the above example, and you'll find that you can make a change once before getting an error about span1 not existing. While these properties are more dangerous than the inner tags, they give you additional power. The outerHTML property in particular lets you manipulate and rewrite your HTML any way you like, giving you incredible flexibility.

If that isn't enough for you, Microsoft has given its text-managing elements two strange methods. The insertAdjacentHTML method and the insertAdjacentText method allow you to insert HTML and text before and after your element. They take two parameters. The first, where, can be one of four values: beforeBegin, afterBegin, beforeEnd, or afterEnd. None of these will allow you to overwrite the element's HTML, giving you a safer alternative to the outerText and outerHTML properties. Specifying beforeBegin will insert your text or HTML outside the opening tag, while afterBegin inserts the material within the element, right next to the opening tag. Specifying beforeEnd inserts the material within the element, right before the closing tag, while afterEnd inserts the material outside the element, immediately after the closing tag. The second parameter is your text or HTML.

 

Different Worlds: textRanges objects vs. Element properties and methods

So far, most of the work we've done has been with property manipulation. Internet Explorer makes it simple to access element properties, rarely requiring you to create extra variables that mirror the contents of the document's own object and property values. When we've done so, it's been mostly as a convenience, and, as we saw with the puzzle example in Chapter 7, using properties can spare us the need for additional variables. The easy scriptability of properties, combined with an event model that lets you easily pinpoint the source and nature of an event, makes it possible to create lightweight, powerful code. With Internet Explorer 4.0 Preview Release 2, Microsoft added properties that let you easily script the content of elements as well as their attributes. Properties have taken over much of the turf that textRanges controlled previously, but textRanges remain an important part of Microsoft's plans.

Working with textRanges is more demanding. You must create a variable to hold a textRange object; textRange objects are not "natural" objects of a document. You won't find them in the all collection. Every time you want to create a textRange, you must determine which element you'll use as the base for your textRange and decide how to access that data. Because the methods for creating textRanges are exposed on the document object, you must always refer to document methods when you create them. Changing the content of your textRange, except in very simple situations, requires that you make a method call (to pasteHTML) instead of just setting a value like the innerHTML and outerHTML properties of an element.

This extra work means, however, that you can change anything in your document at any time. If you want to remove a section of your page and replace it, you can. If you want to excise an object that hasn't been responding the way you wanted, you can. If you want to make changes to the document based on the content of the document, you can. As you'll see in Chapter 9, you can even use textRange objects to treat your document as a primitive data storage container.

The textRange object is not especially stable. Although it's safe to use, be sure you test it first. Results will be repeatable but aren't always predictable, especially when you overwrite tags.

The way the textRange handles elements strongly resembles the way that event bubbling passes objects. Elements are contained within other elements, making it possible to collect many elements with the same request. For example, let's take a simple HTML document:

<HTML><BODY ID=bodydoc>
<H1 ID=headline>This is the headline</H1>
<P ID=main>This document contains some <B ID=bolded>bold</B>, <I ID=italicized>italic</I>, and <SPAN ID=ordinary>plain</SPAN> text. All of it is contained within the "main" paragraph element, which itself is contained by the "bodydoc" body element.</P>
<P ID=minor>This minor paragraph is contained only by the "bodydoc" body element</P>
</BODY></HTML>

Figuring out the structure here is reasonably simple. If you want to create a textRange based on the BODY tag, it will include the headline and both paragraphs, including all the subsections of the "main" paragraph. If you create textRanges based on the headline or the "minor" paragraph, they'll contain only the text within their tagsets. The main paragraph would include its own text, even including all the small sections created by the formatting tags and the SPAN. The formatting tags and the SPAN would create textRanges that included only the text within them. Table 8.1 goes into more detail.

Table 8.1 Ways to create textRanges

Declaration

Resulting Contents of r

r=document.body.createTextRange();

<H1 ID=headline>This is the headline</H1>
<P ID=main>This document contains some <B ID=bolded>bold</B>, <I ID=italicized>italic</I>, and <SPAN ID=ordinary>plain</SPAN> text. All of it is contained within the "main" paragraph element, which itself is contained by the "bodydoc" body element.</P>
<P ID=minor>This minor paragraph is only contained by the "bodydoc" body element</P>

r=document.body.createTextRange();
r.moveToElementText(main);

This document contains some <B ID=bolded>bold</B>, <I ID=italicized>italic</I>, and <SPAN ID=ordinary>plain</SPAN> text. All of it is contained within the "main" paragraph element, which itself is contained by the "bodydoc" body element.

r=document.body.createTextRange();
r.moveToElementText(bolded);

bold

This approach is simple but powerful. The textRange object has two properties and many methods. The text property contains the text contained in the range without any HTML markup. You can read and write this value directly. The htmlText property, which is read-only, contains the HTML contained in the space marked off by the text range object.

Remember that a textRange object doesn't actually contain the text that you use to access it. The text and htmlText properties are a little misleading. They aren't properties in the same sense that a color is a property of a style. The textRange is a window onto the document you're working with, and start and end provide the window frame's boundary. What you see through that window is material on the other side of the glass and not a painting directly on the glass. This means that you can't use the duplicate method (as in rangeBackup=rangeOriginal.duplicate) to preserve the original content of your range. When the contents of rangeOriginal change, the contents of rangeBackup also change. After all, they're similar window frames looking out at the same scenery. To create a backup variable successfully, you must create a different variable—a string rather than a textRange—to store the htmlText of your range.

Creating textRange Objects From Elements

Create a textRange that encompasses a single element requires two method calls, one of them a method of the document object, the other a method of the textRange object. The first one, document.createTextRange(), creates a text range that includes the entire content of the body section of the document, unless you specify start and end parameters. The second call is to [rangeName].moveToElementText(), which takes an element as its argument.

At some point, you'll probably want to use the textRange object to manipulate the contents of your document. It's reasonably easy to do so, but there are a few things you should keep in mind. First, the new property options (innerHTML, outerHTML, innerText, and outerText) provide a much simpler way to change most of your document content. Second, replacing initial text with new text is never a great way to "hide" data. You can do it, but anyone who looks at your source code can tell what you're trying to hide. Finally, adding HTML to your document with pasteHTML forces the browser to parse the entire contents of the string you hand it. If you make a mistake and include elements that have duplicate IDs or remove important parts of your page by accident, the browser won't stop you. Everything will look as if it's working - until your script finds it can't reach the objects it was expecting and starts producing strange error messages that are hard to trace. Overall, textRange objects are a blunt tool for working with HTML, and you'll frequently find better solutions in other styles of scripting.

We'll begin with the testbed (TRB) HTML we used for our property examples. First, let's use the action1() function to create a textRange object from the span1 ("violently") element and replace that text with whatever you've entered in the text box.

function action1(){
var r
r=document.body.createTextRange();
r.moveToElementText(span1);
r.text=entryBox.value;//uses text property to change text
}

If you try this, you'll see that you can type anything you like into the text box, and Internet Explorer will obligingly place it where the word "violently" used to be. However, it doesn't do anything with embedded HTML tags. They appear in the text with the rest of the document, as shown in Figure 8.4.

If we want to eliminate the tags, we'll need to use a different means of changing the text. Rather than change its text property, we'll use the pasteHTML method:

function action2(){
r=document.body.createTextRange();r.moveToElementText(span1);
r.pasteHTML(entryBox.value);//uses textRange to change HTML
}

If you try this, you can type in any HTML code you like, and the document will reflect what you've typed (see Figure 8.5).

You can even enter tags for images and other multimedia elements if you like. It's quite a change from the usual edit-and-load cycle, a strange improvement on the current generation of HTML editors.

The pasteHTML method allows you to change any part of your document any time. When the structure you've built isn't enough and you want to knock it all down, you can use pasteHTML to start afresh. (You could also load a new page, but you can't easily load only part of a page.) Try it with a few tags to see what happens. One problem you'll encounter quickly is that any tags you put at the beginning of your entry will hang over your page forever, or at least until you refresh the view. If you put a space in front of your entry, though, you can prevent this, but at the cost of strange spacing between words. The space is enough of a placeholder that you can bounce out the code the next time around. On the bright side, tags you forget to close aren't allowed to interfere with the rest of your page. Try entering <I>italic and see what happens (very little). (There are a few glitches, though, especially with cascading style sheet tags.) Because of these glitches, you'll most likely want to use the innerHTML and outerHTML properties unless you need to make changes to your document based on content.

Creating your textRange objects based on elements or even the entire document doesn't limit you to working with the entire content of the textRange you initially created. The textRange object includes several methods to let you be more selective. You can use the move method to change your range, choosing a start point within the current range:

function action2(){
var replaceRange;
replaceRange=document.body.createTextRange();
replaceRange.moveToElementText(span1);
replaceRange.move("Character",2);
replaceRange.pasteHTML(entryBox.value);
}

If you try this odd action, type test and click the Action 2button. You'll see the screen shown in Figure 8.6.

The move method and its siblings moveEnd and moveStart allow you to specify targets with a new syntax. If you use only move, the whole range collapses, becoming more of a cursor than a range. The moveEnd and moveStart methods let you build a larger range, moving only one of the endpoints of the range instead of the whole thing. You can use the move method with the kinds of units shown in Table 8.2.

Table 8.2 - textRange units

Unit

Meaning

character

Counts in characters

Word

Counts in words separated by spaces

Sentence

Takes you to the beginning of the nth sentence. It seems to use periods, question marks, and exclamation points as the breaking characters.

Story

Appears in practice to take you to the next paragraph.

The syntax for all three of these methods is the same: [rangeName].move(unit, count). You don't have to specify how many units to move. The default is 1.

Two other methods can help you manage ranges. The collapse method collapses your range to its start or its end. If you specify true (-1), you get the start; if you specify false (0), you get the end. The expand method takes a unit for its argument and helps you gather the stray half a word or quarter of a sentence you missed when you moved mathematically. It expands the range's beginning and end so that they include whole units.

Now that we've covered the basics of textRange objects, we'll explore some of their more powerful features. By the end, you may think you're getting a programmer's view of Microsoft Word. I suspect that's what Microsoft has in mind for these controls.

Creating and Manipulating Content-Based textRange Objects

Creating textRange objects from elements can be useful when you're building an interface, but there may be times when you need to create your ranges more flexibly. The method [rangeName].findText(value) allows you to search for a specific piece of text and move your textRange to enclose the next occurrence of it so you can manipulate it. This method is most useful for a find function or a search-and-replace, which we'll implement in this section. We'll use the same interface we created earlier, adding to it as necessary until we've implemented a full search-and-replace function like that in a simple word processor.

First, we need a search engine. Microsoft has obligingly given us the [rangeName].findText(value) method, which takes a string to search for. Let's try this and see how it works. Instead of making the Action 1 button replace text, we'll make it find and highlight it.

function action1(){
var seekRange;
seekRange=document.body.createTextRange();
seekRange.findText(entryBox.value);
seekRange.select();
}

It's simple, but it should give you an idea of what's coming (see Figure 8.7).

We used a new method of the textRange object to highlight the text: select(). It doesn't take any parameters or return any values; it makes your textRange into the current selection. As we'll see, select() is the first arrival of a new set of tools.

Now that we've built our little search engine, it's time to tie it to additional tools that will let it do some work. So far, we've used the entire document as our range, without worrying about insertion points or selections. By creating an additional variable to make our textRange object persistent, we'll be able to create a search-and-replace function worthy of a simple word processor.

<HTML>
<HEAD><TITLE>textRange object testing zone</TITLE></HEAD>
<BODY BGCOLOR=#FFFFFF>
<H1 ID=headline>Testing Zone...</H1>

<P ID=para1>Please put on your helmet. HTML may shift <SPAN ID=span1 STYLE="color:red">violently</SPAN> in this location.</P>
<P ID=para2>Management is not responsible for developers who choose not to wear protective <SPAN ID=span2>headgear</SPAN>.</P>
<INPUT TYPE="button" VALUE="Search" onclick="action1()">
<INPUT TYPE="button" VALUE="Replace" onclick="action2()"> <BR>
Search Text:<INPUT TYPE="TEXT" ID=entryBox onblur="action1"><BR>
Replace Text:<INPUT TYPE="TEXT" ID=replaceBox><BR>
<SCRIPT LANGUAGE="JavaScript">
function action1(){ //search function
seekRange=document.body.createTextRange();
seekRange.findText(entryBox.value);
if (seekRange != null ){
seekRange.select();
}
}

function action2(){//replace function
action1();//find it, if they forgot to do that.
if (seekRange != null ){
seekRange.pasteHTML(replaceBox.value);
}
}
var seekRange;
</SCRIPT>
</BODY>
</HTML>

It's not the most elegant interface yet, but it's getting there (see Figure 8.8).

Now that we have a simple search-and-replace function, we need to start thinking about what it is we're replacing with this blunt instrument. We're adding and deleting elements, and the computer is doing its best to keep up, but you'll frequently find that you need to take control of the element management. Creating pages that make extensive use of textRange objects is difficult. It's easy to inadvertently refer to elements that you either just deleted or haven't yet created.

Managing Elements That Get Trapped in Your textRange

The first part of managing elements involves determining where you are in the document. Fortunately, two textRange methods can give you some basic orientation. The parentElement() method takes as its argument an integer that represent the character position of the location you're checking. We'll use our search framework to try this:

<HTML>

<HEAD><TITLE>textRange object testing zone</TITLE></HEAD>

<BODY BGCOLOR=#FFFFFF>

<H1 ID=headline>Testing Zone...</H1>

<P ID=para1>Please put on your helmet. HTML may shift <SPAN ID=span1 STYLE="color:red">violently</SPAN> in this location.</P>

<P ID=para2>Management is not responsible for developers who choose not to wear protective <SPAN ID=span2>headgear</SPAN>.</P>

<INPUT TYPE="button" VALUE="Search" onclick="action1()">

<INPUT TYPE="button" VALUE="Look up" onclick="action2()"> <BR>

 

Search Text:<INPUT TYPE="TEXT" ID=entryBox onblur="action1"><BR>

<SCRIPT LANGUAGE="JavaScript">

function action1(){ //search function

seekRange=document.body.createTextRange();

seekRange.findText(entryBox.value);//findText replaces rangeFromText

if (seekRange != null ){

seekRange.select();

}

}

function action2(){

action1();

checkNumber=seekRange.start;//we want to check the first character

checkElement=seekRange.parentElement(checkNumber) //get element

checkID=checkElement.id //get just ID of element

alert(checkID); //announce ID

}

var seekRange;

</SCRIPT>

</BODY>

</HTML>

 

The action1() function here is the same one we just used in our search; the new developments take place in the action2() function. First, we get the start value for the seekRange object we just found. Then we use the parentElement method to find out which element contains that first character. The parentElement method returns a full object reference, which is useful if we want to make changes to the object. In our case, we want to announce its name to the world (see Figure 8.9).

Dynamic HTML objects have one other oddly intriguing method—scrollIntoView()—which takes true or false for a parameter. If you supply true, the window scrolls to the beginning of your object; if false, it scrolls to the end. You'll need to use parentElement() on your textRange object to find an element that approximates the location of your textRange, but this could help you make a useful search function for long documents.

Internet Explorer 4.0 also includes a few additional methods for the document and textRange objects. It appears that Microsoft wants to make it possible to add macro-like commands to Internet Explorer. The textRange object has an executeCommand() method that accepts a command ID number and a variant value as parameters. This method executes "a command on the range. For example, changing the formatting of the text." It still doesn't seem to work yet (in Preview Release 2), at least with the documents I've been able to create, but it looks as if this method will be the hook for adding text manipulation functions to the browser. The methods you can use to change for commands -queryCommandEnabled(cmdID), queryCommandIndeterm(cmdID), queryCommandState(cmdID), queryCommandSupported(cmdID), and queryCommandText(cmdID) - all seem to be connected to the load cycle of ActiveX and other objects that we saw in Chapter 5. When these methods appear, expect the functions to provide the text processing power your scripts will need to actively manage text, incoming as well as outgoing. Microsoft will be prepackaging several of these commands with the browser.

Microsoft is starting to make it possible to create full-fledged applications in its browser, but it needs to provide a more comprehensive set of tools that can respond to user selections as well as user actions such as clicking and dragging and dropping. The tools we have now are a bit crude, but they have enough power to make a start of it. By the next iteration, we'll be able to create something more akin to a word processor. When the objects are complete, Internet Explorer will have the beginnings of a system that will let users manage the content of Web pages directly.

Back to the index

Copyright 1997 by Simon St.Laurent