Extending SVG for XForms
May 22, 2002
Introduction
If you've read my previous columns, it should be clear that SVG provides what programmers and designers need to create interactive 2D graphics. What makes SVG a little special is the versatility it inherits from XML. XML vocabularies are meant to be extended and intermixed with one another. But despite some noteworthy efforts to create XML-aware browsers, like XSmiles, Amaya, and Mozilla, rock-solid SVG-centric browser applications are not quite ready. But it may be possible to start implementing XForms UI controls in SVG.
In previous columns we looked at SVG DOM in simple drag and drop or event handling
and
remote animation control. This month we're going to implement the XForms
<button>
element with CSS support, which will serve as an applied study
on extending SVG's semantics with foreign elements and attributes.
The Task at Hand
Our task has three parts: parsing, instantiating objects, and interacting with them.
Parsing is the part where we are going to look up XForms content in our document and
try to
make some sense of it. We will then use the gathered information to build an internal
EcmaScript object model and draw the form controls. Interaction is the part where
we use
event handlers to create the different states of the button and allow for callbacks
to be
registered when it is pressed. This month, we'll take care of the XForms
<button>
element alone, concentrating on the process of integrating a
new namespace with SVG; we'll look at the the interactivity and object modeling next
month.
So our first task is to read an SVG+XForms document and draw a static button. Consider
this
simple document:
<?xml version="1.0"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:a3="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" xmlns:forms="http://www.w3.org/2002/01/forms" a3:scriptImplementation="Adobe" xml:space="preserve" onload="XForms.init(evt)"> <title>Simple implementation of the XForms <button> element</title> <script a3:scriptImplementation="Adobe" type="text/ecmascript" xlink:href="XFormsButton.es" /> <g transform="translate(50,50)"> <forms:button style=" color: blue; border-color: red; border-width: 1px; background-color: pink;"> <forms:caption>This is a test</forms:caption> </forms:button> </g> </svg>
View this example as (static) SVG
In addition to the Adobe-specific attributes, you'll notice the declaration of the
XForms
namespace on the <svg>
element and the <forms:button>
element in that namespace. You will also notice the use of CSS properties straight
out of
the CSS 2 Box Model. These are all things
that we will need to parse to draw a pink button.
Parsing XForms elements with the DOM
To get started, we have to navigate through the XML tree looking for
<forms:button>
elements, which hold the data we need. The fact that
these elements are described in a separate namespace make this easy. In fact, getting
a data
structure (here, a NodeList) holding all the buttons is done in a single line of code:
var buttonElements = root.getElementsByTagNameNS(XForms.ns, 'button');
If you look at the small EcmaScript
library I've built, you'll see that I've referenced the XForms namespace in an object,
so it is easy to reference. This line of code was taken from the Button.init()
function, called by the XForms.init()
that we call when loading the SVG
document is done. The rest of this function will recurse through all the nodes in
our
NodeList and call further initialization code on each element:
for (var i=0; i<buttonElements.length; i++) { var elem = buttonElements.item(i); var button = new Button(elem); button._draw(); }
Related Reading |
We have built a custom Button
EcmaScript object that can be constructed with
a pointer to a <forms:button>
element as an argument. Looking at that
object we can see it has the following methods defined: generateSVGStyle()
and
generateSVG()
. Both these methods will be called from
Button._draw()
, which renders the button as SVG.
In order to parse XML with the DOM we read the caption's text from the
<forms:caption>
element. Text in XML is a bit tricky to handle. We have
something called text nodes, but unfortunately, a text node might not be exactly what
you
expect. Even though you printed out a nicely-formatted line of text, you might end
up with a
heap of text nodes that will be a pain to process. Here's how we took a crack at it
in the
Caption.getText()
method:
Caption.prototype.getText = function () { this.element.normalize(); return this.element.firstChild.data; }
The normalize()
method (from the mother-of-all Node interfaces)
is a great help here, since calling it on our <forms:caption>
element
will ensure we will encounter only one text node for the whole string. After calling
it, we
can safely return our first and only child's data
.
Parsing CSS strings with the DOM
Locating and making sense of the actual XML elements in the XForms namespace was
a good
start. But our <forms:button>
elements also have some CSS properties that
we need to parse since SVG does not support the CSS 2 Box Model as-is. When reading
CSS from
SVG elements, we use the style
field of SVG DOM objects, which returns a nice
CSSStyleDeclaration CSS DOM object. The SVG Viewer knows from the DTD that the
style
attribute in the SVG world maps to CSS DOM objects. Alas, the
style
attribute is just a random attribute in XForms as far as the SVG Viewer
is concerned, so it's up to us to parse the CSS data.
I read from the <forms:button>
's style
attributes and we
build a CSSStyleDeclaration
object from it. The CSS DOM does not have
functionality to do this, so I implemented it in Button.generateSVGStyle()
:
var style = this.element.getAttribute('style'); var dummy = document.createElementNS(SVG.ns, 'rect'); dummy.setAttribute('style', style);
The trick is to create a dummy SVG element, which will never get rendered, in order
use
its ability to return a CSSStyleDeclaration
from its style
attribute. So we read the style
attribute from our
<forms:button>
element, fed it to our dummy SVG element, and now we
will be able to get its corresponding CSSStyleDeclaration
by calling
dummy.style
. Now that we have an easy access to the CSS data, we can
construct a new CSS string for the SVG representation of our button:
var fill = dummy.style.getPropertyValue('background-color'); var stroke = dummy.style.getPropertyValue('border-color'); var strokeWidth = dummy.style.getPropertyValue('border-width'); if (fill == '') { fill = '#d4d0c8'; } if (stroke == '') { stroke = '#404040'; } if (strokeWidth == '') { strokeWidth = '1px'; } style = ''; style += 'shape-rendering: optimizeSpeed; '; style += 'fill: ' + fill + '; '; style += 'stroke: ' + stroke + '; '; style += 'stroke-width: ' + strokeWidth + '; ';
I also took the liberty to specify default values for the XForms CSS properties.
Having a
<forms:button>
element with no style specified will result in an
unattractive grayish color. It's always nice to allow for some flexibility. Here we
could
also have created another CSSStyleDeclaration
to start building our new CSS
string. It would have been less error-prone, but here just creating a simple string
proved
to be just fine. Another thing that would have been smart here would have been to
create an
EcmaScript CSSStyleDeclaration
object that would have allowed both constructing
from a plain string containing CSS data and printing the object to a plain string.
This
would have hidden the SVG dummy element's logic, but I tried to keep the code as simple
as
possible.
Drawing the Button
Now that we have figured out the XML and CSS parsing, it would be the right time
to start
drawing something on the screen. Back in the initialization method you noticed the
call
button._draw()
. Taking a closer look at that method, we see it starts by
asking for the SVG to be generated through a call to this.generateSVG()
. Let's
check out the Button.generateSVG()
method:
Button.prototype.generateSVG = function () { this.caption.generateSVG(); this.SVGElement = document.createElementNS(SVG.ns, 'rect'); var width = Math.round(this.caption.getSize().width + this.marginLeft * 2); var height = Math.round(this.caption.getSize().height + this.marginTop * 2); this.SVGElement.setAttribute('width', width); this.SVGElement.setAttribute('height', height); this.SVGElement.setAttribute('style', this.style); }
We start by asking our button's caption to generate its SVG code:
Caption.prototype.generateSVG = function () { this.SVGElement = document.createElementNS(SVG.ns, 'text'); var textNode = document.createTextNode(this.getText()); this.SVGElement.appendChild(textNode); var x = this.button.marginLeft; var y = this.getSize().height + this.button.marginTop; this.SVGElement.setAttribute('x', x); this.SVGElement.setAttribute('y', y); this.SVGElement.setAttribute('style', this.SVGStyle); }
Both methods go through approximately the same process. The Button
creates an
SVG <rect>
element, while the Caption
generates a
<text>
element. Both also apply the CSS style computed with their
respective generateSVGStyle()
to their style
attributes. The
tricky part in drawing a button is to get the size of the SVG <text>
element we will create in order to compute its location as well as its wrapping rectangle's
size. In effect, I've implemented a getSize()
method on the
Caption
object:
Caption.prototype.getSize = function () { return this.SVGElement.getBBox(); }
Also in Sacré SVG |
And who's showing up? It's our old friend the SVGLocatable::getBBox()
method
from the SVG DOM. Not only does it apply to SVG shapes, but it also applies to text
elements. So all we need to do is take into account the margin settings we have specified
on
our Button
object (the marginLeft
and marginTop
properties) to figure out the correct layout. Now the last thing to worry about is
where we
are going to put that new SVG code in our existing DOM tree.
Looking back at the Button._draw()
method, we can see that the new graphics
(a <rect>
and a <text>
) have been appended as siblings
to the XForms button element. Doing so offers two main advantages. First, it kept
the XForms
XML and SVG representation at the same hierarchical levels, retaining the structural
meaning
of the original XForms element. Second, our SVG element's will now inherit transformations
which will allow explicit positioning that's completely painless since the SVG Viewer
will
take care of it. So in the end, we managed to combine comfort with value, a good deal
all in
all.
Wrapping the First Part Up
Though this was a bit of a trek, all we've ended up with is a static button, though it can be style and positioned. For now it was important to be able to make sense of a document with multiple namespaces through DOM scripting. In next month's column we'll add SVG interactivity.