In Mozilla, scripting plays important roles in the XPFE. Whether developers refer to script access and security, user interface logic, XPCOM object invocation, or script execution in element event handlers, scripting is so integral to application development that Mozilla, as a development platform, would be inconceivable without it.
The core scripting language used in Mozilla is JavaScript. Although it has had a reputation as an unsophisticated language used mostly in web pages, JavaScript is more like a first-tier programming language. Modularity, good exception handing, regular expression enhancement, and number formatting are just some features of the new JavaScript 1.5,[*] which is based on the ECMA-262 standard.[*] JavaScript 2.0, due sometime late in 2002, promises to be an even bigger promotion of the language.
Three distinct levels of JavaScript are identified in this chapter. A user interface level manipulates content through the DOM, a client layer calls on the services provided by XPCOM, and, finally, an application layer is available in which JavaScript can create an XPCOM component. The following section describes these levels in detail.
As you have already seen in some examples in this book, the user interface uses JavaScript extensively to create behavior and to glue various widgets together into a coherent whole. When you add code to the event handler of one element to manipulate another-for example, when you update the value of a textbox using a XUL button-you take advantage of this first "level" of scriptability. In this role, JavaScript uses the Document Object Model (DOM) to access parts of the user interface as a hierarchical collection of objects. The section "Adding Scripts to the UI," later in this chapter, discusses this highest level of scripting.
At a second level, JavaScript glues the entire user interface to the XPCOM libraries beneath, which create the application core. At this level, XPConnect (see the section "What Is XPConnect?" later in this chapter) provides a bridge that makes these components "scriptable," which means that they can be invoked from JavaScript and used from the user interface layer. When JavaScript calls methods and gets data from scriptable components, it uses this second layer of scriptability.
Finally, at the third and ultimate level of Mozilla scripting, JavaScript can be used as a "first-order" language for creating the application core itself, for writing software components or libraries whose services are called. We discuss this third level of scripting and provide a long example in the section "Creating a JavaScript XPCOM Component" in Chapter 8.
When you use JavaScript in these contexts, the application architecture looks something like Figure 5-1, in which scripting binds the user interface to the application core through XPConnect and can reside as a software component using such technologies as XPIDL and XPCOM.
In the application layer of Mozilla, there is little distinction between a web page and the graphical user interface. Mozilla's implementation of the DOM is fundamentally the same for both XUL and HTML. In both cases, state changes and events are propagated through various DOM calls, meaning that the UI itself is content-not unlike that of a web page. In application development, where the difference between application "chrome" and rendered content is typically big, this uniformity is a significant step forward.
The DOM is an API used to access HTML and XML documents. It does two things for web developers: provides a structural representation of the document and defines the way the structure should be accessed from script. In the Mozilla XPFE framework, this functionality allows you to manipulate the user interface as a structured group of nodes, create new UI and content, and remove elements as needed.
Because it is designed to access arbitrary HTML and XML, the DOM applies not only to XUL, but also to MathML, SVG, and other XML markup. By connecting web pages and XML documents to scripts or programming languages, the DOM is not a particular application, product, or proprietary ordering of web pages. Rather, it is an API-an interface that vendors must implement if their products are to conform to the W3C DOM standard. Mozilla's commitment to standards ensures that its applications and tools do just that.
When you use JavaScript to create new elements in an HTML file or
change the attributes of a XUL button
, you access an
object model in which these structures are organized. This model is the
DOM for that document or data. The DOM provides a context for the
scripting language to operate in. The specific context for web and XML
documents-the top-level window
object, the elements that
make up a web document, and the data stored in those elements as
children-is standardized in several different specifications, the most
recent of which is the upcoming DOM Level 3 standard.
The DOM specifications are split into different levels overseen by the W3C. Each level provides its own features and Mozilla has varying, but nearly complete, levels of support for each. Currently, Mozilla's support for the DOM can be summarized as follows:
Mozilla strives to be standards-compliant, but typically reaches full support only when those standards have become recommendations rather than working drafts. Currently, Level 1 and Level 2 are recommendations and Level 3 is a working draft.
Standards like the DOM make Mozilla an especially attractive software development kit (SDK) for web developers. The same layout engine that renders web content also draws the GUI and pushes web development out of the web page into the application chrome. The DOM provides a consistent, unified interface for accessing all the documents you develop, making the content and chrome accessible for easy cross-platform development and deployment.
Methods in
the DOM allow you to access and manipulate any element in the user
interface or in the content of a web page. Getting and setting
attributes, creating elements, hiding elements, and appending children
all involve direct manipulation of the DOM. The DOM mediates all
interaction between scripts and the interface itself, so even when you
do something as simple as changing an image when the user clicks a
button, you use the DOM to register an event handler with the button
and DOM attributes on the image
element to change its
source.
The DOM Level 1 and Level 2 Core specifications contain multiple interfaces, including Node, NodeList, Element, and Document. The following sections describe some interface methods used to manipulate the object model of application chrome, documents, or metadata in Mozilla. The Document and Element interfaces, in particular, contain useful methods for XUL developers.
The code
samples in this chapter use a method called dump( )
to
print data to STDOUT. This method is primarily used for debugging your
code and is turned on using a PREF
. You can turn this PREF
on using the following code:
const PREFS_CID = "@mozilla.org/preferences;1";
const PREFS_I_PREF = "nsIPref";
const PREF_STRING = "browser.dom.window.dump.enabled";
try {
var Pref = new Components.Constructor(PREFS_CID, PREFS_I_PREF);
var pref = new Pref( );
pref.SetBoolPref(PREF_STRING, true);
} catch(e) {}
This code is necessary only if you are doing development with a
release distribution build of Mozilla. If you are using a debug or
nightly build, this PREF
can be set from the preferences
panel by selecting Edit > Preferences > Debug > Enable
JavaScript dump( ) output.
getElementById(aId)
is
perhaps the most commonly used DOM method in any programming domain.
This is a convenient way to get a reference to an element object by
passing that element's id
as an argument, where the id
acts as a unique identifier for that element.
DOM calls like this are at the heart of Mozilla UI functionality. getElementById
is the main programmatic entry point into the chrome and is essential
for any dynamic manipulation of XUL elements. For example, to get a box
element in script (i.e., to get a reference to it so you can call its
methods or read data from it), you must refer to it by using the box id
:
<box id="my-id" />
Since the return value of getElementById
is a reference
to the specified element object, you usually assign it to a variable
like this:
var boxEl = document.getElementById('my-id');
dump("boxEl="+boxEl+"\n");
console output: boxEl=[object XULElement]
Once you have the box element available as boxEl
, you
can use other DOM methods like getAttribute
and setAttribute
to change its layout, its position, its state, or other features.
Attributes
are properties that are defined directly on an element. XUL elements
have attributes such as disabled
, height
, style
,orient
,
and label
.
<box id="my-id" foo="hello 1" bar="hello 2" />
In the snippet above, the strings "my-id," "hello 1," and "hello 2" are values of the box element attributes. Note that Gecko does not enforce a set of attributes for XUL elements. XUL documents must be well-formed, but they are not validated against any particular XUL DTD or schema. This lack of enforcement means that attributes can be placed on elements ad hoc. Although this placement can be confusing, particularly when you look at the source code for the Mozilla browser itself, it can be very helpful when you create your own applications and want to track the data that interests you.
Once you have an object assigned to a variable, you can use the DOM
method getAttribute
to get a reference to any attribute in
that object. The getAttribute
method takes the name of the
desired attribute as a string. For example, if you add an attribute
called foo
to a box element, you can access that
attribute's value and assign it to a variable:
<box id="my-id" foo="this is the foo attribute" />
<script>
var boxEl = document.getElementById('my-id');
var foo = boxEl.getAttribute('foo');
dump(foo+'\n');
</script>
The dump
method outputs the string "this is the foo
attribute," which is the value of the attribute foo
. You
can also add or change existing attributes with the setAttribute
DOM method.
The setAttribute
method changes an existing attribute value. This method is useful for
changing the state of an element-its visibility, size, order within a
parent, layout and position, style, etc. It takes two arguments: the
attribute name and the new value.
<box id="my-id" foo="this is the foo attribute" />
<script>
boxEl=document.getElementById('my-id');
boxEl.setAttribute('foo', 'this is the foo attribute changed');
var foo = boxEl.getAttribute('foo');
dump(foo+'\n');
</script>
The script above outputs the string "this is the foo attribute
changed" to the console. You can also use setAttribute
to
create a new attribute if it does not already exist:
<box id="my-id" />
<script>
boxEl=document.getElementById('my-id');
boxEl.setAttribute('bar', 'this is the new attribute bar');
</script>
By setting an attribute that doesn't already exist, you create it
dynamically, adding a value to the hierarchical representation of nodes
that form the current document object. After this code is executed, the boxEl
element is the same as an element whose bar
attribute was
hardcoded into the XUL:
<box id="my-id" bar="this is the new attribute bar" />
These sorts of ad hoc changes give you complete control over the state of the application interface.
If you
need to dynamically create an element that doesn't already exist-for
example, to add a new row to a table displaying rows of information, you
can use the method createElement
. To create and add a text
element to your box example, for example, you can use the following
code:
<box id="my-id" />
<script>
boxEl = document.getElementById('my-id');
var textEl = document.createElement('description');
boxEl.appendChild(textEl);
</script>
Once you create the new element and assign it to the textEl
variable, you can use appendChild
to insert it into the
object tree. In this case, it is appended to boxEl
, which
becomes the insertion point.
For mixed namespace documents like XUL and HTML, you can use a
variation of createElement
called createElementNS
.
To create a mixed namespace element, use this code:
var node =
document.createElementNS('http://www.w3.org/1999.xhtml', 'html:div');
Namespace variations for other functions include setAttributeNS
,getElementsByTagNameNS
,
and hasAttributeNS
.
In
addition to setting the label
attribute on an element, you
can create new text in the interface by using the DOM method createTextNode
,
as shown in the following example:
<description id="explain" />
<script>
var description = document.getElementById("explain");
if (description) {
if (!description.childNodes.length) {
var textNode = document.createTextNode("Newly text");
description.appendChild(textNode);
}
else if (description.childNodes.length == 1 ) {
description.childNodes[0].nodeValue = "Replacement text";
}
}
</script>
Notice the use of appendChild
. This method, discussed
next, is used to insert the new element or text node into the DOM tree
after it is created. Create-and-append is a common two-step process for
adding new elements to the object model.
To dynamically
add an element to a document, you need to use the method appendChild(
)
. This method adds a newly created element to an existing
parent node by appending to it. If a visible widget is added, this
change is visible in the interface immediately.
<groupbox id="my-id" />
<script>
var existingEl = document.getElementById('my-id');
var captionEl = document.createElement('caption');
existingEl.appendChild(captionEl);
captionEl.setAttribute('label', 'This is a new caption');
captionEl.setAttribute('style', 'color: blue;');
</script>
This example creates a new element, gets an existing parent element
from the document, and then uses appendChild( )
to insert
that new element into the document. It also uses setAttribute
to add an attribute value and some CSS style rules, which can highlight
the new element in the existing interface.
For
elements that already exist, a copy method allows you to duplicate
elements to avoid having to recreate them from scratch. cloneNode
,
which is a method on the element
object rather than the document
,
returns a copy of the given node.
<script>
// this is untested --pete
var element = document.getElementById('my-id');
var clone = element.cloneNode(false);
dump(`element='+element+'\n');
dump(`clone='+clone+'\n');
</script>
The method takes a Boolean-optional parameter that specifies whether the copy is "deep." Deep copies duplicate all descendants of a node as well as the node itself.
Another
very useful method is getElementsByTagName
. This method
returns an array of elements of the specified type. The argument used is
the string element type. "box," for example, could be used to
obtain an array of all boxes in a document. The array is zero-based, so
the elements start at 0 and end with the last occurrence of the element
in the document. If you have three boxes in a document and want to
reference each box, you can do it as follows:
<box id="box-one" />
<box id="box-two" />
<box id="box-three" />
<script>
document.getElementsByTagName('box')[0];
document.getElementsByTagName('box')[1];
document.getElementsByTagName('box')[2];
</script.
Or you can get the array and index into it like this:
var box = document.getElementsByTagName('box');
box[0], the first object in the returned array, is a XUL box.
To see the number of boxes on a page, you can use the length
property of an array:
var len = document.getElementsByTagName('box').length;
dump(l+'\n');
console output: 3
To output the id
of the box:
<box id="box-one" />
<box id="box-two" />
<box id="box-three" />
<script>
var el = document.getElementsByTagName('box');
var tagId = el[0].id;
dump(tagId+"\n");
</script>
console output: box-one
To get to an attribute of the second box:
<box id="box-one" />
<box id="box-two" foo="some attribute for the second box" />
<box id="box-three" />
<script>
var el = document.getElementsByTagName('box');
var att = el[1].getAttribute('foo');
dump(att +"\n");
</script>
console output: some attribute for the second box
getElementsByTagName
is a handy way to obtain DOM
elements without using getElementById
. Not all elements
have id
attributes, so other means of getting at the
elements must be used occasionally.[*]
In addition
to a basic set of attributes, an element may have many properties. These
properties don't typically appear in the markup for the element, so they
can be harder to learn and remember. To see the properties of an element
object node, however, you can use a JavaScript for
in
loop to iterate through the list, as shown in Example
5-1.
Example 5-1: Printing element properties to the console
<box id="my-id" />
<script>
var el = document.getElementById('my-id');
for (var list in el)
dump("property = "+list+"\n");
</script>
console output(subset):
property = id
property = className
property = style
property = boxObject
property = tagName
property = nodeName
. . .
Note the implicit functionality in the el
object
itself: when you iterate over the object reference, you ask for all
members of the class of which that object is an instance. This simple
example "spells" the object out to the console. Since the DOM recognizes
the window as another element (albeit the root element) in the Document
Object Model, you can use a similar script in Example
5-2 to get the properties of the window itself.
Example 5-2: Printing the window properties
<script>
var el = document.getElementById('test-win');
for(var list in el)
dump("property = "+list+"\n");
</script>
console output(subset):
property = nodeName
property = nodeValue
property = nodeType
property = parentNode
property = childNodes
property = firstChild
. . .
The output in Example 5-2 is a small subset of
all the DOM properties associated with a XUL window and the other XUL
elements, but you can see all of them if you run the example. Analyzing
output like this can familiarize you with the interfaces available from window
and other DOM objects.
You can also use a
DOM method to access elements with specific properties by using getElementsByAttribute
.
This method takes the name and value of the attribute as arguments and
returns an array of nodes that contain these attribute values:
<checkbox id="box-one" />
<checkbox id="box-two" checked="true"/>
<checkbox id="box-three" checked="true"/>
<script>
var chcks = document.getElementsByAttribute("checked", "true");
var count = chcks.length;
dump(count + " items checked \n");
</script>
One interesting use of this method is to toggle the state of
elements in an interface, as when you get all menu items whose disabled
attribute is set to true and set them to false. In the xFly sample, you
can add this functionality with a few simple updates. In the xfly.js
file in the xFly package, add the function defined in Example
5-3.
Example 5-3: Adding toggle functionality to xFly
function toggleCheck( ) {
// get the elements before you make any changes
var chex = document.getElementsByAttribute("disabled", "true");
var unchex = document.getElementsByAttribute("disabled", "false");
for (var i=0; i<chex.length; i++)
chex[i].setAttributte("checked", "false");
for (var i=0; i<unchex.length; i++)
unchex[i].setAttributte("checked", "true");
}
Although this example doesn't update elements whose disabled
attribute is not specified, you can call this function from a new menu
item and have it update all menus whose checked state you do monitor, as
shown in Example 5-4.
Example 5-4: Adding Toggle menus to xFly
<menubar id="appbar">
<menu label="File">
<menupopup>
<menuitem label="New"/>
<menuitem label="Open"/>
</menupopup>
</menu>
<menu label="Edit">
<menupopup>
<menuitem label="Toggle" oncommand="toggleCheck( );" />
</menupopup>
</menu>
<menu label="Fly Types">
<menupopup>
<menuitem label="House" disabled="true" />
<menuitem label="Horse" disabled="true" />
<menuitem label="Fruit" disabled="false" />
</menupopup>
</menu>
</menubar>
When you add this to the xFly application window (from Example 2-10, for example, above the basic vbox
structure), you get an application menu bar with a menu item, Toggle,
that reverses the checked state of the three items in the "Fly Types"
menu, as seen in Figure 5-2.
The following section explains more about hooking scripts up to the
interface. Needless to say, when you use a method like getElementsByAttribute
that operates on all elements with a particular attribute value, you
must be careful not to grab elements you didn't intend (like a button
elsewhere in the application that gets disabled for other purpose).
Once you are comfortable with how JavaScript works in the context of the user interface layer and are familiar with some of the primary DOM methods used to manipulate the various elements and attributes, you can add your own scripts to your application. Though you can use other techniques to get scripts into the UI, one of the most common methods is to use Mozilla's event model, which is described in the next few sections.
Events are input messages that pass information from the user interface to the application code. Capturing this information, or event handling, is how you usually tell scripts when to start and stop.
When the user clicks a XUL button, for instance, the button "listens" for the click event, and may also handle that event. If the button itself does not handle the event (e.g., by supplying executable JavaScript in an event handler attribute), then the event "bubbles," or travels further up into the hierarchy of elements above the button. The event handlers in Example 5-3 use simple inline JavaScript to show that the given event (e.g., the window loading in the first example, the button getting clicked in the second, and so on) was fired and handled.
As in HTML, predefined event handlers are available as attributes on a XUL element. These attributes are entry points where you can hook in your JavaScript code, as these examples show. Note that event handler attributes are technically a shortcut, for which the alternative is to register event listeners explicitly to specified elements. The value of these on[event] event handler attributes is the inline JavaScript that should be executed when that event is triggered. Example 5-5 shows some basic button activation events.
Example 5-5: Basic event handler attributes
<window onload="dump('this window has loaded\n');" />
<button label="onclick-test"
onclick="dump('The event handler onclick has just been used\n');" />
<button label="oncommand-test"
oncommand="dump('The event handler oncommand has just been used\n');" />
<menulist id="custom"
onchange="doMyCustomFunction( );" />
While the window and button events in Example 5-5
carry out some inline script, there is a variation with the onchange
handler attached to the menulist
element. onchange
contains a JavaScript function call whose definition may live in the XUL
document itself or in an external file that is included by using the src
attribute on a script
element:
<script type="application/x-javascript" src="chrome://mypackage/content/myfile.js" />
A large basic set of event handler attributes is available for use on XUL elements (and HTML elements). Appendix C has a full listing of these events along with explanations. The following subset shows the potential for script interaction when the UI uses event handlers:
Some of these event handlers work only on particular elements, such
as window
, which listens for the load
event,
the paint
event, and other special events.
To see all event handler attributes on a particular element, you can
execute the short script in Example 5-6, which
uses the for
in
loop in JavaScript to iterate
over the members of an object-in this
case, a XUL element.
Example 5-6: Getting event handler attributes from an element
<script type="application/x-javascript">
function listElementHandlers(aObj)
{
if(!aObj)
return null;
for(var list in aObj)
if(list.match(/^on/))
dump(list+'\n');
}
</script>
<button label="oncommand" oncommand="listElementHandlers(this);" />
The function you added in Example 5-4 is also an example of event handler code in an application's interface.
The event model in Mozilla is the general framework for how events work and move around in the user interface. As you've already seen, events tend to rise up through the DOM hierarchy-a natural process referred to as event propagation or event bubbling. The next two sections describe event propagation and its complement, event capturing.
This availability of events in
nodes above the element of origin is known as event propagation or event
bubbling. Event bubbling means you can handle events anywhere above the
event-raising element in the hierarchy. When events are handled by
elements that did not initiate those events, you must determine which
element below actually raised the event. For example, if an event
handler in a menu element handles an event raised by one of the menu
items, then the menu should be able to identify the raising element and
take the appropriate action, as shown in Example 5-7.
In this example, a JavaScript function determines which menuitem
was selected and responds appropriately.
Example 5-7: Event propagation
<script type="application/x-javascript">
function doCMD(el) {
v = el.getAttribute("label")
switch (v) {
case "New":
alert('New clicked');
break;
case "Open":
alert('Open clicked');
break;
case "Close":
alert('Close clicked');
break;
}
}
</script>
...
<menu class="menu" label="File" oncommand="doCMD(event.target)">
<menupopup>
<menuitem label="New" />
<menuitem label="Open" />
<menuitem label="Close" />
</menupopup>
</menu>
The event handler in the parent node menu finds out which child menuitem
was actually clicked by using event.target
and takes action
accordingly. Let's walk through another possible scenario. If a user of
an application selects an item from a menu list, you could get the node
of that item by using event.target
. Your script could then
abstract that item's value or other information, if necessary.
When an
event is raised, it is typically handled by any node interested in it as
it continues its way up the DOM hierarchy. In some cases, you may want
to handle an event and then prevent it from bubbling further up, which
is where the DOM Event
method stopPropagation( )
comes in handy.
Example 5-8 demonstrates how event bubbling can
be arrested very simply. When the XUL document in Example
5-8 loads, an event listener is registered with a row
in the tree. The event listener handles the event by executing the
function stopEvent( )
. This function calls an event object
method, stopPropagation
, which keeps the event from
bubbling further up into the DOM. Note that the tree itself has an onclick
event handler that should display a message when clicked. However, the stopEvent(
)
method has stopped propagation, so after the data in the table
is updated, the event phase is effectively ended. In this case, the
function was used to trap the event and handle it only there.
Example 5-8: stopPropagation( ) event function
<?xml version="1.0"?>
<!DOCTYPE window>
<window id="test-win"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
orient="vertical"
onload="load( );">
<script type="application/x-javascript">
function load( ) {
el = document.getElementById("t");
el.addEventListener("click", stopEvent, false);
}
function stopEvent(e) {
// this ought to keep t-daddy from getting the click.
e.stopPropagation( );
}
</script>
<tree>
<!-- tree columns definition omitted -->
<treechildren flex="1" >
<treeitem id="t-daddy"
onclick="alert('t-daddy');" // this event is never fired
container="true" parent="true">
<treerow id="t">
<treecell label="O'Reilly" id="t1" />
<treecell label="http://www.oreilly.com" id="t2" />
</treerow>
</treeitem>
</treechildren>
</tree>
</window>
Event
capturing is the complement of event bubbling. The DOM provides the addEventListener
method for creating event listeners on nodes that do not otherwise
supply them. When you register an event listener on an ancestor of the
event target (i.e., any node above the event-raising element in the node
hierarchy), you can use event capturing to handle the event in the
ancestor before it is heard in the target itself or any intervening
nodes.
To take advantage of event capturing (or event bubbling with
elements that do not already have event listeners), you must add an
event listener to the element that wants to capture events occurring
below it. Any XUL element may use the DOM addEventListener
method to register itself to capture events. The syntax for using this
method in XUL is shown here:
XULelement = document.getElementById("id of XULelement");
XULelement.addEventListener("event name", "event handler code",
useCapture bool);
The event handler code argument can be inline code or the name of a
function. The useCapture
parameter specifies whether the event listener wants to use event
capturing or be registered to listen for events that bubble up the
hierarchy normally. In Figure 5-3, the alert
dialog invoked by the menuitem
itself is not displayed,
since the root window
element used event capture to handle
the event itself.
An onload
event handler for a XUL window can also register a box
element to capture all click events that are raised from its child
elements:
var bbox = document.getElementById("bigbox");
if (bbox) {
bbox.addEventListener("click", "alert('captured')", true);
}
...
<box id="bigbox">
<menu label="File">
<menupopup>
<menuitem label="New" onclick="alert('not captured')" />
...
<menupopup>
</menu>
</box>
Much of what makes the Mozilla UI both flexible and programmable is its ability to dynamically alter the CSS style rules for elements at runtime. For example, if you have a button, you can toggle its visibility by using a simple combination of JavaScript and CSS. Given a basic set of buttons like this:
<button id="somebutton" class="testButton" label="foo" />
<spacer flex="1" />
<button id="ctlbutton"
class="testButton"
label="make disappear"
oncommand="disappear( );" />
as well as a stylesheet import statement at the top of the XUL like this:
<?xml-stylesheet href="test.css" type="text/css"?>
and a simple CSS file in your chrome/xfly/content directory called test.css that contains the following style rule:
#somebutton[hidden="true"]{ display: none; }
.testButton{
border : 1px outset #cccccc;
background-color : #cccccc;
padding : 4px;
margin : 50px;
}
You can call
setAttribute
in your script to hide the button at runtime.
<script>
function disappear( ){
return document.getElementById('somebutton').setAttribute('hidden', true);
}
</script>
The previous code snippet makes a visible button disappear by
setting its hidden
attribute to true. Adding a few more
lines, you can toggle the visibility of the button, also making it
appear if it is hidden:
<script>
function disappear( ){
const defaultLabel = "make disappear";
const newLabel = "make reappear";
var button = document.getElementById('somebutton');
var ctlButton = document.getElementById('ctlbutton');
if(!button.getAttribute('hidden')) {
button.setAttribute('hidden', true);
ctlButton.setAttribute('label', newLabel);
} else {
button.removeAttribute('hidden');
ctlButton.setAttribute('label', defaultLabel);
}
return;
}
</script>
Another useful application of this functionality is to collapse elements such as toolbars, boxes, and iframes in your application.
The setAttribute
method can also be used to update the
element's class
attribute with which style rules are so
often associated. toolbarbutton-1
and button-toolbar
are two different classes of button. You can change a button from a toolbarbutton-1-
the
large button used in the browser-to a standard toolbar button using the
following DOM code:
// get the Back button in the browser
b1 = document.getElementById("back-button");\
b1.setAttribute("class", "button-toolbar");
This dynamically demotes the Back button to an ordinary toolbar button. Code such as this assumes, of course, that you know the classes that are used to style the various widgets in the interface.
You can also set the style
attribute directly using the
DOM:
el = document.getElementById("some-element");
el.setAttribute("style", "background-color:darkblue;");
Be aware, however, that when you set the style
attribute in this way, you are overwriting whatever style properties may
already have been defined in the style
attribute. If the
document referenced in the snippet above by the ID some-element
has a style
attribute in which the font size is set to
18pc, for example, that information is erased when the style attribute
is manipulated in this way.
Using the createElement
method in XUL lets you accomplish things similar to document.write
in HTML, with which you can create new pages and parts of a web page. In Example 5-9,
createElement
is used to generate a menu dynamically.
Example 5-9: Dynamic menu generation
<?xml version="1.0"?>
<?xml-stylesheet href="test.css" type="text/css"?>
<!DOCTYPE window>
<window id="test-win"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="test"
style="
min-width : 200px;
min-height: 200px;">
<script>
<![CDATA[
function generate( ){
var d = document;
var popup = d.getElementById('menupopup');
var menuitems = new Array('menuitem_1',
'menuitem_2', 'menuitem_3',
'menuitem_4', 'menuitem_5');
var l = menuitems.length;
var newElement;
for(var i=0; i<l; i++)
{
newElement = d.createElement('menuitem');
newElement.setAttribute('id', menuitems[i]);
newElement.setAttribute('label', menuitems[i]);
popup.appendChild(newElement);
}
return true;
}
]]>
</script>
<menu label="a menu">
<menupopup id="menupopup">
</menupopup>
</menu>
<spacer flex="1" />
<button id="ctlbutton" class="testButton" label="generate" oncommand="generate( );" />
</window>
The JavaScript function
generate( )
in Example 5-9 gets the menupopup
as the parent element for the new elements, creates five menuitems
in an array called menuitems
, and stores five string ID
names for those menuitems
.
The variable l is the length of the array. The variable newElement
is a placeholder for elements created by using the createElement
method inside of the for
loop. generate( )
assigns newElement
on each iteration of the loop and
creates a new menuitem
each time, providing a way to
dynamically generate a list of menu choices based on input data or user
feedback. Try this example and experiment with different sources of
data, such as a menu of different auto manufacturers, different styles
on group of boxes that come from user selection, or tabular data in a
tree.
As the scale of your application development increases and your applications grow new windows and components, you may become interested in passing data around and ensuring that the data remains in scope. Misunderstanding that scope often leads to problems when beginning Mozilla applications.
The general rule is that all scripts pulled in by the base XUL document and scripts included in overlays of this document are in the same scope. Therefore, any global variables you declare in any of these scripts can be used by any other scripts in the same scope. The decision to put a class structure or more sophisticated design in place is up to you.
The relationship of a parent and child window indicates the importance of storing data in language constructs that can be passed around. This code shows a common way for a parent to pass data to a window it spawns:
var obj = new Object ( );
obj.res = "";
window.openDialog("chrome://xfly/content/foo.xul", 'foo_main',
"chrome,resizable,scrollbars,dialog=yes,close,modal=yes",
obj);
The previous
code snippet creates a new JavaScript object, obj
, and
assigns the value of an empty string to that object's res
property. The object is then passed by reference to the new window as
the last parameter of the openDialog( )
method so it can be
manipulated in the scope of the child window:
function onOk( ) {
window.arguments[0].res = "ok";
return;
}
function onCancel( ) {
window.arguments[0].res = "cancel";
return;
}
In that child window, the object is available as an indexed item in
the special window.arguments
array. This array holds a list
of the arguments passed to a window when it is created.
window.arguments[0] is a reference to the first argument in the openDialog(
)
parameter list that is not a part of the input parameters for
that method, window.arguments[1] is the second argument, and so on.
Using window.arguments
is the most common way to pass
objects and other data around between documents.
When the user clicks a button in the displayed dialog (i.e., the OK
or Cancel button), one of the functions sets a value to the res
property of the passed-in object. The object is in the scope of the
newly created window. When control is passed back to the script that
launched the window, the return value can be checked:
if (obj.res != "ok") {
dump("User has cancelled the dialog");
return;
}
In this case, a simple dump statement prints the result, but you can also test the result in your application code and fork accordingly.
At the second level of scripting, XPConnect binds JavaScript and the user interface to the application core. Here, JavaScript can access all XPCOM components that implement scriptable libraries and services through a special global object whose methods and properties can be used in JavaScript. Consider these JavaScript snippets from the Mozilla source code:
// add filters to the file picker
fp.appendFilters( nsIFilePicker.HTML );
// display a directory in the file picker
fp.displayDirectory ( dir );
// read a line from an open file
file.readLine(tmpBuf, 1024, didTruncate);
// create a new directory
this.fileInst.create( DIRECTORY, parseInt(permissions) );
retval=OK;
The filepicker
, file
, and localfile
components that these JavaScript objects represent are a tiny fraction
of the components available via XPConnect to programmers in Mozilla.
This section describes how to find these components, create the
corresponding JavaScript objects, and use them in your application
programming.
Until now, scripting has referred to scripting the DOM, manipulating various elements in the interface, and using methods available in Mozilla JavaScript files. However, for real applications like the Mozilla browser itself, this may be only the beginning. The UI must be hooked up to the application code and services (i.e., the application's actual functionality) to be more than just a visual interface. This is where XPConnect and XPCOM come in.
Browsing the Web, reading email, and parsing XML files are examples of application-level services in Mozilla. They are part of Mozilla's lower-level functionality. This functionality is usually written and compiled in platform-native code and typically written in C++. This functionality is also most often organized into modules, which take advantage of Mozilla's cross-platform component object model (XPCOM), and are known as XPCOM components. The relationship of these components and the application services they provide to the interface is shown in Figure 5-4.
In Mozilla, XPConnect is the bridge between JavaScript and XPCOM components. The XPConnect technology wraps natively compiled components with JavaScript objects. XPCOM, Mozilla's own cross-platform component technology, is the framework on top of which these scriptable components are built. Using JavaScript and XPConnect, you can create instances of these components and use their methods and properties as you do any regular JavaScript object, as described here. You can access any or all of the functionality in Mozilla in this way.
Chapter 8 describes more about the XPConnect technology and how it connects components to the interface. It also describes the components themselves and their interfaces, the XPCOM technology, and how you can create your own XPCOM components.
Example 5-10 demonstrates
the creation and use of an XPCOM component
in JavaScript. In this example, the script instantiates the filepicker
object and then uses it to display a file picker dialog with all of the
file filters selected. To run this example, add the function to your xfly.js
file and call it from an event handler on the "New" menu item you added
in Example 3-5. Example 5-10 Scriptable component example
// chooseApp: Open file picker and prompt user for application.
chooseApp: function( ) {
var nsIFilePicker = Components.interfaces.nsIFilePicker;
var fp =
Components.classes["@mozilla.org/filepicker;1"].
createInstance( nsIFilePicker );
fp.init( this.mDialog,
this.getString( "chooseAppFilePickerTitle" ),
nsIFilePicker.modeOpen );
fp.appendFilters( nsIFilePicker.filterAll );
if ( fp.show( ) == nsIFilePicker.returnOK && fp.file ) {
this.choseApp = true;
this.chosenApp = fp.file;
// Update dialog.
this.updateApplicationName(this.chosenApp.unicodePath);
}
Note the first two lines in the function and the way they work
together to create the fp
filepicker
object.
The first line in the function assigns the name of the nsFile-picker
interface to the nsIFilePicker
variable in JavaScript. This
variable is used in the second line, where the instance is created from
the component to specify which interface on that component should be
used. Discovering and using library interfaces is an important aspect of
XPCOM, where components always implement at least two interfaces.
In Example 5-11, an HTML file (stored locally, since it wouldn't have the required XPConnect access as a remote file because of security boundaries) loaded in Mozilla instantiates a Mozilla sound component and plays a sound with it. Go ahead and try it. Example 5-11 Scripting components from HTML
<html>
<head>
<title>Sound Service Play Example</title>
</head>
<body>
<script>
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
var url = Components.classes["@mozilla.org/network/standard-
url;1"].createInstance( );
url = url.QueryInterface(Components.interfaces.nsIURL);
url.spec = "resource:/res/samples/test.wav";
var sample = Components.classes["@mozilla.org/sound;1"].createInstance( );
sample = sample.QueryInterface(Components.interfaces.nsISound);
</script>
<form name="form">
<input type="button" value="Play Sound" onclick="sample.play(url);">
<form>
</body>
</html>
As in Example 5-10, the classes[ ] array on
the special Mozilla Components
object refers to a
particular component-in this case, the sound
component-by
contract ID. All XPCOM objects must have a contract ID that uniquely
identifies them with the domain, the component name, and a version
number ["@mozilla.org/sound;1"], respectively. See the "XPCOM Identifiers" section in Chapter 8 for more information about this.
Most components
are scripted in Mozilla. In fact, the challenge is not to find cases
when this scripting occurs (which you can learn by searching LXR for the Components
),
but to find Mozilla components that don't use scriptable components.
Finding components and interfaces in Mozilla and seeing how they are
used can be useful when writing your own application.
The Mozilla Component Viewer is a great tool for discovering components and provides a convenient UI for seeing components and looking at their interfaces from within Mozilla. The Component Viewer can be built as an extension to Mozilla (see "cview" in the extensions directory of the Mozilla source), or it can be downloaded and installed as a separate XPI from http://www.hacksrus.com/~ginda/cview/.Appendix B describes the Component Viewer in more detail.
Commonly used XPCOM objects in the browser and other Mozilla applications include file objects, RDF services, URL objects, and category managers.
In all cases,
the way to get the object into script is to instantiate it with the
special classes
object and use the createInstance( )
method on the class to select the interface you want to use. These two
steps are often done together, as in the following example, which gets
the component with the contract ID ldap-connection;1
,
instantiates an object from the nsILDAPConnection interface,
and then calls a method on that object:
var connection = Components.classes
["@mozilla.org/network/ldap-connection;1"].
createInstance(Components.interfaces.nsILDAPConnection);
connection.init(queryURL.host, queryURL.port, null,
generateGetTargetsBoundCallback( ));
These two common processes-getting a component and selecting one of its interfaces to assign to an object-can also be separated into two different statements:
// get the ldap connection component
var connection = Components.classes
["@mozilla.org/network/ldap-connection;1";
// create an object from the nsILDAPConnection interface;
connection.createInstance(Components.interfaces.nsILDAPConnection);
// call the init( ) method on that object
connection.init(queryURL.host, queryURL.port, null,
generateGetTargetsBoundCallback( ));
Mozilla constantly uses these processes. Wherever functionality is organized into XPCOM objects (and most of it is), these two statements bring that functionality into JavaScript as high-level and user-friendly JavaScript objects.
There are two ways to use JavaScript in the third, deepest level of application programming. The first is to organize your JavaScript into libraries so your functions can be reused, distributed, and perhaps collaborated upon.
The second way is to write a JavaScript component, create a separate interface for that component, and compile it as an XPCOM component whose methods and data can be accessed from XPConnect (using JavaScript). This kind of application programming is described in Chapter 8, which includes examples of creating new interfaces, implementing them in JavaScript or C++, and compiling, testing, and using the resulting component in the Mozilla interface.
This section introduces the library organization method of JavaScript application programming. The JSLib code discussed here is a group of JavaScript libraries currently being developed by Mozilla contributors and is especially useful for working with the XPFE and other aspects of the Mozilla application/package programming model. When you include the right source files at the top of your JavaScript and/or XUL file, you can use the functions defined in JSLib libraries as you would use any third-party library or built-in functions. You may even want to contribute to the JSLib project yourself if you think functionality is missing and as your Mozilla programming skills grow.
The open
source JSLib project makes life easier for developers. The JSLib package
implements some of the key XPCOM components just discussed and wraps
them in simpler, JavaScript interfaces, which means that you can use the
services of common XPCOM components without having to do any of the
instantiation, interface selection, or glue code yourself. Collectively,
these interfaces are intended to provide a general-purpose library for
Mozilla application developers. To understand what JSLib does, consider
the following short snippet from the JSLib source file jslib/io/file.js,
which implements a close( )
function for open file objects
and provides a handy way to clean up things when you finish editing a
file in the filesystem.
/********************* CLOSE ********************************
* void close( ) *
* *
* void file close *
* return type void(null) *
* takes no arguments closes an open file stream and *
* deletes member var instances of objects *
* Ex: *
* var p='/tmp/foo.dat'; *
* var f=new File(p); *
* fopen( ); *
* f.close( ); *
* *
* outputs: void(null) *
************************************************************/
File.prototype.close = function( )
{
/***************** Destroy Instances *********************/
if(this.mFileChannel) delete this.mFileChannel;
if(this.mInputStream) delete this.mInputStream;
if(this.mTransport) delete this.mTransport;
if(this.mMode) this.mMode=null;
if(this.mOutStream) {
this.mOutStream.close( );
delete this.mOutStream;
}
if(this.mLineBuffer) this.mLineBuffer=null;
this.mPosition = 0;
/***************** Destroy Instances *********************/
return;
}
To use the close
method as it's defined here, import
the file.js source file into your JavaScript, create a file
object (as shown in the examples below), and call its close( )
method.
Most examples in this section are in xpcshell, but using these libraries in your user interface JavaScript is just as easy. You can access these libraries from a XUL file, as the section "Using the DirUtils class," later in this chapter, demonstrates.
xpcshell is the command-line interpreter to JavaScript and XPConnect. This shell that uses XPConnect to call and instantiate scriptable XPCOM interfaces. It is used primarily for debugging and testing scripts.
To run xpcshell, you need to go to the Mozilla bin directory or have that folder in your PATH. For each platform, enter:
Windows:
xpcshell.exe
Unix:
./run-mozilla.sh ./xpcshell
To run xpcshell on Unix, you need to supply environment variables that the interpreter needs. You can use the run-mozilla.sh shell script that resides in the Mozilla bin directory.
$ ./run-mozilla.sh ./xpcshell
To see the available options for xpcshell, type this: $ ./run-mozilla.sh ./xpcshell --help JavaScript-C 1.5 pre-release 4a 2002-03-21 usage: xpcshell [-s] [-w] [-W] [-v version] [-f scriptfile] [scriptfile] [scriptarg...] The two most important parameters here are -w, which enables warnings output, and -s, which turns on strict mode.
The source files for JSLib are well annotated and easy to read. JSLib provide easy-to-use interfaces for creating instances of components (e.g., File objects), performing necessary error checking, and ensuring proper usage. To use a function like the one just shown, simply include the source file you need in your XUL:
<script type="application/x-JavaScript"
src="chrome://jslib/content/jslib.js" />
Then you can include the specific library files you need in your JavaScript code by using the include method:
include("chrome://jslib/content/io/file.js");
include("chrome://jslib/content/zip/zip.js");
To use the JavaScript libraries, install the JSLib package in Mozilla. The package is available as a tarball, a zip file, or as CVS sources. The easiest way to obtain it is to install it from the Web using Mozilla's XPInstall technology, described in Chapter 6.
Using your Mozilla browser, go to http://jslib.mozdev.org/installation.html and click the installation hyperlink. The link uses XPInstall to install JSLIB and make it available to you in Mozilla. To test whether it is installed properly, type the following code in your shell:
./mozilla -chrome chrome://jslib/content/
You should see a simple window that says "welcome to jslib."
Currently available JavaScript functions in the JSLib package are divided into different modules that, in turn, are divided into different classes defined in source files such as file.js, dir.js, and fileUtils.js.Table 5-1 describes the basic classes in the JSLib package's I/O module and describes how they are used.
The JSLib File
class exposes most local file routines from the nsIFile
interface. The File
class is part of the JSLib I/O module,
and is defined in jslib/io/file.js. Here is how you load the
library from xpcshell:
$ ./run-mozilla.sh ./xpcshell -w -s
js> load(`chrome/jslib/jslib.js');
*********************
JS_LIB DEBUG IS ON
*********************
js>
Once JSLib is loaded, you can load the File
module with
an include
statement:
js> include(`chrome://jslib/content/io/file.js');
*** Chrome Registration of package: Checking for contents.rdf at
resource:/chrome/jslib/
*** load: filesystem.js OK
*** load: file.js OK
true
js>
Note that file.js loads filesystem.js in turn. The
class FileSystem
in filesystem.js is the base
class for the File
object. You can also load file.js
by using the top-level construct JS_LIB_PATH
:
js> include(JS_LIB_PATH+'io/file.js');
Once you have the file.js module loaded, you can create an
instance of a File
object and call methods on it to
manipulate the file and path it represents:
js> var f = new File('/tmp/foo');
js> f;
[object Object]
js> f.help; // listing of everything available to the object
. . .
js> f.path;
/tmp/foo
js> f.exists( ); // see if /tmp/foo exists
false
js> f.create( ); // it doesn't, so create it.
js> f.exists( );
true
js> f.isFile( ); // is it a file?
true
js> f.open('w'); // open the file for writing
true
js> f.write('this is line #1\n');
true
js> f.close( );
js> f.open( ); // open the file again and
js> f.read( ); // read back the data
// you can also use default flag 'r' for reading
this is line #1
js> f.close( );
You can also assign the contents of the file to a variable for later use, iterative loops through the file contents, or updates to the data:
js> f.open( );
true
js> var contents = f.read( );
js> f.close( );
js> print(contents);
this is line #1
js>
// rename the file
js> f.move(`/tmp/foo.dat');
foo.dat
filesystem.js:move successful!
js> f.path;
/tmp/foo.dat
These examples show some ways the JSLib File
object can
manipulate local files. Using these interfaces can make life a lot
easier by letting you focus on creating your Mozilla application without
having to implement XPCOM nsIFile
objects manually from
your script.
To create an
instance of the FileUtils
class, use the FileUtils
constructor:
js> var fu = new FileUtils( );
js> fu;
[object Object]
Then look at the object by calling its help
method:
js> fu.help;
The difference between using the File and FileUtils interfaces is that methods and properties on the latter are singleton and require a path argument, while the FileUtils utilities are general purpose and not bound to any particular file. The FileUtils interface has several handy I/O utilities for converting, testing, and using URLs, of which this example shows a few:
js> fu.exists('/tmp');
true
// convert a chrome path to a url
js> fu.chromeToPath('chrome://jslib/content/');
/usr/src/mozilla/dist/bin/chrome/jslib/jslib.xul
// convert a file URL path to a local file path
js> fu.urlToPath('file:///tmp/foo.dat');
/tmp/foo.dat
Most methods on the FileUtils
objects are identical to
the methods found in file.js, except they require a path
argument. Another handy method in the FileUtils
class is spawn
,
which spawns an external executable from the operating system. It's
used as follows:
js> fu.spawn('/usr/X11R6/bin/Eterm');
This command spawns a new Eterm with no argument. To open an Eterm with vi, you could also use this code:
js> fu.spawn('/usr/X11R6/bin/Eterm', ['-e/usr/bin/vi']);
Checking to see if three different files exist would take several
lines when using the File class, but the FileUtils
class is
optimized for this type of check, as the following listing shows:
js> var fu=new FileUtils( );
js> fu.exists('/tmp');
true
js> fu.exists('/tmp/foo.dat');
true
js> fu.exists('/tmp/foo.baz');
false
You need to initialize the FileUtils
class only once to
use its members and handle local files robustly.
The Dir
class is custom-made for working with directory structures on a local
filesystem. To create an instance of the Dir
class, call
its constructor and then its help
method to see the class
properties:
js> var d = new Dir('/tmp');
js> d.help;
Dir
inherits from the same base class as File
,
which is why it looks similar, but it implements methods used
specifically for directory manipulation:
js> d.path;
/tmp
js> d.exists( );
true
js> d.isDir( );
true
The methods all work like those in the File
and FileUtils
classes, so you can append a new directory name to the object, see if it
exists, and create it if (it does not) by entering:
js> d.append('newDir');
/tmp/newDir
js> d.path;
/tmp/newDir
js> d.exists( );
false
js> d.create( );
js> d.exists( );
true
Note that
some methods in the DirUtils
class cannot be called from
xpcshell and instead must be called from a XUL window into which the
proper JSLib source file was imported. The following XUL file provides
two buttons that display information in textboxes about the system
directories:
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
id="dir-utils-window"
orient="vertical"
autostretch="never">
<script type="application/x-javascript" src="chrome://jslib/content/io/dirUtils.js"/>
<script>
var du = new DirUtils( );
function getChromeDir( ) {
cd = du.getChromeDir( );
textfield1 = document.getElementById("tf1");
textfield1.setAttribute("value", cd);
}
function getMozDir( ) {
md = du.getMozHomeDir( );
textfield2 = document.getElementById("tf2");
textfield2.setAttribute("value", md);
}
</script>
<box>
<button id="chrome" onclick="getChromeDir( );" label="chrome" />
<textbox id="tf1" value="chrome dir" />
</box>
<box>
<button id="moz" onclick="getMozDir( );" label="mozdir" />
<textbox id="tf2" value="moz dir" />
</box>
</window>
[Back] This book does not pretend to give a complete overview of JavaScript. You can view the full JavaScript 1.5 reference online at http://developer.netscape.com/docs/manuals/js/core/jsref15/contents.html.
[Back] The third edition of the ECMA-262 ECMAScript Language Specification can be found at http://www.ecma.ch/ecma1/STAND/ECMA-262.HTM.
[Back] You can use other DOM methods, but these methods are most commonly used in the XPFE. Mozilla's support for the DOM is so thorough that you can use the W3C specifications as a list of methods and properties available to you in the chrome and in the web content the browser displays. The full W3C activity pages, including links to the specifications implemented by Mozilla, can be found at http://www.w3.org/DOM/ .