[vhffs-dev] [1366] Back button handling in AJAX public part

[ Thread Index | Date Index | More vhffs.org/vhffs-dev Archives ]


Revision: 1366
Author:   beuss
Date:     2009-03-10 18:01:45 +0100 (Tue, 10 Mar 2009)

Log Message:
-----------
Back button handling in AJAX public part

Modified Paths:
--------------
    trunk/vhffs-panel/Makefile.am
    trunk/vhffs-panel/js/public.js
    trunk/vhffs-panel/js/vhffs/Common.js
    trunk/vhffs-public/templates/layouts/public.tt

Added Paths:
-----------
    trunk/vhffs-panel/js/dojo/back.js
    trunk/vhffs-panel/js/dojo/resources/
    trunk/vhffs-panel/js/dojo/resources/blank.gif
    trunk/vhffs-panel/js/dojo/resources/iframe_history.html


Modified: trunk/vhffs-panel/Makefile.am
===================================================================
--- trunk/vhffs-panel/Makefile.am	2009-03-10 12:40:12 UTC (rev 1365)
+++ trunk/vhffs-panel/Makefile.am	2009-03-10 17:01:45 UTC (rev 1366)
@@ -1,7 +1,10 @@
 javascripts = js/prototype.js \
 	js/commons.js \
 	js/dijit/dijit.js \
+	js/dojo/back.js \
 	js/dojo/dojo.js \
+	js/dojo/resources/blank.gif \
+	js/dojo/resources/iframe_history.html \
 	js/public.js \
 	js/tooltip.js \
 	js/vhffs/Common.js \

Added: trunk/vhffs-panel/js/dojo/back.js
===================================================================
--- trunk/vhffs-panel/js/dojo/back.js	                        (rev 0)
+++ trunk/vhffs-panel/js/dojo/back.js	2009-03-10 17:01:45 UTC (rev 1366)
@@ -0,0 +1,394 @@
+if(!dojo._hasResource["dojo.back"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo.back"] = true;
+dojo.provide("dojo.back");
+
+/*=====
+dojo.back = {
+	// summary: Browser history management resources
+}
+=====*/
+
+
+(function(){ 
+	var back = dojo.back;
+
+	// everyone deals with encoding the hash slightly differently
+
+	function getHash(){ 
+		var h = window.location.hash;
+		if(h.charAt(0) == "#"){ h = h.substring(1); }
+		return dojo.isMozilla ? h : decodeURIComponent(h); 
+	}
+	
+	function setHash(h){
+		if(!h){ h = ""; }
+		window.location.hash = encodeURIComponent(h);
+		historyCounter = history.length;
+	}
+	
+	// if we're in the test for these methods, expose them on dojo.back. ok'd with alex.
+	if(dojo.exists("tests.back-hash")){
+		back.getHash = getHash;
+		back.setHash = setHash;		
+	}
+	
+	var initialHref = (typeof(window) !== "undefined") ? window.location.href : "";
+	var initialHash = (typeof(window) !== "undefined") ? getHash() : "";
+	var initialState = null;
+
+	var locationTimer = null;
+	var bookmarkAnchor = null;
+	var historyIframe = null;
+	var forwardStack = [];
+	var historyStack = [];
+	var moveForward = false;
+	var changingUrl = false;
+	var historyCounter;
+
+	function handleBackButton(){
+		//summary: private method. Do not call this directly.
+
+		//The "current" page is always at the top of the history stack.
+		//console.debug("handlingBackButton");
+		var current = historyStack.pop();
+		if(!current){ return; }
+		var last = historyStack[historyStack.length-1];
+		if(!last && historyStack.length == 0){
+			last = initialState;
+		}
+		if(last){
+			if(last.kwArgs["back"]){
+				last.kwArgs["back"]();
+			}else if(last.kwArgs["backButton"]){
+				last.kwArgs["backButton"]();
+			}else if(last.kwArgs["handle"]){
+				last.kwArgs.handle("back");
+			}
+		}
+		forwardStack.push(current);
+		//console.debug("done handling back");
+	}
+
+	back.goBack = handleBackButton;
+
+	function handleForwardButton(){
+		//summary: private method. Do not call this directly.
+		//console.debug("handling forward");
+		var last = forwardStack.pop();
+		if(!last){ return; }
+		if(last.kwArgs["forward"]){
+			last.kwArgs.forward();
+		}else if(last.kwArgs["forwardButton"]){
+			last.kwArgs.forwardButton();
+		}else if(last.kwArgs["handle"]){
+			last.kwArgs.handle("forward");
+		}
+		historyStack.push(last);
+		//console.debug("done handling forward");
+	}
+
+	back.goForward = handleForwardButton;
+
+	function createState(url, args, hash){
+		//summary: private method. Do not call this directly.
+		return {"url": url, "kwArgs": args, "urlHash": hash};	//Object
+	}
+
+	function getUrlQuery(url){
+		//summary: private method. Do not call this directly.
+		var segments = url.split("?");
+		if(segments.length < 2){
+			return null; //null
+		}
+		else{
+			return segments[1]; //String
+		}
+	}
+	
+	function loadIframeHistory(){
+		//summary: private method. Do not call this directly.
+		var url = (dojo.config["dojoIframeHistoryUrl"] || dojo.moduleUrl("dojo", "resources/iframe_history.html")) + "?" + (new Date()).getTime();
+		moveForward = true;
+        if(historyIframe){
+		    dojo.isSafari ? historyIframe.location = url : window.frames[historyIframe.name].location = url;
+        }else{
+            //console.warn("dojo.back: Not initialised. You need to call dojo.back.init() from a <script> block that lives inside the <body> tag.");
+        }
+		return url; //String
+	}
+
+	function checkLocation(){
+		//console.debug("checking url");
+		if(!changingUrl){
+			var hsl = historyStack.length;
+			
+			var hash = getHash();
+
+			if((hash === initialHash||window.location.href == initialHref)&&(hsl == 1)){
+				// FIXME: could this ever be a forward button?
+				// we can't clear it because we still need to check for forwards. Ugg.
+				// clearInterval(this.locationTimer);
+				handleBackButton();
+				return;
+			}
+			
+			// first check to see if we could have gone forward. We always halt on
+			// a no-hash item.
+			if(forwardStack.length > 0){
+				if(forwardStack[forwardStack.length-1].urlHash === hash){
+					handleForwardButton();
+					return;
+				}
+			}
+	
+			// ok, that didn't work, try someplace back in the history stack
+			if((hsl >= 2)&&(historyStack[hsl-2])){
+				if(historyStack[hsl-2].urlHash === hash){
+					handleBackButton();
+					return;
+				}
+			}
+			
+			if(dojo.isSafari && dojo.isSafari < 3){
+				var hisLen = history.length;
+				if(hisLen > historyCounter) handleForwardButton();
+				else if(hisLen < historyCounter) handleBackButton();
+			  historyCounter = hisLen;
+			}
+		}
+		//console.debug("done checking");
+	};
+	
+	back.init = function(){
+		//summary: Initializes the undo stack. This must be called from a <script> 
+		//         block that lives inside the <body> tag to prevent bugs on IE.
+		if(dojo.byId("dj_history")){ return; } // prevent reinit
+		var src = dojo.config["dojoIframeHistoryUrl"] || dojo.moduleUrl("dojo", "resources/iframe_history.html");
+		document.write('<iframe style="border:0;width:1px;height:1px;position:absolute;visibility:hidden;bottom:0;right:0;" name="dj_history" id="dj_history" src="' + src + '"></iframe>');
+	};
+
+	back.setInitialState = function(/*Object*/args){
+		//summary: 
+		//		Sets the state object and back callback for the very first page
+		//		that is loaded.
+		//description:
+		//		It is recommended that you call this method as part of an event
+		//		listener that is registered via dojo.addOnLoad().
+		//args: Object
+		//		See the addToHistory() function for the list of valid args properties.
+		initialState = createState(initialHref, args, initialHash);
+	};
+
+	//FIXME: Make these doc comments not be awful. At least they're not wrong.
+	//FIXME: Would like to support arbitrary back/forward jumps. Have to rework iframeLoaded among other things.
+	//FIXME: is there a slight race condition in moz using change URL with the timer check and when
+	//       the hash gets set? I think I have seen a back/forward call in quick succession, but not consistent.
+
+	
+	/*=====
+	dojo.__backArgs = function(kwArgs){
+		// back: Function?
+		//		A function to be called when this state is reached via the user
+		//		clicking the back button.
+		//	forward: Function?
+		//		Upon return to this state from the "back, forward" combination
+		//		of navigation steps, this function will be called. Somewhat
+		//		analgous to the semantic of an "onRedo" event handler.
+		//	changeUrl: Boolean?|String?
+		//		Boolean indicating whether or not to create a unique hash for
+		//		this state. If a string is passed instead, it is used as the
+		//		hash.
+	}
+	=====*/
+
+	back.addToHistory = function(/*dojo.__backArgs*/ args){
+		//	summary: 
+		//		adds a state object (args) to the history list. 
+		//	description:
+		//		To support getting back button notifications, the object
+		//		argument should implement a function called either "back",
+		//		"backButton", or "handle". The string "back" will be passed as
+		//		the first and only argument to this callback.
+		//	
+		//		To support getting forward button notifications, the object
+		//		argument should implement a function called either "forward",
+		//		"forwardButton", or "handle". The string "forward" will be
+		//		passed as the first and only argument to this callback.
+		//
+		//		If you want the browser location string to change, define "changeUrl" on the object. If the
+		//		value of "changeUrl" is true, then a unique number will be appended to the URL as a fragment
+		//		identifier (http://some.domain.com/path#uniquenumber). If it is any other value that does
+		//		not evaluate to false, that value will be used as the fragment identifier. For example,
+		//		if changeUrl: 'page1', then the URL will look like: http://some.domain.com/path#page1
+		//
+	 	//	example:
+		//		|	dojo.back.addToHistory({
+		//		|		back: function(){ console.debug('back pressed'); },
+		//		|		forward: function(){ console.debug('forward pressed'); },
+		//		|		changeUrl: true
+		//		|	});
+
+		//	BROWSER NOTES:
+		//  Safari 1.2: 
+		//	back button "works" fine, however it's not possible to actually
+		//	DETECT that you've moved backwards by inspecting window.location.
+		//	Unless there is some other means of locating.
+		//	FIXME: perhaps we can poll on history.length?
+		//	Safari 2.0.3+ (and probably 1.3.2+):
+		//	works fine, except when changeUrl is used. When changeUrl is used,
+		//	Safari jumps all the way back to whatever page was shown before
+		//	the page that uses dojo.undo.browser support.
+		//	IE 5.5 SP2:
+		//	back button behavior is macro. It does not move back to the
+		//	previous hash value, but to the last full page load. This suggests
+		//	that the iframe is the correct way to capture the back button in
+		//	these cases.
+		//	Don't test this page using local disk for MSIE. MSIE will not create 
+		//	a history list for iframe_history.html if served from a file: URL. 
+		//	The XML served back from the XHR tests will also not be properly 
+		//	created if served from local disk. Serve the test pages from a web 
+		//	server to test in that browser.
+		//	IE 6.0:
+		//	same behavior as IE 5.5 SP2
+		//	Firefox 1.0+:
+		//	the back button will return us to the previous hash on the same
+		//	page, thereby not requiring an iframe hack, although we do then
+		//	need to run a timer to detect inter-page movement.
+
+		//If addToHistory is called, then that means we prune the
+		//forward stack -- the user went back, then wanted to
+		//start a new forward path.
+		forwardStack = []; 
+
+		var hash = null;
+		var url = null;
+		if(!historyIframe){
+			if(dojo.config["useXDomain"] && !dojo.config["dojoIframeHistoryUrl"]){
+				console.debug("dojo.back: When using cross-domain Dojo builds,"
+					+ " please save iframe_history.html to your domain and set djConfig.dojoIframeHistoryUrl"
+					+ " to the path on your domain to iframe_history.html");
+			}
+			historyIframe = window.frames["dj_history"];
+		}
+		if(!bookmarkAnchor){
+			bookmarkAnchor = document.createElement("a");
+			dojo.body().appendChild(bookmarkAnchor);
+			bookmarkAnchor.style.display = "none";
+		}
+		if(args["changeUrl"]){
+			hash = ""+ ((args["changeUrl"]!==true) ? args["changeUrl"] : (new Date()).getTime());
+			
+			//If the current hash matches the new one, just replace the history object with
+			//this new one. It doesn't make sense to track different state objects for the same
+			//logical URL. This matches the browser behavior of only putting in one history
+			//item no matter how many times you click on the same #hash link, at least in Firefox
+			//and Safari, and there is no reliable way in those browsers to know if a #hash link
+			//has been clicked on multiple times. So making this the standard behavior in all browsers
+			//so that dojo.back's behavior is the same in all browsers.
+			if(historyStack.length == 0 && initialState.urlHash == hash){
+				initialState = createState(url, args, hash);
+				return;
+			}else if(historyStack.length > 0 && historyStack[historyStack.length - 1].urlHash == hash){
+				historyStack[historyStack.length - 1] = createState(url, args, hash);
+				return;
+			}
+
+			changingUrl = true;
+			setTimeout(function() { 
+					setHash(hash); 
+					changingUrl = false; 					
+				}, 1);
+			bookmarkAnchor.href = hash;
+			
+			if(dojo.isIE){
+				url = loadIframeHistory();
+
+				var oldCB = args["back"]||args["backButton"]||args["handle"];
+
+				//The function takes handleName as a parameter, in case the
+				//callback we are overriding was "handle". In that case,
+				//we will need to pass the handle name to handle.
+				var tcb = function(handleName){
+					if(getHash() != ""){
+						setTimeout(function() { setHash(hash); }, 1);
+					}
+					//Use apply to set "this" to args, and to try to avoid memory leaks.
+					oldCB.apply(this, [handleName]);
+				};
+		
+				//Set interceptor function in the right place.
+				if(args["back"]){
+					args.back = tcb;
+				}else if(args["backButton"]){
+					args.backButton = tcb;
+				}else if(args["handle"]){
+					args.handle = tcb;
+				}
+		
+				var oldFW = args["forward"]||args["forwardButton"]||args["handle"];
+		
+				//The function takes handleName as a parameter, in case the
+				//callback we are overriding was "handle". In that case,
+				//we will need to pass the handle name to handle.
+				var tfw = function(handleName){
+					if(getHash() != ""){
+						setHash(hash);
+					}
+					if(oldFW){ // we might not actually have one
+						//Use apply to set "this" to args, and to try to avoid memory leaks.
+						oldFW.apply(this, [handleName]);
+					}
+				};
+
+				//Set interceptor function in the right place.
+				if(args["forward"]){
+					args.forward = tfw;
+				}else if(args["forwardButton"]){
+					args.forwardButton = tfw;
+				}else if(args["handle"]){
+					args.handle = tfw;
+				}
+
+			}else if(!dojo.isIE){
+				// start the timer
+				if(!locationTimer){
+					locationTimer = setInterval(checkLocation, 200);
+				}
+				
+			}
+		}else{
+			url = loadIframeHistory();
+		}
+
+		historyStack.push(createState(url, args, hash));
+	};
+
+	back._iframeLoaded = function(evt, ifrLoc){
+		//summary: 
+		//		private method. Do not call this directly.
+		var query = getUrlQuery(ifrLoc.href);
+		if(query == null){ 
+			// alert("iframeLoaded");
+			// we hit the end of the history, so we should go back
+			if(historyStack.length == 1){
+				handleBackButton();
+			}
+			return;
+		}
+		if(moveForward){
+			// we were expecting it, so it's not either a forward or backward movement
+			moveForward = false;
+			return;
+		}
+	
+		//Check the back stack first, since it is more likely.
+		//Note that only one step back or forward is supported.
+		if(historyStack.length >= 2 && query == getUrlQuery(historyStack[historyStack.length-2].url)){
+			handleBackButton();
+		}else if(forwardStack.length > 0 && query == getUrlQuery(forwardStack[forwardStack.length-1].url)){
+			handleForwardButton();
+		}
+	};
+ })();
+
+}

Added: trunk/vhffs-panel/js/dojo/resources/blank.gif
===================================================================
(Binary files differ)


Property changes on: trunk/vhffs-panel/js/dojo/resources/blank.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: trunk/vhffs-panel/js/dojo/resources/iframe_history.html
===================================================================
--- trunk/vhffs-panel/js/dojo/resources/iframe_history.html	                        (rev 0)
+++ trunk/vhffs-panel/js/dojo/resources/iframe_history.html	2009-03-10 17:01:45 UTC (rev 1366)
@@ -0,0 +1,79 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
+
+<html xmlns="http://www.w3.org/1999/xhtml"; xml:lang="en" lang="en">
+<head>
+	<title></title>
+	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
+	<script type="text/javascript">
+	// <!--
+	var noInit = false;
+	
+	function defineParams(sparams){
+		if(sparams){
+			var ss = (sparams.indexOf("&amp;") >= 0) ? "&amp;" : "&";
+			sparams = sparams.split(ss);
+			for(var x=0; x<sparams.length; x++){
+				var tp = sparams[x].split("=");
+				if(typeof window[tp[0]] != "undefined"){
+					window[tp[0]] = ((tp[1]=="true")||(tp[1]=="false")) ? eval(tp[1]) : tp[1];
+				}
+			}
+		}
+	}
+	
+	function init(){
+		// parse the query string if there is one to try to get params that
+		// we can act on. Also allow params to be in a fragment identifier.
+		var query = null;
+		var frag = null;
+		var url = document.location.href;
+		var hashIndex = url.indexOf("#");
+		
+		//Extract fragment identifier
+		if(hashIndex != -1){
+			frag = url.substring(hashIndex + 1, url.length);
+			url = url.substring(0, hashIndex);
+		}
+
+		//Extract querystring
+		var parts = url.split("?");
+		if(parts.length == 2){
+			query = parts[1];
+		}
+
+		defineParams(query);
+		defineParams(frag);
+
+		if(noInit){ return; }
+		var hasParentDojo = false;
+		try{
+			hasParentDojo = window.parent != window && window.parent["dojo"];
+		}catch(e){
+			alert("Initializing iframe_history.html failed. If you are using a cross-domain Dojo build,"
+				+ " please save iframe_history.html to your domain and set djConfig.dojoIframeHistoryUrl"
+				+ " to the path on your domain to iframe_history.html");
+			throw e;
+		}
+
+		if(hasParentDojo){
+			//Set the page title so IE history shows up with a somewhat correct name.
+			document.title = window.parent.document.title;
+			
+			//Notify parent that we are loaded.
+			var pdj = window.parent.dojo;
+			if(pdj["back"]){
+				pdj.back._iframeLoaded(null, window.location);
+			}
+		}
+
+	}
+	// -->
+	</script>
+</head>
+<body onload="try{ init(); }catch(e){ alert(e); }">
+	<h4>The Dojo Toolkit -- iframe_history.html</h4>
+
+	<p>This file is used in Dojo's back/fwd button management.</p>
+</body>
+</html>

Modified: trunk/vhffs-panel/js/public.js
===================================================================
--- trunk/vhffs-panel/js/public.js	2009-03-10 12:40:12 UTC (rev 1365)
+++ trunk/vhffs-panel/js/public.js	2009-03-10 17:01:45 UTC (rev 1366)
@@ -55,12 +55,16 @@
 	var form = dojo.byId('AdvancedSearchGroupForm');
 	dojo.connect(form, 'onsubmit', function(e) {
 		dojo.stopEvent(e);
+		var url = dojo.attr(form, 'action');
+		var content = dojo.formToObject(form);
+		var container = dojo.byId('public-content');
+		dojo.back.addToHistory(new vhffs.Common.pageState(url, container, content));
 		dojo.xhrPost({
-			url: dojo.attr(form, 'action'),
-			'form': form,
+			'url': url,
+			'content': content,
 			load: function(response) {
-				vhffs.Common.loadContent(dojo.byId('public-content'), response);
-				vhffs.Common.ajaxizeLinks(dojo.byId('public-content'));
+				vhffs.Common.loadContent(container, response);
+				vhffs.Common.ajaxizeLinks(container);
 			}
 		});
 	});
@@ -184,12 +188,16 @@
 	var form = dojo.byId('SearchUserForm');
 	dojo.connect(form, 'onsubmit', function(e) {
 		dojo.stopEvent(e);
+		var url = dojo.attr(form, 'action');
+		var content = dojo.formToObject(form);
+		var container = dojo.byId('public-content');
+		dojo.back.addToHistory(new vhffs.Common.pageState(url, container, content));
 		dojo.xhrPost({
-			url: dojo.attr(form, 'action'),
-			'form': form,
+			'url': url,
+			'content': content,
 			load: function(response) {
-				vhffs.Common.loadContent(dojo.byId('public-content'), response);
-				vhffs.Common.ajaxizeLinks(dojo.byId('public-content'));
+				vhffs.Common.loadContent(container, response);
+				vhffs.Common.ajaxizeLinks(container);
 			}
 		});
 	});

Modified: trunk/vhffs-panel/js/vhffs/Common.js
===================================================================
--- trunk/vhffs-panel/js/vhffs/Common.js	2009-03-10 12:40:12 UTC (rev 1365)
+++ trunk/vhffs-panel/js/vhffs/Common.js	2009-03-10 17:01:45 UTC (rev 1366)
@@ -29,9 +29,11 @@
  *  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  *  POSSIBILITY OF SUCH DAMAGE.
  */
- 
+
 dojo.provide('vhffs.Common');
 
+dojo.require('dojo.back');
+
 dojo.declare('vhffs.Common', null, {});
 
 dojo.mixin(vhffs.Common, {
@@ -41,6 +43,7 @@
 				dojo.stopEvent(e);
 				var href = dojo.attr(link, 'href');
 				if(href != '#') {
+					dojo.back.addToHistory(new vhffs.Common.pageState(href, contentTarget));
 					dojo.xhrGet({
 						url: href,
 						load: function(response) {
@@ -108,3 +111,58 @@
 		);
 	}
 });
+
+// Back and forward handling with Ajax
+
+dojo.declare('vhffs.Common.pageState', null, {
+	/**
+	 * Creates a new pageState.
+	 * url is the URL to load when this page is to
+	 * be restored, target the container in which the
+	 * content will be injected and postObject an optional
+	 * object containing post data (request will be GET if
+	 * it evaluates to false).
+	 */
+	constructor: function(url, target, postObject) {
+		this.url = url;
+		this.target = target;
+		this.postObject = postObject;
+	},
+
+	back: function() {
+		this.loadUrl();
+	},
+
+	forward: function() {
+		this.loadUrl();
+	},
+
+	loadUrl: function() {
+		var href = this.url;
+		var contentTarget = this.target;
+
+		if(this.postObject) {
+			var postContent = this.postObject;
+			dojo.xhrPost({
+				url: href,
+				load: function(response) {
+					vhffs.Common.loadContent(contentTarget, response);
+				},
+				content: postContent
+			});
+		} else {
+			dojo.xhrGet({
+				url: href,
+				load: function(response) {
+					vhffs.Common.loadContent(contentTarget, response);
+				}
+			});
+		}
+	}
+});
+
+dojo.addOnLoad(function() {
+	var initState = new vhffs.Common.pageState(document.location.pathname, dojo.byId('public-content'));
+	dojo.back.setInitialState(initState);
+});
+

Modified: trunk/vhffs-public/templates/layouts/public.tt
===================================================================
--- trunk/vhffs-public/templates/layouts/public.tt	2009-03-10 12:40:12 UTC (rev 1365)
+++ trunk/vhffs-public/templates/layouts/public.tt	2009-03-10 17:01:45 UTC (rev 1366)
@@ -7,9 +7,17 @@
   
 [%# TODO: Add a parameter to include extra-js %]
   
+  <script language="JavaScript" type="text/javascript">
+    // Dojo configuration
+    djConfig = {
+      preventBackButtonFix: false
+    };
+  </script>
+
   <script type="text/javascript" src="/js/dojo/dojo.js"></script>
   <script type="text/javascript" src="/js/dijit/dijit.js"></script>
   <script type="text/javascript" src="/js/public.js"></script>
+  <script type="text/javascript">dojo.back.init();</script>
   <title>Vhffs::Virtual hosting for free software</title>
 </head>
 <body>
@@ -34,4 +42,4 @@
 	[% INCLUDE parts/footer.tt %]
 	</div>
   </div>
-</div>
\ No newline at end of file
+</div>


Mail converted by MHonArc 2.6.19+ http://listengine.tuxfamily.org/