Case Study: Modifying the Spry Photo Gallery to work in the AIR Application Sandbox

The Spry Photo Gallery has been by far the most popular demo within the Spry framework, so it is not a surprise that folks have been wanting to create AIR applications based on a version of the Spry Photo Gallery that was modified to suit their needs. This was easy to do with early beta versions of the AIR Runtime, but once AIR Beta 3 introduced the concept of an Application Sandbox, things got a bit more complicated. The complications stem from the fact that Spry's region processing code relies on innerHTML to insert re-generated content, and its use of the JavaScript built-in eval() function to evaluate the JavaScript expressions within spry:if, spry:when, and spry:test attributes, and to execute code within <script> tags that were inside regions. Use of innerHTML and eval() is normally fine within a normal browser context, but when running in the AIR Application Sandbox, a stricter security policy is enforced to prevent unauthorized script from executing. In the Application Sandbox, after the onload event fires, all on* attributes and <script> tags within markup, that is inserted with innerHTML, are ignored, and calls to eval() are ignored except those that request the evaluation of a strict JSON string.

This article will walk you through the modifications that were necessary to make the Spry Photo Gallery run within the Application Sandbox of the Adobe AIR 1.0 Runtime. You can view the final version of the Spry Photo Gallery that runs in the AIR Application Sandbox here.

The Original Source

Before jumping right into the changes that need to be made, lets first take a look at the original Spry Photo Gallery source, and discuss why it won't work in the AIR Application Sandbox.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- Copyright (c) 2006. Adobe Systems Incorporated. All rights reserved. -->
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:spry="http://ns.adobe.com/spry">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Gallery</title>
<link href="../../css/screen.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript" src="../../includes/SpryEffects.js"></script>
<script type="text/javascript">
var dsGalleries = new Spry.Data.XMLDataSet("galleries/galleries.xml", "galleries/gallery");
var dsGallery = new Spry.Data.XMLDataSet("galleries/{dsGalleries::@base}{dsGalleries::@file}", "gallery");
var dsPhotos = new Spry.Data.XMLDataSet("galleries/{dsGalleries::@base}{dsGalleries::@file}", "gallery/photos/photo");
</script>
<script src="gallery.js"  type="text/javascript"></script>

</head>
<body id="gallery">
<noscript>
<h1>This page requires JavaScript. Please enable JavaScript in your browser and reload this page.</h1>
</noscript>
<div id="wrap">
  <h1 id="albumName" spry:region="dsGallery">{sitename} <span class="return"><a href="../index.html">Back to Demos</a></span> <span class="source"><a href="source.html">View Source </a></span></h1>
  <div id="previews">
    <div id="galleries" spry:region="dsGalleries">
      <label for="gallerySelect">View:</label>

      <select spry:repeatchildren="dsGalleries" spry:choose="choose" id="gallerySelect" onchange="dsGalleries.setCurrentRowNumber(this.selectedIndex);">
        <option spry:when="{ds_RowNumber} == {ds_CurrentRowNumber}" selected="selected">{sitename}</option>
        <option spry:default="default">{sitename}</option>
      </select>
    </div>
    <div id="controls">
      <ul id="transport">
        <li><a href="#" onclick="StopSlideShow(); AdvanceToNextImage(true);" title="Previous">Previous</a></li>

        <li class="pausebtn"><a href="#" onclick="if (gSlideShowOn) StopSlideShow(); else StartSlideShow();" title="Play/Pause" id="playLabel">Play</a></li>
        <li><a href="#" onclick="StopSlideShow(); AdvanceToNextImage();" title="Next">Next</a></li>
      </ul>
    </div>
    <div id="thumbnails" spry:region="dsPhotos dsGalleries dsGallery">
      <div spry:repeat="dsPhotos" onclick="HandleThumbnailClick('{ds_RowID}');" onmouseover="GrowThumbnail(this.getElementsByTagName('img')[0], '{@thumbwidth}', '{@thumbheight}');" onmouseout="ShrinkThumbnail(this.getElementsByTagName('img')[0]);"> <img id="tn{ds_RowID}" alt="thumbnail for {@thumbpath}" src="galleries/{dsGalleries::@base}{dsGallery::thumbnail/@base}{@thumbpath}" width="24" height="24" style="left: 0px; right: 0px;" /> </div>
      <p class="ClearAll"></p>

    </div>
  </div>
  <div id="picture">
    <div id="mainImageOutline" style="width: 0px; height: 0px;"><img id="mainImage" alt="main image" src=""/></div>
  </div>
  <p class="clear"></p>
</div>
</body>
</html>

The hilighted sections of markup in the source above are the problematic areas of the file. Taking a look at the first hilighted section, known as the "galleries" region:

    <div id="galleries" spry:region="dsGalleries">
      <label for="gallerySelect">View:</label>

      <select spry:repeatchildren="dsGalleries" spry:choose="choose" id="gallerySelect" onchange="dsGalleries.setCurrentRowNumber(this.selectedIndex);">
        <option spry:when="{ds_RowNumber} == {ds_CurrentRowNumber}" selected="selected">{sitename}</option>
        <option spry:default="default">{sitename}</option>
      </select>
    </div>

We see that it contains an onchange attribute that triggers some JavaScript and a spry:when conditional attribute that contains a JavaScript expression. Because this <select> is in a spry:region, we know that it will get dynamically inserted into the document via innerHTML. This means that the onchange attribute on the select will be ignored when running in the AIR Application Sandbox. The result is that nothing will get executed when the user changes the value of the select widget. The spry:when attribute is part of a spry:choose constructor, otherwise known as a case statement. What is basically happening there is that the developer wants to write out the <option> tag with the spry:when attribute if the row number for the row currently being processed, matches the data set's notion of the current row. If it does not match, then the <option> with the spry:default attribute is written out. The problem here is that Spry uses the eval() function to evaluate the JavaScript expression described in the spry:when attribute. This evaluation typically does not happen until after the document's onload event fires, so we know that this will not work in the AIR Application Sandbox because eval() gets disabled after the onload event fires.

The second hilighted section, known as the "thumbnails" region:

    <div id="thumbnails" spry:region="dsPhotos dsGalleries dsGallery">
      <div spry:repeat="dsPhotos" onclick="HandleThumbnailClick('{ds_RowID}');" onmouseover="GrowThumbnail(this.getElementsByTagName('img')[0], '{@thumbwidth}', '{@thumbheight}');" onmouseout="ShrinkThumbnail(this.getElementsByTagName('img')[0]);"> <img id="tn{ds_RowID}" alt="thumbnail for {@thumbpath}" src="galleries/{dsGalleries::@base}{dsGallery::thumbnail/@base}{@thumbpath}" width="24" height="24" style="left: 0px; right: 0px;" /> </div>
      <p class="ClearAll"></p>

    </div>

makes use of more on* attributes. In this section, a <div> containing an <img> is generated for each row in the dsPhotos data set. For each <div> that is generated, an onclick attribute is used to trigger code to select the image to show in the main image area, and onmouseover and onmouseout attributes are used to trigger the growing and shrinking of the actual thumbnails. Once again, since all of this markup is within a spry:region, these on* attributes will be ignored when running in the AIR Application Sandbox.

If you looked closely at the full source of the original Spry Photo Gallery, you may have noticed that there is markup with on* attributes on them that we did not hilight. For example, in the "controls" section:

    <div id="controls">
      <ul id="transport">
        <li><a href="#" onclick="StopSlideShow(); AdvanceToNextImage(true);" title="Previous">Previous</a></li>

        <li class="pausebtn"><a href="#" onclick="if (gSlideShowOn) StopSlideShow(); else StartSlideShow();" title="Play/Pause" id="playLabel">Play</a></li>
        <li><a href="#" onclick="StopSlideShow(); AdvanceToNextImage();" title="Next">Next</a></li>
      </ul>
    </div>

The reason this code is not problematic, is because this markup is not dynamically generated and inserted into the document. Since it exists at the time the document is loaded, prior to the onload event firing, any on* attributes the markup contains will function as expected, without any problems, even when running in the AIR Application Sandbox.

Fixing the "galleries" Region

To get the "galleries" region working under the AIR Application Sandbox, we need to take care of the onchange and spry:when attributes.

    <div id="galleries" spry:region="dsGalleries">
      <label for="gallerySelect">View:</label>

      <select spry:repeatchildren="dsGalleries" spry:choose="choose" id="gallerySelect" onchange="dsGalleries.setCurrentRowNumber(this.selectedIndex);">
        <option spry:when="{ds_RowNumber} == {ds_CurrentRowNumber}" selected="selected">{sitename}</option>
        <option spry:default="default">{sitename}</option>
      </select>
    </div>

The solution for the onchange attribute is to unobtrusively attach an onchange event handler to the <select> element after the markup for the region is re-generated and inserted into the document. Regions have an observer mechanism that allows the developer to register a function or an object as an observer of a region. This allows the developer's code to listen for specific events of interest. The one we are interested in is the "onPostUpdate" event, which fires after a region's markup is re-generated and inserted into the document. We will remove the onchange attribute off of the <select> element, and move its code into an observer that is triggered whenever the "galleries" region re-generates:


...

<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript" src="../../includes/SpryEffects.js"></script>
<script type="text/javascript" src="../../includes/SpryDOMUtils.js"></script>
<script type="text/javascript">
var dsGalleries = new Spry.Data.XMLDataSet("galleries/galleries.xml", "galleries/gallery");
var dsGallery = new Spry.Data.XMLDataSet("galleries/{dsGalleries::@base}{dsGalleries::@file}", "gallery");
var dsPhotos = new Spry.Data.XMLDataSet("galleries/{dsGalleries::@base}{dsGalleries::@file}", "gallery/photos/photo");

...

Spry.Data.Region.addObserver("galleries", { onPostUpdate: function()
{
	Spry.$$("#gallerySelect").addEventListener("change", function(){ dsGalleries.setCurrentRowNumber(this.selectedIndex); }, false);
}});


...

</script>

...

    <div id="galleries" spry:region="dsGalleries">
      <label for="gallerySelect">View:</label>

      <select spry:repeatchildren="dsGalleries" spry:choose="choose" id="gallerySelect">
        <option spry:when="{ds_RowNumber} == {ds_CurrentRowNumber}" selected="selected">{sitename}</option>
        <option spry:default="default">{sitename}</option>
      </select>
    </div>

...

In the code above, the onchange attribute was removed from the <select> element. You will notice that we have added a <script> include for SpryDOMUtils.js because we are going to use the Element Selector Utility (aka Spry.$$) to aid us with unobtrusively attaching behaviors, and that is where the Element Selector is defined. The interesting part in the code above is the definition of the onPostUpdate observer. In it, we are using Spry.$$() to find the element with the id "gallerySelect", which is our <select> element. If it is found, a function, that contains the code that was previously in our onchange attribute, is added as a "change" listener on the <select>. So now anytime the user changes the value of the select, this function will get triggered, even when running in the AIR Application Sandbox.

To fix the problem with the spry:when attribute, we are going to take advantage of a new feature that was added for the Spry 1.6.1 release, nicknamed "function::". The idea is that you can replace the value of any spry:if, spry:when, or spry:test attribute with a value that starts with "function::" and ends with the name of a function to call, and the Spry region processing code will call that function whenever that spry:if, spry:when, or spry:test attribute is to be evaluated.


...

<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript" src="../../includes/SpryEffects.js"></script>
<script type="text/javascript" src="../../includes/SpryDOMUtils.js"></script>
<script type="text/javascript">
var dsGalleries = new Spry.Data.XMLDataSet("galleries/galleries.xml", "galleries/gallery");
var dsGallery = new Spry.Data.XMLDataSet("galleries/{dsGalleries::@base}{dsGalleries::@file}", "gallery");
var dsPhotos = new Spry.Data.XMLDataSet("galleries/{dsGalleries::@base}{dsGalleries::@file}", "gallery/photos/photo");

...

function IsCurrentGalleryRow(rgn, lookupFunc)
{
	return lookupFunc("{dsGalleries::ds_RowNumber}") == lookupFunc("{dsGalleries::ds_CurrentRowNumber}");
}

Spry.Data.Region.addObserver("galleries", { onPostUpdate: function()
{
	Spry.$$("#gallerySelect").addEventListener("change", function(){ dsGalleries.setCurrentRowNumber(this.selectedIndex); }, false);
}});


...

</script>

...

    <div id="galleries" spry:region="dsGalleries">
      <label for="gallerySelect">View:</label>

      <select spry:repeatchildren="dsGalleries" spry:choose="choose" id="gallerySelect">
        <option spry:when="function::IsCurrentGalleryRow" selected="selected">{sitename}</option>
        <option spry:default="default">{sitename}</option>
      </select>
    </div>

...

So in the code above, we have replaced the value of the spry:when attribute, which previously contained a JavaScript expression with some data references, with "function::IsCurrentGalleryRow". This means that each time the spry:when attribute is evaluated, the function IsCurrentGalleryRow() will get called, and the region processing code is going to expect that function to return a true, false, zero, or non-zero result, just as the evaluation of a JavaScript expression would.

Taking a look at the implementation of IsCurrentGalleryRow():

function IsCurrentGalleryRow(rgn, lookupFunc)
{
	return lookupFunc("{dsGalleries::ds_RowNumber}") == lookupFunc("{dsGalleries::ds_CurrentRowNumber}");
}

 

We see that it is expecting to be passed 2 arguments. The first argument is the name of the region that is currently being processed/re-generated. The second argument is a reference to a function that can be used to lookup the value of a data reference in the current processing context of the region.

This is what the function signature for any function you use with "function::" should look like.

Keep in mind that the names of these two arguments can be whatever you wish. The second argument will have to match whatever is used in the expression. So in the above sample, 'rgn' can be named anything, and whatever is used for 'lookupFunc' is the same name that is used in lookupFunc("{dsGalleries::ds_RowNumber}").

The actual contents of the function is up to you to implement, and in this specific case, it is simply the same JavaScript expression we previously had in our spry:when attribute:

spry:when="{ds_RowNumber} == {ds_CurrentRowNumber}"

except that it is now preceded by the keyword "return", and each data reference is surrounded by a call to lookupFunc to retrieve the same values that would've been used if we were using a JavaScript expression directly in the spry:when attribute:

	return lookupFunc("{dsGalleries::ds_RowNumber}") == lookupFunc("{dsGalleries::ds_CurrentRowNumber}");

That is all of the changes that were necessary to get the "galleries" section working under the AIR Application Sandbox. You can view the changes in context here.

Fixing the "thumbnails" Region

The only thing keeping the "thumbnails" region from working in the AIR Application Sandbox is the presence of onclick, onmouseover and onmouseout attributes.

    <div id="thumbnails" spry:region="dsPhotos dsGalleries dsGallery">
      <div spry:repeat="dsPhotos" onclick="HandleThumbnailClick('{ds_RowID}');" onmouseover="GrowThumbnail(this.getElementsByTagName('img')[0], '{@thumbwidth}', '{@thumbheight}');" onmouseout="ShrinkThumbnail(this.getElementsByTagName('img')[0]);"> <img id="tn{ds_RowID}" alt="thumbnail for {@thumbpath}" src="galleries/{dsGalleries::@base}{dsGallery::thumbnail/@base}{@thumbpath}" width="24" height="24" style="left: 0px; right: 0px;" /> </div>
      <p class="ClearAll"></p>

    </div>

From our experience with the "galleries" region above, we know that we can fix this by unobtrusively attaching these behaviors using an onPostUpdate region observer, but there is a wrinkle in this specific case. If you look closely at the JavaScript within the onclick, onmouseover and onmouseout attributes, you will notice that they are using data references like {ds_RowID}, {@thumbwidth} and {@thumbheight}. If we simply translate this code, by placing the JavaScript in the on* attribute directly into a function handler, as we did for the "galleries" region:

Spry.Data.Region.addObserver("thumbnails", { onPostUpdate: function()
{
	var results = Spry.$$("#thumbnails > div");
	results.addEventListener("click", function(){ HandleThumbnailClick('{ds_RowID}'); }, false);
	results.addEventListener("mouseover", function(){ GrowThumbnail(this.getElementsByTagName('img')[0], '{@thumbwidth}', '{@thumbheight}'); }, false);
	results.addEventListener("mouseout", function(){ ShrinkThumbnail(this.getElementsByTagName('img')[0]); }, false);
}});

The functions that would be triggered on those events would be passing the actual data reference strings, instead of the values we need. So, we need some way of tying the specific values of ds_RowID, @thumbwidth and @thumbheight to each <div> that contains a thumbnail image.

One easy solution, is to place custom attributes on the <div> and set their values to the data references we are interested in and access them from the event handlers using getAttribute():


Spry.Data.Region.addObserver("thumbnails", { onPostUpdate: function()
{
	var results = Spry.$$("#thumbnails > div");
	results.addEventListener("click", function(){ HandleThumbnailClick(this.getAttribute("rowid")); }, false);
	results.addEventListener("mouseover", function(){ GrowThumbnail(this.getElementsByTagName('img')[0], this.getAttribute("thumbwidth"), this.getAttribute("thumbheight")); }, false);
	results.addEventListener("mouseout", function(){ ShrinkThumbnail(this.getElementsByTagName('img')[0]); }, false);
}});

...

    <div id="thumbnails" spry:region="dsPhotos dsGalleries dsGallery">
      <div spry:repeat="dsPhotos" rowid="{ds_RowID}" thumbwidth="{@thumbwidth}" thumbheight="{@thumbheight}"> <img id="tn{ds_RowID}" alt="thumbnail for {@thumbpath}" src="galleries/{dsGalleries::@base}{dsGallery::thumbnail/@base}{@thumbpath}" width="24" height="24" style="left: 0px; right: 0px;" /> </div>
      <p class="ClearAll"></p>

    </div>

The idea here is that when Spry re-generates the region, as part of normal processing, the data references in the custom attributes will be turned into real values from the row in the data set that was used to generate that <div>. So when the event handler functions are triggered, and they use getAttribute() to fetch a specific custom attribute, they will get real values back that they can pass to the functions they trigger.

The problem with this approach is that it does not scale too well. If you need access to 'N' number of data references, you will need to add 'N' number of custom attributes.

For the Spry Photo Gallery, we chose to use another approach. Instead of adding custom attributes, we added an id attribute to the <div> in question and placed a {ds_RowID} data reference inside it:

    <div id="thumbnails" spry:region="dsPhotos dsGalleries dsGallery">
      <div spry:repeat="dsPhotos" id="tnd{ds_RowID}"> <img id="tn{ds_RowID}" alt="thumbnail for {@thumbpath}" src="galleries/{dsGalleries::@base}{dsGallery::thumbnail/@base}{@thumbpath}" width="24" height="24" style="left: 0px; right: 0px;" /> </div>
      <p class="ClearAll"></p>

    </div>

The idea here is that after the Spry region code re-generates the markup, it ends up looking something like this:

    <div id="thumbnails">
      <div id="tnd0"> <img ...
      <div id="tnd1"> <img ...
      <div id="tnd2"> <img ...
      <div id="tnd3"> <img ...
      <div id="tnd4"> <img ...

      ...

      <div id="tnd23"> <img ...

      <p class="ClearAll"></p>
    </div>

Since the ds_RowID of the row that was used to generate the each <div> is part of the id for that <div>, we can do some string manipulation to extract the ds_RowID from the id value, and use it to get the data set row we need to get values for "@thumbwidth", "@thumbheight", etc.

The "thumbnails" region observer code ends up looking like this:

...

Spry.Data.Region.addObserver("thumbnails", { onPostUpdate: function()
{
	var results = Spry.$$("#thumbnails > div");

	results.addEventListener("click", function()
	{
		HandleThumbnailClick(this.id.replace(/^tnd/, ""));
	}, false);

	results.addEventListener("mouseover", function()
	{
		var row = dsPhotos.getRowByID(this.id.replace(/^tnd/, ""));
		GrowThumbnail(this.getElementsByTagName('img')[0], row["@thumbwidth"], row["@thumbheight"]);
	}, false);

	results.addEventListener("mouseout", function(){ ShrinkThumbnail(this.getElementsByTagName('img')[0]); }, false);
}});

...

    <div id="thumbnails" spry:region="dsPhotos dsGalleries dsGallery">
      <div spry:repeat="dsPhotos" id="tnd{ds_RowID}"> <img id="tn{ds_RowID}" alt="thumbnail for {@thumbpath}" src="galleries/{dsGalleries::@base}{dsGallery::thumbnail/@base}{@thumbpath}" width="24" height="24" style="left: 0px; right: 0px;" /> </div>
      <p class="ClearAll"></p>

    </div>

The hilighted code within the event handler functions show where the ds_RowID is getting extracted from the id attribute of the <div>.

Seeing All the Changes In Context

Below is the complete source for the new version of the Spry Photo Gallery that works in the AIR Application Sandbox. All of the additions/changes we have made are hilighted. To see the new version of the Photo Gallery in action, go here. To view instructions on how to package the new version of the gallery into an AIR package, go here.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- Copyright (c) 2006. Adobe Systems Incorporated. All rights reserved. -->
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:spry="http://ns.adobe.com/spry">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Gallery</title>
<link href="../../css/screen.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript" src="../../includes/SpryEffects.js"></script>
<script type="text/javascript" src="../../includes/SpryDOMUtils.js"></script>
<script type="text/javascript">
var dsGalleries = new Spry.Data.XMLDataSet("galleries/galleries.xml", "galleries/gallery");
var dsGallery = new Spry.Data.XMLDataSet("galleries/{dsGalleries::@base}{dsGalleries::@file}", "gallery");
var dsPhotos = new Spry.Data.XMLDataSet("galleries/{dsGalleries::@base}{dsGalleries::@file}", "gallery/photos/photo");

// IsCurrentGalleryRow() gets invoked by the following markup:
//
//      <option spry:when="function::IsCurrentGalleryRow" selected="selected">
//
// It is a replacement for the following JS expression:
//
//     <option spry:when="{ds_RowNumber} == {ds_CurrentRowNumber}" selected="selected">
//
// which was used in the original gallery, but cannot be used in the AIR application
// sandbox because eval() is disabled.

function IsCurrentGalleryRow(rgn, lookupFunc)
{
	return lookupFunc("{ds_RowNumber}") == lookupFunc("{ds_CurrentRowNumber}");
}

// This is an observer on the "galleries" region. Any time the region is re-generated,
// we need to re-add the onchange event listener so we can load the gallery the user selected.

Spry.Data.Region.addObserver("galleries", { onPostUpdate: function()
{
	Spry.$$("#gallerySelect").addEventListener("change", function(){ dsGalleries.setCurrentRowNumber(this.selectedIndex); }, false);
}});

// This is an observer on the "thumbnails" region. Any time the region is re-generated,
// we need to re-add the thumbnail zooming and click to show image behaviors.

Spry.Data.Region.addObserver("thumbnails", { onPostUpdate: function()
{
	// Get all of the divs that are directly underneath the
	// div with the id "thumbnails".

	var results = Spry.$$("#thumbnails > div");

	// Add the onclick handler that will set the main image
	// when the user clicks on the thumbnail.

	results.addEventListener("click", function()
	{
		// The id attribute of the div contains the id of the row in the dsPhotos data set
		// that was used to generate it. Extract out the rowID and pass it to HandleThumbnailClick().

		HandleThumbnailClick(this.id.replace(/^tnd/, ""));
	}, false);

	// Add the onmouseover handler that will cause the thumbnails
	// to grow.

	results.addEventListener("mouseover", function()
	{
		// The id attribute of the div contains the id of the row in the dsPhotos data set
		// that was used to generate it. Extract out the rowID and use it to get the row from
		// the dsPhotos data set.
		
		var row = dsPhotos.getRowByID(this.id.replace(/^tnd/, ""));

		// Now call GrowThumbnail() and pass it the values in the @thumbwidth and @thumbheight columns.

		GrowThumbnail(this.getElementsByTagName('img')[0], row["@thumbwidth"], row["@thumbheight"]);
	}, false);

	// Add the onmouseout handler that will cause the thumbnails
	// to shrink.

	results.addEventListener("mouseout", function(){ ShrinkThumbnail(this.getElementsByTagName('img')[0]); }, false);
}});

</script>
<script src="gallery.js"  type="text/javascript"></script>
</head>
<body id="gallery">
<noscript>
<h1>This page requires JavaScript. Please enable JavaScript in your browser and reload this page.</h1>
</noscript>
<div id="wrap">
  <h1 id="albumName" spry:region="dsGallery">{sitename}</h1>
  <div id="previews">
    <div id="galleries" spry:region="dsGalleries">
      <label for="gallerySelect">View:</label>
      <select spry:repeatchildren="dsGalleries" spry:choose="choose" id="gallerySelect">
        <option spry:when="function::IsCurrentGalleryRow" selected="selected">{sitename}</option>
        <option spry:default="default">{sitename}</option>
      </select>
    </div>
    <div id="controls">
      <ul id="transport">
        <li><a href="#" onclick="StopSlideShow(); AdvanceToNextImage(true);" title="Previous">Previous</a></li>
        <li class="pausebtn"><a href="#" onclick="if (gSlideShowOn) StopSlideShow(); else StartSlideShow();" title="Play/Pause" id="playLabel">Play</a></li>
        <li><a href="#" onclick="StopSlideShow(); AdvanceToNextImage();" title="Next">Next</a></li>
      </ul>
    </div>
    <div id="thumbnails" spry:region="dsPhotos dsGalleries dsGallery">
      <div spry:repeat="dsPhotos" id="tnd{ds_RowID}"> <img id="tn{ds_RowID}" alt="thumbnail for {@thumbpath}" src="galleries/{dsGalleries::@base}{dsGallery::thumbnail/@base}{@thumbpath}" width="24" height="24" style="left: 0px; right: 0px;" /> </div>
      <p class="ClearAll"></p>
    </div>
  </div>
  <div id="picture">
    <div id="mainImageOutline" style="width: 0px; height: 0px;"><img id="mainImage" alt="main image" src=""/></div>
  </div>
  <p class="clear"></p>
</div>
</body>
</html>

Copyright © 2008. Adobe Systems Incorporated. All rights reserved.