- Published:July 4th, 2008
- Comments:5 Comments
- Category:Domplate, Extending Firebug Tutorial, Planet Mozilla
Even if this part is also about web services integration, the main purpose is actually to show another (and a bit more complex) example of Domplate usage. In this part I have used Yahoo! Search Web Services to track inbound links and demonstrate how Domplate can be used together with real data. Final extension in this part will discover what pages link to the current page in the browser.
Skinning the extension
This is actually the first time when we are using some CSS. So, we have to extend our directory structure again and create a new directory where all relevant files will be located.
helloworld@janodvarko.cz/ chrome/ content/ helloworld/ helloWorld.xul helloWorld.js locale/ en-US/ helloWorld.dtd helloWorld.properties skin/ classic/ ajax-loader.gif helloWorld.css defaults/ preferences/ prefs.js chrome.manifest install.rdf
Of course don't forget to add the following line into chrome.manifest file.
There are two new files inside the new chrome/skin/classic directory: (1) helloWorld.css contains all CSS rules for the panel content HTML and (2) animated ajax-loader.gif icon used to inform the user about asynchronous process in progress. Notice that the icon is created using this handy site. Any other CSS and image files needed by the extension should be put into this directory.
Using CSS in a panel
As soon as we have the CSS file at proper location, we have to make sure it's included into the HTML page that represents content of the panel. Firebug framework uses internal panel.html file, which represents default DOM document for all panels. Each panel creates its own node in the document and content of this node is displayed as a content of the panel.
Here is a brief structure of the document how it looks like if three (console, script, HelloWorld) panels exists.
+ HTML + HEAD - BODY + DIV class="panelNode panelNode-console" + DIV class="panelNode panelNode-script" + DIV class="panelNode panelNode-HelloWorld"
Each root node has automatically generated class name so, there can be a custom CSS rule associated (see helloWorld.css). Each panel can access its content node through a member variable panelNode
(see below).
The following addStyleSheet()
method is created to append our helloWorld.css stylesheet in to the document.
{
// . . .
addStyleSheet: function(doc)
{
// Make sure the stylesheet isn't appended twice.
if ($("hwStyles", doc))
return;
var styleSheet = createStyleSheet(doc,
"chrome://helloworld/skin/helloWorld.css");
styleSheet.setAttribute("id", "hwStyles");
addStyleSheet(doc, styleSheet);
}
});
The method takes one parameter doc
, which represents panel's document. If the style
element (with hwStyles ID) doesn't exist yet, it's created and appended into the document. We are utilizing createStyleSheet()
and addStyleSheet()
methods from Firebug framework.
There a two moments when the stylesheet has to be inserted in the document. When the panel is initialized and when Firebug's UI is detached and opened in a new window. In the latter case a new DOM document (again panel.html) is created and all existing content copied into it. However, we have to insert the stylesheet manually again.
So, this is the first case: addStyleSheet()
is called when the panel is initialized.
HelloWorldPanel.prototype = extend(Firebug.Panel,
{
// . . .
initialize: function()
{
Firebug.Panel.initialize.apply(this, arguments);
Firebug.HelloWorldModel.addStyleSheet(this.document);
// . . .
}
});
And this is the second case: addStyleSheet()
is called when FB's UI is detached and opened in a new window.
{
// . . .
reattachContext: function(browser, context)
{
var panel = context.getPanel(panelName);
this.addStyleSheet(panel.document);
}
});
Yahoo! Search Web Services
This web service is easy to use and so, doesn't pollute the example with a lot of irrelevant source code. Thus, we can concentrate on Domplate stuff.
The structure of the result data is as follows (see online XML and JSON example).
+ ResultSet + Result - Result * Title * Url * ClickUrl
There can be more properties in the structure, but these are important for now.
Domplate for search result data
Finally the most important part of this extension is the domplate object. As usually take a look at the source code first.
{
resultTag:
TABLE({class: "searchResultSet", cellpadding: 0, cellspacing: 0},
TBODY(
FOR("result", "$ResultSet.Result|resultIterator",
TR({class: "searchResult"},
TD({class: "searchResultTitle"},
"$result.Title"
),
TD({class: "searchResultUrl",
onclick: "$onClickResult"},
"$result.Url"
)
)
)
)
),
loadingTag:
DIV({class: "searchResultLoading"},
IMG({src: "chrome://helloworld/skin/ajax-loader.gif"})
),
onClickResult: function(event)
{
openNewTab(event.target.innerHTML);
},
resultIterator: function(result)
{
if (!result)
return [];
if (result instanceof Array)
return result;
return [result];
}
});
There are two template tags defined within the template. The loadingTag
is used when a search is currently in progress and the
resultTag
generates list of search results. There is also one event handler onClickResult()
and array iterator.
List of results is constructed using a simple table where rows are generated dynamically using a FOR
constructor. This constructor represents a loop and is used to iterate through the elements of an array and generate content.
The constructor takes two parameters. The first defines a variable, which contains the actual value in each cycle and the second is an array to be iterated. The logic is the same as for..in statement.
// . . .
)
for (var result in ResultSet.Result) {
// . . .
}
The resultIterator()
method isn't necessary if the ResultSet.Result
would be always an array. However, in case of just one search result, the variable represents directly the result. So, this little helper, which is called at the beginning of iteration converts the ResultSet.Result
object always to an array.
The onClickResult()
handler is assoicated with the second column and its purpose is to open a new tab with the clicked result URL.
The Domplate syntax could seem a bit cryptic at this moment, but don't worry I'll write more about it later.
Initiate the search
The search is executed automatically when the panel is initialized.
HelloWorldPanel.prototype = extend(Firebug.Panel,
{
// . . .
initialize: function()
{
Firebug.Panel.initialize.apply(this, arguments);
Firebug.HelloWorldModel.addStyleSheet(this.document);
searchResultRep.loadingTag.replace({}, this.panelNode, null);
var searchUrl =
"http://search.yahooapis.com/SiteExplorerService/V1/inlinkData?" +
"appid=YahooDemo&output=json&query=" +
this.context.window.location.host;
var panel = this;
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState == 4 && request.status == 200) {
var data = eval("(" + request.responseText + ")");
searchResultRep.resultTag.replace(
data, panel.panelNode, searchResultRep);
}
}
request.open("GET", searchUrl, true);
request.send(null);
}
});
The logic behind the code is following:
1. Display loading icon
2. Get URL of the current page and complete Yahoo search URL
3. Execute XHR
4. When XHR returns some JSON data use eval to create Javascript object
5. Put data into the panel (replace the loading icon)
Notice that the example uses demo ID. You should get valid application ID for real application here. Further, the panel displays only the first 50 results. Even if the service provides a way how to get the other results, I have decided to stop the example here. But of course, any tips how to implement suitable and simple paging mechanism are greatly appreciated!
This is how the panel look when the search is in progress...
And this is how the panel should look like if there are any results...
Download the sample here.
5 Comments
Thanks again for doing a great job in explaining all this.
"The Domplate syntax could seem a bit cryptic at this moment, but don't worry I'll write more about it later."
I hope that means theres more coming in the series. These are great.
Thanks! Yeah, there'll be more about Firebug/Domplate yet.
Great tutorial!
However, the code nor your XPI file works with the latest Firebug (1.3.0).
The error is in the domplate file:
Error: ResultSet is not defined
Source File: chrome://firebug/content/domplate.js
Line: 233
Any ideas?
I had trouble getting Firebug to find my stylesheet. I ended up with the following in my chrome.manifest:
skin #{extension} classic/1.0 skin/
I then put my stylesheet in the skin/ folder at the root of my extension. My stylesheet URL was then
chrome://#{extension}/skin/#{stylesheet}
I got these settings from the Extension Wizard available here:
http://ted.mielczarek.org/code/mozilla/extensionwiz/