Abstract
Developing a browser-ready SVG and ECMAScript (a.k.a. standard JavaScript) application requires coordinating your best XML, DOM, graphics, and Javascript programming backgrounds. When it all works, SVG can take your application to an amazing level of dynamic interaction. But when things go wrong, diagnosing SVG or Javascript issues can be downright frustrating.
To help understand the inner workings of your own SVG and ECMAScript application, this paper outlines background knowledge and steps to incorporate an interactive event logger into your own SVG applications. An event logger is a useful way to get a picture of what's going on inside an interactive SVG and ECMAScript application and can help to diagnose complicated interactions among DOM, SVG, and user events. In the end, the insights you gain from watching your own SVG event logger in action may be just what you need to understand and maybe fix what went wrong.
Intended audience: Prior experience programming ECMAScript/Javascript, or at least have a programming background. Also some prior exposure to SVG, although only a few SVG graphic primitives are employed.
Table of Contents
With modern SVG enabled browsers, authors may define SVG documents that are responsive to user interface events. When a browser visits such an SVG document, the browser's SVG engine renders the graphic and provides interactivity by processing UI events. The SVG engine determines the target element when the mouse moves or a click occurs and calls ECMAScript event handlers defined for the target element.
First, we first review some background foundations about the SVG DOM, ECMAScript event listeners, and z-ordering. Small SVG and ECMAScript examples illustrate these ideas to reinforce the concepts.
Common ECMAScript event listeners for SVG graphic elements include:
onclick(evt)
onmousedown(evt)
onmouseup(evt)
onmouseover(evt)
onmouseout(evt)
onmousemove(evt)
Let's review the event processing method used for determining the SVG target element. The SVG engine selects the top-most relevant graphics element under the pointer when the user event occurs. The notion of "relevant" has to do with the circumstances that an SVG graphic element can be the target for a pointer event. We'll get to the meaning of "top-most", in a moment, when we discuss z-ordering.
To be relevant the SVG graphic element must be displayed, i.e., an attribute of
display="none"
would eliminate SVG graphic elements from consideration. By default, only the visible part of an SVG graphic element can receive events. But this can be further controlled by the attribute,
pointer-events
. The following example illustrates two SVG circles without fill. The unfilled, empty inside of one circle is dead while the other responds to UI events.
<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<!DOCTYPE svg>
<svg xmlns="http://www.w3.org/2000/svg">
<!-- Define Background Fill Pattern -->
<defs>
<pattern id="polkadots" patternUnits="userSpaceOnUse"
width="20" height="20" viewBox="-10 -10 20 20">
<circle r="5" fill="lightgray"/>
</pattern>
</defs>
<!-- Background rectangle fills the canvas with polkadots -->
<rect x="0" y="0" width="100%" height="100%" fill="url(#polkadots)"/>
<!-- Two Circles with different pointer-events -->
<circle cx="25%" cy="50%" r="20%" fill="none" stroke-width="5%" stroke="black"
pointer-events="visiblePainted"
onclick="alert('I was Clicked');"/>
<circle cx="75%" cy="50%" r="20%" fill="none" stroke-width="5%" stroke="black"
pointer-events="all"
onclick="alert('I was Clicked');"/>
<!-- visibility="hidden" -->
<text x="25%" y="75%">pointer-events="visiblePainted" (default)</text>
<text x="75%" y="75%">pointer-events="all"</text>
</svg>
The next figure is the same as the previous except that we use
visibility="hidden"
attribute from the commented out section. Notice that the now hidden circle still responds to a mouse click.
Hidden elements are still present and can still respond to UI events.
According to the SVG 1.1 Spec,
Elements in an SVG document fragment have an implicit drawing order, with the first elements in the SVG document fragment getting "painted" first. Subsequent elements are painted on top of previously painted elements.
This is also known as "z-ordering": elements later in document order appear on top.
When SVG graphic elements overlap, the SVG rendering mechanism specifies not only the painting but also the ordering of event targets. It doesn't matter if the top-most is translucent or even hidden. If the top-most element is deemed "relevant" it alone receives the UI event while excluding elements lower in z-ordering (i.e., earlier in document order)from receiving events. The following two examples illustrate.
This final example on z-ordering shows what happens when adding to the blue rect the attribute,
pointer-events="none"
The blue rect is painted, but from an event processing point-of-view it's gone.
The "big hidden rect" is a design technique used in building interactive SVG documents. According the SVG 1.1 Spec,
If there are no graphics elements whose relevant graphics content is under the pointer (i.e., there is no target element), the event is not dispatched.
But an SVG application may want to respond to UI events even when there are no visible elements. The big hidden rect solves this problem.
Again, the problem is that an SVG Engine only delivers events to SVG graphic objects, but where there are no objects there can be no events. To solve the problem use a hidden rectangle whose dimensions cover the entire canvas. The hidden rectangle overrides the default event processing by specifying
pointer-events="all"
. For example,
<rect id="bigrect" height="100%" width="100%" pointer-events="all" visibility="hidden"/>
Event processing can be further controlled through DOM mutation. A big hidden rect can be programmed to accept or ignore pointer events.
bigrect.setAttributeNS(null, "pointer-events", "none");
bigrect.setAttributeNS(null, "pointer-events", "all");
Combining the ideas of z-order, explicitly controlled visibility and pointer-events, and the use of the big hidden rect technique, a variety of UI experiences can be built.
Now we turn our attention to the practical issues needed to build an own SVG logger. Here are some design goals:
Keep it simple. In particular, to achieve simplicity in the Logger's ECMAScript code, requiring the programmer to do a bit more cut-and-paste of template SVG and ECMAScript boilerplate will be OK.
Make it possible to install more than one instance of a logger so that different log event streams can be monitored in parallel.
Provide a convenience method to install several event listeners for all the common mouse events by registering callbacks on a given SVG element. When installing, be sure to preserve any event listeners already registered on that SVG element.
Once installed, make it relatively easy to leave loggers in place, while making visuals invisible with events turned off.
The logger should be minimally intrusive, but full transparency is not required. In otherwords, the logger could introduce semantic deltas from the same without loggers installed, but this difference is probably not critical, since the loggers can always be left in place, made invisible, and turned off.
The logger is implemented in ECMAScript class whose source is in one file,
logger.js that can be linked into an SVG application.
The logger's core data structure is implemented with parallel Javascript arrays. These act like a table and record for each event, a sequence number and logging message. The arrays store a fixed number of records and operate FIFO, discarding older log entries.
The logger's core data is painted onto a predetermined group of SVG text elements. Since the group is an SVG element with the specified id, this is easy to do by calling DOM mutation setters on each of the group's children text elements. By using id's independent groups represent parallel logger streams.
logger = function(logId) {
this.initTxtElmts=function(logId) {
var textGroup = svgDocument.getElementById(logId);
for each (n in textGroup.childNodes) {
// checks that we are visiting svg:text node-- skip past whitespace nodes
if (n.nodeName == "text") {
txtElmts.push(n.firstChild);
}
}
}
this.paintStats = function() {
var i=0;
for each (line in lines) {
txtElmts[i].data = line;
i++;
}
}
this.logSvgMouseEvent = function(msg, evt) {
var x = evt.clientX;
var y = evt.clientY;
this.log(msg + "(" + x + "," + y + ")");
}
this.log = function(msg) {
lines.push(sequenceNumber + ":" + msg);
if (lines.length > txtElmts.length) {
lines.shift();
}
this.paintStats();
sequenceNumber++;
}
this.addMouseListener = function(loggerName, id, obj, eventCallback) {
var value = obj.getAttributeNS(null, eventCallback);
obj.setAttributeNS(null, eventCallback, loggerName+".logSvgMouseEvent('"+id+":"+eventCallback+"', evt);"+value);
value = obj.getAttributeNS(null, eventCallback);
}
this.decorateMouseListeners = function(loggerName, id) {
var obj = svgDocument.getElementById(id);
this.addMouseListener(loggerName, id, obj, "onclick");
this.addMouseListener(loggerName, id, obj, "onmousedown");
this.addMouseListener(loggerName, id, obj, "onmouseup");
this.addMouseListener(loggerName, id, obj, "onmouseover");
this.addMouseListener(loggerName, id, obj, "onmouseout");
this.addMouseListener(loggerName, id, obj, "onmousemove");
}
var sequenceNumber = 0;
var txtElmts = [];
var lines = [];
this.initTxtElmts(logId);
}
To activate the logger, we follow a few steps:
Insert a script tag,
<script type="text/ecmascript" xlink:href="logger.js"/>
which pulls in the logger code.
Add the line,
installLoggers();
to our init() function
and the following code to the end of our script,
var log1;
var log2;
installLoggers = function() {
log1 = new logger("log1");
log1.decorateMouseListeners("log1", "cyan");
log1.decorateMouseListeners("log1", "yellow");
log1.decorateMouseListeners("log1", "red");
log2 = new logger("log2");
log2.decorateMouseListeners("log2", "bigrect");
}
Add the groups of SVG text elements, which have the group id'ed with the logger name. Here is a group for log2.
<g id="log2">
<text x="5" y="20">-</text>
<text x="5" y="35">-</text>
<text x="5" y="50">-</text>
<text x="5" y="65">-</text>
<text x="5" y="80">-</text>
<text x="5" y="95">-</text>
<text x="5" y="110">-</text>
<text x="5" y="125">-</text>
<text x="5" y="140">-</text>
<text x="5" y="155">-</text>
<text x="5" y="170">-</text>
<text x="5" y="185">-</text>
</g>
To facilitate easier editing or repositioning, these can be nested within another g orsvg element.
Now let's turn our attention to a involved example that will benefit from logging.
The drag-and-drop application employs three circles, stroked and filled with a lighter color, and a big hidden rect.
<!-- three circles --> <circle id="cyan" cx="150" cy="100" r="50" stroke-width="25" stroke="cyan" fill="lightcyan" onmousedown="dndInstance.capture(evt)" /> <circle id="yellow" cx="250" cy="100" r="50" stroke-width="25" stroke="yellow" fill="lightyellow" onmousedown="dndInstance.capture(evt)" /> <circle id="red" cx="350" cy="100" r="50" stroke-width="25" stroke="red" fill="pink" onmousedown="dndInstance.capture(evt)" /> <!-- big hidden rect --> <rect id="bigrect" height="100%" width="100%" pointer-events="none" visibility="hidden" onmouseup="dndInstance.release(evt)" onmouseout="dndInstance.release(evt)" onmousemove="dndInstance.drag(evt)" />
The interactivity is controlled by three methods, defined on dnd.
capture()
drag()
release()
These coordinate the drag-and-drop behavior by mutating the x and y coordinates of the captured circle and by activating and deactivating the pointer-events of the big hidden rect.
The application oscillates between two states: dragging and not-dragging. It begins in the not-dragging state. The big hidden rect ignores events, while each of the three circles waits for a
mousedown()
event.
When a UI event hits a circle, that circle's event callback, the method,
capture()
, is called.
capture()
does two things: it memoizes the target SVG element and turns on pointer-events for the big hidden rect. The effects of
capture()
are to put the application into the dragging state.
Note, that because the big hidden rect is last in document order it's on top. Circles will no longer get any events, since only the rect receives events and the rect obscures everything. The big hidden rect receives all events in the dragging state.
While dragging, mouse movements are handled by mutating the x, y coordinates of the captured circle. Dragging continues until either the mouse is released or it's dragged off of the SVG canvas. Either of these events calls
release()
which resets the big hidden rect's pointer-events to "none" and reverts to the non-dragging state.
The next figure shows all the ECMAScript code.
init = function(evt) {
if ( window.svgDocument == null )
svgDocument = evt.target.ownerDocument;
dndInstance = new dnd();
}
dnd = function() {
var x;
var y;
var offsetX;
var offsetY;
var svgObject;
var moveSvgObject = function() {
svgObject.setAttributeNS(null, "cx", x+offsetX);
svgObject.setAttributeNS(null, "cy", y+offsetY);
}
this.capture = function(evt) {
svgObject = evt.target;
var bigrect = svgDocument.getElementById("bigrect");
x = evt.clientX;
y = evt.clientY;
offsetX = svgObject.getAttributeNS(null, "cx")-x;
offsetY = svgObject.getAttributeNS(null, "cy")-y;
bigrect.setAttributeNS(null, "pointer-events", "all");
moveSvgObject();
}
this.release = function(evt) {
svgObject=null;
var bigrect = svgDocument.getElementById("bigrect");
bigrect.setAttributeNS(null, "pointer-events", "none");
}
this.drag = function(evt) {
x = evt.clientX;
y = evt.clientY;
moveSvgObject();
}
}
Here is the same application "logging enabled". One logger displays events from circles and the other logger displays events from the big hidden rectangle.
Unfortunately, the external javascript code cannot be linked from the conference website, so in order to view the DND with loggers activated, download and save this figure as dnd.svg then download the listing from the previous section and save to another file namedlogger.js
The ECMAScript logger class presented here was derived from tutorial material aimed at teaching SVG to intermediate SVG and ECMAScript developers. There are several areas where the logger could be extended to yeild a developer framework for SVG and ECMAScript logging.
Integration with JavaScript cross-platform libraries.
Generating SVG text templates from the logger class would obviate the developer's cutting and pasting.
Introduce a subclass or callback function to abstract logging semantics. logger.js presented here, is hardwired to display x,y coordinate events.
Robust handling of errors/warnings.
More interactive UI controls for logger presentation UI, e.g., button to turn on and off display of logger, scrollbar, etc.
Add query and filtering capabitilies. Add event severities and types.