In this tutorial, we’ll learn how to add on to Bespin’s graphical user interface with a new, custom component.
Introduction
Since Bespin is a webapp, any Bespin command could conceivably pop up some bit of user interface to interact with the user. It’s just a matter of creating some DOM nodes and placing them on the screen.
But, if you want something a bit more persistent and integrated, you’ll need to learn a bit about how Bespin’s GUI is put together. That’s what this tutorial is about.
At the same time, we’re going to talk about how Bespin plugins can themselves be pluggable.
In this tutorial, we assume that you’re already familiar with using dryice and the basics of making plugins. We’re also going to assume a directory structure like the one from the commands tutorial.
By the end of this tutorial, we’ll have something that looks toolbar-like. We’re not trying to create a fully-functional toolbar here, because the focus is on hooking in to Bespin’s GUI.
This tutorial should work with Bespin 0.9a1 or later. However, there is a bug in Firefox 3.6.x that will cause the toolbar to not display properly. Bespin 0.9a2 has a workaround for this, and the bug itself is fixed in Firefox versions after 3.6.x.
Creating Our Plugin
As with the commands tutorial, we’re assuming that you’re working within a directory called bespintutorial
that has Bespin itself in a subdirectory called bespin
.
In plugins that have user interface elements, you will almost always need to create a “multi file plugin”. That just means we’ll be creating a directory rather than a single .js file.
We’ll create a directory called tutorialtoolbar
. To turn it into a plugin, we need to create a file called package.json
in the tutorialtoolbar
directory.
We also need a dryice manifest so that we can see our plugin in action. Create a file called toolbar.json
in the bespintutorial
directory. Here’s what we’ll put in it to start with:
{
"output_dir": "../build",
"plugins": ["embedded", "tutorialtoolbar"],
"search_path": [".."]
}
Switch to the bespin
directory and run:
python dryice.py -s 8080 ../toolbar.json
You shouldn’t get any errors from dryice. That means that it was able to find our tutorialtoolbar plugin. If you open your browser to http://localhost:8080/, you’ll see a normal looking Bespin. Now we just need to put something in our plugin.
Creating A Simple View
For a first step, we’ll just toss a string up on the screen. In the tutorialtoolbar
directory, create a new file called index.js
. Here’s how our toolbar component will start out:
exports.ToolbarView = function() {
var elem = document.createElement("div");
elem.innerHTML = "<b>Toolbar!</b>";
this.element = elem;
};
We’re creating a Toolbar
class. When a new Toolbar
is created, it gets a new element which just has our string in it. We set that to element
on the instance. This is the key to GUI components that are integrated into Bespin’s UI: they just offer up an element
that Bespin will put in place.
The next step is telling the Bespin system about our new component. We do that in the package.json
file, which will now look like this:
{
"provides": [
{
"ep": "factory",
"name": "tutorialtoolbar",
"action": "new",
"pointer": "#ToolbarView"
}
]
}
The factory extension point is used for extensions that provide components for Bespin to automatically instantiate. That may sound kind of vague, but you’ll see in a moment that it’s actually easy to use. The action
tells Bespin that it’s going to be creating a new instance and the pointer
is telling Bespin to look in the tutorialtoolbar plugin’s index
module for something called ToolbarView
.
We’ve written code that will generate our initial toolbar and we’ve told Bespin that this is available. The next step is to tell Bespin to put one into the UI. This can be done either in the dryice manifest file or even at runtime when useBespin
is called. We’ll do it in the manifest file (toolbar.json
). Here’s the new manifest:
{
"output_dir": "../build",
"plugins": ["embedded", "tutorialtoolbar"],
"search_path": [".."],
"config": {
"objects": {
"toolbar": {
"factory": "tutorialtoolbar"
}
},
"gui": {
"north": {
"component": "toolbar"
}
}
}
}
The new section is the config
section. This provides the application configuration (about which you can read more in the Embedder’s Guide).
The objects
part of the config is telling Bespin to create a “global” (global within Bespin) object called toolbar
. Bespin will look for a factory called tutorialtoolbar
, which happens to be what we defined in the package.json
file.
The gui
part of the config is telling Bespin to toss the component (object) that we created with the name toolbar
into the “north” part of the interface. Bespin uses a simple “border layout” with north, south, east, west and center locations. So, our toolbar should appear at the top of the page.
Refresh your browser, and you should see our Toolbar! appear at the top. If it appears at the bottom, you’ve witnessed the bug mentioned in the introduction.
CSS3 Flexible Box Model (an aside)
CSS3 includes a module called the Flexible Box Model (sometimes called flexbox), which Paul Rouget did a great job of introducing in this Mozilla Hacks article.
Traditionally, HTML+CSS have been quite nice for laying out documents but not so great for laying out user interfaces. Flexbox makes laying out UI a far easier process.
Bespin uses flexbox to create the border layout. When you declare that a GUI component belongs in the “north”, all Bespin has to do is add the “north” class to the element, and it pops into the right place.
Flexbox would also be a great way to create a toolbar, but we’re not going to go that far in this tutorial.
By using flexbox, we let the browser do all of the layout which is generally faster and smoother than JavaScript-based layouts.
Making Our Toolbar Pluggable
One of Bespin’s main features is that it is customizable and extendable. While it’s certainly possible to make a static toolbar with a certain collection of functions, it’s a lot more interesting to create a toolbar that is itself pluggable. Plus, that’s part of the point of our tutorial.
We’ll start by defining an extension point of our own in package.json
. Add this as another item in the provides
list in package.json
:
{
"ep": "extensionpoint",
"name": "tutorialtoolbaritem",
"description": "Toolbar item views",
"params": [
{
"name": "name",
"description": "name of this toolbar item",
"type": "string"
},
{
"name": "pointer",
"description": "pointer to a component that can be instantiated with new and has an element defined on it."
}
]
}
Extension points themselves are defined via the extensionpoint
extension point. Seems a bit circular, but it works. We’re creating an extension point called tutorialtoolbaritem
. Bespin is designed to be introspectable, so we provide some documentation about the extension point via the description
and params
properties.
We’re going to make tutorialtoolbaritem
s look a lot like Bespin’s GUI components (an object with an element
property).
Now, we need to make our toolbar go out and find the registered tutorialtoolbaritem
s. We’ll change index.js
to look like this:
var catalog = require("bespin:plugins").catalog;
exports.ToolbarView = function() {
var elem = document.createElement("menu");
elem.setAttribute('class', "tutorial-toolbar");
elem.setAttribute('type', "toolbar");
this.element = elem;
this._items = [];
var extensions = catalog.getExtensions('tutorialtoolbaritem');
var self = this;
extensions.forEach(function(ext) {
ext.load().then(function(item) {
item = new item();
self._items.push(item);
elem.appendChild(item.element);
});
});
};
The first thing we’re going to do is change the element we’re creating into an HTML5 menu element. By giving it a type of toolbar
, we’re letting the browser know that we’re creating a toolbar. Right now, the browsers don’t render the toolbar in any special way, but we can use CSS to render the toolbar however we wish.
Next, we’ll make a list to keep track of the items we find. Speaking of finding the items, we need to talk to the plugin catalog about that. In the first line of the file, we imported the catalog. Now, we call getExtensions which will return all of the tutorialtoolbaritem
s.
We use var self = this
because we’ve got some nested callbacks and that’s a bit nicer to look at than binding each function individually. Then, we loop through the list of extensions and call load
on each one.
The load
function returns a Promise. Promises provide a convenient way to manage callbacks for asynchronous behavior. For our purposes here, the only difference between a Promise and a normal callback function is that we don’t pass the callback directly to load
. Instead, we call then
on the promise and pass the callback to that.
Back in our metadata for the tutorialtoolbaritem
extension point, we defined the pointer
property as a pointer to a component that can be instantiated with new and has an element defined on it. So, when we load
the extension, that’s what we’re going to get back. With the tutorialtoolbar item in hand, we can call new
on it, add that new instance to our items list and then add the instance’s element to our toolbar.
And, with that, we have created a dynamically extendable toolbar.
Adding Some Items
Of course, if we reload our Bespin, the toolbar will be rather boring. We haven’t added any toolbar items!
With the infrastructure that we put in place in the last section, we can create tutorialtoolbaritem
s in any plugin. For convenience, we’ll just add a couple of items in the tutorialtoolbar plugin.
We’ll make a new module (file) for them in the tutorialtoolbar
directory called items.js
. At this stage, we’re not going to make our toolbar items do anything, but we just want to have something to display. Here is our items.js
file:
exports.Logo = function() {
var li = document.createElement('li');
li.innerHTML = "Logo here";
this.element = li;
};
exports.OpenFileIndicator = function() {
var li = document.createElement('li');
li.innerHTML = "SampleProject — readme.txt";
this.element = li;
};
exports.Save = function() {
var li = document.createElement('li');
li.innerHTML = "Save";
this.element = li;
};
exports.PositionIndicator = function() {
var li = document.createElement('li');
li.innerHTML = "Row 0, Column 0";
this.element = li;
};
As you can see, each one of these functions does nothing but create an li
element (as per the HTML5 spec for menus) and add that element to the instance on the element
property.
One more thing to do before we can see our toolbar. We need to register the extensions for our tutorialtoolbar
extension point. Back in package.json
, we’re going to add these four more items to our provides
property:
{
"ep": "tutorialtoolbaritem",
"name": "logo",
"pointer": "items#Logo"
},
{
"ep": "tutorialtoolbaritem",
"name": "openfileindicator",
"pointer": "items#OpenFileIndicator"
},
{
"ep": "tutorialtoolbaritem",
"name": "save",
"pointer": "items#Save"
},
{
"ep": "tutorialtoolbaritem",
"name": "positionindicator",
"pointer": "items#PositionIndicator"
}
You can see that each one of these refers to the items
module in the pointer, since that’s where we put these items.
For reference, here’s the complete package.json
file at this stage:
{
"provides": [
{
"ep": "factory",
"name": "tutorialtoolbar",
"action": "new",
"pointer": "#ToolbarView"
},
{
"ep": "extensionpoint",
"name": "toolbaritem",
"description": "Toolbar item views",
"params": [
{
"name": "name",
"description": "name of this toolbar item",
"type": "string"
},
{
"name": "pointer",
"description": "pointer to a component that can be instantiated with new and has an element defined on it."
}
]
},
{
"ep": "tutorialtoolbaritem",
"name": "logo",
"pointer": "items#Logo"
},
{
"ep": "tutorialtoolbaritem",
"name": "openfileindicator",
"pointer": "items#OpenFileIndicator"
},
{
"ep": "tutorialtoolbaritem",
"name": "save",
"pointer": "items#Save"
},
{
"ep": "tutorialtoolbaritem",
"name": "positionindicator",
"pointer": "items#PositionIndicator"
}
]
}
With that, we’ve done everything we need to do to see our toolbar. Let’s reload the page. You should see something that looks like this:
It worked! It has our items in it! Doesn’t look much like a toolbar, though, does it?
Styling Your Plugins
To make our toolbar look like a toolbar, we need to add some styles. In order to support themes properly, Bespin uses LESS, which is an extended CSS syntax.
In the tutorialtoolbar
directory, create a resources
directory. In there, create a file called toolbar.less
. We need to tell Bespin’s theme manager about this file, so we need to add one more thing to the provides
property in package.json
:
{
"ep": "themestyles",
"url": [ "toolbar.less" ]
}
themestyles
extensions provide URLs (relative to the plugin’s resources directory) to the LESS files that this plugin needs to have loaded. The theme manager will automatically load these files as needed.
And what’s in this mysterious toolbar.less
file?
.bespin {
.tutorial-toolbar {
display: block;
background-color: #000;
border-bottom: solid #424038 1px;
-moz-box-shadow: 0px 0px 3px 3px #000;
-webkit-box-shadow: 0px 0px 3px 3px #000;
color: #ffffff;
font-family: Helvetica, Arial;
width: 100%;
margin: 0px;
padding: 0px;
}
.tutorial-toolbar li {
display: inline;
padding: 0px 24px 0px 0px;
}
}
This looks like fairly normal CSS, right? Except what’s that .bespin
doing surrounding those other rules? When LESS expands this out to standard CSS, the result would be something like this:
.bespin .tutorial-toolbar {
}
.bespin .tutorial-toolbar li {
}
So, the surrounding .bespin
is a nice little shorthand, and it prevents CSS from leaking out onto the page. When you consider the use of Bespin Embedded on other sites, it’s a good idea to ensure that all of our styles are scoped for use only within Bespin.
With these styles in place, let’s reload the page. Much nicer, eh?
Wrapping Up
On the one hand, this tutorial didn’t give us a functioning toolbar. It’s all static text. On the other hand, look at everything we did cover:
- multi-file plugin structure
- creating a component that Bespin can display
- the awesomeness that is CSS3’s Flexible Box Model
- configuring our component for display in Bespin
- defining and using our own extension points