Chapter 5. Scripting Mozilla
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,[1] which is based on the ECMA-262 standard.[2] 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.
5.1. Faces of JavaScript in Mozilla
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 Section 5.3, 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 Section 5.4.1 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 Section 8.2.1 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.
Notes
5.2. JavaScript and the DOM
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.
5.2.1. What Is the DOM?
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.
5.2.2. The DOM Standards and Mozilla
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:
- DOM Level 1: Excellent
- DOM Level 2: Good
- DOM Level 3: Poor; under construction
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.
5.2.3. DOM Methods and Properties
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.
5.2.3.1. Using dump( ) to print to STDOUT
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.
5.2.3.2. getElementById
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.
5.2.3.3. getAttribute
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.
5.2.3.4. setAttribute
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.
5.2.3.5. createElement
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.
5.2.3.6. createTextNode
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.
5.2.3.7. appendChild
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.
5.2.3.8. cloneNode
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.
5.2.3.9. getElementsByTagName
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(len+'\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(tag Id+"\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.[1]
5.2.3.10. Getting an element object and its properties
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.
5.2.3.11. Retrieving elements by property
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++) for (var i=0; i<chex.length; i++) chex[i].setAttributte("disabled", "false"); for (var i=0; i<unchex.length; i++) unchex[i].setAttributte("disabled", "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).
Notes
5.3. Adding Scripts to the UI
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.
5.3.1. Handling Events from a XUL Element
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:
onabort onblur onerror onfocus onchange onclick oncontextmenu ondestroy onload onpaint onkeydown onkeypress onkeyup onunload onmousemove onmouseout onmouseover onmouseup onmousedown onrest onresize onscroll onselect onsubmit
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.
5.3.2. Events and the Mozilla Event Model
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.
5.3.2.1. Event propagation and event bubbling
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.
5.3.2.1.1. Trapping events
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>
5.3.2.2. Capturing events
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>
5.3.3. Changing an Element's CSS Style Using JavaScript
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.
5.3.4. Creating Elements Dynamically
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.
5.3.5. Sharing Data Between Documents
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.
5.3.5.1. Scope in Mozilla
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);
5.3.5.2. Using the window.arguments array
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.
5.4. XPConnect and Scriptable Components
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.
5.4.1. What Is XPConnect?
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.
5.4.1.1. Creating XPCOM objects in script
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 nsFilepicker 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
<head> <title>Sound Service Play Example</title> <script> function play() { netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); var sample = Components.classes["@mozilla.org/sound;1"].createInstance(); sample = sample.QueryInterface(Components.interfaces.nsISound); const SND_NETWORK_STD_CID = "@mozilla.org/network/standard-url;1"; const SND_I_URL = "nsIURL"; const SND_URL = new C.Constructor(SND_NETWORK_STD_CID, SND_I_URL); var url = new SND_URL(); url.spec = 'http://jslib.mozdev.org/test.wav'; sample.play(url); } </script> </head> <form name="form"> <input type="button" value="Play Sound" onclick="play();"> </form>
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 Section 8.1.5 section in Chapter 8 for more information about this.
5.4.1.2. Finding components and interfaces
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.
5.4.1.3. Selecting the appropriate interface from the component
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.
5.5. JavaScript Application Code
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.
5.5.1. JavaScript Libraries
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.
xpcshell
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 Section 5.5.1.6, 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");
5.5.1.1. Installing JSLib
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.”
5.5.1.2. The JSLib libraries
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.
Class / (filename) | Description |
File / (file.js) | Contains most routines associated with the File object (implementing nsIFile). The library is part of the jslib I/O module. |
FileUtils / (fileUtils.js) | The chrome registry to local file path conversion, file metadata, etc. |
Dir / (dir.js) | Directory creation; variations of directory listings. |
DirUtils / (dirUtils.js) | Paths to useful Mozilla directories and files such as chrome, prefs, bookmarks, localstore, etc. |
5.5.1.3. Using the File class
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.
5.5.1.4. Using the FileUtils class
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.
5.5.1.5. Using the Dir class
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
5.5.1.6. Using the DirUtils class
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>
[1] 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/index.html?content=javascript.html.
[2] The third edition of the EMCA-262 EMCAScript Language Specification can be found at http://www.ecma.ch/ecma1/STAND/ECMA-262.HTM.
[1] 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/.
Get Creating Applications with Mozilla now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.