TruerWords Logo
Google
 
Web www.truerwords.net

Search TruerWords

Welcome
Sign Up  Log On

Rapid Application Development with Mozilla, Errata

Rapid Application Development with Mozilla, by Nigel McFarlane, is a well written textbook-like tutelage. Unfortunately, it has a number of undocumented errors. Many of these errors are typographical in nature, but (unfortunately) are even apparent in the downloadable source code the author has made available from his web site. This page lists my findings, in the hope that they'll save someone a bit of time.

Actually, not everything listed here is errata, in the strictest sense. I also attempt to clear up areas of confusion and offer suggestions where apparent holes are left. None of this is meant as a criticism of the book or Mr. McFarlane's efforts, I'm just trying to help other readers.

Table of Contents

Items are listed here by page number, book section, listing or example number (if any), and finally the title of the appropriate listing, example, or section.

Chapter 1
Chapter 3
Chapter 5
Chapter 6
Chapter 7
Chapter 10
Chapter 11
Chapter 12
Chapter 13
Chapter 14
Chapter 15
Chapter 16
Chapter 16, Bonus Material

This is my own bonus material, where I help the reader complete the NoteTaker tool with the pieces missing in the book.

Please let me know if I've left anything out!

Errata Listing

Page 35, Section 1.6.5, NoteTaker Preparation

Step 3 says to create subdirectories named chrome, locale, and skin. That should be content, locale, and skin.

Page 35, Section 1.6.5, NoteTaker Preparation

Package registration is done in the chrome/installed-chrome.txt file, not chrome/install-chrome.txt

Pages 96 & 97, Section 3.5.2, Configuring and Using Locales

The author specifies at the end of section 3.5.1 that setting up NoteTaker's locales system reduces the readability of the book's examples, "so we don't apply it here." The beginning of 3.5.2 then states that, "For your own projects, here is how to get localization working," and goes on to explain the procedure.

Don't believe it. You will use it later in the book, starting with section 5.6.4, "Reading String Bundles via XPCOM" (which is a Hands-On section).

Even if this is not considered errata, one must admit that it's rather misleading as the author actually seems to deprecate the lesson on Locales before explaining it.

Page 97, Section 3.5.2, Configuring and Using Locales

A few lines from the top of this section, a text element with the literal value "Cancel" is changed to use an entity reference so that Mozilla's localization technique can be put into practice. However, entity references must end with a semicolon, which is missing in the example. It should look like this:

<text value="&editDialog.cancel;"/>
Page 140-141, Section 5.2, Standards, Browsers, and <SCRIPT>

Four examples are offered for including an external javascript in an XML (or HTML) document. In each of them, the src attribute is missing its closing quote.

Another problem: the third example is completely wrong (I think). The type attribute must be a content type, meaning something like "text/javascript" or "application/x-javascript". That third example in the book was probably meant to use the language attribute, which does let you specify the version of the language. Unfortunately, that attribute was deprecated a long time ago and so should not be used at all.

Here's how it should have looked. I've included the corrected third example, but marked it for deletion as a reminder not to use it.

<script type="application/x-javascript" src="code.js"/>
<script type="text/javascript" src="code.js"/>
<script language="JavaScript1.2" src="code.js"/>
<script src="code.js"/>

Thanks to gulliver for catching the missing quotes. When he pointed that out, I looked more closely and found the slightly-more-significant problem with the third example.

Page 187, Section 5.6.1, Scripting <deck> via the DOM

The second <toolbarbutton> should be labeled Keywords, not Edit

Page 192, Section 5.6.4, Listing 5.11, Reading String Bundles via XPCOM

The method name for reading a string from a string bundle via the nsIStringBundleService is GetStringFromName, not getStringFromName. So, the relevant line from Listing 5.11 should read:

value = sbundle.GetStringFromName("dialog." + names[i]);
Page 212, Section 6.1.4.1, Listing 6.9, Processing order for a broadcast event

Though this isn't listed as a functional example, it would work if the two calls to getElementById were corrected. Their prefix "document." is missing in each case. They should look like this:

document.getElementById("b").setAttribute("foo", str);

and this:

document.getElementById("x1").setAttribute("style", "color:red");

Put the corrected functions into NoteTaker's javascript file, and put the XUL elements into the editDialog.xul file, and the example works just fine.

Yes, it's true that it wasn't written to be run as-is, but that's true for too much of the code in this book. At least a working example gives the reader something to experiment with.

The same is true for most of the listings in this area of the book: most of the code is usable (especially if you copy it into an already working project like NoteTaker), except for the missing "document." on the front of getElementById().

Page 213, Section 6.1.4.1, Listing 6.10, Processing order for a broadcast event.

Though this isn't listed as a functional example, it would be functional if the only line of the body of the produce() function were correct. getElementById is not defined on its own: call it as a method of the document object. So, it should look like this:

document.getElementById("b").setAttribute("style", str);

Put that produce() function into NoteTaker's javascript file, and put the XUL elements into the editDialog.xul file, and the example works just fine. Yes, it's true that it wasn't written to be run as-is, but that's true for too much of the code in this book. At least a working example gives the reader something to experiment with.

The same is true for most of the listings in this area of the book: most of the code is usable (especially if you copy it into an already working project like NoteTaker), except for the missing "document." on the front of getElementById().

Also note that this listing's title appears to be incorrect. It should probably be something like, "Use a nested <observes> tag to simplify attribute changes."

Page 216, section 6.1.4.3, Listing 6.12, XPCOM Broadcasters and Observers

The first line of the sample code should say:

var observer = { ... as in Listing 6-7 ... };

instead of referring to listing 6-6 which is completely unrelated.

Page 258, Section 7.6, Listing 7.4, Hands On: NoteTaker Events and Forms

Very minor: the second line is missing a closing double-quote. It should read:

<textbox id="dialog.details" multiline="true" flex="err1"/>
Page 262, Section 7.7.2, Listing 7.7, Picking Apart Complex Widgets

The title of this listing is "Example of an asynchronous XMLHttpRequest," which has nothing at all to do with the contents of the listing. That's probably a copy-and-paste error, as that title is from an earlier listing.

This listing actually shows the "informal CSS styles" that "enhance some of the examples in the book" to "provide better visual hints." (I pulled that from the paragraph right above the listing.)

Page 329, Section 10.2.3, Context Menus

In the first paragraph, the author describes how contextual menus are revealed by saying, "They appear the same as other menus, except that they are exposed by a context click (apple-click on Macintosh, right-click on other platforms)."

Two problems with this:

  1. There is no apple-click. The correct term is command-click. (At least he didn't say "clover-click," which would have caused me to have a seizure.)
  2. Contextual menus are actually revealed with a control-click (hold down the control key, not the command key), not a command-click.
Page 350, Section 10.9, Hands On: NoteTaker Dialogs

The reader is instructed to create a "note" object in the browser window, which will store the temporary state of the current note. Unfortunately, there are no instructions for how to put this object in the browser window. Also, the note object shown in the book has two typos: the first semicolon should be a comma, and there should be a semicolon after the closing curly brace.

Put the note object in the NoteTaker tool bar's javascript file (probably toolbar.js). Here's what it should actually look like:

var note = {
    url: null,
    summary: "",
    details: "",
    chop_query: true, home_page: false,
    width: 100, height: 90, top: 80, left: 70
};
Page 351, Section 10.9, Listing 10.4, Save and load of NoteTaker dialog box data to the main window

Two tasks (or commands) are shown. One saves a note back to the object in the browser window from the editor dialog, and the other loads that note from the browser window into the editor dialog.

Both tasks use an identical call document.getElementById to find the 'widget' (form element) in the XUL dialog box to which the data should be loaded or read from. Both tasks have the same typo: they're missing a closing parenthesis before the semi-colon. The corrected form is shown here:

widget = document.getElementById("dialog." + field.replace(/_/, "-") );
Page 373, Section 11.3.3, Figure 11.2, Reduced graph of boy-dog tuple links

The edge-line from triple '1 plays-with 5' to '1 is-named 2' should instead go from '1 plays-with 5' to vertex (1).

Page 403, Section 11.6, Hands On: NoteTaker: Data Models

The end of the third paragraph on page 403 mentions that "most of the issues with the first four facts in Listing 11.13" have been resolved. Listing 11.13 is way back on page 394, and has nothing to do with the "issues" referred to. It should actually refer to Listing 11.15, on page 402.

Page 408, Section 11.7, Listing 11.20, downloads.rdf file after single complete download

Another copy-and-paste error. The title of this listing has nothing to do with its contents, and is simply a copy of the title from listing 11.13.

The correct title would be something like, "Writing out your fact store to an RDF file"

Page 414, Section 12.1, Listing 12.4, Merged document resulting from a master and two overlays

The first <box> is missing a closing tag, which should be placed immediately after the first label tag.

<window>
    <box id="one">
        <label="Red"/>
	</box>
    <box id="two">
        <label value="Amber"/>
        <label value="Green"/>
    </box>
</window>
Page 429-432, Section 12.5, Hands On: The NoteTaker Toolbar

I'll state right up front that I'm not completely sure about this one. There's definitely a bug, and I think my fix is right, but I'm not positive. (Nevermind that, this error and my fix have been confirmed.)

The hands on section walks the reader through the process of converting the NoteTaker toolbar into a browser overlay. This includes modifications to the toolbar.xul file (now called browserOverlay.xul) and contents.rdf. The first is to convert the toolbar to an overlay, and the second is to register the overlay with the chrome system.

Unfortunately, it seems that the command "notetaker-open-dialog" fails. The dialog doesn't open. Actually, it seems that it starts to open, but then freezes. On the other hand, if you load the toolbar into the window by manually entering the chrome URL into the browser, everything works fine.

I'm pretty sure this is an error in the book, and the files offered for download by the author have the same problem! They don't work. (If they were tested, it was probably only by loading them into the browser as describe above.)

It looks like the problem is that the action() function is trying to open the dialog with a relative URL, "editDialog.xul," but if the toolbar is merged into the browser window the relative URL doesn't work anymore. After the toolbar overlay is merged with the navigator.xul file, the editDialog.xul file is no longer available at the relative URL. editDialog.xul is being sought in the same directory as navigator.xul instead of in the same directory as browserOverlay.xul.

My solution was to use the full URL to editDialog.xul (meaning "chrome://notetaker/content/editDialog.xul") when calling window.openDialog().

I welcome comments/feedback on this one, as I'm not sure about any of this. (Just Reply to this message in the forum.)

Page 462, Section 13.3.15.1, Seminal Data

The second paragraph refers to Listing 13.4 as an example of tree data specified in XUL. That listing is on the very next page, but it's written entirely in javascript. The correct listing is 13.3, way back on page 452.

Page 478, Section 13.5.2, Listing 13.11, keywords_click() handler for NoteTaker keywords.

This listing should be entitled, "keywords_select() handler for NoteTaker keywords."

Page 479, Section 13.5.2, Listing 13.11, keywords_click() handler for NoteTaker keywords.

The first line on page 479 redeclares the items variable which was first declared in the fourth line of the code on page 478. This only generates a warning, but should still be avoided.

The solution is easy. Since the two declarations are identical, just delete the second one.

Page 483, Section 13.5.3.1, Listing 13.15, JavaScript object for the dynamic listbox builder

In the next-to-last line of code, the _listbox variable is missing its underscore. It should say:

item.setAttribute( "label", _listbox.myview.getItemText( i ) );

Also, the prototype definition is missing the final semi-colon after the closing curlybrace. Compare with Listing 13.14.

Page 500, Section 14.1, Listing 14.1, Trivial example of an RDF file used for templates

There are three <Description> elements in this RDF sample. The last two are written as empty elements, but the first one is not. Unfortunately, the first one is missing its closing tag, which should be just after the closing </Test:Container> tag and before the second <Description> tag.

<Description about="http://www.example.org/>
    <Test:Container>
        <Seq about="urn:test:seqroot">
            
<li resource="urn:test:welcome"/>
            
<li resource="urn:test:message"/>
        </Seq>
    </Test:Container>
</Description>
<Description about="urn:test...

A missed closing tag wouldn't normally be a big deal, except for how Mozilla deals with faulty RDF: it truncates it! Ugh. No error message.

Mr. McFarlane actually wrote to warn me about this one in advance, saying it was one of the more serious 'typos.' I don't think it compares to the confusion at the end of chapter 13, but I appreciate the fore-warning.

Pages 508-509, Section 14.2.1.3, Query Solution Strategy, Figures 14.3 and 14.4

These two graphs are a little confusing, if not wrong. The edge between two vertices in the graph represent the predicate of the triple, and are read in the order shown by the arrow at the end of the vertex. For example, (1)----is-named---->[Tom] shows that "is-named" is the predicate.

How, then, can the new items at the bottom of the graph be explained? (2) -- the dog Spot -- is the owner of (7) -- a tennis ball. That's fine. Yet the graph then states that (7) plays-with [softball]. Huh? Comparing (5) with (7), it seems that the final predicate edge should say "type-of" rather than "plays-with."

That isn't enough, though. The graph is supposed to be showing the answer to the query, "What things do Tom's dogs play with?" The "owner" predicate edge between 2 and 7 is bold to indicate that it is one part of the query's solution, yet ownership wasn't part of the query.

In the end, (2)---owner--->(7)---plays-with--->[softball] should instead be (2)---plays-with--->(7)---type-of--->[softball].

Maybe I'm just being picky, but I think explanations at the conceptual level need to be completely clear and accurate, as this builds the foundation for the functional learning that comes later.

Pages 546, Section 14.8, Hands On: NoteTaker Data Made Live

The location of the rdf file is listed as:

chrome://notetaker/contents/notetaker.rdf

Not for the first time, the name of the directory is wrong (see the first error).

Take out the 's'.

Pages 548, Section 14.8.1, Listing 14.18, Test Data for NoteTaker XUL Templates

There are actually three errors in this listing.

  1. In the third line, xmlns is misspelled as xmnls.

  2. In the ninth line on the second page, the </NT:height> tag is missing the "/" (forward slash).

  3. The sample RDF has an extra </Description> tag at the very end.

        </Description>
        </Description>
    </RDF>

    Take out the extra tag and the document will then be well formed.

What's particularly amusing about these errors is that the next paragraph goes on at some length about debugging your XML/RDF file to make sure it's exactly right, and that this file requires "five times as much care as <tree>."

Page 551, Section 14.8.2, Listing 14.21, Templated Popup Menu for the NoteTaker Toolbar.

The <menupopup> tag is given two new attributes: datasources and ref. Ref is fine. Datasources only works if you load the chrome into the browser window, because the book uses a relative URL to the RDF file. Since the NoteTaker toolbar is actually being loaded as an overlay, the relative URL refers to the same directory as Navigator's chrome files, not Notetaker's chrome.

Without the following correction, the toolbar overlay doesn't work.

This is essentially a repeat of a similar error from chapter 12.

To fix it, change this:

<menupopup datasources="notetaker.rdf" ...

To this:

<menupopup datasources="chrome://notetaker/content/notetaker.rdf" ...
Page 552, Section 14.8.2, Listing 14.22, Templated Textbox for the NoteTaker Toolbar.

This listing has the same datasources error as the previous item. Fix it by using the full url to the RDF file.

Note that this is not an issue for the next listing (14.23), which is for the dialog window. It's only an issue for overlays.

Page 553, Section 14.8.3, Listing 14.23, Templated listbox for the NoteTaker toolbar.

The title of this listing is wrong. This listing isn't for the NoteTaker toolbar, it's for the NoteTaker dialog.

Page 557, Section 14.8.5, Listing 14.26, Template rebuilding code for the NoteTaker toolbar

This code simply doesn't work. The symptom is that when the browser window first opens, the textbox and the popup menu both contain the data from the RDF file, but the content disappears within a couple of seconds. The javascript console reports that "builder has no properties."

After hours of research, I could not decide if there was a bug in the sample code, a bug in Mozilla, or both... but I was leaning towards "both." I asked Mr. McFarlane, and after a bit of his own research he confirmed that there are layers of bugs at work here.

  1. Menus don't have builders, so delete the line that calls menu.firstChild.builder.rebuild(). Since that line is no longer needed, the line that finds the menu can also be deleted. Now your menu will stick around instead of being emptied out when refresh_toolbar() is fired off by content_poll().
  2. The parentNode of 'notetaker-toolbar.summary' is the <action> element, which does not have a 'ref' attribute. The ref attribute actually needs to be changed way up down the element tree, on box.parentNode.parentNode.parentNode.parentNode, which is the <box> tag with the ref attribute. (See listing 14.22 on page 522).

Here's the final, working code for the refresh_toolbar() function.

function refresh_toolbar()
{
    var tbox = window.document.getElementById( 'notetaker-toolbar.summary' );
    var box  = tbox.parentNode.parentNode.parentNode.parentNode;
    box.setAttribute( 'ref', note.url );
    box.builder.rebuild();
}

However, one note from Mr. McFarlane:

The code is not complete - Chapter 16 provides a complete solution. Although Ch 14 provides an interim solution that looks like it's complete, the intent of the book is to finish with Ch 16...
Page 559, Section 14.8.5, Listing 14.29, Template rebuilding for NoteTaker Edit dialog box.

The refresh_dialog function is supposed to update both the listbox and the tree, but instead it just updates the listbox twice. To fix it, change this line:

var tree = window.document.getElementById('dialog.keywords');

to this:

var tree = window.document.getElementById('dialog.related');
Page 575, Section 15.2.4.2, <stylesheet>.

The HTML stylesheet tag doesn't have a src attribute.

Page 602, Section 15.7.2, Listing 15.14, Trivial example of XML binding inheritance

The title for this listing is wrong, and is apparently just another copy-and-paste error (compare with listing 15.13 on page 591).

The correct title should be something like, "Skeletal XBL document for noteplacer binding widget".

Page 603, Section 15.7.2, Adding XBL Content

In the first paragraph after the four bullet points (actually marked with little hands in the book), a reference is made to the <notepicker> tag being used as the source for the 'collapsed=pageless' attribute.

I think that's supposed to be <noteplacer>.

Page 611, Section 15.7.4, Integrating the Binding

The CSS binding code listed in the book looks like this (in the page's second paragraph):

noteplacer {
    -moz-binding : url("noteplacer.xml#noteplacer");
}

That's the first time we (the readers) have actually been told what to call the XBL file (noteplacer.xml). However, back on page 602, in the paragraph right after listing 15.14, we were told, The set of bindings specific to NoteTaker we've called "notetaker" and the binding specifically for the <noteplacer> tag we've called "noteplacer."

In other words, the document will contain all the bindings for NoteTaker, and the id of the <bindings> tag is "notetaker." Therefore, the file should be called notetaker.xml or notetaker-bindings.xml, not noteplacer.xml.

If you read this one and thought, "Ok, now he's just being picky," you're probably right.

Page 612, Section 15.7.4, Listing 15.23, Test page for the noteplacer XBL binding

The <noteplacer> tag doesn't have an id, but is required in order for the install() function to work. The corrected tag should look like this:

<noteplacer id="test" screenx="1024" screeny="768" scale="4">

This error is fixed in the downloadable code offered by Mr. McFarlane on the publisher's web site.

Page 662, Section 16.5.2, Cache Control

In an explanation of how the cache can be used for prefetching of resources, the author describes HTTP GET requests as follows:

Prefetching only works for http: URLs that are not HTTP GET requests (a request must not have a ?param= part).

An HTTP request can be a GET reqeust with or without any query args. The same is true for a POST request or HEAD request. Args are just an extension of the URL, when it comes right down to it.

So, it's not clear if the cache is actually limited to objects retrieved via a GET request, or only GET requests that don't have a ? in the URL, or to any request without a ? in the URL. I would hope that it actually honors the cache-control (and related) headers that are sent with most HTTP responses.

Page 682, Section 16.6.3.1, Listing 16.12, NoteTaker toolbar changes for data-source-based templates.

The second line of this listing is incorrect, as there is no init_handler function. It says:

window.addEventListener("load", init_handler, true );

It should say:

window.addEventListener("load", init_toolbar, true);
Page 683, Section 16.6.3.2, Edit Dialog Changes

The second paragraph mentions the need to replace datasources="notetaker.rdf" with datasources="rdf:null", but that's not enough. For this to work properly, I also had to change the ref attribute from

ref="http://saturn/test2.html"

to

ref="about:blank"

This same change can be found in Mr. McFarlane's downloadable code.

Page 684, Section 16.6.3.2, Listing 16.13, NoteTaker Dialog changes for data-source-based templates.

There are four errors in this listing (one is extremely minor), which I'll combine here for simplicity. They are:

  • In the first line, don't quote "true".
  • The declaration of the init_dialog function needs to accept an "origin" parameter.
  • Both calls to dialog.getElementById pass the wrong parameter.

I didn't find any errors in the last function, refresh_dialog(), so I've left it out. Here's the code as presented in the book:

window.addEventListener( "load", init_dialog, "true" );
 
function init_dialog()
{
    if ( origin != "timed" ) {
        // avoid running inside any onload handler
        setTimeout("init_dialog('timed')", 1);
    }
    else
    {
        var listbox = document.getElementById('notetaker.keywords');
        listbox.database.AddDataSource(window.opener.noteSession.datasource);
        
        var tree = document.getElementById('notetaker.related');
        tree.database.AddDataSource(window.opener.noteSession.datasource);
        
        refresh_dialog();
    }
}

Here's the corrected, working code:

window.addEventListener( "load", init_dialog, true );
 
function init_dialog( origin )
{
    if ( origin != "timed" )
    {
        // avoid running inside an onload handler
        setTimeout( "init_dialog( 'timed' )", 1 );
    }
    else
    {
        var listbox = document.getElementById( "editDialog.keywords" );
        // logToConsole( window.opener.noteSession.datasource );
        listbox.database.AddDataSource( window.opener.noteSession.datasource );
        
        var tree = document.getElementById( "editDialog.related" );
        tree.database.AddDataSource( window.opener.noteSession.datasource );
        
        refresh_dialog();
    }
}
Page 686, Section 16.6.4, Listing 16.14, NoteTaker script-based query. (1 of 3)

In the first line on page 686, the variable "prop_node" is declared. It's never used. It should have been declared as "pred_node." pred_node is used twice near the end of the listing, but wasn't ever declared.

var matching_node, pred_node, value_node

The alternative is to change pred_node to prop_node in two places in the loop at the end of the listing.

Page 686, Section 16.6.4, Listing 16.14, NoteTaker script-based query. (2 of 3)

There are two "else" blocks at the end of the if/else if/else statements. There should only be one, and the last one is just wasting space.

if ( container.IndexOf( url_node ) != -1 )
{
    matching_node = url_node;
    this.url = url;
    this.chop_query = false;
}
else if ( container.IndexOf( chopped_node ) != -1 )
{
    matching_node = chopped_node;
    this.url = url.replace( /?.*/, "" );
}
else
{
    this.clear();        /**** SEE THE NEXT ITEM ****/
    // this.url = null; /****  FOR EXPLANATION  ****/
    return;             /****  OF THIS CHANGE.  ****/
}
else
    return;
Page 686, Section 16.6.4, Listing 16.14, NoteTaker script-based query. (3 of 3)

The else block sets the note object's url property to null if a note could not be found in the fact store for the current page. Unfortunately, it doesn't set the note's other properties to appropriate default values.

The result is that when you navigate from a page with a note to a page without a note, the fields in the toolbar and the edit window keep the old values (from the page that had a note). If the user clicks the Edit button, the edit window shows the summary, description, and numbers from the last page that had a note. Very confusing.

The fix for this is two-fold: first we'll change the else block to call the previously unused note.clear() method (as shown in the previous item):

else
{
    this.clear();
    // this.url = null;
    return;
}

then we'll update note.clear() (in the Note prototype definition) to reset all of the properties to the defaults:

clear: function() {
    this.url = null;
    this.summary = "";
    this.details = "";
    this.chop_query = true;
    this.home_page = false;
    this.width = 100;
    this.height = 100;
    this.top = 100;
    this.left = 100;
},

Now when the user navigates from a page with a note to one without, all of the fields are cleared out. (Actually, the keyword menu on the toolbar is not cleared, but I haven't figured that out yet.)

Page 687, Section 16.6.4, Scripted RDF Queries Using XPCOM Interfaces

The bottom half of the page discusses the initialization of the datasource property of the NoteSession object. The reader is shown a line from the Notes.js file, and told to change it to something else. In both lines, "rdf." is missing from the front of the call to GetDataSource().

Corrected, the book should instruct the reader to find this line:

this.datasource = rdf.GetDataSource( url.spec );

and replace it with this one:

this.datasource = rdf.GetDataSourceBlocking(url.spec);

Without the "rdf." (which refers to the rdf service initialized earlier in the same script), the script generates an error and execution halts.

Page 690, Section 16.6.6, Listing 16.15, NoteTaker script-based RDF update and save. (1 of 4)

The first 'if' block doesn't work correctly. If note.url is null then the block is entered, but then if the keyword field is empty AND the summary field does not equal note.summary, the secondary 'if' statement is NOT entered. Thus, update_type remains as null, and the next two 'if' statements are skipped. The result is that changes to the note are not always saved to disk.

That first 'if' statement is corrected by merging the two tests.

I really hated the idea that saving a new note from the toolbar would "chop the query," so I disabled that feature. In the code below, I've commented it out and marked it for deletion, but that's not really an error so leave it as it was in the book if you feel differently.

The 'else if' block also synchronizes note.summary to the summary field's value, if there's anything in the field. (note.summary needs to have the current value before being saved to the RDF file later in the code).

Finally, I added an else block to the end of the 'if/else if' block, to catch those cases that slip through and prevent Flush() from being called (at the end of the script) in the untested/unmanaged cases.

The following code replaces lines 4 to 20 on page 690.

if ( note.url != null && ( keyword.value != "" || summary.value != note.summary ) )
{
    update_type = "partial";  // existing note: update keyword, summary
    url_node = rdf.GetResource( note.url );
    note.summary = summary.value;  // get them back into sync
}
else if ( window.content && window.content.document && window.content.document.visited )
{
    update_type = "complete";  // create a new note
    
    // url_node = window.content.document.location.href
    // url_node = url_node.replace( /?.*/, "" );  // toolbar chops query... that's stupid
    url_node = rdf.GetResource( window.content.document.location.href );
    
    // synchronize note.summary to the summary field
    if ( ( ! note.summary || note.summary.length == 0 ) && ( summary.value.length != 0 ) )
    {
        note.summary = summary.value;
    }
}
else return;
Page 690, Section 16.6.6, Listing 16.15, NoteTaker script-based RDF update and save. (2 of 4)

The second 'if' block, which deals with "complete" updates of a note, is really for creating a new note. This is apparent from the fourth line after the 'if' statement, which appends the current page's URL to the RDF's urn:notetaker:notes container.

In fact, that's all this block should do, everything else should be moved to the next 'if' block (which is the generic case and runs as long as update_type has any value).

Why? Because later on, in listing 16.16, the dialog's notetaker-save action is updated to just run the toolbar's notetaker-save. Since the edit dialog can edit an existing note, all of the property-saving code needs to move into that more generic 'if' block. Otherwise, changes made in the edit dialog are lost when the window is closed, because it triggers only a partial save.

Finally, there are cases which are marked as "complete" saves even though the current page's URL is already in the RDF. Therefore, the code has also been updated to test for the URL in the RDF before appending it. Otherwise, additional copies of the URL are added to the urn:notetaker:notes container, but only the first one is ever used. (That doesn't hurt anything, but it's still the wrong thing for it to do. The file will just keep growing and growing every time you save a note from the edit dialog.)

Here's the final 'if' block that deals with "complete" note updates. It's much shorter than what's in the book, and replaces lines 21 to 37 on page 690.

if ( update_type == "complete" )  // set up for creation of a new note
{
    // add the note's url to the note container
    var note_cont = rdf.GetResource( "urn:notetaker:notes" );
    container.Init( noteSession.datasource, note_cont );
    
    // append it only if the URL isn't already listed
    if ( container.IndexOf( url_node ) == -1 )
    {
        container.AppendElement( url_node );
    }
}
Page 690-691, Section 16.6.6, Listing 16.15, NoteTaker script-based RDF update and save. (3 of 4)

(This one's really long. Couldn't be avoided, sorry.)

The third 'if' block runs as long as update_type is not null: it's the generic case, and runs for all note updates.

As mentioned in the previous item, this block needs to handle all updates to existing notes. New notes are created in the previous 'if' block, and changes made to keywords are handled elsewhere, but all other changes must happen here. The edit dialog's save-changes action now runs the toolbar's save-changes action, so we need to handle all of the remaining attributes: summary, details, chop_query, home_page, top, left, width, and height.

This requires a number of changes: simply moving the property-update code from the previous 'if' to this block is not enough, for two reasons: it omits chop_query and home_page, and it adds the properties to the RDF even if they're already there, resulting in duplicate entries (such as, two or more <NT:top> entries in a single <RDF:Description>). Only the first copy of a duplicated entry is used by the code, so it acts like the value wasn't saved. (Open the RDF in a text editor and you'll see the dupes.)

To fix it, the code checks to see if the fact is already in the RDF. If it is, then we call noteSession.datasource.Change, otherwise we still call assert just like before. To make the code a little more readable, I've added a function, assert_singleton(), which is called when looping through the property names.

Finally, at the end of the block, we test to make sure the new keyword is not already in the urn:notetaker:keywords container before adding it, to prevent duplicates.

The completed block of code (from my working copy of NoteTaker) is as follows:

// generic case, common to both types of update: summary, position, size and keywords
if ( update_type != null )
{
    function assert_singleton( property_name )
    {
        var pred = rdf.GetResource( ns + property_name );
        var old_node = noteSession.datasource.GetTarget( url_node, pred, true );
        var new_node = rdf.GetLiteral( note[ property_name ] );
        
        if ( new_node )  // the fact was in the Description
        {
            noteSession.datasource.Change( url_node, pred, old_node, new_node );
        }
        else  // store the new fact
        {
            noteSession.datasource.Assert( url_node, pred, old_node, new_node );
        }
    }
    
    var names = ["summary", "details", "chop_query", "home_page",
                 "top", "left", "width", "height"];
    for ( var i = 0; i < names.length; i++ )
    {
        assert_singleton( names[ i ] );
    }
    
    if ( keyword.value )
    {
        // begin work on a single new keyword
        var keyword_node = rdf.GetResource( "urn:notetaker:keyword:" + keyword.value );
        var keyword_value = rdf.GetLiteral( keyword.value );
        
        // make this keyword related to one other keyword for this note (why?)
        var keyword_pred = rdf.GetResource( ns + "keyword" );
        var related_pred = rdf.GetResource( ns + "related" );
        var keyword2 = noteSession.datasource.GetTarget( url_node, keyword_pred, true );
        if ( keyword2 )
        {
            noteSession.datasource.Assert( keyword_node, related_pred, keyword2, true );
        }
        
        // add the keyword to this note
        noteSession.datasource.Assert( url_node, keyword_pred, keyword_node, true );
        
        // state the keyword itself (set its label)
        var label_pred = rdf.GetResource( ns + "label" );
        noteSession.datasource.Assert( keyword_node, label_pred, keyword_value, true );
        
        // add the keyword to the container listing all keywords
        var keyword_cont = rdf.GetResource( "urn:notetaker:keywords" );
        container.Init( noteSession.datasource, keyword_cont );
        
        // make sure it's not already there
        if ( container.IndexOf( keyword_node ) == -1 )
        {
            // append the keyword to the list
            container.AppendElement( keyword_node );
        }
    }
}
Page 691, Section 16.6.6, Listing 16.15, NoteTaker script-based RDF update and save. (4 of 4)

Finally, this is the last item for listing 16.15. It deals with a simple omission from the very end of the listing.

Review the content_poll() function from your toolbar's main javascript file (toolbar-action.js, probably). You'll see that as long as there is a note to display, and doc.visited isn't set to true, the note will be displayed or refreshed. content_poll() runs every second. Therefore, we don't need to call note.resolve() at the end of listing 16.15, we just need to set window.container.document.visited to false.

Change this:

note.resolve( url_node.Value );
display_note();

to this:

window.content.document.visited = false;
Page 693, Section 16.6.6, Listing 16.16, Improvements to the dialog box notetaker-save command.

In an earlier note, we changed the toolbar's notetaker-save action, and that change included setting note.summary to the current value of the toolbar's summary field, to make sure we're saving the current value (otherwise, the notetaker-save action would often save the old summary).

Listing 16.16 changes the edit dialog to use the toolbar's notetaker-save action. Therefore, we need to make sure that the toolbar's summary field is up to date. (Otherwise note.summary's newly updated value will be overwritten by the toolbar's summary field's old value.)

We could do it directly, by copying note.summary to the toolbar, but there is already a function in the toolbar for handling this: refresh_toolbar(). (This has an additional benefit: the keywords field will also be refreshed, if we ever figure out how to get that part of refresh_toolbar() working correctly.)

The only change is a small addition to the last line of listing 16.16. Insert "refresh_toolbar(); " inside the quotes, just before the word 'execute', like this:

window.opener.setTimeout( 'refresh_toolbar(); execute( "notetaker-save" )', 1 );

You might not consider this an error, but it's a change that I found was required in order to make the code work correctly.

Page 693, Section 16.6.6, Listing 16.17, Command controller for dialog box's RDF keywords

In the second line of the listing, the property '_cmds' is left blank. It should contain a list of all of the commands supported by this controller. None of the other listings fill it in. So, change this:

var keywordController = {
    _cmds : { },

to this:

var keywordController = {
    _cmds : {
        "notetaker-keyword-add":        true,
        "notetaker-keyword-delete":     true,
        "notetaker-keyword-commit":     true,
        "notetaker-keyword-undo-all":   true
    },

Without this change, the keywordController.supportsCommand() function always returns false.

Page 693, Section 16.6.6, Listing 16.17, Command controller for dialog box's RDF keywords (1 of 2)

Although not used by the empty framework set up by this listing, listing 16.18 uses the variables 'Cc' and 'Ci'. Insert the following two lines at the very top of your keywordController.js file:

var Cc = Components.classes;
var Ci = Components.interfaces;

If you don't, then you will be unable to close the edit dialog, because an error is generated before the window is closed.

Page 693, Section 16.6.6, Listing 16.17, Command controller for dialog box's RDF keywords (2 of 2)

In the second line of the listing, the property '_cmds' is left blank. It should contain a list of all of the commands supported by this controller. None of the other listings fill it in. So, change this:

var keywordController = {
    _cmds : { },

to this:

var keywordController = {
    _cmds : {
        "notetaker-keyword-add":        true,
        "notetaker-keyword-delete":     true,
        "notetaker-keyword-commit":     true,
        "notetaker-keyword-undo-all":   true
    },

Without this change, the keywordController.supportsCommand() function always returns false.

Page 697, Section 16.6.6, Listing 16.21, doCommand() initialization with save and delete operations

In the "notetaker-keyword-delete" case (which is most of page 697), there are two alternate behaviors delimited with an if/else statement. The first *entirely* removes keywords and keyword relationships (referred to as "related facts") if the keyword is only used in the current note. Otherwise the keyword is used in other notes, meaning the keyword can't be delete. In that case, those keyword relationships that are due to the current note are deleted.

That's confusing, but so is the logic that spawned this code. ;-)

The error is in the "else" block, which starts about halfway down the page. Look for this line:

else // this keyword is used elsewhere.

Five lines after that 'else' statement is the beginning of a 'while' loop, which is where we find the problem. That 'while' should be closed before the next one begins ten lines later. Instead, the second 'while' is nested inside the first one.

Here's the working version:

else // this keyword is used elsewhere.
{
    // delete related facts where keywords that this keyword
    // relates to are only found in the current note.
    enum1 = this._ds.GetTargets(keyword_node, this._related, true);
    while (enum1.hasMoreElements())
    {
        keyword2 = enum1.getNext().QueryInterface(Ci.nsIRDFNode);
        enum2 = this._ds.GetSources(this._keyword, keyword2, true);
        
        test_node = enum2.getNext().QueryInterface(Ci.nsIRDFNode);
        if (!enum2.hasMoreElements() && test_node.EqualsNode(url_node))
            this._LoggedUnassert(keyword_node, this._related, keyword2);
    }
    
    // delete related facts where keyword that relates to this
    // keyword are only found in the current note.
    enum1 = this._ds.GetSources(this._related, keyword_node, true);
    while (enum1.hasMoreElements())
    {
        keyword2 = enum1.getNext().QueryInterface(Ci.nsIRDFNode);
        enum2 = this._ds.GetSources(this._keyword, keyword2, true);
        
        test_node = enum2.getNext().QueryInterface(Ci.nsIRDFNode);
        if (!enum2.hasMoreElements() && test_node.EqualsNode(url_node))
            this._LoggedUnassert(keyword2, this._related, keyword_node);
    }
}

Note that this also requires removing one of the three closing curly braces at the end of the listing, as we've inserted one after the first while loop.

Unfortunately, this fix does not address what may be a mistake in the logic itself. That is, keywords probably shouldn't be deleted just because they aren't currently being used in a note. The "starter" RDF file has a list of keywords, but they're not all being used in the beginning. The way the keyword delete routine is implemented in the book, all keywords could be deleted (entirely) from the RDF file by first assigning them to notes, and then deleting them from the notes. The keyword popup on the toolbar would then be empty.

Pages 695-698, Section 16.6.6, Bonus Material: My Own (Seth's) Improvements to the Keyword Controller

The book says on page 699, "Astute readers will note that the ur:notetaker:keywords <Seq> should be updated by these -add and -delete commands. We haven't done that because these commands are complicated enough as it is, and some trickery is required to to fit those further updates in with the undo system."

Actually, it wasn't that difficult to implement, and it's important to have this working. Without it, notetaker's behavior is rather weird, as keywords created in the edit dialog are not reflected in the toolbar's popup menu (because the rdf file's Seq isn't updated).

Here's a summary of what you'll need to do. (That should be enough for most readers to figure it out, but please let me know if anything isn't clear.)

Summary

In order to add and remove items from the <Seq> that lists the keywords, we need to use RDF's 'container' api. We've already used it once, in the toolbar-action.js file when we save new notes (the note's url is added to a <Seq>). If you've downloaded Mr. McFarlane's sample code, look in the "Moz14" directory for a file called, "nsIRDFContainer.idl." We'll have to initialize this API in the init() function of the keywordController.js file.

To add items to the <Seq>, we call the AppendElement function. To remove items, we call RemoveElement. However, rather than call these API entry points directly, we need to wrap them in "Logged" functions, to make Undo possible.

_LoggedAppendElement: function ( urn, node )
{
    var cont = this._rdf.GetResource( urn );
    this._container.Init( this._ds, cont );
    
    // make sure the element is not already in the list
    if ( this._container.IndexOf( node ) == -1 )
    {
        // append the element to the list
        this._container.AppendElement( node );
        
        // add it to the undo stack
        this._undo_stack.push( { type: 'append', append: true, surn: urn, anode: node } );
    }
},

_LoggedRemoveElement: function ( urn, node )
{
    var cont = this._rdf.GetResource( urn );
    this._container.Init( this._ds, cont );
    
    // make sure the element is in the list
    if ( this._container.IndexOf( node ) != -1 )
    {
        // remove element from the list
        this._container.RemoveElement( node, true );
        
        // add it to the undo stack
        this._undo_stack.push( { type: 'append', append: false, surn: urn, anode: node } );
    }
},

As you can see in that example, we'll also need to slightly change the format of the _undo_stack, to support more than one kind of action. We'll add a "type" property with each item put on the stack, and that type will tell the 'notetaker-undo-all' action how to process each item. Note that you need to add the "type" attribute _LoggedAssert and _LoggedUnassert, also. Here's my example from _LoggedUnassert:

this._undo_stack.push( { type: 'assert', assert: false, sterm: sub, pterm: pred, oterm: obj } );

Finally, most of the actions in keywordController.js need to be updated to make changes to the urn:notetaker:keywords <Seq> by calling our new "logging" Append and Remove functions. (The only action that doesn't need an update is notetaker-keyword-commit... which is only two lines long!)

Here's the complete, working keywordController.doCommand() function from my own, functional copy of notetaker:

doCommand: function ( cmd ) {
    var url     = window.opener.content.document.location.href;
    var keyword = window.document.getElementById( "editDialog.keyword" ).value;
    
    // don't work with whitespace-only keywords
    if ( keyword.match( /^s*$/ ) ) return;
    
    var keyword_node  = this._rdf.GetResource( "urn:notetaker:keyword:" + keyword );
    var keyword_value = this._rdf.GetLiteral( keyword );
    var url_node      = this._rdf.GetResource( url );
    var test_node, keyword2, enum1, enum2;
    var keyword_cont;
    
    logToConsole( "KeywordController: executing command: " + cmd );
    
    switch ( cmd )
    {
        case "notetaker-keyword-add":
            // relate this keyword to an existing keyword, if there is one
            keyword2 = this._ds.GetTarget( url_node, this._keyword, true );
            if ( keyword2 )
            {
                this._LoggedAssert( keyword_node, this._related, keyword2 );
            }
            
            // give the keyword a label (literal)
            this._LoggedAssert( keyword_node, this._label, keyword_value );
            
            // add this keyword to the current note
            this._LoggedAssert( url_node, this._keyword, keyword_node );
            
            // add the keyword to the container holding all keywords
            this._LoggedAppendElement( "urn:notetaker:keywords", keyword_node );
            
            break;
        case "notetaker-keyword-delete":
            // remove this keyword from the current note
            this._LoggedUnassert( url_node, this._keyword, keyword_node );
            
            // remove this keyword and related facts if it's not used elsewhere
            enum1 = this._ds.GetSources( this._keyword, keyword_node, true );
            if ( ! enum1.hasMoreElements() )
            {
                // 'un-state' the keyword, remove its label
                this._LoggedUnassert( keyword_node, this._label, keyword_value );
                
                // this keyword is related to that keyword
                enum2 = this._ds.GetTargets( keyword_node, this._related, true );
                while ( enum2.hasMoreElements() )
                {
                    this._LoggedUnassert( keyword_node, this._related, enum2.getNext().QueryInterface( Ci.nsIRDFNode ) );
                }
                
                // that keyword is related to this keyword
                enum2 = this._ds.GetSources( this._related, keyword_node, true );
                while ( enum2.hasMoreElements() )
                {
                    this._LoggedUnassert( enum2.getNext().QueryInterface( Ci.nsIRDFNode ), this._related, keyword_node );
                }
                
                // remove the keyword from the master list ()
                this._LoggedRemoveElement( "urn:notetaker:keywords", keyword_node );
            }
            else  // this keyword is used elesewhere
            {
                // delete related facts where a keyword that relates to this keyword
                // are only found in the current note
                // ( sheesh, what a mouthful )
                enum1 = this._ds.GetTargets( keyword_node, this._related, true );
                while ( enum1.hasMoreElements() )
                {
                    keyword2 = enum1.getNext().QueryInterface( Ci.nsIRDFNode );
                    enum2 = this._ds.GetSources( this._keyword, keyword2, true );
                    
                    test_node = enum2.getNext().QueryInterface( Ci.nsIRDFNode );
                    if ( ! enum2.hasMoreElements() && test_node.EqualsNode( url_node ) )
                    {
                        this._LoggedUnassert( keyword_node, this._related, keyword2 );
                    }
                    
                    // delete related facts where a keyword that relates to this
                    // keyword is only found in the current note
                    enum1 = this._ds.GetSources( this._related, keyword_node, true );
                    while ( enum1.hasMoreElements() )
                    {
                        keyword2 = enum1.getNext().QueryInterface( Ci.nsIRDFNode );
                        enum2 = this._ds.GetSources( this._keyword, keyword2, true );
                        
                        test_node = enum2.getNext().QueryInterface( Ci.nsIRDFNode );
                        if ( ! enum2.hasMoreElements() && test_node.EqualsNode( url_node ) )
                        {
                            this._LoggedUnassert( keyword2, this._related, keyword_node );
                        }
                    }
                }
            }
            break;
        case "notetaker-keyword-commit":
            this._undo_stack = [];
            break;
        case "notetaker-keyword-undo-all":
            var undo_item, cont;
            
            logToConsole( "Undo all: " + this._undo_stack.length + " items to undo." );
            
            while ( this._undo_stack.length > 0 )
            {
                undo_item = this._undo_stack.pop();
                switch ( undo_item.type )
                {
                    case "assert":
                        if ( undo_item.assert )
                        {
                            this._ds.Unassert( undo_item.sterm, undo_item.pterm, undo_item.oterm, true );
                        }
                        else
                        {
                            this._ds.Assert( undo_item.sterm, undo_item.pterm, undo_item.oterm, true );
                        }
                        
                        break;
                    case "append":
                        cont = this._rdf.GetResource( undo_item.surn );
                        this._container.Init( this._ds, cont );
                        
                        if ( undo_item.append )
                        {
                            this._container.RemoveElement( undo_item.anode, true );
                        }
                        else
                        {
                            this._container.AppendElement( undo_item.anode );
                        }
                        
                        break;
                }  // switch
            }
            break;
    }
}


TruerWords
is Seth Dillingham's
personal web site.
Truer words were never spoken.