SPARK FAQ - Creating a Container Widget

Alastair Fettes
University of Victoria
Victoria
British Columbia
Canada
alastairf@schemasoft.org
http://www.schemasoft.org

Biography

Alastair grew up in Calgary and later moved to Victoria on Vancouver Island. In August of 1998 Alastair moved to Rome Italy to complete his high school education at the international school Sainto Stefano di Roma (http://www.ststephens.it/). After completing this year abroad Alastair returned to Victoria to begin his post-secondary education. Alastair is currently in his final term of Computer Engineering in the faculty of Engineering at the University of Victoria. He enjoys many physical activities including rollerblading, skateboarding, soccer, ultimate and frisbee golf. Alastair also works with the open source group SPARK (http://www.schemasoft.org) in his free time.


Abstract


This is a paper on how to create a container widget that follows the SPARK framework. This FAQ is for people new to the SPARK framework and goes through the basic steps one must follow to create their own customized widgets. It describes the process of defining the functionality, creating the SVG, creating the code and finally on how to skin the SVG. It should help the reader understand the framework and learn how to create a basic widget. The widget that will be created is a graphnode widget.


Table of Contents


1. Setup
2. Designing the SVG
     2.1 SPARK SVG Requirements
     2.2 Container SVG Requirements
     2.3 Widget Specific SVG
3. Writing the Code
     3.1 Interfaces
     3.2 Code Reuse
     3.3 Original Code
4. Using the Widget
     4.1 SPARKFactory Class
     4.2 The Application
         4.2.1 SVG
         4.2.2 Code
     4.3 Decorating the Widget
5. Skinning the Widget
6. The Final Product

1. Setup

The widget that we wish to create is a graph node. The widget will have the ability to contain both other widgets (specifically geared towards other graph nodes) and any type of svg. A line will be drawn from the center of the graphnode to the center of the contained svg.

The other function that we wish this widget to perform is to have the ability to move it's contents. This will allow the user to organize their graph however they wish. It will be assumed that if the containing svg or widget already (or will receive) a mouse event defined below there will be problems. To perform this movement we will need the following mouse events:

  1. mousedown - when the mouse is pressed down on the svg content (either the graphnode root or one of it's contents), a boolean flag called bMoving will be set to true. The button will also request SPARK global mouse focus.
    This is done in the case that the client browser rendering of the move lags behind the actual mouse movement. If the mouse moves outside the button area we still wish our button to receive the mouse events.
  2. mousemove - if the boolean flag bMoving has been set to true, this will calculate the new position of the mouse and adjust the buttons translate() function as appropriate.
    In the case that button's svg elements are not receiving the mouse events (i.e. the mouse is not currently over any of the button's visible svg elements) and the mouse is still down, it will still receive the events through the global SPARK mouse focus (as described in point 1).
  3. mouseup - the boolean flag will be set to false once again. SPARK mouse focus will be released.

Another point that must be decided is how to accomplish the movement. From past experience it has been found to be desireable to use a drag square for the movement as opposed to moving the entire widget. Therefore for this demonstration the widget will have the following flow:

  1. When the user clicks the mousedown, a bounding box rect will surround the svg content.
  2. When the user moves the mouse (with the mousedown), the bounding box rect will be moved to follow the mouse. This will be produced with the getBBox function.
  3. When the user releases the mouse, the button will be moved to the same location that the bounding rect was at and the svg conetnt will be static once more (i.e. not moving).

Also, we will modify the transform="translate()" attribute/function to perform the movement. Finally, the name of our widget will be "GraphNode" - suiteably self descriptive.

2. Designing the SVG

Once the desired functionality has been designed, the next step is to design the SVG.

2.1 SPARK SVG Requirements

The first requirments of the SPARK framework is to satisfy the generic SPARK widget SVG requirments. The generic widget SVG looks as follows:

<g id="uniqueID" class="SPARK widgetType widgetName CSS">
    <desc>Required - Widget accessibility description</desc>
    <metadata>Optional - Model storage location</metadata>
    ... any svg content ...
</g>

Figure 1: Generic Widget SVG Structure

Take note of the id and class attributes as well as the desc element. These three are all required under the SPARK framework. Note the class attribute is "SPARK widgetType widgetName" - these three are required and the CSS is optional. Also note that initially we have no use for the metadata element. It is not a necessity for this demonstration.

2.2 Container SVG Requirements

Using our description from Chapter 1 we may classify our widget as an Container widget. It may contain other widgets. The SPARK description of an container widget is as follows:

A container is defined as a widget that contains other widgets. ... A container may or may not have a visual representation on the screen and is allowed direct knowledge of other widgets that it contains. A container may contain any type of widget including both atom and container widgets.

Figure 2: Definition of a Container Widget

The SVG skeleton of a SPARK Container widget is as follows (note the class attribute - "SPARK container WidgetName"):

    <g id="uniqueID" class="SPARK container WidgetName">
        <desc>Required - Description</desc>
        <metadata>Optional - Model</metadata>
        ... Any SVG content (View)...
        ... 1 or more Contents structures (may be nested)...
    </g>

Figure 3: Container SVG Structure

The contents structure is described below.

    <g  class="SPARK contents">
        ... Any SVG content (View)... and
        ... Any number of Atom and/or Container widgets...
    </g>

Figure 4: Container Content SVG Structure

Therefore, combining the SPARK definition and the Container definition the following SVG structure is created (the widget will be the only in the application therefore the ID has been designated gn-1 for graphnode-1):

    <g id="mb-1" class="SPARK container GraphNode">
        <desc>
            This is a graph node container.  It may contain both other 
            widgets (including other graph nodes) and generic svg content.
            You may drag and move the central node (will move the entire 
            group) or one of it's conected nodes at will.
        </desc>
        <g class="SPARK contents"/>
    </g>

2.3 Widget Specific SVG

Once the SVG requirements of the SPARK framework have been fulfilled, the widget specific SVG may be created.

Since the actual display of the center of the graph node is trivial (can be any svg we wish) this is fairly redundant. For this demonstration we will use a simple rect as the center of the graph, like so:

    <rect id="gn-1-center" stroke="black" fill="white" 
        width="100" height="50"/>

Note the id on the rect element. For our example we will need to know where the apex of the graph node is located so that we may draw the lines to the center of it. Therefore we will use the id concention of "graphnodeID" followed by "-center". This way it is trivial to getElementById() to find the central point of the graph node.

Another item we need to take into account is the location of the lines themselves. We will wish to have these lines somewhat organized (in a central location) such that when an item is moved the lines may be updated. We will use the following construct with ID contention of "graphnodedID" followed by "-lines".

<g id="gn-1-lines" />

Initially we will also have some fairly simple content. To demonstrate the flexibility of the graph node we will place different types of svg content including circles, ellipse's and rect's.

    <g id="gn-1" class="SPARK container GraphNode" transform="translate(100,50)">
        <desc>
            This is a graph node container.  It may contain both other 
            widgets (including other graph nodes) and generic svg content.
            You may drag and move the central node (will move the entire 
            group) or one of it's conected nodes at will.
        </desc>
        <g id="gn-1-lines" />
        <rect id="gn-1-center" stroke="black" fill="white" width="100" height="50"/>
        <g class="SPARK contents">
            <circle r="30" stroke-width="2" stroke="black" 
                fill="red" opacity="0.5" transform="translate(0,100)"/>
            <circle r="25" stroke-width="2" stroke="black" 
                fill="blue" opacity="0.5" transform="translate(100,0)"/>
            <rect width="100" height="50" stroke-width="2" stroke="black" 
                fill="green" opacity="0.5" transform="translate(100,100)"/>
            <ellipse rx="25" ry="75" stroke-width="2" stroke="black" 
                fill="orange" opacity="0.5" transform="translate(-25,-25)"/>
        </g>
    </g>

Figure 5: GraphNode SVG

3. Writing the Code

3.1 Interfaces

The widget we are creating is a container widget so therefore we must implement two interfaces (Observable and Observer) and extend one class (Container). The interfaces are defined as follows (Java definitions):

/**
 * @file observable.java
 *
 * Describes the Observable interface for the SPARK framework.
 *
 * @author Alastair Fettes
 * 
 * @see org.SchemaSoft.dom.SPARK.Observer
 * @see org.SchemaSoft.dom.SPARK.SPARK
 */
 
package org.SchemaSoft.dom.SPARK;

public interface Observable
{
    /**
     * Add an observer to the set of listeners for this widget.
     * @param in_pObserver The observer to add.
     */
    public void addObserver( Observer in_pObserver );
    
    /**
     * Remove an observer from the set of listeners for this widget.
     * @param in_pObserver The observer to remove.
     */
    public void removeObserver( Observer in_pObserver );
}

Figure 6: Observable Interface (Java)

/**
 * @file observer.java
 *
 * Describes the Observer interface for the SPARK framework.
 *
 * @author Alastair Fettes
 * 
 * @see org.SchemaSoft.dom.SPARK.Observable
 * @see org.SchemaSoft.dom.SPARK.SPARK
 */
 
package org.SchemaSoft.dom.SPARK;

public interface Observer
{
    /**
     * Called by an observable when it changes state (i.e. handles an event).
     * @param in_pObservable The observable doing the notification.
     */
    public void notify( Observable in_pObservable);
}

Figure 7: Observer Interface (Java)

/**
 * @file container.java
 *
 * Describes the Container Widget type from the SPARK framework.
 *
 * @author Alastair Fettes
 * 
 * @see org.SchemaSoft.dom.SPARK.Observable
 * @see org.SchemaSoft.dom.SPARK.Observer
 * @see org.SchemaSoft.dom.SPARK.SPARK
 */
 
package org.SchemaSoft.dom.SPARK;
 
public abstract class Container extends SPARK 
                                implements Observer, Observable
{
    /**
     * Add a container or atom to this container widget.
     * @param in_pWidget The atom or container to add.
     */
    public void addWidget( SPARK in_pWidget );
    
    /**
     * Remove a container or atom from this container widget.
     * @param in_pWidget The atom or container to remove.
     */
    public void removeWidget( SPARK in_pWidget );
    
    /**
     * Interface members.
     */
    public void notify( Observable in_pObservable );
    public void addObserver( Observer in_pObserver );            
    public void removeObserver( Observer in_pObserver );
}

Figure 8: Container.java (Java)

The Container class extends the SPARK class, defined below.

/**
 * @file spark.java
 *
 * The SPARK class describes basic Widget functionality.  All widgets
 * in the SPARK kit must inherit from the SPARK class either directly
 * or indirectly through another class such as Atom or Container.
 * 
 * The SPARK class also contains static helper methods and properties.
 *
 * @author Alastair Fettes
 *
 * @see java.util.Vector
 */

import java.util.Vector;

package org.SchemaSoft.dom.SPARK;

public class SPARK
{
    /**
     * The document-wide unique id of this widget.
     */
    public final string ID;
    
    /**
     * Retrieve the widget's state.
     * @return The widgets state value.
     */
    public Object getState();
    
    /**
     * Set the widgets state.
     * @param in_state The new state value.
     */
    public void setState( Object in_state );

    // Static functions and variables removed
    // see Helper Classes section for definition.
    // (These are not neccessary to implement a widget).
}

Figure 9: SPARK.java (Java)

3.2 Code Reuse

As there are already other container widgets created (namely the RadioButtonGroup widget) most of the work for this part has already been done for us. The following code can be used from the RadioButtonGroup class in order to implement the GraphNode script class.

The following code was reused from the RadioButtonGroup widget class. The following is straight from that class with the names changed appropriately. The description and need for each copy is posted along with the code.

The constructor was used to get the general code for doing inheritance. This guarnatees that the ID and CLASS attributes will be set. We also wish to instantiate any contained widgets using the SPARKFactory class.

/**
 * GraphNodeconstructor.
 * @param in_node The SVG base node for the GraphNodewidget.
 */
function GraphNode( in_node )
{
    // inheritance
    this.base = Container;
    this.base( in_node.getAttribute( "id" ), GraphNode.CLASS );
    
    for( var i = 0; i < in_node.childNodes.length; i++ )
    {
        var node = in_node.childNodes.item(i);

        if( node.nodeType != 3 )
        {
            if( node.nodeName == "desc" && this.anchor == null )
            {
                this.anchor = node;
            }
            else if( node.getAttribute("class").match(Container.CONTENTS_REGEX) )
            {
                // init any widgets
                SPARKFactory.createContents( node, this );
            }
        }
    }
}

Figure 10: Constructor

Theese static variables were used to do two things. First it is used to set the inheritance that GraphNode extends the Container class. Second it sets the global static variables for the CLASS and REGEX which are used to distinguish GraphNode class group SVG elements from other widget types group SVG elements.

/**
 * Regular expression defining the class attribute for a GraphNode
 * SVGElement root element.
 */
GraphNode.REGEX = /^SPARK container GraphNode/;

/**
 * String expression defining the class attribute for a GraphNode
 * SVGElement root element.
 */
GraphNode.CLASS = "SPARK container GraphNode";

/**
 * public class GraphNode extends Container
 */
GraphNode.prototype = new Container;

Figure 11: Static Variables

The state and value of a Container widget are largely dependent on the function of the widget itself. These are not part of the superclass Atom due to this fact. Therefore we may implement these at will.

/**
 * Get the state of this GraphNode.
 */
GraphNode.prototype.getState = function()
{
    return( null );
};

/**
 * Get the state of this GraphNode.
 */
GraphNode.prototype.setState = function( in_state )
{
    //
};


/**
 * Interface member.
 * @param in_observable
 */
GraphNode.prototype.notify = function( in_observable )
{
    this.updateLines();
};

Figure 12: State and Value Functions

This is just a useful function that was copied for simplicity and ease sake.

/**
 * Interface Member.
 * Notify all observers.
 */
GraphNode.prototype.notifyObservers = function()
{
    for( var i = 0; i < this.m_vObservers.length; i++ )
    {
        this.m_vObservers[i].notify( this );
    }
};

Figure 13: notifyObservers()

This we have yet to implement. It should always be present in the case that the application developer requires the ability to create a widget on the fly./

/**
 * Factory function for creating a new GraphNode.
 * @param in_parent The parent SVGElement to this widget.
 * @return The new GraphNode.
 */
GraphNode.createWidget = function( in_parent )
{
    // to do
    return( null );
};

Figure 14: createWidget Factory Function

3.3 Original Code

Now that the necessary code to comply with the framework definition has been completed, the widget's specialized code may be added. To accomodate the desired functionality we must implement a number of event handlers plus two helper functions. To do this the following methods must be added:

  1. GraphNode.prototype.mousedown
  2. GraphNode.prototype.mouseup
  3. GraphNode.prototype.mousemove
  4. GraphNode.prototype.updateLines
  5. GraphNode.generateLine - static function

To support these the constructor must be modified. Also we need some specific member variables to accomodate the movement of the widget. Note the call to SPARKFactory.createContents - this is a helper function that you may pass the contents construct (see Figure 4 ) to instantiate all member widgets. The modifications to the constructor are as follows:

/**
 * Create a new GraphNode instance using an existing SVGElement
 * as the root.
 * @param in_node The root SVGElement.
 */
function GraphNode( in_node )
{
    // inheritance
    this.base = Container;
    this.base( in_node.getAttribute( "id" ), GraphNode.CLASS );

    // graphnode members
    this.anchor     = null;
    this.bMoving    = false; // boolean flag
    this.pDragPoint = null;  // SVGPoint that will be used to calc the position
    this.pDragRect  = null;  // the rect used to outline the dragging
    this.pDragSVG   = null;  // the svg currently being dragged
    this.bMoveSelf  = false; // boolean for moving the graph node and it's sub nodes

    for( var i = 0; i < in_node.childNodes.length; i++ )
    {
        var node = in_node.childNodes.item(i);

        if( node.nodeType != 3 )
        {
            if( node.nodeName == "desc" && this.anchor == null )
            {
                this.anchor = node;
            }
            else if( node.nodeName == "metadata" && this.model == null )
            {
                this.model = node;
            }
            else if( node.getAttribute( "class" ).match( Container.CONTENTS_REGEX ) )
            {
                // init any widgets
                SPARKFactory.createContents( node, this );

                for( var j = 0; j < node.childNodes.length; j++ )
                {
                    // add event handlers to all svg
                    var t_node = node.childNodes.item(j);

                    if( t_node.nodeType != 3 )
                    {
                        var t_class = t_node.getAttribute( "class" );

                        if( !t_class.match( GraphNode.REGEX ) )
                        {
                            // add event handlers to contained SVG, except graphnodes
                            t_node.addEventListener( "mousedown", this, false );
                            t_node.addEventListener( "mouseup", this, false );
                            t_node.addEventListener( "mousemove", this, false );
                        }
                    }
                }
            }
        }
    }

    in_node.addEventListener( "mousedown", this, false );
    in_node.addEventListener( "mouseup", this, false );
    in_node.addEventListener( "mousemove", this, false );

    // draw the lines from the center of the graphnode to the center 
    // of it's contained svg
    this.updateLines();
}

Figure 15: Modified GraphNode Constructor

The mouse event handling code must accomplish the tasks listed in Chapter 1 . This is accomplished as follows (see inline documentation for descriptions):

/**
 * Handle mousedown events.
 * @param evt The mousedown event.
 */
GraphNode.prototype.mousedown = function( evt )
{
    if( !this.bMoving && evt.currentTarget != this.anchor.parentNode )
    {
        this.bMoving      = true;       // now moving
        SPARK.MOUSE_FOCUS = this;       // request global mouse focus

        this.pDragSVG     = evt.currentTarget;

        var SVGRoot     = this.anchor.ownerDocument.documentElement;
        this.pDragPoint = SVGRoot.createSVGPoint();

        var point = getNodeCoordinates(
            this.pDragSVG,
            evt.clientX,
            evt.clientY
        );

        // save the point where the draggin started
        this.pDragPoint.x = point.x;
        this.pDragPoint.y = point.y;

        // get the bounding box of the element
        var bbox = this.pDragSVG.getBBox();

        var t_node = this.anchor.ownerDocument.createElement( "rect" );
        t_node.setAttribute( "width", bbox.width );
        t_node.setAttribute( "height", bbox.height );
        t_node.setAttribute( "stroke", "black" );
        t_node.setAttribute( "fill", "none"  );

        // convert the transform so we can overlay the bounding rect
        var attr = this.pDragSVG.getAttribute( "transform" ).split( "," );
        var transformX =
            parseInt( attr[0].substring( 10, attr[0].length )) + bbox.x;
        var transformY =
            parseInt( attr[1].substring( 0, attr[1].length - 1 )) + bbox.y;

        t_node.setAttribute( "x", transformX );
        t_node.setAttribute( "y", transformY );

        // save a pointer to this node
        this.pDragRect = t_node;
        this.anchor.parentNode.appendChild( t_node );

        // event was successfully handled
        evt.stopPropagation();
    }
    else if( !this.bMoving && evt.currentTarget == this.anchor.parentNode )
    {
        this.bMoving      = true;       // now moving
        this.bMoveSelf    = true;       // moving the graph node itself
        SPARK.MOUSE_FOCUS = this;       // request global mouse focus

        var SVGRoot     = this.anchor.ownerDocument.documentElement;
        this.pDragPoint = SVGRoot.createSVGPoint();

        var point = getNodeCoordinates(
            this.anchor.parentNode,
            evt.clientX,
            evt.clientY
        );

        // save the point where the draggin started
        this.pDragPoint.x = point.x;
        this.pDragPoint.y = point.y;

        // get the bounding box of the element
        var bbox = this.anchor.parentNode.getBBox();

        var t_node = this.anchor.ownerDocument.createElement( "rect" );
        t_node.setAttribute( "width", bbox.width );
        t_node.setAttribute( "height", bbox.height );
        t_node.setAttribute( "stroke", "black" );
        t_node.setAttribute( "fill", "none"  );

        t_node.setAttribute( "x", bbox.x );
        t_node.setAttribute( "y", bbox.y );

        this.pDragRect = t_node;
        this.anchor.parentNode.appendChild( t_node );

        // event was successfully handled
        evt.stopPropagation();
    }
};

/**
 * Handle mousemove events.
 * @param evt The mousemove event.
 */
GraphNode.prototype.mousemove = function( evt )
{
    if( this.bMoving && !this.bMoveSelf )
    {
        // if the button is in the "moving" state

        // simply adjust the x and y positions of the
        // skeleton svg rect element.  It is assumed that
        // the skeleton drag rect has an id the same as
        // the widget with "-drag" added at the end.

        var point  = getNodeCoordinates(
            this.pDragSVG,
            evt.clientX,
            evt.clientY
        );

        var transformX = point.x - this.pDragPoint.x;
        var transformY = point.y - this.pDragPoint.y;
        this.pDragRect.setAttribute(
            "transform",
            "translate(" + transformX + "," + transformY + ")"
        );

        // event was successfully handled
        evt.stopPropagation();
    }
    else if( this.bMoving && this.bMoveSelf )
    {
        var point  = getNodeCoordinates(
            this.anchor.parentNode,
            evt.clientX,
            evt.clientY
        );

        var transformX = point.x - this.pDragPoint.x;
        var transformY = point.y - this.pDragPoint.y;
        this.pDragRect.setAttribute(
            "transform",
            "translate(" + transformX + "," + transformY + ")"
        );

        // event was successfully handled
        evt.stopPropagation();
    }
};

/**
 * Handle mouseup events.
 * @param evt The mouseup event.
 */
GraphNode.prototype.mouseup = function( evt )
{
    if( this.bMoving && !this.bMoveSelf )
    {
        var point = getNodeCoordinates(
            this.pDragSVG,
            evt.clientX,
            evt.clientY
        );

        // calculate the differences in the new position versus the old position.
        var deltaX = point.x - this.pDragPoint.x;
        var deltaY = point.y - this.pDragPoint.y;

        // change the translate function on the g element to adjust the location
        // of the widget
        var attr = this.pDragSVG.getAttribute( "transform" ).split( "," );

        var transformX =
            parseInt( attr[0].substring( 10, attr[0].length )) + deltaX;
        var transformY =
            parseInt( attr[1].substring( 0, attr[1].length - 1 )) + deltaY;

        // modify the groups transform with the new position
        this.pDragSVG.setAttribute(
            "transform",
            "translate(" + transformX + "," + transformY + ")"
        );

        // remove the drag rectangle
        this.anchor.parentNode.removeChild( this.pDragRect );

        // update the lines
        this.updateLines();

        // release mouse focus, no longer moving
        SPARK.MOUSE_FOCUS = null;
        this.bMoving = false;
        this.pDragPoint = null;
        this.pDragRect  = null;
        this.pDragSVG   = null;

        // event was successfully handled
        evt.stopPropagation();
    }
    else if( this.bMoving && this.bMoveSelf )
    {
        var point = getNodeCoordinates(
            this.anchor.parentNode,
            evt.clientX,
            evt.clientY
        );

        // calculate the differences in the new position versus the old position.
        var deltaX = point.x - this.pDragPoint.x;
        var deltaY = point.y - this.pDragPoint.y;

        // change the translate function on the g element to adjust the location
        // of the widget
        var attr = this.anchor.parentNode.getAttribute( "transform" ).split( "," );

        var transformX =
            parseInt( attr[0].substring( 10, attr[0].length )) + deltaX;
        var transformY =
            parseInt( attr[1].substring( 0, attr[1].length - 1 )) + deltaY;

        // modify the groups transform with the new position
        this.anchor.parentNode.setAttribute(
            "transform",
            "translate(" + transformX + "," + transformY + ")"
        );

        // remove the drag rectangle
        this.anchor.parentNode.removeChild( this.pDragRect );

        // update the lines
        this.updateLines();

        // release mouse focus, no longer moving
        SPARK.MOUSE_FOCUS = null;
        this.bMoving      = false;
        this.bMoveSelf    = false;
        this.pDragPoint   = null;
        this.pDragRect    = null;
        this.pDragSVG     = null;

        // notify any observers of the state change.
        this.notifyObservers();

        // event was successfully handled
        evt.stopPropagation();
    }
};

Figure 16: GraphNodeEvent Handlers

4. Using the Widget

In order for our widget to be used there are a number of tasks that must first be accomplished. The SPARKFactory class must be modified to support the widget and the App class must be created to initialize our application. Finally the SPARKDecorator class may be modified (if necessary) to add application specific functionality to the widgets.

4.1 SPARKFactory Class

The first task is to modify the SPARKFactory class. This class is used to let the application recognize and distinguish SVG nodes that define SPARK widgets. It is also used to instantiate the widgets as they are discovered. There are two places in the SPARKFactory class that must be modified: the init and the createContainer functions. The modifications will look as follows (assuming this is the only widget type that is currently being used):

/**
 * Registers widget types with the SPARK class.
 *
 * NOTE: Must be modified by UI programmer to support required
 * widget types.  Add the following for each widget type you wish
 * to use:
 *
 * SPARK.registerType( WidgetClass.REGEX );
 */
SPARKFactory.init = function()
{
    // added to support the GraphNodeclass
    SPARK.registerWidgetType( GraphNode.REGEX );
};

Figure 17: SPARKFactory init Function Modifications

/**
 * Create a new container widget from the given SVG element.
 *
 * NOTE: Must be modified by UI programmer to support required
 * widget types.  Add the following for each widget type you wish
 * to use (in pseudo code):
 *
 * if( (in_node/@class).match( WidgetClass.REGEX ) )
 * {
 *     widget = new WidgetClass();
 * }
 *
 * @param in_node The SVG element.
 * @return The new container or null.
 */
SPARKFactory.createContainer = function( in_node )
{
    var widget = null;
    var sClass = in_node.getAttribute( "class" );

    if( sClass.match( GraphNode.REGEX ) )
    {
        widget = new GraphNode( in_node );
    }

    return( widget );
};

Figure 18: SPARKFactory createContainer Function Modifications

4.2 The Application

4.2.1 SVG

The SVG document has been decided to look as follows:

<svg xmlns="http://www.w3.org/2000/svg" 
     width="100%" 
     height="100%" 
     onload="App.init(evt);">
    <title>GraphNodeDemonstration Application</title>
    <!-- script includes here -->
    <g id="application">
        <g id="SPARK" 
           onmousemove="SPARK.handleMouseEvent(evt);" 
           onmouseup="SPARK.handleMouseEvent(evt);" 
           onmousedown="SPARK.handleMouseEvent(evt);" 
           onclick="SPARK.handleMouseEvent(evt);" 
           onkeypress="SPARK.handleKeyboardEvent(evt);" 
           onkeydown="SPARK.handleKeyboardEvent(evt);" 
           onkeyup="SPARK.handleKeyboardEvent(evt);">
            <g id="eventCatcherLayer">
                <rect height="300%" 
                      width="300%" 
                      y="-100%" 
                      x="-100%" 
                      fill="white" 
                      opacity="0"/>
            </g>
            <!-- widget SVG here -->
        </g>
    </g>
</svg>

The applications (demonstrations) SVG has some features that must be explained. First the App.init function is called when the SVG is first loaded. Therefore in the App.init function we must take the opportunity to initalized our base widgets. This may be done however the developer wishes but for this demonstration we will place all widgets inside the group element with the id SPARK.

The SPARK group has been created to hold our application widgets. It is the location of our user interface. This of course is not part of the SPARK framework and may be decided however the developer wishes. For this demonstration it has been used for simplicity sake. The SPARK group has a number of event handlers. These are used, in combination with the eventCatcherLayer to catch all client events (that are not caught and processed) and direct them towards the global SPARK class to handle and distribute the event to the subscribing widget as appropriate. Once again these are not part of the framework - the are simply a useful tool that has been used in previous applications.

The last step with the SVG is to include the script files and insert our widgets. The positions for both the Widgets and the source includes have been listed with comments. We will be inserting the appropriate SVG to these two locations to make our simple demonstration application. The completed SVG looks as follows (note an nested GraphNode widgets have been added for a second demonstration):

<svg xmlns="http://www.w3.org/2000/svg" 
     width="100%" 
     height="100%" 
     onload="App.init(evt);">
    <title>GraphNodeDemonstration Application</title>
    <script xlink:href="spark.js"/>
    <script xlink:href="atom.js"/>
    <script xlink:href="container.js"/>
    <script xlink:href="GraphNode.js"/>
    <script xlink:href="sparkfactory.js"/>
    <script xlink:href="sparkdecorator.js"/>
    <script xlink:href="app.js"/>
    <g id="application">
        <g id="SPARK" 
           onmousemove="SPARK.handleMouseEvent(evt);" 
           onmouseup="SPARK.handleMouseEvent(evt);" 
           onmousedown="SPARK.handleMouseEvent(evt);" 
           onclick="SPARK.handleMouseEvent(evt);" 
           onkeypress="SPARK.handleKeyboardEvent(evt);" 
           onkeydown="SPARK.handleKeyboardEvent(evt);" 
           onkeyup="SPARK.handleKeyboardEvent(evt);">
            <g id="eventCatcherLayer">
                <rect height="300%" width="300%" y="-100%" x="-100%" 
                      fill="white" opacity="0"/>
            </g>
            <g id="gn-1" class="SPARK container GraphNode" transform="translate(100,50)">
                <desc>
                    This is a graph node container.  It may contain both other 
                    widgets (including other graph nodes) and generic svg content.
                    You may drag and move the central node (will move the entire 
                    group) or one of it's conected nodes at will.
                </desc>
                <g id="gn-1-lines" />
                <rect id="gn-1-center" stroke="black" fill="white" width="100" height="50"/>
                <g class="SPARK contents">
                    <circle r="30" stroke-width="2" stroke="black" 
                        fill="red" opacity="0.5" transform="translate(0,100)"/>
                    <circle r="25" stroke-width="2" stroke="black" 
                        fill="blue" opacity="0.5" transform="translate(100,0)"/>
                    <rect width="100" height="50" stroke-width="2" stroke="black" 
                        fill="green" opacity="0.5" transform="translate(100,100)"/>
                    <ellipse rx="25" ry="75" stroke-width="2" stroke="black" 
                        fill="orange" opacity="0.5" transform="translate(-25,-25)"/>
                </g>
            </g>
        </g>
    </g>
</svg>

Figure 19: Complete Demonstration SVG

4.2.2 Code

The application code is fairly simple to create. First the SPARKFactory is initalized so that we may use it to create widgets. This init function was modified earlier to add support for the different widget types that will be used. Also the SPARKDecorator and the SPARKFactory.setDecorator call will be discussed Section 4.3 . Next the room of the application is selected. From this root we loop through all the child SVG elements and pass them to the factory to attempt to create a widget with them. The App code looks as follows:

/**
 * Loads the application only.
 */

/**
 * public class App
 */
function App(){};

/**
 * This is the root svg element of the application.
 */
App.ROOT = null;

/**
 * Initialize the application.
 * @param evt The onload event.
 */
App.init = function( evt )
{
    // initialize the factory.
    SPARKFactory.init();
    // set the decorator
    SPARKFactory.setDecorator( new SPARKDecorator() );

    var SVGDoc = evt.target.ownerDocument;
    App.ROOT   = SVGDoc.getElementById( "SPARK" );

    for( var i = 0; i < App.ROOT.childNodes.length; i++ )
    {
        var node = App.ROOT.childNodes.item(i);

        if( node.nodeType != 3 )
        {
            var widget = SPARKFactory.createWidget( node );
        }
    }
};

Figure 20: Application ECMAScript Code

4.3 Decorating the Widget

In an application there will be certain widgets that require custom code. To accomodate this the SPARK framework as the SPARKDecorator class. For our demonstration we will be adding a very simple application specific change to one of our two widgets. This is meant only to show the possibilities for using the SPARKDecorator class.

The change the will be added on the initalization of the application. The desired functionality is to change it such that when any GraphNode is clicked, it will alert with it's id. The modifications to the SPARKDecorator class are see below:

/**
 * @file SPARKDecorator.es
 *
 * Used to "decorate" widgets (as they are crated)
 * with custom logic that does not belong in every instance
 * of that widget.
 *
 * NOTE: Will require modifications by application programmer.
 *
 * @see org.SchemaSoft.dom.SPARK.SPARK;
 * @see org.SchemaSoft.dom.SPARK.SPARKFactory;
 *
 * @author Alastair Fettes
 */

/**
 * public class SPARKDecorator implements Decorator
 */
function SPARKDecorator(){}

/**
 * Add custom logic to the input widget.
 *
 * NOTE: Must be modified by application programmer with the app
 * specific logic.
 *
 * @param in_widget An Atom or Container widget.
 */
SPARKDecorator.prototype.decorate = function( in_widget )
{
    if( in_widget.CLASS.match( GraphNode.REGEX ) )
    {
        // whenever a graphnode is clicked it will alert with it's id
        in_widget.click = function( evt )
        {
            alert( this.ID );
            evt.stopPropagation();
        };

        in_widget.anchor.parentNode.addEventListener( "click", in_widget, false );
    }
};

Figure 21: SPARKDecorator Class

5. Skinning the Widget

6. The Final Product

The final product can be found here: /resources/samples/faq/createcontainer/graphnode.svg. A second demo where a Binary Tree was created can be found here: /resources/samples/faq/createcontainer/graphnode2.svg

XHTML rendition created by gcapaper Web Publisher v2.1, © 2001-3 Schema Software Inc.