Thursday, October 26, 2006

Cross-browser inline SVG solved

In three previous posts (1, 2, 3) I've discussed getting browser-side dynamically generated SVGs to render properly across browsers. I now have a solution which is working for me.

Below is a screenshot my test application now. The XML used as sample data and the XSLT to product the SVG data was provided earlier.



(Note: I was also able to add some linking between the SVG and HTML so that as you mouse over each bar it will be highlighted in the SVG and simultaneously highlighted the corresponding row in the HTML table.)

In order to make this work I used the embed tag for IE and the object tag for all other browsers (tested with Firefox and Opera). My HTML has a div named svgDiv which will dynamically be given the SVG to render. For Firefox I can create my object with the dynamic SVG in one step, but for Internet Explorer, I first have to embed a static, blank SVG (as mentioned earlier) and then replace the blank SVG with the dynamic SVG. However, I determined that this could not be done all in one JavaScript function. I found that if I programatically add the embedded blank SVG, and them immediately replace the blank SVG with the dynamic one, it would fail, but having something like an alert message between adding the blank SVG and replacing it with the dynamic SVG would work. The only explanation I could reason for this is that the actual addition of the embed element is not completed by the browser until the JavaScript reaches a pause in processing (ie. not flushed/committed), and that the need to create the alert popup causes the changes to be committed. (does anyone know how to make the browser commit the changes?). My solution to this problem was to add the blank SVG when the page is loaded (if IE) so that it will be available for me to replace when I'm ready.

When the page is loaded, if the browser is IE I call this function to add the blank SVG:

// embedBlankSVG
//
// add an <embed> tag to display a blank static SVG
// this is needed for IE to replace with dynamic content later
SVG.prototype.embedBlankSVG = function(width, height) {
var svgEmbed = document.createElement('embed');
svgEmbed.setAttribute('codebase', 'http://www.adobe.com/svg/viewer/install/');
svgEmbed.setAttribute('classid', 'clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2');
svgEmbed.setAttribute('pluginspage', 'http://www.adobe.com/svg/viewer/install/');
svgEmbed.setAttribute('src', 'blank.svg');
svgEmbed.setAttribute('width', width);
svgEmbed.setAttribute('height', height);
svgEmbed.setAttribute('type', 'image/svg+xml');
svgEmbed.setAttribute('id', 'embeddedSVG');
this.svgDiv.appendChild(svgEmbed);
};


Then, whenever I want to render a new SVG, I invoke this function:

// render
//
// render the dynamic SVG in the page
SVG.prototype.render = function() {
var str = this.toString();

if (this.isIE) {
// For IE, we use an <embed> tab

// build replacement SVG document
var newSvgElement = document.createElement("svg");
newSvgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
newSvgElement.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink");
newSvgElement.setAttribute("width", this.width);
newSvgElement.setAttribute("height", this.height);
newSvgElement.innerHTML = str;

// replace blank SVG with new SVG
domtodom(newSvgElement, 'embeddedSVG');
} else {
this.clearPage();

// For all others (mainly Mozilla based), we use an <object> tag
// with the SVG data added inline

var svgObject = document.createElement('object');
svgObject.setAttribute('name', 'svgObj');
svgObject.setAttribute('codebase', 'http://www.adobe.com/svg/viewer/install/');
svgObject.setAttribute('classid', 'clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2');
svgObject.setAttribute('data', 'data:image/svg+xml,'+ str);
svgObject.setAttribute('width', '850');
svgObject.setAttribute('height', '350');
svgObject.setAttribute('type', 'image/svg+xml');
this.svgDiv.appendChild(svgObject);
}
}


where the toString function produces SVG data from XML data (whether read from a file or returned in an XMLHttpRequest response) and the domtodom function comes from Chris Bayes' domtodom.js

This was tested in Firefox 2.0, IE 7.0, and Opera 9.02 (though the table highlighting isn't currently working in Opera. It says I have a Security Error: attempted to read protected variable).


Help requested


I would like to be able to correspondingly highlight bars in the graph when I mouse over the table rows, but every attempt to add the:

<script type="text/ecmascript">
<![CDATA[
...
]]>
</script>

to the SVG data (as described here) immediately causes Firefox to crash.

Updated 12/07/2006 per comment below


The comment below asked what the clearPage function does. Since I can't format it nicely in the comments section, I decided to update the post with it. It doesn't really do anything too profound which is why I didn't post it originally.

// clear out any old displayed data from the page
SVG.prototype.clearPage = function() {
this.svgDiv.innerHTML = '';
this.tableDiv.innerHTML = '';

if (this.isIE) {
this.embedBlankSVG(this.svgWidth, this.svgHeight);
}
}


Additionally, while I'm updating this posting anyway, I was able to add JavaScript to my SVG through the XSLT (see Help Requested section) by adding it like this in the XSL:

<xsl:element name="script">
<xsl:attribute name="type">
<xsl:value-of select="'text/ecmascript'" />
</xsl:attribute>
<xsl:text>
&lt;![CDATA[
...
]]&gt;
</xsl:text>
</xsl:element>

I haven't gotten it to do everything that I want yet, but at least the browser no longer immediately crashes.

Tuesday, October 24, 2006

Scalable Vector Graphics (SVG) Intro

I ran across an introduction to SVG today entitled, Scalable Vector Graphics (SVG), Creating High-End 2D Graphics Using XML while I was looking for something else, but it gives a nice summary of SVG so I thought I'd share it. It covers the main questions:

  • What is SVG (and what is is not)?
  • How do you use SVG?
  • Why should you use SVG?
It's a few years old (2002), but still a nice article.

Thursday, October 19, 2006

IE7, any real improvement?

Now that IE7 is out, I will be 'upgrading' to it to ensure my web sites support it, but I wonder have much of an upgrade it really is -- especially since they've had 5 years to improve it.

I got an email this morning from eWEEK highlighting Jim Rapoza's glowing review of IE7. One one hand, he acknowledges that IE7 hasn't made any revolutionary step forward in the browser space, but has merely caught up as he says,

While we wouldn't yet call IE 7 one of the best browsers available today, Microsoft has greatly closed the distance between its browser and those of its competitors. Version 7 catches IE up with now-common browser features...



but after summarizing the key user interface and security enhancements, he makes this statement,

On the standards side, IE 7 does a much better job than previous versions of the product at supporting key standards, such as Cascading Style Sheets support and JavaScript. IE 7 still doesn't have perfect standards support, but it is much better than previous versions of the browser.



That statement puzzles me since according to this analysis which summarizes web browser standards support, I see very little improvement in standards support except in CSS 2.1 & 3 Basic Selectors.

To me, it sounds like Microsoft has merely put a new glossy finish on a rotting infrastructure (a whitewashed tomb so to speak) which is really disappointing to me, since after listening to this interview of the IE7 team by the Ajaxians last month, I was more optimistic about standards support.

Thursday, October 05, 2006

Using Cookies in JavaScript

Update (February 23, 2007): I have updated the cookies functionality below to utilize flash storage if available and fall back to cookies if not available. See Cookies and Flash for local storage

Whether you like them or not, cookies are an easy way store simple user specific information across web pages and sessions. Here are the basic JavaScript methods to create (and update), read, or delete (CRUD) cookies with JavaScript.


function createCookie(name, value, days) {
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
}
else {
var expires = ";";
}
document.cookie = name + "=" + value + expires + "; path=/";
}

function readCookie(name) {
var nameEQ = name + "=";
var cookies = document.cookie.split(';');
for(var i = 0; i < cookies.length;i++) {
var cookie = cookies[i];
while (cookie.charAt(0) == ' ') {
cookie = cookie.substring(1, cookie.length);
}
if (cookie.indexOf(nameEQ) == 0) {
return cookie.substring(nameEQ.length, cookie.length);
}
}
return null;
}

function eraseCookie(name) {
createCookie(name, "", -1);
}


Detailed explanation of code how this code works is available here. This script was originally written by Scott Andrew.

Add/remove options to/from a select list

In my past two posts I've show how to move options between select lists and reorder options in a list. Now, to round out my little series on selection list manipulation with JavaScript, I'll give simple functions to add a value or remove a value from a list.


// addSelectOption
//
// Add the single select option to the selection list with the id specified
//
function addSelectOption(selectId, value, display) {
if (display == null) {
display = value;
}
var anOption = document.createElement('option');
anOption.value = value;
anOption.innerHTML = display;
document.getElementById(selectId).appendChild(anOption);
return anOption;
}

// removeSelectOption
//
// Remove the option with the specified value from the list of options
// in the selection list with the id specified
//
function removeSelectOption(selectId, value) {
var select = document.getElementById(selectId);
var kids = select.childNodes;
var numkids = kids.length;
for (var i = 0; i < numkids; i++) {
if (kids[i].value == value) {
select.removeChild(kids[i]);
break;
}
}
}

Wednesday, October 04, 2006

Move options up and down select lists


In my previous post I showed how to move options in a select list from one list to another with JavaScript. Once you've moved them, you may want to reorder them if order is important. So, now I'll show how to do that.

First, define your control in HTML to look something like what is shown to the right where you have up and down buttons which will be used to move selected elements up and down in the list.

Your HTML will look something like:


<select id="orderedList" multiple="multiple"></select>
<img src="moveup.gif" alt="Move Up" onclick="moveOptionsUp('orderedList')" />
<img src="movedown.gif" alt="Move Down" onclick="moveOptionsDown('orderedList')" />

As you can see, I've added two image which when you click on them invoke the moveOptionsUp and moveOptionsDown function. The JavaScript for these functions looks like:

// moveOptionsUp
//
// move the selected options up one location in the select list
//
function moveOptionsUp(selectId) {
var selectList = document.getElementById(selectId);
var selectOptions = selectList.getElementsByTagName('option');
for (var i = 1; i < selectOptions.length; i++) {
var opt = selectOptions[i];
if (opt.selected) {
selectList.removeChild(opt);
selectList.insertBefore(opt, selectOptions[i - 1]);
}
}
}

and

// moveOptionsDown
//
// move the selected options down one location in the select list
//
function moveOptionsDown(selectId) {
var selectList = document.getElementById(selectId);
var selectOptions = selectList.getElementsByTagName('option');
for (var i = selectOptions.length - 2; i >= 0; i--) {
var opt = selectOptions[i];
if (opt.selected) {
var nextOpt = selectOptions[i + 1];
opt = selectList.removeChild(opt);
nextOpt = selectList.replaceChild(opt, nextOpt);
selectList.insertBefore(nextOpt, opt);
}
}
}

Unfortunately there's no insertAfter function for a node, so the moveOptionsDown function is a little funky.

Move options across select lists


Perhaps you've seen controls like what is shown to the right where you have some list of items in one selection list and you can move them in and out of another by pressing the '<' and '>' buttons and you want to know how to do it.

Here's one way. First, define your select lists and buttons similarly to that shown below. In my case, I fill in the select options with AJAX, so I have no options defined in the HTML, but if you generated the HTML on the server, then you'll have options in the HTML as well.


<select id="available" size="10" multiple="multiple"></select>
<input type="button" value=">"
onclick="moveOptionsAcross($('available'), $('selected'))" />></input>
<input type="button" value="<"
onclick="moveOptionsAcross($('selected'), $('available'))" /><</input>
<select id="selected" size="10" multiple="multiple"></select>


Now include the following JavaScript code:


// moveOptionsAcross
//
// Move selected options from one select list to another
//
function moveOptionsAcross(fromSelectList, toSelectList) {
var selectOptions = fromSelectList.getElementsByTagName('option');
for (var i = 0; i < selectOptions.length; i++) {
var opt = selectOptions[i];
if (opt.selected) {
fromSelectList.removeChild(opt);
toSelectList.appendChild(opt);

// originally, this loop decremented from length to 0 so that you
// wouldn't have to worry about adjusting the index. However, then
// moving multiple options resulted in the order being reversed from when
// was in the original selection list which can be confusing to the user.
// So now, the index is adjusted to make sure we don't skip an option.
i--;
}
}
}


You should now be able to select options in one list and press the appropriate button to move them to the other list.

Checkbox as Radio Buttons

Sometimes you want the appearance of a checkbox on your form, but you want it to function like a radio button where only a single option can be selected at a time. The following JavaScript function will make a group of checkboxes function like a radiogroup.


function singleSelectCheckbox(checkbox) {
var aGroup = checkbox.parentNode;
var allInputs = aGroup.getElementsByTagName("input");
for (var i = 0; i < allInputs.length; i++) {
if (allInputs[i] == checkbox) {
continue;
} else {
allInputs[i].checked = false;
}
}
}

This assumes you've placed the checkboxes to work as a radio button group in some sort of containing HTML element (a td or span or something like that).
Your HTML code would look something like:

<span>
<input onclick="singleSelectCheckbox(this)" type="checkbox">Option 1</input>
<input onclick="singleSelectCheckbox(this)" type="checkbox">Option 2</input>
<input onclick="singleSelectCheckbox(this)" type="checkbox">Option 3</input>
</span>