5.11. Combining Forces: A Custom Newsletter
To round out the discussion of dynamic
content, I am going to present an application that demonstrates
several aspects of dynamic content in action. Unfortunately, the
Macintosh version of IE is missing some key ingredients to make this
application run on that platform, so this only works on IE 5 and
later for Windows and Netscape 6 and later. The example is a
newsletter that adjusts its content based on the
reader's filtering choices. For ease of
demonstration, the newsletter arrives with a total of five stories
(containing some real text and some gibberish to fill space)
condensed into a single document. A controller box in the upper right
corner of the page allows the reader to filter the stories so that
only those stories containing specified keywords appear on the page
(see Figure 5-3). Not only does the application
filter the stories, it orders them based on the number of matching
keywords in the stories. In a real application of this type, you
might store a profile of subject keywords on the client machine as a
cookie and let the document automatically perform the filtering as it
loads.
Figure 5-3. A newsletter that uses DHTML to customize its content
Each story arrives inside a
div element of class wrapper;
each story also has a unique ID that is essentially a serial number
identifying the date of the story and its number among the stories of
that day. Nested inside each div element are both
an h3 element (class of
headline) and one or more p
elements (class of story). In Example 5-14, the style sheet definition includes
placeholders for assigning style rules to each of those classes. At
load time, all items of the wrapper class are
hidden, so they are ignored by the rendering engine.
The
controller box (ID of filter) with all the
checkboxes is defined as an absolute-positioned element at the top
right of the page. In real life, this type of controller might be
better handled as a fixed-position element (if only more browsers
supported this style).
The only other noteworthy element is a div element
of ID myNews (just above the first story
div element). This is an empty placeholder where
stories will be inserted for viewing by the user.
The onload event
handler of the body element triggers the searching
and sorting of stories, as does a click on any of the checkboxes in
the controller box. Two global variables assist in searching and
sorting. The keywords array is established at
initialization time to store all the keywords from the checkboxes.
The foundStories array is filled each time a new
filtering task is requested. Each entry in the
foundStories array is an object with two
properties: id, which corresponds to the ID of a
selected story, and weight, which is a numeric
value that indicates how many times a keyword appears in that story.
Now
skip to the filter( ) function, which is the
primary function of this application. It is invoked at load time and
by each click on a checkbox. The first task is to clear the
myNews element by removing all child nodes if any
are present. Then the function looks for each div
element with a class name of wrapper, so that the
div elements can be passed along to the
searchAndWeigh( ) function. This is where a
DOM-specific invocation of a text range object allows for extraction
of just the text from the story. Because the W3C DOM text range
doesn't offer the convenience of
IE's findText( ) function, we use
the old standby of the indexOf( ) function for the
string value. By manipulating the start position of the
indexOf( ) action inside a
while loop, the function can count the number of
matches for each keyword within the text.
For each chosen keyword match, the parent element of the current
div (the element whose tags surround the matched
text) is passed to the getDIVId( ) function. This
function makes sure the parent element of the found item has a class
associated with it (meaning that it is of the
wrapper, headline, or
story class). The goal is to find the
wrapper class of the matched string, so
getDIVId( ) works its way up the chain of parent
elements until it finds a wrapper element. Now
it's time to add the story belonging to the
wrapper class element to the array of found
stories. But since the story may have been found during an earlier
match, there is a check to see if it's already in
the array. If so, the array entry's
weight property is incremented by one. Otherwise,
the new story is added to the foundStories array.
Since it is conceivable that no story
may have a matched keyword (or no keywords are selected), a short
routine loads the foundStories array with
information from every story in the document. Thus, if there are no
matches, the stories appear in the order in which they were entered
into the document. Otherwise, the foundStories
array is sorted by the weight property of each
array entry.
The finale of Example 5-14 is at hand. With the
foundStories array as a guide, the hidden
div elements are cloned (to preserve the originals
untouched). The className properties of the clones
are set to a different class selector whose
display style property allows the element to be
displayed. Then each clone is appended to the end of the
myNews element. As the last step, the
foundStories array is emptied, so it is ready to
do it all over again when the reader clicks on another checkbox.
Example 5-14. A custom newsletter filter that uses DHTML
<html>
<head>
<title>Today in Jollywood</title>
<style type="text/css">
body {font-family: Arial, Helvetica, sans-serif;
background-color:#ffffff}
#banner {font-family:Comic Sans MS, Helvetica, sans-serif;
font-size:22px}
#date {font-family:Comic Sans MS, Helvetica, sans-serif;
font-size:20px}
.wrapper {display:none}
.unwrapper {display:block}
.headline {}
.story {}
#filter {position:absolute; top:10px; left:330px; width:400px;
border:solid red 3px; padding:2px;
font-size:12px; background-color:coral}
</style>
<script language="JavaScript" type="text/javascript">
// Global variables and object constructor
var keywords = new Array( );
var foundStories = new Array( );
function story(id, weight) {
this.id = id;
this.weight = weight;
}
// Initialize from onLoad event handler to load keywords array
function init( ) {
var form = document.filterer;
for (var i = 0; i < form.elements.length; i++) {
keywords[i] = form.elements[i].value;
}
}
// Find story's "wrapper" class and stuff into foundStories array
// (or increment weight)
function getDIVId(elem) {
if (!elem.className) {
return;
}
while (elem.className != "wrapper") {
elem = elem.parentNode;
}
if (elem.className != "wrapper") {
return;
}
for (var i = 0; i < foundStories.length; i++) {
if (foundStories[i].id == elem.id) {
foundStories[i].weight++;
return;
}
}
foundStories[foundStories.length] = new story(elem.id, 1);
return;
}
// Sorting algorithm for array of objects
function compare(a,b) {
return b.weight - a.weight;
}
// Look for keyword match(es) in a div's text range
function searchAndWeigh(div) {
var txtRange, txt, start;
var isW3C = (typeof Range != "undefined") ? true : false;
var isIE = (document.body.createTextRange) ? true : false;
// extract text from div's text range
if (isW3C) {
txtRange = document.createRange( );
txtRange.selectNode(div);
txt = txtRange.toString( );
} else if (isIE) {
txtRange = document.body.createTextRange( );
txtRange.moveToElementText(div);
txt = txtRange.text;
} else {
return;
}
// search text for matches
for (var i = 0; i < keywords.length; i++) {
// But only for checkmarked keywords
if (document.filterer.elements[i].checked) {
start = 0;
// use indexOf( ), advancing start index as needed
while (txt.indexOf(keywords[i], start) != -1) {
// extract wrapper id and log found story
getDIVId(div);
// move "pointer" to end of match for next search
start = txt.indexOf(keywords[i], start) + keywords[i].length;
}
}
}
}
// Main function finds matches and displays stories
function filter( ) {
var divs, i;
var news = document.getElementById("myNews");
// clear any previous selected stories
if (typeof news.childNodes == "undefined") {return;}
while (news.hasChildNodes( )) {
news.removeChild(news.firstChild);
}
// look for keyword matches
divs = document.getElementsByTagName("div");
for (i = 0; i < divs.length; i++) {
if (divs[i].className && divs[i].className == "wrapper") {
searchAndWeigh(divs[i]);
}
}
if (foundStories.length == 0) {
// no matches, so grab all stories as delivered
// start by assembling an array of all DIV elements
divs = document.getElementsByTagName("div");
for (i = 0; i < divs.length; i++) {
if (divs[i].className && divs[i].className == "wrapper") {
foundStories[foundStories.length] = new story(divs[i].id);
}
}
} else {
// sort selected stories by weight
foundStories.sort(compare);
}
var oneStory = "";
for (i = 0; i < foundStories.length; i++) {
oneStory = document.getElementById(foundStories[i].id).cloneNode(true);
oneStory.className = "unwrapper";
document.getElementById("myNews").appendChild(oneStory);
}
foundStories.length = 0;
}
</script>
</head>
<body onload="init();filter( );">
<h1 id="banner">Today in Jollywood</h1>
<h2 id="date">Tuesday, April 1, 2003</h2>
<hr>
<div id="myNews">
</div>
<div class="wrapper" id="N040103001">
<h3 class="headline">Kevin Costner Begins New Epic</h3>
<p class="story">Oscar-winning director and actor, Kevin Costner has begun location
shooting on a new film based on an epic story. Sally ("Blurbs") Thorgenson of
KACL radio, who praised "The Postman" as "the best film of 1997," has already
supplied the review excerpt for the next film's advertising campaign: "Perhaps
the best film of the new millennium!" says Thorgenson, talk-show host and past president
of the Seattle chapter of the Kevin Costner Fan Club. The Innscouldn't it the
trumple from rathe night she signs. Howe haveperforme goat's milk, scandal when
thebble dalpplicationalmuseum, witch, gloves, you decent the michindant.</p>
</div>
<div class="wrapper" id="N040103002">
<h3 class="headline">Critic's Poll Looking Bleak</h3>
<p class="story">A recent poll of the top film critics shows a preference for
foreign films this year. "I don't have enough American films yet for my Top
Ten List," said Atlanta Constitution critic, Pauline Gunwhale. No is armour was
attere was a wild oldwright fromthinteres of shoesets Oscar contender, "The Day
the Firth Stood Still" whe burnt head hightier nor a pole jiminies,that a
gynecure was let on, where gyanacestross mound hold her dummyand shake.</p>
</div>
<div class="wrapper" id="N040103003">
<h3 class="headline">Summer Blockbuster Wrap-Up</h3>
<p class="story">Despite a world-wide boycott from some religious groups, the
animated film "The Satanic Mermaid" won the hearts and dollars of movie-goers
this summer. Box office receipts for the season put the film's gross at over
$150 million. Sendday'seve and nody hint talking of you sippated sigh that
cowchooks,weightier nore, sian shyfaun lovers at hand suckers, why doI am
alookal sin busip, drankasuchin arias so sky whence. </p>
</div>
<div class="wrapper" id="N040103004">
<h3 class="headline">Musical in Tarentino's Future?</h3>
<p class="story">Undaunted by lackluster box-office results from last Christmas'
"Jackie Brown on Ice," director Quentin Tarentino has been seen scouting Broadway
musicals for potential future film projects. "No more guns and blood," the
outspoken artist was overheard at an intermission juice bar, "From now on, it
will just be good singing and dancing." He crumblin if so be somegoat's milk
sense. Really? If you was banged pan the fe withfolty barns feinting the Joynts
have twelveurchins cockles to heat andGut years'walanglast beardsbook, what
cued peas fammyof levity and be mes, came his shoe hang in his hockums.</p>
</div>
<div class="wrapper" id="N040103005">
<h3 class="headline">Letterman to Appear in Sequel</h3>
<p class="story">As if one cameo appearance weren't enough, TV talk show host
David Letterman will reprise his role as the dock-side monkey vendor in "Cabin
Boy II," coming to theaters this Christmas. Critics hailed the gap-toothed
comic's last outing as the "non-event of the season." This the way thing,what
seven wrothscoffing bedouee lipoleums. Kiss this mand shoos arouna peck of
night, in sum ear of old Willingdone. Thejinnies and scampull's syrup.</p>
</div>
<hr>
<p id="copyright">Copyright 2003 Jollywood Blabber, Inc. All Rights Reserved.</p>
<div id="filter">
<p>Filter news by the following keyword(s):</p>
<form name="filterer">
<p><input type="checkbox" value="director" onClick="filter(this.form)">director
<input type="checkbox" value="box" onClick="filter(this.form)">box (office)
<input type="checkbox" value="critic" onClick="filter(this.form)">critic
<input type="checkbox" value="summer" onClick="filter(this.form)">summer
<input type="checkbox" value="Christmas" onClick="filter(this.form)">Christmas</p>
</form>
</div>
</body>
</html>
Some people might argue that it is
a waste of bandwidth to download content that the viewer may not
need. But unless you have a CGI program running on the server that
can query the user's preferences and assemble a
single document from matching documents, the alternative is to have
the client make numerous HTTP requests for each desired story. When
you want to give the user quick access to changeable content, a brief
initial delay in downloading the complete content is preferable to
individual delays later in the process.
Example 5-14
demonstrates that even when IE has its own way of doing things (as in
its TextRange object), you can combine the
proprietary DOM with W3C DOM syntax that it does support (as with the
cloneNode( ) and appendNode( )
methods). This makes it easier to implement applications that change
document content in both DOMs.
 |  |  | | 5.10. Working with Text Ranges |  | 6. Scripting Events |
Copyright © 2003 O'Reilly & Associates. All rights reserved.
|